use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::{
direction::SplitOrientation,
id::PaneId,
layout::LayoutNode,
pane::InputPolicy,
};
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PaneSlot(pub u32);
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SpawnSpec {
pub slot: PaneSlot,
pub shell: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub cwd: Option<String>,
#[serde(default)]
pub env: Vec<(String, String)>,
#[serde(default)]
pub title: String,
#[serde(default)]
pub input_policy: InputPolicy,
}
impl SpawnSpec {
#[must_use]
pub fn shell(slot: PaneSlot, shell: impl Into<String>) -> Self {
Self {
slot,
shell: shell.into(),
args: Vec::new(),
cwd: None,
env: Vec::new(),
title: String::new(),
input_policy: InputPolicy::default(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum LayoutPlan {
Leaf { slot: PaneSlot },
Split {
orientation: SplitOrientation,
ratio: f32,
a: Box<LayoutPlan>,
b: Box<LayoutPlan>,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct WindowPlan {
pub name: String,
pub layout: LayoutPlan,
pub active_slot: PaneSlot,
}
impl WindowPlan {
#[must_use]
pub fn single(name: impl Into<String>, slot: PaneSlot) -> Self {
Self {
name: name.into(),
layout: LayoutPlan::leaf(slot),
active_slot: slot,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum PlanError {
DuplicateSlot(PaneSlot),
BadRatio(f32),
}
impl LayoutPlan {
#[must_use]
pub fn leaf(slot: PaneSlot) -> Self {
Self::Leaf { slot }
}
#[must_use]
pub fn split(orientation: SplitOrientation, a: LayoutPlan, b: LayoutPlan) -> Self {
Self::Split {
orientation,
ratio: 0.5,
a: Box::new(a),
b: Box::new(b),
}
}
#[must_use]
pub fn slots(&self) -> Vec<PaneSlot> {
let mut out = Vec::new();
self.collect(&mut out);
out
}
fn collect(&self, out: &mut Vec<PaneSlot>) {
match self {
Self::Leaf { slot } => out.push(*slot),
Self::Split { a, b, .. } => {
a.collect(out);
b.collect(out);
}
}
}
#[must_use]
pub fn slot_count(&self) -> usize {
match self {
Self::Leaf { .. } => 1,
Self::Split { a, b, .. } => a.slot_count() + b.slot_count(),
}
}
#[must_use]
pub fn leftmost_slot(&self) -> PaneSlot {
match self {
Self::Leaf { slot } => *slot,
Self::Split { a, .. } => a.leftmost_slot(),
}
}
pub fn validate(&self) -> Result<(), PlanError> {
let mut seen = Vec::new();
self.validate_into(&mut seen)
}
fn validate_into(&self, seen: &mut Vec<PaneSlot>) -> Result<(), PlanError> {
match self {
Self::Leaf { slot } => {
if seen.contains(slot) {
return Err(PlanError::DuplicateSlot(*slot));
}
seen.push(*slot);
Ok(())
}
Self::Split { ratio, a, b, .. } => {
if !(*ratio > 0.0 && *ratio < 1.0) {
return Err(PlanError::BadRatio(*ratio));
}
a.validate_into(seen)?;
b.validate_into(seen)
}
}
}
pub fn realize(&self, mint: &mut impl FnMut(PaneSlot) -> PaneId) -> LayoutNode {
match self {
Self::Leaf { slot } => LayoutNode::leaf(mint(*slot)),
Self::Split {
orientation,
ratio,
a,
b,
} => LayoutNode::Split {
orientation: *orientation,
ratio: *ratio,
a: Box::new(a.realize(mint)),
b: Box::new(b.realize(mint)),
},
}
}
#[must_use]
pub fn from_node(node: &LayoutNode) -> (LayoutPlan, BTreeMap<PaneSlot, PaneId>) {
let mut next = 0u32;
let mut map = BTreeMap::new();
let plan = Self::from_node_into(node, &mut next, &mut map);
(plan, map)
}
pub fn from_node_into(
node: &LayoutNode,
next: &mut u32,
map: &mut BTreeMap<PaneSlot, PaneId>,
) -> LayoutPlan {
match node {
LayoutNode::Leaf { pane } => {
let slot = PaneSlot(*next);
*next += 1;
map.insert(slot, *pane);
LayoutPlan::Leaf { slot }
}
LayoutNode::Split {
orientation,
ratio,
a,
b,
} => LayoutPlan::Split {
orientation: *orientation,
ratio: *ratio,
a: Box::new(Self::from_node_into(a, next, map)),
b: Box::new(Self::from_node_into(b, next, map)),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::Rect;
#[test]
fn slots_traverse_left_to_right() {
let p = LayoutPlan::split(
SplitOrientation::Horizontal,
LayoutPlan::leaf(PaneSlot(0)),
LayoutPlan::split(
SplitOrientation::Vertical,
LayoutPlan::leaf(PaneSlot(1)),
LayoutPlan::leaf(PaneSlot(2)),
),
);
assert_eq!(p.slots(), vec![PaneSlot(0), PaneSlot(1), PaneSlot(2)]);
assert_eq!(p.slot_count(), 3);
}
#[test]
fn validate_rejects_duplicate_slot() {
let p = LayoutPlan::split(
SplitOrientation::Vertical,
LayoutPlan::leaf(PaneSlot(7)),
LayoutPlan::leaf(PaneSlot(7)),
);
assert_eq!(p.validate(), Err(PlanError::DuplicateSlot(PaneSlot(7))));
}
#[test]
fn validate_rejects_degenerate_ratio() {
let p = LayoutPlan::Split {
orientation: SplitOrientation::Vertical,
ratio: 1.0,
a: Box::new(LayoutPlan::leaf(PaneSlot(0))),
b: Box::new(LayoutPlan::leaf(PaneSlot(1))),
};
assert_eq!(p.validate(), Err(PlanError::BadRatio(1.0)));
}
#[test]
fn realize_maps_slots_to_minted_panes_and_the_live_algebra_applies() {
let plan = LayoutPlan::split(
SplitOrientation::Vertical,
LayoutPlan::leaf(PaneSlot(0)),
LayoutPlan::leaf(PaneSlot(1)),
);
let mut mint = |s: PaneSlot| PaneId(100 + u64::from(s.0));
let live = plan.realize(&mut mint);
assert_eq!(live.panes(), vec![PaneId(100), PaneId(101)]);
live.validate().unwrap();
let rects = live.compute_rects(Rect::sized(80, 24));
let total: u32 = rects.iter().map(|(_, r)| r.area()).sum();
assert_eq!(total, 80 * 24); }
#[test]
fn realize_preserves_ratio_and_orientation() {
let plan = LayoutPlan::Split {
orientation: SplitOrientation::Horizontal,
ratio: 0.25,
a: Box::new(LayoutPlan::leaf(PaneSlot(0))),
b: Box::new(LayoutPlan::leaf(PaneSlot(1))),
};
let mut mint = |s: PaneSlot| PaneId(u64::from(s.0));
match plan.realize(&mut mint) {
LayoutNode::Split { orientation, ratio, .. } => {
assert_eq!(orientation, SplitOrientation::Horizontal);
assert!((ratio - 0.25).abs() < f32::EPSILON);
}
LayoutNode::Leaf { .. } => panic!("expected a split"),
}
}
#[test]
fn spawn_spec_serde_round_trips() {
let s = SpawnSpec::shell(PaneSlot(3), "/bin/zsh");
let json = serde_json::to_string(&s).unwrap();
let back: SpawnSpec = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
fn sample_plan() -> LayoutPlan {
LayoutPlan::Split {
orientation: SplitOrientation::Vertical,
ratio: 0.3,
a: Box::new(LayoutPlan::leaf(PaneSlot(0))),
b: Box::new(LayoutPlan::Split {
orientation: SplitOrientation::Horizontal,
ratio: 0.7,
a: Box::new(LayoutPlan::leaf(PaneSlot(1))),
b: Box::new(LayoutPlan::leaf(PaneSlot(2))),
}),
}
}
#[test]
fn from_node_dehydrates_a_live_tree_with_canonical_slots() {
let plan = sample_plan();
let mut mint = |s: PaneSlot| PaneId(100 + u64::from(s.0));
let node = plan.realize(&mut mint);
let (plan2, map) = LayoutPlan::from_node(&node);
assert_eq!(plan2, plan);
assert_eq!(map[&PaneSlot(0)], PaneId(100));
assert_eq!(map[&PaneSlot(1)], PaneId(101));
assert_eq!(map[&PaneSlot(2)], PaneId(102));
}
#[test]
fn realize_of_from_node_reconstructs_the_original_live_tree() {
let node = LayoutNode::Split {
orientation: SplitOrientation::Horizontal,
ratio: 0.4,
a: Box::new(LayoutNode::leaf(PaneId(7))),
b: Box::new(LayoutNode::Split {
orientation: SplitOrientation::Vertical,
ratio: 0.6,
a: Box::new(LayoutNode::leaf(PaneId(9))),
b: Box::new(LayoutNode::leaf(PaneId(2))),
}),
};
let (plan, map) = LayoutPlan::from_node(&node);
let mut mint = |s: PaneSlot| map[&s];
assert_eq!(plan.realize(&mut mint), node);
assert_eq!(plan.slots(), vec![PaneSlot(0), PaneSlot(1), PaneSlot(2)]);
}
#[test]
fn from_node_of_a_single_pane_is_one_slot() {
let (plan, map) = LayoutPlan::from_node(&LayoutNode::leaf(PaneId(42)));
assert_eq!(plan, LayoutPlan::leaf(PaneSlot(0)));
assert_eq!(map[&PaneSlot(0)], PaneId(42));
}
}