use ratatui::layout::{Constraint, Layout, Rect};
pub struct AppLayout {
pub header_top_sep: Rect,
pub header: Rect,
pub header_bot_sep: Rect,
pub body: Rect,
pub input_sep: Rect,
pub todo: Rect,
pub input: Rect,
pub input_bottom_sep: Rect,
pub help: Rect,
pub footer: Option<Rect>,
}
pub fn compute(
area: Rect,
input_lines: u16,
show_header: bool,
todo_height: u16,
help_height: u16,
) -> AppLayout {
let input_height = input_lines.max(1);
let header_height: u16 = u16::from(show_header);
let header_bot_sep_height: u16 = u16::from(show_header);
let zero = Rect::new(area.x, area.y, area.width, 0);
if area.height < 8 {
let [body, input, input_bottom_sep, help] = Layout::vertical([
Constraint::Min(1),
Constraint::Length(input_height),
Constraint::Length(1),
Constraint::Length(help_height),
])
.areas(area);
AppLayout {
header_top_sep: zero,
header: zero,
header_bot_sep: zero,
body,
todo: zero,
input_sep: Rect::new(area.x, input.y, area.width, 0),
input,
input_bottom_sep,
help,
footer: None,
}
} else {
let [
header_top_sep,
header,
header_bot_sep,
body,
input_sep,
todo,
input,
input_bottom_sep,
help,
footer,
] = Layout::vertical([
Constraint::Length(header_bot_sep_height),
Constraint::Length(header_height),
Constraint::Length(header_bot_sep_height),
Constraint::Min(3),
Constraint::Length(1),
Constraint::Length(todo_height),
Constraint::Length(input_height),
Constraint::Length(1),
Constraint::Length(help_height),
Constraint::Length(1),
])
.areas(area);
AppLayout {
header_top_sep,
header,
header_bot_sep,
body,
input_sep,
todo,
input,
input_bottom_sep,
help,
footer: Some(footer),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
fn area(w: u16, h: u16) -> Rect {
Rect::new(0, 0, w, h)
}
fn total_height(layout: &AppLayout) -> u16 {
layout.header_top_sep.height
+ layout.header.height
+ layout.header_bot_sep.height
+ layout.body.height
+ layout.todo.height
+ layout.input_sep.height
+ layout.input.height
+ layout.input_bottom_sep.height
+ layout.help.height
+ layout.footer.map_or(0, |f| f.height)
}
fn visible_areas(layout: &AppLayout) -> Vec<Rect> {
let mut areas = vec![
layout.header_top_sep,
layout.header,
layout.header_bot_sep,
layout.body,
layout.input_sep,
layout.todo,
layout.input,
layout.input_bottom_sep,
layout.help,
];
if let Some(f) = layout.footer {
areas.push(f);
}
areas.into_iter().filter(|r| r.height > 0).collect()
}
fn assert_no_overlap_and_ordered(layout: &AppLayout) {
let areas = visible_areas(layout);
for i in 1..areas.len() {
let prev = areas[i - 1];
let curr = areas[i];
assert!(
prev.y + prev.height <= curr.y,
"Area {i}-1 ({prev:?}) overlaps or is not before area {i} ({curr:?})"
);
}
}
#[test]
fn normal_terminal_with_header() {
let layout = compute(area(80, 24), 1, true, 0, 0);
assert!(layout.footer.is_some());
assert_eq!(layout.header.height, 1);
assert_eq!(layout.header_bot_sep.height, 1);
assert!(layout.body.height >= 3);
assert_eq!(layout.input_sep.height, 1);
assert_eq!(layout.input.height, 1);
assert_eq!(layout.input_bottom_sep.height, 1);
assert_eq!(layout.footer.unwrap().height, 1);
}
#[test]
fn normal_all_areas_sum_to_total() {
let layout = compute(area(80, 24), 1, true, 3, 2);
assert_eq!(total_height(&layout), 24);
}
#[test]
fn normal_no_header() {
let layout = compute(area(80, 24), 1, false, 0, 0);
assert_eq!(layout.header.height, 0);
assert_eq!(layout.header_bot_sep.height, 0);
assert!(layout.footer.is_some());
}
#[test]
fn ultra_compact_no_header_no_footer() {
let layout = compute(area(80, 6), 1, true, 0, 0);
assert_eq!(layout.header.height, 0);
assert!(layout.footer.is_none());
assert_eq!(layout.todo.height, 0);
}
#[test]
fn ultra_compact_areas_sum_to_total() {
let layout = compute(area(80, 6), 1, true, 0, 0);
assert_eq!(total_height(&layout), 6);
}
#[test]
fn todo_panel_gets_requested_height() {
let layout = compute(area(80, 24), 1, true, 5, 0);
assert_eq!(layout.todo.height, 5);
}
#[test]
fn zero_todo_height_produces_zero_area() {
let layout = compute(area(80, 24), 1, true, 0, 0);
assert_eq!(layout.todo.height, 0);
}
#[test]
fn help_gets_requested_height() {
let layout = compute(area(80, 24), 1, true, 0, 4);
assert_eq!(layout.help.height, 4);
}
#[test]
fn multi_line_input() {
let layout = compute(area(80, 24), 5, true, 0, 0);
assert_eq!(layout.input.height, 5);
}
#[test]
fn input_lines_zero_clamped_to_one() {
let layout = compute(area(80, 24), 0, true, 0, 0);
assert_eq!(layout.input.height, 1);
}
#[test]
fn ultra_compact_threshold_exactly_8() {
let layout = compute(area(80, 8), 1, true, 0, 0);
assert!(layout.footer.is_some());
}
#[test]
fn ultra_compact_threshold_7() {
let layout = compute(area(80, 7), 1, true, 0, 0);
assert!(layout.footer.is_none());
}
#[test]
fn large_terminal() {
let layout = compute(area(200, 100), 3, true, 5, 2);
assert_eq!(total_height(&layout), 100);
assert!(layout.body.height >= 3);
}
#[test]
fn width_carries_through() {
let layout = compute(area(120, 24), 1, true, 0, 0);
assert_eq!(layout.header.width, 120);
assert_eq!(layout.body.width, 120);
assert_eq!(layout.input.width, 120);
}
#[test]
fn no_overlap_between_areas() {
let layout = compute(area(80, 24), 2, true, 3, 1);
assert_no_overlap_and_ordered(&layout);
}
#[test]
fn everything_maxed_out() {
let layout = compute(area(80, 24), 3, true, 5, 3);
assert!(layout.body.height >= 3);
assert_eq!(total_height(&layout), 24);
}
#[test]
fn offset_area_respects_origin() {
let r = Rect::new(10, 5, 80, 24);
let layout = compute(r, 1, true, 0, 0);
assert_eq!(layout.header.x, 10);
assert_eq!(layout.body.x, 10);
assert_eq!(layout.input.x, 10);
assert_eq!(layout.body.width, 80);
assert_eq!(layout.header_top_sep.y, 5);
assert_eq!(layout.header.y, 6);
assert_eq!(total_height(&layout), 24);
}
#[test]
fn offset_area_compact() {
let r = Rect::new(5, 10, 60, 6);
let layout = compute(r, 1, true, 0, 0);
assert!(layout.footer.is_none());
assert_eq!(layout.body.x, 5);
assert_eq!(total_height(&layout), 6);
}
#[test]
fn zero_height_area() {
let layout = compute(area(80, 0), 1, true, 0, 0);
assert!(layout.footer.is_none());
}
#[test]
fn height_one() {
let layout = compute(area(80, 1), 1, true, 0, 0);
assert!(layout.footer.is_none());
assert_eq!(total_height(&layout), 1);
}
#[test]
fn height_two() {
let layout = compute(area(80, 2), 1, true, 0, 0);
assert_eq!(total_height(&layout), 2);
}
#[test]
fn width_one() {
let layout = compute(Rect::new(0, 0, 1, 24), 1, true, 0, 0);
assert_eq!(layout.body.width, 1);
assert_eq!(layout.input.width, 1);
assert_eq!(total_height(&layout), 24);
}
#[test]
fn width_zero() {
let layout = compute(area(0, 24), 1, true, 0, 0);
assert_eq!(layout.body.width, 0);
assert_eq!(total_height(&layout), 24);
}
#[test]
fn input_larger_than_terminal() {
let layout = compute(area(80, 10), 50, true, 0, 0);
assert_eq!(total_height(&layout), 10);
}
#[test]
fn competing_constraints_squeeze_body() {
let layout = compute(area(80, 12), 3, true, 4, 3);
assert_eq!(total_height(&layout), 12);
}
#[test]
fn compact_with_help() {
let layout = compute(area(80, 6), 1, true, 0, 2);
assert!(layout.footer.is_none());
assert_eq!(layout.help.height, 2);
assert_eq!(total_height(&layout), 6);
}
#[test]
fn compact_with_multiline_input() {
let layout = compute(area(80, 7), 3, true, 0, 0);
assert!(layout.footer.is_none());
assert_eq!(layout.input.height, 3);
assert_eq!(total_height(&layout), 7);
}
#[test]
fn normal_mode_y_ordering() {
let layout = compute(area(80, 30), 2, true, 3, 1);
assert_no_overlap_and_ordered(&layout);
}
#[test]
fn compact_mode_y_ordering() {
let layout = compute(area(80, 6), 1, true, 0, 1);
assert_no_overlap_and_ordered(&layout);
}
#[test]
fn footer_at_bottom() {
let layout = compute(area(80, 24), 1, true, 0, 0);
let footer = layout.footer.unwrap();
assert_eq!(footer.y + footer.height, 24);
}
#[test]
fn body_follows_header_bot_sep() {
let layout = compute(area(80, 24), 1, true, 0, 0);
assert_eq!(
layout.body.y,
layout.header.y + layout.header.height + layout.header_bot_sep.height
);
}
#[test]
fn parametric_sizes_invariants() {
for h in [1, 2, 3, 5, 7, 8, 10, 15, 24, 50, 100] {
for w in [1, 10, 80, 200] {
let layout = compute(Rect::new(0, 0, w, h), 1, true, 0, 0);
assert_eq!(total_height(&layout), h, "Height mismatch for {w}x{h}");
for a in visible_areas(&layout) {
assert_eq!(a.width, w, "Width mismatch in area {a:?} for {w}x{h}");
}
}
}
}
#[test]
fn parametric_features_invariants() {
for input in [0, 1, 3, 10] {
for todo in [0, 2, 5] {
for help in [0, 1, 3] {
let layout = compute(area(80, 30), input, true, todo, help);
assert_eq!(
total_height(&layout),
30,
"Height mismatch for input={input} todo={todo} help={help}"
);
assert_no_overlap_and_ordered(&layout);
}
}
}
}
}