Skip to main content

cranpose_ui_graphics/
geometry.rs

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