1use 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
35pub 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#[derive(Debug)]
50pub struct Slices {
51 parts: Vec<Part>,
52}
53
54pub enum Part {
56 Slice(Arc<Picture>),
60 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 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 pub fn paints(&self) -> bool {
98 self.parts.iter().any(|part| matches!(part, Part::Slice(_)))
99 }
100
101 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
119struct Slicer<'a> {
122 parts: Vec<Part>,
123 slice: SliceRecording,
124 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 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 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
194struct 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 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 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 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 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 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 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
315fn 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
339fn 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
354fn 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
369fn 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
383fn 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
403fn 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 #[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 struct RecordedBackdrop {
669 filter: Option<valo::ImageFilter>,
670 key: Option<u64>,
672 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 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 #[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 #[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, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, ]);
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 #[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 #[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 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 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}