use ratatui::layout::Rect;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Grid {
pub columns: usize,
pub rows: usize,
}
pub fn choose_grid(task_count: usize, terminal: Rect) -> Grid {
if task_count <= 1 {
return Grid {
columns: 1,
rows: 1,
};
}
let terminal_aspect = f64::from(terminal.width.max(1)) / f64::from(terminal.height.max(1));
let target_grid_aspect = (terminal_aspect / 2.0).max(0.25);
(1..=task_count)
.map(|columns| {
let rows = task_count.div_ceil(columns);
let empty = columns * rows - task_count;
let aspect = columns as f64 / rows as f64;
let shape_cost = (aspect / target_grid_aspect).ln().abs();
let empty_cost = empty as f64 / task_count as f64;
(shape_cost + empty_cost, empty, Grid { columns, rows })
})
.min_by(|left, right| {
left.0
.total_cmp(&right.0)
.then_with(|| left.1.cmp(&right.1))
})
.map(|candidate| candidate.2)
.unwrap()
}
pub fn pane_rects(area: Rect, grid: Grid, task_count: usize) -> Vec<Rect> {
grid_rects(area, grid)
.into_iter()
.take(task_count)
.collect()
}
pub fn grid_rects(area: Rect, grid: Grid) -> Vec<Rect> {
let widths = segments(area.width, grid.columns);
let heights = segments(area.height, grid.rows);
let mut rects = Vec::with_capacity(grid.columns.saturating_mul(grid.rows));
let mut y = area.y;
for height in heights {
let mut x = area.x;
for &width in &widths {
rects.push(Rect::new(x, y, width, height));
x = x.saturating_add(width);
}
y = y.saturating_add(height);
}
rects
}
fn segments(total: u16, count: usize) -> Vec<u16> {
let count = count.max(1) as u16;
let base = total / count;
let remainder = total % count;
(0..count)
.map(|index| base + u16::from(index < remainder))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layouts_common_task_counts_sensibly() {
let terminal = Rect::new(0, 0, 120, 40);
assert_eq!(
choose_grid(1, terminal),
Grid {
columns: 1,
rows: 1
}
);
assert_eq!(
choose_grid(2, terminal),
Grid {
columns: 2,
rows: 1
}
);
assert_eq!(
choose_grid(4, terminal),
Grid {
columns: 2,
rows: 2
}
);
assert_eq!(
choose_grid(5, terminal),
Grid {
columns: 3,
rows: 2
}
);
}
#[test]
fn pane_rects_fill_the_grid_without_overlap() {
let area = Rect::new(0, 0, 11, 7);
let rects = grid_rects(
area,
Grid {
columns: 2,
rows: 2,
},
);
assert_eq!(rects[0], Rect::new(0, 0, 6, 4));
assert_eq!(rects[1], Rect::new(6, 0, 5, 4));
assert_eq!(rects[2], Rect::new(0, 4, 6, 3));
assert_eq!(rects[3], Rect::new(6, 4, 5, 3));
}
#[test]
fn pane_rects_return_only_task_slots() {
let area = Rect::new(0, 0, 11, 7);
let rects = pane_rects(
area,
Grid {
columns: 2,
rows: 2,
},
3,
);
assert_eq!(rects.len(), 3);
assert_eq!(rects[2], Rect::new(0, 4, 6, 3));
}
}