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