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>, crate::FxBuildHasher>> =
669            RefCell::new(HashMap::default());
670    }
671
672    let mut hasher = crate::FxHasher::default();
673    text.hash(&mut hasher);
674    let key = hasher.finish();
675
676    POOL.with(|pool| {
677        let mut pool = pool.borrow_mut();
678        if let Some(shared) = pool.get(&key)
679            && &**shared == text
680        {
681            return Rc::clone(shared);
682        }
683        let shared: Rc<str> = Rc::from(text);
684        if pool.len() >= POOL_CAPACITY {
685            pool.clear();
686        }
687        pool.insert(key, Rc::clone(&shared));
688        shared
689    })
690}
691
692/// Describes a shadow to be rendered. Each renderer chooses how to blur.
693impl DrawPrimitive {
694    /// The primitive moved by `dx`, `dy`. Brush geometry is relative to the
695    /// primitive's rect, so it moves along.
696    pub fn translate(self, dx: f32, dy: f32) -> Self {
697        match self {
698            DrawPrimitive::Content => DrawPrimitive::Content,
699            DrawPrimitive::Blend {
700                primitive,
701                blend_mode,
702            } => DrawPrimitive::Blend {
703                primitive: Box::new(primitive.translate(dx, dy)),
704                blend_mode,
705            },
706            DrawPrimitive::Rect {
707                rect,
708                brush,
709                stroke,
710            } => DrawPrimitive::Rect {
711                rect: rect.translate(dx, dy),
712                brush,
713                stroke,
714            },
715            DrawPrimitive::RoundRect {
716                rect,
717                brush,
718                radii,
719                stroke,
720            } => DrawPrimitive::RoundRect {
721                rect: rect.translate(dx, dy),
722                brush,
723                radii,
724                stroke,
725            },
726            DrawPrimitive::Arc {
727                rect,
728                brush,
729                center,
730                radius,
731                start_angle,
732                sweep_angle,
733                stroke,
734                inner_radius,
735            } => DrawPrimitive::Arc {
736                rect: rect.translate(dx, dy),
737                brush,
738                center: Point::new(center.x + dx, center.y + dy),
739                radius,
740                start_angle,
741                sweep_angle,
742                stroke,
743                inner_radius,
744            },
745            DrawPrimitive::Image {
746                rect,
747                image,
748                alpha,
749                color_filter,
750                sampling,
751                src_rect,
752            } => DrawPrimitive::Image {
753                rect: rect.translate(dx, dy),
754                image,
755                alpha,
756                color_filter,
757                sampling,
758                src_rect,
759            },
760            DrawPrimitive::Text(mut text) => {
761                text.rect = text.rect.translate(dx, dy);
762                DrawPrimitive::Text(text)
763            }
764            DrawPrimitive::Shadow(ShadowPrimitive::Drop {
765                shape,
766                cutout,
767                blur_radius,
768                blend_mode,
769            }) => DrawPrimitive::Shadow(ShadowPrimitive::Drop {
770                shape: Box::new(shape.translate(dx, dy)),
771                cutout: cutout.map(|cutout| Box::new(cutout.translate(dx, dy))),
772                blur_radius,
773                blend_mode,
774            }),
775            DrawPrimitive::Shadow(ShadowPrimitive::Inner {
776                fill,
777                cutout,
778                blur_radius,
779                blend_mode,
780                clip_rect,
781            }) => DrawPrimitive::Shadow(ShadowPrimitive::Inner {
782                fill: Box::new(fill.translate(dx, dy)),
783                cutout: Box::new(cutout.translate(dx, dy)),
784                blur_radius,
785                blend_mode,
786                clip_rect: clip_rect.translate(dx, dy),
787            }),
788        }
789    }
790}
791
792#[derive(Clone, Debug, PartialEq)]
793pub enum ShadowPrimitive {
794    /// Drop shadow: render shape behind content, blurred. `cutout` knocks
795    /// the element's own (unoffset) shape out of the silhouette before the
796    /// blur so translucent surfaces never sample their own shadow.
797    Drop {
798        shape: Box<DrawPrimitive>,
799        cutout: Option<Box<DrawPrimitive>>,
800        blur_radius: f32,
801        blend_mode: BlendMode,
802    },
803    /// Inner shadow: render fill + cutout to offscreen, blur, clip to bounds.
804    Inner {
805        fill: Box<DrawPrimitive>,
806        cutout: Box<DrawPrimitive>,
807        blur_radius: f32,
808        blend_mode: BlendMode,
809        /// Element bounds — blurred result must be clipped here.
810        clip_rect: Rect,
811    },
812}
813
814pub trait DrawScope {
815    fn size(&self) -> Size;
816    fn draw_content(&mut self);
817    fn draw_rect(&mut self, brush: Brush);
818    fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode);
819    /// Draws a rectangle at the specified position and size.
820    fn draw_rect_at(&mut self, rect: Rect, brush: Brush);
821    fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode);
822    fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii);
823    fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode);
824    /// Draws a rounded rectangle at the specified position and size.
825    fn draw_round_rect_at(&mut self, rect: Rect, brush: Brush, radii: CornerRadii);
826    fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32);
827    fn draw_circle_blend(
828        &mut self,
829        brush: Brush,
830        center: Point,
831        radius: f32,
832        blend_mode: BlendMode,
833    );
834
835    /// Strokes the outline of the whole scope rect.
836    fn draw_rect_stroked(&mut self, brush: Brush, stroke: Stroke);
837    fn draw_rect_stroked_blend(&mut self, brush: Brush, stroke: Stroke, blend_mode: BlendMode);
838    /// Strokes the outline of `rect`.
839    fn draw_rect_at_stroked(&mut self, rect: Rect, brush: Brush, stroke: Stroke);
840    fn draw_rect_at_stroked_blend(
841        &mut self,
842        rect: Rect,
843        brush: Brush,
844        stroke: Stroke,
845        blend_mode: BlendMode,
846    );
847    /// Strokes the outline of the whole scope rect with rounded corners.
848    fn draw_round_rect_stroked(&mut self, brush: Brush, radii: CornerRadii, stroke: Stroke);
849    fn draw_round_rect_stroked_blend(
850        &mut self,
851        brush: Brush,
852        radii: CornerRadii,
853        stroke: Stroke,
854        blend_mode: BlendMode,
855    );
856    /// Strokes the outline of `rect` with rounded corners.
857    fn draw_round_rect_at_stroked(
858        &mut self,
859        rect: Rect,
860        brush: Brush,
861        radii: CornerRadii,
862        stroke: Stroke,
863    );
864    fn draw_round_rect_at_stroked_blend(
865        &mut self,
866        rect: Rect,
867        brush: Brush,
868        radii: CornerRadii,
869        stroke: Stroke,
870        blend_mode: BlendMode,
871    );
872    /// Strokes a circle outline. Lowers to a stroked rounded rect, so it shares
873    /// the fill pipeline and batches with every other shape.
874    fn draw_circle_stroked(&mut self, brush: Brush, center: Point, radius: f32, stroke: Stroke);
875    fn draw_circle_stroked_blend(
876        &mut self,
877        brush: Brush,
878        center: Point,
879        radius: f32,
880        stroke: Stroke,
881        blend_mode: BlendMode,
882    );
883
884    /// Strokes a circular arc.
885    ///
886    /// Angles are in **radians**, `0` points along **+X**, and increasing
887    /// angles sweep **clockwise on screen** (Cranpose uses y-down device
888    /// coordinates, so this matches `atan2(dy, dx)` and the sweep-gradient
889    /// brush). A negative `sweep_angle` sweeps counter-clockwise; `|sweep| >=
890    /// 2π` draws a closed ring.
891    ///
892    /// The stroke is centered on `radius`, so the band covers
893    /// `radius ± width/2`. [`StrokeCap`](crate::StrokeCap) shapes the two ends.
894    /// Nothing is drawn for a zero sweep, a non-positive width, or non-finite
895    /// input.
896    fn draw_arc(
897        &mut self,
898        brush: Brush,
899        center: Point,
900        radius: f32,
901        start_angle: f32,
902        sweep_angle: f32,
903        stroke: Stroke,
904    );
905    #[expect(clippy::too_many_arguments)]
906    fn draw_arc_blend(
907        &mut self,
908        brush: Brush,
909        center: Point,
910        radius: f32,
911        start_angle: f32,
912        sweep_angle: f32,
913        stroke: Stroke,
914        blend_mode: BlendMode,
915    );
916
917    /// Fills an annular sector — the region between `inner_radius` and
918    /// `outer_radius`, limited to an angular sweep, with **flat radial ends**.
919    ///
920    /// This is the shape a stroked arc cannot express: its ends are straight
921    /// lines through the center, not caps. `inner_radius = 0` fills a pie
922    /// wedge. Angle convention is identical to [`draw_arc`](Self::draw_arc).
923    /// Nothing is drawn when `inner_radius >= outer_radius`, the sweep is zero,
924    /// or any input is non-finite.
925    fn draw_annular_sector(
926        &mut self,
927        brush: Brush,
928        center: Point,
929        inner_radius: f32,
930        outer_radius: f32,
931        start_angle: f32,
932        sweep_angle: f32,
933    );
934    #[expect(clippy::too_many_arguments)]
935    fn draw_annular_sector_blend(
936        &mut self,
937        brush: Brush,
938        center: Point,
939        inner_radius: f32,
940        outer_radius: f32,
941        start_angle: f32,
942        sweep_angle: f32,
943        blend_mode: BlendMode,
944    );
945
946    fn draw_image(&mut self, image: ImageBitmap);
947    fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode);
948    fn draw_image_at(
949        &mut self,
950        rect: Rect,
951        image: ImageBitmap,
952        alpha: f32,
953        color_filter: Option<ColorFilter>,
954    );
955    fn draw_image_at_sampled(
956        &mut self,
957        rect: Rect,
958        image: ImageBitmap,
959        alpha: f32,
960        color_filter: Option<ColorFilter>,
961        sampling: ImageSampling,
962    );
963    fn draw_image_at_blend(
964        &mut self,
965        rect: Rect,
966        image: ImageBitmap,
967        alpha: f32,
968        color_filter: Option<ColorFilter>,
969        blend_mode: BlendMode,
970    );
971    /// Draws a sub-region of an image. `src_rect` is in image-pixel
972    /// coordinates; `dst_rect` is in scope coordinates.
973    fn draw_image_src(
974        &mut self,
975        image: ImageBitmap,
976        src_rect: Rect,
977        dst_rect: Rect,
978        alpha: f32,
979        color_filter: Option<ColorFilter>,
980    );
981    fn draw_image_src_sampled(
982        &mut self,
983        image: ImageBitmap,
984        src_rect: Rect,
985        dst_rect: Rect,
986        alpha: f32,
987        color_filter: Option<ColorFilter>,
988        sampling: ImageSampling,
989    );
990    fn draw_image_src_blend(
991        &mut self,
992        image: ImageBitmap,
993        src_rect: Rect,
994        dst_rect: Rect,
995        alpha: f32,
996        color_filter: Option<ColorFilter>,
997        blend_mode: BlendMode,
998    );
999    /// Fills a parsed SVG path in scope coordinates (path units are dp).
1000    ///
1001    /// The fill is rasterized on the CPU into a supersampled, anti-aliased
1002    /// bitmap covering the path bounds and drawn as an image primitive, so
1003    /// it works on every render backend. Parse the path once with
1004    /// [`crate::VectorPath::parse`] and redraw it per frame. Solid brushes
1005    /// are honored exactly; gradient brushes currently fall back to their
1006    /// first stop color.
1007    fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush);
1008    /// Parses SVG path data (the `d` attribute syntax: `M/m L/l H/h V/v
1009    /// C/c S/s Q/q T/t A/a Z/z`) and fills it. Invalid path data draws
1010    /// nothing. Prefer [`crate::VectorPath::parse`] +
1011    /// [`draw_vector_path`](Self::draw_vector_path) to avoid re-parsing
1012    /// and to surface parse errors.
1013    fn draw_svg_path(&mut self, d: &str, brush: Brush) {
1014        if let Ok(path) = crate::VectorPath::parse(d) {
1015            self.draw_vector_path(&path, brush);
1016        }
1017    }
1018
1019    /// The block size, line height and first baseline `text` would occupy in
1020    /// `style`.
1021    ///
1022    /// Free to call repeatedly: the underlying text stack caches metrics on
1023    /// `(text, style)`, so a game can measure every label every frame to center
1024    /// it without touching a font file more than once.
1025    fn measure_text(&self, text: &str, style: &DrawTextStyle) -> TextMeasurement;
1026
1027    /// Draws `text` inside the whole scope rect, positioned by
1028    /// [`DrawTextStyle::align`] and [`DrawTextStyle::vertical_align`].
1029    fn draw_text(&mut self, brush: Brush, text: &str, style: &DrawTextStyle) {
1030        self.draw_text_at(Rect::from_size(self.size()), brush, text, style);
1031    }
1032
1033    /// Draws `text` inside `rect`, positioned by [`DrawTextStyle::align`] and
1034    /// [`DrawTextStyle::vertical_align`].
1035    ///
1036    /// The glyphs are *not* clipped to `rect` — it is an alignment box, not a
1037    /// viewport. A `rect` narrower than the measured text overflows in the
1038    /// direction the alignment implies; clip the layer if that matters.
1039    fn draw_text_at(&mut self, rect: Rect, brush: Brush, text: &str, style: &DrawTextStyle);
1040
1041    /// Draws `text` with the top-left corner of its block at `top_left`.
1042    ///
1043    /// Alignment is a no-op here because the box is the measurement — this is
1044    /// the "I already know where it goes" form, and the one to pair with
1045    /// [`measure_text`](Self::measure_text) for hand-rolled centering.
1046    fn draw_text_from(&mut self, top_left: Point, brush: Brush, text: &str, style: &DrawTextStyle) {
1047        if text.is_empty() {
1048            return;
1049        }
1050        let measurement = self.measure_text(text, style);
1051        self.draw_text_at(
1052            Rect::from_origin_size(top_left, measurement.size),
1053            brush,
1054            text,
1055            &DrawTextStyle {
1056                align: TextAlign::Left,
1057                vertical_align: TextVerticalAlign::Top,
1058                ..style.clone()
1059            },
1060        );
1061    }
1062
1063    fn into_primitives(self) -> Vec<DrawPrimitive>;
1064}
1065
1066/// Resolves the top-left corner a text block of `measurement` gets when it is
1067/// aligned inside `rect`.
1068///
1069/// Split out so the placement rule is stated once and can be unit-tested
1070/// against the measurement it is derived from.
1071pub fn align_text_block(rect: Rect, measurement: TextMeasurement, style: &DrawTextStyle) -> Point {
1072    let x = match style.align {
1073        TextAlign::Left => rect.x,
1074        TextAlign::Center => rect.x + (rect.width - measurement.size.width) * 0.5,
1075        TextAlign::Right => rect.x + rect.width - measurement.size.width,
1076    };
1077    let y = match style.vertical_align {
1078        TextVerticalAlign::Top => rect.y,
1079        TextVerticalAlign::Center => rect.y + (rect.height - measurement.size.height) * 0.5,
1080        TextVerticalAlign::Bottom => rect.y + rect.height - measurement.size.height,
1081        TextVerticalAlign::Baseline => rect.y - measurement.first_baseline,
1082    };
1083    Point::new(x, y)
1084}
1085
1086#[derive(Default)]
1087pub struct DrawScopeDefault {
1088    size: Size,
1089    recording: CommandRecorder,
1090    text_measurer: Option<Rc<dyn DrawTextMeasurer>>,
1091}
1092
1093const RECORDED_PRIMITIVE_COUNTS_LIMIT: usize = 64;
1094
1095thread_local! {
1096    static RECORDED_PRIMITIVE_COUNTS: std::cell::RefCell<
1097        std::collections::HashMap<(u32, u32), usize, crate::FxBuildHasher>,
1098    > = std::cell::RefCell::new(std::collections::HashMap::default());
1099}
1100
1101fn recorded_primitive_capacity(size: Size) -> usize {
1102    RECORDED_PRIMITIVE_COUNTS.with(|counts| {
1103        counts
1104            .borrow()
1105            .get(&(size.width.to_bits(), size.height.to_bits()))
1106            .copied()
1107            .unwrap_or(0)
1108    })
1109}
1110
1111fn note_recorded_primitive_count(size: Size, count: usize) {
1112    RECORDED_PRIMITIVE_COUNTS.with(|counts| {
1113        let mut counts = counts.borrow_mut();
1114        if counts.len() >= RECORDED_PRIMITIVE_COUNTS_LIMIT {
1115            counts.clear();
1116        }
1117        counts.insert((size.width.to_bits(), size.height.to_bits()), count);
1118    });
1119}
1120
1121impl DrawScopeDefault {
1122    pub fn new(size: Size) -> Self {
1123        Self::with_storage(size, None, CommandRecording::default())
1124    }
1125
1126    /// A scope that measures text with the app's fonts.
1127    ///
1128    /// The framework calls this for every draw closure it runs; `new` exists
1129    /// for callers that never draw text.
1130    pub fn with_text_measurer(size: Size, text_measurer: Rc<dyn DrawTextMeasurer>) -> Self {
1131        Self::with_storage(size, Some(text_measurer), CommandRecording::default())
1132    }
1133
1134    /// A scope recording into `storage`, a recording the caller kept from an
1135    /// earlier frame so its buffers keep the capacity they earned.
1136    pub fn with_text_measurer_reusing(
1137        size: Size,
1138        text_measurer: Rc<dyn DrawTextMeasurer>,
1139        storage: CommandRecording,
1140    ) -> Self {
1141        Self::with_storage(size, Some(text_measurer), storage)
1142    }
1143
1144    fn with_storage(
1145        size: Size,
1146        text_measurer: Option<Rc<dyn DrawTextMeasurer>>,
1147        recording: CommandRecording,
1148    ) -> Self {
1149        let mut recording = CommandRecorder::reusing(recording);
1150        recording.reserve_shapes(recorded_primitive_capacity(size));
1151        Self {
1152            size,
1153            recording,
1154            text_measurer,
1155        }
1156    }
1157
1158    /// How many `draw_content` markers this scope has recorded.
1159    pub fn content_marker_count(&self) -> u32 {
1160        self.recording.content_markers()
1161    }
1162
1163    /// Runs `draw` in this scope's rect shrunk by `insets`, where a draw
1164    /// modifier placed after layout padding draws: `draw` sees the inner
1165    /// size, and what it records lands offset by the leading insets.
1166    pub fn inset(&mut self, insets: EdgeInsets, draw: impl FnOnce(&mut DrawScopeDefault)) {
1167        let inner = insets.inset_rect(self.size);
1168        let mut scope = Self::with_storage(
1169            Size {
1170                width: inner.width,
1171                height: inner.height,
1172            },
1173            self.text_measurer.clone(),
1174            CommandRecording::default(),
1175        );
1176        draw(&mut scope);
1177        let primitives = scope.into_primitives();
1178        self.push_recorded(
1179            primitives
1180                .into_iter()
1181                .map(|primitive| primitive.translate(inner.x, inner.y)),
1182        );
1183    }
1184
1185    /// Records primitives already built, as if each had been drawn here.
1186    pub fn push_recorded(&mut self, primitives: impl IntoIterator<Item = DrawPrimitive>) {
1187        for primitive in primitives {
1188            self.recording.push_primitive(primitive);
1189        }
1190    }
1191
1192    /// The recording, in the storage it was recorded into, so the caller
1193    /// can lend it to the same command's next recording.
1194    pub fn finish(self) -> CommandRecording {
1195        note_recorded_primitive_count(self.size, self.recording.len());
1196        self.recording.finish()
1197    }
1198
1199    fn push_blended_primitive(&mut self, primitive: DrawPrimitive, blend_mode: BlendMode) {
1200        if blend_mode != BlendMode::SrcOver {
1201            self.recording.push_other(DrawPrimitive::Blend {
1202                primitive: Box::new(primitive),
1203                blend_mode,
1204            });
1205            return;
1206        }
1207        self.recording.push_other(primitive);
1208    }
1209
1210    #[expect(clippy::too_many_arguments)]
1211    #[inline]
1212    fn push_arc(
1213        &mut self,
1214        brush: Brush,
1215        center: Point,
1216        radius: f32,
1217        start_angle: f32,
1218        sweep_angle: f32,
1219        stroke: Option<Stroke>,
1220        inner_radius: f32,
1221        blend_mode: BlendMode,
1222    ) {
1223        let args = ArcRecordArgs {
1224            brush: &brush,
1225            center,
1226            radius,
1227            start_angle,
1228            sweep_angle,
1229            stroke,
1230            inner_radius,
1231            blend_mode,
1232        };
1233        let geometry = normalized_band(&args);
1234        if geometry.is_degenerate() {
1235            return;
1236        }
1237        self.recording.push_scope_arc(&args, &geometry);
1238    }
1239}
1240
1241impl DrawScope for DrawScopeDefault {
1242    fn size(&self) -> Size {
1243        self.size
1244    }
1245
1246    fn draw_content(&mut self) {
1247        self.recording.push_content();
1248    }
1249
1250    fn draw_rect(&mut self, brush: Brush) {
1251        self.draw_rect_blend(brush, BlendMode::SrcOver);
1252    }
1253
1254    fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode) {
1255        self.recording
1256            .push_rect(Rect::from_size(self.size), &brush, None, blend_mode);
1257    }
1258
1259    fn draw_rect_at(&mut self, rect: Rect, brush: Brush) {
1260        self.draw_rect_at_blend(rect, brush, BlendMode::SrcOver);
1261    }
1262
1263    fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode) {
1264        self.recording.push_rect(rect, &brush, None, blend_mode);
1265    }
1266
1267    fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii) {
1268        self.draw_round_rect_blend(brush, radii, BlendMode::SrcOver);
1269    }
1270
1271    fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode) {
1272        self.recording
1273            .push_round_rect(Rect::from_size(self.size), &brush, radii, None, blend_mode);
1274    }
1275
1276    fn draw_round_rect_at(&mut self, rect: Rect, brush: Brush, radii: CornerRadii) {
1277        self.recording
1278            .push_round_rect(rect, &brush, radii, None, BlendMode::SrcOver);
1279    }
1280
1281    fn draw_rect_stroked(&mut self, brush: Brush, stroke: Stroke) {
1282        self.draw_rect_stroked_blend(brush, stroke, BlendMode::SrcOver);
1283    }
1284
1285    fn draw_rect_stroked_blend(&mut self, brush: Brush, stroke: Stroke, blend_mode: BlendMode) {
1286        self.draw_rect_at_stroked_blend(Rect::from_size(self.size), brush, stroke, blend_mode);
1287    }
1288
1289    fn draw_rect_at_stroked(&mut self, rect: Rect, brush: Brush, stroke: Stroke) {
1290        self.draw_rect_at_stroked_blend(rect, brush, stroke, BlendMode::SrcOver);
1291    }
1292
1293    fn draw_rect_at_stroked_blend(
1294        &mut self,
1295        rect: Rect,
1296        brush: Brush,
1297        stroke: Stroke,
1298        blend_mode: BlendMode,
1299    ) {
1300        if !stroke.is_visible() {
1301            return;
1302        }
1303        self.recording
1304            .push_rect(rect, &brush, Some(stroke), blend_mode);
1305    }
1306
1307    fn draw_round_rect_stroked(&mut self, brush: Brush, radii: CornerRadii, stroke: Stroke) {
1308        self.draw_round_rect_stroked_blend(brush, radii, stroke, BlendMode::SrcOver);
1309    }
1310
1311    fn draw_round_rect_stroked_blend(
1312        &mut self,
1313        brush: Brush,
1314        radii: CornerRadii,
1315        stroke: Stroke,
1316        blend_mode: BlendMode,
1317    ) {
1318        self.draw_round_rect_at_stroked_blend(
1319            Rect::from_size(self.size),
1320            brush,
1321            radii,
1322            stroke,
1323            blend_mode,
1324        );
1325    }
1326
1327    fn draw_round_rect_at_stroked(
1328        &mut self,
1329        rect: Rect,
1330        brush: Brush,
1331        radii: CornerRadii,
1332        stroke: Stroke,
1333    ) {
1334        self.draw_round_rect_at_stroked_blend(rect, brush, radii, stroke, BlendMode::SrcOver);
1335    }
1336
1337    fn draw_round_rect_at_stroked_blend(
1338        &mut self,
1339        rect: Rect,
1340        brush: Brush,
1341        radii: CornerRadii,
1342        stroke: Stroke,
1343        blend_mode: BlendMode,
1344    ) {
1345        if !stroke.is_visible() {
1346            return;
1347        }
1348        self.recording
1349            .push_round_rect(rect, &brush, radii, Some(stroke), blend_mode);
1350    }
1351
1352    fn draw_circle_stroked(&mut self, brush: Brush, center: Point, radius: f32, stroke: Stroke) {
1353        self.draw_circle_stroked_blend(brush, center, radius, stroke, BlendMode::SrcOver);
1354    }
1355
1356    fn draw_circle_stroked_blend(
1357        &mut self,
1358        brush: Brush,
1359        center: Point,
1360        radius: f32,
1361        stroke: Stroke,
1362        blend_mode: BlendMode,
1363    ) {
1364        if !stroke.is_visible() || !radius.is_finite() {
1365            return;
1366        }
1367        let radius = radius.max(0.0);
1368        let diameter = radius * 2.0;
1369        self.draw_round_rect_at_stroked_blend(
1370            Rect {
1371                x: center.x - radius,
1372                y: center.y - radius,
1373                width: diameter,
1374                height: diameter,
1375            },
1376            brush,
1377            CornerRadii::uniform(radius),
1378            stroke,
1379            blend_mode,
1380        );
1381    }
1382
1383    fn draw_arc(
1384        &mut self,
1385        brush: Brush,
1386        center: Point,
1387        radius: f32,
1388        start_angle: f32,
1389        sweep_angle: f32,
1390        stroke: Stroke,
1391    ) {
1392        self.draw_arc_blend(
1393            brush,
1394            center,
1395            radius,
1396            start_angle,
1397            sweep_angle,
1398            stroke,
1399            BlendMode::SrcOver,
1400        );
1401    }
1402
1403    fn draw_arc_blend(
1404        &mut self,
1405        brush: Brush,
1406        center: Point,
1407        radius: f32,
1408        start_angle: f32,
1409        sweep_angle: f32,
1410        stroke: Stroke,
1411        blend_mode: BlendMode,
1412    ) {
1413        if !stroke.is_visible() {
1414            return;
1415        }
1416        self.push_arc(
1417            brush,
1418            center,
1419            radius,
1420            start_angle,
1421            sweep_angle,
1422            Some(stroke),
1423            0.0,
1424            blend_mode,
1425        );
1426    }
1427
1428    fn draw_annular_sector(
1429        &mut self,
1430        brush: Brush,
1431        center: Point,
1432        inner_radius: f32,
1433        outer_radius: f32,
1434        start_angle: f32,
1435        sweep_angle: f32,
1436    ) {
1437        self.draw_annular_sector_blend(
1438            brush,
1439            center,
1440            inner_radius,
1441            outer_radius,
1442            start_angle,
1443            sweep_angle,
1444            BlendMode::SrcOver,
1445        );
1446    }
1447
1448    fn draw_annular_sector_blend(
1449        &mut self,
1450        brush: Brush,
1451        center: Point,
1452        inner_radius: f32,
1453        outer_radius: f32,
1454        start_angle: f32,
1455        sweep_angle: f32,
1456        blend_mode: BlendMode,
1457    ) {
1458        self.push_arc(
1459            brush,
1460            center,
1461            outer_radius,
1462            start_angle,
1463            sweep_angle,
1464            None,
1465            inner_radius,
1466            blend_mode,
1467        );
1468    }
1469
1470    fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32) {
1471        self.draw_circle_blend(brush, center, radius, BlendMode::SrcOver);
1472    }
1473
1474    fn draw_circle_blend(
1475        &mut self,
1476        brush: Brush,
1477        center: Point,
1478        radius: f32,
1479        blend_mode: BlendMode,
1480    ) {
1481        let radius = radius.max(0.0);
1482        let diameter = radius * 2.0;
1483        self.recording.push_round_rect(
1484            Rect {
1485                x: center.x - radius,
1486                y: center.y - radius,
1487                width: diameter,
1488                height: diameter,
1489            },
1490            &brush,
1491            CornerRadii::uniform(radius),
1492            None,
1493            blend_mode,
1494        );
1495    }
1496
1497    fn draw_image(&mut self, image: ImageBitmap) {
1498        self.draw_image_blend(image, BlendMode::SrcOver);
1499    }
1500
1501    fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode) {
1502        self.push_blended_primitive(
1503            DrawPrimitive::Image {
1504                rect: Rect::from_size(self.size),
1505                image,
1506                alpha: 1.0,
1507                color_filter: None,
1508                sampling: ImageSampling::Nearest,
1509                src_rect: None,
1510            },
1511            blend_mode,
1512        );
1513    }
1514
1515    fn draw_image_at(
1516        &mut self,
1517        rect: Rect,
1518        image: ImageBitmap,
1519        alpha: f32,
1520        color_filter: Option<ColorFilter>,
1521    ) {
1522        self.draw_image_at_sampled(rect, image, alpha, color_filter, ImageSampling::Nearest);
1523    }
1524
1525    fn draw_image_at_sampled(
1526        &mut self,
1527        rect: Rect,
1528        image: ImageBitmap,
1529        alpha: f32,
1530        color_filter: Option<ColorFilter>,
1531        sampling: ImageSampling,
1532    ) {
1533        self.push_blended_primitive(
1534            DrawPrimitive::Image {
1535                rect,
1536                image,
1537                alpha: alpha.clamp(0.0, 1.0),
1538                color_filter,
1539                sampling,
1540                src_rect: None,
1541            },
1542            BlendMode::SrcOver,
1543        );
1544    }
1545
1546    fn draw_image_at_blend(
1547        &mut self,
1548        rect: Rect,
1549        image: ImageBitmap,
1550        alpha: f32,
1551        color_filter: Option<ColorFilter>,
1552        blend_mode: BlendMode,
1553    ) {
1554        self.push_blended_primitive(
1555            DrawPrimitive::Image {
1556                rect,
1557                image,
1558                alpha: alpha.clamp(0.0, 1.0),
1559                color_filter,
1560                sampling: ImageSampling::Nearest,
1561                src_rect: None,
1562            },
1563            blend_mode,
1564        );
1565    }
1566
1567    fn draw_image_src(
1568        &mut self,
1569        image: ImageBitmap,
1570        src_rect: Rect,
1571        dst_rect: Rect,
1572        alpha: f32,
1573        color_filter: Option<ColorFilter>,
1574    ) {
1575        self.draw_image_src_blend(
1576            image,
1577            src_rect,
1578            dst_rect,
1579            alpha,
1580            color_filter,
1581            BlendMode::SrcOver,
1582        );
1583    }
1584
1585    fn draw_image_src_sampled(
1586        &mut self,
1587        image: ImageBitmap,
1588        src_rect: Rect,
1589        dst_rect: Rect,
1590        alpha: f32,
1591        color_filter: Option<ColorFilter>,
1592        sampling: ImageSampling,
1593    ) {
1594        self.push_blended_primitive(
1595            DrawPrimitive::Image {
1596                rect: dst_rect,
1597                image,
1598                alpha: alpha.clamp(0.0, 1.0),
1599                color_filter,
1600                sampling,
1601                src_rect: Some(src_rect),
1602            },
1603            BlendMode::SrcOver,
1604        );
1605    }
1606
1607    fn draw_image_src_blend(
1608        &mut self,
1609        image: ImageBitmap,
1610        src_rect: Rect,
1611        dst_rect: Rect,
1612        alpha: f32,
1613        color_filter: Option<ColorFilter>,
1614        blend_mode: BlendMode,
1615    ) {
1616        self.push_blended_primitive(
1617            DrawPrimitive::Image {
1618                rect: dst_rect,
1619                image,
1620                alpha: alpha.clamp(0.0, 1.0),
1621                color_filter,
1622                sampling: ImageSampling::Nearest,
1623                src_rect: Some(src_rect),
1624            },
1625            blend_mode,
1626        );
1627    }
1628
1629    fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush) {
1630        const SUPERSAMPLE: f32 = 2.0;
1631        const MAX_MASK_PIXELS: f32 = 4096.0;
1632
1633        if path.is_empty() {
1634            return;
1635        }
1636        let bounds = path.bounds();
1637        if bounds.width <= 0.0 || bounds.height <= 0.0 {
1638            return;
1639        }
1640
1641        let color = match &brush {
1642            Brush::Solid(color) => *color,
1643            Brush::LinearGradient { colors, .. }
1644            | Brush::RadialGradient { colors, .. }
1645            | Brush::SweepGradient { colors, .. } => match colors.first() {
1646                Some(color) => *color,
1647                None => return,
1648            },
1649        };
1650        if color.3 <= 0.0 {
1651            return;
1652        }
1653
1654        let origin = Point::new(bounds.x.floor() - 1.0, bounds.y.floor() - 1.0);
1655        let rect_width = (bounds.x + bounds.width).ceil() - origin.x + 1.0;
1656        let rect_height = (bounds.y + bounds.height).ceil() - origin.y + 1.0;
1657        let mask_width = (rect_width * SUPERSAMPLE)
1658            .ceil()
1659            .clamp(1.0, MAX_MASK_PIXELS) as usize;
1660        let mask_height = (rect_height * SUPERSAMPLE)
1661            .ceil()
1662            .clamp(1.0, MAX_MASK_PIXELS) as usize;
1663
1664        let red = (color.0.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
1665        let green = (color.1.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
1666        let blue = (color.2.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
1667        let alpha = color.3.clamp(0.0, 1.0);
1668        let key = vector_path_mask_key(
1669            path,
1670            origin,
1671            (mask_width, mask_height),
1672            [red, green, blue],
1673            alpha,
1674        );
1675        let cached = vector_path_mask_cache_get(key);
1676        let image = match cached {
1677            Some(image) => image,
1678            None => {
1679                let mask = path.coverage_mask(mask_width, mask_height, origin, SUPERSAMPLE);
1680                let mut pixels = Vec::with_capacity(mask.len() * 4);
1681                for coverage in mask {
1682                    pixels.extend_from_slice(&[
1683                        red,
1684                        green,
1685                        blue,
1686                        (alpha * coverage as f32 + 0.5) as u8,
1687                    ]);
1688                }
1689                let Ok(image) =
1690                    ImageBitmap::from_rgba8(mask_width as u32, mask_height as u32, pixels)
1691                else {
1692                    return;
1693                };
1694                vector_path_mask_cache_put(key, image.clone());
1695                image
1696            }
1697        };
1698
1699        self.recording.push_other(DrawPrimitive::Image {
1700            rect: Rect {
1701                x: origin.x,
1702                y: origin.y,
1703                width: rect_width,
1704                height: rect_height,
1705            },
1706            image,
1707            alpha: 1.0,
1708            color_filter: None,
1709            sampling: ImageSampling::Linear,
1710            src_rect: None,
1711        });
1712    }
1713
1714    fn measure_text(&self, text: &str, style: &DrawTextStyle) -> TextMeasurement {
1715        match &self.text_measurer {
1716            Some(measurer) => measurer.measure_text(text, style),
1717            None => estimate_text_measurement(text, style),
1718        }
1719    }
1720
1721    fn draw_text_at(&mut self, rect: Rect, brush: Brush, text: &str, style: &DrawTextStyle) {
1722        if text.is_empty() {
1723            return;
1724        }
1725        let Some(color) = solid_fill_color(&brush) else {
1726            return;
1727        };
1728        if color.3 <= 0.0 {
1729            return;
1730        }
1731        let measurement = self.measure_text(text, style);
1732        if !(measurement.size.width > 0.0 && measurement.size.height > 0.0) {
1733            return;
1734        }
1735        let origin = align_text_block(rect, measurement, style);
1736        if !origin.x.is_finite() || !origin.y.is_finite() {
1737            return;
1738        }
1739        self.recording
1740            .push_other(DrawPrimitive::Text(Box::new(TextPrimitive {
1741                rect: Rect::from_origin_size(origin, measurement.size),
1742                text: shared_text_str(text),
1743                style: style.clone(),
1744                color,
1745            })));
1746    }
1747
1748    fn into_primitives(self) -> Vec<DrawPrimitive> {
1749        self.finish().into_primitives_with_markers()
1750    }
1751}
1752
1753/// The single color a brush paints with, or its first stop for a gradient.
1754///
1755/// Text is filled per glyph from one vertex color, so a gradient cannot be
1756/// honored; this mirrors the fallback [`DrawScope::draw_vector_path`] documents.
1757fn solid_fill_color(brush: &Brush) -> Option<Color> {
1758    match brush {
1759        Brush::Solid(color) => Some(*color),
1760        Brush::LinearGradient { colors, .. }
1761        | Brush::RadialGradient { colors, .. }
1762        | Brush::SweepGradient { colors, .. } => colors.first().copied(),
1763    }
1764}
1765
1766#[cfg(test)]
1767#[path = "tests/geometry_tests.rs"]
1768mod tests;