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