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