pub mod arrange;
pub mod input;
pub mod render;
pub mod runtime;
use std::cmp::Reverse;
use std::fmt;
use ratatui::layout::Rect;
const WEIGHT_UNIT: u32 = 120;
const RESIZE_STEP: u32 = 12;
const MIN_WEIGHT: u32 = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PaneId(u64);
impl fmt::Display for PaneId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "p{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Axis {
Columns,
Rows,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Left,
Right,
Up,
Down,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Resize {
Grow,
Shrink,
}
#[derive(Debug, Clone)]
enum Node {
Pane(PaneId),
Split(Split),
}
#[derive(Debug, Clone)]
struct Split {
axis: Axis,
parts: Vec<Part>,
}
#[derive(Debug, Clone)]
struct Part {
weight: u32,
node: Node,
}
#[derive(Debug, Clone)]
pub struct Tab {
title: String,
root: Node,
focus: PaneId,
}
impl Tab {
fn new(title: String, pane: PaneId) -> Self {
Tab {
title,
root: Node::Pane(pane),
focus: pane,
}
}
pub fn title(&self) -> &str {
&self.title
}
pub fn focus(&self) -> PaneId {
self.focus
}
pub fn pane_count(&self) -> usize {
fn count(node: &Node) -> usize {
match node {
Node::Pane(_) => 1,
Node::Split(split) => split.parts.iter().map(|part| count(&part.node)).sum(),
}
}
count(&self.root)
}
pub fn pane_ids(&self) -> Vec<PaneId> {
fn collect(node: &Node, out: &mut Vec<PaneId>) {
match node {
Node::Pane(id) => out.push(*id),
Node::Split(split) => {
for part in &split.parts {
collect(&part.node, out);
}
}
}
}
let mut panes = Vec::new();
collect(&self.root, &mut panes);
panes
}
pub fn pane_rects(&self, area: Rect) -> Vec<(PaneId, Rect)> {
fn solve(node: &Node, area: Rect, out: &mut Vec<(PaneId, Rect)>) {
match node {
Node::Pane(id) => out.push((*id, area)),
Node::Split(split) => {
let total: u64 = split.parts.iter().map(|part| u64::from(part.weight)).sum();
let extent = u64::from(match split.axis {
Axis::Columns => area.width,
Axis::Rows => area.height,
});
let mut cumulative: u64 = 0;
let mut previous: u16 = 0;
for part in &split.parts {
cumulative += u64::from(part.weight);
let boundary = ((extent * cumulative + total / 2) / total) as u16;
let size = boundary - previous;
let rect = match split.axis {
Axis::Columns => {
Rect::new(area.x + previous, area.y, size, area.height)
}
Axis::Rows => Rect::new(area.x, area.y + previous, area.width, size),
};
solve(&part.node, rect, out);
previous = boundary;
}
}
}
}
let mut out = Vec::new();
solve(&self.root, area, &mut out);
out
}
}
#[derive(Debug, Clone)]
pub struct Workspace {
name: String,
tabs: Vec<Tab>,
active: usize,
next_tab: u32,
}
impl Workspace {
pub fn name(&self) -> &str {
&self.name
}
pub fn tabs(&self) -> impl Iterator<Item = &Tab> {
self.tabs.iter()
}
pub fn tab_count(&self) -> usize {
self.tabs.len()
}
pub fn active_index(&self) -> usize {
self.active
}
pub fn active_tab(&self) -> Option<&Tab> {
self.tabs.get(self.active)
}
}
#[derive(Debug, Clone)]
pub struct WorkspaceState {
workspaces: Vec<Workspace>,
active: usize,
next_pane: u64,
next_workspace: u32,
}
impl Default for WorkspaceState {
fn default() -> Self {
Self::new()
}
}
impl WorkspaceState {
pub fn new() -> Self {
let mut state = WorkspaceState {
workspaces: Vec::new(),
active: 0,
next_pane: 1,
next_workspace: 1,
};
state.create_workspace();
state
}
pub fn is_empty(&self) -> bool {
self.workspaces.is_empty()
}
pub fn workspace_count(&self) -> usize {
self.workspaces.len()
}
pub fn active_index(&self) -> usize {
self.active
}
pub fn active_workspace(&self) -> Option<&Workspace> {
self.workspaces.get(self.active)
}
fn active_tab_mut(&mut self) -> Option<&mut Tab> {
let workspace = self.workspaces.get_mut(self.active)?;
workspace.tabs.get_mut(workspace.active)
}
pub fn active_tab(&self) -> Option<&Tab> {
self.active_workspace().and_then(Workspace::active_tab)
}
fn mint_pane(&mut self) -> PaneId {
let id = PaneId(self.next_pane);
self.next_pane += 1;
id
}
pub fn create_workspace(&mut self) -> PaneId {
let pane = self.mint_pane();
let name = self.next_workspace.to_string();
self.next_workspace += 1;
self.workspaces.push(Workspace {
name,
tabs: vec![Tab::new("1".to_owned(), pane)],
active: 0,
next_tab: 2,
});
self.active = self.workspaces.len() - 1;
pane
}
pub fn create_tab(&mut self) -> Option<PaneId> {
let pane = self.mint_pane();
let workspace = self.workspaces.get_mut(self.active)?;
let title = workspace.next_tab.to_string();
workspace.next_tab += 1;
workspace.tabs.push(Tab::new(title, pane));
workspace.active = workspace.tabs.len() - 1;
Some(pane)
}
pub fn split(&mut self, axis: Axis) -> Option<PaneId> {
self.active_tab_mut()?;
let pane = self.mint_pane();
let tab = self.active_tab_mut()?;
if split_at(&mut tab.root, tab.focus, axis, pane) {
tab.focus = pane;
Some(pane)
} else {
None
}
}
pub fn close_pane(&mut self) {
let Some(tab) = self.active_tab_mut() else {
return;
};
if matches!(tab.root, Node::Pane(_)) {
self.close_tab();
return;
}
if let Some(next) = remove_at(&mut tab.root, tab.focus) {
tab.focus = next;
}
}
pub fn close_tab(&mut self) {
let Some(workspace) = self.workspaces.get_mut(self.active) else {
return;
};
if workspace.tabs.len() <= 1 {
self.close_workspace();
return;
}
workspace.tabs.remove(workspace.active);
if workspace.active >= workspace.tabs.len() {
workspace.active = workspace.tabs.len() - 1;
}
}
pub fn close_workspace(&mut self) {
if self.workspaces.is_empty() {
return;
}
self.workspaces.remove(self.active);
if self.active >= self.workspaces.len() && !self.workspaces.is_empty() {
self.active = self.workspaces.len() - 1;
}
}
pub fn next_workspace(&mut self) {
if !self.workspaces.is_empty() {
self.active = (self.active + 1) % self.workspaces.len();
}
}
pub fn previous_workspace(&mut self) {
if !self.workspaces.is_empty() {
self.active = (self.active + self.workspaces.len() - 1) % self.workspaces.len();
}
}
pub fn select_workspace(&mut self, index: usize) {
if index < self.workspaces.len() {
self.active = index;
}
}
pub fn next_tab(&mut self) {
if let Some(workspace) = self.workspaces.get_mut(self.active)
&& !workspace.tabs.is_empty()
{
workspace.active = (workspace.active + 1) % workspace.tabs.len();
}
}
pub fn previous_tab(&mut self) {
if let Some(workspace) = self.workspaces.get_mut(self.active)
&& !workspace.tabs.is_empty()
{
workspace.active = (workspace.active + workspace.tabs.len() - 1) % workspace.tabs.len();
}
}
pub fn select_tab(&mut self, index: usize) {
if let Some(workspace) = self.workspaces.get_mut(self.active)
&& index < workspace.tabs.len()
{
workspace.active = index;
}
}
pub fn focus_pane(&mut self, id: PaneId) -> bool {
let Some(tab) = self.active_tab_mut() else {
return false;
};
if contains_pane(&tab.root, id) {
tab.focus = id;
true
} else {
false
}
}
pub fn move_focus(&mut self, dir: Direction, area: Rect) {
let Some(tab) = self.active_tab_mut() else {
return;
};
let rects = tab.pane_rects(area);
let Some(&(_, from)) = rects.iter().find(|(id, _)| *id == tab.focus) else {
return;
};
let next = rects
.iter()
.filter(|(id, rect)| {
*id != tab.focus && rect.width > 0 && rect.height > 0 && beyond(from, *rect, dir)
})
.map(|&(id, rect)| {
(
(
edge_gap(from, rect, dir),
Reverse(lateral_overlap(from, rect, dir)),
rect.y,
rect.x,
),
id,
)
})
.min_by_key(|&(key, _)| key)
.map(|(_, id)| id);
if let Some(id) = next {
tab.focus = id;
}
}
pub fn resize(&mut self, axis: Axis, action: Resize) {
let Some(tab) = self.active_tab_mut() else {
return;
};
resize_at(&mut tab.root, tab.focus, axis, action);
}
}
fn contains_pane(node: &Node, target: PaneId) -> bool {
match node {
Node::Pane(id) => *id == target,
Node::Split(split) => split
.parts
.iter()
.any(|part| contains_pane(&part.node, target)),
}
}
fn first_pane(node: &Node) -> PaneId {
match node {
Node::Pane(id) => *id,
Node::Split(split) => first_pane(&split.parts[0].node),
}
}
fn split_at(node: &mut Node, target: PaneId, axis: Axis, new: PaneId) -> bool {
match node {
Node::Pane(id) if *id == target => {
*node = Node::Split(Split {
axis,
parts: vec![
Part {
weight: WEIGHT_UNIT,
node: Node::Pane(target),
},
Part {
weight: WEIGHT_UNIT,
node: Node::Pane(new),
},
],
});
true
}
Node::Pane(_) => false,
Node::Split(split) => {
if split.axis == axis
&& let Some(index) = split
.parts
.iter()
.position(|part| matches!(&part.node, Node::Pane(id) if *id == target))
{
let weight = split.parts[index].weight;
let half = (weight / 2).max(1);
split.parts[index].weight = weight.saturating_sub(half).max(1);
split.parts.insert(
index + 1,
Part {
weight: half,
node: Node::Pane(new),
},
);
return true;
}
split
.parts
.iter_mut()
.any(|part| split_at(&mut part.node, target, axis, new))
}
}
}
fn remove_at(node: &mut Node, target: PaneId) -> Option<PaneId> {
let Node::Split(split) = node else {
return None;
};
if let Some(index) = split
.parts
.iter()
.position(|part| matches!(&part.node, Node::Pane(id) if *id == target))
{
split.parts.remove(index);
let neighbour = index.min(split.parts.len() - 1);
let next = first_pane(&split.parts[neighbour].node);
if split.parts.len() == 1 {
let only = split.parts.remove(0);
*node = only.node;
}
return Some(next);
}
for part in &mut split.parts {
if let Some(next) = remove_at(&mut part.node, target) {
return Some(next);
}
}
None
}
fn resize_at(node: &mut Node, target: PaneId, axis: Axis, action: Resize) -> bool {
let Node::Split(split) = node else {
return false;
};
let Some(index) = split
.parts
.iter()
.position(|part| contains_pane(&part.node, target))
else {
return false;
};
if resize_at(&mut split.parts[index].node, target, axis, action) {
return true;
}
if split.axis != axis || split.parts.len() < 2 {
return false;
}
let neighbour = if index + 1 < split.parts.len() {
index + 1
} else {
index - 1
};
let (donor, taker) = match action {
Resize::Grow => (neighbour, index),
Resize::Shrink => (index, neighbour),
};
let step = RESIZE_STEP.min(split.parts[donor].weight.saturating_sub(MIN_WEIGHT));
if step > 0 {
split.parts[donor].weight -= step;
split.parts[taker].weight += step;
}
true
}
fn beyond(from: Rect, to: Rect, dir: Direction) -> bool {
match dir {
Direction::Left => to.x.saturating_add(to.width) <= from.x,
Direction::Right => to.x >= from.x.saturating_add(from.width),
Direction::Up => to.y.saturating_add(to.height) <= from.y,
Direction::Down => to.y >= from.y.saturating_add(from.height),
}
}
fn edge_gap(from: Rect, to: Rect, dir: Direction) -> u16 {
match dir {
Direction::Left => from.x - (to.x + to.width),
Direction::Right => to.x - (from.x + from.width),
Direction::Up => from.y - (to.y + to.height),
Direction::Down => to.y - (from.y + from.height),
}
}
fn lateral_overlap(from: Rect, to: Rect, dir: Direction) -> u16 {
let (from_start, from_end, to_start, to_end) = match dir {
Direction::Left | Direction::Right => {
(from.y, from.y + from.height, to.y, to.y + to.height)
}
Direction::Up | Direction::Down => (from.x, from.x + from.width, to.x, to.x + to.width),
};
from_end
.min(to_end)
.saturating_sub(from_start.max(to_start))
}
#[cfg(test)]
mod tests {
use super::*;
const AREA: Rect = Rect {
x: 0,
y: 0,
width: 80,
height: 24,
};
fn rect_of(state: &WorkspaceState, id: PaneId) -> Rect {
let tab = state.active_tab().expect("active tab");
tab.pane_rects(AREA)
.into_iter()
.find(|(pane, _)| *pane == id)
.map(|(_, rect)| rect)
.unwrap_or_else(|| panic!("{id} has no rect"))
}
#[test]
fn workspace_the_opening_state_is_one_of_each() {
let state = WorkspaceState::new();
assert!(!state.is_empty());
assert_eq!(state.workspace_count(), 1);
let workspace = state.active_workspace().expect("workspace");
assert_eq!(workspace.name(), "1");
assert_eq!(workspace.tab_count(), 1);
let tab = workspace.active_tab().expect("tab");
assert_eq!(tab.title(), "1");
assert_eq!(tab.pane_count(), 1);
let rects = tab.pane_rects(AREA);
assert_eq!(rects.len(), 1);
assert_eq!(rects[0].1, AREA, "one pane fills the whole area");
assert_eq!(tab.focus(), rects[0].0);
}
#[test]
fn workspace_create_switches_to_the_new_workspace_and_tab() {
let mut state = WorkspaceState::new();
state.create_workspace();
assert_eq!(state.workspace_count(), 2);
assert_eq!(state.active_index(), 1);
assert_eq!(state.active_workspace().expect("workspace").name(), "2");
state.create_tab();
let workspace = state.active_workspace().expect("workspace");
assert_eq!(workspace.tab_count(), 2);
assert_eq!(workspace.active_index(), 1);
assert_eq!(workspace.active_tab().expect("tab").title(), "2");
}
#[test]
fn workspace_split_gives_the_new_pane_focus_and_half_the_share() {
let mut state = WorkspaceState::new();
let first = state.active_tab().expect("tab").focus();
state.split(Axis::Columns);
let tab = state.active_tab().expect("tab");
assert_eq!(tab.pane_count(), 2);
let second = tab.focus();
assert_ne!(second, first, "the new pane takes focus");
let left = rect_of(&state, first);
let right = rect_of(&state, second);
assert_eq!(left.width, 40);
assert_eq!(right.width, 40);
assert_eq!(right.x, 40, "the newcomer sits after the pane it split");
assert_eq!(left.height, AREA.height, "columns split shares every row");
}
#[test]
fn workspace_same_axis_split_joins_as_a_sibling_and_divides_the_split_pane_only() {
let mut state = WorkspaceState::new();
let first = state.active_tab().expect("tab").focus();
state.split(Axis::Columns);
let second = state.active_tab().expect("tab").focus();
state.split(Axis::Columns);
let third = state.active_tab().expect("tab").focus();
assert_eq!(rect_of(&state, first).width, 40);
assert_eq!(rect_of(&state, second).width, 20);
assert_eq!(rect_of(&state, third).width, 20);
for id in [first, second, third] {
assert_eq!(rect_of(&state, id).height, AREA.height);
}
}
#[test]
fn workspace_cross_axis_split_nests_inside_the_focused_pane() {
let mut state = WorkspaceState::new();
let first = state.active_tab().expect("tab").focus();
state.split(Axis::Columns);
let second = state.active_tab().expect("tab").focus();
state.split(Axis::Rows);
let third = state.active_tab().expect("tab").focus();
let left = rect_of(&state, first);
let top_right = rect_of(&state, second);
let bottom_right = rect_of(&state, third);
assert_eq!(left.height, AREA.height, "the other column is untouched");
assert_eq!(
top_right.x, bottom_right.x,
"the rows split stays in its column"
);
assert_eq!(top_right.height, 12);
assert_eq!(bottom_right.height, 12);
assert_eq!(bottom_right.y, 12);
}
#[test]
fn workspace_pane_rects_tile_the_area_exactly() {
let mut state = WorkspaceState::new();
state.split(Axis::Columns);
state.split(Axis::Rows);
state.split(Axis::Columns);
let tab = state.active_tab().expect("tab");
let area = Rect::new(3, 2, 77, 23);
let rects = tab.pane_rects(area);
assert_eq!(rects.len(), tab.pane_count());
let cells: u32 = rects
.iter()
.map(|(_, rect)| u32::from(rect.width) * u32::from(rect.height))
.sum();
assert_eq!(
cells,
u32::from(area.width) * u32::from(area.height),
"the panes cover every cell exactly once"
);
for (id, rect) in &rects {
assert!(
rect.x >= area.x
&& rect.y >= area.y
&& rect.x + rect.width <= area.x + area.width
&& rect.y + rect.height <= area.y + area.height,
"{id} at {rect:?} escapes {area:?}"
);
}
}
#[test]
fn workspace_close_pane_returns_focus_to_the_neighbour_that_absorbed_the_space() {
let mut state = WorkspaceState::new();
state.split(Axis::Columns);
let second = state.active_tab().expect("tab").focus();
state.split(Axis::Columns);
state.close_pane();
let tab = state.active_tab().expect("tab");
assert_eq!(tab.pane_count(), 2);
assert_eq!(
tab.focus(),
second,
"closing the last sibling focuses the one before it — the pane \
whose band absorbed the freed share"
);
assert_eq!(state.workspace_count(), 1, "the tab and workspace survive");
}
#[test]
fn workspace_closing_a_nested_pane_collapses_the_husk_split() {
let mut state = WorkspaceState::new();
let first = state.active_tab().expect("tab").focus();
state.split(Axis::Columns);
let second = state.active_tab().expect("tab").focus();
state.split(Axis::Rows);
state.close_pane();
let tab = state.active_tab().expect("tab");
assert_eq!(tab.pane_count(), 2);
assert_eq!(tab.focus(), second, "focus lands on the surviving sibling");
assert_eq!(rect_of(&state, second).height, AREA.height);
assert_eq!(rect_of(&state, first).height, AREA.height);
}
#[test]
fn workspace_the_close_cascade_ends_empty_and_stays_empty() {
let mut state = WorkspaceState::new();
state.create_tab();
state.close_pane();
assert_eq!(
state.active_workspace().expect("workspace").tab_count(),
1,
"the last pane of a tab closes the tab"
);
state.close_pane();
assert!(
state.is_empty(),
"the last pane of the last tab of the last workspace empties the surface"
);
state.close_pane();
state.close_tab();
state.close_workspace();
state.split(Axis::Rows);
state.create_tab();
state.next_workspace();
state.previous_tab();
state.resize(Axis::Columns, Resize::Grow);
state.move_focus(Direction::Left, AREA);
assert!(state.is_empty());
state.create_workspace();
assert!(!state.is_empty());
assert_eq!(
state.active_workspace().expect("workspace").name(),
"2",
"workspace names mint forward; a closed name is never reissued"
);
}
#[test]
fn workspace_close_tab_and_close_workspace_keep_a_valid_active_index() {
let mut state = WorkspaceState::new();
state.create_tab();
state.create_tab();
assert_eq!(
state.active_workspace().expect("workspace").active_index(),
2
);
state.close_tab();
let workspace = state.active_workspace().expect("workspace");
assert_eq!(workspace.tab_count(), 2);
assert_eq!(
workspace.active_index(),
1,
"closing the last tab activates the one now at the end"
);
state.create_workspace();
state.create_workspace();
state.close_workspace();
assert_eq!(state.workspace_count(), 2);
assert_eq!(state.active_index(), 1);
}
#[test]
fn workspace_navigation_wraps_in_both_directions() {
let mut state = WorkspaceState::new();
state.create_workspace();
state.create_workspace();
assert_eq!(state.active_index(), 2);
state.next_workspace();
assert_eq!(state.active_index(), 0, "next wraps forward");
state.previous_workspace();
assert_eq!(state.active_index(), 2, "previous wraps back");
state.create_tab();
state.next_tab();
assert_eq!(
state.active_workspace().expect("workspace").active_index(),
0,
"tab next wraps"
);
state.previous_tab();
assert_eq!(
state.active_workspace().expect("workspace").active_index(),
1,
"tab previous wraps"
);
state.select_tab(0);
assert_eq!(
state.active_workspace().expect("workspace").active_index(),
0
);
state.select_tab(9);
assert_eq!(
state.active_workspace().expect("workspace").active_index(),
0,
"selecting a tab that does not exist moves nothing"
);
}
#[test]
fn workspace_focus_pane_takes_a_live_target_and_refuses_a_stale_one() {
let mut state = WorkspaceState::new();
let first = state.active_tab().expect("tab").focus();
state.split(Axis::Columns);
let second = state.active_tab().expect("tab").focus();
assert!(state.focus_pane(first));
assert_eq!(state.active_tab().expect("tab").focus(), first);
state.focus_pane(second);
state.close_pane();
assert!(
!state.focus_pane(second),
"a closed pane's id must not move focus"
);
assert_eq!(state.active_tab().expect("tab").focus(), first);
}
#[test]
fn workspace_directional_focus_prefers_the_straight_across_neighbour() {
let mut state = WorkspaceState::new();
let p1 = state.active_tab().expect("tab").focus();
state.split(Axis::Columns);
let p2 = state.active_tab().expect("tab").focus();
state.split(Axis::Rows);
let p3 = state.active_tab().expect("tab").focus();
state.focus_pane(p1);
state.split(Axis::Rows);
let p4 = state.active_tab().expect("tab").focus();
state.move_focus(Direction::Right, AREA);
assert_eq!(
state.active_tab().expect("tab").focus(),
p3,
"bottom-left moves straight across to bottom-right, not diagonally"
);
state.move_focus(Direction::Up, AREA);
assert_eq!(state.active_tab().expect("tab").focus(), p2);
state.move_focus(Direction::Left, AREA);
assert_eq!(state.active_tab().expect("tab").focus(), p1);
state.move_focus(Direction::Down, AREA);
assert_eq!(state.active_tab().expect("tab").focus(), p4);
}
#[test]
fn workspace_focus_at_an_edge_stays_put() {
let mut state = WorkspaceState::new();
let first = state.active_tab().expect("tab").focus();
state.split(Axis::Columns);
state.focus_pane(first);
state.move_focus(Direction::Left, AREA);
assert_eq!(
state.active_tab().expect("tab").focus(),
first,
"no pane lies leftward, so focus does not move"
);
state.move_focus(Direction::Up, AREA);
assert_eq!(state.active_tab().expect("tab").focus(), first);
}
#[test]
fn workspace_resize_moves_the_shared_boundary_and_respects_the_floor() {
let mut state = WorkspaceState::new();
let first = state.active_tab().expect("tab").focus();
state.split(Axis::Columns);
let second = state.active_tab().expect("tab").focus();
let before = rect_of(&state, second).width;
state.resize(Axis::Columns, Resize::Grow);
let grown = rect_of(&state, second).width;
assert!(
grown > before,
"grow widens the focused pane ({before} -> {grown})"
);
assert_eq!(
rect_of(&state, first).width + grown,
AREA.width,
"the two panes still tile the row"
);
state.resize(Axis::Columns, Resize::Shrink);
assert_eq!(
rect_of(&state, second).width,
before,
"shrink undoes exactly one grow step"
);
for _ in 0..64 {
state.resize(Axis::Columns, Resize::Grow);
}
let floored = rect_of(&state, first).width;
assert!(
floored > 0,
"the floor keeps the donor visible at this size"
);
state.resize(Axis::Columns, Resize::Grow);
assert_eq!(
rect_of(&state, first).width,
floored,
"at the floor a further grow moves nothing"
);
}
#[test]
fn workspace_resize_on_an_axis_with_no_split_is_a_no_op() {
let mut state = WorkspaceState::new();
state.split(Axis::Columns);
let second = state.active_tab().expect("tab").focus();
let before = rect_of(&state, second);
state.resize(Axis::Rows, Resize::Grow);
assert_eq!(
rect_of(&state, second),
before,
"no rows split exists anywhere above the pane"
);
}
#[test]
fn workspace_resize_acts_at_the_nearest_enclosing_split_on_the_axis() {
let mut state = WorkspaceState::new();
let p1 = state.active_tab().expect("tab").focus();
state.split(Axis::Columns);
state.split(Axis::Rows);
state.split(Axis::Columns);
let p4 = state.active_tab().expect("tab").focus();
let outer_before = rect_of(&state, p1).width;
let inner_before = rect_of(&state, p4).width;
state.resize(Axis::Columns, Resize::Grow);
assert_eq!(
rect_of(&state, p1).width,
outer_before,
"the outer columns boundary does not move"
);
assert!(
rect_of(&state, p4).width > inner_before,
"the innermost columns split absorbs the step"
);
}
#[test]
fn workspace_pane_ids_are_never_reused() {
let mut state = WorkspaceState::new();
let mut seen = std::collections::BTreeSet::new();
seen.insert(state.active_tab().expect("tab").focus());
for _ in 0..8 {
state.split(Axis::Columns);
assert!(
seen.insert(state.active_tab().expect("tab").focus()),
"a fresh pane must carry a fresh id"
);
state.close_pane();
}
}
#[test]
fn workspace_a_zero_sized_area_still_answers_totally() {
let mut state = WorkspaceState::new();
state.split(Axis::Columns);
let tab = state.active_tab().expect("tab");
let rects = tab.pane_rects(Rect::new(0, 0, 0, 0));
assert_eq!(rects.len(), 2, "every pane still gets a rectangle");
assert!(rects.iter().all(|(_, rect)| rect.width == 0));
state.move_focus(Direction::Right, Rect::new(0, 0, 0, 0));
}
}