1pub use crate::core::rect::Margin;
6use crate::core::rect::Rect;
7
8pub use crate::core::text::Alignment;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
12pub enum Direction {
13 #[default]
14 Vertical,
15 Horizontal,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum Constraint {
21 Length(u16),
23 Percentage(u16),
25 Min(u16),
27 Max(u16),
29 Ratio(u32, u32),
31 Fill(u16),
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
37pub enum Flex {
38 #[default]
40 Start,
41 Center,
43 End,
45 SpaceBetween,
47 SpaceAround,
49}
50
51#[derive(Debug, Clone)]
53pub struct Layout {
54 direction: Direction,
55 constraints: Vec<Constraint>,
56 margin: Margin,
57 flex: Flex,
58 spacing: i16,
59}
60
61impl Default for Layout {
62 fn default() -> Self {
63 Self {
64 direction: Direction::Vertical,
65 constraints: Vec::new(),
66 margin: Margin::ZERO,
67 flex: Flex::Start,
68 spacing: 0,
69 }
70 }
71}
72
73impl Layout {
74 pub fn new(direction: Direction, constraints: impl Into<Vec<Constraint>>) -> Self {
75 Self {
76 direction,
77 constraints: constraints.into(),
78 ..Default::default()
79 }
80 }
81
82 pub fn vertical(constraints: impl Into<Vec<Constraint>>) -> Self {
83 Self::new(Direction::Vertical, constraints)
84 }
85
86 pub fn horizontal(constraints: impl Into<Vec<Constraint>>) -> Self {
87 Self::new(Direction::Horizontal, constraints)
88 }
89
90 pub fn direction(mut self, direction: Direction) -> Self {
91 self.direction = direction;
92 self
93 }
94
95 pub fn constraints(mut self, constraints: impl Into<Vec<Constraint>>) -> Self {
96 self.constraints = constraints.into();
97 self
98 }
99
100 pub fn margin(mut self, margin: Margin) -> Self {
101 self.margin = margin;
102 self
103 }
104
105 pub fn flex(mut self, flex: Flex) -> Self {
106 self.flex = flex;
107 self
108 }
109
110 pub fn spacing(mut self, spacing: i16) -> Self {
111 self.spacing = spacing;
112 self
113 }
114
115 pub fn split(&self, area: Rect) -> Vec<Rect> {
117 let inner = area.inner(self.margin);
118 if self.constraints.is_empty() || inner.is_empty() {
119 return vec![inner];
120 }
121
122 let total_space = match self.direction {
123 Direction::Vertical => inner.height,
124 Direction::Horizontal => inner.width,
125 };
126
127 let n = self.constraints.len();
128 let total_spacing = if n > 1 {
129 (n as i32 - 1) * self.spacing as i32
130 } else {
131 0
132 };
133 let available = (total_space as i32 - total_spacing).max(0) as u16;
134
135 let mut sizes: Vec<u16> = self
137 .constraints
138 .iter()
139 .map(|c| match c {
140 Constraint::Length(l) => (*l).min(available),
141 Constraint::Percentage(p) => ((available as u32 * *p as u32) / 100) as u16,
142 Constraint::Min(m) => *m,
143 Constraint::Max(m) => (*m).min(available),
144 Constraint::Ratio(num, den) => {
145 (available as u32 * *num).checked_div(*den).unwrap_or(0) as u16
146 }
147 Constraint::Fill(_) => 0,
148 })
149 .collect();
150
151 let fixed_total: u16 = sizes.iter().sum();
153 let remaining = available.saturating_sub(fixed_total);
154
155 let fill_total_weight: u16 = self
156 .constraints
157 .iter()
158 .filter_map(|c| match c {
159 Constraint::Fill(w) => Some(*w),
160 _ => None,
161 })
162 .sum();
163
164 if fill_total_weight > 0 && remaining > 0 {
165 let mut distributed = 0u16;
166 let fill_count = self
167 .constraints
168 .iter()
169 .filter(|c| matches!(c, Constraint::Fill(_)))
170 .count();
171 let mut fill_idx = 0;
172
173 for (i, c) in self.constraints.iter().enumerate() {
174 if let Constraint::Fill(w) = c {
175 fill_idx += 1;
176 let share = if fill_idx == fill_count {
177 remaining - distributed
179 } else {
180 ((remaining as u32 * *w as u32) / fill_total_weight as u32) as u16
181 };
182 sizes[i] = share;
183 distributed += share;
184 }
185 }
186 }
187
188 {
192 let used: u16 = sizes.iter().sum();
193 let leftover = available.saturating_sub(used);
194 let min_count = self
195 .constraints
196 .iter()
197 .filter(|c| matches!(c, Constraint::Min(_)))
198 .count();
199 if min_count > 0 && leftover > 0 {
200 let share = leftover / min_count as u16;
201 let mut distributed = 0u16;
202 let mut idx = 0;
203 for (i, c) in self.constraints.iter().enumerate() {
204 if let Constraint::Min(_) = c {
205 idx += 1;
206 let extra = if idx == min_count {
207 leftover - distributed
208 } else {
209 share
210 };
211 sizes[i] += extra;
212 distributed += extra;
213 }
214 }
215 }
216 }
217
218 for (i, c) in self.constraints.iter().enumerate() {
220 match c {
221 Constraint::Min(m) => sizes[i] = sizes[i].max(*m),
222 Constraint::Max(m) => sizes[i] = sizes[i].min(*m),
223 _ => {}
224 }
225 }
226
227 let total_used: u16 = sizes.iter().sum();
229 if total_used > available {
230 let scale = available as f64 / total_used as f64;
232 let mut shrunk_total = 0u16;
233 for (i, size) in sizes.iter_mut().enumerate() {
234 if i == n - 1 {
235 *size = available - shrunk_total;
236 } else {
237 *size = (*size as f64 * scale) as u16;
238 shrunk_total += *size;
239 }
240 }
241 }
242
243 let mut rects = Vec::with_capacity(n);
245 let actual_total: u16 = sizes.iter().sum();
246 let excess = available.saturating_sub(actual_total);
247
248 let start_offset = match self.flex {
249 Flex::Start | Flex::SpaceBetween => 0,
250 Flex::Center | Flex::SpaceAround => excess / 2,
251 Flex::End => excess,
252 };
253
254 let mut pos = match self.direction {
255 Direction::Vertical => inner.y + start_offset,
256 Direction::Horizontal => inner.x + start_offset,
257 };
258
259 for (i, size) in sizes.iter().enumerate() {
260 let rect = match self.direction {
261 Direction::Vertical => Rect::new(inner.x, pos, inner.width, *size),
262 Direction::Horizontal => Rect::new(pos, inner.y, *size, inner.height),
263 };
264 rects.push(rect);
265 pos = pos.saturating_add(*size);
266 if i < n - 1 {
267 pos = (pos as i32 + self.spacing as i32).max(0) as u16;
268 }
269 }
270
271 rects
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn vertical_fixed_lengths() {
281 let area = Rect::new(0, 0, 80, 24);
282 let rects =
283 Layout::vertical(vec![Constraint::Length(3), Constraint::Length(5)]).split(area);
284 assert_eq!(rects.len(), 2);
285 assert_eq!(rects[0], Rect::new(0, 0, 80, 3));
286 assert_eq!(rects[1], Rect::new(0, 3, 80, 5));
287 }
288
289 #[test]
290 fn fill_distributes_remaining() {
291 let area = Rect::new(0, 0, 80, 24);
292 let rects = Layout::vertical(vec![Constraint::Length(4), Constraint::Fill(1)]).split(area);
293 assert_eq!(rects[0].height, 4);
294 assert_eq!(rects[1].height, 20);
295 }
296
297 #[test]
298 fn horizontal_with_margin() {
299 let area = Rect::new(0, 0, 80, 24);
300 let rects =
301 Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
302 .margin(Margin::uniform(1))
303 .split(area);
304 assert_eq!(rects[0].x, 1);
305 assert_eq!(rects[0].width + rects[1].width, 78);
306 }
307
308 #[test]
309 fn min_absorbs_remaining_space() {
310 let area = Rect::new(0, 0, 80, 24);
311 let rects = Layout::vertical(vec![Constraint::Min(0), Constraint::Length(3)]).split(area);
312 assert_eq!(rects[0].height, 21);
313 assert_eq!(rects[1].height, 3);
314 assert_eq!(rects[1].y, 21);
315 }
316}