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