Skip to main content

ferroui_base/rendering/composition/server/dirty_rects/
single_dirty_rect_tracker.rs

1use 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
11/// Tracks the dirty area as a single bounding rectangle.
12pub 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        // Nothing is visible before the frame is finalized.
109        assert_eq!(LtrbRect::default(), tracker.combined_rect());
110
111        tracker.finalize_frame(BOUNDS);
112        // Union (10,10)-(50.5,35.2), inflated by 1, right/bottom rounded up.
113        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        // Entirely outside: the intersection is empty.
126        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}