1use std::sync::Arc;
2
3use valo_geometry::{FillRule, Matrix, Path, PathBuilder, Rect};
4
5use crate::{ClipOp, DisplayList, Image, 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, Copy, Debug)]
42pub struct Backdrop {
43 pub sigma: f32,
47 pub shared_key: Option<u64>,
51}
52
53impl Backdrop {
54 pub fn blur(sigma: f32) -> Self {
56 Self {
57 sigma,
58 shared_key: None,
59 }
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<(f32, Option<u64>)>,
96 hinted: bool,
101}
102
103impl Default for DisplayListBuilder {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109impl DisplayListBuilder {
110 pub fn new() -> Self {
112 Self {
113 ops: Vec::new(),
114 scopes: vec![Scope {
115 transform: Matrix::IDENTITY,
116 clip: None,
117 is_layer: false,
118 }],
119 layers: Vec::new(),
120 backdrop_groups: Vec::new(),
121 pending_clips: vec![Vec::new()],
122 slots: 0,
123 bounds: None,
124 draw_count: 0,
125 backdrop_reads: 0,
126 }
127 }
128
129 pub fn save(&mut self) {
133 self.scopes.push(Scope {
134 is_layer: false,
135 ..*self.top()
136 });
137 self.pending_clips.push(Vec::new());
138 self.ops.push(Op::Save);
139 }
140
141 pub fn save_count(&self) -> usize {
147 self.scopes.len()
148 }
149
150 pub fn save_layer(&mut self, bounds_hint: Option<Rect>, paint: &Paint) {
156 self.save_layer_inner(bounds_hint, paint, None, None);
157 }
158
159 pub fn save_layer_mask(&mut self, bounds_hint: Option<Rect>, kind: MaskKind) {
165 let paint = Paint {
166 blend_mode: crate::BlendMode::DstIn,
167 ..Paint::default()
168 };
169 self.save_layer_inner(bounds_hint, &paint, Some(kind), None);
170 }
171
172 pub fn save_layer_backdrop(
182 &mut self,
183 bounds_hint: Option<Rect>,
184 paint: &Paint,
185 backdrop: Backdrop,
186 ) {
187 let backdrop = (backdrop.sigma > 0.0).then_some((backdrop.sigma, backdrop.shared_key));
190 self.save_layer_inner(bounds_hint, paint, None, backdrop);
191 }
192
193 fn save_layer_inner(
194 &mut self,
195 bounds_hint: Option<Rect>,
196 paint: &Paint,
197 mask_composite: Option<MaskKind>,
198 backdrop: Option<(f32, Option<u64>)>,
199 ) {
200 let device_hint = bounds_hint.map(|h| self.top().transform.map_rect(&h));
201 let mut scope = Scope {
202 is_layer: true,
203 ..*self.top()
204 };
205 if let Some(h) = device_hint {
208 scope.clip = Some(match scope.clip {
209 None => h,
210 Some(c) => c.intersect(&h).unwrap_or_default(),
211 });
212 }
213 let floods_scope = paint.blend_mode.is_destructive()
217 || paint
218 .color_filter
219 .is_some_and(|filter| filter.modifies_transparent_black())
220 || paint
221 .image_filter
222 .as_ref()
223 .is_some_and(|filter| filter.modifies_transparent_black())
224 || backdrop.is_some();
229 let flooded_bounds = floods_scope.then(|| scope.clip.unwrap_or(Rect::EVERYTHING));
230 if backdrop.is_some() {
231 self.backdrop_reads += 1;
232 }
233 self.scopes.push(scope);
234 self.pending_clips.push(Vec::new());
235 self.layers.push(LayerScope {
236 op_index: self.ops.len(),
237 bounds: flooded_bounds,
238 child_bounds: Vec::new(),
239 compatible: true,
240 blur_pad: paint.device_effect_padding(&self.top().transform),
243 backdrop,
244 hinted: device_hint.is_some(),
245 });
246 self.ops.push(Op::SaveLayer {
249 paint: paint.clone(),
250 mask_composite,
251 scope_bounds: Rect::default(), base_slot: self.slots,
253 composite_slot: 0,
254 can_elide: false,
255 backdrop_sigma: backdrop.map(|(sigma, _)| sigma),
256 backdrop_key: backdrop.and_then(|(_, key)| key),
257 });
258 }
259
260 pub fn restore(&mut self) {
264 if self.scopes.len() == 1 {
265 debug_assert!(false, "restore() without matching save()");
266 return;
267 }
268 let scope = self.scopes.pop().expect("checked above");
269 self.expire_scope_clips(); if scope.is_layer {
271 self.close_layer();
272 }
273 self.ops.push(Op::Restore);
274 }
275
276 pub fn translate(&mut self, tx: f32, ty: f32) {
278 self.concat(&Matrix::translation(tx, ty));
279 }
280
281 pub fn scale(&mut self, sx: f32, sy: f32) {
283 self.concat(&Matrix::scale(sx, sy));
284 }
285
286 pub fn rotate(&mut self, radians: f32) {
290 self.concat(&Matrix::rotation(radians));
291 }
292
293 pub fn concat(&mut self, local: &Matrix) {
295 let top = self.top_mut();
296 top.transform = top.transform.then(local);
297 self.ops.push(Op::Transform(*local));
298 }
299
300 pub fn clip_rect(&mut self, rect: impl Into<Rect>, op: ClipOp) {
304 let rect = rect.into();
305 self.clip_path(&rect_path(rect), FillRule::NonZero, op);
306 }
307
308 pub fn clip_rrect(&mut self, rect: impl Into<Rect>, radius: f32, op: ClipOp) {
310 let rect = rect.into();
311 self.clip_rrect_radii(rect, [radius; 4], op);
312 }
313
314 pub fn clip_rrect_radii(&mut self, rect: impl Into<Rect>, radii: [f32; 4], op: ClipOp) {
318 let rect = positive_rect(rect.into());
319 let mut p = PathBuilder::new();
320 p.rrect_radii(rect, radii);
321 self.clip_path(&p.build(), FillRule::NonZero, op);
322 }
323
324 pub fn clip_rrect_radii_elliptical(
328 &mut self,
329 rect: impl Into<Rect>,
330 radii: [[f32; 2]; 4],
331 op: ClipOp,
332 ) {
333 let rect = positive_rect(rect.into());
334 if let Some(circular) = circular_radii(radii) {
335 return self.clip_rrect_radii(rect, circular, op);
336 }
337 let mut p = PathBuilder::new();
338 p.rrect_radii_elliptical(rect, radii);
339 self.clip_path(&p.build(), FillRule::NonZero, op);
340 }
341
342 pub fn clip_path(&mut self, path: &Arc<Path>, fill_rule: FillRule, op: ClipOp) {
352 let bounds = self.top().transform.map_rect(&path.bounds());
353 self.shrink_clip(op, bounds);
354 self.pending_clips
355 .last_mut()
356 .expect("root scope")
357 .push(self.ops.len());
358 self.ops.push(Op::ClipPath {
359 path: Arc::clone(path),
360 fill_rule,
361 op,
362 expiry_slot: 0, });
364 }
365
366 pub fn draw_rect(&mut self, rect: impl Into<Rect>, paint: &Paint) {
370 let rect = rect.into();
371 if paint.is_nop() {
372 return;
373 }
374 if matches!(paint.style, crate::PaintStyle::Stroke(_)) {
375 return self.draw_path(&rect_path(rect), FillRule::NonZero, paint);
378 }
379 if rect.is_empty() {
380 return;
381 }
382 if is_analytic_blur(paint) {
383 self.record_rrect_blur(rect, [0.0; 4], paint);
384 return;
385 }
386 let Some(bounds) = self.clipped_device_bounds(&paint.effect_bounds(rect)) else {
387 return; };
389 let slot = self.take_draw_slot(bounds, supports_opacity(paint));
390 self.ops.push(Op::DrawRect {
391 rect,
392 paint: paint.clone(),
393 bounds,
394 slot,
395 });
396 }
397
398 pub fn draw_path(&mut self, path: &Arc<Path>, fill_rule: FillRule, paint: &Paint) {
400 if path.is_empty() || paint.is_nop() {
401 return;
402 }
403 let scale = self.top().transform.max_scale();
404 let local = paint.effect_bounds(path.bounds().expand(paint.stroke_padding_at_scale(scale)));
405 let Some(bounds) = self.clipped_device_bounds(&local) else {
406 return;
407 };
408 let slot = self.take_draw_slot(bounds, supports_opacity(paint));
409 self.ops.push(Op::DrawPath {
410 path: Arc::clone(path),
411 fill_rule,
412 paint: paint.clone(),
413 bounds,
414 slot,
415 });
416 }
417
418 pub fn draw_circle(
420 &mut self,
421 center: impl Into<valo_geometry::Point>,
422 radius: f32,
423 paint: &Paint,
424 ) {
425 let mut p = PathBuilder::new();
426 p.circle(center, radius);
427 self.draw_path(&p.build(), FillRule::NonZero, paint);
428 }
429
430 pub fn draw_rrect(&mut self, rect: impl Into<Rect>, radius: f32, paint: &Paint) {
432 let rect = rect.into();
433 self.draw_rrect_radii(rect, [radius; 4], paint);
434 }
435
436 pub fn draw_rrect_radii(&mut self, rect: impl Into<Rect>, radii: [f32; 4], paint: &Paint) {
440 let rect = positive_rect(rect.into());
441 if rect.is_empty() || paint.is_nop() {
442 return;
443 }
444 if is_analytic_blur(paint) {
445 self.record_rrect_blur(rect, radii, paint);
446 return;
447 }
448 let mut p = PathBuilder::new();
449 p.rrect_radii(rect, radii);
450 self.draw_path(&p.build(), FillRule::NonZero, paint);
451 }
452
453 pub fn draw_rrect_radii_elliptical(
457 &mut self,
458 rect: impl Into<Rect>,
459 radii: [[f32; 2]; 4],
460 paint: &Paint,
461 ) {
462 let rect = positive_rect(rect.into());
463 if let Some(circular) = circular_radii(radii) {
464 return self.draw_rrect_radii(rect, circular, paint);
465 }
466 if rect.is_empty() || paint.is_nop() {
467 return;
468 }
469 let mut p = PathBuilder::new();
470 p.rrect_radii_elliptical(rect, radii);
471 self.draw_path(&p.build(), FillRule::NonZero, paint);
472 }
473
474 pub fn draw_image(&mut self, image: &Image, dst: Rect, paint: &Paint) {
478 let src = Rect::new(0.0, 0.0, image.width(), image.height());
479 self.draw_image_rect(image, src, dst, Sampling::default(), paint);
480 }
481
482 pub fn draw_image_rect(
487 &mut self,
488 image: &Image,
489 src: Rect,
490 dst: Rect,
491 sampling: Sampling,
492 paint: &Paint,
493 ) {
494 if dst.is_empty() || src.is_empty() || paint.is_nop() {
495 return;
496 }
497 let Some(bounds) = self.clipped_device_bounds(&paint.effect_bounds(dst)) else {
498 return;
499 };
500 let slot = self.take_draw_slot(bounds, supports_opacity(paint));
501 self.ops.push(Op::DrawImage {
502 image: image.clone(),
503 src,
504 dst,
505 sampling,
506 paint: paint.clone(),
507 bounds,
508 slot,
509 });
510 }
511
512 pub fn draw_glyph_run(
517 &mut self,
518 font: std::sync::Arc<valo_text::Font>,
519 size: f32,
520 paint: &Paint,
521 glyphs: Arc<Vec<crate::GlyphPos>>,
522 local_bounds: Rect,
523 ) {
524 if glyphs.is_empty() || paint.is_nop() {
525 return;
526 }
527 let scale = self.top().transform.max_scale();
528 let padded = paint.effect_bounds(local_bounds.expand(paint.stroke_padding_at_scale(scale)));
529 let Some(bounds) = self.clipped_device_bounds(&padded) else {
530 return;
531 };
532 let distributes = supports_opacity(paint) && paint.shader.is_none();
535 let slot = self.take_draw_slot(bounds, distributes);
536 self.ops.push(Op::GlyphRun {
537 font,
538 size,
539 paint: paint.clone(),
540 glyphs,
541 bounds,
542 slot,
543 });
544 }
545
546 pub fn draw_display_list(&mut self, list: &Arc<DisplayList>) {
548 self.embed_display_list(list, false);
549 }
550
551 pub fn draw_display_list_cached(&mut self, list: &Arc<DisplayList>) {
556 self.embed_display_list(list, true);
557 }
558
559 fn embed_display_list(&mut self, list: &Arc<DisplayList>, cache: bool) {
560 let Some(child_bounds) = list.bounds() else {
561 return; };
563 let Some(bounds) = self.clipped_device_bounds(&child_bounds) else {
564 return;
565 };
566 let base_slot = self.slots;
567 self.slots += list.depth_slots();
568 self.draw_count += list.draw_count();
569 self.backdrop_reads += list.backdrop_reads();
570 self.union_bounds(bounds);
571 self.note_layer_child(bounds, false);
573 self.ops.push(Op::DrawDisplayList {
574 list: Arc::clone(list),
575 bounds,
576 base_slot,
577 cache,
578 });
579 }
580
581 pub fn build(mut self) -> DisplayList {
587 while self.scopes.len() > 1 {
590 self.restore();
591 }
592 self.expire_scope_clips(); DisplayList::new(
594 self.ops,
595 self.bounds,
596 self.draw_count,
597 self.slots,
598 self.backdrop_groups,
599 self.backdrop_reads,
600 )
601 }
602
603 fn top(&self) -> &Scope {
606 self.scopes.last().expect("scope stack never empty")
607 }
608
609 fn top_mut(&mut self) -> &mut Scope {
610 self.scopes.last_mut().expect("scope stack never empty")
611 }
612
613 fn close_layer(&mut self) {
618 let layer = self.layers.pop().expect("is_layer scope had a LayerScope");
619 self.slots += 1; let mut scope_bounds = layer.bounds.unwrap_or_default();
621 if layer.blur_pad > 0.0 && !scope_bounds.is_empty() {
622 scope_bounds = scope_bounds.expand(layer.blur_pad);
623 }
624
625 let Op::SaveLayer {
626 paint,
627 mask_composite: _,
628 scope_bounds: sb,
629 base_slot: _,
630 composite_slot,
631 can_elide,
632 ..
633 } = &mut self.ops[layer.op_index]
634 else {
635 unreachable!("LayerScope.op_index always points at SaveLayer");
636 };
637 *sb = scope_bounds;
638 *composite_slot = self.slots;
639 *can_elide = layer.compatible
642 && paint.is_opacity_only()
643 && layer.backdrop.is_none()
644 && !layer.hinted;
645
646 let supports = paint.blend_mode == crate::BlendMode::SrcOver;
651 if let Some((sigma, Some(key))) = layer.backdrop {
652 self.note_backdrop_group(key, scope_bounds, sigma);
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, sigma: f32) {
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.sigma != Some(sigma) {
679 group.sigma = None; }
681 }
682 None => self.backdrop_groups.push(crate::BackdropGroup {
683 key,
684 union_bounds: bounds,
685 sigma: Some(sigma),
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 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!(group.sigma, Some(8.0), "one σ across the key: shareable");
1230 assert_eq!(dl.draw_count(), 3, "each layer's composite is a draw");
1231 assert_eq!(dl.depth_slots(), 3);
1232 }
1233
1234 #[test]
1235 fn mixed_sigma_under_one_key_clears_the_shared_sigma() {
1236 let mut b = DisplayListBuilder::new();
1237 glass(&mut b, Rect::new(0.0, 0.0, 50.0, 50.0), 4.0, Some(7));
1238 glass(&mut b, Rect::new(100.0, 0.0, 50.0, 50.0), 12.0, Some(7));
1239 let dl = b.build();
1240 let group = dl.backdrop_group(7).expect("key 7 recorded");
1241 assert_eq!(group.sigma, None, "disagreeing σ cannot share one blur");
1242 }
1243
1244 #[test]
1245 fn opacity_group_elides_over_a_backdrop_layer() {
1246 let mut b = DisplayListBuilder::new();
1247 b.save_layer(None, &alpha_layer(0.5));
1248 glass(&mut b, Rect::new(0.0, 0.0, 50.0, 50.0), 4.0, None);
1249 b.restore();
1250 let layers = layer_facts(&b.build());
1251 assert_eq!(layers.len(), 2, "the opacity group and the glass inside it");
1252 assert!(
1253 layers[0].3,
1254 "the group's alpha lands on the glass composite — the whole point \
1255 of backdrop-as-a-layer-property: glass keeps blurring while the \
1256 group fades"
1257 );
1258 assert!(!layers[1].3, "the glass itself needs a texture to seed");
1259 }
1260
1261 #[test]
1267 fn a_clip_does_not_forfeit_elision_around_glass() {
1268 let mut b = DisplayListBuilder::new();
1269 b.save_layer(None, &alpha_layer(0.5));
1270 b.save();
1271 let mut clip = PathBuilder::new();
1272 clip.rect(Rect::new(0.0, 0.0, 60.0, 60.0));
1273 b.clip_path(&clip.build(), FillRule::NonZero, ClipOp::Intersect);
1274 glass(&mut b, Rect::new(0.0, 0.0, 50.0, 50.0), 4.0, None);
1275 b.restore();
1276 b.restore();
1277 let layers = layer_facts(&b.build());
1278 assert_eq!(layers.len(), 2);
1279 assert!(layers[0].3, "the clipped fade still elides");
1280 assert!(!layers[1].3);
1281 }
1282
1283 #[test]
1284 fn backdrop_reads_count_unshared_and_nested() {
1285 let mut child = DisplayListBuilder::new();
1286 glass(&mut child, Rect::new(0.0, 0.0, 50.0, 50.0), 4.0, None);
1287 let child = Arc::new(child.build());
1288 assert_eq!(child.backdrop_reads(), 1, "unshared reads count too");
1289
1290 let mut parent = DisplayListBuilder::new();
1291 glass(&mut parent, Rect::new(0.0, 0.0, 50.0, 50.0), 8.0, Some(7));
1292 parent.draw_display_list(&child);
1293 let parent = parent.build();
1294 assert_eq!(parent.backdrop_reads(), 2, "own layer + the nested list's");
1295
1296 let mut clean = DisplayListBuilder::new();
1297 clean.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red());
1298 assert_eq!(clean.build().backdrop_reads(), 0);
1299 }
1300
1301 #[cfg(feature = "serde")]
1302 #[test]
1303 fn serde_dump_is_readable_json() {
1304 let mut b = DisplayListBuilder::new();
1307 b.translate(1.0, 2.0);
1308 b.draw_rect(Rect::new(0.0, 0.0, 10.0, 10.0), &red());
1309 let dl = b.build();
1310 let json: serde_json::Value = serde_json::to_value(&dl).unwrap();
1311 assert_eq!(json["ops"].as_array().unwrap().len(), dl.ops().len());
1312 assert!(json["ops"][1]["DrawRect"]["slot"].is_number());
1313 }
1314}