diff --git a/core/src/image.rs b/core/src/image.rs index 70fbade052..85d9d4758c 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use std::sync::Arc; /// A handle of some image data. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Handle { id: u64, data: Data, @@ -110,6 +110,14 @@ impl std::hash::Hash for Bytes { } } +impl PartialEq for Bytes { + fn eq(&self, other: &Self) -> bool { + self.as_ref() == other.as_ref() + } +} + +impl Eq for Bytes {} + impl AsRef<[u8]> for Bytes { fn as_ref(&self) -> &[u8] { self.0.as_ref().as_ref() @@ -125,7 +133,7 @@ impl std::ops::Deref for Bytes { } /// The data of a raster image. -#[derive(Clone, Hash)] +#[derive(Clone, PartialEq, Eq, Hash)] pub enum Data { /// File data Path(PathBuf), diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 4fe9151957..7ff324cb89 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -66,6 +66,11 @@ impl Rectangle { Size::new(self.width, self.height) } + /// Returns the area of the [`Rectangle`]. + pub fn area(&self) -> f32 { + self.width * self.height + } + /// Returns true if the given [`Point`] is contained in the [`Rectangle`]. pub fn contains(&self, point: Point) -> bool { self.x <= point.x @@ -74,6 +79,15 @@ impl Rectangle { && point.y <= self.y + self.height } + /// Returns true if the current [`Rectangle`] is completely within the given + /// `container`. + pub fn is_within(&self, container: &Rectangle) -> bool { + container.contains(self.position()) + && container.contains( + self.position() + Vector::new(self.width, self.height), + ) + } + /// Computes the intersection with the given [`Rectangle`]. pub fn intersection( &self, @@ -100,6 +114,30 @@ impl Rectangle { } } + /// Returns whether the [`Rectangle`] intersects with the given one. + pub fn intersects(&self, other: &Self) -> bool { + self.intersection(other).is_some() + } + + /// Computes the union with the given [`Rectangle`]. + pub fn union(&self, other: &Self) -> Self { + let x = self.x.min(other.x); + let y = self.y.min(other.y); + + let lower_right_x = (self.x + self.width).max(other.x + other.width); + let lower_right_y = (self.y + self.height).max(other.y + other.height); + + let width = lower_right_x - x; + let height = lower_right_y - y; + + Rectangle { + x, + y, + width, + height, + } + } + /// Snaps the [`Rectangle`] to __unsigned__ integer coordinates. pub fn snap(self) -> Rectangle { Rectangle { @@ -109,6 +147,16 @@ impl Rectangle { height: self.height as u32, } } + + /// Expands the [`Rectangle`] a given amount. + pub fn expand(self, amount: f32) -> Self { + Self { + x: self.x - amount, + y: self.y - amount, + width: self.width + amount * 2.0, + height: self.height + amount * 2.0, + } + } } impl std::ops::Mul for Rectangle { diff --git a/core/src/svg.rs b/core/src/svg.rs index 9b98877a2e..54e9434e05 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use std::sync::Arc; /// A handle of Svg data. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Handle { id: u64, data: Arc, @@ -57,7 +57,7 @@ impl Hash for Handle { } /// The data of a vectorial image. -#[derive(Clone, Hash)] +#[derive(Clone, Hash, PartialEq, Eq)] pub enum Data { /// File data Path(PathBuf), diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 98d2bc5940..d9aae7b9ab 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -36,7 +36,8 @@ pub fn main() -> Result<(), Box> { web_sys::window() .and_then(|win| win.document()) .and_then(|doc| doc.get_element_by_id("iced_canvas")) - .and_then(|element| element.dyn_into::().ok())? + .and_then(|element| element.dyn_into::().ok()) + .expect("Get canvas element") }; #[cfg(not(target_arch = "wasm32"))] env_logger::init(); diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index f1ce6b3aa3..125ea17d22 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -31,7 +31,7 @@ version = "0.9" path = "../core" [dependencies.tiny-skia] -version = "0.8" +version = "0.9" optional = true [dependencies.image] diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs new file mode 100644 index 0000000000..5aab06b17b --- /dev/null +++ b/graphics/src/damage.rs @@ -0,0 +1,142 @@ +use crate::core::{Rectangle, Size}; +use crate::Primitive; + +use std::sync::Arc; + +pub fn regions(a: &Primitive, b: &Primitive) -> Vec { + match (a, b) { + ( + Primitive::Group { + primitives: primitives_a, + }, + Primitive::Group { + primitives: primitives_b, + }, + ) => return list(primitives_a, primitives_b), + ( + Primitive::Clip { + bounds: bounds_a, + content: content_a, + .. + }, + Primitive::Clip { + bounds: bounds_b, + content: content_b, + .. + }, + ) => { + if bounds_a == bounds_b { + return regions(content_a, content_b) + .into_iter() + .filter_map(|r| r.intersection(&bounds_a.expand(1.0))) + .collect(); + } else { + return vec![bounds_a.expand(1.0), bounds_b.expand(1.0)]; + } + } + ( + Primitive::Translate { + translation: translation_a, + content: content_a, + }, + Primitive::Translate { + translation: translation_b, + content: content_b, + }, + ) => { + if translation_a == translation_b { + return regions(content_a, content_b) + .into_iter() + .map(|r| r + *translation_a) + .collect(); + } + } + ( + Primitive::Cache { content: content_a }, + Primitive::Cache { content: content_b }, + ) => { + if Arc::ptr_eq(content_a, content_b) { + return vec![]; + } + } + _ if a == b => return vec![], + _ => {} + } + + let bounds_a = a.bounds(); + let bounds_b = b.bounds(); + + if bounds_a == bounds_b { + vec![bounds_a] + } else { + vec![bounds_a, bounds_b] + } +} + +pub fn list(previous: &[Primitive], current: &[Primitive]) -> Vec { + let damage = previous + .iter() + .zip(current) + .flat_map(|(a, b)| regions(a, b)); + + if previous.len() == current.len() { + damage.collect() + } else { + let (smaller, bigger) = if previous.len() < current.len() { + (previous, current) + } else { + (current, previous) + }; + + // Extend damage by the added/removed primitives + damage + .chain(bigger[smaller.len()..].iter().map(Primitive::bounds)) + .collect() + } +} + +pub fn group( + mut damage: Vec, + scale_factor: f32, + bounds: Size, +) -> Vec { + use std::cmp::Ordering; + + const AREA_THRESHOLD: f32 = 20_000.0; + + let bounds = Rectangle { + x: 0.0, + y: 0.0, + width: bounds.width as f32, + height: bounds.height as f32, + }; + + damage.sort_by(|a, b| { + a.x.partial_cmp(&b.x) + .unwrap_or(Ordering::Equal) + .then_with(|| a.y.partial_cmp(&b.y).unwrap_or(Ordering::Equal)) + }); + + let mut output = Vec::new(); + let mut scaled = damage + .into_iter() + .filter_map(|region| (region * scale_factor).intersection(&bounds)) + .filter(|region| region.width >= 1.0 && region.height >= 1.0); + + if let Some(mut current) = scaled.next() { + for region in scaled { + let union = current.union(®ion); + + if union.area() - current.area() - region.area() <= AREA_THRESHOLD { + current = union; + } else { + output.push(current); + current = region; + } + } + + output.push(current); + } + + output +} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 0c50db52d5..e3de40254c 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -28,6 +28,7 @@ mod viewport; pub mod backend; pub mod compositor; +pub mod damage; pub mod primitive; pub mod renderer; diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index 195b62daa5..d6a2c4c4cc 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -7,7 +7,7 @@ use bytemuck::{Pod, Zeroable}; use std::sync::Arc; /// A rendering primitive. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum Primitive { /// A text primitive @@ -147,10 +147,71 @@ impl Primitive { content: Box::new(self), } } + + pub fn bounds(&self) -> Rectangle { + match self { + Self::Text { + bounds, + horizontal_alignment, + vertical_alignment, + .. + } => { + let mut bounds = *bounds; + + bounds.x = match horizontal_alignment { + alignment::Horizontal::Left => bounds.x, + alignment::Horizontal::Center => { + bounds.x - bounds.width / 2.0 + } + alignment::Horizontal::Right => bounds.x - bounds.width, + }; + + bounds.y = match vertical_alignment { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => { + bounds.y - bounds.height / 2.0 + } + alignment::Vertical::Bottom => bounds.y - bounds.height, + }; + + bounds.expand(1.5) + } + Self::Quad { bounds, .. } + | Self::Image { bounds, .. } + | Self::Svg { bounds, .. } => bounds.expand(1.0), + Self::Clip { bounds, .. } => bounds.expand(1.0), + Self::SolidMesh { size, .. } | Self::GradientMesh { size, .. } => { + Rectangle::with_size(*size) + } + #[cfg(feature = "tiny-skia")] + Self::Fill { path, .. } | Self::Stroke { path, .. } => { + let bounds = path.bounds(); + + Rectangle { + x: bounds.x(), + y: bounds.y(), + width: bounds.width(), + height: bounds.height(), + } + .expand(1.0) + } + Self::Group { primitives } => primitives + .iter() + .map(Self::bounds) + .fold(Rectangle::with_size(Size::ZERO), |a, b| { + Rectangle::union(&a, &b) + }), + Self::Translate { + translation, + content, + } => content.bounds() + *translation, + Self::Cache { content } => content.bounds(), + } + } } /// A set of [`Vertex2D`] and indices representing a list of triangles. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Mesh2D { /// The vertices of the mesh pub vertices: Vec, @@ -162,7 +223,7 @@ pub struct Mesh2D { } /// A two-dimensional vertex. -#[derive(Copy, Clone, Debug, Zeroable, Pod)] +#[derive(Copy, Clone, Debug, PartialEq, Zeroable, Pod)] #[repr(C)] pub struct Vertex2D { /// The vertex position in 2D space. @@ -170,7 +231,7 @@ pub struct Vertex2D { } /// A two-dimensional vertex with a color. -#[derive(Copy, Clone, Debug, Zeroable, Pod)] +#[derive(Copy, Clone, Debug, PartialEq, Zeroable, Pod)] #[repr(C)] pub struct ColoredVertex2D { /// The vertex position in 2D space. diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index 7bc462ef45..23e594be72 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -32,7 +32,7 @@ impl Renderer { } } - /// Returns the [`Backend`] of the [`Renderer`]. + /// Returns a reference to the [`Backend`] of the [`Renderer`]. pub fn backend(&self) -> &B { &self.backend } diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index 218e7e332e..48ed4b1f1c 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -4,6 +4,7 @@ use crate::graphics::{Error, Viewport}; use crate::{Renderer, Settings}; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use std::env; pub enum Compositor { #[cfg(feature = "wgpu")] @@ -28,53 +29,13 @@ impl crate::graphics::Compositor for Compositor { settings: Self::Settings, compatible_window: Option<&W>, ) -> Result<(Self, Self::Renderer), Error> { - #[cfg(feature = "wgpu")] - let new_wgpu = |settings: Self::Settings, compatible_window| { - let (compositor, backend) = iced_wgpu::window::compositor::new( - iced_wgpu::Settings { - default_font: settings.default_font, - default_text_size: settings.default_text_size, - antialiasing: settings.antialiasing, - ..iced_wgpu::Settings::from_env() - }, - compatible_window, - )?; - - Ok(( - Self::Wgpu(compositor), - Renderer::new(crate::Backend::Wgpu(backend)), - )) - }; - - #[cfg(feature = "tiny-skia")] - let new_tiny_skia = |settings: Self::Settings, _compatible_window| { - let (compositor, backend) = iced_tiny_skia::window::compositor::new( - iced_tiny_skia::Settings { - default_font: settings.default_font, - default_text_size: settings.default_text_size, - }, - ); - - Ok(( - Self::TinySkia(compositor), - Renderer::new(crate::Backend::TinySkia(backend)), - )) - }; - - let fail = |_, _| Err(Error::GraphicsAdapterNotFound); - - let candidates = &[ - #[cfg(feature = "wgpu")] - new_wgpu, - #[cfg(feature = "tiny-skia")] - new_tiny_skia, - fail, - ]; + let candidates = + Candidate::list_from_env().unwrap_or(Candidate::default_list()); let mut error = Error::GraphicsAdapterNotFound; for candidate in candidates { - match candidate(settings, compatible_window) { + match candidate.build(settings, compatible_window) { Ok((compositor, renderer)) => { return Ok((compositor, renderer)) } @@ -162,11 +123,10 @@ impl crate::graphics::Compositor for Compositor { ), #[cfg(feature = "tiny-skia")] ( - Self::TinySkia(compositor), + Self::TinySkia(_compositor), crate::Backend::TinySkia(backend), Surface::TinySkia(surface), ) => iced_tiny_skia::window::compositor::present( - compositor, backend, surface, primitives, @@ -183,3 +143,86 @@ impl crate::graphics::Compositor for Compositor { }) } } + +enum Candidate { + Wgpu, + TinySkia, +} + +impl Candidate { + fn default_list() -> Vec { + vec![ + #[cfg(feature = "wgpu")] + Self::Wgpu, + #[cfg(feature = "tiny-skia")] + Self::TinySkia, + ] + } + + fn list_from_env() -> Option> { + let backends = env::var("ICED_BACKEND").ok()?; + + Some( + backends + .split(',') + .map(str::trim) + .map(|backend| match backend { + "wgpu" => Self::Wgpu, + "tiny-skia" => Self::TinySkia, + _ => panic!("unknown backend value: \"{backend}\""), + }) + .collect(), + ) + } + + fn build( + self, + settings: Settings, + _compatible_window: Option<&W>, + ) -> Result<(Compositor, Renderer), Error> { + match self { + #[cfg(feature = "wgpu")] + Self::Wgpu => { + let (compositor, backend) = iced_wgpu::window::compositor::new( + iced_wgpu::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: settings.antialiasing, + ..iced_wgpu::Settings::from_env() + }, + _compatible_window, + )?; + + Ok(( + Compositor::Wgpu(compositor), + Renderer::new(crate::Backend::Wgpu(backend)), + )) + } + #[cfg(feature = "tiny-skia")] + Self::TinySkia => { + let (compositor, backend) = + iced_tiny_skia::window::compositor::new( + iced_tiny_skia::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + }, + ); + + Ok(( + Compositor::TinySkia(compositor), + Renderer::new(crate::Backend::TinySkia(backend)), + )) + } + #[cfg(not(feature = "wgpu"))] + Self::Wgpu => { + panic!("`wgpu` feature was not enabled in `iced_renderer`") + } + #[cfg(not(feature = "tiny-skia"))] + Self::TinySkia => { + panic!( + "`tiny-skia` feature was not enabled in `iced_renderer`" + ); + } + } + } +} diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index f629dab934..a3bddc931c 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -11,7 +11,7 @@ geometry = ["iced_graphics/geometry"] [dependencies] raw-window-handle = "0.5" softbuffer = "0.2" -tiny-skia = "0.8" +tiny-skia = "0.9" cosmic-text = "0.8" bytemuck = "1" rustc-hash = "1.1" @@ -32,5 +32,5 @@ version = "1.6.1" features = ["std"] [dependencies.resvg] -version = "0.29" +version = "0.32" optional = true diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 58076b840f..9c69e1d21b 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -1,4 +1,3 @@ -use crate::core::alignment; use crate::core::text; use crate::core::{Background, Color, Font, Point, Rectangle, Size, Vector}; use crate::graphics::backend; @@ -37,51 +36,99 @@ impl Backend { pub fn draw>( &mut self, pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: &mut tiny_skia::ClipMask, + clip_mask: &mut tiny_skia::Mask, primitives: &[Primitive], viewport: &Viewport, + damage: &[Rectangle], background_color: Color, overlay: &[T], ) { - pixels.fill(into_color(background_color)); - + let physical_size = viewport.physical_size(); let scale_factor = viewport.scale_factor() as f32; - for primitive in primitives { - self.draw_primitive( - primitive, - pixels, - clip_mask, + if !overlay.is_empty() { + let path = tiny_skia::PathBuilder::from_rect( + tiny_skia::Rect::from_xywh( + 0.0, + 0.0, + physical_size.width as f32, + physical_size.height as f32, + ) + .expect("Create damage rectangle"), + ); + + pixels.fill_path( + &path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(into_color(Color { + a: 0.1, + ..background_color + })), + anti_alias: false, + ..Default::default() + }, + tiny_skia::FillRule::default(), + tiny_skia::Transform::identity(), None, - scale_factor, - Vector::ZERO, ); } - for (i, text) in overlay.iter().enumerate() { - const OVERLAY_TEXT_SIZE: f32 = 20.0; - - self.draw_primitive( - &Primitive::Text { - content: text.as_ref().to_owned(), - size: OVERLAY_TEXT_SIZE, - bounds: Rectangle { - x: 10.0, - y: 10.0 + i as f32 * OVERLAY_TEXT_SIZE * 1.2, - width: f32::INFINITY, - height: f32::INFINITY, - }, - color: Color::BLACK, - font: Font::MONOSPACE, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + for ®ion in damage { + let path = tiny_skia::PathBuilder::from_rect( + tiny_skia::Rect::from_xywh( + region.x, + region.y, + region.width, + region.height, + ) + .expect("Create damage rectangle"), + ); + + pixels.fill_path( + &path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(into_color( + background_color, + )), + anti_alias: false, + ..Default::default() }, - pixels, - clip_mask, + tiny_skia::FillRule::default(), + tiny_skia::Transform::identity(), None, - scale_factor, - Vector::ZERO, ); + + adjust_clip_mask(clip_mask, region); + + for primitive in primitives { + self.draw_primitive( + primitive, + pixels, + clip_mask, + region, + scale_factor, + Vector::ZERO, + ); + } + + if !overlay.is_empty() { + pixels.stroke_path( + &path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(into_color( + Color::from_rgb(1.0, 0.0, 0.0), + )), + anti_alias: false, + ..tiny_skia::Paint::default() + }, + &tiny_skia::Stroke { + width: 1.0, + ..tiny_skia::Stroke::default() + }, + tiny_skia::Transform::identity(), + None, + ); + } } self.text_pipeline.trim_cache(); @@ -97,8 +144,8 @@ impl Backend { &mut self, primitive: &Primitive, pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: &mut tiny_skia::ClipMask, - clip_bounds: Option, + clip_mask: &mut tiny_skia::Mask, + clip_bounds: Rectangle, scale_factor: f32, translation: Vector, ) { @@ -110,6 +157,15 @@ impl Backend { border_width, border_color, } => { + let physical_bounds = (*bounds + translation) * scale_factor; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + let transform = tiny_skia::Transform::from_translate( translation.x, translation.y, @@ -117,7 +173,6 @@ impl Backend { .post_scale(scale_factor, scale_factor); let path = rounded_rectangle(*bounds, *border_radius); - let clip_mask = clip_bounds.map(|_| clip_mask as &_); pixels.fill_path( &path, @@ -165,6 +220,16 @@ impl Backend { horizontal_alignment, vertical_alignment, } => { + let physical_bounds = + (primitive.bounds() + translation) * scale_factor; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + self.text_pipeline.draw( content, (*bounds + translation) * scale_factor, @@ -174,24 +239,28 @@ impl Backend { *horizontal_alignment, *vertical_alignment, pixels, - clip_bounds.map(|_| clip_mask as &_), + clip_mask, ); } #[cfg(feature = "image")] Primitive::Image { handle, bounds } => { + let physical_bounds = (*bounds + translation) * scale_factor; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + let transform = tiny_skia::Transform::from_translate( translation.x, translation.y, ) .post_scale(scale_factor, scale_factor); - self.raster_pipeline.draw( - handle, - *bounds, - pixels, - transform, - clip_bounds.map(|_| clip_mask as &_), - ); + self.raster_pipeline + .draw(handle, *bounds, pixels, transform, clip_mask); } #[cfg(feature = "svg")] Primitive::Svg { @@ -199,12 +268,21 @@ impl Backend { bounds, color, } => { + let physical_bounds = (*bounds + translation) * scale_factor; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + self.vector_pipeline.draw( handle, *color, (*bounds + translation) * scale_factor, pixels, - clip_bounds.map(|_| clip_mask as &_), + clip_mask, ); } Primitive::Fill { @@ -213,6 +291,23 @@ impl Backend { rule, transform, } => { + let bounds = path.bounds(); + + let physical_bounds = (Rectangle { + x: bounds.x(), + y: bounds.y(), + width: bounds.width(), + height: bounds.height(), + } + translation) + * scale_factor; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + pixels.fill_path( path, paint, @@ -220,7 +315,7 @@ impl Backend { transform .post_translate(translation.x, translation.y) .post_scale(scale_factor, scale_factor), - clip_bounds.map(|_| clip_mask as &_), + clip_mask, ); } Primitive::Stroke { @@ -229,6 +324,23 @@ impl Backend { stroke, transform, } => { + let bounds = path.bounds(); + + let physical_bounds = (Rectangle { + x: bounds.x(), + y: bounds.y(), + width: bounds.width(), + height: bounds.height(), + } + translation) + * scale_factor; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + pixels.stroke_path( path, paint, @@ -236,7 +348,7 @@ impl Backend { transform .post_translate(translation.x, translation.y) .post_scale(scale_factor, scale_factor), - clip_bounds.map(|_| clip_mask as &_), + clip_mask, ); } Primitive::Group { primitives } => { @@ -267,29 +379,38 @@ impl Backend { Primitive::Clip { bounds, content } => { let bounds = (*bounds + translation) * scale_factor; - if bounds.x + bounds.width <= 0.0 - || bounds.y + bounds.height <= 0.0 - || bounds.x as u32 >= pixels.width() - || bounds.y as u32 >= pixels.height() - { - return; - } - - adjust_clip_mask(clip_mask, pixels, bounds); + if bounds == clip_bounds { + self.draw_primitive( + content, + pixels, + clip_mask, + bounds, + scale_factor, + translation, + ); + } else if let Some(bounds) = clip_bounds.intersection(&bounds) { + if bounds.x + bounds.width <= 0.0 + || bounds.y + bounds.height <= 0.0 + || bounds.x as u32 >= pixels.width() + || bounds.y as u32 >= pixels.height() + || bounds.width <= 1.0 + || bounds.height <= 1.0 + { + return; + } + + adjust_clip_mask(clip_mask, bounds); - self.draw_primitive( - content, - pixels, - clip_mask, - Some(bounds), - scale_factor, - translation, - ); + self.draw_primitive( + content, + pixels, + clip_mask, + bounds, + scale_factor, + translation, + ); - if let Some(bounds) = clip_bounds { - adjust_clip_mask(clip_mask, pixels, bounds); - } else { - clip_mask.clear(); + adjust_clip_mask(clip_mask, clip_bounds); } } Primitive::Cache { content } => { @@ -462,11 +583,9 @@ fn arc_to( } } -fn adjust_clip_mask( - clip_mask: &mut tiny_skia::ClipMask, - pixels: &tiny_skia::PixmapMut<'_>, - bounds: Rectangle, -) { +fn adjust_clip_mask(clip_mask: &mut tiny_skia::Mask, bounds: Rectangle) { + clip_mask.clear(); + let path = { let mut builder = tiny_skia::PathBuilder::new(); builder.push_rect(bounds.x, bounds.y, bounds.width, bounds.height); @@ -474,15 +593,12 @@ fn adjust_clip_mask( builder.finish().unwrap() }; - clip_mask - .set_path( - pixels.width(), - pixels.height(), - &path, - tiny_skia::FillRule::EvenOdd, - true, - ) - .expect("Set path of clipping area"); + clip_mask.fill_path( + &path, + tiny_skia::FillRule::EvenOdd, + false, + tiny_skia::Transform::default(), + ); } impl iced_graphics::Backend for Backend { diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs deleted file mode 100644 index 22daaedc57..0000000000 --- a/tiny_skia/src/primitive.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::{Rectangle, Vector}; - -use std::sync::Arc; - -#[derive(Debug, Clone)] -pub enum Primitive { - /// A group of primitives - Group { - /// The primitives of the group - primitives: Vec, - }, - /// A clip primitive - Clip { - /// The bounds of the clip - bounds: Rectangle, - /// The content of the clip - content: Box, - }, - /// A primitive that applies a translation - Translate { - /// The translation vector - translation: Vector, - - /// The primitive to translate - content: Box, - }, - /// A cached primitive. - /// - /// This can be useful if you are implementing a widget where primitive - /// generation is expensive. - Cached { - /// The cached primitive - cache: Arc, - }, - /// A basic primitive. - Basic(iced_graphics::Primitive), -} - -impl iced_graphics::backend::Primitive for Primitive { - fn translate(self, translation: Vector) -> Self { - Self::Translate { - translation, - content: Box::new(self), - } - } - - fn clip(self, bounds: Rectangle) -> Self { - Self::Clip { - bounds, - content: Box::new(self), - } - } -} - -#[derive(Debug, Clone, Default)] -pub struct Recording(pub(crate) Vec); - -impl iced_graphics::backend::Recording for Recording { - type Primitive = Primitive; - - fn push(&mut self, primitive: Primitive) { - self.0.push(primitive); - } - - fn push_basic(&mut self, basic: iced_graphics::Primitive) { - self.0.push(Primitive::Basic(basic)); - } - - fn group(self) -> Self::Primitive { - Primitive::Group { primitives: self.0 } - } - - fn clear(&mut self) { - self.0.clear(); - } -} - -impl Recording { - pub fn primitives(&self) -> &[Primitive] { - &self.0 - } -} diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index 2fd73f8c7d..3887ec8dc0 100644 --- a/tiny_skia/src/raster.rs +++ b/tiny_skia/src/raster.rs @@ -31,7 +31,7 @@ impl Pipeline { bounds: Rectangle, pixels: &mut tiny_skia::PixmapMut<'_>, transform: tiny_skia::Transform, - clip_mask: Option<&tiny_skia::ClipMask>, + 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; diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index e0e893bd7a..1246bbd566 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -50,7 +50,7 @@ impl Pipeline { horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: Option<&tiny_skia::ClipMask>, + clip_mask: Option<&tiny_skia::Mask>, ) { let font_system = self.font_system.get_mut(); let key = Key { @@ -336,6 +336,7 @@ struct Cache { entries: FxHashMap, recently_used: FxHashSet, hasher: HashBuilder, + trim_count: usize, } #[cfg(not(target_arch = "wasm32"))] @@ -345,11 +346,14 @@ type HashBuilder = twox_hash::RandomXxHashBuilder64; type HashBuilder = std::hash::BuildHasherDefault; impl Cache { + const TRIM_INTERVAL: usize = 300; + fn new() -> Self { Self { entries: FxHashMap::default(), recently_used: FxHashSet::default(), hasher: HashBuilder::default(), + trim_count: 0, } } @@ -397,10 +401,16 @@ impl Cache { } fn trim(&mut self) { - self.entries - .retain(|key, _| self.recently_used.contains(key)); + if self.trim_count > Self::TRIM_INTERVAL { + self.entries + .retain(|key, _| self.recently_used.contains(key)); - self.recently_used.clear(); + self.recently_used.clear(); + + self.trim_count = 0; + } else { + self.trim_count += 1; + } } } diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs index 8509b761e6..fc411fdd8b 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -32,7 +32,7 @@ impl Pipeline { color: Option, bounds: Rectangle, pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: Option<&tiny_skia::ClipMask>, + clip_mask: Option<&tiny_skia::Mask>, ) { if let Some(image) = self.cache.borrow_mut().draw( handle, @@ -72,6 +72,8 @@ struct RasterKey { impl Cache { fn load(&mut self, handle: &Handle) -> Option<&usvg::Tree> { + use usvg::TreeParsing; + let id = handle.id(); if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) { @@ -131,9 +133,9 @@ impl Cache { resvg::render( tree, if size.width > size.height { - usvg::FitTo::Width(size.width) + resvg::FitTo::Width(size.width) } else { - usvg::FitTo::Height(size.height) + resvg::FitTo::Height(size.height) }, tiny_skia::Transform::default(), image.as_mut(), diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index cea1cabfb4..9999a18888 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -1,5 +1,6 @@ -use crate::core::Color; +use crate::core::{Color, Rectangle}; use crate::graphics::compositor::{self, Information, SurfaceError}; +use crate::graphics::damage; use crate::graphics::{Error, Primitive, Viewport}; use crate::{Backend, Renderer, Settings}; @@ -7,13 +8,15 @@ use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use std::marker::PhantomData; pub struct Compositor { - clip_mask: tiny_skia::ClipMask, _theme: PhantomData, } pub struct Surface { window: softbuffer::GraphicsContext, buffer: Vec, + clip_mask: tiny_skia::Mask, + primitives: Option>, + background_color: Color, } impl crate::graphics::Compositor for Compositor { @@ -43,6 +46,10 @@ impl crate::graphics::Compositor for Compositor { Surface { window, buffer: vec![0; width as usize * height as usize], + clip_mask: tiny_skia::Mask::new(width, height) + .expect("Create clip mask"), + primitives: None, + background_color: Color::BLACK, } } @@ -53,6 +60,9 @@ impl crate::graphics::Compositor for Compositor { height: u32, ) { surface.buffer.resize((width * height) as usize, 0); + surface.clip_mask = + tiny_skia::Mask::new(width, height).expect("Create clip mask"); + surface.primitives = None; } fn fetch_information(&self) -> Information { @@ -72,7 +82,6 @@ impl crate::graphics::Compositor for Compositor { ) -> Result<(), SurfaceError> { renderer.with_primitives(|backend, primitives| { present( - self, backend, surface, primitives, @@ -85,18 +94,15 @@ impl crate::graphics::Compositor for Compositor { } pub fn new(settings: Settings) -> (Compositor, Backend) { - // TOD ( Compositor { - clip_mask: tiny_skia::ClipMask::new(), _theme: PhantomData, }, Backend::new(settings), ) } -pub fn present>( - compositor: &mut Compositor, +pub fn present>( backend: &mut Backend, surface: &mut Surface, primitives: &[Primitive], @@ -105,17 +111,39 @@ pub fn present>( overlay: &[T], ) -> Result<(), compositor::SurfaceError> { let physical_size = viewport.physical_size(); + let scale_factor = viewport.scale_factor() as f32; + + let mut pixels = tiny_skia::PixmapMut::from_bytes( + bytemuck::cast_slice_mut(&mut surface.buffer), + physical_size.width, + physical_size.height, + ) + .expect("Create pixel map"); + + let damage = surface + .primitives + .as_deref() + .and_then(|last_primitives| { + (surface.background_color == background_color) + .then(|| damage::list(last_primitives, primitives)) + }) + .unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]); + + if damage.is_empty() { + return Ok(()); + } + + surface.primitives = Some(primitives.to_vec()); + surface.background_color = background_color; + + let damage = damage::group(damage, scale_factor, physical_size); backend.draw( - &mut tiny_skia::PixmapMut::from_bytes( - bytemuck::cast_slice_mut(&mut surface.buffer), - physical_size.width, - physical_size.height, - ) - .expect("Create pixel map"), - &mut compositor.clip_mask, + &mut pixels, + &mut surface.clip_mask, primitives, viewport, + &damage, background_color, overlay, ); diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 14dcd55074..9f9bd06651 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -23,7 +23,7 @@ once_cell = "1.0" rustc-hash = "1.1" [target.'cfg(target_arch = "wasm32")'.dependencies] -wgpu = { version = "0.14", features = ["webgl"] } +wgpu = { version = "0.15", features = ["webgl"] } [dependencies.twox-hash] version = "1.6" @@ -58,7 +58,7 @@ version = "1.0" optional = true [dependencies.resvg] -version = "0.29" +version = "0.32" optional = true [dependencies.tracing] diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index 3624e46be6..58bdf64a68 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -43,6 +43,8 @@ type ColorFilter = Option<[u8; 4]>; impl Cache { /// Load svg pub fn load(&mut self, handle: &svg::Handle) -> &Svg { + use usvg::TreeParsing; + if self.svgs.contains_key(&handle.id()) { return self.svgs.get(&handle.id()).unwrap(); } @@ -116,9 +118,9 @@ impl Cache { resvg::render( tree, if width > height { - usvg::FitTo::Width(width) + resvg::FitTo::Width(width) } else { - usvg::FitTo::Height(height) + resvg::FitTo::Height(height) }, tiny_skia::Transform::default(), img.as_mut(), diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index ff041bdff8..266a2c87fc 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -46,7 +46,7 @@ impl Settings { /// - `primary` pub fn from_env() -> Self { Settings { - internal_backend: backend_from_env() + internal_backend: wgpu::util::backend_bits_from_env() .unwrap_or(wgpu::Backends::all()), ..Self::default() } @@ -64,18 +64,3 @@ impl Default for Settings { } } } - -fn backend_from_env() -> Option { - std::env::var("WGPU_BACKEND").ok().map(|backend| { - match backend.to_lowercase().as_str() { - "vulkan" => wgpu::Backends::VULKAN, - "metal" => wgpu::Backends::METAL, - "dx12" => wgpu::Backends::DX12, - "dx11" => wgpu::Backends::DX11, - "gl" => wgpu::Backends::GL, - "webgpu" => wgpu::Backends::BROWSER_WEBGPU, - "primary" => wgpu::Backends::PRIMARY, - other => panic!("Unknown backend: {other}"), - } - }) -} diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 0df8dd023b..eb15a45801 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -2,10 +2,13 @@ mod msaa; use crate::buffer::r#static::Buffer; -use crate::core::{Gradient, Size}; +use crate::core::Size; use crate::graphics::{Antialiasing, Transformation}; use crate::layer::mesh::{self, Mesh}; +#[cfg(not(target_arch = "wasm32"))] +use crate::core::Gradient; + #[cfg(feature = "tracing")] use tracing::info_span; diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 54ab83ea7a..c55ffa4687 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -53,11 +53,12 @@ impl Compositor { let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: if settings.antialiasing.is_none() { - wgpu::PowerPreference::LowPower - } else { - wgpu::PowerPreference::HighPerformance - }, + power_preference: wgpu::util::power_preference_from_env() + .unwrap_or(if settings.antialiasing.is_none() { + wgpu::PowerPreference::LowPower + } else { + wgpu::PowerPreference::HighPerformance + }), compatible_surface: compatible_surface.as_ref(), force_fallback_adapter: false, }) diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 0faa51c0a9..9db382f73f 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -1084,6 +1084,8 @@ pub fn draw( let render = |renderer: &mut Renderer| { if let Some((cursor, color)) = cursor { renderer.fill_quad(cursor, color); + } else { + renderer.with_translation(Vector::ZERO, |_| {}); } renderer.fill_text(Text {