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