use crate::core::rect::Rect;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Constraint {
Length(u16),
Min(u16),
Max(u16),
Percentage(u16),
Ratio(u32, u32),
Fill(u16),
}
impl Constraint {
pub fn apply(&self, total: u16) -> u16 {
match *self {
Constraint::Length(l) => l.min(total),
Constraint::Min(_) => total,
Constraint::Max(m) => total.min(m),
Constraint::Percentage(p) => (total as f32 * p as f32 / 100.0) as u16,
Constraint::Ratio(a, b) => {
if b == 0 {
0
} else {
(total as f32 * a as f32 / b as f32) as u16
}
}
Constraint::Fill(_) => total,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Direction {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Layout {
pub direction: Direction,
pub constraints: Vec<Constraint>,
pub margin: Rect,
}
impl Layout {
pub fn default() -> Self {
Self {
direction: Direction::Vertical,
constraints: Vec::new(),
margin: Rect::ZERO,
}
}
pub fn horizontal(constraints: Vec<Constraint>) -> Self {
Self {
direction: Direction::Horizontal,
constraints,
margin: Rect::ZERO,
}
}
pub fn vertical(constraints: Vec<Constraint>) -> Self {
Self {
direction: Direction::Vertical,
constraints,
margin: Rect::ZERO,
}
}
pub fn margin(mut self, margin: Rect) -> Self {
self.margin = margin;
self
}
pub fn direction(mut self, direction: Direction) -> Self {
self.direction = direction;
self
}
pub fn constraints<I>(mut self, constraints: I) -> Self
where
I: Into<Vec<Constraint>>,
{
self.constraints = constraints.into();
self
}
pub fn split(&self, area: Rect) -> Vec<Rect> {
let inner = area.inner(self.margin);
match self.direction {
Direction::Horizontal => self.split_horizontal(inner),
Direction::Vertical => self.split_vertical(inner),
}
}
fn split_horizontal(&self, area: Rect) -> Vec<Rect> {
let positions = allocate_sizes(area.width, &self.constraints);
let mut rects = Vec::with_capacity(self.constraints.len());
let mut x = area.x;
for &size in &positions {
let available = area.right().saturating_sub(x);
let width = size.min(available);
rects.push(Rect::new(x, area.y, width, area.height));
x = x.saturating_add(width);
}
rects
}
fn split_vertical(&self, area: Rect) -> Vec<Rect> {
let positions = allocate_sizes(area.height, &self.constraints);
let mut rects = Vec::with_capacity(self.constraints.len());
let mut y = area.y;
for &size in &positions {
let available = area.bottom().saturating_sub(y);
let height = size.min(available);
rects.push(Rect::new(area.x, y, area.width, height));
y = y.saturating_add(height);
}
rects
}
}
fn allocate_sizes(total: u16, constraints: &[Constraint]) -> Vec<u16> {
if constraints.is_empty() {
return Vec::new();
}
let total_usize = total as usize;
let mut sizes = vec![0usize; constraints.len()];
let mut fixed_total = 0usize;
let mut flex = Vec::new();
for (idx, constraint) in constraints.iter().copied().enumerate() {
match constraint {
Constraint::Length(length) => {
sizes[idx] = length as usize;
fixed_total = fixed_total.saturating_add(sizes[idx]);
}
Constraint::Max(max) => {
sizes[idx] = (max as usize).min(total_usize);
fixed_total = fixed_total.saturating_add(sizes[idx]);
}
Constraint::Percentage(percent) => {
sizes[idx] = total_usize.saturating_mul(percent as usize) / 100;
fixed_total = fixed_total.saturating_add(sizes[idx]);
}
Constraint::Ratio(numerator, denominator) => {
sizes[idx] = if denominator == 0 {
0
} else {
total_usize.saturating_mul(numerator as usize) / denominator as usize
};
fixed_total = fixed_total.saturating_add(sizes[idx]);
}
Constraint::Min(min) => flex.push((idx, min as usize, 1usize)),
Constraint::Fill(weight) => flex.push((idx, 0usize, weight.max(1) as usize)),
}
}
let remaining = total_usize.saturating_sub(fixed_total);
if !flex.is_empty() {
let min_total: usize = flex.iter().map(|(_, min, _)| *min).sum();
if remaining >= min_total {
for (idx, min, _) in &flex {
sizes[*idx] = *min;
}
let extra = remaining - min_total;
let weight_total: usize = flex.iter().map(|(_, _, weight)| *weight).sum();
let mut used_extra = 0usize;
for (pos, (idx, _, weight)) in flex.iter().enumerate() {
let add = if pos + 1 == flex.len() {
extra.saturating_sub(used_extra)
} else {
extra.saturating_mul(*weight) / weight_total.max(1)
};
sizes[*idx] = sizes[*idx].saturating_add(add);
used_extra = used_extra.saturating_add(add);
}
} else {
let mut used = 0usize;
for (pos, (idx, min, _)) in flex.iter().enumerate() {
let size = if pos + 1 == flex.len() {
remaining.saturating_sub(used)
} else {
remaining.saturating_mul(*min) / min_total.max(1)
};
sizes[*idx] = size;
used = used.saturating_add(size);
}
}
}
let mut used = 0usize;
sizes
.into_iter()
.map(|size| {
let clamped = size.min(total_usize.saturating_sub(used));
used = used.saturating_add(clamped);
clamped as u16
})
.collect()
}
impl Default for Layout {
fn default() -> Self {
Self::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_constraint_length() {
assert_eq!(Constraint::Length(5).apply(20), 5);
assert_eq!(Constraint::Length(25).apply(20), 20);
}
#[test]
fn test_constraint_percentage() {
assert_eq!(Constraint::Percentage(50).apply(20), 10);
assert_eq!(Constraint::Percentage(100).apply(20), 20);
}
#[test]
fn test_layout_split_vertical() {
let layout = Layout::vertical(vec![
Constraint::Length(3),
Constraint::Length(5),
Constraint::Min(0),
]);
let area = Rect::new(0, 0, 40, 20);
let rects = layout.split(area);
assert_eq!(rects.len(), 3);
assert_eq!(rects[0].height, 3);
assert_eq!(rects[1].height, 5);
assert_eq!(rects[2].y, 8);
assert_eq!(rects[2].height, 12);
}
#[test]
fn test_layout_split_horizontal() {
let layout = Layout::horizontal(vec![
Constraint::Percentage(33),
Constraint::Percentage(33),
Constraint::Min(0),
]);
let area = Rect::new(0, 0, 30, 10);
let rects = layout.split(area);
assert_eq!(rects.len(), 3);
assert_eq!(rects[0].width, 9);
assert_eq!(rects[0].height, 10);
assert_eq!(rects[2].width, 12);
}
#[test]
fn test_layout_length_min_length_matches_remaining_space() {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Length(3),
Constraint::Min(0),
Constraint::Length(2),
]);
let rects = layout.split(Rect::new(0, 0, 20, 10));
assert_eq!(rects[0], Rect::new(0, 0, 20, 3));
assert_eq!(rects[1], Rect::new(0, 3, 20, 5));
assert_eq!(rects[2], Rect::new(0, 8, 20, 2));
}
#[test]
fn test_layout_never_overflows_area() {
let layout = Layout::horizontal(vec![
Constraint::Length(8),
Constraint::Length(8),
Constraint::Min(0),
]);
let rects = layout.split(Rect::new(2, 1, 10, 3));
assert_eq!(rects.last().unwrap().right(), 12);
}
}