1use std::sync::Arc;
2
3use valo_geometry::{FillRule, Matrix, Path, PathBuilder, Rect};
4
5use crate::{ClipOp, DisplayList, Image, ImageFilter, MaskKind, Op, Paint, Sampling};
6
7pub struct DisplayListBuilder {
13 ops: Vec<Op>,
14 scopes: Vec<Scope>,
15 layers: Vec<LayerScope>,
18 backdrop_groups: Vec<crate::BackdropGroup>,
21 pending_clips: Vec<Vec<usize>>,
24 slots: u32,
27 bounds: Option<Rect>,
28 draw_count: u32,
29 backdrop_reads: u32,
33 root: GroupOpacity,
36}
37
38#[derive(Clone, Debug)]
43pub struct Backdrop {
44 pub filter: ImageFilter,
46 pub shared_key: Option<u64>,
49}
50
51impl Backdrop {
52 pub fn new(filter: ImageFilter) -> Self {
54 Self {
55 filter,
56 shared_key: None,
57 }
58 }
59
60 pub fn blur(sigma: f32) -> Self {
62 Self::new(ImageFilter::blur(sigma, sigma))
63 }
64
65 pub fn shared(mut self, key: u64) -> Self {
67 self.shared_key = Some(key);
68 self
69 }
70}
71
72#[derive(Clone, Copy)]
75struct Scope {
76 transform: Matrix,
77 clip: Option<Rect>,
78 is_layer: bool,
79}
80
81struct GroupOpacity {
87 compatible: bool,
89 union: Option<Rect>,
94}
95
96impl GroupOpacity {
97 fn new() -> Self {
98 Self {
99 compatible: true,
100 union: None,
101 }
102 }
103
104 fn note(&mut self, bounds: Rect, supports_opacity: bool) {
107 if !self.compatible {
108 return;
109 }
110 let overlaps = self.union.is_some_and(|union| union.intersects(&bounds));
111 if !supports_opacity || overlaps {
112 self.compatible = false;
113 return;
114 }
115 self.union = Some(match self.union {
116 Some(union) => union.union(&bounds),
117 None => bounds,
118 });
119 }
120}
121
122struct LayerScope {
124 op_index: usize,
126 bounds: Option<Rect>,
128 group: GroupOpacity,
130 blur_pad: f32,
132 backdrop: Option<Backdrop>,
134 hinted: bool,
139}
140
141impl Default for DisplayListBuilder {
142 fn default() -> Self {
143 Self::new()
144 }
145}
146
147impl DisplayListBuilder {
148 pub fn new() -> Self {
150 Self {
151 ops: Vec::new(),
152 scopes: vec![Scope {
153 transform: Matrix::IDENTITY,
154 clip: None,
155 is_layer: false,
156 }],
157 layers: Vec::new(),
158 backdrop_groups: Vec::new(),
159 pending_clips: vec![Vec::new()],
160 slots: 0,
161 bounds: None,
162 draw_count: 0,
163 backdrop_reads: 0,
164 root: GroupOpacity::new(),
165 }
166 }
167
168 pub fn save(&mut self) {
172 self.scopes.push(Scope {
173 is_layer: false,
174 ..*self.top()
175 });
176 self.pending_clips.push(Vec::new());
177 self.ops.push(Op::Save);
178 }
179
180 pub fn save_count(&self) -> usize {
186 self.scopes.len()
187 }
188
189 pub fn save_layer(&mut self, bounds_hint: Option<Rect>, paint: &Paint) {
195 self.save_layer_inner(bounds_hint, paint, None, None);
196 }
197
198 pub fn save_layer_mask(&mut self, bounds_hint: Option<Rect>, kind: MaskKind) {
204 let paint = Paint {
205 blend_mode: crate::BlendMode::DstIn,
206 ..Paint::default()
207 };
208 self.save_layer_inner(bounds_hint, &paint, Some(kind), None);
209 }
210
211 pub fn save_layer_backdrop(
221 &mut self,
222 bounds_hint: Option<Rect>,
223 paint: &Paint,
224 backdrop: Backdrop,
225 ) {
226 let backdrop = (!backdrop.filter.is_nop()).then_some(backdrop);
227 self.save_layer_inner(bounds_hint, paint, None, backdrop);
228 }
229
230 fn save_layer_inner(
231 &mut self,
232 bounds_hint: Option<Rect>,
233 paint: &Paint,
234 mask_composite: Option<MaskKind>,
235 backdrop: Option<Backdrop>,
236 ) {
237 let device_hint = bounds_hint.map(|h| self.top().transform.map_rect(&h));
238 let mut scope = Scope {
239 is_layer: true,
240 ..*self.top()
241 };
242 if let Some(h) = device_hint {
245 scope.clip = Some(match scope.clip {
246 None => h,
247 Some(c) => c.intersect(&h).unwrap_or_default(),
248 });
249 }
250 let floods_scope = paint.blend_mode.is_destructive()
254 || paint
255 .color_filter
256 .is_some_and(|filter| filter.modifies_transparent_black())
257 || paint
258 .image_filter
259 .as_ref()
260 .is_some_and(|filter| filter.modifies_transparent_black())
261 || backdrop.is_some();
266 let flooded_bounds = floods_scope.then(|| scope.clip.unwrap_or(Rect::EVERYTHING));
267 if backdrop.is_some() {
268 self.backdrop_reads += 1;
269 }
270 self.scopes.push(scope);
271 self.pending_clips.push(Vec::new());
272 self.layers.push(LayerScope {
273 op_index: self.ops.len(),
274 bounds: flooded_bounds,
275 group: GroupOpacity::new(),
276 blur_pad: paint.device_effect_padding(&self.top().transform),
279 backdrop: backdrop.clone(),
280 hinted: device_hint.is_some(),
281 });
282 self.ops.push(Op::SaveLayer {
285 paint: paint.clone(),
286 mask_composite,
287 scope_bounds: Rect::default(), base_slot: self.slots,
289 composite_slot: 0,
290 can_elide: false,
291 backdrop_key: backdrop.as_ref().and_then(|b| b.shared_key),
292 backdrop_filter: backdrop.map(|b| b.filter),
293 });
294 }
295
296 pub fn restore(&mut self) {
300 if self.scopes.len() == 1 {
301 debug_assert!(false, "restore() without matching save()");
302 return;
303 }
304 let scope = self.scopes.pop().expect("checked above");
305 self.expire_scope_clips(); if scope.is_layer {
307 self.close_layer();
308 }
309 self.ops.push(Op::Restore);
310 }
311
312 pub fn translate(&mut self, tx: f32, ty: f32) {
314 self.concat(&Matrix::translation(tx, ty));
315 }
316
317 pub fn scale(&mut self, sx: f32, sy: f32) {
319 self.concat(&Matrix::scale(sx, sy));
320 }
321
322 pub fn rotate(&mut self, radians: f32) {
326 self.concat(&Matrix::rotation(radians));
327 }
328
329 pub fn concat(&mut self, local: &Matrix) {
331 let top = self.top_mut();
332 top.transform = top.transform.then(local);
333 self.ops.push(Op::Transform(*local));
334 }
335
336 pub fn clip_rect(&mut self, rect: impl Into<Rect>, op: ClipOp) {
340 let rect = rect.into();
341 self.clip_path(&rect_path(rect), FillRule::NonZero, op);
342 }
343
344 pub fn clip_rrect(&mut self, rect: impl Into<Rect>, radius: f32, op: ClipOp) {
346 let rect = rect.into();
347 self.clip_rrect_radii(rect, [radius; 4], op);
348 }
349
350 pub fn clip_rrect_radii(&mut self, rect: impl Into<Rect>, radii: [f32; 4], op: ClipOp) {
354 let rect = positive_rect(rect.into());
355 let mut p = PathBuilder::new();
356 p.rrect_radii(rect, radii);
357 self.clip_path(&p.build(), FillRule::NonZero, op);
358 }
359
360 pub fn clip_rrect_radii_elliptical(
364 &mut self,
365 rect: impl Into<Rect>,
366 radii: [[f32; 2]; 4],
367 op: ClipOp,
368 ) {
369 let rect = positive_rect(rect.into());
370 if let Some(circular) = circular_radii(radii) {
371 return self.clip_rrect_radii(rect, circular, op);
372 }
373 let mut p = PathBuilder::new();
374 p.rrect_radii_elliptical(rect, radii);
375 self.clip_path(&p.build(), FillRule::NonZero, op);
376 }
377
378 pub fn clip_path(&mut self, path: &Arc<Path>, fill_rule: FillRule, op: ClipOp) {
388 let bounds = self.top().transform.map_rect(&path.bounds());
389 self.shrink_clip(op, bounds);
390 self.pending_clips
391 .last_mut()
392 .expect("root scope")
393 .push(self.ops.len());
394 self.ops.push(Op::ClipPath {
395 path: Arc::clone(path),
396 fill_rule,
397 op,
398 expiry_slot: 0, });
400 }
401
402 pub fn draw_rect(&mut self, rect: impl Into<Rect>, paint: &Paint) {
406 let rect = rect.into();
407 if paint.is_nop() {
408 return;
409 }
410 if matches!(paint.style, crate::PaintStyle::Stroke(_)) {
411 return self.draw_path(&rect_path(rect), FillRule::NonZero, paint);
414 }
415 if rect.is_empty() {
416 return;
417 }
418 if is_analytic_blur(paint) {
419 self.record_rrect_blur(rect, [0.0; 4], paint);
420 return;
421 }
422 let Some(bounds) = self.clipped_device_bounds(&paint.effect_bounds(rect)) else {
423 return; };
425 let slot = self.take_draw_slot(bounds, supports_opacity(paint));
426 self.ops.push(Op::DrawRect {
427 rect,
428 paint: paint.clone(),
429 bounds,
430 slot,
431 });
432 }
433
434 pub fn draw_path(&mut self, path: &Arc<Path>, fill_rule: FillRule, paint: &Paint) {
436 if path.is_empty() || paint.is_nop() {
437 return;
438 }
439 let scale = self.top().transform.max_scale();
440 let local = paint.effect_bounds(path.bounds().expand(paint.stroke_padding_at_scale(scale)));
441 let Some(bounds) = self.clipped_device_bounds(&local) else {
442 return;
443 };
444 let slot = self.take_draw_slot(bounds, supports_opacity(paint));
445 self.ops.push(Op::DrawPath {
446 path: Arc::clone(path),
447 fill_rule,
448 paint: paint.clone(),
449 bounds,
450 slot,
451 });
452 }
453
454 pub fn draw_circle(
456 &mut self,
457 center: impl Into<valo_geometry::Point>,
458 radius: f32,
459 paint: &Paint,
460 ) {
461 let mut p = PathBuilder::new();
462 p.circle(center, radius);
463 self.draw_path(&p.build(), FillRule::NonZero, paint);
464 }
465
466 pub fn draw_rrect(&mut self, rect: impl Into<Rect>, radius: f32, paint: &Paint) {
468 let rect = rect.into();
469 self.draw_rrect_radii(rect, [radius; 4], paint);
470 }
471
472 pub fn draw_rrect_radii(&mut self, rect: impl Into<Rect>, radii: [f32; 4], paint: &Paint) {
476 let rect = positive_rect(rect.into());
477 if rect.is_empty() || paint.is_nop() {
478 return;
479 }
480 if is_analytic_blur(paint) {
481 self.record_rrect_blur(rect, radii, paint);
482 return;
483 }
484 let mut p = PathBuilder::new();
485 p.rrect_radii(rect, radii);
486 self.draw_path(&p.build(), FillRule::NonZero, paint);
487 }
488
489 pub fn draw_rrect_radii_elliptical(
493 &mut self,
494 rect: impl Into<Rect>,
495 radii: [[f32; 2]; 4],
496 paint: &Paint,
497 ) {
498 let rect = positive_rect(rect.into());
499 if let Some(circular) = circular_radii(radii) {
500 return self.draw_rrect_radii(rect, circular, paint);
501 }
502 if rect.is_empty() || paint.is_nop() {
503 return;
504 }
505 let mut p = PathBuilder::new();
506 p.rrect_radii_elliptical(rect, radii);
507 self.draw_path(&p.build(), FillRule::NonZero, paint);
508 }
509
510 pub fn draw_image(&mut self, image: &Image, dst: Rect, paint: &Paint) {
514 let src = Rect::new(0.0, 0.0, image.width(), image.height());
515 self.draw_image_rect(image, src, dst, Sampling::default(), paint);
516 }
517
518 pub fn draw_image_rect(
523 &mut self,
524 image: &Image,
525 src: Rect,
526 dst: Rect,
527 sampling: Sampling,
528 paint: &Paint,
529 ) {
530 if dst.is_empty() || src.is_empty() || paint.is_nop() {
531 return;
532 }
533 let Some(bounds) = self.clipped_device_bounds(&paint.effect_bounds(dst)) else {
534 return;
535 };
536 let slot = self.take_draw_slot(bounds, supports_opacity(paint));
537 self.ops.push(Op::DrawImage {
538 image: image.clone(),
539 src,
540 dst,
541 sampling,
542 paint: paint.clone(),
543 bounds,
544 slot,
545 });
546 }
547
548 pub fn draw_glyph_run(
553 &mut self,
554 font: std::sync::Arc<valo_text::Font>,
555 size: f32,
556 paint: &Paint,
557 glyphs: Arc<Vec<crate::GlyphPos>>,
558 local_bounds: Rect,
559 ) {
560 if glyphs.is_empty() || paint.is_nop() {
561 return;
562 }
563 let scale = self.top().transform.max_scale();
564 let padded = paint.effect_bounds(local_bounds.expand(paint.stroke_padding_at_scale(scale)));
565 let Some(bounds) = self.clipped_device_bounds(&padded) else {
566 return;
567 };
568 let distributes = supports_opacity(paint) && paint.shader.is_none();
571 let slot = self.take_draw_slot(bounds, distributes);
572 self.ops.push(Op::GlyphRun {
573 font,
574 size,
575 paint: paint.clone(),
576 glyphs,
577 bounds,
578 slot,
579 });
580 }
581
582 pub fn draw_display_list(&mut self, list: &Arc<DisplayList>) {
584 self.embed_display_list(list, false);
585 }
586
587 pub fn draw_display_list_cached(&mut self, list: &Arc<DisplayList>) {
592 self.embed_display_list(list, true);
593 }
594
595 fn embed_display_list(&mut self, list: &Arc<DisplayList>, cache: bool) {
596 let Some(child_bounds) = list.bounds() else {
597 return; };
599 let Some(bounds) = self.clipped_device_bounds(&child_bounds) else {
600 return;
601 };
602 let base_slot = self.slots;
603 self.slots += list.depth_slots();
604 self.draw_count += list.draw_count();
605 self.backdrop_reads += list.backdrop_reads();
606 self.union_bounds(bounds);
607 self.note_layer_child(bounds, list.supports_opacity());
611 self.ops.push(Op::DrawDisplayList {
612 list: Arc::clone(list),
613 bounds,
614 base_slot,
615 cache,
616 });
617 }
618
619 pub fn build(mut self) -> DisplayList {
625 while self.scopes.len() > 1 {
628 self.restore();
629 }
630 self.expire_scope_clips(); DisplayList::new(
632 self.ops,
633 self.bounds,
634 self.draw_count,
635 self.slots,
636 self.backdrop_groups,
637 self.backdrop_reads,
638 self.root.compatible,
639 )
640 }
641
642 fn top(&self) -> &Scope {
645 self.scopes.last().expect("scope stack never empty")
646 }
647
648 fn top_mut(&mut self) -> &mut Scope {
649 self.scopes.last_mut().expect("scope stack never empty")
650 }
651
652 fn close_layer(&mut self) {
657 let layer = self.layers.pop().expect("is_layer scope had a LayerScope");
658 self.slots += 1; let mut scope_bounds = layer.bounds.unwrap_or_default();
660 if layer.blur_pad > 0.0 && !scope_bounds.is_empty() {
661 scope_bounds = scope_bounds.expand(layer.blur_pad);
662 }
663
664 let Op::SaveLayer {
665 paint,
666 mask_composite: _,
667 scope_bounds: sb,
668 base_slot: _,
669 composite_slot,
670 can_elide,
671 ..
672 } = &mut self.ops[layer.op_index]
673 else {
674 unreachable!("LayerScope.op_index always points at SaveLayer");
675 };
676 *sb = scope_bounds;
677 *composite_slot = self.slots;
678 *can_elide = layer.group.compatible
681 && paint.is_opacity_only()
682 && layer.backdrop.is_none()
683 && !layer.hinted;
684
685 let supports = paint.blend_mode == crate::BlendMode::SrcOver;
690 if let Some(Backdrop {
691 filter,
692 shared_key: Some(key),
693 }) = layer.backdrop
694 {
695 self.note_backdrop_group(key, scope_bounds, filter);
696 }
697 self.draw_count += 1; self.union_bounds(scope_bounds);
699 self.note_layer_child(scope_bounds, supports);
700 }
701
702 fn record_rrect_blur(&mut self, rect: Rect, radii: [f32; 4], paint: &Paint) {
704 let Some(bounds) = self.clipped_device_bounds(&rect.expand(paint.mask_padding())) else {
705 return;
706 };
707 let slot = self.take_draw_slot(bounds, supports_opacity(paint));
708 self.ops.push(Op::RRectBlur {
709 rect,
710 radii: valo_geometry::constrain_radii(&rect, radii),
711 paint: paint.clone(),
712 bounds,
713 slot,
714 });
715 }
716
717 fn note_backdrop_group(&mut self, key: u64, bounds: Rect, filter: ImageFilter) {
718 match self.backdrop_groups.iter_mut().find(|g| g.key == key) {
719 Some(group) => {
720 group.union_bounds = group.union_bounds.union(&bounds);
721 if group.filter.as_ref() != Some(&filter) {
722 group.filter = None; }
724 }
725 None => self.backdrop_groups.push(crate::BackdropGroup {
726 key,
727 union_bounds: bounds,
728 filter: Some(filter),
729 }),
730 }
731 }
732
733 fn clipped_device_bounds(&self, local: &Rect) -> Option<Rect> {
736 let device = self.top().transform.map_rect(local);
737 match self.top().clip {
738 None => Some(device),
739 Some(clip) => device.intersect(&clip),
740 }
741 }
742
743 fn shrink_clip(&mut self, op: ClipOp, shape_bounds: Rect) {
746 if op == ClipOp::Difference {
747 return;
748 }
749 let top = self.top_mut();
750 top.clip = Some(match top.clip {
751 None => shape_bounds,
752 Some(c) => c.intersect(&shape_bounds).unwrap_or_default(), });
754 }
755
756 fn expire_scope_clips(&mut self) {
760 let pending = self.pending_clips.pop().expect("scope stack never empty");
761 if !pending.is_empty() {
762 self.slots += 1;
763 for idx in pending {
764 let Op::ClipPath { expiry_slot, .. } = &mut self.ops[idx] else {
765 unreachable!("pending_clips indexes only ClipPath ops");
766 };
767 *expiry_slot = self.slots;
768 }
769 }
770 if self.pending_clips.is_empty() {
771 self.pending_clips.push(Vec::new()); }
773 }
774
775 fn take_draw_slot(&mut self, device_bounds: Rect, supports_opacity: bool) -> u32 {
776 self.slots += 1;
777 self.draw_count += 1;
778 self.union_bounds(device_bounds);
779 self.note_layer_child(device_bounds, supports_opacity);
780 self.slots
781 }
782
783 fn union_bounds(&mut self, b: Rect) {
784 self.bounds = Some(match self.bounds {
785 Some(cur) => cur.union(&b),
786 None => b,
787 });
788 }
789
790 fn note_layer_child(&mut self, bounds: Rect, supports_opacity: bool) {
793 let Some(layer) = self.layers.last_mut() else {
794 self.root.note(bounds, supports_opacity);
795 return;
796 };
797 layer.bounds = Some(match layer.bounds {
798 Some(cur) => cur.union(&bounds),
799 None => bounds,
800 });
801 layer.group.note(bounds, supports_opacity);
802 }
803}
804
805fn supports_opacity(paint: &Paint) -> bool {
809 paint.color_filter.is_none()
814 && paint.effective_image_filter().is_none()
815 && matches!(
816 paint.blend_mode,
817 crate::BlendMode::SrcOver | crate::BlendMode::Plus
818 )
819}
820
821fn circular_radii(radii: [[f32; 2]; 4]) -> Option<[f32; 4]> {
826 radii
827 .iter()
828 .all(|[x, y]| x == y)
829 .then(|| radii.map(|[x, _]| x))
830}
831
832fn positive_rect(rect: Rect) -> Rect {
835 let x = if rect.width < 0.0 {
836 rect.x + rect.width
837 } else {
838 rect.x
839 };
840 let y = if rect.height < 0.0 {
841 rect.y + rect.height
842 } else {
843 rect.y
844 };
845 Rect::new(x, y, rect.width.abs(), rect.height.abs())
846}
847
848fn is_analytic_blur(paint: &Paint) -> bool {
849 paint.mask_blur.is_some()
850 && paint.shader.is_none()
851 && paint.color_filter.is_none()
855 && paint.effective_image_filter().is_none()
856 && matches!(paint.style, crate::PaintStyle::Fill)
857}
858
859fn rect_path(r: Rect) -> Arc<Path> {
860 let mut p = PathBuilder::new();
861 p.rect(r);
862 p.build()
863}
864
865#[cfg(test)]
866mod tests {
867 use super::*;
868 use crate::BlendMode;
869 use valo_geometry::Color;
870
871 #[test]
872 fn save_count_tracks_saves_layers_and_restores() {
873 let mut builder = DisplayListBuilder::new();
874 assert_eq!(builder.save_count(), 1);
875
876 builder.save();
877 assert_eq!(builder.save_count(), 2);
878
879 builder.save_layer(None, &Paint::default());
880 assert_eq!(builder.save_count(), 3);
881
882 builder.restore();
883 assert_eq!(builder.save_count(), 2);
884 builder.restore();
885 assert_eq!(builder.save_count(), 1);
886 }
887
888 #[test]
889 fn rounded_rects_normalize_inverted_edges_like_flutter() {
890 let mut builder = DisplayListBuilder::new();
891 builder.draw_rrect(
892 Rect::from_ltrb(-1.0, -10.0 / 3.0, 1.0, -10.0),
893 1.0,
894 &Paint::from_color(Color::WHITE),
895 );
896
897 let list = builder.build();
898 let Op::DrawPath { path, .. } = &list.ops()[0] else {
899 panic!("rounded rectangle should record as a path");
900 };
901 assert_eq!(
902 path.bounds(),
903 Rect::from_ltrb(-1.0, -10.0, 1.0, -10.0 / 3.0)
904 );
905 }
906 fn red() -> Paint {
907 Paint::from_color(Color::rgb(1.0, 0.0, 0.0))
908 }
909
910 fn alpha_layer(a: f32) -> Paint {
911 Paint::from_color(Color::rgba(0.0, 0.0, 0.0, a))
912 }
913
914 fn find_clip(dl: &DisplayList) -> (&Op, u32) {
915 for op in dl.ops() {
916 if let Op::ClipPath { expiry_slot, .. } = op {
917 return (op, *expiry_slot);
918 }
919 }
920 panic!("no clip recorded");
921 }
922
923 fn layer_facts(dl: &DisplayList) -> Vec<(Rect, u32, u32, bool)> {
927 dl.ops()
928 .iter()
929 .filter_map(|op| match op {
930 Op::SaveLayer {
931 scope_bounds,
932 base_slot,
933 composite_slot,
934 can_elide,
935 ..
936 } => Some((*scope_bounds, *base_slot, *composite_slot, *can_elide)),
937 _ => None,
938 })
939 .collect()
940 }
941
942 fn find_layer(dl: &DisplayList) -> (Rect, u32, u32, bool) {
943 *layer_facts(dl).first().expect("no layer recorded")
944 }
945
946 #[test]
947 fn oracle_bounds_follow_transforms() {
948 let mut b = DisplayListBuilder::new();
949 b.save();
950 b.translate(100.0, 50.0);
951 b.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red());
952 b.restore();
953 let dl = b.build();
954 assert_eq!(dl.bounds(), Some(Rect::new(100.0, 50.0, 10.0, 10.0)));
955 assert_eq!(dl.draw_count(), 1);
956 assert_eq!(dl.depth_slots(), 1);
957 }
958
959 #[test]
960 fn clip_shrinks_recorded_draw_bounds() {
961 let mut b = DisplayListBuilder::new();
962 b.save();
963 b.clip_rect(Rect::new(0.0, 0.0, 50.0, 50.0), ClipOp::Intersect);
964 b.draw_rect(Rect::new(25.0, 25.0, 100.0, 100.0), &red());
965 b.restore();
966 let dl = b.build();
967 assert_eq!(dl.bounds(), Some(Rect::new(25.0, 25.0, 25.0, 25.0)));
968 }
969
970 #[test]
971 fn fully_clipped_draw_is_dropped() {
972 let mut b = DisplayListBuilder::new();
973 b.save();
974 b.clip_rect(Rect::new(0.0, 0.0, 10.0, 10.0), ClipOp::Intersect);
975 b.draw_rect(Rect::new(500.0, 500.0, 10.0, 10.0), &red());
976 b.restore();
977 let dl = b.build();
978 assert_eq!(dl.draw_count(), 0);
979 }
980
981 #[test]
982 fn clip_expiry_is_the_restore_slot() {
983 let mut b = DisplayListBuilder::new();
984 b.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red()); b.save();
986 b.clip_rect(Rect::new(0.0, 0.0, 50.0, 50.0), ClipOp::Intersect);
987 b.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red()); b.restore(); b.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red()); let dl = b.build();
991 let (_, expiry) = find_clip(&dl);
992 assert_eq!(expiry, 3);
993 assert_eq!(dl.depth_slots(), 4);
994 }
995
996 #[test]
997 fn root_clip_expires_at_end_of_list() {
998 let mut b = DisplayListBuilder::new();
999 b.clip_rect(Rect::new(0.0, 0.0, 50.0, 50.0), ClipOp::Intersect);
1000 b.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red()); let dl = b.build();
1002 let (_, expiry) = find_clip(&dl);
1003 assert_eq!(expiry, 2, "root clips expire at the virtual end slot");
1004 assert_eq!(dl.depth_slots(), 2);
1005 }
1006
1007 #[test]
1008 fn difference_clip_keeps_bounds_conservative() {
1009 let mut b = DisplayListBuilder::new();
1010 b.save();
1011 b.clip_rect(Rect::new(0.0, 0.0, 50.0, 50.0), ClipOp::Difference);
1012 b.draw_rect(Rect::new(0.0, 0.0, 100.0, 100.0), &red());
1013 b.restore();
1014 let dl = b.build();
1015 assert_eq!(dl.bounds(), Some(Rect::new(0.0, 0.0, 100.0, 100.0)));
1016 }
1017
1018 #[test]
1019 fn nested_list_folds_oracle_and_offsets_slots() {
1020 let mut inner = DisplayListBuilder::new();
1021 inner.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red());
1022 inner.draw_rect(Rect::new(20.0, 0.0, 10.0, 10.0), &red());
1023 let inner = Arc::new(inner.build());
1024
1025 let mut outer = DisplayListBuilder::new();
1026 outer.draw_rect(Rect::new(0.0, 0.0, 5.0, 5.0), &red()); outer.translate(5.0, 5.0);
1028 outer.draw_display_list(&inner); outer.draw_rect(Rect::new(0.0, 0.0, 5.0, 5.0), &red()); let outer = outer.build();
1031
1032 assert_eq!(outer.draw_count(), 4);
1033 assert_eq!(outer.depth_slots(), 4);
1034 let base = outer
1035 .ops()
1036 .iter()
1037 .find_map(|op| match op {
1038 Op::DrawDisplayList { base_slot, .. } => Some(*base_slot),
1039 _ => None,
1040 })
1041 .unwrap();
1042 assert_eq!(base, 1);
1043 }
1044
1045 #[test]
1046 fn nop_draws_are_dropped() {
1047 let mut b = DisplayListBuilder::new();
1048 b.draw_rect(Rect::new(0.0, 0.0, 0.0, 10.0), &red()); b.draw_rect(
1050 Rect::new(0.0, 0.0, 10.0, 10.0),
1051 &Paint {
1052 color: Color::TRANSPARENT,
1053 blend_mode: BlendMode::SrcOver,
1054 ..Default::default()
1055 },
1056 );
1057 let dl = b.build();
1058 assert_eq!(dl.ops().len(), 0);
1059 assert_eq!(dl.bounds(), None);
1060 }
1061
1062 #[test]
1065 fn layer_oracle_bounds_and_slots() {
1066 let mut b = DisplayListBuilder::new();
1067 b.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red()); b.save_layer(None, &alpha_layer(0.5)); b.draw_rect(Rect::new(20.0, 20.0, 30.0, 30.0), &red()); b.draw_rect(Rect::new(60.0, 20.0, 30.0, 30.0), &red()); b.restore(); b.draw_rect(Rect::new(0.0, 40.0, 10.0, 10.0), &red()); let dl = b.build();
1074
1075 let (bounds, base_slot, composite_slot, can_elide) = find_layer(&dl);
1076 assert_eq!(bounds, Rect::new(20.0, 20.0, 70.0, 30.0));
1077 assert_eq!(base_slot, 1, "scope opened after one parent draw");
1078 assert_eq!(composite_slot, 4, "children keep the global line");
1079 assert!(
1080 can_elide,
1081 "disjoint SrcOver children + alpha-only composite"
1082 );
1083 assert_eq!(
1084 dl.depth_slots(),
1085 5,
1086 "one global depth line (Impeller's current_depth_)"
1087 );
1088 assert_eq!(dl.draw_count(), 5, "4 rects + the composite");
1089 }
1090
1091 #[test]
1092 fn overlapping_children_forfeit_elision() {
1093 let mut b = DisplayListBuilder::new();
1094 b.save_layer(None, &alpha_layer(0.5));
1095 b.draw_rect(Rect::new(0.0, 0.0, 30.0, 30.0), &red());
1096 b.draw_rect(Rect::new(10.0, 10.0, 30.0, 30.0), &red()); b.restore();
1098 let (_, _, _, can_elide) = find_layer(&b.build());
1099 assert!(!can_elide);
1100 }
1101
1102 #[test]
1103 fn advanced_blend_composite_forfeits_elision() {
1104 let mut b = DisplayListBuilder::new();
1105 let paint = Paint {
1106 color: Color::rgba(0.0, 0.0, 0.0, 0.5),
1107 blend_mode: BlendMode::Multiply,
1108 ..Default::default()
1109 };
1110 b.save_layer(None, &paint);
1111 b.draw_rect(Rect::new(0.0, 0.0, 30.0, 30.0), &red());
1112 b.restore();
1113 let (_, _, _, can_elide) = find_layer(&b.build());
1114 assert!(!can_elide);
1115 }
1116
1117 #[test]
1118 fn destructive_layer_composite_floods_the_active_clip() {
1119 let mut b = DisplayListBuilder::new();
1120 b.clip_rect(Rect::new(4.0, 6.0, 80.0, 60.0), ClipOp::Intersect);
1121 b.save_layer(
1122 None,
1123 &Paint {
1124 blend_mode: BlendMode::SrcIn,
1125 ..Default::default()
1126 },
1127 );
1128 b.draw_rect(Rect::new(20.0, 20.0, 10.0, 10.0), &red());
1129 b.restore();
1130 let (bounds, ..) = find_layer(&b.build());
1131 assert_eq!(bounds, Rect::new(4.0, 6.0, 80.0, 60.0));
1132 }
1133
1134 #[test]
1135 fn clip_inside_layer_keeps_elision() {
1136 let mut b = DisplayListBuilder::new();
1137 b.save_layer(None, &alpha_layer(0.5));
1138 b.clip_rect(Rect::new(0.0, 0.0, 50.0, 50.0), ClipOp::Intersect);
1139 b.draw_rect(Rect::new(0.0, 0.0, 30.0, 30.0), &red());
1140 b.restore();
1141 let (_, _, _, can_elide) = find_layer(&b.build());
1142 assert!(can_elide);
1145 }
1146
1147 #[test]
1148 fn an_embedded_list_of_disjoint_draws_keeps_elision() {
1149 let mut inner = DisplayListBuilder::new();
1150 inner.draw_rect(Rect::new(0.0, 0.0, 30.0, 30.0), &red());
1151 inner.draw_rect(Rect::new(40.0, 0.0, 30.0, 30.0), &red());
1152 let inner = Arc::new(inner.build());
1153 assert!(inner.supports_opacity());
1154
1155 let mut b = DisplayListBuilder::new();
1156 b.save_layer(None, &alpha_layer(0.5));
1157 b.draw_display_list(&inner);
1158 b.restore();
1159 let (_, _, _, can_elide) = find_layer(&b.build());
1160 assert!(can_elide, "the list answers for its children");
1161 }
1162
1163 #[test]
1164 fn an_embedded_list_of_overlapping_draws_forfeits_elision() {
1165 let mut inner = DisplayListBuilder::new();
1166 inner.draw_rect(Rect::new(0.0, 0.0, 30.0, 30.0), &red());
1167 inner.draw_rect(Rect::new(10.0, 10.0, 30.0, 30.0), &red());
1168 let inner = Arc::new(inner.build());
1169 assert!(!inner.supports_opacity());
1170
1171 let mut b = DisplayListBuilder::new();
1172 b.save_layer(None, &alpha_layer(0.5));
1173 b.draw_display_list(&inner);
1174 b.restore();
1175 let (_, _, _, can_elide) = find_layer(&b.build());
1176 assert!(!can_elide);
1177 }
1178
1179 #[test]
1180 fn two_embedded_lists_that_overlap_forfeit_elision() {
1181 let mut inner = DisplayListBuilder::new();
1182 inner.draw_rect(Rect::new(0.0, 0.0, 30.0, 30.0), &red());
1183 let inner = Arc::new(inner.build());
1184
1185 let mut b = DisplayListBuilder::new();
1186 b.save_layer(None, &alpha_layer(0.5));
1187 b.draw_display_list(&inner);
1188 b.translate(10.0, 10.0);
1189 b.draw_display_list(&inner);
1190 b.restore();
1191 let (_, _, _, can_elide) = find_layer(&b.build());
1192 assert!(!can_elide);
1193 }
1194
1195 #[test]
1196 fn a_child_inside_the_union_of_earlier_ones_forfeits_elision() {
1197 let mut b = DisplayListBuilder::new();
1200 b.save_layer(None, &alpha_layer(0.5));
1201 b.draw_rect(Rect::new(0.0, 0.0, 30.0, 30.0), &red());
1202 b.draw_rect(Rect::new(40.0, 40.0, 30.0, 30.0), &red());
1203 b.draw_rect(Rect::new(40.0, 0.0, 30.0, 30.0), &red());
1204 b.restore();
1205 let (_, _, _, can_elide) = find_layer(&b.build());
1206 assert!(!can_elide);
1207 }
1208
1209 #[test]
1210 fn a_layer_composite_is_one_child_of_the_root() {
1211 let mut b = DisplayListBuilder::new();
1212 b.draw_rect(Rect::new(0.0, 0.0, 30.0, 30.0), &red());
1213 b.save_layer(None, &alpha_layer(0.5));
1214 b.draw_rect(Rect::new(10.0, 10.0, 30.0, 30.0), &red()); b.restore();
1216 assert!(!b.build().supports_opacity());
1217 }
1218
1219 #[test]
1220 fn bounds_hint_crops_the_scope() {
1221 let mut b = DisplayListBuilder::new();
1222 b.save_layer(Some(Rect::new(0.0, 0.0, 40.0, 40.0)), &alpha_layer(0.5));
1223 b.draw_rect(Rect::new(20.0, 20.0, 100.0, 100.0), &red());
1224 b.restore();
1225 let (bounds, ..) = find_layer(&b.build());
1226 assert_eq!(bounds, Rect::new(20.0, 20.0, 20.0, 20.0));
1227 }
1228
1229 #[test]
1230 fn clips_inside_layers_expire_within_the_scope_span() {
1231 let mut b = DisplayListBuilder::new();
1232 b.save_layer(None, &alpha_layer(0.5)); b.save();
1234 b.clip_rect(Rect::new(0.0, 0.0, 50.0, 50.0), ClipOp::Intersect);
1235 b.draw_rect(Rect::new(0.0, 0.0, 30.0, 30.0), &red()); b.restore(); b.restore(); let dl = b.build();
1239 let (_, expiry) = find_clip(&dl);
1240 assert_eq!(expiry, 2, "expiry sits inside the layer's span");
1241 let (_, base_slot, composite_slot, _) = find_layer(&dl);
1242 assert_eq!((base_slot, composite_slot), (0, 3));
1243 }
1244
1245 #[test]
1248 fn solid_mask_blur_records_the_analytic_op() {
1249 let mut b = DisplayListBuilder::new();
1250 let paint = Paint {
1251 mask_blur: Some(crate::MaskBlur::new(4.0)),
1252 ..red()
1253 };
1254 b.draw_rect(Rect::new(20.0, 20.0, 40.0, 40.0), &paint);
1255 b.draw_rrect(Rect::new(100.0, 20.0, 40.0, 40.0), 8.0, &paint);
1256 let dl = b.build();
1257 let blurs: Vec<_> = dl
1258 .ops()
1259 .iter()
1260 .filter_map(|op| match op {
1261 Op::RRectBlur { radii, bounds, .. } => Some((*radii, *bounds)),
1262 _ => None,
1263 })
1264 .collect();
1265 assert_eq!(blurs.len(), 2);
1266 assert_eq!(blurs[0].0, [0.0; 4]);
1267 assert_eq!(blurs[1].0, [8.0; 4]);
1268 assert_eq!(blurs[0].1, Rect::new(8.0, 8.0, 64.0, 64.0));
1270 }
1271
1272 #[test]
1273 fn shader_mask_blur_stays_general_but_pads_bounds() {
1274 let mut b = DisplayListBuilder::new();
1275 let paint = Paint {
1276 mask_blur: Some(crate::MaskBlur::new(2.0)),
1277 shader: Some(crate::Shader::linear(
1278 valo_geometry::Point::new(0.0, 0.0),
1279 valo_geometry::Point::new(10.0, 0.0),
1280 Color::BLACK,
1281 Color::WHITE,
1282 )),
1283 color: Color::WHITE,
1284 ..Default::default()
1285 };
1286 b.draw_rect(Rect::new(10.0, 10.0, 20.0, 20.0), &paint);
1287 let dl = b.build();
1288 let Op::DrawRect { bounds, .. } = &dl.ops()[0] else {
1289 panic!("shader paints keep the general op");
1290 };
1291 assert_eq!(*bounds, Rect::new(4.0, 4.0, 32.0, 32.0));
1292 }
1293
1294 #[test]
1295 fn hinted_layer_forfeits_elision() {
1296 let mut b = DisplayListBuilder::new();
1297 b.save_layer(Some(Rect::new(0.0, 0.0, 40.0, 40.0)), &alpha_layer(0.5));
1298 b.draw_rect(Rect::new(0.0, 0.0, 30.0, 30.0), &red());
1299 b.restore();
1300 let (_, _, _, can_elide) = find_layer(&b.build());
1301 assert!(!can_elide, "the hint is a crop; eliding would un-crop it");
1302 }
1303
1304 fn glass(b: &mut DisplayListBuilder, rect: Rect, sigma: f32, key: Option<u64>) {
1308 b.save_layer_backdrop(
1309 Some(rect),
1310 &Paint::default(),
1311 Backdrop {
1312 filter: ImageFilter::blur(sigma, sigma),
1313 shared_key: key,
1314 },
1315 );
1316 b.restore();
1317 }
1318
1319 #[test]
1320 fn shared_backdrops_group_by_key() {
1321 let mut b = DisplayListBuilder::new();
1322 glass(&mut b, Rect::new(0.0, 0.0, 50.0, 50.0), 8.0, Some(7));
1323 glass(&mut b, Rect::new(100.0, 0.0, 50.0, 50.0), 8.0, Some(7));
1324 glass(&mut b, Rect::new(0.0, 100.0, 50.0, 50.0), 8.0, None);
1325 let dl = b.build();
1326 let group = dl.backdrop_group(7).expect("key 7 recorded");
1327 assert_eq!(group.union_bounds, Rect::new(0.0, 0.0, 150.0, 50.0));
1329 assert_eq!(
1330 group.filter,
1331 Some(ImageFilter::blur(8.0, 8.0)),
1332 "one σ across the key: shareable"
1333 );
1334 assert_eq!(dl.draw_count(), 3, "each layer's composite is a draw");
1335 assert_eq!(dl.depth_slots(), 3);
1336 }
1337
1338 #[test]
1339 fn mixed_sigma_under_one_key_clears_the_shared_sigma() {
1340 let mut b = DisplayListBuilder::new();
1341 glass(&mut b, Rect::new(0.0, 0.0, 50.0, 50.0), 4.0, Some(7));
1342 glass(&mut b, Rect::new(100.0, 0.0, 50.0, 50.0), 12.0, Some(7));
1343 let dl = b.build();
1344 let group = dl.backdrop_group(7).expect("key 7 recorded");
1345 assert_eq!(group.filter, None, "disagreeing σ cannot share one blur");
1346 }
1347
1348 #[test]
1349 fn opacity_group_elides_over_a_backdrop_layer() {
1350 let mut b = DisplayListBuilder::new();
1351 b.save_layer(None, &alpha_layer(0.5));
1352 glass(&mut b, Rect::new(0.0, 0.0, 50.0, 50.0), 4.0, None);
1353 b.restore();
1354 let layers = layer_facts(&b.build());
1355 assert_eq!(layers.len(), 2, "the opacity group and the glass inside it");
1356 assert!(
1357 layers[0].3,
1358 "the group's alpha lands on the glass composite — the whole point \
1359 of backdrop-as-a-layer-property: glass keeps blurring while the \
1360 group fades"
1361 );
1362 assert!(!layers[1].3, "the glass itself needs a texture to seed");
1363 }
1364
1365 #[test]
1371 fn a_clip_does_not_forfeit_elision_around_glass() {
1372 let mut b = DisplayListBuilder::new();
1373 b.save_layer(None, &alpha_layer(0.5));
1374 b.save();
1375 let mut clip = PathBuilder::new();
1376 clip.rect(Rect::new(0.0, 0.0, 60.0, 60.0));
1377 b.clip_path(&clip.build(), FillRule::NonZero, ClipOp::Intersect);
1378 glass(&mut b, Rect::new(0.0, 0.0, 50.0, 50.0), 4.0, None);
1379 b.restore();
1380 b.restore();
1381 let layers = layer_facts(&b.build());
1382 assert_eq!(layers.len(), 2);
1383 assert!(layers[0].3, "the clipped fade still elides");
1384 assert!(!layers[1].3);
1385 }
1386
1387 #[test]
1388 fn backdrop_reads_count_unshared_and_nested() {
1389 let mut child = DisplayListBuilder::new();
1390 glass(&mut child, Rect::new(0.0, 0.0, 50.0, 50.0), 4.0, None);
1391 let child = Arc::new(child.build());
1392 assert_eq!(child.backdrop_reads(), 1, "unshared reads count too");
1393
1394 let mut parent = DisplayListBuilder::new();
1395 glass(&mut parent, Rect::new(0.0, 0.0, 50.0, 50.0), 8.0, Some(7));
1396 parent.draw_display_list(&child);
1397 let parent = parent.build();
1398 assert_eq!(parent.backdrop_reads(), 2, "own layer + the nested list's");
1399
1400 let mut clean = DisplayListBuilder::new();
1401 clean.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red());
1402 assert_eq!(clean.build().backdrop_reads(), 0);
1403 }
1404
1405 #[cfg(feature = "serde")]
1406 #[test]
1407 fn serde_dump_is_readable_json() {
1408 let mut b = DisplayListBuilder::new();
1411 b.translate(1.0, 2.0);
1412 b.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red());
1413 let dl = b.build();
1414 let json: serde_json::Value = serde_json::to_value(&dl).unwrap();
1415 assert_eq!(json["ops"].as_array().unwrap().len(), dl.ops().len());
1416 assert!(json["ops"][1]["DrawRect"]["slot"].is_number());
1417 }
1418}