Skip to main content

inset_embedder/
scene_builder.rs

1//! Flutter counterpart: dart:ui `SceneBuilder` / `Scene`.
2//!
3//! The engine's builder composites retained `EngineLayer`s; valo composites a display list, so
4//! every push is a canvas scope on one recording and `build` closes it. Layers push here every
5//! frame; nothing is retained between frames (see the rendering crate's `PORTING.md`).
6
7use std::fmt;
8use std::sync::Arc;
9
10use crate::geometry::{Clip, Color, Matrix4, Offset, RRect, Rect, rrect_radii_elliptical};
11use crate::painting::{
12    Backdrop, BlendMode, Canvas, ClipOp, FillRule, ImageFilter, Paint, Path, Picture, valo,
13};
14
15/// Flutter `Scene` — what [`SceneBuilder::build`] produces and [`crate::View::present`] takes.
16pub type Scene = Picture;
17
18/// Builds a [`Scene`] containing the given visuals.
19///
20/// A [`Scene`] can then be rendered using [`crate::View::present`].
21///
22/// To draw graphical operations onto a [`Scene`], first create a [`Canvas`], record into it, and
23/// add the resulting [`Picture`] with [`add_picture`](Self::add_picture). Each `push_*` opens a
24/// compositing scope that [`pop`](Self::pop) closes; unbalanced scopes are a bug the builder
25/// asserts on.
26pub struct SceneBuilder {
27    canvas: Canvas,
28    /// Open `push_*` scopes; every `pop` closes one. dart:ui asserts the same balance.
29    open_scopes: usize,
30}
31
32impl fmt::Debug for SceneBuilder {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        f.debug_struct("SceneBuilder")
35            .field("open_scopes", &self.open_scopes)
36            .finish_non_exhaustive()
37    }
38}
39
40impl Default for SceneBuilder {
41    fn default() -> SceneBuilder {
42        SceneBuilder::new()
43    }
44}
45
46impl SceneBuilder {
47    /// Creates an empty [`SceneBuilder`] object.
48    pub fn new() -> SceneBuilder {
49        SceneBuilder {
50            canvas: Canvas::new(),
51            open_scopes: 0,
52        }
53    }
54
55    /// Pushes a transform operation onto the operation stack.
56    ///
57    /// The objects are transformed by the given matrix before rasterization.
58    pub fn push_transform(&mut self, matrix4: &Matrix4) {
59        self.open(|canvas| {
60            canvas.save();
61            canvas.concat(matrix4);
62        });
63    }
64
65    /// Pushes an offset operation onto the operation stack.
66    ///
67    /// This is equivalent to [`push_transform`](Self::push_transform) with a matrix with only
68    /// translation.
69    pub fn push_offset(&mut self, dx: f64, dy: f64) {
70        self.open(|canvas| {
71            canvas.save();
72            canvas.translate(dx as f32, dy as f32);
73        });
74    }
75
76    /// Pushes a rectangular clip operation onto the operation stack.
77    ///
78    /// Rasterization outside the given rectangle is discarded.
79    pub fn push_clip_rect(&mut self, rect: Rect, clip_behavior: Clip) {
80        self.open(|canvas| {
81            open_clip(canvas, clip_behavior, rect.into());
82            canvas.clip_rect(rect, ClipOp::Intersect);
83        });
84    }
85
86    /// Pushes a rounded-rectangular clip operation onto the operation stack.
87    ///
88    /// Rasterization outside the given rounded rectangle is discarded.
89    pub fn push_clip_rrect(&mut self, rrect: RRect, clip_behavior: Clip) {
90        self.open(|canvas| {
91            open_clip(canvas, clip_behavior, rrect.outer_rect().into());
92            canvas.clip_rrect_radii_elliptical(
93                rrect.outer_rect(),
94                rrect_radii_elliptical(rrect),
95                ClipOp::Intersect,
96            );
97        });
98    }
99
100    /// Pushes a path clip operation onto the operation stack.
101    ///
102    /// Rasterization outside the given path is discarded.
103    pub fn push_clip_path(&mut self, path: &Arc<Path>, clip_behavior: Clip) {
104        self.open(|canvas| {
105            open_clip(canvas, clip_behavior, path.bounds());
106            canvas.clip_path(path, FillRule::NonZero, ClipOp::Intersect);
107        });
108    }
109
110    /// Pushes an opacity operation onto the operation stack.
111    ///
112    /// The given alpha value is blended into the alpha value of the objects' rasterization. An
113    /// alpha value of 0 makes the objects entirely invisible. An alpha value of 255 has no
114    /// effect (i.e., the objects retain the current opacity).
115    pub fn push_opacity(&mut self, alpha: i32, offset: Offset) {
116        self.open(|canvas| {
117            let paint = Paint {
118                color: Color::from_argb(alpha, 0, 0, 0).into(),
119                ..Paint::default()
120            };
121            canvas.save_layer(None, &paint);
122            canvas.translate(offset.dx() as f32, offset.dy() as f32);
123        });
124    }
125
126    /// Pushes a backdrop filter operation onto the operation stack.
127    ///
128    /// The given filter is applied to the current contents of the scene as far back as the most
129    /// recent save layer and rendered back to the scene using the indicated `blend_mode` prior to
130    /// rasterizing the child layers.
131    ///
132    /// Filters sharing a `backdrop_id` sample one backdrop, as Flutter's `BackdropKey` groups
133    /// them. valo's backdrop is a blur: a colour filter in `filter` contributes nothing (see the
134    /// host's `PORTING.md`).
135    pub fn push_backdrop_filter(
136        &mut self,
137        filter: &ImageFilter,
138        blend_mode: BlendMode,
139        backdrop_id: Option<u64>,
140    ) {
141        let (sigma, bounds) = backdrop_blur(filter).unwrap_or((0.0, None));
142        let mut backdrop = Backdrop::blur(sigma as f32);
143        if let Some(id) = backdrop_id {
144            backdrop = backdrop.shared(id);
145        }
146        let paint = Paint {
147            blend_mode,
148            ..Paint::default()
149        };
150        self.open(|canvas| {
151            canvas.save_layer_backdrop(bounds.map(Into::into), &paint, backdrop);
152        });
153    }
154
155    /// Ends the effect of the most recently pushed operation.
156    ///
157    /// # Panics
158    ///
159    /// In debug builds, if nothing is pushed.
160    pub fn pop(&mut self) {
161        debug_assert!(self.open_scopes > 0, "SceneBuilder::pop without a push");
162        self.open_scopes -= 1;
163        self.canvas.restore();
164    }
165
166    /// Adds a [`Picture`] to the scene.
167    ///
168    /// The picture is rasterized at the given `offset`. The hints are for a raster cache valo
169    /// does not have; they are accepted so a layer can pass what Flutter passes.
170    pub fn add_picture(
171        &mut self,
172        offset: Offset,
173        picture: &Arc<Picture>,
174        is_complex_hint: bool,
175        will_change_hint: bool,
176    ) {
177        let _ = (is_complex_hint, will_change_hint);
178        self.canvas.save();
179        self.canvas
180            .translate(offset.dx() as f32, offset.dy() as f32);
181        self.canvas.draw_display_list(picture);
182        self.canvas.restore();
183    }
184
185    /// Finishes building the scene.
186    ///
187    /// # Panics
188    ///
189    /// In debug builds, if a pushed operation was not popped.
190    pub fn build(self) -> Scene {
191        debug_assert_eq!(self.open_scopes, 0, "SceneBuilder::build with an open push");
192        self.canvas.build()
193    }
194
195    fn open(&mut self, push: impl FnOnce(&mut Canvas)) {
196        push(&mut self.canvas);
197        self.open_scopes += 1;
198    }
199}
200
201/// The save that starts a clip scope. `Clip::AntiAliasWithSaveLayer` opens a layer over the
202/// clip's bounds, as the engine's clip layers do.
203fn open_clip(canvas: &mut Canvas, clip_behavior: Clip, bounds: valo::Rect) {
204    match clip_behavior {
205        Clip::None | Clip::HardEdge | Clip::AntiAlias => canvas.save(),
206        Clip::AntiAliasWithSaveLayer => canvas.save_layer(Some(bounds), &Paint::default()),
207    }
208}
209
210/// The blur a valo [`Backdrop`] can sample the scene with, out of an arbitrary filter tree: its
211/// sigma and the bounds it is confined to.
212fn backdrop_blur(filter: &ImageFilter) -> Option<(f64, Option<Rect>)> {
213    match filter {
214        ImageFilter::Blur {
215            sigma_x, bounds, ..
216        } => Some((*sigma_x, *bounds)),
217        ImageFilter::Color(_) => None,
218        ImageFilter::Compose { outer, inner } => {
219            backdrop_blur(inner).or_else(|| backdrop_blur(outer))
220        }
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn pushes_and_pops_balance() {
230        let mut builder = SceneBuilder::new();
231        builder.push_offset(1.0, 2.0);
232        builder.push_opacity(128, Offset::ZERO);
233        builder.pop();
234        builder.pop();
235        let _scene = builder.build();
236    }
237
238    #[test]
239    #[should_panic(expected = "open push")]
240    fn build_with_an_open_push_panics() {
241        let mut builder = SceneBuilder::new();
242        builder.push_offset(0.0, 0.0);
243        let _scene = builder.build();
244    }
245
246    #[test]
247    #[should_panic(expected = "without a push")]
248    fn pop_without_a_push_panics() {
249        SceneBuilder::new().pop();
250    }
251}