diff --git a/benches/wgpu.rs b/benches/wgpu.rs index 35696abc..d09277ed 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -72,7 +72,7 @@ fn benchmark<'a>( view: impl Fn(usize) -> Element<'a, (), Theme, Renderer>, ) { use iced_wgpu::graphics; - use iced_wgpu::graphics::Antialiasing; + use iced_wgpu::graphics::{Antialiasing, Shell}; use iced_wgpu::wgpu; use iced_winit::core; use iced_winit::runtime; @@ -85,6 +85,7 @@ fn benchmark<'a>( queue.clone(), format, Some(Antialiasing::MSAAx4), + Shell::headless(), ); let mut renderer = Renderer::new(engine, Font::DEFAULT, Pixels::from(16)); diff --git a/core/src/border.rs b/core/src/border.rs index cf73284c..232211db 100644 --- a/core/src/border.rs +++ b/core/src/border.rs @@ -241,9 +241,9 @@ impl From for Radius { } } -impl From for Radius { - fn from(w: u16) -> Self { - Self::from(f32::from(w)) +impl From for Radius { + fn from(w: u32) -> Self { + Self::from(w as f32) } } diff --git a/core/src/image.rs b/core/src/image.rs index f985636a..9540140c 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -1,11 +1,15 @@ //! Load and draw raster graphics. pub use bytes::Bytes; +use crate::border; use crate::{Radians, Rectangle, Size}; use rustc_hash::FxHasher; + use std::hash::{Hash, Hasher}; +use std::io; use std::path::{Path, PathBuf}; +use std::sync::{Arc, Weak}; /// A raster image that can be drawn. #[derive(Debug, Clone, PartialEq)] @@ -19,6 +23,11 @@ pub struct Image { /// The rotation to be applied to the image; on its center. pub rotation: Radians, + /// The border radius of the [`Image`]. + /// + /// Currently, this will only be applied to the `clip_bounds`. + pub border_radius: border::Radius, + /// The opacity of the image. /// /// 0 means transparent. 1 means opaque. @@ -38,6 +47,7 @@ impl Image { handle: handle.into(), filter_method: FilterMethod::default(), rotation: Radians(0.0), + border_radius: border::Radius::default(), opacity: 1.0, snap: false, } @@ -177,10 +187,12 @@ impl From<&Handle> for Handle { impl std::fmt::Debug for Handle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Path(_, path) => write!(f, "Path({path:?})"), - Self::Bytes(_, _) => write!(f, "Bytes(...)"), - Self::Rgba { width, height, .. } => { - write!(f, "Pixels({width} * {height})") + Self::Path(id, path) => write!(f, "Path({id:?}, {path:?})"), + Self::Bytes(id, _) => write!(f, "Bytes({id:?}, ...)"), + Self::Rgba { + id, width, height, .. + } => { + write!(f, "Pixels({id:?}, {width} * {height})") } } } @@ -227,6 +239,67 @@ pub enum FilterMethod { Nearest, } +/// A memory allocation of a [`Handle`], often in GPU memory. +/// +/// Renderers tend to decode and upload image data concurrently to +/// avoid blocking the user interface. This means that when you use a +/// [`Handle`] in a widget, there may be a slight frame delay until it +/// is finally visible. If you are animating images, this can cause +/// undesirable flicker. +/// +/// When you obtain an [`Allocation`] explicitly, you get the guarantee +/// that using a [`Handle`] will draw the corresponding [`Image`] +/// immediately in the next frame. +/// +/// This guarantee is valid as long as you hold an [`Allocation`]. +/// Only when you drop all its clones, the renderer may choose to free +/// the memory of the [`Handle`]. Be careful! +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Allocation(Arc); + +/// Some memory taken by an [`Allocation`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Memory { + handle: Handle, + size: Size, +} + +impl Allocation { + /// Returns a weak reference to the [`Memory`] of the [`Allocation`]. + pub fn downgrade(&self) -> Weak { + Arc::downgrade(&self.0) + } + + /// Upgrades a [`Weak`] memory reference to an [`Allocation`]. + pub fn upgrade(weak: &Weak) -> Option { + Weak::upgrade(weak).map(Allocation) + } + + /// Returns the [`Handle`] of this [`Allocation`]. + pub fn handle(&self) -> &Handle { + &self.0.handle + } + + /// Returns the [`Size`] of the image of this [`Allocation`]. + pub fn size(&self) -> Size { + self.0.size + } +} + +/// Creates a new [`Allocation`] for the given handle. +/// +/// This should only be used internally by renderer implementations. +/// +/// # Safety +/// Must only be created once the [`Handle`] is allocated in memory. +#[allow(unsafe_code)] +pub unsafe fn allocate(handle: &Handle, size: Size) -> Allocation { + Allocation(Arc::new(Memory { + handle: handle.clone(), + size, + })) +} + /// A [`Renderer`] that can render raster graphics. /// /// [renderer]: crate::renderer @@ -236,9 +309,48 @@ pub trait Renderer: crate::Renderer { /// [`Handle`]: Self::Handle type Handle: Clone; + /// Loads an image and returns an explicit [`Allocation`] to it. + /// + /// If the image is not already loaded, this method will block! You should + /// generally not use it in drawing logic if you want to avoid frame drops. + fn load_image(&self, handle: &Self::Handle) -> Result; + /// Returns the dimensions of an image for the given [`Handle`]. - fn measure_image(&self, handle: &Self::Handle) -> Size; + /// + /// If the image is not already loaded, the [`Renderer`] may choose to return + /// `None`, load the image in the background, and then trigger a relayout. + /// + /// If you need a measurement right away, consider using [`Renderer::load_image`]. + fn measure_image(&self, handle: &Self::Handle) -> Option>; /// Draws an [`Image`] inside the provided `bounds`. - fn draw_image(&mut self, image: Image, bounds: Rectangle); + /// + /// If the image is not already loaded, the [`Renderer`] may choose to render + /// nothing, load the image in the background, and then trigger a redraw. + /// + /// If you need to draw an image right away, consider using [`Renderer::load_image`] + /// and hold on to an [`Allocation`] first. + fn draw_image( + &mut self, + image: Image, + bounds: Rectangle, + clip_bounds: Rectangle, + ); +} + +/// An image loading error. +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + /// The image data was invalid or could not be decoded. + #[error("the image data was invalid or could not be decoded: {0}")] + Invalid(Arc), + /// The image file was not found. + #[error("the image file could not be opened: {0}")] + Inaccessible(Arc), + /// Loading images is unsupported. + #[error("loading images is unsupported")] + Unsupported, + /// Not enough memory to allocate the image. + #[error("not enough memory to allocate the image")] + OutOfMemory, } diff --git a/core/src/renderer.rs b/core/src/renderer.rs index 84d48304..89157c56 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -2,6 +2,7 @@ #[cfg(debug_assertions)] mod null; +use crate::image; use crate::{ Background, Border, Color, Font, Pixels, Rectangle, Shadow, Size, Transformation, Vector, @@ -62,6 +63,15 @@ pub trait Renderer { /// Resets the [`Renderer`] to start drawing in the `new_bounds` from scratch. fn reset(&mut self, new_bounds: Rectangle); + + /// Creates an [`image::Allocation`] for the given [`image::Handle`] and calls the given callback with it. + fn allocate_image( + &mut self, + handle: &image::Handle, + callback: impl FnOnce(Result) + + Send + + 'static, + ); } /// A polygon with four sides. diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index ff5baa3b..3c17265d 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -24,6 +24,17 @@ impl Renderer for () { _background: impl Into, ) { } + + fn allocate_image( + &mut self, + handle: &image::Handle, + callback: impl FnOnce(Result) + + Send + + 'static, + ) { + #[allow(unsafe_code)] + callback(Ok(unsafe { image::allocate(handle, Size::new(100, 100)) })); + } } impl text::Renderer for () { @@ -204,11 +215,25 @@ impl text::Editor for () { impl image::Renderer for () { type Handle = image::Handle; - fn measure_image(&self, _handle: &Self::Handle) -> Size { - Size::default() + fn load_image( + &self, + handle: &Self::Handle, + ) -> Result { + #[allow(unsafe_code)] + Ok(unsafe { image::allocate(handle, Size::new(100, 100)) }) } - fn draw_image(&mut self, _image: Image, _bounds: Rectangle) {} + fn measure_image(&self, _handle: &Self::Handle) -> Option> { + Some(Size::new(100, 100)) + } + + fn draw_image( + &mut self, + _image: Image, + _bounds: Rectangle, + _clip_bounds: Rectangle, + ) { + } } impl svg::Renderer for () { @@ -216,5 +241,11 @@ impl svg::Renderer for () { Size::default() } - fn draw_svg(&mut self, _svg: svg::Svg, _bounds: Rectangle) {} + fn draw_svg( + &mut self, + _svg: svg::Svg, + _bounds: Rectangle, + _clip_bounds: Rectangle, + ) { + } } diff --git a/core/src/svg.rs b/core/src/svg.rs index ac19b223..4a25b7e7 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -155,5 +155,5 @@ pub trait Renderer: crate::Renderer { fn measure_svg(&self, handle: &Handle) -> Size; /// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`. - fn draw_svg(&mut self, svg: Svg, bounds: Rectangle); + fn draw_svg(&mut self, svg: Svg, bounds: Rectangle, clip_bounds: Rectangle); } diff --git a/examples/gallery/src/civitai.rs b/examples/gallery/src/civitai.rs index 97f0ad89..f7475525 100644 --- a/examples/gallery/src/civitai.rs +++ b/examples/gallery/src/civitai.rs @@ -1,11 +1,12 @@ -use bytes::Bytes; use serde::Deserialize; use sipper::{Straw, sipper}; use tokio::task; use std::fmt; use std::io; -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; + +static CLIENT: LazyLock = LazyLock::new(reqwest::Client::new); #[derive(Debug, Clone, Deserialize)] pub struct Image { @@ -18,14 +19,12 @@ impl Image { pub const LIMIT: usize = 96; pub async fn list() -> Result, Error> { - let client = reqwest::Client::new(); - #[derive(Deserialize)] struct Response { items: Vec, } - let response: Response = client + let response: Response = CLIENT .get("https://civitai.com/api/v1/images") .query(&[ ("sort", "Most Reactions"), @@ -39,7 +38,11 @@ impl Image { .json() .await?; - Ok(response.items) + Ok(response + .items + .into_iter() + .filter(|image| !image.url.ends_with(".mp4")) + .collect()) } pub async fn blurhash( @@ -54,17 +57,15 @@ impl Image { rgba: Rgba { width, height, - pixels: Bytes::from(pixels), + pixels: Bytes(pixels.into()), }, }) }) .await? } - pub fn download(self, size: Size) -> impl Straw { + pub fn download(self, size: Size) -> impl Straw { sipper(async move |mut sender| { - let client = reqwest::Client::new(); - if let Size::Thumbnail { width, height } = size { let image = self.clone(); @@ -75,14 +76,16 @@ impl Image { })); } - let bytes = client + let bytes = CLIENT .get(match size { Size::Original => self.url, Size::Thumbnail { width, .. } => self .url .split("/") .map(|part| { - if part.starts_with("width=") { + if part.starts_with("width=") + || part.starts_with("original=") + { format!("width={}", width * 2) // High DPI } else { part.to_owned() @@ -97,21 +100,7 @@ impl Image { .bytes() .await?; - let image = task::spawn_blocking(move || { - Ok::<_, Error>( - image::ImageReader::new(io::Cursor::new(bytes)) - .with_guessed_format()? - .decode()? - .to_rgba8(), - ) - }) - .await??; - - Ok(Rgba { - width: image.width(), - height: image.height(), - pixels: Bytes::from(image.into_raw()), - }) + Ok(Bytes(bytes)) }) } } @@ -142,6 +131,29 @@ impl fmt::Debug for Rgba { } } +#[derive(Clone)] +pub struct Bytes(bytes::Bytes); + +impl Bytes { + pub fn as_slice(&self) -> &[u8] { + &self.0 + } +} + +impl From for bytes::Bytes { + fn from(value: Bytes) -> Self { + value.0 + } +} + +impl fmt::Debug for Bytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Compressed") + .field("bytes", &self.0.len()) + .finish() + } +} + #[derive(Debug, Clone, Copy)] pub enum Size { Original, @@ -154,7 +166,7 @@ pub enum Error { RequestFailed(Arc), IOFailed(Arc), JoinFailed(Arc), - ImageDecodingFailed(Arc), + ImageDecodingFailed, BlurhashDecodingFailed(Arc), } @@ -176,12 +188,6 @@ impl From for Error { } } -impl From for Error { - fn from(error: image::ImageError) -> Self { - Self::ImageDecodingFailed(Arc::new(error)) - } -} - impl From for Error { fn from(error: blurhash::Error) -> Self { Self::BlurhashDecodingFailed(Arc::new(error)) diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 182fd606..f6ed0077 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -4,9 +4,10 @@ //! some smooth animations. mod civitai; -use crate::civitai::{Error, Id, Image, Rgba, Size}; +use crate::civitai::{Bytes, Error, Id, Image, Rgba, Size}; use iced::animation; +use iced::border; use iced::time::{Instant, milliseconds}; use iced::widget::{ button, container, float, grid, image, mouse_area, opaque, scrollable, @@ -18,7 +19,7 @@ use iced::{ Subscription, Task, Theme, color, }; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; fn main() -> iced::Result { iced::application::timed( @@ -35,6 +36,8 @@ fn main() -> iced::Result { struct Gallery { images: Vec, previews: HashMap, + visible: HashSet, + downloaded: HashSet, viewer: Viewer, now: Instant, } @@ -43,8 +46,10 @@ struct Gallery { enum Message { ImagesListed(Result, Error>), ImagePoppedIn(Id), - ImageDownloaded(Result), - ThumbnailDownloaded(Id, Result), + ImagePoppedOut(Id), + ImageDownloaded(Result), + ThumbnailDownloaded(Id, Result), + ThumbnailAllocated(Id, Result), ThumbnailHovered(Id, bool), BlurhashDecoded(Id, civitai::Blurhash), Open(Id), @@ -58,6 +63,8 @@ impl Gallery { Self { images: Vec::new(), previews: HashMap::new(), + visible: HashSet::new(), + downloaded: HashSet::new(), viewer: Viewer::new(), now: Instant::now(), }, @@ -102,6 +109,28 @@ impl Gallery { return Task::none(); }; + let _ = self.visible.insert(id); + + if self.downloaded.contains(&id) { + let Some(Preview::Ready { + thumbnail, + blurhash, + }) = self.previews.get_mut(&id) + else { + return Task::none(); + }; + + if let Some(blurhash) = blurhash { + blurhash.show(now); + } + + return to_rgba(thumbnail.bytes.clone()) + .then(image::allocate) + .map(Message::ThumbnailAllocated.with(id)); + } + + let _ = self.downloaded.insert(id); + Task::sip( image.download(Size::Thumbnail { width: Preview::WIDTH, @@ -111,20 +140,53 @@ impl Gallery { Message::ThumbnailDownloaded.with(id), ) } - Message::ImageDownloaded(Ok(rgba)) => { - self.viewer.show(rgba, self.now); + Message::ImagePoppedOut(id) => { + let _ = self.visible.remove(&id); + + if let Some(Preview::Ready { + thumbnail, + blurhash, + }) = self.previews.get_mut(&id) + { + thumbnail.reset(); + + if let Some(blurhash) = blurhash { + blurhash.reset(); + } + } Task::none() } - Message::ThumbnailDownloaded(id, Ok(rgba)) => { - let thumbnail = if let Some(preview) = self.previews.remove(&id) - { - preview.load(rgba, self.now) + Message::ImageDownloaded(Ok(allocation)) => { + self.viewer.show(allocation, self.now); + + Task::none() + } + Message::ThumbnailDownloaded(id, Ok(bytes)) => { + let preview = if let Some(preview) = self.previews.remove(&id) { + preview.load(bytes.clone()) } else { - Preview::ready(rgba, self.now) + Preview::ready(bytes.clone()) }; - let _ = self.previews.insert(id, thumbnail); + let _ = self.previews.insert(id, preview); + + to_rgba(bytes) + .then(image::allocate) + .map(Message::ThumbnailAllocated.with(id)) + } + Message::ThumbnailAllocated(id, Ok(allocation)) => { + if !self.visible.contains(&id) { + return Task::none(); + } + + let Some(Preview::Ready { thumbnail, .. }) = + self.previews.get_mut(&id) + else { + return Task::none(); + }; + + thumbnail.show(allocation, now); Task::none() } @@ -156,10 +218,12 @@ impl Gallery { self.viewer.open(self.now); - Task::perform( - image.download(Size::Original), - Message::ImageDownloaded, - ) + Task::future(image.download(Size::Original)) + .and_then(|bytes| { + image::allocate(image::Handle::from_bytes(bytes)) + .map_err(|_| Error::ImageDecodingFailed) + }) + .map(Message::ImageDownloaded) } Message::Close => { self.viewer.close(self.now); @@ -172,6 +236,11 @@ impl Gallery { | Message::ThumbnailDownloaded(_, Err(error)) => { dbg!(error); + Task::none() + } + Message::ThumbnailAllocated(_, Err(error)) => { + dbg!(error); + Task::none() } } @@ -181,8 +250,25 @@ impl Gallery { let images = self .images .iter() - .map(|image| card(image, self.previews.get(&image.id), self.now)) - .chain((self.images.len()..=Image::LIMIT).map(|_| placeholder())); + .map(|image| { + card( + image, + if self.visible.contains(&image.id) { + self.previews.get(&image.id) + } else { + None + }, + self.now, + ) + }) + .chain( + if self.images.is_empty() { + 0..Image::LIMIT + } else { + 0..0 + } + .map(|_| placeholder()), + ); let gallery = grid(images) .fluid(Preview::WIDTH) @@ -203,12 +289,15 @@ fn card<'a>( ) -> Element<'a, Message> { let image = if let Some(preview) = preview { let thumbnail: Element<'_, _> = - if let Preview::Ready { thumbnail, .. } = &preview { + if let Preview::Ready { thumbnail, .. } = &preview + && let Some(allocation) = &thumbnail.allocation + { float( - image(&thumbnail.handle) + image(allocation.handle()) .width(Fill) .content_fit(ContentFit::Cover) - .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now)), + .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now)) + .border_radius(BORDER_RADIUS), ) .scale(thumbnail.zoom.interpolate(1.0, 1.1, now)) .translate(move |bounds, viewport| { @@ -223,7 +312,7 @@ fn card<'a>( blur_radius: thumbnail.zoom.interpolate(0.0, 20.0, now), ..Shadow::default() }, - ..float::Style::default() + shadow_border_radius: border::radius(BORDER_RADIUS), }) .into() } else { @@ -234,7 +323,8 @@ fn card<'a>( let blurhash = image(&blurhash.handle) .width(Fill) .content_fit(ContentFit::Cover) - .opacity(blurhash.fade_in.interpolate(0.0, 1.0, now)); + .opacity(blurhash.fade_in.interpolate(0.0, 1.0, now)) + .border_radius(BORDER_RADIUS); stack![blurhash, thumbnail].into() } else { @@ -244,11 +334,11 @@ fn card<'a>( space::horizontal().into() }; - let card = mouse_area(container(image).style(container::dark)) + let card = mouse_area(container(image).style(rounded)) .on_enter(Message::ThumbnailHovered(metadata.id, true)) .on_exit(Message::ThumbnailHovered(metadata.id, false)); - if let Some(preview) = preview { + let card: Element<'_, _> = if let Some(preview) = preview { let is_thumbnail = matches!(preview, Preview::Ready { .. }); button(card) @@ -257,14 +347,17 @@ fn card<'a>( .style(button::text) .into() } else { - sensor(card) - .on_show(|_| Message::ImagePoppedIn(metadata.id)) - .into() - } + card.into() + }; + + sensor(card) + .on_show(|_| Message::ImagePoppedIn(metadata.id)) + .on_hide(Message::ImagePoppedOut(metadata.id)) + .into() } fn placeholder<'a>() -> Element<'a, Message> { - container(space()).style(container::dark).into() + container(space()).style(rounded).into() } enum Preview { @@ -282,8 +375,21 @@ struct Blurhash { fade_in: Animation, } +impl Blurhash { + pub fn show(&mut self, now: Instant) { + self.fade_in.go_mut(true, now); + } + + pub fn reset(&mut self) { + self.fade_in = Animation::new(false) + .easing(animation::Easing::EaseIn) + .very_quick(); + } +} + struct Thumbnail { - handle: image::Handle, + bytes: Bytes, + allocation: Option, fade_in: Animation, zoom: Animation, } @@ -308,21 +414,21 @@ impl Preview { } } - fn ready(rgba: Rgba, now: Instant) -> Self { + fn ready(bytes: Bytes) -> Self { Self::Ready { blurhash: None, - thumbnail: Thumbnail::new(rgba, now), + thumbnail: Thumbnail::new(bytes), } } - fn load(self, rgba: Rgba, now: Instant) -> Self { + fn load(self, bytes: Bytes) -> Self { let Self::Loading { blurhash } = self else { return self; }; Self::Ready { blurhash: Some(blurhash), - thumbnail: Thumbnail::new(rgba, now), + thumbnail: Thumbnail::new(bytes), } } @@ -335,9 +441,15 @@ impl Preview { fn is_animating(&self, now: Instant) -> bool { match &self { Self::Loading { blurhash } => blurhash.fade_in.is_animating(now), - Self::Ready { thumbnail, .. } => { + Self::Ready { + thumbnail, + blurhash, + } => { thumbnail.fade_in.is_animating(now) || thumbnail.zoom.is_animating(now) + || blurhash.as_ref().is_some_and(|blurhash| { + blurhash.fade_in.is_animating(now) + }) } } } @@ -349,30 +461,45 @@ impl Preview { blurhash: Some(blurhash), thumbnail, .. - } if thumbnail.fade_in.is_animating(now) => Some(blurhash), + } if !thumbnail.fade_in.value() + || thumbnail.fade_in.is_animating(now) => + { + Some(blurhash) + } Self::Ready { .. } => None, } } } impl Thumbnail { - pub fn new(rgba: Rgba, now: Instant) -> Self { + pub fn new(bytes: Bytes) -> Self { Self { - handle: image::Handle::from_rgba( - rgba.width, - rgba.height, - rgba.pixels, - ), - fade_in: Animation::new(false).slow().go(true, now), + bytes, + allocation: None, + fade_in: Animation::new(false) + .easing(animation::Easing::EaseIn) + .slow(), zoom: Animation::new(false) .quick() .easing(animation::Easing::EaseInOut), } } + + pub fn reset(&mut self) { + self.allocation = None; + self.fade_in = Animation::new(false) + .easing(animation::Easing::EaseIn) + .quick(); + } + + pub fn show(&mut self, allocation: image::Allocation, now: Instant) { + self.allocation = Some(allocation); + self.fade_in.go_mut(true, now); + } } struct Viewer { - image: Option, + image: Option, background_fade_in: Animation, image_fade_in: Animation, } @@ -395,12 +522,8 @@ impl Viewer { self.background_fade_in.go_mut(true, now); } - fn show(&mut self, rgba: Rgba, now: Instant) { - self.image = Some(image::Handle::from_rgba( - rgba.width, - rgba.height, - rgba.pixels, - )); + fn show(&mut self, allocation: image::Allocation, now: Instant) { + self.image = Some(allocation); self.background_fade_in.go_mut(true, now); self.image_fade_in.go_mut(true, now); } @@ -422,8 +545,8 @@ impl Viewer { return None; } - let image = self.image.as_ref().map(|handle| { - image(handle) + let image = self.image.as_ref().map(|allocation| { + image(allocation.handle()) .width(Fill) .height(Fill) .opacity(self.image_fade_in.interpolate(0.0, 1.0, now)) @@ -444,3 +567,30 @@ impl Viewer { )) } } + +fn to_rgba(bytes: Bytes) -> Task { + Task::future(async move { + tokio::task::spawn_blocking(move || { + match ::image::load_from_memory(bytes.as_slice()) { + Ok(image) => { + let rgba = image.to_rgba8(); + + image::Handle::from_rgba( + rgba.width(), + rgba.height(), + rgba.into_raw(), + ) + } + _ => image::Handle::from_bytes(bytes), + } + }) + .await + .unwrap() + }) +} + +fn rounded(theme: &Theme) -> container::Style { + container::dark(theme).border(border::rounded(BORDER_RADIUS)) +} + +const BORDER_RADIUS: u32 = 10; diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index b98b8273..5084742b 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -4,7 +4,7 @@ mod scene; use controls::Controls; use scene::Scene; -use iced_wgpu::graphics::Viewport; +use iced_wgpu::graphics::{Shell, Viewport}; use iced_wgpu::{Engine, Renderer, wgpu}; use iced_winit::Clipboard; use iced_winit::conversion; @@ -150,6 +150,7 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { queue.clone(), format, None, + Shell::headless(), ); Renderer::new(engine, Font::default(), Pixels::from(16)) diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index e0c8eb46..3ec99542 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -2,7 +2,7 @@ //! surfaces. use crate::core::Color; use crate::futures::{MaybeSend, MaybeSync}; -use crate::{Error, Settings, Viewport}; +use crate::{Error, Settings, Shell, Viewport}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use thiserror::Error; @@ -21,8 +21,9 @@ pub trait Compositor: Sized { fn new( settings: Settings, compatible_window: W, + shell: Shell, ) -> impl Future> { - Self::with_backend(settings, compatible_window, None) + Self::with_backend(settings, compatible_window, shell, None) } /// Creates a new [`Compositor`] with a backend preference. @@ -32,6 +33,7 @@ pub trait Compositor: Sized { fn with_backend( _settings: Settings, _compatible_window: W, + _shell: Shell, _backend: Option<&str>, ) -> impl Future>; @@ -153,6 +155,7 @@ impl Compositor for () { async fn with_backend( _settings: Settings, _compatible_window: W, + _shell: Shell, _preferred_backend: Option<&str>, ) -> Result { Ok(()) diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 171edd80..c9d4e45f 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -7,33 +7,45 @@ use crate::core::image; use crate::core::svg; /// A raster or vector image. +#[allow(missing_docs)] #[derive(Debug, Clone, PartialEq)] pub enum Image { /// A raster image. - Raster(image::Image, Rectangle), + Raster { + image: image::Image, + bounds: Rectangle, + clip_bounds: Rectangle, + }, /// A vector image. - Vector(svg::Svg, Rectangle), + Vector { + svg: svg::Svg, + bounds: Rectangle, + clip_bounds: Rectangle, + }, } impl Image { /// Returns the bounds of the [`Image`]. pub fn bounds(&self) -> Rectangle { match self { - Image::Raster(image, bounds) => bounds.rotate(image.rotation), - Image::Vector(svg, bounds) => bounds.rotate(svg.rotation), + Image::Raster { image, bounds, .. } => { + bounds.rotate(image.rotation) + } + Image::Vector { svg, bounds, .. } => bounds.rotate(svg.rotation), } } } +/// An image buffer. +#[cfg(feature = "image")] +pub type Buffer = ::image::ImageBuffer<::image::Rgba, image::Bytes>; + #[cfg(feature = "image")] /// Tries to load an image by its [`Handle`]. /// /// [`Handle`]: image::Handle -pub fn load( - handle: &image::Handle, -) -> ::image::ImageResult<::image::ImageBuffer<::image::Rgba, image::Bytes>> -{ +pub fn load(handle: &image::Handle) -> Result { use bitflags::bitflags; bitflags! { @@ -85,7 +97,7 @@ pub fn load( let (width, height, pixels) = match handle { image::Handle::Path(_, path) => { - let image = ::image::open(path)?; + let image = ::image::open(path).map_err(to_error)?; let operation = std::fs::File::open(path) .ok() @@ -102,7 +114,8 @@ pub fn load( ) } image::Handle::Bytes(_, bytes) => { - let image = ::image::load_from_memory(bytes)?; + let image = ::image::load_from_memory(bytes).map_err(to_error)?; + let operation = Operation::from_exif(&mut std::io::Cursor::new(bytes)) .ok() @@ -127,10 +140,22 @@ pub fn load( if let Some(image) = ::image::ImageBuffer::from_raw(width, height, pixels) { Ok(image) } else { - Err(::image::error::ImageError::Limits( + Err(to_error(::image::error::ImageError::Limits( ::image::error::LimitError::from_kind( ::image::error::LimitErrorKind::DimensionError, ), - )) + ))) + } +} + +#[cfg(feature = "image")] +fn to_error(error: ::image::ImageError) -> image::Error { + use std::sync::Arc; + + match error { + ::image::ImageError::IoError(error) => { + image::Error::Inaccessible(Arc::new(error)) + } + error => image::Error::Invalid(Arc::new(error)), } } diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index ed6ec87f..ff5f39c4 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -21,6 +21,7 @@ pub mod gradient; pub mod image; pub mod layer; pub mod mesh; +pub mod shell; pub mod text; #[cfg(feature = "geometry")] @@ -35,6 +36,7 @@ pub use image::Image; pub use layer::Layer; pub use mesh::Mesh; pub use settings::Settings; +pub use shell::Shell; pub use text::Text; pub use viewport::Viewport; diff --git a/graphics/src/shell.rs b/graphics/src/shell.rs new file mode 100644 index 00000000..55004d7d --- /dev/null +++ b/graphics/src/shell.rs @@ -0,0 +1,45 @@ +//! Control the windowing runtime from a renderer. +use std::sync::Arc; + +/// A windowing shell. +#[derive(Clone)] +pub struct Shell(Arc); + +impl Shell { + /// Creates a new [`Shell`]. + pub fn new(notifier: impl Notifier) -> Self { + Self(Arc::new(notifier)) + } + + /// Creates a headless [`Shell`]. + pub fn headless() -> Self { + struct Headless; + + impl Notifier for Headless { + fn request_redraw(&self) {} + + fn invalidate_layout(&self) {} + } + + Self::new(Headless) + } + + /// Requests for all windows of the [`Shell`] to be redrawn. + pub fn request_redraw(&self) { + self.0.request_redraw(); + } + + /// Requests for all layouts of the [`Shell`] to be recomputed. + pub fn invalidate_layout(&self) { + self.0.invalidate_layout(); + } +} + +/// A type that can notify a shell of certain events. +pub trait Notifier: Send + Sync + 'static { + /// Requests for all windows of the [`Shell`] to be redrawn. + fn request_redraw(&self); + + /// Requests for all layouts of the [`Shell`] to be recomputed. + fn invalidate_layout(&self); +} diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 5e775e38..16cec4a3 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -6,9 +6,9 @@ use crate::core::{ self, Background, Color, Font, Image, Pixels, Point, Rectangle, Size, Svg, Transformation, }; -use crate::graphics; use crate::graphics::compositor; use crate::graphics::mesh; +use crate::graphics::{self, Shell}; use std::borrow::Cow; @@ -69,6 +69,16 @@ where fn end_transformation(&mut self) { delegate!(self, renderer, renderer.end_transformation()); } + + fn allocate_image( + &mut self, + handle: &image::Handle, + callback: impl FnOnce(Result) + + Send + + 'static, + ) { + delegate!(self, renderer, renderer.allocate_image(handle, callback)); + } } impl core::text::Renderer for Renderer @@ -146,12 +156,28 @@ where { type Handle = A::Handle; - fn measure_image(&self, handle: &Self::Handle) -> Size { + fn load_image( + &self, + handle: &Self::Handle, + ) -> Result { + delegate!(self, renderer, renderer.load_image(handle)) + } + + fn measure_image(&self, handle: &Self::Handle) -> Option> { delegate!(self, renderer, renderer.measure_image(handle)) } - fn draw_image(&mut self, image: Image, bounds: Rectangle) { - delegate!(self, renderer, renderer.draw_image(image, bounds)); + fn draw_image( + &mut self, + image: Image, + bounds: Rectangle, + clip_bounds: Rectangle, + ) { + delegate!( + self, + renderer, + renderer.draw_image(image, bounds, clip_bounds) + ); } } @@ -164,8 +190,13 @@ where delegate!(self, renderer, renderer.measure_svg(handle)) } - fn draw_svg(&mut self, svg: Svg, bounds: Rectangle) { - delegate!(self, renderer, renderer.draw_svg(svg, bounds)); + fn draw_svg( + &mut self, + svg: Svg, + bounds: Rectangle, + clip_bounds: Rectangle, + ) { + delegate!(self, renderer, renderer.draw_svg(svg, bounds, clip_bounds)); } } @@ -216,6 +247,7 @@ where async fn with_backend( settings: graphics::Settings, compatible_window: W, + shell: Shell, backend: Option<&str>, ) -> Result { use std::env; @@ -242,8 +274,13 @@ where let mut errors = vec![]; for backend in candidates.iter().map(Option::as_deref) { - match A::with_backend(settings, compatible_window.clone(), backend) - .await + match A::with_backend( + settings, + compatible_window.clone(), + shell.clone(), + backend, + ) + .await { Ok(compositor) => return Ok(Self::Primary(compositor)), Err(error) => { @@ -251,8 +288,13 @@ where } } - match B::with_backend(settings, compatible_window.clone(), backend) - .await + match B::with_backend( + settings, + compatible_window.clone(), + shell.clone(), + backend, + ) + .await { Ok(compositor) => return Ok(Self::Secondary(compositor)), Err(error) => { diff --git a/runtime/src/image.rs b/runtime/src/image.rs new file mode 100644 index 00000000..53b736cc --- /dev/null +++ b/runtime/src/image.rs @@ -0,0 +1,24 @@ +//! Allocate images explicitly to control presentation. +use crate::core::image::Handle; +use crate::futures::futures::channel::oneshot; +use crate::task::{self, Task}; + +pub use crate::core::image::{Allocation, Error}; + +/// An image action. +#[derive(Debug)] +pub enum Action { + /// Allocates the given [`Handle`]. + Allocate(Handle, oneshot::Sender>), +} + +/// Allocates an image [`Handle`]. +/// +/// When you obtain an [`Allocation`] explicitly, you get the guarantee +/// that using a [`Handle`] will draw the corresponding image immediately +/// in the next frame. +pub fn allocate(handle: impl Into) -> Task> { + task::oneshot(|sender| { + crate::Action::Image(Action::Allocate(handle.into(), sender)) + }) +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 691d6d73..dd85139f 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -11,6 +11,7 @@ #![cfg_attr(docsrs, feature(doc_cfg))] pub mod clipboard; pub mod font; +pub mod image; pub mod keyboard; pub mod system; pub mod task; @@ -55,6 +56,9 @@ pub enum Action { /// Run a system action. System(system::Action), + /// An image action. + Image(image::Action), + /// Recreate all user interfaces and redraw all windows. Reload, @@ -81,6 +85,7 @@ impl Action { Action::Clipboard(action) => Err(Action::Clipboard(action)), Action::Window(action) => Err(Action::Window(action)), Action::System(action) => Err(Action::System(action)), + Action::Image(action) => Err(Action::Image(action)), Action::Reload => Err(Action::Reload), Action::Exit => Err(Action::Exit), } @@ -105,6 +110,7 @@ where } Action::Window(_) => write!(f, "Action::Window"), Action::System(action) => write!(f, "Action::System({action:?})"), + Action::Image(_) => write!(f, "Action::Image"), Action::Reload => write!(f, "Action::Reload"), Action::Exit => write!(f, "Action::Exit"), } diff --git a/runtime/src/task.rs b/runtime/src/task.rs index 0ed78b54..209c1176 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -379,14 +379,30 @@ impl Task> { /// The success value is provided to the closure to create the subsequent [`Task`]. pub fn and_then( self, - f: impl Fn(T) -> Task + MaybeSend + 'static, - ) -> Task + f: impl Fn(T) -> Task> + MaybeSend + 'static, + ) -> Task> where T: MaybeSend + 'static, E: MaybeSend + 'static, A: MaybeSend + 'static, { - self.then(move |option| option.map_or_else(|_| Task::none(), &f)) + self.then(move |result| { + result.map_or_else(|error| Task::done(Err(error)), &f) + }) + } + + /// Maps the error type of this [`Task`] to a different one using the given + /// function. + pub fn map_err( + self, + f: impl Fn(E) -> E2 + MaybeSend + 'static, + ) -> Task> + where + T: MaybeSend + 'static, + E: MaybeSend + 'static, + E2: MaybeSend + 'static, + { + self.map(move |result| result.map_err(&f)) } } diff --git a/runtime/src/window.rs b/runtime/src/window.rs index f2ed4e5e..057acb8f 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -174,6 +174,12 @@ pub enum Action { /// Set the window size increment. SetResizeIncrements(Id, Option), + + /// Redraws all the windows. + RedrawAll, + + /// Recomputes the layouts of all the windows. + RelayoutAll, } /// Subscribes to the frames of the window of the running application. diff --git a/src/application.rs b/src/application.rs index 143dfcc4..f007e9dc 100644 --- a/src/application.rs +++ b/src/application.rs @@ -198,10 +198,17 @@ impl Application

