//! Panes: the screen split into rectangles, the way vim splits windows. //! //! The layout is a tree. A leaf is a pane; a split lays its parts side by //! side or one above the other, each with a share of the room. Splitting a //! pane halves its room, closing one gives its room to a neighbor, and //! `Ctrl-w =` evens the shares out. //! //! [`Panes`] holds what each pane shows (`T`: a view and a document, in the //! app) apart from the layout, so the layout logic stays plain and testable. use std::collections::BTreeMap; use ratatui::layout::{Position, Rect}; use crate::model::Direction; /// A pane, while it's open. Ids aren't reused. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PaneId(u32); /// How a split arranges its parts. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Arrange { /// Next to each other, left to right (`Ctrl-w v`, `:vsplit`). SideBySide, /// One above the other (`Ctrl-w s`, `:split`). Stacked, } /// A part's room when a split is made or evened out. Splitting halves a /// share, so it starts big enough to be halved many times over. const FULL_SHARE: u32 = 1 << 20; #[derive(Debug, Clone, PartialEq, Eq)] enum Layout { Pane(PaneId), Split { arrange: Arrange, parts: Vec }, } #[derive(Debug, Clone, PartialEq, Eq)] struct Part { layout: Layout, /// This part's room, relative to the other parts of its split. share: u32, } /// The panes, their layout, and which one has the focus. #[derive(Debug, Clone)] pub struct Panes { layout: Layout, /// What each pane shows. Every pane in the layout is here and the other /// way round, and `focused` is always one of them: every method keeps it /// so. contents: BTreeMap, focused: PaneId, next_id: u32, } impl Panes { /// One pane, showing `first`. pub fn new(first: T) -> Panes { let id = PaneId(0); Panes { layout: Layout::Pane(id), contents: BTreeMap::from([(id, first)]), focused: id, next_id: 1 } } /// The pane with the focus. pub fn focused(&self) -> PaneId { self.focused } /// What the focused pane shows. pub fn current(&self) -> &T { self.contents.get(&self.focused).expect("the focused pane is always open") } pub fn current_mut(&mut self) -> &mut T { self.contents.get_mut(&self.focused).expect("the focused pane is always open") } pub fn get(&self, id: PaneId) -> Option<&T> { self.contents.get(&id) } pub fn get_mut(&mut self, id: PaneId) -> Option<&mut T> { self.contents.get_mut(&id) } /// How many panes there are. pub fn count(&self) -> usize { self.contents.len() } /// Every pane and what it shows, in no particular order. pub fn iter_mut(&mut self) -> impl Iterator { self.contents.iter_mut().map(|(&id, content)| (id, content)) } /// Splits the focused pane in two. The new pane, showing `content`, /// comes first (left or above, as in vim) and takes the focus. pub fn split(&mut self, arrange: Arrange, content: T) -> PaneId { let id = PaneId(self.next_id); self.next_id += 1; self.contents.insert(id, content); let focused = self.focused; split_in(&mut self.layout, focused, id, arrange); self.focused = id; id } /// Closes the focused pane and gives its room to a neighbor, which takes /// the focus. The last pane can't close: `None`. pub fn close(&mut self) -> Option { if self.contents.len() < 2 { return None; } let closing = self.focused; let next = close_in(&mut self.layout, closing)?; self.focused = next; self.contents.remove(&closing) } /// Closes every pane but the focused one, and returns what they showed. pub fn only(&mut self) -> Vec { self.layout = Layout::Pane(self.focused); let others: Vec = self.contents.keys().copied().filter(|&id| id != self.focused).collect(); others.into_iter().filter_map(|id| self.contents.remove(&id)).collect() } /// Gives every part of every split the same room (`Ctrl-w =`). pub fn equalize(&mut self) { equalize(&mut self.layout); } /// Where each pane is in `area`, in reading order: left to right, top to /// bottom, the way the layout nests. pub fn areas(&self, area: Rect) -> Vec<(PaneId, Rect)> { let mut found = Vec::new(); areas(&self.layout, area, &mut found); found } /// The pane at a position on screen, if there's one. pub fn at(&self, area: Rect, position: Position) -> Option { self.areas(area).into_iter().find(|(_, rect)| rect.contains(position)).map(|(id, _)| id) } /// Gives the focus to a pane. Returns false if there's no such pane. pub fn focus(&mut self, id: PaneId) -> bool { let open = self.contents.contains_key(&id); if open { self.focused = id; } open } /// `Ctrl-w h j k l`: the focus goes to the nearest pane that way, the /// one beside the focused pane's top-left corner first. Returns false if /// there's none. pub fn focus_toward(&mut self, direction: Direction, area: Rect) -> bool { let areas = self.areas(area); let Some(&(_, from)) = areas.iter().find(|(id, _)| *id == self.focused) else { return false }; let best = areas .iter() .filter(|(id, _)| *id != self.focused) .filter_map(|&(id, rect)| Some((distance(from, rect, direction)?, id))) .min_by_key(|&(key, _)| key); match best { Some((_, id)) => self.focus(id), None => false, } } /// `Ctrl-w w`: the next pane in reading order, back to the first after /// the last. pub fn focus_next(&mut self) { let mut order = Vec::new(); panes_in(&self.layout, &mut order); let here = order.iter().position(|&id| id == self.focused).unwrap_or(0); if let Some(&next) = order.get((here + 1) % order.len().max(1)) { self.focused = next; } } } /// How far `to` is from `from` going `direction`, for picking the pane to /// focus: `None` unless it's that way and level with `from`. Nearer wins, /// then the one beside `from`'s top-left corner, then the top-most or /// left-most. fn distance(from: Rect, to: Rect, direction: Direction) -> Option<(u16, bool, u16)> { let level_rows = to.top() < from.bottom() && from.top() < to.bottom(); let level_cols = to.left() < from.right() && from.left() < to.right(); let (gap, level, beside, along) = match direction { Direction::Left => { (from.left().checked_sub(to.right())?, level_rows, row_in(from.top(), to), to.top()) } Direction::Right => { (to.left().checked_sub(from.right())?, level_rows, row_in(from.top(), to), to.top()) } Direction::Up => { (from.top().checked_sub(to.bottom())?, level_cols, col_in(from.left(), to), to.left()) } Direction::Down => { (to.top().checked_sub(from.bottom())?, level_cols, col_in(from.left(), to), to.left()) } }; level.then_some((gap, !beside, along)) } fn row_in(row: u16, rect: Rect) -> bool { rect.top() <= row && row < rect.bottom() } fn col_in(col: u16, rect: Rect) -> bool { rect.left() <= col && col < rect.right() } /// Splits pane `target` in `layout`, putting `new` before it. fn split_in(layout: &mut Layout, target: PaneId, new: PaneId, arrange: Arrange) -> bool { match layout { Layout::Pane(id) if *id == target => { let old = Part { layout: Layout::Pane(target), share: FULL_SHARE }; let part = Part { layout: Layout::Pane(new), share: FULL_SHARE }; *layout = Layout::Split { arrange, parts: vec![part, old] }; true } Layout::Pane(_) => false, Layout::Split { arrange: own, parts } => { let same_way = *own == arrange; for index in 0..parts.len() { let is_target = parts[index].layout == Layout::Pane(target); if is_target && same_way { // Same way as this split: the new pane joins it, and the // two share the old pane's room. let share = parts[index].share; parts[index].share = share - share / 2; let part = Part { layout: Layout::Pane(new), share: (share / 2).max(1) }; parts.insert(index, part); return true; } if split_in(&mut parts[index].layout, target, new, arrange) { return true; } } false } } } /// Takes pane `target` out of `layout`, giving its room to the part before /// it (or after it, for a first part). Returns the pane that gets the /// focus. fn close_in(layout: &mut Layout, target: PaneId) -> Option { let Layout::Split { parts, .. } = layout else { return None }; let index = parts.iter().position(|part| part.layout == Layout::Pane(target)); let next = match index { Some(index) => { let removed = parts.remove(index); let neighbor = index.saturating_sub(1).min(parts.len().saturating_sub(1)); let part = parts.get_mut(neighbor)?; part.share = part.share.saturating_add(removed.share); first_pane(&part.layout) } None => parts.iter_mut().find_map(|part| close_in(&mut part.layout, target))?, }; // A split left with one part is just that part. if parts.len() == 1 && let Some(only) = parts.pop() { *layout = only.layout; } flatten(layout); Some(next) } /// A split inside a split that arranges the same way joins its parent, so /// `Ctrl-w v` twice gives three panes side by side, not a nest. fn flatten(layout: &mut Layout) { let Layout::Split { arrange, parts } = layout else { return }; let mut flat = Vec::with_capacity(parts.len()); for mut part in std::mem::take(parts) { flatten(&mut part.layout); match part.layout { Layout::Split { arrange: inner, parts: inner_parts } if inner == *arrange => { let total: u64 = inner_parts.iter().map(|inner| u64::from(inner.share)).sum::().max(1); for mut inner_part in inner_parts { let share = u64::from(part.share) * u64::from(inner_part.share) / total; inner_part.share = u32::try_from(share).unwrap_or(u32::MAX).max(1); flat.push(inner_part); } } layout => flat.push(Part { layout, ..part }), } } *parts = flat; } fn equalize(layout: &mut Layout) { if let Layout::Split { parts, .. } = layout { for part in parts { part.share = FULL_SHARE; equalize(&mut part.layout); } } } fn first_pane(layout: &Layout) -> PaneId { match layout { Layout::Pane(id) => *id, Layout::Split { parts, .. } => parts.first().map_or(PaneId(0), |part| first_pane(&part.layout)), } } fn panes_in(layout: &Layout, found: &mut Vec) { match layout { Layout::Pane(id) => found.push(*id), Layout::Split { parts, .. } => parts.iter().for_each(|part| panes_in(&part.layout, found)), } } /// Lays `layout` out in `area`: each part gets room in proportion to its /// share, and the last one takes what rounding leaves. fn areas(layout: &Layout, area: Rect, found: &mut Vec<(PaneId, Rect)>) { let (arrange, parts) = match layout { Layout::Pane(id) => return found.push((*id, area)), Layout::Split { arrange, parts } => (*arrange, parts), }; let length = match arrange { Arrange::SideBySide => area.width, Arrange::Stacked => area.height, }; let total: u64 = parts.iter().map(|part| u64::from(part.share)).sum::().max(1); let mut start = 0u16; for (index, part) in parts.iter().enumerate() { let size = if index + 1 == parts.len() { length - start } else { let size = u64::from(length) * u64::from(part.share) / total; u16::try_from(size).unwrap_or(length).min(length - start) }; let rect = match arrange { Arrange::SideBySide => Rect::new(area.x + start, area.y, size, area.height), Arrange::Stacked => Rect::new(area.x, area.y + start, area.width, size), }; areas(&part.layout, rect, found); start += size; } } #[cfg(test)] mod tests { use super::*; const SCREEN: Rect = Rect { x: 0, y: 0, width: 80, height: 20 }; fn rects<'a>(panes: &Panes<&'a str>) -> Vec<(&'a str, Rect)> { panes.areas(SCREEN).into_iter().map(|(id, rect)| (*panes.get(id).unwrap(), rect)).collect() } #[test] fn splits_halve_the_pane_and_the_new_one_comes_first() { let mut panes = Panes::new("canvas"); panes.split(Arrange::SideBySide, "outline"); assert_eq!( rects(&panes), [("outline", Rect::new(0, 0, 40, 20)), ("canvas", Rect::new(40, 0, 40, 20))] ); assert_eq!(*panes.current(), "outline", "the new pane has the focus"); panes.split(Arrange::Stacked, "note"); assert_eq!( rects(&panes), [ ("note", Rect::new(0, 0, 40, 10)), ("outline", Rect::new(0, 10, 40, 10)), ("canvas", Rect::new(40, 0, 40, 20)), ] ); // Side by side again, from the canvas: it joins the top split. panes.focus_toward(Direction::Right, SCREEN); panes.split(Arrange::SideBySide, "board"); let widths: Vec<(&str, u16)> = rects(&panes).into_iter().map(|(name, rect)| (name, rect.width)).collect(); assert_eq!(widths, [("note", 40), ("outline", 40), ("board", 20), ("canvas", 20)]); } #[test] fn focus_moves_to_the_pane_that_way() { let mut panes = Panes::new("right"); panes.split(Arrange::SideBySide, "top left"); panes.split(Arrange::Stacked, "top left 2"); // top left 2 above top left, both left of right. assert_eq!(*panes.current(), "top left 2"); assert!(panes.focus_toward(Direction::Down, SCREEN)); assert_eq!(*panes.current(), "top left"); assert!(panes.focus_toward(Direction::Right, SCREEN)); assert_eq!(*panes.current(), "right"); assert!(panes.focus_toward(Direction::Left, SCREEN)); assert_eq!(*panes.current(), "top left 2", "level with the top: the top one"); assert!(!panes.focus_toward(Direction::Up, SCREEN), "nothing above"); panes.focus_next(); assert_eq!(*panes.current(), "top left"); panes.focus_next(); panes.focus_next(); assert_eq!(*panes.current(), "top left 2", "after the last, the first"); } #[test] fn closing_gives_the_room_to_a_neighbor() { let mut panes = Panes::new("canvas"); panes.split(Arrange::SideBySide, "outline"); panes.split(Arrange::Stacked, "note"); assert_eq!(panes.close(), Some("note")); assert_eq!(*panes.current(), "outline", "the neighbor takes the focus"); assert_eq!( rects(&panes), [("outline", Rect::new(0, 0, 40, 20)), ("canvas", Rect::new(40, 0, 40, 20))] ); assert_eq!(panes.close(), Some("outline")); assert_eq!(panes.close(), None, "the last pane stays"); assert_eq!(rects(&panes), [("canvas", SCREEN)]); } #[test] fn only_and_even_sizes() { let mut panes = Panes::new("a"); panes.split(Arrange::SideBySide, "b"); panes.split(Arrange::SideBySide, "c"); let widths = |panes: &Panes<&str>| rects(panes).into_iter().map(|(_, rect)| rect.width).collect::>(); assert_eq!(widths(&panes), [20, 20, 40]); panes.equalize(); assert_eq!(widths(&panes), [26, 26, 28]); let mut closed = panes.only(); closed.sort_unstable(); assert_eq!(closed, ["a", "b"]); assert_eq!(rects(&panes), [("c", SCREEN)]); assert_eq!(panes.at(SCREEN, Position::new(70, 3)), Some(panes.focused())); } }