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