Skip to main content

cranpose_ui_graphics/
geometry.rs

1//! Geometric primitives: Point, Size, Rect, Insets, Path
2
3use crate::{Brush, Color, ColorFilter, ImageBitmap};
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        /// Optional source rectangle in image-pixel coordinates.
406        /// When `None`, the entire image is drawn. When `Some`, only the
407        /// specified sub-region of the source image is sampled.
408        src_rect: Option<Rect>,
409    },
410    /// Shadow that requires blur processing. The renderer decides technique
411    /// (GPU blur, CPU approximation, etc.).
412    Shadow(ShadowPrimitive),
413}
414
415/// Describes a shadow to be rendered. Each renderer chooses how to blur.
416#[derive(Clone, Debug, PartialEq)]
417pub enum ShadowPrimitive {
418    /// Drop shadow: render shape behind content, blurred.
419    Drop {
420        shape: Box<DrawPrimitive>,
421        blur_radius: f32,
422        blend_mode: BlendMode,
423    },
424    /// Inner shadow: render fill + cutout to offscreen, blur, clip to bounds.
425    Inner {
426        fill: Box<DrawPrimitive>,
427        cutout: Box<DrawPrimitive>,
428        blur_radius: f32,
429        blend_mode: BlendMode,
430        /// Element bounds — blurred result must be clipped here.
431        clip_rect: Rect,
432    },
433}
434
435pub trait DrawScope {
436    fn size(&self) -> Size;
437    fn draw_content(&mut self);
438    fn draw_rect(&mut self, brush: Brush);
439    fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode);
440    /// Draws a rectangle at the specified position and size.
441    fn draw_rect_at(&mut self, rect: Rect, brush: Brush);
442    fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode);
443    fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii);
444    fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode);
445    fn draw_image(&mut self, image: ImageBitmap);
446    fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode);
447    fn draw_image_at(
448        &mut self,
449        rect: Rect,
450        image: ImageBitmap,
451        alpha: f32,
452        color_filter: Option<ColorFilter>,
453    );
454    fn draw_image_at_blend(
455        &mut self,
456        rect: Rect,
457        image: ImageBitmap,
458        alpha: f32,
459        color_filter: Option<ColorFilter>,
460        blend_mode: BlendMode,
461    );
462    /// Draws a sub-region of an image. `src_rect` is in image-pixel
463    /// coordinates; `dst_rect` is in scope coordinates.
464    fn draw_image_src(
465        &mut self,
466        image: ImageBitmap,
467        src_rect: Rect,
468        dst_rect: Rect,
469        alpha: f32,
470        color_filter: Option<ColorFilter>,
471    );
472    fn draw_image_src_blend(
473        &mut self,
474        image: ImageBitmap,
475        src_rect: Rect,
476        dst_rect: Rect,
477        alpha: f32,
478        color_filter: Option<ColorFilter>,
479        blend_mode: BlendMode,
480    );
481    fn into_primitives(self) -> Vec<DrawPrimitive>;
482}
483
484#[derive(Default)]
485pub struct DrawScopeDefault {
486    size: Size,
487    primitives: Vec<DrawPrimitive>,
488}
489
490impl DrawScopeDefault {
491    pub fn new(size: Size) -> Self {
492        Self {
493            size,
494            primitives: Vec::new(),
495        }
496    }
497
498    fn push_blended_primitive(&mut self, primitive: DrawPrimitive, blend_mode: BlendMode) {
499        if blend_mode == BlendMode::SrcOver {
500            self.primitives.push(primitive);
501        } else {
502            self.primitives.push(DrawPrimitive::Blend {
503                primitive: Box::new(primitive),
504                blend_mode,
505            });
506        }
507    }
508}
509
510impl DrawScope for DrawScopeDefault {
511    fn size(&self) -> Size {
512        self.size
513    }
514
515    fn draw_content(&mut self) {
516        self.primitives.push(DrawPrimitive::Content);
517    }
518
519    fn draw_rect(&mut self, brush: Brush) {
520        self.draw_rect_blend(brush, BlendMode::SrcOver);
521    }
522
523    fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode) {
524        self.push_blended_primitive(
525            DrawPrimitive::Rect {
526                rect: Rect::from_size(self.size),
527                brush,
528            },
529            blend_mode,
530        );
531    }
532
533    fn draw_rect_at(&mut self, rect: Rect, brush: Brush) {
534        self.draw_rect_at_blend(rect, brush, BlendMode::SrcOver);
535    }
536
537    fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode) {
538        self.push_blended_primitive(DrawPrimitive::Rect { rect, brush }, blend_mode);
539    }
540
541    fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii) {
542        self.draw_round_rect_blend(brush, radii, BlendMode::SrcOver);
543    }
544
545    fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode) {
546        self.push_blended_primitive(
547            DrawPrimitive::RoundRect {
548                rect: Rect::from_size(self.size),
549                brush,
550                radii,
551            },
552            blend_mode,
553        );
554    }
555
556    fn draw_image(&mut self, image: ImageBitmap) {
557        self.draw_image_blend(image, BlendMode::SrcOver);
558    }
559
560    fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode) {
561        self.push_blended_primitive(
562            DrawPrimitive::Image {
563                rect: Rect::from_size(self.size),
564                image,
565                alpha: 1.0,
566                color_filter: None,
567                src_rect: None,
568            },
569            blend_mode,
570        );
571    }
572
573    fn draw_image_at(
574        &mut self,
575        rect: Rect,
576        image: ImageBitmap,
577        alpha: f32,
578        color_filter: Option<ColorFilter>,
579    ) {
580        self.draw_image_at_blend(rect, image, alpha, color_filter, BlendMode::SrcOver);
581    }
582
583    fn draw_image_at_blend(
584        &mut self,
585        rect: Rect,
586        image: ImageBitmap,
587        alpha: f32,
588        color_filter: Option<ColorFilter>,
589        blend_mode: BlendMode,
590    ) {
591        self.push_blended_primitive(
592            DrawPrimitive::Image {
593                rect,
594                image,
595                alpha: alpha.clamp(0.0, 1.0),
596                color_filter,
597                src_rect: None,
598            },
599            blend_mode,
600        );
601    }
602
603    fn draw_image_src(
604        &mut self,
605        image: ImageBitmap,
606        src_rect: Rect,
607        dst_rect: Rect,
608        alpha: f32,
609        color_filter: Option<ColorFilter>,
610    ) {
611        self.draw_image_src_blend(
612            image,
613            src_rect,
614            dst_rect,
615            alpha,
616            color_filter,
617            BlendMode::SrcOver,
618        );
619    }
620
621    fn draw_image_src_blend(
622        &mut self,
623        image: ImageBitmap,
624        src_rect: Rect,
625        dst_rect: Rect,
626        alpha: f32,
627        color_filter: Option<ColorFilter>,
628        blend_mode: BlendMode,
629    ) {
630        self.push_blended_primitive(
631            DrawPrimitive::Image {
632                rect: dst_rect,
633                image,
634                alpha: alpha.clamp(0.0, 1.0),
635                color_filter,
636                src_rect: Some(src_rect),
637            },
638            blend_mode,
639        );
640    }
641
642    fn into_primitives(self) -> Vec<DrawPrimitive> {
643        self.primitives
644    }
645}
646
647#[cfg(test)]
648mod tests {
649    use super::*;
650    use crate::{Color, ImageBitmap, RenderEffect};
651
652    fn assert_image_alpha(primitive: &DrawPrimitive, expected: f32) {
653        match primitive {
654            DrawPrimitive::Image { alpha, .. } => assert!((alpha - expected).abs() < 1e-5),
655            DrawPrimitive::Blend { primitive, .. } => assert_image_alpha(primitive, expected),
656            other => panic!("expected image primitive, got {other:?}"),
657        }
658    }
659
660    fn unwrap_image(primitive: &DrawPrimitive) -> &DrawPrimitive {
661        match primitive {
662            DrawPrimitive::Image { .. } => primitive,
663            DrawPrimitive::Blend { primitive, .. } => unwrap_image(primitive),
664            other => panic!("expected image primitive, got {other:?}"),
665        }
666    }
667
668    #[test]
669    fn draw_content_inserts_content_marker() {
670        let mut scope = DrawScopeDefault::new(Size::new(8.0, 8.0));
671        scope.draw_rect(Brush::solid(Color::WHITE));
672        scope.draw_content();
673        scope.draw_rect_blend(Brush::solid(Color::BLACK), BlendMode::DstOut);
674
675        let primitives = scope.into_primitives();
676        assert!(matches!(primitives[1], DrawPrimitive::Content));
677        assert!(matches!(
678            primitives[2],
679            DrawPrimitive::Blend {
680                blend_mode: BlendMode::DstOut,
681                ..
682            }
683        ));
684    }
685
686    #[test]
687    fn draw_rect_blend_wraps_non_default_modes() {
688        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
689        scope.draw_rect_blend(Brush::solid(Color::RED), BlendMode::DstOut);
690
691        let primitives = scope.into_primitives();
692        assert_eq!(primitives.len(), 1);
693        match &primitives[0] {
694            DrawPrimitive::Blend {
695                primitive,
696                blend_mode,
697            } => {
698                assert_eq!(*blend_mode, BlendMode::DstOut);
699                assert!(matches!(**primitive, DrawPrimitive::Rect { .. }));
700            }
701            other => panic!("expected blended primitive, got {other:?}"),
702        }
703    }
704
705    #[test]
706    fn rect_union_encloses_both_inputs() {
707        let lhs = Rect {
708            x: 10.0,
709            y: 5.0,
710            width: 8.0,
711            height: 4.0,
712        };
713        let rhs = Rect {
714            x: 4.0,
715            y: 7.0,
716            width: 10.0,
717            height: 6.0,
718        };
719
720        assert_eq!(
721            lhs.union(rhs),
722            Rect {
723                x: 4.0,
724                y: 5.0,
725                width: 14.0,
726                height: 8.0,
727            }
728        );
729    }
730
731    #[test]
732    fn draw_image_uses_scope_size_as_default_rect() {
733        let mut scope = DrawScopeDefault::new(Size::new(40.0, 24.0));
734        let image = ImageBitmap::from_rgba8(2, 2, vec![255; 16]).expect("image");
735        scope.draw_image(image.clone());
736        let primitives = scope.into_primitives();
737        assert_eq!(primitives.len(), 1);
738        match unwrap_image(&primitives[0]) {
739            DrawPrimitive::Image {
740                rect,
741                image: actual,
742                alpha,
743                color_filter,
744                src_rect,
745            } => {
746                assert_eq!(*rect, Rect::from_size(Size::new(40.0, 24.0)));
747                assert_eq!(*actual, image);
748                assert_eq!(*alpha, 1.0);
749                assert!(color_filter.is_none());
750                assert!(src_rect.is_none());
751            }
752            other => panic!("expected image primitive, got {other:?}"),
753        }
754    }
755
756    #[test]
757    fn draw_image_src_stores_src_rect() {
758        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
759        let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
760        let src = Rect {
761            x: 10.0,
762            y: 20.0,
763            width: 30.0,
764            height: 40.0,
765        };
766        let dst = Rect {
767            x: 0.0,
768            y: 0.0,
769            width: 60.0,
770            height: 80.0,
771        };
772        scope.draw_image_src(image.clone(), src, dst, 0.8, None);
773        let primitives = scope.into_primitives();
774        assert_eq!(primitives.len(), 1);
775        match unwrap_image(&primitives[0]) {
776            DrawPrimitive::Image {
777                rect,
778                image: actual,
779                alpha,
780                src_rect,
781                ..
782            } => {
783                assert_eq!(*rect, dst);
784                assert_eq!(*actual, image);
785                assert!((alpha - 0.8).abs() < 1e-5);
786                assert_eq!(*src_rect, Some(src));
787            }
788            other => panic!("expected image primitive, got {other:?}"),
789        }
790    }
791
792    #[test]
793    fn draw_image_at_clamps_alpha() {
794        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
795        let image = ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image");
796        scope.draw_image_at(
797            Rect::from_origin_size(Point::new(2.0, 3.0), Size::new(5.0, 6.0)),
798            image,
799            3.0,
800            Some(ColorFilter::Tint(Color::from_rgba_u8(128, 128, 255, 255))),
801        );
802        assert_image_alpha(&scope.into_primitives()[0], 1.0);
803    }
804
805    #[test]
806    fn graphics_layer_clone_with_render_effect() {
807        let layer = GraphicsLayer {
808            render_effect: Some(RenderEffect::blur(10.0)),
809            backdrop_effect: Some(RenderEffect::blur(6.0)),
810            color_filter: Some(ColorFilter::tint(Color::from_rgba_u8(128, 200, 255, 255))),
811            alpha: 0.5,
812            rotation_z: 12.0,
813            shadow_elevation: 4.0,
814            shape: LayerShape::Rounded(RoundedCornerShape::uniform(6.0)),
815            clip: true,
816            compositing_strategy: CompositingStrategy::Offscreen,
817            blend_mode: BlendMode::SrcOver,
818            ..Default::default()
819        };
820        let cloned = layer.clone();
821        assert_eq!(cloned.alpha, 0.5);
822        assert!(cloned.render_effect.is_some());
823        assert!(cloned.backdrop_effect.is_some());
824        assert_eq!(layer.color_filter, cloned.color_filter);
825        assert_eq!(layer.render_effect, cloned.render_effect);
826        assert_eq!(layer.backdrop_effect, cloned.backdrop_effect);
827        assert!((cloned.rotation_z - 12.0).abs() < 1e-6);
828        assert!((cloned.shadow_elevation - 4.0).abs() < 1e-6);
829        assert_eq!(
830            cloned.shape,
831            LayerShape::Rounded(RoundedCornerShape::uniform(6.0))
832        );
833        assert!(cloned.clip);
834        assert_eq!(cloned.compositing_strategy, CompositingStrategy::Offscreen);
835        assert_eq!(cloned.blend_mode, BlendMode::SrcOver);
836    }
837
838    #[test]
839    fn graphics_layer_default_has_no_effect() {
840        let layer = GraphicsLayer::default();
841        assert!(layer.color_filter.is_none());
842        assert!(layer.render_effect.is_none());
843        assert!(layer.backdrop_effect.is_none());
844        assert_eq!(layer.compositing_strategy, CompositingStrategy::Auto);
845        assert_eq!(layer.blend_mode, BlendMode::SrcOver);
846        assert_eq!(layer.alpha, 1.0);
847        assert_eq!(layer.transform_origin, TransformOrigin::CENTER);
848        assert!((layer.camera_distance - 8.0).abs() < 1e-6);
849        assert_eq!(layer.shape, LayerShape::Rectangle);
850        assert!(!layer.clip);
851        assert_eq!(layer.ambient_shadow_color, Color::BLACK);
852        assert_eq!(layer.spot_shadow_color, Color::BLACK);
853    }
854
855    #[test]
856    fn transform_origin_construction() {
857        let origin = TransformOrigin::new(0.25, 0.75);
858        assert!((origin.pivot_fraction_x - 0.25).abs() < 1e-6);
859        assert!((origin.pivot_fraction_y - 0.75).abs() < 1e-6);
860    }
861
862    #[test]
863    fn layer_shape_default_is_rectangle() {
864        assert_eq!(LayerShape::default(), LayerShape::Rectangle);
865    }
866}