ferroui_base/rendering/composition/server/dirty_rects/
single_dirty_rect_tracker.rs1use super::i_dirty_rect_tracker::{
2 full_union, pixel_rect_from_rect_unscaled, pixel_rect_to_ltrb_rect_unscaled, VisualizeRandom,
3};
4use super::{IDirtyRectCollector, IDirtyRectTracker};
5use crate::media::immutable::ImmutableSolidColorBrush;
6use crate::media::{BoxShadows, Color};
7use crate::platform::{IDrawingContextImpl, LtrbRect};
8use crate::Thickness;
9use std::cell::Cell;
10
11pub struct SingleDirtyRectTracker {
13 rect: Cell<Option<LtrbRect>>,
14 extended_rect: Cell<LtrbRect>,
15 random: VisualizeRandom,
16}
17
18impl SingleDirtyRectTracker {
19 pub fn new() -> Self {
20 Self { rect: Cell::new(None), extended_rect: Cell::new(LtrbRect::default()), random: VisualizeRandom::new() }
21 }
22}
23
24impl Default for SingleDirtyRectTracker {
25 fn default() -> Self {
26 Self::new()
27 }
28}
29
30impl IDirtyRectCollector for SingleDirtyRectTracker {
31 fn add_rect(&self, rect: LtrbRect) {
32 self.rect.set(full_union(self.rect.get(), Some(rect)));
33 }
34}
35
36impl IDirtyRectTracker for SingleDirtyRectTracker {
37 fn finalize_frame(&self, bounds: LtrbRect) {
38 self.extended_rect.set(match self.rect.get() {
39 Some(rect) => pixel_rect_to_ltrb_rect_unscaled(pixel_rect_from_rect_unscaled(
40 rect.inflate(Thickness::uniform(1.0)).intersect_or_empty(bounds),
41 )),
42 None => LtrbRect::default(),
43 });
44 }
45
46 fn begin_draw(&self, ctx: &mut dyn IDrawingContextImpl) {
47 ctx.push_clip(self.extended_rect.get().to_rect());
48 }
49
50 fn end_draw(&self, ctx: &mut dyn IDrawingContextImpl) {
51 ctx.pop_clip();
52 }
53
54 fn is_empty(&self) -> bool {
55 self.rect.get().map(|r| r.is_zero_size()).unwrap_or(true)
56 }
57
58 fn intersects(&self, rect: LtrbRect) -> bool {
59 self.extended_rect.get().intersects(rect)
60 }
61
62 fn initialize(&self, _bounds: LtrbRect) {
63 self.rect.set(None);
64 }
65
66 fn visualize(&self, context: &mut dyn IDrawingContextImpl) {
67 let brush = ImmutableSolidColorBrush::new(Color::new(
68 30,
69 self.random.next(255) as u8,
70 self.random.next(255) as u8,
71 self.random.next(255) as u8,
72 ));
73 context.draw_rectangle(Some(&brush), None, self.extended_rect.get().to_rect().into(), &BoxShadows::default());
74 }
75
76 fn is_single_dirty_rect_tracker(&self) -> bool {
77 true
78 }
79
80 fn combined_rect(&self) -> LtrbRect {
81 self.extended_rect.get()
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::super::i_dirty_rect_tracker::tests::MockDrawingContext;
88 use super::*;
89
90 const BOUNDS: LtrbRect = LtrbRect::new(0.0, 0.0, 100.0, 100.0);
91
92 #[test]
93 fn starts_empty() {
94 let tracker = SingleDirtyRectTracker::new();
95 assert!(tracker.is_empty());
96 tracker.finalize_frame(BOUNDS);
97 assert_eq!(LtrbRect::default(), tracker.combined_rect());
98 assert!(!tracker.intersects(BOUNDS));
99 }
100
101 #[test]
102 fn unions_rects_and_inflates_on_finalize() {
103 let tracker = SingleDirtyRectTracker::new();
104 tracker.initialize(BOUNDS);
105 tracker.add_rect(LtrbRect::new(10.0, 10.0, 20.0, 20.0));
106 tracker.add_rect(LtrbRect::new(40.5, 30.0, 50.5, 35.2));
107 assert!(!tracker.is_empty());
108 assert_eq!(LtrbRect::default(), tracker.combined_rect());
110
111 tracker.finalize_frame(BOUNDS);
112 assert_eq!(LtrbRect::new(9.0, 9.0, 52.0, 37.0), tracker.combined_rect());
114 assert!(tracker.intersects(LtrbRect::new(51.0, 36.0, 60.0, 60.0)));
115 assert!(!tracker.intersects(LtrbRect::new(52.0, 37.0, 60.0, 60.0)));
116 }
117
118 #[test]
119 fn finalize_clips_to_bounds() {
120 let tracker = SingleDirtyRectTracker::new();
121 tracker.add_rect(LtrbRect::new(-20.0, 90.0, 300.0, 400.0));
122 tracker.finalize_frame(BOUNDS);
123 assert_eq!(LtrbRect::new(0.0, 89.0, 100.0, 100.0), tracker.combined_rect());
124
125 let tracker = SingleDirtyRectTracker::new();
127 tracker.add_rect(LtrbRect::new(200.0, 200.0, 300.0, 300.0));
128 tracker.finalize_frame(BOUNDS);
129 assert_eq!(LtrbRect::default(), tracker.combined_rect());
130 assert!(!tracker.is_empty());
131 }
132
133 #[test]
134 fn zero_size_rect_counts_as_empty() {
135 let tracker = SingleDirtyRectTracker::new();
136 tracker.add_rect(LtrbRect::new(5.0, 5.0, 5.0, 50.0));
137 assert!(tracker.is_empty());
138 }
139
140 #[test]
141 fn initialize_resets_the_accumulated_rect_but_not_the_finalized_one() {
142 let tracker = SingleDirtyRectTracker::new();
143 tracker.add_rect(LtrbRect::new(10.0, 10.0, 20.0, 20.0));
144 tracker.finalize_frame(BOUNDS);
145 tracker.initialize(BOUNDS);
146 assert!(tracker.is_empty());
147 assert_eq!(LtrbRect::new(9.0, 9.0, 21.0, 21.0), tracker.combined_rect());
148 tracker.finalize_frame(BOUNDS);
149 assert_eq!(LtrbRect::default(), tracker.combined_rect());
150 }
151
152 #[test]
153 fn begin_and_end_draw_push_and_pop_the_clip() {
154 let tracker = SingleDirtyRectTracker::new();
155 tracker.add_rect(LtrbRect::new(10.0, 10.0, 20.0, 20.0));
156 tracker.finalize_frame(BOUNDS);
157 let mut ctx = MockDrawingContext::default();
158 tracker.begin_draw(&mut ctx);
159 tracker.end_draw(&mut ctx);
160 tracker.visualize(&mut ctx);
161 assert_eq!(3, ctx.log.len());
162 assert_eq!("push_clip 9, 9, 12, 12", ctx.log[0]);
163 assert_eq!("pop_clip", ctx.log[1]);
164 assert!(ctx.log[2].starts_with("rect #1e"), "{}", ctx.log[2]);
165 assert!(ctx.log[2].ends_with(" 9, 9, 12, 12"), "{}", ctx.log[2]);
166 }
167}