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_image(&mut self, image: ImageBitmap);
447    fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode);
448    fn draw_image_at(
449        &mut self,
450        rect: Rect,
451        image: ImageBitmap,
452        alpha: f32,
453        color_filter: Option<ColorFilter>,
454    );
455    fn draw_image_at_sampled(
456        &mut self,
457        rect: Rect,
458        image: ImageBitmap,
459        alpha: f32,
460        color_filter: Option<ColorFilter>,
461        sampling: ImageSampling,
462    );
463    fn draw_image_at_blend(
464        &mut self,
465        rect: Rect,
466        image: ImageBitmap,
467        alpha: f32,
468        color_filter: Option<ColorFilter>,
469        blend_mode: BlendMode,
470    );
471    /// Draws a sub-region of an image. `src_rect` is in image-pixel
472    /// coordinates; `dst_rect` is in scope coordinates.
473    fn draw_image_src(
474        &mut self,
475        image: ImageBitmap,
476        src_rect: Rect,
477        dst_rect: Rect,
478        alpha: f32,
479        color_filter: Option<ColorFilter>,
480    );
481    fn draw_image_src_sampled(
482        &mut self,
483        image: ImageBitmap,
484        src_rect: Rect,
485        dst_rect: Rect,
486        alpha: f32,
487        color_filter: Option<ColorFilter>,
488        sampling: ImageSampling,
489    );
490    fn draw_image_src_blend(
491        &mut self,
492        image: ImageBitmap,
493        src_rect: Rect,
494        dst_rect: Rect,
495        alpha: f32,
496        color_filter: Option<ColorFilter>,
497        blend_mode: BlendMode,
498    );
499    fn into_primitives(self) -> Vec<DrawPrimitive>;
500}
501
502#[derive(Default)]
503pub struct DrawScopeDefault {
504    size: Size,
505    primitives: Vec<DrawPrimitive>,
506}
507
508impl DrawScopeDefault {
509    pub fn new(size: Size) -> Self {
510        Self {
511            size,
512            primitives: Vec::new(),
513        }
514    }
515
516    fn push_blended_primitive(&mut self, primitive: DrawPrimitive, blend_mode: BlendMode) {
517        if blend_mode == BlendMode::SrcOver {
518            self.primitives.push(primitive);
519        } else {
520            self.primitives.push(DrawPrimitive::Blend {
521                primitive: Box::new(primitive),
522                blend_mode,
523            });
524        }
525    }
526}
527
528impl DrawScope for DrawScopeDefault {
529    fn size(&self) -> Size {
530        self.size
531    }
532
533    fn draw_content(&mut self) {
534        self.primitives.push(DrawPrimitive::Content);
535    }
536
537    fn draw_rect(&mut self, brush: Brush) {
538        self.draw_rect_blend(brush, BlendMode::SrcOver);
539    }
540
541    fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode) {
542        self.push_blended_primitive(
543            DrawPrimitive::Rect {
544                rect: Rect::from_size(self.size),
545                brush,
546            },
547            blend_mode,
548        );
549    }
550
551    fn draw_rect_at(&mut self, rect: Rect, brush: Brush) {
552        self.draw_rect_at_blend(rect, brush, BlendMode::SrcOver);
553    }
554
555    fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode) {
556        self.push_blended_primitive(DrawPrimitive::Rect { rect, brush }, blend_mode);
557    }
558
559    fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii) {
560        self.draw_round_rect_blend(brush, radii, BlendMode::SrcOver);
561    }
562
563    fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode) {
564        self.push_blended_primitive(
565            DrawPrimitive::RoundRect {
566                rect: Rect::from_size(self.size),
567                brush,
568                radii,
569            },
570            blend_mode,
571        );
572    }
573
574    fn draw_image(&mut self, image: ImageBitmap) {
575        self.draw_image_blend(image, BlendMode::SrcOver);
576    }
577
578    fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode) {
579        self.push_blended_primitive(
580            DrawPrimitive::Image {
581                rect: Rect::from_size(self.size),
582                image,
583                alpha: 1.0,
584                color_filter: None,
585                sampling: ImageSampling::Nearest,
586                src_rect: None,
587            },
588            blend_mode,
589        );
590    }
591
592    fn draw_image_at(
593        &mut self,
594        rect: Rect,
595        image: ImageBitmap,
596        alpha: f32,
597        color_filter: Option<ColorFilter>,
598    ) {
599        self.draw_image_at_sampled(rect, image, alpha, color_filter, ImageSampling::Nearest);
600    }
601
602    fn draw_image_at_sampled(
603        &mut self,
604        rect: Rect,
605        image: ImageBitmap,
606        alpha: f32,
607        color_filter: Option<ColorFilter>,
608        sampling: ImageSampling,
609    ) {
610        self.push_blended_primitive(
611            DrawPrimitive::Image {
612                rect,
613                image,
614                alpha: alpha.clamp(0.0, 1.0),
615                color_filter,
616                sampling,
617                src_rect: None,
618            },
619            BlendMode::SrcOver,
620        );
621    }
622
623    fn draw_image_at_blend(
624        &mut self,
625        rect: Rect,
626        image: ImageBitmap,
627        alpha: f32,
628        color_filter: Option<ColorFilter>,
629        blend_mode: BlendMode,
630    ) {
631        self.push_blended_primitive(
632            DrawPrimitive::Image {
633                rect,
634                image,
635                alpha: alpha.clamp(0.0, 1.0),
636                color_filter,
637                sampling: ImageSampling::Nearest,
638                src_rect: None,
639            },
640            blend_mode,
641        );
642    }
643
644    fn draw_image_src(
645        &mut self,
646        image: ImageBitmap,
647        src_rect: Rect,
648        dst_rect: Rect,
649        alpha: f32,
650        color_filter: Option<ColorFilter>,
651    ) {
652        self.draw_image_src_blend(
653            image,
654            src_rect,
655            dst_rect,
656            alpha,
657            color_filter,
658            BlendMode::SrcOver,
659        );
660    }
661
662    fn draw_image_src_sampled(
663        &mut self,
664        image: ImageBitmap,
665        src_rect: Rect,
666        dst_rect: Rect,
667        alpha: f32,
668        color_filter: Option<ColorFilter>,
669        sampling: ImageSampling,
670    ) {
671        self.push_blended_primitive(
672            DrawPrimitive::Image {
673                rect: dst_rect,
674                image,
675                alpha: alpha.clamp(0.0, 1.0),
676                color_filter,
677                sampling,
678                src_rect: Some(src_rect),
679            },
680            BlendMode::SrcOver,
681        );
682    }
683
684    fn draw_image_src_blend(
685        &mut self,
686        image: ImageBitmap,
687        src_rect: Rect,
688        dst_rect: Rect,
689        alpha: f32,
690        color_filter: Option<ColorFilter>,
691        blend_mode: BlendMode,
692    ) {
693        self.push_blended_primitive(
694            DrawPrimitive::Image {
695                rect: dst_rect,
696                image,
697                alpha: alpha.clamp(0.0, 1.0),
698                color_filter,
699                sampling: ImageSampling::Nearest,
700                src_rect: Some(src_rect),
701            },
702            blend_mode,
703        );
704    }
705
706    fn into_primitives(self) -> Vec<DrawPrimitive> {
707        self.primitives
708    }
709}
710
711#[cfg(test)]
712mod tests {
713    use super::*;
714    use crate::{Color, ImageBitmap, RenderEffect};
715
716    fn assert_image_alpha(primitive: &DrawPrimitive, expected: f32) {
717        match primitive {
718            DrawPrimitive::Image { alpha, .. } => assert!((alpha - expected).abs() < 1e-5),
719            DrawPrimitive::Blend { primitive, .. } => assert_image_alpha(primitive, expected),
720            other => panic!("expected image primitive, got {other:?}"),
721        }
722    }
723
724    fn unwrap_image(primitive: &DrawPrimitive) -> &DrawPrimitive {
725        match primitive {
726            DrawPrimitive::Image { .. } => primitive,
727            DrawPrimitive::Blend { primitive, .. } => unwrap_image(primitive),
728            other => panic!("expected image primitive, got {other:?}"),
729        }
730    }
731
732    #[test]
733    fn draw_content_inserts_content_marker() {
734        let mut scope = DrawScopeDefault::new(Size::new(8.0, 8.0));
735        scope.draw_rect(Brush::solid(Color::WHITE));
736        scope.draw_content();
737        scope.draw_rect_blend(Brush::solid(Color::BLACK), BlendMode::DstOut);
738
739        let primitives = scope.into_primitives();
740        assert!(matches!(primitives[1], DrawPrimitive::Content));
741        assert!(matches!(
742            primitives[2],
743            DrawPrimitive::Blend {
744                blend_mode: BlendMode::DstOut,
745                ..
746            }
747        ));
748    }
749
750    #[test]
751    fn draw_rect_blend_wraps_non_default_modes() {
752        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
753        scope.draw_rect_blend(Brush::solid(Color::RED), BlendMode::DstOut);
754
755        let primitives = scope.into_primitives();
756        assert_eq!(primitives.len(), 1);
757        match &primitives[0] {
758            DrawPrimitive::Blend {
759                primitive,
760                blend_mode,
761            } => {
762                assert_eq!(*blend_mode, BlendMode::DstOut);
763                assert!(matches!(**primitive, DrawPrimitive::Rect { .. }));
764            }
765            other => panic!("expected blended primitive, got {other:?}"),
766        }
767    }
768
769    #[test]
770    fn rect_union_encloses_both_inputs() {
771        let lhs = Rect {
772            x: 10.0,
773            y: 5.0,
774            width: 8.0,
775            height: 4.0,
776        };
777        let rhs = Rect {
778            x: 4.0,
779            y: 7.0,
780            width: 10.0,
781            height: 6.0,
782        };
783
784        assert_eq!(
785            lhs.union(rhs),
786            Rect {
787                x: 4.0,
788                y: 5.0,
789                width: 14.0,
790                height: 8.0,
791            }
792        );
793    }
794
795    #[test]
796    fn draw_image_uses_scope_size_as_default_rect() {
797        let mut scope = DrawScopeDefault::new(Size::new(40.0, 24.0));
798        let image = ImageBitmap::from_rgba8(2, 2, vec![255; 16]).expect("image");
799        scope.draw_image(image.clone());
800        let primitives = scope.into_primitives();
801        assert_eq!(primitives.len(), 1);
802        match unwrap_image(&primitives[0]) {
803            DrawPrimitive::Image {
804                rect,
805                image: actual,
806                alpha,
807                color_filter,
808                sampling,
809                src_rect,
810            } => {
811                assert_eq!(*rect, Rect::from_size(Size::new(40.0, 24.0)));
812                assert_eq!(*actual, image);
813                assert_eq!(*alpha, 1.0);
814                assert!(color_filter.is_none());
815                assert_eq!(*sampling, ImageSampling::Nearest);
816                assert!(src_rect.is_none());
817            }
818            other => panic!("expected image primitive, got {other:?}"),
819        }
820    }
821
822    #[test]
823    fn draw_image_src_stores_src_rect() {
824        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
825        let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
826        let src = Rect {
827            x: 10.0,
828            y: 20.0,
829            width: 30.0,
830            height: 40.0,
831        };
832        let dst = Rect {
833            x: 0.0,
834            y: 0.0,
835            width: 60.0,
836            height: 80.0,
837        };
838        scope.draw_image_src(image.clone(), src, dst, 0.8, None);
839        let primitives = scope.into_primitives();
840        assert_eq!(primitives.len(), 1);
841        match unwrap_image(&primitives[0]) {
842            DrawPrimitive::Image {
843                rect,
844                image: actual,
845                alpha,
846                sampling,
847                src_rect,
848                ..
849            } => {
850                assert_eq!(*rect, dst);
851                assert_eq!(*actual, image);
852                assert!((alpha - 0.8).abs() < 1e-5);
853                assert_eq!(*sampling, ImageSampling::Nearest);
854                assert_eq!(*src_rect, Some(src));
855            }
856            other => panic!("expected image primitive, got {other:?}"),
857        }
858    }
859
860    #[test]
861    fn draw_image_at_sampled_records_requested_sampling() {
862        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
863        let image = ImageBitmap::from_rgba8(8, 8, vec![255; 8 * 8 * 4]).expect("image");
864        let dst = Rect {
865            x: 2.0,
866            y: 3.0,
867            width: 40.0,
868            height: 30.0,
869        };
870
871        scope.draw_image_at_sampled(dst, image.clone(), 0.7, None, ImageSampling::Linear);
872
873        let primitives = scope.into_primitives();
874        assert_eq!(primitives.len(), 1);
875        match unwrap_image(&primitives[0]) {
876            DrawPrimitive::Image {
877                rect,
878                image: actual,
879                alpha,
880                sampling,
881                src_rect,
882                ..
883            } => {
884                assert_eq!(*rect, dst);
885                assert_eq!(*actual, image);
886                assert!((alpha - 0.7).abs() < 1e-5);
887                assert_eq!(*sampling, ImageSampling::Linear);
888                assert!(src_rect.is_none());
889            }
890            other => panic!("expected image primitive, got {other:?}"),
891        }
892    }
893
894    #[test]
895    fn draw_image_src_sampled_records_requested_sampling() {
896        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
897        let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
898        let src = Rect {
899            x: 4.0,
900            y: 6.0,
901            width: 16.0,
902            height: 20.0,
903        };
904        let dst = Rect {
905            x: 8.0,
906            y: 10.0,
907            width: 32.0,
908            height: 40.0,
909        };
910
911        scope.draw_image_src_sampled(image.clone(), src, dst, 0.5, None, ImageSampling::Linear);
912
913        let primitives = scope.into_primitives();
914        assert_eq!(primitives.len(), 1);
915        match unwrap_image(&primitives[0]) {
916            DrawPrimitive::Image {
917                rect,
918                image: actual,
919                alpha,
920                sampling,
921                src_rect,
922                ..
923            } => {
924                assert_eq!(*rect, dst);
925                assert_eq!(*actual, image);
926                assert!((alpha - 0.5).abs() < 1e-5);
927                assert_eq!(*sampling, ImageSampling::Linear);
928                assert_eq!(*src_rect, Some(src));
929            }
930            other => panic!("expected image primitive, got {other:?}"),
931        }
932    }
933
934    #[test]
935    fn draw_image_at_clamps_alpha() {
936        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
937        let image = ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image");
938        scope.draw_image_at(
939            Rect::from_origin_size(Point::new(2.0, 3.0), Size::new(5.0, 6.0)),
940            image,
941            3.0,
942            Some(ColorFilter::Tint(Color::from_rgba_u8(128, 128, 255, 255))),
943        );
944        assert_image_alpha(&scope.into_primitives()[0], 1.0);
945    }
946
947    #[test]
948    fn graphics_layer_clone_with_render_effect() {
949        let layer = GraphicsLayer {
950            render_effect: Some(RenderEffect::blur(10.0)),
951            backdrop_effect: Some(RenderEffect::blur(6.0)),
952            color_filter: Some(ColorFilter::tint(Color::from_rgba_u8(128, 200, 255, 255))),
953            alpha: 0.5,
954            rotation_z: 12.0,
955            shadow_elevation: 4.0,
956            shape: LayerShape::Rounded(RoundedCornerShape::uniform(6.0)),
957            clip: true,
958            compositing_strategy: CompositingStrategy::Offscreen,
959            blend_mode: BlendMode::SrcOver,
960            ..Default::default()
961        };
962        let cloned = layer.clone();
963        assert_eq!(cloned.alpha, 0.5);
964        assert!(cloned.render_effect.is_some());
965        assert!(cloned.backdrop_effect.is_some());
966        assert_eq!(layer.color_filter, cloned.color_filter);
967        assert_eq!(layer.render_effect, cloned.render_effect);
968        assert_eq!(layer.backdrop_effect, cloned.backdrop_effect);
969        assert!((cloned.rotation_z - 12.0).abs() < 1e-6);
970        assert!((cloned.shadow_elevation - 4.0).abs() < 1e-6);
971        assert_eq!(
972            cloned.shape,
973            LayerShape::Rounded(RoundedCornerShape::uniform(6.0))
974        );
975        assert!(cloned.clip);
976        assert_eq!(cloned.compositing_strategy, CompositingStrategy::Offscreen);
977        assert_eq!(cloned.blend_mode, BlendMode::SrcOver);
978    }
979
980    #[test]
981    fn graphics_layer_default_has_no_effect() {
982        let layer = GraphicsLayer::default();
983        assert!(layer.color_filter.is_none());
984        assert!(layer.render_effect.is_none());
985        assert!(layer.backdrop_effect.is_none());
986        assert_eq!(layer.compositing_strategy, CompositingStrategy::Auto);
987        assert_eq!(layer.blend_mode, BlendMode::SrcOver);
988        assert_eq!(layer.alpha, 1.0);
989        assert_eq!(layer.transform_origin, TransformOrigin::CENTER);
990        assert!((layer.camera_distance - 8.0).abs() < 1e-6);
991        assert_eq!(layer.shape, LayerShape::Rectangle);
992        assert!(!layer.clip);
993        assert_eq!(layer.ambient_shadow_color, Color::BLACK);
994        assert_eq!(layer.spot_shadow_color, Color::BLACK);
995    }
996
997    #[test]
998    fn transform_origin_construction() {
999        let origin = TransformOrigin::new(0.25, 0.75);
1000        assert!((origin.pivot_fraction_x - 0.25).abs() < 1e-6);
1001        assert!((origin.pivot_fraction_y - 0.75).abs() < 1e-6);
1002    }
1003
1004    #[test]
1005    fn layer_shape_default_is_rectangle() {
1006        assert_eq!(LayerShape::default(), LayerShape::Rectangle);
1007    }
1008}