Skip to main content

cranpose_ui_graphics/
geometry.rs

1//! Geometric primitives: Point, Size, Rect, Insets, Path
2
3use 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    /// Returns the intersection of two rectangles, or `None` if they don't overlap.
78    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/// Padding values for each edge of a rectangle.
112#[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/// Blend mode used for draw primitives.
332///
333/// This mirrors Jetpack Compose's blend-mode vocabulary while the renderer
334/// currently guarantees `SrcOver` and `DstOut` behavior.
335#[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/// Controls how a graphics layer is composited into its parent target.
370#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
371pub enum CompositingStrategy {
372    /// Use renderer heuristics (default).
373    #[default]
374    Auto,
375    /// Render this layer to an offscreen target, then composite.
376    Offscreen,
377    /// Multiply alpha on source colors without allocating an offscreen layer.
378    ModulateAlpha,
379}
380
381#[derive(Clone, Debug, PartialEq)]
382pub enum DrawPrimitive {
383    /// Marker emitted by `draw_content()` inside `draw_with_content`.
384    /// This is consumed by the modifier pipeline and never rendered directly.
385    Content,
386    /// Wrapper to associate a draw primitive with a non-default blend mode.
387    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        /// Optional source rectangle in image-pixel coordinates.
407        /// When `None`, the entire image is drawn. When `Some`, only the
408        /// specified sub-region of the source image is sampled.
409        src_rect: Option<Rect>,
410    },
411    /// Shadow that requires blur processing. The renderer decides technique
412    /// (GPU blur, CPU approximation, etc.).
413    Shadow(ShadowPrimitive),
414}
415
416/// Describes a shadow to be rendered. Each renderer chooses how to blur.
417#[derive(Clone, Debug, PartialEq)]
418pub enum ShadowPrimitive {
419    /// Drop shadow: render shape behind content, blurred.
420    Drop {
421        shape: Box<DrawPrimitive>,
422        blur_radius: f32,
423        blend_mode: BlendMode,
424    },
425    /// Inner shadow: render fill + cutout to offscreen, blur, clip to bounds.
426    Inner {
427        fill: Box<DrawPrimitive>,
428        cutout: Box<DrawPrimitive>,
429        blur_radius: f32,
430        blend_mode: BlendMode,
431        /// Element bounds — blurred result must be clipped here.
432        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    /// Draws a rectangle at the specified position and size.
442    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    /// Draws a sub-region of an image. `src_rect` is in image-pixel
480    /// coordinates; `dst_rect` is in scope coordinates.
481    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 into_primitives(self) -> Vec<DrawPrimitive>;
508}
509
510#[derive(Default)]
511pub struct DrawScopeDefault {
512    size: Size,
513    primitives: Vec<DrawPrimitive>,
514}
515
516impl DrawScopeDefault {
517    pub fn new(size: Size) -> Self {
518        Self {
519            size,
520            primitives: Vec::new(),
521        }
522    }
523
524    fn push_blended_primitive(&mut self, primitive: DrawPrimitive, blend_mode: BlendMode) {
525        if blend_mode == BlendMode::SrcOver {
526            self.primitives.push(primitive);
527        } else {
528            self.primitives.push(DrawPrimitive::Blend {
529                primitive: Box::new(primitive),
530                blend_mode,
531            });
532        }
533    }
534}
535
536impl DrawScope for DrawScopeDefault {
537    fn size(&self) -> Size {
538        self.size
539    }
540
541    fn draw_content(&mut self) {
542        self.primitives.push(DrawPrimitive::Content);
543    }
544
545    fn draw_rect(&mut self, brush: Brush) {
546        self.draw_rect_blend(brush, BlendMode::SrcOver);
547    }
548
549    fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode) {
550        self.push_blended_primitive(
551            DrawPrimitive::Rect {
552                rect: Rect::from_size(self.size),
553                brush,
554            },
555            blend_mode,
556        );
557    }
558
559    fn draw_rect_at(&mut self, rect: Rect, brush: Brush) {
560        self.draw_rect_at_blend(rect, brush, BlendMode::SrcOver);
561    }
562
563    fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode) {
564        self.push_blended_primitive(DrawPrimitive::Rect { rect, brush }, blend_mode);
565    }
566
567    fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii) {
568        self.draw_round_rect_blend(brush, radii, BlendMode::SrcOver);
569    }
570
571    fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode) {
572        self.push_blended_primitive(
573            DrawPrimitive::RoundRect {
574                rect: Rect::from_size(self.size),
575                brush,
576                radii,
577            },
578            blend_mode,
579        );
580    }
581
582    fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32) {
583        self.draw_circle_blend(brush, center, radius, BlendMode::SrcOver);
584    }
585
586    fn draw_circle_blend(
587        &mut self,
588        brush: Brush,
589        center: Point,
590        radius: f32,
591        blend_mode: BlendMode,
592    ) {
593        let radius = radius.max(0.0);
594        let diameter = radius * 2.0;
595        self.push_blended_primitive(
596            DrawPrimitive::RoundRect {
597                rect: Rect {
598                    x: center.x - radius,
599                    y: center.y - radius,
600                    width: diameter,
601                    height: diameter,
602                },
603                brush,
604                radii: CornerRadii::uniform(radius),
605            },
606            blend_mode,
607        );
608    }
609
610    fn draw_image(&mut self, image: ImageBitmap) {
611        self.draw_image_blend(image, BlendMode::SrcOver);
612    }
613
614    fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode) {
615        self.push_blended_primitive(
616            DrawPrimitive::Image {
617                rect: Rect::from_size(self.size),
618                image,
619                alpha: 1.0,
620                color_filter: None,
621                sampling: ImageSampling::Nearest,
622                src_rect: None,
623            },
624            blend_mode,
625        );
626    }
627
628    fn draw_image_at(
629        &mut self,
630        rect: Rect,
631        image: ImageBitmap,
632        alpha: f32,
633        color_filter: Option<ColorFilter>,
634    ) {
635        self.draw_image_at_sampled(rect, image, alpha, color_filter, ImageSampling::Nearest);
636    }
637
638    fn draw_image_at_sampled(
639        &mut self,
640        rect: Rect,
641        image: ImageBitmap,
642        alpha: f32,
643        color_filter: Option<ColorFilter>,
644        sampling: ImageSampling,
645    ) {
646        self.push_blended_primitive(
647            DrawPrimitive::Image {
648                rect,
649                image,
650                alpha: alpha.clamp(0.0, 1.0),
651                color_filter,
652                sampling,
653                src_rect: None,
654            },
655            BlendMode::SrcOver,
656        );
657    }
658
659    fn draw_image_at_blend(
660        &mut self,
661        rect: Rect,
662        image: ImageBitmap,
663        alpha: f32,
664        color_filter: Option<ColorFilter>,
665        blend_mode: BlendMode,
666    ) {
667        self.push_blended_primitive(
668            DrawPrimitive::Image {
669                rect,
670                image,
671                alpha: alpha.clamp(0.0, 1.0),
672                color_filter,
673                sampling: ImageSampling::Nearest,
674                src_rect: None,
675            },
676            blend_mode,
677        );
678    }
679
680    fn draw_image_src(
681        &mut self,
682        image: ImageBitmap,
683        src_rect: Rect,
684        dst_rect: Rect,
685        alpha: f32,
686        color_filter: Option<ColorFilter>,
687    ) {
688        self.draw_image_src_blend(
689            image,
690            src_rect,
691            dst_rect,
692            alpha,
693            color_filter,
694            BlendMode::SrcOver,
695        );
696    }
697
698    fn draw_image_src_sampled(
699        &mut self,
700        image: ImageBitmap,
701        src_rect: Rect,
702        dst_rect: Rect,
703        alpha: f32,
704        color_filter: Option<ColorFilter>,
705        sampling: ImageSampling,
706    ) {
707        self.push_blended_primitive(
708            DrawPrimitive::Image {
709                rect: dst_rect,
710                image,
711                alpha: alpha.clamp(0.0, 1.0),
712                color_filter,
713                sampling,
714                src_rect: Some(src_rect),
715            },
716            BlendMode::SrcOver,
717        );
718    }
719
720    fn draw_image_src_blend(
721        &mut self,
722        image: ImageBitmap,
723        src_rect: Rect,
724        dst_rect: Rect,
725        alpha: f32,
726        color_filter: Option<ColorFilter>,
727        blend_mode: BlendMode,
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: ImageSampling::Nearest,
736                src_rect: Some(src_rect),
737            },
738            blend_mode,
739        );
740    }
741
742    fn into_primitives(self) -> Vec<DrawPrimitive> {
743        self.primitives
744    }
745}
746
747#[cfg(test)]
748mod tests {
749    use super::*;
750    use crate::{Color, ImageBitmap, RenderEffect};
751
752    fn assert_image_alpha(primitive: &DrawPrimitive, expected: f32) {
753        match primitive {
754            DrawPrimitive::Image { alpha, .. } => assert!((alpha - expected).abs() < 1e-5),
755            DrawPrimitive::Blend { primitive, .. } => assert_image_alpha(primitive, expected),
756            other => panic!("expected image primitive, got {other:?}"),
757        }
758    }
759
760    fn unwrap_image(primitive: &DrawPrimitive) -> &DrawPrimitive {
761        match primitive {
762            DrawPrimitive::Image { .. } => primitive,
763            DrawPrimitive::Blend { primitive, .. } => unwrap_image(primitive),
764            other => panic!("expected image primitive, got {other:?}"),
765        }
766    }
767
768    #[test]
769    fn draw_content_inserts_content_marker() {
770        let mut scope = DrawScopeDefault::new(Size::new(8.0, 8.0));
771        scope.draw_rect(Brush::solid(Color::WHITE));
772        scope.draw_content();
773        scope.draw_rect_blend(Brush::solid(Color::BLACK), BlendMode::DstOut);
774
775        let primitives = scope.into_primitives();
776        assert!(matches!(primitives[1], DrawPrimitive::Content));
777        assert!(matches!(
778            primitives[2],
779            DrawPrimitive::Blend {
780                blend_mode: BlendMode::DstOut,
781                ..
782            }
783        ));
784    }
785
786    #[test]
787    fn draw_rect_blend_wraps_non_default_modes() {
788        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
789        scope.draw_rect_blend(Brush::solid(Color::RED), BlendMode::DstOut);
790
791        let primitives = scope.into_primitives();
792        assert_eq!(primitives.len(), 1);
793        match &primitives[0] {
794            DrawPrimitive::Blend {
795                primitive,
796                blend_mode,
797            } => {
798                assert_eq!(*blend_mode, BlendMode::DstOut);
799                assert!(matches!(**primitive, DrawPrimitive::Rect { .. }));
800            }
801            other => panic!("expected blended primitive, got {other:?}"),
802        }
803    }
804
805    #[test]
806    fn draw_circle_records_centered_round_rect() {
807        let mut scope = DrawScopeDefault::new(Size::new(40.0, 40.0));
808        scope.draw_circle(Brush::solid(Color::BLUE), Point::new(12.0, 16.0), 5.0);
809
810        let primitives = scope.into_primitives();
811        assert_eq!(primitives.len(), 1);
812        match &primitives[0] {
813            DrawPrimitive::RoundRect { rect, radii, .. } => {
814                assert_eq!(
815                    *rect,
816                    Rect {
817                        x: 7.0,
818                        y: 11.0,
819                        width: 10.0,
820                        height: 10.0,
821                    }
822                );
823                assert_eq!(*radii, CornerRadii::uniform(5.0));
824            }
825            other => panic!("expected circular round rect, got {other:?}"),
826        }
827    }
828
829    #[test]
830    fn draw_circle_blend_wraps_non_default_modes() {
831        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
832        scope.draw_circle_blend(
833            Brush::solid(Color::RED),
834            Point::new(5.0, 5.0),
835            3.0,
836            BlendMode::Plus,
837        );
838
839        let primitives = scope.into_primitives();
840        assert_eq!(primitives.len(), 1);
841        match &primitives[0] {
842            DrawPrimitive::Blend {
843                primitive,
844                blend_mode,
845            } => {
846                assert_eq!(*blend_mode, BlendMode::Plus);
847                assert!(matches!(**primitive, DrawPrimitive::RoundRect { .. }));
848            }
849            other => panic!("expected blended circle primitive, got {other:?}"),
850        }
851    }
852
853    #[test]
854    fn rect_union_encloses_both_inputs() {
855        let lhs = Rect {
856            x: 10.0,
857            y: 5.0,
858            width: 8.0,
859            height: 4.0,
860        };
861        let rhs = Rect {
862            x: 4.0,
863            y: 7.0,
864            width: 10.0,
865            height: 6.0,
866        };
867
868        assert_eq!(
869            lhs.union(rhs),
870            Rect {
871                x: 4.0,
872                y: 5.0,
873                width: 14.0,
874                height: 8.0,
875            }
876        );
877    }
878
879    #[test]
880    fn draw_image_uses_scope_size_as_default_rect() {
881        let mut scope = DrawScopeDefault::new(Size::new(40.0, 24.0));
882        let image = ImageBitmap::from_rgba8(2, 2, vec![255; 16]).expect("image");
883        scope.draw_image(image.clone());
884        let primitives = scope.into_primitives();
885        assert_eq!(primitives.len(), 1);
886        match unwrap_image(&primitives[0]) {
887            DrawPrimitive::Image {
888                rect,
889                image: actual,
890                alpha,
891                color_filter,
892                sampling,
893                src_rect,
894            } => {
895                assert_eq!(*rect, Rect::from_size(Size::new(40.0, 24.0)));
896                assert_eq!(*actual, image);
897                assert_eq!(*alpha, 1.0);
898                assert!(color_filter.is_none());
899                assert_eq!(*sampling, ImageSampling::Nearest);
900                assert!(src_rect.is_none());
901            }
902            other => panic!("expected image primitive, got {other:?}"),
903        }
904    }
905
906    #[test]
907    fn draw_image_src_stores_src_rect() {
908        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
909        let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
910        let src = Rect {
911            x: 10.0,
912            y: 20.0,
913            width: 30.0,
914            height: 40.0,
915        };
916        let dst = Rect {
917            x: 0.0,
918            y: 0.0,
919            width: 60.0,
920            height: 80.0,
921        };
922        scope.draw_image_src(image.clone(), src, dst, 0.8, None);
923        let primitives = scope.into_primitives();
924        assert_eq!(primitives.len(), 1);
925        match unwrap_image(&primitives[0]) {
926            DrawPrimitive::Image {
927                rect,
928                image: actual,
929                alpha,
930                sampling,
931                src_rect,
932                ..
933            } => {
934                assert_eq!(*rect, dst);
935                assert_eq!(*actual, image);
936                assert!((alpha - 0.8).abs() < 1e-5);
937                assert_eq!(*sampling, ImageSampling::Nearest);
938                assert_eq!(*src_rect, Some(src));
939            }
940            other => panic!("expected image primitive, got {other:?}"),
941        }
942    }
943
944    #[test]
945    fn draw_image_at_sampled_records_requested_sampling() {
946        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
947        let image = ImageBitmap::from_rgba8(8, 8, vec![255; 8 * 8 * 4]).expect("image");
948        let dst = Rect {
949            x: 2.0,
950            y: 3.0,
951            width: 40.0,
952            height: 30.0,
953        };
954
955        scope.draw_image_at_sampled(dst, image.clone(), 0.7, None, ImageSampling::Linear);
956
957        let primitives = scope.into_primitives();
958        assert_eq!(primitives.len(), 1);
959        match unwrap_image(&primitives[0]) {
960            DrawPrimitive::Image {
961                rect,
962                image: actual,
963                alpha,
964                sampling,
965                src_rect,
966                ..
967            } => {
968                assert_eq!(*rect, dst);
969                assert_eq!(*actual, image);
970                assert!((alpha - 0.7).abs() < 1e-5);
971                assert_eq!(*sampling, ImageSampling::Linear);
972                assert!(src_rect.is_none());
973            }
974            other => panic!("expected image primitive, got {other:?}"),
975        }
976    }
977
978    #[test]
979    fn draw_image_src_sampled_records_requested_sampling() {
980        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
981        let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
982        let src = Rect {
983            x: 4.0,
984            y: 6.0,
985            width: 16.0,
986            height: 20.0,
987        };
988        let dst = Rect {
989            x: 8.0,
990            y: 10.0,
991            width: 32.0,
992            height: 40.0,
993        };
994
995        scope.draw_image_src_sampled(image.clone(), src, dst, 0.5, None, ImageSampling::Linear);
996
997        let primitives = scope.into_primitives();
998        assert_eq!(primitives.len(), 1);
999        match unwrap_image(&primitives[0]) {
1000            DrawPrimitive::Image {
1001                rect,
1002                image: actual,
1003                alpha,
1004                sampling,
1005                src_rect,
1006                ..
1007            } => {
1008                assert_eq!(*rect, dst);
1009                assert_eq!(*actual, image);
1010                assert!((alpha - 0.5).abs() < 1e-5);
1011                assert_eq!(*sampling, ImageSampling::Linear);
1012                assert_eq!(*src_rect, Some(src));
1013            }
1014            other => panic!("expected image primitive, got {other:?}"),
1015        }
1016    }
1017
1018    #[test]
1019    fn draw_image_at_clamps_alpha() {
1020        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
1021        let image = ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image");
1022        scope.draw_image_at(
1023            Rect::from_origin_size(Point::new(2.0, 3.0), Size::new(5.0, 6.0)),
1024            image,
1025            3.0,
1026            Some(ColorFilter::Tint(Color::from_rgba_u8(128, 128, 255, 255))),
1027        );
1028        assert_image_alpha(&scope.into_primitives()[0], 1.0);
1029    }
1030
1031    #[test]
1032    fn graphics_layer_clone_with_render_effect() {
1033        let layer = GraphicsLayer {
1034            render_effect: Some(RenderEffect::blur(10.0)),
1035            backdrop_effect: Some(RenderEffect::blur(6.0)),
1036            color_filter: Some(ColorFilter::tint(Color::from_rgba_u8(128, 200, 255, 255))),
1037            alpha: 0.5,
1038            rotation_z: 12.0,
1039            shadow_elevation: 4.0,
1040            shape: LayerShape::Rounded(RoundedCornerShape::uniform(6.0)),
1041            clip: true,
1042            compositing_strategy: CompositingStrategy::Offscreen,
1043            blend_mode: BlendMode::SrcOver,
1044            ..Default::default()
1045        };
1046        let cloned = layer.clone();
1047        assert_eq!(cloned.alpha, 0.5);
1048        assert!(cloned.render_effect.is_some());
1049        assert!(cloned.backdrop_effect.is_some());
1050        assert_eq!(layer.color_filter, cloned.color_filter);
1051        assert_eq!(layer.render_effect, cloned.render_effect);
1052        assert_eq!(layer.backdrop_effect, cloned.backdrop_effect);
1053        assert!((cloned.rotation_z - 12.0).abs() < 1e-6);
1054        assert!((cloned.shadow_elevation - 4.0).abs() < 1e-6);
1055        assert_eq!(
1056            cloned.shape,
1057            LayerShape::Rounded(RoundedCornerShape::uniform(6.0))
1058        );
1059        assert!(cloned.clip);
1060        assert_eq!(cloned.compositing_strategy, CompositingStrategy::Offscreen);
1061        assert_eq!(cloned.blend_mode, BlendMode::SrcOver);
1062    }
1063
1064    #[test]
1065    fn graphics_layer_default_has_no_effect() {
1066        let layer = GraphicsLayer::default();
1067        assert!(layer.color_filter.is_none());
1068        assert!(layer.render_effect.is_none());
1069        assert!(layer.backdrop_effect.is_none());
1070        assert_eq!(layer.compositing_strategy, CompositingStrategy::Auto);
1071        assert_eq!(layer.blend_mode, BlendMode::SrcOver);
1072        assert_eq!(layer.alpha, 1.0);
1073        assert_eq!(layer.transform_origin, TransformOrigin::CENTER);
1074        assert!((layer.camera_distance - 8.0).abs() < 1e-6);
1075        assert_eq!(layer.shape, LayerShape::Rectangle);
1076        assert!(!layer.clip);
1077        assert_eq!(layer.ambient_shadow_color, Color::BLACK);
1078        assert_eq!(layer.spot_shadow_color, Color::BLACK);
1079    }
1080
1081    #[test]
1082    fn transform_origin_construction() {
1083        let origin = TransformOrigin::new(0.25, 0.75);
1084        assert!((origin.pivot_fraction_x - 0.25).abs() < 1e-6);
1085        assert!((origin.pivot_fraction_y - 0.75).abs() < 1e-6);
1086    }
1087
1088    #[test]
1089    fn layer_shape_default_is_rectangle() {
1090        assert_eq!(LayerShape::default(), LayerShape::Rectangle);
1091    }
1092}