Skip to main content

ravenrs_core/
scene.rs

1//! The display list: an ordered list of primitives in logical pixels.
2//!
3//! Widgets append primitives in painter's order (back to front). Renderers may
4//! batch consecutive primitives of the same kind but must preserve this order.
5
6use std::fmt;
7use std::sync::Arc;
8
9use crate::{Color, Point, Rect};
10
11/// How a shape is filled.
12#[derive(Clone, Copy, Debug, PartialEq)]
13#[non_exhaustive]
14#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
15pub enum Brush {
16    /// A single flat color.
17    Solid(Color),
18    /// A two-stop linear gradient.
19    LinearGradient(LinearGradient),
20}
21
22impl From<Color> for Brush {
23    fn from(c: Color) -> Self {
24        Self::Solid(c)
25    }
26}
27
28impl From<LinearGradient> for Brush {
29    fn from(g: LinearGradient) -> Self {
30        Self::LinearGradient(g)
31    }
32}
33
34/// A two-stop linear gradient. `start` and `end` are relative to the shape's
35/// rectangle: `(0, 0)` is its top-left corner and `(1, 1)` its bottom-right.
36#[derive(Clone, Copy, Debug, PartialEq)]
37#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
38pub struct LinearGradient {
39    /// Gradient start, relative to the shape's rectangle.
40    pub start: Point,
41    /// Gradient end, relative to the shape's rectangle.
42    pub end: Point,
43    /// Color at `start`.
44    pub from: Color,
45    /// Color at `end`.
46    pub to: Color,
47}
48
49#[cfg_attr(feature = "live", ravenrs_live::expose)]
50impl LinearGradient {
51    /// Top-to-bottom gradient from `from` to `to`.
52    pub const fn vertical(from: Color, to: Color) -> Self {
53        Self { start: Point::new(0.0, 0.0), end: Point::new(0.0, 1.0), from, to }
54    }
55}
56
57/// A solid stroke drawn inside a shape's edge.
58#[derive(Clone, Copy, Debug, PartialEq)]
59#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
60pub struct Border {
61    /// Stroke width in logical pixels.
62    pub width: f32,
63    /// Stroke color.
64    pub color: Color,
65}
66
67/// A soft drop shadow. `blur` is the Gaussian standard deviation times two,
68/// matching what CSS `box-shadow` calls the blur radius.
69#[derive(Clone, Copy, Debug, PartialEq)]
70#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
71pub struct Shadow {
72    /// Shadow offset from the shape, in logical pixels.
73    pub offset: Point,
74    /// Blur radius in logical pixels (CSS `box-shadow` semantics).
75    pub blur: f32,
76    /// Shadow color, usually translucent black.
77    pub color: Color,
78}
79
80#[cfg_attr(feature = "live", ravenrs_live::expose)]
81impl Shadow {
82    /// The `shadow-soft` token of Raven Dark: `0 8px 24px rgba(0,0,0,0.35)`.
83    pub const fn soft() -> Self {
84        Self { offset: Point::new(0.0, 8.0), blur: 24.0, color: Color::rgba(0.0, 0.0, 0.0, 0.35) }
85    }
86
87    /// The `shadow-raised` token of Raven Dark: `0 16px 40px rgba(0,0,0,0.45)`.
88    pub const fn raised() -> Self {
89        Self { offset: Point::new(0.0, 16.0), blur: 40.0, color: Color::rgba(0.0, 0.0, 0.0, 0.45) }
90    }
91}
92
93/// A rounded rectangle with optional border and shadow. The renderer draws it with
94/// a signed-distance function, so corners stay crisp at any scale factor.
95#[derive(Clone, Copy, Debug, PartialEq)]
96#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
97pub struct Quad {
98    /// Bounds of the rectangle, in logical pixels.
99    pub rect: Rect,
100    /// Corner radii: top-left, top-right, bottom-right, bottom-left.
101    pub corner_radii: [f32; 4],
102    /// Interior fill.
103    pub fill: Brush,
104    /// Optional stroke along the edge.
105    pub border: Option<Border>,
106    /// Optional drop shadow drawn behind the shape.
107    pub shadow: Option<Shadow>,
108}
109
110#[cfg_attr(feature = "live", ravenrs_live::expose)]
111impl Quad {
112    /// Creates a sharp-cornered quad with no border or shadow.
113    ///
114    /// ```
115    /// use ravenrs_core::{Color, Quad, Rect, Scene};
116    ///
117    /// let mut scene = Scene::new();
118    /// scene.push_quad(Quad::new(Rect::new(0.0, 0.0, 120.0, 32.0), Color::hex(0x3b82f6)).radius(6.0));
119    /// assert_eq!(scene.len(), 1);
120    /// ```
121    pub fn new(rect: Rect, fill: impl Into<Brush>) -> Self {
122        Self { rect, corner_radii: [0.0; 4], fill: fill.into(), border: None, shadow: None }
123    }
124
125    /// Sets all four corner radii to `r` logical pixels.
126    pub fn radius(mut self, r: f32) -> Self {
127        self.corner_radii = [r; 4];
128        self
129    }
130
131    /// Adds a border of `width` logical pixels.
132    pub fn border(mut self, width: f32, color: Color) -> Self {
133        self.border = Some(Border { width, color });
134        self
135    }
136
137    /// Adds a drop shadow.
138    pub fn shadow(mut self, shadow: Shadow) -> Self {
139        self.shadow = Some(shadow);
140        self
141    }
142}
143
144/// Font bytes plus a stable identity used as a cache key by the glyph atlas.
145#[derive(Clone)]
146#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
147pub struct FontRef {
148    /// Unique identity of these font bytes; equal ids must mean equal data.
149    pub id: u64,
150    /// Raw font file bytes (TTF, OTF or a collection).
151    pub data: Arc<dyn AsRef<[u8]> + Send + Sync>,
152    /// Index inside a font collection; 0 for single-font files.
153    pub index: u32,
154}
155
156#[cfg_attr(feature = "live", ravenrs_live::expose)]
157impl FontRef {
158    /// Borrows the raw font file bytes.
159    pub fn bytes(&self) -> &[u8] {
160        (*self.data).as_ref()
161    }
162}
163
164impl fmt::Debug for FontRef {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        f.debug_struct("FontRef").field("id", &self.id).field("index", &self.index).finish()
167    }
168}
169
170impl PartialEq for FontRef {
171    fn eq(&self, other: &Self) -> bool {
172        self.id == other.id && self.index == other.index
173    }
174}
175
176/// One shaped glyph and its pen position.
177#[derive(Clone, Copy, Debug, PartialEq)]
178#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
179pub struct PositionedGlyph {
180    /// Glyph index inside the font.
181    pub id: u32,
182    /// Pen position on the baseline, in logical pixels.
183    pub x: f32,
184    /// Baseline position in logical pixels.
185    pub y: f32,
186}
187
188/// Shaped glyphs sharing one font, size and color.
189#[derive(Clone, Debug, PartialEq)]
190#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
191pub struct GlyphRun {
192    /// Font the glyphs come from.
193    pub font: FontRef,
194    /// Font size in logical pixels.
195    pub size: f32,
196    /// Text color.
197    pub color: Color,
198    /// Glyphs in drawing order.
199    pub glyphs: Vec<PositionedGlyph>,
200}
201
202/// Decoded pixels: RGBA8, straight (not premultiplied) alpha, sRGB, row-major.
203/// Cloning is cheap; the renderer caches the GPU texture by `id`.
204#[derive(Clone)]
205#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
206pub struct ImageData {
207    id: u64,
208    width: u32,
209    height: u32,
210    pixels: Arc<[u8]>,
211}
212
213#[cfg_attr(feature = "live", ravenrs_live::expose)]
214impl ImageData {
215    /// Wraps RGBA8 pixels. Returns `None` when the buffer does not match the size.
216    pub fn from_rgba8(width: u32, height: u32, pixels: impl Into<Arc<[u8]>>) -> Option<Self> {
217        let pixels = pixels.into();
218        if width == 0 || height == 0 || pixels.len() != width as usize * height as usize * 4 {
219            return None;
220        }
221        static NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
222        let id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
223        Some(Self { id, width, height, pixels })
224    }
225
226    /// Builds an image by evaluating `f(x, y)` for every pixel (procedural art, tests).
227    pub fn from_fn(width: u32, height: u32, f: impl Fn(u32, u32) -> Color) -> Option<Self> {
228        let mut pixels = Vec::with_capacity(width as usize * height as usize * 4);
229        for y in 0..height {
230            for x in 0..width {
231                let c = f(x, y);
232                let to8 = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
233                pixels.extend_from_slice(&[to8(c.r), to8(c.g), to8(c.b), to8(c.a)]);
234            }
235        }
236        Self::from_rgba8(width, height, pixels)
237    }
238
239    /// Stable identity used as the GPU cache key.
240    pub fn id(&self) -> u64 {
241        self.id
242    }
243
244    /// Width in pixels.
245    pub fn width(&self) -> u32 {
246        self.width
247    }
248
249    /// Height in pixels.
250    pub fn height(&self) -> u32 {
251        self.height
252    }
253
254    /// RGBA8 pixel bytes, row-major, `width * height * 4` long.
255    pub fn pixels(&self) -> &[u8] {
256        &self.pixels
257    }
258
259    /// Width divided by height.
260    pub fn aspect_ratio(&self) -> f32 {
261        self.width as f32 / self.height as f32
262    }
263}
264
265impl fmt::Debug for ImageData {
266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267        f.debug_struct("ImageData").field("id", &self.id).field("size", &(self.width, self.height)).finish()
268    }
269}
270
271impl PartialEq for ImageData {
272    fn eq(&self, other: &Self) -> bool {
273        self.id == other.id
274    }
275}
276
277/// An image drawn into a rectangle, with optional rounded corners.
278#[derive(Clone, Debug, PartialEq)]
279#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
280pub struct ImageQuad {
281    /// Destination rectangle in logical pixels.
282    pub rect: Rect,
283    /// Image to draw.
284    pub image: ImageData,
285    /// The part of the image to show, in normalized coordinates (u0, v0, u1, v1).
286    pub uv: [f32; 4],
287    /// Corner radii in logical pixels: top-left, top-right, bottom-right, bottom-left.
288    pub corner_radii: [f32; 4],
289    /// Opacity multiplier, `0.0` to `1.0`.
290    pub opacity: f32,
291}
292
293#[cfg_attr(feature = "live", ravenrs_live::expose)]
294impl ImageQuad {
295    /// Draws the whole image stretched into `rect`, opaque and sharp-cornered.
296    pub fn new(rect: Rect, image: ImageData) -> Self {
297        Self { rect, image, uv: [0.0, 0.0, 1.0, 1.0], corner_radii: [0.0; 4], opacity: 1.0 }
298    }
299
300    /// Sets all four corner radii to `r` logical pixels.
301    pub fn radius(mut self, r: f32) -> Self {
302        self.corner_radii = [r; 4];
303        self
304    }
305}
306
307/// One step of a [`PathFill`]'s outline.
308#[derive(Clone, Copy, Debug, PartialEq)]
309#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
310pub enum PathElement {
311    /// Starts a contour here.
312    MoveTo(Point),
313    /// A straight line to here.
314    LineTo(Point),
315    /// A cubic Bézier curve through two control points, ending at the last point.
316    CubicTo(Point, Point, Point),
317    /// Closes the contour. A contour left open is closed anyway when filled.
318    Close,
319}
320
321/// Which parts of a self-crossing outline count as inside.
322#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
323#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
324pub enum FillRule {
325    /// Inside wherever the contours wind around the point at all.
326    #[default]
327    NonZero,
328    /// Inside wherever an odd number of contours surround the point.
329    EvenOdd,
330}
331
332/// A color along a gradient, from `0.0` to `1.0`.
333#[derive(Clone, Copy, Debug, PartialEq)]
334#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
335pub struct ColorStop {
336    /// Where along the gradient.
337    pub offset: f32,
338    /// The color there.
339    pub color: Color,
340}
341
342/// What fills a [`PathFill`], in the coordinates of its outline.
343#[derive(Clone, Debug, PartialEq)]
344#[non_exhaustive]
345#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
346pub enum PathPaint {
347    /// One color.
348    Solid(Color),
349    /// Colors along the line from `start` to `end`.
350    Linear {
351        /// Where the first stop is.
352        start: Point,
353        /// Where the last stop is.
354        end: Point,
355        /// The colors, by increasing offset.
356        stops: Arc<[ColorStop]>,
357    },
358    /// Colors from `center` outward to `radius`.
359    Radial {
360        /// Where the first stop is.
361        center: Point,
362        /// How far the last stop is.
363        radius: f32,
364        /// The colors, by increasing offset.
365        stops: Arc<[ColorStop]>,
366    },
367    /// An image, seen through the outline.
368    Image {
369        /// The picture.
370        image: ImageData,
371        /// Takes the image's own square, from 0 to 1 on each side, to where it goes:
372        /// the same six numbers as [`PathFill::transform`].
373        placement: [f32; 6],
374    },
375}
376
377/// What a [`PathFill`] is seen through, in the same coordinates: another outline.
378#[derive(Clone, Debug, PartialEq)]
379#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
380pub struct PathClip {
381    /// The outline.
382    pub elements: Arc<[PathElement]>,
383    /// Which parts of it are inside.
384    pub rule: FillRule,
385    /// Shows what is outside the outline, instead of what is inside.
386    pub inverted: bool,
387}
388
389/// A shape made of curves, filled with exact coverage at each pixel, so it stays
390/// sharp at any size. A stroke is a fill of its expanded outline.
391#[derive(Clone, Debug, PartialEq)]
392#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
393pub struct PathFill {
394    /// The outline.
395    pub elements: Arc<[PathElement]>,
396    /// Which parts of it are inside.
397    pub rule: FillRule,
398    /// What fills it.
399    pub paint: PathPaint,
400    /// `[a, b, c, d, e, f]`, taking `(x, y)` to `(a x + c y + e, b x + d y + f)` in
401    /// logical pixels.
402    pub transform: [f32; 6],
403    /// Multiplies its alpha, from `0.0` to `1.0`.
404    pub opacity: f32,
405    /// Shows it only through this outline.
406    pub clip: Option<Arc<PathClip>>,
407}
408
409#[cfg_attr(feature = "live", ravenrs_live::expose)]
410impl PathFill {
411    /// The transform that changes nothing.
412    pub const IDENTITY: [f32; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
413
414    /// `elements` filled with one color, in logical pixels.
415    pub fn solid(elements: impl Into<Arc<[PathElement]>>, color: Color) -> Self {
416        Self {
417            elements: elements.into(),
418            rule: FillRule::NonZero,
419            paint: PathPaint::Solid(color),
420            transform: Self::IDENTITY,
421            opacity: 1.0,
422            clip: None,
423        }
424    }
425}
426
427/// One entry of the display list.
428#[derive(Clone, Debug, PartialEq)]
429#[non_exhaustive]
430#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
431pub enum Primitive {
432    /// A rounded rectangle.
433    Quad(Quad),
434    /// A run of shaped glyphs.
435    Glyphs(GlyphRun),
436    /// A textured rectangle.
437    Image(ImageQuad),
438    /// A filled shape made of curves.
439    Path(PathFill),
440    /// Restricts everything until the matching [`Primitive::PopClip`] to the
441    /// intersection of this rectangle and the enclosing clip.
442    PushClip(Rect),
443    /// Restores the clip active before the matching [`Primitive::PushClip`].
444    PopClip,
445}
446
447/// A visual change applied to painted content without touching layout: what a
448/// compositor would do to a layer, here done to the display list.
449#[derive(Clone, Copy, Debug, PartialEq)]
450#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
451pub struct Effect {
452    /// Multiplies alpha, `0.0` to `1.0`. Each primitive fades on its own, so
453    /// shapes that overlap show through each other while partly faded.
454    pub opacity: f32,
455    /// Movement in logical pixels.
456    pub offset: Point,
457    /// Size multiplier around the pivot.
458    pub scale: f32,
459}
460
461#[cfg_attr(feature = "live", ravenrs_live::expose)]
462impl Effect {
463    /// No change.
464    pub const IDENTITY: Self = Self { opacity: 1.0, offset: Point::new(0.0, 0.0), scale: 1.0 };
465
466    /// Fully transparent.
467    pub const fn hidden() -> Self {
468        Self { opacity: 0.0, ..Self::IDENTITY }
469    }
470
471    /// Moved by `(x, y)` logical pixels.
472    pub const fn moved(x: f32, y: f32) -> Self {
473        Self { offset: Point::new(x, y), ..Self::IDENTITY }
474    }
475
476    /// Scaled around the pivot.
477    pub const fn scaled(scale: f32) -> Self {
478        Self { scale, ..Self::IDENTITY }
479    }
480
481    /// The same effect with this opacity.
482    pub const fn with_opacity(mut self, opacity: f32) -> Self {
483        self.opacity = opacity;
484        self
485    }
486
487    /// The same effect with this scale.
488    pub const fn with_scale(mut self, scale: f32) -> Self {
489        self.scale = scale;
490        self
491    }
492
493    /// The effect `t` of the way from `self` to `to`: `0.0` is `self`, `1.0` is `to`.
494    /// `t` is not clamped, so springs and overshooting curves carry through.
495    pub fn lerp(self, to: Self, t: f32) -> Self {
496        let mix = |a: f32, b: f32| a + (b - a) * t;
497        Self {
498            opacity: mix(self.opacity, to.opacity).clamp(0.0, 1.0),
499            offset: Point::new(mix(self.offset.x, to.offset.x), mix(self.offset.y, to.offset.y)),
500            scale: mix(self.scale, to.scale).max(0.0),
501        }
502    }
503
504    /// `self` followed by `next`, as one effect: opacities and scales multiply and
505    /// offsets add, the way two nested elements with these effects would look.
506    pub fn then(self, next: Self) -> Self {
507        Self {
508            opacity: self.opacity * next.opacity,
509            offset: Point::new(self.offset.x + next.offset.x, self.offset.y + next.offset.y),
510            scale: self.scale * next.scale,
511        }
512    }
513
514    /// True when applying the effect would change nothing.
515    pub fn is_identity(&self) -> bool {
516        *self == Self::IDENTITY
517    }
518
519    fn point(&self, p: Point, pivot: Point) -> Point {
520        Point::new(
521            pivot.x + (p.x - pivot.x) * self.scale + self.offset.x,
522            pivot.y + (p.y - pivot.y) * self.scale + self.offset.y,
523        )
524    }
525
526    fn rect(&self, r: Rect, pivot: Point) -> Rect {
527        Rect::from_origin_size(self.point(r.origin, pivot), r.size.scale(self.scale))
528    }
529
530    fn color(&self, c: Color) -> Color {
531        c.with_alpha(c.a * self.opacity)
532    }
533
534    fn apply(&self, primitive: &mut Primitive, pivot: Point) {
535        match primitive {
536            Primitive::Quad(q) => {
537                q.rect = self.rect(q.rect, pivot);
538                q.corner_radii = q.corner_radii.map(|r| r * self.scale);
539                q.fill = match q.fill {
540                    Brush::Solid(c) => Brush::Solid(self.color(c)),
541                    Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradient {
542                        from: self.color(g.from),
543                        to: self.color(g.to),
544                        ..g
545                    }),
546                };
547                if let Some(b) = &mut q.border {
548                    b.width *= self.scale;
549                    b.color = self.color(b.color);
550                }
551                if let Some(s) = &mut q.shadow {
552                    s.offset = Point::new(s.offset.x * self.scale, s.offset.y * self.scale);
553                    s.blur *= self.scale;
554                    s.color = self.color(s.color);
555                }
556            }
557            Primitive::Glyphs(run) => {
558                run.size *= self.scale;
559                run.color = self.color(run.color);
560                for g in &mut run.glyphs {
561                    let p = self.point(Point::new(g.x, g.y), pivot);
562                    (g.x, g.y) = (p.x, p.y);
563                }
564            }
565            Primitive::Path(path) => {
566                path.opacity *= self.opacity;
567                let [a, b, c, d, e, f] = path.transform;
568                let k = self.scale;
569                path.transform = [
570                    a * k,
571                    b * k,
572                    c * k,
573                    d * k,
574                    pivot.x + self.offset.x + (e - pivot.x) * k,
575                    pivot.y + self.offset.y + (f - pivot.y) * k,
576                ];
577            }
578            Primitive::Image(image) => {
579                image.rect = self.rect(image.rect, pivot);
580                image.corner_radii = image.corner_radii.map(|r| r * self.scale);
581                image.opacity *= self.opacity;
582            }
583            Primitive::PushClip(rect) => *rect = self.rect(*rect, pivot),
584            Primitive::PopClip => {}
585        }
586    }
587}
588
589impl Default for Effect {
590    fn default() -> Self {
591        Self::IDENTITY
592    }
593}
594
595/// An ordered display list. Reused between frames to avoid reallocating.
596#[derive(Clone, Debug, Default)]
597#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
598pub struct Scene {
599    primitives: Vec<Primitive>,
600    clear_color: Color,
601}
602
603/// A closure of the playground draws into a scene of its own, which is then added to the real
604/// one, as a `&mut Scene` cannot leave the paint.
605#[cfg(feature = "live")]
606impl ravenrs_live::bridge::Scratch for Scene {
607    fn fresh() -> Self {
608        Self::new()
609    }
610
611    fn merge_into(self, target: &mut Self) {
612        target.extend_from_slice(self.primitives());
613    }
614}
615
616#[cfg_attr(feature = "live", ravenrs_live::expose)]
617impl Scene {
618    /// Creates an empty display list with a transparent clear color.
619    pub fn new() -> Self {
620        Self::default()
621    }
622
623    /// Empties the list but keeps its allocation.
624    pub fn clear(&mut self) {
625        self.primitives.clear();
626    }
627
628    /// Sets the color the frame is cleared to before drawing.
629    pub fn set_clear_color(&mut self, color: Color) {
630        self.clear_color = color;
631    }
632
633    /// Color the frame is cleared to before drawing.
634    pub fn clear_color(&self) -> Color {
635        self.clear_color
636    }
637
638    /// Appends a shape made of curves.
639    pub fn push_path(&mut self, path: PathFill) {
640        self.primitives.push(Primitive::Path(path));
641    }
642
643    /// Appends a rounded rectangle.
644    pub fn push_quad(&mut self, quad: Quad) {
645        self.primitives.push(Primitive::Quad(quad));
646    }
647
648    /// Appends a glyph run; empty runs are skipped.
649    pub fn push_glyphs(&mut self, run: GlyphRun) {
650        if !run.glyphs.is_empty() {
651            self.primitives.push(Primitive::Glyphs(run));
652        }
653    }
654
655    /// Appends an image.
656    pub fn push_image(&mut self, image: ImageQuad) {
657        self.primitives.push(Primitive::Image(image));
658    }
659
660    /// Clips subsequent primitives to `rect` (intersected with the current clip)
661    /// until [`Scene::pop_clip`].
662    pub fn push_clip(&mut self, rect: Rect) {
663        self.primitives.push(Primitive::PushClip(rect));
664    }
665
666    /// Ends the clip started by the matching [`Scene::push_clip`].
667    pub fn pop_clip(&mut self) {
668        self.primitives.push(Primitive::PopClip);
669    }
670
671    /// Applies `effect` to a range of primitives, scaling around `pivot`.
672    ///
673    /// ```
674    /// use ravenrs_core::{Color, Effect, Point, Primitive, Quad, Rect, Scene};
675    ///
676    /// let mut scene = Scene::new();
677    /// scene.push_quad(Quad::new(Rect::new(0.0, 0.0, 10.0, 10.0), Color::WHITE));
678    /// scene.apply_effect(0..1, Effect::moved(5.0, 0.0), Point::ZERO);
679    /// let Primitive::Quad(q) = &scene.primitives()[0] else { unreachable!() };
680    /// assert_eq!(q.rect.x(), 5.0);
681    /// ```
682    pub fn apply_effect(&mut self, range: std::ops::Range<usize>, effect: Effect, pivot: Point) {
683        if effect.is_identity() {
684            return;
685        }
686        if let Some(primitives) = self.primitives.get_mut(range) {
687            for primitive in primitives {
688                effect.apply(primitive, pivot);
689            }
690        }
691    }
692
693    /// Appends primitives recorded earlier.
694    pub fn extend_from_slice(&mut self, primitives: &[Primitive]) {
695        self.primitives.extend_from_slice(primitives);
696    }
697
698    /// All primitives in painter's order.
699    pub fn primitives(&self) -> &[Primitive] {
700        &self.primitives
701    }
702
703    /// Number of primitives.
704    pub fn len(&self) -> usize {
705        self.primitives.len()
706    }
707
708    /// Returns `true` when no primitives have been pushed.
709    pub fn is_empty(&self) -> bool {
710        self.primitives.is_empty()
711    }
712}
713
714#[cfg(test)]
715mod tests {
716    use super::*;
717
718    #[test]
719    fn image_data_validates_size_and_gets_unique_ids() {
720        assert!(ImageData::from_rgba8(2, 2, vec![0u8; 15]).is_none());
721        let a = ImageData::from_rgba8(2, 2, vec![0u8; 16]).unwrap();
722        let b = ImageData::from_fn(2, 1, |x, _| if x == 0 { Color::WHITE } else { Color::BLACK }).unwrap();
723        assert_ne!(a.id(), b.id());
724        assert_eq!(&b.pixels()[..8], &[255, 255, 255, 255, 0, 0, 0, 255]);
725        assert_eq!(a, a.clone());
726    }
727
728    #[test]
729    fn empty_glyph_runs_are_skipped() {
730        let mut scene = Scene::new();
731        scene.push_glyphs(GlyphRun {
732            font: FontRef { id: 1, data: Arc::new(Vec::<u8>::new()), index: 0 },
733            size: 12.0,
734            color: Color::WHITE,
735            glyphs: vec![],
736        });
737        assert!(scene.is_empty());
738        scene.push_quad(Quad::new(Rect::new(0.0, 0.0, 1.0, 1.0), Color::BLACK).radius(4.0));
739        assert_eq!(scene.len(), 1);
740        scene.clear();
741        assert!(scene.is_empty());
742    }
743}