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
98/// Padding values for each edge of a rectangle.
99#[derive(Clone, Copy, Debug, Default, PartialEq)]
100pub struct EdgeInsets {
101    pub left: f32,
102    pub top: f32,
103    pub right: f32,
104    pub bottom: f32,
105}
106
107impl EdgeInsets {
108    pub fn uniform(all: f32) -> Self {
109        Self {
110            left: all,
111            top: all,
112            right: all,
113            bottom: all,
114        }
115    }
116
117    pub fn horizontal(horizontal: f32) -> Self {
118        Self {
119            left: horizontal,
120            right: horizontal,
121            ..Self::default()
122        }
123    }
124
125    pub fn vertical(vertical: f32) -> Self {
126        Self {
127            top: vertical,
128            bottom: vertical,
129            ..Self::default()
130        }
131    }
132
133    pub fn symmetric(horizontal: f32, vertical: f32) -> Self {
134        Self {
135            left: horizontal,
136            right: horizontal,
137            top: vertical,
138            bottom: vertical,
139        }
140    }
141
142    pub fn from_components(left: f32, top: f32, right: f32, bottom: f32) -> Self {
143        Self {
144            left,
145            top,
146            right,
147            bottom,
148        }
149    }
150
151    pub fn is_zero(&self) -> bool {
152        self.left == 0.0 && self.top == 0.0 && self.right == 0.0 && self.bottom == 0.0
153    }
154
155    pub fn horizontal_sum(&self) -> f32 {
156        self.left + self.right
157    }
158
159    pub fn vertical_sum(&self) -> f32 {
160        self.top + self.bottom
161    }
162}
163
164impl AddAssign for EdgeInsets {
165    fn add_assign(&mut self, rhs: Self) {
166        self.left += rhs.left;
167        self.top += rhs.top;
168        self.right += rhs.right;
169        self.bottom += rhs.bottom;
170    }
171}
172
173#[derive(Clone, Copy, Debug, Default, PartialEq)]
174pub struct CornerRadii {
175    pub top_left: f32,
176    pub top_right: f32,
177    pub bottom_right: f32,
178    pub bottom_left: f32,
179}
180
181impl CornerRadii {
182    pub fn uniform(radius: f32) -> Self {
183        Self {
184            top_left: radius,
185            top_right: radius,
186            bottom_right: radius,
187            bottom_left: radius,
188        }
189    }
190}
191
192#[derive(Clone, Copy, Debug, PartialEq)]
193pub struct RoundedCornerShape {
194    radii: CornerRadii,
195}
196
197impl RoundedCornerShape {
198    pub fn new(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self {
199        Self {
200            radii: CornerRadii {
201                top_left,
202                top_right,
203                bottom_right,
204                bottom_left,
205            },
206        }
207    }
208
209    pub fn uniform(radius: f32) -> Self {
210        Self {
211            radii: CornerRadii::uniform(radius),
212        }
213    }
214
215    pub fn with_radii(radii: CornerRadii) -> Self {
216        Self { radii }
217    }
218
219    pub fn resolve(&self, width: f32, height: f32) -> CornerRadii {
220        let mut resolved = self.radii;
221        let max_width = (width / 2.0).max(0.0);
222        let max_height = (height / 2.0).max(0.0);
223        resolved.top_left = resolved.top_left.clamp(0.0, max_width).min(max_height);
224        resolved.top_right = resolved.top_right.clamp(0.0, max_width).min(max_height);
225        resolved.bottom_right = resolved.bottom_right.clamp(0.0, max_width).min(max_height);
226        resolved.bottom_left = resolved.bottom_left.clamp(0.0, max_width).min(max_height);
227        resolved
228    }
229
230    pub fn radii(&self) -> CornerRadii {
231        self.radii
232    }
233}
234
235#[derive(Clone, Copy, Debug, PartialEq)]
236pub struct TransformOrigin {
237    pub pivot_fraction_x: f32,
238    pub pivot_fraction_y: f32,
239}
240
241impl TransformOrigin {
242    pub const fn new(pivot_fraction_x: f32, pivot_fraction_y: f32) -> Self {
243        Self {
244            pivot_fraction_x,
245            pivot_fraction_y,
246        }
247    }
248
249    pub const CENTER: TransformOrigin = TransformOrigin::new(0.5, 0.5);
250}
251
252impl Default for TransformOrigin {
253    fn default() -> Self {
254        Self::CENTER
255    }
256}
257
258#[derive(Clone, Copy, Debug, Default, PartialEq)]
259pub enum LayerShape {
260    #[default]
261    Rectangle,
262    Rounded(RoundedCornerShape),
263}
264
265#[derive(Clone, Debug, PartialEq)]
266pub struct GraphicsLayer {
267    pub alpha: f32,
268    pub scale: f32,
269    pub scale_x: f32,
270    pub scale_y: f32,
271    pub rotation_x: f32,
272    pub rotation_y: f32,
273    pub rotation_z: f32,
274    pub camera_distance: f32,
275    pub transform_origin: TransformOrigin,
276    pub translation_x: f32,
277    pub translation_y: f32,
278    pub shadow_elevation: f32,
279    pub ambient_shadow_color: Color,
280    pub spot_shadow_color: Color,
281    pub shape: LayerShape,
282    pub clip: bool,
283    pub compositing_strategy: CompositingStrategy,
284    pub blend_mode: BlendMode,
285    pub color_filter: Option<ColorFilter>,
286    pub render_effect: Option<crate::render_effect::RenderEffect>,
287    pub backdrop_effect: Option<crate::render_effect::RenderEffect>,
288}
289
290impl Default for GraphicsLayer {
291    fn default() -> Self {
292        Self {
293            alpha: 1.0,
294            scale: 1.0,
295            scale_x: 1.0,
296            scale_y: 1.0,
297            rotation_x: 0.0,
298            rotation_y: 0.0,
299            rotation_z: 0.0,
300            camera_distance: 8.0,
301            transform_origin: TransformOrigin::CENTER,
302            translation_x: 0.0,
303            translation_y: 0.0,
304            shadow_elevation: 0.0,
305            ambient_shadow_color: Color::BLACK,
306            spot_shadow_color: Color::BLACK,
307            shape: LayerShape::Rectangle,
308            clip: false,
309            compositing_strategy: CompositingStrategy::Auto,
310            blend_mode: BlendMode::SrcOver,
311            color_filter: None,
312            render_effect: None,
313            backdrop_effect: None,
314        }
315    }
316}
317
318/// Blend mode used for draw primitives.
319///
320/// This mirrors Jetpack Compose's blend-mode vocabulary while the renderer
321/// currently guarantees `SrcOver` and `DstOut` behavior.
322#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
323pub enum BlendMode {
324    Clear,
325    Src,
326    Dst,
327    #[default]
328    SrcOver,
329    DstOver,
330    SrcIn,
331    DstIn,
332    SrcOut,
333    DstOut,
334    SrcAtop,
335    DstAtop,
336    Xor,
337    Plus,
338    Modulate,
339    Screen,
340    Overlay,
341    Darken,
342    Lighten,
343    ColorDodge,
344    ColorBurn,
345    HardLight,
346    SoftLight,
347    Difference,
348    Exclusion,
349    Multiply,
350    Hue,
351    Saturation,
352    Color,
353    Luminosity,
354}
355
356/// Controls how a graphics layer is composited into its parent target.
357#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
358pub enum CompositingStrategy {
359    /// Use renderer heuristics (default).
360    #[default]
361    Auto,
362    /// Render this layer to an offscreen target, then composite.
363    Offscreen,
364    /// Multiply alpha on source colors without allocating an offscreen layer.
365    ModulateAlpha,
366}
367
368#[derive(Clone, Debug, PartialEq)]
369pub enum DrawPrimitive {
370    /// Marker emitted by `draw_content()` inside `draw_with_content`.
371    /// This is consumed by the modifier pipeline and never rendered directly.
372    Content,
373    /// Wrapper to associate a draw primitive with a non-default blend mode.
374    Blend {
375        primitive: Box<DrawPrimitive>,
376        blend_mode: BlendMode,
377    },
378    Rect {
379        rect: Rect,
380        brush: Brush,
381    },
382    RoundRect {
383        rect: Rect,
384        brush: Brush,
385        radii: CornerRadii,
386    },
387    Image {
388        rect: Rect,
389        image: ImageBitmap,
390        alpha: f32,
391        color_filter: Option<ColorFilter>,
392        /// Optional source rectangle in image-pixel coordinates.
393        /// When `None`, the entire image is drawn. When `Some`, only the
394        /// specified sub-region of the source image is sampled.
395        src_rect: Option<Rect>,
396    },
397    /// Shadow that requires blur processing. The renderer decides technique
398    /// (GPU blur, CPU approximation, etc.).
399    Shadow(ShadowPrimitive),
400}
401
402/// Describes a shadow to be rendered. Each renderer chooses how to blur.
403#[derive(Clone, Debug, PartialEq)]
404pub enum ShadowPrimitive {
405    /// Drop shadow: render shape behind content, blurred.
406    Drop {
407        shape: Box<DrawPrimitive>,
408        blur_radius: f32,
409        blend_mode: BlendMode,
410    },
411    /// Inner shadow: render fill + cutout to offscreen, blur, clip to bounds.
412    Inner {
413        fill: Box<DrawPrimitive>,
414        cutout: Box<DrawPrimitive>,
415        blur_radius: f32,
416        blend_mode: BlendMode,
417        /// Element bounds — blurred result must be clipped here.
418        clip_rect: Rect,
419    },
420}
421
422pub trait DrawScope {
423    fn size(&self) -> Size;
424    fn draw_content(&mut self);
425    fn draw_rect(&mut self, brush: Brush);
426    fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode);
427    /// Draws a rectangle at the specified position and size.
428    fn draw_rect_at(&mut self, rect: Rect, brush: Brush);
429    fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode);
430    fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii);
431    fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode);
432    fn draw_image(&mut self, image: ImageBitmap);
433    fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode);
434    fn draw_image_at(
435        &mut self,
436        rect: Rect,
437        image: ImageBitmap,
438        alpha: f32,
439        color_filter: Option<ColorFilter>,
440    );
441    fn draw_image_at_blend(
442        &mut self,
443        rect: Rect,
444        image: ImageBitmap,
445        alpha: f32,
446        color_filter: Option<ColorFilter>,
447        blend_mode: BlendMode,
448    );
449    /// Draws a sub-region of an image. `src_rect` is in image-pixel
450    /// coordinates; `dst_rect` is in scope coordinates.
451    fn draw_image_src(
452        &mut self,
453        image: ImageBitmap,
454        src_rect: Rect,
455        dst_rect: Rect,
456        alpha: f32,
457        color_filter: Option<ColorFilter>,
458    );
459    fn draw_image_src_blend(
460        &mut self,
461        image: ImageBitmap,
462        src_rect: Rect,
463        dst_rect: Rect,
464        alpha: f32,
465        color_filter: Option<ColorFilter>,
466        blend_mode: BlendMode,
467    );
468    fn into_primitives(self) -> Vec<DrawPrimitive>;
469}
470
471#[derive(Default)]
472pub struct DrawScopeDefault {
473    size: Size,
474    primitives: Vec<DrawPrimitive>,
475}
476
477impl DrawScopeDefault {
478    pub fn new(size: Size) -> Self {
479        Self {
480            size,
481            primitives: Vec::new(),
482        }
483    }
484
485    fn push_blended_primitive(&mut self, primitive: DrawPrimitive, blend_mode: BlendMode) {
486        if blend_mode == BlendMode::SrcOver {
487            self.primitives.push(primitive);
488        } else {
489            self.primitives.push(DrawPrimitive::Blend {
490                primitive: Box::new(primitive),
491                blend_mode,
492            });
493        }
494    }
495}
496
497impl DrawScope for DrawScopeDefault {
498    fn size(&self) -> Size {
499        self.size
500    }
501
502    fn draw_content(&mut self) {
503        self.primitives.push(DrawPrimitive::Content);
504    }
505
506    fn draw_rect(&mut self, brush: Brush) {
507        self.draw_rect_blend(brush, BlendMode::SrcOver);
508    }
509
510    fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode) {
511        self.push_blended_primitive(
512            DrawPrimitive::Rect {
513                rect: Rect::from_size(self.size),
514                brush,
515            },
516            blend_mode,
517        );
518    }
519
520    fn draw_rect_at(&mut self, rect: Rect, brush: Brush) {
521        self.draw_rect_at_blend(rect, brush, BlendMode::SrcOver);
522    }
523
524    fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode) {
525        self.push_blended_primitive(DrawPrimitive::Rect { rect, brush }, blend_mode);
526    }
527
528    fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii) {
529        self.draw_round_rect_blend(brush, radii, BlendMode::SrcOver);
530    }
531
532    fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode) {
533        self.push_blended_primitive(
534            DrawPrimitive::RoundRect {
535                rect: Rect::from_size(self.size),
536                brush,
537                radii,
538            },
539            blend_mode,
540        );
541    }
542
543    fn draw_image(&mut self, image: ImageBitmap) {
544        self.draw_image_blend(image, BlendMode::SrcOver);
545    }
546
547    fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode) {
548        self.push_blended_primitive(
549            DrawPrimitive::Image {
550                rect: Rect::from_size(self.size),
551                image,
552                alpha: 1.0,
553                color_filter: None,
554                src_rect: None,
555            },
556            blend_mode,
557        );
558    }
559
560    fn draw_image_at(
561        &mut self,
562        rect: Rect,
563        image: ImageBitmap,
564        alpha: f32,
565        color_filter: Option<ColorFilter>,
566    ) {
567        self.draw_image_at_blend(rect, image, alpha, color_filter, BlendMode::SrcOver);
568    }
569
570    fn draw_image_at_blend(
571        &mut self,
572        rect: Rect,
573        image: ImageBitmap,
574        alpha: f32,
575        color_filter: Option<ColorFilter>,
576        blend_mode: BlendMode,
577    ) {
578        self.push_blended_primitive(
579            DrawPrimitive::Image {
580                rect,
581                image,
582                alpha: alpha.clamp(0.0, 1.0),
583                color_filter,
584                src_rect: None,
585            },
586            blend_mode,
587        );
588    }
589
590    fn draw_image_src(
591        &mut self,
592        image: ImageBitmap,
593        src_rect: Rect,
594        dst_rect: Rect,
595        alpha: f32,
596        color_filter: Option<ColorFilter>,
597    ) {
598        self.draw_image_src_blend(
599            image,
600            src_rect,
601            dst_rect,
602            alpha,
603            color_filter,
604            BlendMode::SrcOver,
605        );
606    }
607
608    fn draw_image_src_blend(
609        &mut self,
610        image: ImageBitmap,
611        src_rect: Rect,
612        dst_rect: Rect,
613        alpha: f32,
614        color_filter: Option<ColorFilter>,
615        blend_mode: BlendMode,
616    ) {
617        self.push_blended_primitive(
618            DrawPrimitive::Image {
619                rect: dst_rect,
620                image,
621                alpha: alpha.clamp(0.0, 1.0),
622                color_filter,
623                src_rect: Some(src_rect),
624            },
625            blend_mode,
626        );
627    }
628
629    fn into_primitives(self) -> Vec<DrawPrimitive> {
630        self.primitives
631    }
632}
633
634#[cfg(test)]
635mod tests {
636    use super::*;
637    use crate::{Color, ImageBitmap, RenderEffect};
638
639    fn assert_image_alpha(primitive: &DrawPrimitive, expected: f32) {
640        match primitive {
641            DrawPrimitive::Image { alpha, .. } => assert!((alpha - expected).abs() < 1e-5),
642            DrawPrimitive::Blend { primitive, .. } => assert_image_alpha(primitive, expected),
643            other => panic!("expected image primitive, got {other:?}"),
644        }
645    }
646
647    fn unwrap_image(primitive: &DrawPrimitive) -> &DrawPrimitive {
648        match primitive {
649            DrawPrimitive::Image { .. } => primitive,
650            DrawPrimitive::Blend { primitive, .. } => unwrap_image(primitive),
651            other => panic!("expected image primitive, got {other:?}"),
652        }
653    }
654
655    #[test]
656    fn draw_content_inserts_content_marker() {
657        let mut scope = DrawScopeDefault::new(Size::new(8.0, 8.0));
658        scope.draw_rect(Brush::solid(Color::WHITE));
659        scope.draw_content();
660        scope.draw_rect_blend(Brush::solid(Color::BLACK), BlendMode::DstOut);
661
662        let primitives = scope.into_primitives();
663        assert!(matches!(primitives[1], DrawPrimitive::Content));
664        assert!(matches!(
665            primitives[2],
666            DrawPrimitive::Blend {
667                blend_mode: BlendMode::DstOut,
668                ..
669            }
670        ));
671    }
672
673    #[test]
674    fn draw_rect_blend_wraps_non_default_modes() {
675        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
676        scope.draw_rect_blend(Brush::solid(Color::RED), BlendMode::DstOut);
677
678        let primitives = scope.into_primitives();
679        assert_eq!(primitives.len(), 1);
680        match &primitives[0] {
681            DrawPrimitive::Blend {
682                primitive,
683                blend_mode,
684            } => {
685                assert_eq!(*blend_mode, BlendMode::DstOut);
686                assert!(matches!(**primitive, DrawPrimitive::Rect { .. }));
687            }
688            other => panic!("expected blended primitive, got {other:?}"),
689        }
690    }
691
692    #[test]
693    fn draw_image_uses_scope_size_as_default_rect() {
694        let mut scope = DrawScopeDefault::new(Size::new(40.0, 24.0));
695        let image = ImageBitmap::from_rgba8(2, 2, vec![255; 16]).expect("image");
696        scope.draw_image(image.clone());
697        let primitives = scope.into_primitives();
698        assert_eq!(primitives.len(), 1);
699        match unwrap_image(&primitives[0]) {
700            DrawPrimitive::Image {
701                rect,
702                image: actual,
703                alpha,
704                color_filter,
705                src_rect,
706            } => {
707                assert_eq!(*rect, Rect::from_size(Size::new(40.0, 24.0)));
708                assert_eq!(*actual, image);
709                assert_eq!(*alpha, 1.0);
710                assert!(color_filter.is_none());
711                assert!(src_rect.is_none());
712            }
713            other => panic!("expected image primitive, got {other:?}"),
714        }
715    }
716
717    #[test]
718    fn draw_image_src_stores_src_rect() {
719        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
720        let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
721        let src = Rect {
722            x: 10.0,
723            y: 20.0,
724            width: 30.0,
725            height: 40.0,
726        };
727        let dst = Rect {
728            x: 0.0,
729            y: 0.0,
730            width: 60.0,
731            height: 80.0,
732        };
733        scope.draw_image_src(image.clone(), src, dst, 0.8, None);
734        let primitives = scope.into_primitives();
735        assert_eq!(primitives.len(), 1);
736        match unwrap_image(&primitives[0]) {
737            DrawPrimitive::Image {
738                rect,
739                image: actual,
740                alpha,
741                src_rect,
742                ..
743            } => {
744                assert_eq!(*rect, dst);
745                assert_eq!(*actual, image);
746                assert!((alpha - 0.8).abs() < 1e-5);
747                assert_eq!(*src_rect, Some(src));
748            }
749            other => panic!("expected image primitive, got {other:?}"),
750        }
751    }
752
753    #[test]
754    fn draw_image_at_clamps_alpha() {
755        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
756        let image = ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image");
757        scope.draw_image_at(
758            Rect::from_origin_size(Point::new(2.0, 3.0), Size::new(5.0, 6.0)),
759            image,
760            3.0,
761            Some(ColorFilter::Tint(Color::from_rgba_u8(128, 128, 255, 255))),
762        );
763        assert_image_alpha(&scope.into_primitives()[0], 1.0);
764    }
765
766    #[test]
767    fn graphics_layer_clone_with_render_effect() {
768        let layer = GraphicsLayer {
769            render_effect: Some(RenderEffect::blur(10.0)),
770            backdrop_effect: Some(RenderEffect::blur(6.0)),
771            color_filter: Some(ColorFilter::tint(Color::from_rgba_u8(128, 200, 255, 255))),
772            alpha: 0.5,
773            rotation_z: 12.0,
774            shadow_elevation: 4.0,
775            shape: LayerShape::Rounded(RoundedCornerShape::uniform(6.0)),
776            clip: true,
777            compositing_strategy: CompositingStrategy::Offscreen,
778            blend_mode: BlendMode::SrcOver,
779            ..Default::default()
780        };
781        let cloned = layer.clone();
782        assert_eq!(cloned.alpha, 0.5);
783        assert!(cloned.render_effect.is_some());
784        assert!(cloned.backdrop_effect.is_some());
785        assert_eq!(layer.color_filter, cloned.color_filter);
786        assert_eq!(layer.render_effect, cloned.render_effect);
787        assert_eq!(layer.backdrop_effect, cloned.backdrop_effect);
788        assert!((cloned.rotation_z - 12.0).abs() < 1e-6);
789        assert!((cloned.shadow_elevation - 4.0).abs() < 1e-6);
790        assert_eq!(
791            cloned.shape,
792            LayerShape::Rounded(RoundedCornerShape::uniform(6.0))
793        );
794        assert!(cloned.clip);
795        assert_eq!(cloned.compositing_strategy, CompositingStrategy::Offscreen);
796        assert_eq!(cloned.blend_mode, BlendMode::SrcOver);
797    }
798
799    #[test]
800    fn graphics_layer_default_has_no_effect() {
801        let layer = GraphicsLayer::default();
802        assert!(layer.color_filter.is_none());
803        assert!(layer.render_effect.is_none());
804        assert!(layer.backdrop_effect.is_none());
805        assert_eq!(layer.compositing_strategy, CompositingStrategy::Auto);
806        assert_eq!(layer.blend_mode, BlendMode::SrcOver);
807        assert_eq!(layer.alpha, 1.0);
808        assert_eq!(layer.transform_origin, TransformOrigin::CENTER);
809        assert!((layer.camera_distance - 8.0).abs() < 1e-6);
810        assert_eq!(layer.shape, LayerShape::Rectangle);
811        assert!(!layer.clip);
812        assert_eq!(layer.ambient_shadow_color, Color::BLACK);
813        assert_eq!(layer.spot_shadow_color, Color::BLACK);
814    }
815
816    #[test]
817    fn transform_origin_construction() {
818        let origin = TransformOrigin::new(0.25, 0.75);
819        assert!((origin.pivot_fraction_x - 0.25).abs() < 1e-6);
820        assert!((origin.pivot_fraction_y - 0.75).abs() < 1e-6);
821    }
822
823    #[test]
824    fn layer_shape_default_is_rectangle() {
825        assert_eq!(LayerShape::default(), LayerShape::Rectangle);
826    }
827}