aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-07-27 12:26:02 +0200
committerGitHub <noreply@github.com>2026-07-27 12:26:02 +0200
commit828a1508c6366049ab3756aaba7631f7a4e5a81c (patch)
treea579ef49d57d340d604771bf1b3ce680f9dd6bc5
parentfix(windows): prefer PATHEXT variants over bare filename in resolver (#5364) (diff)
downloadsl0ppij-828a1508c6366049ab3756aaba7631f7a4e5a81c.tar.gz
sl0ppij-828a1508c6366049ab3756aaba7631f7a4e5a81c.tar.xz
sl0ppij-828a1508c6366049ab3756aaba7631f7a4e5a81c.zip
chore: replace unmaintained cassowary with maintained fork kasuari (#5416)
* chore: replace unmaintained cassowary with maintained fork kasuari * add pr
-rw-r--r--CHANGELOG.md1
-rw-r--r--Cargo.lock37
-rw-r--r--zellij-server/Cargo.toml2
-rw-r--r--zellij-server/src/panes/tiled_panes/mod.rs8
-rw-r--r--zellij-server/src/panes/tiled_panes/pane_resizer.rs21
-rw-r--r--zellij-server/src/panes/tiled_panes/unit/mock_pane.rs368
-rw-r--r--zellij-server/src/panes/tiled_panes/unit/pane_resizer_tests.rs911
-rw-r--r--zellij-server/src/tab/unit/tab_tests.rs4
8 files changed, 1330 insertions, 22 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6db75732..cc63e07f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* fix: properly pad lines before CUF (https://github.com/zellij-org/zellij/pull/5377)
* fix: clear existing pane name when entering rename input (https://github.com/zellij-org/zellij/pull/5369)
* fix: occasional input spam when pressing ESC and holding the mouse over slow SSH connections (https://github.com/zellij-org/zellij/pull/5323)
+* chore: switch cassowary deps to maintained fork kasuari (https://github.com/zellij-org/zellij/pull/5416)
## [0.44.3] - 2026-05-13
* fix(windows): bump windows-sys to 0.59 to align manifest with code, fixing source builds via `cargo install`/`cargo binstall` (https://github.com/zellij-org/zellij/pull/5139)
diff --git a/Cargo.lock b/Cargo.lock
index 595fb986..01298b94 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -439,12 +439,6 @@ dependencies = [
]
[[package]]
-name = "cassowary"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
-
-[[package]]
name = "castaway"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1085,6 +1079,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1355,7 +1355,18 @@ version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
dependencies = [
- "foldhash",
+ "foldhash 0.1.4",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.2.0",
]
[[package]]
@@ -1866,6 +1877,16 @@ dependencies = [
]
[[package]]
+name = "kasuari"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899"
+dependencies = [
+ "hashbrown 0.16.1",
+ "thiserror 2.0.12",
+]
+
+[[package]]
name = "kdl"
version = "4.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5044,7 +5065,6 @@ dependencies = [
"base64 0.13.0",
"byteorder",
"bytes",
- "cassowary",
"chrono",
"close_fds",
"crossbeam",
@@ -5053,6 +5073,7 @@ dependencies = [
"insta",
"interprocess",
"isahc",
+ "kasuari",
"kdl",
"lazy_static",
"libc",
diff --git a/zellij-server/Cargo.toml b/zellij-server/Cargo.toml
index 0f04acef..859b7433 100644
--- a/zellij-server/Cargo.toml
+++ b/zellij-server/Cargo.toml
@@ -16,12 +16,12 @@ async-trait = { version = "0.1.50", default-features = false }
base64 = { version = "0.13.0", default-features = false, features = ["std"] }
byteorder = { version = "1.4.3", default-features = false, features = ["std"] }
bytes = { version = "1.6.0", default-features = false, features = ["std"] }
-cassowary = { version = "0.3.0", default-features = false }
chrono = { version = "0.4.19", default-features = false, features = ["std", "clock"] }
crossbeam = { version = "0.8", default-features = false, features = ["crossbeam-channel", "std"] }
highway = { version = "0.6.4", default-features = false, features = ["std"] }
interprocess = { workspace = true }
isahc = { workspace = true }
+kasuari = { version = "0.4.12", default-features = false }
kdl = { workspace = true }
lazy_static = { workspace = true }
log = { workspace = true }
diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs
index 88bec53a..9bc67713 100644
--- a/zellij-server/src/panes/tiled_panes/mod.rs
+++ b/zellij-server/src/panes/tiled_panes/mod.rs
@@ -2,6 +2,14 @@ mod pane_resizer;
mod stacked_panes;
mod tiled_pane_grid;
+#[cfg(test)]
+#[path = "./unit/mock_pane.rs"]
+mod pane_resizer_test_mock;
+
+#[cfg(test)]
+#[path = "./unit/pane_resizer_tests.rs"]
+mod pane_resizer_tests;
+
use crate::resize_pty;
use tiled_pane_grid::{split, TiledPaneGrid, RESIZE_PERCENT};
diff --git a/zellij-server/src/panes/tiled_panes/pane_resizer.rs b/zellij-server/src/panes/tiled_panes/pane_resizer.rs
index 506481d4..8453fa76 100644
--- a/zellij-server/src/panes/tiled_panes/pane_resizer.rs
+++ b/zellij-server/src/panes/tiled_panes/pane_resizer.rs
@@ -1,10 +1,6 @@
use super::stacked_panes::StackedPanes;
use crate::{panes::PaneId, tab::Pane};
-use cassowary::{
- strength::{REQUIRED, STRONG},
- Expression, Solver, Variable,
- WeightedRelation::EQ,
-};
+use kasuari::{Expression, Solver, Strength, Variable, WeightedRelation::EQ};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
@@ -74,7 +70,7 @@ impl<'a> PaneResizer<'a> {
.collect();
self.solver
- .add_constraints(&constraints)
+ .add_constraints(constraints)
.map_err(|e| format!("{:?}", e))?;
Ok(grid)
@@ -307,7 +303,7 @@ impl<'a> PaneResizer<'a> {
}
}
-fn constrain_spans(space: usize, spans: &[Span]) -> HashSet<cassowary::Constraint> {
+fn constrain_spans(space: usize, spans: &[Span]) -> HashSet<kasuari::Constraint> {
let mut constraints = HashSet::new();
// Calculating "flexible" space (space not consumed by fixed-size spans)
@@ -323,14 +319,17 @@ fn constrain_spans(space: usize, spans: &[Span]) -> HashSet<cassowary::Constrain
let full_size = spans
.iter()
.fold(Expression::from_constant(0.0), |acc, s| acc + s.size_var);
- constraints.insert(full_size.clone() | EQ(REQUIRED) | space as f64);
+ constraints.insert(full_size.clone() | EQ(Strength::REQUIRED) | space as f64);
// Try to maintain ratios and lock non-flexible sizes
for span in spans {
match span.size.constraint {
- Constraint::Fixed(s) => constraints.insert(span.size_var | EQ(REQUIRED) | s as f64),
- Constraint::Percent(p) => constraints
- .insert((span.size_var / new_flex_space as f64) | EQ(STRONG) | (p / 100.0)),
+ Constraint::Fixed(s) => {
+ constraints.insert(span.size_var | EQ(Strength::REQUIRED) | s as f64)
+ },
+ Constraint::Percent(p) => constraints.insert(
+ (span.size_var / new_flex_space as f64) | EQ(Strength::STRONG) | (p / 100.0),
+ ),
};
}
diff --git a/zellij-server/src/panes/tiled_panes/unit/mock_pane.rs b/zellij-server/src/panes/tiled_panes/unit/mock_pane.rs
new file mode 100644
index 00000000..c21f5297
--- /dev/null
+++ b/zellij-server/src/panes/tiled_panes/unit/mock_pane.rs
@@ -0,0 +1,368 @@
+#![allow(dead_code)]
+
+use super::pane_resizer::PaneResizer;
+use crate::panes::PaneId;
+use crate::tab::Pane;
+use crate::ui::pane_boundaries_frame::FrameParams;
+use crate::{
+ output::{CharacterChunk, SixelImageChunk},
+ pty::VteBytes,
+ ClientId,
+};
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::rc::Rc;
+use std::time::Instant;
+use zellij_utils::data::{InputMode, PaletteColor, PaneContents};
+use zellij_utils::errors::prelude::*;
+use zellij_utils::input::layout::Run as LayoutRun;
+use zellij_utils::input::layout::SplitDirection;
+use zellij_utils::pane_size::Offset;
+use zellij_utils::pane_size::{Dimension, PaneGeom};
+
+#[derive(Clone, Copy, Debug)]
+pub struct PaneSpec {
+ pub id: PaneId,
+ pub x: usize,
+ pub y: usize,
+ pub cols: Dimension,
+ pub rows: Dimension,
+ pub stacked: Option<usize>,
+ pub logical_position: Option<usize>,
+ pub geom_override: Option<PaneGeom>,
+}
+
+impl PaneSpec {
+ pub fn geom(&self) -> PaneGeom {
+ PaneGeom {
+ x: self.x,
+ y: self.y,
+ rows: self.rows,
+ cols: self.cols,
+ stacked: self.stacked,
+ is_pinned: false,
+ logical_position: self.logical_position,
+ }
+ }
+}
+
+pub fn dim_pct(percent: f64, inner: usize) -> Dimension {
+ let mut dimension = Dimension::percent(percent);
+ dimension.set_inner(inner);
+ dimension
+}
+
+pub fn dim_fixed(size: usize) -> Dimension {
+ Dimension::fixed(size)
+}
+
+pub fn spec(id: u32, x: usize, y: usize, cols: Dimension, rows: Dimension) -> PaneSpec {
+ PaneSpec {
+ id: PaneId::Terminal(id),
+ x,
+ y,
+ cols,
+ rows,
+ stacked: None,
+ logical_position: None,
+ geom_override: None,
+ }
+}
+
+pub fn spec_stacked(
+ id: u32,
+ x: usize,
+ y: usize,
+ cols: Dimension,
+ rows: Dimension,
+ stack_id: usize,
+) -> PaneSpec {
+ PaneSpec {
+ id: PaneId::Terminal(id),
+ x,
+ y,
+ cols,
+ rows,
+ stacked: Some(stack_id),
+ logical_position: None,
+ geom_override: None,
+ }
+}
+
+pub fn with_override(base: PaneSpec, geom_override: PaneGeom) -> PaneSpec {
+ PaneSpec {
+ geom_override: Some(geom_override),
+ ..base
+ }
+}
+
+pub struct MockPane {
+ pid: PaneId,
+ geom: PaneGeom,
+ geom_override: Option<PaneGeom>,
+}
+
+impl MockPane {
+ pub fn from_spec(spec: PaneSpec) -> Self {
+ MockPane {
+ pid: spec.id,
+ geom: spec.geom(),
+ geom_override: spec.geom_override,
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct PaneSnapshot {
+ pub id: PaneId,
+ pub geom: PaneGeom,
+ pub current: PaneGeom,
+ pub geom_override: Option<PaneGeom>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum Outcome {
+ Ok,
+ PaneSizeUnchanged,
+ OtherErr(String),
+}
+
+#[derive(Clone, Debug)]
+pub struct Run {
+ pub outcome: Outcome,
+ pub panes: Vec<PaneSnapshot>,
+}
+
+pub fn run_layout(specs: &[PaneSpec], direction: SplitDirection, space: usize) -> Run {
+ let mut owned: Vec<Box<dyn Pane>> = specs
+ .iter()
+ .map(|s| Box::new(MockPane::from_spec(*s)) as Box<dyn Pane>)
+ .collect();
+
+ let mut map: HashMap<PaneId, &mut Box<dyn Pane>> = HashMap::new();
+ for (s, b) in specs.iter().zip(owned.iter_mut()) {
+ map.insert(s.id, b);
+ }
+ let map = Rc::new(RefCell::new(map));
+
+ let result = PaneResizer::new(map.clone()).layout(direction, space);
+
+ let outcome = match result {
+ Ok(()) => Outcome::Ok,
+ Err(e) => match e.downcast_ref::<ZellijError>() {
+ Some(ZellijError::PaneSizeUnchanged) => Outcome::PaneSizeUnchanged,
+ _ => Outcome::OtherErr(format!("{:#}", e)),
+ },
+ };
+
+ let mut panes: Vec<PaneSnapshot> = map
+ .borrow()
+ .iter()
+ .map(|(id, p)| PaneSnapshot {
+ id: *id,
+ geom: p.position_and_size(),
+ current: p.current_geom(),
+ geom_override: p.geom_override(),
+ })
+ .collect();
+ panes.sort_by_key(|s| s.id);
+
+ Run { outcome, panes }
+}
+
+impl Pane for MockPane {
+ fn x(&self) -> usize {
+ unimplemented!()
+ }
+ fn y(&self) -> usize {
+ unimplemented!()
+ }
+ fn rows(&self) -> usize {
+ unimplemented!()
+ }
+ fn cols(&self) -> usize {
+ unimplemented!()
+ }
+ fn get_content_x(&self) -> usize {
+ unimplemented!()
+ }
+ fn get_content_y(&self) -> usize {
+ unimplemented!()
+ }
+ fn get_content_columns(&self) -> usize {
+ unimplemented!()
+ }
+ fn get_content_rows(&self) -> usize {
+ unimplemented!()
+ }
+ fn reset_size_and_position_override(&mut self) {
+ unimplemented!()
+ }
+ fn set_geom(&mut self, position_and_size: PaneGeom) {
+ self.geom = position_and_size;
+ }
+ fn set_geom_override(&mut self, pane_geom: PaneGeom) {
+ self.geom_override = Some(pane_geom);
+ }
+ fn handle_pty_bytes(&mut self, _bytes: VteBytes) {
+ unimplemented!()
+ }
+ fn handle_plugin_bytes(&mut self, _client_id: ClientId, _bytes: VteBytes) {
+ unimplemented!()
+ }
+ fn cursor_coordinates(&self, _client_id: Option<ClientId>) -> Option<(usize, usize, bool)> {
+ unimplemented!()
+ }
+ fn position_and_size(&self) -> PaneGeom {
+ self.geom
+ }
+ fn current_geom(&self) -> PaneGeom {
+ self.geom_override.unwrap_or(self.geom)
+ }
+ fn geom_override(&self) -> Option<PaneGeom> {
+ self.geom_override
+ }
+ fn should_render(&self) -> bool {
+ unimplemented!()
+ }
+ fn set_should_render(&mut self, _should_render: bool) {
+ unimplemented!()
+ }
+ fn set_should_render_boundaries(&mut self, _should_render: bool) {
+ unimplemented!()
+ }
+ fn selectable(&self) -> bool {
+ unimplemented!()
+ }
+ fn set_selectable(&mut self, _selectable: bool) {
+ unimplemented!()
+ }
+ fn render(
+ &mut self,
+ _client_id: Option<ClientId>,
+ ) -> Result<Option<(Vec<CharacterChunk>, Option<String>, Vec<SixelImageChunk>)>> {
+ unimplemented!()
+ }
+ fn render_frame(
+ &mut self,
+ _client_id: ClientId,
+ _frame_params: FrameParams,
+ _input_mode: InputMode,
+ ) -> Result<Option<(Vec<CharacterChunk>, Option<String>)>> {
+ unimplemented!()
+ }
+ fn render_fake_cursor(
+ &mut self,
+ _cursor_color: PaletteColor,
+ _text_color: PaletteColor,
+ ) -> Option<String> {
+ unimplemented!()
+ }
+ fn render_terminal_title(&mut self, _input_mode: InputMode) -> String {
+ unimplemented!()
+ }
+ fn update_name(&mut self, _name: &str) {
+ unimplemented!()
+ }
+ fn pid(&self) -> PaneId {
+ self.pid
+ }
+ fn reduce_height(&mut self, _percent: f64) {
+ unimplemented!()
+ }
+ fn increase_height(&mut self, _percent: f64) {
+ unimplemented!()
+ }
+ fn reduce_width(&mut self, _percent: f64) {
+ unimplemented!()
+ }
+ fn increase_width(&mut self, _percent: f64) {
+ unimplemented!()
+ }
+ fn push_down(&mut self, _count: usize) {
+ unimplemented!()
+ }
+ fn push_right(&mut self, _count: usize) {
+ unimplemented!()
+ }
+ fn pull_left(&mut self, _count: usize) {
+ unimplemented!()
+ }
+ fn pull_up(&mut self, _count: usize) {
+ unimplemented!()
+ }
+ fn clear_screen(&mut self) {
+ unimplemented!()
+ }
+ fn scroll_up(&mut self, _count: usize, _client_id: ClientId) {
+ unimplemented!()
+ }
+ fn scroll_down(&mut self, _count: usize, _client_id: ClientId) {
+ unimplemented!()
+ }
+ fn clear_scroll(&mut self) {
+ unimplemented!()
+ }
+ fn is_scrolled(&self) -> bool {
+ unimplemented!()
+ }
+ fn active_at(&self) -> Instant {
+ unimplemented!()
+ }
+ fn set_active_at(&mut self, _instant: Instant) {
+ unimplemented!()
+ }
+ fn set_frame(&mut self, _frame: bool) {
+ unimplemented!()
+ }
+ fn set_content_offset(&mut self, _offset: Offset) {
+ unimplemented!()
+ }
+ fn store_pane_name(&mut self) {
+ unimplemented!()
+ }
+ fn load_pane_name(&mut self) {
+ unimplemented!()
+ }
+ fn set_borderless(&mut self, _borderless: bool) {
+ unimplemented!()
+ }
+ fn borderless(&self) -> bool {
+ unimplemented!()
+ }
+ fn set_exclude_from_sync(&mut self, _exclude_from_sync: bool) {
+ unimplemented!()
+ }
+ fn exclude_from_sync(&self) -> bool {
+ unimplemented!()
+ }
+ fn add_red_pane_frame_color_override(&mut self, _error_text: Option<String>) {
+ unimplemented!()
+ }
+ fn clear_pane_frame_color_override(&mut self, _client_id: Option<ClientId>) {
+ unimplemented!()
+ }
+ fn frame_color_override(&self) -> Option<PaletteColor> {
+ unimplemented!()
+ }
+ fn invoked_with(&self) -> &Option<LayoutRun> {
+ unimplemented!()
+ }
+ fn set_title(&mut self, _title: String) {
+ unimplemented!()
+ }
+ fn current_title(&self) -> String {
+ unimplemented!()
+ }
+ fn custom_title(&self) -> Option<String> {
+ unimplemented!()
+ }
+ fn pane_contents(
+ &self,
+ _client_id: Option<ClientId>,
+ _get_full_scrollback: bool,
+ _max_scrollback_lines: Option<usize>,
+ ) -> PaneContents {
+ unimplemented!()
+ }
+}
diff --git a/zellij-server/src/panes/tiled_panes/unit/pane_resizer_tests.rs b/zellij-server/src/panes/tiled_panes/unit/pane_resizer_tests.rs
new file mode 100644
index 00000000..c21a14f5
--- /dev/null
+++ b/zellij-server/src/panes/tiled_panes/unit/pane_resizer_tests.rs
@@ -0,0 +1,911 @@
+#![allow(dead_code)]
+
+use super::pane_resizer_test_mock::{
+ dim_fixed, dim_pct, run_layout, spec, spec_stacked, with_override, Outcome, PaneSnapshot,
+ PaneSpec, Run,
+};
+use crate::panes::PaneId;
+use std::collections::BTreeMap;
+use zellij_utils::input::layout::SplitDirection;
+use zellij_utils::pane_size::{Constraint, PaneGeom};
+
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+enum Class {
+ A,
+ B,
+ Artefact,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+enum Axis {
+ Cols,
+ Rows,
+}
+
+struct Fixture {
+ name: &'static str,
+ specs: Vec<PaneSpec>,
+ direction: SplitDirection,
+ space: usize,
+ class: Class,
+ bands: Vec<Vec<PaneId>>,
+}
+
+impl Fixture {
+ fn axis(&self) -> Axis {
+ match self.direction {
+ SplitDirection::Horizontal => Axis::Cols,
+ SplitDirection::Vertical => Axis::Rows,
+ }
+ }
+ fn run(&self) -> Run {
+ run_layout(&self.specs, self.direction, self.space)
+ }
+ fn has_stacked(&self) -> bool {
+ self.specs.iter().any(|s| s.stacked.is_some())
+ }
+}
+
+fn tid(id: u32) -> PaneId {
+ PaneId::Terminal(id)
+}
+
+fn fixtures() -> Vec<Fixture> {
+ let h = SplitDirection::Horizontal;
+ let v = SplitDirection::Vertical;
+ vec![
+ Fixture {
+ name: "single_pane_grows_to_space",
+ specs: vec![spec(1, 0, 0, dim_pct(100.0, 40), dim_pct(100.0, 10))],
+ direction: h,
+ space: 80,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "empty_pane_map_reports_unchanged",
+ specs: vec![],
+ direction: h,
+ space: 80,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "two_panes_split_available_space",
+ specs: vec![
+ spec(1, 0, 0, dim_pct(50.0, 40), dim_pct(100.0, 10)),
+ spec(2, 40, 0, dim_pct(50.0, 40), dim_pct(100.0, 10)),
+ ],
+ direction: h,
+ space: 100,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "identical_sizes_report_unchanged",
+ specs: vec![
+ spec(1, 0, 0, dim_pct(50.0, 40), dim_pct(100.0, 10)),
+ spec(2, 40, 0, dim_pct(50.0, 40), dim_pct(100.0, 10)),
+ ],
+ direction: h,
+ space: 80,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "unchanged_sizes_still_reposition",
+ specs: vec![
+ spec(1, 0, 0, dim_pct(50.0, 40), dim_pct(100.0, 10)),
+ spec(2, 45, 0, dim_pct(50.0, 40), dim_pct(100.0, 10)),
+ ],
+ direction: h,
+ space: 80,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "vertical_split_resizes_rows",
+ specs: vec![
+ spec(1, 0, 0, dim_pct(100.0, 80), dim_pct(50.0, 10)),
+ spec(2, 0, 10, dim_pct(100.0, 80), dim_pct(50.0, 10)),
+ ],
+ direction: v,
+ space: 30,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "three_panes_round_up_first",
+ specs: vec![
+ spec(1, 0, 0, dim_pct(33.3, 26), dim_pct(100.0, 10)),
+ spec(2, 26, 0, dim_pct(33.3, 27), dim_pct(100.0, 10)),
+ spec(3, 53, 0, dim_pct(33.4, 27), dim_pct(100.0, 10)),
+ ],
+ direction: h,
+ space: 100,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "over_rounded_band_shrinks_last",
+ specs: vec![
+ spec(1, 0, 0, dim_pct(33.4999, 33), dim_pct(100.0, 10)),
+ spec(2, 33, 0, dim_pct(33.4999, 33), dim_pct(100.0, 10)),
+ spec(3, 66, 0, dim_pct(33.0002, 34), dim_pct(100.0, 10)),
+ ],
+ direction: h,
+ space: 100,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "fixed_pane_locks_and_percent_absorbs",
+ specs: vec![
+ spec(1, 0, 0, dim_fixed(20), dim_pct(100.0, 10)),
+ spec(2, 20, 0, dim_pct(100.0, 60), dim_pct(100.0, 10)),
+ ],
+ direction: h,
+ space: 100,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "fixed_pane_excluded_from_rounding",
+ specs: vec![
+ spec(1, 0, 0, dim_fixed(21), dim_pct(100.0, 10)),
+ spec(2, 21, 0, dim_pct(50.0, 30), dim_pct(100.0, 10)),
+ spec(3, 51, 0, dim_pct(50.0, 29), dim_pct(100.0, 10)),
+ ],
+ direction: h,
+ space: 100,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "pane_spanning_two_bands_adjusted_once",
+ specs: vec![
+ spec(1, 0, 0, dim_pct(33.3, 26), dim_pct(50.0, 10)),
+ spec(3, 26, 0, dim_pct(66.7, 54), dim_pct(100.0, 20)),
+ spec(4, 0, 10, dim_pct(16.65, 13), dim_pct(50.0, 10)),
+ spec(5, 13, 10, dim_pct(16.65, 13), dim_pct(50.0, 10)),
+ ],
+ direction: h,
+ space: 100,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "stack_main_pane_carries_whole_stack",
+ specs: vec![
+ spec_stacked(1, 0, 0, dim_pct(50.0, 40), dim_fixed(1), 0),
+ spec_stacked(2, 0, 1, dim_pct(50.0, 40), dim_pct(50.0, 8), 0),
+ spec_stacked(3, 0, 9, dim_pct(50.0, 40), dim_fixed(1), 0),
+ spec(4, 40, 0, dim_pct(50.0, 40), dim_pct(100.0, 10)),
+ ],
+ direction: v,
+ space: 12,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "short_stack_aborts_without_error",
+ specs: vec![
+ spec_stacked(1, 0, 0, dim_pct(100.0, 80), dim_fixed(1), 0),
+ spec_stacked(2, 0, 1, dim_pct(100.0, 80), dim_pct(100.0, 8), 0),
+ spec_stacked(3, 0, 9, dim_pct(100.0, 80), dim_fixed(1), 0),
+ ],
+ direction: v,
+ space: 2,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "override_pane_updates_override_only",
+ specs: {
+ let one = spec(1, 0, 0, dim_pct(50.0, 40), dim_pct(100.0, 10));
+ let two = spec(2, 40, 0, dim_pct(50.0, 40), dim_pct(100.0, 10));
+ vec![one, with_override(two, two.geom())]
+ },
+ direction: h,
+ space: 100,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "no_room_for_all_spans",
+ specs: vec![
+ spec(1, 0, 0, dim_pct(50.0, 40), dim_pct(100.0, 10)),
+ spec(2, 40, 0, dim_pct(50.0, 40), dim_pct(100.0, 10)),
+ ],
+ direction: h,
+ space: 1,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "conflicting_required_constraints_fail",
+ specs: vec![spec(1, 0, 0, dim_fixed(10), dim_pct(100.0, 10))],
+ direction: h,
+ space: 20,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "stack_without_flexible_pane_is_ignored",
+ specs: vec![
+ spec_stacked(1, 0, 0, dim_pct(100.0, 80), dim_fixed(1), 0),
+ spec_stacked(2, 0, 1, dim_pct(100.0, 80), dim_fixed(1), 0),
+ ],
+ direction: v,
+ space: 20,
+ class: Class::A,
+ bands: vec![],
+ },
+ Fixture {
+ name: "under_specified_percents",
+ specs: vec![
+ spec(1, 0, 0, dim_pct(30.0, 40), dim_pct(100.0, 10)),
+ spec(2, 40, 0, dim_pct(30.0, 40), dim_pct(100.0, 10)),
+ ],
+ direction: h,
+ space: 100,
+ class: Class::B,
+ bands: vec![vec![tid(1), tid(2)]],
+ },
+ Fixture {
+ name: "over_specified_percents",
+ specs: vec![
+ spec(1, 0, 0, dim_pct(60.0, 40), dim_pct(100.0, 10)),
+ spec(2, 40, 0, dim_pct(60.0, 40), dim_pct(100.0, 10)),
+ ],
+ direction: h,
+ space: 100,
+ class: Class::B,
+ bands: vec![vec![tid(1), tid(2)]],
+ },
+ Fixture {
+ name: "degenerate_across_two_bands",
+ specs: vec![
+ spec(1, 0, 0, dim_pct(60.0, 26), dim_pct(50.0, 10)),
+ spec(3, 26, 0, dim_pct(60.0, 54), dim_pct(100.0, 20)),
+ spec(4, 0, 10, dim_pct(30.0, 13), dim_pct(50.0, 10)),
+ spec(5, 13, 10, dim_pct(30.0, 13), dim_pct(50.0, 10)),
+ ],
+ direction: h,
+ space: 100,
+ class: Class::B,
+ bands: vec![vec![tid(1), tid(3)], vec![tid(4), tid(5), tid(3)]],
+ },
+ Fixture {
+ name: "fixed_and_degenerate_percents",
+ specs: vec![
+ spec(1, 0, 0, dim_fixed(20), dim_pct(100.0, 10)),
+ spec(2, 20, 0, dim_pct(60.0, 30), dim_pct(100.0, 10)),
+ spec(3, 50, 0, dim_pct(60.0, 30), dim_pct(100.0, 10)),
+ ],
+ direction: h,
+ space: 100,
+ class: Class::B,
+ bands: vec![vec![tid(1), tid(2), tid(3)]],
+ },
+ Fixture {
+ name: "zero_flex_space_division",
+ specs: vec![
+ spec(1, 0, 0, dim_fixed(20), dim_pct(100.0, 10)),
+ spec(2, 20, 0, dim_pct(50.0, 20), dim_pct(100.0, 10)),
+ ],
+ direction: h,
+ space: 20,
+ class: Class::Artefact,
+ bands: vec![],
+ },
+ Fixture {
+ name: "saturated_flex_space_division",
+ specs: vec![
+ spec(1, 0, 0, dim_fixed(30), dim_pct(100.0, 10)),
+ spec(2, 30, 0, dim_pct(50.0, 20), dim_pct(100.0, 10)),
+ ],
+ direction: h,
+ space: 20,
+ class: Class::Artefact,
+ bands: vec![],
+ },
+ Fixture {
+ name: "stacked_pane_with_override",
+ specs: {
+ let two = spec_stacked(2, 0, 1, dim_pct(100.0, 80), dim_pct(100.0, 8), 0);
+ vec![
+ spec_stacked(1, 0, 0, dim_pct(100.0, 80), dim_fixed(1), 0),
+ with_override(two, two.geom()),
+ spec_stacked(3, 0, 9, dim_pct(100.0, 80), dim_fixed(1), 0),
+ ]
+ },
+ direction: SplitDirection::Vertical,
+ space: 20,
+ class: Class::Artefact,
+ bands: vec![],
+ },
+ ]
+}
+
+fn fixture(name: &str) -> Fixture {
+ fixtures()
+ .into_iter()
+ .find(|f| f.name == name)
+ .unwrap_or_else(|| panic!("unknown fixture `{}`", name))
+}
+
+fn pane(run: &Run, id: u32) -> PaneSnapshot {
+ *run.panes
+ .iter()
+ .find(|p| p.id == PaneId::Terminal(id))
+ .unwrap_or_else(|| panic!("no snapshot for pane {}", id))
+}
+
+fn input_spec(specs: &[PaneSpec], id: PaneId) -> PaneSpec {
+ *specs
+ .iter()
+ .find(|s| s.id == id)
+ .unwrap_or_else(|| panic!("no input spec for {:?}", id))
+}
+
+fn assert_ok(run: &Run, name: &str) {
+ assert_eq!(run.outcome, Outcome::Ok, "fixture `{}`", name);
+}
+
+fn assert_geom_field_eq(actual: &PaneGeom, expected: &PaneGeom, ctx: &str) {
+ assert_eq!(actual.x, expected.x, "{}: x", ctx);
+ assert_eq!(actual.y, expected.y, "{}: y", ctx);
+ assert_eq!(
+ actual.cols.constraint, expected.cols.constraint,
+ "{}: cols.constraint",
+ ctx
+ );
+ assert_eq!(
+ actual.cols.as_usize(),
+ expected.cols.as_usize(),
+ "{}: cols.inner",
+ ctx
+ );
+ assert_eq!(
+ actual.rows.constraint, expected.rows.constraint,
+ "{}: rows.constraint",
+ ctx
+ );
+ assert_eq!(
+ actual.rows.as_usize(),
+ expected.rows.as_usize(),
+ "{}: rows.inner",
+ ctx
+ );
+ assert_eq!(actual.stacked, expected.stacked, "{}: stacked", ctx);
+ assert_eq!(actual.is_pinned, expected.is_pinned, "{}: is_pinned", ctx);
+ assert_eq!(
+ actual.logical_position, expected.logical_position,
+ "{}: logical_position",
+ ctx
+ );
+}
+
+fn axis_dim(geom: &PaneGeom, axis: Axis) -> (usize, usize, Constraint) {
+ match axis {
+ Axis::Cols => (geom.x, geom.cols.as_usize(), geom.cols.constraint),
+ Axis::Rows => (geom.y, geom.rows.as_usize(), geom.rows.constraint),
+ }
+}
+
+fn assert_result_is_benign(run: &Run, name: &str) {
+ match &run.outcome {
+ Outcome::Ok | Outcome::PaneSizeUnchanged => {},
+ Outcome::OtherErr(msg) => panic!("fixture `{}` produced an error: {}", name, msg),
+ }
+}
+
+fn assert_constraints_preserved(run: &Run, input: &[PaneSpec], axis: Axis, name: &str) {
+ for snapshot in &run.panes {
+ let expected = input_spec(input, snapshot.id).geom();
+ let (_, _, actual_constraint) = axis_dim(&snapshot.current, axis);
+ let (_, _, expected_constraint) = axis_dim(&expected, axis);
+ assert_eq!(
+ actual_constraint, expected_constraint,
+ "fixture `{}`: {:?} constraint on {:?}",
+ name, snapshot.id, axis
+ );
+ }
+}
+
+fn assert_cross_axis_untouched(run: &Run, input: &[PaneSpec], axis: Axis, name: &str) {
+ for snapshot in &run.panes {
+ let expected = input_spec(input, snapshot.id).geom();
+ match axis {
+ Axis::Cols => {
+ assert_eq!(
+ snapshot.current.y, expected.y,
+ "fixture `{}`: {:?} y",
+ name, snapshot.id
+ );
+ assert_eq!(
+ snapshot.current.rows.constraint, expected.rows.constraint,
+ "fixture `{}`: {:?} rows.constraint",
+ name, snapshot.id
+ );
+ assert_eq!(
+ snapshot.current.rows.as_usize(),
+ expected.rows.as_usize(),
+ "fixture `{}`: {:?} rows.inner",
+ name,
+ snapshot.id
+ );
+ },
+ Axis::Rows => {
+ assert_eq!(
+ snapshot.current.x, expected.x,
+ "fixture `{}`: {:?} x",
+ name, snapshot.id
+ );
+ assert_eq!(
+ snapshot.current.cols.constraint, expected.cols.constraint,
+ "fixture `{}`: {:?} cols.constraint",
+ name, snapshot.id
+ );
+ assert_eq!(
+ snapshot.current.cols.as_usize(),
+ expected.cols.as_usize(),
+ "fixture `{}`: {:?} cols.inner",
+ name,
+ snapshot.id
+ );
+ },
+ }
+ }
+}
+
+fn assert_metadata_preserved(run: &Run, input: &[PaneSpec], name: &str) {
+ for snapshot in &run.panes {
+ let expected = input_spec(input, snapshot.id).geom();
+ assert_eq!(
+ snapshot.current.stacked, expected.stacked,
+ "fixture `{}`: {:?} stacked",
+ name, snapshot.id
+ );
+ assert_eq!(
+ snapshot.current.logical_position, expected.logical_position,
+ "fixture `{}`: {:?} logical_position",
+ name, snapshot.id
+ );
+ }
+}
+
+fn assert_min_size(run: &Run, axis: Axis, name: &str) {
+ for snapshot in &run.panes {
+ let (_, size, _) = axis_dim(&snapshot.current, axis);
+ assert!(
+ size >= 1,
+ "fixture `{}`: {:?} has {:?} size {}",
+ name,
+ snapshot.id,
+ axis,
+ size
+ );
+ }
+}
+
+fn assert_fixed_panes_locked(run: &Run, input: &[PaneSpec], axis: Axis, name: &str) {
+ for snapshot in &run.panes {
+ let expected = input_spec(input, snapshot.id).geom();
+ if let (_, _, Constraint::Fixed(n)) = axis_dim(&expected, axis) {
+ let (_, size, _) = axis_dim(&snapshot.current, axis);
+ assert_eq!(
+ size, n,
+ "fixture `{}`: {:?} fixed pane size drifted",
+ name, snapshot.id
+ );
+ }
+ }
+}
+
+fn assert_band_partitions_space(run: &Run, band: &[PaneId], axis: Axis, space: usize, name: &str) {
+ let mut members: Vec<(usize, usize)> = band
+ .iter()
+ .map(|id| {
+ let snapshot = run
+ .panes
+ .iter()
+ .find(|p| p.id == *id)
+ .unwrap_or_else(|| panic!("fixture `{}`: no snapshot for {:?}", name, id));
+ let (pos, size, _) = axis_dim(&snapshot.current, axis);
+ (pos, size)
+ })
+ .collect();
+ members.sort_by_key(|(pos, _)| *pos);
+ assert!(
+ !members.is_empty(),
+ "fixture `{}`: empty band supplied",
+ name
+ );
+ assert_eq!(
+ members[0].0, 0,
+ "fixture `{}`: band {:?} does not start at 0",
+ name, band
+ );
+ for window in members.windows(2) {
+ assert_eq!(
+ window[1].0,
+ window[0].0 + window[0].1,
+ "fixture `{}`: band {:?} has a gap or overlap",
+ name,
+ band
+ );
+ }
+ let last = members[members.len() - 1];
+ assert_eq!(
+ last.0 + last.1,
+ space,
+ "fixture `{}`: band {:?} does not fill {}",
+ name,
+ band,
+ space
+ );
+}
+
+fn assert_no_mutation(run: &Run, input: &[PaneSpec], name: &str) {
+ for snapshot in &run.panes {
+ let spec = input_spec(input, snapshot.id);
+ assert_geom_field_eq(
+ &snapshot.geom,
+ &spec.geom(),
+ &format!("fixture `{}`: {:?} geom", name, snapshot.id),
+ );
+ let expected_current = spec.geom_override.unwrap_or_else(|| spec.geom());
+ assert_geom_field_eq(
+ &snapshot.current,
+ &expected_current,
+ &format!("fixture `{}`: {:?} current", name, snapshot.id),
+ );
+ match (snapshot.geom_override, spec.geom_override) {
+ (None, None) => {},
+ (Some(actual), Some(expected)) => assert_geom_field_eq(
+ &actual,
+ &expected,
+ &format!("fixture `{}`: {:?} geom_override", name, snapshot.id),
+ ),
+ (actual, expected) => panic!(
+ "fixture `{}`: {:?} geom_override {:?} != {:?}",
+ name, snapshot.id, actual, expected
+ ),
+ }
+ }
+}
+
+fn assert_class_b_invariants(fixture: &Fixture, run: &Run) {
+ let axis = fixture.axis();
+ assert_result_is_benign(run, fixture.name);
+ assert_constraints_preserved(run, &fixture.specs, axis, fixture.name);
+ if !fixture.has_stacked() {
+ assert_cross_axis_untouched(run, &fixture.specs, axis, fixture.name);
+ }
+ assert_metadata_preserved(run, &fixture.specs, fixture.name);
+ assert_min_size(run, axis, fixture.name);
+ assert_fixed_panes_locked(run, &fixture.specs, axis, fixture.name);
+ for band in &fixture.bands {
+ assert_band_partitions_space(run, band, axis, fixture.space, fixture.name);
+ }
+}
+
+fn canonical(run: &Run) -> String {
+ let outcome = match &run.outcome {
+ Outcome::Ok => "ok".to_string(),
+ Outcome::PaneSizeUnchanged => "unchanged".to_string(),
+ Outcome::OtherErr(msg) => format!("err({})", msg),
+ };
+ let mut out = outcome;
+ for snapshot in &run.panes {
+ out.push_str(&format!(
+ " | {:?} x={} y={} cols={:?}:{} rows={:?}:{} stacked={:?} logical={:?} override={}",
+ snapshot.id,
+ snapshot.geom.x,
+ snapshot.geom.y,
+ snapshot.geom.cols.constraint,
+ snapshot.geom.cols.as_usize(),
+ snapshot.geom.rows.constraint,
+ snapshot.geom.rows.as_usize(),
+ snapshot.geom.stacked,
+ snapshot.geom.logical_position,
+ match &snapshot.geom_override {
+ None => "none".to_string(),
+ Some(g) => format!(
+ "x={} y={} cols={:?}:{} rows={:?}:{} stacked={:?} logical={:?}",
+ g.x,
+ g.y,
+ g.cols.constraint,
+ g.cols.as_usize(),
+ g.rows.constraint,
+ g.rows.as_usize(),
+ g.stacked,
+ g.logical_position
+ ),
+ }
+ ));
+ }
+ out
+}
+
+#[test]
+fn single_pane_grows_to_space() {
+ let f = fixture("single_pane_grows_to_space");
+ let run = f.run();
+ assert_ok(&run, f.name);
+ let p = pane(&run, 1);
+ assert_eq!(p.geom.x, 0);
+ assert_eq!(p.geom.cols.constraint, Constraint::Percent(100.0));
+ assert_eq!(p.geom.cols.as_usize(), 80);
+ assert_eq!(p.geom.y, 0);
+ assert_eq!(p.geom.rows.constraint, Constraint::Percent(100.0));
+ assert_eq!(p.geom.rows.as_usize(), 10);
+}
+
+#[test]
+fn empty_pane_map_reports_unchanged() {
+ let f = fixture("empty_pane_map_reports_unchanged");
+ let run = f.run();
+ assert_eq!(run.outcome, Outcome::PaneSizeUnchanged);
+ assert!(run.panes.is_empty());
+}
+
+#[test]
+fn two_panes_split_available_space() {
+ let f = fixture("two_panes_split_available_space");
+ let run = f.run();
+ assert_ok(&run, f.name);
+ let one = pane(&run, 1);
+ let two = pane(&run, 2);
+ assert_eq!(one.geom.x, 0);
+ assert_eq!(one.geom.cols.as_usize(), 50);
+ assert_eq!(two.geom.x, 50);
+ assert_eq!(two.geom.cols.as_usize(), 50);
+}
+
+#[test]
+fn identical_sizes_report_unchanged() {
+ let f = fixture("identical_sizes_report_unchanged");
+ let run = f.run();
+ assert_eq!(run.outcome, Outcome::PaneSizeUnchanged);
+ assert_no_mutation(&run, &f.specs, f.name);
+}
+
+#[test]
+fn unchanged_sizes_still_reposition() {
+ let f = fixture("unchanged_sizes_still_reposition");
+ let run = f.run();
+ assert_eq!(run.outcome, Outcome::PaneSizeUnchanged);
+ assert_eq!(pane(&run, 2).geom.x, 40);
+}
+
+#[test]
+fn vertical_split_resizes_rows() {
+ let f = fixture("vertical_split_resizes_rows");
+ let run = f.run();
+ assert_ok(&run, f.name);
+ let one = pane(&run, 1);
+ let two = pane(&run, 2);
+ assert_eq!(one.geom.y, 0);
+ assert_eq!(one.geom.rows.as_usize(), 15);
+ assert_eq!(two.geom.y, 15);
+ assert_eq!(two.geom.rows.as_usize(), 15);
+ assert_eq!(one.geom.x, 0);
+ assert_eq!(one.geom.cols.as_usize(), 80);
+ assert_eq!(two.geom.x, 0);
+ assert_eq!(two.geom.cols.as_usize(), 80);
+}
+
+#[test]
+fn three_panes_round_up_first() {
+ let f = fixture("three_panes_round_up_first");
+ let run = f.run();
+ assert_ok(&run, f.name);
+ let sizes: Vec<usize> = [1, 2, 3]
+ .iter()
+ .map(|id| pane(&run, *id).geom.cols.as_usize())
+ .collect();
+ let positions: Vec<usize> = [1, 2, 3].iter().map(|id| pane(&run, *id).geom.x).collect();
+ assert_eq!(sizes, vec![34, 33, 33]);
+ assert_eq!(positions, vec![0, 34, 67]);
+}
+
+#[test]
+fn over_rounded_band_shrinks_last() {
+ let f = fixture("over_rounded_band_shrinks_last");
+ let run = f.run();
+ assert_ok(&run, f.name);
+ let sizes: Vec<usize> = [1, 2, 3]
+ .iter()
+ .map(|id| pane(&run, *id).geom.cols.as_usize())
+ .collect();
+ let positions: Vec<usize> = [1, 2, 3].iter().map(|id| pane(&run, *id).geom.x).collect();
+ assert_eq!(sizes, vec![34, 33, 33]);
+ assert_eq!(positions, vec![0, 34, 67]);
+ for id in [1u32, 2, 3] {
+ assert_eq!(
+ pane(&run, id).geom.cols.constraint,
+ input_spec(&f.specs, tid(id)).cols.constraint
+ );
+ }
+}
+
+#[test]
+fn fixed_pane_locks_and_percent_absorbs() {
+ let f = fixture("fixed_pane_locks_and_percent_absorbs");
+ let run = f.run();
+ assert_ok(&run, f.name);
+ let one = pane(&run, 1);
+ let two = pane(&run, 2);
+ assert_eq!(one.geom.x, 0);
+ assert_eq!(one.geom.cols.constraint, Constraint::Fixed(20));
+ assert_eq!(one.geom.cols.as_usize(), 20);
+ assert_eq!(two.geom.x, 20);
+ assert_eq!(two.geom.cols.as_usize(), 80);
+}
+
+#[test]
+fn fixed_pane_excluded_from_rounding() {
+ let f = fixture("fixed_pane_excluded_from_rounding");
+ let run = f.run();
+ assert_ok(&run, f.name);
+ let sizes: Vec<usize> = [1, 2, 3]
+ .iter()
+ .map(|id| pane(&run, *id).geom.cols.as_usize())
+ .collect();
+ let positions: Vec<usize> = [1, 2, 3].iter().map(|id| pane(&run, *id).geom.x).collect();
+ assert_eq!(sizes, vec![21, 40, 39]);
+ assert_eq!(positions, vec![0, 21, 61]);
+ assert_eq!(pane(&run, 1).geom.cols.constraint, Constraint::Fixed(21));
+}
+
+#[test]
+fn pane_spanning_two_bands_adjusted_once() {
+ let f = fixture("pane_spanning_two_bands_adjusted_once");
+ let run = f.run();
+ assert_ok(&run, f.name);
+ let expected = [
+ (1u32, 0usize, 33usize),
+ (3, 33, 67),
+ (4, 0, 17),
+ (5, 17, 16),
+ ];
+ for (id, x, cols) in expected {
+ let p = pane(&run, id);
+ assert_eq!(p.geom.x, x, "pane {} x", id);
+ assert_eq!(p.geom.cols.as_usize(), cols, "pane {} cols", id);
+ }
+}
+
+#[test]
+fn stack_main_pane_carries_whole_stack() {
+ let f = fixture("stack_main_pane_carries_whole_stack");
+ let run = f.run();
+ assert_ok(&run, f.name);
+ let expected = [(1u32, 0usize, 1usize), (2, 1, 10), (3, 11, 1), (4, 0, 12)];
+ for (id, y, rows) in expected {
+ let p = pane(&run, id);
+ assert_eq!(p.geom.y, y, "pane {} y", id);
+ assert_eq!(p.geom.rows.as_usize(), rows, "pane {} rows", id);
+ }
+}
+
+#[test]
+fn short_stack_aborts_without_error() {
+ let f = fixture("short_stack_aborts_without_error");
+ let run = f.run();
+ assert_ok(&run, f.name);
+ assert_no_mutation(&run, &f.specs, f.name);
+}
+
+#[test]
+fn override_pane_updates_override_only() {
+ let f = fixture("override_pane_updates_override_only");
+ let run = f.run();
+ assert_ok(&run, f.name);
+ let one = pane(&run, 1);
+ assert_eq!(one.geom.x, 0);
+ assert_eq!(one.geom.cols.as_usize(), 50);
+ assert_eq!(one.geom_override, None);
+
+ let two = pane(&run, 2);
+ assert_geom_field_eq(
+ &two.geom,
+ &input_spec(&f.specs, tid(2)).geom(),
+ "pane 2 geom",
+ );
+ let override_geom = two.geom_override.expect("pane 2 lost its geom_override");
+ assert_eq!(override_geom.x, 50);
+ assert_eq!(override_geom.cols.as_usize(), 50);
+ assert_geom_field_eq(&two.current, &override_geom, "pane 2 current");
+}
+
+#[test]
+fn no_room_for_all_spans() {
+ let f = fixture("no_room_for_all_spans");
+ let run = f.run();
+ match &run.outcome {
+ Outcome::OtherErr(msg) => assert!(
+ msg.contains("Ran out of room for spans"),
+ "unexpected error: {}",
+ msg
+ ),
+ other => panic!("expected OtherErr, got {:?}", other),
+ }
+ assert_no_mutation(&run, &f.specs, f.name);
+}
+
+#[test]
+fn conflicting_required_constraints_fail() {
+ let f = fixture("conflicting_required_constraints_fail");
+ let run = f.run();
+ assert!(
+ matches!(run.outcome, Outcome::OtherErr(_)),
+ "expected OtherErr, got {:?}",
+ run.outcome
+ );
+ assert_ne!(run.outcome, Outcome::PaneSizeUnchanged);
+ if let Outcome::OtherErr(msg) = &run.outcome {
+ assert!(
+ !msg.contains("Ran out of room"),
+ "unexpected error: {}",
+ msg
+ );
+ }
+ assert_no_mutation(&run, &f.specs, f.name);
+}
+
+#[test]
+fn stack_without_flexible_pane_is_ignored() {
+ let f = fixture("stack_without_flexible_pane_is_ignored");
+ let run = f.run();
+ assert_eq!(run.outcome, Outcome::PaneSizeUnchanged);
+ assert_no_mutation(&run, &f.specs, f.name);
+}
+
+#[test]
+fn under_specified_percents() {
+ let f = fixture("under_specified_percents");
+ assert_class_b_invariants(&f, &f.run());
+}
+
+#[test]
+fn over_specified_percents() {
+ let f = fixture("over_specified_percents");
+ assert_class_b_invariants(&f, &f.run());
+}
+
+#[test]
+fn degenerate_across_two_bands() {
+ let f = fixture("degenerate_across_two_bands");
+ assert_class_b_invariants(&f, &f.run());
+}
+
+#[test]
+fn fixed_and_degenerate_percents() {
+ let f = fixture("fixed_and_degenerate_percents");
+ assert_class_b_invariants(&f, &f.run());
+}
+
+#[test]
+fn zero_flex_space_division() {
+ let f = fixture("zero_flex_space_division");
+ let run = f.run();
+ println!("{} = {}", f.name, canonical(&run));
+}
+
+#[test]
+fn saturated_flex_space_division() {
+ let f = fixture("saturated_flex_space_division");
+ let run = f.run();
+ println!("{} = {}", f.name, canonical(&run));
+}
+
+#[test]
+fn stacked_pane_with_override() {
+ let f = fixture("stacked_pane_with_override");
+ let run = f.run();
+ println!("{} = {}", f.name, canonical(&run));
+}
diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs
index 1e6d7539..e9f8a93e 100644
--- a/zellij-server/src/tab/unit/tab_tests.rs
+++ b/zellij-server/src/tab/unit/tab_tests.rs
@@ -1159,7 +1159,7 @@ pub fn resize_while_fullscreen_updates_hidden_pane_geometry() {
// When a host-terminal resize arrives while a pane is fullscreen, every
// hidden pane's geometry must be updated to match the new display area.
// Otherwise their `inner` cell counts stay sized for the old display and
- // toggling fullscreen off hands the cassowary solver coordinates that
+ // toggling fullscreen off hands the layout solver coordinates that
// fall outside the viewport, producing layout-solve failures and a
// corrupt render.
//
@@ -1197,7 +1197,7 @@ pub fn resize_while_fullscreen_updates_hidden_pane_geometry() {
// Collect the panes hidden by the fullscreen state and verify each one
// already fits the new display area; if any extends beyond it, exiting
- // fullscreen would hand the cassowary solver an unsatisfiable layout.
+ // fullscreen would hand the layout solver an unsatisfiable layout.
let hidden_pane_ids: Vec<PaneId> = tab
.tiled_panes
.panes