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 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 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 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 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 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 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}