iced_tiny_skia/
lib.rs

1#![allow(missing_docs)]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3pub mod window;
4
5mod engine;
6mod layer;
7mod primitive;
8mod settings;
9mod text;
10
11#[cfg(feature = "image")]
12mod raster;
13
14#[cfg(feature = "svg")]
15mod vector;
16
17#[cfg(feature = "geometry")]
18pub mod geometry;
19
20use iced_debug as debug;
21pub use iced_graphics as graphics;
22pub use iced_graphics::core;
23
24pub use layer::Layer;
25pub use primitive::Primitive;
26pub use settings::Settings;
27
28#[cfg(feature = "geometry")]
29pub use geometry::Geometry;
30
31use crate::core::renderer;
32use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation};
33use crate::engine::Engine;
34use crate::graphics::Viewport;
35use crate::graphics::compositor;
36use crate::graphics::text::{Editor, Paragraph};
37
38/// A [`tiny-skia`] graphics renderer for [`iced`].
39///
40/// [`tiny-skia`]: https://github.com/RazrFalcon/tiny-skia
41/// [`iced`]: https://github.com/iced-rs/iced
42#[derive(Debug)]
43pub struct Renderer {
44    default_font: Font,
45    default_text_size: Pixels,
46    layers: layer::Stack,
47    engine: Engine, // TODO: Shared engine
48}
49
50impl Renderer {
51    pub fn new(default_font: Font, default_text_size: Pixels) -> Self {
52        Self {
53            default_font,
54            default_text_size,
55            layers: layer::Stack::new(),
56            engine: Engine::new(),
57        }
58    }
59
60    pub fn layers(&mut self) -> &[Layer] {
61        self.layers.flush();
62        self.layers.as_slice()
63    }
64
65    pub fn draw(
66        &mut self,
67        pixels: &mut tiny_skia::PixmapMut<'_>,
68        clip_mask: &mut tiny_skia::Mask,
69        viewport: &Viewport,
70        damage: &[Rectangle],
71        background_color: Color,
72    ) {
73        let scale_factor = viewport.scale_factor();
74
75        self.layers.flush();
76
77        for &damage_bounds in damage {
78            let damage_bounds = damage_bounds * scale_factor;
79
80            let path = tiny_skia::PathBuilder::from_rect(
81                tiny_skia::Rect::from_xywh(
82                    damage_bounds.x,
83                    damage_bounds.y,
84                    damage_bounds.width,
85                    damage_bounds.height,
86                )
87                .expect("Create damage rectangle"),
88            );
89
90            pixels.fill_path(
91                &path,
92                &tiny_skia::Paint {
93                    shader: tiny_skia::Shader::SolidColor(engine::into_color(background_color)),
94                    anti_alias: false,
95                    blend_mode: tiny_skia::BlendMode::Source,
96                    ..Default::default()
97                },
98                tiny_skia::FillRule::default(),
99                tiny_skia::Transform::identity(),
100                None,
101            );
102
103            for layer in self.layers.iter() {
104                let Some(layer_bounds) = damage_bounds.intersection(&(layer.bounds * scale_factor))
105                else {
106                    continue;
107                };
108
109                engine::adjust_clip_mask(clip_mask, layer_bounds);
110
111                if !layer.quads.is_empty() {
112                    let render_span = debug::render(debug::Primitive::Quad);
113                    for (quad, background) in &layer.quads {
114                        self.engine.draw_quad(
115                            quad,
116                            background,
117                            Transformation::scale(scale_factor),
118                            pixels,
119                            clip_mask,
120                            layer_bounds,
121                        );
122                    }
123                    render_span.finish();
124                }
125
126                if !layer.primitives.is_empty() {
127                    let render_span = debug::render(debug::Primitive::Triangle);
128
129                    for group in &layer.primitives {
130                        let Some(group_bounds) =
131                            (group.clip_bounds() * group.transformation() * scale_factor)
132                                .intersection(&layer_bounds)
133                        else {
134                            continue;
135                        };
136
137                        engine::adjust_clip_mask(clip_mask, group_bounds);
138
139                        for primitive in group.as_slice() {
140                            self.engine.draw_primitive(
141                                primitive,
142                                group.transformation() * Transformation::scale(scale_factor),
143                                pixels,
144                                clip_mask,
145                                group_bounds,
146                            );
147                        }
148
149                        engine::adjust_clip_mask(clip_mask, layer_bounds);
150                    }
151
152                    render_span.finish();
153                }
154
155                if !layer.images.is_empty() {
156                    let render_span = debug::render(debug::Primitive::Image);
157
158                    for image in &layer.images {
159                        self.engine.draw_image(
160                            image,
161                            Transformation::scale(scale_factor),
162                            pixels,
163                            clip_mask,
164                            layer_bounds,
165                        );
166                    }
167
168                    render_span.finish();
169                }
170
171                if !layer.text.is_empty() {
172                    let render_span = debug::render(debug::Primitive::Image);
173
174                    for group in &layer.text {
175                        for text in group.as_slice() {
176                            self.engine.draw_text(
177                                text,
178                                group.transformation() * Transformation::scale(scale_factor),
179                                pixels,
180                                clip_mask,
181                                layer_bounds,
182                            );
183                        }
184                    }
185
186                    render_span.finish();
187                }
188            }
189        }
190
191        self.engine.trim();
192    }
193}
194
195impl core::Renderer for Renderer {
196    fn start_layer(&mut self, bounds: Rectangle) {
197        self.layers.push_clip(bounds);
198    }
199
200    fn end_layer(&mut self) {
201        self.layers.pop_clip();
202    }
203
204    fn start_transformation(&mut self, transformation: Transformation) {
205        self.layers.push_transformation(transformation);
206    }
207
208    fn end_transformation(&mut self) {
209        self.layers.pop_transformation();
210    }
211
212    fn fill_quad(&mut self, quad: renderer::Quad, background: impl Into<Background>) {
213        let (layer, transformation) = self.layers.current_mut();
214        layer.draw_quad(quad, background.into(), transformation);
215    }
216
217    fn reset(&mut self, new_bounds: Rectangle) {
218        self.layers.reset(new_bounds);
219    }
220
221    fn allocate_image(
222        &mut self,
223        _handle: &core::image::Handle,
224        callback: impl FnOnce(Result<core::image::Allocation, core::image::Error>) + Send + 'static,
225    ) {
226        #[cfg(feature = "image")]
227        #[allow(unsafe_code)]
228        // TODO: Concurrency
229        callback(self.engine.raster_pipeline.load(_handle));
230
231        #[cfg(not(feature = "image"))]
232        callback(Err(core::image::Error::Unsupported));
233    }
234}
235
236impl core::text::Renderer for Renderer {
237    type Font = Font;
238    type Paragraph = Paragraph;
239    type Editor = Editor;
240
241    const ICON_FONT: Font = Font::with_name("Iced-Icons");
242    const CHECKMARK_ICON: char = '\u{f00c}';
243    const ARROW_DOWN_ICON: char = '\u{e800}';
244    const ICED_LOGO: char = '\u{e801}';
245    const SCROLL_UP_ICON: char = '\u{e802}';
246    const SCROLL_DOWN_ICON: char = '\u{e803}';
247    const SCROLL_LEFT_ICON: char = '\u{e804}';
248    const SCROLL_RIGHT_ICON: char = '\u{e805}';
249
250    fn default_font(&self) -> Self::Font {
251        self.default_font
252    }
253
254    fn default_size(&self) -> Pixels {
255        self.default_text_size
256    }
257
258    fn fill_paragraph(
259        &mut self,
260        text: &Self::Paragraph,
261        position: Point,
262        color: Color,
263        clip_bounds: Rectangle,
264    ) {
265        let (layer, transformation) = self.layers.current_mut();
266
267        layer.draw_paragraph(text, position, color, clip_bounds, transformation);
268    }
269
270    fn fill_editor(
271        &mut self,
272        editor: &Self::Editor,
273        position: Point,
274        color: Color,
275        clip_bounds: Rectangle,
276    ) {
277        let (layer, transformation) = self.layers.current_mut();
278        layer.draw_editor(editor, position, color, clip_bounds, transformation);
279    }
280
281    fn fill_text(
282        &mut self,
283        text: core::Text,
284        position: Point,
285        color: Color,
286        clip_bounds: Rectangle,
287    ) {
288        let (layer, transformation) = self.layers.current_mut();
289        layer.draw_text(text, position, color, clip_bounds, transformation);
290    }
291}
292
293impl graphics::text::Renderer for Renderer {
294    fn fill_raw(&mut self, raw: graphics::text::Raw) {
295        let (layer, transformation) = self.layers.current_mut();
296        layer.draw_text_raw(raw, transformation);
297    }
298}
299
300#[cfg(feature = "geometry")]
301impl graphics::geometry::Renderer for Renderer {
302    type Geometry = Geometry;
303    type Frame = geometry::Frame;
304
305    fn new_frame(&self, bounds: Rectangle) -> Self::Frame {
306        geometry::Frame::new(bounds)
307    }
308
309    fn draw_geometry(&mut self, geometry: Self::Geometry) {
310        let (layer, transformation) = self.layers.current_mut();
311
312        match geometry {
313            Geometry::Live {
314                primitives,
315                images,
316                text,
317                clip_bounds,
318            } => {
319                layer.draw_primitive_group(primitives, clip_bounds, transformation);
320
321                for image in images {
322                    layer.draw_image(image, transformation);
323                }
324
325                layer.draw_text_group(text, clip_bounds, transformation);
326            }
327            Geometry::Cache(cache) => {
328                layer.draw_primitive_cache(cache.primitives, cache.clip_bounds, transformation);
329
330                for image in cache.images.iter() {
331                    layer.draw_image(image.clone(), transformation);
332                }
333
334                layer.draw_text_cache(cache.text, cache.clip_bounds, transformation);
335            }
336        }
337    }
338}
339
340impl graphics::mesh::Renderer for Renderer {
341    fn draw_mesh(&mut self, _mesh: graphics::Mesh) {
342        log::warn!("iced_tiny_skia does not support drawing meshes");
343    }
344
345    fn draw_mesh_cache(&mut self, _cache: iced_graphics::mesh::Cache) {
346        log::warn!("iced_tiny_skia does not support drawing meshes");
347    }
348}
349
350#[cfg(feature = "image")]
351impl core::image::Renderer for Renderer {
352    type Handle = core::image::Handle;
353
354    fn load_image(
355        &self,
356        handle: &Self::Handle,
357    ) -> Result<core::image::Allocation, core::image::Error> {
358        self.engine.raster_pipeline.load(handle)
359    }
360
361    fn measure_image(&self, handle: &Self::Handle) -> Option<crate::core::Size<u32>> {
362        self.engine.raster_pipeline.dimensions(handle)
363    }
364
365    fn draw_image(&mut self, image: core::Image, bounds: Rectangle, clip_bounds: Rectangle) {
366        let (layer, transformation) = self.layers.current_mut();
367        layer.draw_raster(image, bounds, clip_bounds, transformation);
368    }
369}
370
371#[cfg(feature = "svg")]
372impl core::svg::Renderer for Renderer {
373    fn measure_svg(&self, handle: &core::svg::Handle) -> crate::core::Size<u32> {
374        self.engine.vector_pipeline.viewport_dimensions(handle)
375    }
376
377    fn draw_svg(&mut self, svg: core::Svg, bounds: Rectangle, clip_bounds: Rectangle) {
378        let (layer, transformation) = self.layers.current_mut();
379        layer.draw_svg(svg, bounds, clip_bounds, transformation);
380    }
381}
382
383impl compositor::Default for Renderer {
384    type Compositor = window::Compositor;
385}
386
387impl renderer::Headless for Renderer {
388    async fn new(
389        default_font: Font,
390        default_text_size: Pixels,
391        backend: Option<&str>,
392    ) -> Option<Self> {
393        if backend.is_some_and(|backend| !["tiny-skia", "tiny_skia"].contains(&backend)) {
394            return None;
395        }
396
397        Some(Self::new(default_font, default_text_size))
398    }
399
400    fn name(&self) -> String {
401        "tiny-skia".to_owned()
402    }
403
404    fn screenshot(
405        &mut self,
406        size: Size<u32>,
407        scale_factor: f32,
408        background_color: Color,
409    ) -> Vec<u8> {
410        let viewport = Viewport::with_physical_size(size, scale_factor);
411
412        window::compositor::screenshot(self, &viewport, background_color)
413    }
414}