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