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: Vec<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 compact_recording_materializes_in_recorded_order() {
1627        let size = Size::new(100.0, 100.0);
1628        let solid = Brush::solid(Color::WHITE);
1629        let gradient = Brush::vertical_gradient(vec![Color::RED, Color::BLUE], 0.0, 100.0);
1630        let center = Point::new(50.0, 50.0);
1631        let stroke = Stroke::new(4.0);
1632        let rect = Rect {
1633            x: 10.0,
1634            y: 20.0,
1635            width: 30.0,
1636            height: 40.0,
1637        };
1638        let batch = vec![
1639            DrawPrimitive::Content,
1640            DrawPrimitive::Rect {
1641                rect,
1642                brush: solid.clone(),
1643                stroke: None,
1644            },
1645        ];
1646
1647        let record = |scope: &mut DrawScopeDefault| {
1648            scope.draw_rect_at(rect, solid.clone());
1649            scope.draw_arc(solid.clone(), center, 30.0, 0.5, 1.5, stroke);
1650            scope.draw_rect_at(rect, gradient.clone());
1651            scope.draw_circle(solid.clone(), center, 12.0);
1652            scope.draw_arc(solid.clone(), center, 30.0, 0.5, 0.0, stroke);
1653            scope.draw_rect_at_blend(rect, solid.clone(), BlendMode::Plus);
1654            scope.draw_content();
1655            scope.draw_annular_sector(gradient.clone(), center, 10.0, 20.0, 0.0, 2.0);
1656            scope.push_recorded(batch.clone());
1657        };
1658
1659        let mut compact = DrawScopeDefault::new(size);
1660        record(&mut compact);
1661        let finished = compact.finish();
1662
1663        let arc_via_ordinary = |brush: Brush, radius: f32, start: f32, sweep: f32| {
1664            let mut scope = DrawScopeDefault::new(size);
1665            scope.draw_arc(brush, center, radius, start, sweep, stroke);
1666            scope.into_primitives().remove(0)
1667        };
1668        let expected = vec![
1669            DrawPrimitive::Rect {
1670                rect,
1671                brush: solid.clone(),
1672                stroke: None,
1673            },
1674            arc_via_ordinary(solid.clone(), 30.0, 0.5, 1.5),
1675            DrawPrimitive::Rect {
1676                rect,
1677                brush: gradient.clone(),
1678                stroke: None,
1679            },
1680            DrawPrimitive::RoundRect {
1681                rect: Rect {
1682                    x: center.x - 12.0,
1683                    y: center.y - 12.0,
1684                    width: 24.0,
1685                    height: 24.0,
1686                },
1687                brush: solid.clone(),
1688                radii: CornerRadii::uniform(12.0),
1689                stroke: None,
1690            },
1691            DrawPrimitive::Blend {
1692                primitive: Box::new(DrawPrimitive::Rect {
1693                    rect,
1694                    brush: solid.clone(),
1695                    stroke: None,
1696                }),
1697                blend_mode: BlendMode::Plus,
1698            },
1699            DrawPrimitive::Content,
1700            {
1701                let mut scope = DrawScopeDefault::new(size);
1702                scope.draw_annular_sector(gradient.clone(), center, 10.0, 20.0, 0.0, 2.0);
1703                scope.into_primitives().remove(0)
1704            },
1705            DrawPrimitive::Content,
1706            DrawPrimitive::Rect {
1707                rect,
1708                brush: solid.clone(),
1709                stroke: None,
1710            },
1711        ];
1712        assert_eq!(finished.content_markers(), 2);
1713        assert_eq!(finished.into_primitives_with_markers(), expected);
1714    }
1715
1716    #[test]
1717    fn redrawing_the_same_text_shares_one_str_allocation() {
1718        let first = shared_text_str("BREAK THE RING");
1719        let second = shared_text_str("BREAK THE RING");
1720        assert!(Rc::ptr_eq(&first, &second));
1721        assert_eq!(&*second, "BREAK THE RING");
1722    }
1723
1724    #[test]
1725    fn different_text_gets_its_own_str() {
1726        let first = shared_text_str("340");
1727        let second = shared_text_str("350");
1728        assert!(!Rc::ptr_eq(&first, &second));
1729        assert_eq!(&*first, "340");
1730        assert_eq!(&*second, "350");
1731    }
1732
1733    #[test]
1734    fn the_text_pool_survives_overflowing_its_capacity() {
1735        for index in 0..600 {
1736            let text = format!("run-{index}");
1737            assert_eq!(&*shared_text_str(&text), text.as_str());
1738        }
1739        assert_eq!(&*shared_text_str("still correct"), "still correct");
1740    }
1741
1742    fn assert_image_alpha(primitive: &DrawPrimitive, expected: f32) {
1743        match primitive {
1744            DrawPrimitive::Image { alpha, .. } => assert!((alpha - expected).abs() < 1e-5),
1745            DrawPrimitive::Blend { primitive, .. } => assert_image_alpha(primitive, expected),
1746            other => panic!("expected image primitive, got {other:?}"),
1747        }
1748    }
1749
1750    fn unwrap_image(primitive: &DrawPrimitive) -> &DrawPrimitive {
1751        match primitive {
1752            DrawPrimitive::Image { .. } => primitive,
1753            DrawPrimitive::Blend { primitive, .. } => unwrap_image(primitive),
1754            other => panic!("expected image primitive, got {other:?}"),
1755        }
1756    }
1757
1758    #[test]
1759    fn draw_svg_path_emits_supersampled_image_over_path_bounds() {
1760        let mut scope = DrawScopeDefault::new(Size::new(32.0, 32.0));
1761        scope.draw_svg_path("M 4 4 H 20 V 20 H 4 Z", Brush::solid(Color::RED));
1762
1763        let primitives = scope.into_primitives();
1764        assert_eq!(primitives.len(), 1);
1765        let DrawPrimitive::Image { rect, image, .. } = &primitives[0] else {
1766            panic!("expected image primitive, got {:?}", primitives[0]);
1767        };
1768
1769        assert_eq!((rect.x, rect.y), (3.0, 3.0));
1770        assert_eq!((rect.width, rect.height), (18.0, 18.0));
1771        assert_eq!((image.width(), image.height()), (36, 36));
1772
1773        let pixels = image.pixels();
1774        let index = (18 * 36 + 18) * 4;
1775        assert_eq!(
1776            &pixels[index..index + 4],
1777            &[255, 0, 0, 255],
1778            "path interior must be opaque brush color"
1779        );
1780        assert_eq!(pixels[3], 0, "outside the path must stay transparent");
1781    }
1782
1783    #[test]
1784    fn draw_svg_path_ignores_invalid_data() {
1785        let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
1786        scope.draw_svg_path("definitely not a path", Brush::solid(Color::WHITE));
1787        assert!(scope.into_primitives().is_empty());
1788    }
1789
1790    #[test]
1791    fn draw_vector_path_applies_brush_alpha() {
1792        let path = crate::VectorPath::parse("M 0 0 H 8 V 8 H 0 Z").expect("valid path");
1793        let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
1794        scope.draw_vector_path(&path, Brush::solid(Color::rgba(0.0, 0.0, 1.0, 0.5)));
1795
1796        let primitives = scope.into_primitives();
1797        let DrawPrimitive::Image { image, .. } = &primitives[0] else {
1798            panic!("expected image primitive");
1799        };
1800        let pixels = image.pixels();
1801        let width = image.width() as usize;
1802        let index = ((image.height() as usize / 2) * width + width / 2) * 4;
1803        assert_eq!(&pixels[index..index + 3], &[0, 0, 255]);
1804        let alpha = pixels[index + 3];
1805        assert!(
1806            (alpha as i32 - 128).abs() <= 2,
1807            "interior alpha must honor the brush alpha, got {alpha}"
1808        );
1809    }
1810
1811    #[test]
1812    fn the_same_path_and_color_reuse_one_raster() {
1813        let path = crate::VectorPath::parse("M 0 0 H 7 V 7 H 0 Z").expect("valid path");
1814        let raster_of = |brush: Brush| {
1815            let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
1816            scope.draw_vector_path(&path, brush);
1817            let primitives = scope.into_primitives();
1818            let DrawPrimitive::Image { image, .. } = &primitives[0] else {
1819                panic!("expected image primitive");
1820            };
1821            image.clone()
1822        };
1823
1824        let first = raster_of(Brush::solid(Color::rgba(0.0, 0.0, 1.0, 1.0)));
1825        let second = raster_of(Brush::solid(Color::rgba(0.0, 0.0, 1.0, 1.0)));
1826        assert_eq!(first.id(), second.id());
1827
1828        let other_color = raster_of(Brush::solid(Color::rgba(1.0, 0.0, 0.0, 1.0)));
1829        assert_ne!(first.id(), other_color.id());
1830
1831        let wider = crate::VectorPath::parse("M 0 0 H 9 V 7 H 0 Z").expect("valid path");
1832        let mut scope = DrawScopeDefault::new(Size::new(16.0, 16.0));
1833        scope.draw_vector_path(&wider, Brush::solid(Color::rgba(0.0, 0.0, 1.0, 1.0)));
1834        let primitives = scope.into_primitives();
1835        let DrawPrimitive::Image { image, .. } = &primitives[0] else {
1836            panic!("expected image primitive");
1837        };
1838        assert_ne!(first.id(), image.id());
1839    }
1840
1841    #[test]
1842    fn draw_content_inserts_content_marker() {
1843        let mut scope = DrawScopeDefault::new(Size::new(8.0, 8.0));
1844        scope.draw_rect(Brush::solid(Color::WHITE));
1845        scope.draw_content();
1846        scope.draw_rect_blend(Brush::solid(Color::BLACK), BlendMode::DstOut);
1847
1848        let primitives = scope.into_primitives();
1849        assert!(matches!(primitives[1], DrawPrimitive::Content));
1850        assert!(matches!(
1851            primitives[2],
1852            DrawPrimitive::Blend {
1853                blend_mode: BlendMode::DstOut,
1854                ..
1855            }
1856        ));
1857    }
1858
1859    #[test]
1860    fn a_text_block_is_placed_by_its_alignment_inside_the_rect() {
1861        let rect = Rect {
1862            x: 10.0,
1863            y: 20.0,
1864            width: 100.0,
1865            height: 40.0,
1866        };
1867        let measurement = TextMeasurement {
1868            size: Size::new(60.0, 16.0),
1869            line_height: 16.0,
1870            first_baseline: 12.0,
1871            line_count: 1,
1872        };
1873        let style = |align, vertical| {
1874            DrawTextStyle::default()
1875                .with_align(align)
1876                .with_vertical_align(vertical)
1877        };
1878
1879        let left = align_text_block(
1880            rect,
1881            measurement,
1882            &style(TextAlign::Left, TextVerticalAlign::Top),
1883        );
1884        assert_eq!(left, Point::new(10.0, 20.0));
1885
1886        let centered = align_text_block(
1887            rect,
1888            measurement,
1889            &style(TextAlign::Center, TextVerticalAlign::Center),
1890        );
1891        assert_eq!(centered, Point::new(10.0 + 20.0, 20.0 + 12.0));
1892
1893        let right = align_text_block(
1894            rect,
1895            measurement,
1896            &style(TextAlign::Right, TextVerticalAlign::Bottom),
1897        );
1898        assert_eq!(right, Point::new(50.0, 44.0));
1899
1900        let baseline = align_text_block(
1901            rect,
1902            measurement,
1903            &style(TextAlign::Left, TextVerticalAlign::Baseline),
1904        );
1905        assert_eq!(baseline, Point::new(10.0, 20.0 - 12.0));
1906    }
1907
1908    #[test]
1909    fn draw_rect_blend_wraps_non_default_modes() {
1910        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
1911        scope.draw_rect_blend(Brush::solid(Color::RED), BlendMode::DstOut);
1912
1913        let primitives = scope.into_primitives();
1914        assert_eq!(primitives.len(), 1);
1915        match &primitives[0] {
1916            DrawPrimitive::Blend {
1917                primitive,
1918                blend_mode,
1919            } => {
1920                assert_eq!(*blend_mode, BlendMode::DstOut);
1921                assert!(matches!(**primitive, DrawPrimitive::Rect { .. }));
1922            }
1923            other => panic!("expected blended primitive, got {other:?}"),
1924        }
1925    }
1926
1927    #[test]
1928    fn draw_circle_records_centered_round_rect() {
1929        let mut scope = DrawScopeDefault::new(Size::new(40.0, 40.0));
1930        scope.draw_circle(Brush::solid(Color::BLUE), Point::new(12.0, 16.0), 5.0);
1931
1932        let primitives = scope.into_primitives();
1933        assert_eq!(primitives.len(), 1);
1934        match &primitives[0] {
1935            DrawPrimitive::RoundRect { rect, radii, .. } => {
1936                assert_eq!(
1937                    *rect,
1938                    Rect {
1939                        x: 7.0,
1940                        y: 11.0,
1941                        width: 10.0,
1942                        height: 10.0,
1943                    }
1944                );
1945                assert_eq!(*radii, CornerRadii::uniform(5.0));
1946            }
1947            other => panic!("expected circular round rect, got {other:?}"),
1948        }
1949    }
1950
1951    #[test]
1952    fn draw_circle_blend_wraps_non_default_modes() {
1953        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
1954        scope.draw_circle_blend(
1955            Brush::solid(Color::RED),
1956            Point::new(5.0, 5.0),
1957            3.0,
1958            BlendMode::Plus,
1959        );
1960
1961        let primitives = scope.into_primitives();
1962        assert_eq!(primitives.len(), 1);
1963        match &primitives[0] {
1964            DrawPrimitive::Blend {
1965                primitive,
1966                blend_mode,
1967            } => {
1968                assert_eq!(*blend_mode, BlendMode::Plus);
1969                assert!(matches!(**primitive, DrawPrimitive::RoundRect { .. }));
1970            }
1971            other => panic!("expected blended circle primitive, got {other:?}"),
1972        }
1973    }
1974
1975    #[test]
1976    fn rect_union_encloses_both_inputs() {
1977        let lhs = Rect {
1978            x: 10.0,
1979            y: 5.0,
1980            width: 8.0,
1981            height: 4.0,
1982        };
1983        let rhs = Rect {
1984            x: 4.0,
1985            y: 7.0,
1986            width: 10.0,
1987            height: 6.0,
1988        };
1989
1990        assert_eq!(
1991            lhs.union(rhs),
1992            Rect {
1993                x: 4.0,
1994                y: 5.0,
1995                width: 14.0,
1996                height: 8.0,
1997            }
1998        );
1999    }
2000
2001    #[test]
2002    fn draw_image_uses_scope_size_as_default_rect() {
2003        let mut scope = DrawScopeDefault::new(Size::new(40.0, 24.0));
2004        let image = ImageBitmap::from_rgba8(2, 2, vec![255; 16]).expect("image");
2005        scope.draw_image(image.clone());
2006        let primitives = scope.into_primitives();
2007        assert_eq!(primitives.len(), 1);
2008        match unwrap_image(&primitives[0]) {
2009            DrawPrimitive::Image {
2010                rect,
2011                image: actual,
2012                alpha,
2013                color_filter,
2014                sampling,
2015                src_rect,
2016            } => {
2017                assert_eq!(*rect, Rect::from_size(Size::new(40.0, 24.0)));
2018                assert_eq!(*actual, image);
2019                assert_eq!(*alpha, 1.0);
2020                assert!(color_filter.is_none());
2021                assert_eq!(*sampling, ImageSampling::Nearest);
2022                assert!(src_rect.is_none());
2023            }
2024            other => panic!("expected image primitive, got {other:?}"),
2025        }
2026    }
2027
2028    #[test]
2029    fn draw_image_src_stores_src_rect() {
2030        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2031        let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
2032        let src = Rect {
2033            x: 10.0,
2034            y: 20.0,
2035            width: 30.0,
2036            height: 40.0,
2037        };
2038        let dst = Rect {
2039            x: 0.0,
2040            y: 0.0,
2041            width: 60.0,
2042            height: 80.0,
2043        };
2044        scope.draw_image_src(image.clone(), src, dst, 0.8, None);
2045        let primitives = scope.into_primitives();
2046        assert_eq!(primitives.len(), 1);
2047        match unwrap_image(&primitives[0]) {
2048            DrawPrimitive::Image {
2049                rect,
2050                image: actual,
2051                alpha,
2052                sampling,
2053                src_rect,
2054                ..
2055            } => {
2056                assert_eq!(*rect, dst);
2057                assert_eq!(*actual, image);
2058                assert!((alpha - 0.8).abs() < 1e-5);
2059                assert_eq!(*sampling, ImageSampling::Nearest);
2060                assert_eq!(*src_rect, Some(src));
2061            }
2062            other => panic!("expected image primitive, got {other:?}"),
2063        }
2064    }
2065
2066    #[test]
2067    fn draw_image_at_sampled_records_requested_sampling() {
2068        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2069        let image = ImageBitmap::from_rgba8(8, 8, vec![255; 8 * 8 * 4]).expect("image");
2070        let dst = Rect {
2071            x: 2.0,
2072            y: 3.0,
2073            width: 40.0,
2074            height: 30.0,
2075        };
2076
2077        scope.draw_image_at_sampled(dst, image.clone(), 0.7, None, ImageSampling::Linear);
2078
2079        let primitives = scope.into_primitives();
2080        assert_eq!(primitives.len(), 1);
2081        match unwrap_image(&primitives[0]) {
2082            DrawPrimitive::Image {
2083                rect,
2084                image: actual,
2085                alpha,
2086                sampling,
2087                src_rect,
2088                ..
2089            } => {
2090                assert_eq!(*rect, dst);
2091                assert_eq!(*actual, image);
2092                assert!((alpha - 0.7).abs() < 1e-5);
2093                assert_eq!(*sampling, ImageSampling::Linear);
2094                assert!(src_rect.is_none());
2095            }
2096            other => panic!("expected image primitive, got {other:?}"),
2097        }
2098    }
2099
2100    #[test]
2101    fn draw_image_src_sampled_records_requested_sampling() {
2102        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2103        let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
2104        let src = Rect {
2105            x: 4.0,
2106            y: 6.0,
2107            width: 16.0,
2108            height: 20.0,
2109        };
2110        let dst = Rect {
2111            x: 8.0,
2112            y: 10.0,
2113            width: 32.0,
2114            height: 40.0,
2115        };
2116
2117        scope.draw_image_src_sampled(image.clone(), src, dst, 0.5, None, ImageSampling::Linear);
2118
2119        let primitives = scope.into_primitives();
2120        assert_eq!(primitives.len(), 1);
2121        match unwrap_image(&primitives[0]) {
2122            DrawPrimitive::Image {
2123                rect,
2124                image: actual,
2125                alpha,
2126                sampling,
2127                src_rect,
2128                ..
2129            } => {
2130                assert_eq!(*rect, dst);
2131                assert_eq!(*actual, image);
2132                assert!((alpha - 0.5).abs() < 1e-5);
2133                assert_eq!(*sampling, ImageSampling::Linear);
2134                assert_eq!(*src_rect, Some(src));
2135            }
2136            other => panic!("expected image primitive, got {other:?}"),
2137        }
2138    }
2139
2140    #[test]
2141    fn draw_image_at_clamps_alpha() {
2142        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
2143        let image = ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image");
2144        scope.draw_image_at(
2145            Rect::from_origin_size(Point::new(2.0, 3.0), Size::new(5.0, 6.0)),
2146            image,
2147            3.0,
2148            Some(ColorFilter::Tint(Color::from_rgba_u8(128, 128, 255, 255))),
2149        );
2150        assert_image_alpha(&scope.into_primitives()[0], 1.0);
2151    }
2152
2153    #[test]
2154    fn graphics_layer_clone_with_render_effect() {
2155        let layer = GraphicsLayer {
2156            render_effect: Some(RenderEffect::blur(10.0)),
2157            backdrop_effect: Some(RenderEffect::blur(6.0)),
2158            color_filter: Some(ColorFilter::tint(Color::from_rgba_u8(128, 200, 255, 255))),
2159            alpha: 0.5,
2160            rotation_z: 12.0,
2161            shadow_elevation: 4.0,
2162            shape: LayerShape::Rounded(RoundedCornerShape::uniform(6.0)),
2163            clip: true,
2164            compositing_strategy: CompositingStrategy::Offscreen,
2165            blend_mode: BlendMode::SrcOver,
2166            ..Default::default()
2167        };
2168        let cloned = layer.clone();
2169        assert_eq!(cloned.alpha, 0.5);
2170        assert!(cloned.render_effect.is_some());
2171        assert!(cloned.backdrop_effect.is_some());
2172        assert_eq!(layer.color_filter, cloned.color_filter);
2173        assert_eq!(layer.render_effect, cloned.render_effect);
2174        assert_eq!(layer.backdrop_effect, cloned.backdrop_effect);
2175        assert!((cloned.rotation_z - 12.0).abs() < 1e-6);
2176        assert!((cloned.shadow_elevation - 4.0).abs() < 1e-6);
2177        assert_eq!(
2178            cloned.shape,
2179            LayerShape::Rounded(RoundedCornerShape::uniform(6.0))
2180        );
2181        assert!(cloned.clip);
2182        assert_eq!(cloned.compositing_strategy, CompositingStrategy::Offscreen);
2183        assert_eq!(cloned.blend_mode, BlendMode::SrcOver);
2184    }
2185
2186    #[test]
2187    fn graphics_layer_default_has_no_effect() {
2188        let layer = GraphicsLayer::default();
2189        assert!(layer.color_filter.is_none());
2190        assert!(layer.render_effect.is_none());
2191        assert!(layer.backdrop_effect.is_none());
2192        assert_eq!(layer.compositing_strategy, CompositingStrategy::Auto);
2193        assert_eq!(layer.blend_mode, BlendMode::SrcOver);
2194        assert_eq!(layer.alpha, 1.0);
2195        assert_eq!(layer.transform_origin, TransformOrigin::CENTER);
2196        assert!((layer.camera_distance - 8.0).abs() < 1e-6);
2197        assert_eq!(layer.shape, LayerShape::Rectangle);
2198        assert!(!layer.clip);
2199        assert_eq!(layer.ambient_shadow_color, Color::BLACK);
2200        assert_eq!(layer.spot_shadow_color, Color::BLACK);
2201    }
2202
2203    #[test]
2204    fn transform_origin_construction() {
2205        let origin = TransformOrigin::new(0.25, 0.75);
2206        assert!((origin.pivot_fraction_x - 0.25).abs() < 1e-6);
2207        assert!((origin.pivot_fraction_y - 0.75).abs() < 1e-6);
2208    }
2209
2210    #[test]
2211    fn layer_shape_default_is_rectangle() {
2212        assert_eq!(LayerShape::default(), LayerShape::Rectangle);
2213    }
2214
2215    use std::f32::consts::{FRAC_PI_2, PI};
2216
2217    use crate::{StrokeCap, StrokeJoin};
2218
2219    fn approx(a: f32, b: f32) -> bool {
2220        (a - b).abs() < 0.25
2221    }
2222
2223    fn scope(size: f32) -> DrawScopeDefault {
2224        DrawScopeDefault::new(Size::new(size, size))
2225    }
2226
2227    #[test]
2228    fn draw_rect_stroked_records_scope_rect_and_stroke() {
2229        let mut scope = scope(20.0);
2230        scope.draw_rect_stroked(
2231            Brush::solid(Color::RED),
2232            Stroke::new(3.0).with_join(StrokeJoin::Bevel),
2233        );
2234
2235        let primitives = scope.into_primitives();
2236        assert_eq!(primitives.len(), 1);
2237        match &primitives[0] {
2238            DrawPrimitive::Rect {
2239                rect,
2240                stroke: Some(stroke),
2241                ..
2242            } => {
2243                assert_eq!(*rect, Rect::from_size(Size::new(20.0, 20.0)));
2244                assert_eq!(stroke.width, 3.0);
2245                assert_eq!(stroke.join, StrokeJoin::Bevel);
2246            }
2247            other => panic!("expected stroked rect, got {other:?}"),
2248        }
2249    }
2250
2251    #[test]
2252    fn draw_rect_at_stroked_records_requested_rect() {
2253        let mut scope = scope(50.0);
2254        let rect = Rect {
2255            x: 4.0,
2256            y: 6.0,
2257            width: 12.0,
2258            height: 9.0,
2259        };
2260        scope.draw_rect_at_stroked(rect, Brush::solid(Color::BLUE), Stroke::new(2.0));
2261        match &scope.into_primitives()[0] {
2262            DrawPrimitive::Rect {
2263                rect: actual,
2264                stroke: Some(stroke),
2265                ..
2266            } => {
2267                assert_eq!(*actual, rect);
2268                assert_eq!(stroke.width, 2.0);
2269            }
2270            other => panic!("expected stroked rect, got {other:?}"),
2271        }
2272    }
2273
2274    #[test]
2275    fn draw_round_rect_stroked_keeps_radii_and_stroke() {
2276        let mut scope = scope(30.0);
2277        scope.draw_round_rect_stroked(
2278            Brush::solid(Color::GREEN),
2279            CornerRadii::uniform(5.0),
2280            Stroke::new(4.0).with_join(StrokeJoin::Round),
2281        );
2282        match &scope.into_primitives()[0] {
2283            DrawPrimitive::RoundRect {
2284                rect,
2285                radii,
2286                stroke: Some(stroke),
2287                ..
2288            } => {
2289                assert_eq!(*rect, Rect::from_size(Size::new(30.0, 30.0)));
2290                assert_eq!(*radii, CornerRadii::uniform(5.0));
2291                assert_eq!(stroke.width, 4.0);
2292                assert_eq!(stroke.join, StrokeJoin::Round);
2293            }
2294            other => panic!("expected stroked round rect, got {other:?}"),
2295        }
2296    }
2297
2298    #[test]
2299    fn draw_round_rect_at_stroked_records_requested_rect() {
2300        let mut scope = scope(60.0);
2301        let rect = Rect {
2302            x: 1.0,
2303            y: 2.0,
2304            width: 20.0,
2305            height: 10.0,
2306        };
2307        scope.draw_round_rect_at_stroked(
2308            rect,
2309            Brush::solid(Color::WHITE),
2310            CornerRadii::uniform(3.0),
2311            Stroke::new(1.5),
2312        );
2313        match &scope.into_primitives()[0] {
2314            DrawPrimitive::RoundRect {
2315                rect: actual,
2316                radii,
2317                stroke: Some(stroke),
2318                ..
2319            } => {
2320                assert_eq!(*actual, rect);
2321                assert_eq!(*radii, CornerRadii::uniform(3.0));
2322                assert_eq!(stroke.width, 1.5);
2323            }
2324            other => panic!("expected stroked round rect, got {other:?}"),
2325        }
2326    }
2327
2328    #[test]
2329    fn draw_circle_stroked_lowers_to_stroked_round_rect() {
2330        let mut scope = scope(40.0);
2331        scope.draw_circle_stroked(
2332            Brush::solid(Color::BLUE),
2333            Point::new(12.0, 16.0),
2334            5.0,
2335            Stroke::new(2.0),
2336        );
2337        match &scope.into_primitives()[0] {
2338            DrawPrimitive::RoundRect {
2339                rect,
2340                radii,
2341                stroke: Some(stroke),
2342                ..
2343            } => {
2344                assert_eq!(
2345                    *rect,
2346                    Rect {
2347                        x: 7.0,
2348                        y: 11.0,
2349                        width: 10.0,
2350                        height: 10.0,
2351                    }
2352                );
2353                assert_eq!(*radii, CornerRadii::uniform(5.0));
2354                assert_eq!(stroke.width, 2.0);
2355            }
2356            other => panic!("expected stroked circular round rect, got {other:?}"),
2357        }
2358    }
2359
2360    #[test]
2361    fn draw_arc_records_arc_primitive_with_tight_bounds() {
2362        let mut scope = scope(200.0);
2363        scope.draw_arc(
2364            Brush::solid(Color::RED),
2365            Point::new(100.0, 100.0),
2366            50.0,
2367            0.0,
2368            FRAC_PI_2,
2369            Stroke::new(10.0),
2370        );
2371        let primitives = scope.into_primitives();
2372        assert_eq!(primitives.len(), 1);
2373        match &primitives[0] {
2374            DrawPrimitive::Arc {
2375                rect,
2376                center,
2377                radius,
2378                start_angle,
2379                sweep_angle,
2380                stroke: Some(stroke),
2381                inner_radius,
2382                ..
2383            } => {
2384                assert_eq!(*center, Point::new(100.0, 100.0));
2385                assert_eq!(*radius, 50.0);
2386                assert_eq!(*start_angle, 0.0);
2387                assert!(approx(*sweep_angle, FRAC_PI_2));
2388                assert_eq!(stroke.width, 10.0);
2389                assert_eq!(*inner_radius, 0.0);
2390                assert!(approx(rect.x, 100.0), "{rect:?}");
2391                assert!(approx(rect.y, 100.0), "{rect:?}");
2392                assert!(approx(rect.width, 55.0), "{rect:?}");
2393                assert!(approx(rect.height, 55.0), "{rect:?}");
2394            }
2395            other => panic!("expected arc primitive, got {other:?}"),
2396        }
2397    }
2398
2399    #[test]
2400    fn draw_arc_bounds_cover_a_quadrant_spanning_sweep() {
2401        let mut scope = scope(200.0);
2402        scope.draw_arc(
2403            Brush::solid(Color::RED),
2404            Point::new(100.0, 100.0),
2405            50.0,
2406            0.0,
2407            3.0 * FRAC_PI_2,
2408            Stroke::new(4.0),
2409        );
2410        let DrawPrimitive::Arc { rect, .. } = &scope.into_primitives()[0] else {
2411            panic!("expected arc primitive");
2412        };
2413        assert!(approx(rect.x, 48.0), "{rect:?}");
2414        assert!(approx(rect.y, 48.0), "{rect:?}");
2415        assert!(approx(rect.width, 104.0), "{rect:?}");
2416        assert!(approx(rect.height, 104.0), "{rect:?}");
2417    }
2418
2419    #[test]
2420    fn draw_annular_sector_records_inner_radius_and_no_stroke() {
2421        let mut scope = scope(200.0);
2422        scope.draw_annular_sector(
2423            Brush::solid(Color::WHITE),
2424            Point::new(100.0, 100.0),
2425            30.0,
2426            50.0,
2427            0.0,
2428            PI,
2429        );
2430        match &scope.into_primitives()[0] {
2431            DrawPrimitive::Arc {
2432                rect,
2433                center,
2434                radius,
2435                inner_radius,
2436                stroke,
2437                sweep_angle,
2438                ..
2439            } => {
2440                assert!(stroke.is_none(), "annular sectors are filled, not stroked");
2441                assert_eq!(*center, Point::new(100.0, 100.0));
2442                assert_eq!(*radius, 50.0);
2443                assert_eq!(*inner_radius, 30.0);
2444                assert!(approx(*sweep_angle, PI));
2445                assert!(approx(rect.x, 50.0), "{rect:?}");
2446                assert!(approx(rect.y, 100.0), "{rect:?}");
2447                assert!(approx(rect.width, 100.0), "{rect:?}");
2448                assert!(approx(rect.height, 50.0), "{rect:?}");
2449            }
2450            other => panic!("expected arc primitive, got {other:?}"),
2451        }
2452    }
2453
2454    #[test]
2455    fn draw_arc_blend_wraps_non_default_modes() {
2456        let mut scope = scope(100.0);
2457        scope.draw_arc_blend(
2458            Brush::solid(Color::RED),
2459            Point::new(50.0, 50.0),
2460            20.0,
2461            0.0,
2462            1.0,
2463            Stroke::new(2.0),
2464            BlendMode::DstOut,
2465        );
2466        match &scope.into_primitives()[0] {
2467            DrawPrimitive::Blend {
2468                primitive,
2469                blend_mode,
2470            } => {
2471                assert_eq!(*blend_mode, BlendMode::DstOut);
2472                assert!(matches!(**primitive, DrawPrimitive::Arc { .. }));
2473            }
2474            other => panic!("expected blended arc, got {other:?}"),
2475        }
2476    }
2477
2478    #[test]
2479    fn draw_annular_sector_blend_wraps_non_default_modes() {
2480        let mut scope = scope(100.0);
2481        scope.draw_annular_sector_blend(
2482            Brush::solid(Color::RED),
2483            Point::new(50.0, 50.0),
2484            5.0,
2485            20.0,
2486            0.0,
2487            1.0,
2488            BlendMode::Plus,
2489        );
2490        assert!(matches!(
2491            &scope.into_primitives()[0],
2492            DrawPrimitive::Blend {
2493                blend_mode: BlendMode::Plus,
2494                ..
2495            }
2496        ));
2497    }
2498
2499    #[test]
2500    fn stroked_blend_variants_wrap_non_default_modes() {
2501        let mut scope = scope(20.0);
2502        scope.draw_rect_stroked_blend(
2503            Brush::solid(Color::RED),
2504            Stroke::new(2.0),
2505            BlendMode::DstOut,
2506        );
2507        scope.draw_round_rect_stroked_blend(
2508            Brush::solid(Color::RED),
2509            CornerRadii::uniform(2.0),
2510            Stroke::new(2.0),
2511            BlendMode::DstOut,
2512        );
2513        scope.draw_circle_stroked_blend(
2514            Brush::solid(Color::RED),
2515            Point::new(10.0, 10.0),
2516            5.0,
2517            Stroke::new(2.0),
2518            BlendMode::DstOut,
2519        );
2520        let primitives = scope.into_primitives();
2521        assert_eq!(primitives.len(), 3);
2522        for primitive in &primitives {
2523            assert!(
2524                matches!(
2525                    primitive,
2526                    DrawPrimitive::Blend {
2527                        blend_mode: BlendMode::DstOut,
2528                        ..
2529                    }
2530                ),
2531                "expected blended primitive, got {primitive:?}"
2532            );
2533        }
2534    }
2535
2536    #[test]
2537    fn negative_sweeps_and_overlong_sweeps_produce_finite_bounds() {
2538        let mut scope = scope(200.0);
2539        scope.draw_arc(
2540            Brush::solid(Color::RED),
2541            Point::new(100.0, 100.0),
2542            40.0,
2543            FRAC_PI_2,
2544            -FRAC_PI_2,
2545            Stroke::new(4.0),
2546        );
2547        scope.draw_arc(
2548            Brush::solid(Color::RED),
2549            Point::new(100.0, 100.0),
2550            40.0,
2551            0.3,
2552            crate::stroke::TAU * 4.0,
2553            Stroke::new(4.0),
2554        );
2555        let primitives = scope.into_primitives();
2556        assert_eq!(primitives.len(), 2);
2557
2558        let DrawPrimitive::Arc { rect: negative, .. } = &primitives[0] else {
2559            panic!("expected arc");
2560        };
2561        assert!(approx(negative.x, 100.0), "{negative:?}");
2562        assert!(approx(negative.y, 100.0), "{negative:?}");
2563        assert!(approx(negative.width, 42.0), "{negative:?}");
2564
2565        let DrawPrimitive::Arc { rect: full, .. } = &primitives[1] else {
2566            panic!("expected arc");
2567        };
2568        assert!(approx(full.x, 58.0), "{full:?}");
2569        assert!(approx(full.width, 84.0), "{full:?}");
2570        assert!(approx(full.height, 84.0), "{full:?}");
2571    }
2572
2573    #[test]
2574    fn degenerate_stroke_and_arc_inputs_emit_nothing_and_never_panic() {
2575        let mut scope = scope(50.0);
2576        let brush = Brush::solid(Color::RED);
2577        let center = Point::new(25.0, 25.0);
2578
2579        scope.draw_rect_stroked(brush.clone(), Stroke::new(0.0));
2580        scope.draw_rect_stroked(brush.clone(), Stroke::new(-4.0));
2581        scope.draw_rect_stroked(brush.clone(), Stroke::new(f32::NAN));
2582        scope.draw_round_rect_stroked(brush.clone(), CornerRadii::uniform(2.0), Stroke::new(0.0));
2583        scope.draw_circle_stroked(brush.clone(), center, 10.0, Stroke::new(0.0));
2584        scope.draw_circle_stroked(brush.clone(), center, f32::NAN, Stroke::new(2.0));
2585        scope.draw_arc(brush.clone(), center, 10.0, 0.0, 0.0, Stroke::new(2.0));
2586        scope.draw_arc(brush.clone(), center, 10.0, 0.0, f32::NAN, Stroke::new(2.0));
2587        scope.draw_arc(
2588            brush.clone(),
2589            center,
2590            f32::INFINITY,
2591            0.0,
2592            1.0,
2593            Stroke::new(2.0),
2594        );
2595        scope.draw_arc(brush.clone(), center, 10.0, 0.0, 1.0, Stroke::new(0.0));
2596        scope.draw_arc(brush.clone(), center, 0.0, 0.0, 1.0, Stroke::new(0.0));
2597        scope.draw_annular_sector(brush.clone(), center, 10.0, 10.0, 0.0, 1.0);
2598        scope.draw_annular_sector(brush.clone(), center, 20.0, 10.0, 0.0, 1.0);
2599        scope.draw_annular_sector(brush.clone(), center, 0.0, 0.0, 0.0, 1.0);
2600        scope.draw_annular_sector(brush.clone(), center, 0.0, 10.0, 0.0, 0.0);
2601        scope.draw_annular_sector(brush, center, f32::NAN, 10.0, 0.0, 1.0);
2602
2603        assert!(
2604            scope.into_primitives().is_empty(),
2605            "degenerate stroke/arc requests must not reach the renderer"
2606        );
2607    }
2608
2609    #[test]
2610    fn zero_radius_arc_with_positive_width_stays_finite() {
2611        let mut scope = scope(50.0);
2612        scope.draw_arc(
2613            Brush::solid(Color::RED),
2614            Point::new(25.0, 25.0),
2615            0.0,
2616            0.0,
2617            FRAC_PI_2,
2618            Stroke::new(6.0).with_cap(StrokeCap::Round),
2619        );
2620        let primitives = scope.into_primitives();
2621        assert_eq!(primitives.len(), 1);
2622        let DrawPrimitive::Arc { rect, .. } = &primitives[0] else {
2623            panic!("expected arc");
2624        };
2625        for value in [rect.x, rect.y, rect.width, rect.height] {
2626            assert!(value.is_finite(), "{rect:?}");
2627        }
2628        assert!(rect.width > 0.0 && rect.height > 0.0, "{rect:?}");
2629    }
2630
2631    struct FixedAdvanceTextMeasurer {
2632        advance: f32,
2633        line_height: f32,
2634        calls: std::cell::Cell<usize>,
2635    }
2636
2637    impl FixedAdvanceTextMeasurer {
2638        fn shared(advance: f32, line_height: f32) -> Rc<Self> {
2639            Rc::new(Self {
2640                advance,
2641                line_height,
2642                calls: std::cell::Cell::new(0),
2643            })
2644        }
2645    }
2646
2647    impl DrawTextMeasurer for FixedAdvanceTextMeasurer {
2648        fn measure_text(&self, text: &str, _style: &DrawTextStyle) -> TextMeasurement {
2649            self.calls.set(self.calls.get() + 1);
2650            let lines: Vec<&str> = text.split('\n').collect();
2651            let width = lines
2652                .iter()
2653                .map(|line| line.chars().count() as f32 * self.advance)
2654                .fold(0.0_f32, f32::max);
2655            TextMeasurement {
2656                size: Size::new(width, lines.len() as f32 * self.line_height),
2657                line_height: self.line_height,
2658                first_baseline: self.line_height * 0.75,
2659                line_count: lines.len(),
2660            }
2661        }
2662    }
2663
2664    fn text_scope(size: Size) -> (DrawScopeDefault, Rc<FixedAdvanceTextMeasurer>) {
2665        let measurer = FixedAdvanceTextMeasurer::shared(10.0, 20.0);
2666        (
2667            DrawScopeDefault::with_text_measurer(size, measurer.clone()),
2668            measurer,
2669        )
2670    }
2671
2672    fn unwrap_text(primitive: &DrawPrimitive) -> &TextPrimitive {
2673        match primitive {
2674            DrawPrimitive::Text(text) => text,
2675            other => panic!("expected text primitive, got {other:?}"),
2676        }
2677    }
2678
2679    #[test]
2680    fn drawn_text_occupies_exactly_the_box_measure_text_reported() {
2681        let (mut scope, _) = text_scope(Size::new(200.0, 100.0));
2682        let style = DrawTextStyle::new(16.0);
2683        let measured = scope.measure_text("ABCD", &style);
2684
2685        scope.draw_text_from(
2686            Point::new(7.0, 11.0),
2687            Brush::solid(Color::WHITE),
2688            "ABCD",
2689            &style,
2690        );
2691
2692        let primitives = scope.into_primitives();
2693        assert_eq!(primitives.len(), 1);
2694        let text = unwrap_text(&primitives[0]);
2695        assert_eq!(
2696            text.rect,
2697            Rect {
2698                x: 7.0,
2699                y: 11.0,
2700                width: measured.size.width,
2701                height: measured.size.height,
2702            },
2703            "the drawn block must be the measured block, or callers cannot center text"
2704        );
2705        assert_eq!(&*text.text, "ABCD");
2706        assert_eq!(text.color, Color::WHITE);
2707    }
2708
2709    #[test]
2710    fn text_alignment_positions_the_measured_block_inside_the_box() {
2711        let box_rect = Rect {
2712            x: 100.0,
2713            y: 50.0,
2714            width: 200.0,
2715            height: 80.0,
2716        };
2717        let cases = [
2718            (TextAlign::Left, TextVerticalAlign::Top, 100.0, 50.0),
2719            (TextAlign::Center, TextVerticalAlign::Center, 190.0, 80.0),
2720            (TextAlign::Right, TextVerticalAlign::Bottom, 280.0, 110.0),
2721        ];
2722        for (align, vertical_align, expected_x, expected_y) in cases {
2723            let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
2724            let style = DrawTextStyle::new(16.0)
2725                .with_align(align)
2726                .with_vertical_align(vertical_align);
2727            scope.draw_text_at(box_rect, Brush::solid(Color::WHITE), "AB", &style);
2728            let primitives = scope.into_primitives();
2729            let text = unwrap_text(&primitives[0]);
2730            assert!(
2731                approx(text.rect.x, expected_x) && approx(text.rect.y, expected_y),
2732                "{align:?}/{vertical_align:?} placed the block at {:?}",
2733                text.rect
2734            );
2735            assert!(approx(text.rect.width, 20.0) && approx(text.rect.height, 20.0));
2736        }
2737    }
2738
2739    #[test]
2740    fn baseline_aligned_text_hangs_above_the_box_edge() {
2741        let (mut scope, _) = text_scope(Size::new(200.0, 200.0));
2742        let style = DrawTextStyle::new(16.0).with_vertical_align(TextVerticalAlign::Baseline);
2743        let measured = scope.measure_text("Ag", &style);
2744        scope.draw_text_at(
2745            Rect {
2746                x: 0.0,
2747                y: 100.0,
2748                width: 200.0,
2749                height: 0.0,
2750            },
2751            Brush::solid(Color::WHITE),
2752            "Ag",
2753            &style,
2754        );
2755        let primitives = scope.into_primitives();
2756        let text = unwrap_text(&primitives[0]);
2757        assert!(
2758            approx(text.rect.y, 100.0 - measured.first_baseline),
2759            "{:?}",
2760            text.rect
2761        );
2762    }
2763
2764    #[test]
2765    fn draw_text_fills_the_whole_scope_rect() {
2766        let (mut scope, _) = text_scope(Size::new(120.0, 60.0));
2767        let style = DrawTextStyle::new(16.0)
2768            .with_align(TextAlign::Right)
2769            .with_vertical_align(TextVerticalAlign::Bottom);
2770        scope.draw_text(Brush::solid(Color::WHITE), "AB", &style);
2771        let primitives = scope.into_primitives();
2772        let text = unwrap_text(&primitives[0]);
2773        assert!(
2774            approx(text.rect.x, 100.0) && approx(text.rect.y, 40.0),
2775            "{:?}",
2776            text.rect
2777        );
2778    }
2779
2780    #[test]
2781    fn draw_text_from_ignores_alignment_and_anchors_the_top_left() {
2782        let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
2783        let style = DrawTextStyle::new(16.0)
2784            .with_align(TextAlign::Center)
2785            .with_vertical_align(TextVerticalAlign::Bottom);
2786        scope.draw_text_from(
2787            Point::new(30.0, 40.0),
2788            Brush::solid(Color::WHITE),
2789            "AB",
2790            &style,
2791        );
2792        let primitives = scope.into_primitives();
2793        let text = unwrap_text(&primitives[0]);
2794        assert!(
2795            approx(text.rect.x, 30.0) && approx(text.rect.y, 40.0),
2796            "{:?}",
2797            text.rect
2798        );
2799    }
2800
2801    #[test]
2802    fn multiline_text_measures_the_widest_line_and_stacks_the_lines() {
2803        let (mut scope, _) = text_scope(Size::new(400.0, 400.0));
2804        let style = DrawTextStyle::new(16.0);
2805        scope.draw_text_from(Point::ZERO, Brush::solid(Color::WHITE), "AB\nABCDE", &style);
2806        let primitives = scope.into_primitives();
2807        let text = unwrap_text(&primitives[0]);
2808        assert!(approx(text.rect.width, 50.0), "{:?}", text.rect);
2809        assert!(approx(text.rect.height, 40.0), "{:?}", text.rect);
2810    }
2811
2812    #[test]
2813    fn empty_text_draws_nothing_and_never_measures() {
2814        let (mut scope, measurer) = text_scope(Size::new(100.0, 100.0));
2815        scope.draw_text(Brush::solid(Color::WHITE), "", &DrawTextStyle::new(16.0));
2816        scope.draw_text_at(
2817            Rect::from_size(Size::new(10.0, 10.0)),
2818            Brush::solid(Color::WHITE),
2819            "",
2820            &DrawTextStyle::new(16.0),
2821        );
2822        scope.draw_text_from(
2823            Point::ZERO,
2824            Brush::solid(Color::WHITE),
2825            "",
2826            &DrawTextStyle::new(16.0),
2827        );
2828        assert!(scope.into_primitives().is_empty());
2829        assert_eq!(
2830            measurer.calls.get(),
2831            0,
2832            "an empty string must not cost a measurement"
2833        );
2834    }
2835
2836    #[test]
2837    fn invisible_text_draws_nothing() {
2838        let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
2839        let style = DrawTextStyle::new(16.0);
2840        scope.draw_text(Brush::solid(Color(1.0, 1.0, 1.0, 0.0)), "AB", &style);
2841        scope.draw_text(
2842            Brush::LinearGradient {
2843                colors: Vec::new(),
2844                stops: None,
2845                start: Point::ZERO,
2846                end: Point::new(1.0, 1.0),
2847                tile_mode: crate::render_effect::TileMode::Clamp,
2848            },
2849            "AB",
2850            &style,
2851        );
2852        assert!(scope.into_primitives().is_empty());
2853    }
2854
2855    #[test]
2856    fn gradient_text_brushes_fall_back_to_their_first_stop() {
2857        let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
2858        scope.draw_text(
2859            Brush::linear_gradient(vec![Color::RED, Color::BLUE]),
2860            "AB",
2861            &DrawTextStyle::new(16.0),
2862        );
2863        let primitives = scope.into_primitives();
2864        assert_eq!(unwrap_text(&primitives[0]).color, Color::RED);
2865    }
2866
2867    #[test]
2868    fn a_scope_without_a_measurer_falls_back_to_the_font_free_estimate() {
2869        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
2870        let style = DrawTextStyle::new(16.0);
2871        assert_eq!(
2872            scope.measure_text("ABC", &style),
2873            crate::estimate_text_measurement("ABC", &style)
2874        );
2875        scope.draw_text_from(Point::ZERO, Brush::solid(Color::WHITE), "ABC", &style);
2876        let primitives = scope.into_primitives();
2877        let text = unwrap_text(&primitives[0]);
2878        assert!(text.rect.width > 0.0 && text.rect.height > 0.0);
2879    }
2880
2881    #[test]
2882    fn degenerate_text_geometry_emits_nothing_and_never_panics() {
2883        struct DegenerateTextMeasurer;
2884        impl DrawTextMeasurer for DegenerateTextMeasurer {
2885            fn measure_text(&self, _text: &str, _style: &DrawTextStyle) -> TextMeasurement {
2886                TextMeasurement {
2887                    size: Size::new(f32::NAN, 0.0),
2888                    line_height: f32::NAN,
2889                    first_baseline: f32::NAN,
2890                    line_count: 1,
2891                }
2892            }
2893        }
2894
2895        let mut scope = DrawScopeDefault::with_text_measurer(
2896            Size::new(50.0, 50.0),
2897            Rc::new(DegenerateTextMeasurer),
2898        );
2899        scope.draw_text(Brush::solid(Color::WHITE), "AB", &DrawTextStyle::new(16.0));
2900        scope.draw_text_at(
2901            Rect {
2902                x: f32::NAN,
2903                y: 0.0,
2904                width: 10.0,
2905                height: 10.0,
2906            },
2907            Brush::solid(Color::WHITE),
2908            "AB",
2909            &DrawTextStyle::new(16.0),
2910        );
2911        assert!(
2912            scope.into_primitives().is_empty(),
2913            "unmeasurable text must not reach the renderer"
2914        );
2915    }
2916
2917    #[test]
2918    fn text_style_survives_lowering_into_the_primitive() {
2919        let (mut scope, _) = text_scope(Size::new(100.0, 100.0));
2920        let style = DrawTextStyle::new(21.0)
2921            .with_font_family("Fira Sans")
2922            .with_weight(FontWeight::BOLD)
2923            .with_style(FontStyle::Italic)
2924            .with_letter_spacing(2.0)
2925            .with_line_height(26.0);
2926        scope.draw_text(Brush::solid(Color::WHITE), "AB", &style);
2927        let primitives = scope.into_primitives();
2928        assert_eq!(unwrap_text(&primitives[0]).style, style);
2929    }
2930
2931    #[test]
2932    fn a_layers_composite_alpha_is_a_truncated_byte() {
2933        for byte in 0..=255u32 {
2934            let exact = byte as f32 / 255.0;
2935            assert!(
2936                (GraphicsLayer::composite_alpha_8bit(exact) - exact).abs() < 1e-6,
2937                "byte {byte} moved"
2938            );
2939            if byte < 255 {
2940                let nearly_next = (byte as f32 + 0.999) / 255.0;
2941                assert!(
2942                    (GraphicsLayer::composite_alpha_8bit(nearly_next) - exact).abs() < 1e-6,
2943                    "byte {byte} + 0.999 did not truncate"
2944                );
2945            }
2946        }
2947        assert_eq!(GraphicsLayer::composite_alpha_8bit(1.0), 1.0);
2948        assert_eq!(GraphicsLayer::composite_alpha_8bit(0.0), 0.0);
2949        assert_eq!(GraphicsLayer::composite_alpha_8bit(-3.0), 0.0);
2950        assert_eq!(GraphicsLayer::composite_alpha_8bit(7.0), 1.0);
2951    }
2952}