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