//! Frontend-independent binary pane layout and spatial focus navigation. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)] #[serde(rename_all = "snake_case")] pub enum SplitAxis { /// Above/below. Horizontal, /// Left/right. Vertical, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PaneDirection { Left, Down, Up, Right, } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct PaneRect { pub x: u16, pub y: u16, pub width: u16, pub height: u16, } impl PaneRect { fn is_empty(self) -> bool { self.width == 0 || self.height == 0 } } /// Partition a rectangle into balanced halves, with the odd cell in the second. pub fn split_rect(bounds: PaneRect, axis: SplitAxis) -> [PaneRect; 2] { let mut first = bounds; let mut second = bounds; match axis { SplitAxis::Horizontal => { first.height = bounds.height / 2; second.y = bounds.y.saturating_add(first.height); second.height -= first.height; } SplitAxis::Vertical => { first.width = bounds.width / 2; second.x = bounds.x.saturating_add(first.width); second.width -= first.width; } } [first, second] } #[derive(Clone, Debug)] enum Node { Leaf(usize), Split { axis: SplitAxis, children: Box<[Node; 2]>, }, } impl Node { fn contains(&self, id: usize) -> bool { match self { Self::Leaf(leaf) => *leaf == id, Self::Split { children, .. } => children.iter().any(|child| child.contains(id)), } } fn first_id(&self) -> usize { match self { Self::Leaf(id) => *id, Self::Split { children, .. } => children[0].first_id(), } } fn split(&mut self, id: usize, new_id: usize, axis: SplitAxis) -> bool { match self { Self::Leaf(leaf) if *leaf == id => { *self = Self::Split { axis, children: Box::new([Self::Leaf(id), Self::Leaf(new_id)]), }; true } Self::Leaf(_) => false, Self::Split { children, .. } => children .iter_mut() .any(|child| child.split(id, new_id, axis)), } } fn close(&mut self, id: usize) -> Option { let Self::Split { children, .. } = self else { return None; }; for index in 0..2 { if matches!(children[index], Self::Leaf(leaf) if leaf == id) { let sibling = 1 - index; let focus = children[sibling].first_id(); *self = std::mem::replace(&mut children[sibling], Self::Leaf(id)); return Some(focus); } } children.iter_mut().find_map(|child| child.close(id)) } fn visit_rects(&self, bounds: PaneRect, visitor: &mut impl FnMut(usize, PaneRect)) { if bounds.is_empty() { return; } match self { Self::Leaf(id) => visitor(*id, bounds), Self::Split { axis, children } => { let halves = split_rect(bounds, *axis); children[0].visit_rects(halves[0], visitor); children[1].visit_rects(halves[1], visitor); } } } } #[derive(Clone, Debug)] pub struct PaneTree { root: Node, len: usize, } impl PaneTree { pub fn new(id: usize) -> Self { Self { root: Node::Leaf(id), len: 1, } } /// Keep `id` first and insert `new_id` second. IDs must be unique. pub fn split(&mut self, id: usize, new_id: usize, axis: SplitAxis) -> bool { if self.contains(new_id) || !self.root.split(id, new_id, axis) { return false; } self.len += 1; true } /// Close a leaf and expand its sibling; return the sibling's first leaf. /// Unknown IDs and closing the last leaf leave the tree unchanged. pub fn close(&mut self, id: usize) -> Option { let focus = self.root.close(id)?; self.len -= 1; Some(focus) } pub fn len(&self) -> usize { self.len } pub fn is_empty(&self) -> bool { false } pub fn contains(&self, id: usize) -> bool { self.root.contains(id) } /// Visit nonempty leaves in tree order, without allocating. pub fn visit_rects(&self, bounds: PaneRect, mut visitor: impl FnMut(usize, PaneRect)) { self.root.visit_rects(bounds, &mut visitor); } pub fn neighbor(&self, id: usize, direction: PaneDirection, bounds: PaneRect) -> Option { let mut current = None; self.visit_rects(bounds, |leaf, rect| { if leaf == id { current = Some(rect); } }); let current = current?; let mut best = None; self.visit_rects(bounds, |leaf, rect| { if leaf != id { consider_neighbor(current, direction, leaf, rect, &mut best); } }); best.map(|(_, leaf)| leaf) } } // Score: prefer perpendicular overlap, then the nearest forward edge, // perpendicular gap, center alignment, and finally the stable ID. type NeighborScore = (bool, u32, u32, u32, usize); fn neighbor_score( current: PaneRect, direction: PaneDirection, id: usize, candidate: PaneRect, ) -> Option { if current.is_empty() || candidate.is_empty() { return None; } let edges = |rect: PaneRect| { let x = u32::from(rect.x); let y = u32::from(rect.y); (x, y, x + u32::from(rect.width), y + u32::from(rect.height)) }; let (x, y, right, bottom) = edges(current); let (cx, cy, cright, cbottom) = edges(candidate); let (distance, start, end, cstart, cend) = match direction { PaneDirection::Left if cright <= x => (x - cright, y, bottom, cy, cbottom), PaneDirection::Right if cx >= right => (cx - right, y, bottom, cy, cbottom), PaneDirection::Up if cbottom <= y => (y - cbottom, x, right, cx, cright), PaneDirection::Down if cy >= bottom => (cy - bottom, x, right, cx, cright), _ => return None, }; let overlap = start.max(cstart) < end.min(cend); let gap = start.max(cstart).saturating_sub(end.min(cend)); let alignment = (start + end).abs_diff(cstart + cend); Some((!overlap, distance, gap, alignment, id)) } fn consider_neighbor( current: PaneRect, direction: PaneDirection, id: usize, candidate: PaneRect, best: &mut Option<(NeighborScore, usize)>, ) { if let Some(score) = neighbor_score(current, direction, id, candidate) && best.as_ref().is_none_or(|(previous, _)| score < *previous) { *best = Some((score, id)); } } /// Find a forward rectangle, favoring overlap and the closest edge. /// Empty rectangles are ignored; ties are independent of iterator order. pub fn directional_neighbor( current: PaneRect, direction: PaneDirection, candidates: impl Iterator, ) -> Option { let mut best = None; for (id, candidate) in candidates { consider_neighbor(current, direction, id, candidate, &mut best); } best.map(|(_, id)| id) } #[cfg(test)] #[path = "../tests/unit/panes.rs"] mod tests;