use ratatui::layout::Rect;
#[allow(unused)]
use std::cell::RefCell;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Constraint {
Percentage(u16),
Fixed(u16),
Min(u16),
Max(u16),
Ratio(u16, u16),
}
impl Constraint {
pub fn resolve(self, available: u16, fixed_consumed: u16) -> u16 {
let remaining = available.saturating_sub(fixed_consumed);
match self {
Constraint::Percentage(p) => (remaining as u32 * p as u32 / 100) as u16,
Constraint::Fixed(f) => f.min(remaining),
Constraint::Min(m) => m.max(remaining),
Constraint::Max(m) => m.min(remaining),
Constraint::Ratio(num, den) => {
if den == 0 {
return remaining;
}
(remaining as u32 * num as u32 / den as u32) as u16
}
}
}
}
#[derive(Clone, Debug)]
pub struct Layout {
constraints: Vec<Constraint>,
direction: Direction,
spacing: u16,
margin: u16,
name: Option<&'static str>,
cached_layout: RefCell<Option<(Rect, Vec<Rect>)>>,
}
impl Layout {
pub fn new(constraints: Vec<Constraint>) -> Self {
Self {
constraints,
direction: Direction::Horizontal,
spacing: 0,
margin: 0,
name: None,
cached_layout: RefCell::new(None),
}
}
pub fn horizontal(constraints: Vec<Constraint>) -> Self {
Self::new(constraints)
}
pub fn vertical(constraints: Vec<Constraint>) -> Self {
Self {
constraints,
direction: Direction::Vertical,
spacing: 0,
margin: 0,
name: None,
cached_layout: RefCell::new(None),
}
}
pub fn nested(&self, _rect: Rect) -> Layout {
Layout {
constraints: self.constraints.clone(),
direction: self.direction,
spacing: 0, margin: 0, name: self.name,
cached_layout: RefCell::new(None),
}
}
pub fn invalidate_cache(&mut self) {
*self.cached_layout.borrow_mut() = None;
}
pub fn direction(mut self, direction: Direction) -> Self {
self.direction = direction;
self
}
pub fn spacing(mut self, spacing: u16) -> Self {
self.spacing = spacing;
self
}
pub fn margin(mut self, margin: u16) -> Self {
self.margin = margin;
self
}
pub fn name(mut self, name: &'static str) -> Self {
self.name = Some(name);
self
}
pub fn layout(&self, area: Rect) -> Vec<Rect> {
{
let cached = self.cached_layout.borrow();
if let Some((cached_area, cached_result)) = cached.as_ref() {
if cached_area == &area {
return cached_result.clone();
}
}
}
if self.constraints.is_empty() {
return Vec::new();
}
let is_vertical = self.direction == Direction::Vertical;
let main_axis = if is_vertical { area.height } else { area.width };
let cross_axis = if is_vertical { area.width } else { area.height };
let main_axis = main_axis.saturating_sub(2 * self.margin);
let cross_axis = cross_axis.saturating_sub(2 * self.margin);
let main_start = if is_vertical { area.y + self.margin } else { area.x + self.margin };
let cross_start = if is_vertical { area.x + self.margin } else { area.y + self.margin };
let total_spacing = self.spacing * (self.constraints.len() as u16 - 1).saturating_sub(0);
let available = main_axis.saturating_sub(total_spacing);
let mut results = Vec::with_capacity(self.constraints.len());
let mut fixed_total: u32 = 0;
let mut percentages: Vec<(usize, u16)> = Vec::new();
let mut ratios: Vec<(usize, u16, u16)> = Vec::new();
for (i, c) in self.constraints.iter().enumerate() {
match c {
Constraint::Fixed(f) => fixed_total += *f as u32,
Constraint::Min(m) => fixed_total += *m as u32,
Constraint::Max(_) => {}
Constraint::Percentage(p) => percentages.push((i, *p)),
Constraint::Ratio(n, d) => ratios.push((i, *n, *d)),
}
}
let remaining = available.saturating_sub(fixed_total as u16);
let mut sizes = vec![0u16; self.constraints.len()];
for (i, c) in self.constraints.iter().enumerate() {
match c {
Constraint::Fixed(f) => sizes[i] = *f,
Constraint::Min(m) => sizes[i] = (*m).min(remaining),
Constraint::Max(max) => {
let computed = if let Some(idx) = percentages.iter().position(|(j, _)| *j == i)
{
let p = percentages[idx].1;
(remaining as u32 * p as u32 / 100) as u16
} else if let Some(idx) = ratios.iter().position(|(j, _, _)| *j == i) {
let (n, d) = (ratios[idx].1, ratios[idx].2);
if d > 0 {
(remaining as u32 * n as u32 / d as u32) as u16
} else {
remaining
}
} else {
remaining
};
sizes[i] = computed.min(*max);
}
Constraint::Percentage(_) => {}
Constraint::Ratio(_, _) => {}
}
}
let percentage_total: u16 = percentages.iter().map(|(_, p)| p).sum();
let pct_len = percentages.len();
for (i, p) in percentages.iter() {
let size = if percentage_total > 0 {
(remaining as u32 * *p as u32 / percentage_total as u32) as u16
} else {
remaining.saturating_div(pct_len as u16)
};
sizes[*i] = sizes[*i].max(size);
}
let ratio_total: u32 = ratios.iter().map(|(_, n, _)| *n as u32).sum();
for (i, n, d) in ratios.iter() {
if *d > 0 && ratio_total > 0 {
let size = (remaining as u32 * *n as u32 / ratio_total) as u16;
sizes[*i] = sizes[*i].max(size);
}
}
let mut pos = main_start;
for (i, size) in sizes.iter().enumerate() {
let rect = if is_vertical {
Rect::new(cross_start, pos, cross_axis, *size)
} else {
Rect::new(pos, cross_start, *size, cross_axis)
};
results.push(rect);
if i < sizes.len() - 1 {
pos += *size + self.spacing;
}
}
*self.cached_layout.borrow_mut() = Some((area, results.clone()));
results
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_percentage_layout() {
let layout =
Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)]);
let rects = layout.layout(Rect::new(0, 0, 100, 20));
assert_eq!(rects.len(), 2);
assert_eq!(rects[0].width, 50);
assert_eq!(rects[1].width, 50);
}
#[test]
fn test_vertical_constructor() {
let layout =
Layout::vertical(vec![Constraint::Percentage(50), Constraint::Percentage(50)]);
let rects = layout.layout(Rect::new(0, 0, 100, 40));
assert_eq!(rects.len(), 2);
assert_eq!(rects[0].height, 20);
assert_eq!(rects[1].height, 20);
}
#[test]
fn test_nested_layout() {
let parent = Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
.spacing(2);
let child_rect = parent.layout(Rect::new(0, 0, 100, 20))[0];
let nested = parent.nested(child_rect);
let nested_rects = nested.layout(Rect::new(0, 0, 100, 20));
assert_eq!(nested_rects.len(), 2);
assert_eq!(nested_rects[0].width, 50);
assert_eq!(nested_rects[1].width, 50);
}
#[test]
fn test_margin() {
let layout = Layout::horizontal(vec![Constraint::Percentage(100)]).margin(5);
let rects = layout.layout(Rect::new(0, 0, 100, 20));
assert_eq!(rects[0].x, 5);
assert_eq!(rects[0].y, 5);
assert_eq!(rects[0].width, 90);
assert_eq!(rects[0].height, 10);
}
#[test]
fn test_margin_vertical() {
let layout = Layout::vertical(vec![Constraint::Percentage(100)]).margin(3);
let rects = layout.layout(Rect::new(0, 0, 80, 50));
assert_eq!(rects[0].x, 3);
assert_eq!(rects[0].y, 3);
assert_eq!(rects[0].width, 74);
assert_eq!(rects[0].height, 44);
}
#[test]
fn test_spacing_with_margin() {
let layout = Layout::horizontal(vec![Constraint::Fixed(20), Constraint::Fixed(20)])
.spacing(5)
.margin(10);
let rects = layout.layout(Rect::new(0, 0, 100, 20));
assert_eq!(rects[0].x, 10);
assert_eq!(rects[1].x, 35);
assert_eq!(rects[0].width, 20);
assert_eq!(rects[1].width, 20);
}
#[test]
fn test_nested_with_spacing() {
let parent = Layout::horizontal(vec![Constraint::Fixed(50), Constraint::Fixed(50)])
.spacing(2);
let child_rect = parent.layout(Rect::new(0, 0, 102, 20))[0];
let nested = parent.nested(child_rect).spacing(1);
let nested_rects = nested.layout(Rect::new(0, 0, 50, 20));
assert_eq!(nested_rects.len(), 2);
assert_eq!(nested_rects[0].x, 0);
assert_eq!(nested_rects[1].x, 51); }
#[test]
fn test_layout_debug_name() {
let layout = Layout::horizontal(vec![Constraint::Percentage(100)]).name("main");
let debug_str = format!("{:?}", layout);
assert!(debug_str.contains("main"));
}
#[test]
fn test_fixed_and_percentage() {
let layout =
Layout::horizontal(vec![Constraint::Fixed(20), Constraint::Percentage(80)]);
let rects = layout.layout(Rect::new(0, 0, 100, 20));
assert_eq!(rects[0].width, 20);
assert_eq!(rects[1].width, 80);
}
#[test]
fn test_horizontal_alias() {
let layout1 = Layout::new(vec![Constraint::Percentage(100)]);
let layout2 = Layout::horizontal(vec![Constraint::Percentage(100)]);
let rects1 = layout1.layout(Rect::new(0, 0, 100, 20));
let rects2 = layout2.layout(Rect::new(0, 0, 100, 20));
assert_eq!(rects1, rects2);
}
#[test]
fn test_min_constraint() {
let layout =
Layout::horizontal(vec![Constraint::Min(30), Constraint::Percentage(50)]);
let rects = layout.layout(Rect::new(0, 0, 100, 20));
assert_eq!(rects[0].width, 30);
assert_eq!(rects[1].width, 70);
}
#[test]
fn test_ratio() {
let layout = Layout::horizontal(vec![Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
let rects = layout.layout(Rect::new(0, 0, 100, 20));
assert_eq!(rects[0].width, 33);
assert_eq!(rects[1].width, 66);
}
#[test]
fn test_spacing() {
let layout = Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
.spacing(5);
let rects = layout.layout(Rect::new(0, 0, 105, 20));
assert_eq!(rects[0].width, 50);
assert_eq!(rects[1].width, 50);
assert_eq!(rects[0].x, 0);
assert_eq!(rects[1].x, 55);
}
#[test]
fn test_max_constraint() {
let layout = Layout::horizontal(vec![Constraint::Fixed(50), Constraint::Max(20)]);
let rects = layout.layout(Rect::new(0, 0, 100, 20));
assert_eq!(rects[0].width, 50);
assert_eq!(rects[1].width, 20);
}
#[test]
fn test_vertical_layout() {
let layout = Layout::vertical(vec![Constraint::Percentage(50), Constraint::Percentage(50)]);
let rects = layout.layout(Rect::new(0, 0, 100, 40));
assert_eq!(rects.len(), 2);
assert_eq!(rects[0].height, 20);
assert_eq!(rects[1].height, 20);
assert_eq!(rects[0].width, 100);
assert_eq!(rects[0].x, 0);
assert_eq!(rects[0].y, 0);
assert_eq!(rects[1].y, 20);
}
#[test]
fn test_vertical_with_spacing() {
let layout = Layout::vertical(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
.spacing(2);
let rects = layout.layout(Rect::new(0, 0, 100, 42));
assert_eq!(rects.len(), 2);
assert_eq!(rects[0].height, 20);
assert_eq!(rects[1].height, 20);
assert_eq!(rects[0].width, 100);
assert_eq!(rects[1].width, 100);
}
#[test]
fn test_vertical_with_fixed_and_ratio() {
let layout = Layout::vertical(vec![Constraint::Fixed(5), Constraint::Ratio(1, 1)]);
let rects = layout.layout(Rect::new(0, 0, 80, 30));
assert_eq!(rects[0].height, 5);
assert_eq!(rects[1].height, 25);
assert_eq!(rects[0].width, 80);
}
#[test]
fn test_empty_layout() {
let layout = Layout::horizontal(vec![]);
let rects = layout.layout(Rect::new(0, 0, 100, 20));
assert!(rects.is_empty());
}
#[test]
fn test_vertical_layout_with_direction() {
let layout = Layout::new(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
.direction(Direction::Vertical);
let rects = layout.layout(Rect::new(0, 0, 100, 40));
assert_eq!(rects.len(), 2);
assert_eq!(rects[0].height, 20);
assert_eq!(rects[1].height, 20);
}
}