use super::{Constraint, Direction};
use crate::spec_ai_tui::geometry::Rect;
#[derive(Debug, Clone)]
pub struct Layout {
direction: Direction,
constraints: Vec<Constraint>,
margin: u16,
spacing: u16,
}
impl Layout {
pub fn horizontal() -> Self {
Self {
direction: Direction::Horizontal,
constraints: Vec::new(),
margin: 0,
spacing: 0,
}
}
pub fn vertical() -> Self {
Self {
direction: Direction::Vertical,
constraints: Vec::new(),
margin: 0,
spacing: 0,
}
}
pub fn new(direction: Direction) -> Self {
Self {
direction,
constraints: Vec::new(),
margin: 0,
spacing: 0,
}
}
pub fn direction(mut self, direction: Direction) -> Self {
self.direction = direction;
self
}
pub fn constraints<I: IntoIterator<Item = Constraint>>(mut self, constraints: I) -> Self {
self.constraints = constraints.into_iter().collect();
self
}
pub fn margin(mut self, margin: u16) -> Self {
self.margin = margin;
self
}
pub fn spacing(mut self, spacing: u16) -> Self {
self.spacing = spacing;
self
}
pub fn split(&self, area: Rect) -> Vec<Rect> {
let inner = area.inner(self.margin);
if inner.is_empty() || self.constraints.is_empty() {
return vec![];
}
let (total_space, cross_size) = match self.direction {
Direction::Horizontal => (inner.width, inner.height),
Direction::Vertical => (inner.height, inner.width),
};
let num_gaps = self.constraints.len().saturating_sub(1) as u16;
let spacing_total = self.spacing * num_gaps;
let available = total_space.saturating_sub(spacing_total);
let mut sizes: Vec<u16> = vec![0; self.constraints.len()];
let mut remaining = available;
let mut total_fill_weight = 0u32;
for (i, constraint) in self.constraints.iter().enumerate() {
let resolve_base = match constraint {
Constraint::Percentage(_) | Constraint::Ratio(_, _) => available,
_ => remaining,
};
let (size, is_fill) = constraint.resolve(resolve_base);
if is_fill {
total_fill_weight += constraint.fill_weight() as u32;
} else {
sizes[i] = size;
remaining = remaining.saturating_sub(size);
}
}
if total_fill_weight > 0 && remaining > 0 {
let fill_space = remaining;
let mut distributed = 0u16;
for (i, constraint) in self.constraints.iter().enumerate() {
if let Constraint::Fill(weight) = constraint {
let share = (fill_space as u32 * *weight as u32 / total_fill_weight) as u16;
sizes[i] = share;
distributed += share;
}
}
let leftover = fill_space.saturating_sub(distributed);
if leftover > 0 {
for (i, constraint) in self.constraints.iter().enumerate().rev() {
if matches!(constraint, Constraint::Fill(_)) {
sizes[i] = sizes[i].saturating_add(leftover);
break;
}
}
}
}
let mut result = Vec::with_capacity(self.constraints.len());
let mut offset = match self.direction {
Direction::Horizontal => inner.x,
Direction::Vertical => inner.y,
};
for (i, size) in sizes.into_iter().enumerate() {
let rect = match self.direction {
Direction::Horizontal => Rect::new(offset, inner.y, size, cross_size),
Direction::Vertical => Rect::new(inner.x, offset, cross_size, size),
};
result.push(rect);
offset = offset.saturating_add(size);
if i < self.constraints.len() - 1 {
offset = offset.saturating_add(self.spacing);
}
}
result
}
}
impl Default for Layout {
fn default() -> Self {
Self::vertical()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vertical_split_fixed() {
let area = Rect::new(0, 0, 100, 50);
let chunks = Layout::vertical()
.constraints([
Constraint::Fixed(10),
Constraint::Fixed(20),
Constraint::Fixed(10),
])
.split(area);
assert_eq!(chunks.len(), 3);
assert_eq!(chunks[0], Rect::new(0, 0, 100, 10));
assert_eq!(chunks[1], Rect::new(0, 10, 100, 20));
assert_eq!(chunks[2], Rect::new(0, 30, 100, 10));
}
#[test]
fn test_vertical_split_with_fill() {
let area = Rect::new(0, 0, 100, 50);
let chunks = Layout::vertical()
.constraints([
Constraint::Fixed(10),
Constraint::Fill(1),
Constraint::Fixed(5),
])
.split(area);
assert_eq!(chunks.len(), 3);
assert_eq!(chunks[0].height, 10);
assert_eq!(chunks[1].height, 35); assert_eq!(chunks[2].height, 5);
}
#[test]
fn test_horizontal_split() {
let area = Rect::new(0, 0, 100, 50);
let chunks = Layout::horizontal()
.constraints([Constraint::Percentage(30), Constraint::Fill(1)])
.split(area);
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].width, 30);
assert_eq!(chunks[1].width, 70);
assert_eq!(chunks[0].height, 50);
assert_eq!(chunks[1].height, 50);
}
#[test]
fn test_multiple_fills() {
let area = Rect::new(0, 0, 100, 100);
let chunks = Layout::vertical()
.constraints([
Constraint::Fill(1),
Constraint::Fill(2),
Constraint::Fill(1),
])
.split(area);
assert_eq!(chunks.len(), 3);
assert_eq!(chunks[0].height, 25);
assert_eq!(chunks[1].height, 50);
assert_eq!(chunks[2].height, 25);
}
#[test]
fn test_with_margin() {
let area = Rect::new(0, 0, 100, 50);
let chunks = Layout::vertical()
.margin(5)
.constraints([Constraint::Fill(1)])
.split(area);
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0], Rect::new(5, 5, 90, 40)); }
#[test]
fn test_with_spacing() {
let area = Rect::new(0, 0, 100, 50);
let chunks = Layout::vertical()
.spacing(2)
.constraints([
Constraint::Fixed(10),
Constraint::Fixed(10),
Constraint::Fixed(10),
])
.split(area);
assert_eq!(chunks.len(), 3);
assert_eq!(chunks[0].y, 0);
assert_eq!(chunks[1].y, 12); assert_eq!(chunks[2].y, 24); }
#[test]
fn test_percentage() {
let area = Rect::new(0, 0, 100, 100);
let chunks = Layout::vertical()
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(50),
Constraint::Percentage(25),
])
.split(area);
assert_eq!(chunks[0].height, 25);
assert_eq!(chunks[1].height, 50);
assert_eq!(chunks[2].height, 25);
}
#[test]
fn test_empty_constraints() {
let area = Rect::new(0, 0, 100, 50);
let chunks = Layout::vertical().constraints([]).split(area);
assert!(chunks.is_empty());
}
#[test]
fn test_empty_area() {
let area = Rect::new(0, 0, 0, 0);
let chunks = Layout::vertical()
.constraints([Constraint::Fill(1)])
.split(area);
assert!(chunks.is_empty());
}
}