use ratatui::layout::{Constraint, Direction, Layout, Rect};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SplitDirection {
Horizontal,
Vertical,
}
impl From<SplitDirection> for Direction {
fn from(d: SplitDirection) -> Self {
match d {
SplitDirection::Horizontal => Direction::Horizontal,
SplitDirection::Vertical => Direction::Vertical,
}
}
}
#[derive(Debug, Clone)]
pub enum LayoutNode {
Pane(usize),
Slot(usize),
Split {
direction: SplitDirection,
children: Vec<(Constraint, LayoutNode)>,
},
}
pub struct SlotRule {
pub slot_id: usize,
pub trigger_panes: Vec<usize>,
pub then_pane: usize,
pub default_pane: usize,
}
pub struct PageLayoutConfig {
pub tree: LayoutNode,
pub tab_panes: Vec<usize>,
pub slot_rules: Vec<SlotRule>,
}
impl PageLayoutConfig {
pub fn resolve_slots(&self, focused_pane: usize) -> Vec<(usize, usize)> {
self.slot_rules
.iter()
.map(|r| {
let pane = if r.trigger_panes.contains(&focused_pane) {
r.then_pane
} else {
r.default_pane
};
(r.slot_id, pane)
})
.collect()
}
}
pub struct PageFrame {
pub header: Rect,
pub content: Rect,
pub status_bar: Rect,
}
pub fn split_page_frame(area: Rect) -> PageFrame {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Min(3),
Constraint::Length(1),
])
.split(area);
PageFrame {
header: chunks[0],
content: chunks[1],
status_bar: chunks[2],
}
}
pub fn resolve_layout(
area: Rect,
tree: &LayoutNode,
slots: &[(usize, usize)],
) -> Vec<(usize, Rect)> {
let mut result = Vec::new();
resolve_node(area, tree, slots, &mut result);
result
}
fn resolve_node(
area: Rect,
node: &LayoutNode,
slots: &[(usize, usize)],
out: &mut Vec<(usize, Rect)>,
) {
match node {
LayoutNode::Pane(id) => {
out.push((*id, area));
}
LayoutNode::Slot(slot_id) => {
let pane_id = slots
.iter()
.find(|(s, _)| s == slot_id)
.map(|(_, p)| *p)
.unwrap_or(*slot_id);
out.push((pane_id, area));
}
LayoutNode::Split {
direction,
children,
} => {
let constraints: Vec<Constraint> = children.iter().map(|(c, _)| *c).collect();
let chunks = Layout::default()
.direction(Direction::from(*direction))
.constraints(constraints)
.split(area);
for (i, (_, child)) in children.iter().enumerate() {
resolve_node(chunks[i], child, slots, out);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::layout::Rect;
#[test]
fn single_pane_fills_area() {
let area = Rect::new(0, 0, 80, 24);
let tree = LayoutNode::Pane(42);
let result = resolve_layout(area, &tree, &[]);
assert_eq!(result, vec![(42, area)]);
}
#[test]
fn horizontal_split() {
let area = Rect::new(0, 0, 100, 24);
let tree = LayoutNode::Split {
direction: SplitDirection::Horizontal,
children: vec![
(Constraint::Percentage(50), LayoutNode::Pane(0)),
(Constraint::Percentage(50), LayoutNode::Pane(1)),
],
};
let result = resolve_layout(area, &tree, &[]);
assert_eq!(result.len(), 2);
assert_eq!(result[0].0, 0);
assert_eq!(result[1].0, 1);
assert_eq!(result[0].1.height, 24);
assert_eq!(result[1].1.height, 24);
assert_eq!(result[0].1.width + result[1].1.width, 100);
}
#[test]
fn nested_split() {
let area = Rect::new(0, 0, 80, 40);
let tree = LayoutNode::Split {
direction: SplitDirection::Vertical,
children: vec![
(
Constraint::Percentage(50),
LayoutNode::Split {
direction: SplitDirection::Horizontal,
children: vec![
(Constraint::Percentage(50), LayoutNode::Pane(0)),
(Constraint::Percentage(50), LayoutNode::Pane(1)),
],
},
),
(Constraint::Percentage(50), LayoutNode::Pane(2)),
],
};
let result = resolve_layout(area, &tree, &[]);
assert_eq!(result.len(), 3);
assert_eq!(result[0].0, 0);
assert_eq!(result[1].0, 1);
assert_eq!(result[2].0, 2);
assert_eq!(result[0].1.y, result[1].1.y);
assert!(result[2].1.y > result[0].1.y);
}
#[test]
fn slot_resolution() {
let area = Rect::new(0, 0, 80, 24);
let tree = LayoutNode::Slot(0);
let result = resolve_layout(area, &tree, &[(0, 99)]);
assert_eq!(result, vec![(99, area)]);
}
#[test]
fn slot_rule_trigger() {
let config = PageLayoutConfig {
tree: LayoutNode::Slot(0),
tab_panes: vec![0, 1],
slot_rules: vec![SlotRule {
slot_id: 0,
trigger_panes: vec![1, 3],
then_pane: 2,
default_pane: 4,
}],
};
assert_eq!(config.resolve_slots(1), vec![(0, 2)]);
assert_eq!(config.resolve_slots(3), vec![(0, 2)]);
assert_eq!(config.resolve_slots(0), vec![(0, 4)]);
assert_eq!(config.resolve_slots(99), vec![(0, 4)]);
}
#[test]
fn multiple_slot_rules() {
let config = PageLayoutConfig {
tree: LayoutNode::Pane(0),
tab_panes: vec![],
slot_rules: vec![
SlotRule {
slot_id: 0,
trigger_panes: vec![1],
then_pane: 10,
default_pane: 20,
},
SlotRule {
slot_id: 1,
trigger_panes: vec![2],
then_pane: 30,
default_pane: 40,
},
],
};
assert_eq!(config.resolve_slots(1), vec![(0, 10), (1, 40)]);
assert_eq!(config.resolve_slots(2), vec![(0, 20), (1, 30)]);
}
#[test]
fn page_frame_splits_correctly() {
let area = Rect::new(0, 0, 80, 24);
let frame = split_page_frame(area);
assert_eq!(frame.header.height, 1);
assert_eq!(frame.status_bar.height, 1);
assert_eq!(frame.content.height, 22);
assert_eq!(frame.header.y, 0);
assert_eq!(frame.content.y, 1);
assert_eq!(frame.status_bar.y, 23);
}
}