diff options
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | benches/wgpu.rs | 98 | ||||
-rw-r--r-- | core/Cargo.toml | 1 | ||||
-rw-r--r-- | core/src/image.rs | 207 | ||||
-rw-r--r-- | examples/clock/Cargo.toml | 1 | ||||
-rw-r--r-- | examples/clock/src/main.rs | 2 | ||||
-rw-r--r-- | examples/pokedex/src/main.rs | 2 | ||||
-rw-r--r-- | examples/screenshot/src/main.rs | 2 | ||||
-rw-r--r-- | examples/the_matrix/Cargo.toml | 1 | ||||
-rw-r--r-- | examples/the_matrix/src/main.rs | 39 | ||||
-rw-r--r-- | graphics/src/cache.rs | 189 | ||||
-rw-r--r-- | graphics/src/cached.rs | 24 | ||||
-rw-r--r-- | graphics/src/geometry.rs | 2 | ||||
-rw-r--r-- | graphics/src/geometry/cache.rs | 83 | ||||
-rw-r--r-- | graphics/src/image.rs | 52 | ||||
-rw-r--r-- | graphics/src/lib.rs | 4 | ||||
-rw-r--r-- | renderer/src/fallback.rs | 16 | ||||
-rw-r--r-- | runtime/Cargo.toml | 1 | ||||
-rw-r--r-- | runtime/src/window/screenshot.rs | 16 | ||||
-rw-r--r-- | tiny_skia/src/geometry.rs | 5 | ||||
-rw-r--r-- | tiny_skia/src/raster.rs | 6 | ||||
-rw-r--r-- | wgpu/src/geometry.rs | 13 | ||||
-rw-r--r-- | wgpu/src/image/raster.rs | 8 | ||||
-rw-r--r-- | wgpu/src/text.rs | 164 | ||||
-rw-r--r-- | wgpu/src/triangle.rs | 2 | ||||
-rw-r--r-- | widget/src/canvas.rs | 1 | ||||
-rw-r--r-- | widget/src/image.rs | 8 | ||||
-rw-r--r-- | widget/src/image/viewer.rs | 6 |
28 files changed, 606 insertions, 348 deletions
@@ -137,6 +137,7 @@ iced_winit = { version = "0.13.0-dev", path = "winit" } async-std = "1.0" bitflags = "2.0" bytemuck = { version = "1.0", features = ["derive"] } +bytes = "1.6" cosmic-text = "0.10" dark-light = "1.0" futures = "0.3" diff --git a/benches/wgpu.rs b/benches/wgpu.rs index 61b4eb6c..3dd415db 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -3,7 +3,7 @@ use criterion::{criterion_group, criterion_main, Bencher, Criterion}; use iced::alignment; use iced::mouse; -use iced::widget::{canvas, text}; +use iced::widget::{canvas, stack, text}; use iced::{ Color, Element, Font, Length, Pixels, Point, Rectangle, Size, Theme, }; @@ -16,6 +16,13 @@ criterion_group!(benches, wgpu_benchmark); pub fn wgpu_benchmark(c: &mut Criterion) { c.bench_function("wgpu — canvas (light)", |b| benchmark(b, scene(10))); c.bench_function("wgpu — canvas (heavy)", |b| benchmark(b, scene(1_000))); + + c.bench_function("wgpu - layered text (light)", |b| { + benchmark(b, layered_text(10)); + }); + c.bench_function("wgpu - layered text (heavy)", |b| { + benchmark(b, layered_text(1_000)); + }); } fn benchmark( @@ -125,52 +132,59 @@ fn benchmark( }); } -fn scene<'a, Message: 'a, Theme: 'a>( - n: usize, -) -> Element<'a, Message, Theme, Renderer> { +fn scene<'a, Message: 'a>(n: usize) -> Element<'a, Message, Theme, Renderer> { + struct Scene { + n: usize, + } + + impl<Message, Theme> canvas::Program<Message, Theme, Renderer> for Scene { + type State = canvas::Cache<Renderer>; + + fn draw( + &self, + cache: &Self::State, + renderer: &Renderer, + _theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec<canvas::Geometry<Renderer>> { + vec![cache.draw(renderer, bounds.size(), |frame| { + for i in 0..self.n { + frame.fill_rectangle( + Point::new(0.0, i as f32), + Size::new(10.0, 10.0), + Color::WHITE, + ); + } + + for i in 0..self.n { + frame.fill_text(canvas::Text { + content: i.to_string(), + position: Point::new(0.0, i as f32), + color: Color::BLACK, + size: Pixels::from(16), + line_height: text::LineHeight::default(), + font: Font::DEFAULT, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Basic, + }); + } + })] + } + } + canvas(Scene { n }) .width(Length::Fill) .height(Length::Fill) .into() } -struct Scene { +fn layered_text<'a, Message: 'a>( n: usize, -} - -impl<Message, Theme> canvas::Program<Message, Theme, Renderer> for Scene { - type State = canvas::Cache<Renderer>; - - fn draw( - &self, - cache: &Self::State, - renderer: &Renderer, - _theme: &Theme, - bounds: Rectangle, - _cursor: mouse::Cursor, - ) -> Vec<canvas::Geometry<Renderer>> { - vec![cache.draw(renderer, bounds.size(), |frame| { - for i in 0..self.n { - frame.fill_rectangle( - Point::new(0.0, i as f32), - Size::new(10.0, 10.0), - Color::WHITE, - ); - } - - for i in 0..self.n { - frame.fill_text(canvas::Text { - content: i.to_string(), - position: Point::new(0.0, i as f32), - color: Color::BLACK, - size: Pixels::from(16), - line_height: text::LineHeight::default(), - font: Font::DEFAULT, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - shaping: text::Shaping::Basic, - }); - } - })] - } +) -> Element<'a, Message, Theme, Renderer> { + stack((0..n).map(|i| text(format!("I am paragraph {i}!")).into())) + .width(Length::Fill) + .height(Length::Fill) + .into() } diff --git a/core/Cargo.toml b/core/Cargo.toml index 7bd37021..3c557bca 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -19,6 +19,7 @@ advanced = [] [dependencies] bitflags.workspace = true +bytes.workspace = true glam.workspace = true log.workspace = true num-traits.workspace = true diff --git a/core/src/image.rs b/core/src/image.rs index dc74e5c1..c38239bc 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -1,16 +1,45 @@ //! Load and draw raster graphics. +pub use bytes::Bytes; + use crate::{Rectangle, Size}; use rustc_hash::FxHasher; -use std::hash::{Hash, Hasher as _}; -use std::path::PathBuf; -use std::sync::Arc; +use std::hash::{Hash, Hasher}; +use std::path::{Path, PathBuf}; /// A handle of some image data. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Handle { - id: u64, - data: Data, +#[derive(Clone, PartialEq, Eq)] +pub enum Handle { + /// A file handle. The image data will be read + /// from the file path. + /// + /// Use [`from_path`] to create this variant. + /// + /// [`from_path`]: Self::from_path + Path(Id, PathBuf), + + /// A handle pointing to some encoded image bytes in-memory. + /// + /// Use [`from_bytes`] to create this variant. + /// + /// [`from_bytes`]: Self::from_bytes + Bytes(Id, Bytes), + + /// A handle pointing to decoded image pixels in RGBA format. + /// + /// Use [`from_rgba`] to create this variant. + /// + /// [`from_rgba`]: Self::from_rgba + Rgba { + /// The id of this handle. + id: Id, + /// The width of the image. + width: u32, + /// The height of the image. + height: u32, + /// The pixels. + pixels: Bytes, + }, } impl Handle { @@ -18,56 +47,48 @@ impl Handle { /// /// Makes an educated guess about the image format by examining the data in the file. pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle { - Self::from_data(Data::Path(path.into())) - } + let path = path.into(); - /// Creates an image [`Handle`] containing the image pixels directly. This - /// function expects the input data to be provided as a `Vec<u8>` of RGBA - /// pixels. - /// - /// This is useful if you have already decoded your image. - pub fn from_pixels( - width: u32, - height: u32, - pixels: impl AsRef<[u8]> + Send + Sync + 'static, - ) -> Handle { - Self::from_data(Data::Rgba { - width, - height, - pixels: Bytes::new(pixels), - }) + Self::Path(Id::path(&path), path) } - /// Creates an image [`Handle`] containing the image data directly. + /// Creates an image [`Handle`] containing the encoded image data directly. /// /// Makes an educated guess about the image format by examining the given data. /// /// This is useful if you already have your image loaded in-memory, maybe /// because you downloaded or generated it procedurally. - pub fn from_memory( - bytes: impl AsRef<[u8]> + Send + Sync + 'static, - ) -> Handle { - Self::from_data(Data::Bytes(Bytes::new(bytes))) + pub fn from_bytes(bytes: impl Into<Bytes>) -> Handle { + Self::Bytes(Id::unique(), bytes.into()) } - fn from_data(data: Data) -> Handle { - let mut hasher = FxHasher::default(); - data.hash(&mut hasher); - - Handle { - id: hasher.finish(), - data, + /// Creates an image [`Handle`] containing the decoded image pixels directly. + /// + /// This function expects the pixel data to be provided as a collection of [`Bytes`] + /// of RGBA pixels. Therefore, the length of the pixel data should always be + /// `width * height * 4`. + /// + /// This is useful if you have already decoded your image. + pub fn from_rgba( + width: u32, + height: u32, + pixels: impl Into<Bytes>, + ) -> Handle { + Self::Rgba { + id: Id::unique(), + width, + height, + pixels: pixels.into(), } } /// Returns the unique identifier of the [`Handle`]. - pub fn id(&self) -> u64 { - self.id - } - - /// Returns a reference to the image [`Data`]. - pub fn data(&self) -> &Data { - &self.data + pub fn id(&self) -> Id { + match self { + Handle::Path(id, _) + | Handle::Bytes(id, _) + | Handle::Rgba { id, .. } => *id, + } } } @@ -80,90 +101,46 @@ where } } -impl Hash for Handle { - fn hash<H: std::hash::Hasher>(&self, state: &mut H) { - self.id.hash(state); - } -} - -/// A wrapper around raw image data. -/// -/// It behaves like a `&[u8]`. -#[derive(Clone)] -pub struct Bytes(Arc<dyn AsRef<[u8]> + Send + Sync + 'static>); - -impl Bytes { - /// Creates new [`Bytes`] around `data`. - pub fn new(data: impl AsRef<[u8]> + Send + Sync + 'static) -> Self { - Self(Arc::new(data)) - } -} - -impl std::fmt::Debug for Bytes { +impl std::fmt::Debug for Handle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.as_ref().as_ref().fmt(f) + match self { + Self::Path(_, path) => write!(f, "Path({path:?})"), + Self::Bytes(_, _) => write!(f, "Bytes(...)"), + Self::Rgba { width, height, .. } => { + write!(f, "Pixels({width} * {height})") + } + } } } -impl std::hash::Hash for Bytes { - fn hash<H: std::hash::Hasher>(&self, state: &mut H) { - self.0.as_ref().as_ref().hash(state); - } -} +/// The unique identifier of some [`Handle`] data. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id(_Id); -impl PartialEq for Bytes { - fn eq(&self, other: &Self) -> bool { - let a = self.as_ref(); - let b = other.as_ref(); - core::ptr::eq(a, b) || a == b - } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum _Id { + Unique(u64), + Hash(u64), } -impl Eq for Bytes {} - -impl AsRef<[u8]> for Bytes { - fn as_ref(&self) -> &[u8] { - self.0.as_ref().as_ref() - } -} +impl Id { + fn unique() -> Self { + use std::sync::atomic::{self, AtomicU64}; -impl std::ops::Deref for Bytes { - type Target = [u8]; + static NEXT_ID: AtomicU64 = AtomicU64::new(0); - fn deref(&self) -> &[u8] { - self.0.as_ref().as_ref() + Self(_Id::Unique(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed))) } -} -/// The data of a raster image. -#[derive(Clone, PartialEq, Eq, Hash)] -pub enum Data { - /// File data - Path(PathBuf), + fn path(path: impl AsRef<Path>) -> Self { + let hash = { + let mut hasher = FxHasher::default(); + path.as_ref().hash(&mut hasher); - /// In-memory data - Bytes(Bytes), - - /// Decoded image pixels in RGBA format. - Rgba { - /// The width of the image. - width: u32, - /// The height of the image. - height: u32, - /// The pixels. - pixels: Bytes, - }, -} + hasher.finish() + }; -impl std::fmt::Debug for Data { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Data::Path(path) => write!(f, "Path({path:?})"), - Data::Bytes(_) => write!(f, "Bytes(...)"), - Data::Rgba { width, height, .. } => { - write!(f, "Pixels({width} * {height})") - } - } + Self(_Id::Hash(hash)) } } @@ -184,7 +161,7 @@ pub trait Renderer: crate::Renderer { /// The image Handle to be displayed. Iced exposes its own default implementation of a [`Handle`] /// /// [`Handle`]: Self::Handle - type Handle: Clone + Hash; + type Handle: Clone; /// Returns the dimensions of an image for the given [`Handle`]. fn measure_image(&self, handle: &Self::Handle) -> Size<u32>; diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml index 2d3d5908..dc2e5382 100644 --- a/examples/clock/Cargo.toml +++ b/examples/clock/Cargo.toml @@ -10,3 +10,4 @@ iced.workspace = true iced.features = ["canvas", "tokio", "debug"] time = { version = "0.3", features = ["local-offset"] } +tracing-subscriber = "0.3" diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 897f8f1b..d717db36 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -8,6 +8,8 @@ use iced::{ }; pub fn main() -> iced::Result { + tracing_subscriber::fmt::init(); + iced::program("Clock - Iced", Clock::update, Clock::view) .subscription(Clock::subscription) .theme(Clock::theme) diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 0811c08d..35f23236 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -188,7 +188,7 @@ impl Pokemon { { let bytes = reqwest::get(&url).await?.bytes().await?; - Ok(image::Handle::from_memory(bytes)) + Ok(image::Handle::from_bytes(bytes)) } #[cfg(target_arch = "wasm32")] diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index d887c41b..5c175ccc 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -109,7 +109,7 @@ impl Example { fn view(&self) -> Element<'_, Message> { let image: Element<Message> = if let Some(screenshot) = &self.screenshot { - image(image::Handle::from_pixels( + image(image::Handle::from_rgba( screenshot.size.width, screenshot.size.height, screenshot.clone(), diff --git a/examples/the_matrix/Cargo.toml b/examples/the_matrix/Cargo.toml index 17cf443b..775e76e0 100644 --- a/examples/the_matrix/Cargo.toml +++ b/examples/the_matrix/Cargo.toml @@ -10,3 +10,4 @@ iced.workspace = true iced.features = ["canvas", "tokio", "debug"] rand = "0.8" +tracing-subscriber = "0.3" diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs index 55c9da4b..f3a67ac8 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -1,22 +1,25 @@ use iced::mouse; use iced::time::{self, Instant}; use iced::widget::canvas; -use iced::widget::canvas::{Cache, Geometry}; use iced::{ Color, Element, Font, Length, Point, Rectangle, Renderer, Subscription, Theme, }; +use std::cell::RefCell; + pub fn main() -> iced::Result { + tracing_subscriber::fmt::init(); + iced::program("The Matrix - Iced", TheMatrix::update, TheMatrix::view) .subscription(TheMatrix::subscription) .antialiasing(true) .run() } +#[derive(Default)] struct TheMatrix { - ticks: usize, - backgrounds: Vec<Cache>, + tick: usize, } #[derive(Debug, Clone, Copy)] @@ -28,7 +31,7 @@ impl TheMatrix { fn update(&mut self, message: Message) { match message { Message::Tick(_now) => { - self.ticks += 1; + self.tick += 1; } } } @@ -45,35 +48,31 @@ impl TheMatrix { } } -impl Default for TheMatrix { - fn default() -> Self { - let mut backgrounds = Vec::with_capacity(30); - backgrounds.resize_with(30, Cache::default); - - Self { - ticks: 0, - backgrounds, - } - } -} - impl<Message> canvas::Program<Message> for TheMatrix { - type State = (); + type State = RefCell<Vec<canvas::Cache>>; fn draw( &self, - _state: &Self::State, + state: &Self::State, renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor, - ) -> Vec<Geometry> { + ) -> Vec<canvas::Geometry> { use rand::distributions::Distribution; use rand::Rng; const CELL_SIZE: f32 = 10.0; - vec![self.backgrounds[self.ticks % self.backgrounds.len()].draw( + let mut caches = state.borrow_mut(); + + if caches.is_empty() { + let group = canvas::Group::unique(); + + caches.resize_with(30, || canvas::Cache::with_group(group)); + } + + vec![caches[self.tick % caches.len()].draw( renderer, bounds.size(), |frame| { diff --git a/graphics/src/cache.rs b/graphics/src/cache.rs new file mode 100644 index 00000000..bbba79eb --- /dev/null +++ b/graphics/src/cache.rs @@ -0,0 +1,189 @@ +//! Cache computations and efficiently reuse them. +use std::cell::RefCell; +use std::fmt; +use std::sync::atomic::{self, AtomicU64}; + +/// A simple cache that stores generated values to avoid recomputation. +/// +/// Keeps track of the last generated value after clearing. +pub struct Cache<T> { + group: Group, + state: RefCell<State<T>>, +} + +impl<T> Cache<T> { + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + group: Group::singleton(), + state: RefCell::new(State::Empty { previous: None }), + } + } + + /// Creates a new empty [`Cache`] with the given [`Group`]. + /// + /// Caches within the same group may reuse internal rendering storage. + /// + /// You should generally group caches that are likely to change + /// together. + pub fn with_group(group: Group) -> Self { + assert!( + !group.is_singleton(), + "The group {group:?} cannot be shared!" + ); + + Cache { + group, + state: RefCell::new(State::Empty { previous: None }), + } + } + + /// Returns the [`Group`] of the [`Cache`]. + pub fn group(&self) -> Group { + self.group + } + + /// Puts the given value in the [`Cache`]. + /// + /// Notice that, given this is a cache, a mutable reference is not + /// necessary to call this method. You can safely update the cache in + /// rendering code. + pub fn put(&self, value: T) { + *self.state.borrow_mut() = State::Filled { current: value }; + } + + /// Returns a reference cell to the internal [`State`] of the [`Cache`]. + pub fn state(&self) -> &RefCell<State<T>> { + &self.state + } + + /// Clears the [`Cache`]. + pub fn clear(&self) + where + T: Clone, + { + use std::ops::Deref; + + let previous = match self.state.borrow().deref() { + State::Empty { previous } => previous.clone(), + State::Filled { current } => Some(current.clone()), + }; + + *self.state.borrow_mut() = State::Empty { previous }; + } +} + +/// A cache group. +/// +/// Caches that share the same group generally change together. +/// +/// A cache group can be used to implement certain performance +/// optimizations during rendering, like batching or sharing atlases. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Group { + id: u64, + is_singleton: bool, +} + +impl Group { + /// Generates a new unique cache [`Group`]. + pub fn unique() -> Self { + static NEXT: AtomicU64 = AtomicU64::new(0); + + Self { + id: NEXT.fetch_add(1, atomic::Ordering::Relaxed), + is_singleton: false, + } + } + + /// Returns `true` if the [`Group`] can only ever have a + /// single [`Cache`] in it. + /// + /// This is the default kind of [`Group`] assigned when using + /// [`Cache::new`]. + /// + /// Knowing that a [`Group`] will never be shared may be + /// useful for rendering backends to perform additional + /// optimizations. + pub fn is_singleton(self) -> bool { + self.is_singleton + } + + fn singleton() -> Self { + Self { + is_singleton: true, + ..Self::unique() + } + } +} + +impl<T> fmt::Debug for Cache<T> +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use std::ops::Deref; + + let state = self.state.borrow(); + + match state.deref() { + State::Empty { previous } => { + write!(f, "Cache::Empty {{ previous: {previous:?} }}") + } + State::Filled { current } => { + write!(f, "Cache::Filled {{ current: {current:?} }}") + } + } + } +} + +impl<T> Default for Cache<T> { + fn default() -> Self { + Self::new() + } +} + +/// The state of a [`Cache`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum State<T> { + /// The [`Cache`] is empty. + Empty { + /// The previous value of the [`Cache`]. + previous: Option<T>, + }, + /// The [`Cache`] is filled. + Filled { + /// The current value of the [`Cache`] + current: T, + }, +} + +/// A piece of data that can be cached. +pub trait Cached: Sized { + /// The type of cache produced. + type Cache: Clone; + + /// Loads the [`Cache`] into a proper instance. + /// + /// [`Cache`]: Self::Cache + fn load(cache: &Self::Cache) -> Self; + + /// Caches this value, producing its corresponding [`Cache`]. + /// + /// [`Cache`]: Self::Cache + fn cache(self, group: Group, previous: Option<Self::Cache>) -> Self::Cache; +} + +#[cfg(debug_assertions)] +impl Cached for () { + type Cache = (); + + fn load(_cache: &Self::Cache) -> Self {} + + fn cache( + self, + _group: Group, + _previous: Option<Self::Cache>, + ) -> Self::Cache { + } +} diff --git a/graphics/src/cached.rs b/graphics/src/cached.rs deleted file mode 100644 index c0e4e029..00000000 --- a/graphics/src/cached.rs +++ /dev/null @@ -1,24 +0,0 @@ -/// A piece of data that can be cached. -pub trait Cached: Sized { - /// The type of cache produced. - type Cache: Clone; - - /// Loads the [`Cache`] into a proper instance. - /// - /// [`Cache`]: Self::Cache - fn load(cache: &Self::Cache) -> Self; - - /// Caches this value, producing its corresponding [`Cache`]. - /// - /// [`Cache`]: Self::Cache - fn cache(self, previous: Option<Self::Cache>) -> Self::Cache; -} - -#[cfg(debug_assertions)] -impl Cached for () { - type Cache = (); - - fn load(_cache: &Self::Cache) -> Self {} - - fn cache(self, _previous: Option<Self::Cache>) -> Self::Cache {} -} diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs index dd07097f..ab4a7a36 100644 --- a/graphics/src/geometry.rs +++ b/graphics/src/geometry.rs @@ -18,8 +18,8 @@ pub use text::Text; pub use crate::gradient::{self, Gradient}; +use crate::cache::Cached; use crate::core::{self, Size}; -use crate::Cached; /// A renderer capable of drawing some [`Self::Geometry`]. pub trait Renderer: core::Renderer { diff --git a/graphics/src/geometry/cache.rs b/graphics/src/geometry/cache.rs index 665e996b..d70cee0b 100644 --- a/graphics/src/geometry/cache.rs +++ b/graphics/src/geometry/cache.rs @@ -1,8 +1,8 @@ +use crate::cache::{self, Cached}; use crate::core::Size; use crate::geometry::{self, Frame}; -use crate::Cached; -use std::cell::RefCell; +pub use cache::Group; /// A simple cache that stores generated geometry to avoid recomputation. /// @@ -12,7 +12,13 @@ pub struct Cache<Renderer> where Renderer: geometry::Renderer, { - state: RefCell<State<Renderer::Geometry>>, + raw: crate::Cache<Data<<Renderer::Geometry as Cached>::Cache>>, +} + +#[derive(Debug, Clone)] +struct Data<T> { + bounds: Size, + geometry: T, } impl<Renderer> Cache<Renderer> @@ -22,20 +28,25 @@ where /// Creates a new empty [`Cache`]. pub fn new() -> Self { Cache { - state: RefCell::new(State::Empty { previous: None }), + raw: cache::Cache::new(), + } + } + + /// Creates a new empty [`Cache`] with the given [`Group`]. + /// + /// Caches within the same group may reuse internal rendering storage. + /// + /// You should generally group caches that are likely to change + /// together. + pub fn with_group(group: Group) -> Self { + Cache { + raw: crate::Cache::with_group(group), } } /// Clears the [`Cache`], forcing a redraw the next time it is used. pub fn clear(&self) { - use std::ops::Deref; - - let previous = match self.state.borrow().deref() { - State::Empty { previous } => previous.clone(), - State::Filled { geometry, .. } => Some(geometry.clone()), - }; - - *self.state.borrow_mut() = State::Empty { previous }; + self.raw.clear(); } /// Draws geometry using the provided closure and stores it in the @@ -56,27 +67,30 @@ where ) -> Renderer::Geometry { use std::ops::Deref; - let previous = match self.state.borrow().deref() { - State::Empty { previous } => previous.clone(), - State::Filled { - bounds: cached_bounds, - geometry, - } => { - if *cached_bounds == bounds { - return Cached::load(geometry); + let state = self.raw.state(); + + let previous = match state.borrow().deref() { + cache::State::Empty { previous } => { + previous.as_ref().map(|data| data.geometry.clone()) + } + cache::State::Filled { current } => { + if current.bounds == bounds { + return Cached::load(¤t.geometry); } - Some(geometry.clone()) + Some(current.geometry.clone()) } }; let mut frame = Frame::new(renderer, bounds); draw_fn(&mut frame); - let geometry = frame.into_geometry().cache(previous); + let geometry = frame.into_geometry().cache(self.raw.group(), previous); let result = Cached::load(&geometry); - *self.state.borrow_mut() = State::Filled { bounds, geometry }; + *state.borrow_mut() = cache::State::Filled { + current: Data { bounds, geometry }, + }; result } @@ -85,16 +99,10 @@ where impl<Renderer> std::fmt::Debug for Cache<Renderer> where Renderer: geometry::Renderer, + <Renderer::Geometry as Cached>::Cache: std::fmt::Debug, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let state = self.state.borrow(); - - match *state { - State::Empty { .. } => write!(f, "Cache::Empty"), - State::Filled { bounds, .. } => { - write!(f, "Cache::Filled {{ bounds: {bounds:?} }}") - } - } + write!(f, "{:?}", &self.raw) } } @@ -106,16 +114,3 @@ where Self::new() } } - -enum State<Geometry> -where - Geometry: Cached, -{ - Empty { - previous: Option<Geometry::Cache>, - }, - Filled { - bounds: Size, - geometry: Geometry::Cache, - }, -} diff --git a/graphics/src/image.rs b/graphics/src/image.rs index c6135e9e..04c45057 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -50,7 +50,8 @@ impl Image { /// [`Handle`]: image::Handle pub fn load( handle: &image::Handle, -) -> ::image::ImageResult<::image::DynamicImage> { +) -> ::image::ImageResult<::image::ImageBuffer<::image::Rgba<u8>, image::Bytes>> +{ use bitflags::bitflags; bitflags! { @@ -100,8 +101,8 @@ pub fn load( } } - match handle.data() { - image::Data::Path(path) => { + let (width, height, pixels) = match handle { + image::Handle::Path(_, path) => { let image = ::image::open(path)?; let operation = std::fs::File::open(path) @@ -110,33 +111,44 @@ pub fn load( .and_then(|mut reader| Operation::from_exif(&mut reader).ok()) .unwrap_or_else(Operation::empty); - Ok(operation.perform(image)) + let rgba = operation.perform(image).into_rgba8(); + + ( + rgba.width(), + rgba.height(), + image::Bytes::from(rgba.into_raw()), + ) } - image::Data::Bytes(bytes) => { + image::Handle::Bytes(_, bytes) => { let image = ::image::load_from_memory(bytes)?; let operation = Operation::from_exif(&mut std::io::Cursor::new(bytes)) .ok() .unwrap_or_else(Operation::empty); - Ok(operation.perform(image)) + let rgba = operation.perform(image).into_rgba8(); + + ( + rgba.width(), + rgba.height(), + image::Bytes::from(rgba.into_raw()), + ) } - image::Data::Rgba { + image::Handle::Rgba { width, height, pixels, - } => { - if let Some(image) = - ::image::ImageBuffer::from_vec(*width, *height, pixels.to_vec()) - { - Ok(::image::DynamicImage::ImageRgba8(image)) - } else { - Err(::image::error::ImageError::Limits( - ::image::error::LimitError::from_kind( - ::image::error::LimitErrorKind::DimensionError, - ), - )) - } - } + .. + } => (*width, *height, pixels.clone()), + }; + + if let Some(image) = ::image::ImageBuffer::from_raw(width, height, pixels) { + Ok(image) + } else { + Err(::image::error::ImageError::Limits( + ::image::error::LimitError::from_kind( + ::image::error::LimitErrorKind::DimensionError, + ), + )) } } diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 865ebd97..b5ef55e7 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -9,10 +9,10 @@ )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] mod antialiasing; -mod cached; mod settings; mod viewport; +pub mod cache; pub mod color; pub mod compositor; pub mod damage; @@ -27,7 +27,7 @@ pub mod text; pub mod geometry; pub use antialiasing::Antialiasing; -pub use cached::Cached; +pub use cache::Cache; pub use compositor::Compositor; pub use error::Error; pub use gradient::Gradient; diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index c932de00..5f69b420 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -428,8 +428,8 @@ where mod geometry { use super::Renderer; use crate::core::{Point, Radians, Rectangle, Size, Vector}; + use crate::graphics::cache::{self, Cached}; use crate::graphics::geometry::{self, Fill, Path, Stroke, Text}; - use crate::graphics::Cached; impl<A, B> geometry::Renderer for Renderer<A, B> where @@ -483,21 +483,25 @@ mod geometry { } } - fn cache(self, previous: Option<Self::Cache>) -> Self::Cache { + fn cache( + self, + group: cache::Group, + previous: Option<Self::Cache>, + ) -> Self::Cache { match (self, previous) { ( Self::Primary(geometry), Some(Geometry::Primary(previous)), - ) => Geometry::Primary(geometry.cache(Some(previous))), + ) => Geometry::Primary(geometry.cache(group, Some(previous))), (Self::Primary(geometry), None) => { - Geometry::Primary(geometry.cache(None)) + Geometry::Primary(geometry.cache(group, None)) } ( Self::Secondary(geometry), Some(Geometry::Secondary(previous)), - ) => Geometry::Secondary(geometry.cache(Some(previous))), + ) => Geometry::Secondary(geometry.cache(group, Some(previous))), (Self::Secondary(geometry), None) => { - Geometry::Secondary(geometry.cache(None)) + Geometry::Secondary(geometry.cache(group, None)) } _ => unreachable!(), } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 21503462..703c3ed9 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -18,6 +18,7 @@ debug = [] multi-window = [] [dependencies] +bytes.workspace = true iced_core.workspace = true iced_futures.workspace = true iced_futures.features = ["thread-pool"] diff --git a/runtime/src/window/screenshot.rs b/runtime/src/window/screenshot.rs index 21e04718..fb318110 100644 --- a/runtime/src/window/screenshot.rs +++ b/runtime/src/window/screenshot.rs @@ -1,8 +1,8 @@ //! Take screenshots of a window. use crate::core::{Rectangle, Size}; +use bytes::Bytes; use std::fmt::{Debug, Formatter}; -use std::sync::Arc; /// Data of a screenshot, captured with `window::screenshot()`. /// @@ -10,7 +10,7 @@ use std::sync::Arc; #[derive(Clone)] pub struct Screenshot { /// The bytes of the [`Screenshot`]. - pub bytes: Arc<Vec<u8>>, + pub bytes: Bytes, /// The size of the [`Screenshot`]. pub size: Size<u32>, } @@ -28,9 +28,9 @@ impl Debug for Screenshot { impl Screenshot { /// Creates a new [`Screenshot`]. - pub fn new(bytes: Vec<u8>, size: Size<u32>) -> Self { + pub fn new(bytes: impl Into<Bytes>, size: Size<u32>) -> Self { Self { - bytes: Arc::new(bytes), + bytes: bytes.into(), size, } } @@ -68,7 +68,7 @@ impl Screenshot { ); Ok(Self { - bytes: Arc::new(chopped), + bytes: Bytes::from(chopped), size: Size::new(region.width, region.height), }) } @@ -80,6 +80,12 @@ impl AsRef<[u8]> for Screenshot { } } +impl From<Screenshot> for Bytes { + fn from(screenshot: Screenshot) -> Self { + screenshot.bytes + } +} + #[derive(Debug, thiserror::Error)] /// Errors that can occur when cropping a [`Screenshot`]. pub enum CropError { diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 117daf41..02b6e1b9 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -1,9 +1,10 @@ use crate::core::text::LineHeight; use crate::core::{Pixels, Point, Radians, Rectangle, Size, Vector}; +use crate::graphics::cache::{self, Cached}; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; use crate::graphics::geometry::{self, Path, Style}; -use crate::graphics::{Cached, Gradient, Text}; +use crate::graphics::{Gradient, Text}; use crate::Primitive; use std::rc::Rc; @@ -32,7 +33,7 @@ impl Cached for Geometry { Self::Cache(cache.clone()) } - fn cache(self, _previous: Option<Cache>) -> Cache { + fn cache(self, _group: cache::Group, _previous: Option<Cache>) -> Cache { match self { Self::Live { primitives, diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index 176b0da9..907fce7c 100644 --- a/tiny_skia/src/raster.rs +++ b/tiny_skia/src/raster.rs @@ -71,8 +71,8 @@ impl Pipeline { #[derive(Debug, Default)] struct Cache { - entries: FxHashMap<u64, Option<Entry>>, - hits: FxHashSet<u64>, + entries: FxHashMap<raster::Id, Option<Entry>>, + hits: FxHashSet<raster::Id>, } impl Cache { @@ -83,7 +83,7 @@ impl Cache { let id = handle.id(); if let hash_map::Entry::Vacant(entry) = self.entries.entry(id) { - let image = graphics::image::load(handle).ok()?.into_rgba8(); + let image = graphics::image::load(handle).ok()?; let mut buffer = vec![0u32; image.width() as usize * image.height() as usize]; diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 60967082..f6213e1d 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -3,6 +3,7 @@ use crate::core::text::LineHeight; use crate::core::{ Pixels, Point, Radians, Rectangle, Size, Transformation, Vector, }; +use crate::graphics::cache::{self, Cached}; use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ @@ -10,7 +11,7 @@ use crate::graphics::geometry::{ }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; -use crate::graphics::{self, Cached, Text}; +use crate::graphics::{self, Text}; use crate::text; use crate::triangle; @@ -38,7 +39,11 @@ impl Cached for Geometry { Geometry::Cached(cache.clone()) } - fn cache(self, previous: Option<Self::Cache>) -> Self::Cache { + fn cache( + self, + group: cache::Group, + previous: Option<Self::Cache>, + ) -> Self::Cache { match self { Self::Live { meshes, text } => { if let Some(mut previous) = previous { @@ -51,14 +56,14 @@ impl Cached for Geometry { if let Some(cache) = &mut previous.text { cache.update(text); } else { - previous.text = text::Cache::new(text); + previous.text = text::Cache::new(group, text); } previous } else { Cache { meshes: triangle::Cache::new(meshes), - text: text::Cache::new(text), + text: text::Cache::new(group, text), } } } diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs index 7a837f28..4d3c3125 100644 --- a/wgpu/src/image/raster.rs +++ b/wgpu/src/image/raster.rs @@ -10,7 +10,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; #[derive(Debug)] pub enum Memory { /// Image data on host - Host(image_rs::ImageBuffer<image_rs::Rgba<u8>, Vec<u8>>), + Host(image_rs::ImageBuffer<image_rs::Rgba<u8>, image::Bytes>), /// Storage entry Device(atlas::Entry), /// Image not found @@ -38,8 +38,8 @@ impl Memory { /// Caches image raster data #[derive(Debug, Default)] pub struct Cache { - map: FxHashMap<u64, Memory>, - hits: FxHashSet<u64>, + map: FxHashMap<image::Id, Memory>, + hits: FxHashSet<image::Id>, should_trim: bool, } @@ -51,7 +51,7 @@ impl Cache { } let memory = match graphics::image::load(handle) { - Ok(image) => Memory::Host(image.to_rgba8()), + Ok(image) => Memory::Host(image), Err(image_rs::error::ImageError::IoError(_)) => Memory::NotFound, Err(_) => Memory::Invalid, }; diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 38712660..7e683c77 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,7 +1,8 @@ use crate::core::alignment; use crate::core::{Rectangle, Size, Transformation}; +use crate::graphics::cache; use crate::graphics::color; -use crate::graphics::text::cache::{self, Cache as BufferCache}; +use crate::graphics::text::cache::{self as text_cache, Cache as BufferCache}; use crate::graphics::text::{font_system, to_color, Editor, Paragraph}; use rustc_hash::FxHashMap; @@ -35,6 +36,7 @@ pub enum Item { #[derive(Debug, Clone)] pub struct Cache { id: Id, + group: cache::Group, text: Rc<[Text]>, version: usize, } @@ -43,7 +45,7 @@ pub struct Cache { pub struct Id(u64); impl Cache { - pub fn new(text: Vec<Text>) -> Option<Self> { + pub fn new(group: cache::Group, text: Vec<Text>) -> Option<Self> { static NEXT_ID: AtomicU64 = AtomicU64::new(0); if text.is_empty() { @@ -52,12 +54,17 @@ impl Cache { Some(Self { id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), + group, text: Rc::from(text), version: 0, }) } pub fn update(&mut self, text: Vec<Text>) { + if self.text.is_empty() && text.is_empty() { + return; + } + self.text = Rc::from(text); self.version += 1; } @@ -65,29 +72,41 @@ impl Cache { struct Upload { renderer: glyphon::TextRenderer, - atlas: glyphon::TextAtlas, buffer_cache: BufferCache, transformation: Transformation, version: usize, + group_version: usize, text: rc::Weak<[Text]>, + _atlas: rc::Weak<()>, } #[derive(Default)] pub struct Storage { + groups: FxHashMap<cache::Group, Group>, uploads: FxHashMap<Id, Upload>, } +struct Group { + atlas: glyphon::TextAtlas, + version: usize, + should_trim: bool, + handle: Rc<()>, // Keeps track of active uploads +} + impl Storage { pub fn new() -> Self { Self::default() } - fn get(&self, cache: &Cache) -> Option<&Upload> { + fn get(&self, cache: &Cache) -> Option<(&glyphon::TextAtlas, &Upload)> { if cache.text.is_empty() { return None; } - self.uploads.get(&cache.id) + self.groups + .get(&cache.group) + .map(|group| &group.atlas) + .zip(self.uploads.get(&cache.id)) } fn prepare( @@ -101,42 +120,63 @@ impl Storage { bounds: Rectangle, target_size: Size<u32>, ) { + let group_count = self.groups.len(); + + let group = self.groups.entry(cache.group).or_insert_with(|| { + log::debug!( + "New text atlas: {:?} (total: {})", + cache.group, + group_count + 1 + ); + + Group { + atlas: glyphon::TextAtlas::with_color_mode( + device, queue, format, COLOR_MODE, + ), + version: 0, + should_trim: false, + handle: Rc::new(()), + } + }); + match self.uploads.entry(cache.id) { hash_map::Entry::Occupied(entry) => { let upload = entry.into_mut(); - if !cache.text.is_empty() - && (upload.version != cache.version - || upload.transformation != new_transformation) + if upload.version != cache.version + || upload.group_version != group.version + || upload.transformation != new_transformation { - let _ = prepare( - device, - queue, - encoder, - &mut upload.renderer, - &mut upload.atlas, - &mut upload.buffer_cache, - &cache.text, - bounds, - new_transformation, - target_size, - ); + if !cache.text.is_empty() { + let _ = prepare( + device, + queue, + encoder, + &mut upload.renderer, + &mut group.atlas, + &mut upload.buffer_cache, + &cache.text, + bounds, + new_transformation, + target_size, + ); + } + + // Only trim if glyphs have changed + group.should_trim = + group.should_trim || upload.version != cache.version; upload.text = Rc::downgrade(&cache.text); upload.version = cache.version; + upload.group_version = group.version; upload.transformation = new_transformation; upload.buffer_cache.trim(); - upload.atlas.trim(); } } hash_map::Entry::Vacant(entry) => { - let mut atlas = glyphon::TextAtlas::with_color_mode( - device, queue, format, COLOR_MODE, - ); - let mut renderer = glyphon::TextRenderer::new( - &mut atlas, + &mut group.atlas, device, wgpu::MultisampleState::default(), None, @@ -144,29 +184,34 @@ impl Storage { let mut buffer_cache = BufferCache::new(); - let _ = prepare( - device, - queue, - encoder, - &mut renderer, - &mut atlas, - &mut buffer_cache, - &cache.text, - bounds, - new_transformation, - target_size, - ); + if !cache.text.is_empty() { + let _ = prepare( + device, + queue, + encoder, + &mut renderer, + &mut group.atlas, + &mut buffer_cache, + &cache.text, + bounds, + new_transformation, + target_size, + ); + } let _ = entry.insert(Upload { renderer, - atlas, buffer_cache, transformation: new_transformation, version: 0, + group_version: group.version, text: Rc::downgrade(&cache.text), + _atlas: Rc::downgrade(&group.handle), }); - log::info!( + group.should_trim = cache.group.is_singleton(); + + log::debug!( "New text upload: {} (total: {})", cache.id.0, self.uploads.len() @@ -178,6 +223,37 @@ impl Storage { pub fn trim(&mut self) { self.uploads .retain(|_id, upload| upload.text.strong_count() > 0); + + self.groups.retain(|id, group| { + let active_uploads = Rc::weak_count(&group.handle); + + if active_uploads == 0 { + log::debug!("Dropping text atlas: {id:?}"); + + return false; + } + + if group.should_trim { + log::trace!("Trimming text atlas: {id:?}"); + + group.atlas.trim(); + group.should_trim = false; + + // We only need to worry about glyph fighting + // when the atlas may be shared by multiple + // uploads. + if !id.is_singleton() { + log::debug!( + "Invalidating text atlas: {id:?} \ + (uploads: {active_uploads})" + ); + + group.version += 1; + } + } + + true + }); } } @@ -306,10 +382,10 @@ impl Pipeline { layer_count += 1; } Item::Cached { cache, .. } => { - if let Some(upload) = storage.get(cache) { + if let Some((atlas, upload)) = storage.get(cache) { upload .renderer - .render(&upload.atlas, render_pass) + .render(atlas, render_pass) .expect("Render cached text"); } } @@ -345,7 +421,7 @@ fn prepare( enum Allocation { Paragraph(Paragraph), Editor(Editor), - Cache(cache::KeyHash), + Cache(text_cache::KeyHash), Raw(Arc<glyphon::Buffer>), } @@ -369,7 +445,7 @@ fn prepare( } => { let (key, _) = buffer_cache.allocate( font_system, - cache::Key { + text_cache::Key { content, size: f32::from(*size), line_height: f32::from(*line_height), diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index ca36de82..b0551f55 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -138,7 +138,7 @@ impl Storage { batch: Rc::downgrade(&cache.batch), }); - log::info!( + log::debug!( "New mesh upload: {} (total: {})", cache.id.0, self.uploads.len() diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 42f92de0..be09f163 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -6,6 +6,7 @@ mod program; pub use event::Event; pub use program::Program; +pub use crate::graphics::cache::Group; pub use crate::graphics::geometry::{ fill, gradient, path, stroke, Fill, Gradient, LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, diff --git a/widget/src/image.rs b/widget/src/image.rs index f673c7b3..21d371b7 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -11,8 +11,6 @@ use crate::core::{ ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, }; -use std::hash::Hash; - pub use image::{FilterMethod, Handle}; /// Creates a new [`Viewer`] with the given image `Handle`. @@ -128,7 +126,7 @@ pub fn draw<Renderer, Handle>( filter_method: FilterMethod, ) where Renderer: image::Renderer<Handle = Handle>, - Handle: Clone + Hash, + Handle: Clone, { let Size { width, height } = renderer.measure_image(handle); let image_size = Size::new(width as f32, height as f32); @@ -167,7 +165,7 @@ impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer> for Image<Handle> where Renderer: image::Renderer<Handle = Handle>, - Handle: Clone + Hash, + Handle: Clone, { fn size(&self) -> Size<Length> { Size { @@ -216,7 +214,7 @@ impl<'a, Message, Theme, Renderer, Handle> From<Image<Handle>> for Element<'a, Message, Theme, Renderer> where Renderer: image::Renderer<Handle = Handle>, - Handle: Clone + Hash + 'a, + Handle: Clone + 'a, { fn from(image: Image<Handle>) -> Element<'a, Message, Theme, Renderer> { Element::new(image) diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 75d73b19..214cb996 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -10,8 +10,6 @@ use crate::core::{ Vector, Widget, }; -use std::hash::Hash; - /// A frame that displays an image with the ability to zoom in/out and pan. #[allow(missing_debug_implementations)] pub struct Viewer<Handle> { @@ -94,7 +92,7 @@ impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer> for Viewer<Handle> where Renderer: image::Renderer<Handle = Handle>, - Handle: Clone + Hash, + Handle: Clone, { fn tag(&self) -> tree::Tag { tree::Tag::of::<State>() @@ -401,7 +399,7 @@ impl<'a, Message, Theme, Renderer, Handle> From<Viewer<Handle>> where Renderer: 'a + image::Renderer<Handle = Handle>, Message: 'a, - Handle: Clone + Hash + 'a, + Handle: Clone + 'a, { fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Theme, Renderer> { Element::new(viewer) |