Skip to main content

inset_embedder/scene/
slices.rs

1//! A frame's layer tree turned into what a host composites, in paint order: the drawing between
2//! composition layers, its slices, and each composition layer with what the containers above it
3//! do to it.
4//!
5//! Flutter's rasterizer walks its tree with a stack of the containers open and, at each platform
6//! view, has its embedder hand it a new canvas (`PlatformViewLayer::Paint`): the stack restores the
7//! old canvas to where it started, so what was painted is a slice of its own, and reapplies every
8//! open container on the new one (`LayerStateStack::set_delegate`). This walk does the same, and
9//! hands the host the slices and the placements as data instead of calling it at each composition
10//! layer, since a host here gets the tree itself. A host that shows every composition layer beneath
11//! one buffer draws [`Slices::picture_with_room`]; one that wants a buffer above a composition
12//! layer, as Flutter's iOS overlays are, draws each slice into its own, or walks the tree itself.
13//!
14//! A video's frames are drawn into the slice they lie in, under the containers open there, as
15//! flow's `TextureLayer` paints from the registry its context carries: the host hands the walk
16//! the image of each reader's frame as it draws, so the scene itself never holds a frame.
17//!
18//! The invariant a later editor must keep: a slice is a whole recording, every container open at
19//! its ends closed in it, so the slices drawn in order draw what the tree paints; and a slice is
20//! kept only when it paints, so a scene of composition layers alone has none.
21
22use std::fmt;
23use std::sync::Arc;
24
25use super::Scene;
26use super::layer::{ContainerLayer, DisplayListLayer, Effect, EmbeddedLayer, Layer};
27use super::placement::{Mutator, Placement};
28use crate::composition::CompositionLayer;
29use crate::geometry::{Clip, Color, Matrix4, Offset, Rect, rrect_radii_elliptical};
30use crate::media::VideoFrameReader;
31use crate::painting::{
32    Backdrop, BlendMode, Canvas, ClipOp, FillRule, Image, ImageFilter, Paint, Picture, valo,
33};
34
35/// The scene's slices and composition layers, in paint order: what Flutter's embedder hands its
36/// compositor as `FlutterLayer`s, before any slice is rendered. `frame_image` answers the image
37/// of the frame each video's reader shows in this draw, `None` for one with nothing to show; a
38/// host without video answers `None` for all.
39pub fn slices(
40    scene: &Scene,
41    frame_image: impl Fn(&Arc<dyn VideoFrameReader>) -> Option<Image>,
42) -> Slices {
43    let mut slicer = Slicer::new(&frame_image);
44    slicer.visit_container(&scene.root);
45    slicer.finish()
46}
47
48/// What [`slices`] makes of a scene.
49#[derive(Debug)]
50pub struct Slices {
51    parts: Vec<Part>,
52}
53
54/// One part of a scene, in paint order.
55pub enum Part {
56    /// What the scene paints between two composition layers, or before the first or after the
57    /// last, every container open at its ends closed in it: flow's `EmbedderViewSlice`. A stretch
58    /// that paints nothing has none, as Flutter's embedder renders no slice without contents.
59    Slice(Arc<Picture>),
60    /// A composition layer, placed under the transforms, clips and opacities above it.
61    CompositionLayer {
62        layer: Arc<dyn CompositionLayer>,
63        placement: Placement,
64    },
65}
66
67impl fmt::Debug for Part {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            Part::Slice(picture) => f.debug_tuple("Slice").field(picture).finish(),
71            Part::CompositionLayer { layer, placement } => f
72                .debug_struct("CompositionLayer")
73                .field("layer", &Arc::as_ptr(layer).cast::<()>())
74                .field("placement", placement)
75                .finish(),
76        }
77    }
78}
79
80impl Slices {
81    pub fn parts(&self) -> &[Part] {
82        &self.parts
83    }
84
85    /// The composition layers the scene places, each with its placement, the bottom-most first.
86    pub fn composition_layers(
87        &self,
88    ) -> impl Iterator<Item = (&Arc<dyn CompositionLayer>, &Placement)> {
89        self.parts.iter().filter_map(|part| match part {
90            Part::CompositionLayer { layer, placement } => Some((layer, placement)),
91            Part::Slice(_) => None,
92        })
93    }
94
95    /// Whether the scene paints anything besides its composition layers. One that does not changes
96    /// no pixel of a buffer the composition layers show beneath.
97    pub fn paints(&self) -> bool {
98        self.parts.iter().any(|part| matches!(part, Part::Slice(_)))
99    }
100
101    /// The slices as one picture with room made at each composition layer between them: what a host
102    /// draws that shows every composition layer beneath its one buffer. The room is the composition
103    /// layer's rectangle cleared under the transforms and clips above it, by the opacity above it,
104    /// so the buffer keeps that share of what was painted beneath and the composition layer shows
105    /// through the rest. It is cleared between slices, in no layer of theirs, so it reaches the
106    /// buffer.
107    pub fn picture_with_room(&self) -> Picture {
108        let mut canvas = Canvas::new();
109        for part in &self.parts {
110            match part {
111                Part::Slice(picture) => canvas.draw_display_list(picture),
112                Part::CompositionLayer { placement, .. } => make_room(&mut canvas, placement),
113            }
114        }
115        canvas.build()
116    }
117}
118
119/// The walk: the parts so far, the slice being recorded, the containers open, and where the
120/// frames of videos come from.
121struct Slicer<'a> {
122    parts: Vec<Part>,
123    slice: SliceRecording,
124    /// The effects of the containers the walk is in, the root's first.
125    open: Vec<&'a Effect>,
126    frame_image: &'a dyn Fn(&Arc<dyn VideoFrameReader>) -> Option<Image>,
127}
128
129impl<'a> Slicer<'a> {
130    fn new(frame_image: &'a dyn Fn(&Arc<dyn VideoFrameReader>) -> Option<Image>) -> Slicer<'a> {
131        Slicer {
132            parts: Vec::new(),
133            slice: SliceRecording::new(),
134            open: Vec::new(),
135            frame_image,
136        }
137    }
138
139    fn visit(&mut self, layer: &'a Layer) {
140        match layer {
141            Layer::Container(container) => self.visit_container(container),
142            Layer::DisplayList(picture) => self.slice.draw_picture(picture),
143            Layer::Embedded(embedded) => self.place_composition_layer(embedded),
144            Layer::Video(video) => {
145                if let Some(image) = (self.frame_image)(&video.reader) {
146                    self.slice.draw_image(&image, video.rect);
147                }
148            }
149        }
150    }
151
152    fn visit_container(&mut self, container: &'a ContainerLayer) {
153        self.slice.open(&container.effect);
154        self.open.push(&container.effect);
155        for child in &container.children {
156            self.visit(child);
157        }
158        self.open.pop();
159        self.slice.close();
160    }
161
162    /// What was painted so far becomes a slice of its own, and the composition layer is placed
163    /// above it with what the open containers do.
164    fn place_composition_layer(&mut self, embedded: &EmbeddedLayer) {
165        self.end_slice();
166        self.parts.push(Part::CompositionLayer {
167            layer: Arc::clone(&embedded.layer),
168            placement: Placement {
169                rect: embedded.rect,
170                mutators: mutators_of(&self.open),
171            },
172        });
173    }
174
175    /// Closes the open containers on the slice being recorded, keeps it if it paints, and goes on
176    /// in a new one with the same containers open.
177    fn end_slice(&mut self) {
178        let next = SliceRecording::reopening(&self.open);
179        let ended = std::mem::replace(&mut self.slice, next);
180        self.keep(ended.finish(self.open.len()));
181    }
182
183    fn keep(&mut self, slice: Option<Arc<Picture>>) {
184        self.parts.extend(slice.map(Part::Slice));
185    }
186
187    fn finish(mut self) -> Slices {
188        let last = std::mem::replace(&mut self.slice, SliceRecording::new());
189        self.keep(last.finish(0));
190        Slices { parts: self.parts }
191    }
192}
193
194/// One slice as it is recorded: the canvas, with a scope open for each container the walk is
195/// in, and whether anything drawn in it changes a pixel.
196struct SliceRecording {
197    canvas: Canvas,
198    paints: bool,
199}
200
201impl SliceRecording {
202    fn new() -> SliceRecording {
203        SliceRecording {
204            canvas: Canvas::new(),
205            paints: false,
206        }
207    }
208
209    /// A slice that goes on under the containers `open`, the root's first.
210    fn reopening(open: &[&Effect]) -> SliceRecording {
211        let mut slice = SliceRecording::new();
212        for effect in open {
213            slice.reopen(effect);
214        }
215        slice
216    }
217
218    /// Opens a scope doing what `effect` does, as the walk enters its container.
219    fn open(&mut self, effect: &Effect) {
220        if let Effect::BackdropFilter {
221            filter,
222            blend_mode,
223            backdrop_id,
224        } = effect
225        {
226            let backdrop = valo_backdrop(filter, *backdrop_id);
227            self.canvas
228                .save_layer_backdrop(None, &blending(*blend_mode), backdrop);
229            self.paints = true;
230        } else {
231            self.reopen(effect);
232        }
233    }
234
235    /// Opens a scope doing what `effect` does in a slice after the one its container began in. A
236    /// backdrop filter has filtered what lies beneath already, so it opens a plain layer of its
237    /// blend mode, as flow's `BackdropFilterEntry::reapply` does.
238    fn reopen(&mut self, effect: &Effect) {
239        let canvas = &mut self.canvas;
240        match effect {
241            Effect::None => canvas.save(),
242            Effect::Transform(matrix) => {
243                canvas.save();
244                canvas.concat(matrix);
245            }
246            Effect::ClipRect {
247                rect,
248                clip_behavior,
249            } => {
250                open_clip(canvas, *clip_behavior, (*rect).into());
251                canvas.clip_rect(*rect, ClipOp::Intersect);
252            }
253            Effect::ClipRRect {
254                rrect,
255                clip_behavior,
256            } => {
257                open_clip(canvas, *clip_behavior, rrect.outer_rect().into());
258                canvas.clip_rrect_radii_elliptical(
259                    rrect.outer_rect(),
260                    rrect_radii_elliptical(*rrect),
261                    ClipOp::Intersect,
262                );
263            }
264            Effect::ClipPath {
265                path,
266                clip_behavior,
267            } => {
268                open_clip(canvas, *clip_behavior, path.bounds());
269                canvas.clip_path(path, FillRule::NonZero, ClipOp::Intersect);
270            }
271            Effect::Opacity { alpha, offset } => {
272                let paint = Paint {
273                    color: Color::from_argb(i32::from(*alpha), 0, 0, 0).into(),
274                    ..Paint::default()
275                };
276                canvas.save_layer(None, &paint);
277                canvas.translate(offset.dx() as f32, offset.dy() as f32);
278            }
279            Effect::BackdropFilter { blend_mode, .. } => {
280                canvas.save_layer(None, &blending(*blend_mode));
281            }
282        }
283    }
284
285    /// Closes the innermost scope.
286    fn close(&mut self) {
287        self.canvas.restore();
288    }
289
290    fn draw_picture(&mut self, layer: &DisplayListLayer) {
291        let canvas = &mut self.canvas;
292        canvas.save();
293        canvas.translate(layer.offset.dx() as f32, layer.offset.dy() as f32);
294        canvas.draw_display_list(&layer.picture);
295        canvas.restore();
296        self.paints |= layer.picture.draw_count() > 0;
297    }
298
299    /// The whole of `image` drawn into `rect`, sampled linearly.
300    fn draw_image(&mut self, image: &Image, rect: Rect) {
301        self.canvas
302            .draw_image(image, rect.into(), &Paint::default());
303        self.paints = true;
304    }
305
306    /// The slice, its `open` scopes closed; `None` when it paints nothing.
307    fn finish(mut self, open: usize) -> Option<Arc<Picture>> {
308        for _ in 0..open {
309            self.close();
310        }
311        self.paints.then(|| Arc::new(self.canvas.build()))
312    }
313}
314
315/// What the containers whose effects are `open` do to a composition layer placed in them, the
316/// outermost first. An offset opacity moves before it fades, as flow's `OpacityLayer` translates
317/// before it applies its opacity. A backdrop filter does nothing to a composition layer beneath the
318/// buffer it reads.
319fn mutators_of(open: &[&Effect]) -> Vec<Mutator> {
320    let mut mutators = Vec::new();
321    for effect in open {
322        match effect {
323            Effect::None | Effect::BackdropFilter { .. } => {}
324            Effect::Transform(matrix) => mutators.push(Mutator::Transform(*matrix)),
325            Effect::ClipRect { rect, .. } => mutators.push(Mutator::ClipRect(*rect)),
326            Effect::ClipRRect { rrect, .. } => mutators.push(Mutator::ClipRRect(*rrect)),
327            Effect::ClipPath { path, .. } => mutators.push(Mutator::ClipPath(Arc::clone(path))),
328            Effect::Opacity { alpha, offset } => {
329                if *offset != Offset::ZERO {
330                    mutators.push(Mutator::Transform(translation(*offset)));
331                }
332                mutators.push(Mutator::Opacity(*alpha));
333            }
334        }
335    }
336    mutators
337}
338
339/// Clears `placement`'s rectangle under the transforms and clips above it, by the opacity above
340/// it. A composition layer nothing lets show gets no room.
341fn make_room(canvas: &mut Canvas, placement: &Placement) {
342    let opacity = placement.opacity();
343    if opacity <= 0.0 {
344        return;
345    }
346    canvas.save();
347    for mutator in &placement.mutators {
348        apply(canvas, mutator);
349    }
350    canvas.draw_rect(placement.rect, &clearing(opacity));
351    canvas.restore();
352}
353
354/// Records what `mutator` does to what is drawn after it; an opacity is the caller's to apply.
355fn apply(canvas: &mut Canvas, mutator: &Mutator) {
356    match mutator {
357        Mutator::Transform(matrix) => canvas.concat(matrix),
358        Mutator::ClipRect(rect) => canvas.clip_rect(*rect, ClipOp::Intersect),
359        Mutator::ClipRRect(rrect) => canvas.clip_rrect_radii_elliptical(
360            rrect.outer_rect(),
361            rrect_radii_elliptical(*rrect),
362            ClipOp::Intersect,
363        ),
364        Mutator::ClipPath(path) => canvas.clip_path(path, FillRule::NonZero, ClipOp::Intersect),
365        Mutator::Opacity(_) => {}
366    }
367}
368
369/// The paint that takes `opacity` of what lies beneath away: all of it at 1, a clear hole.
370fn clearing(opacity: f64) -> Paint {
371    Paint {
372        color: valo::Color {
373            r: 0.0,
374            g: 0.0,
375            b: 0.0,
376            a: opacity as f32,
377        },
378        blend_mode: BlendMode::DstOut,
379        ..Paint::default()
380    }
381}
382
383/// The save that starts a clip scope. `Clip::AntiAliasWithSaveLayer` opens a layer over the
384/// clip's bounds, as the engine's clip layers do.
385fn open_clip(canvas: &mut Canvas, clip_behavior: Clip, bounds: valo::Rect) {
386    match clip_behavior {
387        Clip::None | Clip::HardEdge | Clip::AntiAlias => canvas.save(),
388        Clip::AntiAliasWithSaveLayer => canvas.save_layer(Some(bounds), &Paint::default()),
389    }
390}
391
392fn blending(blend_mode: BlendMode) -> Paint {
393    Paint {
394        blend_mode,
395        ..Paint::default()
396    }
397}
398
399fn translation(offset: Offset) -> Matrix4 {
400    Matrix4::translation(offset.dx() as f32, offset.dy() as f32)
401}
402
403/// The valo [`Backdrop`] that samples the scene through `filter`, shared by every backdrop with
404/// the same `backdrop_id`.
405fn valo_backdrop(filter: &ImageFilter, backdrop_id: Option<u64>) -> Backdrop {
406    let backdrop = Backdrop::new(filter.to_valo());
407    match backdrop_id {
408        Some(id) => backdrop.shared(id),
409        None => backdrop,
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416    use crate::SceneBuilder;
417    use crate::geometry::{RRect, Radius, Rect};
418    use crate::painting::{ColorFilter, flattened_ops};
419
420    fn square() -> Arc<Picture> {
421        let mut canvas = Canvas::new();
422        canvas.draw_rect(Rect::from_ltwh(0.0, 0.0, 10.0, 10.0), &Paint::default());
423        Arc::new(canvas.build())
424    }
425
426    fn idle_layer() -> Arc<dyn CompositionLayer> {
427        super::super::idle_layer()
428    }
429
430    #[test]
431    fn a_composition_layer_carries_the_transforms_clips_and_opacity_above_it() {
432        let layer = idle_layer();
433        let rrect = RRect::from_rect_and_radius(
434            Rect::from_ltwh(0.0, 0.0, 50.0, 50.0),
435            Radius::circular(8.0),
436        );
437        let mut builder = SceneBuilder::new();
438        builder.push_transform(&Matrix4::scale(2.0, 2.0));
439        builder.push_clip_rect(Rect::from_ltwh(0.0, 0.0, 100.0, 100.0), Clip::HardEdge);
440        builder.push_opacity(128, Offset::new(5.0, 6.0));
441        builder.push_backdrop_filter(&ImageFilter::blur(2.0, 2.0), BlendMode::SrcOver, None);
442        builder.push_clip_rrect(rrect, Clip::AntiAlias);
443        builder.add_embedded_layer(Arc::clone(&layer), Rect::from_ltwh(1.0, 2.0, 30.0, 20.0));
444        for _ in 0..5 {
445            builder.pop();
446        }
447        let slices = slices(&builder.build(), |_| None);
448        let [(placed, placement)] = slices.composition_layers().collect::<Vec<_>>()[..] else {
449            panic!("one layer: {:?}", slices.parts());
450        };
451        assert!(Arc::ptr_eq(placed, &layer));
452        assert_eq!(
453            placement,
454            &Placement {
455                rect: Rect::from_ltwh(1.0, 2.0, 30.0, 20.0),
456                mutators: vec![
457                    Mutator::Transform(Matrix4::scale(2.0, 2.0)),
458                    Mutator::ClipRect(Rect::from_ltwh(0.0, 0.0, 100.0, 100.0)),
459                    Mutator::Transform(Matrix4::translation(5.0, 6.0)),
460                    Mutator::Opacity(128),
461                    Mutator::ClipRRect(rrect),
462                ],
463            },
464            "the backdrop filter does nothing to the layer"
465        );
466    }
467
468    #[test]
469    fn a_composition_layer_in_a_retained_container_is_placed_again() {
470        let mut builder = SceneBuilder::new();
471        builder.push_offset(10.0, 0.0);
472        builder.add_embedded_layer(idle_layer(), Rect::from_ltwh(0.0, 0.0, 4.0, 4.0));
473        let retained = builder.pop();
474        let _first = builder.build();
475
476        let mut builder = SceneBuilder::new();
477        builder.push_offset(0.0, 20.0);
478        builder.add_retained(&retained);
479        builder.pop();
480        let slices = slices(&builder.build(), |_| None);
481        let (_, placement) = slices.composition_layers().next().unwrap();
482        assert_eq!(placement.bounds(), Rect::from_ltwh(10.0, 20.0, 4.0, 4.0));
483    }
484
485    #[test]
486    fn a_composition_layer_ends_the_slice_and_the_next_opens_under_the_same_containers() {
487        let mut builder = SceneBuilder::new();
488        builder.push_opacity(128, Offset::ZERO);
489        builder.add_picture(Offset::ZERO, &square(), false, false);
490        builder.add_embedded_layer(idle_layer(), Rect::from_ltwh(0.0, 0.0, 4.0, 4.0));
491        builder.add_picture(Offset::new(20.0, 0.0), &square(), false, false);
492        builder.pop();
493        let slices = slices(&builder.build(), |_| None);
494        let [
495            Part::Slice(beneath),
496            Part::CompositionLayer { .. },
497            Part::Slice(above),
498        ] = slices.parts()
499        else {
500            panic!(
501                "a slice, the composition layer, a slice: {:?}",
502                slices.parts()
503            );
504        };
505        for slice in [beneath, above] {
506            let layers = slice
507                .ops()
508                .iter()
509                .filter(|op| matches!(op, valo::Op::SaveLayer { .. }))
510                .count();
511            let restores = slice
512                .ops()
513                .iter()
514                .filter(|op| matches!(op, valo::Op::Restore))
515                .count();
516            assert_eq!(layers, 1, "the opacity in each slice: {:?}", slice.ops());
517            assert_eq!(restores, 3, "the root, the opacity and the picture's own");
518        }
519        assert!(slices.paints());
520    }
521
522    #[test]
523    fn a_scene_of_composition_layers_alone_paints_nothing() {
524        let mut builder = SceneBuilder::new();
525        builder.push_opacity(128, Offset::ZERO);
526        builder.push_clip_rect(
527            Rect::from_ltwh(0.0, 0.0, 9.0, 9.0),
528            Clip::AntiAliasWithSaveLayer,
529        );
530        builder.add_embedded_layer(idle_layer(), Rect::from_ltwh(0.0, 0.0, 4.0, 4.0));
531        builder.add_picture(Offset::ZERO, &Arc::new(Canvas::new().build()), false, false);
532        builder.add_embedded_layer(idle_layer(), Rect::from_ltwh(4.0, 0.0, 4.0, 4.0));
533        builder.pop();
534        builder.pop();
535        let slices = slices(&builder.build(), |_| None);
536        assert!(
537            slices
538                .parts()
539                .iter()
540                .all(|part| matches!(part, Part::CompositionLayer { .. })),
541            "{:?}",
542            slices.parts()
543        );
544        assert!(!slices.paints());
545        assert_eq!(slices.composition_layers().count(), 2);
546    }
547
548    #[test]
549    fn a_backdrop_filter_paints_though_nothing_is_drawn_in_it() {
550        let mut builder = SceneBuilder::new();
551        builder.push_backdrop_filter(&ImageFilter::blur(2.0, 2.0), BlendMode::SrcOver, None);
552        builder.pop();
553        assert!(slices(&builder.build(), |_| None).paints());
554    }
555
556    #[test]
557    fn room_is_cleared_under_the_composition_layers_transforms_and_clips_between_the_slices() {
558        let mut builder = SceneBuilder::new();
559        builder.add_picture(Offset::ZERO, &square(), false, false);
560        builder.push_transform(&Matrix4::scale(2.0, 2.0));
561        builder.push_clip_rect(Rect::from_ltwh(0.0, 0.0, 3.0, 3.0), Clip::HardEdge);
562        builder.add_embedded_layer(idle_layer(), Rect::from_ltwh(1.0, 1.0, 4.0, 4.0));
563        builder.pop();
564        builder.pop();
565        let picture = slices(&builder.build(), |_| None).picture_with_room();
566        let ops = picture.ops();
567        assert!(
568            matches!(ops[0], valo::Op::DrawDisplayList { .. }),
569            "the slice first: {ops:?}"
570        );
571        let hole = ops
572            .iter()
573            .position(|op| matches!(op, valo::Op::DrawRect { paint, .. } if paint.blend_mode == BlendMode::DstOut))
574            .expect("a hole");
575        let before_hole = &ops[1..hole];
576        assert!(
577            before_hole
578                .iter()
579                .any(|op| matches!(op, valo::Op::Transform(_)))
580                && before_hole
581                    .iter()
582                    .any(|op| matches!(op, valo::Op::ClipPath { .. })),
583            "{ops:?}"
584        );
585        assert!(
586            !ops.iter()
587                .any(|op| matches!(op, valo::Op::SaveLayer { .. })),
588            "the hole is in no layer: {ops:?}"
589        );
590    }
591
592    #[test]
593    fn a_composition_layer_nothing_lets_show_gets_no_room() {
594        let mut builder = SceneBuilder::new();
595        builder.add_picture(Offset::ZERO, &square(), false, false);
596        builder.push_opacity(0, Offset::ZERO);
597        builder.add_embedded_layer(idle_layer(), Rect::from_ltwh(1.0, 1.0, 4.0, 4.0));
598        builder.pop();
599        let picture = slices(&builder.build(), |_| None).picture_with_room();
600        assert!(
601            !flattened_ops(&picture)
602                .iter()
603                .any(|op| matches!(op, valo::Op::DrawRect { paint, .. } if paint.blend_mode == BlendMode::DstOut)),
604        );
605    }
606
607    #[test]
608    fn a_colour_filter_composed_over_a_blur_reaches_the_backdrop_in_its_order() {
609        let saturation = ColorFilter::saturation(1.8);
610        let filter =
611            ImageFilter::compose(ImageFilter::Color(saturation), ImageFilter::blur(4.0, 4.0));
612        let backdrop = recorded_backdrop(&filter, Some(7));
613        assert_eq!(
614            backdrop.filter,
615            Some(valo::ImageFilter::compose(
616                valo::ImageFilter::Color(saturation.to_valo()),
617                valo::ImageFilter::blur(4.0, 4.0),
618            ))
619        );
620        assert_eq!(backdrop.key, Some(7), "the backdrop is still shared");
621    }
622
623    #[test]
624    fn an_uneven_blur_reaches_the_backdrop_with_both_sigmas() {
625        let backdrop = recorded_backdrop(&ImageFilter::blur(2.0, 6.0), None);
626        assert_eq!(backdrop.filter, Some(valo::ImageFilter::blur(2.0, 6.0)));
627        assert_eq!(backdrop.key, None);
628    }
629
630    #[test]
631    fn a_bounded_blur_reaches_the_backdrop_with_its_bounds_and_leaves_the_layer_unconfined() {
632        let glass = Rect::from_ltwh(8.0, 8.0, 16.0, 16.0);
633        let bounded = recorded_backdrop(&ImageFilter::blur(2.0, 3.0).bounds(glass), None);
634        assert_eq!(
635            bounded.filter,
636            Some(valo::ImageFilter::bounded_blur(2.0, 3.0, glass.into()))
637        );
638        let unbounded = recorded_backdrop(&ImageFilter::blur(2.0, 3.0), None);
639        assert_eq!(
640            bounded.bounds, unbounded.bounds,
641            "the bounds limit what the blur reads, not the layer"
642        );
643    }
644
645    /// A backdrop filter over a composition layer opens its backdrop in the slice beneath the
646    /// composition layer only: above it, it is a plain layer, as flow's reapplied backdrop entry
647    /// is.
648    #[test]
649    fn a_backdrop_filter_reads_the_backdrop_once_across_a_composition_layer() {
650        let mut builder = SceneBuilder::new();
651        builder.push_backdrop_filter(&ImageFilter::blur(2.0, 2.0), BlendMode::SrcOver, None);
652        builder.add_embedded_layer(idle_layer(), Rect::from_ltwh(0.0, 0.0, 4.0, 4.0));
653        builder.add_picture(Offset::ZERO, &square(), false, false);
654        builder.pop();
655        let picture = slices(&builder.build(), |_| None).picture_with_room();
656        assert_eq!(picture.backdrop_reads(), 1);
657        let layers = flattened_ops(&picture)
658            .into_iter()
659            .filter(|op| matches!(op, valo::Op::SaveLayer { .. }))
660            .count();
661        assert_eq!(
662            layers, 2,
663            "the backdrop's, and the plain one above the composition layer"
664        );
665    }
666
667    /// What a scene records of the one backdrop a push opened.
668    struct RecordedBackdrop {
669        filter: Option<valo::ImageFilter>,
670        /// What the backdrop's snapshot is shared under.
671        key: Option<u64>,
672        /// The bounds of the backdrop's layer.
673        bounds: valo::Bounds,
674    }
675
676    fn recorded_backdrop(filter: &ImageFilter, backdrop_id: Option<u64>) -> RecordedBackdrop {
677        let mut builder = SceneBuilder::new();
678        builder.push_backdrop_filter(filter, BlendMode::SrcOver, backdrop_id);
679        builder.pop();
680        flattened_ops(&slices(&builder.build(), |_| None).picture_with_room())
681            .into_iter()
682            .find_map(|op| match op {
683                valo::Op::SaveLayer {
684                    backdrop,
685                    scope_bounds,
686                    ..
687                } => Some(RecordedBackdrop {
688                    key: backdrop.as_ref().and_then(|backdrop| backdrop.shared_key),
689                    filter: backdrop.map(|backdrop| backdrop.filter),
690                    bounds: scope_bounds,
691                }),
692                _ => None,
693            })
694            .expect("a backdrop filter opens a layer")
695    }
696
697    /// Checks of what a scene shows once drawn. They need `test_support`'s GPU device, so they
698    /// run where the `test-support` feature is on, as it is in a test run of the workspace.
699    /// A reader with no frame of its own: what a video shows before its first frame.
700    struct NoFrames;
701
702    impl VideoFrameReader for NoFrames {
703        fn frame_for(
704            &self,
705            _display_time: std::time::Instant,
706        ) -> Option<Arc<dyn crate::VideoFrame>> {
707            None
708        }
709    }
710
711    /// The walk asks the host for each video's frame as it draws it, which is how a host knows
712    /// the readers it shows; one with nothing to show draws nothing.
713    #[test]
714    fn a_video_asks_for_its_frame_and_draws_nothing_without_one() {
715        let reader: Arc<dyn VideoFrameReader> = Arc::new(NoFrames);
716        let mut builder = SceneBuilder::new();
717        builder.add_video(Arc::clone(&reader), Rect::from_ltwh(0.0, 0.0, 4.0, 4.0));
718        let asked = std::cell::Cell::new(0);
719        let slices = slices(&builder.build(), |asked_for| {
720            assert!(Arc::ptr_eq(asked_for, &reader));
721            asked.set(asked.get() + 1);
722            None
723        });
724        assert_eq!(asked.get(), 1);
725        assert!(!slices.paints(), "{:?}", slices.parts());
726    }
727
728    #[cfg(feature = "test-support")]
729    mod drawn {
730        use super::*;
731        use crate::test_support::drawn_pixels;
732
733        const SIZE: u32 = 32;
734
735        /// A video's frame is part of the picture: drawn into its rectangle, cut by the clip
736        /// above it like anything painted there.
737        #[test]
738        fn a_video_frame_is_drawn_in_its_slice_under_the_containers_above_it() {
739            let reader: Arc<dyn VideoFrameReader> = Arc::new(NoFrames);
740            let whole = Rect::from_ltwh(0.0, 0.0, SIZE as f64, SIZE as f64);
741            let mut builder = SceneBuilder::new();
742            builder.push_clip_rect(
743                Rect::from_ltwh(0.0, 0.0, SIZE as f64 / 2.0, SIZE as f64),
744                Clip::HardEdge,
745            );
746            builder.add_video(reader, whole);
747            builder.pop();
748            let frame = crate::test_support::solid_image([4, 4], [255, 0, 0, 255]);
749            let slices = slices(&builder.build(), |_| Some(frame.clone()));
750            assert!(slices.paints());
751            let pixels = drawn_pixels(slices.picture_with_room(), [SIZE, SIZE]);
752            assert_near(
753                pixel_at(&pixels, 8, 16),
754                [255, 0, 0, 255],
755                "inside the clip",
756            );
757            assert_eq!(pixel_at(&pixels, 24, 16)[3], 0, "outside the clip");
758        }
759
760        #[test]
761        fn a_colour_filter_composed_over_a_blur_recolours_the_backdrop() {
762            let swap_red_and_green = ColorFilter::matrix([
763                0.0, 1.0, 0.0, 0.0, 0.0, //
764                1.0, 0.0, 0.0, 0.0, 0.0, //
765                0.0, 0.0, 1.0, 0.0, 0.0, //
766                0.0, 0.0, 0.0, 1.0, 0.0, //
767            ]);
768            let glass = Rect::from_ltwh(8.0, 8.0, 16.0, 16.0);
769            let filter = ImageFilter::compose(
770                ImageFilter::Color(swap_red_and_green),
771                ImageFilter::blur(2.0, 2.0).bounds(glass),
772            );
773            let mut builder = SceneBuilder::new();
774            builder.add_picture(Offset::ZERO, &filled(Color::new(0xFFFF0000)), false, false);
775            builder.push_clip_rect(glass, Clip::HardEdge);
776            builder.push_backdrop_filter(&filter, BlendMode::SrcOver, None);
777            builder.pop();
778            builder.pop();
779            let pixels = drawn(&builder.build());
780            assert_near(
781                pixel_at(&pixels, 16, 16),
782                [0, 255, 0, 255],
783                "inside the glass",
784            );
785            assert_near(
786                pixel_at(&pixels, 2, 2),
787                [255, 0, 0, 255],
788                "outside the glass",
789            );
790        }
791
792        /// A composition layer under glass shows through it, unfiltered: the room is made after the
793        /// backdrop's layer closes, so it reaches the buffer.
794        #[test]
795        fn a_composition_layer_under_a_backdrop_filter_shows_through_it() {
796            let mut builder = SceneBuilder::new();
797            builder.add_picture(Offset::ZERO, &filled(Color::new(0xFFFFFFFF)), false, false);
798            builder.push_backdrop_filter(&ImageFilter::blur(2.0, 2.0), BlendMode::SrcOver, None);
799            builder.add_embedded_layer(idle_layer(), Rect::from_ltwh(0.0, 0.0, 16.0, 32.0));
800            builder.pop();
801            let pixels = drawn(&builder.build());
802            assert_eq!(pixel_at(&pixels, 8, 16)[3], 0, "over the composition layer");
803            assert_near(pixel_at(&pixels, 28, 16), [255, 255, 255, 255], "beside it");
804        }
805
806        /// The hole keeps half of what was painted beneath a composition layer at half opacity, and
807        /// what the same opacity paints above the composition layer stays.
808        #[test]
809        fn a_composition_layer_under_an_opacity_shows_through_by_that_opacity() {
810            let mut builder = SceneBuilder::new();
811            builder.add_picture(Offset::ZERO, &filled(Color::new(0xFFFFFFFF)), false, false);
812            builder.push_opacity(128, Offset::ZERO);
813            builder.add_embedded_layer(idle_layer(), Rect::from_ltwh(0.0, 0.0, 16.0, 32.0));
814            builder.add_picture(
815                Offset::ZERO,
816                &square_of(Color::new(0xFF0000FF), 4.0),
817                false,
818                false,
819            );
820            builder.pop();
821            let pixels = drawn(&builder.build());
822            assert_near(
823                pixel_at(&pixels, 24, 16),
824                [255, 255, 255, 255],
825                "beside the composition layer",
826            );
827            assert_near(
828                pixel_at(&pixels, 8, 16),
829                [255, 255, 255, 127],
830                "over the composition layer",
831            );
832            let above = pixel_at(&pixels, 1, 1);
833            assert!(
834                above[2] > above[0] && above[3] > 127,
835                "the blue square painted above the composition layer: {above:?}"
836            );
837        }
838
839        #[test]
840        fn a_hole_under_a_rounded_clip_keeps_the_corners() {
841            let mut builder = SceneBuilder::new();
842            builder.add_picture(Offset::ZERO, &filled(Color::new(0xFFFFFFFF)), false, false);
843            builder.push_clip_rrect(
844                RRect::from_rect_and_radius(
845                    Rect::from_ltwh(0.0, 0.0, SIZE.into(), SIZE.into()),
846                    Radius::circular(12.0),
847                ),
848                Clip::AntiAlias,
849            );
850            builder.add_embedded_layer(
851                idle_layer(),
852                Rect::from_ltwh(0.0, 0.0, SIZE.into(), SIZE.into()),
853            );
854            builder.pop();
855            let pixels = drawn(&builder.build());
856            assert_near(
857                pixel_at(&pixels, 0, 0),
858                [255, 255, 255, 255],
859                "in the corner",
860            );
861            assert_eq!(pixel_at(&pixels, 16, 16)[3], 0, "in the middle");
862        }
863
864        fn drawn(scene: &Scene) -> Vec<u8> {
865            drawn_pixels(slices(scene, |_| None).picture_with_room(), [SIZE, SIZE])
866        }
867
868        /// A picture that covers the whole scene in `color`.
869        fn filled(color: Color) -> Arc<Picture> {
870            square_of(color, SIZE.into())
871        }
872
873        fn square_of(color: Color, side: f64) -> Arc<Picture> {
874            let mut canvas = Canvas::new();
875            let paint = Paint {
876                color: color.into(),
877                ..Paint::default()
878            };
879            canvas.draw_rect(Rect::from_ltwh(0.0, 0.0, side, side), &paint);
880            Arc::new(canvas.build())
881        }
882
883        fn pixel_at(pixels: &[u8], x: usize, y: usize) -> [u8; 4] {
884            let start = (y * SIZE as usize + x) * 4;
885            pixels[start..start + 4]
886                .try_into()
887                .expect("four bytes a pixel")
888        }
889
890        /// Within a step or two of rounding in each channel.
891        fn assert_near(actual: [u8; 4], expected: [u8; 4], place: &str) {
892            let near = actual
893                .iter()
894                .zip(expected)
895                .all(|(actual, expected)| actual.abs_diff(expected) <= 2);
896            assert!(near, "{place}: {actual:?}, expected {expected:?}");
897        }
898    }
899}