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