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