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 {
421 shape: Box<DrawPrimitive>,
422 blur_radius: f32,
423 blend_mode: BlendMode,
424 },
425 Inner {
427 fill: Box<DrawPrimitive>,
428 cutout: Box<DrawPrimitive>,
429 blur_radius: f32,
430 blend_mode: BlendMode,
431 clip_rect: Rect,
433 },
434}
435
436pub trait DrawScope {
437 fn size(&self) -> Size;
438 fn draw_content(&mut self);
439 fn draw_rect(&mut self, brush: Brush);
440 fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode);
441 fn draw_rect_at(&mut self, rect: Rect, brush: Brush);
443 fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode);
444 fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii);
445 fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode);
446 fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32);
447 fn draw_circle_blend(
448 &mut self,
449 brush: Brush,
450 center: Point,
451 radius: f32,
452 blend_mode: BlendMode,
453 );
454 fn draw_image(&mut self, image: ImageBitmap);
455 fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode);
456 fn draw_image_at(
457 &mut self,
458 rect: Rect,
459 image: ImageBitmap,
460 alpha: f32,
461 color_filter: Option<ColorFilter>,
462 );
463 fn draw_image_at_sampled(
464 &mut self,
465 rect: Rect,
466 image: ImageBitmap,
467 alpha: f32,
468 color_filter: Option<ColorFilter>,
469 sampling: ImageSampling,
470 );
471 fn draw_image_at_blend(
472 &mut self,
473 rect: Rect,
474 image: ImageBitmap,
475 alpha: f32,
476 color_filter: Option<ColorFilter>,
477 blend_mode: BlendMode,
478 );
479 fn draw_image_src(
482 &mut self,
483 image: ImageBitmap,
484 src_rect: Rect,
485 dst_rect: Rect,
486 alpha: f32,
487 color_filter: Option<ColorFilter>,
488 );
489 fn draw_image_src_sampled(
490 &mut self,
491 image: ImageBitmap,
492 src_rect: Rect,
493 dst_rect: Rect,
494 alpha: f32,
495 color_filter: Option<ColorFilter>,
496 sampling: ImageSampling,
497 );
498 fn draw_image_src_blend(
499 &mut self,
500 image: ImageBitmap,
501 src_rect: Rect,
502 dst_rect: Rect,
503 alpha: f32,
504 color_filter: Option<ColorFilter>,
505 blend_mode: BlendMode,
506 );
507 fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush);
516 fn draw_svg_path(&mut self, d: &str, brush: Brush) {
522 if let Ok(path) = crate::VectorPath::parse(d) {
523 self.draw_vector_path(&path, brush);
524 }
525 }
526 fn into_primitives(self) -> Vec<DrawPrimitive>;
527}
528
529#[derive(Default)]
530pub struct DrawScopeDefault {
531 size: Size,
532 primitives: Vec<DrawPrimitive>,
533}
534
535impl DrawScopeDefault {
536 pub fn new(size: Size) -> Self {
537 Self {
538 size,
539 primitives: Vec::new(),
540 }
541 }
542
543 fn push_blended_primitive(&mut self, primitive: DrawPrimitive, blend_mode: BlendMode) {
544 if blend_mode == BlendMode::SrcOver {
545 self.primitives.push(primitive);
546 } else {
547 self.primitives.push(DrawPrimitive::Blend {
548 primitive: Box::new(primitive),
549 blend_mode,
550 });
551 }
552 }
553}
554
555impl DrawScope for DrawScopeDefault {
556 fn size(&self) -> Size {
557 self.size
558 }
559
560 fn draw_content(&mut self) {
561 self.primitives.push(DrawPrimitive::Content);
562 }
563
564 fn draw_rect(&mut self, brush: Brush) {
565 self.draw_rect_blend(brush, BlendMode::SrcOver);
566 }
567
568 fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode) {
569 self.push_blended_primitive(
570 DrawPrimitive::Rect {
571 rect: Rect::from_size(self.size),
572 brush,
573 },
574 blend_mode,
575 );
576 }
577
578 fn draw_rect_at(&mut self, rect: Rect, brush: Brush) {
579 self.draw_rect_at_blend(rect, brush, BlendMode::SrcOver);
580 }
581
582 fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode) {
583 self.push_blended_primitive(DrawPrimitive::Rect { rect, brush }, blend_mode);
584 }
585
586 fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii) {
587 self.draw_round_rect_blend(brush, radii, BlendMode::SrcOver);
588 }
589
590 fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode) {
591 self.push_blended_primitive(
592 DrawPrimitive::RoundRect {
593 rect: Rect::from_size(self.size),
594 brush,
595 radii,
596 },
597 blend_mode,
598 );
599 }
600
601 fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32) {
602 self.draw_circle_blend(brush, center, radius, BlendMode::SrcOver);
603 }
604
605 fn draw_circle_blend(
606 &mut self,
607 brush: Brush,
608 center: Point,
609 radius: f32,
610 blend_mode: BlendMode,
611 ) {
612 let radius = radius.max(0.0);
613 let diameter = radius * 2.0;
614 self.push_blended_primitive(
615 DrawPrimitive::RoundRect {
616 rect: Rect {
617 x: center.x - radius,
618 y: center.y - radius,
619 width: diameter,
620 height: diameter,
621 },
622 brush,
623 radii: CornerRadii::uniform(radius),
624 },
625 blend_mode,
626 );
627 }
628
629 fn draw_image(&mut self, image: ImageBitmap) {
630 self.draw_image_blend(image, BlendMode::SrcOver);
631 }
632
633 fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode) {
634 self.push_blended_primitive(
635 DrawPrimitive::Image {
636 rect: Rect::from_size(self.size),
637 image,
638 alpha: 1.0,
639 color_filter: None,
640 sampling: ImageSampling::Nearest,
641 src_rect: None,
642 },
643 blend_mode,
644 );
645 }
646
647 fn draw_image_at(
648 &mut self,
649 rect: Rect,
650 image: ImageBitmap,
651 alpha: f32,
652 color_filter: Option<ColorFilter>,
653 ) {
654 self.draw_image_at_sampled(rect, image, alpha, color_filter, ImageSampling::Nearest);
655 }
656
657 fn draw_image_at_sampled(
658 &mut self,
659 rect: Rect,
660 image: ImageBitmap,
661 alpha: f32,
662 color_filter: Option<ColorFilter>,
663 sampling: ImageSampling,
664 ) {
665 self.push_blended_primitive(
666 DrawPrimitive::Image {
667 rect,
668 image,
669 alpha: alpha.clamp(0.0, 1.0),
670 color_filter,
671 sampling,
672 src_rect: None,
673 },
674 BlendMode::SrcOver,
675 );
676 }
677
678 fn draw_image_at_blend(
679 &mut self,
680 rect: Rect,
681 image: ImageBitmap,
682 alpha: f32,
683 color_filter: Option<ColorFilter>,
684 blend_mode: BlendMode,
685 ) {
686 self.push_blended_primitive(
687 DrawPrimitive::Image {
688 rect,
689 image,
690 alpha: alpha.clamp(0.0, 1.0),
691 color_filter,
692 sampling: ImageSampling::Nearest,
693 src_rect: None,
694 },
695 blend_mode,
696 );
697 }
698
699 fn draw_image_src(
700 &mut self,
701 image: ImageBitmap,
702 src_rect: Rect,
703 dst_rect: Rect,
704 alpha: f32,
705 color_filter: Option<ColorFilter>,
706 ) {
707 self.draw_image_src_blend(
708 image,
709 src_rect,
710 dst_rect,
711 alpha,
712 color_filter,
713 BlendMode::SrcOver,
714 );
715 }
716
717 fn draw_image_src_sampled(
718 &mut self,
719 image: ImageBitmap,
720 src_rect: Rect,
721 dst_rect: Rect,
722 alpha: f32,
723 color_filter: Option<ColorFilter>,
724 sampling: ImageSampling,
725 ) {
726 self.push_blended_primitive(
727 DrawPrimitive::Image {
728 rect: dst_rect,
729 image,
730 alpha: alpha.clamp(0.0, 1.0),
731 color_filter,
732 sampling,
733 src_rect: Some(src_rect),
734 },
735 BlendMode::SrcOver,
736 );
737 }
738
739 fn draw_image_src_blend(
740 &mut self,
741 image: ImageBitmap,
742 src_rect: Rect,
743 dst_rect: Rect,
744 alpha: f32,
745 color_filter: Option<ColorFilter>,
746 blend_mode: BlendMode,
747 ) {
748 self.push_blended_primitive(
749 DrawPrimitive::Image {
750 rect: dst_rect,
751 image,
752 alpha: alpha.clamp(0.0, 1.0),
753 color_filter,
754 sampling: ImageSampling::Nearest,
755 src_rect: Some(src_rect),
756 },
757 blend_mode,
758 );
759 }
760
761 fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush) {
762 const SUPERSAMPLE: f32 = 2.0;
767 const MAX_MASK_PIXELS: f32 = 4096.0;
769
770 if path.is_empty() {
771 return;
772 }
773 let bounds = path.bounds();
774 if bounds.width <= 0.0 || bounds.height <= 0.0 {
775 return;
776 }
777
778 let color = match &brush {
779 Brush::Solid(color) => *color,
780 Brush::LinearGradient { colors, .. }
781 | Brush::RadialGradient { colors, .. }
782 | Brush::SweepGradient { colors, .. } => match colors.first() {
783 Some(color) => *color,
784 None => return,
785 },
786 };
787 if color.3 <= 0.0 {
788 return;
789 }
790
791 let origin = Point::new(bounds.x.floor() - 1.0, bounds.y.floor() - 1.0);
794 let rect_width = (bounds.x + bounds.width).ceil() - origin.x + 1.0;
795 let rect_height = (bounds.y + bounds.height).ceil() - origin.y + 1.0;
796 let mask_width = (rect_width * SUPERSAMPLE)
797 .ceil()
798 .clamp(1.0, MAX_MASK_PIXELS) as usize;
799 let mask_height = (rect_height * SUPERSAMPLE)
800 .ceil()
801 .clamp(1.0, MAX_MASK_PIXELS) as usize;
802
803 let mask = path.coverage_mask(mask_width, mask_height, origin, SUPERSAMPLE);
804
805 let red = (color.0.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
806 let green = (color.1.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
807 let blue = (color.2.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
808 let alpha = color.3.clamp(0.0, 1.0);
809
810 let mut pixels = Vec::with_capacity(mask.len() * 4);
811 for coverage in mask {
812 pixels.extend_from_slice(&[red, green, blue, (alpha * coverage as f32 + 0.5) as u8]);
813 }
814
815 let Ok(image) = ImageBitmap::from_rgba8(mask_width as u32, mask_height as u32, pixels)
816 else {
817 return;
818 };
819
820 self.primitives.push(DrawPrimitive::Image {
821 rect: Rect {
822 x: origin.x,
823 y: origin.y,
824 width: rect_width,
825 height: rect_height,
826 },
827 image,
828 alpha: 1.0,
829 color_filter: None,
830 sampling: ImageSampling::Linear,
831 src_rect: None,
832 });
833 }
834
835 fn into_primitives(self) -> Vec<DrawPrimitive> {
836 self.primitives
837 }
838}
839
840#[cfg(test)]
841mod tests {
842 use super::*;
843 use crate::{Color, ImageBitmap, RenderEffect};
844
845 fn assert_image_alpha(primitive: &DrawPrimitive, expected: f32) {
846 match primitive {
847 DrawPrimitive::Image { alpha, .. } => assert!((alpha - expected).abs() < 1e-5),
848 DrawPrimitive::Blend { primitive, .. } => assert_image_alpha(primitive, expected),
849 other => panic!("expected image primitive, got {other:?}"),
850 }
851 }
852
853 fn unwrap_image(primitive: &DrawPrimitive) -> &DrawPrimitive {
854 match primitive {
855 DrawPrimitive::Image { .. } => primitive,
856 DrawPrimitive::Blend { primitive, .. } => unwrap_image(primitive),
857 other => panic!("expected image primitive, got {other:?}"),
858 }
859 }
860
861 #[test]
862 fn draw_svg_path_emits_supersampled_image_over_path_bounds() {
863 let mut scope = DrawScopeDefault::new(Size::new(32.0, 32.0));
864 scope.draw_svg_path("M 4 4 H 20 V 20 H 4 Z", Brush::solid(Color::RED));
865
866 let primitives = scope.into_primitives();
867 assert_eq!(primitives.len(), 1);
868 let DrawPrimitive::Image { rect, image, .. } = &primitives[0] else {
869 panic!("expected image primitive, got {:?}", primitives[0]);
870 };
871
872 assert_eq!((rect.x, rect.y), (3.0, 3.0));
874 assert_eq!((rect.width, rect.height), (18.0, 18.0));
875 assert_eq!((image.width(), image.height()), (36, 36));
877
878 let pixels = image.pixels();
881 let index = (18 * 36 + 18) * 4;
882 assert_eq!(
883 &pixels[index..index + 4],
884 &[255, 0, 0, 255],
885 "path interior must be opaque brush color"
886 );
887 assert_eq!(pixels[3], 0, "outside the path must stay transparent");
889 }
890
891 #[test]
892 fn draw_svg_path_ignores_invalid_data() {
893 let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
894 scope.draw_svg_path("definitely not a path", Brush::solid(Color::WHITE));
895 assert!(scope.into_primitives().is_empty());
896 }
897
898 #[test]
899 fn draw_vector_path_applies_brush_alpha() {
900 let path = crate::VectorPath::parse("M 0 0 H 8 V 8 H 0 Z").expect("valid path");
901 let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
902 scope.draw_vector_path(&path, Brush::solid(Color::rgba(0.0, 0.0, 1.0, 0.5)));
903
904 let primitives = scope.into_primitives();
905 let DrawPrimitive::Image { image, .. } = &primitives[0] else {
906 panic!("expected image primitive");
907 };
908 let pixels = image.pixels();
909 let width = image.width() as usize;
911 let index = ((image.height() as usize / 2) * width + width / 2) * 4;
912 assert_eq!(&pixels[index..index + 3], &[0, 0, 255]);
913 let alpha = pixels[index + 3];
914 assert!(
915 (alpha as i32 - 128).abs() <= 2,
916 "interior alpha must honor the brush alpha, got {alpha}"
917 );
918 }
919
920 #[test]
921 fn draw_content_inserts_content_marker() {
922 let mut scope = DrawScopeDefault::new(Size::new(8.0, 8.0));
923 scope.draw_rect(Brush::solid(Color::WHITE));
924 scope.draw_content();
925 scope.draw_rect_blend(Brush::solid(Color::BLACK), BlendMode::DstOut);
926
927 let primitives = scope.into_primitives();
928 assert!(matches!(primitives[1], DrawPrimitive::Content));
929 assert!(matches!(
930 primitives[2],
931 DrawPrimitive::Blend {
932 blend_mode: BlendMode::DstOut,
933 ..
934 }
935 ));
936 }
937
938 #[test]
939 fn draw_rect_blend_wraps_non_default_modes() {
940 let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
941 scope.draw_rect_blend(Brush::solid(Color::RED), BlendMode::DstOut);
942
943 let primitives = scope.into_primitives();
944 assert_eq!(primitives.len(), 1);
945 match &primitives[0] {
946 DrawPrimitive::Blend {
947 primitive,
948 blend_mode,
949 } => {
950 assert_eq!(*blend_mode, BlendMode::DstOut);
951 assert!(matches!(**primitive, DrawPrimitive::Rect { .. }));
952 }
953 other => panic!("expected blended primitive, got {other:?}"),
954 }
955 }
956
957 #[test]
958 fn draw_circle_records_centered_round_rect() {
959 let mut scope = DrawScopeDefault::new(Size::new(40.0, 40.0));
960 scope.draw_circle(Brush::solid(Color::BLUE), Point::new(12.0, 16.0), 5.0);
961
962 let primitives = scope.into_primitives();
963 assert_eq!(primitives.len(), 1);
964 match &primitives[0] {
965 DrawPrimitive::RoundRect { rect, radii, .. } => {
966 assert_eq!(
967 *rect,
968 Rect {
969 x: 7.0,
970 y: 11.0,
971 width: 10.0,
972 height: 10.0,
973 }
974 );
975 assert_eq!(*radii, CornerRadii::uniform(5.0));
976 }
977 other => panic!("expected circular round rect, got {other:?}"),
978 }
979 }
980
981 #[test]
982 fn draw_circle_blend_wraps_non_default_modes() {
983 let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
984 scope.draw_circle_blend(
985 Brush::solid(Color::RED),
986 Point::new(5.0, 5.0),
987 3.0,
988 BlendMode::Plus,
989 );
990
991 let primitives = scope.into_primitives();
992 assert_eq!(primitives.len(), 1);
993 match &primitives[0] {
994 DrawPrimitive::Blend {
995 primitive,
996 blend_mode,
997 } => {
998 assert_eq!(*blend_mode, BlendMode::Plus);
999 assert!(matches!(**primitive, DrawPrimitive::RoundRect { .. }));
1000 }
1001 other => panic!("expected blended circle primitive, got {other:?}"),
1002 }
1003 }
1004
1005 #[test]
1006 fn rect_union_encloses_both_inputs() {
1007 let lhs = Rect {
1008 x: 10.0,
1009 y: 5.0,
1010 width: 8.0,
1011 height: 4.0,
1012 };
1013 let rhs = Rect {
1014 x: 4.0,
1015 y: 7.0,
1016 width: 10.0,
1017 height: 6.0,
1018 };
1019
1020 assert_eq!(
1021 lhs.union(rhs),
1022 Rect {
1023 x: 4.0,
1024 y: 5.0,
1025 width: 14.0,
1026 height: 8.0,
1027 }
1028 );
1029 }
1030
1031 #[test]
1032 fn draw_image_uses_scope_size_as_default_rect() {
1033 let mut scope = DrawScopeDefault::new(Size::new(40.0, 24.0));
1034 let image = ImageBitmap::from_rgba8(2, 2, vec![255; 16]).expect("image");
1035 scope.draw_image(image.clone());
1036 let primitives = scope.into_primitives();
1037 assert_eq!(primitives.len(), 1);
1038 match unwrap_image(&primitives[0]) {
1039 DrawPrimitive::Image {
1040 rect,
1041 image: actual,
1042 alpha,
1043 color_filter,
1044 sampling,
1045 src_rect,
1046 } => {
1047 assert_eq!(*rect, Rect::from_size(Size::new(40.0, 24.0)));
1048 assert_eq!(*actual, image);
1049 assert_eq!(*alpha, 1.0);
1050 assert!(color_filter.is_none());
1051 assert_eq!(*sampling, ImageSampling::Nearest);
1052 assert!(src_rect.is_none());
1053 }
1054 other => panic!("expected image primitive, got {other:?}"),
1055 }
1056 }
1057
1058 #[test]
1059 fn draw_image_src_stores_src_rect() {
1060 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
1061 let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
1062 let src = Rect {
1063 x: 10.0,
1064 y: 20.0,
1065 width: 30.0,
1066 height: 40.0,
1067 };
1068 let dst = Rect {
1069 x: 0.0,
1070 y: 0.0,
1071 width: 60.0,
1072 height: 80.0,
1073 };
1074 scope.draw_image_src(image.clone(), src, dst, 0.8, None);
1075 let primitives = scope.into_primitives();
1076 assert_eq!(primitives.len(), 1);
1077 match unwrap_image(&primitives[0]) {
1078 DrawPrimitive::Image {
1079 rect,
1080 image: actual,
1081 alpha,
1082 sampling,
1083 src_rect,
1084 ..
1085 } => {
1086 assert_eq!(*rect, dst);
1087 assert_eq!(*actual, image);
1088 assert!((alpha - 0.8).abs() < 1e-5);
1089 assert_eq!(*sampling, ImageSampling::Nearest);
1090 assert_eq!(*src_rect, Some(src));
1091 }
1092 other => panic!("expected image primitive, got {other:?}"),
1093 }
1094 }
1095
1096 #[test]
1097 fn draw_image_at_sampled_records_requested_sampling() {
1098 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
1099 let image = ImageBitmap::from_rgba8(8, 8, vec![255; 8 * 8 * 4]).expect("image");
1100 let dst = Rect {
1101 x: 2.0,
1102 y: 3.0,
1103 width: 40.0,
1104 height: 30.0,
1105 };
1106
1107 scope.draw_image_at_sampled(dst, image.clone(), 0.7, None, ImageSampling::Linear);
1108
1109 let primitives = scope.into_primitives();
1110 assert_eq!(primitives.len(), 1);
1111 match unwrap_image(&primitives[0]) {
1112 DrawPrimitive::Image {
1113 rect,
1114 image: actual,
1115 alpha,
1116 sampling,
1117 src_rect,
1118 ..
1119 } => {
1120 assert_eq!(*rect, dst);
1121 assert_eq!(*actual, image);
1122 assert!((alpha - 0.7).abs() < 1e-5);
1123 assert_eq!(*sampling, ImageSampling::Linear);
1124 assert!(src_rect.is_none());
1125 }
1126 other => panic!("expected image primitive, got {other:?}"),
1127 }
1128 }
1129
1130 #[test]
1131 fn draw_image_src_sampled_records_requested_sampling() {
1132 let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
1133 let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
1134 let src = Rect {
1135 x: 4.0,
1136 y: 6.0,
1137 width: 16.0,
1138 height: 20.0,
1139 };
1140 let dst = Rect {
1141 x: 8.0,
1142 y: 10.0,
1143 width: 32.0,
1144 height: 40.0,
1145 };
1146
1147 scope.draw_image_src_sampled(image.clone(), src, dst, 0.5, None, ImageSampling::Linear);
1148
1149 let primitives = scope.into_primitives();
1150 assert_eq!(primitives.len(), 1);
1151 match unwrap_image(&primitives[0]) {
1152 DrawPrimitive::Image {
1153 rect,
1154 image: actual,
1155 alpha,
1156 sampling,
1157 src_rect,
1158 ..
1159 } => {
1160 assert_eq!(*rect, dst);
1161 assert_eq!(*actual, image);
1162 assert!((alpha - 0.5).abs() < 1e-5);
1163 assert_eq!(*sampling, ImageSampling::Linear);
1164 assert_eq!(*src_rect, Some(src));
1165 }
1166 other => panic!("expected image primitive, got {other:?}"),
1167 }
1168 }
1169
1170 #[test]
1171 fn draw_image_at_clamps_alpha() {
1172 let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
1173 let image = ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image");
1174 scope.draw_image_at(
1175 Rect::from_origin_size(Point::new(2.0, 3.0), Size::new(5.0, 6.0)),
1176 image,
1177 3.0,
1178 Some(ColorFilter::Tint(Color::from_rgba_u8(128, 128, 255, 255))),
1179 );
1180 assert_image_alpha(&scope.into_primitives()[0], 1.0);
1181 }
1182
1183 #[test]
1184 fn graphics_layer_clone_with_render_effect() {
1185 let layer = GraphicsLayer {
1186 render_effect: Some(RenderEffect::blur(10.0)),
1187 backdrop_effect: Some(RenderEffect::blur(6.0)),
1188 color_filter: Some(ColorFilter::tint(Color::from_rgba_u8(128, 200, 255, 255))),
1189 alpha: 0.5,
1190 rotation_z: 12.0,
1191 shadow_elevation: 4.0,
1192 shape: LayerShape::Rounded(RoundedCornerShape::uniform(6.0)),
1193 clip: true,
1194 compositing_strategy: CompositingStrategy::Offscreen,
1195 blend_mode: BlendMode::SrcOver,
1196 ..Default::default()
1197 };
1198 let cloned = layer.clone();
1199 assert_eq!(cloned.alpha, 0.5);
1200 assert!(cloned.render_effect.is_some());
1201 assert!(cloned.backdrop_effect.is_some());
1202 assert_eq!(layer.color_filter, cloned.color_filter);
1203 assert_eq!(layer.render_effect, cloned.render_effect);
1204 assert_eq!(layer.backdrop_effect, cloned.backdrop_effect);
1205 assert!((cloned.rotation_z - 12.0).abs() < 1e-6);
1206 assert!((cloned.shadow_elevation - 4.0).abs() < 1e-6);
1207 assert_eq!(
1208 cloned.shape,
1209 LayerShape::Rounded(RoundedCornerShape::uniform(6.0))
1210 );
1211 assert!(cloned.clip);
1212 assert_eq!(cloned.compositing_strategy, CompositingStrategy::Offscreen);
1213 assert_eq!(cloned.blend_mode, BlendMode::SrcOver);
1214 }
1215
1216 #[test]
1217 fn graphics_layer_default_has_no_effect() {
1218 let layer = GraphicsLayer::default();
1219 assert!(layer.color_filter.is_none());
1220 assert!(layer.render_effect.is_none());
1221 assert!(layer.backdrop_effect.is_none());
1222 assert_eq!(layer.compositing_strategy, CompositingStrategy::Auto);
1223 assert_eq!(layer.blend_mode, BlendMode::SrcOver);
1224 assert_eq!(layer.alpha, 1.0);
1225 assert_eq!(layer.transform_origin, TransformOrigin::CENTER);
1226 assert!((layer.camera_distance - 8.0).abs() < 1e-6);
1227 assert_eq!(layer.shape, LayerShape::Rectangle);
1228 assert!(!layer.clip);
1229 assert_eq!(layer.ambient_shadow_color, Color::BLACK);
1230 assert_eq!(layer.spot_shadow_color, Color::BLACK);
1231 }
1232
1233 #[test]
1234 fn transform_origin_construction() {
1235 let origin = TransformOrigin::new(0.25, 0.75);
1236 assert!((origin.pivot_fraction_x - 0.25).abs() < 1e-6);
1237 assert!((origin.pivot_fraction_y - 0.75).abs() < 1e-6);
1238 }
1239
1240 #[test]
1241 fn layer_shape_default_is_rectangle() {
1242 assert_eq!(LayerShape::default(), LayerShape::Rectangle);
1243 }
1244}