1use core::fmt;
2
3#[derive(Debug, Default, 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 is_empty(&self) -> bool {
26 self.width == 0 || self.height == 0
27 }
28
29 pub fn intersection(&self, other: LayoutRect) -> LayoutRect {
31 let x1 = self.x.max(other.x);
32 let y1 = self.y.max(other.y);
33 let self_right = self.x.saturating_add(i32::from(self.width));
34 let other_right = other.x.saturating_add(i32::from(other.width));
35 let self_bottom = self.y.saturating_add(i32::from(self.height));
36 let other_bottom = other.y.saturating_add(i32::from(other.height));
37 let x2 = self_right.min(other_right);
38 let y2 = self_bottom.min(other_bottom);
39 if x2 <= x1 || y2 <= y1 {
40 LayoutRect {
41 x: 0,
42 y: 0,
43 width: 0,
44 height: 0,
45 }
46 } else {
47 LayoutRect {
48 x: x1,
49 y: y1,
50 width: (x2 - x1) as u16,
51 height: (y2 - y1) as u16,
52 }
53 }
54 }
55
56 pub fn contains<T: Into<i32>>(&self, col: T, row: T) -> bool {
59 if self.width == 0 || self.height == 0 {
60 return false;
61 }
62 let col = col.into();
63 let row = row.into();
64 let max_x = self.x.saturating_add(i32::from(self.width));
65 let max_y = self.y.saturating_add(i32::from(self.height));
66 col >= self.x && col < max_x && row >= self.y && row < max_y
67 }
68
69 pub fn clamp(self, bounds: LayoutRect) -> LayoutRect {
70 let x1 = self.x.max(bounds.x);
71 let y1 = self.y.max(bounds.y);
72 let self_right = self.x.saturating_add(i32::from(self.width));
73 let bounds_right = bounds.x.saturating_add(i32::from(bounds.width));
74 let self_bottom = self.y.saturating_add(i32::from(self.height));
75 let bounds_bottom = bounds.y.saturating_add(i32::from(bounds.height));
76 let x2 = self_right.min(bounds_right);
77 let y2 = self_bottom.min(bounds_bottom);
78 if x2 <= x1 || y2 <= y1 {
79 return LayoutRect {
80 x: 0,
81 y: 0,
82 width: 0,
83 height: 0,
84 };
85 }
86 LayoutRect {
87 x: x1,
88 y: y1,
89 width: (x2.saturating_sub(x1)) as u16,
90 height: (y2.saturating_sub(y1)) as u16,
91 }
92 }
93
94 pub fn visible_portion(self, bounds: LayoutRect) -> LayoutRect {
95 self.clamp(bounds)
96 }
97
98 pub fn intersects(self, other: LayoutRect) -> bool {
99 let a_right = self.x.saturating_add(i32::from(self.width));
100 let a_bottom = self.y.saturating_add(i32::from(self.height));
101 let b_right = other.x.saturating_add(i32::from(other.width));
102 let b_bottom = other.y.saturating_add(i32::from(other.height));
103 self.x < b_right && a_right > other.x && self.y < b_bottom && a_bottom > other.y
104 }
105
106 pub fn screen_to_local_point(&self, col: u16, row: u16) -> (i32, i32) {
112 (i32::from(col) - self.x, i32::from(row) - self.y)
113 }
114}
115
116pub fn rect_contains<T: Into<i32>>(rect: &LayoutRect, col: T, row: T) -> bool {
119 rect.contains(col, row)
120}
121
122pub fn inset<T: Into<u16> + Copy>(
126 rect: LayoutRect,
127 left: T,
128 right: T,
129 top: T,
130 bottom: T,
131) -> LayoutRect {
132 let left: u16 = left.into();
133 let right: u16 = right.into();
134 let top: u16 = top.into();
135 let bottom: u16 = bottom.into();
136 LayoutRect {
137 x: rect.x.saturating_add(i32::from(left)),
138 y: rect.y.saturating_add(i32::from(top)),
139 width: rect.width.saturating_sub(left.saturating_add(right)),
140 height: rect.height.saturating_sub(top.saturating_add(bottom)),
141 }
142}
143
144pub fn gap_insert<T: Into<u16>>(
148 rect: LayoutRect,
149 gap: T,
150 index: usize,
151 orientation: Orientation,
152) -> LayoutRect {
153 let offset = gap.into().saturating_mul(index as u16);
154 match orientation {
155 Orientation::Horizontal => LayoutRect {
156 x: rect.x.saturating_add(i32::from(offset)),
157 ..rect
158 },
159 Orientation::Vertical => LayoutRect {
160 y: rect.y.saturating_add(i32::from(offset)),
161 ..rect
162 },
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum Quadrant {
169 North,
170 South,
171 East,
172 West,
173}
174
175impl Quadrant {
176 pub fn to_insert_position(self) -> crate::snap::InsertPosition {
178 use crate::snap::InsertPosition;
179 match self {
180 Quadrant::North => InsertPosition::Top,
181 Quadrant::South => InsertPosition::Bottom,
182 Quadrant::West => InsertPosition::Left,
183 Quadrant::East => InsertPosition::Right,
184 }
185 }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum Orientation {
191 Horizontal,
193 Vertical,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub struct Ratio(pub u16, pub u16);
203
204impl Ratio {
205 pub fn half() -> Self {
207 Ratio(1, 1)
208 }
209
210 pub fn left_part(&self) -> u16 {
212 self.0
213 }
214
215 pub fn right_part(&self) -> u16 {
217 self.1
218 }
219
220 pub fn total(&self) -> u16 {
222 self.0 + self.1
223 }
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228pub struct SizeConstraints {
229 pub min_width: u16,
230 pub min_height: u16,
231}
232
233impl SizeConstraints {
234 pub fn fits_split(&self, area: &LayoutRect, orientation: Orientation) -> bool {
235 match orientation {
236 Orientation::Horizontal => {
237 area.width / 2 >= self.min_width && area.height >= self.min_height
238 }
239 Orientation::Vertical => {
240 area.height / 2 >= self.min_height && area.width >= self.min_width
241 }
242 }
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum LayoutError {
249 ConstraintViolated(SizeConstraints),
251 NotFound,
253}
254
255impl fmt::Display for LayoutError {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 match self {
258 LayoutError::ConstraintViolated(c) => {
259 write!(
260 f,
261 "minimum dimension violated (min {}x{})",
262 c.min_width, c.min_height
263 )
264 }
265 LayoutError::NotFound => write!(f, "target node not found"),
266 }
267 }
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum RectSpec {
275 Absolute(LayoutRect),
277 Percent {
279 x: u16,
280 y: u16,
281 width: u16,
282 height: u16,
283 },
284}
285
286impl RectSpec {
287 pub fn resolve(&self, bounds: LayoutRect) -> LayoutRect {
289 match *self {
290 RectSpec::Absolute(r) => r,
291 RectSpec::Percent {
292 x,
293 y,
294 width,
295 height,
296 } => {
297 let bw = i32::from(bounds.width);
298 let bh = i32::from(bounds.height);
299 LayoutRect {
300 x: bounds.x.saturating_add(bw * i32::from(x) / 100),
301 y: bounds.y.saturating_add(bh * i32::from(y) / 100),
302 width: ((bw * i32::from(width) / 100) as u16).min(bounds.width),
303 height: ((bh * i32::from(height) / 100) as u16).min(bounds.height),
304 }
305 }
306 }
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 fn r(x: i32, y: i32, w: u16, h: u16) -> LayoutRect {
315 LayoutRect {
316 x,
317 y,
318 width: w,
319 height: h,
320 }
321 }
322
323 #[test]
324 fn center_of_rect() {
325 assert_eq!(r(10, 20, 100, 60).center(), (60, 50));
326 }
327
328 #[test]
329 fn contains_inside() {
330 assert!(r(0, 0, 10, 10).contains(5, 5));
331 }
332
333 #[test]
334 fn contains_outside() {
335 assert!(!r(0, 0, 10, 10).contains(10, 10));
336 }
337
338 #[test]
339 fn contains_zero_dim() {
340 assert!(!r(0, 0, 0, 10).contains(0, 0));
341 }
342
343 #[test]
344 fn ratio_half() {
345 assert_eq!(Ratio::half(), Ratio(1, 1));
346 }
347
348 #[test]
349 fn clamp_within_bounds() {
350 let result = r(5, 5, 10, 10).clamp(r(0, 0, 20, 20));
351 assert_eq!(result, r(5, 5, 10, 10));
352 }
353
354 #[test]
355 fn clamp_partially_outside() {
356 let result = r(-5, -5, 20, 20).clamp(r(0, 0, 10, 10));
357 assert_eq!(result, r(0, 0, 10, 10));
358 }
359
360 #[test]
361 fn clamp_fully_outside() {
362 let result = r(100, 100, 10, 10).clamp(r(0, 0, 10, 10));
363 assert_eq!(result.width, 0);
364 assert_eq!(result.height, 0);
365 }
366
367 #[test]
368 fn intersects_overlapping() {
369 assert!(r(0, 0, 10, 10).intersects(r(5, 5, 10, 10)));
370 }
371
372 #[test]
373 fn intersects_non_overlapping() {
374 assert!(!r(0, 0, 10, 10).intersects(r(20, 20, 10, 10)));
375 }
376
377 #[test]
378 fn visible_portion_same_as_clamp() {
379 let r1 = r(-5, -5, 20, 20);
380 let bounds = r(0, 0, 10, 10);
381 assert_eq!(r1.visible_portion(bounds), r1.clamp(bounds));
382 }
383
384 #[test]
385 fn inset_shrinks_rect() {
386 let result = inset(r(10, 10, 100, 50), 5u16, 5u16, 2u16, 2u16);
387 assert_eq!(result.x, 15);
388 assert_eq!(result.y, 12);
389 assert_eq!(result.width, 90);
390 assert_eq!(result.height, 46);
391 }
392
393 #[test]
394 fn gap_insert_horizontal() {
395 let result = gap_insert(r(0, 0, 80, 24), 2u16, 1, Orientation::Horizontal);
396 assert_eq!(result.x, 2);
397 assert_eq!(result.y, 0);
398 }
399
400 #[test]
401 fn gap_insert_vertical() {
402 let result = gap_insert(r(0, 0, 80, 24), 2u16, 1, Orientation::Vertical);
403 assert_eq!(result.x, 0);
404 assert_eq!(result.y, 2);
405 }
406
407 #[test]
408 fn rect_spec_absolute() {
409 let spec = RectSpec::Absolute(r(10, 20, 30, 40));
410 let resolved = spec.resolve(r(0, 0, 80, 24));
411 assert_eq!(resolved, r(10, 20, 30, 40));
412 }
413
414 #[test]
415 fn rect_spec_percent() {
416 let spec = RectSpec::Percent {
417 x: 50,
418 y: 50,
419 width: 50,
420 height: 50,
421 };
422 let resolved = spec.resolve(r(0, 0, 100, 100));
423 assert_eq!(resolved, r(50, 50, 50, 50));
424 }
425
426 #[test]
427 fn screen_to_local_point_inside() {
428 let rect = LayoutRect {
429 x: 10,
430 y: 10,
431 width: 80,
432 height: 24,
433 };
434 let (dx, dy) = rect.screen_to_local_point(15, 25);
435 assert_eq!(dx, 5);
436 assert_eq!(dy, 15);
437 }
438
439 #[test]
440 fn screen_to_local_point_negative_delta() {
441 let rect = LayoutRect {
442 x: 10,
443 y: 10,
444 width: 80,
445 height: 24,
446 };
447 let (dx, dy) = rect.screen_to_local_point(2, 2);
448 assert_eq!(dx, -8);
449 assert_eq!(dy, -8);
450 }
451}