summaryrefslogtreecommitdiffstats
path: root/tiny_skia/src
diff options
context:
space:
mode:
Diffstat (limited to 'tiny_skia/src')
-rw-r--r--tiny_skia/src/backend.rs1020
-rw-r--r--tiny_skia/src/engine.rs856
-rw-r--r--tiny_skia/src/geometry.rs171
-rw-r--r--tiny_skia/src/layer.rs341
-rw-r--r--tiny_skia/src/lib.rs405
-rw-r--r--tiny_skia/src/primitive.rs30
-rw-r--r--tiny_skia/src/raster.rs12
-rw-r--r--tiny_skia/src/settings.rs14
-rw-r--r--tiny_skia/src/text.rs31
-rw-r--r--tiny_skia/src/vector.rs21
-rw-r--r--tiny_skia/src/window/compositor.rs95
11 files changed, 1817 insertions, 1179 deletions
diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs
deleted file mode 100644
index b6487b38..00000000
--- a/tiny_skia/src/backend.rs
+++ /dev/null
@@ -1,1020 +0,0 @@
-use crate::core::{
- Background, Color, Gradient, Rectangle, Size, Transformation, Vector,
-};
-use crate::graphics::backend;
-use crate::graphics::text;
-use crate::graphics::{Damage, Viewport};
-use crate::primitive::{self, Primitive};
-
-use std::borrow::Cow;
-
-pub struct Backend {
- 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() -> Self {
- Self {
- 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,
- Transformation::IDENTITY,
- );
- }
-
- 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,
- transformation: Transformation,
- ) {
- match primitive {
- Primitive::Quad {
- bounds,
- background,
- border,
- shadow,
- } => {
- debug_assert!(
- bounds.width.is_normal(),
- "Quad with non-normal width!"
- );
- debug_assert!(
- bounds.height.is_normal(),
- "Quad with non-normal height!"
- );
-
- let physical_bounds = (*bounds * transformation) * 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 = into_transform(transformation)
- .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 = <[f32; 4]>::from(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);
-
- if shadow.color.a > 0.0 {
- let shadow_bounds = (Rectangle {
- x: bounds.x + shadow.offset.x - shadow.blur_radius,
- y: bounds.y + shadow.offset.y - shadow.blur_radius,
- width: bounds.width + shadow.blur_radius * 2.0,
- height: bounds.height + shadow.blur_radius * 2.0,
- } * transformation)
- * scale_factor;
-
- let radii = fill_border_radius
- .into_iter()
- .map(|radius| radius * scale_factor)
- .collect::<Vec<_>>();
- 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
- * scale_factor)
- - half_width,
- y - physical_bounds.position().y
- - (shadow.offset.y
- * scale_factor)
- - half_height,
- ),
- size,
- &radii,
- );
- let shadow_alpha = 1.0
- - smoothstep(
- -shadow.blur_radius * scale_factor,
- shadow.blur_radius * 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(),
- None,
- );
- }
- }
-
- 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 = <[f32; 4]>::from(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::Paragraph {
- paragraph,
- position,
- color,
- clip_bounds: _, // TODO: Support text clip bounds
- } => {
- let physical_bounds =
- Rectangle::new(*position, paragraph.min_bounds)
- * transformation
- * 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_paragraph(
- paragraph,
- *position,
- *color,
- scale_factor,
- pixels,
- clip_mask,
- transformation,
- );
- }
- Primitive::Editor {
- editor,
- position,
- color,
- clip_bounds: _, // TODO: Support text clip bounds
- } => {
- let physical_bounds = Rectangle::new(*position, editor.bounds)
- * transformation
- * 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_editor(
- editor,
- *position,
- *color,
- scale_factor,
- pixels,
- clip_mask,
- transformation,
- );
- }
- Primitive::Text {
- content,
- bounds,
- color,
- size,
- line_height,
- font,
- horizontal_alignment,
- vertical_alignment,
- shaping,
- clip_bounds: _, // TODO: Support text clip bounds
- } => {
- let physical_bounds =
- primitive.bounds() * transformation * 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_cached(
- content,
- *bounds,
- *color,
- *size,
- *line_height,
- *font,
- *horizontal_alignment,
- *vertical_alignment,
- *shaping,
- scale_factor,
- pixels,
- clip_mask,
- transformation,
- );
- }
- Primitive::RawText(text::Raw {
- buffer,
- position,
- color,
- clip_bounds: _, // TODO: Support text clip bounds
- }) => {
- let Some(buffer) = buffer.upgrade() else {
- return;
- };
-
- let (width, height) = buffer.size();
-
- let physical_bounds =
- Rectangle::new(*position, Size::new(width, height))
- * transformation
- * 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_raw(
- &buffer,
- *position,
- *color,
- scale_factor,
- pixels,
- clip_mask,
- transformation,
- );
- }
- #[cfg(feature = "image")]
- Primitive::Image {
- handle,
- filter_method,
- bounds,
- } => {
- let physical_bounds = (*bounds * transformation) * 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 = into_transform(transformation)
- .post_scale(scale_factor, scale_factor);
-
- self.raster_pipeline.draw(
- handle,
- *filter_method,
- *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 * transformation) * 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 * transformation) * 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,
- }) => {
- let bounds = path.bounds();
-
- let physical_bounds = (Rectangle {
- x: bounds.x(),
- y: bounds.y(),
- width: bounds.width(),
- height: bounds.height(),
- } * transformation)
- * 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,
- into_transform(transformation)
- .post_scale(scale_factor, scale_factor),
- clip_mask,
- );
- }
- Primitive::Custom(primitive::Custom::Stroke {
- path,
- paint,
- stroke,
- }) => {
- let bounds = path.bounds();
-
- let physical_bounds = (Rectangle {
- x: bounds.x(),
- y: bounds.y(),
- width: bounds.width().max(1.0),
- height: bounds.height().max(1.0),
- } * transformation)
- * 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,
- into_transform(transformation)
- .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,
- transformation,
- );
- }
- }
- Primitive::Transform {
- transformation: new_transformation,
- content,
- } => {
- self.draw_primitive(
- content,
- pixels,
- clip_mask,
- clip_bounds,
- scale_factor,
- transformation * *new_transformation,
- );
- }
- Primitive::Clip { bounds, content } => {
- let bounds = (*bounds * transformation) * scale_factor;
-
- if bounds == clip_bounds {
- self.draw_primitive(
- content,
- pixels,
- clip_mask,
- bounds,
- scale_factor,
- transformation,
- );
- } 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,
- transformation,
- );
-
- adjust_clip_mask(clip_mask, clip_bounds);
- }
- }
- Primitive::Cache { content } => {
- self.draw_primitive(
- content,
- pixels,
- clip_mask,
- clip_bounds,
- scale_factor,
- transformation,
- );
- }
- }
- }
-}
-
-impl Default for Backend {
- fn default() -> Self {
- Self::new()
- }
-}
-
-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 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(),
- );
-}
-
-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
-}
-
-impl iced_graphics::Backend for Backend {
- type Primitive = primitive::Custom;
-}
-
-impl backend::Text for Backend {
- 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,
- ) -> crate::core::Size<u32> {
- self.raster_pipeline.dimensions(handle)
- }
-}
-
-#[cfg(feature = "svg")]
-impl backend::Svg for Backend {
- fn viewport_dimensions(
- &self,
- handle: &crate::core::svg::Handle,
- ) -> crate::core::Size<u32> {
- self.vector_pipeline.viewport_dimensions(handle)
- }
-}
diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs
new file mode 100644
index 00000000..028b304f
--- /dev/null
+++ b/tiny_skia/src/engine.rs
@@ -0,0 +1,856 @@
+use crate::core::renderer::Quad;
+use crate::core::{
+ Background, Color, Gradient, Rectangle, Size, Transformation, Vector,
+};
+use crate::graphics::{Image, Text};
+use crate::text;
+use crate::Primitive;
+
+#[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,
+ ) {
+ debug_assert!(
+ quad.bounds.width.is_normal(),
+ "Quad with non-normal width!"
+ );
+ debug_assert!(
+ quad.bounds.height.is_normal(),
+ "Quad with non-normal height!"
+ );
+
+ let physical_bounds = quad.bounds * transformation;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ 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::<Vec<_>>();
+ 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(),
+ None,
+ );
+ }
+ }
+
+ 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<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: 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: _, // TODO
+ transformation: local_transformation,
+ } => {
+ let transformation = transformation * *local_transformation;
+
+ let physical_bounds =
+ Rectangle::new(*position, paragraph.min_bounds)
+ * 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_paragraph(
+ paragraph,
+ *position,
+ *color,
+ pixels,
+ clip_mask,
+ transformation,
+ );
+ }
+ Text::Editor {
+ editor,
+ position,
+ color,
+ clip_bounds: _, // TODO
+ transformation: local_transformation,
+ } => {
+ let transformation = transformation * *local_transformation;
+
+ let physical_bounds =
+ Rectangle::new(*position, editor.bounds) * 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_editor(
+ editor,
+ *position,
+ *color,
+ pixels,
+ clip_mask,
+ transformation,
+ );
+ }
+ Text::Cached {
+ content,
+ bounds,
+ color,
+ size,
+ line_height,
+ font,
+ horizontal_alignment,
+ vertical_alignment,
+ shaping,
+ clip_bounds: text_bounds, // TODO
+ } => {
+ let physical_bounds = *text_bounds * 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_cached(
+ content,
+ *bounds,
+ *color,
+ *size,
+ *line_height,
+ *font,
+ *horizontal_alignment,
+ *vertical_alignment,
+ *shaping,
+ 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, 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,
+ layer_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
+ };
+
+ let Some(clip_bounds) =
+ layer_bounds.intersection(&physical_bounds)
+ else {
+ return;
+ };
+
+ let clip_mask =
+ (physical_bounds != 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(),
+ y: bounds.y(),
+ width: bounds.width(),
+ height: bounds.height(),
+ } * transformation
+ };
+
+ let Some(clip_bounds) =
+ layer_bounds.intersection(&physical_bounds)
+ else {
+ return;
+ };
+
+ let clip_mask =
+ (physical_bounds != 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 {
+ handle,
+ filter_method,
+ bounds,
+ rotation,
+ opacity,
+ } => {
+ 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(*rotation);
+
+ let transform = into_transform(_transformation).post_rotate_at(
+ radians.to_degrees(),
+ center.x,
+ center.y,
+ );
+
+ self.raster_pipeline.draw(
+ handle,
+ *filter_method,
+ *bounds,
+ *opacity,
+ _pixels,
+ transform,
+ clip_mask,
+ );
+ }
+ #[cfg(feature = "svg")]
+ Image::Vector {
+ handle,
+ color,
+ bounds,
+ rotation,
+ opacity,
+ } => {
+ 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(*rotation);
+
+ let transform = into_transform(_transformation).post_rotate_at(
+ radians.to_degrees(),
+ center.x,
+ center.y,
+ );
+
+ self.vector_pipeline.draw(
+ handle,
+ *color,
+ physical_bounds,
+ *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 = {
+ 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(),
+ );
+}
diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs
index 16787f89..02b6e1b9 100644
--- a/tiny_skia/src/geometry.rs
+++ b/tiny_skia/src/geometry.rs
@@ -1,47 +1,102 @@
use crate::core::text::LineHeight;
-use crate::core::{
- Pixels, Point, Radians, Rectangle, Size, Transformation, Vector,
-};
+use crate::core::{Pixels, Point, Radians, Rectangle, Size, Vector};
+use crate::graphics::cache::{self, Cached};
use crate::graphics::geometry::fill::{self, Fill};
use crate::graphics::geometry::stroke::{self, Stroke};
-use crate::graphics::geometry::{Path, Style, Text};
-use crate::graphics::Gradient;
-use crate::primitive::{self, Primitive};
+use crate::graphics::geometry::{self, Path, Style};
+use crate::graphics::{Gradient, Text};
+use crate::Primitive;
+
+use std::rc::Rc;
+
+#[derive(Debug)]
+pub enum Geometry {
+ Live {
+ text: Vec<Text>,
+ primitives: Vec<Primitive>,
+ clip_bounds: Rectangle,
+ },
+ Cache(Cache),
+}
+
+#[derive(Debug, Clone)]
+pub struct Cache {
+ pub text: Rc<[Text]>,
+ pub primitives: Rc<[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>) -> Cache {
+ match self {
+ Self::Live {
+ primitives,
+ text,
+ clip_bounds,
+ } => Cache {
+ primitives: Rc::from(primitives),
+ text: Rc::from(text),
+ clip_bounds,
+ },
+ Self::Cache(cache) => cache,
+ }
+ }
+}
+#[derive(Debug)]
pub struct Frame {
- size: Size,
+ clip_bounds: Rectangle,
transform: tiny_skia::Transform,
stack: Vec<tiny_skia::Transform>,
primitives: Vec<Primitive>,
+ text: Vec<Text>,
}
impl Frame {
pub fn new(size: Size) -> Self {
+ Self::with_clip(Rectangle::with_size(size))
+ }
+
+ pub fn with_clip(clip_bounds: Rectangle) -> Self {
Self {
- size,
- transform: tiny_skia::Transform::identity(),
+ clip_bounds,
stack: Vec::new(),
primitives: Vec::new(),
+ text: Vec::new(),
+ transform: tiny_skia::Transform::from_translate(
+ clip_bounds.x,
+ clip_bounds.y,
+ ),
}
}
+}
+
+impl geometry::frame::Backend for Frame {
+ type Geometry = Geometry;
- pub fn width(&self) -> f32 {
- self.size.width
+ fn width(&self) -> f32 {
+ self.clip_bounds.width
}
- pub fn height(&self) -> f32 {
- self.size.height
+ fn height(&self) -> f32 {
+ self.clip_bounds.height
}
- pub fn size(&self) -> Size {
- self.size
+ fn size(&self) -> Size {
+ self.clip_bounds.size()
}
- pub fn center(&self) -> Point {
- Point::new(self.size.width / 2.0, self.size.height / 2.0)
+ fn center(&self) -> Point {
+ Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.height / 2.0)
}
- pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
+ fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
let Some(path) =
convert_path(path).and_then(|path| path.transform(self.transform))
else {
@@ -53,15 +108,14 @@ impl Frame {
let mut paint = into_paint(fill.style);
paint.shader.transform(self.transform);
- self.primitives
- .push(Primitive::Custom(primitive::Custom::Fill {
- path,
- paint,
- rule: into_fill_rule(fill.rule),
- }));
+ self.primitives.push(Primitive::Fill {
+ path,
+ paint,
+ rule: into_fill_rule(fill.rule),
+ });
}
- pub fn fill_rectangle(
+ fn fill_rectangle(
&mut self,
top_left: Point,
size: Size,
@@ -81,15 +135,14 @@ impl Frame {
};
paint.shader.transform(self.transform);
- self.primitives
- .push(Primitive::Custom(primitive::Custom::Fill {
- path,
- paint,
- rule: into_fill_rule(fill.rule),
- }));
+ self.primitives.push(Primitive::Fill {
+ path,
+ paint,
+ rule: into_fill_rule(fill.rule),
+ });
}
- pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
+ fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
let Some(path) =
convert_path(path).and_then(|path| path.transform(self.transform))
else {
@@ -102,20 +155,19 @@ impl Frame {
let mut paint = into_paint(stroke.style);
paint.shader.transform(self.transform);
- self.primitives
- .push(Primitive::Custom(primitive::Custom::Stroke {
- path,
- paint,
- stroke: skia_stroke,
- }));
+ self.primitives.push(Primitive::Stroke {
+ path,
+ paint,
+ stroke: skia_stroke,
+ });
}
- pub fn fill_text(&mut self, text: impl Into<Text>) {
+ fn fill_text(&mut self, text: impl Into<geometry::Text>) {
let text = text.into();
let (scale_x, scale_y) = self.transform.get_scale();
- if self.transform.is_scale_translate()
+ if !self.transform.has_skew()
&& scale_x == scale_y
&& scale_x > 0.0
&& scale_y > 0.0
@@ -157,12 +209,12 @@ impl Frame {
};
// TODO: Honor layering!
- self.primitives.push(Primitive::Text {
+ self.text.push(Text::Cached {
content: text.content,
bounds,
color: text.color,
size,
- line_height,
+ line_height: line_height.to_absolute(size),
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
@@ -174,50 +226,51 @@ impl Frame {
}
}
- pub fn push_transform(&mut self) {
+ fn push_transform(&mut self) {
self.stack.push(self.transform);
}
- pub fn pop_transform(&mut self) {
+ 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::Transform {
- transformation: Transformation::translate(at.x, at.y),
- content: Box::new(frame.into_primitive()),
- });
+ fn draft(&mut self, clip_bounds: Rectangle) -> Self {
+ Self::with_clip(clip_bounds)
+ }
+
+ fn paste(&mut self, frame: Self, _at: Point) {
+ self.primitives.extend(frame.primitives);
+ self.text.extend(frame.text);
}
- pub fn translate(&mut self, translation: Vector) {
+ fn translate(&mut self, translation: Vector) {
self.transform =
self.transform.pre_translate(translation.x, translation.y);
}
- pub fn rotate(&mut self, angle: impl Into<Radians>) {
+ fn rotate(&mut self, angle: impl Into<Radians>) {
self.transform = self.transform.pre_concat(
tiny_skia::Transform::from_rotate(angle.into().0.to_degrees()),
);
}
- pub fn scale(&mut self, scale: impl Into<f32>) {
+ fn scale(&mut self, scale: impl Into<f32>) {
let scale = scale.into();
self.scale_nonuniform(Vector { x: scale, y: scale });
}
- pub fn scale_nonuniform(&mut self, scale: impl Into<Vector>) {
+ fn scale_nonuniform(&mut self, scale: impl Into<Vector>) {
let scale = scale.into();
self.transform = self.transform.pre_scale(scale.x, scale.y);
}
- pub fn into_primitive(self) -> Primitive {
- Primitive::Clip {
- bounds: Rectangle::new(Point::ORIGIN, self.size),
- content: Box::new(Primitive::Group {
- primitives: self.primitives,
- }),
+ fn into_geometry(self) -> Geometry {
+ Geometry::Live {
+ primitives: self.primitives,
+ text: self.text,
+ clip_bounds: self.clip_bounds,
}
}
}
diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs
new file mode 100644
index 00000000..48fca1d8
--- /dev/null
+++ b/tiny_skia/src/layer.rs
@@ -0,0 +1,341 @@
+use crate::core::{
+ image, renderer::Quad, svg, Background, Color, Point, Radians, Rectangle,
+ Transformation,
+};
+use crate::graphics::damage;
+use crate::graphics::layer;
+use crate::graphics::text::{Editor, Paragraph, Text};
+use crate::graphics::{self, Image};
+use crate::Primitive;
+
+use std::rc::Rc;
+
+pub type Stack = layer::Stack<Layer>;
+
+#[derive(Debug, Clone)]
+pub struct Layer {
+ pub bounds: Rectangle,
+ pub quads: Vec<(Quad, Background)>,
+ pub primitives: Vec<Item<Primitive>>,
+ pub text: Vec<Item<Text>>,
+ pub images: Vec<Image>,
+}
+
+impl Layer {
+ pub fn draw_quad(
+ &mut self,
+ mut quad: Quad,
+ background: Background,
+ transformation: Transformation,
+ ) {
+ quad.bounds = quad.bounds * transformation;
+ self.quads.push((quad, background));
+ }
+
+ pub fn draw_paragraph(
+ &mut self,
+ paragraph: &Paragraph,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ let paragraph = Text::Paragraph {
+ paragraph: paragraph.downgrade(),
+ position,
+ color,
+ clip_bounds,
+ transformation,
+ };
+
+ self.text.push(Item::Live(paragraph));
+ }
+
+ pub fn draw_editor(
+ &mut self,
+ editor: &Editor,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ let editor = Text::Editor {
+ editor: editor.downgrade(),
+ position,
+ color,
+ clip_bounds,
+ transformation,
+ };
+
+ self.text.push(Item::Live(editor));
+ }
+
+ pub fn draw_text(
+ &mut self,
+ text: crate::core::Text,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ let text = Text::Cached {
+ content: text.content,
+ bounds: Rectangle::new(position, text.bounds) * transformation,
+ color,
+ size: text.size * transformation.scale_factor(),
+ line_height: text.line_height.to_absolute(text.size)
+ * transformation.scale_factor(),
+ font: text.font,
+ horizontal_alignment: text.horizontal_alignment,
+ vertical_alignment: text.vertical_alignment,
+ shaping: text.shaping,
+ clip_bounds: clip_bounds * transformation,
+ };
+
+ self.text.push(Item::Live(text));
+ }
+
+ pub fn draw_text_group(
+ &mut self,
+ text: Vec<Text>,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ self.text
+ .push(Item::Group(text, clip_bounds, transformation));
+ }
+
+ pub fn draw_text_cache(
+ &mut self,
+ text: Rc<[Text]>,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ self.text
+ .push(Item::Cached(text, clip_bounds, transformation));
+ }
+
+ pub fn draw_image(
+ &mut self,
+ handle: image::Handle,
+ filter_method: image::FilterMethod,
+ bounds: Rectangle,
+ transformation: Transformation,
+ rotation: Radians,
+ opacity: f32,
+ ) {
+ let image = Image::Raster {
+ handle,
+ filter_method,
+ bounds: bounds * transformation,
+ rotation,
+ opacity,
+ };
+
+ self.images.push(image);
+ }
+
+ pub fn draw_svg(
+ &mut self,
+ handle: svg::Handle,
+ color: Option<Color>,
+ bounds: Rectangle,
+ transformation: Transformation,
+ rotation: Radians,
+ opacity: f32,
+ ) {
+ let svg = Image::Vector {
+ handle,
+ color,
+ bounds: bounds * transformation,
+ rotation,
+ opacity,
+ };
+
+ self.images.push(svg);
+ }
+
+ pub fn draw_primitive_group(
+ &mut self,
+ primitives: Vec<Primitive>,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ self.primitives.push(Item::Group(
+ primitives,
+ clip_bounds,
+ transformation,
+ ));
+ }
+
+ pub fn draw_primitive_cache(
+ &mut self,
+ primitives: Rc<[Primitive]>,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ self.primitives.push(Item::Cached(
+ primitives,
+ clip_bounds,
+ transformation,
+ ));
+ }
+
+ pub fn damage(previous: &Self, current: &Self) -> Vec<Rectangle> {
+ if previous.bounds != current.bounds {
+ return vec![previous.bounds, current.bounds];
+ }
+
+ let mut damage = damage::list(
+ &previous.quads,
+ &current.quads,
+ |(quad, _)| {
+ quad.bounds
+ .expand(1.0)
+ .intersection(&current.bounds)
+ .into_iter()
+ .collect()
+ },
+ |(quad_a, background_a), (quad_b, background_b)| {
+ quad_a == quad_b && background_a == background_b
+ },
+ );
+
+ let text = damage::diff(
+ &previous.text,
+ &current.text,
+ |item| {
+ item.as_slice()
+ .iter()
+ .filter_map(Text::visible_bounds)
+ .map(|bounds| bounds * item.transformation())
+ .collect()
+ },
+ |text_a, text_b| {
+ damage::list(
+ text_a.as_slice(),
+ text_b.as_slice(),
+ |text| {
+ text.visible_bounds()
+ .into_iter()
+ .map(|bounds| bounds * text_a.transformation())
+ .collect()
+ },
+ |text_a, text_b| text_a == text_b,
+ )
+ },
+ );
+
+ let primitives = damage::list(
+ &previous.primitives,
+ &current.primitives,
+ |item| match item {
+ Item::Live(primitive) => vec![primitive.visible_bounds()],
+ Item::Group(primitives, group_bounds, transformation) => {
+ primitives
+ .as_slice()
+ .iter()
+ .map(Primitive::visible_bounds)
+ .map(|bounds| bounds * *transformation)
+ .filter_map(|bounds| bounds.intersection(group_bounds))
+ .collect()
+ }
+ Item::Cached(_, bounds, _) => {
+ vec![*bounds]
+ }
+ },
+ |primitive_a, primitive_b| match (primitive_a, primitive_b) {
+ (
+ Item::Cached(cache_a, bounds_a, transformation_a),
+ Item::Cached(cache_b, bounds_b, transformation_b),
+ ) => {
+ Rc::ptr_eq(cache_a, cache_b)
+ && bounds_a == bounds_b
+ && transformation_a == transformation_b
+ }
+ _ => false,
+ },
+ );
+
+ let images = damage::list(
+ &previous.images,
+ &current.images,
+ |image| vec![image.bounds().expand(1.0)],
+ Image::eq,
+ );
+
+ damage.extend(text);
+ damage.extend(primitives);
+ damage.extend(images);
+ damage
+ }
+}
+
+impl Default for Layer {
+ fn default() -> Self {
+ Self {
+ bounds: Rectangle::INFINITE,
+ quads: Vec::new(),
+ primitives: Vec::new(),
+ text: Vec::new(),
+ images: Vec::new(),
+ }
+ }
+}
+
+impl graphics::Layer for Layer {
+ fn with_bounds(bounds: Rectangle) -> Self {
+ Self {
+ bounds,
+ ..Self::default()
+ }
+ }
+
+ fn flush(&mut self) {}
+
+ fn resize(&mut self, bounds: graphics::core::Rectangle) {
+ self.bounds = bounds;
+ }
+
+ fn reset(&mut self) {
+ self.bounds = Rectangle::INFINITE;
+
+ self.quads.clear();
+ self.primitives.clear();
+ self.text.clear();
+ self.images.clear();
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum Item<T> {
+ Live(T),
+ Group(Vec<T>, Rectangle, Transformation),
+ Cached(Rc<[T]>, Rectangle, Transformation),
+}
+
+impl<T> Item<T> {
+ pub fn transformation(&self) -> Transformation {
+ match self {
+ Item::Live(_) => Transformation::IDENTITY,
+ Item::Group(_, _, transformation)
+ | Item::Cached(_, _, transformation) => *transformation,
+ }
+ }
+
+ pub fn clip_bounds(&self) -> Rectangle {
+ match self {
+ Item::Live(_) => Rectangle::INFINITE,
+ Item::Group(_, clip_bounds, _)
+ | Item::Cached(_, clip_bounds, _) => *clip_bounds,
+ }
+ }
+
+ pub fn as_slice(&self) -> &[T] {
+ match self {
+ Item::Live(item) => std::slice::from_ref(item),
+ Item::Group(group, _, _) => group.as_slice(),
+ Item::Cached(cache, _, _) => cache,
+ }
+ }
+}
diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs
index e7294f9b..1aabff00 100644
--- a/tiny_skia/src/lib.rs
+++ b/tiny_skia/src/lib.rs
@@ -1,9 +1,9 @@
-#![forbid(rust_2018_idioms)]
-#![deny(unsafe_code, unused_results, rustdoc::broken_intra_doc_links)]
+#![allow(missing_docs)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub mod window;
-mod backend;
+mod engine;
+mod layer;
mod primitive;
mod settings;
mod text;
@@ -20,12 +20,407 @@ pub mod geometry;
pub use iced_graphics as graphics;
pub use iced_graphics::core;
-pub use backend::Backend;
+pub use layer::Layer;
pub use primitive::Primitive;
pub use settings::Settings;
+#[cfg(feature = "geometry")]
+pub use geometry::Geometry;
+
+use crate::core::renderer;
+use crate::core::{
+ Background, Color, Font, Pixels, Point, Rectangle, Transformation,
+};
+use crate::engine::Engine;
+use crate::graphics::compositor;
+use crate::graphics::text::{Editor, Paragraph};
+use crate::graphics::Viewport;
+
/// 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 = iced_graphics::Renderer<Backend>;
+#[derive(Debug)]
+pub struct Renderer {
+ default_font: Font,
+ default_text_size: Pixels,
+ layers: layer::Stack,
+ engine: Engine, // TODO: Shared engine
+}
+
+impl Renderer {
+ pub fn new(default_font: Font, default_text_size: Pixels) -> Self {
+ Self {
+ default_font,
+ default_text_size,
+ layers: layer::Stack::new(),
+ engine: Engine::new(),
+ }
+ }
+
+ pub fn layers(&mut self) -> &[Layer] {
+ self.layers.flush();
+ self.layers.as_slice()
+ }
+
+ pub fn draw<T: AsRef<str>>(
+ &mut self,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: &mut tiny_skia::Mask,
+ 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(engine::into_color(
+ Color {
+ a: 0.1,
+ ..background_color
+ },
+ )),
+ anti_alias: false,
+ ..Default::default()
+ },
+ tiny_skia::FillRule::default(),
+ tiny_skia::Transform::identity(),
+ None,
+ );
+ }
+
+ self.layers.flush();
+
+ for &region in damage {
+ let region = region * scale_factor;
+
+ 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(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(clip_bounds) =
+ region.intersection(&(layer.bounds * scale_factor))
+ else {
+ continue;
+ };
+
+ engine::adjust_clip_mask(clip_mask, clip_bounds);
+
+ for (quad, background) in &layer.quads {
+ self.engine.draw_quad(
+ quad,
+ background,
+ Transformation::scale(scale_factor),
+ pixels,
+ clip_mask,
+ clip_bounds,
+ );
+ }
+
+ for group in &layer.primitives {
+ let Some(new_clip_bounds) = (group.clip_bounds()
+ * scale_factor)
+ .intersection(&clip_bounds)
+ else {
+ continue;
+ };
+
+ engine::adjust_clip_mask(clip_mask, new_clip_bounds);
+
+ for primitive in group.as_slice() {
+ self.engine.draw_primitive(
+ primitive,
+ group.transformation()
+ * Transformation::scale(scale_factor),
+ pixels,
+ clip_mask,
+ clip_bounds,
+ );
+ }
+
+ engine::adjust_clip_mask(clip_mask, clip_bounds);
+ }
+
+ for group in &layer.text {
+ for text in group.as_slice() {
+ self.engine.draw_text(
+ text,
+ group.transformation()
+ * Transformation::scale(scale_factor),
+ pixels,
+ clip_mask,
+ clip_bounds,
+ );
+ }
+ }
+
+ for image in &layer.images {
+ self.engine.draw_image(
+ image,
+ Transformation::scale(scale_factor),
+ pixels,
+ clip_mask,
+ clip_bounds,
+ );
+ }
+ }
+
+ if !overlay.is_empty() {
+ pixels.stroke_path(
+ &path,
+ &tiny_skia::Paint {
+ shader: tiny_skia::Shader::SolidColor(
+ engine::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.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<Background>,
+ ) {
+ let (layer, transformation) = self.layers.current_mut();
+ layer.draw_quad(quad, background.into(), transformation);
+ }
+
+ fn clear(&mut self) {
+ self.layers.clear();
+ }
+}
+
+impl core::text::Renderer for Renderer {
+ type Font = Font;
+ type Paragraph = Paragraph;
+ type Editor = Editor;
+
+ 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) -> Self::Font {
+ self.default_font
+ }
+
+ fn default_size(&self) -> Pixels {
+ self.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);
+ }
+}
+
+#[cfg(feature = "geometry")]
+impl graphics::geometry::Renderer for Renderer {
+ type Geometry = Geometry;
+ type Frame = geometry::Frame;
+
+ fn new_frame(&self, size: core::Size) -> Self::Frame {
+ geometry::Frame::new(size)
+ }
+
+ fn draw_geometry(&mut self, geometry: Self::Geometry) {
+ let (layer, transformation) = self.layers.current_mut();
+
+ match geometry {
+ Geometry::Live {
+ primitives,
+ text,
+ clip_bounds,
+ } => {
+ layer.draw_primitive_group(
+ primitives,
+ clip_bounds,
+ transformation,
+ );
+
+ layer.draw_text_group(text, clip_bounds, transformation);
+ }
+ Geometry::Cache(cache) => {
+ layer.draw_primitive_cache(
+ cache.primitives,
+ cache.clip_bounds,
+ 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");
+ }
+}
+
+#[cfg(feature = "image")]
+impl core::image::Renderer for Renderer {
+ type Handle = core::image::Handle;
+
+ fn measure_image(&self, handle: &Self::Handle) -> crate::core::Size<u32> {
+ self.engine.raster_pipeline.dimensions(handle)
+ }
+
+ fn draw_image(
+ &mut self,
+ handle: Self::Handle,
+ filter_method: core::image::FilterMethod,
+ bounds: Rectangle,
+ rotation: core::Radians,
+ opacity: f32,
+ ) {
+ let (layer, transformation) = self.layers.current_mut();
+ layer.draw_image(
+ handle,
+ filter_method,
+ bounds,
+ transformation,
+ rotation,
+ opacity,
+ );
+ }
+}
+
+#[cfg(feature = "svg")]
+impl core::svg::Renderer for Renderer {
+ fn measure_svg(
+ &self,
+ handle: &core::svg::Handle,
+ ) -> crate::core::Size<u32> {
+ self.engine.vector_pipeline.viewport_dimensions(handle)
+ }
+
+ fn draw_svg(
+ &mut self,
+ handle: core::svg::Handle,
+ color: Option<Color>,
+ bounds: Rectangle,
+ rotation: core::Radians,
+ opacity: f32,
+ ) {
+ let (layer, transformation) = self.layers.current_mut();
+ layer.draw_svg(
+ handle,
+ color,
+ bounds,
+ transformation,
+ rotation,
+ opacity,
+ );
+ }
+}
+
+impl compositor::Default for Renderer {
+ type Compositor = window::Compositor;
+}
diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs
index 7718d542..5de51047 100644
--- a/tiny_skia/src/primitive.rs
+++ b/tiny_skia/src/primitive.rs
@@ -1,10 +1,7 @@
use crate::core::Rectangle;
-use crate::graphics::Damage;
-
-pub type Primitive = crate::graphics::Primitive<Custom>;
#[derive(Debug, Clone, PartialEq)]
-pub enum Custom {
+pub enum Primitive {
/// A path filled with some paint.
Fill {
/// The path to fill.
@@ -25,20 +22,19 @@ pub enum Custom {
},
}
-impl Damage for Custom {
- fn bounds(&self) -> Rectangle {
- match self {
- Self::Fill { path, .. } | Self::Stroke { path, .. } => {
- let bounds = path.bounds();
+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(),
- }
- .expand(1.0)
- }
+ 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
index 5f17ae60..c40f55b2 100644
--- a/tiny_skia/src/raster.rs
+++ b/tiny_skia/src/raster.rs
@@ -6,6 +6,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
use std::cell::RefCell;
use std::collections::hash_map;
+#[derive(Debug)]
pub struct Pipeline {
cache: RefCell<Cache>,
}
@@ -30,6 +31,7 @@ impl Pipeline {
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>,
@@ -55,6 +57,7 @@ impl Pipeline {
image,
&tiny_skia::PixmapPaint {
quality,
+ opacity,
..Default::default()
},
transform,
@@ -68,10 +71,10 @@ impl Pipeline {
}
}
-#[derive(Default)]
+#[derive(Debug, Default)]
struct Cache {
- entries: FxHashMap<u64, Option<Entry>>,
- hits: FxHashSet<u64>,
+ entries: FxHashMap<raster::Id, Option<Entry>>,
+ hits: FxHashSet<raster::Id>,
}
impl Cache {
@@ -82,7 +85,7 @@ impl Cache {
let id = handle.id();
if let hash_map::Entry::Vacant(entry) = self.entries.entry(id) {
- let image = graphics::image::load(handle).ok()?.into_rgba8();
+ let image = graphics::image::load(handle).ok()?;
let mut buffer =
vec![0u32; image.width() as usize * image.height() as usize];
@@ -119,6 +122,7 @@ impl Cache {
}
}
+#[derive(Debug)]
struct Entry {
width: u32,
height: u32,
diff --git a/tiny_skia/src/settings.rs b/tiny_skia/src/settings.rs
index ec27b218..672c49f3 100644
--- a/tiny_skia/src/settings.rs
+++ b/tiny_skia/src/settings.rs
@@ -1,8 +1,9 @@
use crate::core::{Font, Pixels};
+use crate::graphics;
-/// The settings of a [`Backend`].
+/// The settings of a [`Compositor`].
///
-/// [`Backend`]: crate::Backend
+/// [`Compositor`]: crate::window::Compositor
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Settings {
/// The default [`Font`] to use.
@@ -22,3 +23,12 @@ impl Default for Settings {
}
}
}
+
+impl From<graphics::Settings> for Settings {
+ fn from(settings: graphics::Settings) -> Self {
+ Self {
+ default_font: settings.default_font,
+ default_text_size: settings.default_text_size,
+ }
+ }
+}
diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs
index d28cc483..c71deb10 100644
--- a/tiny_skia/src/text.rs
+++ b/tiny_skia/src/text.rs
@@ -1,5 +1,5 @@
use crate::core::alignment;
-use crate::core::text::{LineHeight, Shaping};
+use crate::core::text::Shaping;
use crate::core::{
Color, Font, Pixels, Point, Rectangle, Size, Transformation,
};
@@ -13,7 +13,7 @@ use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::hash_map;
-#[allow(missing_debug_implementations)]
+#[derive(Debug)]
pub struct Pipeline {
glyph_cache: GlyphCache,
cache: RefCell<Cache>,
@@ -27,6 +27,8 @@ impl Pipeline {
}
}
+ // TODO: Shared engine
+ #[allow(dead_code)]
pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
font_system()
.write()
@@ -41,7 +43,6 @@ impl Pipeline {
paragraph: &paragraph::Weak,
position: Point,
color: Color,
- scale_factor: f32,
pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: Option<&tiny_skia::Mask>,
transformation: Transformation,
@@ -62,7 +63,6 @@ impl Pipeline {
color,
paragraph.horizontal_alignment(),
paragraph.vertical_alignment(),
- scale_factor,
pixels,
clip_mask,
transformation,
@@ -74,7 +74,6 @@ impl Pipeline {
editor: &editor::Weak,
position: Point,
color: Color,
- scale_factor: f32,
pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: Option<&tiny_skia::Mask>,
transformation: Transformation,
@@ -95,7 +94,6 @@ impl Pipeline {
color,
alignment::Horizontal::Left,
alignment::Vertical::Top,
- scale_factor,
pixels,
clip_mask,
transformation,
@@ -108,17 +106,16 @@ impl Pipeline {
bounds: Rectangle,
color: Color,
size: Pixels,
- line_height: LineHeight,
+ line_height: Pixels,
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>,
transformation: Transformation,
) {
- let line_height = f32::from(line_height.to_absolute(size));
+ let line_height = f32::from(line_height);
let mut font_system = font_system().write().expect("Write font system");
let font_system = font_system.raw();
@@ -149,7 +146,6 @@ impl Pipeline {
color,
horizontal_alignment,
vertical_alignment,
- scale_factor,
pixels,
clip_mask,
transformation,
@@ -161,7 +157,6 @@ impl Pipeline {
buffer: &cosmic_text::Buffer,
position: Point,
color: Color,
- scale_factor: f32,
pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: Option<&tiny_skia::Mask>,
transformation: Transformation,
@@ -178,7 +173,6 @@ impl Pipeline {
color,
alignment::Horizontal::Left,
alignment::Vertical::Top,
- scale_factor,
pixels,
clip_mask,
transformation,
@@ -199,12 +193,11 @@ fn draw(
color: Color,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
- scale_factor: f32,
pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: Option<&tiny_skia::Mask>,
transformation: Transformation,
) {
- let bounds = bounds * transformation * scale_factor;
+ let bounds = bounds * transformation;
let x = match horizontal_alignment {
alignment::Horizontal::Left => bounds.x,
@@ -222,8 +215,8 @@ fn draw(
for run in buffer.layout_runs() {
for glyph in run.glyphs {
- let physical_glyph = glyph
- .physical((x, y), scale_factor * transformation.scale_factor());
+ let physical_glyph =
+ glyph.physical((x, y), transformation.scale_factor());
if let Some((buffer, placement)) = glyph_cache.allocate(
physical_glyph.cache_key,
@@ -247,10 +240,8 @@ fn draw(
pixels.draw_pixmap(
physical_glyph.x + placement.left,
physical_glyph.y - placement.top
- + (run.line_y
- * scale_factor
- * transformation.scale_factor())
- .round() as i32,
+ + (run.line_y * transformation.scale_factor()).round()
+ as i32,
pixmap,
&tiny_skia::PixmapPaint {
opacity,
diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs
index fd1ab3de..bbe08cb8 100644
--- a/tiny_skia/src/vector.rs
+++ b/tiny_skia/src/vector.rs
@@ -4,11 +4,13 @@ use crate::graphics::text;
use resvg::usvg::{self, TreeTextToPath};
use rustc_hash::{FxHashMap, FxHashSet};
+use tiny_skia::Transform;
use std::cell::RefCell;
use std::collections::hash_map;
use std::fs;
+#[derive(Debug)]
pub struct Pipeline {
cache: RefCell<Cache>,
}
@@ -32,7 +34,9 @@ impl Pipeline {
handle: &Handle,
color: Option<Color>,
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(
@@ -44,8 +48,11 @@ impl Pipeline {
bounds.x as i32,
bounds.y as i32,
image,
- &tiny_skia::PixmapPaint::default(),
- tiny_skia::Transform::identity(),
+ &tiny_skia::PixmapPaint {
+ opacity,
+ ..tiny_skia::PixmapPaint::default()
+ },
+ transform,
clip_mask,
);
}
@@ -203,3 +210,13 @@ impl Cache {
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/compositor.rs b/tiny_skia/src/window/compositor.rs
index a98825f1..153af6d5 100644
--- a/tiny_skia/src/window/compositor.rs
+++ b/tiny_skia/src/window/compositor.rs
@@ -1,44 +1,55 @@
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 crate::graphics::error::{self, Error};
+use crate::graphics::{self, Viewport};
+use crate::{Layer, Renderer, Settings};
use std::collections::VecDeque;
-use std::future::{self, Future};
use std::num::NonZeroU32;
+#[allow(missing_debug_implementations)]
pub struct Compositor {
context: softbuffer::Context<Box<dyn compositor::Window>>,
settings: Settings,
}
+#[allow(missing_debug_implementations)]
pub struct Surface {
window: softbuffer::Surface<
Box<dyn compositor::Window>,
Box<dyn compositor::Window>,
>,
clip_mask: tiny_skia::Mask,
- primitive_stack: VecDeque<Vec<Primitive>>,
+ layer_stack: VecDeque<Vec<Layer>>,
background_color: Color,
max_age: u8,
}
impl crate::graphics::Compositor for Compositor {
- type Settings = Settings;
type Renderer = Renderer;
type Surface = Surface;
- fn new<W: compositor::Window>(
- settings: Self::Settings,
+ async fn with_backend<W: compositor::Window>(
+ settings: graphics::Settings,
compatible_window: W,
- ) -> impl Future<Output = Result<Self, Error>> {
- future::ready(Ok(new(settings, compatible_window)))
+ backend: Option<&str>,
+ ) -> Result<Self, Error> {
+ match backend {
+ None | Some("tiny-skia") | Some("tiny_skia") => {
+ Ok(new(settings.into(), compatible_window))
+ }
+ Some(backend) => Err(Error::GraphicsAdapterNotFound {
+ backend: "tiny-skia",
+ reason: error::Reason::DidNotMatch {
+ preferred_backend: backend.to_owned(),
+ },
+ }),
+ }
}
fn create_renderer(&self) -> Self::Renderer {
Renderer::new(
- Backend::new(),
self.settings.default_font,
self.settings.default_text_size,
)
@@ -60,7 +71,7 @@ impl crate::graphics::Compositor for Compositor {
window,
clip_mask: tiny_skia::Mask::new(width, height)
.expect("Create clip mask"),
- primitive_stack: VecDeque::new(),
+ layer_stack: VecDeque::new(),
background_color: Color::BLACK,
max_age: 0,
};
@@ -86,7 +97,7 @@ impl crate::graphics::Compositor for Compositor {
surface.clip_mask =
tiny_skia::Mask::new(width, height).expect("Create clip mask");
- surface.primitive_stack.clear();
+ surface.layer_stack.clear();
}
fn fetch_information(&self) -> Information {
@@ -104,16 +115,7 @@ impl crate::graphics::Compositor for Compositor {
background_color: Color,
overlay: &[T],
) -> Result<(), compositor::SurfaceError> {
- renderer.with_primitives(|backend, primitives| {
- present(
- backend,
- surface,
- primitives,
- viewport,
- background_color,
- overlay,
- )
- })
+ present(renderer, surface, viewport, background_color, overlay)
}
fn screenshot<T: AsRef<str>>(
@@ -124,16 +126,7 @@ impl crate::graphics::Compositor for Compositor {
background_color: Color,
overlay: &[T],
) -> Vec<u8> {
- renderer.with_primitives(|backend, primitives| {
- screenshot(
- surface,
- backend,
- primitives,
- viewport,
- background_color,
- overlay,
- )
- })
+ screenshot(renderer, surface, viewport, background_color, overlay)
}
}
@@ -149,38 +142,42 @@ pub fn new<W: compositor::Window>(
}
pub fn present<T: AsRef<str>>(
- backend: &mut Backend,
+ renderer: &mut Renderer,
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 buffer = surface
.window
.buffer_mut()
.map_err(|_| compositor::SurfaceError::Lost)?;
- let last_primitives = {
+ let last_layers = {
let age = buffer.age();
surface.max_age = surface.max_age.max(age);
- surface.primitive_stack.truncate(surface.max_age as usize);
+ surface.layer_stack.truncate(surface.max_age as usize);
if age > 0 {
- surface.primitive_stack.get(age as usize - 1)
+ surface.layer_stack.get(age as usize - 1)
} else {
None
}
};
- let damage = last_primitives
- .and_then(|last_primitives| {
- (surface.background_color == background_color)
- .then(|| damage::list(last_primitives, primitives))
+ let damage = last_layers
+ .and_then(|last_layers| {
+ (surface.background_color == background_color).then(|| {
+ damage::diff(
+ last_layers,
+ renderer.layers(),
+ |layer| vec![layer.bounds],
+ Layer::damage,
+ )
+ })
})
.unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]);
@@ -188,10 +185,11 @@ pub fn present<T: AsRef<str>>(
return Ok(());
}
- surface.primitive_stack.push_front(primitives.to_vec());
+ surface.layer_stack.push_front(renderer.layers().to_vec());
surface.background_color = background_color;
- let damage = damage::group(damage, scale_factor, physical_size);
+ let damage =
+ damage::group(damage, Rectangle::with_size(viewport.logical_size()));
let mut pixels = tiny_skia::PixmapMut::from_bytes(
bytemuck::cast_slice_mut(&mut buffer),
@@ -200,10 +198,9 @@ pub fn present<T: AsRef<str>>(
)
.expect("Create pixel map");
- backend.draw(
+ renderer.draw(
&mut pixels,
&mut surface.clip_mask,
- primitives,
viewport,
&damage,
background_color,
@@ -214,9 +211,8 @@ pub fn present<T: AsRef<str>>(
}
pub fn screenshot<T: AsRef<str>>(
+ renderer: &mut Renderer,
surface: &mut Surface,
- backend: &mut Backend,
- primitives: &[Primitive],
viewport: &Viewport,
background_color: Color,
overlay: &[T],
@@ -226,7 +222,7 @@ pub fn screenshot<T: AsRef<str>>(
let mut offscreen_buffer: Vec<u32> =
vec![0; size.width as usize * size.height as usize];
- backend.draw(
+ renderer.draw(
&mut tiny_skia::PixmapMut::from_bytes(
bytemuck::cast_slice_mut(&mut offscreen_buffer),
size.width,
@@ -234,7 +230,6 @@ pub fn screenshot<T: AsRef<str>>(
)
.expect("Create offscreen pixel map"),
&mut surface.clip_mask,
- primitives,
viewport,
&[Rectangle::with_size(Size::new(
size.width as f32,