Skip to main content

lgui_core/runtime/frame/
dirty.rs

1use crate::core::UiRect;
2
3#[derive(Clone, Copy, Debug)]
4pub struct DirtyStrategy {
5    pub max_rects: usize,
6    pub full_area_ratio: f32,
7    pub dirty_outset: f32,
8}
9
10impl Default for DirtyStrategy {
11    fn default() -> Self {
12        Self {
13            max_rects: 12,
14            full_area_ratio: 0.90,
15            dirty_outset: 2.0,
16        }
17    }
18}
19
20#[derive(Clone, Debug)]
21pub struct DirtyRegionSet {
22    viewport: UiRect,
23    rects: Vec<UiRect>,
24    full: bool,
25    fallback_reason: Option<&'static str>,
26    strategy: DirtyStrategy,
27}
28
29impl DirtyRegionSet {
30    pub fn new(viewport: UiRect) -> Self {
31        Self::with_strategy(viewport, DirtyStrategy::default())
32    }
33
34    pub fn with_strategy(viewport: UiRect, strategy: DirtyStrategy) -> Self {
35        Self {
36            viewport,
37            rects: Vec::new(),
38            full: false,
39            fallback_reason: None,
40            strategy,
41        }
42    }
43
44    pub fn full(viewport: UiRect) -> Self {
45        let mut set = Self::new(viewport);
46        set.mark_full();
47        set
48    }
49
50    pub fn mark_full(&mut self) {
51        self.mark_full_with_reason("explicit");
52    }
53
54    pub fn mark_full_with_reason(&mut self, reason: &'static str) {
55        self.full = true;
56        self.fallback_reason = Some(reason);
57        self.rects.clear();
58    }
59
60    pub fn add(&mut self, rect: UiRect) {
61        if self.full {
62            return;
63        }
64        let Some(rect) = rect
65            .inflate(self.strategy.dirty_outset, self.strategy.dirty_outset)
66            .intersect(self.viewport)
67        else {
68            return;
69        };
70        if rect.width() <= 0.0 || rect.height() <= 0.0 {
71            return;
72        }
73
74        let mut merged = rect;
75        let mut index = 0;
76        while index < self.rects.len() {
77            if should_merge(self.rects[index], merged) {
78                merged = self.rects.remove(index).union(merged);
79            } else {
80                index += 1;
81            }
82        }
83        self.rects.push(merged);
84        self.normalize();
85    }
86
87    pub fn add_all(&mut self, rects: impl IntoIterator<Item = UiRect>) {
88        for rect in rects {
89            self.add(rect);
90        }
91    }
92
93    pub fn is_full(&self) -> bool {
94        self.full
95    }
96
97    pub fn is_empty(&self) -> bool {
98        !self.full && self.rects.is_empty()
99    }
100
101    pub fn rects(&self) -> &[UiRect] {
102        &self.rects
103    }
104
105    pub fn rect_count(&self) -> usize {
106        if self.full {
107            1
108        } else {
109            self.rects.len()
110        }
111    }
112
113    pub fn dirty_area(&self) -> f64 {
114        if self.full {
115            area(self.viewport)
116        } else {
117            self.rects.iter().map(|rect| area(*rect)).sum()
118        }
119    }
120
121    pub fn viewport_area(&self) -> f64 {
122        area(self.viewport)
123    }
124
125    pub fn area_ratio(&self) -> f32 {
126        let viewport_area = self.viewport_area();
127        if viewport_area <= 0.0 {
128            return 1.0;
129        }
130        (self.dirty_area() / viewport_area) as f32
131    }
132
133    pub fn viewport(&self) -> UiRect {
134        self.viewport
135    }
136
137    pub fn effective_rects(&self) -> Vec<UiRect> {
138        if self.full {
139            vec![self.viewport]
140        } else {
141            self.rects.clone()
142        }
143    }
144
145    pub fn fallback_reason(&self) -> Option<&'static str> {
146        self.fallback_reason
147    }
148
149    fn normalize(&mut self) {
150        if self.rects.len() > self.strategy.max_rects {
151            self.mark_full_with_reason("too-many-rects");
152            return;
153        }
154        let viewport_area = area(self.viewport);
155        if viewport_area <= 0.0 {
156            self.mark_full_with_reason("invalid-viewport");
157            return;
158        }
159        let dirty_area: f64 = self.rects.iter().map(|rect| area(*rect)).sum();
160        if (dirty_area / viewport_area) as f32 >= self.strategy.full_area_ratio {
161            self.mark_full_with_reason("area-threshold");
162        }
163    }
164}
165
166fn should_merge(a: UiRect, b: UiRect) -> bool {
167    a.intersect(b).is_some() || expanded(a, 2.0).intersect(b).is_some()
168}
169
170fn expanded(rect: UiRect, amount: f32) -> UiRect {
171    rect.inflate(amount, amount)
172}
173
174fn area(rect: UiRect) -> f64 {
175    rect.width().max(0.0) as f64 * rect.height().max(0.0) as f64
176}