Skip to main content

cranpose_ui_graphics/
geometry.rs

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