iced_tiny_skia/
engine.rs

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