iced_tiny_skia/
engine.rs

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