use crate::core::rect::{Margin, Rect};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Direction {
#[default]
Vertical,
Horizontal,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Constraint {
Length(f32),
Percentage(f32),
Min(f32),
Max(f32),
Ratio(u32, u32),
Fill(f32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Flex {
#[default]
Start,
Center,
End,
SpaceBetween,
SpaceAround,
SpaceEvenly,
}
#[derive(Debug, Clone)]
pub struct Layout {
direction: Direction,
constraints: Vec<Constraint>,
margin: Margin,
flex: Flex,
spacing: f32,
}
impl Default for Layout {
fn default() -> Self {
Self {
direction: Direction::Vertical,
constraints: Vec::new(),
margin: Margin::ZERO,
flex: Flex::Start,
spacing: 0.0,
}
}
}
impl Layout {
pub fn new(direction: Direction, constraints: impl Into<Vec<Constraint>>) -> Self {
Self {
direction,
constraints: constraints.into(),
..Default::default()
}
}
pub fn vertical(constraints: impl Into<Vec<Constraint>>) -> Self {
Self::new(Direction::Vertical, constraints)
}
pub fn horizontal(constraints: impl Into<Vec<Constraint>>) -> Self {
Self::new(Direction::Horizontal, constraints)
}
pub fn direction(mut self, direction: Direction) -> Self {
self.direction = direction;
self
}
pub fn constraints(mut self, constraints: impl Into<Vec<Constraint>>) -> Self {
self.constraints = constraints.into();
self
}
pub fn margin(mut self, margin: Margin) -> Self {
self.margin = margin;
self
}
pub fn flex(mut self, flex: Flex) -> Self {
self.flex = flex;
self
}
pub fn spacing(mut self, spacing: f32) -> Self {
self.spacing = spacing;
self
}
pub fn split(&self, area: Rect) -> Vec<Rect> {
let inner = area.inner(&self.margin);
if self.constraints.is_empty() || inner.is_empty() {
return vec![inner];
}
let total_space = match self.direction {
Direction::Vertical => inner.height,
Direction::Horizontal => inner.width,
};
let n = self.constraints.len();
let total_spacing = if n > 1 {
(n as f32 - 1.0) * self.spacing
} else {
0.0
};
let available = (total_space - total_spacing).max(0.0);
let mut sizes: Vec<f32> = self
.constraints
.iter()
.map(|c| match c {
Constraint::Length(l) => l.min(available),
Constraint::Percentage(p) => available * p / 100.0,
Constraint::Min(m) => *m,
Constraint::Max(m) => m.min(available),
Constraint::Ratio(num, den) => {
if *den == 0 {
0.0
} else {
available * *num as f32 / *den as f32
}
}
Constraint::Fill(_) => 0.0,
})
.collect();
let fixed_total: f32 = sizes.iter().sum();
let remaining = (available - fixed_total).max(0.0);
let fill_total_weight: f32 = self
.constraints
.iter()
.filter_map(|c| match c {
Constraint::Fill(w) => Some(*w),
_ => None,
})
.sum();
if fill_total_weight > 0.0 && remaining > 0.0 {
for (i, c) in self.constraints.iter().enumerate() {
if let Constraint::Fill(w) = c {
sizes[i] = remaining * w / fill_total_weight;
}
}
}
let used: f32 = sizes.iter().sum();
let leftover = (available - used).max(0.0);
let min_count = self
.constraints
.iter()
.filter(|c| matches!(c, Constraint::Min(_)))
.count();
if min_count > 0 && leftover > 0.0 {
let share = leftover / min_count as f32;
for (i, c) in self.constraints.iter().enumerate() {
if matches!(c, Constraint::Min(_)) {
sizes[i] += share;
}
}
}
let total_used: f32 = sizes.iter().sum();
let excess = (available - total_used).max(0.0);
let offsets = match self.flex {
Flex::Start => vec![0.0; n],
Flex::End => {
let mut o = vec![0.0; n];
if n > 0 {
o[0] = excess;
}
o
}
Flex::Center => {
let mut o = vec![0.0; n];
if n > 0 {
o[0] = excess / 2.0;
}
o
}
Flex::SpaceBetween => {
let mut o = vec![0.0; n];
if n > 1 {
let gap = excess / (n as f32 - 1.0);
for item in o.iter_mut().skip(1) {
*item = gap;
}
}
o
}
Flex::SpaceAround => {
let mut o = vec![0.0; n];
let gap = excess / n as f32;
for (i, item) in o.iter_mut().enumerate() {
*item = if i == 0 { gap / 2.0 } else { gap };
}
o
}
Flex::SpaceEvenly => {
let mut o = vec![0.0; n];
let gap = excess / (n as f32 + 1.0);
for item in o.iter_mut() {
*item = gap;
}
o
}
};
let mut results = Vec::with_capacity(n);
let mut pos = match self.direction {
Direction::Vertical => inner.y,
Direction::Horizontal => inner.x,
};
for (i, size) in sizes.iter().enumerate() {
pos += offsets[i];
let rect = match self.direction {
Direction::Vertical => Rect::new(inner.x, pos, inner.width, *size),
Direction::Horizontal => Rect::new(pos, inner.y, *size, inner.height),
};
results.push(rect);
pos += size;
if i < n - 1 {
pos += self.spacing;
}
}
results
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vertical_split() {
let area = Rect::new(0.0, 0.0, 200.0, 400.0);
let chunks =
Layout::vertical(vec![Constraint::Length(100.0), Constraint::Fill(1.0)]).split(area);
assert_eq!(chunks.len(), 2);
assert!((chunks[0].height - 100.0).abs() < 0.01);
assert!((chunks[1].height - 300.0).abs() < 0.01);
}
#[test]
fn horizontal_split_with_spacing() {
let area = Rect::new(0.0, 0.0, 300.0, 100.0);
let chunks = Layout::horizontal(vec![
Constraint::Fill(1.0),
Constraint::Fill(1.0),
Constraint::Fill(1.0),
])
.spacing(10.0)
.split(area);
assert_eq!(chunks.len(), 3);
let expected = (300.0 - 20.0) / 3.0;
assert!((chunks[0].width - expected).abs() < 0.01);
}
}