1use std::collections::HashMap;
4
5use crate::console::{Console, ConsoleOptions, DynRenderable, Renderable};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct Region {
14 pub x: usize,
15 pub y: usize,
16 pub width: usize,
17 pub height: usize,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Direction {
27 Horizontal,
29 Vertical,
31}
32
33#[derive(Debug, Clone)]
39pub enum LayoutNode {
40 Split {
42 direction: Direction,
44 sizes: Vec<usize>,
46 children: Vec<LayoutNode>,
48 },
49 Leaf {
51 name: String,
53 renderable: Option<String>,
55 size: Option<usize>,
57 },
58}
59
60impl LayoutNode {
61 pub fn split(direction: Direction, children: Vec<LayoutNode>) -> Self {
66 let sizes = vec![1; children.len()];
67 Self::Split { direction, sizes, children }
68 }
69
70 pub fn sizes(mut self, sizes: Vec<usize>) -> Self {
72 if let Self::Split { sizes: ref mut s, .. } = self {
73 *s = sizes;
74 }
75 self
76 }
77}
78
79#[derive(Debug)]
86pub struct Layout {
87 pub root: LayoutNode,
89 pub visible: bool,
91 pub minimum_size: usize,
93 pub renderables: HashMap<String, DynRenderable>,
95 pub splitters: Vec<Box<dyn Splitter>>,
97 next_pane_id: usize,
99}
100
101impl Layout {
102 pub fn new(root: LayoutNode) -> Self {
104 Self {
105 root,
106 visible: true,
107 minimum_size: 1,
108 renderables: HashMap::new(),
109 splitters: Vec::new(),
110 next_pane_id: 0,
111 }
112 }
113
114 pub fn from_renderable(
119 name: impl Into<String>,
120 renderable: impl Renderable + Send + Sync + 'static,
121 ) -> Self {
122 let name = name.into();
123 let node = LayoutNode::Leaf {
124 name: name.clone(),
125 renderable: None,
126 size: None,
127 };
128 let mut renderables = HashMap::new();
129 renderables.insert(name, DynRenderable::new(renderable));
130 Self {
131 root: node,
132 visible: true,
133 minimum_size: 1,
134 renderables,
135 splitters: Vec::new(),
136 next_pane_id: 1,
137 }
138 }
139
140 pub fn split(&mut self, direction: Direction) -> &mut LayoutNode {
146 let name_a = format!("_split_a_{}", self.next_pane_id);
147 let name_b = format!("_split_b_{}", self.next_pane_id + 1);
148 self.next_pane_id += 2;
149
150 let old_root = std::mem::replace(
151 &mut self.root,
152 LayoutNode::Split {
153 direction,
154 sizes: vec![1, 1],
155 children: vec![
156 LayoutNode::Leaf { name: name_a, renderable: None, size: None },
157 LayoutNode::Leaf { name: name_b, renderable: None, size: None },
158 ],
159 },
160 );
161
162 if let LayoutNode::Split { ref mut children, .. } = self.root {
164 children[0] = old_root;
165 }
166
167 &mut self.root
168 }
169
170 pub fn unsplit(&mut self) {
173 let replacement = std::mem::replace(
174 &mut self.root,
175 LayoutNode::Leaf { name: String::new(), renderable: None, size: None },
176 );
177 match replacement {
178 LayoutNode::Split { mut children, .. } if !children.is_empty() => {
179 self.root = children.remove(0);
180 }
181 other => {
182 self.root = other;
183 }
184 }
185 }
186
187 pub fn split_column(&mut self) -> &mut Self {
189 self.split(Direction::Horizontal);
190 self
191 }
192
193 pub fn split_row(&mut self) -> &mut Self {
195 self.split(Direction::Vertical);
196 self
197 }
198
199 pub fn add_split(
207 &mut self,
208 renderable: impl Renderable + Send + Sync + 'static,
209 ratio: usize,
210 ) -> usize {
211 let id = self.next_pane_id;
212 self.next_pane_id += 1;
213 let name = format!("_pane_{}", id);
214 self.renderables.insert(name.clone(), DynRenderable::new(renderable));
215
216 match &mut self.root {
217 LayoutNode::Split { children, sizes, .. } => {
218 children.push(LayoutNode::Leaf {
219 name: name.clone(),
220 renderable: None,
221 size: None,
222 });
223 sizes.push(ratio);
224 children.len() - 1
225 }
226 LayoutNode::Leaf { .. } => {
227 let old_root = std::mem::replace(
229 &mut self.root,
230 LayoutNode::Split {
231 direction: Direction::Vertical,
232 sizes: vec![1, ratio],
233 children: vec![
234 LayoutNode::Leaf {
235 name: String::new(),
236 renderable: None,
237 size: None,
238 },
239 LayoutNode::Leaf {
240 name: name.clone(),
241 renderable: None,
242 size: None,
243 },
244 ],
245 },
246 );
247 if let LayoutNode::Split { ref mut children, .. } = self.root {
248 children[0] = old_root;
249 }
250 1
251 }
252 }
253 }
254
255 pub fn renderable(&self) -> Option<&dyn Renderable> {
257 match &self.root {
258 LayoutNode::Leaf { name, .. } => {
259 self.renderables.get(name).map(|dr| dr as &dyn Renderable)
260 }
261 _ => None,
262 }
263 }
264
265 pub fn children(&self) -> &[LayoutNode] {
267 match &self.root {
268 LayoutNode::Split { children, .. } => children,
269 _ => &[],
270 }
271 }
272
273 pub fn splitters(&self) -> Vec<&dyn Splitter> {
275 self.splitters.iter().map(|s| s.as_ref()).collect()
276 }
277
278 pub fn tree(&self) -> &LayoutNode {
280 &self.root
281 }
282
283 pub fn map(&mut self, f: impl Fn(&dyn Renderable) -> DynRenderable) {
286 let mut new_renderables = HashMap::new();
287 for (name, dr) in &self.renderables {
288 let new_dr = f(dr as &dyn Renderable);
289 new_renderables.insert(name.clone(), new_dr);
290 }
291 self.renderables = new_renderables;
292 }
293
294 pub fn get(&self, name: &str) -> Option<&dyn Renderable> {
296 self.renderables.get(name).map(|dr| dr as &dyn Renderable)
297 }
298
299 pub fn update(
301 &mut self,
302 name: &str,
303 renderable: impl Renderable + Send + Sync + 'static,
304 ) -> bool {
305 if self.renderables.contains_key(name) {
306 self.renderables.insert(name.to_string(), DynRenderable::new(renderable));
307 true
308 } else {
309 false
310 }
311 }
312
313 pub fn refresh_screen(&mut self, console: &mut Console) {
318 if !self.visible {
319 return;
320 }
321 let dims = crate::console::ConsoleDimensions::detect();
322 let regions = self.compute(dims.width, dims.height);
323 for (name, _region) in ®ions {
325 if let Some(renderable) = self.renderables.get(name) {
326 let rendered = renderable.render(&ConsoleOptions::default());
329 let text = rendered.to_ansi();
330 if !text.is_empty() {
331 let _ = console.print_str(&text);
332 }
333 }
334 }
335 }
336
337 pub fn compute(&self, total_width: usize, total_height: usize) -> Vec<(String, Region)> {
342 let mut regions = Vec::new();
343 let region = Region { x: 0, y: 0, width: total_width, height: total_height };
344 Self::layout_node(&self.root, region, &mut regions);
345 regions
346 }
347
348 fn layout_node(node: &LayoutNode, region: Region, out: &mut Vec<(String, Region)>) {
349 match node {
350 LayoutNode::Leaf { name, size, .. } => {
351 let mut r = region;
352 if let Some(s) = size {
353 r.width = r.width.min(*s);
354 r.height = r.height.min(*s);
355 } else {
356 r.width = r.width.max(2);
357 r.height = r.height.max(1);
358 }
359 out.push((name.clone(), r));
360 }
361 LayoutNode::Split { direction, sizes, children } => {
362 let total_size: usize = sizes.iter().sum();
363 let count = children.len();
364
365 match direction {
366 Direction::Horizontal => {
367 let mut x = region.x;
368 let total_spacing = count.saturating_sub(1);
369 let avail = region.width.saturating_sub(total_spacing);
370 for (i, child) in children.iter().enumerate() {
371 let ratio = sizes.get(i).copied().unwrap_or(1);
372 let child_w = (avail * ratio) / total_size;
373 let child_r = Region {
374 x,
375 y: region.y,
376 width: child_w.max(1),
377 height: region.height,
378 };
379 Self::layout_node(child, child_r, out);
380 x += child_w + 1; }
382 }
383 Direction::Vertical => {
384 let mut y = region.y;
385 for (i, child) in children.iter().enumerate() {
386 let ratio = sizes.get(i).copied().unwrap_or(1);
387 let child_h = (region.height * ratio) / total_size;
388 let child_r = Region {
389 x: region.x,
390 y,
391 width: region.width,
392 height: child_h.max(1),
393 };
394 Self::layout_node(child, child_r, out);
395 y += child_h;
396 }
397 }
398 }
399 }
400 }
401 }
402}
403
404pub trait Splitter: std::fmt::Debug {
413 fn split(&self, region: &Region, children: &[LayoutNode], direction: &Direction) -> Vec<Region>;
417}
418
419#[derive(Debug)]
421pub struct NoSplitter;
422
423impl Splitter for NoSplitter {
424 fn split(&self, region: &Region, children: &[LayoutNode], direction: &Direction) -> Vec<Region> {
425 let count = children.len().max(1);
426 match direction {
427 Direction::Horizontal => {
428 let col_width = region.width / count;
429 children
430 .iter()
431 .enumerate()
432 .map(|(i, _)| Region {
433 x: region.x + i * col_width,
434 y: region.y,
435 width: col_width,
436 height: region.height,
437 })
438 .collect()
439 }
440 Direction::Vertical => {
441 let row_height = region.height / count;
442 children
443 .iter()
444 .enumerate()
445 .map(|(i, _)| Region {
446 x: region.x,
447 y: region.y + i * row_height,
448 width: region.width,
449 height: row_height,
450 })
451 .collect()
452 }
453 }
454 }
455}
456
457#[derive(Debug)]
459pub struct ColumnSplitter;
460
461impl Splitter for ColumnSplitter {
462 fn split(&self, region: &Region, children: &[LayoutNode], _direction: &Direction) -> Vec<Region> {
463 let count = children.len().max(1);
464 let col_width = region.width / count;
465 children
466 .iter()
467 .enumerate()
468 .map(|(i, _)| Region {
469 x: region.x + i * col_width,
470 y: region.y,
471 width: col_width,
472 height: region.height,
473 })
474 .collect()
475 }
476}
477
478#[derive(Debug)]
480pub struct RowSplitter;
481
482impl Splitter for RowSplitter {
483 fn split(&self, region: &Region, children: &[LayoutNode], _direction: &Direction) -> Vec<Region> {
484 let count = children.len().max(1);
485 let row_height = region.height / count;
486 children
487 .iter()
488 .enumerate()
489 .map(|(i, _)| Region {
490 x: region.x,
491 y: region.y + i * row_height,
492 width: region.width,
493 height: row_height,
494 })
495 .collect()
496 }
497}
498
499#[cfg(test)]
504mod tests {
505 use super::*;
506
507 #[test]
508 fn test_region_defaults() {
509 let r = Region { x: 0, y: 0, width: 80, height: 24 };
510 assert_eq!(r.width, 80);
511 assert_eq!(r.height, 24);
512 }
513
514 #[test]
515 fn test_layout_single_leaf() {
516 let node = LayoutNode::Leaf {
517 name: "root".into(),
518 renderable: None,
519 size: None,
520 };
521 let layout = Layout::new(node);
522 let regions = layout.compute(80, 24);
523 assert_eq!(regions.len(), 1);
524 assert_eq!(regions[0].0, "root");
525 }
526
527 #[test]
528 fn test_layout_horizontal_split() {
529 let children = vec![
530 LayoutNode::Leaf { name: "left".into(), renderable: None, size: None },
531 LayoutNode::Leaf { name: "right".into(), renderable: None, size: None },
532 ];
533 let node = LayoutNode::split(Direction::Horizontal, children);
534 let layout = Layout::new(node);
535 let regions = layout.compute(80, 24);
536 assert_eq!(regions.len(), 2);
537 assert!(regions[0].1.x < regions[1].1.x);
538 }
539
540 #[test]
541 fn test_layout_vertical_split() {
542 let children = vec![
543 LayoutNode::Leaf { name: "top".into(), renderable: None, size: None },
544 LayoutNode::Leaf { name: "bottom".into(), renderable: None, size: None },
545 ];
546 let node = LayoutNode::split(Direction::Vertical, children);
547 let layout = Layout::new(node);
548 let regions = layout.compute(80, 24);
549 assert_eq!(regions.len(), 2);
550 assert!(regions[0].1.y < regions[1].1.y);
551 }
552
553 #[test]
554 fn test_split_method() {
555 let mut layout = Layout::new(LayoutNode::Leaf {
556 name: "root".into(),
557 renderable: None,
558 size: None,
559 });
560 layout.split(Direction::Horizontal);
561 match &layout.root {
563 LayoutNode::Split { children, .. } => {
564 assert_eq!(children.len(), 2);
565 }
566 _ => panic!("expected Split after split()"),
567 }
568 }
569
570 #[test]
571 fn test_unsplit_method() {
572 let mut layout = Layout::new(LayoutNode::Leaf {
573 name: "root".into(),
574 renderable: None,
575 size: None,
576 });
577 layout.split(Direction::Horizontal);
578 layout.unsplit();
579 match &layout.root {
581 LayoutNode::Leaf { .. } => {} _ => panic!("expected Leaf after unsplit()"),
583 }
584 }
585
586 #[test]
587 fn test_split_column() {
588 let mut layout = Layout::new(LayoutNode::Leaf {
589 name: "root".into(),
590 renderable: None,
591 size: None,
592 });
593 layout.split_column();
594 match &layout.root {
595 LayoutNode::Split { direction, .. } => {
596 assert_eq!(*direction, Direction::Horizontal);
597 }
598 _ => panic!("expected Horizontal split"),
599 }
600 }
601
602 #[test]
603 fn test_split_row() {
604 let mut layout = Layout::new(LayoutNode::Leaf {
605 name: "root".into(),
606 renderable: None,
607 size: None,
608 });
609 layout.split_row();
610 match &layout.root {
611 LayoutNode::Split { direction, .. } => {
612 assert_eq!(*direction, Direction::Vertical);
613 }
614 _ => panic!("expected Vertical split"),
615 }
616 }
617
618 #[test]
619 fn test_children_method() {
620 let mut layout = Layout::new(LayoutNode::Leaf {
621 name: "root".into(),
622 renderable: None,
623 size: None,
624 });
625 assert!(layout.children().is_empty());
627 layout.split(Direction::Horizontal);
628 assert_eq!(layout.children().len(), 2);
629 }
630
631 #[test]
632 fn test_tree_method() {
633 let layout = Layout::new(LayoutNode::Leaf {
634 name: "root".into(),
635 renderable: None,
636 size: None,
637 });
638 match layout.tree() {
639 LayoutNode::Leaf { name, .. } => assert_eq!(name, "root"),
640 _ => panic!("expected Leaf"),
641 }
642 }
643
644 #[test]
645 fn test_renderable_none_for_empty_layout() {
646 let layout = Layout::new(LayoutNode::Leaf {
647 name: "root".into(),
648 renderable: None,
649 size: None,
650 });
651 assert!(layout.renderable().is_none());
653 }
654
655 #[test]
656 fn test_from_renderable() {
657 let layout = Layout::from_renderable("main", "hello world");
658 assert!(layout.get("main").is_some());
659 }
660
661 #[test]
662 fn test_get_and_update() {
663 let mut layout = Layout::from_renderable("main", "initial");
664 assert!(layout.get("main").is_some());
665
666 let updated = layout.update("main", "updated");
667 assert!(updated);
668
669 let not_found = layout.update("nonexistent", "nope");
671 assert!(!not_found);
672 }
673
674 #[test]
675 fn test_map() {
676 let mut layout = Layout::from_renderable("main", "hello");
677 layout.map(|_r| DynRenderable::new("mapped"));
678 assert!(layout.get("main").is_some());
679 }
680
681 #[test]
682 fn test_add_split_to_leaf() {
683 let mut layout = Layout::from_renderable("main", "content");
684 let id = layout.add_split("second", 2);
685 match &layout.root {
687 LayoutNode::Split { children, sizes, .. } => {
688 assert_eq!(children.len(), 2);
689 assert_eq!(*sizes, vec![1, 2]);
690 assert_eq!(id, 1);
691 }
692 _ => panic!("expected Split after add_split"),
693 }
694 }
695
696 #[test]
697 fn test_no_splitter() {
698 let splitter = NoSplitter;
699 let children = vec![
700 LayoutNode::Leaf { name: "a".into(), renderable: None, size: None },
701 LayoutNode::Leaf { name: "b".into(), renderable: None, size: None },
702 ];
703 let region = Region { x: 0, y: 0, width: 80, height: 24 };
704 let regions = splitter.split(®ion, &children, &Direction::Horizontal);
705 assert_eq!(regions.len(), 2);
706 assert_eq!(regions[0].width, 40);
707 assert_eq!(regions[1].width, 40);
708 }
709
710 #[test]
711 fn test_column_splitter() {
712 let splitter = ColumnSplitter;
713 let children = vec![
714 LayoutNode::Leaf { name: "a".into(), renderable: None, size: None },
715 LayoutNode::Leaf { name: "b".into(), renderable: None, size: None },
716 LayoutNode::Leaf { name: "c".into(), renderable: None, size: None },
717 ];
718 let region = Region { x: 0, y: 0, width: 90, height: 24 };
719 let regions = splitter.split(®ion, &children, &Direction::Vertical);
720 assert_eq!(regions.len(), 3);
721 assert_eq!(regions[0].width, 30);
722 assert_eq!(regions[1].x, 30);
723 assert_eq!(regions[2].x, 60);
724 }
725
726 #[test]
727 fn test_row_splitter() {
728 let splitter = RowSplitter;
729 let children = vec![
730 LayoutNode::Leaf { name: "a".into(), renderable: None, size: None },
731 LayoutNode::Leaf { name: "b".into(), renderable: None, size: None },
732 ];
733 let region = Region { x: 0, y: 0, width: 80, height: 24 };
734 let regions = splitter.split(®ion, &children, &Direction::Horizontal);
735 assert_eq!(regions.len(), 2);
736 assert_eq!(regions[0].height, 12);
737 assert_eq!(regions[1].y, 12);
738 }
739
740 #[test]
741 fn test_splitters_method() {
742 let layout = Layout::new(LayoutNode::Leaf {
743 name: "root".into(),
744 renderable: None,
745 size: None,
746 });
747 assert!(layout.splitters().is_empty());
748 }
749
750 #[test]
751 fn test_compute_with_fixed_size() {
752 let node = LayoutNode::Leaf {
753 name: "fixed".into(),
754 renderable: None,
755 size: Some(10),
756 };
757 let layout = Layout::new(node);
758 let regions = layout.compute(80, 24);
759 assert_eq!(regions[0].1.width, 10);
760 assert_eq!(regions[0].1.height, 10);
761 }
762}