spec_ai/spec_ai_tui/layout/
flex.rs1use super::{Constraint, Direction};
4use crate::spec_ai_tui::geometry::Rect;
5
6#[derive(Debug, Clone)]
8pub struct Layout {
9 direction: Direction,
10 constraints: Vec<Constraint>,
11 margin: u16,
12 spacing: u16,
13}
14
15impl Layout {
16 pub fn horizontal() -> Self {
18 Self {
19 direction: Direction::Horizontal,
20 constraints: Vec::new(),
21 margin: 0,
22 spacing: 0,
23 }
24 }
25
26 pub fn vertical() -> Self {
28 Self {
29 direction: Direction::Vertical,
30 constraints: Vec::new(),
31 margin: 0,
32 spacing: 0,
33 }
34 }
35
36 pub fn new(direction: Direction) -> Self {
38 Self {
39 direction,
40 constraints: Vec::new(),
41 margin: 0,
42 spacing: 0,
43 }
44 }
45
46 pub fn direction(mut self, direction: Direction) -> Self {
48 self.direction = direction;
49 self
50 }
51
52 pub fn constraints<I: IntoIterator<Item = Constraint>>(mut self, constraints: I) -> Self {
54 self.constraints = constraints.into_iter().collect();
55 self
56 }
57
58 pub fn margin(mut self, margin: u16) -> Self {
60 self.margin = margin;
61 self
62 }
63
64 pub fn spacing(mut self, spacing: u16) -> Self {
66 self.spacing = spacing;
67 self
68 }
69
70 pub fn split(&self, area: Rect) -> Vec<Rect> {
72 let inner = area.inner(self.margin);
74 if inner.is_empty() || self.constraints.is_empty() {
75 return vec![];
76 }
77
78 let (total_space, cross_size) = match self.direction {
80 Direction::Horizontal => (inner.width, inner.height),
81 Direction::Vertical => (inner.height, inner.width),
82 };
83
84 let num_gaps = self.constraints.len().saturating_sub(1) as u16;
86 let spacing_total = self.spacing * num_gaps;
87 let available = total_space.saturating_sub(spacing_total);
88
89 let mut sizes: Vec<u16> = vec![0; self.constraints.len()];
91 let mut remaining = available;
92 let mut total_fill_weight = 0u32;
93
94 for (i, constraint) in self.constraints.iter().enumerate() {
95 let resolve_base = match constraint {
97 Constraint::Percentage(_) | Constraint::Ratio(_, _) => available,
98 _ => remaining,
99 };
100 let (size, is_fill) = constraint.resolve(resolve_base);
101 if is_fill {
102 total_fill_weight += constraint.fill_weight() as u32;
103 } else {
104 sizes[i] = size;
105 remaining = remaining.saturating_sub(size);
106 }
107 }
108
109 if total_fill_weight > 0 && remaining > 0 {
111 let fill_space = remaining;
112 let mut distributed = 0u16;
113
114 for (i, constraint) in self.constraints.iter().enumerate() {
115 if let Constraint::Fill(weight) = constraint {
116 let share = (fill_space as u32 * *weight as u32 / total_fill_weight) as u16;
118 sizes[i] = share;
119 distributed += share;
120 }
121 }
122
123 let leftover = fill_space.saturating_sub(distributed);
125 if leftover > 0 {
126 for (i, constraint) in self.constraints.iter().enumerate().rev() {
127 if matches!(constraint, Constraint::Fill(_)) {
128 sizes[i] = sizes[i].saturating_add(leftover);
129 break;
130 }
131 }
132 }
133 }
134
135 let mut result = Vec::with_capacity(self.constraints.len());
137 let mut offset = match self.direction {
138 Direction::Horizontal => inner.x,
139 Direction::Vertical => inner.y,
140 };
141
142 for (i, size) in sizes.into_iter().enumerate() {
143 let rect = match self.direction {
144 Direction::Horizontal => Rect::new(offset, inner.y, size, cross_size),
145 Direction::Vertical => Rect::new(inner.x, offset, cross_size, size),
146 };
147 result.push(rect);
148
149 offset = offset.saturating_add(size);
150 if i < self.constraints.len() - 1 {
151 offset = offset.saturating_add(self.spacing);
152 }
153 }
154
155 result
156 }
157}
158
159impl Default for Layout {
160 fn default() -> Self {
161 Self::vertical()
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_vertical_split_fixed() {
171 let area = Rect::new(0, 0, 100, 50);
172 let chunks = Layout::vertical()
173 .constraints([
174 Constraint::Fixed(10),
175 Constraint::Fixed(20),
176 Constraint::Fixed(10),
177 ])
178 .split(area);
179
180 assert_eq!(chunks.len(), 3);
181 assert_eq!(chunks[0], Rect::new(0, 0, 100, 10));
182 assert_eq!(chunks[1], Rect::new(0, 10, 100, 20));
183 assert_eq!(chunks[2], Rect::new(0, 30, 100, 10));
184 }
185
186 #[test]
187 fn test_vertical_split_with_fill() {
188 let area = Rect::new(0, 0, 100, 50);
189 let chunks = Layout::vertical()
190 .constraints([
191 Constraint::Fixed(10),
192 Constraint::Fill(1),
193 Constraint::Fixed(5),
194 ])
195 .split(area);
196
197 assert_eq!(chunks.len(), 3);
198 assert_eq!(chunks[0].height, 10);
199 assert_eq!(chunks[1].height, 35); assert_eq!(chunks[2].height, 5);
201 }
202
203 #[test]
204 fn test_horizontal_split() {
205 let area = Rect::new(0, 0, 100, 50);
206 let chunks = Layout::horizontal()
207 .constraints([Constraint::Percentage(30), Constraint::Fill(1)])
208 .split(area);
209
210 assert_eq!(chunks.len(), 2);
211 assert_eq!(chunks[0].width, 30);
212 assert_eq!(chunks[1].width, 70);
213 assert_eq!(chunks[0].height, 50);
214 assert_eq!(chunks[1].height, 50);
215 }
216
217 #[test]
218 fn test_multiple_fills() {
219 let area = Rect::new(0, 0, 100, 100);
220 let chunks = Layout::vertical()
221 .constraints([
222 Constraint::Fill(1),
223 Constraint::Fill(2),
224 Constraint::Fill(1),
225 ])
226 .split(area);
227
228 assert_eq!(chunks.len(), 3);
229 assert_eq!(chunks[0].height, 25);
231 assert_eq!(chunks[1].height, 50);
232 assert_eq!(chunks[2].height, 25);
233 }
234
235 #[test]
236 fn test_with_margin() {
237 let area = Rect::new(0, 0, 100, 50);
238 let chunks = Layout::vertical()
239 .margin(5)
240 .constraints([Constraint::Fill(1)])
241 .split(area);
242
243 assert_eq!(chunks.len(), 1);
244 assert_eq!(chunks[0], Rect::new(5, 5, 90, 40)); }
246
247 #[test]
248 fn test_with_spacing() {
249 let area = Rect::new(0, 0, 100, 50);
250 let chunks = Layout::vertical()
251 .spacing(2)
252 .constraints([
253 Constraint::Fixed(10),
254 Constraint::Fixed(10),
255 Constraint::Fixed(10),
256 ])
257 .split(area);
258
259 assert_eq!(chunks.len(), 3);
260 assert_eq!(chunks[0].y, 0);
261 assert_eq!(chunks[1].y, 12); assert_eq!(chunks[2].y, 24); }
264
265 #[test]
266 fn test_percentage() {
267 let area = Rect::new(0, 0, 100, 100);
268 let chunks = Layout::vertical()
269 .constraints([
270 Constraint::Percentage(25),
271 Constraint::Percentage(50),
272 Constraint::Percentage(25),
273 ])
274 .split(area);
275
276 assert_eq!(chunks[0].height, 25);
277 assert_eq!(chunks[1].height, 50);
278 assert_eq!(chunks[2].height, 25);
279 }
280
281 #[test]
282 fn test_empty_constraints() {
283 let area = Rect::new(0, 0, 100, 50);
284 let chunks = Layout::vertical().constraints([]).split(area);
285 assert!(chunks.is_empty());
286 }
287
288 #[test]
289 fn test_empty_area() {
290 let area = Rect::new(0, 0, 0, 0);
291 let chunks = Layout::vertical()
292 .constraints([Constraint::Fill(1)])
293 .split(area);
294 assert!(chunks.is_empty());
295 }
296}