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