{ #[cfg(feature = "tester")] let program = iced_tester::attach(self); - #[cfg(all(feature = "debug", not(feature = "tester")))] + #[cfg(all( + feature = "debug", + not(feature = "tester"), + not(target_arch = "wasm32") + ))] let program = iced_devtools::attach(self); - #[cfg(not(any(feature = "tester", feature = "debug")))] + #[cfg(not(any( + feature = "tester", + all(feature = "debug", not(target_arch = "wasm32")) + )))] let program = self; Ok(shell::run(program)?) diff --git a/src/lib.rs b/src/lib.rs index 129fc682..b3933670 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -625,6 +625,13 @@ pub mod widget { pub use iced_runtime::widget::*; pub use iced_widget::*; + #[cfg(feature = "image")] + pub mod image { + //! Images display raster graphics in different formats (PNG, JPG, etc.). + pub use iced_runtime::image::{Allocation, Error, allocate}; + pub use iced_widget::image::*; + } + // We hide the re-exported modules by `iced_widget` mod core {} mod graphics {} diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 0bd653d7..cc013a45 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -257,6 +257,10 @@ impl Emulator

{ // TODO dbg!(action); } + iced_runtime::Action::Image(action) => { + // TODO + dbg!(action); + } runtime::Action::Exit => { // TODO } diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index 2b829b0f..69423361 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -550,7 +550,7 @@ impl Engine { ) { match image { #[cfg(feature = "image")] - Image::Raster(raster, bounds) => { + Image::Raster { image, bounds, .. } => { let physical_bounds = *bounds * _transformation; if !_clip_bounds.intersects(&physical_bounds) { @@ -561,7 +561,7 @@ impl Engine { .then_some(_clip_mask as &_); let center = physical_bounds.center(); - let radians = f32::from(raster.rotation); + let radians = f32::from(image.rotation); let transform = into_transform(_transformation).post_rotate_at( radians.to_degrees(), @@ -570,17 +570,17 @@ impl Engine { ); self.raster_pipeline.draw( - &raster.handle, - raster.filter_method, + &image.handle, + image.filter_method, *bounds, - raster.opacity, + image.opacity, _pixels, transform, clip_mask, ); } #[cfg(feature = "svg")] - Image::Vector(svg, bounds) => { + Image::Vector { svg, bounds, .. } => { let physical_bounds = *bounds * _transformation; if !_clip_bounds.intersects(&physical_bounds) { diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index a1c0d8a6..a45d9522 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -307,7 +307,11 @@ impl geometry::frame::Backend for Frame { image.rotation += external_rotation; - self.images.push(graphics::Image::Raster(image, bounds)); + self.images.push(graphics::Image::Raster { + image, + bounds, + clip_bounds: self.clip_bounds, + }); } fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into) { @@ -318,7 +322,11 @@ impl geometry::frame::Backend for Frame { svg.rotation += external_rotation; - self.images.push(Image::Vector(svg, bounds)); + self.images.push(Image::Vector { + svg, + bounds, + clip_bounds: self.clip_bounds, + }); } } diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index 00097f83..951f4b14 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -117,11 +117,19 @@ impl Layer { pub fn draw_image(&mut self, image: Image, transformation: Transformation) { match image { - Image::Raster(raster, bounds) => { - self.draw_raster(raster, bounds, transformation); + Image::Raster { + image, + bounds, + clip_bounds, + } => { + self.draw_raster(image, bounds, clip_bounds, transformation); } - Image::Vector(svg, bounds) => { - self.draw_svg(svg, bounds, transformation); + Image::Vector { + svg, + bounds, + clip_bounds, + } => { + self.draw_svg(svg, bounds, clip_bounds, transformation); } } } @@ -130,9 +138,18 @@ impl Layer { &mut self, image: core::Image, bounds: Rectangle, + clip_bounds: Rectangle, transformation: Transformation, ) { - let image = Image::Raster(image, bounds * transformation); + let image = Image::Raster { + image: core::Image { + border_radius: image.border_radius + * transformation.scale_factor(), + ..image + }, + bounds: bounds * transformation, + clip_bounds: clip_bounds * transformation, + }; self.images.push(image); } @@ -141,9 +158,14 @@ impl Layer { &mut self, svg: Svg, bounds: Rectangle, + clip_bounds: Rectangle, transformation: Transformation, ) { - let svg = Image::Vector(svg, bounds * transformation); + let svg = Image::Vector { + svg, + bounds: bounds * transformation, + clip_bounds: clip_bounds * transformation, + }; self.images.push(svg); } diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 085468dc..0d04c033 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -228,6 +228,22 @@ impl core::Renderer for Renderer { fn reset(&mut self, new_bounds: Rectangle) { self.layers.reset(new_bounds); } + + fn allocate_image( + &mut self, + handle: &core::image::Handle, + callback: impl FnOnce(Result) + + Send + + 'static, + ) { + #[cfg(feature = "image")] + #[allow(unsafe_code)] + // TODO: Concurrency + callback(self.engine.raster_pipeline.load(handle)); + + #[cfg(not(feature = "image"))] + callback(Err(core::image::Error::Unsupported)) + } } impl core::text::Renderer for Renderer { @@ -350,13 +366,28 @@ impl graphics::mesh::Renderer for Renderer { impl core::image::Renderer for Renderer { type Handle = core::image::Handle; - fn measure_image(&self, handle: &Self::Handle) -> crate::core::Size { + fn load_image( + &self, + handle: &Self::Handle, + ) -> Result { + self.engine.raster_pipeline.load(handle) + } + + fn measure_image( + &self, + handle: &Self::Handle, + ) -> Option> { self.engine.raster_pipeline.dimensions(handle) } - fn draw_image(&mut self, image: core::Image, bounds: Rectangle) { + fn draw_image( + &mut self, + image: core::Image, + bounds: Rectangle, + clip_bounds: Rectangle, + ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_raster(image, bounds, transformation); + layer.draw_raster(image, bounds, clip_bounds, transformation); } } @@ -369,9 +400,14 @@ impl core::svg::Renderer for Renderer { self.engine.vector_pipeline.viewport_dimensions(handle) } - fn draw_svg(&mut self, svg: core::Svg, bounds: Rectangle) { + fn draw_svg( + &mut self, + svg: core::Svg, + bounds: Rectangle, + clip_bounds: Rectangle, + ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg(svg, bounds, transformation); + layer.draw_svg(svg, bounds, clip_bounds, transformation); } } diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index c40f55b2..56ed8c47 100644 --- a/tiny_skia/src/raster.rs +++ b/tiny_skia/src/raster.rs @@ -18,12 +18,24 @@ impl Pipeline { } } - pub fn dimensions(&self, handle: &raster::Handle) -> Size { - if let Some(image) = self.cache.borrow_mut().allocate(handle) { - Size::new(image.width(), image.height()) - } else { - Size::new(0, 0) - } + pub fn load( + &self, + handle: &raster::Handle, + ) -> Result { + let mut cache = self.cache.borrow_mut(); + let image = cache.allocate(handle)?; + + #[allow(unsafe_code)] + Ok(unsafe { + raster::allocate(handle, Size::new(image.width(), image.height())) + }) + } + + pub fn dimensions(&self, handle: &raster::Handle) -> Option> { + let mut cache = self.cache.borrow_mut(); + let image = cache.allocate(handle).ok()?; + + Some(Size::new(image.width(), image.height())) } pub fn draw( @@ -36,34 +48,34 @@ impl Pipeline { transform: tiny_skia::Transform, clip_mask: Option<&tiny_skia::Mask>, ) { - if let Some(image) = self.cache.borrow_mut().allocate(handle) { - let width_scale = bounds.width / image.width() as f32; - let height_scale = bounds.height / image.height() as f32; + let mut cache = self.cache.borrow_mut(); - let transform = transform.pre_scale(width_scale, height_scale); + let Ok(image) = cache.allocate(handle) else { + return; + }; - let quality = match filter_method { - raster::FilterMethod::Linear => { - tiny_skia::FilterQuality::Bilinear - } - raster::FilterMethod::Nearest => { - tiny_skia::FilterQuality::Nearest - } - }; + let width_scale = bounds.width / image.width() as f32; + let height_scale = bounds.height / image.height() as f32; - pixels.draw_pixmap( - (bounds.x / width_scale) as i32, - (bounds.y / height_scale) as i32, - image, - &tiny_skia::PixmapPaint { - quality, - opacity, - ..Default::default() - }, - transform, - clip_mask, - ); - } + let transform = transform.pre_scale(width_scale, height_scale); + + let quality = match filter_method { + raster::FilterMethod::Linear => tiny_skia::FilterQuality::Bilinear, + raster::FilterMethod::Nearest => tiny_skia::FilterQuality::Nearest, + }; + + pixels.draw_pixmap( + (bounds.x / width_scale) as i32, + (bounds.y / height_scale) as i32, + image, + &tiny_skia::PixmapPaint { + quality, + opacity, + ..Default::default() + }, + transform, + clip_mask, + ); } pub fn trim_cache(&mut self) { @@ -81,11 +93,18 @@ impl Cache { pub fn allocate( &mut self, handle: &raster::Handle, - ) -> Option> { + ) -> Result, raster::Error> { let id = handle.id(); if let hash_map::Entry::Vacant(entry) = self.entries.entry(id) { - let image = graphics::image::load(handle).ok()?; + let image = match graphics::image::load(handle) { + Ok(image) => image, + Err(error) => { + let _ = entry.insert(None); + + return Err(error); + } + }; let mut buffer = vec![0u32; image.width() as usize * image.height() as usize]; @@ -106,14 +125,21 @@ impl Cache { } let _ = self.hits.insert(id); - self.entries.get(&id).unwrap().as_ref().map(|entry| { - tiny_skia::PixmapRef::from_bytes( - bytemuck::cast_slice(&entry.pixels), - entry.width, - entry.height, - ) - .expect("Build pixmap from image bytes") - }) + + Ok(self + .entries + .get(&id) + .unwrap() + .as_ref() + .map(|entry| { + tiny_skia::PixmapRef::from_bytes( + bytemuck::cast_slice(&entry.pixels), + entry.width, + entry.height, + ) + .expect("Build pixmap from image bytes") + }) + .expect("Image should be allocated")) } fn trim(&mut self) { diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 9ede982d..b7511d71 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -2,7 +2,7 @@ use crate::core::{Color, Rectangle, Size}; use crate::graphics::compositor::{self, Information}; use crate::graphics::damage; use crate::graphics::error::{self, Error}; -use crate::graphics::{self, Viewport}; +use crate::graphics::{self, Shell, Viewport}; use crate::{Layer, Renderer, Settings}; use std::collections::VecDeque; @@ -31,6 +31,7 @@ impl crate::graphics::Compositor for Compositor { async fn with_backend( settings: graphics::Settings, compatible_window: W, + _shell: Shell, backend: Option<&str>, ) -> Result { match backend { diff --git a/wgpu/src/buffer.rs b/wgpu/src/buffer.rs index dba35e48..172922da 100644 --- a/wgpu/src/buffer.rs +++ b/wgpu/src/buffer.rs @@ -42,7 +42,7 @@ impl Buffer { } pub fn resize(&mut self, device: &wgpu::Device, new_count: usize) -> bool { - let new_size = (std::mem::size_of::() * new_count) as u64; + let new_size = next_copy_size::(new_count); if self.size < new_size { self.raw = device.create_buffer(&wgpu::BufferDescriptor { diff --git a/wgpu/src/engine.rs b/wgpu/src/engine.rs index 574223a4..4bc30b7f 100644 --- a/wgpu/src/engine.rs +++ b/wgpu/src/engine.rs @@ -1,4 +1,4 @@ -use crate::graphics::Antialiasing; +use crate::graphics::{Antialiasing, Shell}; use crate::primitive; use crate::quad; use crate::text; @@ -18,6 +18,7 @@ pub struct Engine { #[cfg(any(feature = "image", feature = "svg"))] pub(crate) image_pipeline: crate::image::Pipeline, pub(crate) primitive_storage: Arc>, + _shell: Shell, } impl Engine { @@ -27,6 +28,7 @@ impl Engine { queue: wgpu::Queue, format: wgpu::TextureFormat, antialiasing: Option, // TODO: Initialize AA pipelines lazily + shell: Shell, ) -> Self { Self { format, @@ -52,14 +54,16 @@ impl Engine { device, queue, + _shell: shell, } } #[cfg(any(feature = "image", feature = "svg"))] - pub fn create_image_cache( - &self, - device: &wgpu::Device, - ) -> crate::image::Cache { - self.image_pipeline.create_cache(device) + pub fn create_image_cache(&self) -> crate::image::Cache { + self.image_pipeline.create_cache( + &self.device, + &self.queue, + &self._shell, + ) } } diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 872d9fe1..f82adedb 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -431,8 +431,14 @@ impl geometry::frame::Backend for Frame { self.transforms.current.transform_rectangle(bounds); image.rotation += external_rotation; + image.border_radius = + image.border_radius * self.transforms.current.scale().0; - self.images.push(Image::Raster(image, bounds)); + self.images.push(Image::Raster { + image, + bounds, + clip_bounds: self.clip_bounds, + }); } fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into) { @@ -443,7 +449,11 @@ impl geometry::frame::Backend for Frame { svg.rotation += external_rotation; - self.images.push(Image::Vector(svg, bounds)); + self.images.push(Image::Vector { + svg, + bounds, + clip_bounds: self.clip_bounds, + }); } } diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs index 4e01fbe5..38de2462 100644 --- a/wgpu/src/image/atlas.rs +++ b/wgpu/src/image/atlas.rs @@ -10,7 +10,8 @@ pub use layer::Layer; use allocator::Allocator; -pub const SIZE: u32 = 2048; +pub const DEFAULT_SIZE: u32 = 2048; +pub const MAX_SIZE: u32 = 2048; use crate::core::Size; use crate::graphics::color; @@ -19,11 +20,12 @@ use std::sync::Arc; #[derive(Debug)] pub struct Atlas { + size: u32, backend: wgpu::Backend, texture: wgpu::Texture, texture_view: wgpu::TextureView, - texture_bind_group: wgpu::BindGroup, - texture_layout: Arc, + texture_bind_group: Arc, + texture_layout: wgpu::BindGroupLayout, layers: Vec, } @@ -31,8 +33,19 @@ impl Atlas { pub fn new( device: &wgpu::Device, backend: wgpu::Backend, - texture_layout: Arc, + texture_layout: wgpu::BindGroupLayout, ) -> Self { + Self::with_size(device, backend, texture_layout, DEFAULT_SIZE) + } + + pub fn with_size( + device: &wgpu::Device, + backend: wgpu::Backend, + texture_layout: wgpu::BindGroupLayout, + size: u32, + ) -> Self { + let size = size.min(MAX_SIZE); + let layers = match backend { // On the GL backend we start with 2 layers, to help wgpu figure // out that this texture is `GL_TEXTURE_2D_ARRAY` rather than `GL_TEXTURE_2D` @@ -42,8 +55,8 @@ impl Atlas { }; let extent = wgpu::Extent3d { - width: SIZE, - height: SIZE, + width: size, + height: size, depth_or_array_layers: layers.len() as u32, }; @@ -80,30 +93,28 @@ impl Atlas { }); Atlas { + size, backend, texture, texture_view, - texture_bind_group, + texture_bind_group: Arc::new(texture_bind_group), texture_layout, layers, } } - pub fn bind_group(&self) -> &wgpu::BindGroup { + pub fn bind_group(&self) -> &Arc { &self.texture_bind_group } - pub fn layer_count(&self) -> usize { - self.layers.len() - } - pub fn upload( &mut self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, width: u32, height: u32, - data: &[u8], + pixels: &[u8], ) -> Option { let entry = { let current_size = self.layers.len(); @@ -111,59 +122,32 @@ impl Atlas { // We grow the internal texture after allocating if necessary let new_layers = self.layers.len() - current_size; - self.grow(new_layers, device, encoder); + self.grow(new_layers, device, encoder, self.backend); entry }; log::debug!("Allocated atlas entry: {entry:?}"); - // It is a webgpu requirement that: - // BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0 - // So we calculate padded_width by rounding width up to the next - // multiple of wgpu::COPY_BYTES_PER_ROW_ALIGNMENT. - let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; - let padding = (align - (4 * width) % align) % align; - let padded_width = (4 * width + padding) as usize; - let padded_data_size = padded_width * height as usize; - - let mut padded_data = vec![0; padded_data_size]; - - for row in 0..height as usize { - let offset = row * padded_width; - - padded_data[offset..offset + 4 * width as usize].copy_from_slice( - &data[row * 4 * width as usize..(row + 1) * 4 * width as usize], - ); - } - match &entry { Entry::Contiguous(allocation) => { self.upload_allocation( - &padded_data, - width, - height, - padding, - 0, - allocation, - device, - encoder, + pixels, width, 0, allocation, device, encoder, belt, ); } Entry::Fragmented { fragments, .. } => { for fragment in fragments { let (x, y) = fragment.position; - let offset = (y * padded_width as u32 + 4 * x) as usize; + let offset = 4 * (y * width + x) as usize; self.upload_allocation( - &padded_data, + pixels, width, - height, - padding, offset, &fragment.allocation, device, encoder, + belt, ); } } @@ -172,7 +156,7 @@ impl Atlas { if log::log_enabled!(log::Level::Debug) { log::debug!( "Atlas layers: {} (busy: {}, allocations: {})", - self.layer_count(), + self.layers.len(), self.layers.iter().filter(|layer| !layer.is_empty()).count(), self.layers.iter().map(Layer::allocations).sum::(), ); @@ -198,7 +182,7 @@ impl Atlas { fn allocate(&mut self, width: u32, height: u32) -> Option { // Allocate one layer if texture fits perfectly - if width == SIZE && height == SIZE { + if width == self.size && height == self.size { let mut empty_layers = self .layers .iter_mut() @@ -208,27 +192,31 @@ impl Atlas { if let Some((i, layer)) = empty_layers.next() { *layer = Layer::Full; - return Some(Entry::Contiguous(Allocation::Full { layer: i })); + return Some(Entry::Contiguous(Allocation::Full { + layer: i, + size: self.size, + })); } self.layers.push(Layer::Full); return Some(Entry::Contiguous(Allocation::Full { layer: self.layers.len() - 1, + size: self.size, })); } // Split big textures across multiple layers - if width > SIZE || height > SIZE { + if width > self.size || height > self.size { let mut fragments = Vec::new(); let mut y = 0; while y < height { - let height = std::cmp::min(height - y, SIZE); + let height = std::cmp::min(height - y, self.size); let mut x = 0; while x < width { - let width = std::cmp::min(width - x, SIZE); + let width = std::cmp::min(width - x, self.size); let allocation = self.allocate(width, height)?; @@ -255,7 +243,7 @@ impl Atlas { for (i, layer) in self.layers.iter_mut().enumerate() { match layer { Layer::Empty => { - let mut allocator = Allocator::new(SIZE); + let mut allocator = Allocator::new(self.size); if let Some(region) = allocator.allocate(width, height) { *layer = Layer::Busy(allocator); @@ -263,6 +251,7 @@ impl Atlas { return Some(Entry::Contiguous(Allocation::Partial { region, layer: i, + atlas_size: self.size, })); } } @@ -271,6 +260,7 @@ impl Atlas { return Some(Entry::Contiguous(Allocation::Partial { region, layer: i, + atlas_size: self.size, })); } } @@ -279,7 +269,7 @@ impl Atlas { } // Create new layer with atlas allocator - let mut allocator = Allocator::new(SIZE); + let mut allocator = Allocator::new(self.size); if let Some(region) = allocator.allocate(width, height) { self.layers.push(Layer::Busy(allocator)); @@ -287,6 +277,7 @@ impl Atlas { return Some(Entry::Contiguous(Allocation::Partial { region, layer: self.layers.len() - 1, + atlas_size: self.size, })); } @@ -298,10 +289,10 @@ impl Atlas { log::debug!("Deallocating atlas: {allocation:?}"); match allocation { - Allocation::Full { layer } => { + Allocation::Full { layer, .. } => { self.layers[*layer] = Layer::Empty; } - Allocation::Partial { layer, region } => { + Allocation::Partial { layer, region, .. } => { let layer = &mut self.layers[*layer]; if let Layer::Busy(allocator) = layer { @@ -316,55 +307,134 @@ impl Atlas { } fn upload_allocation( - &mut self, - data: &[u8], + &self, + pixels: &[u8], image_width: u32, - image_height: u32, - padding: u32, offset: usize, allocation: &Allocation, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, ) { - use wgpu::util::DeviceExt; - let (x, y) = allocation.position(); let Size { width, height } = allocation.size(); let layer = allocation.layer(); + let padding = allocation.padding(); - let extent = wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }; + // It is a webgpu requirement that: + // BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0 + // So we calculate bytes_per_row by rounding width up to the next + // multiple of wgpu::COPY_BYTES_PER_ROW_ALIGNMENT. + let bytes_per_row = (4 * (width + padding.width * 2)) + .next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) + as usize; + let total_bytes = + bytes_per_row * (height + padding.height * 2) as usize; - let buffer = - device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("image upload buffer"), - contents: data, - usage: wgpu::BufferUsages::COPY_SRC, - }); + let buffer_slice = belt.allocate( + wgpu::BufferSize::new(total_bytes as u64).unwrap(), + wgpu::BufferSize::new(8 * 4).unwrap(), + device, + ); + const PIXEL: usize = 4; + + let mut fragment = buffer_slice.get_mapped_range_mut(); + let w = width as usize; + let h = height as usize; + let pad_w = padding.width as usize; + let pad_h = padding.height as usize; + let stride = PIXEL * w; + + // Copy image rows + for row in 0..h { + let src = offset + row * PIXEL * image_width as usize; + let dst = (row + pad_h) * bytes_per_row; + + fragment[dst + PIXEL * pad_w..dst + PIXEL * pad_w + stride] + .copy_from_slice(&pixels[src..src + stride]); + + // Add padding to the sides, if needed + for i in 0..pad_w { + fragment[dst + PIXEL * i..dst + PIXEL * (i + 1)] + .copy_from_slice(&pixels[src..src + PIXEL]); + + fragment[dst + stride + PIXEL * (pad_w + i) + ..dst + stride + PIXEL * (pad_w + i + 1)] + .copy_from_slice( + &pixels[src + stride - PIXEL..src + stride], + ); + } + } + + // Add padding on top and bottom + for row in 0..pad_h { + let dst_top = row * bytes_per_row; + let dst_bottom = (pad_h + h + row) * bytes_per_row; + let src_top = offset; + let src_bottom = offset + (h - 1) * PIXEL * image_width as usize; + + // Top + fragment[dst_top + PIXEL * pad_w..dst_top + PIXEL * (pad_w + w)] + .copy_from_slice(&pixels[src_top..src_top + PIXEL * w]); + + // Bottom + fragment + [dst_bottom + PIXEL * pad_w..dst_bottom + PIXEL * (pad_w + w)] + .copy_from_slice(&pixels[src_bottom..src_bottom + PIXEL * w]); + + // Corners + for i in 0..pad_w { + // Top left + fragment[dst_top + PIXEL * i..dst_top + PIXEL * (i + 1)] + .copy_from_slice(&pixels[offset..offset + PIXEL]); + + // Top right + fragment[dst_top + PIXEL * (w + pad_w + i) + ..dst_top + PIXEL * (w + pad_w + i + 1)] + .copy_from_slice( + &pixels[offset + PIXEL * (w - 1)..offset + PIXEL * w], + ); + + // Bottom left + fragment[dst_bottom + PIXEL * i..dst_bottom + PIXEL * (i + 1)] + .copy_from_slice(&pixels[src_bottom..src_bottom + PIXEL]); + + // Bottom right + fragment[dst_bottom + PIXEL * (w + pad_w + i) + ..dst_bottom + PIXEL * (w + pad_w + i + 1)] + .copy_from_slice( + &pixels[src_bottom + PIXEL * (w - 1) + ..src_bottom + PIXEL * w], + ); + } + } + + // Copy actual image encoder.copy_buffer_to_texture( wgpu::TexelCopyBufferInfo { - buffer: &buffer, + buffer: buffer_slice.buffer(), layout: wgpu::TexelCopyBufferLayout { - offset: offset as u64, - bytes_per_row: Some(4 * image_width + padding), - rows_per_image: Some(image_height), + offset: buffer_slice.offset(), + bytes_per_row: Some(bytes_per_row as u32), + rows_per_image: Some(height + padding.height * 2), }, }, wgpu::TexelCopyTextureInfo { texture: &self.texture, mip_level: 0, origin: wgpu::Origin3d { - x, - y, + x: x - padding.width, + y: y - padding.height, z: layer as u32, }, aspect: wgpu::TextureAspect::default(), }, - extent, + wgpu::Extent3d { + width: width + padding.width * 2, + height: height + padding.height * 2, + depth_or_array_layers: 1, + }, ); } @@ -373,6 +443,7 @@ impl Atlas { amount: usize, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, + backend: wgpu::Backend, ) { if amount == 0 { return; @@ -383,7 +454,7 @@ impl Atlas { // some unused memory on GL, but it's better than not being able to grow the atlas past a depth // of 6! // https://github.com/gfx-rs/wgpu/blob/004e3efe84a320d9331371ed31fa50baa2414911/wgpu-hal/src/gles/mod.rs#L371 - let depth_or_array_layers = match self.backend { + let depth_or_array_layers = match backend { wgpu::Backend::Gl if self.layers.len() == 6 => 7, _ => self.layers.len() as u32, }; @@ -391,8 +462,8 @@ impl Atlas { let new_texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("iced_wgpu::image texture atlas"), size: wgpu::Extent3d { - width: SIZE, - height: SIZE, + width: self.size, + height: self.size, depth_or_array_layers, }, mip_level_count: 1, @@ -440,8 +511,8 @@ impl Atlas { aspect: wgpu::TextureAspect::default(), }, wgpu::Extent3d { - width: SIZE, - height: SIZE, + width: self.size, + height: self.size, depth_or_array_layers: 1, }, ); @@ -455,7 +526,7 @@ impl Atlas { }); self.texture_bind_group = - device.create_bind_group(&wgpu::BindGroupDescriptor { + Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("iced_wgpu::image texture atlas bind group"), layout: &self.texture_layout, entries: &[wgpu::BindGroupEntry { @@ -464,6 +535,6 @@ impl Atlas { &self.texture_view, ), }], - }); + })); } } diff --git a/wgpu/src/image/atlas/allocation.rs b/wgpu/src/image/atlas/allocation.rs index 11289771..295a7a13 100644 --- a/wgpu/src/image/atlas/allocation.rs +++ b/wgpu/src/image/atlas/allocation.rs @@ -1,14 +1,16 @@ use crate::core::Size; -use crate::image::atlas::{self, allocator}; +use crate::image::atlas::allocator; #[derive(Debug)] pub enum Allocation { Partial { layer: usize, region: allocator::Region, + atlas_size: u32, }, Full { layer: usize, + size: u32, }, } @@ -23,14 +25,28 @@ impl Allocation { pub fn size(&self) -> Size { match self { Allocation::Partial { region, .. } => region.size(), - Allocation::Full { .. } => Size::new(atlas::SIZE, atlas::SIZE), + Allocation::Full { size, .. } => Size::new(*size, *size), + } + } + + pub fn padding(&self) -> Size { + match self { + Allocation::Partial { region, .. } => region.padding(), + Allocation::Full { .. } => Size::new(0, 0), } } pub fn layer(&self) -> usize { match self { Allocation::Partial { layer, .. } => *layer, - Allocation::Full { layer } => *layer, + Allocation::Full { layer, .. } => *layer, + } + } + + pub fn atlas_size(&self) -> u32 { + match self { + Allocation::Partial { atlas_size, .. } => *atlas_size, + Allocation::Full { size, .. } => *size, } } } diff --git a/wgpu/src/image/atlas/allocator.rs b/wgpu/src/image/atlas/allocator.rs index a51ac1f5..263619f6 100644 --- a/wgpu/src/image/atlas/allocator.rs +++ b/wgpu/src/image/atlas/allocator.rs @@ -1,3 +1,5 @@ +use crate::core; + use guillotiere::{AtlasAllocator, Size}; pub struct Allocator { @@ -6,6 +8,8 @@ pub struct Allocator { } impl Allocator { + const PADDING: u32 = 1; + pub fn new(size: u32) -> Allocator { let raw = AtlasAllocator::new(Size::new(size as i32, size as i32)); @@ -16,12 +20,38 @@ impl Allocator { } pub fn allocate(&mut self, width: u32, height: u32) -> Option { - let allocation = - self.raw.allocate(Size::new(width as i32, height as i32))?; + let size = self.raw.size(); + + let padded_width = width + Self::PADDING * 2; + let padded_height = height + Self::PADDING * 2; + + let pad_width = padded_width as i32 <= size.width; + let pad_height = padded_height as i32 <= size.height; + + let mut allocation = self.raw.allocate(Size::new( + if pad_width { padded_width } else { width } as i32, + if pad_height { padded_height } else { height } as i32, + ))?; + + if pad_width { + allocation.rectangle.min.x += Self::PADDING as i32; + allocation.rectangle.max.x -= Self::PADDING as i32; + } + + if pad_height { + allocation.rectangle.min.y += Self::PADDING as i32; + allocation.rectangle.max.y -= Self::PADDING as i32; + } self.allocations += 1; - Some(Region { allocation }) + Some(Region { + allocation, + padding: core::Size::new( + if pad_width { Self::PADDING } else { 0 }, + if pad_height { Self::PADDING } else { 0 }, + ), + }) } pub fn deallocate(&mut self, region: &Region) { @@ -41,6 +71,7 @@ impl Allocator { pub struct Region { allocation: guillotiere::Allocation, + padding: core::Size, } impl Region { @@ -50,10 +81,14 @@ impl Region { (rectangle.min.x as u32, rectangle.min.y as u32) } - pub fn size(&self) -> crate::core::Size { + pub fn size(&self) -> core::Size { let size = self.allocation.rectangle.size(); - crate::core::Size::new(size.width as u32, size.height as u32) + core::Size::new(size.width as u32, size.height as u32) + } + + pub fn padding(&self) -> crate::core::Size { + self.padding } } diff --git a/wgpu/src/image/cache.rs b/wgpu/src/image/cache.rs index 94f7071d..f832b239 100644 --- a/wgpu/src/image/cache.rs +++ b/wgpu/src/image/cache.rs @@ -1,47 +1,199 @@ use crate::core::{self, Size}; +use crate::graphics::Shell; use crate::image::atlas::{self, Atlas}; +#[cfg(all(feature = "image", not(target_arch = "wasm32")))] +use worker::Worker; + +#[cfg(feature = "image")] +use std::collections::HashMap; + use std::sync::Arc; -#[derive(Debug)] pub struct Cache { atlas: Atlas, #[cfg(feature = "image")] - raster: crate::image::raster::Cache, + raster: Raster, #[cfg(feature = "svg")] vector: crate::image::vector::Cache, + #[cfg(all(feature = "image", not(target_arch = "wasm32")))] + worker: Worker, } impl Cache { pub fn new( device: &wgpu::Device, + _queue: &wgpu::Queue, backend: wgpu::Backend, - layout: Arc, + layout: wgpu::BindGroupLayout, + _shell: &Shell, ) -> Self { + #[cfg(all(feature = "image", not(target_arch = "wasm32")))] + let worker = + Worker::new(device, _queue, backend, layout.clone(), _shell); + Self { atlas: Atlas::new(device, backend, layout), #[cfg(feature = "image")] - raster: crate::image::raster::Cache::default(), + raster: Raster { + cache: crate::image::raster::Cache::default(), + pending: HashMap::new(), + belt: wgpu::util::StagingBelt::new(2 * 1024 * 1024), + }, #[cfg(feature = "svg")] vector: crate::image::vector::Cache::default(), + #[cfg(all(feature = "image", not(target_arch = "wasm32")))] + worker, } } - pub fn bind_group(&self) -> &wgpu::BindGroup { - self.atlas.bind_group() - } + #[cfg(feature = "image")] + pub fn allocate_image( + &mut self, + handle: &core::image::Handle, + callback: impl FnOnce(Result) + + Send + + 'static, + ) { + use crate::image::raster::Memory; - pub fn layer_count(&self) -> usize { - self.atlas.layer_count() + let callback = Box::new(callback); + + if let Some(callbacks) = self.raster.pending.get_mut(&handle.id()) { + callbacks.push(callback); + return; + } + + if let Some(Memory::Device { + allocation, entry, .. + }) = self.raster.cache.get_mut(handle) + { + if let Some(allocation) = allocation + .as_ref() + .and_then(core::image::Allocation::upgrade) + { + callback(Ok(allocation)); + return; + } + + #[allow(unsafe_code)] + let new = unsafe { core::image::allocate(handle, entry.size()) }; + *allocation = Some(new.downgrade()); + callback(Ok(new)); + + return; + } + + let _ = self.raster.pending.insert(handle.id(), vec![callback]); + + #[cfg(not(target_arch = "wasm32"))] + self.worker.load(handle); } #[cfg(feature = "image")] - pub fn measure_image(&mut self, handle: &core::image::Handle) -> Size { - self.raster.load(handle).dimensions() + pub fn load_image( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + handle: &core::image::Handle, + ) -> Result { + use crate::image::raster::Memory; + + if !self.raster.cache.contains(handle) { + self.raster.cache.insert(handle, Memory::load(handle)); + } + + match self.raster.cache.get_mut(handle).unwrap() { + Memory::Host(image) => { + let mut encoder = device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("raster image upload"), + }, + ); + + let entry = self.atlas.upload( + device, + &mut encoder, + &mut self.raster.belt, + image.width(), + image.height(), + image, + ); + + self.raster.belt.finish(); + let submission = queue.submit([encoder.finish()]); + self.raster.belt.recall(); + + let Some(entry) = entry else { + return Err(core::image::Error::OutOfMemory); + }; + + let _ = device + .poll(wgpu::PollType::WaitForSubmissionIndex(submission)); + + #[allow(unsafe_code)] + let allocation = unsafe { + core::image::allocate( + handle, + Size::new(image.width(), image.height()), + ) + }; + + self.raster.cache.insert( + handle, + Memory::Device { + entry, + bind_group: None, + allocation: Some(allocation.downgrade()), + }, + ); + + Ok(allocation) + } + Memory::Device { + entry, allocation, .. + } => { + if let Some(allocation) = allocation + .as_ref() + .and_then(core::image::Allocation::upgrade) + { + return Ok(allocation); + } + + #[allow(unsafe_code)] + let new = + unsafe { core::image::allocate(handle, entry.size()) }; + + *allocation = Some(new.downgrade()); + + Ok(new) + } + Memory::Error(error) => Err(error.clone()), + } + } + + #[cfg(feature = "image")] + pub fn measure_image( + &mut self, + handle: &core::image::Handle, + ) -> Option> { + self.receive(); + + let image = load_image( + &mut self.raster.cache, + &mut self.raster.pending, + #[cfg(not(target_arch = "wasm32"))] + &self.worker, + handle, + None, + )?; + + Some(image.dimensions()) } #[cfg(feature = "svg")] pub fn measure_svg(&mut self, handle: &core::svg::Handle) -> Size { + // TODO: Concurrency self.vector.load(handle).viewport_dimensions() } @@ -50,9 +202,66 @@ impl Cache { &mut self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, handle: &core::image::Handle, - ) -> Option<&atlas::Entry> { - self.raster.upload(device, encoder, handle, &mut self.atlas) + ) -> Option<(&atlas::Entry, &Arc)> { + use crate::image::raster::Memory; + + self.receive(); + + let memory = load_image( + &mut self.raster.cache, + &mut self.raster.pending, + #[cfg(not(target_arch = "wasm32"))] + &self.worker, + handle, + None, + )?; + + if let Memory::Device { + entry, bind_group, .. + } = memory + { + return Some(( + entry, + bind_group.as_ref().unwrap_or(self.atlas.bind_group()), + )); + } + + let image = memory.host()?; + + const MAX_SYNC_SIZE: usize = 2 * 1024 * 1024; + + // TODO: Concurrent Wasm support + if image.len() < MAX_SYNC_SIZE || cfg!(target_arch = "wasm32") { + let entry = self.atlas.upload( + device, + encoder, + belt, + image.width(), + image.height(), + &image, + )?; + + *memory = Memory::Device { + entry, + bind_group: None, + allocation: None, + }; + + if let Memory::Device { entry, .. } = memory { + return Some((entry, self.atlas.bind_group())); + } + } + + if !self.raster.pending.contains_key(&handle.id()) { + let _ = self.raster.pending.insert(handle.id(), Vec::new()); + + #[cfg(not(target_arch = "wasm32"))] + self.worker.upload(handle, image); + } + + None } #[cfg(feature = "svg")] @@ -60,27 +269,361 @@ impl Cache { &mut self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, handle: &core::svg::Handle, color: Option, - size: [f32; 2], + size: Size, scale: f32, - ) -> Option<&atlas::Entry> { - self.vector.upload( - device, - encoder, - handle, - color, - size, - scale, - &mut self.atlas, - ) + ) -> Option<(&atlas::Entry, &Arc)> { + // TODO: Concurrency + self.vector + .upload( + device, + encoder, + belt, + handle, + color, + size, + scale, + &mut self.atlas, + ) + .map(|entry| (entry, self.atlas.bind_group())) } pub fn trim(&mut self) { #[cfg(feature = "image")] - self.raster.trim(&mut self.atlas); + self.raster.cache.trim(&mut self.atlas, |_bind_group| { + #[cfg(not(target_arch = "wasm32"))] + self.worker.drop(_bind_group); + }); #[cfg(feature = "svg")] - self.vector.trim(&mut self.atlas); + self.vector.trim(&mut self.atlas); // TODO: Concurrency + } + + #[cfg(feature = "image")] + fn receive(&mut self) { + #[cfg(not(target_arch = "wasm32"))] + while let Ok(work) = self.worker.try_recv() { + use crate::image::raster::Memory; + + match work { + worker::Work::Upload { + handle, + entry, + bind_group, + } => { + let callbacks = self.raster.pending.remove(&handle.id()); + + let allocation = if let Some(callbacks) = callbacks { + #[allow(unsafe_code)] + let allocation = unsafe { + core::image::allocate(&handle, entry.size()) + }; + + let reference = allocation.downgrade(); + + for callback in callbacks { + callback(Ok(allocation.clone())); + } + + Some(reference) + } else { + None + }; + + self.raster.cache.insert( + &handle, + Memory::Device { + entry, + bind_group: Some(bind_group), + allocation, + }, + ); + } + worker::Work::Error { handle, error } => { + let callbacks = self.raster.pending.remove(&handle.id()); + + if let Some(callbacks) = callbacks { + for callback in callbacks { + callback(Err(error.clone())); + } + } + + self.raster.cache.insert(&handle, Memory::Error(error)); + } + } + } + } +} + +#[cfg(all(feature = "image", not(target_arch = "wasm32")))] +impl Drop for Cache { + fn drop(&mut self) { + self.worker.quit(); + } +} + +#[cfg(feature = "image")] +struct Raster { + cache: crate::image::raster::Cache, + pending: HashMap>, + belt: wgpu::util::StagingBelt, +} + +#[cfg(feature = "image")] +type Callback = + Box) + Send>; + +#[cfg(feature = "image")] +fn load_image<'a>( + cache: &'a mut crate::image::raster::Cache, + pending: &mut HashMap>, + #[cfg(not(target_arch = "wasm32"))] worker: &Worker, + handle: &core::image::Handle, + callback: Option, +) -> Option<&'a mut crate::image::raster::Memory> { + use crate::image::raster::Memory; + + if !cache.contains(handle) { + if cfg!(target_arch = "wasm32") { + // TODO: Concurrent support for Wasm + cache.insert(handle, Memory::load(handle)); + } else if let core::image::Handle::Rgba { .. } = handle { + // Load RGBA handles synchronously, since it's very cheap + cache.insert(handle, Memory::load(handle)); + } else if !pending.contains_key(&handle.id()) { + let _ = pending.insert(handle.id(), Vec::from_iter(callback)); + + #[cfg(not(target_arch = "wasm32"))] + worker.load(handle); + } + } + + cache.get_mut(handle) +} + +#[cfg(all(feature = "image", not(target_arch = "wasm32")))] +mod worker { + use crate::core::image; + use crate::graphics::Shell; + use crate::image::atlas::{self, Atlas}; + use crate::image::raster; + + use std::sync::Arc; + use std::sync::mpsc; + use std::thread; + + pub struct Worker { + jobs: mpsc::SyncSender, + quit: mpsc::SyncSender<()>, + work: mpsc::Receiver, + handle: Option>, + } + + impl Worker { + pub fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + backend: wgpu::Backend, + texture_layout: wgpu::BindGroupLayout, + shell: &Shell, + ) -> Self { + let (jobs_sender, jobs_receiver) = mpsc::sync_channel(1_000); + let (quit_sender, quit_receiver) = mpsc::sync_channel(1); + let (work_sender, work_receiver) = mpsc::sync_channel(1_000); + + let instance = Instance { + device: device.clone(), + queue: queue.clone(), + backend, + texture_layout, + shell: shell.clone(), + belt: wgpu::util::StagingBelt::new(4 * 1024 * 1024), + jobs: jobs_receiver, + output: work_sender, + quit: quit_receiver, + }; + + let handle = thread::spawn(move || instance.run()); + + Self { + jobs: jobs_sender, + quit: quit_sender, + work: work_receiver, + handle: Some(handle), + } + } + + pub fn load(&self, handle: &image::Handle) { + let _ = self.jobs.send(Job::Load(handle.clone())); + } + + pub fn upload(&self, handle: &image::Handle, image: raster::Image) { + let _ = self.jobs.send(Job::Upload { + handle: handle.clone(), + width: image.width(), + height: image.height(), + rgba: image.into_raw(), + }); + } + + pub fn drop(&self, bind_group: Arc) { + let _ = self.jobs.send(Job::Drop(bind_group)); + } + + pub fn try_recv(&self) -> Result { + self.work.try_recv() + } + + pub fn quit(&mut self) { + let _ = self.quit.try_send(()); + let _ = self.jobs.send(Job::Quit); + let _ = self.handle.take().map(thread::JoinHandle::join); + } + } + + pub struct Instance { + device: wgpu::Device, + queue: wgpu::Queue, + backend: wgpu::Backend, + texture_layout: wgpu::BindGroupLayout, + shell: Shell, + belt: wgpu::util::StagingBelt, + jobs: mpsc::Receiver, + output: mpsc::SyncSender, + quit: mpsc::Receiver<()>, + } + + #[derive(Debug)] + enum Job { + Load(image::Handle), + Upload { + handle: image::Handle, + rgba: image::Bytes, + width: u32, + height: u32, + }, + Drop(Arc), + Quit, + } + + pub enum Work { + Upload { + handle: image::Handle, + entry: atlas::Entry, + bind_group: Arc, + }, + Error { + handle: image::Handle, + error: image::Error, + }, + } + + impl Instance { + fn run(mut self) { + loop { + if self.quit.try_recv().is_ok() { + return; + } + + let Ok(job) = self.jobs.recv() else { + return; + }; + + match job { + Job::Load(handle) => { + match crate::graphics::image::load(&handle) { + Ok(image) => self.upload( + handle, + image.width(), + image.height(), + image.into_raw(), + Shell::invalidate_layout, + ), + Err(error) => { + let _ = self + .output + .send(Work::Error { handle, error }); + } + } + } + Job::Upload { + handle, + rgba, + width, + height, + } => { + self.upload( + handle, + width, + height, + rgba, + Shell::request_redraw, + ); + } + Job::Drop(bind_group) => { + drop(bind_group); + } + Job::Quit => return, + } + } + } + + fn upload( + &mut self, + handle: image::Handle, + width: u32, + height: u32, + rgba: image::Bytes, + callback: fn(&Shell), + ) { + let mut encoder = self.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("raster image upload"), + }, + ); + + let mut atlas = Atlas::with_size( + &self.device, + self.backend, + self.texture_layout.clone(), + width.max(height), + ); + + let Some(entry) = atlas.upload( + &self.device, + &mut encoder, + &mut self.belt, + width, + height, + &rgba, + ) else { + return; + }; + + let output = self.output.clone(); + let shell = self.shell.clone(); + + self.belt.finish(); + let submission = self.queue.submit([encoder.finish()]); + self.belt.recall(); + + let bind_group = atlas.bind_group().clone(); + + self.queue.on_submitted_work_done(move || { + let _ = output.send(Work::Upload { + handle, + entry, + bind_group, + }); + + callback(&shell); + }); + + let _ = self + .device + .poll(wgpu::PollType::WaitForSubmissionIndex(submission)); + } } } diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index 51d2acef..ba98f443 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -10,7 +10,9 @@ mod raster; mod vector; use crate::Buffer; +use crate::core::border; use crate::core::{Rectangle, Size, Transformation}; +use crate::graphics::Shell; use bytemuck::{Pod, Zeroable}; @@ -27,7 +29,7 @@ pub struct Pipeline { backend: wgpu::Backend, nearest_sampler: wgpu::Sampler, linear_sampler: wgpu::Sampler, - texture_layout: Arc, + texture_layout: wgpu::BindGroupLayout, constant_layout: wgpu::BindGroupLayout, } @@ -131,24 +133,26 @@ impl Pipeline { array_stride: mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: &wgpu::vertex_attr_array!( - // Position - 0 => Float32x2, // Center - 1 => Float32x2, - // Scale - 2 => Float32x2, + 0 => Float32x2, + // Clip bounds + 1 => Float32x4, + // Border radius + 2 => Float32x4, + // Tile + 3 => Float32x4, // Rotation - 3 => Float32, - // Opacity 4 => Float32, + // Opacity + 5 => Float32, // Atlas position - 5 => Float32x2, - // Atlas scale 6 => Float32x2, + // Atlas scale + 7 => Float32x2, // Layer - 7 => Sint32, + 8 => Sint32, // Snap - 8 => Uint32, + 9 => Uint32, ), }], compilation_options: @@ -196,13 +200,24 @@ impl Pipeline { backend, nearest_sampler, linear_sampler, - texture_layout: Arc::new(texture_layout), + texture_layout, constant_layout, } } - pub fn create_cache(&self, device: &wgpu::Device) -> Cache { - Cache::new(device, self.backend, self.texture_layout.clone()) + pub fn create_cache( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + shell: &Shell, + ) -> Cache { + Cache::new( + device, + queue, + self.backend, + self.texture_layout.clone(), + shell, + ) } } @@ -210,6 +225,8 @@ impl Pipeline { pub struct State { layers: Vec, prepare_layer: usize, + nearest_instances: Vec, + linear_instances: Vec, } impl State { @@ -228,69 +245,6 @@ impl State { transformation: Transformation, scale: f32, ) { - let nearest_instances: &mut Vec = &mut Vec::new(); - let linear_instances: &mut Vec = &mut Vec::new(); - - for image in images { - match &image { - #[cfg(feature = "image")] - Image::Raster(image, bounds) => { - if let Some(atlas_entry) = - cache.upload_raster(device, encoder, &image.handle) - { - add_instances( - [bounds.x, bounds.y], - [bounds.width, bounds.height], - f32::from(image.rotation), - image.opacity, - image.snap, - atlas_entry, - match image.filter_method { - crate::core::image::FilterMethod::Nearest => { - nearest_instances - } - crate::core::image::FilterMethod::Linear => { - linear_instances - } - }, - ); - } - } - #[cfg(not(feature = "image"))] - Image::Raster { .. } => {} - - #[cfg(feature = "svg")] - Image::Vector(svg, bounds) => { - let size = [bounds.width, bounds.height]; - - if let Some(atlas_entry) = cache.upload_vector( - device, - encoder, - &svg.handle, - svg.color, - size, - scale, - ) { - add_instances( - [bounds.x, bounds.y], - size, - f32::from(svg.rotation), - svg.opacity, - true, - atlas_entry, - nearest_instances, - ); - } - } - #[cfg(not(feature = "svg"))] - Image::Vector { .. } => {} - } - } - - if nearest_instances.is_empty() && linear_instances.is_empty() { - return; - } - if self.layers.len() <= self.prepare_layer { self.layers.push(Layer::new( device, @@ -302,23 +256,129 @@ impl State { let layer = &mut self.layers[self.prepare_layer]; + let mut atlas: Option> = None; + + for image in images { + match &image { + #[cfg(feature = "image")] + Image::Raster { + image, + bounds, + clip_bounds, + } => { + if let Some((atlas_entry, bind_group)) = cache + .upload_raster(device, encoder, belt, &image.handle) + { + match atlas.as_mut() { + None => { + atlas = Some(bind_group.clone()); + } + Some(atlas) if atlas != bind_group => { + layer.push( + atlas, + &self.nearest_instances, + &self.linear_instances, + ); + + *atlas = Arc::clone(bind_group); + } + _ => {} + } + + add_instances( + *bounds, + *clip_bounds, + image.border_radius, + f32::from(image.rotation), + image.opacity, + image.snap, + atlas_entry, + match image.filter_method { + crate::core::image::FilterMethod::Nearest => { + &mut self.nearest_instances + } + crate::core::image::FilterMethod::Linear => { + &mut self.linear_instances + } + }, + ); + } + } + #[cfg(not(feature = "image"))] + Image::Raster { .. } => continue, + + #[cfg(feature = "svg")] + Image::Vector { + svg, + bounds, + clip_bounds, + } => { + if let Some((atlas_entry, bind_group)) = cache + .upload_vector( + device, + encoder, + belt, + &svg.handle, + svg.color, + bounds.size(), + scale, + ) + { + match atlas.as_mut() { + None => { + atlas = Some(bind_group.clone()); + } + Some(atlas) if atlas != bind_group => { + layer.push( + atlas, + &self.nearest_instances, + &self.linear_instances, + ); + + *atlas = bind_group.clone(); + } + _ => {} + } + + add_instances( + *bounds, + *clip_bounds, + border::radius(0), + f32::from(svg.rotation), + svg.opacity, + true, + atlas_entry, + &mut self.nearest_instances, + ); + } + } + #[cfg(not(feature = "svg"))] + Image::Vector { .. } => continue, + } + } + + if let Some(atlas) = &atlas { + layer.push(atlas, &self.nearest_instances, &self.linear_instances); + } + layer.prepare( device, encoder, belt, - nearest_instances, - linear_instances, transformation, scale, + &self.nearest_instances, + &self.linear_instances, ); self.prepare_layer += 1; + self.nearest_instances.clear(); + self.linear_instances.clear(); } pub fn render<'a>( &'a self, pipeline: &'a Pipeline, - cache: &'a Cache, layer: usize, bounds: Rectangle, render_pass: &mut wgpu::RenderPass<'a>, @@ -333,13 +393,15 @@ impl State { bounds.height, ); - render_pass.set_bind_group(1, cache.bind_group(), &[]); - layer.render(render_pass); } } pub fn trim(&mut self) { + for layer in &mut self.layers[..self.prepare_layer] { + layer.clear(); + } + self.prepare_layer = 0; } } @@ -347,8 +409,19 @@ impl State { #[derive(Debug)] struct Layer { uniforms: wgpu::Buffer, - nearest: Data, - linear: Data, + instances: Buffer, + nearest: Vec, + nearest_layout: wgpu::BindGroup, + nearest_total: usize, + linear: Vec, + linear_layout: wgpu::BindGroup, + linear_total: usize, +} + +#[derive(Debug)] +struct Group { + atlas: Arc, + instance_count: usize, } impl Layer { @@ -365,16 +438,70 @@ impl Layer { mapped_at_creation: false, }); - let nearest = - Data::new(device, constant_layout, nearest_sampler, &uniforms); + let instances = Buffer::new( + device, + "iced_wgpu::image instance buffer", + Instance::INITIAL, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); - let linear = - Data::new(device, constant_layout, linear_sampler, &uniforms); + let nearest_layout = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::image constants bind group"), + layout: constant_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer( + wgpu::BufferBinding { + buffer: &uniforms, + offset: 0, + size: None, + }, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler( + nearest_sampler, + ), + }, + ], + }); + + let linear_layout = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::image constants bind group"), + layout: constant_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer( + wgpu::BufferBinding { + buffer: &uniforms, + offset: 0, + size: None, + }, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler( + linear_sampler, + ), + }, + ], + }); Self { uniforms, - nearest, - linear, + instances, + nearest: Vec::new(), + nearest_layout, + nearest_total: 0, + linear: Vec::new(), + linear_layout, + linear_total: 0, } } @@ -383,10 +510,10 @@ impl Layer { device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, belt: &mut wgpu::util::StagingBelt, - nearest_instances: &[Instance], - linear_instances: &[Instance], transformation: Transformation, scale_factor: f32, + nearest: &[Instance], + linear: &[Instance], ) { let uniforms = Uniforms { transform: transformation.into(), @@ -405,102 +532,96 @@ impl Layer { ) .copy_from_slice(bytes); - self.nearest - .upload(device, encoder, belt, nearest_instances); + let _ = self + .instances + .resize(device, self.nearest_total + self.linear_total); - self.linear.upload(device, encoder, belt, linear_instances); - } + let mut offset = 0; - fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { - self.nearest.render(render_pass); - self.linear.render(render_pass); - } -} + if !nearest.is_empty() { + offset += self.instances.write(device, encoder, belt, 0, nearest); + } -#[derive(Debug)] -struct Data { - constants: wgpu::BindGroup, - instances: Buffer, - instance_count: usize, -} - -impl Data { - pub fn new( - device: &wgpu::Device, - constant_layout: &wgpu::BindGroupLayout, - sampler: &wgpu::Sampler, - uniforms: &wgpu::Buffer, - ) -> Self { - let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::image constants bind group"), - layout: constant_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer( - wgpu::BufferBinding { - buffer: uniforms, - offset: 0, - size: None, - }, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(sampler), - }, - ], - }); - - let instances = Buffer::new( - device, - "iced_wgpu::image instance buffer", - Instance::INITIAL, - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - ); - - Self { - constants, - instances, - instance_count: 0, + if !linear.is_empty() { + let _ = self.instances.write(device, encoder, belt, offset, linear); } } - fn upload( + fn push( &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - belt: &mut wgpu::util::StagingBelt, - instances: &[Instance], + atlas: &Arc, + nearest: &[Instance], + linear: &[Instance], ) { - self.instance_count = instances.len(); + let new_nearest = nearest.len() - self.nearest_total; - if self.instance_count == 0 { - return; + if new_nearest > 0 { + self.nearest.push(Group { + atlas: atlas.clone(), + instance_count: new_nearest, + }); + + self.nearest_total = nearest.len(); } - let _ = self.instances.resize(device, instances.len()); - let _ = self.instances.write(device, encoder, belt, 0, instances); + let new_linear = linear.len() - self.linear_total; + + if new_linear > 0 { + self.linear.push(Group { + atlas: atlas.clone(), + instance_count: new_linear, + }); + + self.linear_total = linear.len(); + } } fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { - if self.instance_count == 0 { - return; - } - - render_pass.set_bind_group(0, &self.constants, &[]); render_pass.set_vertex_buffer(0, self.instances.slice(..)); - render_pass.draw(0..6, 0..self.instance_count as u32); + let mut offset = 0; + + if !self.nearest.is_empty() { + render_pass.set_bind_group(0, &self.nearest_layout, &[]); + + for group in &self.nearest { + render_pass.set_bind_group(1, group.atlas.as_ref(), &[]); + render_pass + .draw(0..6, offset..offset + group.instance_count as u32); + + offset += group.instance_count as u32; + } + } + + if !self.linear.is_empty() { + render_pass.set_bind_group(0, &self.linear_layout, &[]); + + for group in &self.linear { + render_pass.set_bind_group(1, group.atlas.as_ref(), &[]); + render_pass + .draw(0..6, offset..offset + group.instance_count as u32); + + offset += group.instance_count as u32; + } + } + } + + fn clear(&mut self) { + self.nearest.clear(); + self.nearest_total = 0; + + self.linear.clear(); + self.linear_total = 0; } } #[repr(C)] #[derive(Debug, Clone, Copy, Zeroable, Pod)] struct Instance { - _position: [f32; 2], _center: [f32; 2], - _size: [f32; 2], + _clip_bounds: [f32; 4], + _border_radius: [f32; 4], + _tile: [f32; 4], _rotation: f32, _opacity: f32, _position_in_atlas: [f32; 2], @@ -524,8 +645,9 @@ struct Uniforms { } fn add_instances( - image_position: [f32; 2], - image_size: [f32; 2], + bounds: Rectangle, + clip_bounds: Rectangle, + border_radius: border::Radius, rotation: f32, opacity: f32, snap: bool, @@ -533,16 +655,26 @@ fn add_instances( instances: &mut Vec, ) { let center = [ - image_position[0] + image_size[0] / 2.0, - image_position[1] + image_size[1] / 2.0, + bounds.x + bounds.width / 2.0, + bounds.y + bounds.height / 2.0, ]; + let clip_bounds = [ + clip_bounds.x, + clip_bounds.y, + clip_bounds.width, + clip_bounds.height, + ]; + + let border_radius = border_radius.into(); + match entry { atlas::Entry::Contiguous(allocation) => { add_instance( - image_position, center, - image_size, + clip_bounds, + border_radius, + [bounds.x, bounds.y, bounds.width, bounds.height], rotation, opacity, snap, @@ -551,32 +683,35 @@ fn add_instances( ); } atlas::Entry::Fragmented { fragments, size } => { - let scaling_x = image_size[0] / size.width as f32; - let scaling_y = image_size[1] / size.height as f32; + let scaling_x = bounds.width / size.width as f32; + let scaling_y = bounds.height / size.height as f32; for fragment in fragments { let allocation = &fragment.allocation; - - let [x, y] = image_position; let (fragment_x, fragment_y) = fragment.position; + let Size { width: fragment_width, height: fragment_height, } = allocation.size(); - let position = [ - x + fragment_x as f32 * scaling_x, - y + fragment_y as f32 * scaling_y, - ]; - - let size = [ + let tile = [ + bounds.x + fragment_x as f32 * scaling_x, + bounds.y + fragment_y as f32 * scaling_y, fragment_width as f32 * scaling_x, fragment_height as f32 * scaling_y, ]; add_instance( - position, center, size, rotation, opacity, snap, - allocation, instances, + center, + clip_bounds, + border_radius, + tile, + rotation, + opacity, + snap, + allocation, + instances, ); } } @@ -585,9 +720,10 @@ fn add_instances( #[inline] fn add_instance( - position: [f32; 2], center: [f32; 2], - size: [f32; 2], + clip_bounds: [f32; 4], + border_radius: [f32; 4], + tile: [f32; 4], rotation: f32, opacity: f32, snap: bool, @@ -597,20 +733,22 @@ fn add_instance( let (x, y) = allocation.position(); let Size { width, height } = allocation.size(); let layer = allocation.layer(); + let atlas_size = allocation.atlas_size(); let instance = Instance { - _position: position, _center: center, - _size: size, + _clip_bounds: clip_bounds, + _border_radius: border_radius, + _tile: tile, _rotation: rotation, _opacity: opacity, _position_in_atlas: [ - (x as f32 + 0.5) / atlas::SIZE as f32, - (y as f32 + 0.5) / atlas::SIZE as f32, + x as f32 / atlas_size as f32, + y as f32 / atlas_size as f32, ], _size_in_atlas: [ - (width as f32 - 1.0) / atlas::SIZE as f32, - (height as f32 - 1.0) / atlas::SIZE as f32, + width as f32 / atlas_size as f32, + height as f32 / atlas_size as f32, ], _layer: layer as u32, _snap: snap as u32, diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs index 470c39e2..881e75e3 100644 --- a/wgpu/src/image/raster.rs +++ b/wgpu/src/image/raster.rs @@ -1,26 +1,35 @@ use crate::core::Size; use crate::core::image; use crate::graphics; -use crate::graphics::image::image_rs; use crate::image::atlas::{self, Atlas}; use rustc_hash::{FxHashMap, FxHashSet}; +use std::sync::{Arc, Weak}; + +pub type Image = graphics::image::Buffer; /// Entry in cache corresponding to an image handle #[derive(Debug)] pub enum Memory { /// Image data on host - Host(image_rs::ImageBuffer, image::Bytes>), + Host(Image), /// Storage entry - Device(atlas::Entry), - /// Image not found - NotFound, - /// Invalid image data - Invalid, + Device { + entry: atlas::Entry, + bind_group: Option>, + allocation: Option>, + }, + Error(image::Error), } impl Memory { - /// Width and height of image + pub fn load(handle: &image::Handle) -> Self { + match graphics::image::load(handle) { + Ok(image) => Self::Host(image), + Err(error) => Self::Error(error), + } + } + pub fn dimensions(&self) -> Size { match self { Memory::Host(image) => { @@ -28,14 +37,19 @@ impl Memory { Size::new(width, height) } - Memory::Device(entry) => entry.size(), - Memory::NotFound => Size::new(1, 1), - Memory::Invalid => Size::new(1, 1), + Memory::Device { entry, .. } => entry.size(), + Memory::Error(_) => Size::new(1, 1), + } + } + + pub fn host(&self) -> Option { + match self { + Memory::Host(image) => Some(image.clone()), + Memory::Device { .. } | Memory::Error(_) => None, } } } -/// Caches image raster data #[derive(Debug, Default)] pub struct Cache { map: FxHashMap, @@ -44,51 +58,28 @@ pub struct Cache { } impl Cache { - /// Load image - pub fn load(&mut self, handle: &image::Handle) -> &mut Memory { - if self.contains(handle) { - return self.get(handle).unwrap(); - } + pub fn get_mut(&mut self, handle: &image::Handle) -> Option<&mut Memory> { + let _ = self.hits.insert(handle.id()); - let memory = match graphics::image::load(handle) { - Ok(image) => Memory::Host(image), - Err(image_rs::error::ImageError::IoError(_)) => Memory::NotFound, - Err(_) => Memory::Invalid, - }; + self.map.get_mut(&handle.id()) + } + + pub fn insert(&mut self, handle: &image::Handle, memory: Memory) { + let _ = self.map.insert(handle.id(), memory); + let _ = self.hits.insert(handle.id()); self.should_trim = true; - - self.insert(handle, memory); - self.get(handle).unwrap() } - /// Load image and upload raster data - pub fn upload( + pub fn contains(&self, handle: &image::Handle) -> bool { + self.map.contains_key(&handle.id()) + } + + pub fn trim( &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - handle: &image::Handle, atlas: &mut Atlas, - ) -> Option<&atlas::Entry> { - let memory = self.load(handle); - - if let Memory::Host(image) = memory { - let (width, height) = image.dimensions(); - - let entry = atlas.upload(device, encoder, width, height, image)?; - - *memory = Memory::Device(entry); - } - - if let Memory::Device(allocation) = memory { - Some(allocation) - } else { - None - } - } - - /// Trim cache misses from cache - pub fn trim(&mut self, atlas: &mut Atlas) { + on_drop: impl Fn(Arc), + ) { // Only trim if new entries have landed in the `Cache` if !self.should_trim { return; @@ -96,11 +87,31 @@ impl Cache { let hits = &self.hits; - self.map.retain(|k, memory| { - let retain = hits.contains(k); + self.map.retain(|id, memory| { + // Retain active allocations + if let Memory::Device { allocation, .. } = memory + && allocation + .as_ref() + .is_some_and(|allocation| allocation.strong_count() > 0) + { + return true; + } - if !retain && let Memory::Device(entry) = memory { - atlas.remove(entry); + let retain = hits.contains(id); + + if !retain { + log::debug!("Dropping image allocation: {id:?}"); + + if let Memory::Device { + entry, bind_group, .. + } = memory + { + if let Some(bind_group) = bind_group.take() { + on_drop(bind_group); + } else { + atlas.remove(entry); + } + } } retain @@ -109,18 +120,4 @@ impl Cache { self.hits.clear(); self.should_trim = false; } - - fn get(&mut self, handle: &image::Handle) -> Option<&mut Memory> { - let _ = self.hits.insert(handle.id()); - - self.map.get_mut(&handle.id()) - } - - fn insert(&mut self, handle: &image::Handle, memory: Memory) { - let _ = self.map.insert(handle.id(), memory); - } - - fn contains(&self, handle: &image::Handle) -> bool { - self.map.contains_key(&handle.id()) - } } diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index e55ade38..31fb2fa6 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -94,17 +94,18 @@ impl Cache { &mut self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, handle: &svg::Handle, color: Option, - [width, height]: [f32; 2], + size: Size, scale: f32, atlas: &mut Atlas, ) -> Option<&atlas::Entry> { let id = handle.id(); let (width, height) = ( - (scale * width).ceil() as u32, - (scale * height).ceil() as u32, + (scale * size.width).ceil() as u32, + (scale * size.height).ceil() as u32, ); let color = color.map(Color::into_rgba8); @@ -167,14 +168,15 @@ impl Cache { }); } - let allocation = - atlas.upload(device, encoder, width, height, &rgba)?; + let allocation = atlas + .upload(device, encoder, belt, width, height, &rgba)?; log::debug!("allocating {id} {width}x{height}"); let _ = self.svg_hits.insert(id); let _ = self.rasterized_hits.insert(key); let _ = self.rasterized.insert(key, allocation); + self.should_trim = true; self.rasterized.get(&key) } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 5ddb8461..f7ee2a3a 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -128,11 +128,19 @@ impl Layer { pub fn draw_image(&mut self, image: Image, transformation: Transformation) { match image { - Image::Raster(image, bounds) => { - self.draw_raster(image, bounds, transformation); + Image::Raster { + image, + bounds, + clip_bounds, + } => { + self.draw_raster(image, bounds, clip_bounds, transformation); } - Image::Vector(svg, bounds) => { - self.draw_svg(svg, bounds, transformation); + Image::Vector { + svg, + bounds, + clip_bounds, + } => { + self.draw_svg(svg, bounds, clip_bounds, transformation); } } } @@ -141,9 +149,18 @@ impl Layer { &mut self, image: core::Image, bounds: Rectangle, + clip_bounds: Rectangle, transformation: Transformation, ) { - let image = Image::Raster(image, bounds * transformation); + let image = Image::Raster { + image: core::Image { + border_radius: image.border_radius + * transformation.scale_factor(), + ..image + }, + bounds: bounds * transformation, + clip_bounds: clip_bounds * transformation, + }; self.images.push(image); } @@ -152,9 +169,14 @@ impl Layer { &mut self, svg: Svg, bounds: Rectangle, + clip_bounds: Rectangle, transformation: Transformation, ) { - let svg = Image::Vector(svg, bounds * transformation); + let svg = Image::Vector { + svg, + bounds: bounds * transformation, + clip_bounds: clip_bounds * transformation, + }; self.images.push(svg); } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 6b34889e..01de8800 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -65,8 +65,8 @@ use crate::core::renderer; use crate::core::{ Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, }; -use crate::graphics::Viewport; use crate::graphics::text::{Editor, Paragraph}; +use crate::graphics::{Shell, Viewport}; /// A [`wgpu`] graphics renderer for [`iced`]. /// @@ -117,9 +117,7 @@ impl Renderer { image: image::State::new(), #[cfg(any(feature = "svg", feature = "image"))] - image_cache: std::cell::RefCell::new( - engine.create_image_cache(&engine.device), - ), + image_cache: std::cell::RefCell::new(engine.create_image_cache()), // TODO: Resize belt smartly (?) // It would be great if the `StagingBelt` API exposed methods @@ -313,8 +311,10 @@ impl Renderer { self.layers.merge(); for layer in self.layers.iter() { + let clip_bounds = layer.bounds * scale_factor; + if physical_bounds - .intersection(&(layer.bounds * scale_factor)) + .intersection(&clip_bounds) .and_then(Rectangle::snap) .is_none() { @@ -460,8 +460,6 @@ impl Renderer { #[cfg(any(feature = "svg", feature = "image"))] let mut image_layer = 0; - #[cfg(any(feature = "svg", feature = "image"))] - let image_cache = self.image_cache.borrow(); let scale_factor = viewport.scale_factor(); let physical_bounds = Rectangle::::from(Rectangle::with_size( @@ -632,7 +630,6 @@ impl Renderer { let render_span = debug::render(debug::Primitive::Image); self.image.render( &self.engine.image_pipeline, - &image_cache, image_layer, scissor_rect, &mut render_pass, @@ -701,6 +698,19 @@ impl core::Renderer for Renderer { fn reset(&mut self, new_bounds: Rectangle) { self.layers.reset(new_bounds); } + + fn allocate_image( + &mut self, + _handle: &core::image::Handle, + _callback: impl FnOnce(Result) + + Send + + 'static, + ) { + #[cfg(feature = "image")] + self.image_cache + .get_mut() + .allocate_image(_handle, _callback); + } } impl core::text::Renderer for Renderer { @@ -765,13 +775,29 @@ impl core::text::Renderer for Renderer { impl core::image::Renderer for Renderer { type Handle = core::image::Handle; - fn measure_image(&self, handle: &Self::Handle) -> core::Size { + fn load_image( + &self, + handle: &Self::Handle, + ) -> Result { + self.image_cache.borrow_mut().load_image( + &self.engine.device, + &self.engine.queue, + handle, + ) + } + + fn measure_image(&self, handle: &Self::Handle) -> Option> { self.image_cache.borrow_mut().measure_image(handle) } - fn draw_image(&mut self, image: core::Image, bounds: Rectangle) { + fn draw_image( + &mut self, + image: core::Image, + bounds: Rectangle, + clip_bounds: Rectangle, + ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_raster(image, bounds, transformation); + layer.draw_raster(image, bounds, clip_bounds, transformation); } } @@ -781,9 +807,14 @@ impl core::svg::Renderer for Renderer { self.image_cache.borrow_mut().measure_svg(handle) } - fn draw_svg(&mut self, svg: core::Svg, bounds: Rectangle) { + fn draw_svg( + &mut self, + svg: core::Svg, + bounds: Rectangle, + clip_bounds: Rectangle, + ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg(svg, bounds, transformation); + layer.draw_svg(svg, bounds, clip_bounds, transformation); } } @@ -910,6 +941,7 @@ impl renderer::Headless for Renderer { wgpu::TextureFormat::Rgba8Unorm }, Some(graphics::Antialiasing::MSAAx4), + Shell::headless(), ); Some(Self::new(engine, default_font, default_text_size)) diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index bc922838..72a5c60e 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -9,63 +9,117 @@ struct Globals { struct VertexInput { @builtin(vertex_index) vertex_index: u32, - @location(0) pos: vec2, - @location(1) center: vec2, - @location(2) scale: vec2, - @location(3) rotation: f32, - @location(4) opacity: f32, - @location(5) atlas_pos: vec2, - @location(6) atlas_scale: vec2, - @location(7) layer: i32, - @location(8) snap: u32, + @location(0) center: vec2, + @location(1) clip_bounds: vec4, + @location(2) border_radius: vec4, + @location(3) tile: vec4, + @location(4) rotation: f32, + @location(5) opacity: f32, + @location(6) atlas_pos: vec2, + @location(7) atlas_scale: vec2, + @location(8) layer: i32, + @location(9) snap: u32, } struct VertexOutput { @builtin(position) position: vec4, - @location(0) uv: vec2, - @location(1) layer: f32, // this should be an i32, but naga currently reads that as requiring interpolation. - @location(2) opacity: f32, + @location(0) @interpolate(flat) clip_bounds: vec4, + @location(1) @interpolate(flat) border_radius: vec4, + @location(2) @interpolate(flat) atlas: vec4, + @location(3) @interpolate(flat) layer: i32, + @location(4) @interpolate(flat) opacity: f32, + @location(5) uv: vec2, } @vertex fn vs_main(input: VertexInput) -> VertexOutput { var out: VertexOutput; - // Generate a vertex position in the range [0, 1] from the vertex index. - var v_pos = vertex_position(input.vertex_index); + // Generate a vertex position in the range [0, 1] from the vertex index + let corner = vertex_position(input.vertex_index); - // Map the vertex position to the atlas texture. - out.uv = vec2(v_pos * input.atlas_scale + input.atlas_pos); - out.layer = f32(input.layer); - out.opacity = input.opacity; + let tile = input.tile; + let center = input.center; - // Calculate the vertex position and move the center to the origin - v_pos = input.pos + v_pos * input.scale - input.center; - - // Apply the rotation around the center of the image - let cos_rot = cos(input.rotation); - let sin_rot = sin(input.rotation); - let rotate = mat4x4( - vec4(cos_rot, sin_rot, 0.0, 0.0), - vec4(-sin_rot, cos_rot, 0.0, 0.0), - vec4(0.0, 0.0, 1.0, 0.0), - vec4(0.0, 0.0, 0.0, 1.0) + // List the unrotated tile corners + let corners = array, 4>( + tile.xy, // Top left + tile.xy + vec2(tile.z, 0.0), // Top right + tile.xy + vec2(0.0, tile.w), // Bottom left + tile.xy + tile.zw // Bottom right ); - // Calculate the final position of the vertex - out.position = vec4(vec2(globals.scale_factor), 1.0, 1.0) * (vec4(input.center, 0.0, 0.0) + rotate * vec4(v_pos, 0.0, 1.0)); + // Rotate tile corners around center + let cos_r = cos(-input.rotation); // Clockwise + let sin_r = sin(-input.rotation); + var rotated = array, 4>(); + for (var i = 0u; i < 4u; i++) { + let c = corners[i] - input.center; + rotated[i] = vec2(c.x * cos_r - c.y * sin_r, c.x * sin_r + c.y * cos_r) + input.center; + } + + // Find bounding box of rotated tile + var min_xy = rotated[0]; + var max_xy = rotated[0]; + for (var i = 1u; i < 4u; i++) { + min_xy = min(min_xy, rotated[i]); + max_xy = max(max_xy, rotated[i]); + } + let rotated_bounds = vec4(min_xy, max_xy - min_xy); + + // Intersect with clip bounds + let clip_min = max(rotated_bounds.xy, input.clip_bounds.xy); + let clip_max = min(rotated_bounds.xy + rotated_bounds.zw, input.clip_bounds.xy + input.clip_bounds.zw); + let clipped_tile = vec4(clip_min, max(vec2(0.0), clip_max - clip_min)); + + // Calculate the vertex position + let v_pos = clipped_tile.xy + corner * clipped_tile.zw; + out.position = vec4(vec2(globals.scale_factor), 1.0, 1.0) * vec4(v_pos, 0.0, 1.0); + + // Calculate rotated UV + let uv = input.atlas_pos + (v_pos - tile.xy) / tile.zw * input.atlas_scale; + let uv_center = input.atlas_pos + input.atlas_scale / 2.0; + + let d = uv - uv_center; + out.uv = vec2(d.x * cos_r - d.y * sin_r, d.x * sin_r + d.y * cos_r) + uv_center; + + // Snap position to the pixel grid if bool(input.snap) { out.position = round(out.position); } out.position = globals.transform * out.position; + out.clip_bounds = globals.scale_factor * input.clip_bounds; + out.border_radius = globals.scale_factor * input.border_radius; + out.atlas = vec4(input.atlas_pos, input.atlas_pos + input.atlas_scale); + out.layer = input.layer; + out.opacity = input.opacity; return out; } @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { - // Sample the texture at the given UV coordinate and layer. - return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)) * vec4(1.0, 1.0, 1.0, input.opacity); + let fragment = input.position.xy; + let position = input.clip_bounds.xy; + let scale = input.clip_bounds.zw; + + let d = rounded_box_sdf( + 2.0 * (fragment - position - scale / 2.0), + scale, + input.border_radius * 2.0, + ) / 2.0; + + let antialias: f32 = clamp(1.0 - d, 0.0, 1.0); + let inside = all(input.uv >= input.atlas.xy) && all(input.uv <= input.atlas.zw); + + return textureSample(u_texture, u_sampler, input.uv, input.layer) * vec4(1.0, 1.0, 1.0, antialias * input.opacity * f32(inside)); +} + +fn rounded_box_sdf(p: vec2, size: vec2, corners: vec4) -> f32 { + var box_half = select(corners.yz, corners.xw, p.x > 0.0); + var corner = select(box_half.y, box_half.x, p.y > 0.0); + var q = abs(p) - size + corner; + return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - corner; } diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index c1ba7df2..8a0bb72a 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -3,7 +3,7 @@ use crate::core::Color; use crate::graphics::color; use crate::graphics::compositor; use crate::graphics::error; -use crate::graphics::{self, Viewport}; +use crate::graphics::{self, Shell, Viewport}; use crate::settings::{self, Settings}; use crate::{Engine, Renderer}; @@ -50,6 +50,7 @@ impl Compositor { pub async fn request( settings: Settings, compatible_window: Option, + shell: Shell, ) -> Result { let instance = wgpu::util::new_instance_with_webgpu_detection( &wgpu::InstanceDescriptor { @@ -181,6 +182,7 @@ impl Compositor { queue, format, settings.antialiasing, + shell, ); return Ok(Compositor { @@ -206,8 +208,9 @@ impl Compositor { pub async fn new( settings: Settings, compatible_window: W, + shell: Shell, ) -> Result { - Compositor::request(settings, Some(compatible_window)).await + Compositor::request(settings, Some(compatible_window), shell).await } /// Presents the given primitives with the given [`Compositor`]. @@ -260,6 +263,7 @@ impl graphics::Compositor for Compositor { async fn with_backend( settings: graphics::Settings, compatible_window: W, + shell: Shell, backend: Option<&str>, ) -> Result { match backend { @@ -274,7 +278,7 @@ impl graphics::Compositor for Compositor { settings.present_mode = present_mode; } - Ok(new(settings, compatible_window).await?) + Ok(new(settings, compatible_window, shell).await?) } Some(backend) => Err(graphics::Error::GraphicsAdapterNotFound { backend: "wgpu", diff --git a/widget/src/image.rs b/widget/src/image.rs index 18b7a3a3..c5108d81 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -19,6 +19,7 @@ pub mod viewer; pub use viewer::Viewer; +use crate::core::border; use crate::core::image; use crate::core::layout; use crate::core::mouse; @@ -59,6 +60,7 @@ pub struct Image { width: Length, height: Length, crop: Option>, + border_radius: border::Radius, content_fit: ContentFit, filter_method: FilterMethod, rotation: Rotation, @@ -75,6 +77,7 @@ impl Image { width: Length::Shrink, height: Length::Shrink, crop: None, + border_radius: border::Radius::default(), content_fit: ContentFit::default(), filter_method: FilterMethod::default(), rotation: Rotation::default(), @@ -164,6 +167,18 @@ impl Image { self.crop = Some(region); self } + + /// Sets the [`border::Radius`] of the [`Image`]. + /// + /// Currently, it will only be applied around the rectangular bounding box + /// of the [`Image`]. + pub fn border_radius( + mut self, + border_radius: impl Into, + ) -> Self { + self.border_radius = border_radius.into(); + self + } } /// Computes the layout of an [`Image`]. @@ -182,7 +197,8 @@ where Renderer: image::Renderer, { // The raw w/h of the underlying image - let image_size = crop(renderer.measure_image(handle), region); + let image_size = + crop(renderer.measure_image(handle).unwrap_or_default(), region); // The rotated size of the image let rotated_size = rotation.apply(image_size); @@ -224,7 +240,7 @@ fn drawing_bounds( where Renderer: image::Renderer, { - let original_size = renderer.measure_image(handle); + let original_size = renderer.measure_image(handle).unwrap_or_default(); let image_size = crop(original_size, region); let rotated_size = rotation.apply(image_size); let adjusted_fit = content_fit.fit(rotated_size, bounds.size()); @@ -281,10 +297,6 @@ where Rectangle::new(position + crop_offset, final_size) } -fn must_clip(bounds: Rectangle, drawing_bounds: Rectangle) -> bool { - drawing_bounds.width > bounds.width || drawing_bounds.height > bounds.height -} - fn crop(size: Size, region: Option>) -> Size { if let Some(region) = region { Size::new( @@ -300,9 +312,9 @@ fn crop(size: Size, region: Option>) -> Size { pub fn draw( renderer: &mut Renderer, layout: Layout<'_>, - viewport: &Rectangle, handle: &Handle, crop: Option>, + border_radius: border::Radius, content_fit: ContentFit, filter_method: FilterMethod, rotation: Rotation, @@ -323,51 +335,17 @@ pub fn draw( scale, ); - if must_clip(bounds, drawing_bounds) { - if let Some(bounds) = bounds.intersection(viewport) { - renderer.with_layer(bounds, |renderer| { - render( - renderer, - handle, - filter_method, - rotation, - opacity, - drawing_bounds, - ); - }); - } - } else { - render( - renderer, - handle, - filter_method, - rotation, - opacity, - drawing_bounds, - ); - } -} - -fn render( - renderer: &mut Renderer, - handle: &Handle, - filter_method: FilterMethod, - rotation: Rotation, - opacity: f32, - drawing_bounds: Rectangle, -) where - Renderer: image::Renderer, - Handle: Clone, -{ renderer.draw_image( image::Image { handle: handle.clone(), + border_radius, filter_method, rotation: rotation.radians(), opacity, snap: true, }, drawing_bounds, + bounds, ); } @@ -411,14 +389,14 @@ where _style: &renderer::Style, layout: Layout<'_>, _cursor: mouse::Cursor, - viewport: &Rectangle, + _viewport: &Rectangle, ) { draw( renderer, layout, - viewport, &self.handle, self.crop, + self.border_radius, self.content_fit, self.filter_method, self.rotation, diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 29da5d6d..209b3728 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -1,4 +1,5 @@ //! Zoom and pan on an image. +use crate::core::border; use crate::core::image::{self, FilterMethod}; use crate::core::layout; use crate::core::mouse; @@ -122,7 +123,9 @@ where limits: &layout::Limits, ) -> layout::Node { // The raw w/h of the underlying image - let image_size = renderer.measure_image(&self.handle); + let image_size = + renderer.measure_image(&self.handle).unwrap_or_default(); + let image_size = Size::new(image_size.width as f32, image_size.height as f32); @@ -313,7 +316,7 @@ where _style: &renderer::Style, layout: Layout<'_>, _cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); @@ -347,12 +350,14 @@ where renderer.draw_image( Image { handle: self.handle.clone(), + border_radius: border::Radius::default(), filter_method: self.filter_method, rotation: Radians(0.0), opacity: 1.0, snap: true, }, drawing_bounds, + *viewport, ); }); }; @@ -433,7 +438,9 @@ pub fn scaled_image_size( where Renderer: image::Renderer, { - let Size { width, height } = renderer.measure_image(handle); + let Size { width, height } = + renderer.measure_image(handle).unwrap_or_default(); + let image_size = Size::new(width as f32, height as f32); let adjusted_fit = content_fit.fit(image_size, bounds); diff --git a/widget/src/svg.rs b/widget/src/svg.rs index dabe499f..9ed3d870 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -240,25 +240,16 @@ where let style = theme.style(&self.class, status); - let render = |renderer: &mut Renderer| { - renderer.draw_svg( - svg::Svg { - handle: self.handle.clone(), - color: style.color, - rotation: self.rotation.radians(), - opacity: self.opacity, - }, - drawing_bounds, - ); - }; - - if adjusted_fit.width > bounds.width - || adjusted_fit.height > bounds.height - { - renderer.with_layer(bounds, render); - } else { - render(renderer); - } + renderer.draw_svg( + svg::Svg { + handle: self.handle.clone(), + color: style.color, + rotation: self.rotation.radians(), + opacity: self.opacity, + }, + drawing_bounds, + bounds, + ); } } diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 75269d6a..764fc1c3 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -49,7 +49,8 @@ use crate::futures::futures::task; use crate::futures::futures::{Future, StreamExt}; use crate::futures::subscription; use crate::futures::{Executor, Runtime}; -use crate::graphics::{Compositor, compositor}; +use crate::graphics::{Compositor, Shell, compositor}; +use crate::runtime::image; use crate::runtime::system; use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::{Action, Task}; @@ -587,8 +588,10 @@ async fn run_instance

( let default_fonts = default_fonts.clone(); async move { + let shell = Shell::new(proxy.clone()); + let mut compositor = - ::Compositor::new(graphics_settings, window).await; + ::Compositor::new(graphics_settings, window, shell).await; if let Ok(compositor) = &mut compositor { for font in default_fonts { @@ -824,7 +827,7 @@ async fn run_instance

( .get_mut(&id) .expect("Get user interface"); - let draw_span = debug::draw(id); + let interact_span = debug::interact(id); let mut change_count = 0; let state = loop { @@ -947,7 +950,9 @@ async fn run_instance

( user_interfaces.get_mut(&id).unwrap(); } }; + interact_span.finish(); + let draw_span = debug::draw(id); interface.draw( &mut window.renderer, window.state.theme(), @@ -1646,6 +1651,26 @@ fn run_action<'a, P, C>( let _ = window.raw.set_cursor_hittest(true); } } + window::Action::RedrawAll => { + for (_id, window) in window_manager.iter_mut() { + window.raw.request_redraw(); + } + } + window::Action::RelayoutAll => { + for (id, window) in window_manager.iter_mut() { + if let Some(ui) = interfaces.remove(&id) { + let _ = interfaces.insert( + id, + ui.relayout( + window.state.logical_size(), + &mut window.renderer, + ), + ); + } + + window.raw.request_redraw(); + } + } }, Action::System(action) => match action { system::Action::GetInformation(_channel) => { @@ -1706,6 +1731,21 @@ fn run_action<'a, P, C>( } } } + Action::Image(action) => match action { + image::Action::Allocate(handle, sender) => { + use core::Renderer as _; + + // TODO: Shared image cache in compositor + if let Some((_id, window)) = window_manager.iter_mut().next() { + window.renderer.allocate_image( + &handle, + move |allocation| { + let _ = sender.send(allocation); + }, + ); + } + } + }, Action::LoadFont { bytes, channel } => { if let Some(compositor) = compositor { // TODO: Error handling (?) diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index 92758c5f..5a56659f 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -4,7 +4,9 @@ use crate::futures::futures::{ select, task::{Context, Poll}, }; +use crate::graphics::shell; use crate::runtime::Action; +use crate::runtime::window; use std::pin::Pin; /// An event loop proxy with backpressure that implements `Sink`. @@ -134,3 +136,16 @@ impl Sink> for Proxy { Poll::Ready(Ok(())) } } + +impl shell::Notifier for Proxy +where + T: Send, +{ + fn request_redraw(&self) { + self.send_action(Action::Window(window::Action::RedrawAll)); + } + + fn invalidate_layout(&self) { + self.send_action(Action::Window(window::Action::RelayoutAll)); + } +}