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