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 let (width, _height) = match self.direction {
195 Direction::Horizontal => (context.width as u16, context.height.unwrap_or(0) as u16),
196 Direction::Vertical => (context.width as u16, context.height.unwrap_or(0) as u16),
197 };
198
199 let mut segments = Vec::new();
200
201 if self.direction == Direction::Vertical {
202 if let Some(total_height) = context.height {
203 let splits = self.calculate_splits(total_height as u16);
205
206 for (i, child) in self.children.iter().enumerate() {
207 let h = splits[i] as usize;
208 if h == 0 {
209 continue;
210 }
211
212 let child_ctx = RenderContext {
214 width: context.width,
215 height: Some(h),
216 };
217 let child_segments = child.render(&child_ctx);
218
219 let mut count = 0;
222 for segment in child_segments {
223 if count < h {
224 segments.push(segment);
225 count += 1;
226 } else {
227 break;
228 }
229 }
230
231 if count < h {
233 let blank_line = " ".repeat(context.width);
234 for _ in count..h {
235 segments.push(Segment::new(vec![crate::text::Span::raw(
236 blank_line.clone(),
237 )]));
238 }
239 }
240 }
241 } else {
242 for child in &self.children {
244 let child_segments = child.render(context);
245 segments.extend(child_segments);
246 }
247 }
248 } else {
249 let splits = self.calculate_splits(width);
251 let mut columns_output: Vec<Vec<Segment>> = Vec::new();
252 let mut max_lines = 0;
253
254 let target_height = context.height;
257
258 for (i, child) in self.children.iter().enumerate() {
259 let w = splits[i] as usize;
260 if w == 0 {
261 columns_output.push(Vec::new());
262 continue;
263 }
264
265 let child_ctx = RenderContext {
267 width: w,
268 height: target_height,
269 };
270 let child_segs = child.render(&child_ctx);
271 max_lines = std::cmp::max(max_lines, child_segs.len());
272 columns_output.push(child_segs);
273 }
274
275 let final_lines = target_height.unwrap_or(max_lines);
279
280 for line_idx in 0..final_lines {
282 let mut line_spans = Vec::new();
283 for (col_idx, _child) in self.children.iter().enumerate() {
284 let w = splits[col_idx] as usize;
285 let segs = &columns_output[col_idx];
286
287 if line_idx < segs.len() {
288 line_spans.extend(segs[line_idx].spans.clone());
289 } else {
290 line_spans.push(crate::text::Span::raw(" ".repeat(w)));
292 }
293 }
294 segments.push(Segment::line(line_spans));
295 }
296 }
297
298 segments
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_calculate_splits_ratios() {
308 let mut layout = Layout::new();
310 layout.split_row(vec![
311 Layout::new().with_ratio(1),
312 Layout::new().with_ratio(1),
313 ]);
314 let splits = layout.calculate_splits(100);
315 assert_eq!(splits, vec![50, 50]);
316
317 let mut layout = Layout::new();
319 layout.split_row(vec![
320 Layout::new().with_ratio(1),
321 Layout::new().with_ratio(3),
322 ]);
323 let splits = layout.calculate_splits(100);
324 assert_eq!(splits, vec![25, 75]);
325 }
326
327 #[test]
328 fn test_calculate_splits_fixed() {
329 let mut layout = Layout::new();
330 layout.split_row(vec![
331 Layout::new().with_size(10),
332 Layout::new().with_size(20),
333 ]);
334 let splits = layout.calculate_splits(100);
335 assert_eq!(splits[0], 10);
339 assert_eq!(splits[1], 20);
340 }
341
342 #[test]
343 fn test_calculate_splits_mixed() {
344 let mut layout = Layout::new();
345 layout.split_row(vec![
346 Layout::new().with_size(10), Layout::new().with_ratio(1), Layout::new().with_ratio(1), ]);
350 let splits = layout.calculate_splits(100);
351 assert_eq!(splits, vec![10, 45, 45]);
352 }
353
354 #[test]
355 fn test_calculate_splits_rounding() {
356 let mut layout = Layout::new();
359 layout.split_row(vec![
360 Layout::new().with_ratio(1),
361 Layout::new().with_ratio(1),
362 Layout::new().with_ratio(1),
363 ]);
364 let splits = layout.calculate_splits(100);
365 assert_eq!(splits, vec![33, 33, 34]);
366 assert_eq!(splits.iter().sum::<u16>(), 100);
367 }
368 #[test]
369 fn test_calculate_splits_min_size_simple() {
370 let mut layout = Layout::new();
371 layout.split_row(vec![
372 Layout::new().with_ratio(1).with_minimum_size(60),
373 Layout::new().with_ratio(1),
374 ]);
375 let splits = layout.calculate_splits(100);
376 assert_eq!(splits, vec![60, 40]);
377 }
378
379 #[test]
380 fn test_calculate_splits_min_size_priority() {
381 let mut layout = Layout::new();
382 layout.split_row(vec![
383 Layout::new().with_ratio(1).with_minimum_size(80),
384 Layout::new().with_ratio(1).with_minimum_size(10),
385 ]);
386 let splits = layout.calculate_splits(100);
387 assert_eq!(splits, vec![80, 20]);
388 }
389
390 #[test]
391 fn test_calculate_splits_complex_min() {
392 let mut layout = Layout::new();
393 layout.split_row(vec![
394 Layout::new().with_size(5),
395 Layout::new().with_ratio(1).with_minimum_size(10),
396 Layout::new().with_ratio(1),
397 ]);
398 let splits = layout.calculate_splits(20);
399 assert_eq!(splits, vec![5, 10, 5]);
400 }
401
402 #[test]
403 fn test_vertical_split_ratios() {
404 let mut layout = Layout::new();
405 layout.split_column(vec![
406 Layout::new().with_ratio(1).with_name("Top"),
407 Layout::new().with_ratio(1).with_name("Bottom"),
408 ]);
409
410 let context = RenderContext {
412 width: 80,
413 height: Some(10),
414 };
415 let segments = layout.render(&context);
416
417 assert_eq!(segments.len(), 10);
419 if !segments.is_empty() {
421 assert_eq!(segments[0].plain_text().len(), 80);
422 }
423 }
424
425 #[test]
426 fn test_vertical_split_stacking() {
427 let mut layout = Layout::new();
428 layout.split_column(vec![
429 Layout::new().with_size(1).with_name("Top"),
430 Layout::new().with_name("Bottom"),
431 ]);
432
433 let context = RenderContext {
435 width: 80,
436 height: None,
437 };
438 let segments = layout.render(&context);
439
440 assert_eq!(segments.len(), 2);
442 }
443
444 #[test]
445 fn test_horizontal_split_propagates_height() {
446 let mut layout = Layout::new();
447 layout.split_row(vec![
448 Layout::new().with_ratio(1),
449 Layout::new().with_ratio(1),
450 ]);
451
452 let context = RenderContext {
454 width: 80,
455 height: Some(5),
456 };
457 let segments = layout.render(&context);
458
459 assert_eq!(segments.len(), 5);
461 }
462}