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