diff options
Diffstat (limited to '')
| -rw-r--r-- | tiny_skia/Cargo.toml | 40 | ||||
| -rw-r--r-- | tiny_skia/src/backend.rs | 548 | ||||
| -rw-r--r-- | tiny_skia/src/geometry.rs | 287 | ||||
| -rw-r--r-- | tiny_skia/src/lib.rs | 26 | ||||
| -rw-r--r-- | tiny_skia/src/primitive.rs | 82 | ||||
| -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 | 431 | ||||
| -rw-r--r-- | tiny_skia/src/vector.rs | 180 | ||||
| -rw-r--r-- | tiny_skia/src/window.rs | 3 | ||||
| -rw-r--r-- | tiny_skia/src/window/compositor.rs | 130 | 
11 files changed, 1867 insertions, 0 deletions
| diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml new file mode 100644 index 00000000..69197589 --- /dev/null +++ b/tiny_skia/Cargo.toml @@ -0,0 +1,40 @@ +[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.8" +bytemuck = "1" +rustc-hash = "1.1" +ouroboros = "0.15" +kurbo = "0.9" + +[dependencies.iced_graphics] +version = "0.7" +path = "../graphics" +features = ["tiny-skia"] + +[dependencies.cosmic-text] +features = ["std", "swash"] +git = "https://github.com/hecrj/cosmic-text" +rev = "81080c1b9498933b43c1889601a7ea6a3d16e161" + +[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.29" +optional = true diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs new file mode 100644 index 00000000..ba063f4e --- /dev/null +++ b/tiny_skia/src/backend.rs @@ -0,0 +1,548 @@ +use crate::core::alignment; +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::ClipMask, +        primitives: &[Primitive], +        viewport: &Viewport, +        background_color: Color, +        overlay: &[T], +    ) { +        pixels.fill(into_color(background_color)); + +        let scale_factor = viewport.scale_factor() as f32; + +        for primitive in primitives { +            self.draw_primitive( +                primitive, +                pixels, +                clip_mask, +                None, +                scale_factor, +                Vector::ZERO, +            ); +        } + +        for (i, text) in overlay.iter().enumerate() { +            const OVERLAY_TEXT_SIZE: f32 = 20.0; + +            self.draw_primitive( +                &Primitive::Text { +                    content: text.as_ref().to_owned(), +                    size: OVERLAY_TEXT_SIZE, +                    bounds: Rectangle { +                        x: 10.0, +                        y: 10.0 + i as f32 * OVERLAY_TEXT_SIZE * 1.2, +                        width: f32::INFINITY, +                        height: f32::INFINITY, +                    }, +                    color: Color::BLACK, +                    font: Font::Monospace, +                    horizontal_alignment: alignment::Horizontal::Left, +                    vertical_alignment: alignment::Vertical::Top, +                }, +                pixels, +                clip_mask, +                None, +                scale_factor, +                Vector::ZERO, +            ); +        } + +        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::ClipMask, +        clip_bounds: Option<Rectangle>, +        scale_factor: f32, +        translation: Vector, +    ) { +        match primitive { +            Primitive::Quad { +                bounds, +                background, +                border_radius, +                border_width, +                border_color, +            } => { +                let transform = tiny_skia::Transform::from_translate( +                    translation.x, +                    translation.y, +                ) +                .post_scale(scale_factor, scale_factor); + +                let path = rounded_rectangle(*bounds, *border_radius); +                let clip_mask = clip_bounds.map(|_| clip_mask as &_); + +                pixels.fill_path( +                    &path, +                    &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, +            } => { +                self.text_pipeline.draw( +                    content, +                    (*bounds + translation) * scale_factor, +                    *color, +                    *size * scale_factor, +                    *font, +                    *horizontal_alignment, +                    *vertical_alignment, +                    pixels, +                    clip_bounds.map(|_| clip_mask as &_), +                ); +            } +            #[cfg(feature = "image")] +            Primitive::Image { handle, bounds } => { +                let transform = tiny_skia::Transform::from_translate( +                    translation.x, +                    translation.y, +                ) +                .post_scale(scale_factor, scale_factor); + +                self.raster_pipeline.draw( +                    handle, +                    *bounds, +                    pixels, +                    transform, +                    clip_bounds.map(|_| clip_mask as &_), +                ); +            } +            #[cfg(feature = "svg")] +            Primitive::Svg { +                handle, +                bounds, +                color, +            } => { +                self.vector_pipeline.draw( +                    handle, +                    *color, +                    (*bounds + translation) * scale_factor, +                    pixels, +                    clip_bounds.map(|_| clip_mask as &_), +                ); +            } +            Primitive::Fill { +                path, +                paint, +                rule, +                transform, +            } => { +                pixels.fill_path( +                    path, +                    paint, +                    *rule, +                    transform +                        .post_translate(translation.x, translation.y) +                        .post_scale(scale_factor, scale_factor), +                    clip_bounds.map(|_| clip_mask as &_), +                ); +            } +            Primitive::Stroke { +                path, +                paint, +                stroke, +                transform, +            } => { +                pixels.stroke_path( +                    path, +                    paint, +                    stroke, +                    transform +                        .post_translate(translation.x, translation.y) +                        .post_scale(scale_factor, scale_factor), +                    clip_bounds.map(|_| clip_mask as &_), +                ); +            } +            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; + +                adjust_clip_mask(clip_mask, pixels, bounds); + +                self.draw_primitive( +                    content, +                    pixels, +                    clip_mask, +                    Some(bounds), +                    scale_factor, +                    translation, +                ); + +                if let Some(bounds) = clip_bounds { +                    adjust_clip_mask(clip_mask, pixels, bounds); +                } else { +                    clip_mask.clear(); +                } +            } +            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::ClipMask, +    pixels: &tiny_skia::PixmapMut<'_>, +    bounds: Rectangle, +) { +    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 +        .set_path( +            pixels.width(), +            pixels.height(), +            &path, +            tiny_skia::FillRule::EvenOdd, +            true, +        ) +        .expect("Set path of clipping area"); +} + +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::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, +    ) -> (f32, f32) { +        self.text_pipeline.measure(contents, size, font, bounds) +    } + +    fn hit_test( +        &self, +        contents: &str, +        size: f32, +        font: Font, +        bounds: Size, +        point: Point, +        nearest_only: bool, +    ) -> Option<text::Hit> { +        self.text_pipeline.hit_test( +            contents, +            size, +            font, +            bounds, +            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..c66621dd --- /dev/null +++ b/tiny_skia/src/geometry.rs @@ -0,0 +1,287 @@ +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 path = convert_path(path); +        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 path = convert_path(&Path::rectangle(top_left, size)); +        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 path = convert_path(path); +        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, +        }); +    } + +    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) {} + +    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) -> 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(); +                } +            } +        } +    } + +    builder +        .finish() +        .expect("Convert lyon path to tiny_skia path") +} + +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/primitive.rs b/tiny_skia/src/primitive.rs new file mode 100644 index 00000000..22daaedc --- /dev/null +++ b/tiny_skia/src/primitive.rs @@ -0,0 +1,82 @@ +use crate::{Rectangle, Vector}; + +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub enum Primitive { +    /// A group of primitives +    Group { +        /// The primitives of the group +        primitives: Vec<Primitive>, +    }, +    /// A clip primitive +    Clip { +        /// The bounds of the clip +        bounds: Rectangle, +        /// The content of the clip +        content: Box<Primitive>, +    }, +    /// A primitive that applies a translation +    Translate { +        /// The translation vector +        translation: Vector, + +        /// The primitive to translate +        content: Box<Primitive>, +    }, +    /// A cached primitive. +    /// +    /// This can be useful if you are implementing a widget where primitive +    /// generation is expensive. +    Cached { +        /// The cached primitive +        cache: Arc<Primitive>, +    }, +    /// A basic primitive. +    Basic(iced_graphics::Primitive), +} + +impl iced_graphics::backend::Primitive for Primitive { +    fn translate(self, translation: Vector) -> Self { +        Self::Translate { +            translation, +            content: Box::new(self), +        } +    } + +    fn clip(self, bounds: Rectangle) -> Self { +        Self::Clip { +            bounds, +            content: Box::new(self), +        } +    } +} + +#[derive(Debug, Clone, Default)] +pub struct Recording(pub(crate) Vec<Primitive>); + +impl iced_graphics::backend::Recording for Recording { +    type Primitive = Primitive; + +    fn push(&mut self, primitive: Primitive) { +        self.0.push(primitive); +    } + +    fn push_basic(&mut self, basic: iced_graphics::Primitive) { +        self.0.push(Primitive::Basic(basic)); +    } + +    fn group(self) -> Self::Primitive { +        Primitive::Group { primitives: self.0 } +    } + +    fn clear(&mut self) { +        self.0.clear(); +    } +} + +impl Recording { +    pub fn primitives(&self) -> &[Primitive] { +        &self.0 +    } +} diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs new file mode 100644 index 00000000..2fd73f8c --- /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::ClipMask>, +    ) { +        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..9e4be0c4 --- /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::SansSerif, +            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..f2935efa --- /dev/null +++ b/tiny_skia/src/text.rs @@ -0,0 +1,431 @@ +use crate::core::alignment; +use crate::core::text::Hit; +use crate::core::{Color, Font, 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 { +    system: Option<System>, +    glyph_cache: GlyphCache, +} + +#[ouroboros::self_referencing] +struct System { +    fonts: cosmic_text::FontSystem, + +    #[borrows(fonts)] +    #[not_covariant] +    measurement_cache: RefCell<Cache<'this>>, + +    #[borrows(fonts)] +    #[not_covariant] +    render_cache: Cache<'this>, +} + +impl Pipeline { +    pub fn new() -> Self { +        Pipeline { +            system: Some( +                SystemBuilder { +                    fonts: cosmic_text::FontSystem::new_with_fonts( +                        [cosmic_text::fontdb::Source::Binary(Arc::new( +                            include_bytes!("../../wgpu/fonts/Iced-Icons.ttf") +                                .as_slice(), +                        ))] +                        .into_iter(), +                    ), +                    measurement_cache_builder: |_| RefCell::new(Cache::new()), +                    render_cache_builder: |_| Cache::new(), +                } +                .build(), +            ), +            glyph_cache: GlyphCache::new(), +        } +    } + +    pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { +        let heads = self.system.take().unwrap().into_heads(); + +        let (locale, mut db) = heads.fonts.into_locale_and_db(); + +        db.load_font_source(cosmic_text::fontdb::Source::Binary(Arc::new( +            bytes.into_owned(), +        ))); + +        self.system = Some( +            SystemBuilder { +                fonts: cosmic_text::FontSystem::new_with_locale_and_db( +                    locale, db, +                ), +                measurement_cache_builder: |_| RefCell::new(Cache::new()), +                render_cache_builder: |_| Cache::new(), +            } +            .build(), +        ); +    } + +    pub fn draw( +        &mut self, +        content: &str, +        bounds: Rectangle, +        color: Color, +        size: f32, +        font: Font, +        horizontal_alignment: alignment::Horizontal, +        vertical_alignment: alignment::Vertical, +        pixels: &mut tiny_skia::PixmapMut<'_>, +        clip_mask: Option<&tiny_skia::ClipMask>, +    ) { +        self.system.as_mut().unwrap().with_mut(|fields| { +            let key = Key { +                bounds: { +                    let size = bounds.size(); + +                    // TODO: Reuse buffers from layouting +                    Size::new(size.width.ceil(), size.height.ceil()) +                }, +                content, +                font, +                size, +            }; + +            let (_, buffer) = fields.render_cache.allocate(fields.fonts, 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(fields.fonts); + +            for run in buffer.layout_runs() { +                for glyph in run.glyphs { +                    if let Some((buffer, placement)) = self +                        .glyph_cache +                        .allocate(glyph.cache_key, color, &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.system +            .as_mut() +            .unwrap() +            .with_render_cache_mut(|cache| cache.trim()); + +        self.glyph_cache.trim(); +    } + +    pub fn measure( +        &self, +        content: &str, +        size: f32, +        font: Font, +        bounds: Size, +    ) -> (f32, f32) { +        self.system.as_ref().unwrap().with(|fields| { +            let mut measurement_cache = fields.measurement_cache.borrow_mut(); + +            let (_, paragraph) = measurement_cache.allocate( +                fields.fonts, +                Key { +                    content, +                    size, +                    font, +                    bounds, +                }, +            ); + +            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, +        point: Point, +        _nearest_only: bool, +    ) -> Option<Hit> { +        self.system.as_ref().unwrap().with(|fields| { +            let mut measurement_cache = fields.measurement_cache.borrow_mut(); + +            let (_, paragraph) = measurement_cache.allocate( +                fields.fonts, +                Key { +                    content, +                    size, +                    font, +                    bounds, +                }, +            ); + +            let cursor = paragraph.hit(point.x, point.y)?; + +            Some(Hit::CharOffset(cursor.index)) +        }) +    } + +    pub fn trim_measurement_cache(&mut self) { +        self.system +            .as_mut() +            .unwrap() +            .with_measurement_cache_mut(|cache| cache.borrow_mut().trim()); +    } +} + +fn to_family(font: Font) -> cosmic_text::Family<'static> { +    match font { +        Font::Name(name) => cosmic_text::Family::Name(name), +        Font::SansSerif => cosmic_text::Family::SansSerif, +        Font::Serif => cosmic_text::Family::Serif, +        Font::Cursive => cosmic_text::Family::Cursive, +        Font::Fantasy => cosmic_text::Family::Fantasy, +        Font::Monospace => cosmic_text::Family::Monospace, +    } +} + +#[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 { +    fn new() -> Self { +        GlyphCache::default() +    } + +    fn allocate( +        &mut self, +        cache_key: cosmic_text::CacheKey, +        color: Color, +        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(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 > 300 { +            self.entries +                .retain(|key, _| self.recently_used.contains(key)); + +            self.recently_used.clear(); + +            self.trim_count = 0; +        } else { +            self.trim_count += 1; +        } +    } +} + +struct Cache<'a> { +    entries: FxHashMap<KeyHash, cosmic_text::Buffer<'a>>, +    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<'a> Cache<'a> { +    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, +        fonts: &'a cosmic_text::FontSystem, +        key: Key<'_>, +    ) -> (KeyHash, &mut cosmic_text::Buffer<'a>) { +        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(fonts, metrics); + +            buffer.set_size( +                key.bounds.width, +                key.bounds.height.max(key.size * 1.2), +            ); +            buffer.set_text( +                key.content, +                cosmic_text::Attrs::new() +                    .family(to_family(key.font)) +                    .monospaced(matches!(key.font, Font::Monospace)), +            ); + +            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, +} + +type KeyHash = u64; diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs new file mode 100644 index 00000000..8509b761 --- /dev/null +++ b/tiny_skia/src/vector.rs @@ -0,0 +1,180 @@ +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::ClipMask>, +    ) { +        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> { +        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 { +                    usvg::FitTo::Width(size.width) +                } else { +                    usvg::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..cea1cabf --- /dev/null +++ b/tiny_skia/src/window/compositor.rs @@ -0,0 +1,130 @@ +use crate::core::Color; +use crate::graphics::compositor::{self, Information, SurfaceError}; +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> { +    clip_mask: tiny_skia::ClipMask, +    _theme: PhantomData<Theme>, +} + +pub struct Surface { +    window: softbuffer::GraphicsContext, +    buffer: Vec<u32>, +} + +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], +        } +    } + +    fn configure_surface( +        &mut self, +        surface: &mut Surface, +        width: u32, +        height: u32, +    ) { +        surface.buffer.resize((width * height) as usize, 0); +    } + +    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( +                self, +                backend, +                surface, +                primitives, +                viewport, +                background_color, +                overlay, +            ) +        }) +    } +} + +pub fn new<Theme>(settings: Settings) -> (Compositor<Theme>, Backend) { +    // TOD +    ( +        Compositor { +            clip_mask: tiny_skia::ClipMask::new(), +            _theme: PhantomData, +        }, +        Backend::new(settings), +    ) +} + +pub fn present<Theme, T: AsRef<str>>( +    compositor: &mut Compositor<Theme>, +    backend: &mut Backend, +    surface: &mut Surface, +    primitives: &[Primitive], +    viewport: &Viewport, +    background_color: Color, +    overlay: &[T], +) -> Result<(), compositor::SurfaceError> { +    let physical_size = viewport.physical_size(); + +    backend.draw( +        &mut tiny_skia::PixmapMut::from_bytes( +            bytemuck::cast_slice_mut(&mut surface.buffer), +            physical_size.width, +            physical_size.height, +        ) +        .expect("Create pixel map"), +        &mut compositor.clip_mask, +        primitives, +        viewport, +        background_color, +        overlay, +    ); + +    surface.window.set_buffer( +        &surface.buffer, +        physical_size.width as u16, +        physical_size.height as u16, +    ); + +    Ok(()) +} | 
