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