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