use ratatui::layout::Rect;
pub const CHAT_CONTENT_MAX_W: u16 = 120;
pub const MAX_INPUT_ROWS: u16 = 8;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Layout {
pub full: Rect,
pub top_frame: Rect,
pub chat: Rect,
pub left_panel: Rect,
pub right_panel: Rect,
pub chat_v_left_col: u16,
pub chat_v_right_col: u16,
pub chat_bot_frame: Rect,
pub avatar_box: Rect,
pub input_box: Rect,
pub right_margin: Rect,
pub status: Rect,
}
impl Layout {
#[allow(dead_code)]
pub fn new(cols: u16, rows: u16, input_rows: u16) -> Self {
Self::with_panels(cols, rows, input_rows, true, true)
}
pub fn with_panels(
cols: u16,
rows: u16,
input_rows: u16,
show_left: bool,
show_right: bool,
) -> Self {
let input_rows = input_rows.clamp(1, MAX_INPUT_ROWS);
let full = Rect::new(0, 0, cols, rows);
let fixed_v = 5_u16; let chat_h = rows.saturating_sub(input_rows).saturating_sub(fixed_v);
let top_frame = Rect::new(0, 0, cols, if rows >= 1 { 1 } else { 0 });
let chat_content_top = 1_u16.min(rows);
let chat_bot_frame_y = chat_content_top.saturating_add(chat_h);
let bottom_strip_top_y = chat_bot_frame_y.saturating_add(1);
let input_top_y = bottom_strip_top_y.saturating_add(1);
let bottom_strip_bot_y = input_top_y.saturating_add(input_rows);
let status_y = rows.saturating_sub(1);
let line_w = cols.saturating_sub(2);
let base_chat_w = line_w.min(CHAT_CONTENT_MAX_W);
let base_gutter = line_w.saturating_sub(base_chat_w) / 2;
let left_gutter = if show_left { base_gutter } else { 0 };
let freed =
if show_left { 0 } else { base_gutter } + if show_right { 0 } else { base_gutter };
let chat_content_w = base_chat_w.saturating_add(freed);
let chat_v_left_col = left_gutter; let chat_x = chat_v_left_col.saturating_add(1);
let chat_v_right_col = chat_x.saturating_add(chat_content_w);
let right_panel_x = chat_v_right_col.saturating_add(1);
let right_panel_w = cols.saturating_sub(right_panel_x);
let chat = Rect::new(chat_x, chat_content_top, chat_content_w, chat_h);
let left_panel = Rect::new(0, chat_content_top, left_gutter, chat_h);
let right_panel = Rect::new(right_panel_x, chat_content_top, right_panel_w, chat_h);
let chat_bot_frame = Rect::new(0, chat_bot_frame_y, cols, 1);
let strip_h = input_rows.saturating_add(2);
let avatar_box = Rect::new(0, bottom_strip_top_y, left_gutter, strip_h);
let input_box_x = chat_v_left_col;
let input_box_w = chat_v_right_col
.saturating_sub(chat_v_left_col)
.saturating_add(1);
let input_box = Rect::new(input_box_x, bottom_strip_top_y, input_box_w, strip_h);
let right_margin_w = cols.saturating_sub(right_panel_x);
let right_margin = Rect::new(right_panel_x, bottom_strip_top_y, right_margin_w, strip_h);
let status = Rect::new(0, status_y, cols, if rows >= 1 { 1 } else { 0 });
let _ = bottom_strip_bot_y;
Self {
full,
top_frame,
chat,
left_panel,
right_panel,
chat_v_left_col,
chat_v_right_col,
chat_bot_frame,
avatar_box,
input_box,
right_margin,
status,
}
}
#[allow(dead_code)]
pub fn chat_height(&self) -> u16 {
self.chat.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wide_terminal_centers_chat() {
let l = Layout::new(200, 30, 1);
assert_eq!(l.chat_v_left_col, 39);
assert_eq!(l.chat.x, 40);
assert_eq!(l.chat.width, CHAT_CONTENT_MAX_W);
assert_eq!(l.chat_v_right_col, 160);
assert_eq!(l.right_panel.x, 161);
assert_eq!(l.right_panel.width, 39);
assert_eq!(l.left_panel.width, 39);
}
#[test]
fn hiding_right_panel_expands_chat() {
let both = Layout::new(200, 30, 1);
let l = Layout::with_panels(200, 30, 1, true, false);
assert_eq!(l.left_panel.width, both.left_panel.width);
assert_eq!(l.right_panel.width, 0);
assert_eq!(l.chat.width, both.chat.width + both.right_panel.width);
assert_eq!(l.chat.x, both.chat.x); assert_eq!(
l.left_panel.width + 1 + l.chat.width + 1 + l.right_panel.width,
200
);
}
#[test]
fn hiding_left_panel_expands_chat() {
let both = Layout::new(200, 30, 1);
let l = Layout::with_panels(200, 30, 1, false, true);
assert_eq!(l.left_panel.width, 0);
assert_eq!(l.right_panel.width, both.right_panel.width);
assert_eq!(l.chat.width, both.chat.width + both.left_panel.width);
assert_eq!(l.chat_v_left_col, 0);
assert_eq!(l.chat.x, 1);
assert_eq!(
l.left_panel.width + 1 + l.chat.width + 1 + l.right_panel.width,
200
);
}
#[test]
fn hiding_both_panels_fills_chat() {
let l = Layout::with_panels(200, 30, 1, false, false);
assert_eq!(l.left_panel.width, 0);
assert_eq!(l.right_panel.width, 0);
assert_eq!(l.chat.width, 198); assert_eq!(l.chat.x, 1);
}
#[test]
fn avatar_box_tracks_left_gutter() {
let both = Layout::new(200, 30, 1);
assert_eq!(both.avatar_box.width, both.left_panel.width);
let left_hidden = Layout::with_panels(200, 30, 1, false, true);
assert_eq!(left_hidden.avatar_box.width, 0);
assert_eq!(left_hidden.input_box.x, 0);
}
#[test]
fn new_matches_with_panels_both_visible() {
assert_eq!(
Layout::new(200, 30, 1),
Layout::with_panels(200, 30, 1, true, true)
);
assert_eq!(
Layout::new(80, 24, 3),
Layout::with_panels(80, 24, 3, true, true)
);
}
#[test]
fn narrow_terminal_drops_side_panels() {
let l = Layout::new(80, 24, 1);
assert_eq!(l.left_panel.width, 0);
assert_eq!(l.right_panel.width, 0);
assert_eq!(l.chat.width, 78); assert_eq!(l.chat_v_left_col, 0);
assert_eq!(l.chat_v_right_col, 79);
}
#[test]
fn vertical_tiling_covers_viewport() {
let l = Layout::new(200, 30, 1);
let rows = 30_u16;
assert_eq!(
1 + l.chat.height + 1 + l.input_box.height + 1,
rows,
"vertical tiling: {:?}",
l
);
assert_eq!(l.top_frame.y + l.top_frame.height, l.chat.y);
assert_eq!(l.chat.y + l.chat.height, l.chat_bot_frame.y);
assert_eq!(l.chat_bot_frame.y + l.chat_bot_frame.height, l.input_box.y);
assert_eq!(l.input_box.y + l.input_box.height, l.status.y);
}
#[test]
fn horizontal_tiling_on_chat_row() {
let l = Layout::new(200, 30, 1);
let cols = 200_u16;
let sum = l.left_panel.width + 1 + l.chat.width + 1 + l.right_panel.width;
assert_eq!(sum, cols, "horizontal tiling: {:?}", l);
assert_eq!(l.chat_v_left_col, l.left_panel.width);
assert_eq!(l.chat_v_right_col, cols - l.right_panel.width - 1);
}
#[test]
fn input_box_aligns_with_chat_verticals() {
let l = Layout::new(200, 30, 1);
assert_eq!(l.input_box.x, l.chat_v_left_col);
assert_eq!(l.input_box.x + l.input_box.width - 1, l.chat_v_right_col);
}
#[test]
fn bottom_strip_horizontal_tiles() {
let l = Layout::new(200, 30, 1);
assert_eq!(l.avatar_box.x + l.avatar_box.width, l.input_box.x);
assert_eq!(l.input_box.x + l.input_box.width, l.right_margin.x);
assert_eq!(l.right_margin.x + l.right_margin.width, 200);
}
#[test]
fn growing_input_shrinks_chat() {
let one = Layout::new(200, 30, 1);
let eight = Layout::new(200, 30, 8);
assert_eq!(one.chat.height - 7, eight.chat.height);
assert!(eight.input_box.y < one.input_box.y);
assert_eq!(one.status.y, eight.status.y);
}
#[test]
fn input_rows_clamps_to_max() {
let big = Layout::new(200, 40, 99);
let max = Layout::new(200, 40, MAX_INPUT_ROWS);
assert_eq!(big.input_box.height, max.input_box.height);
assert_eq!(big.chat.height, max.chat.height);
}
#[test]
fn degenerate_small_viewport_is_safe() {
let l = Layout::new(20, 6, 1);
assert!(l.chat.x + l.chat.width <= 20);
assert!(l.status.y < 6);
}
}