use ratatui::layout::Rect;
use crate::tui::app::App;
use crate::tui::work_surface::model::{
self, RailPanel, WorkSurfacePlacement, visible_rows_for_panel,
};
use crate::tui::work_surface::panels;
use super::{progress_shares_goal_row, top_goal_title, top_todo_progress};
const SIDE_RAIL_MIN_HOST_WIDTH: u16 = 72;
const SIDE_RAIL_MIN_CHAT_WIDTH: u16 = 40;
fn effective_placement(configured: WorkSurfacePlacement, host_width: u16) -> WorkSurfacePlacement {
if configured == WorkSurfacePlacement::Off {
return WorkSurfacePlacement::Off;
}
if host_width < SIDE_RAIL_MIN_HOST_WIDTH {
WorkSurfacePlacement::Top
} else {
configured
}
}
pub fn height(app: &mut App, width: u16, terminal_height: u16, rail_budget: u16) -> u16 {
app.work_surface.effective_placement = effective_placement(app.work_surface.placement, width);
if app.work_surface.effective_placement == WorkSurfacePlacement::Off {
collapse_strip(app);
return 0;
}
if app.work_surface.panel == RailPanel::Context {
if app.work_surface.effective_placement != WorkSurfacePlacement::Top {
return 0;
}
if !panels::panel_has_useful_content(app, app.work_surface.panel) {
collapse_strip(app);
return 0;
}
let cap = top_cap(app, terminal_height, rail_budget);
if cap < model::TOP_HEIGHT_MIN {
collapse_strip(app);
return 0;
}
let goal_rows = u16::from(top_goal_title(app).is_some());
let content_width = usize::from(width.saturating_sub(2).max(1));
let content_rows = panels::panel_content_row_count(
app,
app.work_surface.panel,
content_width,
goal_rows > 0,
);
if content_rows == 0 && goal_rows == 0 {
collapse_strip(app);
return 0;
}
let desired = u16::try_from(content_rows)
.unwrap_or(u16::MAX)
.saturating_add(goal_rows)
.saturating_add(1); return desired.clamp(model::TOP_HEIGHT_MIN, cap);
}
let rows = visible_rows_for_panel(app);
let goal_rows = u16::from(
app.work_surface.effective_placement == WorkSurfacePlacement::Top
&& top_goal_title(app).is_some(),
);
if rows.is_empty() {
if goal_rows == 0 {
collapse_strip(app);
app.work_surface.latest_rows.clear();
app.work_surface.visible_rows = 0;
app.work_surface.total_rows = 0;
app.work_surface.scroll_offset = 0;
return 0;
}
if app.work_surface.effective_placement != WorkSurfacePlacement::Top {
return 0;
}
let cap = top_cap(app, terminal_height, rail_budget);
if cap < model::TOP_HEIGHT_MIN {
collapse_strip(app);
return 0;
}
return (goal_rows.saturating_add(1)).clamp(model::TOP_HEIGHT_MIN, cap);
}
if app.work_surface.effective_placement != WorkSurfacePlacement::Top {
return 0;
}
let cap = top_cap(app, terminal_height, rail_budget);
if cap < model::TOP_HEIGHT_MIN {
collapse_strip(app);
return 0;
}
let list_rows = rows
.iter()
.filter(|row| row.selectable || row.id.0.starts_with("section:"))
.count();
let progress = u16::from(
top_todo_progress(app, &rows).is_some() && !progress_shares_goal_row(width, goal_rows > 0),
);
let desired = u16::try_from(list_rows)
.unwrap_or(u16::MAX)
.saturating_add(progress)
.saturating_add(goal_rows)
.saturating_add(1);
desired.clamp(model::TOP_HEIGHT_MIN, cap)
}
fn ambient_cap(terminal_height: u16, rail_budget: u16) -> u16 {
terminal_height
.saturating_div(2)
.clamp(model::TOP_HEIGHT_MIN, model::TOP_HEIGHT_MAX)
.min(rail_budget)
}
fn top_cap(app: &App, terminal_height: u16, rail_budget: u16) -> u16 {
app.work_surface
.top_height
.min(ambient_cap(terminal_height, rail_budget))
}
fn collapse_strip(app: &mut App) {
app.work_surface.last_area = None;
app.work_surface.hitboxes.clear();
app.work_surface.focused = false;
app.work_surface.selected = None;
app.work_surface.opened = None;
app.work_surface.hovered = None;
app.work_surface.resizing = false;
app.work_surface.divider_hovered = false;
}
pub fn split_chat(app: &mut App, area: Rect, min_chat_width: u16) -> (Rect, Option<Rect>) {
let placement = effective_placement(app.work_surface.placement, area.width);
app.work_surface.effective_placement = placement;
if placement == WorkSurfacePlacement::Top || placement == WorkSurfacePlacement::Off {
return (area, None);
}
if !side_rail_has_content(app) {
return (area, None);
}
let min_chat_width = min_chat_width.max(SIDE_RAIL_MIN_CHAT_WIDTH);
let rail_width = app
.work_surface
.side_width
.clamp(model::SIDE_WIDTH_MIN, model::SIDE_WIDTH_MAX)
.min(area.width.saturating_sub(min_chat_width));
if rail_width < model::SIDE_WIDTH_MIN {
app.work_surface.effective_placement = WorkSurfacePlacement::Top;
return (area, None);
}
let chat_width = area.width.saturating_sub(rail_width);
match placement {
WorkSurfacePlacement::Left => (
Rect {
x: area.x.saturating_add(rail_width),
width: chat_width,
..area
},
Some(Rect {
width: rail_width,
..area
}),
),
WorkSurfacePlacement::Right => (
Rect {
width: chat_width,
..area
},
Some(Rect {
x: area.x.saturating_add(chat_width),
width: rail_width,
..area
}),
),
WorkSurfacePlacement::Top | WorkSurfacePlacement::Off => (area, None),
}
}
fn side_rail_has_content(app: &mut App) -> bool {
match app.work_surface.panel {
RailPanel::Context => panels::panel_has_useful_content(app, RailPanel::Context),
_ => !visible_rows_for_panel(app).is_empty(),
}
}