1use crate::console::RenderContext;
2use crate::renderable::{Renderable, Segment};
3use std::sync::Arc;
4
5#[derive(Clone)]
7pub struct Layout {
8 renderable: Option<Arc<dyn Renderable + Send + Sync>>,
10 children: Vec<Layout>,
12 direction: Direction,
14 size: Option<u16>,
16 ratio: u32,
18 name: Option<String>,
20 minimum_size: u16,
22 visible: bool,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq)]
27pub enum Direction {
28 Horizontal,
29 Vertical,
30}
31
32impl Layout {
33 pub fn new() -> Self {
35 Self {
36 renderable: None,
37 children: Vec::new(),
38 direction: Direction::Vertical,
39 size: None,
40 ratio: 1,
41 name: None,
42 minimum_size: 0,
43 visible: true,
44 }
45 }
46
47 pub fn with_name(mut self, name: &str) -> Self {
49 self.name = Some(name.to_string());
50 self
51 }
52
53 pub fn with_size(mut self, size: u16) -> Self {
55 self.size = Some(size);
56 self
57 }
58
59 pub fn with_ratio(mut self, ratio: u32) -> Self {
61 self.ratio = ratio;
62 self
63 }
64
65 pub fn with_minimum_size(mut self, size: u16) -> Self {
67 self.minimum_size = size;
68 self
69 }
70
71 pub fn update<R: Renderable + Send + Sync + 'static>(&mut self, renderable: R) {
73 self.renderable = Some(Arc::new(renderable));
74 }
75
76 pub fn children_mut(&mut self) -> &mut Vec<Layout> {
78 &mut self.children
79 }
80
81 pub fn split_row(&mut self, layouts: Vec<Layout>) {
83 self.direction = Direction::Horizontal;
84 self.children = layouts;
85 }
86
87 pub fn split_column(&mut self, layouts: Vec<Layout>) {
89 self.direction = Direction::Vertical;
90 self.children = layouts;
91 }
92
93 fn calculate_splits(&self, total_size: u16) -> Vec<u16> {
95 let count = self.children.len();
96 if count == 0 {
97 return Vec::new();
98 }
99
100 let mut sizes = vec![0; count];
101 let mut remaining = total_size;
102 let mut flexible_indices = Vec::new();
103
104 for (i, child) in self.children.iter().enumerate() {
106 if let Some(fixed) = child.size {
107 let s = std::cmp::min(fixed, remaining);
108 sizes[i] = s;
109 remaining -= s;
110 } else {
111 flexible_indices.push(i);
112 }
113 }
114
115 let mut candidates = flexible_indices;
117
118 while !candidates.is_empty() {
119 let total_ratio: u32 = candidates.iter().map(|&i| self.children[i].ratio).sum();
120
121 if remaining == 0 || total_ratio == 0 {
123 for &i in &candidates {
124 sizes[i] = 0;
125 }
126 break;
127 }
128
129 let unit = remaining as f64 / total_ratio as f64;
130
131 let mut violator = None;
133 for (idx_in_candidates, &i) in candidates.iter().enumerate() {
134 let child = &self.children[i];
135 let ideal = child.ratio as f64 * unit;
136 if ideal < child.minimum_size as f64 {
137 violator = Some(idx_in_candidates);
138 break; }
140 }
141
142 if let Some(idx_c) = violator {
143 let i = candidates.remove(idx_c);
144 let child = &self.children[i];
145 let s = std::cmp::min(child.minimum_size, remaining);
146 sizes[i] = s;
147 remaining -= s;
148 } else {
149 let mut distributed = 0;
151 for (idx, &i) in candidates.iter().enumerate() {
152 let child = &self.children[i];
153 let s = if idx == candidates.len() - 1 {
154 remaining - distributed
155 } else {
156 (child.ratio as f64 * unit).round() as u16
157 };
158 sizes[i] = s;
159 distributed += s;
160 }
161 break;
162 }
163 }
164
165 sizes
166 }
167}
168
169impl Default for Layout {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175impl Renderable for Layout {
176 fn render(&self, context: &RenderContext) -> Vec<Segment> {
177 if !self.visible {
178 return Vec::new();
179 }
180
181 if self.children.is_empty() {
183 if let Some(r) = &self.renderable {
184 return r.render(context);
187 }
188 let blank_line = " ".repeat(context.width);
190 return vec![Segment::new(vec![crate::text::Span::raw(blank_line)])];
191 }
192
193
194 let (width, _height) = match self.direction {
196 Direction::Horizontal => (context.width as u16, context.height.unwrap_or(0) as u16),
197 Direction::Vertical => (
198 context.width as u16,
199 context.height.unwrap_or(0) as u16,
200 ),
201 };
202
203 let mut segments = Vec::new();
204
205 if self.direction == Direction::Vertical {
206 if let Some(total_height) = context.height {
207 let splits = self.calculate_splits(total_height as u16);
209
210 for (i, child) in self.children.iter().enumerate() {
211 let h = splits[i] as usize;
212 if h == 0 {
213 continue;
214 }
215
216 let child_ctx = RenderContext {
218 width: context.width,
219 height: Some(h),
220 };
221 let child_segments = child.render(&child_ctx);
222
223 let mut count = 0;
226 for segment in child_segments {
227 if count < h {
228 segments.push(segment);
229 count += 1;
230 } else {
231 break;
232 }
233 }
234
235 if count < h {
237 let blank_line = " ".repeat(context.width);
238 for _ in count..h {
239 segments.push(Segment::new(vec![crate::text::Span::raw(blank_line.clone())]));
240 }
241 }
242 }
243 } else {
244 for child in &self.children {
246 let child_segments = child.render(context);
247 segments.extend(child_segments);
248 }
249 }
250 } else {
251 let splits = self.calculate_splits(width);
253 let mut columns_output: Vec<Vec<Segment>> = Vec::new();
254 let mut max_lines = 0;
255
256 let target_height = context.height;
259
260 for (i, child) in self.children.iter().enumerate() {
261 let w = splits[i] as usize;
262 if w == 0 {
263 columns_output.push(Vec::new());
264 continue;
265 }
266
267 let child_ctx = RenderContext {
269 width: w,
270 height: target_height,
271 };
272 let child_segs = child.render(&child_ctx);
273 max_lines = std::cmp::max(max_lines, child_segs.len());
274 columns_output.push(child_segs);
275 }
276
277 let final_lines = target_height.unwrap_or(max_lines);
281
282 for line_idx in 0..final_lines {
284 let mut line_spans = Vec::new();
285 for (col_idx, _child) in self.children.iter().enumerate() {
286 let w = splits[col_idx] as usize;
287 let segs = &columns_output[col_idx];
288
289 if line_idx < segs.len() {
290 line_spans.extend(segs[line_idx].spans.clone());
291 } else {
292 line_spans.push(crate::text::Span::raw(" ".repeat(w)));
294 }
295 }
296 segments.push(Segment::line(line_spans));
297 }
298 }
299
300 segments
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn test_calculate_splits_ratios() {
310 let mut layout = Layout::new();
312 layout.split_row(vec![
313 Layout::new().with_ratio(1),
314 Layout::new().with_ratio(1),
315 ]);
316 let splits = layout.calculate_splits(100);
317 assert_eq!(splits, vec![50, 50]);
318
319 let mut layout = Layout::new();
321 layout.split_row(vec![
322 Layout::new().with_ratio(1),
323 Layout::new().with_ratio(3),
324 ]);
325 let splits = layout.calculate_splits(100);
326 assert_eq!(splits, vec![25, 75]);
327 }
328
329 #[test]
330 fn test_calculate_splits_fixed() {
331 let mut layout = Layout::new();
332 layout.split_row(vec![
333 Layout::new().with_size(10),
334 Layout::new().with_size(20),
335 ]);
336 let splits = layout.calculate_splits(100);
337 assert_eq!(splits[0], 10);
341 assert_eq!(splits[1], 20);
342 }
343
344 #[test]
345 fn test_calculate_splits_mixed() {
346 let mut layout = Layout::new();
347 layout.split_row(vec![
348 Layout::new().with_size(10), Layout::new().with_ratio(1), Layout::new().with_ratio(1), ]);
352 let splits = layout.calculate_splits(100);
353 assert_eq!(splits, vec![10, 45, 45]);
354 }
355
356 #[test]
357 fn test_calculate_splits_rounding() {
358 let mut layout = Layout::new();
361 layout.split_row(vec![
362 Layout::new().with_ratio(1),
363 Layout::new().with_ratio(1),
364 Layout::new().with_ratio(1),
365 ]);
366 let splits = layout.calculate_splits(100);
367 assert_eq!(splits, vec![33, 33, 34]);
368 assert_eq!(splits.iter().sum::<u16>(), 100);
369 }
370 #[test]
371 fn test_calculate_splits_min_size_simple() {
372 let mut layout = Layout::new();
373 layout.split_row(vec![
374 Layout::new().with_ratio(1).with_minimum_size(60),
375 Layout::new().with_ratio(1),
376 ]);
377 let splits = layout.calculate_splits(100);
378 assert_eq!(splits, vec![60, 40]);
379 }
380
381 #[test]
382 fn test_calculate_splits_min_size_priority() {
383 let mut layout = Layout::new();
384 layout.split_row(vec![
385 Layout::new().with_ratio(1).with_minimum_size(80),
386 Layout::new().with_ratio(1).with_minimum_size(10),
387 ]);
388 let splits = layout.calculate_splits(100);
389 assert_eq!(splits, vec![80, 20]);
390 }
391
392 #[test]
393 fn test_calculate_splits_complex_min() {
394 let mut layout = Layout::new();
395 layout.split_row(vec![
396 Layout::new().with_size(5),
397 Layout::new().with_ratio(1).with_minimum_size(10),
398 Layout::new().with_ratio(1),
399 ]);
400 let splits = layout.calculate_splits(20);
401 assert_eq!(splits, vec![5, 10, 5]);
402 }
403
404 #[test]
405 fn test_vertical_split_ratios() {
406 let mut layout = Layout::new();
407 layout.split_column(vec![
408 Layout::new().with_ratio(1).with_name("Top"),
409 Layout::new().with_ratio(1).with_name("Bottom"),
410 ]);
411
412 let context = RenderContext { width: 80, height: Some(10) };
414 let segments = layout.render(&context);
415
416 assert_eq!(segments.len(), 10);
418 if !segments.is_empty() {
420 assert_eq!(segments[0].plain_text().len(), 80);
421 }
422 }
423
424 #[test]
425 fn test_vertical_split_stacking() {
426 let mut layout = Layout::new();
427 layout.split_column(vec![
428 Layout::new().with_size(1).with_name("Top"),
429 Layout::new().with_name("Bottom"),
430 ]);
431
432 let context = RenderContext { width: 80, height: None };
434 let segments = layout.render(&context);
435
436 assert_eq!(segments.len(), 2);
438 }
439
440 #[test]
441 fn test_horizontal_split_propagates_height() {
442 let mut layout = Layout::new();
443 layout.split_row(vec![
444 Layout::new().with_ratio(1),
445 Layout::new().with_ratio(1),
446 ]);
447
448 let context = RenderContext { width: 80, height: Some(5) };
450 let segments = layout.render(&context);
451
452 assert_eq!(segments.len(), 5);
454 }
455}