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