use retroglyph_core::Rect;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Constraint {
Fixed(u16),
Percent(u16),
Fill(u16),
Min(u16),
Max(u16),
}
impl Constraint {
fn base(self, total: u16) -> u16 {
match self {
Self::Fixed(n) | Self::Min(n) => n.min(total),
Self::Percent(p) => {
let p = u32::from(p.min(100));
#[allow(clippy::cast_possible_truncation)]
{
(u32::from(total) * p / 100) as u16
}
}
Self::Fill(_) | Self::Max(_) => 0,
}
}
}
const STACK_CAP: usize = 8;
enum SmallBuf<T: Copy + Default, const N: usize> {
Stack([T; N], usize),
Heap(Vec<T>),
}
impl<T: Copy + Default, const N: usize> SmallBuf<T, N> {
fn with_capacity(cap: usize) -> Self {
if cap <= N {
Self::Stack([T::default(); N], 0)
} else {
Self::Heap(Vec::with_capacity(cap))
}
}
fn push(&mut self, value: T) {
match self {
Self::Stack(buf, len) => {
buf[*len] = value;
*len += 1;
}
Self::Heap(vec) => vec.push(value),
}
}
}
impl<T: Copy + Default, const N: usize> std::ops::Deref for SmallBuf<T, N> {
type Target = [T];
fn deref(&self) -> &[T] {
match self {
Self::Stack(buf, len) => &buf[..*len],
Self::Heap(vec) => vec,
}
}
}
impl<T: Copy + Default, const N: usize> std::ops::DerefMut for SmallBuf<T, N> {
fn deref_mut(&mut self) -> &mut [T] {
match self {
Self::Stack(buf, len) => &mut buf[..*len],
Self::Heap(vec) => vec,
}
}
}
impl<T: Copy + Default, const N: usize> std::ops::Index<usize> for SmallBuf<T, N> {
type Output = T;
fn index(&self, idx: usize) -> &T {
&(**self)[idx]
}
}
impl<T: Copy + Default, const N: usize> std::ops::IndexMut<usize> for SmallBuf<T, N> {
fn index_mut(&mut self, idx: usize) -> &mut T {
&mut (**self)[idx]
}
}
fn solve(total: u16, constraints: &[Constraint]) -> SmallBuf<u16, STACK_CAP> {
let mut sizes: SmallBuf<u16, STACK_CAP> = SmallBuf::with_capacity(constraints.len());
for c in constraints {
sizes.push(c.base(total));
}
let mut used: u16 = 0;
for size in sizes.iter_mut() {
let room = total.saturating_sub(used);
*size = (*size).min(room);
used += *size;
}
let mut flexible: SmallBuf<(usize, u16, Option<u16>), STACK_CAP> =
SmallBuf::with_capacity(constraints.len());
for (i, c) in constraints.iter().enumerate() {
match c {
Constraint::Fill(weight) => flexible.push((i, *weight, None)),
Constraint::Min(_) => flexible.push((i, 1, None)),
Constraint::Max(cap) => flexible.push((i, 1, Some(*cap))),
Constraint::Fixed(_) | Constraint::Percent(_) => {}
}
}
if !flexible.is_empty() {
let remainder = total.saturating_sub(used);
let total_weight: u32 = flexible.iter().map(|&(_, w, _)| u32::from(w)).sum();
if let Some(total_weight) = std::num::NonZeroU32::new(total_weight) {
let mut shares: SmallBuf<u32, STACK_CAP> = SmallBuf::with_capacity(flexible.len());
let mut fracs: SmallBuf<u32, STACK_CAP> = SmallBuf::with_capacity(flexible.len());
let mut floor_sum: u32 = 0;
for &(_, weight, _) in flexible.iter() {
let product = u32::from(remainder) * u32::from(weight);
let share = product / total_weight;
fracs.push(product % total_weight);
shares.push(share);
floor_sum += share;
}
let mut leftover = u32::from(remainder).saturating_sub(floor_sum);
let mut order: SmallBuf<usize, STACK_CAP> = SmallBuf::with_capacity(flexible.len());
for idx in 0..flexible.len() {
order.push(idx);
}
order.sort_by(|&a, &b| fracs[b].cmp(&fracs[a]).then(a.cmp(&b)));
for &idx in order.iter() {
if leftover == 0 {
break;
}
shares[idx] += 1;
leftover -= 1;
}
for (k, &(i, _, cap)) in flexible.iter().enumerate() {
#[allow(clippy::cast_possible_truncation)]
let share = shares[k] as u16;
let grown = sizes[i].saturating_add(share);
sizes[i] = cap.map_or(grown, |max| grown.min(max));
}
}
}
sizes
}
#[must_use]
pub fn split_v(area: Rect, constraints: &[Constraint]) -> Vec<Rect> {
let sizes = solve(area.height(), constraints);
let mut y = area.top();
sizes
.iter()
.copied()
.map(|h| {
let rect = Rect::new(area.left(), y, area.width(), h);
y = y.saturating_add(h);
rect
})
.collect()
}
#[must_use]
pub fn split_h(area: Rect, constraints: &[Constraint]) -> Vec<Rect> {
let sizes = solve(area.width(), constraints);
let mut x = area.left();
sizes
.iter()
.copied()
.map(|w| {
let rect = Rect::new(x, area.top(), w, area.height());
x = x.saturating_add(w);
rect
})
.collect()
}
fn interleave_gaps(constraints: &[Constraint], spacing: u16) -> Vec<Constraint> {
let mut out = Vec::with_capacity(constraints.len().saturating_mul(2).saturating_sub(1));
for (i, &c) in constraints.iter().enumerate() {
if i > 0 {
out.push(Constraint::Fixed(spacing));
}
out.push(c);
}
out
}
#[must_use]
pub fn split_h_spaced(area: Rect, constraints: &[Constraint], spacing: u16) -> Vec<Rect> {
if spacing == 0 || constraints.len() < 2 {
return split_h(area, constraints);
}
split_h(area, &interleave_gaps(constraints, spacing))
.into_iter()
.step_by(2)
.collect()
}
#[must_use]
pub fn split_v_spaced(area: Rect, constraints: &[Constraint], spacing: u16) -> Vec<Rect> {
if spacing == 0 || constraints.len() < 2 {
return split_v(area, constraints);
}
split_v(area, &interleave_gaps(constraints, spacing))
.into_iter()
.step_by(2)
.collect()
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Flex {
#[default]
Start,
End,
Center,
SpaceBetween,
SpaceAround,
}
fn place(total: u16, sizes: &[u16], flex: Flex) -> Vec<u16> {
let content: u16 = sizes.iter().fold(0u16, |a, &b| a.saturating_add(b));
let slack = total.saturating_sub(content);
let n = sizes.len();
let mut offsets = Vec::with_capacity(n);
let packed_from = |start: u16| {
let mut pos = start;
sizes
.iter()
.map(|&s| {
let at = pos;
pos = pos.saturating_add(s);
at
})
.collect::<Vec<u16>>()
};
match flex {
Flex::End => offsets = packed_from(slack),
Flex::Center => offsets = packed_from(slack / 2),
Flex::SpaceBetween if n > 1 => {
#[allow(clippy::cast_possible_truncation)]
let gaps = n as u16 - 1;
let gap = slack / gaps;
let mut extra = slack % gaps;
let mut pos = 0;
for (i, &s) in sizes.iter().enumerate() {
offsets.push(pos);
pos = pos.saturating_add(s);
if i + 1 < n {
pos = pos.saturating_add(gap + u16::from(extra > 0));
extra = extra.saturating_sub(1);
}
}
}
Flex::Start | Flex::SpaceBetween => offsets = packed_from(0),
Flex::SpaceAround => {
#[allow(clippy::cast_possible_truncation)]
let gaps = n as u16 + 1;
let unit = slack / gaps;
let mut extra = slack % gaps;
let mut pos = unit + u16::from(extra > 0);
extra = extra.saturating_sub(u16::from(extra > 0));
for &s in sizes {
offsets.push(pos);
pos = pos.saturating_add(s);
pos = pos.saturating_add(unit + u16::from(extra > 0));
extra = extra.saturating_sub(u16::from(extra > 0));
}
}
}
offsets
}
#[must_use]
pub fn split_v_flex(area: Rect, constraints: &[Constraint], flex: Flex) -> Vec<Rect> {
let sizes = solve(area.height(), constraints);
let offsets = place(area.height(), &sizes, flex);
offsets
.into_iter()
.zip(sizes.iter().copied())
.map(|(y, h)| Rect::new(area.left(), area.top().saturating_add(y), area.width(), h))
.collect()
}
#[must_use]
pub fn split_h_flex(area: Rect, constraints: &[Constraint], flex: Flex) -> Vec<Rect> {
let sizes = solve(area.width(), constraints);
let offsets = place(area.width(), &sizes, flex);
offsets
.into_iter()
.zip(sizes.iter().copied())
.map(|(x, w)| Rect::new(area.left().saturating_add(x), area.top(), w, area.height()))
.collect()
}
#[must_use]
pub fn centered_rect(screen: Rect, width: u16, height: u16) -> Rect {
let width = width.min(screen.width());
let height = height.min(screen.height());
let x = screen.left() + (screen.width() - width) / 2;
let y = screen.top() + (screen.height() - height) / 2;
Rect::new(x, y, width, height)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vertical_split_sums_and_clamps() {
let area = Rect::new(0, 0, 20, 10);
let panes = split_v(
area,
&[
Constraint::Fixed(1),
Constraint::Fill(1),
Constraint::Fixed(1),
],
);
assert_eq!(panes.len(), 3);
assert_eq!(panes[0].height(), 1);
assert_eq!(panes[1].height(), 8);
assert_eq!(panes[2].height(), 1);
assert_eq!(panes[0].top(), 0);
assert_eq!(panes[1].top(), 1);
assert_eq!(panes[2].top(), 9);
assert_eq!(panes[2].bottom(), area.bottom());
for p in &panes {
assert_eq!(p.width(), 20);
}
}
#[test]
fn horizontal_percent_and_fill() {
let area = Rect::new(0, 0, 100, 5);
let panes = split_h(area, &[Constraint::Percent(30), Constraint::Fill(1)]);
assert_eq!(panes[0].width(), 30);
assert_eq!(panes[1].width(), 70);
assert_eq!(panes[0].left(), 0);
assert_eq!(panes[1].left(), 30);
assert_eq!(panes[1].right(), area.right());
}
#[test]
fn fill_remainder_distributes_evenly() {
let area = Rect::new(0, 0, 10, 1);
let panes = split_h(
area,
&[
Constraint::Fill(1),
Constraint::Fill(1),
Constraint::Fill(1),
],
);
let widths: Vec<u16> = panes.iter().map(Rect::width).collect();
assert_eq!(widths, vec![4, 3, 3]);
assert_eq!(widths.iter().sum::<u16>(), 10);
}
#[test]
fn oversized_fixed_is_clamped() {
let area = Rect::new(0, 0, 5, 3);
let panes = split_h(area, &[Constraint::Fixed(10), Constraint::Fixed(10)]);
assert_eq!(panes[0].width(), 5);
assert_eq!(panes[1].width(), 0);
for p in &panes {
assert!(p.right() <= area.right());
}
}
#[test]
fn no_fill_leaves_gap() {
let area = Rect::new(0, 0, 10, 4);
let panes = split_v(area, &[Constraint::Fixed(2), Constraint::Fixed(2)]);
assert_eq!(panes[0].height(), 2);
assert_eq!(panes[1].height(), 2);
assert_eq!(panes[1].bottom(), 4);
}
#[test]
fn min_gets_at_least_its_floor_plus_a_share() {
let area = Rect::new(0, 0, 10, 1);
let panes = split_h(area, &[Constraint::Min(3), Constraint::Fill(1)]);
let widths: Vec<u16> = panes.iter().map(Rect::width).collect();
assert_eq!(widths, vec![7, 3]);
assert_eq!(widths.iter().sum::<u16>(), 10);
}
#[test]
fn min_floor_holds_when_share_would_be_smaller() {
let area = Rect::new(0, 0, 10, 1);
let panes = split_h(
area,
&[Constraint::Min(4), Constraint::Fill(1), Constraint::Fill(1)],
);
let widths: Vec<u16> = panes.iter().map(Rect::width).collect();
assert_eq!(widths[0], 6);
assert_eq!(widths[1], 2);
assert_eq!(widths[2], 2);
assert_eq!(widths.iter().sum::<u16>(), 10);
}
#[test]
fn max_caps_its_share_and_leaves_the_rest_unclaimed() {
let area = Rect::new(0, 0, 10, 1);
let panes = split_h(area, &[Constraint::Fill(1), Constraint::Max(2)]);
let widths: Vec<u16> = panes.iter().map(Rect::width).collect();
assert_eq!(widths, vec![5, 2]);
assert_eq!(widths.iter().sum::<u16>(), 7);
}
#[test]
fn weighted_fill_splits_proportionally() {
let area = Rect::new(0, 0, 12, 1);
let panes = split_h(area, &[Constraint::Fill(1), Constraint::Fill(2)]);
let widths: Vec<u16> = panes.iter().map(Rect::width).collect();
assert_eq!(widths, vec![4, 8]);
assert_eq!(widths.iter().sum::<u16>(), 12);
}
#[test]
fn weighted_fill_at_weight_one_matches_equal_distribution() {
let area = Rect::new(0, 0, 10, 1);
let panes = split_h(
area,
&[
Constraint::Fill(5),
Constraint::Fill(5),
Constraint::Fill(5),
],
);
let widths: Vec<u16> = panes.iter().map(Rect::width).collect();
assert_eq!(widths, vec![4, 3, 3]);
assert_eq!(widths.iter().sum::<u16>(), 10);
}
#[test]
fn weighted_fill_leftover_goes_to_the_largest_fractional_share() {
let area = Rect::new(0, 0, 10, 1);
let panes = split_h(
area,
&[
Constraint::Fill(3),
Constraint::Fill(2),
Constraint::Fill(2),
],
);
let widths: Vec<u16> = panes.iter().map(Rect::width).collect();
assert_eq!(widths, vec![4, 3, 3]);
assert_eq!(widths.iter().sum::<u16>(), 10);
}
#[test]
fn fill_weight_zero_claims_no_share_of_the_remainder() {
let area = Rect::new(0, 0, 10, 1);
let panes = split_h(area, &[Constraint::Fill(0), Constraint::Fill(1)]);
let widths: Vec<u16> = panes.iter().map(Rect::width).collect();
assert_eq!(widths, vec![0, 10]);
}
#[test]
fn all_fill_weights_zero_leaves_the_remainder_unclaimed() {
let area = Rect::new(0, 0, 10, 1);
let panes = split_h(area, &[Constraint::Fill(0), Constraint::Fill(0)]);
let widths: Vec<u16> = panes.iter().map(Rect::width).collect();
assert_eq!(widths, vec![0, 0]);
}
#[test]
fn weighted_fill_mixes_with_min_and_max_at_weight_one() {
let area = Rect::new(0, 0, 20, 1);
let panes = split_h(
area,
&[
Constraint::Fill(3),
Constraint::Min(2),
Constraint::Fill(1),
Constraint::Max(10),
],
);
let widths: Vec<u16> = panes.iter().map(Rect::width).collect();
assert_eq!(widths, vec![9, 5, 3, 3]);
assert_eq!(widths.iter().sum::<u16>(), 20);
}
#[test]
fn flex_start_matches_split_v() {
let area = Rect::new(0, 0, 10, 4);
let constraints = [Constraint::Fixed(2), Constraint::Fixed(2)];
let legacy = split_v(area, &constraints);
let flexed = split_v_flex(area, &constraints, Flex::Start);
assert_eq!(legacy, flexed);
}
#[test]
fn flex_end_pushes_leftover_before_the_panes() {
let area = Rect::new(0, 0, 10, 10);
let panes = split_v_flex(
area,
&[Constraint::Fixed(2), Constraint::Fixed(2)],
Flex::End,
);
assert_eq!(panes[0].top(), 6);
assert_eq!(panes[1].top(), 8);
assert_eq!(panes[1].bottom(), 10);
}
#[test]
fn flex_center_splits_leftover_around_the_panes() {
let area = Rect::new(0, 0, 10, 10);
let panes = split_v_flex(area, &[Constraint::Fixed(4)], Flex::Center);
assert_eq!(panes[0].top(), 3);
assert_eq!(panes[0].bottom(), 7);
}
#[test]
fn flex_space_between_puts_leftover_between_panes_only() {
let area = Rect::new(0, 0, 10, 1);
let panes = split_h_flex(
area,
&[Constraint::Fixed(2), Constraint::Fixed(2)],
Flex::SpaceBetween,
);
assert_eq!(panes[0].left(), 0);
assert_eq!(panes[0].right(), 2);
assert_eq!(panes[1].left(), 8);
assert_eq!(panes[1].right(), 10);
}
#[test]
fn flex_space_around_puts_equal_gaps_at_both_edges() {
let area = Rect::new(0, 0, 9, 1);
let panes = split_h_flex(area, &[Constraint::Fixed(3)], Flex::SpaceAround);
assert_eq!(panes[0].left(), 3);
assert_eq!(panes[0].right(), 6);
}
#[test]
fn spaced_split_carves_out_gaps_between_panes() {
let area = Rect::new(0, 0, 59, 6);
let panes = split_h_spaced(
area,
&[
Constraint::Fill(1),
Constraint::Fill(1),
Constraint::Fill(1),
],
1,
);
assert_eq!(panes.len(), 3);
let widths: Vec<u16> = panes.iter().map(Rect::width).collect();
assert_eq!(widths, vec![19, 19, 19]);
assert_eq!(panes[1].left(), panes[0].right() + 1);
assert_eq!(panes[2].left(), panes[1].right() + 1);
}
#[test]
fn spaced_split_falls_back_with_one_pane_or_no_spacing() {
let area = Rect::new(0, 0, 10, 1);
assert_eq!(
split_h_spaced(area, &[Constraint::Fill(1)], 1),
split_h(area, &[Constraint::Fill(1)])
);
assert_eq!(
split_h_spaced(area, &[Constraint::Fill(1), Constraint::Fill(1)], 0),
split_h(area, &[Constraint::Fill(1), Constraint::Fill(1)])
);
}
#[test]
fn vertical_spaced_split_matches_horizontal_shape() {
let area = Rect::new(0, 0, 6, 59);
let panes = split_v_spaced(
area,
&[
Constraint::Fill(1),
Constraint::Fill(1),
Constraint::Fill(1),
],
1,
);
let heights: Vec<u16> = panes.iter().map(Rect::height).collect();
assert_eq!(heights, vec![19, 19, 19]);
assert_eq!(panes[1].top(), panes[0].bottom() + 1);
}
#[test]
fn centered_rect_centers_within_the_screen() {
let screen = Rect::new(0, 0, 20, 10);
let r = centered_rect(screen, 10, 4);
assert_eq!(r, Rect::new(5, 3, 10, 4));
}
#[test]
fn centered_rect_clamps_to_the_screen_size_when_larger() {
let screen = Rect::new(0, 0, 20, 10);
let r = centered_rect(screen, 100, 100);
assert_eq!(r, Rect::new(0, 0, 20, 10));
}
#[test]
fn centered_rect_respects_a_non_origin_screen() {
let screen = Rect::new(5, 5, 20, 10);
let r = centered_rect(screen, 10, 4);
assert_eq!(r, Rect::new(10, 8, 10, 4));
}
#[test]
fn split_beyond_stack_cap_matches_small_case_behavior() {
let panes = 20; #[allow(clippy::cast_possible_truncation)]
let panes_u16 = panes as u16;
let area = Rect::new(0, 0, panes_u16, 1);
let constraints = vec![Constraint::Fixed(1); panes];
let widths: Vec<u16> = split_h(area, &constraints)
.iter()
.map(Rect::width)
.collect();
assert_eq!(widths, vec![1u16; panes]);
assert_eq!(widths.iter().sum::<u16>(), panes_u16);
}
#[test]
fn weighted_fill_beyond_stack_cap_matches_small_case_proportions() {
let area = Rect::new(0, 0, 100, 1);
let constraints = vec![Constraint::Fill(1); 20];
let widths: Vec<u16> = split_h(area, &constraints)
.iter()
.map(Rect::width)
.collect();
assert_eq!(widths.len(), 20);
assert_eq!(widths.iter().sum::<u16>(), 100);
assert!(widths.iter().all(|&w| w == 5));
}
}