use ratatui::layout::{Direction, Rect};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct PaneId(u32);
static NEXT_PANE_ID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(1);
impl PaneId {
pub fn alloc() -> Self {
Self(NEXT_PANE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed))
}
pub fn raw(self) -> u32 {
self.0
}
pub fn from_raw(id: u32) -> Self {
Self(id)
}
}
#[derive(Clone)]
pub struct PaneInfo {
pub id: PaneId,
pub rect: Rect,
pub inner_rect: Rect,
pub is_focused: bool,
}
#[derive(Clone)]
pub struct SplitBorder {
pub pos: u16,
pub direction: Direction,
pub area: Rect,
pub path: Vec<bool>,
}
#[derive(Debug, Clone, Copy)]
pub enum NavDirection {
Left,
Right,
Up,
Down,
}
pub enum Node {
Pane(PaneId),
Split {
direction: Direction,
ratio: f32,
first: Box<Node>,
second: Box<Node>,
},
}
pub struct TileLayout {
root: Node,
focus: PaneId,
}
impl TileLayout {
pub fn new() -> (Self, PaneId) {
let root_id = PaneId::alloc();
(
Self {
root: Node::Pane(root_id),
focus: root_id,
},
root_id,
)
}
pub fn focused(&self) -> PaneId {
self.focus
}
pub fn pane_count(&self) -> usize {
count_panes(&self.root)
}
pub fn panes(&self, area: Rect) -> Vec<PaneInfo> {
let mut result = Vec::new();
collect_panes(&self.root, area, self.focus, &mut result);
result
}
pub fn splits(&self, area: Rect) -> Vec<SplitBorder> {
let mut result = Vec::new();
collect_splits(&self.root, area, vec![], &mut result);
result
}
pub fn split_focused(&mut self, direction: Direction) -> PaneId {
let new_id = PaneId::alloc();
let placeholder = PaneId::from_raw(0);
let old = std::mem::replace(&mut self.root, Node::Pane(placeholder));
self.root = split_at(old, self.focus, direction, new_id);
self.focus = new_id;
new_id
}
pub fn close_focused(&mut self) -> bool {
if self.pane_count() <= 1 {
return false;
}
let target = self.focus;
let ids = self.pane_ids();
let pos = ids.iter().position(|id| *id == target).unwrap();
let new_focus = if pos + 1 < ids.len() {
ids[pos + 1]
} else {
ids[pos - 1]
};
let placeholder = PaneId::from_raw(0);
let old = std::mem::replace(&mut self.root, Node::Pane(placeholder));
if let Some(new_root) = remove_pane(old, target) {
self.root = new_root;
self.focus = new_focus;
true
} else {
false
}
}
pub fn focus_next(&mut self) {
let ids = self.pane_ids();
if let Some(pos) = ids.iter().position(|id| *id == self.focus) {
self.focus = ids[(pos + 1) % ids.len()];
}
}
pub fn focus_prev(&mut self) {
let ids = self.pane_ids();
if let Some(pos) = ids.iter().position(|id| *id == self.focus) {
self.focus = ids[(pos + ids.len() - 1) % ids.len()];
}
}
pub fn focus_pane(&mut self, id: PaneId) {
if self.pane_ids().contains(&id) {
self.focus = id;
}
}
pub fn set_ratio_at(&mut self, path: &[bool], ratio: f32) {
set_ratio_at(&mut self.root, path, ratio.clamp(0.1, 0.9));
}
pub fn resize_focused(&mut self, nav: NavDirection, delta: f32, area: Rect) {
let panes = self.panes(area);
let Some(focused) = panes.iter().find(|p| p.is_focused) else {
return;
};
let focused_rect = focused.rect;
let splits = self.splits(area);
let target_dir = match nav {
NavDirection::Left | NavDirection::Right => Direction::Horizontal,
NavDirection::Up | NavDirection::Down => Direction::Vertical,
};
let grows = matches!(nav, NavDirection::Right | NavDirection::Down);
let best = splits
.iter()
.filter(|s| s.direction == target_dir)
.filter(|s| match target_dir {
Direction::Horizontal => {
let near_right = (s.pos as i32 - (focused_rect.x + focused_rect.width) as i32).unsigned_abs() <= 1;
let near_left = (s.pos as i32 - focused_rect.x as i32).unsigned_abs() <= 1;
near_right || near_left
}
Direction::Vertical => {
let near_bottom = (s.pos as i32 - (focused_rect.y + focused_rect.height) as i32).unsigned_abs() <= 1;
let near_top = (s.pos as i32 - focused_rect.y as i32).unsigned_abs() <= 1;
near_bottom || near_top
}
})
.min_by_key(|s| {
match (target_dir, grows) {
(Direction::Horizontal, true) => {
((focused_rect.x + focused_rect.width) as i32 - s.pos as i32).unsigned_abs()
}
(Direction::Horizontal, false) => {
(focused_rect.x as i32 - s.pos as i32).unsigned_abs()
}
(Direction::Vertical, true) => {
((focused_rect.y + focused_rect.height) as i32 - s.pos as i32).unsigned_abs()
}
(Direction::Vertical, false) => {
(focused_rect.y as i32 - s.pos as i32).unsigned_abs()
}
}
});
if let Some(split) = best {
let path = split.path.clone();
let current_ratio = get_ratio_at(&self.root, &path).unwrap_or(0.5);
let adj = if grows { delta } else { -delta };
self.set_ratio_at(&path, current_ratio + adj);
}
}
pub fn pane_ids(&self) -> Vec<PaneId> {
let mut ids = Vec::new();
collect_ids(&self.root, &mut ids);
ids
}
pub fn root(&self) -> &Node {
&self.root
}
pub fn from_saved(root: Node, focus: PaneId) -> Self {
Self { root, focus }
}
}
pub fn find_in_direction(
focused: &PaneInfo,
direction: NavDirection,
panes: &[PaneInfo],
) -> Option<PaneId> {
let fr = focused.rect;
panes
.iter()
.filter(|p| p.id != focused.id)
.filter(|p| {
let r = p.rect;
match direction {
NavDirection::Left => {
r.x + r.width <= fr.x && ranges_overlap(r.y, r.height, fr.y, fr.height)
}
NavDirection::Right => {
r.x >= fr.x + fr.width && ranges_overlap(r.y, r.height, fr.y, fr.height)
}
NavDirection::Up => {
r.y + r.height <= fr.y && ranges_overlap(r.x, r.width, fr.x, fr.width)
}
NavDirection::Down => {
r.y >= fr.y + fr.height && ranges_overlap(r.x, r.width, fr.x, fr.width)
}
}
})
.min_by_key(|p| {
let r = p.rect;
match direction {
NavDirection::Left => fr.x.saturating_sub(r.x + r.width),
NavDirection::Right => r.x.saturating_sub(fr.x + fr.width),
NavDirection::Up => fr.y.saturating_sub(r.y + r.height),
NavDirection::Down => r.y.saturating_sub(fr.y + fr.height),
}
})
.map(|p| p.id)
}
fn ranges_overlap(a_start: u16, a_len: u16, b_start: u16, b_len: u16) -> bool {
a_start < b_start + b_len && a_start + a_len > b_start
}
fn count_panes(node: &Node) -> usize {
match node {
Node::Pane(_) => 1,
Node::Split { first, second, .. } => count_panes(first) + count_panes(second),
}
}
fn collect_panes(node: &Node, area: Rect, focus: PaneId, result: &mut Vec<PaneInfo>) {
match node {
Node::Pane(id) => {
result.push(PaneInfo {
id: *id,
rect: area,
inner_rect: area,
is_focused: *id == focus,
});
}
Node::Split {
direction,
ratio,
first,
second,
} => {
let (a, b) = split_rect(area, *direction, *ratio);
collect_panes(first, a, focus, result);
collect_panes(second, b, focus, result);
}
}
}
fn collect_splits(node: &Node, area: Rect, path: Vec<bool>, result: &mut Vec<SplitBorder>) {
if let Node::Split {
direction,
ratio,
first,
second,
} = node
{
let (a, b) = split_rect(area, *direction, *ratio);
let pos = match direction {
Direction::Horizontal => a.x + a.width,
Direction::Vertical => a.y + a.height,
};
result.push(SplitBorder {
pos,
direction: *direction,
area,
path: path.clone(),
});
let mut lp = path.clone();
lp.push(false);
collect_splits(first, a, lp, result);
let mut rp = path;
rp.push(true);
collect_splits(second, b, rp, result);
}
}
fn collect_ids(node: &Node, ids: &mut Vec<PaneId>) {
match node {
Node::Pane(id) => ids.push(*id),
Node::Split { first, second, .. } => {
collect_ids(first, ids);
collect_ids(second, ids);
}
}
}
fn split_at(node: Node, target: PaneId, direction: Direction, new_id: PaneId) -> Node {
match node {
Node::Pane(id) if id == target => Node::Split {
direction,
ratio: 0.5,
first: Box::new(Node::Pane(id)),
second: Box::new(Node::Pane(new_id)),
},
Node::Pane(_) => node,
Node::Split {
direction: d,
ratio,
first,
second,
} => Node::Split {
direction: d,
ratio,
first: Box::new(split_at(*first, target, direction, new_id)),
second: Box::new(split_at(*second, target, direction, new_id)),
},
}
}
fn remove_pane(node: Node, target: PaneId) -> Option<Node> {
match node {
Node::Pane(id) if id == target => None,
Node::Pane(_) => Some(node),
Node::Split {
direction,
ratio,
first,
second,
} => match (remove_pane(*first, target), remove_pane(*second, target)) {
(None, Some(s)) => Some(s),
(Some(f), None) => Some(f),
(Some(f), Some(s)) => Some(Node::Split {
direction,
ratio,
first: Box::new(f),
second: Box::new(s),
}),
(None, None) => None,
},
}
}
fn set_ratio_at(node: &mut Node, path: &[bool], new_ratio: f32) {
if let Node::Split {
ratio,
first,
second,
..
} = node
{
if path.is_empty() {
*ratio = new_ratio;
} else if path[0] {
set_ratio_at(second, &path[1..], new_ratio);
} else {
set_ratio_at(first, &path[1..], new_ratio);
}
}
}
fn get_ratio_at(node: &Node, path: &[bool]) -> Option<f32> {
if let Node::Split {
ratio,
first,
second,
..
} = node
{
if path.is_empty() {
Some(*ratio)
} else if path[0] {
get_ratio_at(second, &path[1..])
} else {
get_ratio_at(first, &path[1..])
}
} else {
None
}
}
fn split_rect(area: Rect, direction: Direction, ratio: f32) -> (Rect, Rect) {
match direction {
Direction::Horizontal => {
let first_w = ((area.width as f32) * ratio).round() as u16;
let second_w = area.width.saturating_sub(first_w);
(
Rect::new(area.x, area.y, first_w, area.height),
Rect::new(area.x + first_w, area.y, second_w, area.height),
)
}
Direction::Vertical => {
let first_h = ((area.height as f32) * ratio).round() as u16;
let second_h = area.height.saturating_sub(first_h);
(
Rect::new(area.x, area.y, area.width, first_h),
Rect::new(area.x, area.y + first_h, area.width, second_h),
)
}
}
}