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}
34
35#[derive(Clone, Debug)]
40pub struct Backdrop {
41 pub filter: ImageFilter,
43 pub shared_key: Option<u64>,
46}
47
48impl Backdrop {
49 pub fn new(filter: ImageFilter) -> Self {
51 Self {
52 filter,
53 shared_key: None,
54 }
55 }
56
57 pub fn blur(sigma: f32) -> Self {
59 Self::new(ImageFilter::blur(sigma, sigma))
60 }
61
62 pub fn shared(mut self, key: u64) -> Self {
64 self.shared_key = Some(key);
65 self
66 }
67}
68
69#[derive(Clone, Copy)]
72struct Scope {
73 transform: Matrix,
74 clip: Option<Rect>,
75 is_layer: bool,
76}
77
78struct LayerScope {
80 op_index: usize,
82 bounds: Option<Rect>,
84 child_bounds: Vec<Rect>,
87 compatible: bool,
90 blur_pad: f32,
92 backdrop: Option<Backdrop>,
94 hinted: bool,
99}
100
101impl Default for DisplayListBuilder {
102 fn default() -> Self {
103 Self::new()
104 }
105}
106
107impl DisplayListBuilder {
108 pub fn new() -> Self {
110 Self {
111 ops: Vec::new(),
112 scopes: vec![Scope {
113 transform: Matrix::IDENTITY,
114 clip: None,
115 is_layer: false,
116 }],
117 layers: Vec::new(),
118 backdrop_groups: Vec::new(),
119 pending_clips: vec![Vec::new()],
120 slots: 0,
121 bounds: None,
122 draw_count: 0,
123 backdrop_reads: 0,
124 }
125 }
126
127 pub fn save(&mut self) {
131 self.scopes.push(Scope {
132 is_layer: false,
133 ..*self.top()
134 });
135 self.pending_clips.push(Vec::new());
136 self.ops.push(Op::Save);
137 }
138
139 pub fn save_count(&self) -> usize {
145 self.scopes.len()
146 }
147
148 pub fn save_layer(&mut self, bounds_hint: Option<Rect>, paint: &Paint) {
154 self.save_layer_inner(bounds_hint, paint, None, None);
155 }
156
157 pub fn save_layer_mask(&mut self, bounds_hint: Option<Rect>, kind: MaskKind) {
163 let paint = Paint {
164 blend_mode: crate::BlendMode::DstIn,
165 ..Paint::default()
166 };
167 self.save_layer_inner(bounds_hint, &paint, Some(kind), None);
168 }
169
170 pub fn save_layer_backdrop(
180 &mut self,
181 bounds_hint: Option<Rect>,
182 paint: &Paint,
183 backdrop: Backdrop,
184 ) {
185 let backdrop = (!backdrop.filter.is_nop()).then_some(backdrop);
186 self.save_layer_inner(bounds_hint, paint, None, backdrop);
187 }
188
189 fn save_layer_inner(
190 &mut self,
191 bounds_hint: Option<Rect>,
192 paint: &Paint,
193 mask_composite: Option<MaskKind>,
194 backdrop: Option<Backdrop>,
195 ) {
196 let device_hint = bounds_hint.map(|h| self.top().transform.map_rect(&h));
197 let mut scope = Scope {
198 is_layer: true,
199 ..*self.top()
200 };
201 if let Some(h) = device_hint {
204 scope.clip = Some(match scope.clip {
205 None => h,
206 Some(c) => c.intersect(&h).unwrap_or_default(),
207 });
208 }
209 let floods_scope = paint.blend_mode.is_destructive()
213 || paint
214 .color_filter
215 .is_some_and(|filter| filter.modifies_transparent_black())
216 || paint
217 .image_filter
218 .as_ref()
219 .is_some_and(|filter| filter.modifies_transparent_black())
220 || backdrop.is_some();
225 let flooded_bounds = floods_scope.then(|| scope.clip.unwrap_or(Rect::EVERYTHING));
226 if backdrop.is_some() {
227 self.backdrop_reads += 1;
228 }
229 self.scopes.push(scope);
230 self.pending_clips.push(Vec::new());
231 self.layers.push(LayerScope {
232 op_index: self.ops.len(),
233 bounds: flooded_bounds,
234 child_bounds: Vec::new(),
235 compatible: true,
236 blur_pad: paint.device_effect_padding(&self.top().transform),
239 backdrop: backdrop.clone(),
240 hinted: device_hint.is_some(),
241 });
242 self.ops.push(Op::SaveLayer {
245 paint: paint.clone(),
246 mask_composite,
247 scope_bounds: Rect::default(), base_slot: self.slots,
249 composite_slot: 0,
250 can_elide: false,
251 backdrop_key: backdrop.as_ref().and_then(|b| b.shared_key),
252 backdrop_filter: backdrop.map(|b| b.filter),
253 });
254 }
255
256 pub fn restore(&mut self) {
260 if self.scopes.len() == 1 {
261 debug_assert!(false, "restore() without matching save()");
262 return;
263 }
264 let scope = self.scopes.pop().expect("checked above");
265 self.expire_scope_clips(); if scope.is_layer {
267 self.close_layer();
268 }
269 self.ops.push(Op::Restore);
270 }
271
272 pub fn translate(&mut self, tx: f32, ty: f32) {
274 self.concat(&Matrix::translation(tx, ty));
275 }
276
277 pub fn scale(&mut self, sx: f32, sy: f32) {
279 self.concat(&Matrix::scale(sx, sy));
280 }
281
282 pub fn rotate(&mut self, radians: f32) {
286 self.concat(&Matrix::rotation(radians));
287 }
288
289 pub fn concat(&mut self, local: &Matrix) {
291 let top = self.top_mut();
292 top.transform = top.transform.then(local);
293 self.ops.push(Op::Transform(*local));
294 }
295
296 pub fn clip_rect(&mut self, rect: impl Into<Rect>, op: ClipOp) {
300 let rect = rect.into();
301 self.clip_path(&rect_path(rect), FillRule::NonZero, op);
302 }
303
304 pub fn clip_rrect(&mut self, rect: impl Into<Rect>, radius: f32, op: ClipOp) {
306 let rect = rect.into();
307 self.clip_rrect_radii(rect, [radius; 4], op);
308 }
309
310 pub fn clip_rrect_radii(&mut self, rect: impl Into<Rect>, radii: [f32; 4], op: ClipOp) {
314 let rect = positive_rect(rect.into());
315 let mut p = PathBuilder::new();
316 p.rrect_radii(rect, radii);
317 self.clip_path(&p.build(), FillRule::NonZero, op);
318 }
319
320 pub fn clip_rrect_radii_elliptical(
324 &mut self,
325 rect: impl Into<Rect>,
326 radii: [[f32; 2]; 4],
327 op: ClipOp,
328 ) {
329 let rect = positive_rect(rect.into());
330 if let Some(circular) = circular_radii(radii) {
331 return self.clip_rrect_radii(rect, circular, op);
332 }
333 let mut p = PathBuilder::new();
334 p.rrect_radii_elliptical(rect, radii);
335 self.clip_path(&p.build(), FillRule::NonZero, op);
336 }
337
338 pub fn clip_path(&mut self, path: &Arc<Path>, fill_rule: FillRule, op: ClipOp) {
348 let bounds = self.top().transform.map_rect(&path.bounds());
349 self.shrink_clip(op, bounds);
350 self.pending_clips
351 .last_mut()
352 .expect("root scope")
353 .push(self.ops.len());
354 self.ops.push(Op::ClipPath {
355 path: Arc::clone(path),
356 fill_rule,
357 op,
358 expiry_slot: 0, });
360 }
361
362 pub fn draw_rect(&mut self, rect: impl Into<Rect>, paint: &Paint) {
366 let rect = rect.into();
367 if paint.is_nop() {
368 return;
369 }
370 if matches!(paint.style, crate::PaintStyle::Stroke(_)) {
371 return self.draw_path(&rect_path(rect), FillRule::NonZero, paint);
374 }
375 if rect.is_empty() {
376 return;
377 }
378 if is_analytic_blur(paint) {
379 self.record_rrect_blur(rect, [0.0; 4], paint);
380 return;
381 }
382 let Some(bounds) = self.clipped_device_bounds(&paint.effect_bounds(rect)) else {
383 return; };
385 let slot = self.take_draw_slot(bounds, supports_opacity(paint));
386 self.ops.push(Op::DrawRect {
387 rect,
388 paint: paint.clone(),
389 bounds,
390 slot,
391 });
392 }
393
394 pub fn draw_path(&mut self, path: &Arc<Path>, fill_rule: FillRule, paint: &Paint) {
396 if path.is_empty() || paint.is_nop() {
397 return;
398 }
399 let scale = self.top().transform.max_scale();
400 let local = paint.effect_bounds(path.bounds().expand(paint.stroke_padding_at_scale(scale)));
401 let Some(bounds) = self.clipped_device_bounds(&local) else {
402 return;
403 };
404 let slot = self.take_draw_slot(bounds, supports_opacity(paint));
405 self.ops.push(Op::DrawPath {
406 path: Arc::clone(path),
407 fill_rule,
408 paint: paint.clone(),
409 bounds,
410 slot,
411 });
412 }
413
414 pub fn draw_circle(
416 &mut self,
417 center: impl Into<valo_geometry::Point>,
418 radius: f32,
419 paint: &Paint,
420 ) {
421 let mut p = PathBuilder::new();
422 p.circle(center, radius);
423 self.draw_path(&p.build(), FillRule::NonZero, paint);
424 }
425
426 pub fn draw_rrect(&mut self, rect: impl Into<Rect>, radius: f32, paint: &Paint) {
428 let rect = rect.into();
429 self.draw_rrect_radii(rect, [radius; 4], paint);
430 }
431
432 pub fn draw_rrect_radii(&mut self, rect: impl Into<Rect>, radii: [f32; 4], paint: &Paint) {
436 let rect = positive_rect(rect.into());
437 if rect.is_empty() || paint.is_nop() {
438 return;
439 }
440 if is_analytic_blur(paint) {
441 self.record_rrect_blur(rect, radii, paint);
442 return;
443 }
444 let mut p = PathBuilder::new();
445 p.rrect_radii(rect, radii);
446 self.draw_path(&p.build(), FillRule::NonZero, paint);
447 }
448
449 pub fn draw_rrect_radii_elliptical(
453 &mut self,
454 rect: impl Into<Rect>,
455 radii: [[f32; 2]; 4],
456 paint: &Paint,
457 ) {
458 let rect = positive_rect(rect.into());
459 if let Some(circular) = circular_radii(radii) {
460 return self.draw_rrect_radii(rect, circular, paint);
461 }
462 if rect.is_empty() || paint.is_nop() {
463 return;
464 }
465 let mut p = PathBuilder::new();
466 p.rrect_radii_elliptical(rect, radii);
467 self.draw_path(&p.build(), FillRule::NonZero, paint);
468 }
469
470 pub fn draw_image(&mut self, image: &Image, dst: Rect, paint: &Paint) {
474 let src = Rect::new(0.0, 0.0, image.width(), image.height());
475 self.draw_image_rect(image, src, dst, Sampling::default(), paint);
476 }
477
478 pub fn draw_image_rect(
483 &mut self,
484 image: &Image,
485 src: Rect,
486 dst: Rect,
487 sampling: Sampling,
488 paint: &Paint,
489 ) {
490 if dst.is_empty() || src.is_empty() || paint.is_nop() {
491 return;
492 }
493 let Some(bounds) = self.clipped_device_bounds(&paint.effect_bounds(dst)) else {
494 return;
495 };
496 let slot = self.take_draw_slot(bounds, supports_opacity(paint));
497 self.ops.push(Op::DrawImage {
498 image: image.clone(),
499 src,
500 dst,
501 sampling,
502 paint: paint.clone(),
503 bounds,
504 slot,
505 });
506 }
507
508 pub fn draw_glyph_run(
513 &mut self,
514 font: std::sync::Arc<valo_text::Font>,
515 size: f32,
516 paint: &Paint,
517 glyphs: Arc<Vec<crate::GlyphPos>>,
518 local_bounds: Rect,
519 ) {
520 if glyphs.is_empty() || paint.is_nop() {
521 return;
522 }
523 let scale = self.top().transform.max_scale();
524 let padded = paint.effect_bounds(local_bounds.expand(paint.stroke_padding_at_scale(scale)));
525 let Some(bounds) = self.clipped_device_bounds(&padded) else {
526 return;
527 };
528 let distributes = supports_opacity(paint) && paint.shader.is_none();
531 let slot = self.take_draw_slot(bounds, distributes);
532 self.ops.push(Op::GlyphRun {
533 font,
534 size,
535 paint: paint.clone(),
536 glyphs,
537 bounds,
538 slot,
539 });
540 }
541
542 pub fn draw_display_list(&mut self, list: &Arc<DisplayList>) {
544 self.embed_display_list(list, false);
545 }
546
547 pub fn draw_display_list_cached(&mut self, list: &Arc<DisplayList>) {
552 self.embed_display_list(list, true);
553 }
554
555 fn embed_display_list(&mut self, list: &Arc<DisplayList>, cache: bool) {
556 let Some(child_bounds) = list.bounds() else {
557 return; };
559 let Some(bounds) = self.clipped_device_bounds(&child_bounds) else {
560 return;
561 };
562 let base_slot = self.slots;
563 self.slots += list.depth_slots();
564 self.draw_count += list.draw_count();
565 self.backdrop_reads += list.backdrop_reads();
566 self.union_bounds(bounds);
567 self.note_layer_child(bounds, false);
569 self.ops.push(Op::DrawDisplayList {
570 list: Arc::clone(list),
571 bounds,
572 base_slot,
573 cache,
574 });
575 }
576
577 pub fn build(mut self) -> DisplayList {
583 while self.scopes.len() > 1 {
586 self.restore();
587 }
588 self.expire_scope_clips(); DisplayList::new(
590 self.ops,
591 self.bounds,
592 self.draw_count,
593 self.slots,
594 self.backdrop_groups,
595 self.backdrop_reads,
596 )
597 }
598
599 fn top(&self) -> &Scope {
602 self.scopes.last().expect("scope stack never empty")
603 }
604
605 fn top_mut(&mut self) -> &mut Scope {
606 self.scopes.last_mut().expect("scope stack never empty")
607 }
608
609 fn close_layer(&mut self) {
614 let layer = self.layers.pop().expect("is_layer scope had a LayerScope");
615 self.slots += 1; let mut scope_bounds = layer.bounds.unwrap_or_default();
617 if layer.blur_pad > 0.0 && !scope_bounds.is_empty() {
618 scope_bounds = scope_bounds.expand(layer.blur_pad);
619 }
620
621 let Op::SaveLayer {
622 paint,
623 mask_composite: _,
624 scope_bounds: sb,
625 base_slot: _,
626 composite_slot,
627 can_elide,
628 ..
629 } = &mut self.ops[layer.op_index]
630 else {
631 unreachable!("LayerScope.op_index always points at SaveLayer");
632 };
633 *sb = scope_bounds;
634 *composite_slot = self.slots;
635 *can_elide = layer.compatible
638 && paint.is_opacity_only()
639 && layer.backdrop.is_none()
640 && !layer.hinted;
641
642 let supports = paint.blend_mode == crate::BlendMode::SrcOver;
647 if let Some(Backdrop {
648 filter,
649 shared_key: Some(key),
650 }) = layer.backdrop
651 {
652 self.note_backdrop_group(key, scope_bounds, filter);
653 }
654 self.draw_count += 1; self.union_bounds(scope_bounds);
656 self.note_layer_child(scope_bounds, supports);
657 }
658
659 fn record_rrect_blur(&mut self, rect: Rect, radii: [f32; 4], paint: &Paint) {
661 let Some(bounds) = self.clipped_device_bounds(&rect.expand(paint.mask_padding())) else {
662 return;
663 };
664 let slot = self.take_draw_slot(bounds, supports_opacity(paint));
665 self.ops.push(Op::RRectBlur {
666 rect,
667 radii: valo_geometry::constrain_radii(&rect, radii),
668 paint: paint.clone(),
669 bounds,
670 slot,
671 });
672 }
673
674 fn note_backdrop_group(&mut self, key: u64, bounds: Rect, filter: ImageFilter) {
675 match self.backdrop_groups.iter_mut().find(|g| g.key == key) {
676 Some(group) => {
677 group.union_bounds = group.union_bounds.union(&bounds);
678 if group.filter.as_ref() != Some(&filter) {
679 group.filter = None; }
681 }
682 None => self.backdrop_groups.push(crate::BackdropGroup {
683 key,
684 union_bounds: bounds,
685 filter: Some(filter),
686 }),
687 }
688 }
689
690 fn clipped_device_bounds(&self, local: &Rect) -> Option<Rect> {
693 let device = self.top().transform.map_rect(local);
694 match self.top().clip {
695 None => Some(device),
696 Some(clip) => device.intersect(&clip),
697 }
698 }
699
700 fn shrink_clip(&mut self, op: ClipOp, shape_bounds: Rect) {
703 if op == ClipOp::Difference {
704 return;
705 }
706 let top = self.top_mut();
707 top.clip = Some(match top.clip {
708 None => shape_bounds,
709 Some(c) => c.intersect(&shape_bounds).unwrap_or_default(), });
711 }
712
713 fn expire_scope_clips(&mut self) {
717 let pending = self.pending_clips.pop().expect("scope stack never empty");
718 if !pending.is_empty() {
719 self.slots += 1;
720 for idx in pending {
721 let Op::ClipPath { expiry_slot, .. } = &mut self.ops[idx] else {
722 unreachable!("pending_clips indexes only ClipPath ops");
723 };
724 *expiry_slot = self.slots;
725 }
726 }
727 if self.pending_clips.is_empty() {
728 self.pending_clips.push(Vec::new()); }
730 }
731
732 fn take_draw_slot(&mut self, device_bounds: Rect, supports_opacity: bool) -> u32 {
733 self.slots += 1;
734 self.draw_count += 1;
735 self.union_bounds(device_bounds);
736 self.note_layer_child(device_bounds, supports_opacity);
737 self.slots
738 }
739
740 fn union_bounds(&mut self, b: Rect) {
741 self.bounds = Some(match self.bounds {
742 Some(cur) => cur.union(&b),
743 None => b,
744 });
745 }
746
747 fn note_layer_child(&mut self, bounds: Rect, supports_opacity: bool) {
751 let Some(layer) = self.layers.last_mut() else {
752 return;
753 };
754 layer.bounds = Some(match layer.bounds {
755 Some(cur) => cur.union(&bounds),
756 None => bounds,
757 });
758 if !layer.compatible {
759 return;
760 }
761 if !supports_opacity {
762 layer.compatible = false;
763 return;
764 }
765 if layer
766 .child_bounds
767 .iter()
768 .any(|prior| prior.intersects(&bounds))
769 {
770 layer.compatible = false;
771 return;
772 }
773 layer.child_bounds.push(bounds);
774 }
775}
776
777fn supports_opacity(paint: &Paint) -> bool {
781 paint.color_filter.is_none()
786 && paint.effective_image_filter().is_none()
787 && matches!(
788 paint.blend_mode,
789 crate::BlendMode::SrcOver | crate::BlendMode::Plus
790 )
791}
792
793fn circular_radii(radii: [[f32; 2]; 4]) -> Option<[f32; 4]> {
798 radii
799 .iter()
800 .all(|[x, y]| x == y)
801 .then(|| radii.map(|[x, _]| x))
802}
803
804fn positive_rect(rect: Rect) -> Rect {
807 let x = if rect.width < 0.0 {
808 rect.x + rect.width
809 } else {
810 rect.x
811 };
812 let y = if rect.height < 0.0 {
813 rect.y + rect.height
814 } else {
815 rect.y
816 };
817 Rect::new(x, y, rect.width.abs(), rect.height.abs())
818}
819
820fn is_analytic_blur(paint: &Paint) -> bool {
821 paint.mask_blur.is_some()
822 && paint.shader.is_none()
823 && paint.color_filter.is_none()
827 && paint.effective_image_filter().is_none()
828 && matches!(paint.style, crate::PaintStyle::Fill)
829}
830
831fn rect_path(r: Rect) -> Arc<Path> {
832 let mut p = PathBuilder::new();
833 p.rect(r);
834 p.build()
835}
836
837#[cfg(test)]
838mod tests {
839 use super::*;
840 use crate::BlendMode;
841 use valo_geometry::Color;
842
843 #[test]
844 fn save_count_tracks_saves_layers_and_restores() {
845 let mut builder = DisplayListBuilder::new();
846 assert_eq!(builder.save_count(), 1);
847
848 builder.save();
849 assert_eq!(builder.save_count(), 2);
850
851 builder.save_layer(None, &Paint::default());
852 assert_eq!(builder.save_count(), 3);
853
854 builder.restore();
855 assert_eq!(builder.save_count(), 2);
856 builder.restore();
857 assert_eq!(builder.save_count(), 1);
858 }
859
860 #[test]
861 fn rounded_rects_normalize_inverted_edges_like_flutter() {
862 let mut builder = DisplayListBuilder::new();
863 builder.draw_rrect(
864 Rect::from_ltrb(-1.0, -10.0 / 3.0, 1.0, -10.0),
865 1.0,
866 &Paint::from_color(Color::WHITE),
867 );
868
869 let list = builder.build();
870 let Op::DrawPath { path, .. } = &list.ops()[0] else {
871 panic!("rounded rectangle should record as a path");
872 };
873 assert_eq!(
874 path.bounds(),
875 Rect::from_ltrb(-1.0, -10.0, 1.0, -10.0 / 3.0)
876 );
877 }
878 fn red() -> Paint {
879 Paint::from_color(Color::rgb(1.0, 0.0, 0.0))
880 }
881
882 fn alpha_layer(a: f32) -> Paint {
883 Paint::from_color(Color::rgba(0.0, 0.0, 0.0, a))
884 }
885
886 fn find_clip(dl: &DisplayList) -> (&Op, u32) {
887 for op in dl.ops() {
888 if let Op::ClipPath { expiry_slot, .. } = op {
889 return (op, *expiry_slot);
890 }
891 }
892 panic!("no clip recorded");
893 }
894
895 fn layer_facts(dl: &DisplayList) -> Vec<(Rect, u32, u32, bool)> {
899 dl.ops()
900 .iter()
901 .filter_map(|op| match op {
902 Op::SaveLayer {
903 scope_bounds,
904 base_slot,
905 composite_slot,
906 can_elide,
907 ..
908 } => Some((*scope_bounds, *base_slot, *composite_slot, *can_elide)),
909 _ => None,
910 })
911 .collect()
912 }
913
914 fn find_layer(dl: &DisplayList) -> (Rect, u32, u32, bool) {
915 *layer_facts(dl).first().expect("no layer recorded")
916 }
917
918 #[test]
919 fn oracle_bounds_follow_transforms() {
920 let mut b = DisplayListBuilder::new();
921 b.save();
922 b.translate(100.0, 50.0);
923 b.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red());
924 b.restore();
925 let dl = b.build();
926 assert_eq!(dl.bounds(), Some(Rect::new(100.0, 50.0, 10.0, 10.0)));
927 assert_eq!(dl.draw_count(), 1);
928 assert_eq!(dl.depth_slots(), 1);
929 }
930
931 #[test]
932 fn clip_shrinks_recorded_draw_bounds() {
933 let mut b = DisplayListBuilder::new();
934 b.save();
935 b.clip_rect(Rect::new(0.0, 0.0, 50.0, 50.0), ClipOp::Intersect);
936 b.draw_rect(Rect::new(25.0, 25.0, 100.0, 100.0), &red());
937 b.restore();
938 let dl = b.build();
939 assert_eq!(dl.bounds(), Some(Rect::new(25.0, 25.0, 25.0, 25.0)));
940 }
941
942 #[test]
943 fn fully_clipped_draw_is_dropped() {
944 let mut b = DisplayListBuilder::new();
945 b.save();
946 b.clip_rect(Rect::new(0.0, 0.0, 10.0, 10.0), ClipOp::Intersect);
947 b.draw_rect(Rect::new(500.0, 500.0, 10.0, 10.0), &red());
948 b.restore();
949 let dl = b.build();
950 assert_eq!(dl.draw_count(), 0);
951 }
952
953 #[test]
954 fn clip_expiry_is_the_restore_slot() {
955 let mut b = DisplayListBuilder::new();
956 b.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red()); b.save();
958 b.clip_rect(Rect::new(0.0, 0.0, 50.0, 50.0), ClipOp::Intersect);
959 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();
963 let (_, expiry) = find_clip(&dl);
964 assert_eq!(expiry, 3);
965 assert_eq!(dl.depth_slots(), 4);
966 }
967
968 #[test]
969 fn root_clip_expires_at_end_of_list() {
970 let mut b = DisplayListBuilder::new();
971 b.clip_rect(Rect::new(0.0, 0.0, 50.0, 50.0), ClipOp::Intersect);
972 b.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red()); let dl = b.build();
974 let (_, expiry) = find_clip(&dl);
975 assert_eq!(expiry, 2, "root clips expire at the virtual end slot");
976 assert_eq!(dl.depth_slots(), 2);
977 }
978
979 #[test]
980 fn difference_clip_keeps_bounds_conservative() {
981 let mut b = DisplayListBuilder::new();
982 b.save();
983 b.clip_rect(Rect::new(0.0, 0.0, 50.0, 50.0), ClipOp::Difference);
984 b.draw_rect(Rect::new(0.0, 0.0, 100.0, 100.0), &red());
985 b.restore();
986 let dl = b.build();
987 assert_eq!(dl.bounds(), Some(Rect::new(0.0, 0.0, 100.0, 100.0)));
988 }
989
990 #[test]
991 fn nested_list_folds_oracle_and_offsets_slots() {
992 let mut inner = DisplayListBuilder::new();
993 inner.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red());
994 inner.draw_rect(Rect::new(20.0, 0.0, 10.0, 10.0), &red());
995 let inner = Arc::new(inner.build());
996
997 let mut outer = DisplayListBuilder::new();
998 outer.draw_rect(Rect::new(0.0, 0.0, 5.0, 5.0), &red()); outer.translate(5.0, 5.0);
1000 outer.draw_display_list(&inner); outer.draw_rect(Rect::new(0.0, 0.0, 5.0, 5.0), &red()); let outer = outer.build();
1003
1004 assert_eq!(outer.draw_count(), 4);
1005 assert_eq!(outer.depth_slots(), 4);
1006 let base = outer
1007 .ops()
1008 .iter()
1009 .find_map(|op| match op {
1010 Op::DrawDisplayList { base_slot, .. } => Some(*base_slot),
1011 _ => None,
1012 })
1013 .unwrap();
1014 assert_eq!(base, 1);
1015 }
1016
1017 #[test]
1018 fn nop_draws_are_dropped() {
1019 let mut b = DisplayListBuilder::new();
1020 b.draw_rect(Rect::new(0.0, 0.0, 0.0, 10.0), &red()); b.draw_rect(
1022 Rect::new(0.0, 0.0, 10.0, 10.0),
1023 &Paint {
1024 color: Color::TRANSPARENT,
1025 blend_mode: BlendMode::SrcOver,
1026 ..Default::default()
1027 },
1028 );
1029 let dl = b.build();
1030 assert_eq!(dl.ops().len(), 0);
1031 assert_eq!(dl.bounds(), None);
1032 }
1033
1034 #[test]
1037 fn layer_oracle_bounds_and_slots() {
1038 let mut b = DisplayListBuilder::new();
1039 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();
1046
1047 let (bounds, base_slot, composite_slot, can_elide) = find_layer(&dl);
1048 assert_eq!(bounds, Rect::new(20.0, 20.0, 70.0, 30.0));
1049 assert_eq!(base_slot, 1, "scope opened after one parent draw");
1050 assert_eq!(composite_slot, 4, "children keep the global line");
1051 assert!(
1052 can_elide,
1053 "disjoint SrcOver children + alpha-only composite"
1054 );
1055 assert_eq!(
1056 dl.depth_slots(),
1057 5,
1058 "one global depth line (Impeller's current_depth_)"
1059 );
1060 assert_eq!(dl.draw_count(), 5, "4 rects + the composite");
1061 }
1062
1063 #[test]
1064 fn overlapping_children_forfeit_elision() {
1065 let mut b = DisplayListBuilder::new();
1066 b.save_layer(None, &alpha_layer(0.5));
1067 b.draw_rect(Rect::new(0.0, 0.0, 30.0, 30.0), &red());
1068 b.draw_rect(Rect::new(10.0, 10.0, 30.0, 30.0), &red()); b.restore();
1070 let (_, _, _, can_elide) = find_layer(&b.build());
1071 assert!(!can_elide);
1072 }
1073
1074 #[test]
1075 fn advanced_blend_composite_forfeits_elision() {
1076 let mut b = DisplayListBuilder::new();
1077 let paint = Paint {
1078 color: Color::rgba(0.0, 0.0, 0.0, 0.5),
1079 blend_mode: BlendMode::Multiply,
1080 ..Default::default()
1081 };
1082 b.save_layer(None, &paint);
1083 b.draw_rect(Rect::new(0.0, 0.0, 30.0, 30.0), &red());
1084 b.restore();
1085 let (_, _, _, can_elide) = find_layer(&b.build());
1086 assert!(!can_elide);
1087 }
1088
1089 #[test]
1090 fn destructive_layer_composite_floods_the_active_clip() {
1091 let mut b = DisplayListBuilder::new();
1092 b.clip_rect(Rect::new(4.0, 6.0, 80.0, 60.0), ClipOp::Intersect);
1093 b.save_layer(
1094 None,
1095 &Paint {
1096 blend_mode: BlendMode::SrcIn,
1097 ..Default::default()
1098 },
1099 );
1100 b.draw_rect(Rect::new(20.0, 20.0, 10.0, 10.0), &red());
1101 b.restore();
1102 let (bounds, ..) = find_layer(&b.build());
1103 assert_eq!(bounds, Rect::new(4.0, 6.0, 80.0, 60.0));
1104 }
1105
1106 #[test]
1107 fn clip_inside_layer_keeps_elision() {
1108 let mut b = DisplayListBuilder::new();
1109 b.save_layer(None, &alpha_layer(0.5));
1110 b.clip_rect(Rect::new(0.0, 0.0, 50.0, 50.0), ClipOp::Intersect);
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);
1117 }
1118
1119 #[test]
1120 fn bounds_hint_crops_the_scope() {
1121 let mut b = DisplayListBuilder::new();
1122 b.save_layer(Some(Rect::new(0.0, 0.0, 40.0, 40.0)), &alpha_layer(0.5));
1123 b.draw_rect(Rect::new(20.0, 20.0, 100.0, 100.0), &red());
1124 b.restore();
1125 let (bounds, ..) = find_layer(&b.build());
1126 assert_eq!(bounds, Rect::new(20.0, 20.0, 20.0, 20.0));
1127 }
1128
1129 #[test]
1130 fn clips_inside_layers_expire_within_the_scope_span() {
1131 let mut b = DisplayListBuilder::new();
1132 b.save_layer(None, &alpha_layer(0.5)); b.save();
1134 b.clip_rect(Rect::new(0.0, 0.0, 50.0, 50.0), ClipOp::Intersect);
1135 b.draw_rect(Rect::new(0.0, 0.0, 30.0, 30.0), &red()); b.restore(); b.restore(); let dl = b.build();
1139 let (_, expiry) = find_clip(&dl);
1140 assert_eq!(expiry, 2, "expiry sits inside the layer's span");
1141 let (_, base_slot, composite_slot, _) = find_layer(&dl);
1142 assert_eq!((base_slot, composite_slot), (0, 3));
1143 }
1144
1145 #[test]
1148 fn solid_mask_blur_records_the_analytic_op() {
1149 let mut b = DisplayListBuilder::new();
1150 let paint = Paint {
1151 mask_blur: Some(crate::MaskBlur::new(4.0)),
1152 ..red()
1153 };
1154 b.draw_rect(Rect::new(20.0, 20.0, 40.0, 40.0), &paint);
1155 b.draw_rrect(Rect::new(100.0, 20.0, 40.0, 40.0), 8.0, &paint);
1156 let dl = b.build();
1157 let blurs: Vec<_> = dl
1158 .ops()
1159 .iter()
1160 .filter_map(|op| match op {
1161 Op::RRectBlur { radii, bounds, .. } => Some((*radii, *bounds)),
1162 _ => None,
1163 })
1164 .collect();
1165 assert_eq!(blurs.len(), 2);
1166 assert_eq!(blurs[0].0, [0.0; 4]);
1167 assert_eq!(blurs[1].0, [8.0; 4]);
1168 assert_eq!(blurs[0].1, Rect::new(8.0, 8.0, 64.0, 64.0));
1170 }
1171
1172 #[test]
1173 fn shader_mask_blur_stays_general_but_pads_bounds() {
1174 let mut b = DisplayListBuilder::new();
1175 let paint = Paint {
1176 mask_blur: Some(crate::MaskBlur::new(2.0)),
1177 shader: Some(crate::Shader::linear(
1178 valo_geometry::Point::new(0.0, 0.0),
1179 valo_geometry::Point::new(10.0, 0.0),
1180 Color::BLACK,
1181 Color::WHITE,
1182 )),
1183 color: Color::WHITE,
1184 ..Default::default()
1185 };
1186 b.draw_rect(Rect::new(10.0, 10.0, 20.0, 20.0), &paint);
1187 let dl = b.build();
1188 let Op::DrawRect { bounds, .. } = &dl.ops()[0] else {
1189 panic!("shader paints keep the general op");
1190 };
1191 assert_eq!(*bounds, Rect::new(4.0, 4.0, 32.0, 32.0));
1192 }
1193
1194 #[test]
1195 fn hinted_layer_forfeits_elision() {
1196 let mut b = DisplayListBuilder::new();
1197 b.save_layer(Some(Rect::new(0.0, 0.0, 40.0, 40.0)), &alpha_layer(0.5));
1198 b.draw_rect(Rect::new(0.0, 0.0, 30.0, 30.0), &red());
1199 b.restore();
1200 let (_, _, _, can_elide) = find_layer(&b.build());
1201 assert!(!can_elide, "the hint is a crop; eliding would un-crop it");
1202 }
1203
1204 fn glass(b: &mut DisplayListBuilder, rect: Rect, sigma: f32, key: Option<u64>) {
1208 b.save_layer_backdrop(
1209 Some(rect),
1210 &Paint::default(),
1211 Backdrop {
1212 filter: ImageFilter::blur(sigma, sigma),
1213 shared_key: key,
1214 },
1215 );
1216 b.restore();
1217 }
1218
1219 #[test]
1220 fn shared_backdrops_group_by_key() {
1221 let mut b = DisplayListBuilder::new();
1222 glass(&mut b, Rect::new(0.0, 0.0, 50.0, 50.0), 8.0, Some(7));
1223 glass(&mut b, Rect::new(100.0, 0.0, 50.0, 50.0), 8.0, Some(7));
1224 glass(&mut b, Rect::new(0.0, 100.0, 50.0, 50.0), 8.0, None);
1225 let dl = b.build();
1226 let group = dl.backdrop_group(7).expect("key 7 recorded");
1227 assert_eq!(group.union_bounds, Rect::new(0.0, 0.0, 150.0, 50.0));
1229 assert_eq!(
1230 group.filter,
1231 Some(ImageFilter::blur(8.0, 8.0)),
1232 "one σ across the key: shareable"
1233 );
1234 assert_eq!(dl.draw_count(), 3, "each layer's composite is a draw");
1235 assert_eq!(dl.depth_slots(), 3);
1236 }
1237
1238 #[test]
1239 fn mixed_sigma_under_one_key_clears_the_shared_sigma() {
1240 let mut b = DisplayListBuilder::new();
1241 glass(&mut b, Rect::new(0.0, 0.0, 50.0, 50.0), 4.0, Some(7));
1242 glass(&mut b, Rect::new(100.0, 0.0, 50.0, 50.0), 12.0, Some(7));
1243 let dl = b.build();
1244 let group = dl.backdrop_group(7).expect("key 7 recorded");
1245 assert_eq!(group.filter, None, "disagreeing σ cannot share one blur");
1246 }
1247
1248 #[test]
1249 fn opacity_group_elides_over_a_backdrop_layer() {
1250 let mut b = DisplayListBuilder::new();
1251 b.save_layer(None, &alpha_layer(0.5));
1252 glass(&mut b, Rect::new(0.0, 0.0, 50.0, 50.0), 4.0, None);
1253 b.restore();
1254 let layers = layer_facts(&b.build());
1255 assert_eq!(layers.len(), 2, "the opacity group and the glass inside it");
1256 assert!(
1257 layers[0].3,
1258 "the group's alpha lands on the glass composite — the whole point \
1259 of backdrop-as-a-layer-property: glass keeps blurring while the \
1260 group fades"
1261 );
1262 assert!(!layers[1].3, "the glass itself needs a texture to seed");
1263 }
1264
1265 #[test]
1271 fn a_clip_does_not_forfeit_elision_around_glass() {
1272 let mut b = DisplayListBuilder::new();
1273 b.save_layer(None, &alpha_layer(0.5));
1274 b.save();
1275 let mut clip = PathBuilder::new();
1276 clip.rect(Rect::new(0.0, 0.0, 60.0, 60.0));
1277 b.clip_path(&clip.build(), FillRule::NonZero, ClipOp::Intersect);
1278 glass(&mut b, Rect::new(0.0, 0.0, 50.0, 50.0), 4.0, None);
1279 b.restore();
1280 b.restore();
1281 let layers = layer_facts(&b.build());
1282 assert_eq!(layers.len(), 2);
1283 assert!(layers[0].3, "the clipped fade still elides");
1284 assert!(!layers[1].3);
1285 }
1286
1287 #[test]
1288 fn backdrop_reads_count_unshared_and_nested() {
1289 let mut child = DisplayListBuilder::new();
1290 glass(&mut child, Rect::new(0.0, 0.0, 50.0, 50.0), 4.0, None);
1291 let child = Arc::new(child.build());
1292 assert_eq!(child.backdrop_reads(), 1, "unshared reads count too");
1293
1294 let mut parent = DisplayListBuilder::new();
1295 glass(&mut parent, Rect::new(0.0, 0.0, 50.0, 50.0), 8.0, Some(7));
1296 parent.draw_display_list(&child);
1297 let parent = parent.build();
1298 assert_eq!(parent.backdrop_reads(), 2, "own layer + the nested list's");
1299
1300 let mut clean = DisplayListBuilder::new();
1301 clean.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red());
1302 assert_eq!(clean.build().backdrop_reads(), 0);
1303 }
1304
1305 #[cfg(feature = "serde")]
1306 #[test]
1307 fn serde_dump_is_readable_json() {
1308 let mut b = DisplayListBuilder::new();
1311 b.translate(1.0, 2.0);
1312 b.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red());
1313 let dl = b.build();
1314 let json: serde_json::Value = serde_json::to_value(&dl).unwrap();
1315 assert_eq!(json["ops"].as_array().unwrap().len(), dl.ops().len());
1316 assert!(json["ops"][1]["DrawRect"]["slot"].is_number());
1317 }
1318}