iced_tiny_skia/
engine.rs

1use crate::Primitive;
2use crate::core::renderer::Quad;
3use crate::core::{
4    Background, Color, Gradient, Rectangle, Size, Transformation, Vector,
5};
6use crate::graphics::{Image, Text};
7use crate::text;
8
9#[derive(Debug)]
10pub struct Engine {
11    text_pipeline: text::Pipeline,
12
13    #[cfg(feature = "image")]
14    pub(crate) raster_pipeline: crate::raster::Pipeline,
15    #[cfg(feature = "svg")]
16    pub(crate) vector_pipeline: crate::vector::Pipeline,
17}
18
19impl Engine {
20    pub fn new() -> Self {
21        Self {
22            text_pipeline: text::Pipeline::new(),
23            #[cfg(feature = "image")]
24            raster_pipeline: crate::raster::Pipeline::new(),
25            #[cfg(feature = "svg")]
26            vector_pipeline: crate::vector::Pipeline::new(),
27        }
28    }
29
30    pub fn draw_quad(
31        &mut self,
32        quad: &Quad,
33        background: &Background,
34        transformation: Transformation,
35        pixels: &mut tiny_skia::PixmapMut<'_>,
36        clip_mask: &mut tiny_skia::Mask,
37        clip_bounds: Rectangle,
38    ) {
39        debug_assert!(
40            quad.bounds.width.is_normal(),
41            "Quad with non-normal width!"
42        );
43        debug_assert!(
44            quad.bounds.height.is_normal(),
45            "Quad with non-normal height!"
46        );
47
48        let physical_bounds = quad.bounds * transformation;
49
50        if !clip_bounds.intersects(&physical_bounds) {
51            return;
52        }
53
54        let clip_mask = (!physical_bounds.is_within(&clip_bounds))
55            .then_some(clip_mask as &_);
56
57        let transform = into_transform(transformation);
58
59        // Make sure the border radius is not larger than the bounds
60        let border_width = quad
61            .border
62            .width
63            .min(quad.bounds.width / 2.0)
64            .min(quad.bounds.height / 2.0);
65
66        let mut fill_border_radius = <[f32; 4]>::from(quad.border.radius);
67
68        for radius in &mut fill_border_radius {
69            *radius = (*radius)
70                .min(quad.bounds.width / 2.0)
71                .min(quad.bounds.height / 2.0);
72        }
73
74        let path = rounded_rectangle(quad.bounds, fill_border_radius);
75
76        let shadow = quad.shadow;
77
78        if shadow.color.a > 0.0 {
79            let shadow_bounds = Rectangle {
80                x: quad.bounds.x + shadow.offset.x - shadow.blur_radius,
81                y: quad.bounds.y + shadow.offset.y - shadow.blur_radius,
82                width: quad.bounds.width + shadow.blur_radius * 2.0,
83                height: quad.bounds.height + shadow.blur_radius * 2.0,
84            } * transformation;
85
86            let radii = fill_border_radius
87                .into_iter()
88                .map(|radius| radius * transformation.scale_factor())
89                .collect::<Vec<_>>();
90            let (x, y, width, height) = (
91                shadow_bounds.x as u32,
92                shadow_bounds.y as u32,
93                shadow_bounds.width as u32,
94                shadow_bounds.height as u32,
95            );
96            let half_width = physical_bounds.width / 2.0;
97            let half_height = physical_bounds.height / 2.0;
98
99            let colors = (y..y + height)
100                .flat_map(|y| (x..x + width).map(move |x| (x as f32, y as f32)))
101                .filter_map(|(x, y)| {
102                    tiny_skia::Size::from_wh(half_width, half_height).map(
103                        |size| {
104                            let shadow_distance = rounded_box_sdf(
105                                Vector::new(
106                                    x - physical_bounds.position().x
107                                        - (shadow.offset.x
108                                            * transformation.scale_factor())
109                                        - half_width,
110                                    y - physical_bounds.position().y
111                                        - (shadow.offset.y
112                                            * transformation.scale_factor())
113                                        - half_height,
114                                ),
115                                size,
116                                &radii,
117                            )
118                            .max(0.0);
119                            let shadow_alpha = 1.0
120                                - smoothstep(
121                                    -shadow.blur_radius
122                                        * transformation.scale_factor(),
123                                    shadow.blur_radius
124                                        * transformation.scale_factor(),
125                                    shadow_distance,
126                                );
127
128                            let mut color = into_color(shadow.color);
129                            color.apply_opacity(shadow_alpha);
130
131                            color.to_color_u8().premultiply()
132                        },
133                    )
134                })
135                .collect();
136
137            if let Some(pixmap) = tiny_skia::IntSize::from_wh(width, height)
138                .and_then(|size| {
139                    tiny_skia::Pixmap::from_vec(
140                        bytemuck::cast_vec(colors),
141                        size,
142                    )
143                })
144            {
145                pixels.draw_pixmap(
146                    x as i32,
147                    y as i32,
148                    pixmap.as_ref(),
149                    &tiny_skia::PixmapPaint::default(),
150                    tiny_skia::Transform::default(),
151                    None,
152                );
153            }
154        }
155
156        pixels.fill_path(
157            &path,
158            &tiny_skia::Paint {
159                shader: match background {
160                    Background::Color(color) => {
161                        tiny_skia::Shader::SolidColor(into_color(*color))
162                    }
163                    Background::Gradient(Gradient::Linear(linear)) => {
164                        let (start, end) =
165                            linear.angle.to_distance(&quad.bounds);
166
167                        let stops: Vec<tiny_skia::GradientStop> = linear
168                            .stops
169                            .into_iter()
170                            .flatten()
171                            .map(|stop| {
172                                tiny_skia::GradientStop::new(
173                                    stop.offset,
174                                    tiny_skia::Color::from_rgba(
175                                        stop.color.b,
176                                        stop.color.g,
177                                        stop.color.r,
178                                        stop.color.a,
179                                    )
180                                    .expect("Create color"),
181                                )
182                            })
183                            .collect();
184
185                        tiny_skia::LinearGradient::new(
186                            tiny_skia::Point {
187                                x: start.x,
188                                y: start.y,
189                            },
190                            tiny_skia::Point { x: end.x, y: end.y },
191                            if stops.is_empty() {
192                                vec![tiny_skia::GradientStop::new(
193                                    0.0,
194                                    tiny_skia::Color::BLACK,
195                                )]
196                            } else {
197                                stops
198                            },
199                            tiny_skia::SpreadMode::Pad,
200                            tiny_skia::Transform::identity(),
201                        )
202                        .expect("Create linear gradient")
203                    }
204                },
205                anti_alias: true,
206                ..tiny_skia::Paint::default()
207            },
208            tiny_skia::FillRule::EvenOdd,
209            transform,
210            clip_mask,
211        );
212
213        if border_width > 0.0 {
214            // Border path is offset by half the border width
215            let border_bounds = Rectangle {
216                x: quad.bounds.x + border_width / 2.0,
217                y: quad.bounds.y + border_width / 2.0,
218                width: quad.bounds.width - border_width,
219                height: quad.bounds.height - border_width,
220            };
221
222            // Make sure the border radius is correct
223            let mut border_radius = <[f32; 4]>::from(quad.border.radius);
224            let mut is_simple_border = true;
225
226            for radius in &mut border_radius {
227                *radius = if *radius == 0.0 {
228                    // Path should handle this fine
229                    0.0
230                } else if *radius > border_width / 2.0 {
231                    *radius - border_width / 2.0
232                } else {
233                    is_simple_border = false;
234                    0.0
235                }
236                .min(border_bounds.width / 2.0)
237                .min(border_bounds.height / 2.0);
238            }
239
240            // Stroking a path works well in this case
241            if is_simple_border {
242                let border_path =
243                    rounded_rectangle(border_bounds, border_radius);
244
245                pixels.stroke_path(
246                    &border_path,
247                    &tiny_skia::Paint {
248                        shader: tiny_skia::Shader::SolidColor(into_color(
249                            quad.border.color,
250                        )),
251                        anti_alias: true,
252                        ..tiny_skia::Paint::default()
253                    },
254                    &tiny_skia::Stroke {
255                        width: border_width,
256                        ..tiny_skia::Stroke::default()
257                    },
258                    transform,
259                    clip_mask,
260                );
261            } else {
262                // Draw corners that have too small border radii as having no border radius,
263                // but mask them with the rounded rectangle with the correct border radius.
264                let mut temp_pixmap = tiny_skia::Pixmap::new(
265                    quad.bounds.width as u32,
266                    quad.bounds.height as u32,
267                )
268                .unwrap();
269
270                let mut quad_mask = tiny_skia::Mask::new(
271                    quad.bounds.width as u32,
272                    quad.bounds.height as u32,
273                )
274                .unwrap();
275
276                let zero_bounds = Rectangle {
277                    x: 0.0,
278                    y: 0.0,
279                    width: quad.bounds.width,
280                    height: quad.bounds.height,
281                };
282                let path = rounded_rectangle(zero_bounds, fill_border_radius);
283
284                quad_mask.fill_path(
285                    &path,
286                    tiny_skia::FillRule::EvenOdd,
287                    true,
288                    transform,
289                );
290                let path_bounds = Rectangle {
291                    x: border_width / 2.0,
292                    y: border_width / 2.0,
293                    width: quad.bounds.width - border_width,
294                    height: quad.bounds.height - border_width,
295                };
296
297                let border_radius_path =
298                    rounded_rectangle(path_bounds, border_radius);
299
300                temp_pixmap.stroke_path(
301                    &border_radius_path,
302                    &tiny_skia::Paint {
303                        shader: tiny_skia::Shader::SolidColor(into_color(
304                            quad.border.color,
305                        )),
306                        anti_alias: true,
307                        ..tiny_skia::Paint::default()
308                    },
309                    &tiny_skia::Stroke {
310                        width: border_width,
311                        ..tiny_skia::Stroke::default()
312                    },
313                    transform,
314                    Some(&quad_mask),
315                );
316
317                pixels.draw_pixmap(
318                    quad.bounds.x as i32,
319                    quad.bounds.y as i32,
320                    temp_pixmap.as_ref(),
321                    &tiny_skia::PixmapPaint::default(),
322                    transform,
323                    clip_mask,
324                );
325            }
326        }
327    }
328
329    pub fn draw_text(
330        &mut self,
331        text: &Text,
332        transformation: Transformation,
333        pixels: &mut tiny_skia::PixmapMut<'_>,
334        clip_mask: &mut tiny_skia::Mask,
335        clip_bounds: Rectangle,
336    ) {
337        match text {
338            Text::Paragraph {
339                paragraph,
340                position,
341                color,
342                clip_bounds: _, // TODO
343                transformation: local_transformation,
344            } => {
345                let transformation = transformation * *local_transformation;
346
347                let physical_bounds =
348                    Rectangle::new(*position, paragraph.min_bounds)
349                        * transformation;
350
351                if !clip_bounds.intersects(&physical_bounds) {
352                    return;
353                }
354
355                let clip_mask = (!physical_bounds.is_within(&clip_bounds))
356                    .then_some(clip_mask as &_);
357
358                self.text_pipeline.draw_paragraph(
359                    paragraph,
360                    *position,
361                    *color,
362                    pixels,
363                    clip_mask,
364                    transformation,
365                );
366            }
367            Text::Editor {
368                editor,
369                position,
370                color,
371                clip_bounds: _, // TODO
372                transformation: local_transformation,
373            } => {
374                let transformation = transformation * *local_transformation;
375
376                let physical_bounds =
377                    Rectangle::new(*position, editor.bounds) * transformation;
378
379                if !clip_bounds.intersects(&physical_bounds) {
380                    return;
381                }
382
383                let clip_mask = (!physical_bounds.is_within(&clip_bounds))
384                    .then_some(clip_mask as &_);
385
386                self.text_pipeline.draw_editor(
387                    editor,
388                    *position,
389                    *color,
390                    pixels,
391                    clip_mask,
392                    transformation,
393                );
394            }
395            Text::Cached {
396                content,
397                bounds,
398                color,
399                size,
400                line_height,
401                font,
402                align_x,
403                align_y,
404                shaping,
405                clip_bounds: text_bounds, // TODO
406            } => {
407                let physical_bounds = *text_bounds * transformation;
408
409                if !clip_bounds.intersects(&physical_bounds) {
410                    return;
411                }
412
413                let clip_mask = (!physical_bounds.is_within(&clip_bounds))
414                    .then_some(clip_mask as &_);
415
416                self.text_pipeline.draw_cached(
417                    content,
418                    *bounds,
419                    *color,
420                    *size,
421                    *line_height,
422                    *font,
423                    *align_x,
424                    *align_y,
425                    *shaping,
426                    pixels,
427                    clip_mask,
428                    transformation,
429                );
430            }
431            Text::Raw {
432                raw,
433                transformation: local_transformation,
434            } => {
435                let Some(buffer) = raw.buffer.upgrade() else {
436                    return;
437                };
438
439                let transformation = transformation * *local_transformation;
440                let (width, height) = buffer.size();
441
442                let physical_bounds = Rectangle::new(
443                    raw.position,
444                    Size::new(
445                        width.unwrap_or(clip_bounds.width),
446                        height.unwrap_or(clip_bounds.height),
447                    ),
448                ) * transformation;
449
450                if !clip_bounds.intersects(&physical_bounds) {
451                    return;
452                }
453
454                let clip_mask = (!physical_bounds.is_within(&clip_bounds))
455                    .then_some(clip_mask as &_);
456
457                self.text_pipeline.draw_raw(
458                    &buffer,
459                    raw.position,
460                    raw.color,
461                    pixels,
462                    clip_mask,
463                    transformation,
464                );
465            }
466        }
467    }
468
469    pub fn draw_primitive(
470        &mut self,
471        primitive: &Primitive,
472        transformation: Transformation,
473        pixels: &mut tiny_skia::PixmapMut<'_>,
474        clip_mask: &mut tiny_skia::Mask,
475        layer_bounds: Rectangle,
476    ) {
477        match primitive {
478            Primitive::Fill { path, paint, rule } => {
479                let physical_bounds = {
480                    let bounds = path.bounds();
481
482                    Rectangle {
483                        x: bounds.x(),
484                        y: bounds.y(),
485                        width: bounds.width(),
486                        height: bounds.height(),
487                    } * transformation
488                };
489
490                let Some(clip_bounds) =
491                    layer_bounds.intersection(&physical_bounds)
492                else {
493                    return;
494                };
495
496                let clip_mask =
497                    (physical_bounds != clip_bounds).then_some(clip_mask as &_);
498
499                pixels.fill_path(
500                    path,
501                    paint,
502                    *rule,
503                    into_transform(transformation),
504                    clip_mask,
505                );
506            }
507            Primitive::Stroke {
508                path,
509                paint,
510                stroke,
511            } => {
512                let physical_bounds = {
513                    let bounds = path.bounds();
514
515                    Rectangle {
516                        x: bounds.x(),
517                        y: bounds.y(),
518                        width: bounds.width(),
519                        height: bounds.height(),
520                    } * transformation
521                };
522
523                let Some(clip_bounds) =
524                    layer_bounds.intersection(&physical_bounds)
525                else {
526                    return;
527                };
528
529                let clip_mask =
530                    (physical_bounds != clip_bounds).then_some(clip_mask as &_);
531
532                pixels.stroke_path(
533                    path,
534                    paint,
535                    stroke,
536                    into_transform(transformation),
537                    clip_mask,
538                );
539            }
540        }
541    }
542
543    pub fn draw_image(
544        &mut self,
545        image: &Image,
546        _transformation: Transformation,
547        _pixels: &mut tiny_skia::PixmapMut<'_>,
548        _clip_mask: &mut tiny_skia::Mask,
549        _clip_bounds: Rectangle,
550    ) {
551        match image {
552            #[cfg(feature = "image")]
553            Image::Raster(raster, bounds) => {
554                let physical_bounds = *bounds * _transformation;
555
556                if !_clip_bounds.intersects(&physical_bounds) {
557                    return;
558                }
559
560                let clip_mask = (!physical_bounds.is_within(&_clip_bounds))
561                    .then_some(_clip_mask as &_);
562
563                let center = physical_bounds.center();
564                let radians = f32::from(raster.rotation);
565
566                let transform = into_transform(_transformation).post_rotate_at(
567                    radians.to_degrees(),
568                    center.x,
569                    center.y,
570                );
571
572                self.raster_pipeline.draw(
573                    &raster.handle,
574                    raster.filter_method,
575                    *bounds,
576                    raster.opacity,
577                    _pixels,
578                    transform,
579                    clip_mask,
580                );
581            }
582            #[cfg(feature = "svg")]
583            Image::Vector(svg, bounds) => {
584                let physical_bounds = *bounds * _transformation;
585
586                if !_clip_bounds.intersects(&physical_bounds) {
587                    return;
588                }
589
590                let clip_mask = (!physical_bounds.is_within(&_clip_bounds))
591                    .then_some(_clip_mask as &_);
592
593                let center = physical_bounds.center();
594                let radians = f32::from(svg.rotation);
595
596                let transform = into_transform(_transformation).post_rotate_at(
597                    radians.to_degrees(),
598                    center.x,
599                    center.y,
600                );
601
602                self.vector_pipeline.draw(
603                    &svg.handle,
604                    svg.color,
605                    physical_bounds,
606                    svg.opacity,
607                    _pixels,
608                    transform,
609                    clip_mask,
610                );
611            }
612            #[cfg(not(feature = "image"))]
613            Image::Raster { .. } => {
614                log::warn!(
615                    "Unsupported primitive in `iced_tiny_skia`: {image:?}",
616                );
617            }
618            #[cfg(not(feature = "svg"))]
619            Image::Vector { .. } => {
620                log::warn!(
621                    "Unsupported primitive in `iced_tiny_skia`: {image:?}",
622                );
623            }
624        }
625    }
626
627    pub fn trim(&mut self) {
628        self.text_pipeline.trim_cache();
629
630        #[cfg(feature = "image")]
631        self.raster_pipeline.trim_cache();
632
633        #[cfg(feature = "svg")]
634        self.vector_pipeline.trim_cache();
635    }
636}
637
638pub fn into_color(color: Color) -> tiny_skia::Color {
639    tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a)
640        .expect("Convert color from iced to tiny_skia")
641}
642
643fn into_transform(transformation: Transformation) -> tiny_skia::Transform {
644    let translation = transformation.translation();
645
646    tiny_skia::Transform {
647        sx: transformation.scale_factor(),
648        kx: 0.0,
649        ky: 0.0,
650        sy: transformation.scale_factor(),
651        tx: translation.x,
652        ty: translation.y,
653    }
654}
655
656fn rounded_rectangle(
657    bounds: Rectangle,
658    border_radius: [f32; 4],
659) -> tiny_skia::Path {
660    let [top_left, top_right, bottom_right, bottom_left] = border_radius;
661
662    if top_left == 0.0
663        && top_right == 0.0
664        && bottom_right == 0.0
665        && bottom_left == 0.0
666    {
667        return tiny_skia::PathBuilder::from_rect(
668            tiny_skia::Rect::from_xywh(
669                bounds.x,
670                bounds.y,
671                bounds.width,
672                bounds.height,
673            )
674            .expect("Build quad rectangle"),
675        );
676    }
677
678    if top_left == top_right
679        && top_left == bottom_right
680        && top_left == bottom_left
681        && top_left == bounds.width / 2.0
682        && top_left == bounds.height / 2.0
683    {
684        return tiny_skia::PathBuilder::from_circle(
685            bounds.x + bounds.width / 2.0,
686            bounds.y + bounds.height / 2.0,
687            top_left,
688        )
689        .expect("Build circle path");
690    }
691
692    let mut builder = tiny_skia::PathBuilder::new();
693
694    builder.move_to(bounds.x + top_left, bounds.y);
695    builder.line_to(bounds.x + bounds.width - top_right, bounds.y);
696
697    if top_right > 0.0 {
698        arc_to(
699            &mut builder,
700            bounds.x + bounds.width - top_right,
701            bounds.y,
702            bounds.x + bounds.width,
703            bounds.y + top_right,
704            top_right,
705        );
706    }
707
708    maybe_line_to(
709        &mut builder,
710        bounds.x + bounds.width,
711        bounds.y + bounds.height - bottom_right,
712    );
713
714    if bottom_right > 0.0 {
715        arc_to(
716            &mut builder,
717            bounds.x + bounds.width,
718            bounds.y + bounds.height - bottom_right,
719            bounds.x + bounds.width - bottom_right,
720            bounds.y + bounds.height,
721            bottom_right,
722        );
723    }
724
725    maybe_line_to(
726        &mut builder,
727        bounds.x + bottom_left,
728        bounds.y + bounds.height,
729    );
730
731    if bottom_left > 0.0 {
732        arc_to(
733            &mut builder,
734            bounds.x + bottom_left,
735            bounds.y + bounds.height,
736            bounds.x,
737            bounds.y + bounds.height - bottom_left,
738            bottom_left,
739        );
740    }
741
742    maybe_line_to(&mut builder, bounds.x, bounds.y + top_left);
743
744    if top_left > 0.0 {
745        arc_to(
746            &mut builder,
747            bounds.x,
748            bounds.y + top_left,
749            bounds.x + top_left,
750            bounds.y,
751            top_left,
752        );
753    }
754
755    builder.finish().expect("Build rounded rectangle path")
756}
757
758fn maybe_line_to(path: &mut tiny_skia::PathBuilder, x: f32, y: f32) {
759    if path.last_point() != Some(tiny_skia::Point { x, y }) {
760        path.line_to(x, y);
761    }
762}
763
764fn arc_to(
765    path: &mut tiny_skia::PathBuilder,
766    x_from: f32,
767    y_from: f32,
768    x_to: f32,
769    y_to: f32,
770    radius: f32,
771) {
772    let svg_arc = kurbo::SvgArc {
773        from: kurbo::Point::new(f64::from(x_from), f64::from(y_from)),
774        to: kurbo::Point::new(f64::from(x_to), f64::from(y_to)),
775        radii: kurbo::Vec2::new(f64::from(radius), f64::from(radius)),
776        x_rotation: 0.0,
777        large_arc: false,
778        sweep: true,
779    };
780
781    match kurbo::Arc::from_svg_arc(&svg_arc) {
782        Some(arc) => {
783            arc.to_cubic_beziers(0.1, |p1, p2, p| {
784                path.cubic_to(
785                    p1.x as f32,
786                    p1.y as f32,
787                    p2.x as f32,
788                    p2.y as f32,
789                    p.x as f32,
790                    p.y as f32,
791                );
792            });
793        }
794        None => {
795            path.line_to(x_to, y_to);
796        }
797    }
798}
799
800fn smoothstep(a: f32, b: f32, x: f32) -> f32 {
801    let x = ((x - a) / (b - a)).clamp(0.0, 1.0);
802
803    x * x * (3.0 - 2.0 * x)
804}
805
806fn rounded_box_sdf(
807    to_center: Vector,
808    size: tiny_skia::Size,
809    radii: &[f32],
810) -> f32 {
811    let radius = match (to_center.x > 0.0, to_center.y > 0.0) {
812        (true, true) => radii[2],
813        (true, false) => radii[1],
814        (false, true) => radii[3],
815        (false, false) => radii[0],
816    };
817
818    let x = (to_center.x.abs() - size.width() + radius).max(0.0);
819    let y = (to_center.y.abs() - size.height() + radius).max(0.0);
820
821    (x.powf(2.0) + y.powf(2.0)).sqrt() - radius
822}
823
824pub fn adjust_clip_mask(clip_mask: &mut tiny_skia::Mask, bounds: Rectangle) {
825    clip_mask.clear();
826
827    let path = {
828        let mut builder = tiny_skia::PathBuilder::new();
829        builder.push_rect(
830            tiny_skia::Rect::from_xywh(
831                bounds.x,
832                bounds.y,
833                bounds.width,
834                bounds.height,
835            )
836            .unwrap(),
837        );
838
839        builder.finish().unwrap()
840    };
841
842    clip_mask.fill_path(
843        &path,
844        tiny_skia::FillRule::EvenOdd,
845        false,
846        tiny_skia::Transform::default(),
847    );
848}