1use std::collections::HashMap;
6use std::sync::{Arc, Mutex, Weak};
7
8use crate::region::Region;
9use crate::screen_buffer::ScreenBuffer;
10use crate::segment::{Segment, Segments};
11use crate::{Console, ConsoleOptions, Renderable};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum SplitterKind {
15 Row,
16 Column,
17}
18
19#[derive(Debug, Clone)]
20pub struct LayoutRender {
21 pub region: Region,
22 pub lines: Vec<Vec<Segment>>,
23}
24
25struct LayoutState {
26 name: Option<String>,
27 size: Option<usize>,
28 minimum_size: usize,
29 ratio: usize,
30 visible: bool,
31 splitter: SplitterKind,
32 renderable: Arc<dyn Renderable>,
33 children: Vec<Layout>,
34 render_map: HashMap<usize, LayoutRender>,
35}
36
37#[derive(Clone)]
39pub struct Layout {
40 id: usize,
41 state: Arc<Mutex<LayoutState>>,
42}
43
44impl std::fmt::Debug for Layout {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 let state = self.state.lock().expect("layout mutex poisoned");
47 f.debug_struct("Layout")
48 .field("name", &state.name)
49 .field("size", &state.size)
50 .field("minimum_size", &state.minimum_size)
51 .field("ratio", &state.ratio)
52 .field("visible", &state.visible)
53 .field("splitter", &state.splitter)
54 .field("children", &state.children.len())
55 .finish_non_exhaustive()
56 }
57}
58
59fn next_layout_id() -> usize {
60 use std::sync::atomic::{AtomicUsize, Ordering};
61 static NEXT: AtomicUsize = AtomicUsize::new(1);
62 NEXT.fetch_add(1, Ordering::Relaxed)
63}
64
65#[derive(Clone)]
66struct Placeholder {
67 layout: Weak<Mutex<LayoutState>>,
68}
69
70impl Renderable for Placeholder {
71 fn render(&self, console: &Console, options: &ConsoleOptions) -> Segments {
72 use crate::{Align, Panel, Pretty, Style, VerticalAlignMethod};
73
74 let Some(layout) = self.layout.upgrade() else {
75 return Segments::new();
76 };
77 let state = layout.lock().expect("layout mutex poisoned");
78 let width = options.max_width;
79 let height = options.height.unwrap_or(options.size.1);
80 let title = if let Some(name) = &state.name {
81 format!("{name:?} ({width} x {height})")
82 } else {
83 format!("({width} x {height})")
84 };
85
86 #[derive(Debug)]
87 #[allow(dead_code)]
88 struct LayoutInfo {
89 name: Option<String>,
90 size: Option<usize>,
91 minimum_size: usize,
92 ratio: usize,
93 visible: bool,
94 splitter: SplitterKind,
95 children: usize,
96 }
97
98 let info = LayoutInfo {
99 name: state.name.clone(),
100 size: state.size,
101 minimum_size: state.minimum_size,
102 ratio: state.ratio,
103 visible: state.visible,
104 splitter: state.splitter,
105 children: state.children.len(),
106 };
107
108 let content =
109 Align::center(Box::new(Pretty::new(&info))).with_vertical(VerticalAlignMethod::Middle);
110
111 let panel = Panel::new(Box::new(content))
112 .with_title(title)
113 .with_border_style(Style::parse("blue").unwrap_or_else(Style::new))
114 .with_height(height);
115
116 panel.render(console, options)
117 }
118}
119
120impl Layout {
121 pub fn new() -> Self {
123 let id = next_layout_id();
124 let state = Arc::new(Mutex::new(LayoutState {
125 name: None,
126 size: None,
127 minimum_size: 1,
128 ratio: 1,
129 visible: true,
130 splitter: SplitterKind::Column,
131 renderable: Arc::new(String::new()),
132 children: Vec::new(),
133 render_map: HashMap::new(),
134 }));
135 let placeholder = Placeholder {
136 layout: Arc::downgrade(&state),
137 };
138 {
139 let mut st = state.lock().expect("layout mutex poisoned");
140 st.renderable = Arc::new(placeholder);
141 }
142
143 Self { id, state }
144 }
145
146 pub fn with_renderable(renderable: impl Renderable + 'static) -> Self {
148 let layout = Self::new();
149 layout.update(renderable);
150 layout
151 }
152
153 pub fn with_name(self, name: impl Into<String>) -> Self {
154 self.state.lock().expect("layout mutex poisoned").name = Some(name.into());
155 self
156 }
157
158 pub fn with_size(self, size: usize) -> Self {
159 self.state.lock().expect("layout mutex poisoned").size = Some(size);
160 self
161 }
162
163 pub fn with_minimum_size(self, minimum_size: usize) -> Self {
164 self.state
165 .lock()
166 .expect("layout mutex poisoned")
167 .minimum_size = minimum_size.max(1);
168 self
169 }
170
171 pub fn with_ratio(self, ratio: usize) -> Self {
172 self.state.lock().expect("layout mutex poisoned").ratio = ratio.max(1);
173 self
174 }
175
176 pub fn with_visible(self, visible: bool) -> Self {
177 self.state.lock().expect("layout mutex poisoned").visible = visible;
178 self
179 }
180
181 pub fn id(&self) -> usize {
182 self.id
183 }
184
185 pub fn name(&self) -> Option<String> {
186 self.state
187 .lock()
188 .expect("layout mutex poisoned")
189 .name
190 .clone()
191 }
192
193 pub fn children(&self) -> Vec<Layout> {
194 let state = self.state.lock().expect("layout mutex poisoned");
195 state
196 .children
197 .iter()
198 .cloned()
199 .filter(|c| c.state.lock().expect("layout mutex poisoned").visible)
200 .collect()
201 }
202
203 pub fn get(&self, name: &str) -> Option<Layout> {
204 let state = self.state.lock().expect("layout mutex poisoned");
205 if state.name.as_deref() == Some(name) {
206 return Some(self.clone());
207 }
208 for child in &state.children {
209 if let Some(found) = child.get(name) {
210 return Some(found);
211 }
212 }
213 None
214 }
215
216 pub fn update(&self, renderable: impl Renderable + 'static) {
217 self.state.lock().expect("layout mutex poisoned").renderable = Arc::new(renderable);
218 }
219
220 pub fn split(&self, splitter: SplitterKind, layouts: Vec<Layout>) {
221 let mut state = self.state.lock().expect("layout mutex poisoned");
222 state.splitter = splitter;
223 state.children = layouts;
224 }
225
226 pub fn split_row(&self, layouts: Vec<Layout>) {
227 self.split(SplitterKind::Row, layouts)
228 }
229
230 pub fn split_column(&self, layouts: Vec<Layout>) {
231 self.split(SplitterKind::Column, layouts)
232 }
233
234 pub fn add_split(&self, layouts: Vec<Layout>) {
235 self.state
236 .lock()
237 .expect("layout mutex poisoned")
238 .children
239 .extend(layouts);
240 }
241
242 pub fn unsplit(&self) {
243 self.state
244 .lock()
245 .expect("layout mutex poisoned")
246 .children
247 .clear();
248 }
249
250 fn visible_children(state: &LayoutState) -> Vec<Layout> {
251 state
252 .children
253 .iter()
254 .cloned()
255 .filter(|c| c.state.lock().expect("layout mutex poisoned").visible)
256 .collect()
257 }
258
259 fn divide_region(
260 children: &[Layout],
261 region: Region,
262 splitter: SplitterKind,
263 ) -> Vec<(Layout, Region)> {
264 let x = region.x;
265 let y = region.y;
266 let width = region.width as usize;
267 let height = region.height as usize;
268 match splitter {
269 SplitterKind::Row => {
270 let widths = ratio_resolve(width as i64, children);
271 let mut offset: i32 = 0;
272 children
273 .iter()
274 .cloned()
275 .zip(widths.into_iter())
276 .map(|(child, w)| {
277 let r = Region::new(x + offset, y, w as u32, region.height);
278 offset += w as i32;
279 (child, r)
280 })
281 .collect()
282 }
283 SplitterKind::Column => {
284 let heights = ratio_resolve(height as i64, children);
285 let mut offset: i32 = 0;
286 children
287 .iter()
288 .cloned()
289 .zip(heights.into_iter())
290 .map(|(child, h)| {
291 let r = Region::new(x, y + offset, region.width, h as u32);
292 offset += h as i32;
293 (child, r)
294 })
295 .collect()
296 }
297 }
298 }
299
300 fn make_region_map(&self, width: usize, height: usize) -> Vec<(Layout, Region)> {
301 let mut stack: Vec<(Layout, Region)> =
302 vec![(self.clone(), Region::new(0, 0, width as u32, height as u32))];
303 let mut layout_regions: Vec<(Layout, Region)> = Vec::new();
304 while let Some((layout, region)) = stack.pop() {
305 layout_regions.push((layout.clone(), region));
306
307 let state = layout.state.lock().expect("layout mutex poisoned");
308 let children = Self::visible_children(&state);
309 if !children.is_empty() {
310 let divided = Self::divide_region(&children, region, state.splitter);
311 for item in divided {
312 stack.push(item);
313 }
314 }
315 }
316
317 layout_regions.sort_by(|a, b| {
318 let ra = a.1;
320 let rb = b.1;
321 (ra.x, ra.y, ra.width, ra.height).cmp(&(rb.x, rb.y, rb.width, rb.height))
322 });
323
324 layout_regions
325 }
326
327 fn render_map(
328 &self,
329 console: &Console,
330 options: &ConsoleOptions,
331 ) -> HashMap<usize, LayoutRender> {
332 let width = options.max_width.max(1);
333 let height = options.height.unwrap_or_else(|| console.height()).max(1);
334 let regions = self.make_region_map(width, height);
335 let leaves: Vec<(Layout, Region)> = regions
336 .into_iter()
337 .filter(|(layout, _)| layout.children().is_empty())
338 .collect();
339
340 let mut render_map: HashMap<usize, LayoutRender> = HashMap::new();
341
342 for (layout, region) in leaves {
343 let mut child_opts = options.clone();
344 let w = region.width as usize;
345 let h = region.height as usize;
346 child_opts.size = (w.max(1), h.max(1));
347 child_opts.min_width = w.max(1);
348 child_opts.max_width = w.max(1);
349 child_opts.max_height = h.max(1);
350 child_opts.height = Some(h.max(1));
351
352 let renderable = {
353 let state = layout.state.lock().expect("layout mutex poisoned");
354 state.renderable.clone()
355 };
356
357 let lines =
358 console.render_lines(renderable.as_ref(), Some(&child_opts), None, true, false);
359 render_map.insert(layout.id, LayoutRender { region, lines });
360 }
361
362 render_map
363 }
364
365 pub fn refresh_screen(
369 &self,
370 console: &mut crate::Console<std::io::Stdout>,
371 layout_name: &str,
372 ) -> std::io::Result<()> {
373 let Some(layout) = self.get(layout_name) else {
374 return Ok(());
375 };
376
377 let region = {
378 let state = self.state.lock().expect("layout mutex poisoned");
379 let Some(render) = state.render_map.get(&layout.id) else {
380 return Ok(());
381 };
382 render.region
383 };
384
385 let mut child_opts = console.options().clone();
386 let w = region.width as usize;
387 let h = region.height as usize;
388 child_opts.size = (w.max(1), h.max(1));
389 child_opts.min_width = w.max(1);
390 child_opts.max_width = w.max(1);
391 child_opts.max_height = h.max(1);
392 child_opts.height = Some(h.max(1));
393
394 let lines = console.render_lines(&layout, Some(&child_opts), None, true, false);
395
396 self.state
398 .lock()
399 .expect("layout mutex poisoned")
400 .render_map
401 .insert(
402 layout.id,
403 LayoutRender {
404 region,
405 lines: lines.clone(),
406 },
407 );
408
409 console.update_screen_lines(&lines, region.x.max(0) as u16, region.y.max(0) as u16)?;
410 Ok(())
411 }
412}
413
414impl Layout {
415 pub fn to_tree(&self) -> crate::tree::Tree {
420 use crate::text::Text;
421 use crate::tree::Tree;
422
423 fn build_label(state: &LayoutState) -> Text {
424 let name = state.name.as_deref().unwrap_or("<unnamed>");
425 let kind = match state.splitter {
426 SplitterKind::Row => "row",
427 SplitterKind::Column => "column",
428 };
429 let size_info = if let Some(s) = state.size {
430 format!(" size={s}")
431 } else {
432 format!(" ratio={}", state.ratio)
433 };
434 Text::plain(format!("{name} ({kind}{size_info})"))
435 }
436
437 fn recurse(layout: &Layout) -> Tree {
438 let state = layout.state.lock().expect("layout mutex poisoned");
439 let label = build_label(&state);
440 let mut node = Tree::new(Box::new(label));
441 for child in &state.children {
442 node.children_mut().push(recurse(child));
443 }
444 node
445 }
446
447 recurse(self)
448 }
449}
450
451impl Renderable for Layout {
452 fn render(&self, console: &Console, options: &ConsoleOptions) -> Segments {
453 if self.children().is_empty() {
455 let renderable = self
456 .state
457 .lock()
458 .expect("layout mutex poisoned")
459 .renderable
460 .clone();
461 return renderable.render(console, options);
462 }
463
464 let width = options.max_width.max(1);
465 let height = options.height.unwrap_or_else(|| console.height()).max(1);
466
467 let render_map = self.render_map(console, options);
468
469 self.state.lock().expect("layout mutex poisoned").render_map = render_map.clone();
471
472 let mut buffer = ScreenBuffer::new(width, height, None);
473 let mut items: Vec<_> = render_map.into_values().collect();
475 items.sort_by(|a, b| {
476 let ra = a.region;
477 let rb = b.region;
478 (ra.x, ra.y, ra.width, ra.height).cmp(&(rb.x, rb.y, rb.width, rb.height))
479 });
480
481 for item in items {
482 let x = item.region.x.max(0) as usize;
483 let y = item.region.y.max(0) as usize;
484 let w = item.region.width as usize;
485 buffer.blit_lines(x, y, w, &item.lines);
486 }
487
488 let lines = buffer.to_styled_lines();
489 let mut out = Segments::new();
490 let new_line = Segment::line();
491 for line in lines {
492 for seg in line {
493 out.push(seg);
494 }
495 out.push(new_line.clone());
496 }
497 out
498 }
499}
500
501fn ratio_resolve(total: i64, layouts: &[Layout]) -> Vec<i64> {
506 let mut sizes: Vec<Option<i64>> = layouts
507 .iter()
508 .map(|layout| layout.state.lock().unwrap().size.map(|s| s as i64))
509 .collect();
510
511 while sizes.iter().any(|s| s.is_none()) {
512 let flexible: Vec<(usize, &Layout)> = sizes
513 .iter()
514 .zip(layouts.iter())
515 .enumerate()
516 .filter_map(|(i, (size, edge))| size.is_none().then_some((i, edge)))
517 .collect();
518
519 let used: i64 = sizes.iter().map(|s| s.unwrap_or(0)).sum();
520 let remaining = total - used;
521 if remaining <= 0 {
522 return sizes
523 .into_iter()
524 .zip(layouts.iter())
525 .map(|(size, edge)| match size {
526 Some(v) => v,
527 None => edge.state.lock().unwrap().minimum_size.max(1) as i64,
528 })
529 .collect();
530 }
531
532 let total_ratio: i64 = flexible
533 .iter()
534 .map(|(_, edge)| edge.state.lock().unwrap().ratio.max(1) as i64)
535 .sum();
536 let portion_num = remaining;
537 let portion_den = total_ratio.max(1);
538
539 let mut fixed_any = false;
541 for (index, edge) in &flexible {
542 let st = edge.state.lock().unwrap();
543 let min = st.minimum_size.max(1) as i64;
544 let ratio = st.ratio.max(1) as i64;
545 if portion_num * ratio <= min * portion_den {
547 sizes[*index] = Some(min);
548 fixed_any = true;
549 break;
550 }
551 }
552 if fixed_any {
553 continue;
554 }
555
556 let mut remainder_num: i64 = 0;
558 for (index, edge) in flexible {
559 let ratio = edge.state.lock().unwrap().ratio.max(1) as i64;
560 let num = portion_num * ratio + remainder_num;
561 let size = num / portion_den;
562 remainder_num = num % portion_den;
563 sizes[index] = Some(size);
564 }
565 break;
566 }
567
568 sizes.into_iter().map(|s| s.unwrap_or(0)).collect()
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574 use crate::Text;
575
576 #[test]
577 fn test_ratio_resolve_respects_fixed_size() {
578 let a = Layout::new().with_size(3);
579 let b = Layout::new().with_ratio(1);
580 let widths = ratio_resolve(10, &[a, b]);
581 assert_eq!(widths, vec![3, 7]);
582 }
583
584 #[test]
585 fn test_layout_split_row_renders_side_by_side() {
586 let console = Console::new();
587 let mut options = console.options().clone();
588 options.max_width = 6;
589 options.size = (6, 2);
590 options.height = Some(2);
591
592 let left = Layout::with_renderable(Text::plain("L")).with_name("left");
593 let right = Layout::with_renderable(Text::plain("R")).with_name("right");
594 let root = Layout::new();
595 root.split_row(vec![left, right]);
596
597 let output: String = root
598 .render(&console, &options)
599 .iter()
600 .map(|s| s.text.to_string())
601 .collect();
602 let lines: Vec<&str> = output.split('\n').collect();
603 assert!(lines[0].contains('L'));
604 assert!(lines[0].contains('R'));
605 }
606
607 #[test]
608 fn test_layout_get_by_name() {
609 let child = Layout::new().with_name("child");
610 let root = Layout::new();
611 root.split_column(vec![child.clone()]);
612 assert!(root.get("child").is_some());
613 }
614
615 #[test]
616 fn test_layout_split_column_stacks() {
617 let console = Console::new();
618 let mut options = console.options().clone();
619 options.max_width = 4;
620 options.size = (4, 2);
621 options.height = Some(2);
622
623 let top = Layout::with_renderable(Text::plain("A")).with_size(1);
624 let bottom = Layout::with_renderable(Text::plain("B"));
625 let root = Layout::new();
626 root.split_column(vec![top, bottom]);
627
628 let output: String = root
629 .render(&console, &options)
630 .iter()
631 .map(|s| s.text.to_string())
632 .collect();
633 let lines: Vec<&str> = output.split('\n').collect();
634 assert!(lines[0].contains('A'));
635 assert!(lines[1].contains('B'));
636 }
637
638 #[test]
639 fn test_layout_to_tree() {
640 let root = Layout::new().with_name("root");
641 let left = Layout::new().with_name("left").with_size(3);
642 let right = Layout::new().with_name("right");
643 root.split_row(vec![left, right]);
644
645 let tree = root.to_tree();
646 assert_eq!(tree.children().len(), 2);
647 }
648
649 #[test]
650 fn test_layout_to_tree_leaf() {
651 let leaf = Layout::new().with_name("leaf");
652 let tree = leaf.to_tree();
653 assert!(tree.children().is_empty());
654 }
655
656 #[test]
657 fn test_layout_nested_regions() {
658 let console = Console::new();
659 let mut options = console.options().clone();
660 options.max_width = 6;
661 options.size = (6, 3);
662 options.height = Some(3);
663
664 let header = Layout::with_renderable(Text::plain("H"))
665 .with_size(1)
666 .with_name("header");
667 let body = Layout::new().with_name("body");
668 let root = Layout::new();
669 root.split_column(vec![header, body.clone()]);
670
671 let left = Layout::with_renderable(Text::plain("L")).with_size(2);
672 let right = Layout::with_renderable(Text::plain("R"));
673 body.split_row(vec![left, right]);
674
675 let output: String = root
676 .render(&console, &options)
677 .iter()
678 .map(|s| s.text.to_string())
679 .collect();
680 let lines: Vec<&str> = output.split('\n').collect();
681 assert!(lines[0].contains('H'));
683 let lpos = lines[1].find('L').unwrap();
685 let rpos = lines[1].find('R').unwrap();
686 assert!(lpos < rpos);
687 }
688}