use retroglyph_core::Rect;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Constraint {
Fixed(u16),
Percent(u16),
Fill,
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,
}
}
}
fn solve(total: u16, constraints: &[Constraint]) -> Vec<u16> {
let mut sizes: Vec<u16> = constraints.iter().map(|c| c.base(total)).collect();
let mut used: u16 = 0;
for size in &mut sizes {
let room = total.saturating_sub(used);
*size = (*size).min(room);
used += *size;
}
let flexible: Vec<(usize, Option<u16>)> = constraints
.iter()
.enumerate()
.filter_map(|(i, c)| match c {
Constraint::Fill | Constraint::Min(_) => Some((i, None)),
Constraint::Max(cap) => Some((i, Some(*cap))),
Constraint::Fixed(_) | Constraint::Percent(_) => None,
})
.collect();
if !flexible.is_empty() {
let remainder = total.saturating_sub(used);
#[allow(clippy::cast_possible_truncation)]
let each = remainder / flexible.len() as u16;
#[allow(clippy::cast_possible_truncation)]
let mut extra = remainder % flexible.len() as u16;
for &(i, cap) in &flexible {
let share = each + u16::from(extra > 0);
extra = extra.saturating_sub(1);
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
.into_iter()
.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
.into_iter()
.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)
.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)
.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, 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]);
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, Constraint::Fill, Constraint::Fill],
);
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]);
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, Constraint::Fill],
);
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, 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 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, Constraint::Fill, Constraint::Fill],
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),
split_h(area, &[Constraint::Fill])
);
assert_eq!(
split_h_spaced(area, &[Constraint::Fill, Constraint::Fill], 0),
split_h(area, &[Constraint::Fill, Constraint::Fill])
);
}
#[test]
fn vertical_spaced_split_matches_horizontal_shape() {
let area = Rect::new(0, 0, 6, 59);
let panes = split_v_spaced(
area,
&[Constraint::Fill, Constraint::Fill, Constraint::Fill],
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));
}
}