1use crate::{Brush, Color, ColorFilter, ImageBitmap, ImageSampling};
4use std::ops::AddAssign;
5
6#[derive(Clone, Copy, Debug, PartialEq, Default)]
7pub struct Point {
8 pub x: f32,
9 pub y: f32,
10}
11
12impl Point {
13 pub const fn new(x: f32, y: f32) -> Self {
14 Self { x, y }
15 }
16
17 pub const ZERO: Point = Point { x: 0.0, y: 0.0 };
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Default)]
21pub struct Size {
22 pub width: f32,
23 pub height: f32,
24}
25
26impl Size {
27 pub const fn new(width: f32, height: f32) -> Self {
28 Self { width, height }
29 }
30
31 pub const ZERO: Size = Size {
32 width: 0.0,
33 height: 0.0,
34 };
35}
36
37#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct Rect {
39 pub x: f32,
40 pub y: f32,
41 pub width: f32,
42 pub height: f32,
43}
44
45impl Rect {
46 pub fn from_origin_size(origin: Point, size: Size) -> Self {
47 Self {
48 x: origin.x,
49 y: origin.y,
50 width: size.width,
51 height: size.height,
52 }
53 }
54
55 pub fn from_size(size: Size) -> Self {
56 Self {
57 x: 0.0,
58 y: 0.0,
59 width: size.width,
60 height: size.height,
61 }
62 }
63
64 pub fn translate(&self, dx: f32, dy: f32) -> Self {
65 Self {
66 x: self.x + dx,
67 y: self.y + dy,
68 width: self.width,
69 height: self.height,
70 }
71 }
72
73 pub fn contains(&self, x: f32, y: f32) -> bool {
74 x >= self.x && y >= self.y && x <= self.x + self.width && y <= self.y + self.height
75 }
76
77 pub fn intersect(&self, other: Rect) -> Option<Rect> {
79 let left = self.x.max(other.x);
80 let top = self.y.max(other.y);
81 let right = (self.x + self.width).min(other.x + other.width);
82 let bottom = (self.y + self.height).min(other.y + other.height);
83 let width = right - left;
84 let height = bottom - top;
85 if width <= 0.0 || height <= 0.0 {
86 None
87 } else {
88 Some(Rect {
89 x: left,
90 y: top,
91 width,
92 height,
93 })
94 }
95 }
96
97 pub fn union(&self, other: Rect) -> Rect {
98 let left = self.x.min(other.x);
99 let top = self.y.min(other.y);
100 let right = (self.x + self.width).max(other.x + other.width);
101 let bottom = (self.y + self.height).max(other.y + other.height);
102 Rect {
103 x: left,
104 y: top,
105 width: (right - left).max(0.0),
106 height: (bottom - top).max(0.0),
107 }
108 }
109}
110
111#[derive(Clone, Copy, Debug, Default, PartialEq)]
113pub struct EdgeInsets {
114 pub left: f32,
115 pub top: f32,
116 pub right: f32,
117 pub bottom: f32,
118}
119
120impl EdgeInsets {
121 pub fn uniform(all: f32) -> Self {
122 Self {
123 left: all,
124 top: all,
125 right: all,
126 bottom: all,
127 }
128 }
129
130 pub fn horizontal(horizontal: f32) -> Self {
131 Self {
132 left: horizontal,
133 right: horizontal,
134 ..Self::default()
135 }
136 }
137
138 pub fn vertical(vertical: f32) -> Self {
139 Self {
140 top: vertical,
141 bottom: vertical,
142 ..Self::default()
143 }
144 }
145
146 pub fn symmetric(horizontal: f32, vertical: f32) -> Self {
147 Self {
148 left: horizontal,
149 right: horizontal,
150 top: vertical,
151 bottom: vertical,
152 }
153 }
154
155 pub fn from_components(left: f32, top: f32, right: f32, bottom: f32) -> Self {
156 Self {
157 left,
158 top,
159 right,
160 bottom,
161 }
162 }
163
164 pub fn is_zero(&self) -> bool {
165 self.left == 0.0 && self.top == 0.0 && self.right == 0.0 && self.bottom == 0.0
166 }
167
168 pub fn horizontal_sum(&self) -> f32 {
169 self.left + self.right
170 }
171
172 pub fn vertical_sum(&self) -> f32 {
173 self.top + self.bottom
174 }
175}
176
177impl AddAssign for EdgeInsets {
178 fn add_assign(&mut self, rhs: Self) {
179 self.left += rhs.left;
180 self.top += rhs.top;
181 self.right += rhs.right;
182 self.bottom += rhs.bottom;
183 }
184}
185
186#[derive(Clone, Copy, Debug, Default, PartialEq)]
187pub struct CornerRadii {
188 pub top_left: f32,
189 pub top_right: f32,
190 pub bottom_right: f32,
191 pub bottom_left: f32,
192}
193
194impl CornerRadii {
195 pub fn uniform(radius: f32) -> Self {
196 Self {
197 top_left: radius,
198 top_right: radius,
199 bottom_right: radius,
200 bottom_left: radius,
201 }
202 }
203}
204
205#[derive(Clone, Copy, Debug, PartialEq)]
206pub struct RoundedCornerShape {
207 radii: CornerRadii,
208}
209
210impl RoundedCornerShape {
211 pub fn new(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self {
212 Self {
213 radii: CornerRadii {
214 top_left,
215 top_right,
216 bottom_right,
217 bottom_left,
218 },
219 }
220 }
221
222 pub fn uniform(radius: f32) -> Self {
223 Self {
224 radii: CornerRadii::uniform(radius),
225 }
226 }
227
228 pub fn with_radii(radii: CornerRadii) -> Self {
229 Self { radii }
230 }
231
232 pub fn resolve(&self, width: f32, height: f32) -> CornerRadii {
233 let mut resolved = self.radii;
234 let max_width = (width / 2.0).max(0.0);
235 let max_height = (height / 2.0).max(0.0);
236 resolved.top_left = resolved.top_left.clamp(0.0, max_width).min(max_height);
237 resolved.top_right = resolved.top_right.clamp(0.0, max_width).min(max_height);
238 resolved.bottom_right = resolved.bottom_right.clamp(0.0, max_width).min(max_height);
239 resolved.bottom_left = resolved.bottom_left.clamp(0.0, max_width).min(max_height);
240 resolved
241 }
242
243 pub fn radii(&self) -> CornerRadii {
244 self.radii
245 }
246}
247
248#[derive(Clone, Copy, Debug, PartialEq)]
249pub struct TransformOrigin {
250 pub pivot_fraction_x: f32,
251 pub pivot_fraction_y: f32,
252}
253
254impl TransformOrigin {
255 pub const fn new(pivot_fraction_x: f32, pivot_fraction_y: f32) -> Self {
256 Self {
257 pivot_fraction_x,
258 pivot_fraction_y,
259 }
260 }
261
262 pub const CENTER: TransformOrigin = TransformOrigin::new(0.5, 0.5);
263}
264
265impl Default for TransformOrigin {
266 fn default() -> Self {
267 Self::CENTER
268 }
269}
270
271#[derive(Clone, Copy, Debug, Default, PartialEq)]
272pub enum LayerShape {
273 #[default]
274 Rectangle,
275 Rounded(RoundedCornerShape),
276}
277
278#[derive(Clone, Debug, PartialEq)]
279pub struct GraphicsLayer {
280 pub alpha: f32,
281 pub scale: f32,
282 pub scale_x: f32,
283 pub scale_y: f32,
284 pub rotation_x: f32,
285 pub rotation_y: f32,
286 pub rotation_z: f32,
287 pub camera_distance: f32,
288 pub transform_origin: TransformOrigin,
289 pub translation_x: f32,
290 pub translation_y: f32,
291 pub shadow_elevation: f32,
292 pub ambient_shadow_color: Color,
293 pub spot_shadow_color: Color,
294 pub shape: LayerShape,
295 pub clip: bool,
296 pub compositing_strategy: CompositingStrategy,
297 pub blend_mode: BlendMode,
298 pub color_filter: Option<ColorFilter>,
299 pub render_effect: Option<crate::render_effect::RenderEffect>,
300 pub backdrop_effect: Option<crate::render_effect::RenderEffect>,
301}
302
303impl Default for GraphicsLayer {
304 fn default() -> Self {
305 Self {
306 alpha: 1.0,
307 scale: 1.0,
308 scale_x: 1.0,
309 scale_y: 1.0,
310 rotation_x: 0.0,
311 rotation_y: 0.0,
312 rotation_z: 0.0,
313 camera_distance: 8.0,
314 transform_origin: TransformOrigin::CENTER,
315 translation_x: 0.0,
316 translation_y: 0.0,
317 shadow_elevation: 0.0,
318 ambient_shadow_color: Color::BLACK,
319 spot_shadow_color: Color::BLACK,
320 shape: LayerShape::Rectangle,
321 clip: false,
322 compositing_strategy: CompositingStrategy::Auto,
323 blend_mode: BlendMode::SrcOver,
324 color_filter: None,
325 render_effect: None,
326 backdrop_effect: None,
327 }
328 }
329}
330
331#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
336pub enum BlendMode {
337 Clear,
338 Src,
339 Dst,
340 #[default]
341 SrcOver,
342 DstOver,
343 SrcIn,
344 DstIn,
345 SrcOut,
346 DstOut,
347 SrcAtop,
348 DstAtop,
349 Xor,
350 Plus,
351 Modulate,
352 Screen,
353 Overlay,
354 Darken,
355 Lighten,
356 ColorDodge,
357 ColorBurn,
358 HardLight,
359 SoftLight,
360 Difference,
361 Exclusion,
362 Multiply,
363 Hue,
364 Saturation,
365 Color,
366 Luminosity,
367}
368
369#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
371pub enum CompositingStrategy {
372 #[default]
374 Auto,
375 Offscreen,
377 ModulateAlpha,
379}
380
381#[derive(Clone, Debug, PartialEq)]
382pub enum DrawPrimitive {
383 Content,
386 Blend {
388 primitive: Box<DrawPrimitive>,
389 blend_mode: BlendMode,
390 },
391 Rect {
392 rect: Rect,
393 brush: Brush,
394 },
395 RoundRect {
396 rect: Rect,
397 brush: Brush,
398 radii: CornerRadii,
399 },
400 Image {
401 rect: Rect,
402 image: ImageBitmap,
403 alpha: f32,
404 color_filter: Option<ColorFilter>,
405 sampling: ImageSampling,
406 src_rect: Option<Rect>,
410 },
411 Shadow(ShadowPrimitive),
414}
415
416#[derive(Clone, Debug, PartialEq)]
418pub enum ShadowPrimitive {
419 Drop {
423 shape: Box<DrawPrimitive>,
424 cutout: Option<Box<DrawPrimitive>>,
425 blur_radius: f32,
426 blend_mode: BlendMode,
427 },
428 Inner {
430 fill: Box<DrawPrimitive>,
431 cutout: Box<DrawPrimitive>,
432 blur_radius: f32,
433 blend_mode: BlendMode,
434 clip_rect: Rect,
436 },
437}
438
439pub trait DrawScope {
440 fn size(&self) -> Size;
441 fn draw_content(&mut self);
442 fn draw_rect(&mut self, brush: Brush);
443 fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode);
444 fn draw_rect_at(&mut self, rect: Rect, brush: Brush);
446 fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode);
447 fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii);
448 fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode);
449 fn draw_round_rect_at(&mut self, rect: Rect, brush: Brush, radii: CornerRadii);
451 fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32);
452 fn draw_circle_blend(
453 &mut self,
454 brush: Brush,
455 center: Point,
456 radius: f32,
457 blend_mode: BlendMode,
458 );
459 fn draw_image(&mut self, image: ImageBitmap);
460 fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode);
461 fn draw_image_at(
462 &mut self,
463 rect: Rect,
464 image: ImageBitmap,
465 alpha: f32,
466 color_filter: Option<ColorFilter>,
467 );
468 fn draw_image_at_sampled(
469 &mut self,
470 rect: Rect,
471 image: ImageBitmap,
472 alpha: f32,
473 color_filter: Option<ColorFilter>,
474 sampling: ImageSampling,
475 );
476 fn draw_image_at_blend(
477 &mut self,
478 rect: Rect,
479 image: ImageBitmap,
480 alpha: f32,
481 color_filter: Option<ColorFilter>,
482 blend_mode: BlendMode,
483 );
484 fn draw_image_src(
487 &mut self,
488 image: ImageBitmap,
489 src_rect: Rect,
490 dst_rect: Rect,
491 alpha: f32,
492 color_filter: Option<ColorFilter>,
493 );
494 fn draw_image_src_sampled(
495 &mut self,
496 image: ImageBitmap,
497 src_rect: Rect,
498 dst_rect: Rect,
499 alpha: f32,
500 color_filter: Option<ColorFilter>,
501 sampling: ImageSampling,
502 );
503 fn draw_image_src_blend(
504 &mut self,
505 image: ImageBitmap,
506 src_rect: Rect,
507 dst_rect: Rect,
508 alpha: f32,
509 color_filter: Option<ColorFilter>,
510 blend_mode: BlendMode,
511 );
512 fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush);
521 fn draw_svg_path(&mut self, d: &str, brush: Brush) {
527 if let Ok(path) = crate::VectorPath::parse(d) {
528 self.draw_vector_path(&path, brush);
529 }
530 }
531 fn into_primitives(self) -> Vec<DrawPrimitive>;
532}
533
534#[derive(Default)]
535pub struct DrawScopeDefault {
536 size: Size,
537 primitives: Vec<DrawPrimitive>,
538}
539
540impl DrawScopeDefault {
541 pub fn new(size: Size) -> Self {
542 Self {
543 size,
544 primitives: Vec::new(),
545 }
546 }
547
548 fn push_blended_primitive(&mut self, primitive: DrawPrimitive, blend_mode: BlendMode) {
549 if blend_mode == BlendMode::SrcOver {
550 self.primitives.push(primitive);
551 } else {
552 self.primitives.push(DrawPrimitive::Blend {
553 primitive: Box::new(primitive),
554 blend_mode,
555 });
556 }
557 }
558}
559
560impl DrawScope for DrawScopeDefault {
561 fn size(&self) -> Size {
562 self.size
563 }
564
565 fn draw_content(&mut self) {
566 self.primitives.push(DrawPrimitive::Content);
567 }
568
569 fn draw_rect(&mut self, brush: Brush) {
570 self.draw_rect_blend(brush, BlendMode::SrcOver);
571 }
572
573 fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode) {
574 self.push_blended_primitive(
575 DrawPrimitive::Rect {
576 rect: Rect::from_size(self.size),
577 brush,
578 },
579 blend_mode,
580 );
581 }
582
583 fn draw_rect_at(&mut self, rect: Rect, brush: Brush) {
584 self.draw_rect_at_blend(rect, brush, BlendMode::SrcOver);
585 }
586
587 fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode) {
588 self.push_blended_primitive(DrawPrimitive::Rect { rect, brush }, blend_mode);
589 }
590
591 fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii) {
592 self.draw_round_rect_blend(brush, radii, BlendMode::SrcOver);
593 }
594
595 fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode) {
596 self.push_blended_primitive(
597 DrawPrimitive::RoundRect {
598 rect: Rect::from_size(self.size),
599 brush,
600 radii,
601 },
602 blend_mode,
603 );
604 }
605
606 fn draw_round_rect_at(&mut self, rect: Rect, brush: Brush, radii: CornerRadii) {
607 self.push_blended_primitive(
608 DrawPrimitive::RoundRect { rect, brush, radii },
609 BlendMode::SrcOver,
610 );
611 }
612
613 fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32) {
614 self.draw_circle_blend(brush, center, radius, BlendMode::SrcOver);
615 }
616
617 fn draw_circle_blend(
618 &mut self,
619 brush: Brush,
620 center: Point,
621 radius: f32,
622 blend_mode: BlendMode,
623 ) {
624 let radius = radius.max(0.0);
625 let diameter = radius * 2.0;
626 self.push_blended_primitive(
627 DrawPrimitive::RoundRect {
628 rect: Rect {
629 x: center.x - radius,
630 y: center.y - radius,
631 width: diameter,
632 height: diameter,
633 },
634 brush,
635 radii: CornerRadii::uniform(radius),
636 },
637 blend_mode,
638 );
639 }
640
641 fn draw_image(&mut self, image: ImageBitmap) {
642 self.draw_image_blend(image, BlendMode::SrcOver);
643 }
644
645 fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode) {
646 self.push_blended_primitive(
647 DrawPrimitive::Image {
648 rect: Rect::from_size(self.size),
649 image,
650 alpha: 1.0,
651 color_filter: None,
652 sampling: ImageSampling::Nearest,
653 src_rect: None,
654 },
655 blend_mode,
656 );
657 }
658
659 fn draw_image_at(
660 &mut self,
661 rect: Rect,
662 image: ImageBitmap,
663 alpha: f32,
664 color_filter: Option<ColorFilter>,
665 ) {
666 self.draw_image_at_sampled(rect, image, alpha, color_filter, ImageSampling::Nearest);
667 }
668
669 fn draw_image_at_sampled(
670 &mut self,
671 rect: Rect,
672 image: ImageBitmap,
673 alpha: f32,
674 color_filter: Option<ColorFilter>,
675 sampling: ImageSampling,
676 ) {
677 self.push_blended_primitive(
678 DrawPrimitive::Image {
679 rect,
680 image,
681 alpha: alpha.clamp(0.0, 1.0),
682 color_filter,
683 sampling,
684 src_rect: None,
685 },
686 BlendMode::SrcOver,
687 );
688 }
689
690 fn draw_image_at_blend(
691 &mut self,
692 rect: Rect,
693 image: ImageBitmap,
694 alpha: f32,
695 color_filter: Option<ColorFilter>,
696 blend_mode: BlendMode,
697 ) {
698 self.push_blended_primitive(
699 DrawPrimitive::Image {
700 rect,
701 image,
702 alpha: alpha.clamp(0.0, 1.0),
703 color_filter,
704 sampling: ImageSampling::Nearest,
705 src_rect: None,
706 },
707 blend_mode,
708 );
709 }
710
711 fn draw_image_src(
712 &mut self,
713 image: ImageBitmap,
714 src_rect: Rect,
715 dst_rect: Rect,
716 alpha: f32,
717 color_filter: Option<ColorFilter>,
718 ) {
719 self.draw_image_src_blend(
720 image,
721 src_rect,
722 dst_rect,
723 alpha,
724 color_filter,
725 BlendMode::SrcOver,
726 );
727 }
728
729 fn draw_image_src_sampled(
730 &mut self,
731 image: ImageBitmap,
732 src_rect: Rect,
733 dst_rect: Rect,
734 alpha: f32,
735 color_filter: Option<ColorFilter>,
736 sampling: ImageSampling,
737 ) {
738 self.push_blended_primitive(
739 DrawPrimitive::Image {
740 rect: dst_rect,
741 image,
742 alpha: alpha.clamp(0.0, 1.0),
743 color_filter,
744 sampling,
745 src_rect: Some(src_rect),
746 },
747 BlendMode::SrcOver,
748 );
749 }
750
751 fn draw_image_src_blend(
752 &mut self,
753 image: ImageBitmap,
754 src_rect: Rect,
755 dst_rect: Rect,
756 alpha: f32,
757 color_filter: Option<ColorFilter>,
758 blend_mode: BlendMode,
759 ) {
760 self.push_blended_primitive(
761 DrawPrimitive::Image {
762 rect: dst_rect,
763 image,
764 alpha: alpha.clamp(0.0, 1.0),
765 color_filter,
766 sampling: ImageSampling::Nearest,
767 src_rect: Some(src_rect),
768 },
769 blend_mode,
770 );
771 }
772
773 fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush) {
774 const SUPERSAMPLE: f32 = 2.0;
779 const MAX_MASK_PIXELS: f32 = 4096.0;
781
782 if path.is_empty() {
783 return;
784 }
785 let bounds = path.bounds();
786 if bounds.width <= 0.0 || bounds.height <= 0.0 {
787 return;
788 }
789
790 let color = match &brush {
791 Brush::Solid(color) => *color,
792 Brush::LinearGradient { colors, .. }
793 | Brush::RadialGradient { colors, .. }
794 | Brush::SweepGradient { colors, .. } => match colors.first() {
795 Some(color) => *color,
796 None => return,
797 },
798 };
799 if color.3 <= 0.0 {
800 return;
801 }
802
803 let origin = Point::new(bounds.x.floor() - 1.0, bounds.y.floor() - 1.0);
806 let rect_width = (bounds.x + bounds.width).ceil() - origin.x + 1.0;
807 let rect_height = (bounds.y + bounds.height).ceil() - origin.y + 1.0;
808 let mask_width = (rect_width * SUPERSAMPLE)
809 .ceil()
810 .clamp(1.0, MAX_MASK_PIXELS) as usize;
811 let mask_height = (rect_height * SUPERSAMPLE)
812 .ceil()
813 .clamp(1.0, MAX_MASK_PIXELS) as usize;
814
815 let mask = path.coverage_mask(mask_width, mask_height, origin, SUPERSAMPLE);
816
817 let red = (color.0.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
818 let green = (color.1.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
819 let blue = (color.2.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
820 let alpha = color.3.clamp(0.0, 1.0);
821
822 let mut pixels = Vec::with_capacity(mask.len() * 4);
823 for coverage in mask {
824 pixels.extend_from_slice(&[red, green, blue, (alpha * coverage as f32 + 0.5) as u8]);
825 }
826
827 let Ok(image) = ImageBitmap::from_rgba8(mask_width as u32, mask_height as u32, pixels)
828 else {
829 return;
830 };
831
832 self.primitives.push(DrawPrimitive::Image {
833 rect: Rect {
834 x: origin.x,
835 y: origin.y,
836 width: rect_width,
837 height: rect_height,
838 },
839 image,
840 alpha: 1.0,
841 color_filter: None,
842 sampling: ImageSampling::Linear,
843 src_rect: None,
844 });
845 }
846
847 fn into_primitives(self) -> Vec<DrawPrimitive> {
848 self.primitives
849 }
850}
851
852#[cfg(test)]
853mod tests {
854 use super::*;
855 use crate::{Color, ImageBitmap, RenderEffect};
856
857 fn assert_image_alpha(primitive: &DrawPrimitive, expected: f32) {
858 match primitive {
859 DrawPrimitive::Image { alpha, .. } => assert!((alpha - expected).abs() < 1e-5),
860 DrawPrimitive::Blend { primitive, .. } => assert_image_alpha(primitive, expected),
861 other => panic!("expected image primitive, got {other:?}"),
862 }
863 }
864
865 fn unwrap_image(primitive: &DrawPrimitive) -> &DrawPrimitive {
866 match primitive {
867 DrawPrimitive::Image { .. } => primitive,
868 DrawPrimitive::Blend { primitive, .. } => unwrap_image(primitive),
869 other => panic!("expected image primitive, got {other:?}"),
870 }
871 }
872
873 #[test]
874 fn draw_svg_path_emits_supersampled_image_over_path_bounds() {
875 let mut scope = DrawScopeDefault::new(Size::new(32.0, 32.0));
876 scope.draw_svg_path("M 4 4 H 20 V 20 H 4 Z", Brush::solid(Color::RED));
877
878 let primitives = scope.into_primitives();
879 assert_eq!(primitives.len(), 1);
880 let DrawPrimitive::Image { rect, image, .. } = &primitives[0] else {
881 panic!("expected image primitive, got {:?}", primitives[0]);
882 };
883
884 assert_eq!((rect.x, rect.y), (3.0, 3.0));
886 assert_eq!((rect.width, rect.height), (18.0, 18.0));
887 assert_eq!((image.width(), image.height()), (36, 36));
889
890 let pixels = image.pixels();
893 let index = (18 * 36 + 18) * 4;
894 assert_eq!(
895 &pixels[index..index + 4],
896 &[255, 0, 0, 255],
897 "path interior must be opaque brush color"
898 );
899 assert_eq!(pixels[3], 0, "outside the path must stay transparent");
901 }
902
903 #[test]
904 fn draw_svg_path_ignores_invalid_data() {
905 let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
906 scope.draw_svg_path("definitely not a path", Brush::solid(Color::WHITE));
907 assert!(scope.into_primitives().is_empty());
908 }
909
910 #[test]
911 fn draw_vector_path_applies_brush_alpha() {
912 let path = crate::VectorPath::parse("M 0 0 H 8 V 8 H 0 Z").expect("valid path");
913 let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
914 scope.draw_vector_path(&path, Brush::solid(Color::rgba(0.0, 0.0, 1.0, 0.5)));
915
916 let primitives = scope.into_primitives();
917 let DrawPrimitive::Image { image, .. } = &primitives[0] else {
918 panic!("expected image primitive");
919 };
920 let pixels = image.pixels();
921 let width = image.width() as usize;
923 let index = ((image.height() as usize / 2) * width + width / 2) * 4;
924 assert_eq!(&pixels[index..index + 3], &[0, 0, 255]);
925 let alpha = pixels[index + 3];
926 assert!(
927 (alpha as i32 - 128).abs() <= 2,
928 "interior alpha must honor the brush alpha, got {alpha}"
929 );
930 }
931
932 #[test]
933 fn draw_content_inserts_content_marker() {
934 let mut scope = DrawScopeDefault::new(Size::new(8.0, 8.0));
935 scope.draw_rect(Brush::solid(Color::WHITE));
936 scope.draw_content();
937 scope.draw_rect_blend(Brush::solid(Color::BLACK), BlendMode::DstOut);
938
939 let primitives = scope.into_primitives();
940 assert!(matches!(primitives[1], DrawPrimitive::Content));
941 assert!(matches!(
942 primitives[2],
943 DrawPrimitive::Blend {
944 blend_mode: BlendMode::DstOut,
945 ..
946 }
947 ));
948 }
949
950 #[test]
951 fn draw_rect_blend_wraps_non_default_modes() {
952 let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
953 scope.draw_rect_blend(Brush::solid(Color::RED), BlendMode::DstOut);
954
955 let primitives = scope.into_primitives();
956 assert_eq!(primitives.len(), 1);
957 match &primitives[0] {
958 DrawPrimitive::Blend {
959 primitive,
960 blend_mode,
961 } => {
962 assert_eq!(*blend_mode, BlendMode::DstOut);
963 assert!(matches!(**primitive, DrawPrimitive::Rect { .. }));
964 }
965 other => panic!("expected blended primitive, got {other:?}"),
966 }
967 }
968
969 #[test]
970 fn draw_circle_records_centered_round_rect() {
971 let mut scope = DrawScopeDefault::new(Size::new(40.0, 40.0));
972 scope.draw_circle(Brush::solid(Color::BLUE), Point::new(12.0, 16.0), 5.0);
973
974 let primitives = scope.into_primitives();
975 assert_eq!(primitives.len(), 1);
976 match &primitives[0] {
977 DrawPrimitive::RoundRect { rect, radii, .. } => {
978 assert_eq!(
979 *rect,
980 Rect {
981 x: 7.0,
982 y: 11.0,
983 width: 10.0,
984 height: 10.0,
985 }
986 );
987 assert_eq!(*radii, CornerRadii::uniform(5.0));
988 }
989 other => panic!("expected circular round rect, got {other:?}"),
990 }
991 }
992
993 #[test]
994 fn draw_circle_blend_wraps_non_default_modes() {
995 let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
996 scope.draw_circle_blend(
997 Brush::solid(Color::RED),
998 Point::new(5.0, 5.0),
999 3.0,
1000 BlendMode::Plus,
1001 );
1002
1003 let primitives = scope.into_primitives();
1004 assert_eq!(primitives.len(), 1);
1005 match &primitives[0] {
1006 DrawPrimitive::Blend {
1007 primitive,
1008 blend_mode,
1009 } => {
1010 assert_eq!(*blend_mode, BlendMode::Plus);
1011 assert!(matches!(**primitive, DrawPrimitive::RoundRect { .. }));
1012 }
1013 other => panic!("expected blended circle primitive, got {other:?}"),
1014 }
1015 }
1016
1017 #[test]
1018 fn rect_union_encloses_both_inputs() {
1019 let lhs = Rect {
1020 x: 10.0,
1021 y: 5.0,
1022 width: 8.0,
1023 height: 4.0,
1024 };
1025 let rhs = Rect {
1026 x: 4.0,
1027 y: 7.0,
1028 width: 10.0,
1029 height: 6.0,
1030 };
1031
1032 assert_eq!(
1033 lhs.union(rhs),
1034 Rect {
1035 x: 4.0,
1036 y: 5.0,
1037 width: 14.0,
1038 height: 8.0,
1039 }
1040 );
1041 }
1042
1043 #[test]
1044 fn draw_image_uses_scope_size_as_default_rect() {
1045 let mut scope = DrawScopeDefault::new(Size::new(40.0, 24.0));
1046 let image = ImageBitmap::from_rgba8(2, 2, vec![255; 16]).expect("image");
1047 scope.draw_image(image.clone());
1048 let primitives = scope.into_primitives();
1049 assert_eq!(primitives.len(), 1);
1050 match unwrap_image(&primitives[0]) {
1051 DrawPrimitive::Image {
1052 rect,
1053 image: actual,
1054 alpha,
1055 color_filter,
1056 sampling,
1057 src_rect,
1058 } => {
1059 assert_eq!(*rect, Rect::from_size(Size::new(40.0, 24.0)));
1060 assert_eq!(*actual, image);
1061 assert_eq!(*alpha, 1.0);
1062 assert!(color_filter.is_none());
1063 assert_eq!(*sampling, ImageSampling::Nearest);
1064 assert!(src_rect.is_none());
1065 }
1066 other => panic!("expected image primitive, got {other:?}"),
1067 }
1068 }
1069
1070 #[test]
1071 fn draw_image_src_stores_src_rect() {
1072 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
1073 let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
1074 let src = Rect {
1075 x: 10.0,
1076 y: 20.0,
1077 width: 30.0,
1078 height: 40.0,
1079 };
1080 let dst = Rect {
1081 x: 0.0,
1082 y: 0.0,
1083 width: 60.0,
1084 height: 80.0,
1085 };
1086 scope.draw_image_src(image.clone(), src, dst, 0.8, None);
1087 let primitives = scope.into_primitives();
1088 assert_eq!(primitives.len(), 1);
1089 match unwrap_image(&primitives[0]) {
1090 DrawPrimitive::Image {
1091 rect,
1092 image: actual,
1093 alpha,
1094 sampling,
1095 src_rect,
1096 ..
1097 } => {
1098 assert_eq!(*rect, dst);
1099 assert_eq!(*actual, image);
1100 assert!((alpha - 0.8).abs() < 1e-5);
1101 assert_eq!(*sampling, ImageSampling::Nearest);
1102 assert_eq!(*src_rect, Some(src));
1103 }
1104 other => panic!("expected image primitive, got {other:?}"),
1105 }
1106 }
1107
1108 #[test]
1109 fn draw_image_at_sampled_records_requested_sampling() {
1110 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
1111 let image = ImageBitmap::from_rgba8(8, 8, vec![255; 8 * 8 * 4]).expect("image");
1112 let dst = Rect {
1113 x: 2.0,
1114 y: 3.0,
1115 width: 40.0,
1116 height: 30.0,
1117 };
1118
1119 scope.draw_image_at_sampled(dst, image.clone(), 0.7, None, ImageSampling::Linear);
1120
1121 let primitives = scope.into_primitives();
1122 assert_eq!(primitives.len(), 1);
1123 match unwrap_image(&primitives[0]) {
1124 DrawPrimitive::Image {
1125 rect,
1126 image: actual,
1127 alpha,
1128 sampling,
1129 src_rect,
1130 ..
1131 } => {
1132 assert_eq!(*rect, dst);
1133 assert_eq!(*actual, image);
1134 assert!((alpha - 0.7).abs() < 1e-5);
1135 assert_eq!(*sampling, ImageSampling::Linear);
1136 assert!(src_rect.is_none());
1137 }
1138 other => panic!("expected image primitive, got {other:?}"),
1139 }
1140 }
1141
1142 #[test]
1143 fn draw_image_src_sampled_records_requested_sampling() {
1144 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
1145 let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
1146 let src = Rect {
1147 x: 4.0,
1148 y: 6.0,
1149 width: 16.0,
1150 height: 20.0,
1151 };
1152 let dst = Rect {
1153 x: 8.0,
1154 y: 10.0,
1155 width: 32.0,
1156 height: 40.0,
1157 };
1158
1159 scope.draw_image_src_sampled(image.clone(), src, dst, 0.5, None, ImageSampling::Linear);
1160
1161 let primitives = scope.into_primitives();
1162 assert_eq!(primitives.len(), 1);
1163 match unwrap_image(&primitives[0]) {
1164 DrawPrimitive::Image {
1165 rect,
1166 image: actual,
1167 alpha,
1168 sampling,
1169 src_rect,
1170 ..
1171 } => {
1172 assert_eq!(*rect, dst);
1173 assert_eq!(*actual, image);
1174 assert!((alpha - 0.5).abs() < 1e-5);
1175 assert_eq!(*sampling, ImageSampling::Linear);
1176 assert_eq!(*src_rect, Some(src));
1177 }
1178 other => panic!("expected image primitive, got {other:?}"),
1179 }
1180 }
1181
1182 #[test]
1183 fn draw_image_at_clamps_alpha() {
1184 let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
1185 let image = ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image");
1186 scope.draw_image_at(
1187 Rect::from_origin_size(Point::new(2.0, 3.0), Size::new(5.0, 6.0)),
1188 image,
1189 3.0,
1190 Some(ColorFilter::Tint(Color::from_rgba_u8(128, 128, 255, 255))),
1191 );
1192 assert_image_alpha(&scope.into_primitives()[0], 1.0);
1193 }
1194
1195 #[test]
1196 fn graphics_layer_clone_with_render_effect() {
1197 let layer = GraphicsLayer {
1198 render_effect: Some(RenderEffect::blur(10.0)),
1199 backdrop_effect: Some(RenderEffect::blur(6.0)),
1200 color_filter: Some(ColorFilter::tint(Color::from_rgba_u8(128, 200, 255, 255))),
1201 alpha: 0.5,
1202 rotation_z: 12.0,
1203 shadow_elevation: 4.0,
1204 shape: LayerShape::Rounded(RoundedCornerShape::uniform(6.0)),
1205 clip: true,
1206 compositing_strategy: CompositingStrategy::Offscreen,
1207 blend_mode: BlendMode::SrcOver,
1208 ..Default::default()
1209 };
1210 let cloned = layer.clone();
1211 assert_eq!(cloned.alpha, 0.5);
1212 assert!(cloned.render_effect.is_some());
1213 assert!(cloned.backdrop_effect.is_some());
1214 assert_eq!(layer.color_filter, cloned.color_filter);
1215 assert_eq!(layer.render_effect, cloned.render_effect);
1216 assert_eq!(layer.backdrop_effect, cloned.backdrop_effect);
1217 assert!((cloned.rotation_z - 12.0).abs() < 1e-6);
1218 assert!((cloned.shadow_elevation - 4.0).abs() < 1e-6);
1219 assert_eq!(
1220 cloned.shape,
1221 LayerShape::Rounded(RoundedCornerShape::uniform(6.0))
1222 );
1223 assert!(cloned.clip);
1224 assert_eq!(cloned.compositing_strategy, CompositingStrategy::Offscreen);
1225 assert_eq!(cloned.blend_mode, BlendMode::SrcOver);
1226 }
1227
1228 #[test]
1229 fn graphics_layer_default_has_no_effect() {
1230 let layer = GraphicsLayer::default();
1231 assert!(layer.color_filter.is_none());
1232 assert!(layer.render_effect.is_none());
1233 assert!(layer.backdrop_effect.is_none());
1234 assert_eq!(layer.compositing_strategy, CompositingStrategy::Auto);
1235 assert_eq!(layer.blend_mode, BlendMode::SrcOver);
1236 assert_eq!(layer.alpha, 1.0);
1237 assert_eq!(layer.transform_origin, TransformOrigin::CENTER);
1238 assert!((layer.camera_distance - 8.0).abs() < 1e-6);
1239 assert_eq!(layer.shape, LayerShape::Rectangle);
1240 assert!(!layer.clip);
1241 assert_eq!(layer.ambient_shadow_color, Color::BLACK);
1242 assert_eq!(layer.spot_shadow_color, Color::BLACK);
1243 }
1244
1245 #[test]
1246 fn transform_origin_construction() {
1247 let origin = TransformOrigin::new(0.25, 0.75);
1248 assert!((origin.pivot_fraction_x - 0.25).abs() < 1e-6);
1249 assert!((origin.pivot_fraction_y - 0.75).abs() < 1e-6);
1250 }
1251
1252 #[test]
1253 fn layer_shape_default_is_rectangle() {
1254 assert_eq!(LayerShape::default(), LayerShape::Rectangle);
1255 }
1256}