Skip to main content

frust_scene/
builder.rs

1//! [`SceneBuilder`]: the widget-facing API for recording paint commands into a [`Scene`].
2
3use kurbo::{Affine, BezPath, Point, Rect};
4use peniko::{Brush, Color, ImageData};
5
6use crate::glyph::GlyphRun;
7use crate::scene::{Command, CornerRadii, DashPattern, PathStyle, Scene};
8use crate::shader::ShaderProgram;
9
10/// Records paint commands into a [`Scene`], maintaining a transform stack.
11///
12/// Widgets call [`SceneBuilder::push_transform`]/[`SceneBuilder::pop_transform`]
13/// around their subtree's paint calls to compose local offsets/scales with the
14/// ancestor transform, mirroring nested widget placement.
15pub struct SceneBuilder<'a> {
16    scene: &'a mut Scene,
17}
18
19impl<'a> SceneBuilder<'a> {
20    /// Wraps `scene` for recording, starting with an identity transform.
21    ///
22    /// The transform stack's backing allocation lives on `scene` itself
23    /// so it's reused across every call
24    /// instead of reallocating a fresh `Vec` per frame — reset here to
25    /// `[Affine::IDENTITY]`, identical to a freshly allocated stack.
26    pub fn new(scene: &'a mut Scene) -> Self {
27        scene.transform_stack.clear();
28        scene.transform_stack.push(Affine::IDENTITY);
29        Self { scene }
30    }
31
32    /// The transform currently in effect (composition of all pushed transforms).
33    pub fn current_transform(&self) -> Affine {
34        *self
35            .scene
36            .transform_stack
37            .last()
38            .expect("transform stack is never empty")
39    }
40
41    /// Pushes a transform, composed with the current one, onto the stack.
42    ///
43    /// Subsequent paint calls use the composed transform until the matching
44    /// [`SceneBuilder::pop_transform`].
45    pub fn push_transform(&mut self, transform: Affine) {
46        let composed = self.current_transform() * transform;
47        self.scene.transform_stack.push(composed);
48    }
49
50    /// Pops the most recently pushed transform, restoring the previous one.
51    ///
52    /// A no-op if called without a matching `push_transform` (the base identity
53    /// transform is never popped).
54    pub fn pop_transform(&mut self) {
55        if self.scene.transform_stack.len() > 1 {
56            self.scene.transform_stack.pop();
57        }
58    }
59
60    /// Records a filled rectangle under the current transform.
61    pub fn fill_rect(&mut self, rect: Rect, brush: Brush) {
62        let transform = self.current_transform();
63        self.scene.push(Command::FillRect {
64            rect,
65            brush,
66            transform,
67        });
68    }
69
70    /// Records a filled rounded rectangle (uniform corner `radius`) under the
71    /// current transform.
72    ///
73    /// The uniform-radius spelling of [`SceneBuilder::fill_rounded_rect_radii`]
74    /// — kept verbatim so every existing call site is unaffected by per-corner
75    /// radii.
76    pub fn fill_rounded_rect(&mut self, rect: Rect, radius: f64, brush: Brush) {
77        self.fill_rounded_rect_radii(rect, CornerRadii::from(radius), brush);
78    }
79
80    /// Records a filled rounded rectangle with per-corner `radii` under the
81    /// current transform.
82    pub fn fill_rounded_rect_radii(&mut self, rect: Rect, radii: CornerRadii, brush: Brush) {
83        let transform = self.current_transform();
84        self.scene.push(Command::RoundedRect {
85            rect,
86            radii,
87            brush,
88            transform,
89        });
90    }
91
92    /// Records a stroked line segment from `p0` to `p1` under the current
93    /// transform.
94    pub fn stroke_line(&mut self, p0: Point, p1: Point, width: f64, brush: Brush) {
95        let transform = self.current_transform();
96        self.scene.push(Command::Line {
97            p0,
98            p1,
99            width,
100            brush,
101            transform,
102        });
103    }
104
105    /// Records a glyph run, composing its own transform with the current one.
106    pub fn draw_glyph_run(&mut self, mut glyph_run: GlyphRun) {
107        glyph_run.transform = self.current_transform() * glyph_run.transform;
108        self.scene.push(Command::GlyphRun(glyph_run));
109    }
110
111    /// Pushes a rectangular clip onto the render backend's clip stack, recording
112    /// the current transform so the clip is applied in the same space as the
113    /// draws it encloses.
114    pub fn push_clip(&mut self, rect: Rect) {
115        let transform = self.current_transform();
116        self.scene.push(Command::PushClip { rect, transform });
117    }
118
119    /// Pushes a clip with uniformly rounded corners (`radius`, in the
120    /// pre-transform coordinate space) onto the render backend's clip stack,
121    /// recording the current transform so the clip is applied in the same space
122    /// as the draws it encloses.
123    ///
124    /// Popped by [`SceneBuilder::pop_clip`] — the same method that pops a
125    /// rectangular [`SceneBuilder::push_clip`], since both share one clip stack
126    /// (see [`Command::PushClipRounded`]).
127    pub fn push_clip_rounded(&mut self, rect: Rect, radius: f64) {
128        self.push_clip_rounded_radii(rect, CornerRadii::from(radius));
129    }
130
131    /// Pushes a clip with per-corner `radii` onto the render backend's clip
132    /// stack, recording the current transform — the per-corner spelling of
133    /// [`SceneBuilder::push_clip_rounded`], popped by the same
134    /// [`SceneBuilder::pop_clip`].
135    pub fn push_clip_rounded_radii(&mut self, rect: Rect, radii: CornerRadii) {
136        let transform = self.current_transform();
137        self.scene.push(Command::PushClipRounded {
138            rect,
139            radii,
140            transform,
141        });
142    }
143
144    /// Pops the most recently pushed clip, rectangular or rounded.
145    pub fn pop_clip(&mut self) {
146        self.scene.push(Command::PopClip);
147    }
148
149    /// Records a decoded image, scaled to fill `dest`, under the current
150    /// transform.
151    ///
152    /// `data` is cloned into the command — cheap, since `ImageData`'s
153    /// `Blob<u8>` is reference-counted internally (see [`Command::Image`]).
154    pub fn draw_image(&mut self, data: &ImageData, dest: Rect) {
155        let transform = self.current_transform();
156        self.scene.push(Command::Image {
157            data: data.clone(),
158            dest,
159            transform,
160        });
161    }
162
163    /// Records a gaussian-blurred rounded-rectangle shadow (uniform corner
164    /// `radius`) under the current transform (see
165    /// [`Command::BlurredRoundedRect`]).
166    pub fn draw_blurred_rounded_rect(
167        &mut self,
168        rect: Rect,
169        radius: f64,
170        std_dev: f64,
171        color: Color,
172    ) {
173        self.draw_blurred_rounded_rect_radii(rect, CornerRadii::from(radius), std_dev, color);
174    }
175
176    /// Records a gaussian-blurred rounded-rectangle shadow with per-corner
177    /// `radii` under the current transform.
178    ///
179    /// Neither render tier has a per-corner blurred primitive, so a non-uniform
180    /// shadow lowers through [`CornerRadii::largest`] at encode time — see
181    /// [`Command::BlurredRoundedRect`] for why the largest corner and not the
182    /// smallest.
183    pub fn draw_blurred_rounded_rect_radii(
184        &mut self,
185        rect: Rect,
186        radii: CornerRadii,
187        std_dev: f64,
188        color: Color,
189    ) {
190        let transform = self.current_transform();
191        self.scene.push(Command::BlurredRoundedRect {
192            rect,
193            radii,
194            std_dev,
195            color,
196            transform,
197        });
198    }
199
200    /// Pushes a translucent layer onto the render backend's layer stack,
201    /// recording the current transform (see [`Command::PushLayer`]).
202    pub fn push_layer(&mut self, rect: Rect, alpha: f32) {
203        let transform = self.current_transform();
204        self.scene.push(Command::PushLayer {
205            rect,
206            alpha,
207            transform,
208        });
209    }
210
211    /// Pops the most recently pushed layer.
212    pub fn pop_layer(&mut self) {
213        self.scene.push(Command::PopLayer);
214    }
215
216    /// Records a rectangle cleared to full transparency (alpha 0) under the
217    /// current transform (see [`Command::ClearRect`]) — the platform-view
218    /// hole-punch. Unlike [`SceneBuilder::fill_rect`] with a transparent brush
219    /// (which composites transparent *over* the backdrop, a no-op), this erases
220    /// whatever was already drawn beneath the rect.
221    pub fn clear_rect(&mut self, rect: Rect) {
222        let transform = self.current_transform();
223        self.scene.push(Command::ClearRect { rect, transform });
224    }
225
226    /// Records a filled arbitrary vector path (e.g. an arc) under the current
227    /// transform, using the nonzero winding rule.
228    pub fn fill_path(&mut self, path: BezPath, brush: Brush) {
229        let transform = self.current_transform();
230        self.scene.push(Command::Path {
231            path,
232            style: PathStyle::Fill,
233            brush,
234            transform,
235        });
236    }
237
238    /// Records a stroked arbitrary vector path (e.g. an arc), with round
239    /// caps/joins, under the current transform.
240    pub fn stroke_path(&mut self, path: BezPath, width: f64, brush: Brush) {
241        let transform = self.current_transform();
242        self.scene.push(Command::Path {
243            path,
244            style: PathStyle::Stroke { width, dash: None },
245            brush,
246            transform,
247        });
248    }
249
250    /// Records a *dashed* stroked path under the current transform: the same
251    /// stroke [`SceneBuilder::stroke_path`] records, carrying a [`DashPattern`]
252    /// the render crate expands into dash segments at encode time.
253    pub fn stroke_path_dashed(
254        &mut self,
255        path: BezPath,
256        width: f64,
257        dash: DashPattern,
258        brush: Brush,
259    ) {
260        let transform = self.current_transform();
261        self.scene.push(Command::Path {
262            path,
263            style: PathStyle::Stroke {
264                width,
265                dash: Some(dash),
266            },
267            brush,
268            transform,
269        });
270    }
271
272    /// Pushes a snapshot bracket, recording the current transform (see
273    /// [`Command::PushSnapshot`]) and incrementing the scene's snapshot
274    /// depth so the matching [`SceneBuilder::pop_snapshot`] knows a bracket
275    /// is open. `key` identifies the body's cache entry across frames;
276    /// `rect`/`alpha`/`scale` are the presentation parameters a renderer may
277    /// apply on top of the body as a whole.
278    pub fn push_snapshot(&mut self, key: u64, rect: Rect, alpha: f32, scale: f64) {
279        let transform = self.current_transform();
280        self.scene.snapshot_depth += 1;
281        self.scene.push(Command::PushSnapshot {
282            key,
283            rect,
284            alpha,
285            scale,
286            transform,
287        });
288    }
289
290    /// Pops the most recently pushed snapshot bracket. A no-op — matching
291    /// [`Command::PopClip`]/[`Command::PopLayer`]'s unbalanced-pop policy —
292    /// when no bracket is open.
293    pub fn pop_snapshot(&mut self) {
294        if self.scene.snapshot_depth > 0 {
295            self.scene.snapshot_depth -= 1;
296            self.scene.push(Command::PopSnapshot);
297        }
298    }
299
300    /// Records a fragment-shader-filled rectangle, scaled to fill `dest`,
301    /// under the current transform (see [`Command::ShaderQuad`]).
302    ///
303    /// The engine renders the fragment program in a pre-pass into an offscreen
304    /// target and draws it over `dest`. Output is treated as premultiplied
305    /// alpha. The rendering is disabled by `FRUST_ENGINE_NO_SHADER_EFFECTS`.
306    ///
307    /// `program` is cloned into the command — cheap, since [`ShaderProgram`]
308    /// clones its id and its `Arc<str>` source handle, never the source
309    /// text. `time` is seconds, app-supplied.
310    pub fn draw_shader(&mut self, program: &ShaderProgram, dest: Rect, time: f32) {
311        let transform = self.current_transform();
312        self.scene.push(Command::ShaderQuad {
313            program: program.clone(),
314            dest,
315            transform,
316            time,
317        });
318    }
319
320    /// Records an externally owned GPU texture, scaled to fill `dest`, under
321    /// the current transform (see [`Command::SceneTexture`]).
322    ///
323    /// `id` is opaque scene-layer data — only the render backend resolves it
324    /// against textures registered with the GPU context; an unregistered id
325    /// draws nothing.
326    pub fn scene_texture(&mut self, id: u64, dest: Rect) {
327        let transform = self.current_transform();
328        self.scene.push(Command::SceneTexture {
329            id,
330            dest,
331            transform,
332        });
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use crate::glyph::{FontHandle, Glyph};
340    use peniko::FontData;
341    use peniko::color::palette::css::RED;
342
343    fn red_brush() -> Brush {
344        Brush::Solid(RED)
345    }
346
347    #[test]
348    fn fill_rect_uses_identity_transform_by_default() {
349        let mut scene = Scene::new();
350        let mut builder = SceneBuilder::new(&mut scene);
351
352        let rect = Rect::new(0.0, 0.0, 10.0, 20.0);
353        builder.fill_rect(rect, red_brush());
354
355        let commands = scene.commands();
356        assert_eq!(commands.len(), 1);
357        match &commands[0] {
358            Command::FillRect {
359                rect: got_rect,
360                transform,
361                ..
362            } => {
363                assert_eq!(*got_rect, rect);
364                assert_eq!(*transform, Affine::IDENTITY);
365            }
366            other => panic!("expected FillRect, got {other:?}"),
367        }
368    }
369
370    #[test]
371    fn transform_stack_composes_for_fill_rect() {
372        let mut scene = Scene::new();
373        let mut builder = SceneBuilder::new(&mut scene);
374
375        let translate = Affine::translate((10.0, 5.0));
376        let scale = Affine::scale(2.0);
377
378        builder.push_transform(translate);
379        builder.push_transform(scale);
380
381        let rect = Rect::new(0.0, 0.0, 1.0, 1.0);
382        builder.fill_rect(rect, red_brush());
383
384        // Popping one level should leave only `translate` in effect.
385        builder.pop_transform();
386        builder.fill_rect(rect, red_brush());
387
388        // One more pop returns to the base identity transform; a further pop
389        // beyond that is a no-op (the base identity is never popped).
390        builder.pop_transform();
391        builder.pop_transform();
392        builder.fill_rect(rect, red_brush());
393
394        let expected = [translate * scale, translate, Affine::IDENTITY];
395        let commands = scene.commands();
396        assert_eq!(commands.len(), expected.len());
397        for (command, expected_transform) in commands.iter().zip(expected) {
398            match command {
399                Command::FillRect { transform, .. } => assert_eq!(*transform, expected_transform),
400                other => panic!("expected FillRect, got {other:?}"),
401            }
402        }
403    }
404
405    #[test]
406    fn glyph_run_round_trips_its_fields() {
407        let mut scene = Scene::new();
408        let mut builder = SceneBuilder::new(&mut scene);
409
410        let font = FontHandle::new(FontData::new(peniko::Blob::from(Vec::<u8>::new()), 0));
411        let glyphs = vec![
412            Glyph {
413                id: 1,
414                x: 0.0,
415                y: 0.0,
416            },
417            Glyph {
418                id: 2,
419                x: 8.0,
420                y: 0.0,
421            },
422        ];
423        let run = GlyphRun {
424            font: font.clone(),
425            font_size: 16.0,
426            brush: red_brush(),
427            transform: Affine::IDENTITY,
428            glyphs: glyphs.clone(),
429        };
430
431        builder.draw_glyph_run(run);
432
433        match &scene.commands()[0] {
434            Command::GlyphRun(got) => {
435                assert_eq!(got.font_size, 16.0);
436                assert_eq!(got.glyphs, glyphs);
437                assert_eq!(got.transform, Affine::IDENTITY);
438            }
439            other => panic!("expected GlyphRun, got {other:?}"),
440        }
441    }
442
443    #[test]
444    fn glyph_run_composes_with_current_transform() {
445        let mut scene = Scene::new();
446        let mut builder = SceneBuilder::new(&mut scene);
447
448        let translate = Affine::translate((3.0, 4.0));
449        builder.push_transform(translate);
450
451        let font = FontHandle::new(FontData::new(peniko::Blob::from(Vec::<u8>::new()), 0));
452        let run = GlyphRun {
453            font,
454            font_size: 12.0,
455            brush: red_brush(),
456            transform: Affine::IDENTITY,
457            glyphs: vec![],
458        };
459        builder.draw_glyph_run(run);
460
461        match &scene.commands()[0] {
462            Command::GlyphRun(got) => assert_eq!(got.transform, translate),
463            other => panic!("expected GlyphRun, got {other:?}"),
464        }
465    }
466
467    #[test]
468    fn push_pop_clip_emit_commands() {
469        let mut scene = Scene::new();
470        let mut builder = SceneBuilder::new(&mut scene);
471
472        let rect = Rect::new(0.0, 0.0, 5.0, 5.0);
473        builder.push_clip(rect);
474        builder.pop_clip();
475
476        let commands = scene.commands();
477        assert_eq!(commands.len(), 2);
478        assert!(matches!(commands[0], Command::PushClip { .. }));
479        assert!(matches!(commands[1], Command::PopClip));
480    }
481
482    #[test]
483    fn rounded_rect_round_trips_radius_and_transform() {
484        let mut scene = Scene::new();
485        let mut builder = SceneBuilder::new(&mut scene);
486        let translate = Affine::translate((2.0, 3.0));
487        builder.push_transform(translate);
488        let rect = Rect::new(0.0, 0.0, 10.0, 8.0);
489        builder.fill_rounded_rect(rect, 4.0, red_brush());
490
491        match &scene.commands()[0] {
492            Command::RoundedRect {
493                rect: got,
494                radii,
495                transform,
496                ..
497            } => {
498                assert_eq!(*got, rect);
499                // The uniform spelling must encode as all-four-corners-equal.
500                assert_eq!(*radii, CornerRadii::uniform(4.0));
501                assert_eq!(*transform, translate);
502            }
503            other => panic!("expected RoundedRect, got {other:?}"),
504        }
505    }
506
507    #[test]
508    fn rounded_rect_radii_round_trips_each_corner() {
509        let mut scene = Scene::new();
510        let mut builder = SceneBuilder::new(&mut scene);
511        let rect = Rect::new(0.0, 0.0, 10.0, 8.0);
512        let radii = CornerRadii::new(1.0, 2.0, 3.0, 4.0);
513        builder.fill_rounded_rect_radii(rect, radii, red_brush());
514
515        match &scene.commands()[0] {
516            Command::RoundedRect {
517                rect: got,
518                radii: r,
519                ..
520            } => {
521                assert_eq!(*got, rect);
522                assert_eq!(*r, radii);
523            }
524            other => panic!("expected RoundedRect, got {other:?}"),
525        }
526    }
527
528    #[test]
529    fn stroke_line_round_trips_endpoints_and_width() {
530        let mut scene = Scene::new();
531        let mut builder = SceneBuilder::new(&mut scene);
532        let p0 = Point::new(1.0, 2.0);
533        let p1 = Point::new(9.0, 12.0);
534        builder.stroke_line(p0, p1, 2.5, red_brush());
535
536        match &scene.commands()[0] {
537            Command::Line {
538                p0: g0,
539                p1: g1,
540                width,
541                transform,
542                ..
543            } => {
544                assert_eq!(*g0, p0);
545                assert_eq!(*g1, p1);
546                assert_eq!(*width, 2.5);
547                assert_eq!(*transform, Affine::IDENTITY);
548            }
549            other => panic!("expected Line, got {other:?}"),
550        }
551    }
552
553    #[test]
554    fn push_clip_captures_current_transform() {
555        let mut scene = Scene::new();
556        let mut builder = SceneBuilder::new(&mut scene);
557        let scale = Affine::scale(2.0);
558        builder.push_transform(scale);
559        builder.push_clip(Rect::new(0.0, 0.0, 5.0, 5.0));
560
561        match &scene.commands()[0] {
562            Command::PushClip { transform, .. } => assert_eq!(*transform, scale),
563            other => panic!("expected PushClip, got {other:?}"),
564        }
565    }
566
567    #[test]
568    fn push_clip_rounded_pops_through_the_shared_pop_clip() {
569        // A rounded push must pop through the SAME `PopClip` a rectangular
570        // push does — one clip stack, not two.
571        let mut scene = Scene::new();
572        let mut builder = SceneBuilder::new(&mut scene);
573
574        builder.push_clip_rounded(Rect::new(0.0, 0.0, 5.0, 5.0), 2.0);
575        builder.pop_clip();
576
577        let commands = scene.commands();
578        assert_eq!(commands.len(), 2);
579        assert!(matches!(commands[0], Command::PushClipRounded { .. }));
580        assert!(matches!(commands[1], Command::PopClip));
581    }
582
583    #[test]
584    fn push_clip_rounded_round_trips_radius_and_captures_current_transform() {
585        let mut scene = Scene::new();
586        let mut builder = SceneBuilder::new(&mut scene);
587        let scale = Affine::scale(2.0);
588        builder.push_transform(scale);
589        let rect = Rect::new(0.0, 0.0, 40.0, 40.0);
590        builder.push_clip_rounded(rect, 8.0);
591
592        match &scene.commands()[0] {
593            Command::PushClipRounded {
594                rect: got,
595                radii,
596                transform,
597            } => {
598                assert_eq!(*got, rect);
599                assert_eq!(*radii, CornerRadii::uniform(8.0));
600                assert_eq!(*transform, scale);
601            }
602            other => panic!("expected PushClipRounded, got {other:?}"),
603        }
604    }
605
606    #[test]
607    fn push_clip_rounded_radii_round_trips_each_corner() {
608        let mut scene = Scene::new();
609        let mut builder = SceneBuilder::new(&mut scene);
610        let rect = Rect::new(0.0, 0.0, 40.0, 40.0);
611        let radii = CornerRadii::new(8.0, 8.0, 0.0, 0.0);
612        builder.push_clip_rounded_radii(rect, radii);
613        builder.pop_clip();
614
615        let commands = scene.commands();
616        assert_eq!(commands.len(), 2);
617        match &commands[0] {
618            Command::PushClipRounded { radii: r, .. } => assert_eq!(*r, radii),
619            other => panic!("expected PushClipRounded, got {other:?}"),
620        }
621        // Still one clip stack: the per-corner push pops through the shared pop.
622        assert!(matches!(commands[1], Command::PopClip));
623    }
624
625    #[test]
626    fn nested_rounded_clip_and_layer_preserve_push_pop_ordering() {
627        // The radiused-mask-over-a-bitmap shape: a rounded clip enclosing an
628        // alpha layer must nest exactly like the rectangular clip does,
629        // preserving command-stream order.
630        let mut scene = Scene::new();
631        let mut builder = SceneBuilder::new(&mut scene);
632        let clip_rect = Rect::new(0.0, 0.0, 100.0, 100.0);
633        let layer_rect = Rect::new(10.0, 10.0, 50.0, 50.0);
634
635        builder.push_clip_rounded(clip_rect, 12.0);
636        builder.push_layer(layer_rect, 0.75);
637        builder.fill_rect(Rect::new(0.0, 0.0, 1.0, 1.0), red_brush());
638        builder.pop_layer();
639        builder.pop_clip();
640
641        let commands = scene.commands();
642        assert_eq!(commands.len(), 5);
643        assert!(matches!(commands[0], Command::PushClipRounded { .. }));
644        assert!(matches!(commands[1], Command::PushLayer { .. }));
645        assert!(matches!(commands[2], Command::FillRect { .. }));
646        assert!(matches!(commands[3], Command::PopLayer));
647        assert!(matches!(commands[4], Command::PopClip));
648    }
649
650    fn two_by_two_image() -> ImageData {
651        ImageData {
652            data: peniko::Blob::from(vec![0u8; 2 * 2 * 4]),
653            format: peniko::ImageFormat::Rgba8,
654            alpha_type: peniko::ImageAlphaType::Alpha,
655            width: 2,
656            height: 2,
657        }
658    }
659
660    #[test]
661    fn draw_image_round_trips_data_and_dest_under_identity_transform() {
662        let mut scene = Scene::new();
663        let mut builder = SceneBuilder::new(&mut scene);
664        let data = two_by_two_image();
665        let dest = Rect::new(0.0, 0.0, 20.0, 20.0);
666        builder.draw_image(&data, dest);
667
668        match &scene.commands()[0] {
669            Command::Image {
670                data: got_data,
671                dest: got_dest,
672                transform,
673            } => {
674                assert_eq!(*got_data, data);
675                assert_eq!(*got_dest, dest);
676                assert_eq!(*transform, Affine::IDENTITY);
677            }
678            other => panic!("expected Image, got {other:?}"),
679        }
680    }
681
682    #[test]
683    fn draw_image_composes_with_current_transform() {
684        let mut scene = Scene::new();
685        let mut builder = SceneBuilder::new(&mut scene);
686        let translate = Affine::translate((5.0, 6.0));
687        builder.push_transform(translate);
688        let data = two_by_two_image();
689        builder.draw_image(&data, Rect::new(0.0, 0.0, 4.0, 4.0));
690
691        match &scene.commands()[0] {
692            Command::Image { transform, .. } => assert_eq!(*transform, translate),
693            other => panic!("expected Image, got {other:?}"),
694        }
695    }
696
697    #[test]
698    fn draw_shader_round_trips_program_dest_and_time_under_identity_transform() {
699        let mut scene = Scene::new();
700        let mut builder = SceneBuilder::new(&mut scene);
701        let program = ShaderProgram::new("fn main() {}");
702        let dest = Rect::new(0.0, 0.0, 20.0, 20.0);
703        builder.draw_shader(&program, dest, 1.5);
704
705        match &scene.commands()[0] {
706            Command::ShaderQuad {
707                program: got_program,
708                dest: got_dest,
709                transform,
710                time,
711            } => {
712                assert_eq!(got_program.id(), program.id());
713                assert_eq!(*got_dest, dest);
714                assert_eq!(*transform, Affine::IDENTITY);
715                assert_eq!(*time, 1.5);
716            }
717            other => panic!("expected ShaderQuad, got {other:?}"),
718        }
719    }
720
721    #[test]
722    fn draw_shader_composes_with_current_transform() {
723        let mut scene = Scene::new();
724        let mut builder = SceneBuilder::new(&mut scene);
725        let translate = Affine::translate((5.0, 6.0));
726        builder.push_transform(translate);
727        let program = ShaderProgram::new("fn main() {}");
728        builder.draw_shader(&program, Rect::new(0.0, 0.0, 4.0, 4.0), 0.0);
729
730        match &scene.commands()[0] {
731            Command::ShaderQuad { transform, .. } => assert_eq!(*transform, translate),
732            other => panic!("expected ShaderQuad, got {other:?}"),
733        }
734    }
735
736    #[test]
737    fn draw_shader_clones_program_sharing_id() {
738        let mut scene = Scene::new();
739        let mut builder = SceneBuilder::new(&mut scene);
740        let program = ShaderProgram::new("fn main() {}");
741        builder.draw_shader(&program, Rect::new(0.0, 0.0, 4.0, 4.0), 0.0);
742
743        match &scene.commands()[0] {
744            Command::ShaderQuad {
745                program: got_program,
746                ..
747            } => {
748                assert_eq!(got_program.id(), program.id());
749                assert_eq!(got_program.source(), program.source());
750            }
751            other => panic!("expected ShaderQuad, got {other:?}"),
752        }
753    }
754
755    #[test]
756    fn draw_blurred_rounded_rect_round_trips_fields_under_identity_transform() {
757        let mut scene = Scene::new();
758        let mut builder = SceneBuilder::new(&mut scene);
759        let rect = Rect::new(0.0, 0.0, 10.0, 8.0);
760        builder.draw_blurred_rounded_rect(rect, 4.0, 2.5, RED);
761
762        match &scene.commands()[0] {
763            Command::BlurredRoundedRect {
764                rect: got_rect,
765                radii,
766                std_dev,
767                color,
768                transform,
769            } => {
770                assert_eq!(*got_rect, rect);
771                assert_eq!(*radii, CornerRadii::uniform(4.0));
772                assert_eq!(*std_dev, 2.5);
773                assert_eq!(*color, RED);
774                assert_eq!(*transform, Affine::IDENTITY);
775            }
776            other => panic!("expected BlurredRoundedRect, got {other:?}"),
777        }
778    }
779
780    #[test]
781    fn draw_blurred_rounded_rect_composes_with_current_transform() {
782        let mut scene = Scene::new();
783        let mut builder = SceneBuilder::new(&mut scene);
784        let translate = Affine::translate((1.0, 2.0));
785        builder.push_transform(translate);
786        builder.draw_blurred_rounded_rect(Rect::new(0.0, 0.0, 5.0, 5.0), 2.0, 1.0, RED);
787
788        match &scene.commands()[0] {
789            Command::BlurredRoundedRect { transform, .. } => assert_eq!(*transform, translate),
790            other => panic!("expected BlurredRoundedRect, got {other:?}"),
791        }
792    }
793
794    #[test]
795    fn draw_blurred_rounded_rect_radii_round_trips_each_corner() {
796        let mut scene = Scene::new();
797        let mut builder = SceneBuilder::new(&mut scene);
798        let radii = CornerRadii::new(12.0, 12.0, 0.0, 0.0);
799        builder.draw_blurred_rounded_rect_radii(Rect::new(0.0, 0.0, 10.0, 8.0), radii, 2.5, RED);
800
801        match &scene.commands()[0] {
802            Command::BlurredRoundedRect { radii: r, .. } => assert_eq!(*r, radii),
803            other => panic!("expected BlurredRoundedRect, got {other:?}"),
804        }
805    }
806
807    #[test]
808    fn push_pop_layer_emit_commands_with_alpha_and_transform() {
809        let mut scene = Scene::new();
810        let mut builder = SceneBuilder::new(&mut scene);
811        let scale = Affine::scale(2.0);
812        builder.push_transform(scale);
813        let rect = Rect::new(0.0, 0.0, 5.0, 5.0);
814        builder.push_layer(rect, 0.5);
815        builder.pop_layer();
816
817        let commands = scene.commands();
818        assert_eq!(commands.len(), 2);
819        match &commands[0] {
820            Command::PushLayer {
821                rect: got_rect,
822                alpha,
823                transform,
824            } => {
825                assert_eq!(*got_rect, rect);
826                assert_eq!(*alpha, 0.5);
827                assert_eq!(*transform, scale);
828            }
829            other => panic!("expected PushLayer, got {other:?}"),
830        }
831        assert!(matches!(commands[1], Command::PopLayer));
832    }
833
834    #[test]
835    fn push_pop_snapshot_emit_commands_with_key_rect_alpha_scale_and_transform() {
836        let mut scene = Scene::new();
837        let mut builder = SceneBuilder::new(&mut scene);
838        let translate = Affine::translate((2.0, 3.0));
839        builder.push_transform(translate);
840        let rect = Rect::new(0.0, 0.0, 40.0, 40.0);
841        builder.push_snapshot(7, rect, 0.5, 0.9);
842        builder.fill_rect(Rect::new(0.0, 0.0, 1.0, 1.0), red_brush());
843        builder.pop_snapshot();
844
845        let commands = scene.commands();
846        assert_eq!(commands.len(), 3);
847        match &commands[0] {
848            Command::PushSnapshot {
849                key,
850                rect: got_rect,
851                alpha,
852                scale,
853                transform,
854            } => {
855                assert_eq!(*key, 7);
856                assert_eq!(*got_rect, rect);
857                assert_eq!(*alpha, 0.5);
858                assert_eq!(*scale, 0.9);
859                assert_eq!(*transform, translate);
860            }
861            other => panic!("expected PushSnapshot, got {other:?}"),
862        }
863        assert!(matches!(commands[1], Command::FillRect { .. }));
864        assert!(matches!(commands[2], Command::PopSnapshot));
865    }
866
867    #[test]
868    fn pop_snapshot_without_a_push_is_a_no_op() {
869        let mut scene = Scene::new();
870        let mut builder = SceneBuilder::new(&mut scene);
871        builder.pop_snapshot();
872        assert!(scene.commands().is_empty());
873        assert_eq!(scene.snapshot_depth, 0);
874    }
875
876    #[test]
877    fn nested_snapshot_brackets_balance() {
878        let mut scene = Scene::new();
879        let mut builder = SceneBuilder::new(&mut scene);
880        let outer = Rect::new(0.0, 0.0, 100.0, 100.0);
881        let inner = Rect::new(10.0, 10.0, 50.0, 50.0);
882
883        builder.push_snapshot(1, outer, 1.0, 1.0);
884        builder.push_snapshot(2, inner, 0.8, 1.2);
885        builder.pop_snapshot();
886        builder.pop_snapshot();
887        // A further, unmatched pop is a no-op — depth never underflows.
888        builder.pop_snapshot();
889
890        let commands = scene.commands();
891        assert_eq!(commands.len(), 4);
892        assert!(matches!(commands[0], Command::PushSnapshot { key: 1, .. }));
893        assert!(matches!(commands[1], Command::PushSnapshot { key: 2, .. }));
894        assert!(matches!(commands[2], Command::PopSnapshot));
895        assert!(matches!(commands[3], Command::PopSnapshot));
896        assert_eq!(scene.snapshot_depth, 0);
897    }
898
899    #[test]
900    fn push_snapshot_increments_depth_and_pop_decrements_it() {
901        let mut scene = Scene::new();
902        {
903            let mut builder = SceneBuilder::new(&mut scene);
904            builder.push_snapshot(1, Rect::new(0.0, 0.0, 1.0, 1.0), 1.0, 1.0);
905        }
906        assert_eq!(scene.snapshot_depth, 1);
907        {
908            let mut builder = SceneBuilder::new(&mut scene);
909            builder.pop_snapshot();
910        }
911        assert_eq!(scene.snapshot_depth, 0);
912    }
913
914    #[test]
915    fn clear_rect_records_command_with_current_transform() {
916        let mut scene = Scene::new();
917        let mut builder = SceneBuilder::new(&mut scene);
918        let translate = Affine::translate((7.0, 4.0));
919        builder.push_transform(translate);
920        let rect = Rect::new(0.0, 0.0, 80.0, 60.0);
921        builder.clear_rect(rect);
922
923        let commands = scene.commands();
924        assert_eq!(commands.len(), 1);
925        match &commands[0] {
926            Command::ClearRect {
927                rect: got_rect,
928                transform,
929            } => {
930                assert_eq!(*got_rect, rect);
931                assert_eq!(*transform, translate);
932            }
933            other => panic!("expected ClearRect, got {other:?}"),
934        }
935    }
936
937    #[test]
938    fn nested_clip_and_layer_preserve_push_pop_ordering() {
939        // push_clip -> push_layer -> pop_layer -> pop_clip: alpha layers must
940        // nest correctly with clips, preserving command-stream order.
941        let mut scene = Scene::new();
942        let mut builder = SceneBuilder::new(&mut scene);
943        let clip_rect = Rect::new(0.0, 0.0, 100.0, 100.0);
944        let layer_rect = Rect::new(10.0, 10.0, 50.0, 50.0);
945
946        builder.push_clip(clip_rect);
947        builder.push_layer(layer_rect, 0.75);
948        builder.fill_rect(Rect::new(0.0, 0.0, 1.0, 1.0), red_brush());
949        builder.pop_layer();
950        builder.pop_clip();
951
952        let commands = scene.commands();
953        assert_eq!(commands.len(), 5);
954        assert!(matches!(commands[0], Command::PushClip { .. }));
955        assert!(matches!(commands[1], Command::PushLayer { .. }));
956        assert!(matches!(commands[2], Command::FillRect { .. }));
957        assert!(matches!(commands[3], Command::PopLayer));
958        assert!(matches!(commands[4], Command::PopClip));
959    }
960
961    fn triangle_path() -> BezPath {
962        let mut path = BezPath::new();
963        path.move_to((0.0, 0.0));
964        path.line_to((10.0, 0.0));
965        path.line_to((5.0, 10.0));
966        path.close_path();
967        path
968    }
969
970    #[test]
971    fn fill_path_round_trips_path_and_style_under_identity_transform() {
972        let mut scene = Scene::new();
973        let mut builder = SceneBuilder::new(&mut scene);
974        let path = triangle_path();
975        builder.fill_path(path.clone(), red_brush());
976
977        match &scene.commands()[0] {
978            Command::Path {
979                path: got_path,
980                style,
981                transform,
982                ..
983            } => {
984                assert_eq!(*got_path, path);
985                assert_eq!(*style, PathStyle::Fill);
986                assert_eq!(*transform, Affine::IDENTITY);
987            }
988            other => panic!("expected Path, got {other:?}"),
989        }
990    }
991
992    #[test]
993    fn stroke_path_round_trips_width_and_style() {
994        let mut scene = Scene::new();
995        let mut builder = SceneBuilder::new(&mut scene);
996        let path = triangle_path();
997        builder.stroke_path(path.clone(), 2.5, red_brush());
998
999        match &scene.commands()[0] {
1000            Command::Path {
1001                path: got_path,
1002                style,
1003                ..
1004            } => {
1005                assert_eq!(*got_path, path);
1006                // An undashed stroke records `dash: None` — the encode-time
1007                // solid path every pre-dash caller keeps taking.
1008                assert_eq!(
1009                    *style,
1010                    PathStyle::Stroke {
1011                        width: 2.5,
1012                        dash: None
1013                    }
1014                );
1015            }
1016            other => panic!("expected Path, got {other:?}"),
1017        }
1018    }
1019
1020    #[test]
1021    fn stroke_path_dashed_round_trips_its_pattern() {
1022        let mut scene = Scene::new();
1023        let mut builder = SceneBuilder::new(&mut scene);
1024        let path = triangle_path();
1025        let dash = DashPattern::new(4.0, 2.0).with_phase(1.0);
1026        builder.stroke_path_dashed(path.clone(), 2.5, dash, red_brush());
1027
1028        match &scene.commands()[0] {
1029            Command::Path {
1030                path: got_path,
1031                style,
1032                ..
1033            } => {
1034                assert_eq!(*got_path, path);
1035                assert_eq!(
1036                    *style,
1037                    PathStyle::Stroke {
1038                        width: 2.5,
1039                        dash: Some(dash)
1040                    }
1041                );
1042            }
1043            other => panic!("expected Path, got {other:?}"),
1044        }
1045    }
1046
1047    #[test]
1048    fn scene_texture_round_trips_id_and_dest_under_identity_transform() {
1049        let mut scene = Scene::new();
1050        let mut builder = SceneBuilder::new(&mut scene);
1051        let dest = Rect::new(0.0, 0.0, 30.0, 40.0);
1052        builder.scene_texture(42, dest);
1053
1054        match &scene.commands()[0] {
1055            Command::SceneTexture {
1056                id,
1057                dest: got_dest,
1058                transform,
1059            } => {
1060                assert_eq!(*id, 42);
1061                assert_eq!(*got_dest, dest);
1062                assert_eq!(*transform, Affine::IDENTITY);
1063            }
1064            other => panic!("expected SceneTexture, got {other:?}"),
1065        }
1066    }
1067
1068    #[test]
1069    fn scene_texture_composes_with_current_transform() {
1070        let mut scene = Scene::new();
1071        let mut builder = SceneBuilder::new(&mut scene);
1072        let translate = Affine::translate((5.0, 6.0));
1073        builder.push_transform(translate);
1074        builder.scene_texture(7, Rect::new(0.0, 0.0, 4.0, 4.0));
1075
1076        match &scene.commands()[0] {
1077            Command::SceneTexture { transform, .. } => assert_eq!(*transform, translate),
1078            other => panic!("expected SceneTexture, got {other:?}"),
1079        }
1080    }
1081
1082    #[test]
1083    fn fill_path_composes_with_current_transform() {
1084        let mut scene = Scene::new();
1085        let mut builder = SceneBuilder::new(&mut scene);
1086        let translate = Affine::translate((3.0, 4.0));
1087        builder.push_transform(translate);
1088        builder.fill_path(triangle_path(), red_brush());
1089
1090        match &scene.commands()[0] {
1091            Command::Path { transform, .. } => assert_eq!(*transform, translate),
1092            other => panic!("expected Path, got {other:?}"),
1093        }
1094    }
1095}