diff options
Diffstat (limited to 'tiny_skia')
| -rw-r--r-- | tiny_skia/Cargo.toml | 39 | ||||
| -rw-r--r-- | tiny_skia/src/backend.rs | 678 | ||||
| -rw-r--r-- | tiny_skia/src/geometry.rs | 299 | ||||
| -rw-r--r-- | tiny_skia/src/lib.rs | 26 | ||||
| -rw-r--r-- | tiny_skia/src/raster.rs | 116 | ||||
| -rw-r--r-- | tiny_skia/src/settings.rs | 24 | ||||
| -rw-r--r-- | tiny_skia/src/text.rs | 440 | ||||
| -rw-r--r-- | tiny_skia/src/vector.rs | 182 | ||||
| -rw-r--r-- | tiny_skia/src/window.rs | 3 | ||||
| -rw-r--r-- | tiny_skia/src/window/compositor.rs | 158 | 
10 files changed, 1965 insertions, 0 deletions
| diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml new file mode 100644 index 00000000..400eee6a --- /dev/null +++ b/tiny_skia/Cargo.toml @@ -0,0 +1,39 @@ +[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.9" +bytemuck = "1" +rustc-hash = "1.1" +kurbo = "0.9" +log = "0.4" + +[dependencies.iced_graphics] +version = "0.8" +path = "../graphics" +features = ["tiny-skia"] + +[dependencies.cosmic-text] +git = "https://github.com/hecrj/cosmic-text.git" +rev = "b85d6a4f2376f8a8a7dadc0f8bcb89d4db10a1c9" + +[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.32" +optional = true diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs new file mode 100644 index 00000000..3ef7e717 --- /dev/null +++ b/tiny_skia/src/backend.rs @@ -0,0 +1,678 @@ +use crate::core::text; +use crate::core::{Background, Color, Font, Point, Rectangle, Size, Vector}; +use crate::graphics::backend; +use crate::graphics::{Primitive, Viewport}; +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 ®ion in damage { +            let path = tiny_skia::PathBuilder::from_rect( +                tiny_skia::Rect::from_xywh( +                    region.x, +                    region.y, +                    region.width, +                    region.height, +                ) +                .expect("Create damage rectangle"), +            ); + +            pixels.fill_path( +                &path, +                &tiny_skia::Paint { +                    shader: tiny_skia::Shader::SolidColor(into_color( +                        background_color, +                    )), +                    anti_alias: false, +                    ..Default::default() +                }, +                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); + +                let path = rounded_rectangle(*bounds, *border_radius); + +                pixels.fill_path( +                    &path, +                    &tiny_skia::Paint { +                        shader: match background { +                            Background::Color(color) => { +                                tiny_skia::Shader::SolidColor(into_color( +                                    *color, +                                )) +                            } +                        }, +                        anti_alias: true, +                        ..tiny_skia::Paint::default() +                    }, +                    tiny_skia::FillRule::EvenOdd, +                    transform, +                    clip_mask, +                ); + +                if *border_width > 0.0 { +                    pixels.stroke_path( +                        &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, +                    ); +                } +            } +            Primitive::Text { +                content, +                bounds, +                color, +                size, +                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) * scale_factor, +                    *color, +                    *size * scale_factor, +                    *font, +                    *horizontal_alignment, +                    *vertical_alignment, +                    *shaping, +                    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(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, +                ); +            } +            Primitive::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::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, +                ); +            } +            Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => { +                // Not supported! +                // TODO: Draw a placeholder (?) / Log it (?) +            } +            _ => { +                // Not supported! +            } +        } +    } +} + +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_right > 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(bounds.x, bounds.y, bounds.width, bounds.height); + +        builder.finish().unwrap() +    }; + +    clip_mask.fill_path( +        &path, +        tiny_skia::FillRule::EvenOdd, +        false, +        tiny_skia::Transform::default(), +    ); +} + +impl iced_graphics::Backend for Backend { +    fn trim_measurements(&mut self) { +        self.text_pipeline.trim_measurement_cache(); +    } +} + +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, +        font: Font, +        bounds: Size, +        shaping: text::Shaping, +    ) -> (f32, f32) { +        self.text_pipeline +            .measure(contents, size, font, bounds, shaping) +    } + +    fn hit_test( +        &self, +        contents: &str, +        size: f32, +        font: Font, +        bounds: Size, +        shaping: text::Shaping, +        point: Point, +        nearest_only: bool, +    ) -> Option<text::Hit> { +        self.text_pipeline.hit_test( +            contents, +            size, +            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..7963fd89 --- /dev/null +++ b/tiny_skia/src/geometry.rs @@ -0,0 +1,299 @@ +use crate::core::Gradient; +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::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::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::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::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, +            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, translation: Vector) { +        self.primitives.push(Primitive::Translate { +            translation, +            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) => 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, +                    }, +                    linear +                        .color_stops +                        .into_iter() +                        .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::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..83baef1c --- /dev/null +++ b/tiny_skia/src/lib.rs @@ -0,0 +1,26 @@ +pub mod window; + +mod backend; +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 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/raster.rs b/tiny_skia/src/raster.rs new file mode 100644 index 00000000..3887ec8d --- /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] = tiny_skia::ColorU8::from_rgba(b, g, r, a) +                    .premultiply() +                    .get(); +            } + +            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..58079cc0 --- /dev/null +++ b/tiny_skia/src/text.rs @@ -0,0 +1,440 @@ +use crate::core::alignment; +use crate::core::font::{self, Font}; +use crate::core::text::{Hit, Shaping}; +use crate::core::{Color, 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, +    measurement_cache: RefCell<Cache>, +    render_cache: 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!("../../wgpu/fonts/Iced-Icons.ttf") +                        .as_slice(), +                ))] +                .into_iter(), +            )), +            glyph_cache: GlyphCache::new(), +            measurement_cache: RefCell::new(Cache::new()), +            render_cache: 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())), +        ); +    } + +    pub fn draw( +        &mut self, +        content: &str, +        bounds: Rectangle, +        color: Color, +        size: f32, +        font: Font, +        horizontal_alignment: alignment::Horizontal, +        vertical_alignment: alignment::Vertical, +        shaping: Shaping, +        pixels: &mut tiny_skia::PixmapMut<'_>, +        clip_mask: Option<&tiny_skia::Mask>, +    ) { +        let font_system = self.font_system.get_mut(); +        let key = Key { +            bounds: { +                let size = bounds.size(); + +                // TODO: Reuse buffers from layouting +                Size::new(size.width.ceil(), size.height.ceil()) +            }, +            content, +            font, +            size, +            shaping, +        }; + +        let (_, buffer) = self.render_cache.allocate(font_system, key); + +        let (total_lines, max_width) = buffer +            .layout_runs() +            .enumerate() +            .fold((0, 0.0), |(_, max), (i, buffer)| { +                (i + 1, buffer.line_w.max(max)) +            }); + +        let total_height = total_lines as f32 * size * 1.2; + +        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 buffer.layout_runs() { +            for glyph in run.glyphs { +                if let Some((buffer, placement)) = self.glyph_cache.allocate( +                    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( +                        x as i32 + glyph.x_int + placement.left, +                        y as i32 - glyph.y_int - placement.top +                            + run.line_y as i32, +                        pixmap, +                        &tiny_skia::PixmapPaint::default(), +                        tiny_skia::Transform::identity(), +                        clip_mask, +                    ); +                } +            } +        } +    } + +    pub fn trim_cache(&mut self) { +        self.render_cache.trim(); +        self.glyph_cache.trim(); +    } + +    pub fn measure( +        &self, +        content: &str, +        size: f32, +        font: Font, +        bounds: Size, +        shaping: Shaping, +    ) -> (f32, f32) { +        let mut measurement_cache = self.measurement_cache.borrow_mut(); + +        let (_, paragraph) = measurement_cache.allocate( +            &mut self.font_system.borrow_mut(), +            Key { +                content, +                size, +                font, +                bounds, +                shaping, +            }, +        ); + +        let (total_lines, max_width) = paragraph +            .layout_runs() +            .enumerate() +            .fold((0, 0.0), |(_, max), (i, buffer)| { +                (i + 1, buffer.line_w.max(max)) +            }); + +        (max_width, size * 1.2 * total_lines as f32) +    } + +    pub fn hit_test( +        &self, +        content: &str, +        size: f32, +        font: Font, +        bounds: Size, +        shaping: Shaping, +        point: Point, +        _nearest_only: bool, +    ) -> Option<Hit> { +        let mut measurement_cache = self.measurement_cache.borrow_mut(); + +        let (_, paragraph) = measurement_cache.allocate( +            &mut self.font_system.borrow_mut(), +            Key { +                content, +                size, +                font, +                bounds, +                shaping, +            }, +        ); + +        let cursor = paragraph.hit(point.x, point.y)?; + +        Some(Hit::CharOffset(cursor.index)) +    } + +    pub fn trim_measurement_cache(&mut self) { +        self.measurement_cache.borrow_mut().trim(); +    } +} + +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] = tiny_skia::ColorU8::from_rgba( +                                b, +                                g, +                                r, +                                image.data[i], +                            ) +                            .premultiply() +                            .get(); + +                            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] = tiny_skia::ColorU8::from_rgba( +                                image.data[i + 2], +                                image.data[i + 1], +                                image.data[i], +                                image.data[i + 3], +                            ) +                            .premultiply() +                            .get(); + +                            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, cosmic_text::Buffer>, +    recently_used: FxHashSet<KeyHash>, +    hasher: HashBuilder, +    trim_count: usize, +} + +#[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(), +            recently_used: FxHashSet::default(), +            hasher: HashBuilder::default(), +            trim_count: 0, +        } +    } + +    fn allocate( +        &mut self, +        font_system: &mut cosmic_text::FontSystem, +        key: Key<'_>, +    ) -> (KeyHash, &mut cosmic_text::Buffer) { +        let hash = { +            let mut hasher = self.hasher.build_hasher(); + +            key.content.hash(&mut hasher); +            key.size.to_bits().hash(&mut hasher); +            key.font.hash(&mut hasher); +            key.bounds.width.to_bits().hash(&mut hasher); +            key.bounds.height.to_bits().hash(&mut hasher); + +            hasher.finish() +        }; + +        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 _ = entry.insert(buffer); +        } + +        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.recently_used.clear(); + +            self.trim_count = 0; +        } else { +            self.trim_count += 1; +        } +    } +} + +#[derive(Debug, Clone, Copy)] +struct Key<'a> { +    content: &'a str, +    size: f32, +    font: Font, +    bounds: Size, +    shaping: Shaping, +} + +type KeyHash = u64; diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs new file mode 100644 index 00000000..fc411fdd --- /dev/null +++ b/tiny_skia/src/vector.rs @@ -0,0 +1,182 @@ +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)?; + +            resvg::render( +                tree, +                if size.width > size.height { +                    resvg::FitTo::Width(size.width) +                } else { +                    resvg::FitTo::Height(size.height) +                }, +                tiny_skia::Transform::default(), +                image.as_mut(), +            )?; + +            if let Some([r, g, b, a]) = key.color { +                // TODO: Blend alpha +                let color = tiny_skia::ColorU8::from_rgba(b, g, r, a) +                    .premultiply() +                    .get() +                    & 0x00FFFFFF; + +                // Apply color filter +                for pixel in +                    bytemuck::cast_slice_mut::<u8, u32>(image.data_mut()) +                { +                    *pixel = *pixel & 0xFF000000 | color; +                } +            } 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..9999a188 --- /dev/null +++ b/tiny_skia/src/window/compositor.rs @@ -0,0 +1,158 @@ +use crate::core::{Color, Rectangle}; +use crate::graphics::compositor::{self, Information, SurfaceError}; +use crate::graphics::damage; +use crate::graphics::{Error, Primitive, Viewport}; +use crate::{Backend, Renderer, Settings}; + +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<(), SurfaceError> { +        renderer.with_primitives(|backend, primitives| { +            present( +                backend, +                surface, +                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(()) +} | 
