Skip to main content

hadrone_core/
validate.rs

1//! Layout validation and automatic repair for persisted or imported grids.
2
3use crate::LayoutItem;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Issues found by [`validate_layout`].
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub enum LayoutIssue {
10    DuplicateId { id: String },
11    NonPositiveSize { id: String, w: i32, h: i32 },
12    OutOfHorizontalBounds { id: String, x: i32, w: i32, cols: i32 },
13    MinMaxWidth { id: String, w: i32, min_w: Option<i32>, max_w: Option<i32> },
14    MinMaxHeight { id: String, h: i32, min_h: Option<i32>, max_h: Option<i32> },
15    Overlap { a: String, b: String },
16}
17
18/// Full validation pass (does not mutate).
19pub fn validate_layout(layout: &[LayoutItem], cols: i32) -> Result<(), Vec<LayoutIssue>> {
20    let mut issues = Vec::new();
21    let mut seen: HashMap<&str, usize> = HashMap::new();
22
23    for item in layout {
24        if seen.insert(&item.id[..], 1).is_some() {
25            issues.push(LayoutIssue::DuplicateId {
26                id: item.id.clone(),
27            });
28        }
29        if item.w < 1 || item.h < 1 {
30            issues.push(LayoutIssue::NonPositiveSize {
31                id: item.id.clone(),
32                w: item.w,
33                h: item.h,
34            });
35        }
36        if item.x < 0 || item.x + item.w > cols {
37            issues.push(LayoutIssue::OutOfHorizontalBounds {
38                id: item.id.clone(),
39                x: item.x,
40                w: item.w,
41                cols,
42            });
43        }
44        if let Some(min) = item.min_w
45            && item.w < min
46        {
47            issues.push(LayoutIssue::MinMaxWidth {
48                id: item.id.clone(),
49                w: item.w,
50                min_w: item.min_w,
51                max_w: item.max_w,
52            });
53        }
54        if let Some(max) = item.max_w
55            && item.w > max
56        {
57            issues.push(LayoutIssue::MinMaxWidth {
58                id: item.id.clone(),
59                w: item.w,
60                min_w: item.min_w,
61                max_w: item.max_w,
62            });
63        }
64        if let Some(min) = item.min_h
65            && item.h < min
66        {
67            issues.push(LayoutIssue::MinMaxHeight {
68                id: item.id.clone(),
69                h: item.h,
70                min_h: item.min_h,
71                max_h: item.max_h,
72            });
73        }
74        if let Some(max) = item.max_h
75            && item.h > max
76        {
77            issues.push(LayoutIssue::MinMaxHeight {
78                id: item.id.clone(),
79                h: item.h,
80                min_h: item.min_h,
81                max_h: item.max_h,
82            });
83        }
84    }
85
86    for i in 0..layout.len() {
87        for j in (i + 1)..layout.len() {
88            if crate::collides(&layout[i], &layout[j]) {
89                issues.push(LayoutIssue::Overlap {
90                    a: layout[i].id.clone(),
91                    b: layout[j].id.clone(),
92                });
93            }
94        }
95    }
96
97    if issues.is_empty() {
98        Ok(())
99    } else {
100        Err(issues)
101    }
102}
103
104/// Clamp positions/sizes into grid and min/max bounds; dedupe ids by suffixing.
105pub fn repair_layout(layout: &mut [LayoutItem], cols: i32) {
106    let mut seen: HashMap<String, u32> = HashMap::new();
107    for item in layout.iter_mut() {
108        let base = item.id.clone();
109        let n = seen.entry(base.clone()).or_insert(0);
110        if *n > 0 {
111            item.id = format!("{base}-{}", *n);
112        }
113        *n += 1;
114
115        item.w = item.w.max(1);
116        item.h = item.h.max(1);
117        item.x = item.x.max(0);
118        if item.w > cols {
119            item.w = cols;
120        }
121        item.x = item.x.min((cols - item.w).max(0));
122
123        if let Some(min) = item.min_w {
124            item.w = item.w.max(min);
125        }
126        if let Some(max) = item.max_w {
127            item.w = item.w.min(max);
128        }
129        if let Some(min) = item.min_h {
130            item.h = item.h.max(min);
131        }
132        if let Some(max) = item.max_h {
133            item.h = item.h.min(max);
134        }
135
136        item.w = item.w.min(cols);
137        item.x = item.x.min((cols - item.w).max(0));
138    }
139}