Skip to main content

cranpose_ui_graphics/
geometry.rs

1//! Geometric primitives: Point, Size, Rect, Insets, Path
2
3use std::{ops::AddAssign, rc::Rc};
4
5use crate::{
6    ArcRecordArgs, Brush, Color, ColorFilter, CommandRecorder, CommandRecording, ImageBitmap,
7    ImageSampling, normalized_band,
8    path::{DrawStyle, Path},
9    stroke::{LineGeometry, Stroke},
10    typography::{
11        DrawTextMeasurer, DrawTextStyle, TextAlign, TextMeasurement, TextVerticalAlign,
12        estimate_text_measurement,
13    },
14};
15
16const VECTOR_PATH_MASK_CACHE_ENTRIES: usize = 96;
17const VECTOR_PATH_MASK_CACHE_BYTES: usize = 8 * 1024 * 1024;
18
19struct VectorPathMaskCache {
20    entries: Vec<(u64, ImageBitmap)>,
21    bytes: usize,
22}
23
24impl VectorPathMaskCache {
25    const fn new() -> Self {
26        Self {
27            entries: Vec::new(),
28            bytes: 0,
29        }
30    }
31
32    fn get(&mut self, key: u64) -> Option<ImageBitmap> {
33        let index = self.entries.iter().position(|(seen, _)| *seen == key)?;
34        let entry = self.entries.remove(index);
35        let image = entry.1.clone();
36        self.entries.push(entry);
37        Some(image)
38    }
39
40    fn put(&mut self, key: u64, image: ImageBitmap) {
41        let bytes = image.pixels().len();
42        if bytes > VECTOR_PATH_MASK_CACHE_BYTES {
43            return;
44        }
45        self.bytes += bytes;
46        self.entries.push((key, image));
47        while self.entries.len() > VECTOR_PATH_MASK_CACHE_ENTRIES
48            || self.bytes > VECTOR_PATH_MASK_CACHE_BYTES
49        {
50            let (_, dropped) = self.entries.remove(0);
51            self.bytes = self.bytes.saturating_sub(dropped.pixels().len());
52        }
53    }
54}
55
56thread_local! {
57    static VECTOR_PATH_MASKS: std::cell::RefCell<VectorPathMaskCache> =
58        const { std::cell::RefCell::new(VectorPathMaskCache::new()) };
59}
60
61fn vector_path_mask_key(
62    path: &crate::VectorPath,
63    origin: Point,
64    mask_size: (usize, usize),
65    rgb: [u8; 3],
66    alpha: f32,
67) -> u64 {
68    use std::hash::Hasher;
69    let mut hasher = crate::fx_hash::FxHasher::default();
70    hasher.write_u8(path.fill_rule() as u8);
71    hasher.write_u32(origin.x.to_bits());
72    hasher.write_u32(origin.y.to_bits());
73    hasher.write_usize(mask_size.0);
74    hasher.write_usize(mask_size.1);
75    hasher.write(&rgb);
76    hasher.write_u32(alpha.to_bits());
77    for subpath in path.subpaths() {
78        hasher.write_usize(subpath.len());
79        for point in subpath {
80            hasher.write_u32(point.x.to_bits());
81            hasher.write_u32(point.y.to_bits());
82        }
83    }
84    hasher.finish()
85}
86
87fn vector_path_mask_cache_get(key: u64) -> Option<ImageBitmap> {
88    VECTOR_PATH_MASKS.with(|cache| cache.borrow_mut().get(key))
89}
90
91fn vector_path_mask_cache_put(key: u64, image: ImageBitmap) {
92    VECTOR_PATH_MASKS.with(|cache| cache.borrow_mut().put(key, image));
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Default)]
96pub struct Point {
97    pub x: f32,
98    pub y: f32,
99}
100
101impl Point {
102    pub const fn new(x: f32, y: f32) -> Self {
103        Self { x, y }
104    }
105
106    pub const ZERO: Point = Point { x: 0.0, y: 0.0 };
107}
108
109#[derive(Clone, Copy, Debug, PartialEq, Default)]
110pub struct Size {
111    pub width: f32,
112    pub height: f32,
113}
114
115impl Size {
116    pub const fn new(width: f32, height: f32) -> Self {
117        Self { width, height }
118    }
119
120    pub const ZERO: Size = Size {
121        width: 0.0,
122        height: 0.0,
123    };
124}
125
126#[derive(Clone, Copy, Debug, PartialEq)]
127pub struct Rect {
128    pub x: f32,
129    pub y: f32,
130    pub width: f32,
131    pub height: f32,
132}
133
134impl Rect {
135    /// The rect that holds nothing: what two clips that do not overlap
136    /// resolve to, so a clip that meets nothing stays a clip instead of
137    /// lifting.
138    pub const EMPTY: Rect = Rect {
139        x: 0.0,
140        y: 0.0,
141        width: 0.0,
142        height: 0.0,
143    };
144
145    pub fn from_origin_size(origin: Point, size: Size) -> Self {
146        Self {
147            x: origin.x,
148            y: origin.y,
149            width: size.width,
150            height: size.height,
151        }
152    }
153
154    pub fn from_size(size: Size) -> Self {
155        Self {
156            x: 0.0,
157            y: 0.0,
158            width: size.width,
159            height: size.height,
160        }
161    }
162
163    pub fn translate(&self, dx: f32, dy: f32) -> Self {
164        Self {
165            x: self.x + dx,
166            y: self.y + dy,
167            width: self.width,
168            height: self.height,
169        }
170    }
171
172    pub fn contains(&self, x: f32, y: f32) -> bool {
173        x >= self.x && y >= self.y && x <= self.x + self.width && y <= self.y + self.height
174    }
175
176    /// Whether the rect covers no area, so nothing clipped to it can paint.
177    pub fn is_empty(&self) -> bool {
178        self.width <= 0.0 || self.height <= 0.0
179    }
180
181    /// Returns the intersection of two rectangles, or `None` if they don't overlap.
182    pub fn intersect(&self, other: Rect) -> Option<Rect> {
183        let left = self.x.max(other.x);
184        let top = self.y.max(other.y);
185        let right = (self.x + self.width).min(other.x + other.width);
186        let bottom = (self.y + self.height).min(other.y + other.height);
187        let width = right - left;
188        let height = bottom - top;
189        if width <= 0.0 || height <= 0.0 {
190            None
191        } else {
192            Some(Rect {
193                x: left,
194                y: top,
195                width,
196                height,
197            })
198        }
199    }
200
201    pub fn union(&self, other: Rect) -> Rect {
202        let left = self.x.min(other.x);
203        let top = self.y.min(other.y);
204        let right = (self.x + self.width).max(other.x + other.width);
205        let bottom = (self.y + self.height).max(other.y + other.height);
206        Rect {
207            x: left,
208            y: top,
209            width: (right - left).max(0.0),
210            height: (bottom - top).max(0.0),
211        }
212    }
213}
214
215/// Padding values for each edge of a rectangle.
216#[derive(Clone, Copy, Debug, Default, PartialEq)]
217pub struct EdgeInsets {
218    pub left: f32,
219    pub top: f32,
220    pub right: f32,
221    pub bottom: f32,
222}
223
224impl EdgeInsets {
225    pub fn uniform(all: f32) -> Self {
226        Self {
227            left: all,
228            top: all,
229            right: all,
230            bottom: all,
231        }
232    }
233
234    pub fn horizontal(horizontal: f32) -> Self {
235        Self {
236            left: horizontal,
237            right: horizontal,
238            ..Self::default()
239        }
240    }
241
242    pub fn vertical(vertical: f32) -> Self {
243        Self {
244            top: vertical,
245            bottom: vertical,
246            ..Self::default()
247        }
248    }
249
250    pub fn symmetric(horizontal: f32, vertical: f32) -> Self {
251        Self {
252            left: horizontal,
253            right: horizontal,
254            top: vertical,
255            bottom: vertical,
256        }
257    }
258
259    pub fn from_components(left: f32, top: f32, right: f32, bottom: f32) -> Self {
260        Self {
261            left,
262            top,
263            right,
264            bottom,
265        }
266    }
267
268    pub fn is_zero(&self) -> bool {
269        self.left == 0.0 && self.top == 0.0 && self.right == 0.0 && self.bottom == 0.0
270    }
271
272    pub fn horizontal_sum(&self) -> f32 {
273        self.left + self.right
274    }
275
276    pub fn vertical_sum(&self) -> f32 {
277        self.top + self.bottom
278    }
279
280    /// The part of a `size` rect these insets leave, at the leading insets;
281    /// it never shrinks below zero.
282    pub fn inset_rect(&self, size: Size) -> Rect {
283        Rect {
284            x: self.left,
285            y: self.top,
286            width: (size.width - self.horizontal_sum()).max(0.0),
287            height: (size.height - self.vertical_sum()).max(0.0),
288        }
289    }
290}
291
292impl AddAssign for EdgeInsets {
293    fn add_assign(&mut self, rhs: Self) {
294        self.left += rhs.left;
295        self.top += rhs.top;
296        self.right += rhs.right;
297        self.bottom += rhs.bottom;
298    }
299}
300
301#[derive(Clone, Copy, Debug, Default, PartialEq)]
302pub struct CornerRadii {
303    pub top_left: f32,
304    pub top_right: f32,
305    pub bottom_right: f32,
306    pub bottom_left: f32,
307}
308
309impl CornerRadii {
310    pub fn uniform(radius: f32) -> Self {
311        Self {
312            top_left: radius,
313            top_right: radius,
314            bottom_right: radius,
315            bottom_left: radius,
316        }
317    }
318}
319
320#[derive(Clone, Copy, Debug, PartialEq)]
321pub struct RoundedCornerShape {
322    radii: CornerRadii,
323}
324
325impl RoundedCornerShape {
326    pub fn new(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self {
327        Self {
328            radii: CornerRadii {
329                top_left,
330                top_right,
331                bottom_right,
332                bottom_left,
333            },
334        }
335    }
336
337    pub fn uniform(radius: f32) -> Self {
338        Self {
339            radii: CornerRadii::uniform(radius),
340        }
341    }
342
343    pub fn with_radii(radii: CornerRadii) -> Self {
344        Self { radii }
345    }
346
347    pub fn resolve(&self, width: f32, height: f32) -> CornerRadii {
348        let mut resolved = self.radii;
349        let max_width = (width / 2.0).max(0.0);
350        let max_height = (height / 2.0).max(0.0);
351        resolved.top_left = resolved.top_left.clamp(0.0, max_width).min(max_height);
352        resolved.top_right = resolved.top_right.clamp(0.0, max_width).min(max_height);
353        resolved.bottom_right = resolved.bottom_right.clamp(0.0, max_width).min(max_height);
354        resolved.bottom_left = resolved.bottom_left.clamp(0.0, max_width).min(max_height);
355        resolved
356    }
357
358    pub fn radii(&self) -> CornerRadii {
359        self.radii
360    }
361}
362
363#[derive(Clone, Copy, Debug, PartialEq)]
364pub struct TransformOrigin {
365    pub pivot_fraction_x: f32,
366    pub pivot_fraction_y: f32,
367}
368
369impl TransformOrigin {
370    pub const fn new(pivot_fraction_x: f32, pivot_fraction_y: f32) -> Self {
371        Self {
372            pivot_fraction_x,
373            pivot_fraction_y,
374        }
375    }
376
377    pub const CENTER: TransformOrigin = TransformOrigin::new(0.5, 0.5);
378}
379
380impl Default for TransformOrigin {
381    fn default() -> Self {
382        Self::CENTER
383    }
384}
385
386#[derive(Clone, Copy, Debug, Default, PartialEq)]
387pub enum LayerShape {
388    #[default]
389    Rectangle,
390    Rounded(RoundedCornerShape),
391}
392
393#[derive(Clone, Debug, PartialEq)]
394pub struct GraphicsLayer {
395    pub alpha: f32,
396    pub scale: f32,
397    pub scale_x: f32,
398    pub scale_y: f32,
399    pub rotation_x: f32,
400    pub rotation_y: f32,
401    pub rotation_z: f32,
402    pub camera_distance: f32,
403    pub transform_origin: TransformOrigin,
404    pub translation_x: f32,
405    pub translation_y: f32,
406    pub shadow_elevation: f32,
407    pub ambient_shadow_color: Color,
408    pub spot_shadow_color: Color,
409    pub shape: LayerShape,
410    pub clip: bool,
411    pub compositing_strategy: CompositingStrategy,
412    pub blend_mode: BlendMode,
413    pub color_filter: Option<ColorFilter>,
414    pub render_effect: Option<crate::render_effect::RenderEffect>,
415    pub backdrop_effect: Option<crate::render_effect::RenderEffect>,
416}
417
418impl GraphicsLayer {
419    /// The alpha an isolated layer is composited at: an **eight-bit** one,
420    /// truncated.
421    ///
422    /// The platform never composites a layer at a float alpha. HWUI hands an
423    /// isolated `RenderNode` to the rasterizer as
424    /// `canvas->saveLayerAlpha(&bounds, (int)(properties.getAlpha() * 255))`
425    /// (`frameworks/base/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp`,
426    /// `setViewProperties`), and `(int)` truncates — 0.5 composites at 127/255,
427    /// not at 128/255. The fraction below that byte is gone before a single pixel
428    /// is blended, so anything that keeps it lands a level out wherever the byte
429    /// and the float fall on opposite sides of a half.
430    ///
431    /// The sibling branch is a float on purpose: where `getHasOverlappingRendering()`
432    /// is false HWUI takes `*alphaMultiplier = properties.getAlpha()` and folds it
433    /// into each draw without ever making a byte of it. That is what
434    /// `CompositingStrategy::ModulateAlpha` names.
435    ///
436    /// Truncating here and **rounding** in [`Color::srgb_8bit`] is not an
437    /// inconsistency: they are different call sites in the platform. A colour's own
438    /// alpha is snapped by `Color`'s constructor, which adds the half; a layer's
439    /// alpha is snapped by HWUI's cast, which does not. Anything modelling a faded
440    /// layer without allocating one — a canvas drawing a list row's fade by hand,
441    /// say — wants this rule and not the other.
442    pub fn composite_alpha_8bit(alpha: f32) -> f32 {
443        (alpha.clamp(0.0, 1.0) * 255.0).floor() / 255.0
444    }
445}
446
447impl Default for GraphicsLayer {
448    fn default() -> Self {
449        Self {
450            alpha: 1.0,
451            scale: 1.0,
452            scale_x: 1.0,
453            scale_y: 1.0,
454            rotation_x: 0.0,
455            rotation_y: 0.0,
456            rotation_z: 0.0,
457            camera_distance: 8.0,
458            transform_origin: TransformOrigin::CENTER,
459            translation_x: 0.0,
460            translation_y: 0.0,
461            shadow_elevation: 0.0,
462            ambient_shadow_color: Color::BLACK,
463            spot_shadow_color: Color::BLACK,
464            shape: LayerShape::Rectangle,
465            clip: false,
466            compositing_strategy: CompositingStrategy::Auto,
467            blend_mode: BlendMode::SrcOver,
468            color_filter: None,
469            render_effect: None,
470            backdrop_effect: None,
471        }
472    }
473}
474
475/// Blend mode used for draw primitives.
476///
477/// This mirrors Jetpack Compose's blend-mode vocabulary while the renderer
478/// currently guarantees `SrcOver` and `DstOut` behavior.
479#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
480pub enum BlendMode {
481    Clear,
482    Src,
483    Dst,
484    #[default]
485    SrcOver,
486    DstOver,
487    SrcIn,
488    DstIn,
489    SrcOut,
490    DstOut,
491    SrcAtop,
492    DstAtop,
493    Xor,
494    Plus,
495    Modulate,
496    Screen,
497    Overlay,
498    Darken,
499    Lighten,
500    ColorDodge,
501    ColorBurn,
502    HardLight,
503    SoftLight,
504    Difference,
505    Exclusion,
506    Multiply,
507    Hue,
508    Saturation,
509    Color,
510    Luminosity,
511}
512
513/// Controls how a graphics layer is composited into its parent target.
514impl BlendMode {
515    /// Every mode in declaration order, so `mode as u32` indexes it.
516    pub const ALL: [BlendMode; 29] = [
517        BlendMode::Clear,
518        BlendMode::Src,
519        BlendMode::Dst,
520        BlendMode::SrcOver,
521        BlendMode::DstOver,
522        BlendMode::SrcIn,
523        BlendMode::DstIn,
524        BlendMode::SrcOut,
525        BlendMode::DstOut,
526        BlendMode::SrcAtop,
527        BlendMode::DstAtop,
528        BlendMode::Xor,
529        BlendMode::Plus,
530        BlendMode::Modulate,
531        BlendMode::Screen,
532        BlendMode::Overlay,
533        BlendMode::Darken,
534        BlendMode::Lighten,
535        BlendMode::ColorDodge,
536        BlendMode::ColorBurn,
537        BlendMode::HardLight,
538        BlendMode::SoftLight,
539        BlendMode::Difference,
540        BlendMode::Exclusion,
541        BlendMode::Multiply,
542        BlendMode::Hue,
543        BlendMode::Saturation,
544        BlendMode::Color,
545        BlendMode::Luminosity,
546    ];
547}
548
549#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
550pub enum CompositingStrategy {
551    /// Use renderer heuristics (default).
552    #[default]
553    Auto,
554    /// Render this layer to an offscreen target, then composite.
555    Offscreen,
556    /// Multiply alpha on source colors without allocating an offscreen layer.
557    ModulateAlpha,
558}
559
560#[derive(Clone, Debug, PartialEq)]
561pub enum DrawPrimitive {
562    /// Marker emitted by `draw_content()` inside `draw_with_content`.
563    /// This is consumed by the modifier pipeline and never rendered directly.
564    Content,
565    /// Wrapper to associate a draw primitive with a non-default blend mode.
566    Blend {
567        primitive: Box<DrawPrimitive>,
568        blend_mode: BlendMode,
569    },
570    Rect {
571        rect: Rect,
572        brush: Brush,
573        /// `None` fills the rect; `Some` strokes its outline, centered on the
574        /// edge (so it bleeds `width / 2` outside `rect`).
575        stroke: Option<Stroke>,
576    },
577    RoundRect {
578        rect: Rect,
579        brush: Brush,
580        radii: CornerRadii,
581        /// `None` fills the rounded rect; `Some` strokes its outline, centered
582        /// on the edge.
583        stroke: Option<Stroke>,
584    },
585    /// A circular band: a stroked arc, or a filled annular sector / pie wedge.
586    ///
587    /// Angles are radians, `0` = +X, increasing **clockwise** on screen (see
588    /// `crate::stroke` for the full convention).
589    ///
590    /// * `stroke = Some(_)` — the band is `radius ± width/2`, its ends shaped
591    ///   by the stroke cap. `inner_radius` is ignored.
592    /// * `stroke = None` — the band is `inner_radius ..= radius` with flat
593    ///   (butt) radial ends; `inner_radius = 0` is a filled pie wedge.
594    Arc {
595        /// Tight bounding box of the rendered band, caps included. Kept as the
596        /// first field (like every other variant) so bbox/culling/clip logic
597        /// treats an arc exactly like any other primitive.
598        rect: Rect,
599        brush: Brush,
600        center: Point,
601        radius: f32,
602        start_angle: f32,
603        sweep_angle: f32,
604        stroke: Option<Stroke>,
605        /// `> 0` turns a filled wedge into an annular sector.
606        inner_radius: f32,
607    },
608    /// A stroked straight segment from `start` to `end`, its ends shaped by
609    /// the stroke's cap: Compose's `drawLine`.
610    Line {
611        /// The box of the two ends; the stroke reaches half its width past
612        /// it, as a stroked rect's does past its rect.
613        rect: Rect,
614        brush: Brush,
615        start: Point,
616        end: Point,
617        stroke: Stroke,
618    },
619    Image {
620        rect: Rect,
621        image: ImageBitmap,
622        alpha: f32,
623        color_filter: Option<ColorFilter>,
624        sampling: ImageSampling,
625        /// Optional source rectangle in image-pixel coordinates.
626        /// When `None`, the entire image is drawn. When `Some`, only the
627        /// specified sub-region of the source image is sampled.
628        src_rect: Option<Rect>,
629    },
630    /// A laid-out run of text. See [`TextPrimitive`].
631    Text(Box<TextPrimitive>),
632    /// Shadow that requires blur processing. The renderer decides technique
633    /// (GPU blur, CPU approximation, etc.).
634    Shadow(ShadowPrimitive),
635}
636
637/// A run of text, positioned and ready to rasterize.
638///
639/// `rect` is *already resolved*: [`DrawScope::draw_text_at`] measures the
640/// string, applies [`DrawTextStyle::align`] / [`DrawTextStyle::vertical_align`] inside
641/// the requested box, and stores the result here. Renderers therefore lay the
642/// glyphs out from `rect`'s top-left and never re-align — which is what keeps
643/// what [`DrawScope::measure_text`] reported and what lands on screen the same
644/// geometry.
645#[derive(Clone, Debug, PartialEq)]
646pub struct TextPrimitive {
647    /// Tight block box: origin is the top-left of the first line's slot, size
648    /// is the measured size.
649    pub rect: Rect,
650    /// Shared so redrawing an unchanged string each frame clones a pointer
651    /// rather than the characters.
652    pub text: std::rc::Rc<str>,
653    pub style: DrawTextStyle,
654    /// Text is filled with a single color: the glyph atlas path modulates one
655    /// vertex color per glyph. Gradient brushes are resolved to their first
656    /// stop by the draw scope, exactly like [`DrawScope::draw_vector_path`].
657    pub color: Color,
658}
659
660/// Returns a shared `Rc<str>` for `text`, reusing the copy made on an earlier
661/// frame when the content matches.
662///
663/// Apps hand `draw_text*` a `&str` every frame, and a score counter or label
664/// is the same characters frame after frame — without this pool every call
665/// copied them into a fresh `Rc<str>` anyway, defeating the sharing
666/// [`TextPrimitive::text`] exists for. Hits are verified by content, so a hash
667/// collision costs one fresh copy, never the wrong text. The pool clears
668/// itself when full; a live scene re-warms within one frame.
669fn shared_text_str(text: &str) -> Rc<str> {
670    use std::{
671        cell::RefCell,
672        collections::HashMap,
673        hash::{Hash, Hasher},
674    };
675
676    const POOL_CAPACITY: usize = 256;
677    thread_local! {
678        static POOL: RefCell<HashMap<u64, Rc<str>, crate::FxBuildHasher>> =
679            RefCell::new(HashMap::default());
680    }
681
682    let mut hasher = crate::FxHasher::default();
683    text.hash(&mut hasher);
684    let key = hasher.finish();
685
686    POOL.with(|pool| {
687        let mut pool = pool.borrow_mut();
688        if let Some(shared) = pool.get(&key)
689            && &**shared == text
690        {
691            return Rc::clone(shared);
692        }
693        let shared: Rc<str> = Rc::from(text);
694        if pool.len() >= POOL_CAPACITY {
695            pool.clear();
696        }
697        pool.insert(key, Rc::clone(&shared));
698        shared
699    })
700}
701
702/// Describes a shadow to be rendered. Each renderer chooses how to blur.
703impl DrawPrimitive {
704    /// The primitive moved by `dx`, `dy`. Brush geometry is relative to the
705    /// primitive's rect, so it moves along.
706    pub fn translate(self, dx: f32, dy: f32) -> Self {
707        match self {
708            DrawPrimitive::Content => DrawPrimitive::Content,
709            DrawPrimitive::Blend {
710                primitive,
711                blend_mode,
712            } => DrawPrimitive::Blend {
713                primitive: Box::new(primitive.translate(dx, dy)),
714                blend_mode,
715            },
716            DrawPrimitive::Rect {
717                rect,
718                brush,
719                stroke,
720            } => DrawPrimitive::Rect {
721                rect: rect.translate(dx, dy),
722                brush,
723                stroke,
724            },
725            DrawPrimitive::RoundRect {
726                rect,
727                brush,
728                radii,
729                stroke,
730            } => DrawPrimitive::RoundRect {
731                rect: rect.translate(dx, dy),
732                brush,
733                radii,
734                stroke,
735            },
736            DrawPrimitive::Arc {
737                rect,
738                brush,
739                center,
740                radius,
741                start_angle,
742                sweep_angle,
743                stroke,
744                inner_radius,
745            } => DrawPrimitive::Arc {
746                rect: rect.translate(dx, dy),
747                brush,
748                center: Point::new(center.x + dx, center.y + dy),
749                radius,
750                start_angle,
751                sweep_angle,
752                stroke,
753                inner_radius,
754            },
755            DrawPrimitive::Line {
756                rect,
757                brush,
758                start,
759                end,
760                stroke,
761            } => DrawPrimitive::Line {
762                rect: rect.translate(dx, dy),
763                brush,
764                start: Point::new(start.x + dx, start.y + dy),
765                end: Point::new(end.x + dx, end.y + dy),
766                stroke,
767            },
768            DrawPrimitive::Image {
769                rect,
770                image,
771                alpha,
772                color_filter,
773                sampling,
774                src_rect,
775            } => DrawPrimitive::Image {
776                rect: rect.translate(dx, dy),
777                image,
778                alpha,
779                color_filter,
780                sampling,
781                src_rect,
782            },
783            DrawPrimitive::Text(mut text) => {
784                text.rect = text.rect.translate(dx, dy);
785                DrawPrimitive::Text(text)
786            }
787            DrawPrimitive::Shadow(ShadowPrimitive::Drop {
788                shape,
789                cutout,
790                blur_radius,
791                blend_mode,
792            }) => DrawPrimitive::Shadow(ShadowPrimitive::Drop {
793                shape: Box::new(shape.translate(dx, dy)),
794                cutout: cutout.map(|cutout| Box::new(cutout.translate(dx, dy))),
795                blur_radius,
796                blend_mode,
797            }),
798            DrawPrimitive::Shadow(ShadowPrimitive::Inner {
799                fill,
800                cutout,
801                blur_radius,
802                blend_mode,
803                clip_rect,
804            }) => DrawPrimitive::Shadow(ShadowPrimitive::Inner {
805                fill: Box::new(fill.translate(dx, dy)),
806                cutout: Box::new(cutout.translate(dx, dy)),
807                blur_radius,
808                blend_mode,
809                clip_rect: clip_rect.translate(dx, dy),
810            }),
811        }
812    }
813}
814
815#[derive(Clone, Debug, PartialEq)]
816pub enum ShadowPrimitive {
817    /// Drop shadow: render shape behind content, blurred. `cutout` knocks
818    /// the element's own (unoffset) shape out of the silhouette before the
819    /// blur so translucent surfaces never sample their own shadow.
820    Drop {
821        shape: Box<DrawPrimitive>,
822        cutout: Option<Box<DrawPrimitive>>,
823        blur_radius: f32,
824        blend_mode: BlendMode,
825    },
826    /// Inner shadow: render fill + cutout to offscreen, blur, clip to bounds.
827    Inner {
828        fill: Box<DrawPrimitive>,
829        cutout: Box<DrawPrimitive>,
830        blur_radius: f32,
831        blend_mode: BlendMode,
832        /// Element bounds — blurred result must be clipped here.
833        clip_rect: Rect,
834    },
835}
836
837pub trait DrawScope {
838    fn size(&self) -> Size;
839    fn draw_content(&mut self);
840    fn draw_rect(&mut self, brush: Brush);
841    fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode);
842    /// Draws a rectangle at the specified position and size.
843    fn draw_rect_at(&mut self, rect: Rect, brush: Brush);
844    fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode);
845    fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii);
846    fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode);
847    /// Draws a rounded rectangle at the specified position and size.
848    fn draw_round_rect_at(&mut self, rect: Rect, brush: Brush, radii: CornerRadii);
849    fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32);
850    fn draw_circle_blend(
851        &mut self,
852        brush: Brush,
853        center: Point,
854        radius: f32,
855        blend_mode: BlendMode,
856    );
857
858    /// Strokes the outline of the whole scope rect.
859    fn draw_rect_stroked(&mut self, brush: Brush, stroke: Stroke);
860    fn draw_rect_stroked_blend(&mut self, brush: Brush, stroke: Stroke, blend_mode: BlendMode);
861    /// Strokes the outline of `rect`.
862    fn draw_rect_at_stroked(&mut self, rect: Rect, brush: Brush, stroke: Stroke);
863    fn draw_rect_at_stroked_blend(
864        &mut self,
865        rect: Rect,
866        brush: Brush,
867        stroke: Stroke,
868        blend_mode: BlendMode,
869    );
870    /// Strokes the outline of the whole scope rect with rounded corners.
871    fn draw_round_rect_stroked(&mut self, brush: Brush, radii: CornerRadii, stroke: Stroke);
872    fn draw_round_rect_stroked_blend(
873        &mut self,
874        brush: Brush,
875        radii: CornerRadii,
876        stroke: Stroke,
877        blend_mode: BlendMode,
878    );
879    /// Strokes the outline of `rect` with rounded corners.
880    fn draw_round_rect_at_stroked(
881        &mut self,
882        rect: Rect,
883        brush: Brush,
884        radii: CornerRadii,
885        stroke: Stroke,
886    );
887    fn draw_round_rect_at_stroked_blend(
888        &mut self,
889        rect: Rect,
890        brush: Brush,
891        radii: CornerRadii,
892        stroke: Stroke,
893        blend_mode: BlendMode,
894    );
895    /// Strokes a circle outline. Lowers to a stroked rounded rect, so it shares
896    /// the fill pipeline and batches with every other shape.
897    fn draw_circle_stroked(&mut self, brush: Brush, center: Point, radius: f32, stroke: Stroke);
898    fn draw_circle_stroked_blend(
899        &mut self,
900        brush: Brush,
901        center: Point,
902        radius: f32,
903        stroke: Stroke,
904        blend_mode: BlendMode,
905    );
906
907    /// Strokes a circular arc.
908    ///
909    /// Angles are in **radians**, `0` points along **+X**, and increasing
910    /// angles sweep **clockwise on screen** (Cranpose uses y-down device
911    /// coordinates, so this matches `atan2(dy, dx)` and the sweep-gradient
912    /// brush). A negative `sweep_angle` sweeps counter-clockwise; `|sweep| >=
913    /// 2π` draws a closed ring.
914    ///
915    /// The stroke is centered on `radius`, so the band covers
916    /// `radius ± width/2`. [`StrokeCap`](crate::StrokeCap) shapes the two ends.
917    /// Nothing is drawn for a zero sweep, a non-positive width, or non-finite
918    /// input.
919    fn draw_arc(
920        &mut self,
921        brush: Brush,
922        center: Point,
923        radius: f32,
924        start_angle: f32,
925        sweep_angle: f32,
926        stroke: Stroke,
927    );
928    #[expect(clippy::too_many_arguments)]
929    fn draw_arc_blend(
930        &mut self,
931        brush: Brush,
932        center: Point,
933        radius: f32,
934        start_angle: f32,
935        sweep_angle: f32,
936        stroke: Stroke,
937        blend_mode: BlendMode,
938    );
939
940    /// Strokes the straight segment from `start` to `end`, centred on it and
941    /// `stroke.width` wide, its ends shaped by `stroke.cap`: Compose's
942    /// `drawLine(brush, start, end, strokeWidth, cap)`. Nothing is drawn
943    /// for a non-positive width, a non-finite end, or a segment of no length
944    /// between butt caps; round and square caps draw a dot there.
945    fn draw_line(&mut self, brush: Brush, start: Point, end: Point, stroke: Stroke);
946    fn draw_line_blend(
947        &mut self,
948        brush: Brush,
949        start: Point,
950        end: Point,
951        stroke: Stroke,
952        blend_mode: BlendMode,
953    );
954
955    /// Fills or strokes `path` with `brush`: Compose's
956    /// `drawPath(path, brush, style)`. A fill closes every contour and
957    /// rasterizes it as [`draw_vector_path`](Self::draw_vector_path) does. A
958    /// stroke draws each flattened edge as a line on the GPU, capped at an
959    /// open contour's ends and joined at its corners as
960    /// [`crate::for_each_stroke_line`] describes; a dashed stroke draws the
961    /// dashes its effect cuts. Where edges meet, a translucent stroke is
962    /// painted twice.
963    fn draw_path(&mut self, path: &Path, brush: Brush, style: DrawStyle);
964    fn draw_path_blend(
965        &mut self,
966        path: &Path,
967        brush: Brush,
968        style: DrawStyle,
969        blend_mode: BlendMode,
970    );
971
972    /// Fills an annular sector — the region between `inner_radius` and
973    /// `outer_radius`, limited to an angular sweep, with **flat radial ends**.
974    ///
975    /// This is the shape a stroked arc cannot express: its ends are straight
976    /// lines through the center, not caps. `inner_radius = 0` fills a pie
977    /// wedge. Angle convention is identical to [`draw_arc`](Self::draw_arc).
978    /// Nothing is drawn when `inner_radius >= outer_radius`, the sweep is zero,
979    /// or any input is non-finite.
980    fn draw_annular_sector(
981        &mut self,
982        brush: Brush,
983        center: Point,
984        inner_radius: f32,
985        outer_radius: f32,
986        start_angle: f32,
987        sweep_angle: f32,
988    );
989    #[expect(clippy::too_many_arguments)]
990    fn draw_annular_sector_blend(
991        &mut self,
992        brush: Brush,
993        center: Point,
994        inner_radius: f32,
995        outer_radius: f32,
996        start_angle: f32,
997        sweep_angle: f32,
998        blend_mode: BlendMode,
999    );
1000
1001    fn draw_image(&mut self, image: ImageBitmap);
1002    fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode);
1003    fn draw_image_at(
1004        &mut self,
1005        rect: Rect,
1006        image: ImageBitmap,
1007        alpha: f32,
1008        color_filter: Option<ColorFilter>,
1009    );
1010    fn draw_image_at_sampled(
1011        &mut self,
1012        rect: Rect,
1013        image: ImageBitmap,
1014        alpha: f32,
1015        color_filter: Option<ColorFilter>,
1016        sampling: ImageSampling,
1017    );
1018    fn draw_image_at_blend(
1019        &mut self,
1020        rect: Rect,
1021        image: ImageBitmap,
1022        alpha: f32,
1023        color_filter: Option<ColorFilter>,
1024        blend_mode: BlendMode,
1025    );
1026    /// Draws a sub-region of an image. `src_rect` is in image-pixel
1027    /// coordinates; `dst_rect` is in scope coordinates.
1028    fn draw_image_src(
1029        &mut self,
1030        image: ImageBitmap,
1031        src_rect: Rect,
1032        dst_rect: Rect,
1033        alpha: f32,
1034        color_filter: Option<ColorFilter>,
1035    );
1036    fn draw_image_src_sampled(
1037        &mut self,
1038        image: ImageBitmap,
1039        src_rect: Rect,
1040        dst_rect: Rect,
1041        alpha: f32,
1042        color_filter: Option<ColorFilter>,
1043        sampling: ImageSampling,
1044    );
1045    fn draw_image_src_blend(
1046        &mut self,
1047        image: ImageBitmap,
1048        src_rect: Rect,
1049        dst_rect: Rect,
1050        alpha: f32,
1051        color_filter: Option<ColorFilter>,
1052        blend_mode: BlendMode,
1053    );
1054    /// Fills a parsed SVG path in scope coordinates (path units are dp).
1055    ///
1056    /// The fill is rasterized on the CPU into a supersampled, anti-aliased
1057    /// bitmap covering the path bounds and drawn as an image primitive, so
1058    /// it works on every render backend. Parse the path once with
1059    /// [`crate::VectorPath::parse`] and redraw it per frame. Solid brushes
1060    /// are honored exactly; gradient brushes currently fall back to their
1061    /// first stop color.
1062    fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush);
1063    /// Parses SVG path data (the `d` attribute syntax: `M/m L/l H/h V/v
1064    /// C/c S/s Q/q T/t A/a Z/z`) and fills it. Invalid path data draws
1065    /// nothing. Prefer [`crate::VectorPath::parse`] +
1066    /// [`draw_vector_path`](Self::draw_vector_path) to avoid re-parsing
1067    /// and to surface parse errors.
1068    fn draw_svg_path(&mut self, d: &str, brush: Brush) {
1069        if let Ok(path) = crate::VectorPath::parse(d) {
1070            self.draw_vector_path(&path, brush);
1071        }
1072    }
1073
1074    /// The block size, line height and first baseline `text` would occupy in
1075    /// `style`.
1076    ///
1077    /// Free to call repeatedly: the underlying text stack caches metrics on
1078    /// `(text, style)`, so a game can measure every label every frame to center
1079    /// it without touching a font file more than once.
1080    fn measure_text(&self, text: &str, style: &DrawTextStyle) -> TextMeasurement;
1081
1082    /// Draws `text` inside the whole scope rect, positioned by
1083    /// [`DrawTextStyle::align`] and [`DrawTextStyle::vertical_align`].
1084    fn draw_text(&mut self, brush: Brush, text: &str, style: &DrawTextStyle) {
1085        self.draw_text_at(Rect::from_size(self.size()), brush, text, style);
1086    }
1087
1088    /// Draws `text` inside `rect`, positioned by [`DrawTextStyle::align`] and
1089    /// [`DrawTextStyle::vertical_align`].
1090    ///
1091    /// The glyphs are *not* clipped to `rect` — it is an alignment box, not a
1092    /// viewport. A `rect` narrower than the measured text overflows in the
1093    /// direction the alignment implies; clip the layer if that matters.
1094    fn draw_text_at(&mut self, rect: Rect, brush: Brush, text: &str, style: &DrawTextStyle);
1095
1096    /// Draws `text` with the top-left corner of its block at `top_left`.
1097    ///
1098    /// Alignment is a no-op here because the box is the measurement — this is
1099    /// the "I already know where it goes" form, and the one to pair with
1100    /// [`measure_text`](Self::measure_text) for hand-rolled centering.
1101    fn draw_text_from(&mut self, top_left: Point, brush: Brush, text: &str, style: &DrawTextStyle) {
1102        if text.is_empty() {
1103            return;
1104        }
1105        let measurement = self.measure_text(text, style);
1106        self.draw_text_at(
1107            Rect::from_origin_size(top_left, measurement.size),
1108            brush,
1109            text,
1110            &DrawTextStyle {
1111                align: TextAlign::Left,
1112                vertical_align: TextVerticalAlign::Top,
1113                ..style.clone()
1114            },
1115        );
1116    }
1117
1118    fn into_primitives(self) -> Vec<DrawPrimitive>;
1119}
1120
1121/// Resolves the top-left corner a text block of `measurement` gets when it is
1122/// aligned inside `rect`.
1123///
1124/// Split out so the placement rule is stated once and can be unit-tested
1125/// against the measurement it is derived from.
1126pub fn align_text_block(rect: Rect, measurement: TextMeasurement, style: &DrawTextStyle) -> Point {
1127    let x = match style.align {
1128        TextAlign::Left => rect.x,
1129        TextAlign::Center => rect.x + (rect.width - measurement.size.width) * 0.5,
1130        TextAlign::Right => rect.x + rect.width - measurement.size.width,
1131    };
1132    let y = match style.vertical_align {
1133        TextVerticalAlign::Top => rect.y,
1134        TextVerticalAlign::Center => rect.y + (rect.height - measurement.size.height) * 0.5,
1135        TextVerticalAlign::Bottom => rect.y + rect.height - measurement.size.height,
1136        TextVerticalAlign::Baseline => rect.y - measurement.first_baseline,
1137    };
1138    Point::new(x, y)
1139}
1140
1141#[derive(Default)]
1142pub struct DrawScopeDefault {
1143    size: Size,
1144    recording: CommandRecorder,
1145    text_measurer: Option<Rc<dyn DrawTextMeasurer>>,
1146}
1147
1148const RECORDED_PRIMITIVE_COUNTS_LIMIT: usize = 64;
1149
1150thread_local! {
1151    static RECORDED_PRIMITIVE_COUNTS: std::cell::RefCell<
1152        std::collections::HashMap<(u32, u32), usize, crate::FxBuildHasher>,
1153    > = std::cell::RefCell::new(std::collections::HashMap::default());
1154}
1155
1156fn recorded_primitive_capacity(size: Size) -> usize {
1157    RECORDED_PRIMITIVE_COUNTS.with(|counts| {
1158        counts
1159            .borrow()
1160            .get(&(size.width.to_bits(), size.height.to_bits()))
1161            .copied()
1162            .unwrap_or(0)
1163    })
1164}
1165
1166fn note_recorded_primitive_count(size: Size, count: usize) {
1167    RECORDED_PRIMITIVE_COUNTS.with(|counts| {
1168        let mut counts = counts.borrow_mut();
1169        if counts.len() >= RECORDED_PRIMITIVE_COUNTS_LIMIT {
1170            counts.clear();
1171        }
1172        counts.insert((size.width.to_bits(), size.height.to_bits()), count);
1173    });
1174}
1175
1176impl DrawScopeDefault {
1177    pub fn new(size: Size) -> Self {
1178        Self::with_storage(size, None, CommandRecording::default())
1179    }
1180
1181    /// A scope that measures text with the app's fonts.
1182    ///
1183    /// The framework calls this for every draw closure it runs; `new` exists
1184    /// for callers that never draw text.
1185    pub fn with_text_measurer(size: Size, text_measurer: Rc<dyn DrawTextMeasurer>) -> Self {
1186        Self::with_storage(size, Some(text_measurer), CommandRecording::default())
1187    }
1188
1189    /// A scope recording into `storage`, a recording the caller kept from an
1190    /// earlier frame so its buffers keep the capacity they earned.
1191    pub fn with_text_measurer_reusing(
1192        size: Size,
1193        text_measurer: Rc<dyn DrawTextMeasurer>,
1194        storage: CommandRecording,
1195    ) -> Self {
1196        Self::with_storage(size, Some(text_measurer), storage)
1197    }
1198
1199    fn with_storage(
1200        size: Size,
1201        text_measurer: Option<Rc<dyn DrawTextMeasurer>>,
1202        recording: CommandRecording,
1203    ) -> Self {
1204        let mut recording = CommandRecorder::reusing(recording);
1205        recording.reserve_shapes(recorded_primitive_capacity(size));
1206        Self {
1207            size,
1208            recording,
1209            text_measurer,
1210        }
1211    }
1212
1213    /// How many `draw_content` markers this scope has recorded.
1214    pub fn content_marker_count(&self) -> u32 {
1215        self.recording.content_markers()
1216    }
1217
1218    /// Runs `draw` in this scope's rect shrunk by `insets`, where a draw
1219    /// modifier placed after layout padding draws: `draw` sees the inner
1220    /// size, and what it records lands offset by the leading insets.
1221    pub fn inset(&mut self, insets: EdgeInsets, draw: impl FnOnce(&mut DrawScopeDefault)) {
1222        let inner = insets.inset_rect(self.size);
1223        let mut scope = Self::with_storage(
1224            Size {
1225                width: inner.width,
1226                height: inner.height,
1227            },
1228            self.text_measurer.clone(),
1229            CommandRecording::default(),
1230        );
1231        draw(&mut scope);
1232        let primitives = scope.into_primitives();
1233        self.push_recorded(
1234            primitives
1235                .into_iter()
1236                .map(|primitive| primitive.translate(inner.x, inner.y)),
1237        );
1238    }
1239
1240    /// Records primitives already built, as if each had been drawn here.
1241    pub fn push_recorded(&mut self, primitives: impl IntoIterator<Item = DrawPrimitive>) {
1242        for primitive in primitives {
1243            self.recording.push_primitive(primitive);
1244        }
1245    }
1246
1247    /// The recording, in the storage it was recorded into, so the caller
1248    /// can lend it to the same command's next recording.
1249    pub fn finish(self) -> CommandRecording {
1250        note_recorded_primitive_count(self.size, self.recording.len());
1251        self.recording.finish()
1252    }
1253
1254    fn push_blended_primitive(&mut self, primitive: DrawPrimitive, blend_mode: BlendMode) {
1255        if blend_mode != BlendMode::SrcOver {
1256            self.recording.push_other(DrawPrimitive::Blend {
1257                primitive: Box::new(primitive),
1258                blend_mode,
1259            });
1260            return;
1261        }
1262        self.recording.push_other(primitive);
1263    }
1264
1265    #[expect(clippy::too_many_arguments)]
1266    #[inline]
1267    fn push_arc(
1268        &mut self,
1269        brush: Brush,
1270        center: Point,
1271        radius: f32,
1272        start_angle: f32,
1273        sweep_angle: f32,
1274        stroke: Option<Stroke>,
1275        inner_radius: f32,
1276        blend_mode: BlendMode,
1277    ) {
1278        let args = ArcRecordArgs {
1279            brush: &brush,
1280            center,
1281            radius,
1282            start_angle,
1283            sweep_angle,
1284            stroke,
1285            inner_radius,
1286            blend_mode,
1287        };
1288        let geometry = normalized_band(&args);
1289        if geometry.is_degenerate() {
1290            return;
1291        }
1292        self.recording.push_scope_arc(&args, &geometry);
1293    }
1294
1295    /// Rasterizes the fill of `path` into a cached coverage image and
1296    /// records it, blended by `blend_mode`.
1297    fn push_vector_path(&mut self, path: &crate::VectorPath, brush: &Brush, blend_mode: BlendMode) {
1298        const SUPERSAMPLE: f32 = 2.0;
1299        const MAX_MASK_PIXELS: f32 = 4096.0;
1300
1301        if path.is_empty() {
1302            return;
1303        }
1304        let bounds = path.bounds();
1305        if bounds.width <= 0.0 || bounds.height <= 0.0 {
1306            return;
1307        }
1308
1309        let color = match brush {
1310            Brush::Solid(color) => *color,
1311            Brush::LinearGradient { colors, .. }
1312            | Brush::RadialGradient { colors, .. }
1313            | Brush::SweepGradient { colors, .. } => match colors.first() {
1314                Some(color) => *color,
1315                None => return,
1316            },
1317        };
1318        if color.3 <= 0.0 {
1319            return;
1320        }
1321
1322        let origin = Point::new(bounds.x.floor() - 1.0, bounds.y.floor() - 1.0);
1323        let rect_width = (bounds.x + bounds.width).ceil() - origin.x + 1.0;
1324        let rect_height = (bounds.y + bounds.height).ceil() - origin.y + 1.0;
1325        let mask_width = (rect_width * SUPERSAMPLE)
1326            .ceil()
1327            .clamp(1.0, MAX_MASK_PIXELS) as usize;
1328        let mask_height = (rect_height * SUPERSAMPLE)
1329            .ceil()
1330            .clamp(1.0, MAX_MASK_PIXELS) as usize;
1331
1332        let red = (color.0.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
1333        let green = (color.1.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
1334        let blue = (color.2.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
1335        let alpha = color.3.clamp(0.0, 1.0);
1336        let key = vector_path_mask_key(
1337            path,
1338            origin,
1339            (mask_width, mask_height),
1340            [red, green, blue],
1341            alpha,
1342        );
1343        let cached = vector_path_mask_cache_get(key);
1344        let image = match cached {
1345            Some(image) => image,
1346            None => {
1347                let mut mask = path.coverage_mask(mask_width, mask_height, origin, SUPERSAMPLE);
1348                for coverage in &mut mask {
1349                    *coverage = (alpha * *coverage as f32 + 0.5) as u8;
1350                }
1351                let Ok(image) = ImageBitmap::from_alpha8(
1352                    mask_width as u32,
1353                    mask_height as u32,
1354                    [red, green, blue],
1355                    mask,
1356                ) else {
1357                    return;
1358                };
1359                vector_path_mask_cache_put(key, image.clone());
1360                image
1361            }
1362        };
1363
1364        let image = DrawPrimitive::Image {
1365            rect: Rect {
1366                x: origin.x,
1367                y: origin.y,
1368                width: rect_width,
1369                height: rect_height,
1370            },
1371            image,
1372            alpha: 1.0,
1373            color_filter: None,
1374            sampling: ImageSampling::Linear,
1375            src_rect: None,
1376        };
1377        self.recording
1378            .push_other(if blend_mode == BlendMode::SrcOver {
1379                image
1380            } else {
1381                DrawPrimitive::Blend {
1382                    primitive: Box::new(image),
1383                    blend_mode,
1384                }
1385            });
1386    }
1387}
1388
1389impl DrawScope for DrawScopeDefault {
1390    fn size(&self) -> Size {
1391        self.size
1392    }
1393
1394    fn draw_content(&mut self) {
1395        self.recording.push_content();
1396    }
1397
1398    fn draw_rect(&mut self, brush: Brush) {
1399        self.draw_rect_blend(brush, BlendMode::SrcOver);
1400    }
1401
1402    fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode) {
1403        self.recording
1404            .push_rect(Rect::from_size(self.size), &brush, None, blend_mode);
1405    }
1406
1407    fn draw_rect_at(&mut self, rect: Rect, brush: Brush) {
1408        self.draw_rect_at_blend(rect, brush, BlendMode::SrcOver);
1409    }
1410
1411    fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode) {
1412        self.recording.push_rect(rect, &brush, None, blend_mode);
1413    }
1414
1415    fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii) {
1416        self.draw_round_rect_blend(brush, radii, BlendMode::SrcOver);
1417    }
1418
1419    fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode) {
1420        self.recording
1421            .push_round_rect(Rect::from_size(self.size), &brush, radii, None, blend_mode);
1422    }
1423
1424    fn draw_round_rect_at(&mut self, rect: Rect, brush: Brush, radii: CornerRadii) {
1425        self.recording
1426            .push_round_rect(rect, &brush, radii, None, BlendMode::SrcOver);
1427    }
1428
1429    fn draw_rect_stroked(&mut self, brush: Brush, stroke: Stroke) {
1430        self.draw_rect_stroked_blend(brush, stroke, BlendMode::SrcOver);
1431    }
1432
1433    fn draw_rect_stroked_blend(&mut self, brush: Brush, stroke: Stroke, blend_mode: BlendMode) {
1434        self.draw_rect_at_stroked_blend(Rect::from_size(self.size), brush, stroke, blend_mode);
1435    }
1436
1437    fn draw_rect_at_stroked(&mut self, rect: Rect, brush: Brush, stroke: Stroke) {
1438        self.draw_rect_at_stroked_blend(rect, brush, stroke, BlendMode::SrcOver);
1439    }
1440
1441    fn draw_rect_at_stroked_blend(
1442        &mut self,
1443        rect: Rect,
1444        brush: Brush,
1445        stroke: Stroke,
1446        blend_mode: BlendMode,
1447    ) {
1448        if !stroke.is_visible() {
1449            return;
1450        }
1451        self.recording
1452            .push_rect(rect, &brush, Some(stroke), blend_mode);
1453    }
1454
1455    fn draw_round_rect_stroked(&mut self, brush: Brush, radii: CornerRadii, stroke: Stroke) {
1456        self.draw_round_rect_stroked_blend(brush, radii, stroke, BlendMode::SrcOver);
1457    }
1458
1459    fn draw_round_rect_stroked_blend(
1460        &mut self,
1461        brush: Brush,
1462        radii: CornerRadii,
1463        stroke: Stroke,
1464        blend_mode: BlendMode,
1465    ) {
1466        self.draw_round_rect_at_stroked_blend(
1467            Rect::from_size(self.size),
1468            brush,
1469            radii,
1470            stroke,
1471            blend_mode,
1472        );
1473    }
1474
1475    fn draw_round_rect_at_stroked(
1476        &mut self,
1477        rect: Rect,
1478        brush: Brush,
1479        radii: CornerRadii,
1480        stroke: Stroke,
1481    ) {
1482        self.draw_round_rect_at_stroked_blend(rect, brush, radii, stroke, BlendMode::SrcOver);
1483    }
1484
1485    fn draw_round_rect_at_stroked_blend(
1486        &mut self,
1487        rect: Rect,
1488        brush: Brush,
1489        radii: CornerRadii,
1490        stroke: Stroke,
1491        blend_mode: BlendMode,
1492    ) {
1493        if !stroke.is_visible() {
1494            return;
1495        }
1496        self.recording
1497            .push_round_rect(rect, &brush, radii, Some(stroke), blend_mode);
1498    }
1499
1500    fn draw_circle_stroked(&mut self, brush: Brush, center: Point, radius: f32, stroke: Stroke) {
1501        self.draw_circle_stroked_blend(brush, center, radius, stroke, BlendMode::SrcOver);
1502    }
1503
1504    fn draw_circle_stroked_blend(
1505        &mut self,
1506        brush: Brush,
1507        center: Point,
1508        radius: f32,
1509        stroke: Stroke,
1510        blend_mode: BlendMode,
1511    ) {
1512        if !stroke.is_visible() || !radius.is_finite() {
1513            return;
1514        }
1515        let radius = radius.max(0.0);
1516        let diameter = radius * 2.0;
1517        self.draw_round_rect_at_stroked_blend(
1518            Rect {
1519                x: center.x - radius,
1520                y: center.y - radius,
1521                width: diameter,
1522                height: diameter,
1523            },
1524            brush,
1525            CornerRadii::uniform(radius),
1526            stroke,
1527            blend_mode,
1528        );
1529    }
1530
1531    fn draw_arc(
1532        &mut self,
1533        brush: Brush,
1534        center: Point,
1535        radius: f32,
1536        start_angle: f32,
1537        sweep_angle: f32,
1538        stroke: Stroke,
1539    ) {
1540        self.draw_arc_blend(
1541            brush,
1542            center,
1543            radius,
1544            start_angle,
1545            sweep_angle,
1546            stroke,
1547            BlendMode::SrcOver,
1548        );
1549    }
1550
1551    fn draw_arc_blend(
1552        &mut self,
1553        brush: Brush,
1554        center: Point,
1555        radius: f32,
1556        start_angle: f32,
1557        sweep_angle: f32,
1558        stroke: Stroke,
1559        blend_mode: BlendMode,
1560    ) {
1561        if !stroke.is_visible() {
1562            return;
1563        }
1564        self.push_arc(
1565            brush,
1566            center,
1567            radius,
1568            start_angle,
1569            sweep_angle,
1570            Some(stroke),
1571            0.0,
1572            blend_mode,
1573        );
1574    }
1575
1576    fn draw_line(&mut self, brush: Brush, start: Point, end: Point, stroke: Stroke) {
1577        self.draw_line_blend(brush, start, end, stroke, BlendMode::SrcOver);
1578    }
1579
1580    fn draw_line_blend(
1581        &mut self,
1582        brush: Brush,
1583        start: Point,
1584        end: Point,
1585        stroke: Stroke,
1586        blend_mode: BlendMode,
1587    ) {
1588        let line = LineGeometry::new(start, end, stroke);
1589        if line.is_degenerate() {
1590            return;
1591        }
1592        self.recording.push_line(&line, &brush, stroke, blend_mode);
1593    }
1594
1595    fn draw_path(&mut self, path: &Path, brush: Brush, style: DrawStyle) {
1596        self.draw_path_blend(path, brush, style, BlendMode::SrcOver);
1597    }
1598
1599    fn draw_path_blend(
1600        &mut self,
1601        path: &Path,
1602        brush: Brush,
1603        style: DrawStyle,
1604        blend_mode: BlendMode,
1605    ) {
1606        let (stroke, dash) = match style {
1607            DrawStyle::Fill => {
1608                self.push_vector_path(
1609                    &path.to_vector_path(crate::PathFillRule::NonZero),
1610                    &brush,
1611                    blend_mode,
1612                );
1613                return;
1614            }
1615            DrawStyle::Stroke(stroke) => (stroke, None),
1616            DrawStyle::DashedStroke(stroke, dash) => (stroke, Some(dash)),
1617        };
1618        if !stroke.is_visible() {
1619            return;
1620        }
1621        let recording = &mut self.recording;
1622        let mut push = |line: LineGeometry| {
1623            if !line.is_degenerate() {
1624                let stroke = Stroke {
1625                    cap: line.cap,
1626                    ..stroke
1627                };
1628                recording.push_line(&line, &brush, stroke, blend_mode);
1629            }
1630        };
1631        for contour in path.contours() {
1632            let Some(dash) = dash else {
1633                crate::for_each_stroke_line(&contour.points, contour.closed, stroke, &mut push);
1634                continue;
1635            };
1636            let closing = contour
1637                .closed
1638                .then(|| contour.points.first().copied())
1639                .flatten();
1640            let points: std::borrow::Cow<'_, [Point]> = match closing {
1641                Some(first) => {
1642                    std::borrow::Cow::Owned(contour.points.iter().copied().chain([first]).collect())
1643                }
1644                None => std::borrow::Cow::Borrowed(&contour.points),
1645            };
1646            dash.for_each_dash(&points, |piece| {
1647                crate::for_each_stroke_line(piece, false, stroke, &mut push);
1648            });
1649        }
1650    }
1651
1652    fn draw_annular_sector(
1653        &mut self,
1654        brush: Brush,
1655        center: Point,
1656        inner_radius: f32,
1657        outer_radius: f32,
1658        start_angle: f32,
1659        sweep_angle: f32,
1660    ) {
1661        self.draw_annular_sector_blend(
1662            brush,
1663            center,
1664            inner_radius,
1665            outer_radius,
1666            start_angle,
1667            sweep_angle,
1668            BlendMode::SrcOver,
1669        );
1670    }
1671
1672    fn draw_annular_sector_blend(
1673        &mut self,
1674        brush: Brush,
1675        center: Point,
1676        inner_radius: f32,
1677        outer_radius: f32,
1678        start_angle: f32,
1679        sweep_angle: f32,
1680        blend_mode: BlendMode,
1681    ) {
1682        self.push_arc(
1683            brush,
1684            center,
1685            outer_radius,
1686            start_angle,
1687            sweep_angle,
1688            None,
1689            inner_radius,
1690            blend_mode,
1691        );
1692    }
1693
1694    fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32) {
1695        self.draw_circle_blend(brush, center, radius, BlendMode::SrcOver);
1696    }
1697
1698    fn draw_circle_blend(
1699        &mut self,
1700        brush: Brush,
1701        center: Point,
1702        radius: f32,
1703        blend_mode: BlendMode,
1704    ) {
1705        let radius = radius.max(0.0);
1706        let diameter = radius * 2.0;
1707        self.recording.push_round_rect(
1708            Rect {
1709                x: center.x - radius,
1710                y: center.y - radius,
1711                width: diameter,
1712                height: diameter,
1713            },
1714            &brush,
1715            CornerRadii::uniform(radius),
1716            None,
1717            blend_mode,
1718        );
1719    }
1720
1721    fn draw_image(&mut self, image: ImageBitmap) {
1722        self.draw_image_blend(image, BlendMode::SrcOver);
1723    }
1724
1725    fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode) {
1726        self.push_blended_primitive(
1727            DrawPrimitive::Image {
1728                rect: Rect::from_size(self.size),
1729                image,
1730                alpha: 1.0,
1731                color_filter: None,
1732                sampling: ImageSampling::Nearest,
1733                src_rect: None,
1734            },
1735            blend_mode,
1736        );
1737    }
1738
1739    fn draw_image_at(
1740        &mut self,
1741        rect: Rect,
1742        image: ImageBitmap,
1743        alpha: f32,
1744        color_filter: Option<ColorFilter>,
1745    ) {
1746        self.draw_image_at_sampled(rect, image, alpha, color_filter, ImageSampling::Nearest);
1747    }
1748
1749    fn draw_image_at_sampled(
1750        &mut self,
1751        rect: Rect,
1752        image: ImageBitmap,
1753        alpha: f32,
1754        color_filter: Option<ColorFilter>,
1755        sampling: ImageSampling,
1756    ) {
1757        self.push_blended_primitive(
1758            DrawPrimitive::Image {
1759                rect,
1760                image,
1761                alpha: alpha.clamp(0.0, 1.0),
1762                color_filter,
1763                sampling,
1764                src_rect: None,
1765            },
1766            BlendMode::SrcOver,
1767        );
1768    }
1769
1770    fn draw_image_at_blend(
1771        &mut self,
1772        rect: Rect,
1773        image: ImageBitmap,
1774        alpha: f32,
1775        color_filter: Option<ColorFilter>,
1776        blend_mode: BlendMode,
1777    ) {
1778        self.push_blended_primitive(
1779            DrawPrimitive::Image {
1780                rect,
1781                image,
1782                alpha: alpha.clamp(0.0, 1.0),
1783                color_filter,
1784                sampling: ImageSampling::Nearest,
1785                src_rect: None,
1786            },
1787            blend_mode,
1788        );
1789    }
1790
1791    fn draw_image_src(
1792        &mut self,
1793        image: ImageBitmap,
1794        src_rect: Rect,
1795        dst_rect: Rect,
1796        alpha: f32,
1797        color_filter: Option<ColorFilter>,
1798    ) {
1799        self.draw_image_src_blend(
1800            image,
1801            src_rect,
1802            dst_rect,
1803            alpha,
1804            color_filter,
1805            BlendMode::SrcOver,
1806        );
1807    }
1808
1809    fn draw_image_src_sampled(
1810        &mut self,
1811        image: ImageBitmap,
1812        src_rect: Rect,
1813        dst_rect: Rect,
1814        alpha: f32,
1815        color_filter: Option<ColorFilter>,
1816        sampling: ImageSampling,
1817    ) {
1818        self.push_blended_primitive(
1819            DrawPrimitive::Image {
1820                rect: dst_rect,
1821                image,
1822                alpha: alpha.clamp(0.0, 1.0),
1823                color_filter,
1824                sampling,
1825                src_rect: Some(src_rect),
1826            },
1827            BlendMode::SrcOver,
1828        );
1829    }
1830
1831    fn draw_image_src_blend(
1832        &mut self,
1833        image: ImageBitmap,
1834        src_rect: Rect,
1835        dst_rect: Rect,
1836        alpha: f32,
1837        color_filter: Option<ColorFilter>,
1838        blend_mode: BlendMode,
1839    ) {
1840        self.push_blended_primitive(
1841            DrawPrimitive::Image {
1842                rect: dst_rect,
1843                image,
1844                alpha: alpha.clamp(0.0, 1.0),
1845                color_filter,
1846                sampling: ImageSampling::Nearest,
1847                src_rect: Some(src_rect),
1848            },
1849            blend_mode,
1850        );
1851    }
1852
1853    fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush) {
1854        self.push_vector_path(path, &brush, BlendMode::SrcOver);
1855    }
1856
1857    fn measure_text(&self, text: &str, style: &DrawTextStyle) -> TextMeasurement {
1858        match &self.text_measurer {
1859            Some(measurer) => measurer.measure_text(text, style),
1860            None => estimate_text_measurement(text, style),
1861        }
1862    }
1863
1864    fn draw_text_at(&mut self, rect: Rect, brush: Brush, text: &str, style: &DrawTextStyle) {
1865        if text.is_empty() {
1866            return;
1867        }
1868        let Some(color) = solid_fill_color(&brush) else {
1869            return;
1870        };
1871        if color.3 <= 0.0 {
1872            return;
1873        }
1874        let measurement = self.measure_text(text, style);
1875        if !(measurement.size.width > 0.0 && measurement.size.height > 0.0) {
1876            return;
1877        }
1878        let origin = align_text_block(rect, measurement, style);
1879        if !origin.x.is_finite() || !origin.y.is_finite() {
1880            return;
1881        }
1882        self.recording
1883            .push_other(DrawPrimitive::Text(Box::new(TextPrimitive {
1884                rect: Rect::from_origin_size(origin, measurement.size),
1885                text: shared_text_str(text),
1886                style: style.clone(),
1887                color,
1888            })));
1889    }
1890
1891    fn into_primitives(self) -> Vec<DrawPrimitive> {
1892        self.finish().into_primitives_with_markers()
1893    }
1894}
1895
1896/// The single color a brush paints with, or its first stop for a gradient.
1897///
1898/// Text is filled per glyph from one vertex color, so a gradient cannot be
1899/// honored; this mirrors the fallback [`DrawScope::draw_vector_path`] documents.
1900fn solid_fill_color(brush: &Brush) -> Option<Color> {
1901    match brush {
1902        Brush::Solid(color) => Some(*color),
1903        Brush::LinearGradient { colors, .. }
1904        | Brush::RadialGradient { colors, .. }
1905        | Brush::SweepGradient { colors, .. } => colors.first().copied(),
1906    }
1907}
1908
1909#[cfg(test)]
1910#[path = "tests/geometry_tests.rs"]
1911mod tests;