From 92671704b1df49e024d439500d1d88448841ad64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 27 Jan 2026 05:14:04 +0100 Subject: [PATCH 01/20] Draft `vello_cpu` renderer --- Cargo.lock | 100 +++- Cargo.toml | 12 +- renderer/Cargo.toml | 16 +- renderer/src/lib.rs | 18 +- tiny_skia/src/engine.rs | 790 -------------------------- tiny_skia/src/geometry.rs | 479 ---------------- tiny_skia/src/lib.rs | 413 -------------- tiny_skia/src/primitive.rs | 40 -- tiny_skia/src/raster.rs | 152 ----- tiny_skia/src/text.rs | 344 ----------- tiny_skia/src/vector.rs | 221 ------- tiny_skia/src/window.rs | 3 - tiny_skia/src/window/compositor.rs | 249 -------- {tiny_skia => vello_cpu}/Cargo.toml | 17 +- {tiny_skia => vello_cpu}/src/layer.rs | 99 +--- vello_cpu/src/lib.rs | 421 ++++++++++++++ 16 files changed, 527 insertions(+), 2847 deletions(-) delete mode 100644 tiny_skia/src/engine.rs delete mode 100644 tiny_skia/src/geometry.rs delete mode 100644 tiny_skia/src/lib.rs delete mode 100644 tiny_skia/src/primitive.rs delete mode 100644 tiny_skia/src/raster.rs delete mode 100644 tiny_skia/src/text.rs delete mode 100644 tiny_skia/src/vector.rs delete mode 100644 tiny_skia/src/window.rs delete mode 100644 tiny_skia/src/window/compositor.rs rename {tiny_skia => vello_cpu}/Cargo.toml (69%) rename {tiny_skia => vello_cpu}/src/layer.rs (70%) create mode 100644 vello_cpu/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 56372e1a1c..e63a380d6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -870,6 +870,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18ef4657441fb193b65f34dc39b3781f0dfec23d3bd94d0eeb4e88cde421edb" +dependencies = [ + "bytemuck", +] + [[package]] name = "color_palette" version = "0.1.0" @@ -1533,6 +1542,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fearless_simd" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb2907d1f08b2b316b9223ced5b0e89d87028ba8deae9764741dba8ff7f3903" +dependencies = [ + "bytemuck", +] + [[package]] name = "ferris" version = "0.1.0" @@ -2392,7 +2410,7 @@ name = "iced_renderer" version = "0.15.0-dev" dependencies = [ "iced_graphics", - "iced_tiny_skia", + "iced_vello_cpu", "iced_wgpu", "log", "thiserror 2.0.18", @@ -2444,19 +2462,15 @@ dependencies = [ ] [[package]] -name = "iced_tiny_skia" +name = "iced_vello_cpu" version = "0.15.0-dev" dependencies = [ "bytemuck", - "cosmic-text", "iced_debug", "iced_graphics", - "kurbo 0.10.4", "log", - "resvg", - "rustc-hash 2.1.1", "softbuffer", - "tiny-skia", + "vello_cpu", ] [[package]] @@ -2644,7 +2658,7 @@ dependencies = [ "rgb", "tiff", "zune-core 0.5.1", - "zune-jpeg 0.5.14", + "zune-jpeg 0.5.15", ] [[package]] @@ -2949,19 +2963,20 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kurbo" -version = "0.10.4" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" dependencies = [ "arrayvec", + "euclid", "smallvec", ] [[package]] name = "kurbo" -version = "0.11.3" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb" dependencies = [ "arrayvec", "euclid", @@ -4010,9 +4025,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "5.1.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +checksum = "0218004a4aae742209bee9c3cef05672f6b2708be36a50add8eb613b1f2a4008" dependencies = [ "num-traits", ] @@ -4148,6 +4163,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "peniko" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2b6aadb221872732e87d465213e9be5af2849b0e8cc5300a8ba98fffa2e00a" +dependencies = [ + "bytemuck", + "color", + "kurbo 0.13.0", + "linebender_resource_handle", + "smallvec", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -5288,9 +5316,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simd_cesu8" @@ -5820,7 +5848,7 @@ dependencies = [ "half", "quick-error", "weezl", - "zune-jpeg 0.5.14", + "zune-jpeg 0.5.15", ] [[package]] @@ -6313,9 +6341,9 @@ checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" [[package]] name = "unicode-segmentation" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da36089a805484bcccfffe0739803392c8298778a2d2f09febf76fac5ad9025b" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-vo" @@ -6414,9 +6442,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -6455,6 +6483,32 @@ dependencies = [ "iced", ] +[[package]] +name = "vello_common" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd1a4c633ce09e7d713df1a6e036644a125e15e0c169cfb5180ddf5836ca04b" +dependencies = [ + "bytemuck", + "fearless_simd", + "hashbrown 0.16.1", + "log", + "peniko", + "skrifa 0.40.0", + "smallvec", +] + +[[package]] +name = "vello_cpu" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162bfe48aabf6a9fdcd401b628c7d9f260c2cbabb343c70a65feba6f7849edc" +dependencies = [ + "bytemuck", + "hashbrown 0.16.1", + "vello_common", +] + [[package]] name = "version_check" version = "0.9.5" @@ -7920,9 +7974,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ "zune-core 0.5.1", ] diff --git a/Cargo.toml b/Cargo.toml index bf7808658b..9c0e72f3b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,13 +22,13 @@ all-features = true maintenance = { status = "actively-developed" } [features] -default = ["wgpu", "tiny-skia", "crisp", "hinting", "web-colors", "thread-pool", "linux-theme-detection", "x11", "wayland"] +default = ["wgpu", "vello-cpu", "crisp", "hinting", "web-colors", "thread-pool", "linux-theme-detection", "x11", "wayland"] # Enables the `wgpu` GPU-accelerated renderer with all its default features (Vulkan, Metal, DX12, OpenGL, and WebGPU) wgpu = ["wgpu-bare", "iced_renderer/wgpu"] # Enables the `wgpu` GPU-accelerated renderer with the minimum required features (no backends!) wgpu-bare = ["iced_renderer/wgpu-bare", "iced_widget/wgpu"] -# Enables the `tiny-skia` software renderer -tiny-skia = ["iced_renderer/tiny-skia"] +# Enables the `vello-cpu` software renderer +vello-cpu = ["iced_renderer/vello-cpu"] # Enables the `image` widget and image clipboard support image = ["image-without-codecs", "image/default", "iced_winit/image"] # Enables the `image` widget, without any built-in codecs of the `image` crate @@ -149,7 +149,7 @@ members = [ "selector", "test", "tester", - "tiny_skia", + "vello_cpu", "wgpu", "widget", "winit", @@ -182,7 +182,7 @@ iced_runtime = { version = "0.15.0-dev", path = "runtime" } iced_selector = { version = "0.15.0-dev", path = "selector" } iced_test = { version = "0.15.0-dev", path = "test" } iced_tester = { version = "0.15.0-dev", path = "tester" } -iced_tiny_skia = { version = "0.15.0-dev", path = "tiny_skia", default-features = false } +iced_vello_cpu = { version = "0.15.0-dev", path = "vello_cpu", default-features = false } iced_wgpu = { version = "0.15.0-dev", path = "wgpu", default-features = false } iced_widget = { version = "0.15.0-dev", path = "widget" } iced_winit = { version = "0.15.0-dev", path = "winit", default-features = false } @@ -226,12 +226,12 @@ smol_str = "0.2" softbuffer = { version = "0.4", default-features = false } sysinfo = "0.33" thiserror = "2" -tiny-skia = { version = "0.11", default-features = false, features = ["std", "simd"] } tokio = "1.0" tracing = "0.1" two-face = { version = "0.4", default-features = false, features = ["syntect-default-fancy"] } unicode-segmentation = "1.0" url = "2.5" +vello_cpu = { version = "0.0.6", default-features = false, features = ["std", "u8_pipeline"] } wasm-bindgen-futures = "0.4" wasmtimer = "0.4.2" web-sys = "=0.3.85" diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index f025927a54..c1e5e261ad 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -16,22 +16,22 @@ workspace = true [features] wgpu = ["iced_wgpu/default"] wgpu-bare = ["iced_wgpu"] -tiny-skia = ["iced_tiny_skia"] -image = ["iced_tiny_skia?/image", "iced_wgpu?/image"] -svg = ["iced_tiny_skia?/svg", "iced_wgpu?/svg"] -geometry = ["iced_graphics/geometry", "iced_tiny_skia?/geometry", "iced_wgpu?/geometry"] +vello-cpu = ["iced_vello_cpu"] +image = ["iced_vello_cpu?/image", "iced_wgpu?/image"] +svg = ["iced_vello_cpu?/svg", "iced_wgpu?/svg"] +geometry = ["iced_graphics/geometry", "iced_vello_cpu?/geometry", "iced_wgpu?/geometry"] web-colors = ["iced_wgpu?/web-colors"] webgl = ["iced_wgpu?/webgl"] fira-sans = ["iced_graphics/fira-sans"] strict-assertions = ["iced_wgpu?/strict-assertions"] -x11 = ["iced_tiny_skia?/x11"] -wayland = ["iced_tiny_skia?/wayland"] +x11 = ["iced_vello_cpu?/x11"] +wayland = ["iced_vello_cpu?/wayland"] [dependencies] iced_graphics.workspace = true -iced_tiny_skia.workspace = true -iced_tiny_skia.optional = true +iced_vello_cpu.workspace = true +iced_vello_cpu.optional = true iced_wgpu.workspace = true iced_wgpu.optional = true diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index d430ccb73b..deb97c6e9c 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -22,35 +22,33 @@ pub type Renderer = renderer::Renderer; /// [`iced`]: https://github.com/iced-rs/iced pub type Compositor = renderer::Compositor; -#[cfg(all(feature = "wgpu-bare", feature = "tiny-skia"))] +#[cfg(all(feature = "wgpu-bare", feature = "vello-cpu"))] mod renderer { - pub type Renderer = crate::fallback::Renderer; + pub type Renderer = crate::fallback::Renderer; - pub type Compositor = crate::fallback::Compositor< - iced_wgpu::window::Compositor, - iced_tiny_skia::window::Compositor, - >; + pub type Compositor = + crate::fallback::Compositor; } -#[cfg(all(feature = "wgpu-bare", not(feature = "tiny-skia")))] +#[cfg(all(feature = "wgpu-bare", not(feature = "vello-cpu")))] mod renderer { pub type Renderer = iced_wgpu::Renderer; pub type Compositor = iced_wgpu::window::Compositor; } -#[cfg(all(not(feature = "wgpu-bare"), feature = "tiny-skia"))] +#[cfg(all(not(feature = "wgpu-bare"), feature = "vello-cpu"))] mod renderer { pub type Renderer = iced_tiny_skia::Renderer; pub type Compositor = iced_tiny_skia::window::Compositor; } -#[cfg(not(any(feature = "wgpu-bare", feature = "tiny-skia")))] +#[cfg(not(any(feature = "wgpu-bare", feature = "vello-cpu")))] mod renderer { #[cfg(not(debug_assertions))] compile_error!( "Cannot compile `iced_renderer` in release mode \ without a renderer feature enabled. \ - Enable either the `wgpu` or `tiny-skia` feature, or both." + Enable either the `wgpu` or `vello-cpu` feature, or both." ); pub type Renderer = (); diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs deleted file mode 100644 index 0f78372903..0000000000 --- a/tiny_skia/src/engine.rs +++ /dev/null @@ -1,790 +0,0 @@ -use crate::Primitive; -use crate::core::renderer::Quad; -use crate::core::{Background, Color, Gradient, Rectangle, Size, Transformation, Vector}; -use crate::graphics::{Image, Text}; -use crate::text; - -#[derive(Debug)] -pub struct Engine { - text_pipeline: text::Pipeline, - - #[cfg(feature = "image")] - pub(crate) raster_pipeline: crate::raster::Pipeline, - #[cfg(feature = "svg")] - pub(crate) vector_pipeline: crate::vector::Pipeline, -} - -impl Engine { - pub fn new() -> Self { - Self { - text_pipeline: text::Pipeline::new(), - #[cfg(feature = "image")] - raster_pipeline: crate::raster::Pipeline::new(), - #[cfg(feature = "svg")] - vector_pipeline: crate::vector::Pipeline::new(), - } - } - - pub fn draw_quad( - &mut self, - quad: &Quad, - background: &Background, - transformation: Transformation, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: &mut tiny_skia::Mask, - clip_bounds: Rectangle, - ) { - let physical_bounds = quad.bounds * transformation; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let transform = into_transform(transformation); - - // Make sure the border radius is not larger than the bounds - let border_width = quad - .border - .width - .min(quad.bounds.width / 2.0) - .min(quad.bounds.height / 2.0); - - let mut fill_border_radius = <[f32; 4]>::from(quad.border.radius); - - for radius in &mut fill_border_radius { - *radius = (*radius) - .min(quad.bounds.width / 2.0) - .min(quad.bounds.height / 2.0); - } - - let path = rounded_rectangle(quad.bounds, fill_border_radius); - - let shadow = quad.shadow; - - if shadow.color.a > 0.0 { - let shadow_bounds = Rectangle { - x: quad.bounds.x + shadow.offset.x - shadow.blur_radius, - y: quad.bounds.y + shadow.offset.y - shadow.blur_radius, - width: quad.bounds.width + shadow.blur_radius * 2.0, - height: quad.bounds.height + shadow.blur_radius * 2.0, - } * transformation; - - let radii = fill_border_radius - .into_iter() - .map(|radius| radius * transformation.scale_factor()) - .collect::>(); - let (x, y, width, height) = ( - shadow_bounds.x as u32, - shadow_bounds.y as u32, - shadow_bounds.width as u32, - shadow_bounds.height as u32, - ); - let half_width = physical_bounds.width / 2.0; - let half_height = physical_bounds.height / 2.0; - - let colors = (y..y + height) - .flat_map(|y| (x..x + width).map(move |x| (x as f32, y as f32))) - .filter_map(|(x, y)| { - tiny_skia::Size::from_wh(half_width, half_height).map(|size| { - let shadow_distance = rounded_box_sdf( - Vector::new( - x - physical_bounds.position().x - - (shadow.offset.x * transformation.scale_factor()) - - half_width, - y - physical_bounds.position().y - - (shadow.offset.y * transformation.scale_factor()) - - half_height, - ), - size, - &radii, - ) - .max(0.0); - let shadow_alpha = 1.0 - - smoothstep( - -shadow.blur_radius * transformation.scale_factor(), - shadow.blur_radius * transformation.scale_factor(), - shadow_distance, - ); - - let mut color = into_color(shadow.color); - color.apply_opacity(shadow_alpha); - - color.to_color_u8().premultiply() - }) - }) - .collect(); - - if let Some(pixmap) = tiny_skia::IntSize::from_wh(width, height) - .and_then(|size| tiny_skia::Pixmap::from_vec(bytemuck::cast_vec(colors), size)) - { - pixels.draw_pixmap( - x as i32, - y as i32, - pixmap.as_ref(), - &tiny_skia::PixmapPaint::default(), - tiny_skia::Transform::default(), - Some(clip_mask), - ); - } - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)).then_some(clip_mask as &_); - - pixels.fill_path( - &path, - &tiny_skia::Paint { - shader: match background { - Background::Color(color) => tiny_skia::Shader::SolidColor(into_color(*color)), - Background::Gradient(Gradient::Linear(linear)) => { - let (start, end) = linear.angle.to_distance(&quad.bounds); - - let stops: Vec = linear - .stops - .into_iter() - .flatten() - .map(|stop| { - tiny_skia::GradientStop::new( - stop.offset, - tiny_skia::Color::from_rgba( - stop.color.b, - stop.color.g, - stop.color.r, - stop.color.a, - ) - .expect("Create color"), - ) - }) - .collect(); - - tiny_skia::LinearGradient::new( - tiny_skia::Point { - x: start.x, - y: start.y, - }, - tiny_skia::Point { x: end.x, y: end.y }, - if stops.is_empty() { - vec![tiny_skia::GradientStop::new(0.0, tiny_skia::Color::BLACK)] - } else { - stops - }, - tiny_skia::SpreadMode::Pad, - tiny_skia::Transform::identity(), - ) - .expect("Create linear gradient") - } - }, - anti_alias: true, - ..tiny_skia::Paint::default() - }, - tiny_skia::FillRule::EvenOdd, - transform, - clip_mask, - ); - - if border_width > 0.0 { - // Border path is offset by half the border width - let border_bounds = Rectangle { - x: quad.bounds.x + border_width / 2.0, - y: quad.bounds.y + border_width / 2.0, - width: quad.bounds.width - border_width, - height: quad.bounds.height - border_width, - }; - - // Make sure the border radius is correct - let mut border_radius = <[f32; 4]>::from(quad.border.radius); - let mut is_simple_border = true; - - for radius in &mut border_radius { - *radius = if *radius == 0.0 { - // Path should handle this fine - 0.0 - } else if *radius > border_width / 2.0 { - *radius - border_width / 2.0 - } else { - is_simple_border = false; - 0.0 - } - .min(border_bounds.width / 2.0) - .min(border_bounds.height / 2.0); - } - - // Stroking a path works well in this case - if is_simple_border { - let border_path = rounded_rectangle(border_bounds, border_radius); - - pixels.stroke_path( - &border_path, - &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor(into_color(quad.border.color)), - anti_alias: true, - ..tiny_skia::Paint::default() - }, - &tiny_skia::Stroke { - width: border_width, - ..tiny_skia::Stroke::default() - }, - transform, - clip_mask, - ); - } else { - // Draw corners that have too small border radii as having no border radius, - // but mask them with the rounded rectangle with the correct border radius. - let mut temp_pixmap = - tiny_skia::Pixmap::new(quad.bounds.width as u32, quad.bounds.height as u32) - .unwrap(); - - let mut quad_mask = - tiny_skia::Mask::new(quad.bounds.width as u32, quad.bounds.height as u32) - .unwrap(); - - let zero_bounds = Rectangle { - x: 0.0, - y: 0.0, - width: quad.bounds.width, - height: quad.bounds.height, - }; - let path = rounded_rectangle(zero_bounds, fill_border_radius); - - quad_mask.fill_path(&path, tiny_skia::FillRule::EvenOdd, true, transform); - let path_bounds = Rectangle { - x: border_width / 2.0, - y: border_width / 2.0, - width: quad.bounds.width - border_width, - height: quad.bounds.height - border_width, - }; - - let border_radius_path = rounded_rectangle(path_bounds, border_radius); - - temp_pixmap.stroke_path( - &border_radius_path, - &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor(into_color(quad.border.color)), - anti_alias: true, - ..tiny_skia::Paint::default() - }, - &tiny_skia::Stroke { - width: border_width, - ..tiny_skia::Stroke::default() - }, - transform, - Some(&quad_mask), - ); - - pixels.draw_pixmap( - quad.bounds.x as i32, - quad.bounds.y as i32, - temp_pixmap.as_ref(), - &tiny_skia::PixmapPaint::default(), - transform, - clip_mask, - ); - } - } - } - - pub fn draw_text( - &mut self, - text: &Text, - transformation: Transformation, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: &mut tiny_skia::Mask, - clip_bounds: Rectangle, - ) { - match text { - Text::Paragraph { - paragraph, - position, - color, - clip_bounds: local_clip_bounds, - transformation: local_transformation, - } => { - let transformation = transformation * *local_transformation; - let Some(clip_bounds) = - clip_bounds.intersection(&(*local_clip_bounds * transformation)) - else { - return; - }; - - let physical_bounds = - Rectangle::new(*position, paragraph.min_bounds) * transformation; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = match physical_bounds.is_within(&clip_bounds) { - true => None, - false => { - adjust_clip_mask(clip_mask, clip_bounds); - Some(clip_mask as &_) - } - }; - - self.text_pipeline.draw_paragraph( - paragraph, - *position, - *color, - pixels, - clip_mask, - transformation, - ); - } - Text::Editor { - editor, - position, - color, - clip_bounds: local_clip_bounds, - transformation: local_transformation, - } => { - let transformation = transformation * *local_transformation; - - let Some(clip_bounds) = - clip_bounds.intersection(&(*local_clip_bounds * transformation)) - else { - return; - }; - - adjust_clip_mask(clip_mask, clip_bounds); - - self.text_pipeline.draw_editor( - editor, - *position, - *color, - pixels, - Some(clip_mask), - transformation, - ); - } - Text::Cached { - content, - bounds, - color, - size, - line_height, - font, - align_x, - align_y, - shaping, - wrapping, - ellipsis, - clip_bounds: local_clip_bounds, - } => { - let physical_bounds = *local_clip_bounds * transformation; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = match physical_bounds.is_within(&clip_bounds) { - true => None, - false => { - adjust_clip_mask(clip_mask, clip_bounds); - Some(clip_mask as &_) - } - }; - - self.text_pipeline.draw_cached( - content, - *bounds, - *color, - *size, - *line_height, - *font, - *align_x, - *align_y, - *shaping, - *wrapping, - *ellipsis, - pixels, - clip_mask, - transformation, - ); - } - Text::Raw { - raw, - transformation: local_transformation, - } => { - let Some(buffer) = raw.buffer.upgrade() else { - return; - }; - - let transformation = transformation * *local_transformation; - let (width, height) = buffer.size(); - - let physical_bounds = Rectangle::new( - raw.position, - Size::new( - width.unwrap_or(clip_bounds.width), - height.unwrap_or(clip_bounds.height), - ), - ) * transformation; - - 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_raw( - &buffer, - raw.position, - raw.color, - pixels, - clip_mask, - transformation, - ); - } - } - } - - pub fn draw_primitive( - &mut self, - primitive: &Primitive, - transformation: Transformation, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: &mut tiny_skia::Mask, - clip_bounds: Rectangle, - ) { - match primitive { - Primitive::Fill { path, paint, rule } => { - let physical_bounds = { - let bounds = path.bounds(); - - Rectangle { - x: bounds.x(), - y: bounds.y(), - width: bounds.width(), - height: bounds.height(), - } * transformation - }; - - 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, - *rule, - into_transform(transformation), - clip_mask, - ); - } - Primitive::Stroke { - path, - paint, - stroke, - } => { - let physical_bounds = { - let bounds = path.bounds(); - - Rectangle { - x: bounds.x() - stroke.width / 2.0, - y: bounds.y() - stroke.width / 2.0, - width: bounds.width() + stroke.width, - height: bounds.height() + stroke.width, - } * transformation - }; - - 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, - stroke, - into_transform(transformation), - clip_mask, - ); - } - } - } - - pub fn draw_image( - &mut self, - image: &Image, - _transformation: Transformation, - _pixels: &mut tiny_skia::PixmapMut<'_>, - _clip_mask: &mut tiny_skia::Mask, - _clip_bounds: Rectangle, - ) { - match image { - #[cfg(feature = "image")] - Image::Raster { - image, - bounds, - clip_bounds: local_clip_bounds, - } => { - let physical_bounds = *local_clip_bounds * _transformation; - - let Some(clip_bounds) = physical_bounds.intersection(&_clip_bounds) else { - return; - }; - - // TODO: Border radius - adjust_clip_mask(_clip_mask, clip_bounds); - - let center = physical_bounds.center(); - let radians = f32::from(image.rotation); - - let transform = into_transform(_transformation).post_rotate_at( - radians.to_degrees(), - center.x, - center.y, - ); - - self.raster_pipeline.draw( - &image.handle, - image.filter_method, - *bounds, - image.opacity, - _pixels, - transform, - Some(_clip_mask), - ); - } - #[cfg(feature = "svg")] - Image::Vector { svg, bounds, .. } => { - let physical_bounds = *bounds * _transformation; - - if !_clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = - (!physical_bounds.is_within(&_clip_bounds)).then_some(_clip_mask as &_); - - let center = physical_bounds.center(); - let radians = f32::from(svg.rotation); - - let transform = into_transform(_transformation).post_rotate_at( - radians.to_degrees(), - center.x, - center.y, - ); - - self.vector_pipeline.draw( - &svg.handle, - svg.color, - *bounds, - svg.opacity, - _pixels, - transform, - clip_mask, - ); - } - #[cfg(not(feature = "image"))] - Image::Raster { .. } => { - log::warn!("Unsupported primitive in `iced_tiny_skia`: {image:?}",); - } - #[cfg(not(feature = "svg"))] - Image::Vector { .. } => { - log::warn!("Unsupported primitive in `iced_tiny_skia`: {image:?}",); - } - } - } - - pub fn trim(&mut self) { - self.text_pipeline.trim_cache(); - - #[cfg(feature = "image")] - self.raster_pipeline.trim_cache(); - - #[cfg(feature = "svg")] - self.vector_pipeline.trim_cache(); - } -} - -pub fn into_color(color: Color) -> tiny_skia::Color { - tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) - .expect("Convert color from iced to tiny_skia") -} - -fn into_transform(transformation: Transformation) -> tiny_skia::Transform { - let translation = transformation.translation(); - - tiny_skia::Transform { - sx: transformation.scale_factor(), - kx: 0.0, - ky: 0.0, - sy: transformation.scale_factor(), - tx: translation.x, - ty: translation.y, - } -} - -fn rounded_rectangle(bounds: Rectangle, border_radius: [f32; 4]) -> tiny_skia::Path { - let [top_left, top_right, bottom_right, bottom_left] = border_radius; - - if top_left == 0.0 && top_right == 0.0 && bottom_right == 0.0 && bottom_left == 0.0 { - return tiny_skia::PathBuilder::from_rect( - tiny_skia::Rect::from_xywh(bounds.x, bounds.y, bounds.width, bounds.height) - .expect("Build quad rectangle"), - ); - } - - if top_left == top_right - && top_left == bottom_right - && top_left == bottom_left - && top_left == bounds.width / 2.0 - && top_left == bounds.height / 2.0 - { - return tiny_skia::PathBuilder::from_circle( - bounds.x + bounds.width / 2.0, - bounds.y + bounds.height / 2.0, - top_left, - ) - .expect("Build circle path"); - } - - let mut builder = tiny_skia::PathBuilder::new(); - - builder.move_to(bounds.x + top_left, bounds.y); - builder.line_to(bounds.x + bounds.width - top_right, bounds.y); - - if top_right > 0.0 { - arc_to( - &mut builder, - bounds.x + bounds.width - top_right, - bounds.y, - bounds.x + bounds.width, - bounds.y + top_right, - top_right, - ); - } - - maybe_line_to( - &mut builder, - bounds.x + bounds.width, - bounds.y + bounds.height - bottom_right, - ); - - if bottom_right > 0.0 { - arc_to( - &mut builder, - bounds.x + bounds.width, - bounds.y + bounds.height - bottom_right, - bounds.x + bounds.width - bottom_right, - bounds.y + bounds.height, - bottom_right, - ); - } - - maybe_line_to( - &mut builder, - bounds.x + bottom_left, - bounds.y + bounds.height, - ); - - if bottom_left > 0.0 { - arc_to( - &mut builder, - bounds.x + bottom_left, - bounds.y + bounds.height, - bounds.x, - bounds.y + bounds.height - bottom_left, - bottom_left, - ); - } - - maybe_line_to(&mut builder, bounds.x, bounds.y + top_left); - - if top_left > 0.0 { - arc_to( - &mut builder, - bounds.x, - bounds.y + top_left, - bounds.x + top_left, - bounds.y, - top_left, - ); - } - - builder.finish().expect("Build rounded rectangle path") -} - -fn maybe_line_to(path: &mut tiny_skia::PathBuilder, x: f32, y: f32) { - if path.last_point() != Some(tiny_skia::Point { x, y }) { - path.line_to(x, y); - } -} - -fn arc_to( - path: &mut tiny_skia::PathBuilder, - x_from: f32, - y_from: f32, - x_to: f32, - y_to: f32, - radius: f32, -) { - let svg_arc = kurbo::SvgArc { - from: kurbo::Point::new(f64::from(x_from), f64::from(y_from)), - to: kurbo::Point::new(f64::from(x_to), f64::from(y_to)), - radii: kurbo::Vec2::new(f64::from(radius), f64::from(radius)), - x_rotation: 0.0, - large_arc: false, - sweep: true, - }; - - match kurbo::Arc::from_svg_arc(&svg_arc) { - Some(arc) => { - arc.to_cubic_beziers(0.1, |p1, p2, p| { - path.cubic_to( - p1.x as f32, - p1.y as f32, - p2.x as f32, - p2.y as f32, - p.x as f32, - p.y as f32, - ); - }); - } - None => { - path.line_to(x_to, y_to); - } - } -} - -fn smoothstep(a: f32, b: f32, x: f32) -> f32 { - let x = ((x - a) / (b - a)).clamp(0.0, 1.0); - - x * x * (3.0 - 2.0 * x) -} - -fn rounded_box_sdf(to_center: Vector, size: tiny_skia::Size, radii: &[f32]) -> f32 { - let radius = match (to_center.x > 0.0, to_center.y > 0.0) { - (true, true) => radii[2], - (true, false) => radii[1], - (false, true) => radii[3], - (false, false) => radii[0], - }; - - let x = (to_center.x.abs() - size.width() + radius).max(0.0); - let y = (to_center.y.abs() - size.height() + radius).max(0.0); - - (x.powf(2.0) + y.powf(2.0)).sqrt() - radius -} - -pub fn adjust_clip_mask(clip_mask: &mut tiny_skia::Mask, bounds: Rectangle) { - clip_mask.clear(); - - let path = tiny_skia::PathBuilder::from_rect( - tiny_skia::Rect::from_xywh(bounds.x, bounds.y, bounds.width, bounds.height) - .expect("Create clip rectangle"), - ); - - clip_mask.fill_path( - &path, - tiny_skia::FillRule::EvenOdd, - false, - tiny_skia::Transform::default(), - ); -} diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs deleted file mode 100644 index cc9913ef1e..0000000000 --- a/tiny_skia/src/geometry.rs +++ /dev/null @@ -1,479 +0,0 @@ -use crate::Primitive; -use crate::core::text::LineHeight; -use crate::core::{self, Pixels, Point, Radians, Rectangle, Size, Svg, 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::{self, Gradient, Image, Text}; - -use std::sync::Arc; - -#[derive(Debug)] -pub enum Geometry { - Live { - text: Vec, - images: Vec, - primitives: Vec, - clip_bounds: Rectangle, - }, - Cache(Cache), -} - -#[derive(Debug, Clone)] -pub struct Cache { - pub text: Arc<[Text]>, - pub images: Arc<[graphics::Image]>, - pub primitives: Arc<[Primitive]>, - pub clip_bounds: Rectangle, -} - -impl Cached for Geometry { - type Cache = Cache; - - fn load(cache: &Cache) -> Self { - Self::Cache(cache.clone()) - } - - fn cache(self, _group: cache::Group, _previous: Option) -> Cache { - match self { - Self::Live { - primitives, - images, - text, - clip_bounds, - } => Cache { - primitives: Arc::from(primitives), - images: Arc::from(images), - text: Arc::from(text), - clip_bounds, - }, - Self::Cache(cache) => cache, - } - } -} - -#[derive(Debug)] -pub struct Frame { - clip_bounds: Rectangle, - transform: tiny_skia::Transform, - stack: Vec, - primitives: Vec, - images: Vec, - text: Vec, -} - -impl Frame { - pub fn new(bounds: Rectangle) -> Self { - Self { - clip_bounds: bounds, - stack: Vec::new(), - primitives: Vec::new(), - images: Vec::new(), - text: Vec::new(), - transform: tiny_skia::Transform::identity(), - } - } -} - -impl geometry::frame::Backend for Frame { - type Geometry = Geometry; - - fn width(&self) -> f32 { - self.clip_bounds.width - } - - fn height(&self) -> f32 { - self.clip_bounds.height - } - - fn size(&self) -> Size { - self.clip_bounds.size() - } - - fn center(&self) -> Point { - Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.height / 2.0) - } - - fn fill(&mut self, path: &Path, fill: impl Into) { - let Some(path) = convert_path(path).and_then(|path| path.transform(self.transform)) else { - return; - }; - - let fill = fill.into(); - - let mut paint = into_paint(fill.style); - paint.shader.transform(self.transform); - - self.primitives.push(Primitive::Fill { - path, - paint, - rule: into_fill_rule(fill.rule), - }); - } - - fn fill_rectangle(&mut self, top_left: Point, size: Size, fill: impl Into) { - let Some(path) = convert_path(&Path::rectangle(top_left, size)) - .and_then(|path| path.transform(self.transform)) - else { - return; - }; - - let fill = fill.into(); - - let mut paint = tiny_skia::Paint { - anti_alias: false, - ..into_paint(fill.style) - }; - paint.shader.transform(self.transform); - - self.primitives.push(Primitive::Fill { - path, - paint, - rule: into_fill_rule(fill.rule), - }); - } - - fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { - let Some(path) = convert_path(path).and_then(|path| path.transform(self.transform)) else { - return; - }; - - let stroke = stroke.into(); - let skia_stroke = into_stroke(&stroke); - - let mut paint = into_paint(stroke.style); - paint.shader.transform(self.transform); - - self.primitives.push(Primitive::Stroke { - path, - paint, - stroke: skia_stroke, - }); - } - - fn stroke_rectangle<'a>(&mut self, top_left: Point, size: Size, stroke: impl Into>) { - self.stroke(&Path::rectangle(top_left, size), stroke); - } - - fn fill_text(&mut self, text: impl Into) { - let text = text.into(); - - let (scale_x, scale_y) = self.transform.get_scale(); - - if !self.transform.has_skew() && scale_x == scale_y && scale_x > 0.0 && scale_y > 0.0 { - let (bounds, size, line_height) = if self.transform.is_identity() { - ( - Rectangle::new(text.position, Size::new(text.max_width, f32::INFINITY)), - text.size, - text.line_height, - ) - } else { - let mut position = [tiny_skia::Point { - x: text.position.x, - y: text.position.y, - }]; - - self.transform.map_points(&mut position); - - let size = text.size.0 * scale_y; - - let line_height = match text.line_height { - LineHeight::Absolute(size) => LineHeight::Absolute(Pixels(size.0 * scale_y)), - LineHeight::Relative(factor) => LineHeight::Relative(factor), - }; - - ( - Rectangle { - x: position[0].x, - y: position[0].y, - width: text.max_width * scale_x, - height: f32::INFINITY, - }, - size.into(), - line_height, - ) - }; - - // TODO: Honor layering! - self.text.push(Text::Cached { - content: text.content, - bounds, - color: text.color, - size, - line_height: line_height.to_absolute(size), - font: text.font, - align_x: text.align_x, - align_y: text.align_y, - shaping: text.shaping, - wrapping: text.wrapping, - ellipsis: text.ellipsis, - clip_bounds: Rectangle::with_size(Size::INFINITE), - }); - } else { - text.draw_with(|path, color| self.fill(&path, color)); - } - } - - fn stroke_text<'a>(&mut self, text: impl Into, stroke: impl Into>) { - let text = text.into(); - let stroke = stroke.into(); - - text.draw_with(|path, _color| self.stroke(&path, stroke)); - } - - fn push_transform(&mut self) { - self.stack.push(self.transform); - } - - fn pop_transform(&mut self) { - self.transform = self.stack.pop().expect("Pop transform"); - } - - fn draft(&mut self, clip_bounds: Rectangle) -> Self { - Self::new(clip_bounds) - } - - fn paste(&mut self, frame: Self) { - self.primitives.extend(frame.primitives); - self.text.extend(frame.text); - self.images.extend(frame.images); - } - - fn translate(&mut self, translation: Vector) { - self.transform = self.transform.pre_translate(translation.x, translation.y); - } - - fn rotate(&mut self, angle: impl Into) { - self.transform = self.transform.pre_concat(tiny_skia::Transform::from_rotate( - angle.into().0.to_degrees(), - )); - } - - fn scale(&mut self, scale: impl Into) { - let scale = scale.into(); - - self.scale_nonuniform(Vector { x: scale, y: scale }); - } - - fn scale_nonuniform(&mut self, scale: impl Into) { - let scale = scale.into(); - - self.transform = self.transform.pre_scale(scale.x, scale.y); - } - - fn into_geometry(self) -> Geometry { - Geometry::Live { - primitives: self.primitives, - images: self.images, - text: self.text, - clip_bounds: self.clip_bounds, - } - } - - fn draw_image(&mut self, bounds: Rectangle, image: impl Into) { - let mut image = image.into(); - - let (bounds, external_rotation) = transform_rectangle(bounds, self.transform); - - image.rotation += external_rotation; - - self.images.push(graphics::Image::Raster { - image, - bounds, - clip_bounds: self.clip_bounds, - }); - } - - fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into) { - let mut svg = svg.into(); - - let (bounds, external_rotation) = transform_rectangle(bounds, self.transform); - - svg.rotation += external_rotation; - - self.images.push(Image::Vector { - svg, - bounds, - clip_bounds: self.clip_bounds, - }); - } -} - -fn transform_rectangle( - rectangle: Rectangle, - transform: tiny_skia::Transform, -) -> (Rectangle, Radians) { - let mut top_left = tiny_skia::Point { - x: rectangle.x, - y: rectangle.y, - }; - - let mut top_right = tiny_skia::Point { - x: rectangle.x + rectangle.width, - y: rectangle.y, - }; - - let mut bottom_left = tiny_skia::Point { - x: rectangle.x, - y: rectangle.y + rectangle.height, - }; - - transform.map_point(&mut top_left); - transform.map_point(&mut top_right); - transform.map_point(&mut bottom_left); - - Rectangle::with_vertices( - Point::new(top_left.x, top_left.y), - Point::new(top_right.x, top_right.y), - Point::new(bottom_left.x, bottom_left.y), - ) -} - -fn convert_path(path: &Path) -> Option { - use iced_graphics::geometry::path::lyon_path; - - let mut builder = tiny_skia::PathBuilder::new(); - let mut last_point = lyon_path::math::Point::default(); - - for event in path.raw() { - match event { - lyon_path::Event::Begin { at } => { - builder.move_to(at.x, at.y); - - last_point = at; - } - lyon_path::Event::Line { from, to } => { - if last_point != from { - builder.move_to(from.x, from.y); - } - - builder.line_to(to.x, to.y); - - last_point = to; - } - lyon_path::Event::Quadratic { from, ctrl, to } => { - if last_point != from { - builder.move_to(from.x, from.y); - } - - builder.quad_to(ctrl.x, ctrl.y, to.x, to.y); - - last_point = to; - } - lyon_path::Event::Cubic { - from, - ctrl1, - ctrl2, - to, - } => { - if last_point != from { - builder.move_to(from.x, from.y); - } - - builder.cubic_to(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y); - - last_point = to; - } - lyon_path::Event::End { close, .. } => { - if close { - builder.close(); - } - } - } - } - - let result = builder.finish(); - - #[cfg(debug_assertions)] - if result.is_none() { - log::warn!("Invalid path: {:?}", path.raw()); - } - - result -} - -pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { - tiny_skia::Paint { - shader: match style { - Style::Solid(color) => tiny_skia::Shader::SolidColor( - tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) - .expect("Create color"), - ), - Style::Gradient(gradient) => match gradient { - Gradient::Linear(linear) => { - let stops: Vec = linear - .stops - .into_iter() - .flatten() - .map(|stop| { - tiny_skia::GradientStop::new( - stop.offset, - tiny_skia::Color::from_rgba( - stop.color.b, - stop.color.g, - stop.color.r, - stop.color.a, - ) - .expect("Create color"), - ) - }) - .collect(); - - tiny_skia::LinearGradient::new( - tiny_skia::Point { - x: linear.start.x, - y: linear.start.y, - }, - tiny_skia::Point { - x: linear.end.x, - y: linear.end.y, - }, - if stops.is_empty() { - vec![tiny_skia::GradientStop::new(0.0, tiny_skia::Color::BLACK)] - } else { - stops - }, - tiny_skia::SpreadMode::Pad, - tiny_skia::Transform::identity(), - ) - .expect("Create linear gradient") - } - }, - }, - anti_alias: true, - ..Default::default() - } -} - -pub fn into_fill_rule(rule: fill::Rule) -> tiny_skia::FillRule { - match rule { - fill::Rule::EvenOdd => tiny_skia::FillRule::EvenOdd, - fill::Rule::NonZero => tiny_skia::FillRule::Winding, - } -} - -pub fn into_stroke(stroke: &Stroke<'_>) -> tiny_skia::Stroke { - tiny_skia::Stroke { - width: stroke.width, - line_cap: match stroke.line_cap { - stroke::LineCap::Butt => tiny_skia::LineCap::Butt, - stroke::LineCap::Square => tiny_skia::LineCap::Square, - stroke::LineCap::Round => tiny_skia::LineCap::Round, - }, - line_join: match stroke.line_join { - stroke::LineJoin::Miter => tiny_skia::LineJoin::Miter, - stroke::LineJoin::Round => tiny_skia::LineJoin::Round, - stroke::LineJoin::Bevel => tiny_skia::LineJoin::Bevel, - }, - dash: if stroke.line_dash.segments.is_empty() { - None - } else { - tiny_skia::StrokeDash::new( - stroke.line_dash.segments.into(), - stroke.line_dash.offset as f32, - ) - }, - ..Default::default() - } -} diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs deleted file mode 100644 index 0f20bbbc1d..0000000000 --- a/tiny_skia/src/lib.rs +++ /dev/null @@ -1,413 +0,0 @@ -#![allow(missing_docs)] -#![cfg_attr(docsrs, feature(doc_cfg))] -pub mod window; - -mod engine; -mod layer; -mod primitive; -mod text; - -#[cfg(feature = "image")] -mod raster; - -#[cfg(feature = "svg")] -mod vector; - -#[cfg(feature = "geometry")] -pub mod geometry; - -use iced_debug as debug; -pub use iced_graphics as graphics; -pub use iced_graphics::core; - -pub use layer::Layer; -pub use primitive::Primitive; - -#[cfg(feature = "geometry")] -pub use geometry::Geometry; - -use crate::core::renderer; -use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation}; -use crate::engine::Engine; -use crate::graphics::Viewport; -use crate::graphics::compositor; -use crate::graphics::text::{Editor, Paragraph}; - -/// A [`tiny-skia`] graphics renderer for [`iced`]. -/// -/// [`tiny-skia`]: https://github.com/RazrFalcon/tiny-skia -/// [`iced`]: https://github.com/iced-rs/iced -#[derive(Debug)] -pub struct Renderer { - settings: renderer::Settings, - layers: layer::Stack, - engine: Engine, // TODO: Shared engine -} - -impl Renderer { - pub fn new(settings: renderer::Settings) -> Self { - Self { - settings, - layers: layer::Stack::new(), - engine: Engine::new(), - } - } - - pub fn layers(&mut self) -> &[Layer] { - self.layers.flush(); - self.layers.as_slice() - } - - pub fn draw( - &mut self, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: &mut tiny_skia::Mask, - viewport: &Viewport, - damage: &[Rectangle], - background_color: Color, - ) { - let scale_factor = viewport.scale_factor(); - self.layers.flush(); - - for &damage_bounds in damage { - let damage_bounds = damage_bounds * scale_factor; - - let path = tiny_skia::PathBuilder::from_rect( - tiny_skia::Rect::from_xywh( - damage_bounds.x, - damage_bounds.y, - damage_bounds.width, - damage_bounds.height, - ) - .expect("Create damage rectangle"), - ); - - pixels.fill_path( - &path, - &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor(engine::into_color(background_color)), - anti_alias: false, - blend_mode: tiny_skia::BlendMode::Source, - ..Default::default() - }, - tiny_skia::FillRule::default(), - tiny_skia::Transform::identity(), - None, - ); - - for layer in self.layers.iter() { - let Some(layer_bounds) = damage_bounds.intersection(&(layer.bounds * scale_factor)) - else { - continue; - }; - - engine::adjust_clip_mask(clip_mask, layer_bounds); - - if !layer.quads.is_empty() { - let render_span = debug::render(debug::Primitive::Quad); - for (quad, background) in &layer.quads { - self.engine.draw_quad( - quad, - background, - Transformation::scale(scale_factor), - pixels, - clip_mask, - layer_bounds, - ); - } - render_span.finish(); - } - - if !layer.primitives.is_empty() { - let render_span = debug::render(debug::Primitive::Triangle); - - for group in &layer.primitives { - let Some(group_bounds) = - (group.clip_bounds() * scale_factor).intersection(&layer_bounds) - else { - continue; - }; - - engine::adjust_clip_mask(clip_mask, group_bounds); - - for primitive in group.as_slice() { - self.engine.draw_primitive( - primitive, - Transformation::scale(scale_factor) * group.transformation(), - pixels, - clip_mask, - group_bounds, - ); - } - - engine::adjust_clip_mask(clip_mask, layer_bounds); - } - - render_span.finish(); - } - - if !layer.images.is_empty() { - let render_span = debug::render(debug::Primitive::Image); - - for image in &layer.images { - self.engine.draw_image( - image, - Transformation::scale(scale_factor), - pixels, - clip_mask, - layer_bounds, - ); - } - - render_span.finish(); - } - - if !layer.text.is_empty() { - let render_span = debug::render(debug::Primitive::Image); - - for group in &layer.text { - for text in group.as_slice() { - self.engine.draw_text( - text, - Transformation::scale(scale_factor) * group.transformation(), - pixels, - clip_mask, - layer_bounds, - ); - } - } - - render_span.finish(); - } - } - } - - self.engine.trim(); - } -} - -impl core::Renderer for Renderer { - fn start_layer(&mut self, bounds: Rectangle) { - self.layers.push_clip(bounds); - } - - fn end_layer(&mut self) { - self.layers.pop_clip(); - } - - fn start_transformation(&mut self, transformation: Transformation) { - self.layers.push_transformation(transformation); - } - - fn end_transformation(&mut self) { - self.layers.pop_transformation(); - } - - fn fill_quad(&mut self, quad: renderer::Quad, background: impl Into) { - let (layer, transformation) = self.layers.current_mut(); - layer.draw_quad(quad, background.into(), transformation); - } - - 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)); - } - - fn hint(&mut self, _scale_factor: f32) { - // TODO: No hinting supported - // We'll replace `tiny-skia` with `vello_cpu` soon - } - - fn scale_factor(&self) -> Option { - None - } - - fn reset(&mut self, new_bounds: Rectangle) { - self.layers.reset(new_bounds); - } -} - -impl core::text::Renderer for Renderer { - type Font = Font; - type Paragraph = Paragraph; - type Editor = Editor; - - const ICON_FONT: Font = Font::new("Iced-Icons"); - const CHECKMARK_ICON: char = '\u{f00c}'; - const ARROW_DOWN_ICON: char = '\u{e800}'; - const ICED_LOGO: char = '\u{e801}'; - const SCROLL_UP_ICON: char = '\u{e802}'; - const SCROLL_DOWN_ICON: char = '\u{e803}'; - const SCROLL_LEFT_ICON: char = '\u{e804}'; - const SCROLL_RIGHT_ICON: char = '\u{e805}'; - - fn default_font(&self) -> Self::Font { - self.settings.default_font - } - - fn default_size(&self) -> Pixels { - self.settings.default_text_size - } - - fn fill_paragraph( - &mut self, - text: &Self::Paragraph, - position: Point, - color: Color, - clip_bounds: Rectangle, - ) { - let (layer, transformation) = self.layers.current_mut(); - - layer.draw_paragraph(text, position, color, clip_bounds, transformation); - } - - fn fill_editor( - &mut self, - editor: &Self::Editor, - position: Point, - color: Color, - clip_bounds: Rectangle, - ) { - let (layer, transformation) = self.layers.current_mut(); - layer.draw_editor(editor, position, color, clip_bounds, transformation); - } - - fn fill_text( - &mut self, - text: core::Text, - position: Point, - color: Color, - clip_bounds: Rectangle, - ) { - let (layer, transformation) = self.layers.current_mut(); - layer.draw_text(text, position, color, clip_bounds, transformation); - } -} - -impl graphics::text::Renderer for Renderer { - fn fill_raw(&mut self, raw: graphics::text::Raw) { - let (layer, transformation) = self.layers.current_mut(); - layer.draw_text_raw(raw, transformation); - } -} - -#[cfg(feature = "geometry")] -impl graphics::geometry::Renderer for Renderer { - type Geometry = Geometry; - type Frame = geometry::Frame; - - fn new_frame(&self, bounds: Rectangle) -> Self::Frame { - geometry::Frame::new(bounds) - } - - fn draw_geometry(&mut self, geometry: Self::Geometry) { - let (layer, transformation) = self.layers.current_mut(); - - match geometry { - Geometry::Live { - primitives, - images, - text, - clip_bounds, - } => { - layer.draw_primitive_group(primitives, clip_bounds, transformation); - - for image in images { - layer.draw_image(image, transformation); - } - - layer.draw_text_group(text, clip_bounds, transformation); - } - Geometry::Cache(cache) => { - layer.draw_primitive_cache(cache.primitives, cache.clip_bounds, transformation); - - for image in cache.images.iter() { - layer.draw_image(image.clone(), transformation); - } - - layer.draw_text_cache(cache.text, cache.clip_bounds, transformation); - } - } - } -} - -impl graphics::mesh::Renderer for Renderer { - fn draw_mesh(&mut self, _mesh: graphics::Mesh) { - log::warn!("iced_tiny_skia does not support drawing meshes"); - } - - fn draw_mesh_cache(&mut self, _cache: iced_graphics::mesh::Cache) { - log::warn!("iced_tiny_skia does not support drawing meshes"); - } -} - -#[cfg(feature = "image")] -impl core::image::Renderer for Renderer { - type Handle = core::image::Handle; - - 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, clip_bounds: Rectangle) { - let (layer, transformation) = self.layers.current_mut(); - layer.draw_raster(image, bounds, clip_bounds, transformation); - } -} - -#[cfg(feature = "svg")] -impl core::svg::Renderer for Renderer { - fn measure_svg(&self, handle: &core::svg::Handle) -> crate::core::Size { - self.engine.vector_pipeline.viewport_dimensions(handle) - } - - 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, clip_bounds, transformation); - } -} - -impl compositor::Default for Renderer { - type Compositor = window::Compositor; -} - -impl renderer::Headless for Renderer { - async fn new(settings: renderer::Settings, backend: Option<&str>) -> Option { - if backend.is_some_and(|backend| !["tiny-skia", "tiny_skia"].contains(&backend)) { - return None; - } - - Some(Self::new(settings)) - } - - fn name(&self) -> String { - "tiny-skia".to_owned() - } - - fn screenshot( - &mut self, - size: Size, - scale_factor: f32, - background_color: Color, - ) -> Vec { - let viewport = Viewport::with_physical_size(size, scale_factor); - - window::compositor::screenshot(self, &viewport, background_color) - } -} diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs deleted file mode 100644 index 5de51047e2..0000000000 --- a/tiny_skia/src/primitive.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::core::Rectangle; - -#[derive(Debug, Clone, PartialEq)] -pub enum Primitive { - /// A path filled with some paint. - Fill { - /// The path to fill. - path: tiny_skia::Path, - /// The paint to use. - paint: tiny_skia::Paint<'static>, - /// The fill rule to follow. - rule: tiny_skia::FillRule, - }, - /// A path stroked with some paint. - Stroke { - /// The path to stroke. - path: tiny_skia::Path, - /// The paint to use. - paint: tiny_skia::Paint<'static>, - /// The stroke settings. - stroke: tiny_skia::Stroke, - }, -} - -impl Primitive { - /// Returns the visible bounds of the [`Primitive`]. - pub fn visible_bounds(&self) -> Rectangle { - let bounds = match self { - Primitive::Fill { path, .. } => path.bounds(), - Primitive::Stroke { path, .. } => path.bounds(), - }; - - Rectangle { - x: bounds.x(), - y: bounds.y(), - width: bounds.width(), - height: bounds.height(), - } - } -} diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs deleted file mode 100644 index bc3323d6d3..0000000000 --- a/tiny_skia/src/raster.rs +++ /dev/null @@ -1,152 +0,0 @@ -use crate::core::image as raster; -use crate::core::{Rectangle, Size}; -use crate::graphics; - -use rustc_hash::{FxHashMap, FxHashSet}; -use std::cell::RefCell; -use std::collections::hash_map; - -#[derive(Debug)] -pub struct Pipeline { - cache: RefCell, -} - -impl Pipeline { - pub fn new() -> Self { - Self { - cache: RefCell::new(Cache::default()), - } - } - - 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( - &mut self, - handle: &raster::Handle, - filter_method: raster::FilterMethod, - bounds: Rectangle, - opacity: f32, - pixels: &mut tiny_skia::PixmapMut<'_>, - transform: tiny_skia::Transform, - clip_mask: Option<&tiny_skia::Mask>, - ) { - let mut cache = self.cache.borrow_mut(); - - let Ok(image) = cache.allocate(handle) else { - return; - }; - - let width_scale = bounds.width / image.width() as f32; - let height_scale = bounds.height / image.height() as f32; - - 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) { - self.cache.borrow_mut().trim(); - } -} - -#[derive(Debug, Default)] -struct Cache { - entries: FxHashMap>, - hits: FxHashSet, -} - -impl Cache { - pub fn allocate( - &mut self, - handle: &raster::Handle, - ) -> Result, raster::Error> { - let id = handle.id(); - - if let hash_map::Entry::Vacant(entry) = self.entries.entry(id) { - let image = match graphics::image::load(handle) { - Ok(image) => image, - Err(error) => { - let _ = entry.insert(None); - - return Err(error); - } - }; - - if image.width() == 0 || image.height() == 0 { - return Err(raster::Error::Empty); - } - - let mut buffer = vec![0u32; image.width() as usize * image.height() as usize]; - - for (i, pixel) in image.pixels().enumerate() { - let [r, g, b, a] = pixel.0; - - buffer[i] = bytemuck::cast(tiny_skia::ColorU8::from_rgba(b, g, r, a).premultiply()); - } - - let _ = entry.insert(Some(Entry { - width: image.width(), - height: image.height(), - pixels: buffer, - })); - } - - let _ = self.hits.insert(id); - - 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) { - self.entries.retain(|key, _| self.hits.contains(key)); - self.hits.clear(); - } -} - -#[derive(Debug)] -struct Entry { - width: u32, - height: u32, - pixels: Vec, -} diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs deleted file mode 100644 index b9e3eb46cc..0000000000 --- a/tiny_skia/src/text.rs +++ /dev/null @@ -1,344 +0,0 @@ -use crate::core::alignment; -use crate::core::text::{Alignment, Ellipsis, Shaping, Wrapping}; -use crate::core::{Color, Font, Pixels, Point, Rectangle, Transformation}; -use crate::graphics::text::cache::{self, Cache}; -use crate::graphics::text::editor; -use crate::graphics::text::font_system; -use crate::graphics::text::paragraph; - -use rustc_hash::{FxHashMap, FxHashSet}; -use std::borrow::Cow; -use std::cell::RefCell; -use std::collections::hash_map; - -#[derive(Debug)] -pub struct Pipeline { - glyph_cache: GlyphCache, - cache: RefCell, -} - -impl Pipeline { - pub fn new() -> Self { - Pipeline { - glyph_cache: GlyphCache::new(), - cache: RefCell::new(Cache::new()), - } - } - - // TODO: Shared engine - #[allow(dead_code)] - pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - font_system() - .write() - .expect("Write font system") - .load_font(bytes); - - self.cache = RefCell::new(Cache::new()); - } - - pub fn draw_paragraph( - &mut self, - paragraph: ¶graph::Weak, - position: Point, - color: Color, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: Option<&tiny_skia::Mask>, - transformation: Transformation, - ) { - let Some(paragraph) = paragraph.upgrade() else { - return; - }; - - let mut font_system = font_system().write().expect("Write font system"); - - draw( - font_system.raw(), - &mut self.glyph_cache, - paragraph.buffer(), - position, - color, - pixels, - clip_mask, - transformation, - ); - } - - pub fn draw_editor( - &mut self, - editor: &editor::Weak, - position: Point, - color: Color, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: Option<&tiny_skia::Mask>, - transformation: Transformation, - ) { - let Some(editor) = editor.upgrade() else { - return; - }; - - let mut font_system = font_system().write().expect("Write font system"); - - draw( - font_system.raw(), - &mut self.glyph_cache, - editor.buffer(), - position, - color, - pixels, - clip_mask, - transformation, - ); - } - - pub fn draw_cached( - &mut self, - content: &str, - bounds: Rectangle, - color: Color, - size: Pixels, - line_height: Pixels, - font: Font, - align_x: Alignment, - align_y: alignment::Vertical, - shaping: Shaping, - wrapping: Wrapping, - ellipsis: Ellipsis, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: Option<&tiny_skia::Mask>, - transformation: Transformation, - ) { - let line_height = f32::from(line_height); - - let mut font_system = font_system().write().expect("Write font system"); - let font_system = font_system.raw(); - - let key = cache::Key { - bounds: bounds.size(), - content, - font, - size: size.into(), - line_height, - shaping, - wrapping, - ellipsis, - align_x, - }; - - let (_, entry) = self.cache.get_mut().allocate(font_system, key); - - let width = entry.min_bounds.width; - let height = entry.min_bounds.height; - - let x = match align_x { - Alignment::Default | Alignment::Left | Alignment::Justified => bounds.x, - Alignment::Center => bounds.x - width / 2.0, - Alignment::Right => bounds.x - width, - }; - - let y = match align_y { - alignment::Vertical::Top => bounds.y, - alignment::Vertical::Center => bounds.y - height / 2.0, - alignment::Vertical::Bottom => bounds.y - height, - }; - - draw( - font_system, - &mut self.glyph_cache, - &entry.buffer, - Point::new(x, y), - color, - pixels, - clip_mask, - transformation, - ); - } - - pub fn draw_raw( - &mut self, - buffer: &cosmic_text::Buffer, - position: Point, - color: Color, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: Option<&tiny_skia::Mask>, - transformation: Transformation, - ) { - let mut font_system = font_system().write().expect("Write font system"); - - draw( - font_system.raw(), - &mut self.glyph_cache, - buffer, - position, - color, - pixels, - clip_mask, - transformation, - ); - } - - pub fn trim_cache(&mut self) { - self.cache.get_mut().trim(); - self.glyph_cache.trim(); - } -} - -fn draw( - font_system: &mut cosmic_text::FontSystem, - glyph_cache: &mut GlyphCache, - buffer: &cosmic_text::Buffer, - position: Point, - color: Color, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: Option<&tiny_skia::Mask>, - transformation: Transformation, -) { - let position = position * transformation; - - let mut swash = cosmic_text::SwashCache::new(); - - for run in buffer.layout_runs() { - for glyph in run.glyphs { - let physical_glyph = - glyph.physical((position.x, position.y), transformation.scale_factor()); - - if let Some((buffer, placement)) = glyph_cache.allocate( - physical_glyph.cache_key, - glyph.color_opt.map(from_color).unwrap_or(color), - font_system, - &mut swash, - ) { - let pixmap = - tiny_skia::PixmapRef::from_bytes(buffer, placement.width, placement.height) - .expect("Create glyph pixel map"); - - let opacity = - color.a * glyph.color_opt.map(|c| c.a() as f32 / 255.0).unwrap_or(1.0); - - pixels.draw_pixmap( - physical_glyph.x + placement.left, - physical_glyph.y - placement.top - + (run.line_y * transformation.scale_factor()).round() as i32, - pixmap, - &tiny_skia::PixmapPaint { - opacity, - ..tiny_skia::PixmapPaint::default() - }, - tiny_skia::Transform::identity(), - clip_mask, - ); - } - } - } -} - -fn from_color(color: cosmic_text::Color) -> Color { - let [r, g, b, a] = color.as_rgba(); - - Color::from_rgba8(r, g, b, a as f32 / 255.0) -} - -#[derive(Debug, Clone, Default)] -struct GlyphCache { - entries: FxHashMap<(cosmic_text::CacheKey, [u8; 3]), (Vec, cosmic_text::Placement)>, - recently_used: FxHashSet<(cosmic_text::CacheKey, [u8; 3])>, - trim_count: usize, -} - -impl GlyphCache { - const TRIM_INTERVAL: usize = 300; - const CAPACITY_LIMIT: usize = 16 * 1024; - - fn new() -> Self { - GlyphCache::default() - } - - fn allocate( - &mut self, - cache_key: cosmic_text::CacheKey, - color: Color, - font_system: &mut cosmic_text::FontSystem, - swash: &mut cosmic_text::SwashCache, - ) -> Option<(&[u8], cosmic_text::Placement)> { - let [r, g, b, _a] = color.into_rgba8(); - let key = (cache_key, [r, g, b]); - - if let hash_map::Entry::Vacant(entry) = self.entries.entry(key) { - // TODO: Outline support - let image = swash.get_image_uncached(font_system, cache_key)?; - - let glyph_size = image.placement.width as usize * image.placement.height as usize; - - if glyph_size == 0 { - return None; - } - - let mut buffer = vec![0u32; glyph_size]; - - match image.content { - cosmic_text::SwashContent::Mask => { - let mut i = 0; - - // TODO: Blend alpha - - for _y in 0..image.placement.height { - for _x in 0..image.placement.width { - buffer[i] = bytemuck::cast( - tiny_skia::ColorU8::from_rgba(b, g, r, image.data[i]).premultiply(), - ); - - i += 1; - } - } - } - cosmic_text::SwashContent::Color => { - let mut i = 0; - - for _y in 0..image.placement.height { - for _x in 0..image.placement.width { - // TODO: Blend alpha - buffer[i >> 2] = bytemuck::cast( - tiny_skia::ColorU8::from_rgba( - image.data[i + 2], - image.data[i + 1], - image.data[i], - image.data[i + 3], - ) - .premultiply(), - ); - - i += 4; - } - } - } - cosmic_text::SwashContent::SubpixelMask => { - // TODO - } - } - - let _ = entry.insert((buffer, image.placement)); - } - - let _ = self.recently_used.insert(key); - - self.entries - .get(&key) - .map(|(buffer, placement)| (bytemuck::cast_slice(buffer.as_slice()), *placement)) - } - - pub fn trim(&mut self) { - if self.trim_count > Self::TRIM_INTERVAL || self.recently_used.len() >= Self::CAPACITY_LIMIT - { - self.entries - .retain(|key, _| self.recently_used.contains(key)); - - self.recently_used.clear(); - - self.entries.shrink_to(Self::CAPACITY_LIMIT); - self.recently_used.shrink_to(Self::CAPACITY_LIMIT); - - self.trim_count = 0; - } else { - self.trim_count += 1; - } - } -} diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs deleted file mode 100644 index f0f9ff972d..0000000000 --- a/tiny_skia/src/vector.rs +++ /dev/null @@ -1,221 +0,0 @@ -use crate::core::svg::{Data, Handle}; -use crate::core::{Color, Rectangle, Size}; - -use resvg::usvg; -use rustc_hash::{FxHashMap, FxHashSet}; -use tiny_skia::Transform; - -use std::cell::RefCell; -use std::collections::hash_map; -use std::fs; -use std::panic; -use std::sync::Arc; - -#[derive(Debug)] -pub struct Pipeline { - cache: RefCell, -} - -impl Pipeline { - pub fn new() -> Self { - Self { - cache: RefCell::new(Cache::default()), - } - } - - pub fn viewport_dimensions(&self, handle: &Handle) -> Size { - self.cache - .borrow_mut() - .viewport_dimensions(handle) - .unwrap_or(Size::new(0, 0)) - } - - pub fn draw( - &mut self, - handle: &Handle, - color: Option, - bounds: Rectangle, - opacity: f32, - pixels: &mut tiny_skia::PixmapMut<'_>, - transform: Transform, - clip_mask: Option<&tiny_skia::Mask>, - ) { - if let Some(image) = self.cache.borrow_mut().draw( - handle, - color, - Size::new( - (bounds.width * transform.sx) as u32, - (bounds.height * transform.sy) as u32, - ), - ) { - pixels.draw_pixmap( - (bounds.x * transform.sx) as i32, - (bounds.y * transform.sy) as i32, - image, - &tiny_skia::PixmapPaint { - opacity, - ..tiny_skia::PixmapPaint::default() - }, - Transform::default(), - clip_mask, - ); - } - } - - pub fn trim_cache(&mut self) { - self.cache.borrow_mut().trim(); - } -} - -#[derive(Default)] -struct Cache { - trees: FxHashMap>, - tree_hits: FxHashSet, - rasters: FxHashMap, - raster_hits: FxHashSet, - fontdb: Option>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -struct RasterKey { - id: u64, - color: Option<[u8; 4]>, - size: Size, -} - -impl Cache { - fn load(&mut self, handle: &Handle) -> Option<&usvg::Tree> { - let id = handle.id(); - - // TODO: Reuse `cosmic-text` font database - if self.fontdb.is_none() { - let mut fontdb = usvg::fontdb::Database::new(); - fontdb.load_system_fonts(); - - self.fontdb = Some(Arc::new(fontdb)); - } - - let options = usvg::Options { - fontdb: self - .fontdb - .as_ref() - .expect("fontdb must be initialized") - .clone(), - ..usvg::Options::default() - }; - - if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) { - let svg = match handle.data() { - Data::Path(path) => fs::read_to_string(path) - .ok() - .and_then(|contents| usvg::Tree::from_str(&contents, &options).ok()), - Data::Bytes(bytes) => usvg::Tree::from_data(bytes, &options).ok(), - }; - - let _ = entry.insert(svg); - } - - let _ = self.tree_hits.insert(id); - self.trees.get(&id).unwrap().as_ref() - } - - fn viewport_dimensions(&mut self, handle: &Handle) -> Option> { - let tree = self.load(handle)?; - let size = tree.size(); - - Some(Size::new(size.width() as u32, size.height() as u32)) - } - - fn draw( - &mut self, - handle: &Handle, - color: Option, - size: Size, - ) -> Option> { - if size.width == 0 || size.height == 0 { - return None; - } - - let key = RasterKey { - id: handle.id(), - color: color.map(Color::into_rgba8), - size, - }; - - #[allow(clippy::map_entry)] - if !self.rasters.contains_key(&key) { - let tree = self.load(handle)?; - - let mut image = tiny_skia::Pixmap::new(size.width, size.height)?; - - let tree_size = tree.size().to_int_size(); - - let target_size = if size.width > size.height { - tree_size.scale_to_width(size.width) - } else { - tree_size.scale_to_height(size.height) - }; - - let transform = if let Some(target_size) = target_size { - let tree_size = tree_size.to_size(); - let target_size = target_size.to_size(); - - tiny_skia::Transform::from_scale( - target_size.width() / tree_size.width(), - target_size.height() / tree_size.height(), - ) - } else { - tiny_skia::Transform::default() - }; - - // SVG rendering can panic on malformed or complex vectors. - // We catch panics to prevent crashes and continue gracefully. - let render = panic::catch_unwind(panic::AssertUnwindSafe(|| { - resvg::render(tree, transform, &mut image.as_mut()); - })); - - if let Err(error) = render { - log::warn!("SVG rendering for {handle:?} panicked: {error:?}"); - } - - if let Some([r, g, b, _]) = key.color { - // Apply color filter - for pixel in bytemuck::cast_slice_mut::(image.data_mut()) { - *pixel = bytemuck::cast( - tiny_skia::ColorU8::from_rgba(b, g, r, (*pixel >> 24) as u8).premultiply(), - ); - } - } else { - // Swap R and B channels for `softbuffer` presentation - for pixel in bytemuck::cast_slice_mut::(image.data_mut()) { - *pixel = *pixel & 0xFF00_FF00 - | ((0x0000_00FF & *pixel) << 16) - | ((0x00FF_0000 & *pixel) >> 16); - } - } - - let _ = self.rasters.insert(key, image); - } - - let _ = self.raster_hits.insert(key); - self.rasters.get(&key).map(tiny_skia::Pixmap::as_ref) - } - - fn trim(&mut self) { - self.trees.retain(|key, _| self.tree_hits.contains(key)); - self.rasters.retain(|key, _| self.raster_hits.contains(key)); - - self.tree_hits.clear(); - self.raster_hits.clear(); - } -} - -impl std::fmt::Debug for Cache { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Cache") - .field("tree_hits", &self.tree_hits) - .field("rasters", &self.rasters) - .field("raster_hits", &self.raster_hits) - .finish_non_exhaustive() - } -} diff --git a/tiny_skia/src/window.rs b/tiny_skia/src/window.rs deleted file mode 100644 index d8d9378e16..0000000000 --- a/tiny_skia/src/window.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod compositor; - -pub use compositor::{Compositor, Surface}; diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs deleted file mode 100644 index 223e230a12..0000000000 --- a/tiny_skia/src/window/compositor.rs +++ /dev/null @@ -1,249 +0,0 @@ -use crate::core::renderer; -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::{Shell, Viewport}; -use crate::{Layer, Renderer}; - -use std::collections::VecDeque; -use std::num::NonZeroU32; - -pub struct Compositor { - context: softbuffer::Context>, -} - -pub struct Surface { - window: softbuffer::Surface, Box>, - clip_mask: tiny_skia::Mask, - frames: VecDeque