use escriba_core::{BufferId, WindowId};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::Viewport;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
pub struct Rect {
pub x: u16,
pub y: u16,
pub w: u16,
pub h: u16,
}
impl Rect {
#[must_use]
pub const fn new(x: u16, y: u16, w: u16, h: u16) -> Self {
Self { x, y, w, h }
}
#[must_use]
pub const fn area(&self) -> u32 {
self.w as u32 * self.h as u32
}
#[must_use]
pub const fn overlaps(&self, other: &Self) -> bool {
self.x < other.x + other.w
&& other.x < self.x + self.w
&& self.y < other.y + other.h
&& other.y < self.y + self.h
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum Axis {
Stacked,
SideBySide,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum Shikiri {
Pane(Window),
Split(Split),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Window {
pub id: WindowId,
pub buffer_id: BufferId,
pub viewport: Viewport,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Split {
pub axis: Axis,
first: Box<Shikiri>,
second: Box<Shikiri>,
rest: Vec<Shikiri>,
}
impl Split {
#[must_use]
pub fn new(axis: Axis, first: Shikiri, second: Shikiri) -> Self {
Self {
axis,
first: Box::new(first),
second: Box::new(second),
rest: Vec::new(),
}
}
pub fn children(&self) -> impl Iterator<Item = &Shikiri> {
std::iter::once(self.first.as_ref())
.chain(std::iter::once(self.second.as_ref()))
.chain(self.rest.iter())
}
pub fn children_mut(&mut self) -> impl Iterator<Item = &mut Shikiri> {
std::iter::once(self.first.as_mut())
.chain(std::iter::once(self.second.as_mut()))
.chain(self.rest.iter_mut())
}
#[must_use]
pub fn len(&self) -> usize {
2 + self.rest.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
false }
pub fn push(&mut self, child: Shikiri) {
self.rest.push(child);
}
pub fn without(&mut self, id: WindowId) -> Option<Shikiri> {
let is_target = |n: &Shikiri| matches!(n, Shikiri::Pane(w) if w.id == id);
if is_target(&self.first) {
if self.rest.is_empty() {
return Some((*self.second).clone());
}
self.first = std::mem::replace(&mut self.second, Box::new(self.rest.remove(0)));
return None;
}
if is_target(&self.second) {
if self.rest.is_empty() {
return Some((*self.first).clone());
}
self.second = Box::new(self.rest.remove(0));
return None;
}
if let Some(i) = self.rest.iter().position(is_target) {
self.rest.remove(i);
return None;
}
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rule {
pub axis: Axis,
pub rect: Rect,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Solved {
pub panes: Vec<(WindowId, Rect)>,
pub rules: Vec<Rule>,
}
impl Solved {
#[must_use]
pub fn rect_of(&self, id: WindowId) -> Option<Rect> {
self.panes.iter().find(|(w, _)| *w == id).map(|(_, r)| *r)
}
}
#[must_use]
pub fn solve(tree: &Shikiri, frame: Rect) -> Solved {
let mut out = Solved::default();
lay(tree, frame, &mut out);
out
}
fn lay(node: &Shikiri, frame: Rect, out: &mut Solved) {
match node {
Shikiri::Pane(w) => out.panes.push((w.id, frame)),
Shikiri::Split(s) => {
let n = u16::try_from(s.len()).unwrap_or(u16::MAX);
let rules = n.saturating_sub(1);
match s.axis {
Axis::Stacked => {
let usable = frame.h.saturating_sub(rules);
let each = usable / n;
let extra = usable % n;
let mut y = frame.y;
for (i, child) in s.children().enumerate() {
let i = u16::try_from(i).unwrap_or(u16::MAX);
let h = each + u16::from(i < extra);
let end = frame.y.saturating_add(frame.h);
let h = h.min(end.saturating_sub(y));
lay(child, Rect::new(frame.x, y, frame.w, h), out);
y = y.saturating_add(h);
if i + 1 < n && y < end {
out.rules.push(Rule {
axis: s.axis,
rect: Rect::new(frame.x, y, frame.w, 1),
});
y = y.saturating_add(1);
}
}
}
Axis::SideBySide => {
let usable = frame.w.saturating_sub(rules);
let each = usable / n;
let extra = usable % n;
let mut x = frame.x;
for (i, child) in s.children().enumerate() {
let i = u16::try_from(i).unwrap_or(u16::MAX);
let w = each + u16::from(i < extra);
let end = frame.x.saturating_add(frame.w);
let w = w.min(end.saturating_sub(x));
lay(child, Rect::new(x, frame.y, w, frame.h), out);
x = x.saturating_add(w);
if i + 1 < n && x < end {
out.rules.push(Rule {
axis: s.axis,
rect: Rect::new(x, frame.y, 1, frame.h),
});
x = x.saturating_add(1);
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pane(id: u64) -> Shikiri {
Shikiri::Pane(Window {
id: WindowId(id),
buffer_id: BufferId(1),
viewport: Viewport::default(),
})
}
fn assert_tiles(solved: &Solved, frame: Rect) {
let mut cover = vec![0u8; frame.area() as usize];
let mut mark = |r: Rect| {
for y in r.y..r.y + r.h {
for x in r.x..r.x + r.w {
let i = (y - frame.y) as usize * frame.w as usize + (x - frame.x) as usize;
cover[i] += 1;
}
}
};
for (_, r) in &solved.panes {
mark(*r);
}
for rule in &solved.rules {
mark(rule.rect);
}
let gaps = cover.iter().filter(|c| **c == 0).count();
let overlaps = cover.iter().filter(|c| **c > 1).count();
assert_eq!(gaps, 0, "uncovered cells: {gaps}");
assert_eq!(overlaps, 0, "doubly-covered cells: {overlaps}");
}
#[test]
fn one_pane_fills_the_frame() {
let f = Rect::new(0, 0, 80, 24);
let s = solve(&pane(1), f);
assert_eq!(s.panes, vec![(WindowId(1), f)]);
assert!(s.rules.is_empty());
assert_tiles(&s, f);
}
#[test]
fn a_vertical_split_tiles_and_favours_the_first_pane() {
let f = Rect::new(0, 0, 80, 24);
let t = Shikiri::Split(Split::new(Axis::SideBySide, pane(1), pane(2)));
let s = solve(&t, f);
assert_eq!(s.panes[0].1.w, 40);
assert_eq!(s.panes[1].1.w, 39);
assert_eq!(s.rules.len(), 1);
assert_tiles(&s, f);
}
#[test]
fn a_horizontal_split_tiles() {
let f = Rect::new(0, 0, 80, 24);
let t = Shikiri::Split(Split::new(Axis::Stacked, pane(1), pane(2)));
let s = solve(&t, f);
assert_eq!(s.panes[0].1.h + s.panes[1].1.h + 1, 24);
assert_tiles(&s, f);
}
#[test]
fn three_ways_split_equally_not_by_repeated_halving() {
let f = Rect::new(0, 0, 80, 24);
let mut sp = Split::new(Axis::SideBySide, pane(1), pane(2));
sp.push(pane(3));
let s = solve(&Shikiri::Split(sp), f);
let widths: Vec<u16> = s.panes.iter().map(|(_, r)| r.w).collect();
assert_eq!(widths, vec![26, 26, 26], "78 usable / 3 = 26 each");
assert_tiles(&s, f);
}
#[test]
fn nested_splits_tile() {
let f = Rect::new(0, 0, 81, 25);
let inner = Shikiri::Split(Split::new(Axis::Stacked, pane(2), pane(3)));
let t = Shikiri::Split(Split::new(Axis::SideBySide, pane(1), inner));
let s = solve(&t, f);
assert_eq!(s.panes.len(), 3);
assert_tiles(&s, f);
}
#[test]
fn every_leaf_appears_exactly_once() {
let f = Rect::new(0, 0, 100, 40);
let inner = Shikiri::Split(Split::new(Axis::Stacked, pane(2), pane(3)));
let mut outer = Split::new(Axis::SideBySide, pane(1), inner);
outer.push(pane(4));
let s = solve(&Shikiri::Split(outer), f);
let mut ids: Vec<u64> = s.panes.iter().map(|(w, _)| w.0).collect();
ids.sort_unstable();
assert_eq!(ids, vec![1, 2, 3, 4]);
}
#[test]
fn panes_never_overlap_each_other() {
let f = Rect::new(0, 0, 100, 40);
let inner = Shikiri::Split(Split::new(Axis::Stacked, pane(2), pane(3)));
let t = Shikiri::Split(Split::new(Axis::SideBySide, pane(1), inner));
let s = solve(&t, f);
for (i, (_, a)) in s.panes.iter().enumerate() {
for (_, b) in s.panes.iter().skip(i + 1) {
assert!(!a.overlaps(b), "{a:?} overlaps {b:?}");
}
}
}
#[test]
fn a_frame_too_small_degrades_instead_of_panicking() {
for (w, h) in [(1u16, 1u16), (2, 1), (1, 2), (3, 3)] {
let f = Rect::new(0, 0, w, h);
let mut sp = Split::new(Axis::SideBySide, pane(1), pane(2));
sp.push(pane(3));
let s = solve(&Shikiri::Split(sp), f);
assert_eq!(s.panes.len(), 3, "{w}x{h}: every leaf still reported");
for (_, r) in &s.panes {
assert!(
r.x.saturating_add(r.w) <= w && r.y.saturating_add(r.h) <= h,
"{w}x{h}: {r:?} escaped the frame",
);
}
for rule in &s.rules {
let r = rule.rect;
assert!(
r.x.saturating_add(r.w) <= w && r.y.saturating_add(r.h) <= h,
"{w}x{h}: rule {r:?} escaped the frame",
);
}
assert_tiles(&s, f);
}
}
#[test]
fn solving_twice_gives_the_same_answer() {
let f = Rect::new(0, 0, 77, 23);
let t = Shikiri::Split(Split::new(Axis::SideBySide, pane(1), pane(2)));
assert_eq!(solve(&t, f), solve(&t, f));
}
#[test]
fn removing_from_a_two_child_split_collapses_it_away() {
let mut sp = Split::new(Axis::SideBySide, pane(1), pane(2));
let survivor = sp.without(WindowId(1)).expect("the split must collapse");
assert_eq!(survivor, pane(2));
}
#[test]
fn removing_from_a_three_child_split_keeps_the_split() {
let mut sp = Split::new(Axis::SideBySide, pane(1), pane(2));
sp.push(pane(3));
assert!(sp.without(WindowId(1)).is_none(), "three minus one is two");
assert_eq!(sp.len(), 2);
let ids: Vec<u64> = sp
.children()
.filter_map(|c| match c {
Shikiri::Pane(w) => Some(w.id.0),
Shikiri::Split(_) => None,
})
.collect();
assert_eq!(ids, vec![2, 3], "the survivors keep their order");
}
#[test]
fn removing_a_middle_child_keeps_order() {
let mut sp = Split::new(Axis::Stacked, pane(1), pane(2));
sp.push(pane(3));
assert!(sp.without(WindowId(2)).is_none());
let ids: Vec<u64> = sp
.children()
.filter_map(|c| match c {
Shikiri::Pane(w) => Some(w.id.0),
Shikiri::Split(_) => None,
})
.collect();
assert_eq!(ids, vec![1, 3]);
}
#[test]
fn removing_an_absent_id_changes_nothing() {
let mut sp = Split::new(Axis::Stacked, pane(1), pane(2));
assert!(sp.without(WindowId(99)).is_none());
assert_eq!(sp.len(), 2);
}
#[test]
fn a_split_cannot_hold_fewer_than_two_children() {
let sp = Split::new(Axis::Stacked, pane(1), pane(2));
assert_eq!(sp.len(), 2);
assert_eq!(sp.children().count(), 2);
}
}