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