Skip to main content

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

1use super::i_dirty_rect_tracker::{
2    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::Color;
7use crate::platform::{
8    IDrawingContextImpl, IPlatformRenderInterface, IPlatformRenderInterfaceRegion, LtrbPixelRect, LtrbRect,
9};
10use crate::Thickness;
11use std::cell::{Cell, RefCell};
12use std::rc::Rc;
13
14/// Tracks the dirty area with a platform region holding every added
15/// rectangle.
16pub struct RegionDirtyRectTracker {
17    region: Rc<dyn IPlatformRenderInterfaceRegion>,
18    rects: RefCell<Vec<LtrbRect>>,
19    random: VisualizeRandom,
20    combined_rect: Cell<LtrbRect>,
21}
22
23impl RegionDirtyRectTracker {
24    pub fn new(platform_render: &dyn IPlatformRenderInterface) -> Self {
25        Self {
26            region: platform_render.create_region(),
27            rects: RefCell::new(Vec::new()),
28            random: VisualizeRandom::new(),
29            combined_rect: Cell::new(LtrbRect::default()),
30        }
31    }
32
33    fn get_inflated_pixel_rect(rc: LtrbRect) -> LtrbPixelRect {
34        // The inflated rectangle is intersected with the rectangle itself, as
35        // in the C# source: the inflation is effectively undone.
36        let inflated = rc.inflate(Thickness::uniform(1.0)).intersect_or_empty(rc);
37        pixel_rect_from_rect_unscaled(inflated)
38    }
39}
40
41impl IDirtyRectCollector for RegionDirtyRectTracker {
42    fn add_rect(&self, rect: LtrbRect) {
43        self.rects.borrow_mut().push(rect);
44    }
45}
46
47impl IDirtyRectTracker for RegionDirtyRectTracker {
48    fn finalize_frame(&self, _bounds: LtrbRect) {
49        self.region.reset();
50        for rc in self.rects.borrow().iter() {
51            self.region.add_rect(Self::get_inflated_pixel_rect(*rc));
52        }
53        self.combined_rect.set(pixel_rect_to_ltrb_rect_unscaled(self.region.bounds()));
54    }
55
56    fn begin_draw(&self, ctx: &mut dyn IDrawingContextImpl) {
57        ctx.push_clip_region(&*self.region);
58    }
59
60    fn end_draw(&self, ctx: &mut dyn IDrawingContextImpl) {
61        ctx.pop_clip();
62    }
63
64    fn is_empty(&self) -> bool {
65        self.rects.borrow().is_empty()
66    }
67
68    fn intersects(&self, rect: LtrbRect) -> bool {
69        self.region.intersects(rect)
70    }
71
72    fn initialize(&self, _bounds: LtrbRect) {
73        self.rects.borrow_mut().clear();
74    }
75
76    fn visualize(&self, context: &mut dyn IDrawingContextImpl) {
77        let brush = ImmutableSolidColorBrush::new(Color::new(
78            150,
79            self.random.next(255) as u8,
80            self.random.next(255) as u8,
81            self.random.next(255) as u8,
82        ));
83        context.draw_region(Some(&brush), None, &*self.region);
84    }
85
86    fn combined_rect(&self) -> LtrbRect {
87        self.combined_rect.get()
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::super::i_dirty_rect_tracker::tests::{MockDrawingContext, MockRenderInterface};
94    use super::*;
95
96    const BOUNDS: LtrbRect = LtrbRect::new(0.0, 0.0, 100.0, 100.0);
97
98    #[test]
99    fn collects_rects_into_the_platform_region_on_finalize() {
100        let platform = MockRenderInterface::default();
101        let tracker = RegionDirtyRectTracker::new(&platform);
102        let region = platform.regions.borrow()[0].clone();
103        assert!(tracker.is_empty());
104
105        tracker.add_rect(LtrbRect::new(10.2, 10.7, 20.1, 20.0));
106        tracker.add_rect(LtrbRect::new(50.0, 60.0, 70.0, 80.0));
107        assert!(!tracker.is_empty());
108        assert!(region.rects.borrow().is_empty());
109
110        tracker.finalize_frame(BOUNDS);
111        // The inflation is undone by the self-intersection; only the pixel
112        // snapping (truncate left/top, ceil right/bottom) remains.
113        assert_eq!(
114            vec![LtrbPixelRect::new(10, 10, 21, 20), LtrbPixelRect::new(50, 60, 70, 80)],
115            *region.rects.borrow()
116        );
117        assert_eq!(LtrbRect::new(10.0, 10.0, 70.0, 80.0), tracker.combined_rect());
118        assert!(tracker.intersects(LtrbRect::new(15.0, 15.0, 16.0, 16.0)));
119        assert!(!tracker.intersects(LtrbRect::new(30.0, 30.0, 40.0, 40.0)));
120    }
121
122    #[test]
123    fn finalize_rebuilds_the_region_and_initialize_clears_the_rects() {
124        let platform = MockRenderInterface::default();
125        let tracker = RegionDirtyRectTracker::new(&platform);
126        let region = platform.regions.borrow()[0].clone();
127        tracker.add_rect(LtrbRect::new(1.0, 1.0, 2.0, 2.0));
128        tracker.finalize_frame(BOUNDS);
129        tracker.finalize_frame(BOUNDS);
130        assert_eq!(1, region.rects.borrow().len());
131
132        tracker.initialize(BOUNDS);
133        assert!(tracker.is_empty());
134        // The region keeps the last finalized frame until the next finalize.
135        assert_eq!(1, region.rects.borrow().len());
136        tracker.finalize_frame(BOUNDS);
137        assert!(region.rects.borrow().is_empty());
138        assert_eq!(LtrbRect::default(), tracker.combined_rect());
139    }
140
141    #[test]
142    fn draw_uses_the_region_clip() {
143        let platform = MockRenderInterface::default();
144        let tracker = RegionDirtyRectTracker::new(&platform);
145        tracker.add_rect(LtrbRect::new(1.0, 1.0, 2.0, 2.0));
146        tracker.finalize_frame(BOUNDS);
147        let mut ctx = MockDrawingContext::default();
148        tracker.begin_draw(&mut ctx);
149        tracker.end_draw(&mut ctx);
150        tracker.visualize(&mut ctx);
151        assert_eq!(
152            vec![
153                "push_clip_region [LtrbPixelRect { left: 1, top: 1, right: 2, bottom: 2 }]".to_string(),
154                "pop_clip".to_string(),
155                "region alpha 150 rects 1".to_string()
156            ],
157            ctx.log
158        );
159    }
160}