summaryrefslogtreecommitdiffstats
path: root/tiny_skia
diff options
context:
space:
mode:
authorLibravatar Bingus <shankern@protonmail.com>2023-07-12 12:23:18 -0700
committerLibravatar Bingus <shankern@protonmail.com>2023-07-12 12:23:18 -0700
commit633f405f3f78bc7f82d2b2061491b0e011137451 (patch)
tree5ebfc1f45d216a5c14a90492563599e6969eab4d /tiny_skia
parent41836dd80d0534608e7aedfbf2319c540a23de1a (diff)
parent21bd51426d900e271206f314e0c915dd41065521 (diff)
downloadiced-633f405f3f78bc7f82d2b2061491b0e011137451.tar.gz
iced-633f405f3f78bc7f82d2b2061491b0e011137451.tar.bz2
iced-633f405f3f78bc7f82d2b2061491b0e011137451.zip
Merge remote-tracking branch 'origin/master' into feat/multi-window-support
# Conflicts: # Cargo.toml # core/src/window/icon.rs # core/src/window/id.rs # core/src/window/position.rs # core/src/window/settings.rs # examples/integration/src/main.rs # examples/integration_opengl/src/main.rs # glutin/src/application.rs # native/src/subscription.rs # native/src/window.rs # runtime/src/window/action.rs # src/lib.rs # src/window.rs # winit/Cargo.toml # winit/src/application.rs # winit/src/icon.rs # winit/src/settings.rs # winit/src/window.rs
Diffstat (limited to 'tiny_skia')
-rw-r--r--tiny_skia/Cargo.toml38
-rw-r--r--tiny_skia/fonts/Iced-Icons.ttfbin0 -> 5108 bytes
-rw-r--r--tiny_skia/src/backend.rs856
-rw-r--r--tiny_skia/src/geometry.rs315
-rw-r--r--tiny_skia/src/lib.rs28
-rw-r--r--tiny_skia/src/primitive.rs48
-rw-r--r--tiny_skia/src/raster.rs116
-rw-r--r--tiny_skia/src/settings.rs24
-rw-r--r--tiny_skia/src/text.rs483
-rw-r--r--tiny_skia/src/vector.rs195
-rw-r--r--tiny_skia/src/window.rs3
-rw-r--r--tiny_skia/src/window/compositor.rs228
12 files changed, 2334 insertions, 0 deletions
diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml
new file mode 100644
index 00000000..d9276ea5
--- /dev/null
+++ b/tiny_skia/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "iced_tiny_skia"
+version = "0.1.0"
+edition = "2021"
+
+[features]
+image = ["iced_graphics/image"]
+svg = ["resvg"]
+geometry = ["iced_graphics/geometry"]
+
+[dependencies]
+raw-window-handle = "0.5"
+softbuffer = "0.2"
+tiny-skia = "0.10"
+bytemuck = "1"
+rustc-hash = "1.1"
+kurbo = "0.9"
+log = "0.4"
+
+[dependencies.iced_graphics]
+version = "0.8"
+path = "../graphics"
+
+[dependencies.cosmic-text]
+git = "https://github.com/hecrj/cosmic-text.git"
+rev = "c3cd24dc972bb8fd55d016c81ac9fa637e0a4ada"
+
+[dependencies.twox-hash]
+version = "1.6"
+default-features = false
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies.twox-hash]
+version = "1.6.1"
+features = ["std"]
+
+[dependencies.resvg]
+version = "0.35"
+optional = true
diff --git a/tiny_skia/fonts/Iced-Icons.ttf b/tiny_skia/fonts/Iced-Icons.ttf
new file mode 100644
index 00000000..e3273141
--- /dev/null
+++ b/tiny_skia/fonts/Iced-Icons.ttf
Binary files differ
diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs
new file mode 100644
index 00000000..a8add70b
--- /dev/null
+++ b/tiny_skia/src/backend.rs
@@ -0,0 +1,856 @@
+use crate::core::text;
+use crate::core::Gradient;
+use crate::core::{Background, Color, Font, Point, Rectangle, Size, Vector};
+use crate::graphics::backend;
+use crate::graphics::{Damage, Viewport};
+use crate::primitive::{self, Primitive};
+use crate::Settings;
+
+use std::borrow::Cow;
+
+pub struct Backend {
+ default_font: Font,
+ default_text_size: f32,
+ text_pipeline: crate::text::Pipeline,
+
+ #[cfg(feature = "image")]
+ raster_pipeline: crate::raster::Pipeline,
+
+ #[cfg(feature = "svg")]
+ vector_pipeline: crate::vector::Pipeline,
+}
+
+impl Backend {
+ pub fn new(settings: Settings) -> Self {
+ Self {
+ default_font: settings.default_font,
+ default_text_size: settings.default_text_size,
+ text_pipeline: crate::text::Pipeline::new(),
+
+ #[cfg(feature = "image")]
+ raster_pipeline: crate::raster::Pipeline::new(),
+
+ #[cfg(feature = "svg")]
+ vector_pipeline: crate::vector::Pipeline::new(),
+ }
+ }
+
+ pub fn draw<T: AsRef<str>>(
+ &mut self,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: &mut tiny_skia::Mask,
+ primitives: &[Primitive],
+ viewport: &Viewport,
+ damage: &[Rectangle],
+ background_color: Color,
+ overlay: &[T],
+ ) {
+ let physical_size = viewport.physical_size();
+ let scale_factor = viewport.scale_factor() as f32;
+
+ 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,
+ );
+ }
+
+ for &region 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,
+ blend_mode: tiny_skia::BlendMode::Source,
+ ..Default::default()
+ },
+ tiny_skia::FillRule::default(),
+ tiny_skia::Transform::identity(),
+ None,
+ );
+
+ 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();
+
+ #[cfg(feature = "image")]
+ self.raster_pipeline.trim_cache();
+
+ #[cfg(feature = "svg")]
+ self.vector_pipeline.trim_cache();
+ }
+
+ fn draw_primitive(
+ &mut self,
+ primitive: &Primitive,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: &mut tiny_skia::Mask,
+ clip_bounds: Rectangle,
+ scale_factor: f32,
+ translation: Vector,
+ ) {
+ match primitive {
+ Primitive::Quad {
+ bounds,
+ background,
+ border_radius,
+ 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,
+ )
+ .post_scale(scale_factor, scale_factor);
+
+ // Make sure the border radius is not larger than the bounds
+ let border_width = border_width
+ .min(bounds.width / 2.0)
+ .min(bounds.height / 2.0);
+
+ let mut fill_border_radius = *border_radius;
+ for radius in &mut fill_border_radius {
+ *radius = (*radius)
+ .min(bounds.width / 2.0)
+ .min(bounds.height / 2.0);
+ }
+ let path = rounded_rectangle(*bounds, fill_border_radius);
+
+ 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(bounds);
+
+ let stops: Vec<tiny_skia::GradientStop> =
+ 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: bounds.x + border_width / 2.0,
+ y: bounds.y + border_width / 2.0,
+ width: bounds.width - border_width,
+ height: bounds.height - border_width,
+ };
+
+ // Make sure the border radius is correct
+ let mut border_radius = *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(*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(
+ bounds.width as u32,
+ bounds.height as u32,
+ )
+ .unwrap();
+
+ let mut quad_mask = tiny_skia::Mask::new(
+ bounds.width as u32,
+ bounds.height as u32,
+ )
+ .unwrap();
+
+ let zero_bounds = Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: bounds.width,
+ height: 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: bounds.width - border_width,
+ height: 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(*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(
+ bounds.x as i32,
+ bounds.y as i32,
+ temp_pixmap.as_ref(),
+ &tiny_skia::PixmapPaint::default(),
+ transform,
+ clip_mask,
+ );
+ }
+ }
+ }
+ Primitive::Text {
+ content,
+ bounds,
+ color,
+ size,
+ line_height,
+ font,
+ horizontal_alignment,
+ vertical_alignment,
+ shaping,
+ } => {
+ 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,
+ *color,
+ *size,
+ *line_height,
+ *font,
+ *horizontal_alignment,
+ *vertical_alignment,
+ *shaping,
+ scale_factor,
+ pixels,
+ 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_mask);
+ }
+ #[cfg(not(feature = "image"))]
+ Primitive::Image { .. } => {
+ log::warn!(
+ "Unsupported primitive in `iced_tiny_skia`: {:?}",
+ primitive
+ );
+ }
+ #[cfg(feature = "svg")]
+ Primitive::Svg {
+ handle,
+ 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_mask,
+ );
+ }
+ #[cfg(not(feature = "svg"))]
+ Primitive::Svg { .. } => {
+ log::warn!(
+ "Unsupported primitive in `iced_tiny_skia`: {:?}",
+ primitive
+ );
+ }
+ Primitive::Custom(primitive::Custom::Fill {
+ path,
+ paint,
+ 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,
+ *rule,
+ transform
+ .post_translate(translation.x, translation.y)
+ .post_scale(scale_factor, scale_factor),
+ clip_mask,
+ );
+ }
+ Primitive::Custom(primitive::Custom::Stroke {
+ path,
+ paint,
+ 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,
+ stroke,
+ transform
+ .post_translate(translation.x, translation.y)
+ .post_scale(scale_factor, scale_factor),
+ clip_mask,
+ );
+ }
+ Primitive::Group { primitives } => {
+ for primitive in primitives {
+ self.draw_primitive(
+ primitive,
+ pixels,
+ clip_mask,
+ clip_bounds,
+ scale_factor,
+ translation,
+ );
+ }
+ }
+ Primitive::Translate {
+ translation: offset,
+ content,
+ } => {
+ self.draw_primitive(
+ content,
+ pixels,
+ clip_mask,
+ clip_bounds,
+ scale_factor,
+ translation + *offset,
+ );
+ }
+ Primitive::Clip { bounds, content } => {
+ let bounds = (*bounds + translation) * scale_factor;
+
+ 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,
+ bounds,
+ scale_factor,
+ translation,
+ );
+
+ adjust_clip_mask(clip_mask, clip_bounds);
+ }
+ }
+ Primitive::Cache { content } => {
+ self.draw_primitive(
+ content,
+ pixels,
+ clip_mask,
+ clip_bounds,
+ scale_factor,
+ translation,
+ );
+ }
+ }
+ }
+}
+
+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 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 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(
+ tiny_skia::Rect::from_xywh(
+ bounds.x,
+ bounds.y,
+ bounds.width,
+ bounds.height,
+ )
+ .unwrap(),
+ );
+
+ builder.finish().unwrap()
+ };
+
+ clip_mask.fill_path(
+ &path,
+ tiny_skia::FillRule::EvenOdd,
+ false,
+ tiny_skia::Transform::default(),
+ );
+}
+
+impl iced_graphics::Backend for Backend {
+ type Primitive = primitive::Custom;
+}
+
+impl backend::Text for Backend {
+ const ICON_FONT: Font = Font::with_name("Iced-Icons");
+ const CHECKMARK_ICON: char = '\u{f00c}';
+ const ARROW_DOWN_ICON: char = '\u{e800}';
+
+ fn default_font(&self) -> Font {
+ self.default_font
+ }
+
+ fn default_size(&self) -> f32 {
+ self.default_text_size
+ }
+
+ fn measure(
+ &self,
+ contents: &str,
+ size: f32,
+ line_height: text::LineHeight,
+ font: Font,
+ bounds: Size,
+ shaping: text::Shaping,
+ ) -> Size {
+ self.text_pipeline.measure(
+ contents,
+ size,
+ line_height,
+ font,
+ bounds,
+ shaping,
+ )
+ }
+
+ fn hit_test(
+ &self,
+ contents: &str,
+ size: f32,
+ line_height: text::LineHeight,
+ font: Font,
+ bounds: Size,
+ shaping: text::Shaping,
+ point: Point,
+ nearest_only: bool,
+ ) -> Option<text::Hit> {
+ self.text_pipeline.hit_test(
+ contents,
+ size,
+ line_height,
+ font,
+ bounds,
+ shaping,
+ point,
+ nearest_only,
+ )
+ }
+
+ fn load_font(&mut self, font: Cow<'static, [u8]>) {
+ self.text_pipeline.load_font(font);
+ }
+}
+
+#[cfg(feature = "image")]
+impl backend::Image for Backend {
+ fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {
+ self.raster_pipeline.dimensions(handle)
+ }
+}
+
+#[cfg(feature = "svg")]
+impl backend::Svg for Backend {
+ fn viewport_dimensions(
+ &self,
+ handle: &crate::core::svg::Handle,
+ ) -> Size<u32> {
+ self.vector_pipeline.viewport_dimensions(handle)
+ }
+}
diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs
new file mode 100644
index 00000000..9bd47556
--- /dev/null
+++ b/tiny_skia/src/geometry.rs
@@ -0,0 +1,315 @@
+use crate::core::{Point, Rectangle, Size, Vector};
+use crate::graphics::geometry::fill::{self, Fill};
+use crate::graphics::geometry::stroke::{self, Stroke};
+use crate::graphics::geometry::{Path, Style, Text};
+use crate::graphics::Gradient;
+use crate::primitive::{self, Primitive};
+
+pub struct Frame {
+ size: Size,
+ transform: tiny_skia::Transform,
+ stack: Vec<tiny_skia::Transform>,
+ primitives: Vec<Primitive>,
+}
+
+impl Frame {
+ pub fn new(size: Size) -> Self {
+ Self {
+ size,
+ transform: tiny_skia::Transform::identity(),
+ stack: Vec::new(),
+ primitives: Vec::new(),
+ }
+ }
+
+ pub fn width(&self) -> f32 {
+ self.size.width
+ }
+
+ pub fn height(&self) -> f32 {
+ self.size.height
+ }
+
+ pub fn size(&self) -> Size {
+ self.size
+ }
+
+ pub fn center(&self) -> Point {
+ Point::new(self.size.width / 2.0, self.size.height / 2.0)
+ }
+
+ pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
+ let Some(path) = convert_path(path) else { return };
+ let fill = fill.into();
+
+ self.primitives
+ .push(Primitive::Custom(primitive::Custom::Fill {
+ path,
+ paint: into_paint(fill.style),
+ rule: into_fill_rule(fill.rule),
+ transform: self.transform,
+ }));
+ }
+
+ pub fn fill_rectangle(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ fill: impl Into<Fill>,
+ ) {
+ let Some(path) = convert_path(&Path::rectangle(top_left, size)) else { return };
+ let fill = fill.into();
+
+ self.primitives
+ .push(Primitive::Custom(primitive::Custom::Fill {
+ path,
+ paint: tiny_skia::Paint {
+ anti_alias: false,
+ ..into_paint(fill.style)
+ },
+ rule: into_fill_rule(fill.rule),
+ transform: self.transform,
+ }));
+ }
+
+ pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
+ let Some(path) = convert_path(path) else { return };
+
+ let stroke = stroke.into();
+ let skia_stroke = into_stroke(&stroke);
+
+ self.primitives
+ .push(Primitive::Custom(primitive::Custom::Stroke {
+ path,
+ paint: into_paint(stroke.style),
+ stroke: skia_stroke,
+ transform: self.transform,
+ }));
+ }
+
+ pub fn fill_text(&mut self, text: impl Into<Text>) {
+ let text = text.into();
+
+ let position = if self.transform.is_identity() {
+ text.position
+ } else {
+ let mut transformed = [tiny_skia::Point {
+ x: text.position.x,
+ y: text.position.y,
+ }];
+
+ self.transform.map_points(&mut transformed);
+
+ Point::new(transformed[0].x, transformed[0].y)
+ };
+
+ // TODO: Use vectorial text instead of primitive
+ self.primitives.push(Primitive::Text {
+ content: text.content,
+ bounds: Rectangle {
+ x: position.x,
+ y: position.y,
+ width: f32::INFINITY,
+ height: f32::INFINITY,
+ },
+ color: text.color,
+ size: text.size,
+ line_height: text.line_height,
+ font: text.font,
+ horizontal_alignment: text.horizontal_alignment,
+ vertical_alignment: text.vertical_alignment,
+ shaping: text.shaping,
+ });
+ }
+
+ pub fn push_transform(&mut self) {
+ self.stack.push(self.transform);
+ }
+
+ pub fn pop_transform(&mut self) {
+ self.transform = self.stack.pop().expect("Pop transform");
+ }
+
+ pub fn clip(&mut self, frame: Self, at: Point) {
+ self.primitives.push(Primitive::Translate {
+ translation: Vector::new(at.x, at.y),
+ content: Box::new(frame.into_primitive()),
+ });
+ }
+
+ pub fn translate(&mut self, translation: Vector) {
+ self.transform =
+ self.transform.pre_translate(translation.x, translation.y);
+ }
+
+ pub fn rotate(&mut self, angle: f32) {
+ self.transform = self
+ .transform
+ .pre_concat(tiny_skia::Transform::from_rotate(angle.to_degrees()));
+ }
+
+ pub fn scale(&mut self, scale: f32) {
+ self.transform = self.transform.pre_scale(scale, scale);
+ }
+
+ pub fn into_primitive(self) -> Primitive {
+ Primitive::Clip {
+ bounds: Rectangle::new(Point::ORIGIN, self.size),
+ content: Box::new(Primitive::Group {
+ primitives: self.primitives,
+ }),
+ }
+ }
+}
+
+fn convert_path(path: &Path) -> Option<tiny_skia::Path> {
+ use iced_graphics::geometry::path::lyon_path;
+
+ let mut builder = tiny_skia::PathBuilder::new();
+ let mut last_point = Default::default();
+
+ for event in path.raw().iter() {
+ 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<tiny_skia::GradientStop> = 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
new file mode 100644
index 00000000..15de6ce2
--- /dev/null
+++ b/tiny_skia/src/lib.rs
@@ -0,0 +1,28 @@
+pub mod window;
+
+mod backend;
+mod primitive;
+mod settings;
+mod text;
+
+#[cfg(feature = "image")]
+mod raster;
+
+#[cfg(feature = "svg")]
+mod vector;
+
+#[cfg(feature = "geometry")]
+pub mod geometry;
+
+pub use iced_graphics as graphics;
+pub use iced_graphics::core;
+
+pub use backend::Backend;
+pub use primitive::Primitive;
+pub use settings::Settings;
+
+/// A [`tiny-skia`] graphics renderer for [`iced`].
+///
+/// [`tiny-skia`]: https://github.com/RazrFalcon/tiny-skia
+/// [`iced`]: https://github.com/iced-rs/iced
+pub type Renderer<Theme> = iced_graphics::Renderer<Backend, Theme>;
diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs
new file mode 100644
index 00000000..0ed24969
--- /dev/null
+++ b/tiny_skia/src/primitive.rs
@@ -0,0 +1,48 @@
+use crate::core::Rectangle;
+use crate::graphics::Damage;
+
+pub type Primitive = crate::graphics::Primitive<Custom>;
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum Custom {
+ /// 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,
+ /// The transform to apply to the path.
+ transform: tiny_skia::Transform,
+ },
+ /// 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,
+ /// The transform to apply to the path.
+ transform: tiny_skia::Transform,
+ },
+}
+
+impl Damage for Custom {
+ fn bounds(&self) -> Rectangle {
+ match self {
+ 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)
+ }
+ }
+ }
+}
diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs
new file mode 100644
index 00000000..dedb127c
--- /dev/null
+++ b/tiny_skia/src/raster.rs
@@ -0,0 +1,116 @@
+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;
+
+pub struct Pipeline {
+ cache: RefCell<Cache>,
+}
+
+impl Pipeline {
+ pub fn new() -> Self {
+ Self {
+ cache: RefCell::new(Cache::default()),
+ }
+ }
+
+ pub fn dimensions(&self, handle: &raster::Handle) -> Size<u32> {
+ if let Some(image) = self.cache.borrow_mut().allocate(handle) {
+ Size::new(image.width(), image.height())
+ } else {
+ Size::new(0, 0)
+ }
+ }
+
+ pub fn draw(
+ &mut self,
+ handle: &raster::Handle,
+ bounds: Rectangle,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ transform: tiny_skia::Transform,
+ clip_mask: Option<&tiny_skia::Mask>,
+ ) {
+ if let Some(image) = self.cache.borrow_mut().allocate(handle) {
+ let width_scale = bounds.width / image.width() as f32;
+ let height_scale = bounds.height / image.height() as f32;
+
+ let transform = transform.pre_scale(width_scale, height_scale);
+
+ pixels.draw_pixmap(
+ (bounds.x / width_scale) as i32,
+ (bounds.y / height_scale) as i32,
+ image,
+ &tiny_skia::PixmapPaint {
+ quality: tiny_skia::FilterQuality::Bilinear,
+ ..Default::default()
+ },
+ transform,
+ clip_mask,
+ );
+ }
+ }
+
+ pub fn trim_cache(&mut self) {
+ self.cache.borrow_mut().trim();
+ }
+}
+
+#[derive(Default)]
+struct Cache {
+ entries: FxHashMap<u64, Option<Entry>>,
+ hits: FxHashSet<u64>,
+}
+
+impl Cache {
+ pub fn allocate(
+ &mut self,
+ handle: &raster::Handle,
+ ) -> Option<tiny_skia::PixmapRef<'_>> {
+ let id = handle.id();
+
+ if let hash_map::Entry::Vacant(entry) = self.entries.entry(id) {
+ let image = graphics::image::load(handle).ok()?.into_rgba8();
+
+ let 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(),
+ );
+ }
+
+ entry.insert(Some(Entry {
+ width: image.width(),
+ height: image.height(),
+ pixels: buffer,
+ }));
+ }
+
+ self.hits.insert(id);
+ self.entries.get(&id).unwrap().as_ref().map(|entry| {
+ tiny_skia::PixmapRef::from_bytes(
+ bytemuck::cast_slice(&entry.pixels),
+ entry.width,
+ entry.height,
+ )
+ .expect("Build pixmap from image bytes")
+ })
+ }
+
+ fn trim(&mut self) {
+ self.entries.retain(|key, _| self.hits.contains(key));
+ self.hits.clear();
+ }
+}
+
+struct Entry {
+ width: u32,
+ height: u32,
+ pixels: Vec<u32>,
+}
diff --git a/tiny_skia/src/settings.rs b/tiny_skia/src/settings.rs
new file mode 100644
index 00000000..abffbfe6
--- /dev/null
+++ b/tiny_skia/src/settings.rs
@@ -0,0 +1,24 @@
+use crate::core::Font;
+
+/// The settings of a [`Backend`].
+///
+/// [`Backend`]: crate::Backend
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Settings {
+ /// The default [`Font`] to use.
+ pub default_font: Font,
+
+ /// The default size of text.
+ ///
+ /// By default, it will be set to `16.0`.
+ pub default_text_size: f32,
+}
+
+impl Default for Settings {
+ fn default() -> Settings {
+ Settings {
+ default_font: Font::default(),
+ default_text_size: 16.0,
+ }
+ }
+}
diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs
new file mode 100644
index 00000000..15f25740
--- /dev/null
+++ b/tiny_skia/src/text.rs
@@ -0,0 +1,483 @@
+use crate::core::alignment;
+use crate::core::font::{self, Font};
+use crate::core::text::{Hit, LineHeight, Shaping};
+use crate::core::{Color, Pixels, Point, Rectangle, Size};
+
+use rustc_hash::{FxHashMap, FxHashSet};
+use std::borrow::Cow;
+use std::cell::RefCell;
+use std::collections::hash_map;
+use std::hash::{BuildHasher, Hash, Hasher};
+use std::sync::Arc;
+
+#[allow(missing_debug_implementations)]
+pub struct Pipeline {
+ font_system: RefCell<cosmic_text::FontSystem>,
+ glyph_cache: GlyphCache,
+ cache: RefCell<Cache>,
+}
+
+impl Pipeline {
+ pub fn new() -> Self {
+ Pipeline {
+ font_system: RefCell::new(cosmic_text::FontSystem::new_with_fonts(
+ [cosmic_text::fontdb::Source::Binary(Arc::new(
+ include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
+ ))]
+ .into_iter(),
+ )),
+ glyph_cache: GlyphCache::new(),
+ cache: RefCell::new(Cache::new()),
+ }
+ }
+
+ pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
+ self.font_system.get_mut().db_mut().load_font_source(
+ cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())),
+ );
+
+ self.cache = RefCell::new(Cache::new());
+ }
+
+ pub fn draw(
+ &mut self,
+ content: &str,
+ bounds: Rectangle,
+ color: Color,
+ size: f32,
+ line_height: LineHeight,
+ font: Font,
+ horizontal_alignment: alignment::Horizontal,
+ vertical_alignment: alignment::Vertical,
+ shaping: Shaping,
+ scale_factor: f32,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: Option<&tiny_skia::Mask>,
+ ) {
+ let line_height = f32::from(line_height.to_absolute(Pixels(size)));
+
+ let font_system = self.font_system.get_mut();
+ let key = Key {
+ bounds: bounds.size(),
+ content,
+ font,
+ size,
+ line_height,
+ shaping,
+ };
+
+ let (_, entry) = self.cache.get_mut().allocate(font_system, key);
+
+ let max_width = entry.bounds.width * scale_factor;
+ let total_height = entry.bounds.height * scale_factor;
+
+ let bounds = bounds * scale_factor;
+
+ let x = match horizontal_alignment {
+ alignment::Horizontal::Left => bounds.x,
+ alignment::Horizontal::Center => bounds.x - max_width / 2.0,
+ alignment::Horizontal::Right => bounds.x - max_width,
+ };
+
+ let y = match vertical_alignment {
+ alignment::Vertical::Top => bounds.y,
+ alignment::Vertical::Center => bounds.y - total_height / 2.0,
+ alignment::Vertical::Bottom => bounds.y - total_height,
+ };
+
+ let mut swash = cosmic_text::SwashCache::new();
+
+ for run in entry.buffer.layout_runs() {
+ for glyph in run.glyphs {
+ let physical_glyph = glyph.physical((x, y), scale_factor);
+
+ if let Some((buffer, placement)) = self.glyph_cache.allocate(
+ physical_glyph.cache_key,
+ color,
+ font_system,
+ &mut swash,
+ ) {
+ let pixmap = tiny_skia::PixmapRef::from_bytes(
+ buffer,
+ placement.width,
+ placement.height,
+ )
+ .expect("Create glyph pixel map");
+
+ pixels.draw_pixmap(
+ physical_glyph.x + placement.left,
+ physical_glyph.y - placement.top
+ + (run.line_y * scale_factor).round() as i32,
+ pixmap,
+ &tiny_skia::PixmapPaint::default(),
+ tiny_skia::Transform::identity(),
+ clip_mask,
+ );
+ }
+ }
+ }
+ }
+
+ pub fn trim_cache(&mut self) {
+ self.cache.get_mut().trim();
+ self.glyph_cache.trim();
+ }
+
+ pub fn measure(
+ &self,
+ content: &str,
+ size: f32,
+ line_height: LineHeight,
+ font: Font,
+ bounds: Size,
+ shaping: Shaping,
+ ) -> Size {
+ let mut measurement_cache = self.cache.borrow_mut();
+
+ let line_height = f32::from(line_height.to_absolute(Pixels(size)));
+
+ let (_, entry) = measurement_cache.allocate(
+ &mut self.font_system.borrow_mut(),
+ Key {
+ content,
+ size,
+ line_height,
+ font,
+ bounds,
+ shaping,
+ },
+ );
+
+ entry.bounds
+ }
+
+ pub fn hit_test(
+ &self,
+ content: &str,
+ size: f32,
+ line_height: LineHeight,
+ font: Font,
+ bounds: Size,
+ shaping: Shaping,
+ point: Point,
+ _nearest_only: bool,
+ ) -> Option<Hit> {
+ let mut measurement_cache = self.cache.borrow_mut();
+
+ let line_height = f32::from(line_height.to_absolute(Pixels(size)));
+
+ let (_, entry) = measurement_cache.allocate(
+ &mut self.font_system.borrow_mut(),
+ Key {
+ content,
+ size,
+ line_height,
+ font,
+ bounds,
+ shaping,
+ },
+ );
+
+ let cursor = entry.buffer.hit(point.x, point.y)?;
+
+ Some(Hit::CharOffset(cursor.index))
+ }
+}
+
+fn measure(buffer: &cosmic_text::Buffer) -> Size {
+ let (width, total_lines) = buffer
+ .layout_runs()
+ .fold((0.0, 0usize), |(width, total_lines), run| {
+ (run.line_w.max(width), total_lines + 1)
+ });
+
+ Size::new(width, total_lines as f32 * buffer.metrics().line_height)
+}
+
+fn to_family(family: font::Family) -> cosmic_text::Family<'static> {
+ match family {
+ font::Family::Name(name) => cosmic_text::Family::Name(name),
+ font::Family::SansSerif => cosmic_text::Family::SansSerif,
+ font::Family::Serif => cosmic_text::Family::Serif,
+ font::Family::Cursive => cosmic_text::Family::Cursive,
+ font::Family::Fantasy => cosmic_text::Family::Fantasy,
+ font::Family::Monospace => cosmic_text::Family::Monospace,
+ }
+}
+
+fn to_weight(weight: font::Weight) -> cosmic_text::Weight {
+ match weight {
+ font::Weight::Thin => cosmic_text::Weight::THIN,
+ font::Weight::ExtraLight => cosmic_text::Weight::EXTRA_LIGHT,
+ font::Weight::Light => cosmic_text::Weight::LIGHT,
+ font::Weight::Normal => cosmic_text::Weight::NORMAL,
+ font::Weight::Medium => cosmic_text::Weight::MEDIUM,
+ font::Weight::Semibold => cosmic_text::Weight::SEMIBOLD,
+ font::Weight::Bold => cosmic_text::Weight::BOLD,
+ font::Weight::ExtraBold => cosmic_text::Weight::EXTRA_BOLD,
+ font::Weight::Black => cosmic_text::Weight::BLACK,
+ }
+}
+
+fn to_stretch(stretch: font::Stretch) -> cosmic_text::Stretch {
+ match stretch {
+ font::Stretch::UltraCondensed => cosmic_text::Stretch::UltraCondensed,
+ font::Stretch::ExtraCondensed => cosmic_text::Stretch::ExtraCondensed,
+ font::Stretch::Condensed => cosmic_text::Stretch::Condensed,
+ font::Stretch::SemiCondensed => cosmic_text::Stretch::SemiCondensed,
+ font::Stretch::Normal => cosmic_text::Stretch::Normal,
+ font::Stretch::SemiExpanded => cosmic_text::Stretch::SemiExpanded,
+ font::Stretch::Expanded => cosmic_text::Stretch::Expanded,
+ font::Stretch::ExtraExpanded => cosmic_text::Stretch::ExtraExpanded,
+ font::Stretch::UltraExpanded => cosmic_text::Stretch::UltraExpanded,
+ }
+}
+
+fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping {
+ match shaping {
+ Shaping::Basic => cosmic_text::Shaping::Basic,
+ Shaping::Advanced => cosmic_text::Shaping::Advanced,
+ }
+}
+
+#[derive(Debug, Clone, Default)]
+struct GlyphCache {
+ entries: FxHashMap<
+ (cosmic_text::CacheKey, [u8; 3]),
+ (Vec<u32>, cosmic_text::Placement),
+ >,
+ recently_used: FxHashSet<(cosmic_text::CacheKey, [u8; 3])>,
+ trim_count: usize,
+}
+
+impl GlyphCache {
+ const TRIM_INTERVAL: usize = 300;
+
+ 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
+ }
+ }
+
+ entry.insert((buffer, image.placement));
+ }
+
+ 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.entries
+ .retain(|key, _| self.recently_used.contains(key));
+
+ self.recently_used.clear();
+
+ self.trim_count = 0;
+ } else {
+ self.trim_count += 1;
+ }
+ }
+}
+
+struct Cache {
+ entries: FxHashMap<KeyHash, Entry>,
+ measurements: FxHashMap<KeyHash, KeyHash>,
+ recently_used: FxHashSet<KeyHash>,
+ hasher: HashBuilder,
+ trim_count: usize,
+}
+
+struct Entry {
+ buffer: cosmic_text::Buffer,
+ bounds: Size,
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+type HashBuilder = twox_hash::RandomXxHashBuilder64;
+
+#[cfg(target_arch = "wasm32")]
+type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>;
+
+impl Cache {
+ const TRIM_INTERVAL: usize = 300;
+
+ fn new() -> Self {
+ Self {
+ entries: FxHashMap::default(),
+ measurements: FxHashMap::default(),
+ recently_used: FxHashSet::default(),
+ hasher: HashBuilder::default(),
+ trim_count: 0,
+ }
+ }
+
+ fn allocate(
+ &mut self,
+ font_system: &mut cosmic_text::FontSystem,
+ key: Key<'_>,
+ ) -> (KeyHash, &mut Entry) {
+ let hash = key.hash(self.hasher.build_hasher());
+
+ if let Some(hash) = self.measurements.get(&hash) {
+ let _ = self.recently_used.insert(*hash);
+
+ return (*hash, self.entries.get_mut(hash).unwrap());
+ }
+
+ if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) {
+ let metrics = cosmic_text::Metrics::new(key.size, key.size * 1.2);
+ let mut buffer = cosmic_text::Buffer::new(font_system, metrics);
+
+ buffer.set_size(
+ font_system,
+ key.bounds.width,
+ key.bounds.height.max(key.size * 1.2),
+ );
+ buffer.set_text(
+ font_system,
+ key.content,
+ cosmic_text::Attrs::new()
+ .family(to_family(key.font.family))
+ .weight(to_weight(key.font.weight))
+ .stretch(to_stretch(key.font.stretch)),
+ to_shaping(key.shaping),
+ );
+
+ let bounds = measure(&buffer);
+
+ let _ = entry.insert(Entry { buffer, bounds });
+
+ for bounds in [
+ bounds,
+ Size {
+ width: key.bounds.width,
+ ..bounds
+ },
+ ] {
+ if key.bounds != bounds {
+ let _ = self.measurements.insert(
+ Key { bounds, ..key }.hash(self.hasher.build_hasher()),
+ hash,
+ );
+ }
+ }
+ }
+
+ let _ = self.recently_used.insert(hash);
+
+ (hash, self.entries.get_mut(&hash).unwrap())
+ }
+
+ fn trim(&mut self) {
+ if self.trim_count > Self::TRIM_INTERVAL {
+ self.entries
+ .retain(|key, _| self.recently_used.contains(key));
+ self.measurements
+ .retain(|_, value| self.recently_used.contains(value));
+
+ self.recently_used.clear();
+
+ self.trim_count = 0;
+ } else {
+ self.trim_count += 1;
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+struct Key<'a> {
+ content: &'a str,
+ size: f32,
+ line_height: f32,
+ font: Font,
+ bounds: Size,
+ shaping: Shaping,
+}
+
+impl Key<'_> {
+ fn hash<H: Hasher>(self, mut hasher: H) -> KeyHash {
+ self.content.hash(&mut hasher);
+ self.size.to_bits().hash(&mut hasher);
+ self.line_height.to_bits().hash(&mut hasher);
+ self.font.hash(&mut hasher);
+ self.bounds.width.to_bits().hash(&mut hasher);
+ self.bounds.height.to_bits().hash(&mut hasher);
+ self.shaping.hash(&mut hasher);
+
+ hasher.finish()
+ }
+}
+
+type KeyHash = u64;
diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs
new file mode 100644
index 00000000..433ca0f5
--- /dev/null
+++ b/tiny_skia/src/vector.rs
@@ -0,0 +1,195 @@
+use crate::core::svg::{Data, Handle};
+use crate::core::{Color, Rectangle, Size};
+
+use resvg::usvg;
+use rustc_hash::{FxHashMap, FxHashSet};
+
+use std::cell::RefCell;
+use std::collections::hash_map;
+use std::fs;
+
+pub struct Pipeline {
+ cache: RefCell<Cache>,
+}
+
+impl Pipeline {
+ pub fn new() -> Self {
+ Self {
+ cache: RefCell::new(Cache::default()),
+ }
+ }
+
+ pub fn viewport_dimensions(&self, handle: &Handle) -> Size<u32> {
+ self.cache
+ .borrow_mut()
+ .viewport_dimensions(handle)
+ .unwrap_or(Size::new(0, 0))
+ }
+
+ pub fn draw(
+ &mut self,
+ handle: &Handle,
+ color: Option<Color>,
+ bounds: Rectangle,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: Option<&tiny_skia::Mask>,
+ ) {
+ if let Some(image) = self.cache.borrow_mut().draw(
+ handle,
+ color,
+ Size::new(bounds.width as u32, bounds.height as u32),
+ ) {
+ pixels.draw_pixmap(
+ bounds.x as i32,
+ bounds.y as i32,
+ image,
+ &tiny_skia::PixmapPaint::default(),
+ tiny_skia::Transform::identity(),
+ clip_mask,
+ );
+ }
+ }
+
+ pub fn trim_cache(&mut self) {
+ self.cache.borrow_mut().trim();
+ }
+}
+
+#[derive(Default)]
+struct Cache {
+ trees: FxHashMap<u64, Option<resvg::usvg::Tree>>,
+ tree_hits: FxHashSet<u64>,
+ rasters: FxHashMap<RasterKey, tiny_skia::Pixmap>,
+ raster_hits: FxHashSet<RasterKey>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+struct RasterKey {
+ id: u64,
+ color: Option<[u8; 4]>,
+ size: Size<u32>,
+}
+
+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) {
+ let svg = match handle.data() {
+ Data::Path(path) => {
+ fs::read_to_string(path).ok().and_then(|contents| {
+ usvg::Tree::from_str(
+ &contents,
+ &usvg::Options::default(),
+ )
+ .ok()
+ })
+ }
+ Data::Bytes(bytes) => {
+ usvg::Tree::from_data(bytes, &usvg::Options::default()).ok()
+ }
+ };
+
+ entry.insert(svg);
+ }
+
+ self.tree_hits.insert(id);
+ self.trees.get(&id).unwrap().as_ref()
+ }
+
+ fn viewport_dimensions(&mut self, handle: &Handle) -> Option<Size<u32>> {
+ let tree = self.load(handle)?;
+
+ Some(Size::new(
+ tree.size.width() as u32,
+ tree.size.height() as u32,
+ ))
+ }
+
+ fn draw(
+ &mut self,
+ handle: &Handle,
+ color: Option<Color>,
+ size: Size<u32>,
+ ) -> Option<tiny_skia::PixmapRef<'_>> {
+ 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()
+ };
+
+ resvg::Tree::from_usvg(tree).render(transform, &mut image.as_mut());
+
+ if let Some([r, g, b, _]) = key.color {
+ // Apply color filter
+ for pixel in
+ bytemuck::cast_slice_mut::<u8, u32>(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::<u8, u32>(image.data_mut())
+ {
+ *pixel = *pixel & 0xFF00FF00
+ | ((0x000000FF & *pixel) << 16)
+ | ((0x00FF0000 & *pixel) >> 16);
+ }
+ }
+
+ self.rasters.insert(key, image);
+ }
+
+ 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();
+ }
+}
diff --git a/tiny_skia/src/window.rs b/tiny_skia/src/window.rs
new file mode 100644
index 00000000..d8d9378e
--- /dev/null
+++ b/tiny_skia/src/window.rs
@@ -0,0 +1,3 @@
+pub mod compositor;
+
+pub use compositor::{Compositor, Surface};
diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs
new file mode 100644
index 00000000..775cf9e5
--- /dev/null
+++ b/tiny_skia/src/window/compositor.rs
@@ -0,0 +1,228 @@
+use crate::core::{Color, Rectangle, Size};
+use crate::graphics::compositor::{self, Information};
+use crate::graphics::damage;
+use crate::graphics::{Error, Viewport};
+use crate::{Backend, Primitive, Renderer, Settings};
+
+use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
+use std::marker::PhantomData;
+
+pub struct Compositor<Theme> {
+ _theme: PhantomData<Theme>,
+}
+
+pub struct Surface {
+ window: softbuffer::GraphicsContext,
+ buffer: Vec<u32>,
+ clip_mask: tiny_skia::Mask,
+ primitives: Option<Vec<Primitive>>,
+ background_color: Color,
+}
+
+impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
+ type Settings = Settings;
+ type Renderer = Renderer<Theme>;
+ type Surface = Surface;
+
+ fn new<W: HasRawWindowHandle + HasRawDisplayHandle>(
+ settings: Self::Settings,
+ _compatible_window: Option<&W>,
+ ) -> Result<(Self, Self::Renderer), Error> {
+ let (compositor, backend) = new(settings);
+
+ Ok((compositor, Renderer::new(backend)))
+ }
+
+ fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(
+ &mut self,
+ window: &W,
+ width: u32,
+ height: u32,
+ ) -> Surface {
+ let window =
+ unsafe { softbuffer::GraphicsContext::new(window, window) }
+ .expect("Create softbuffer for window");
+
+ 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,
+ }
+ }
+
+ fn configure_surface(
+ &mut self,
+ surface: &mut Surface,
+ width: u32,
+ 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 {
+ Information {
+ adapter: String::from("CPU"),
+ backend: String::from("tiny-skia"),
+ }
+ }
+
+ fn present<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ surface: &mut Self::Surface,
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+ ) -> Result<(), compositor::SurfaceError> {
+ renderer.with_primitives(|backend, primitives| {
+ present(
+ backend,
+ surface,
+ primitives,
+ viewport,
+ background_color,
+ overlay,
+ )
+ })
+ }
+
+ fn screenshot<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ surface: &mut Self::Surface,
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+ ) -> Vec<u8> {
+ renderer.with_primitives(|backend, primitives| {
+ screenshot(
+ surface,
+ backend,
+ primitives,
+ viewport,
+ background_color,
+ overlay,
+ )
+ })
+ }
+}
+
+pub fn new<Theme>(settings: Settings) -> (Compositor<Theme>, Backend) {
+ (
+ Compositor {
+ _theme: PhantomData,
+ },
+ Backend::new(settings),
+ )
+}
+
+pub fn present<T: AsRef<str>>(
+ backend: &mut Backend,
+ surface: &mut Surface,
+ primitives: &[Primitive],
+ viewport: &Viewport,
+ background_color: Color,
+ 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 pixels,
+ &mut surface.clip_mask,
+ primitives,
+ viewport,
+ &damage,
+ background_color,
+ overlay,
+ );
+
+ surface.window.set_buffer(
+ &surface.buffer,
+ physical_size.width as u16,
+ physical_size.height as u16,
+ );
+
+ Ok(())
+}
+
+pub fn screenshot<T: AsRef<str>>(
+ surface: &mut Surface,
+ backend: &mut Backend,
+ primitives: &[Primitive],
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+) -> Vec<u8> {
+ let size = viewport.physical_size();
+
+ let mut offscreen_buffer: Vec<u32> =
+ vec![0; size.width as usize * size.height as usize];
+
+ backend.draw(
+ &mut tiny_skia::PixmapMut::from_bytes(
+ bytemuck::cast_slice_mut(&mut offscreen_buffer),
+ size.width,
+ size.height,
+ )
+ .expect("Create offscreen pixel map"),
+ &mut surface.clip_mask,
+ primitives,
+ viewport,
+ &[Rectangle::with_size(Size::new(
+ size.width as f32,
+ size.height as f32,
+ ))],
+ background_color,
+ overlay,
+ );
+
+ offscreen_buffer.iter().fold(
+ Vec::with_capacity(offscreen_buffer.len() * 4),
+ |mut acc, pixel| {
+ const A_MASK: u32 = 0xFF_00_00_00;
+ const R_MASK: u32 = 0x00_FF_00_00;
+ const G_MASK: u32 = 0x00_00_FF_00;
+ const B_MASK: u32 = 0x00_00_00_FF;
+
+ let a = ((A_MASK & pixel) >> 24) as u8;
+ let r = ((R_MASK & pixel) >> 16) as u8;
+ let g = ((G_MASK & pixel) >> 8) as u8;
+ let b = (B_MASK & pixel) as u8;
+
+ acc.extend([r, g, b, a]);
+ acc
+ },
+ )
+}