Skip to main content

cranpose_ui_graphics/
geometry.rs

1//! Geometric primitives: Point, Size, Rect, Insets, Path
2
3use crate::stroke::{arc_band, ArcGeometry, Stroke};
4use crate::typography::{
5    estimate_text_measurement, DrawTextMeasurer, TextAlign, TextMeasurement, TextStyle,
6    TextVerticalAlign,
7};
8use crate::{Brush, Color, ColorFilter, ImageBitmap, ImageSampling};
9use std::ops::AddAssign;
10use std::rc::Rc;
11
12const VECTOR_PATH_MASK_CACHE_ENTRIES: usize = 96;
13const VECTOR_PATH_MASK_CACHE_BYTES: usize = 8 * 1024 * 1024;
14
15struct VectorPathMaskCache {
16    entries: Vec<(u64, ImageBitmap)>,
17    bytes: usize,
18}
19
20impl VectorPathMaskCache {
21    const fn new() -> Self {
22        Self {
23            entries: Vec::new(),
24            bytes: 0,
25        }
26    }
27
28    fn get(&mut self, key: u64) -> Option<ImageBitmap> {
29        let index = self.entries.iter().position(|(seen, _)| *seen == key)?;
30        let entry = self.entries.remove(index);
31        let image = entry.1.clone();
32        self.entries.push(entry);
33        Some(image)
34    }
35
36    fn put(&mut self, key: u64, image: ImageBitmap) {
37        let bytes = image.width() as usize * image.height() as usize * 4;
38        if bytes > VECTOR_PATH_MASK_CACHE_BYTES {
39            return;
40        }
41        self.bytes += bytes;
42        self.entries.push((key, image));
43        while self.entries.len() > VECTOR_PATH_MASK_CACHE_ENTRIES
44            || self.bytes > VECTOR_PATH_MASK_CACHE_BYTES
45        {
46            let (_, dropped) = self.entries.remove(0);
47            self.bytes = self
48                .bytes
49                .saturating_sub(dropped.width() as usize * dropped.height() as usize * 4);
50        }
51    }
52}
53
54thread_local! {
55    static VECTOR_PATH_MASKS: std::cell::RefCell<VectorPathMaskCache> =
56        const { std::cell::RefCell::new(VectorPathMaskCache::new()) };
57}
58
59fn vector_path_mask_key(
60    path: &crate::VectorPath,
61    origin: Point,
62    mask_size: (usize, usize),
63    rgb: [u8; 3],
64    alpha: f32,
65) -> u64 {
66    use std::hash::Hasher;
67    let mut hasher = crate::fx_hash::FxHasher::default();
68    hasher.write_u8(path.fill_rule() as u8);
69    hasher.write_u32(origin.x.to_bits());
70    hasher.write_u32(origin.y.to_bits());
71    hasher.write_usize(mask_size.0);
72    hasher.write_usize(mask_size.1);
73    hasher.write(&rgb);
74    hasher.write_u32(alpha.to_bits());
75    for subpath in path.subpaths() {
76        hasher.write_usize(subpath.len());
77        for point in subpath {
78            hasher.write_u32(point.x.to_bits());
79            hasher.write_u32(point.y.to_bits());
80        }
81    }
82    hasher.finish()
83}
84
85fn vector_path_mask_cache_get(key: u64) -> Option<ImageBitmap> {
86    VECTOR_PATH_MASKS.with(|cache| cache.borrow_mut().get(key))
87}
88
89fn vector_path_mask_cache_put(key: u64, image: ImageBitmap) {
90    VECTOR_PATH_MASKS.with(|cache| cache.borrow_mut().put(key, image));
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Default)]
94pub struct Point {
95    pub x: f32,
96    pub y: f32,
97}
98
99impl Point {
100    pub const fn new(x: f32, y: f32) -> Self {
101        Self { x, y }
102    }
103
104    pub const ZERO: Point = Point { x: 0.0, y: 0.0 };
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Default)]
108pub struct Size {
109    pub width: f32,
110    pub height: f32,
111}
112
113impl Size {
114    pub const fn new(width: f32, height: f32) -> Self {
115        Self { width, height }
116    }
117
118    pub const ZERO: Size = Size {
119        width: 0.0,
120        height: 0.0,
121    };
122}
123
124#[derive(Clone, Copy, Debug, PartialEq)]
125pub struct Rect {
126    pub x: f32,
127    pub y: f32,
128    pub width: f32,
129    pub height: f32,
130}
131
132impl Rect {
133    pub fn from_origin_size(origin: Point, size: Size) -> Self {
134        Self {
135            x: origin.x,
136            y: origin.y,
137            width: size.width,
138            height: size.height,
139        }
140    }
141
142    pub fn from_size(size: Size) -> Self {
143        Self {
144            x: 0.0,
145            y: 0.0,
146            width: size.width,
147            height: size.height,
148        }
149    }
150
151    pub fn translate(&self, dx: f32, dy: f32) -> Self {
152        Self {
153            x: self.x + dx,
154            y: self.y + dy,
155            width: self.width,
156            height: self.height,
157        }
158    }
159
160    pub fn contains(&self, x: f32, y: f32) -> bool {
161        x >= self.x && y >= self.y && x <= self.x + self.width && y <= self.y + self.height
162    }
163
164    /// Returns the intersection of two rectangles, or `None` if they don't overlap.
165    pub fn intersect(&self, other: Rect) -> Option<Rect> {
166        let left = self.x.max(other.x);
167        let top = self.y.max(other.y);
168        let right = (self.x + self.width).min(other.x + other.width);
169        let bottom = (self.y + self.height).min(other.y + other.height);
170        let width = right - left;
171        let height = bottom - top;
172        if width <= 0.0 || height <= 0.0 {
173            None
174        } else {
175            Some(Rect {
176                x: left,
177                y: top,
178                width,
179                height,
180            })
181        }
182    }
183
184    pub fn union(&self, other: Rect) -> Rect {
185        let left = self.x.min(other.x);
186        let top = self.y.min(other.y);
187        let right = (self.x + self.width).max(other.x + other.width);
188        let bottom = (self.y + self.height).max(other.y + other.height);
189        Rect {
190            x: left,
191            y: top,
192            width: (right - left).max(0.0),
193            height: (bottom - top).max(0.0),
194        }
195    }
196}
197
198/// Padding values for each edge of a rectangle.
199#[derive(Clone, Copy, Debug, Default, PartialEq)]
200pub struct EdgeInsets {
201    pub left: f32,
202    pub top: f32,
203    pub right: f32,
204    pub bottom: f32,
205}
206
207impl EdgeInsets {
208    pub fn uniform(all: f32) -> Self {
209        Self {
210            left: all,
211            top: all,
212            right: all,
213            bottom: all,
214        }
215    }
216
217    pub fn horizontal(horizontal: f32) -> Self {
218        Self {
219            left: horizontal,
220            right: horizontal,
221            ..Self::default()
222        }
223    }
224
225    pub fn vertical(vertical: f32) -> Self {
226        Self {
227            top: vertical,
228            bottom: vertical,
229            ..Self::default()
230        }
231    }
232
233    pub fn symmetric(horizontal: f32, vertical: f32) -> Self {
234        Self {
235            left: horizontal,
236            right: horizontal,
237            top: vertical,
238            bottom: vertical,
239        }
240    }
241
242    pub fn from_components(left: f32, top: f32, right: f32, bottom: f32) -> Self {
243        Self {
244            left,
245            top,
246            right,
247            bottom,
248        }
249    }
250
251    pub fn is_zero(&self) -> bool {
252        self.left == 0.0 && self.top == 0.0 && self.right == 0.0 && self.bottom == 0.0
253    }
254
255    pub fn horizontal_sum(&self) -> f32 {
256        self.left + self.right
257    }
258
259    pub fn vertical_sum(&self) -> f32 {
260        self.top + self.bottom
261    }
262}
263
264impl AddAssign for EdgeInsets {
265    fn add_assign(&mut self, rhs: Self) {
266        self.left += rhs.left;
267        self.top += rhs.top;
268        self.right += rhs.right;
269        self.bottom += rhs.bottom;
270    }
271}
272
273#[derive(Clone, Copy, Debug, Default, PartialEq)]
274pub struct CornerRadii {
275    pub top_left: f32,
276    pub top_right: f32,
277    pub bottom_right: f32,
278    pub bottom_left: f32,
279}
280
281impl CornerRadii {
282    pub fn uniform(radius: f32) -> Self {
283        Self {
284            top_left: radius,
285            top_right: radius,
286            bottom_right: radius,
287            bottom_left: radius,
288        }
289    }
290}
291
292#[derive(Clone, Copy, Debug, PartialEq)]
293pub struct RoundedCornerShape {
294    radii: CornerRadii,
295}
296
297impl RoundedCornerShape {
298    pub fn new(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self {
299        Self {
300            radii: CornerRadii {
301                top_left,
302                top_right,
303                bottom_right,
304                bottom_left,
305            },
306        }
307    }
308
309    pub fn uniform(radius: f32) -> Self {
310        Self {
311            radii: CornerRadii::uniform(radius),
312        }
313    }
314
315    pub fn with_radii(radii: CornerRadii) -> Self {
316        Self { radii }
317    }
318
319    pub fn resolve(&self, width: f32, height: f32) -> CornerRadii {
320        let mut resolved = self.radii;
321        let max_width = (width / 2.0).max(0.0);
322        let max_height = (height / 2.0).max(0.0);
323        resolved.top_left = resolved.top_left.clamp(0.0, max_width).min(max_height);
324        resolved.top_right = resolved.top_right.clamp(0.0, max_width).min(max_height);
325        resolved.bottom_right = resolved.bottom_right.clamp(0.0, max_width).min(max_height);
326        resolved.bottom_left = resolved.bottom_left.clamp(0.0, max_width).min(max_height);
327        resolved
328    }
329
330    pub fn radii(&self) -> CornerRadii {
331        self.radii
332    }
333}
334
335#[derive(Clone, Copy, Debug, PartialEq)]
336pub struct TransformOrigin {
337    pub pivot_fraction_x: f32,
338    pub pivot_fraction_y: f32,
339}
340
341impl TransformOrigin {
342    pub const fn new(pivot_fraction_x: f32, pivot_fraction_y: f32) -> Self {
343        Self {
344            pivot_fraction_x,
345            pivot_fraction_y,
346        }
347    }
348
349    pub const CENTER: TransformOrigin = TransformOrigin::new(0.5, 0.5);
350}
351
352impl Default for TransformOrigin {
353    fn default() -> Self {
354        Self::CENTER
355    }
356}
357
358#[derive(Clone, Copy, Debug, Default, PartialEq)]
359pub enum LayerShape {
360    #[default]
361    Rectangle,
362    Rounded(RoundedCornerShape),
363}
364
365#[derive(Clone, Debug, PartialEq)]
366pub struct GraphicsLayer {
367    pub alpha: f32,
368    pub scale: f32,
369    pub scale_x: f32,
370    pub scale_y: f32,
371    pub rotation_x: f32,
372    pub rotation_y: f32,
373    pub rotation_z: f32,
374    pub camera_distance: f32,
375    pub transform_origin: TransformOrigin,
376    pub translation_x: f32,
377    pub translation_y: f32,
378    pub shadow_elevation: f32,
379    pub ambient_shadow_color: Color,
380    pub spot_shadow_color: Color,
381    pub shape: LayerShape,
382    pub clip: bool,
383    pub compositing_strategy: CompositingStrategy,
384    pub blend_mode: BlendMode,
385    pub color_filter: Option<ColorFilter>,
386    pub render_effect: Option<crate::render_effect::RenderEffect>,
387    pub backdrop_effect: Option<crate::render_effect::RenderEffect>,
388}
389
390impl GraphicsLayer {
391    /// The alpha an isolated layer is composited at: an **eight-bit** one,
392    /// truncated.
393    ///
394    /// The platform never composites a layer at a float alpha. HWUI hands an
395    /// isolated `RenderNode` to the rasterizer as
396    /// `canvas->saveLayerAlpha(&bounds, (int)(properties.getAlpha() * 255))`
397    /// (`frameworks/base/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp`,
398    /// `setViewProperties`), and `(int)` truncates — 0.5 composites at 127/255,
399    /// not at 128/255. The fraction below that byte is gone before a single pixel
400    /// is blended, so anything that keeps it lands a level out wherever the byte
401    /// and the float fall on opposite sides of a half.
402    ///
403    /// The sibling branch is a float on purpose: where `getHasOverlappingRendering()`
404    /// is false HWUI takes `*alphaMultiplier = properties.getAlpha()` and folds it
405    /// into each draw without ever making a byte of it. That is what
406    /// `CompositingStrategy::ModulateAlpha` names.
407    ///
408    /// Truncating here and **rounding** in [`Color::srgb_8bit`] is not an
409    /// inconsistency: they are different call sites in the platform. A colour's own
410    /// alpha is snapped by `Color`'s constructor, which adds the half; a layer's
411    /// alpha is snapped by HWUI's cast, which does not. Anything modelling a faded
412    /// layer without allocating one — a canvas drawing a list row's fade by hand,
413    /// say — wants this rule and not the other.
414    pub fn composite_alpha_8bit(alpha: f32) -> f32 {
415        (alpha.clamp(0.0, 1.0) * 255.0).floor() / 255.0
416    }
417}
418
419impl Default for GraphicsLayer {
420    fn default() -> Self {
421        Self {
422            alpha: 1.0,
423            scale: 1.0,
424            scale_x: 1.0,
425            scale_y: 1.0,
426            rotation_x: 0.0,
427            rotation_y: 0.0,
428            rotation_z: 0.0,
429            camera_distance: 8.0,
430            transform_origin: TransformOrigin::CENTER,
431            translation_x: 0.0,
432            translation_y: 0.0,
433            shadow_elevation: 0.0,
434            ambient_shadow_color: Color::BLACK,
435            spot_shadow_color: Color::BLACK,
436            shape: LayerShape::Rectangle,
437            clip: false,
438            compositing_strategy: CompositingStrategy::Auto,
439            blend_mode: BlendMode::SrcOver,
440            color_filter: None,
441            render_effect: None,
442            backdrop_effect: None,
443        }
444    }
445}
446
447/// Blend mode used for draw primitives.
448///
449/// This mirrors Jetpack Compose's blend-mode vocabulary while the renderer
450/// currently guarantees `SrcOver` and `DstOut` behavior.
451#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
452pub enum BlendMode {
453    Clear,
454    Src,
455    Dst,
456    #[default]
457    SrcOver,
458    DstOver,
459    SrcIn,
460    DstIn,
461    SrcOut,
462    DstOut,
463    SrcAtop,
464    DstAtop,
465    Xor,
466    Plus,
467    Modulate,
468    Screen,
469    Overlay,
470    Darken,
471    Lighten,
472    ColorDodge,
473    ColorBurn,
474    HardLight,
475    SoftLight,
476    Difference,
477    Exclusion,
478    Multiply,
479    Hue,
480    Saturation,
481    Color,
482    Luminosity,
483}
484
485/// Controls how a graphics layer is composited into its parent target.
486#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
487pub enum CompositingStrategy {
488    /// Use renderer heuristics (default).
489    #[default]
490    Auto,
491    /// Render this layer to an offscreen target, then composite.
492    Offscreen,
493    /// Multiply alpha on source colors without allocating an offscreen layer.
494    ModulateAlpha,
495}
496
497#[derive(Clone, Debug, PartialEq)]
498pub enum DrawPrimitive {
499    /// Marker emitted by `draw_content()` inside `draw_with_content`.
500    /// This is consumed by the modifier pipeline and never rendered directly.
501    Content,
502    /// Wrapper to associate a draw primitive with a non-default blend mode.
503    Blend {
504        primitive: Box<DrawPrimitive>,
505        blend_mode: BlendMode,
506    },
507    Rect {
508        rect: Rect,
509        brush: Brush,
510        /// `None` fills the rect; `Some` strokes its outline, centered on the
511        /// edge (so it bleeds `width / 2` outside `rect`).
512        stroke: Option<Stroke>,
513    },
514    RoundRect {
515        rect: Rect,
516        brush: Brush,
517        radii: CornerRadii,
518        /// `None` fills the rounded rect; `Some` strokes its outline, centered
519        /// on the edge.
520        stroke: Option<Stroke>,
521    },
522    /// A circular band: a stroked arc, or a filled annular sector / pie wedge.
523    ///
524    /// Angles are radians, `0` = +X, increasing **clockwise** on screen (see
525    /// [`crate::stroke`] for the full convention).
526    ///
527    /// * `stroke = Some(_)` — the band is `radius ± width/2`, its ends shaped
528    ///   by the stroke cap. `inner_radius` is ignored.
529    /// * `stroke = None` — the band is `inner_radius ..= radius` with flat
530    ///   (butt) radial ends; `inner_radius = 0` is a filled pie wedge.
531    Arc {
532        /// Tight bounding box of the rendered band, caps included. Kept as the
533        /// first field (like every other variant) so bbox/culling/clip logic
534        /// treats an arc exactly like any other primitive.
535        rect: Rect,
536        brush: Brush,
537        center: Point,
538        radius: f32,
539        start_angle: f32,
540        sweep_angle: f32,
541        stroke: Option<Stroke>,
542        /// `> 0` turns a filled wedge into an annular sector.
543        inner_radius: f32,
544    },
545    Image {
546        rect: Rect,
547        image: ImageBitmap,
548        alpha: f32,
549        color_filter: Option<ColorFilter>,
550        sampling: ImageSampling,
551        /// Optional source rectangle in image-pixel coordinates.
552        /// When `None`, the entire image is drawn. When `Some`, only the
553        /// specified sub-region of the source image is sampled.
554        src_rect: Option<Rect>,
555    },
556    /// A laid-out run of text. See [`TextPrimitive`].
557    Text(Box<TextPrimitive>),
558    /// Shadow that requires blur processing. The renderer decides technique
559    /// (GPU blur, CPU approximation, etc.).
560    Shadow(ShadowPrimitive),
561}
562
563/// A run of text, positioned and ready to rasterize.
564///
565/// `rect` is *already resolved*: [`DrawScope::draw_text_at`] measures the
566/// string, applies [`TextStyle::align`] / [`TextStyle::vertical_align`] inside
567/// the requested box, and stores the result here. Renderers therefore lay the
568/// glyphs out from `rect`'s top-left and never re-align — which is what keeps
569/// what [`DrawScope::measure_text`] reported and what lands on screen the same
570/// geometry.
571#[derive(Clone, Debug, PartialEq)]
572pub struct TextPrimitive {
573    /// Tight block box: origin is the top-left of the first line's slot, size
574    /// is the measured size.
575    pub rect: Rect,
576    /// Shared so redrawing an unchanged string each frame clones a pointer
577    /// rather than the characters.
578    pub text: std::rc::Rc<str>,
579    pub style: TextStyle,
580    /// Text is filled with a single color: the glyph atlas path modulates one
581    /// vertex color per glyph. Gradient brushes are resolved to their first
582    /// stop by the draw scope, exactly like [`DrawScope::draw_vector_path`].
583    pub color: Color,
584}
585
586/// Returns a shared `Rc<str>` for `text`, reusing the copy made on an earlier
587/// frame when the content matches.
588///
589/// Apps hand `draw_text*` a `&str` every frame, and a score counter or label
590/// is the same characters frame after frame — without this pool every call
591/// copied them into a fresh `Rc<str>` anyway, defeating the sharing
592/// [`TextPrimitive::text`] exists for. Hits are verified by content, so a hash
593/// collision costs one fresh copy, never the wrong text. The pool clears
594/// itself when full; a live scene re-warms within one frame.
595fn shared_text_str(text: &str) -> Rc<str> {
596    use std::cell::RefCell;
597    use std::collections::HashMap;
598    use std::hash::{Hash, Hasher};
599
600    const POOL_CAPACITY: usize = 256;
601    thread_local! {
602        static POOL: RefCell<HashMap<u64, Rc<str>>> = RefCell::new(HashMap::new());
603    }
604
605    let mut hasher = crate::FxHasher::default();
606    text.hash(&mut hasher);
607    let key = hasher.finish();
608
609    POOL.with(|pool| {
610        let mut pool = pool.borrow_mut();
611        if let Some(shared) = pool.get(&key) {
612            if &**shared == text {
613                return Rc::clone(shared);
614            }
615        }
616        let shared: Rc<str> = Rc::from(text);
617        if pool.len() >= POOL_CAPACITY {
618            pool.clear();
619        }
620        pool.insert(key, Rc::clone(&shared));
621        shared
622    })
623}
624
625/// Describes a shadow to be rendered. Each renderer chooses how to blur.
626#[derive(Clone, Debug, PartialEq)]
627pub enum ShadowPrimitive {
628    /// Drop shadow: render shape behind content, blurred. `cutout` knocks
629    /// the element's own (unoffset) shape out of the silhouette before the
630    /// blur so translucent surfaces never sample their own shadow.
631    Drop {
632        shape: Box<DrawPrimitive>,
633        cutout: Option<Box<DrawPrimitive>>,
634        blur_radius: f32,
635        blend_mode: BlendMode,
636    },
637    /// Inner shadow: render fill + cutout to offscreen, blur, clip to bounds.
638    Inner {
639        fill: Box<DrawPrimitive>,
640        cutout: Box<DrawPrimitive>,
641        blur_radius: f32,
642        blend_mode: BlendMode,
643        /// Element bounds — blurred result must be clipped here.
644        clip_rect: Rect,
645    },
646}
647
648pub trait DrawScope {
649    fn size(&self) -> Size;
650    fn draw_content(&mut self);
651    fn draw_rect(&mut self, brush: Brush);
652    fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode);
653    /// Draws a rectangle at the specified position and size.
654    fn draw_rect_at(&mut self, rect: Rect, brush: Brush);
655    fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode);
656    fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii);
657    fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode);
658    /// Draws a rounded rectangle at the specified position and size.
659    fn draw_round_rect_at(&mut self, rect: Rect, brush: Brush, radii: CornerRadii);
660    fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32);
661    fn draw_circle_blend(
662        &mut self,
663        brush: Brush,
664        center: Point,
665        radius: f32,
666        blend_mode: BlendMode,
667    );
668
669    // ── Stroked outlines ────────────────────────────────────────────────────
670    //
671    // Strokes are *centered* on the geometry: a `width`-wide stroke covers
672    // `width / 2` inside and `width / 2` outside the path, like Skia and
673    // Jetpack Compose. A non-positive or non-finite width draws nothing.
674
675    /// Strokes the outline of the whole scope rect.
676    fn draw_rect_stroked(&mut self, brush: Brush, stroke: Stroke);
677    fn draw_rect_stroked_blend(&mut self, brush: Brush, stroke: Stroke, blend_mode: BlendMode);
678    /// Strokes the outline of `rect`.
679    fn draw_rect_at_stroked(&mut self, rect: Rect, brush: Brush, stroke: Stroke);
680    fn draw_rect_at_stroked_blend(
681        &mut self,
682        rect: Rect,
683        brush: Brush,
684        stroke: Stroke,
685        blend_mode: BlendMode,
686    );
687    /// Strokes the outline of the whole scope rect with rounded corners.
688    fn draw_round_rect_stroked(&mut self, brush: Brush, radii: CornerRadii, stroke: Stroke);
689    fn draw_round_rect_stroked_blend(
690        &mut self,
691        brush: Brush,
692        radii: CornerRadii,
693        stroke: Stroke,
694        blend_mode: BlendMode,
695    );
696    /// Strokes the outline of `rect` with rounded corners.
697    fn draw_round_rect_at_stroked(
698        &mut self,
699        rect: Rect,
700        brush: Brush,
701        radii: CornerRadii,
702        stroke: Stroke,
703    );
704    #[allow(clippy::too_many_arguments)]
705    fn draw_round_rect_at_stroked_blend(
706        &mut self,
707        rect: Rect,
708        brush: Brush,
709        radii: CornerRadii,
710        stroke: Stroke,
711        blend_mode: BlendMode,
712    );
713    /// Strokes a circle outline. Lowers to a stroked rounded rect, so it shares
714    /// the fill pipeline and batches with every other shape.
715    fn draw_circle_stroked(&mut self, brush: Brush, center: Point, radius: f32, stroke: Stroke);
716    fn draw_circle_stroked_blend(
717        &mut self,
718        brush: Brush,
719        center: Point,
720        radius: f32,
721        stroke: Stroke,
722        blend_mode: BlendMode,
723    );
724
725    // ── Arcs ────────────────────────────────────────────────────────────────
726
727    /// Strokes a circular arc.
728    ///
729    /// Angles are in **radians**, `0` points along **+X**, and increasing
730    /// angles sweep **clockwise on screen** (Cranpose uses y-down device
731    /// coordinates, so this matches `atan2(dy, dx)` and the sweep-gradient
732    /// brush). A negative `sweep_angle` sweeps counter-clockwise; `|sweep| >=
733    /// 2π` draws a closed ring.
734    ///
735    /// The stroke is centered on `radius`, so the band covers
736    /// `radius ± width/2`. [`StrokeCap`](crate::StrokeCap) shapes the two ends.
737    /// Nothing is drawn for a zero sweep, a non-positive width, or non-finite
738    /// input.
739    #[allow(clippy::too_many_arguments)]
740    fn draw_arc(
741        &mut self,
742        brush: Brush,
743        center: Point,
744        radius: f32,
745        start_angle: f32,
746        sweep_angle: f32,
747        stroke: Stroke,
748    );
749    #[allow(clippy::too_many_arguments)]
750    fn draw_arc_blend(
751        &mut self,
752        brush: Brush,
753        center: Point,
754        radius: f32,
755        start_angle: f32,
756        sweep_angle: f32,
757        stroke: Stroke,
758        blend_mode: BlendMode,
759    );
760
761    /// Fills an annular sector — the region between `inner_radius` and
762    /// `outer_radius`, limited to an angular sweep, with **flat radial ends**.
763    ///
764    /// This is the shape a stroked arc cannot express: its ends are straight
765    /// lines through the center, not caps. `inner_radius = 0` fills a pie
766    /// wedge. Angle convention is identical to [`draw_arc`](Self::draw_arc).
767    /// Nothing is drawn when `inner_radius >= outer_radius`, the sweep is zero,
768    /// or any input is non-finite.
769    #[allow(clippy::too_many_arguments)]
770    fn draw_annular_sector(
771        &mut self,
772        brush: Brush,
773        center: Point,
774        inner_radius: f32,
775        outer_radius: f32,
776        start_angle: f32,
777        sweep_angle: f32,
778    );
779    #[allow(clippy::too_many_arguments)]
780    fn draw_annular_sector_blend(
781        &mut self,
782        brush: Brush,
783        center: Point,
784        inner_radius: f32,
785        outer_radius: f32,
786        start_angle: f32,
787        sweep_angle: f32,
788        blend_mode: BlendMode,
789    );
790
791    fn draw_image(&mut self, image: ImageBitmap);
792    fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode);
793    fn draw_image_at(
794        &mut self,
795        rect: Rect,
796        image: ImageBitmap,
797        alpha: f32,
798        color_filter: Option<ColorFilter>,
799    );
800    fn draw_image_at_sampled(
801        &mut self,
802        rect: Rect,
803        image: ImageBitmap,
804        alpha: f32,
805        color_filter: Option<ColorFilter>,
806        sampling: ImageSampling,
807    );
808    fn draw_image_at_blend(
809        &mut self,
810        rect: Rect,
811        image: ImageBitmap,
812        alpha: f32,
813        color_filter: Option<ColorFilter>,
814        blend_mode: BlendMode,
815    );
816    /// Draws a sub-region of an image. `src_rect` is in image-pixel
817    /// coordinates; `dst_rect` is in scope coordinates.
818    fn draw_image_src(
819        &mut self,
820        image: ImageBitmap,
821        src_rect: Rect,
822        dst_rect: Rect,
823        alpha: f32,
824        color_filter: Option<ColorFilter>,
825    );
826    fn draw_image_src_sampled(
827        &mut self,
828        image: ImageBitmap,
829        src_rect: Rect,
830        dst_rect: Rect,
831        alpha: f32,
832        color_filter: Option<ColorFilter>,
833        sampling: ImageSampling,
834    );
835    fn draw_image_src_blend(
836        &mut self,
837        image: ImageBitmap,
838        src_rect: Rect,
839        dst_rect: Rect,
840        alpha: f32,
841        color_filter: Option<ColorFilter>,
842        blend_mode: BlendMode,
843    );
844    /// Fills a parsed SVG path in scope coordinates (path units are dp).
845    ///
846    /// The fill is rasterized on the CPU into a supersampled, anti-aliased
847    /// bitmap covering the path bounds and drawn as an image primitive, so
848    /// it works on every render backend. Parse the path once with
849    /// [`crate::VectorPath::parse`] and redraw it per frame. Solid brushes
850    /// are honored exactly; gradient brushes currently fall back to their
851    /// first stop color.
852    fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush);
853    /// Parses SVG path data (the `d` attribute syntax: `M/m L/l H/h V/v
854    /// C/c S/s Q/q T/t A/a Z/z`) and fills it. Invalid path data draws
855    /// nothing. Prefer [`crate::VectorPath::parse`] +
856    /// [`draw_vector_path`](Self::draw_vector_path) to avoid re-parsing
857    /// and to surface parse errors.
858    fn draw_svg_path(&mut self, d: &str, brush: Brush) {
859        if let Ok(path) = crate::VectorPath::parse(d) {
860            self.draw_vector_path(&path, brush);
861        }
862    }
863
864    // ── Text ────────────────────────────────────────────────────────────────
865    //
866    // Text is the one primitive a draw scope cannot resolve on its own: it
867    // needs fonts, which live above this crate. Measurement is therefore
868    // delegated to whatever the UI layer installed on the scope, and the same
869    // measurement decides where `draw_text*` puts the glyphs — so a caller that
870    // centers text from `measure_text` and the renderer that rasterizes it are
871    // reading the same numbers.
872    //
873    // There is deliberately no `_blend` variant: the text draw path has no
874    // blend-mode channel (glyphs are composited `SrcOver` against the atlas
875    // coverage mask), so a blended overload could only lie about what it does.
876
877    /// The block size, line height and first baseline `text` would occupy in
878    /// `style`.
879    ///
880    /// Free to call repeatedly: the underlying text stack caches metrics on
881    /// `(text, style)`, so a game can measure every label every frame to center
882    /// it without touching a font file more than once.
883    fn measure_text(&self, text: &str, style: &TextStyle) -> TextMeasurement;
884
885    /// Draws `text` inside the whole scope rect, positioned by
886    /// [`TextStyle::align`] and [`TextStyle::vertical_align`].
887    fn draw_text(&mut self, brush: Brush, text: &str, style: &TextStyle) {
888        self.draw_text_at(Rect::from_size(self.size()), brush, text, style);
889    }
890
891    /// Draws `text` inside `rect`, positioned by [`TextStyle::align`] and
892    /// [`TextStyle::vertical_align`].
893    ///
894    /// The glyphs are *not* clipped to `rect` — it is an alignment box, not a
895    /// viewport. A `rect` narrower than the measured text overflows in the
896    /// direction the alignment implies; clip the layer if that matters.
897    fn draw_text_at(&mut self, rect: Rect, brush: Brush, text: &str, style: &TextStyle);
898
899    /// Draws `text` with the top-left corner of its block at `top_left`.
900    ///
901    /// Alignment is a no-op here because the box is the measurement — this is
902    /// the "I already know where it goes" form, and the one to pair with
903    /// [`measure_text`](Self::measure_text) for hand-rolled centering.
904    fn draw_text_from(&mut self, top_left: Point, brush: Brush, text: &str, style: &TextStyle) {
905        if text.is_empty() {
906            return;
907        }
908        let measurement = self.measure_text(text, style);
909        self.draw_text_at(
910            Rect::from_origin_size(top_left, measurement.size),
911            brush,
912            text,
913            &TextStyle {
914                align: TextAlign::Left,
915                vertical_align: TextVerticalAlign::Top,
916                ..style.clone()
917            },
918        );
919    }
920
921    fn into_primitives(self) -> Vec<DrawPrimitive>;
922}
923
924/// Resolves the top-left corner a text block of `measurement` gets when it is
925/// aligned inside `rect`.
926///
927/// Split out so the placement rule is stated once and can be unit-tested
928/// against the measurement it is derived from.
929pub fn align_text_block(rect: Rect, measurement: TextMeasurement, style: &TextStyle) -> Point {
930    let x = match style.align {
931        TextAlign::Left => rect.x,
932        TextAlign::Center => rect.x + (rect.width - measurement.size.width) * 0.5,
933        TextAlign::Right => rect.x + rect.width - measurement.size.width,
934    };
935    let y = match style.vertical_align {
936        TextVerticalAlign::Top => rect.y,
937        TextVerticalAlign::Center => rect.y + (rect.height - measurement.size.height) * 0.5,
938        TextVerticalAlign::Bottom => rect.y + rect.height - measurement.size.height,
939        TextVerticalAlign::Baseline => rect.y - measurement.first_baseline,
940    };
941    Point::new(x, y)
942}
943
944/// Which typed store holds one recorded draw. The tape preserves the exact
945/// draw order across the per-kind stores.
946#[derive(Clone, Copy, Debug, PartialEq, Eq)]
947#[repr(u8)]
948pub(crate) enum RecordKind {
949    SolidRect,
950    SolidRoundRect,
951    SolidArc,
952    Other,
953}
954
955/// Tagged reference to one recorded draw: kind in the top 2 bits, index into
956/// that kind's typed store in the low 30. Four bytes per entry buys direct
957/// dispatch everywhere a tape range is consumed — no per-kind cursor walks,
958/// no per-frame view tables (sol P4a).
959#[derive(Clone, Copy, Debug, PartialEq, Eq)]
960pub(crate) struct TapeRef(u32);
961
962impl TapeRef {
963    const KIND_SHIFT: u32 = 30;
964    const INDEX_MASK: u32 = (1 << Self::KIND_SHIFT) - 1;
965
966    pub(crate) fn new(kind: RecordKind, index: usize) -> Self {
967        debug_assert!(index < (1usize << Self::KIND_SHIFT));
968        Self(((kind as u32) << Self::KIND_SHIFT) | index as u32)
969    }
970
971    pub(crate) fn kind(self) -> RecordKind {
972        match self.0 >> Self::KIND_SHIFT {
973            0 => RecordKind::SolidRect,
974            1 => RecordKind::SolidRoundRect,
975            2 => RecordKind::SolidArc,
976            _ => RecordKind::Other,
977        }
978    }
979
980    pub(crate) fn index(self) -> usize {
981        (self.0 & Self::INDEX_MASK) as usize
982    }
983
984    /// The packed word itself. Because per-store indices appear on the tape
985    /// in strictly increasing order (see [`CommandRecording::tape`]), `d`
986    /// consecutive tape entries are one kind's run exactly when
987    /// `tape[p + d].raw() == tape[p].raw() + d` — same kind bits, index
988    /// advanced by every step. Span verification decomposes tape ranges into
989    /// per-kind contiguous store runs on this single comparison.
990    pub(crate) fn raw(self) -> u32 {
991        self.0
992    }
993}
994
995/// A solid `SrcOver` rect, recorded as raw values.
996#[derive(Clone, Copy, Debug, PartialEq)]
997pub struct SolidRectRecord {
998    pub rect: Rect,
999    pub color: Color,
1000    pub stroke: Option<Stroke>,
1001}
1002
1003/// A solid `SrcOver` rounded rect (also the lowering of `draw_circle`),
1004/// recorded as raw values.
1005#[derive(Clone, Copy, Debug, PartialEq)]
1006pub struct SolidRoundRectRecord {
1007    pub rect: Rect,
1008    pub radii: CornerRadii,
1009    pub color: Color,
1010    pub stroke: Option<Stroke>,
1011}
1012
1013/// A solid `SrcOver` arc, recorded as the RAW draw parameters — before
1014/// band resolution, tight-bounds trigonometry, or the degeneracy check,
1015/// all of which happen at materialization. Retention verification must be
1016/// able to compare what the app said, ahead of everything deriving from it.
1017#[derive(Clone, Copy, Debug, PartialEq)]
1018pub struct SolidArcRecord {
1019    pub center: Point,
1020    pub radius: f32,
1021    pub start_angle: f32,
1022    pub sweep_angle: f32,
1023    pub inner_radius: f32,
1024    pub color: Color,
1025    pub stroke: Option<Stroke>,
1026}
1027
1028/// One draw command's recording in compact typed form: pure-numeric records
1029/// for the common solid shapes (no `Brush` destructor branch, roughly half
1030/// the bytes of the `DrawPrimitive` they materialize into) and ordinary
1031/// primitives for everything else, with `tape` preserving global order. The
1032/// index each tape entry carries into its per-kind store is the stable
1033/// compact handle for the rare resource-bearing entries (`others` holds
1034/// gradients, images, text, blends, and content markers whole).
1035#[derive(Clone, Debug, Default)]
1036pub struct CommandRecording {
1037    /// INVARIANT: per-store indices appear on the tape in strictly
1038    /// increasing order (0, 1, 2, ... per kind) — recording appends only.
1039    /// Sequential consumers (`finish`'s `others.drain(..)`) rely on it;
1040    /// random-access consumers use the index directly.
1041    pub(crate) tape: Vec<TapeRef>,
1042    pub(crate) rects: Vec<SolidRectRecord>,
1043    pub(crate) round_rects: Vec<SolidRoundRectRecord>,
1044    pub(crate) arcs: Vec<SolidArcRecord>,
1045    pub(crate) others: Vec<DrawPrimitive>,
1046}
1047
1048impl CommandRecording {
1049    /// Total recorded entries (the tape length).
1050    pub fn len(&self) -> usize {
1051        self.tape.len()
1052    }
1053
1054    /// Materializes one tape range into fresh primitives — the emergency
1055    /// path for a bypassed span whose retained draw fell through. `None`
1056    /// when the range does not lie within this recording (cleared buffers,
1057    /// stale range).
1058    pub fn materialize_range(
1059        &self,
1060        tape_start: usize,
1061        tape_end: usize,
1062    ) -> Option<Vec<DrawPrimitive>> {
1063        if tape_start > tape_end || tape_end > self.tape.len() {
1064            return None;
1065        }
1066        let mut out = Vec::with_capacity(tape_end - tape_start);
1067        for entry in &self.tape[tape_start..tape_end] {
1068            match entry.kind() {
1069                RecordKind::SolidRect => {
1070                    let record = self.rects.get(entry.index())?;
1071                    out.push(DrawPrimitive::Rect {
1072                        rect: record.rect,
1073                        brush: Brush::Solid(record.color),
1074                        stroke: record.stroke,
1075                    });
1076                }
1077                RecordKind::SolidRoundRect => {
1078                    let record = self.round_rects.get(entry.index())?;
1079                    out.push(DrawPrimitive::RoundRect {
1080                        rect: record.rect,
1081                        brush: Brush::Solid(record.color),
1082                        radii: record.radii,
1083                        stroke: record.stroke,
1084                    });
1085                }
1086                RecordKind::SolidArc => {
1087                    let record = self.arcs.get(entry.index())?;
1088                    if let Some(primitive) = materialize_solid_arc(record) {
1089                        out.push(primitive);
1090                    }
1091                }
1092                RecordKind::Other => {
1093                    out.push(self.others.get(entry.index())?.clone());
1094                }
1095            }
1096        }
1097        Some(out)
1098    }
1099
1100    pub fn is_empty(&self) -> bool {
1101        self.tape.is_empty()
1102    }
1103
1104    /// Test support: the identity of the tape's allocation, so buffer-reuse
1105    /// tests can assert that re-recording ping-pongs between the same two
1106    /// allocations instead of growing fresh ones every frame.
1107    #[doc(hidden)]
1108    pub fn tape_ptr(&self) -> *const u8 {
1109        self.tape.as_ptr() as *const u8
1110    }
1111
1112    fn clear(&mut self) {
1113        self.tape.clear();
1114        self.rects.clear();
1115        self.round_rects.clear();
1116        self.arcs.clear();
1117        self.others.clear();
1118    }
1119
1120    /// A buffer-reusing deep copy: `clone_from` on every store, so
1121    /// refreshing a long-lived recording (the replay snapshot, twice per
1122    /// convergence cycle on a heavy scene's ~17k-record tape) truncates and
1123    /// copies into the existing allocations instead of cloning five fresh
1124    /// buffers. Semantically identical to `*self = source.clone()`.
1125    pub(crate) fn clone_records_from(&mut self, source: &Self) {
1126        self.tape.clone_from(&source.tape);
1127        self.rects.clone_from(&source.rects);
1128        self.round_rects.clone_from(&source.round_rects);
1129        self.arcs.clone_from(&source.arcs);
1130        self.others.clone_from(&source.others);
1131    }
1132}
1133
1134/// What [`DrawScopeDefault::finish`] hands back: the materialized primitives,
1135/// the marker count that travels with them, and the recording buffers so a
1136/// retaining caller can lend them to the same command's next recording.
1137pub struct FinishedRecording {
1138    pub primitives: Vec<DrawPrimitive>,
1139    pub content_markers: u32,
1140    pub recording: CommandRecording,
1141    /// Tape indices that materialized to nothing (degenerate arcs), in
1142    /// ascending order — empty in practice. Consumers translating tape
1143    /// ranges into primitive ranges subtract the drops before each
1144    /// boundary.
1145    pub dropped: Vec<u32>,
1146}
1147
1148#[derive(Default)]
1149pub struct DrawScopeDefault {
1150    size: Size,
1151    /// The compact recording every draw call writes into; materialized into
1152    /// `out` once, when the scope finishes.
1153    rec: CommandRecording,
1154    /// Materialization target, owned by the consumer across frames (see
1155    /// [`Self::with_recording`]); untouched until [`Self::finish`].
1156    out: Vec<DrawPrimitive>,
1157    /// How many [`DrawPrimitive::Content`] markers this scope has recorded.
1158    /// Consumers splitting a command around its content would otherwise have
1159    /// to re-scan thousands of just-recorded primitives to learn "none".
1160    content_markers: u32,
1161    /// `None` falls back to [`estimate_text_measurement`]. Every scope the
1162    /// framework builds carries the app's real measurer; a hand-built one
1163    /// (tests, tooling) does not have to.
1164    text_measurer: Option<Rc<dyn DrawTextMeasurer>>,
1165}
1166
1167/// Per-thread memory of how many primitives the scope of a given size emitted
1168/// last time, keyed by the scope's size bits. Draw closures re-record every
1169/// frame, and a heavy animated canvas emits thousands of primitives; starting
1170/// its vector at the previous count skips the whole doubling schedule of
1171/// reallocations. Keying by size keeps a small HUD scope from inheriting the
1172/// arena's multi-thousand capacity.
1173const RECORDED_PRIMITIVE_COUNTS_LIMIT: usize = 64;
1174
1175thread_local! {
1176    static RECORDED_PRIMITIVE_COUNTS: std::cell::RefCell<std::collections::HashMap<(u32, u32), usize>> =
1177        std::cell::RefCell::new(std::collections::HashMap::new());
1178}
1179
1180fn recorded_primitive_capacity(size: Size) -> usize {
1181    RECORDED_PRIMITIVE_COUNTS.with(|counts| {
1182        counts
1183            .borrow()
1184            .get(&(size.width.to_bits(), size.height.to_bits()))
1185            .copied()
1186            .unwrap_or(0)
1187    })
1188}
1189
1190fn note_recorded_primitive_count(size: Size, count: usize) {
1191    RECORDED_PRIMITIVE_COUNTS.with(|counts| {
1192        let mut counts = counts.borrow_mut();
1193        if counts.len() >= RECORDED_PRIMITIVE_COUNTS_LIMIT {
1194            counts.clear();
1195        }
1196        counts.insert((size.width.to_bits(), size.height.to_bits()), count);
1197    });
1198}
1199
1200impl DrawScopeDefault {
1201    pub fn new(size: Size) -> Self {
1202        Self::with_recording(size, None, CommandRecording::default(), Vec::new())
1203    }
1204
1205    /// A scope that measures text with the app's fonts.
1206    ///
1207    /// The framework calls this for every draw closure it runs; `new` exists
1208    /// for callers that never draw text.
1209    pub fn with_text_measurer(size: Size, text_measurer: Rc<dyn DrawTextMeasurer>) -> Self {
1210        Self::with_recording(
1211            size,
1212            Some(text_measurer),
1213            CommandRecording::default(),
1214            Vec::new(),
1215        )
1216    }
1217
1218    /// Like [`with_text_measurer`](Self::with_text_measurer), but records
1219    /// into storage the caller already owns. This is the retained-recording
1220    /// path: a command that re-records every frame keeps one buffer whose
1221    /// capacity was earned on earlier frames, instead of walking a fresh
1222    /// vector through the whole doubling schedule again.
1223    pub fn with_text_measurer_reusing(
1224        size: Size,
1225        text_measurer: Rc<dyn DrawTextMeasurer>,
1226        storage: Vec<DrawPrimitive>,
1227    ) -> Self {
1228        Self::with_recording(
1229            size,
1230            Some(text_measurer),
1231            CommandRecording::default(),
1232            storage,
1233        )
1234    }
1235
1236    /// The full retained-recording form: compact recording buffers AND the
1237    /// materialization target both come from the caller, so a command that
1238    /// re-records every frame allocates nothing in the steady state.
1239    pub fn with_recording(
1240        size: Size,
1241        text_measurer: Option<Rc<dyn DrawTextMeasurer>>,
1242        mut recording: CommandRecording,
1243        out: Vec<DrawPrimitive>,
1244    ) -> Self {
1245        recording.clear();
1246        recording.tape.reserve(recorded_primitive_capacity(size));
1247        Self {
1248            size,
1249            rec: recording,
1250            out,
1251            content_markers: 0,
1252            text_measurer,
1253        }
1254    }
1255
1256    /// How many [`DrawPrimitive::Content`] markers this scope has recorded.
1257    /// Command consumers split placements around the count instead of
1258    /// re-scanning thousands of just-recorded primitives to learn "none".
1259    pub fn content_marker_count(&self) -> u32 {
1260        self.content_markers
1261    }
1262
1263    /// The compact recording as recorded so far. Retention verification
1264    /// reads this before materialization decides what to skip.
1265    pub fn recorded(&self) -> &CommandRecording {
1266        &self.rec
1267    }
1268
1269    /// Appends already-recorded primitives verbatim. This is the replay path
1270    /// for pre-built primitive lists (deferred modifier draws recorded
1271    /// earlier, synthesized commands in tests); the marker count stays
1272    /// authoritative because the batch is scanned once on the way in.
1273    pub fn push_recorded(&mut self, primitives: Vec<DrawPrimitive>) {
1274        self.content_markers += primitives
1275            .iter()
1276            .filter(|primitive| matches!(primitive, DrawPrimitive::Content))
1277            .count() as u32;
1278        let base = self.rec.others.len();
1279        self.rec
1280            .tape
1281            .extend((0..primitives.len()).map(|i| TapeRef::new(RecordKind::Other, base + i)));
1282        self.rec.others.extend(primitives);
1283    }
1284
1285    /// Materializes the recording into the `out` storage and hands both
1286    /// back, plus the compact buffers for the caller to retain. This — not
1287    /// recording — is where solid records become `DrawPrimitive`s and where
1288    /// arc bands, tight bounds, and the degeneracy drop happen, so the
1289    /// output is exactly what recording used to produce directly.
1290    pub fn finish(mut self) -> FinishedRecording {
1291        // Composition diagnostic for retention work: how much of a heavy
1292        // command is typed records vs ordinary primitives.
1293        if std::env::var_os("CRANPOSE_RECORD_MIX_DIAG").is_some() && self.rec.tape.len() > 400 {
1294            eprintln!(
1295                "[record-mix] tape={} rects={} round_rects={} arcs={} others={}",
1296                self.rec.tape.len(),
1297                self.rec.rects.len(),
1298                self.rec.round_rects.len(),
1299                self.rec.arcs.len(),
1300                self.rec.others.len(),
1301            );
1302        }
1303        let mut out = std::mem::take(&mut self.out);
1304        out.clear();
1305        out.reserve(self.rec.tape.len());
1306        let mut dropped: Vec<u32> = Vec::new();
1307        {
1308            // `others` moves out via drain; the increasing-index invariant
1309            // on the tape makes tape order equal drain order. Copy stores
1310            // are addressed directly by the entry's index.
1311            let mut others = self.rec.others.drain(..);
1312            for (tape_index, entry) in self.rec.tape.iter().enumerate() {
1313                match entry.kind() {
1314                    RecordKind::SolidRect => {
1315                        let record = &self.rec.rects[entry.index()];
1316                        out.push(DrawPrimitive::Rect {
1317                            rect: record.rect,
1318                            brush: Brush::Solid(record.color),
1319                            stroke: record.stroke,
1320                        });
1321                    }
1322                    RecordKind::SolidRoundRect => {
1323                        let record = &self.rec.round_rects[entry.index()];
1324                        out.push(DrawPrimitive::RoundRect {
1325                            rect: record.rect,
1326                            brush: Brush::Solid(record.color),
1327                            radii: record.radii,
1328                            stroke: record.stroke,
1329                        });
1330                    }
1331                    RecordKind::SolidArc => {
1332                        let record = &self.rec.arcs[entry.index()];
1333                        if let Some(primitive) = materialize_solid_arc(record) {
1334                            out.push(primitive);
1335                        } else {
1336                            dropped.push(tape_index as u32);
1337                        }
1338                    }
1339                    RecordKind::Other => {
1340                        out.push(others.next().expect("tape/others in sync"));
1341                    }
1342                }
1343            }
1344        }
1345        self.rec.clear();
1346        note_recorded_primitive_count(self.size, out.len());
1347        FinishedRecording {
1348            primitives: out,
1349            content_markers: self.content_markers,
1350            recording: self.rec,
1351            dropped,
1352        }
1353    }
1354
1355    /// Like [`Self::finish`], but materializing nothing: the consumer is
1356    /// about to re-emit a PREVIOUS frame's saved emission in place of this
1357    /// recording (the stale-transition serve on a replay collapse frame),
1358    /// so building this frame's primitives — the very cost the serve
1359    /// exists to skip — would be pure waste. The recording and
1360    /// materialization buffers still return, cleared exactly as
1361    /// [`Self::finish`] leaves them, so the command's steady-state
1362    /// ping-pong keeps its earned capacity.
1363    pub fn finish_recording_only(mut self) -> FinishedRecording {
1364        let mut out = std::mem::take(&mut self.out);
1365        out.clear();
1366        note_recorded_primitive_count(self.size, self.rec.tape.len());
1367        self.rec.clear();
1368        FinishedRecording {
1369            primitives: out,
1370            content_markers: self.content_markers,
1371            recording: self.rec,
1372            dropped: Vec::new(),
1373        }
1374    }
1375
1376    /// Like [`Self::finish`], but for a verified command: a retained span
1377    /// whose slot `bypass` approves is NOT materialized — its records cease
1378    /// to exist as per-frame primitives, which is the entire point of
1379    /// retention — and the replay frame comes back with primitive ranges
1380    /// assigned during the same single tape walk. Every other span
1381    /// materializes exactly as [`Self::finish`] would. `AllDynamic`
1382    /// degenerates to plain `finish`.
1383    pub fn finish_replay(
1384        mut self,
1385        center: Point,
1386        outcome: crate::record_replay::ReplayOutcome,
1387        bypass: &mut dyn FnMut(u32) -> bool,
1388    ) -> (
1389        FinishedRecording,
1390        Option<crate::record_replay::CommandReplayFrame>,
1391    ) {
1392        use crate::record_replay::{CommandReplayFrame, FrameSpan, ReplayOutcome, ReplaySpan};
1393        let ReplayOutcome::Spans(replay_spans) = outcome else {
1394            return (self.finish(), None);
1395        };
1396        let tape_len = self.rec.tape.len();
1397        let mut out = std::mem::take(&mut self.out);
1398        out.clear();
1399        let mut dropped: Vec<u32> = Vec::new();
1400        let mut spans: Vec<FrameSpan> = Vec::with_capacity(replay_spans.len());
1401        let mut any_retained = false;
1402        {
1403            // `others` moves out via drain (tape order equals drain order by
1404            // the increasing-index invariant); Copy stores are addressed
1405            // directly by each entry's index, so a bypassed span costs
1406            // nothing to step past.
1407            let mut others = self.rec.others.drain(..);
1408            let tape = &self.rec.tape;
1409            let rects = &self.rec.rects;
1410            let round_rects = &self.rec.round_rects;
1411            let arcs = &self.rec.arcs;
1412            // Materializes one contiguous tape range into `out`. Kept as a
1413            // macro so `out`, `dropped`, and the `others` cursor stay plain
1414            // locals the borrow checker can split by field.
1415            macro_rules! materialize_range {
1416                ($start:expr, $end:expr) => {{
1417                    let prim_start = out.len() as u32;
1418                    for tape_index in $start..$end {
1419                        let entry = tape[tape_index];
1420                        match entry.kind() {
1421                            RecordKind::SolidRect => {
1422                                let record = &rects[entry.index()];
1423                                out.push(DrawPrimitive::Rect {
1424                                    rect: record.rect,
1425                                    brush: Brush::Solid(record.color),
1426                                    stroke: record.stroke,
1427                                });
1428                            }
1429                            RecordKind::SolidRoundRect => {
1430                                let record = &round_rects[entry.index()];
1431                                out.push(DrawPrimitive::RoundRect {
1432                                    rect: record.rect,
1433                                    brush: Brush::Solid(record.color),
1434                                    radii: record.radii,
1435                                    stroke: record.stroke,
1436                                });
1437                            }
1438                            RecordKind::SolidArc => {
1439                                let record = &arcs[entry.index()];
1440                                if let Some(primitive) = materialize_solid_arc(record) {
1441                                    out.push(primitive);
1442                                } else {
1443                                    dropped.push(tape_index as u32);
1444                                }
1445                            }
1446                            RecordKind::Other => {
1447                                out.push(others.next().expect("tape/others in sync"));
1448                            }
1449                        }
1450                    }
1451                    (prim_start, out.len() as u32)
1452                }};
1453            }
1454            for span in replay_spans {
1455                match span {
1456                    ReplaySpan::Dynamic {
1457                        tape_start,
1458                        tape_end,
1459                    } => {
1460                        let range = materialize_range!(tape_start, tape_end);
1461                        if range.1 > range.0 {
1462                            spans.push(FrameSpan::Dynamic { range });
1463                        }
1464                    }
1465                    ReplaySpan::Retained {
1466                        slot,
1467                        capture,
1468                        slot_offset,
1469                        tape_start,
1470                        tape_end,
1471                        transform,
1472                        recolors,
1473                        bounds,
1474                    } => {
1475                        // A retained span holds solid arcs and circles by
1476                        // construction; anything else in its range means
1477                        // the ordinary path must draw it.
1478                        let compact = tape[tape_start..tape_end]
1479                            .iter()
1480                            .all(|entry| entry.kind() != RecordKind::Other);
1481                        if compact && !capture && bypass(slot) {
1482                            // The bypass: the records are never materialized
1483                            // — direct indexing leaves nothing to advance.
1484                            // Capture guaranteed each record one clean
1485                            // shape, so no drop tracking is needed on the
1486                            // way past.
1487                            any_retained = true;
1488                            let position = out.len() as u32;
1489                            spans.push(FrameSpan::Retained {
1490                                slot,
1491                                capture: false,
1492                                slot_offset: slot_offset as u32,
1493                                range: (position, position),
1494                                tape_range: (tape_start as u32, tape_end as u32),
1495                                transform,
1496                                recolors,
1497                                bounds,
1498                            });
1499                            continue;
1500                        }
1501                        let drops_before = dropped.len();
1502                        let range = materialize_range!(tape_start, tape_end);
1503                        if compact && dropped.len() == drops_before {
1504                            any_retained = true;
1505                            spans.push(FrameSpan::Retained {
1506                                slot,
1507                                capture,
1508                                slot_offset: slot_offset as u32,
1509                                range,
1510                                tape_range: (tape_start as u32, tape_end as u32),
1511                                transform,
1512                                recolors,
1513                                bounds,
1514                            });
1515                        } else if range.1 > range.0 {
1516                            spans.push(FrameSpan::Dynamic { range });
1517                        }
1518                    }
1519                }
1520            }
1521        }
1522        // Deliberately NOT cleared: the typed stores must survive until the
1523        // renderer has drawn this frame, so a bypassed span that cannot be
1524        // drawn retained (context drift, op cap) can still be materialized
1525        // on demand from the recording — which the consumer publishes and
1526        // pins to the frame as its owned `fallback`. The next recording's
1527        // scope clears the buffers on construction anyway.
1528        note_recorded_primitive_count(self.size, tape_len);
1529        // `fallback` is attached by the consumer once the recording is
1530        // published under its shared handle — the recording is still owned
1531        // by value here.
1532        let frame = any_retained.then_some(CommandReplayFrame {
1533            center,
1534            spans,
1535            fallback: None,
1536        });
1537        (
1538            FinishedRecording {
1539                primitives: out,
1540                content_markers: self.content_markers,
1541                recording: self.rec,
1542                dropped,
1543            },
1544            frame,
1545        )
1546    }
1547
1548    fn push_other(&mut self, primitive: DrawPrimitive) {
1549        let idx = self.rec.others.len();
1550        self.rec.tape.push(TapeRef::new(RecordKind::Other, idx));
1551        self.rec.others.push(primitive);
1552    }
1553
1554    fn push_blended_primitive(&mut self, primitive: DrawPrimitive, blend_mode: BlendMode) {
1555        if blend_mode != BlendMode::SrcOver {
1556            self.push_other(DrawPrimitive::Blend {
1557                primitive: Box::new(primitive),
1558                blend_mode,
1559            });
1560            return;
1561        }
1562        match primitive {
1563            DrawPrimitive::Rect {
1564                rect,
1565                brush: Brush::Solid(color),
1566                stroke,
1567            } => {
1568                let idx = self.rec.rects.len();
1569                self.rec.tape.push(TapeRef::new(RecordKind::SolidRect, idx));
1570                self.rec.rects.push(SolidRectRecord {
1571                    rect,
1572                    color,
1573                    stroke,
1574                });
1575            }
1576            DrawPrimitive::RoundRect {
1577                rect,
1578                brush: Brush::Solid(color),
1579                radii,
1580                stroke,
1581            } => {
1582                let idx = self.rec.round_rects.len();
1583                self.rec
1584                    .tape
1585                    .push(TapeRef::new(RecordKind::SolidRoundRect, idx));
1586                self.rec.round_rects.push(SolidRoundRectRecord {
1587                    rect,
1588                    radii,
1589                    color,
1590                    stroke,
1591                });
1592            }
1593            other => self.push_other(other),
1594        }
1595    }
1596
1597    /// Shared lowering for [`DrawScope::draw_arc`] and
1598    /// [`DrawScope::draw_annular_sector`].
1599    ///
1600    /// Resolves the band, computes the *tight* bounding box (caps included) and
1601    /// drops degenerate geometry on the floor instead of emitting NaN-bearing
1602    /// primitives the renderers would have to defend against.
1603    #[allow(clippy::too_many_arguments)]
1604    fn push_arc(
1605        &mut self,
1606        brush: Brush,
1607        center: Point,
1608        radius: f32,
1609        start_angle: f32,
1610        sweep_angle: f32,
1611        stroke: Option<Stroke>,
1612        inner_radius: f32,
1613        blend_mode: BlendMode,
1614    ) {
1615        // The common case records raw parameters only; band resolution,
1616        // tight bounds, and the degeneracy drop run at materialization
1617        // (see [`materialize_solid_arc`]), producing identical output.
1618        if blend_mode == BlendMode::SrcOver {
1619            if let Brush::Solid(color) = brush {
1620                let idx = self.rec.arcs.len();
1621                self.rec.tape.push(TapeRef::new(RecordKind::SolidArc, idx));
1622                self.rec.arcs.push(SolidArcRecord {
1623                    center,
1624                    radius,
1625                    start_angle,
1626                    sweep_angle,
1627                    inner_radius,
1628                    color,
1629                    stroke,
1630                });
1631                return;
1632            }
1633        }
1634        let (band_inner, band_outer, cap) = arc_band(radius, inner_radius, stroke);
1635        let geometry = ArcGeometry::new(
1636            center,
1637            band_inner,
1638            band_outer,
1639            start_angle,
1640            sweep_angle,
1641            cap,
1642        );
1643        if geometry.is_degenerate() {
1644            return;
1645        }
1646        self.push_blended_primitive(
1647            DrawPrimitive::Arc {
1648                rect: geometry.bounds(),
1649                brush,
1650                center,
1651                radius,
1652                start_angle,
1653                sweep_angle,
1654                stroke,
1655                inner_radius,
1656            },
1657            blend_mode,
1658        );
1659    }
1660}
1661
1662/// The deferred half of the solid-arc fast path: exactly the lowering
1663/// [`DrawScopeDefault::push_arc`] applies to every other arc, run when the
1664/// recording materializes instead of when the app draws. `None` is the
1665/// degenerate drop.
1666fn materialize_solid_arc(record: &SolidArcRecord) -> Option<DrawPrimitive> {
1667    let (band_inner, band_outer, cap) = arc_band(record.radius, record.inner_radius, record.stroke);
1668    let geometry = ArcGeometry::new(
1669        record.center,
1670        band_inner,
1671        band_outer,
1672        record.start_angle,
1673        record.sweep_angle,
1674        cap,
1675    );
1676    if geometry.is_degenerate() {
1677        return None;
1678    }
1679    Some(DrawPrimitive::Arc {
1680        rect: geometry.bounds(),
1681        brush: Brush::Solid(record.color),
1682        center: record.center,
1683        radius: record.radius,
1684        start_angle: record.start_angle,
1685        sweep_angle: record.sweep_angle,
1686        stroke: record.stroke,
1687        inner_radius: record.inner_radius,
1688    })
1689}
1690
1691impl DrawScope for DrawScopeDefault {
1692    fn size(&self) -> Size {
1693        self.size
1694    }
1695
1696    fn draw_content(&mut self) {
1697        self.content_markers += 1;
1698        self.push_other(DrawPrimitive::Content);
1699    }
1700
1701    fn draw_rect(&mut self, brush: Brush) {
1702        self.draw_rect_blend(brush, BlendMode::SrcOver);
1703    }
1704
1705    fn draw_rect_blend(&mut self, brush: Brush, blend_mode: BlendMode) {
1706        self.push_blended_primitive(
1707            DrawPrimitive::Rect {
1708                rect: Rect::from_size(self.size),
1709                brush,
1710                stroke: None,
1711            },
1712            blend_mode,
1713        );
1714    }
1715
1716    fn draw_rect_at(&mut self, rect: Rect, brush: Brush) {
1717        self.draw_rect_at_blend(rect, brush, BlendMode::SrcOver);
1718    }
1719
1720    fn draw_rect_at_blend(&mut self, rect: Rect, brush: Brush, blend_mode: BlendMode) {
1721        self.push_blended_primitive(
1722            DrawPrimitive::Rect {
1723                rect,
1724                brush,
1725                stroke: None,
1726            },
1727            blend_mode,
1728        );
1729    }
1730
1731    fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii) {
1732        self.draw_round_rect_blend(brush, radii, BlendMode::SrcOver);
1733    }
1734
1735    fn draw_round_rect_blend(&mut self, brush: Brush, radii: CornerRadii, blend_mode: BlendMode) {
1736        self.push_blended_primitive(
1737            DrawPrimitive::RoundRect {
1738                rect: Rect::from_size(self.size),
1739                brush,
1740                radii,
1741                stroke: None,
1742            },
1743            blend_mode,
1744        );
1745    }
1746
1747    fn draw_round_rect_at(&mut self, rect: Rect, brush: Brush, radii: CornerRadii) {
1748        self.push_blended_primitive(
1749            DrawPrimitive::RoundRect {
1750                rect,
1751                brush,
1752                radii,
1753                stroke: None,
1754            },
1755            BlendMode::SrcOver,
1756        );
1757    }
1758
1759    fn draw_rect_stroked(&mut self, brush: Brush, stroke: Stroke) {
1760        self.draw_rect_stroked_blend(brush, stroke, BlendMode::SrcOver);
1761    }
1762
1763    fn draw_rect_stroked_blend(&mut self, brush: Brush, stroke: Stroke, blend_mode: BlendMode) {
1764        self.draw_rect_at_stroked_blend(Rect::from_size(self.size), brush, stroke, blend_mode);
1765    }
1766
1767    fn draw_rect_at_stroked(&mut self, rect: Rect, brush: Brush, stroke: Stroke) {
1768        self.draw_rect_at_stroked_blend(rect, brush, stroke, BlendMode::SrcOver);
1769    }
1770
1771    fn draw_rect_at_stroked_blend(
1772        &mut self,
1773        rect: Rect,
1774        brush: Brush,
1775        stroke: Stroke,
1776        blend_mode: BlendMode,
1777    ) {
1778        if !stroke.is_visible() {
1779            return;
1780        }
1781        self.push_blended_primitive(
1782            DrawPrimitive::Rect {
1783                rect,
1784                brush,
1785                stroke: Some(stroke),
1786            },
1787            blend_mode,
1788        );
1789    }
1790
1791    fn draw_round_rect_stroked(&mut self, brush: Brush, radii: CornerRadii, stroke: Stroke) {
1792        self.draw_round_rect_stroked_blend(brush, radii, stroke, BlendMode::SrcOver);
1793    }
1794
1795    fn draw_round_rect_stroked_blend(
1796        &mut self,
1797        brush: Brush,
1798        radii: CornerRadii,
1799        stroke: Stroke,
1800        blend_mode: BlendMode,
1801    ) {
1802        self.draw_round_rect_at_stroked_blend(
1803            Rect::from_size(self.size),
1804            brush,
1805            radii,
1806            stroke,
1807            blend_mode,
1808        );
1809    }
1810
1811    fn draw_round_rect_at_stroked(
1812        &mut self,
1813        rect: Rect,
1814        brush: Brush,
1815        radii: CornerRadii,
1816        stroke: Stroke,
1817    ) {
1818        self.draw_round_rect_at_stroked_blend(rect, brush, radii, stroke, BlendMode::SrcOver);
1819    }
1820
1821    fn draw_round_rect_at_stroked_blend(
1822        &mut self,
1823        rect: Rect,
1824        brush: Brush,
1825        radii: CornerRadii,
1826        stroke: Stroke,
1827        blend_mode: BlendMode,
1828    ) {
1829        if !stroke.is_visible() {
1830            return;
1831        }
1832        self.push_blended_primitive(
1833            DrawPrimitive::RoundRect {
1834                rect,
1835                brush,
1836                radii,
1837                stroke: Some(stroke),
1838            },
1839            blend_mode,
1840        );
1841    }
1842
1843    fn draw_circle_stroked(&mut self, brush: Brush, center: Point, radius: f32, stroke: Stroke) {
1844        self.draw_circle_stroked_blend(brush, center, radius, stroke, BlendMode::SrcOver);
1845    }
1846
1847    fn draw_circle_stroked_blend(
1848        &mut self,
1849        brush: Brush,
1850        center: Point,
1851        radius: f32,
1852        stroke: Stroke,
1853        blend_mode: BlendMode,
1854    ) {
1855        if !stroke.is_visible() || !radius.is_finite() {
1856            return;
1857        }
1858        let radius = radius.max(0.0);
1859        let diameter = radius * 2.0;
1860        self.draw_round_rect_at_stroked_blend(
1861            Rect {
1862                x: center.x - radius,
1863                y: center.y - radius,
1864                width: diameter,
1865                height: diameter,
1866            },
1867            brush,
1868            CornerRadii::uniform(radius),
1869            stroke,
1870            blend_mode,
1871        );
1872    }
1873
1874    fn draw_arc(
1875        &mut self,
1876        brush: Brush,
1877        center: Point,
1878        radius: f32,
1879        start_angle: f32,
1880        sweep_angle: f32,
1881        stroke: Stroke,
1882    ) {
1883        self.draw_arc_blend(
1884            brush,
1885            center,
1886            radius,
1887            start_angle,
1888            sweep_angle,
1889            stroke,
1890            BlendMode::SrcOver,
1891        );
1892    }
1893
1894    fn draw_arc_blend(
1895        &mut self,
1896        brush: Brush,
1897        center: Point,
1898        radius: f32,
1899        start_angle: f32,
1900        sweep_angle: f32,
1901        stroke: Stroke,
1902        blend_mode: BlendMode,
1903    ) {
1904        if !stroke.is_visible() {
1905            return;
1906        }
1907        self.push_arc(
1908            brush,
1909            center,
1910            radius,
1911            start_angle,
1912            sweep_angle,
1913            Some(stroke),
1914            0.0,
1915            blend_mode,
1916        );
1917    }
1918
1919    fn draw_annular_sector(
1920        &mut self,
1921        brush: Brush,
1922        center: Point,
1923        inner_radius: f32,
1924        outer_radius: f32,
1925        start_angle: f32,
1926        sweep_angle: f32,
1927    ) {
1928        self.draw_annular_sector_blend(
1929            brush,
1930            center,
1931            inner_radius,
1932            outer_radius,
1933            start_angle,
1934            sweep_angle,
1935            BlendMode::SrcOver,
1936        );
1937    }
1938
1939    fn draw_annular_sector_blend(
1940        &mut self,
1941        brush: Brush,
1942        center: Point,
1943        inner_radius: f32,
1944        outer_radius: f32,
1945        start_angle: f32,
1946        sweep_angle: f32,
1947        blend_mode: BlendMode,
1948    ) {
1949        self.push_arc(
1950            brush,
1951            center,
1952            outer_radius,
1953            start_angle,
1954            sweep_angle,
1955            None,
1956            inner_radius,
1957            blend_mode,
1958        );
1959    }
1960
1961    fn draw_circle(&mut self, brush: Brush, center: Point, radius: f32) {
1962        self.draw_circle_blend(brush, center, radius, BlendMode::SrcOver);
1963    }
1964
1965    fn draw_circle_blend(
1966        &mut self,
1967        brush: Brush,
1968        center: Point,
1969        radius: f32,
1970        blend_mode: BlendMode,
1971    ) {
1972        let radius = radius.max(0.0);
1973        let diameter = radius * 2.0;
1974        self.push_blended_primitive(
1975            DrawPrimitive::RoundRect {
1976                rect: Rect {
1977                    x: center.x - radius,
1978                    y: center.y - radius,
1979                    width: diameter,
1980                    height: diameter,
1981                },
1982                brush,
1983                radii: CornerRadii::uniform(radius),
1984                stroke: None,
1985            },
1986            blend_mode,
1987        );
1988    }
1989
1990    fn draw_image(&mut self, image: ImageBitmap) {
1991        self.draw_image_blend(image, BlendMode::SrcOver);
1992    }
1993
1994    fn draw_image_blend(&mut self, image: ImageBitmap, blend_mode: BlendMode) {
1995        self.push_blended_primitive(
1996            DrawPrimitive::Image {
1997                rect: Rect::from_size(self.size),
1998                image,
1999                alpha: 1.0,
2000                color_filter: None,
2001                sampling: ImageSampling::Nearest,
2002                src_rect: None,
2003            },
2004            blend_mode,
2005        );
2006    }
2007
2008    fn draw_image_at(
2009        &mut self,
2010        rect: Rect,
2011        image: ImageBitmap,
2012        alpha: f32,
2013        color_filter: Option<ColorFilter>,
2014    ) {
2015        self.draw_image_at_sampled(rect, image, alpha, color_filter, ImageSampling::Nearest);
2016    }
2017
2018    fn draw_image_at_sampled(
2019        &mut self,
2020        rect: Rect,
2021        image: ImageBitmap,
2022        alpha: f32,
2023        color_filter: Option<ColorFilter>,
2024        sampling: ImageSampling,
2025    ) {
2026        self.push_blended_primitive(
2027            DrawPrimitive::Image {
2028                rect,
2029                image,
2030                alpha: alpha.clamp(0.0, 1.0),
2031                color_filter,
2032                sampling,
2033                src_rect: None,
2034            },
2035            BlendMode::SrcOver,
2036        );
2037    }
2038
2039    fn draw_image_at_blend(
2040        &mut self,
2041        rect: Rect,
2042        image: ImageBitmap,
2043        alpha: f32,
2044        color_filter: Option<ColorFilter>,
2045        blend_mode: BlendMode,
2046    ) {
2047        self.push_blended_primitive(
2048            DrawPrimitive::Image {
2049                rect,
2050                image,
2051                alpha: alpha.clamp(0.0, 1.0),
2052                color_filter,
2053                sampling: ImageSampling::Nearest,
2054                src_rect: None,
2055            },
2056            blend_mode,
2057        );
2058    }
2059
2060    fn draw_image_src(
2061        &mut self,
2062        image: ImageBitmap,
2063        src_rect: Rect,
2064        dst_rect: Rect,
2065        alpha: f32,
2066        color_filter: Option<ColorFilter>,
2067    ) {
2068        self.draw_image_src_blend(
2069            image,
2070            src_rect,
2071            dst_rect,
2072            alpha,
2073            color_filter,
2074            BlendMode::SrcOver,
2075        );
2076    }
2077
2078    fn draw_image_src_sampled(
2079        &mut self,
2080        image: ImageBitmap,
2081        src_rect: Rect,
2082        dst_rect: Rect,
2083        alpha: f32,
2084        color_filter: Option<ColorFilter>,
2085        sampling: ImageSampling,
2086    ) {
2087        self.push_blended_primitive(
2088            DrawPrimitive::Image {
2089                rect: dst_rect,
2090                image,
2091                alpha: alpha.clamp(0.0, 1.0),
2092                color_filter,
2093                sampling,
2094                src_rect: Some(src_rect),
2095            },
2096            BlendMode::SrcOver,
2097        );
2098    }
2099
2100    fn draw_image_src_blend(
2101        &mut self,
2102        image: ImageBitmap,
2103        src_rect: Rect,
2104        dst_rect: Rect,
2105        alpha: f32,
2106        color_filter: Option<ColorFilter>,
2107        blend_mode: BlendMode,
2108    ) {
2109        self.push_blended_primitive(
2110            DrawPrimitive::Image {
2111                rect: dst_rect,
2112                image,
2113                alpha: alpha.clamp(0.0, 1.0),
2114                color_filter,
2115                sampling: ImageSampling::Nearest,
2116                src_rect: Some(src_rect),
2117            },
2118            blend_mode,
2119        );
2120    }
2121
2122    fn draw_vector_path(&mut self, path: &crate::VectorPath, brush: Brush) {
2123        /// Rasterization supersampling factor relative to scope units.
2124        /// Combined with the rasterizer's own sub-scanline anti-aliasing
2125        /// and linear image sampling, this keeps icon edges crisp on
2126        /// high-density screens.
2127        const SUPERSAMPLE: f32 = 2.0;
2128        /// Safety cap for the rasterized mask dimensions.
2129        const MAX_MASK_PIXELS: f32 = 4096.0;
2130
2131        if path.is_empty() {
2132            return;
2133        }
2134        let bounds = path.bounds();
2135        if bounds.width <= 0.0 || bounds.height <= 0.0 {
2136            return;
2137        }
2138
2139        let color = match &brush {
2140            Brush::Solid(color) => *color,
2141            Brush::LinearGradient { colors, .. }
2142            | Brush::RadialGradient { colors, .. }
2143            | Brush::SweepGradient { colors, .. } => match colors.first() {
2144                Some(color) => *color,
2145                None => return,
2146            },
2147        };
2148        if color.3 <= 0.0 {
2149            return;
2150        }
2151
2152        // Rasterize a padded, integer-aligned bounding box so anti-aliased
2153        // edges are never clipped by the mask border.
2154        let origin = Point::new(bounds.x.floor() - 1.0, bounds.y.floor() - 1.0);
2155        let rect_width = (bounds.x + bounds.width).ceil() - origin.x + 1.0;
2156        let rect_height = (bounds.y + bounds.height).ceil() - origin.y + 1.0;
2157        let mask_width = (rect_width * SUPERSAMPLE)
2158            .ceil()
2159            .clamp(1.0, MAX_MASK_PIXELS) as usize;
2160        let mask_height = (rect_height * SUPERSAMPLE)
2161            .ceil()
2162            .clamp(1.0, MAX_MASK_PIXELS) as usize;
2163
2164        let red = (color.0.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
2165        let green = (color.1.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
2166        let blue = (color.2.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
2167        let alpha = color.3.clamp(0.0, 1.0);
2168        let key = vector_path_mask_key(
2169            path,
2170            origin,
2171            (mask_width, mask_height),
2172            [red, green, blue],
2173            alpha,
2174        );
2175        let cached = vector_path_mask_cache_get(key);
2176        let image = match cached {
2177            Some(image) => image,
2178            None => {
2179                let mask = path.coverage_mask(mask_width, mask_height, origin, SUPERSAMPLE);
2180                let mut pixels = Vec::with_capacity(mask.len() * 4);
2181                for coverage in mask {
2182                    pixels.extend_from_slice(&[
2183                        red,
2184                        green,
2185                        blue,
2186                        (alpha * coverage as f32 + 0.5) as u8,
2187                    ]);
2188                }
2189                let Ok(image) =
2190                    ImageBitmap::from_rgba8(mask_width as u32, mask_height as u32, pixels)
2191                else {
2192                    return;
2193                };
2194                vector_path_mask_cache_put(key, image.clone());
2195                image
2196            }
2197        };
2198
2199        self.push_other(DrawPrimitive::Image {
2200            rect: Rect {
2201                x: origin.x,
2202                y: origin.y,
2203                width: rect_width,
2204                height: rect_height,
2205            },
2206            image,
2207            alpha: 1.0,
2208            color_filter: None,
2209            sampling: ImageSampling::Linear,
2210            src_rect: None,
2211        });
2212    }
2213
2214    fn measure_text(&self, text: &str, style: &TextStyle) -> TextMeasurement {
2215        match &self.text_measurer {
2216            Some(measurer) => measurer.measure_text(text, style),
2217            None => estimate_text_measurement(text, style),
2218        }
2219    }
2220
2221    fn draw_text_at(&mut self, rect: Rect, brush: Brush, text: &str, style: &TextStyle) {
2222        // An empty run has no glyphs and a zero-area box, which every renderer
2223        // would drop anyway — stop here so it never reaches the scene.
2224        if text.is_empty() {
2225            return;
2226        }
2227        let Some(color) = solid_fill_color(&brush) else {
2228            return;
2229        };
2230        if color.3 <= 0.0 {
2231            return;
2232        }
2233        let measurement = self.measure_text(text, style);
2234        if !(measurement.size.width > 0.0 && measurement.size.height > 0.0) {
2235            return;
2236        }
2237        let origin = align_text_block(rect, measurement, style);
2238        if !origin.x.is_finite() || !origin.y.is_finite() {
2239            return;
2240        }
2241        self.push_other(DrawPrimitive::Text(Box::new(TextPrimitive {
2242            rect: Rect::from_origin_size(origin, measurement.size),
2243            text: shared_text_str(text),
2244            style: style.clone(),
2245            color,
2246        })));
2247    }
2248
2249    fn into_primitives(self) -> Vec<DrawPrimitive> {
2250        self.finish().primitives
2251    }
2252}
2253
2254/// The single color a brush paints with, or its first stop for a gradient.
2255///
2256/// Text is filled per glyph from one vertex color, so a gradient cannot be
2257/// honored; this mirrors the fallback [`DrawScope::draw_vector_path`] documents.
2258fn solid_fill_color(brush: &Brush) -> Option<Color> {
2259    match brush {
2260        Brush::Solid(color) => Some(*color),
2261        Brush::LinearGradient { colors, .. }
2262        | Brush::RadialGradient { colors, .. }
2263        | Brush::SweepGradient { colors, .. } => colors.first().copied(),
2264    }
2265}
2266
2267#[cfg(test)]
2268mod tests {
2269    use super::*;
2270    use crate::{Color, FontStyle, FontWeight, ImageBitmap, RenderEffect};
2271
2272    /// The compact recorder routes solid `SrcOver` shapes through typed
2273    /// records and everything else through ordinary primitives; the tape
2274    /// must reassemble the exact sequence recording used to produce
2275    /// directly, arc lowering and degeneracy drops included.
2276    #[test]
2277    fn compact_recording_materializes_in_recorded_order() {
2278        let size = Size::new(100.0, 100.0);
2279        let solid = Brush::solid(Color::WHITE);
2280        let gradient = Brush::vertical_gradient(vec![Color::RED, Color::BLUE], 0.0, 100.0);
2281        let center = Point::new(50.0, 50.0);
2282        let stroke = Stroke::new(4.0);
2283        let rect = Rect {
2284            x: 10.0,
2285            y: 20.0,
2286            width: 30.0,
2287            height: 40.0,
2288        };
2289        let batch = vec![
2290            DrawPrimitive::Content,
2291            DrawPrimitive::Rect {
2292                rect,
2293                brush: solid.clone(),
2294                stroke: None,
2295            },
2296        ];
2297
2298        // Interleave every routing path.
2299        let record = |scope: &mut DrawScopeDefault| {
2300            scope.draw_rect_at(rect, solid.clone());
2301            scope.draw_arc(solid.clone(), center, 30.0, 0.5, 1.5, stroke);
2302            scope.draw_rect_at(rect, gradient.clone());
2303            scope.draw_circle(solid.clone(), center, 12.0);
2304            scope.draw_arc(solid.clone(), center, 30.0, 0.5, 0.0, stroke); // degenerate: dropped
2305            scope.draw_rect_at_blend(rect, solid.clone(), BlendMode::Plus);
2306            scope.draw_content();
2307            scope.draw_annular_sector(gradient.clone(), center, 10.0, 20.0, 0.0, 2.0);
2308            scope.push_recorded(batch.clone());
2309        };
2310
2311        let mut compact = DrawScopeDefault::new(size);
2312        record(&mut compact);
2313        let finished = compact.finish();
2314
2315        // The expected sequence, built through the primitives the ordinary
2316        // lowering produces (the non-solid arc still takes that path, so it
2317        // serves as its own reference for the solid one's geometry).
2318        let arc_via_ordinary = |brush: Brush, radius: f32, start: f32, sweep: f32| {
2319            let mut scope = DrawScopeDefault::new(size);
2320            scope.draw_arc(brush, center, radius, start, sweep, stroke);
2321            scope.into_primitives().remove(0)
2322        };
2323        let expected = vec![
2324            DrawPrimitive::Rect {
2325                rect,
2326                brush: solid.clone(),
2327                stroke: None,
2328            },
2329            arc_via_ordinary(solid.clone(), 30.0, 0.5, 1.5),
2330            DrawPrimitive::Rect {
2331                rect,
2332                brush: gradient.clone(),
2333                stroke: None,
2334            },
2335            DrawPrimitive::RoundRect {
2336                rect: Rect {
2337                    x: center.x - 12.0,
2338                    y: center.y - 12.0,
2339                    width: 24.0,
2340                    height: 24.0,
2341                },
2342                brush: solid.clone(),
2343                radii: CornerRadii::uniform(12.0),
2344                stroke: None,
2345            },
2346            DrawPrimitive::Blend {
2347                primitive: Box::new(DrawPrimitive::Rect {
2348                    rect,
2349                    brush: solid.clone(),
2350                    stroke: None,
2351                }),
2352                blend_mode: BlendMode::Plus,
2353            },
2354            DrawPrimitive::Content,
2355            {
2356                let mut scope = DrawScopeDefault::new(size);
2357                scope.draw_annular_sector(gradient.clone(), center, 10.0, 20.0, 0.0, 2.0);
2358                scope.into_primitives().remove(0)
2359            },
2360            DrawPrimitive::Content,
2361            DrawPrimitive::Rect {
2362                rect,
2363                brush: solid.clone(),
2364                stroke: None,
2365            },
2366        ];
2367        assert_eq!(finished.primitives, expected);
2368        assert_eq!(finished.content_markers, 2);
2369    }
2370
2371    /// A recording that reuses another command's buffers (junk capacity in
2372    /// every store) must be byte-identical to one recorded fresh.
2373    #[test]
2374    fn reused_recording_buffers_record_identically_to_fresh() {
2375        let size = Size::new(64.0, 64.0);
2376        let record = |scope: &mut DrawScopeDefault| {
2377            scope.draw_circle(Brush::solid(Color::RED), Point::new(32.0, 32.0), 10.0);
2378            scope.draw_arc(
2379                Brush::solid(Color::BLUE),
2380                Point::new(32.0, 32.0),
2381                20.0,
2382                0.0,
2383                3.0,
2384                Stroke::new(2.0),
2385            );
2386        };
2387
2388        let mut fresh = DrawScopeDefault::new(size);
2389        record(&mut fresh);
2390        let fresh = fresh.finish();
2391
2392        // Dirty the buffers with an unrelated recording first.
2393        let mut dirty = DrawScopeDefault::new(size);
2394        dirty.draw_rect(Brush::solid(Color::BLACK));
2395        dirty.draw_content();
2396        dirty.draw_arc(
2397            Brush::solid(Color::WHITE),
2398            Point::new(1.0, 1.0),
2399            5.0,
2400            1.0,
2401            1.0,
2402            Stroke::new(1.0),
2403        );
2404        let dirty = dirty.finish();
2405
2406        let mut reused =
2407            DrawScopeDefault::with_recording(size, None, dirty.recording, dirty.primitives);
2408        record(&mut reused);
2409        let reused = reused.finish();
2410
2411        assert_eq!(fresh.primitives, reused.primitives);
2412        assert_eq!(fresh.content_markers, reused.content_markers);
2413    }
2414
2415    #[test]
2416    fn redrawing_the_same_text_shares_one_str_allocation() {
2417        let first = shared_text_str("BREAK THE RING");
2418        let second = shared_text_str("BREAK THE RING");
2419        assert!(Rc::ptr_eq(&first, &second));
2420        assert_eq!(&*second, "BREAK THE RING");
2421    }
2422
2423    #[test]
2424    fn different_text_gets_its_own_str() {
2425        let first = shared_text_str("340");
2426        let second = shared_text_str("350");
2427        assert!(!Rc::ptr_eq(&first, &second));
2428        assert_eq!(&*first, "340");
2429        assert_eq!(&*second, "350");
2430    }
2431
2432    #[test]
2433    fn the_text_pool_survives_overflowing_its_capacity() {
2434        for index in 0..600 {
2435            let text = format!("run-{index}");
2436            assert_eq!(&*shared_text_str(&text), text.as_str());
2437        }
2438        assert_eq!(&*shared_text_str("still correct"), "still correct");
2439    }
2440
2441    fn assert_image_alpha(primitive: &DrawPrimitive, expected: f32) {
2442        match primitive {
2443            DrawPrimitive::Image { alpha, .. } => assert!((alpha - expected).abs() < 1e-5),
2444            DrawPrimitive::Blend { primitive, .. } => assert_image_alpha(primitive, expected),
2445            other => panic!("expected image primitive, got {other:?}"),
2446        }
2447    }
2448
2449    fn unwrap_image(primitive: &DrawPrimitive) -> &DrawPrimitive {
2450        match primitive {
2451            DrawPrimitive::Image { .. } => primitive,
2452            DrawPrimitive::Blend { primitive, .. } => unwrap_image(primitive),
2453            other => panic!("expected image primitive, got {other:?}"),
2454        }
2455    }
2456
2457    #[test]
2458    fn draw_svg_path_emits_supersampled_image_over_path_bounds() {
2459        let mut scope = DrawScopeDefault::new(Size::new(32.0, 32.0));
2460        scope.draw_svg_path("M 4 4 H 20 V 20 H 4 Z", Brush::solid(Color::RED));
2461
2462        let primitives = scope.into_primitives();
2463        assert_eq!(primitives.len(), 1);
2464        let DrawPrimitive::Image { rect, image, .. } = &primitives[0] else {
2465            panic!("expected image primitive, got {:?}", primitives[0]);
2466        };
2467
2468        // Padded, integer-aligned bounds: (3,3) to (21,21).
2469        assert_eq!((rect.x, rect.y), (3.0, 3.0));
2470        assert_eq!((rect.width, rect.height), (18.0, 18.0));
2471        // Rasterized at 2x supersampling.
2472        assert_eq!((image.width(), image.height()), (36, 36));
2473
2474        // Probe the pixel at path point (12, 12): mask position
2475        // ((12 - 3) * 2, (12 - 3) * 2) = (18, 18) — fully covered red.
2476        let pixels = image.pixels();
2477        let index = (18 * 36 + 18) * 4;
2478        assert_eq!(
2479            &pixels[index..index + 4],
2480            &[255, 0, 0, 255],
2481            "path interior must be opaque brush color"
2482        );
2483        // A corner outside the square must be transparent.
2484        assert_eq!(pixels[3], 0, "outside the path must stay transparent");
2485    }
2486
2487    #[test]
2488    fn draw_svg_path_ignores_invalid_data() {
2489        let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
2490        scope.draw_svg_path("definitely not a path", Brush::solid(Color::WHITE));
2491        assert!(scope.into_primitives().is_empty());
2492    }
2493
2494    #[test]
2495    fn draw_vector_path_applies_brush_alpha() {
2496        let path = crate::VectorPath::parse("M 0 0 H 8 V 8 H 0 Z").expect("valid path");
2497        let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
2498        scope.draw_vector_path(&path, Brush::solid(Color::rgba(0.0, 0.0, 1.0, 0.5)));
2499
2500        let primitives = scope.into_primitives();
2501        let DrawPrimitive::Image { image, .. } = &primitives[0] else {
2502            panic!("expected image primitive");
2503        };
2504        let pixels = image.pixels();
2505        // Center of the mask: interior pixel with half-alpha blue.
2506        let width = image.width() as usize;
2507        let index = ((image.height() as usize / 2) * width + width / 2) * 4;
2508        assert_eq!(&pixels[index..index + 3], &[0, 0, 255]);
2509        let alpha = pixels[index + 3];
2510        assert!(
2511            (alpha as i32 - 128).abs() <= 2,
2512            "interior alpha must honor the brush alpha, got {alpha}"
2513        );
2514    }
2515
2516    #[test]
2517    fn the_same_path_and_color_reuse_one_raster() {
2518        let path = crate::VectorPath::parse("M 0 0 H 7 V 7 H 0 Z").expect("valid path");
2519        let raster_of = |brush: Brush| {
2520            let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
2521            scope.draw_vector_path(&path, brush);
2522            let primitives = scope.into_primitives();
2523            let DrawPrimitive::Image { image, .. } = &primitives[0] else {
2524                panic!("expected image primitive");
2525            };
2526            image.clone()
2527        };
2528
2529        let first = raster_of(Brush::solid(Color::rgba(0.0, 0.0, 1.0, 1.0)));
2530        let second = raster_of(Brush::solid(Color::rgba(0.0, 0.0, 1.0, 1.0)));
2531        assert_eq!(first.id(), second.id());
2532
2533        let other_color = raster_of(Brush::solid(Color::rgba(1.0, 0.0, 0.0, 1.0)));
2534        assert_ne!(first.id(), other_color.id());
2535
2536        let wider = crate::VectorPath::parse("M 0 0 H 9 V 7 H 0 Z").expect("valid path");
2537        let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
2538        scope.draw_vector_path(&wider, Brush::solid(Color::rgba(0.0, 0.0, 1.0, 1.0)));
2539        let primitives = scope.into_primitives();
2540        let DrawPrimitive::Image { image, .. } = &primitives[0] else {
2541            panic!("expected image primitive");
2542        };
2543        assert_ne!(first.id(), image.id());
2544    }
2545
2546    #[test]
2547    fn draw_content_inserts_content_marker() {
2548        let mut scope = DrawScopeDefault::new(Size::new(8.0, 8.0));
2549        scope.draw_rect(Brush::solid(Color::WHITE));
2550        scope.draw_content();
2551        scope.draw_rect_blend(Brush::solid(Color::BLACK), BlendMode::DstOut);
2552
2553        let primitives = scope.into_primitives();
2554        assert!(matches!(primitives[1], DrawPrimitive::Content));
2555        assert!(matches!(
2556            primitives[2],
2557            DrawPrimitive::Blend {
2558                blend_mode: BlendMode::DstOut,
2559                ..
2560            }
2561        ));
2562    }
2563
2564    #[test]
2565    fn draw_rect_blend_wraps_non_default_modes() {
2566        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
2567        scope.draw_rect_blend(Brush::solid(Color::RED), BlendMode::DstOut);
2568
2569        let primitives = scope.into_primitives();
2570        assert_eq!(primitives.len(), 1);
2571        match &primitives[0] {
2572            DrawPrimitive::Blend {
2573                primitive,
2574                blend_mode,
2575            } => {
2576                assert_eq!(*blend_mode, BlendMode::DstOut);
2577                assert!(matches!(**primitive, DrawPrimitive::Rect { .. }));
2578            }
2579            other => panic!("expected blended primitive, got {other:?}"),
2580        }
2581    }
2582
2583    #[test]
2584    fn draw_circle_records_centered_round_rect() {
2585        let mut scope = DrawScopeDefault::new(Size::new(40.0, 40.0));
2586        scope.draw_circle(Brush::solid(Color::BLUE), Point::new(12.0, 16.0), 5.0);
2587
2588        let primitives = scope.into_primitives();
2589        assert_eq!(primitives.len(), 1);
2590        match &primitives[0] {
2591            DrawPrimitive::RoundRect { rect, radii, .. } => {
2592                assert_eq!(
2593                    *rect,
2594                    Rect {
2595                        x: 7.0,
2596                        y: 11.0,
2597                        width: 10.0,
2598                        height: 10.0,
2599                    }
2600                );
2601                assert_eq!(*radii, CornerRadii::uniform(5.0));
2602            }
2603            other => panic!("expected circular round rect, got {other:?}"),
2604        }
2605    }
2606
2607    #[test]
2608    fn draw_circle_blend_wraps_non_default_modes() {
2609        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
2610        scope.draw_circle_blend(
2611            Brush::solid(Color::RED),
2612            Point::new(5.0, 5.0),
2613            3.0,
2614            BlendMode::Plus,
2615        );
2616
2617        let primitives = scope.into_primitives();
2618        assert_eq!(primitives.len(), 1);
2619        match &primitives[0] {
2620            DrawPrimitive::Blend {
2621                primitive,
2622                blend_mode,
2623            } => {
2624                assert_eq!(*blend_mode, BlendMode::Plus);
2625                assert!(matches!(**primitive, DrawPrimitive::RoundRect { .. }));
2626            }
2627            other => panic!("expected blended circle primitive, got {other:?}"),
2628        }
2629    }
2630
2631    #[test]
2632    fn rect_union_encloses_both_inputs() {
2633        let lhs = Rect {
2634            x: 10.0,
2635            y: 5.0,
2636            width: 8.0,
2637            height: 4.0,
2638        };
2639        let rhs = Rect {
2640            x: 4.0,
2641            y: 7.0,
2642            width: 10.0,
2643            height: 6.0,
2644        };
2645
2646        assert_eq!(
2647            lhs.union(rhs),
2648            Rect {
2649                x: 4.0,
2650                y: 5.0,
2651                width: 14.0,
2652                height: 8.0,
2653            }
2654        );
2655    }
2656
2657    #[test]
2658    fn draw_image_uses_scope_size_as_default_rect() {
2659        let mut scope = DrawScopeDefault::new(Size::new(40.0, 24.0));
2660        let image = ImageBitmap::from_rgba8(2, 2, vec![255; 16]).expect("image");
2661        scope.draw_image(image.clone());
2662        let primitives = scope.into_primitives();
2663        assert_eq!(primitives.len(), 1);
2664        match unwrap_image(&primitives[0]) {
2665            DrawPrimitive::Image {
2666                rect,
2667                image: actual,
2668                alpha,
2669                color_filter,
2670                sampling,
2671                src_rect,
2672            } => {
2673                assert_eq!(*rect, Rect::from_size(Size::new(40.0, 24.0)));
2674                assert_eq!(*actual, image);
2675                assert_eq!(*alpha, 1.0);
2676                assert!(color_filter.is_none());
2677                assert_eq!(*sampling, ImageSampling::Nearest);
2678                assert!(src_rect.is_none());
2679            }
2680            other => panic!("expected image primitive, got {other:?}"),
2681        }
2682    }
2683
2684    #[test]
2685    fn draw_image_src_stores_src_rect() {
2686        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2687        let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
2688        let src = Rect {
2689            x: 10.0,
2690            y: 20.0,
2691            width: 30.0,
2692            height: 40.0,
2693        };
2694        let dst = Rect {
2695            x: 0.0,
2696            y: 0.0,
2697            width: 60.0,
2698            height: 80.0,
2699        };
2700        scope.draw_image_src(image.clone(), src, dst, 0.8, None);
2701        let primitives = scope.into_primitives();
2702        assert_eq!(primitives.len(), 1);
2703        match unwrap_image(&primitives[0]) {
2704            DrawPrimitive::Image {
2705                rect,
2706                image: actual,
2707                alpha,
2708                sampling,
2709                src_rect,
2710                ..
2711            } => {
2712                assert_eq!(*rect, dst);
2713                assert_eq!(*actual, image);
2714                assert!((alpha - 0.8).abs() < 1e-5);
2715                assert_eq!(*sampling, ImageSampling::Nearest);
2716                assert_eq!(*src_rect, Some(src));
2717            }
2718            other => panic!("expected image primitive, got {other:?}"),
2719        }
2720    }
2721
2722    #[test]
2723    fn draw_image_at_sampled_records_requested_sampling() {
2724        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2725        let image = ImageBitmap::from_rgba8(8, 8, vec![255; 8 * 8 * 4]).expect("image");
2726        let dst = Rect {
2727            x: 2.0,
2728            y: 3.0,
2729            width: 40.0,
2730            height: 30.0,
2731        };
2732
2733        scope.draw_image_at_sampled(dst, image.clone(), 0.7, None, ImageSampling::Linear);
2734
2735        let primitives = scope.into_primitives();
2736        assert_eq!(primitives.len(), 1);
2737        match unwrap_image(&primitives[0]) {
2738            DrawPrimitive::Image {
2739                rect,
2740                image: actual,
2741                alpha,
2742                sampling,
2743                src_rect,
2744                ..
2745            } => {
2746                assert_eq!(*rect, dst);
2747                assert_eq!(*actual, image);
2748                assert!((alpha - 0.7).abs() < 1e-5);
2749                assert_eq!(*sampling, ImageSampling::Linear);
2750                assert!(src_rect.is_none());
2751            }
2752            other => panic!("expected image primitive, got {other:?}"),
2753        }
2754    }
2755
2756    #[test]
2757    fn draw_image_src_sampled_records_requested_sampling() {
2758        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2759        let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
2760        let src = Rect {
2761            x: 4.0,
2762            y: 6.0,
2763            width: 16.0,
2764            height: 20.0,
2765        };
2766        let dst = Rect {
2767            x: 8.0,
2768            y: 10.0,
2769            width: 32.0,
2770            height: 40.0,
2771        };
2772
2773        scope.draw_image_src_sampled(image.clone(), src, dst, 0.5, None, ImageSampling::Linear);
2774
2775        let primitives = scope.into_primitives();
2776        assert_eq!(primitives.len(), 1);
2777        match unwrap_image(&primitives[0]) {
2778            DrawPrimitive::Image {
2779                rect,
2780                image: actual,
2781                alpha,
2782                sampling,
2783                src_rect,
2784                ..
2785            } => {
2786                assert_eq!(*rect, dst);
2787                assert_eq!(*actual, image);
2788                assert!((alpha - 0.5).abs() < 1e-5);
2789                assert_eq!(*sampling, ImageSampling::Linear);
2790                assert_eq!(*src_rect, Some(src));
2791            }
2792            other => panic!("expected image primitive, got {other:?}"),
2793        }
2794    }
2795
2796    #[test]
2797    fn draw_image_at_clamps_alpha() {
2798        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
2799        let image = ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image");
2800        scope.draw_image_at(
2801            Rect::from_origin_size(Point::new(2.0, 3.0), Size::new(5.0, 6.0)),
2802            image,
2803            3.0,
2804            Some(ColorFilter::Tint(Color::from_rgba_u8(128, 128, 255, 255))),
2805        );
2806        assert_image_alpha(&scope.into_primitives()[0], 1.0);
2807    }
2808
2809    #[test]
2810    fn graphics_layer_clone_with_render_effect() {
2811        let layer = GraphicsLayer {
2812            render_effect: Some(RenderEffect::blur(10.0)),
2813            backdrop_effect: Some(RenderEffect::blur(6.0)),
2814            color_filter: Some(ColorFilter::tint(Color::from_rgba_u8(128, 200, 255, 255))),
2815            alpha: 0.5,
2816            rotation_z: 12.0,
2817            shadow_elevation: 4.0,
2818            shape: LayerShape::Rounded(RoundedCornerShape::uniform(6.0)),
2819            clip: true,
2820            compositing_strategy: CompositingStrategy::Offscreen,
2821            blend_mode: BlendMode::SrcOver,
2822            ..Default::default()
2823        };
2824        let cloned = layer.clone();
2825        assert_eq!(cloned.alpha, 0.5);
2826        assert!(cloned.render_effect.is_some());
2827        assert!(cloned.backdrop_effect.is_some());
2828        assert_eq!(layer.color_filter, cloned.color_filter);
2829        assert_eq!(layer.render_effect, cloned.render_effect);
2830        assert_eq!(layer.backdrop_effect, cloned.backdrop_effect);
2831        assert!((cloned.rotation_z - 12.0).abs() < 1e-6);
2832        assert!((cloned.shadow_elevation - 4.0).abs() < 1e-6);
2833        assert_eq!(
2834            cloned.shape,
2835            LayerShape::Rounded(RoundedCornerShape::uniform(6.0))
2836        );
2837        assert!(cloned.clip);
2838        assert_eq!(cloned.compositing_strategy, CompositingStrategy::Offscreen);
2839        assert_eq!(cloned.blend_mode, BlendMode::SrcOver);
2840    }
2841
2842    #[test]
2843    fn graphics_layer_default_has_no_effect() {
2844        let layer = GraphicsLayer::default();
2845        assert!(layer.color_filter.is_none());
2846        assert!(layer.render_effect.is_none());
2847        assert!(layer.backdrop_effect.is_none());
2848        assert_eq!(layer.compositing_strategy, CompositingStrategy::Auto);
2849        assert_eq!(layer.blend_mode, BlendMode::SrcOver);
2850        assert_eq!(layer.alpha, 1.0);
2851        assert_eq!(layer.transform_origin, TransformOrigin::CENTER);
2852        assert!((layer.camera_distance - 8.0).abs() < 1e-6);
2853        assert_eq!(layer.shape, LayerShape::Rectangle);
2854        assert!(!layer.clip);
2855        assert_eq!(layer.ambient_shadow_color, Color::BLACK);
2856        assert_eq!(layer.spot_shadow_color, Color::BLACK);
2857    }
2858
2859    #[test]
2860    fn transform_origin_construction() {
2861        let origin = TransformOrigin::new(0.25, 0.75);
2862        assert!((origin.pivot_fraction_x - 0.25).abs() < 1e-6);
2863        assert!((origin.pivot_fraction_y - 0.75).abs() < 1e-6);
2864    }
2865
2866    #[test]
2867    fn layer_shape_default_is_rectangle() {
2868        assert_eq!(LayerShape::default(), LayerShape::Rectangle);
2869    }
2870
2871    // ── Stroke / arc lowering ───────────────────────────────────────────────
2872
2873    use crate::{StrokeCap, StrokeJoin};
2874    use std::f32::consts::{FRAC_PI_2, PI};
2875
2876    /// Arc bounds are conservative-approximate (fast endpoint trig plus a
2877    /// containment pad, see `stroke.rs`); geometry tests compare within that
2878    /// documented slack. The strict containment guard lives in the stroke
2879    /// module's property test.
2880    fn approx(a: f32, b: f32) -> bool {
2881        (a - b).abs() < 0.25
2882    }
2883
2884    fn scope(size: f32) -> DrawScopeDefault {
2885        DrawScopeDefault::new(Size::new(size, size))
2886    }
2887
2888    #[test]
2889    fn draw_rect_stroked_records_scope_rect_and_stroke() {
2890        let mut scope = scope(20.0);
2891        scope.draw_rect_stroked(
2892            Brush::solid(Color::RED),
2893            Stroke::new(3.0).with_join(StrokeJoin::Bevel),
2894        );
2895
2896        let primitives = scope.into_primitives();
2897        assert_eq!(primitives.len(), 1);
2898        match &primitives[0] {
2899            DrawPrimitive::Rect {
2900                rect,
2901                stroke: Some(stroke),
2902                ..
2903            } => {
2904                // The stored rect stays the *geometric* rect; the renderer
2905                // inflates it by half the stroke width when it builds the quad.
2906                assert_eq!(*rect, Rect::from_size(Size::new(20.0, 20.0)));
2907                assert_eq!(stroke.width, 3.0);
2908                assert_eq!(stroke.join, StrokeJoin::Bevel);
2909            }
2910            other => panic!("expected stroked rect, got {other:?}"),
2911        }
2912    }
2913
2914    #[test]
2915    fn draw_rect_at_stroked_records_requested_rect() {
2916        let mut scope = scope(50.0);
2917        let rect = Rect {
2918            x: 4.0,
2919            y: 6.0,
2920            width: 12.0,
2921            height: 9.0,
2922        };
2923        scope.draw_rect_at_stroked(rect, Brush::solid(Color::BLUE), Stroke::new(2.0));
2924        match &scope.into_primitives()[0] {
2925            DrawPrimitive::Rect {
2926                rect: actual,
2927                stroke: Some(stroke),
2928                ..
2929            } => {
2930                assert_eq!(*actual, rect);
2931                assert_eq!(stroke.width, 2.0);
2932            }
2933            other => panic!("expected stroked rect, got {other:?}"),
2934        }
2935    }
2936
2937    #[test]
2938    fn draw_round_rect_stroked_keeps_radii_and_stroke() {
2939        let mut scope = scope(30.0);
2940        scope.draw_round_rect_stroked(
2941            Brush::solid(Color::GREEN),
2942            CornerRadii::uniform(5.0),
2943            Stroke::new(4.0).with_join(StrokeJoin::Round),
2944        );
2945        match &scope.into_primitives()[0] {
2946            DrawPrimitive::RoundRect {
2947                rect,
2948                radii,
2949                stroke: Some(stroke),
2950                ..
2951            } => {
2952                assert_eq!(*rect, Rect::from_size(Size::new(30.0, 30.0)));
2953                assert_eq!(*radii, CornerRadii::uniform(5.0));
2954                assert_eq!(stroke.width, 4.0);
2955                assert_eq!(stroke.join, StrokeJoin::Round);
2956            }
2957            other => panic!("expected stroked round rect, got {other:?}"),
2958        }
2959    }
2960
2961    #[test]
2962    fn draw_round_rect_at_stroked_records_requested_rect() {
2963        let mut scope = scope(60.0);
2964        let rect = Rect {
2965            x: 1.0,
2966            y: 2.0,
2967            width: 20.0,
2968            height: 10.0,
2969        };
2970        scope.draw_round_rect_at_stroked(
2971            rect,
2972            Brush::solid(Color::WHITE),
2973            CornerRadii::uniform(3.0),
2974            Stroke::new(1.5),
2975        );
2976        match &scope.into_primitives()[0] {
2977            DrawPrimitive::RoundRect {
2978                rect: actual,
2979                radii,
2980                stroke: Some(stroke),
2981                ..
2982            } => {
2983                assert_eq!(*actual, rect);
2984                assert_eq!(*radii, CornerRadii::uniform(3.0));
2985                assert_eq!(stroke.width, 1.5);
2986            }
2987            other => panic!("expected stroked round rect, got {other:?}"),
2988        }
2989    }
2990
2991    #[test]
2992    fn draw_circle_stroked_lowers_to_stroked_round_rect() {
2993        // A stroked circle must reuse the round-rect path so it shares the
2994        // shape pipeline (and therefore the batch) with every other shape.
2995        let mut scope = scope(40.0);
2996        scope.draw_circle_stroked(
2997            Brush::solid(Color::BLUE),
2998            Point::new(12.0, 16.0),
2999            5.0,
3000            Stroke::new(2.0),
3001        );
3002        match &scope.into_primitives()[0] {
3003            DrawPrimitive::RoundRect {
3004                rect,
3005                radii,
3006                stroke: Some(stroke),
3007                ..
3008            } => {
3009                assert_eq!(
3010                    *rect,
3011                    Rect {
3012                        x: 7.0,
3013                        y: 11.0,
3014                        width: 10.0,
3015                        height: 10.0,
3016                    }
3017                );
3018                assert_eq!(*radii, CornerRadii::uniform(5.0));
3019                assert_eq!(stroke.width, 2.0);
3020            }
3021            other => panic!("expected stroked circular round rect, got {other:?}"),
3022        }
3023    }
3024
3025    #[test]
3026    fn draw_arc_records_arc_primitive_with_tight_bounds() {
3027        let mut scope = scope(200.0);
3028        scope.draw_arc(
3029            Brush::solid(Color::RED),
3030            Point::new(100.0, 100.0),
3031            50.0,
3032            0.0,
3033            FRAC_PI_2,
3034            Stroke::new(10.0),
3035        );
3036        let primitives = scope.into_primitives();
3037        assert_eq!(primitives.len(), 1);
3038        match &primitives[0] {
3039            DrawPrimitive::Arc {
3040                rect,
3041                center,
3042                radius,
3043                start_angle,
3044                sweep_angle,
3045                stroke: Some(stroke),
3046                inner_radius,
3047                ..
3048            } => {
3049                assert_eq!(*center, Point::new(100.0, 100.0));
3050                assert_eq!(*radius, 50.0);
3051                assert_eq!(*start_angle, 0.0);
3052                assert!(approx(*sweep_angle, FRAC_PI_2));
3053                assert_eq!(stroke.width, 10.0);
3054                assert_eq!(*inner_radius, 0.0);
3055                // Band is 45..55; a 0..90 degree sweep with butt caps spans
3056                // x = 100..155 and y = 100..155.
3057                assert!(approx(rect.x, 100.0), "{rect:?}");
3058                assert!(approx(rect.y, 100.0), "{rect:?}");
3059                assert!(approx(rect.width, 55.0), "{rect:?}");
3060                assert!(approx(rect.height, 55.0), "{rect:?}");
3061            }
3062            other => panic!("expected arc primitive, got {other:?}"),
3063        }
3064    }
3065
3066    #[test]
3067    fn draw_arc_bounds_cover_a_quadrant_spanning_sweep() {
3068        let mut scope = scope(200.0);
3069        // 0 -> 270 degrees: the bounds must be the full outer circle, not the
3070        // chord between the two endpoints.
3071        scope.draw_arc(
3072            Brush::solid(Color::RED),
3073            Point::new(100.0, 100.0),
3074            50.0,
3075            0.0,
3076            3.0 * FRAC_PI_2,
3077            Stroke::new(4.0),
3078        );
3079        let DrawPrimitive::Arc { rect, .. } = &scope.into_primitives()[0] else {
3080            panic!("expected arc primitive");
3081        };
3082        assert!(approx(rect.x, 48.0), "{rect:?}");
3083        assert!(approx(rect.y, 48.0), "{rect:?}");
3084        assert!(approx(rect.width, 104.0), "{rect:?}");
3085        assert!(approx(rect.height, 104.0), "{rect:?}");
3086    }
3087
3088    #[test]
3089    fn draw_annular_sector_records_inner_radius_and_no_stroke() {
3090        let mut scope = scope(200.0);
3091        scope.draw_annular_sector(
3092            Brush::solid(Color::WHITE),
3093            Point::new(100.0, 100.0),
3094            30.0,
3095            50.0,
3096            0.0,
3097            PI,
3098        );
3099        match &scope.into_primitives()[0] {
3100            DrawPrimitive::Arc {
3101                rect,
3102                center,
3103                radius,
3104                inner_radius,
3105                stroke,
3106                sweep_angle,
3107                ..
3108            } => {
3109                assert!(stroke.is_none(), "annular sectors are filled, not stroked");
3110                assert_eq!(*center, Point::new(100.0, 100.0));
3111                assert_eq!(*radius, 50.0);
3112                assert_eq!(*inner_radius, 30.0);
3113                assert!(approx(*sweep_angle, PI));
3114                // 0 -> 180 degrees: x spans -50..+50, y spans 0..+50.
3115                assert!(approx(rect.x, 50.0), "{rect:?}");
3116                assert!(approx(rect.y, 100.0), "{rect:?}");
3117                assert!(approx(rect.width, 100.0), "{rect:?}");
3118                assert!(approx(rect.height, 50.0), "{rect:?}");
3119            }
3120            other => panic!("expected arc primitive, got {other:?}"),
3121        }
3122    }
3123
3124    #[test]
3125    fn draw_arc_blend_wraps_non_default_modes() {
3126        let mut scope = scope(100.0);
3127        scope.draw_arc_blend(
3128            Brush::solid(Color::RED),
3129            Point::new(50.0, 50.0),
3130            20.0,
3131            0.0,
3132            1.0,
3133            Stroke::new(2.0),
3134            BlendMode::DstOut,
3135        );
3136        match &scope.into_primitives()[0] {
3137            DrawPrimitive::Blend {
3138                primitive,
3139                blend_mode,
3140            } => {
3141                assert_eq!(*blend_mode, BlendMode::DstOut);
3142                assert!(matches!(**primitive, DrawPrimitive::Arc { .. }));
3143            }
3144            other => panic!("expected blended arc, got {other:?}"),
3145        }
3146    }
3147
3148    #[test]
3149    fn draw_annular_sector_blend_wraps_non_default_modes() {
3150        let mut scope = scope(100.0);
3151        scope.draw_annular_sector_blend(
3152            Brush::solid(Color::RED),
3153            Point::new(50.0, 50.0),
3154            5.0,
3155            20.0,
3156            0.0,
3157            1.0,
3158            BlendMode::Plus,
3159        );
3160        assert!(matches!(
3161            &scope.into_primitives()[0],
3162            DrawPrimitive::Blend {
3163                blend_mode: BlendMode::Plus,
3164                ..
3165            }
3166        ));
3167    }
3168
3169    #[test]
3170    fn stroked_blend_variants_wrap_non_default_modes() {
3171        let mut scope = scope(20.0);
3172        scope.draw_rect_stroked_blend(
3173            Brush::solid(Color::RED),
3174            Stroke::new(2.0),
3175            BlendMode::DstOut,
3176        );
3177        scope.draw_round_rect_stroked_blend(
3178            Brush::solid(Color::RED),
3179            CornerRadii::uniform(2.0),
3180            Stroke::new(2.0),
3181            BlendMode::DstOut,
3182        );
3183        scope.draw_circle_stroked_blend(
3184            Brush::solid(Color::RED),
3185            Point::new(10.0, 10.0),
3186            5.0,
3187            Stroke::new(2.0),
3188            BlendMode::DstOut,
3189        );
3190        let primitives = scope.into_primitives();
3191        assert_eq!(primitives.len(), 3);
3192        for primitive in &primitives {
3193            assert!(
3194                matches!(
3195                    primitive,
3196                    DrawPrimitive::Blend {
3197                        blend_mode: BlendMode::DstOut,
3198                        ..
3199                    }
3200                ),
3201                "expected blended primitive, got {primitive:?}"
3202            );
3203        }
3204    }
3205
3206    #[test]
3207    fn negative_sweeps_and_overlong_sweeps_produce_finite_bounds() {
3208        let mut scope = scope(200.0);
3209        scope.draw_arc(
3210            Brush::solid(Color::RED),
3211            Point::new(100.0, 100.0),
3212            40.0,
3213            FRAC_PI_2,
3214            -FRAC_PI_2,
3215            Stroke::new(4.0),
3216        );
3217        scope.draw_arc(
3218            Brush::solid(Color::RED),
3219            Point::new(100.0, 100.0),
3220            40.0,
3221            0.3,
3222            crate::stroke::TAU * 4.0,
3223            Stroke::new(4.0),
3224        );
3225        let primitives = scope.into_primitives();
3226        assert_eq!(primitives.len(), 2);
3227
3228        let DrawPrimitive::Arc { rect: negative, .. } = &primitives[0] else {
3229            panic!("expected arc");
3230        };
3231        // 0 -> 90 degrees clockwise, band 38..42.
3232        assert!(approx(negative.x, 100.0), "{negative:?}");
3233        assert!(approx(negative.y, 100.0), "{negative:?}");
3234        assert!(approx(negative.width, 42.0), "{negative:?}");
3235
3236        let DrawPrimitive::Arc { rect: full, .. } = &primitives[1] else {
3237            panic!("expected arc");
3238        };
3239        // Anything past a full turn is a closed ring: the whole outer circle.
3240        assert!(approx(full.x, 58.0), "{full:?}");
3241        assert!(approx(full.width, 84.0), "{full:?}");
3242        assert!(approx(full.height, 84.0), "{full:?}");
3243    }
3244
3245    #[test]
3246    fn degenerate_stroke_and_arc_inputs_emit_nothing_and_never_panic() {
3247        let mut scope = scope(50.0);
3248        let brush = Brush::solid(Color::RED);
3249        let center = Point::new(25.0, 25.0);
3250
3251        // Zero / negative / non-finite stroke widths.
3252        scope.draw_rect_stroked(brush.clone(), Stroke::new(0.0));
3253        scope.draw_rect_stroked(brush.clone(), Stroke::new(-4.0));
3254        scope.draw_rect_stroked(brush.clone(), Stroke::new(f32::NAN));
3255        scope.draw_round_rect_stroked(brush.clone(), CornerRadii::uniform(2.0), Stroke::new(0.0));
3256        scope.draw_circle_stroked(brush.clone(), center, 10.0, Stroke::new(0.0));
3257        scope.draw_circle_stroked(brush.clone(), center, f32::NAN, Stroke::new(2.0));
3258        // Zero and non-finite sweeps.
3259        scope.draw_arc(brush.clone(), center, 10.0, 0.0, 0.0, Stroke::new(2.0));
3260        scope.draw_arc(brush.clone(), center, 10.0, 0.0, f32::NAN, Stroke::new(2.0));
3261        scope.draw_arc(
3262            brush.clone(),
3263            center,
3264            f32::INFINITY,
3265            0.0,
3266            1.0,
3267            Stroke::new(2.0),
3268        );
3269        // Zero-width arc stroke and zero radius with zero width.
3270        scope.draw_arc(brush.clone(), center, 10.0, 0.0, 1.0, Stroke::new(0.0));
3271        scope.draw_arc(brush.clone(), center, 0.0, 0.0, 1.0, Stroke::new(0.0));
3272        // Annular sectors with an empty band.
3273        scope.draw_annular_sector(brush.clone(), center, 10.0, 10.0, 0.0, 1.0);
3274        scope.draw_annular_sector(brush.clone(), center, 20.0, 10.0, 0.0, 1.0);
3275        scope.draw_annular_sector(brush.clone(), center, 0.0, 0.0, 0.0, 1.0);
3276        scope.draw_annular_sector(brush.clone(), center, 0.0, 10.0, 0.0, 0.0);
3277        scope.draw_annular_sector(brush, center, f32::NAN, 10.0, 0.0, 1.0);
3278
3279        assert!(
3280            scope.into_primitives().is_empty(),
3281            "degenerate stroke/arc requests must not reach the renderer"
3282        );
3283    }
3284
3285    #[test]
3286    fn zero_radius_arc_with_positive_width_stays_finite() {
3287        // radius 0 with a fat stroke is a filled wedge of radius width/2 —
3288        // legal, and it must not produce NaN bounds.
3289        let mut scope = scope(50.0);
3290        scope.draw_arc(
3291            Brush::solid(Color::RED),
3292            Point::new(25.0, 25.0),
3293            0.0,
3294            0.0,
3295            FRAC_PI_2,
3296            Stroke::new(6.0).with_cap(StrokeCap::Round),
3297        );
3298        let primitives = scope.into_primitives();
3299        assert_eq!(primitives.len(), 1);
3300        let DrawPrimitive::Arc { rect, .. } = &primitives[0] else {
3301            panic!("expected arc");
3302        };
3303        for value in [rect.x, rect.y, rect.width, rect.height] {
3304            assert!(value.is_finite(), "{rect:?}");
3305        }
3306        assert!(rect.width > 0.0 && rect.height > 0.0, "{rect:?}");
3307    }
3308
3309    // ── Text ────────────────────────────────────────────────────────────────
3310
3311    /// Measures every character as a fixed box, so a test can predict the block
3312    /// a draw is supposed to occupy without depending on a font.
3313    struct FixedAdvanceTextMeasurer {
3314        advance: f32,
3315        line_height: f32,
3316        calls: std::cell::Cell<usize>,
3317    }
3318
3319    impl FixedAdvanceTextMeasurer {
3320        fn shared(advance: f32, line_height: f32) -> Rc<Self> {
3321            Rc::new(Self {
3322                advance,
3323                line_height,
3324                calls: std::cell::Cell::new(0),
3325            })
3326        }
3327    }
3328
3329    impl DrawTextMeasurer for FixedAdvanceTextMeasurer {
3330        fn measure_text(&self, text: &str, _style: &TextStyle) -> TextMeasurement {
3331            self.calls.set(self.calls.get() + 1);
3332            let lines: Vec<&str> = text.split('\n').collect();
3333            let width = lines
3334                .iter()
3335                .map(|line| line.chars().count() as f32 * self.advance)
3336                .fold(0.0_f32, f32::max);
3337            TextMeasurement {
3338                size: Size::new(width, lines.len() as f32 * self.line_height),
3339                line_height: self.line_height,
3340                first_baseline: self.line_height * 0.75,
3341                line_count: lines.len(),
3342            }
3343        }
3344    }
3345
3346    fn text_scope(size: Size) -> (DrawScopeDefault, Rc<FixedAdvanceTextMeasurer>) {
3347        let measurer = FixedAdvanceTextMeasurer::shared(10.0, 20.0);
3348        (
3349            DrawScopeDefault::with_text_measurer(size, measurer.clone()),
3350            measurer,
3351        )
3352    }
3353
3354    fn unwrap_text(primitive: &DrawPrimitive) -> &TextPrimitive {
3355        match primitive {
3356            DrawPrimitive::Text(text) => text,
3357            other => panic!("expected text primitive, got {other:?}"),
3358        }
3359    }
3360
3361    #[test]
3362    fn drawn_text_occupies_exactly_the_box_measure_text_reported() {
3363        let (mut scope, _) = text_scope(Size::new(200.0, 100.0));
3364        let style = TextStyle::new(16.0);
3365        let measured = scope.measure_text("ABCD", &style);
3366
3367        scope.draw_text_from(
3368            Point::new(7.0, 11.0),
3369            Brush::solid(Color::WHITE),
3370            "ABCD",
3371            &style,
3372        );
3373
3374        let primitives = scope.into_primitives();
3375        assert_eq!(primitives.len(), 1);
3376        let text = unwrap_text(&primitives[0]);
3377        assert_eq!(
3378            text.rect,
3379            Rect {
3380                x: 7.0,
3381                y: 11.0,
3382                width: measured.size.width,
3383                height: measured.size.height,
3384            },
3385            "the drawn block must be the measured block, or callers cannot center text"
3386        );
3387        assert_eq!(&*text.text, "ABCD");
3388        assert_eq!(text.color, Color::WHITE);
3389    }
3390
3391    #[test]
3392    fn text_alignment_positions_the_measured_block_inside_the_box() {
3393        let box_rect = Rect {
3394            x: 100.0,
3395            y: 50.0,
3396            width: 200.0,
3397            height: 80.0,
3398        };
3399        // "AB" measures 20x20 with the fixed-advance measurer.
3400        let cases = [
3401            (TextAlign::Left, TextVerticalAlign::Top, 100.0, 50.0),
3402            (TextAlign::Center, TextVerticalAlign::Center, 190.0, 80.0),
3403            (TextAlign::Right, TextVerticalAlign::Bottom, 280.0, 110.0),
3404        ];
3405        for (align, vertical_align, expected_x, expected_y) in cases {
3406            let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
3407            let style = TextStyle::new(16.0)
3408                .with_align(align)
3409                .with_vertical_align(vertical_align);
3410            scope.draw_text_at(box_rect, Brush::solid(Color::WHITE), "AB", &style);
3411            let primitives = scope.into_primitives();
3412            let text = unwrap_text(&primitives[0]);
3413            assert!(
3414                approx(text.rect.x, expected_x) && approx(text.rect.y, expected_y),
3415                "{align:?}/{vertical_align:?} placed the block at {:?}",
3416                text.rect
3417            );
3418            assert!(approx(text.rect.width, 20.0) && approx(text.rect.height, 20.0));
3419        }
3420    }
3421
3422    #[test]
3423    fn baseline_aligned_text_hangs_above_the_box_edge() {
3424        let (mut scope, _) = text_scope(Size::new(200.0, 200.0));
3425        let style = TextStyle::new(16.0).with_vertical_align(TextVerticalAlign::Baseline);
3426        let measured = scope.measure_text("Ag", &style);
3427        scope.draw_text_at(
3428            Rect {
3429                x: 0.0,
3430                y: 100.0,
3431                width: 200.0,
3432                height: 0.0,
3433            },
3434            Brush::solid(Color::WHITE),
3435            "Ag",
3436            &style,
3437        );
3438        let primitives = scope.into_primitives();
3439        let text = unwrap_text(&primitives[0]);
3440        // The box edge is the baseline, so the block starts one ascent above it.
3441        assert!(
3442            approx(text.rect.y, 100.0 - measured.first_baseline),
3443            "{:?}",
3444            text.rect
3445        );
3446    }
3447
3448    #[test]
3449    fn draw_text_fills_the_whole_scope_rect() {
3450        let (mut scope, _) = text_scope(Size::new(120.0, 60.0));
3451        let style = TextStyle::new(16.0)
3452            .with_align(TextAlign::Right)
3453            .with_vertical_align(TextVerticalAlign::Bottom);
3454        scope.draw_text(Brush::solid(Color::WHITE), "AB", &style);
3455        let primitives = scope.into_primitives();
3456        let text = unwrap_text(&primitives[0]);
3457        assert!(
3458            approx(text.rect.x, 100.0) && approx(text.rect.y, 40.0),
3459            "{:?}",
3460            text.rect
3461        );
3462    }
3463
3464    #[test]
3465    fn draw_text_from_ignores_alignment_and_anchors_the_top_left() {
3466        let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
3467        // Alignment would move the block if the anchor form honored it.
3468        let style = TextStyle::new(16.0)
3469            .with_align(TextAlign::Center)
3470            .with_vertical_align(TextVerticalAlign::Bottom);
3471        scope.draw_text_from(
3472            Point::new(30.0, 40.0),
3473            Brush::solid(Color::WHITE),
3474            "AB",
3475            &style,
3476        );
3477        let primitives = scope.into_primitives();
3478        let text = unwrap_text(&primitives[0]);
3479        assert!(
3480            approx(text.rect.x, 30.0) && approx(text.rect.y, 40.0),
3481            "{:?}",
3482            text.rect
3483        );
3484    }
3485
3486    #[test]
3487    fn multiline_text_measures_the_widest_line_and_stacks_the_lines() {
3488        let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
3489        let style = TextStyle::new(16.0);
3490        scope.draw_text_from(Point::ZERO, Brush::solid(Color::WHITE), "AB\nABCDE", &style);
3491        let primitives = scope.into_primitives();
3492        let text = unwrap_text(&primitives[0]);
3493        assert!(approx(text.rect.width, 50.0), "{:?}", text.rect);
3494        assert!(approx(text.rect.height, 40.0), "{:?}", text.rect);
3495    }
3496
3497    #[test]
3498    fn empty_text_draws_nothing_and_never_measures() {
3499        let (mut scope, measurer) = text_scope(Size::new(100.0, 100.0));
3500        scope.draw_text(Brush::solid(Color::WHITE), "", &TextStyle::new(16.0));
3501        scope.draw_text_at(
3502            Rect::from_size(Size::new(10.0, 10.0)),
3503            Brush::solid(Color::WHITE),
3504            "",
3505            &TextStyle::new(16.0),
3506        );
3507        scope.draw_text_from(
3508            Point::ZERO,
3509            Brush::solid(Color::WHITE),
3510            "",
3511            &TextStyle::new(16.0),
3512        );
3513        assert!(scope.into_primitives().is_empty());
3514        assert_eq!(
3515            measurer.calls.get(),
3516            0,
3517            "an empty string must not cost a measurement"
3518        );
3519    }
3520
3521    #[test]
3522    fn invisible_text_draws_nothing() {
3523        let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
3524        let style = TextStyle::new(16.0);
3525        scope.draw_text(Brush::solid(Color(1.0, 1.0, 1.0, 0.0)), "AB", &style);
3526        scope.draw_text(
3527            Brush::LinearGradient {
3528                colors: Vec::new(),
3529                stops: None,
3530                start: Point::ZERO,
3531                end: Point::new(1.0, 1.0),
3532                tile_mode: crate::render_effect::TileMode::Clamp,
3533            },
3534            "AB",
3535            &style,
3536        );
3537        assert!(scope.into_primitives().is_empty());
3538    }
3539
3540    #[test]
3541    fn gradient_text_brushes_fall_back_to_their_first_stop() {
3542        let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
3543        scope.draw_text(
3544            Brush::linear_gradient(vec![Color::RED, Color::BLUE]),
3545            "AB",
3546            &TextStyle::new(16.0),
3547        );
3548        let primitives = scope.into_primitives();
3549        assert_eq!(unwrap_text(&primitives[0]).color, Color::RED);
3550    }
3551
3552    #[test]
3553    fn a_scope_without_a_measurer_falls_back_to_the_font_free_estimate() {
3554        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
3555        let style = TextStyle::new(16.0);
3556        assert_eq!(
3557            scope.measure_text("ABC", &style),
3558            crate::estimate_text_measurement("ABC", &style)
3559        );
3560        scope.draw_text_from(Point::ZERO, Brush::solid(Color::WHITE), "ABC", &style);
3561        let primitives = scope.into_primitives();
3562        let text = unwrap_text(&primitives[0]);
3563        assert!(text.rect.width > 0.0 && text.rect.height > 0.0);
3564    }
3565
3566    #[test]
3567    fn degenerate_text_geometry_emits_nothing_and_never_panics() {
3568        struct DegenerateTextMeasurer;
3569        impl DrawTextMeasurer for DegenerateTextMeasurer {
3570            fn measure_text(&self, _text: &str, _style: &TextStyle) -> TextMeasurement {
3571                TextMeasurement {
3572                    size: Size::new(f32::NAN, 0.0),
3573                    line_height: f32::NAN,
3574                    first_baseline: f32::NAN,
3575                    line_count: 1,
3576                }
3577            }
3578        }
3579
3580        let mut scope = DrawScopeDefault::with_text_measurer(
3581            Size::new(50.0, 50.0),
3582            Rc::new(DegenerateTextMeasurer),
3583        );
3584        scope.draw_text(Brush::solid(Color::WHITE), "AB", &TextStyle::new(16.0));
3585        scope.draw_text_at(
3586            Rect {
3587                x: f32::NAN,
3588                y: 0.0,
3589                width: 10.0,
3590                height: 10.0,
3591            },
3592            Brush::solid(Color::WHITE),
3593            "AB",
3594            &TextStyle::new(16.0),
3595        );
3596        assert!(
3597            scope.into_primitives().is_empty(),
3598            "unmeasurable text must not reach the renderer"
3599        );
3600    }
3601
3602    #[test]
3603    fn text_style_survives_lowering_into_the_primitive() {
3604        let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
3605        let style = TextStyle::new(21.0)
3606            .with_font_family("Fira Sans")
3607            .with_weight(FontWeight::BOLD)
3608            .with_style(FontStyle::Italic)
3609            .with_letter_spacing(2.0)
3610            .with_line_height(26.0);
3611        scope.draw_text(Brush::solid(Color::WHITE), "AB", &style);
3612        let primitives = scope.into_primitives();
3613        assert_eq!(unwrap_text(&primitives[0]).style, style);
3614    }
3615
3616    #[test]
3617    fn a_layers_composite_alpha_is_a_truncated_byte() {
3618        for byte in 0..=255u32 {
3619            let exact = byte as f32 / 255.0;
3620            assert!(
3621                (GraphicsLayer::composite_alpha_8bit(exact) - exact).abs() < 1e-6,
3622                "byte {byte} moved"
3623            );
3624            if byte < 255 {
3625                // Anything above a byte and below the next composites at the
3626                // byte below it, however close to the next it sits. Rounding
3627                // would take the top of that range up, and HWUI's `(int)` does
3628                // not.
3629                let nearly_next = (byte as f32 + 0.999) / 255.0;
3630                assert!(
3631                    (GraphicsLayer::composite_alpha_8bit(nearly_next) - exact).abs() < 1e-6,
3632                    "byte {byte} + 0.999 did not truncate"
3633                );
3634            }
3635        }
3636        assert_eq!(GraphicsLayer::composite_alpha_8bit(1.0), 1.0);
3637        assert_eq!(GraphicsLayer::composite_alpha_8bit(0.0), 0.0);
3638        assert_eq!(GraphicsLayer::composite_alpha_8bit(-3.0), 0.0);
3639        assert_eq!(GraphicsLayer::composite_alpha_8bit(7.0), 1.0);
3640    }
3641}