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 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 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 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 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 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 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: _, 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: _, 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, } => {
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}