1use core::fmt;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct LayoutRect {
9 pub x: i32,
10 pub y: i32,
11 pub width: u16,
12 pub height: u16,
13}
14
15impl LayoutRect {
16 pub fn center(&self) -> (i32, i32) {
18 (
19 self.x + i32::from(self.width) / 2,
20 self.y + i32::from(self.height) / 2,
21 )
22 }
23
24 pub fn contains(&self, col: u16, row: u16) -> bool {
25 if self.width == 0 || self.height == 0 {
26 return false;
27 }
28 let max_x = self.x.saturating_add(i32::from(self.width));
29 let max_y = self.y.saturating_add(i32::from(self.height));
30 i32::from(col) >= self.x
31 && i32::from(col) < max_x
32 && i32::from(row) >= self.y
33 && i32::from(row) < max_y
34 }
35
36 pub fn clamp(self, bounds: LayoutRect) -> LayoutRect {
37 let x1 = self.x.max(bounds.x);
38 let y1 = self.y.max(bounds.y);
39 let self_right = self.x.saturating_add(i32::from(self.width));
40 let bounds_right = bounds.x.saturating_add(i32::from(bounds.width));
41 let self_bottom = self.y.saturating_add(i32::from(self.height));
42 let bounds_bottom = bounds.y.saturating_add(i32::from(bounds.height));
43 let x2 = self_right.min(bounds_right);
44 let y2 = self_bottom.min(bounds_bottom);
45 if x2 <= x1 || y2 <= y1 {
46 return LayoutRect {
47 x: 0,
48 y: 0,
49 width: 0,
50 height: 0,
51 };
52 }
53 LayoutRect {
54 x: x1,
55 y: y1,
56 width: (x2.saturating_sub(x1)) as u16,
57 height: (y2.saturating_sub(y1)) as u16,
58 }
59 }
60
61 pub fn visible_portion(self, bounds: LayoutRect) -> LayoutRect {
62 self.clamp(bounds)
63 }
64
65 pub fn intersects(self, other: LayoutRect) -> bool {
66 let a_right = self.x.saturating_add(i32::from(self.width));
67 let a_bottom = self.y.saturating_add(i32::from(self.height));
68 let b_right = other.x.saturating_add(i32::from(other.width));
69 let b_bottom = other.y.saturating_add(i32::from(other.height));
70 self.x < b_right && a_right > other.x && self.y < b_bottom && a_bottom > other.y
71 }
72}
73
74pub fn rect_contains(rect: &LayoutRect, col: u16, row: u16) -> bool {
76 rect.contains(col, row)
77}
78
79pub fn inset(rect: LayoutRect, left: u16, right: u16, top: u16, bottom: u16) -> LayoutRect {
82 LayoutRect {
83 x: rect.x.saturating_add(i32::from(left)),
84 y: rect.y.saturating_add(i32::from(top)),
85 width: rect.width.saturating_sub(left.saturating_add(right)),
86 height: rect.height.saturating_sub(top.saturating_add(bottom)),
87 }
88}
89
90pub fn gap_insert(
93 rect: LayoutRect,
94 gap: u16,
95 index: usize,
96 orientation: Orientation,
97) -> LayoutRect {
98 let offset = gap.saturating_mul(index as u16);
99 match orientation {
100 Orientation::Horizontal => LayoutRect {
101 x: rect.x.saturating_add(i32::from(offset)),
102 ..rect
103 },
104 Orientation::Vertical => LayoutRect {
105 y: rect.y.saturating_add(i32::from(offset)),
106 ..rect
107 },
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum Orientation {
114 Horizontal,
116 Vertical,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum Quadrant {
123 North,
124 South,
125 East,
126 West,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct Ratio(pub u16, pub u16);
135
136impl Ratio {
137 pub fn half() -> Self {
139 Ratio(1, 1)
140 }
141
142 pub fn left_part(&self) -> u16 {
144 self.0
145 }
146
147 pub fn right_part(&self) -> u16 {
149 self.1
150 }
151
152 pub fn total(&self) -> u16 {
154 self.0 + self.1
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub struct SizeConstraints {
161 pub min_width: u16,
162 pub min_height: u16,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum LayoutError {
168 ConstraintViolated(SizeConstraints),
170 NotFound,
172}
173
174impl fmt::Display for LayoutError {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 match self {
177 LayoutError::ConstraintViolated(c) => {
178 write!(
179 f,
180 "minimum dimension violated (min {}x{})",
181 c.min_width, c.min_height
182 )
183 }
184 LayoutError::NotFound => write!(f, "target node not found"),
185 }
186 }
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum RectSpec {
194 Absolute(LayoutRect),
196 Percent {
198 x: u16,
199 y: u16,
200 width: u16,
201 height: u16,
202 },
203}
204
205impl RectSpec {
206 pub fn resolve(&self, bounds: LayoutRect) -> LayoutRect {
208 match *self {
209 RectSpec::Absolute(r) => r,
210 RectSpec::Percent {
211 x,
212 y,
213 width,
214 height,
215 } => {
216 let bw = i32::from(bounds.width);
217 let bh = i32::from(bounds.height);
218 LayoutRect {
219 x: bounds.x.saturating_add(bw * i32::from(x) / 100),
220 y: bounds.y.saturating_add(bh * i32::from(y) / 100),
221 width: ((bw * i32::from(width) / 100) as u16).min(bounds.width),
222 height: ((bh * i32::from(height) / 100) as u16).min(bounds.height),
223 }
224 }
225 }
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 fn r(x: i32, y: i32, w: u16, h: u16) -> LayoutRect {
234 LayoutRect {
235 x,
236 y,
237 width: w,
238 height: h,
239 }
240 }
241
242 #[test]
243 fn center_of_rect() {
244 assert_eq!(r(10, 20, 100, 60).center(), (60, 50));
245 }
246
247 #[test]
248 fn contains_inside() {
249 assert!(r(0, 0, 10, 10).contains(5, 5));
250 }
251
252 #[test]
253 fn contains_outside() {
254 assert!(!r(0, 0, 10, 10).contains(10, 10));
255 }
256
257 #[test]
258 fn contains_zero_dim() {
259 assert!(!r(0, 0, 0, 10).contains(0, 0));
260 }
261
262 #[test]
263 fn ratio_half() {
264 assert_eq!(Ratio::half(), Ratio(1, 1));
265 }
266
267 #[test]
268 fn clamp_within_bounds() {
269 let result = r(5, 5, 10, 10).clamp(r(0, 0, 20, 20));
270 assert_eq!(result, r(5, 5, 10, 10));
271 }
272
273 #[test]
274 fn clamp_partially_outside() {
275 let result = r(-5, -5, 20, 20).clamp(r(0, 0, 10, 10));
276 assert_eq!(result, r(0, 0, 10, 10));
277 }
278
279 #[test]
280 fn clamp_fully_outside() {
281 let result = r(100, 100, 10, 10).clamp(r(0, 0, 10, 10));
282 assert_eq!(result.width, 0);
283 assert_eq!(result.height, 0);
284 }
285
286 #[test]
287 fn intersects_overlapping() {
288 assert!(r(0, 0, 10, 10).intersects(r(5, 5, 10, 10)));
289 }
290
291 #[test]
292 fn intersects_non_overlapping() {
293 assert!(!r(0, 0, 10, 10).intersects(r(20, 20, 10, 10)));
294 }
295
296 #[test]
297 fn visible_portion_same_as_clamp() {
298 let r1 = r(-5, -5, 20, 20);
299 let bounds = r(0, 0, 10, 10);
300 assert_eq!(r1.visible_portion(bounds), r1.clamp(bounds));
301 }
302
303 #[test]
304 fn inset_shrinks_rect() {
305 let result = inset(r(10, 10, 100, 50), 5, 5, 2, 2);
306 assert_eq!(result.x, 15);
307 assert_eq!(result.y, 12);
308 assert_eq!(result.width, 90);
309 assert_eq!(result.height, 46);
310 }
311
312 #[test]
313 fn gap_insert_horizontal() {
314 let result = gap_insert(r(0, 0, 80, 24), 2, 1, Orientation::Horizontal);
315 assert_eq!(result.x, 2);
316 assert_eq!(result.y, 0);
317 }
318
319 #[test]
320 fn gap_insert_vertical() {
321 let result = gap_insert(r(0, 0, 80, 24), 2, 1, Orientation::Vertical);
322 assert_eq!(result.x, 0);
323 assert_eq!(result.y, 2);
324 }
325
326 #[test]
327 fn rect_spec_absolute() {
328 let spec = RectSpec::Absolute(r(10, 20, 30, 40));
329 let resolved = spec.resolve(r(0, 0, 80, 24));
330 assert_eq!(resolved, r(10, 20, 30, 40));
331 }
332
333 #[test]
334 fn rect_spec_percent() {
335 let spec = RectSpec::Percent {
336 x: 50,
337 y: 50,
338 width: 50,
339 height: 50,
340 };
341 let resolved = spec.resolve(r(0, 0, 100, 100));
342 assert_eq!(resolved, r(50, 50, 50, 50));
343 }
344}