use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::Style;
use ratatui::widgets::Block;
use ratatui::Frame;
use crate::app::{App, AppScreen};
use crate::features;
use crate::widgets;
pub fn render(app: &mut App, frame: &mut Frame) {
let bg = app.theme().bg;
frame.render_widget(
Block::default().style(Style::default().bg(bg)),
frame.area(),
);
match app.screen {
AppScreen::Welcome => {
features::repo::view::render(&*app, frame, frame.area());
}
AppScreen::DirBrowser => {
features::repo::view::render_browser(app, frame, frame.area());
}
AppScreen::Main => {
render_main(app, frame);
}
}
}
fn render_main(app: &mut App, frame: &mut Frame) {
let initial_load = app.tab().is_loading && app.tab().commits.is_empty();
if initial_load {
features::skeleton::render(app, frame);
return;
}
let ideal_sidebar = ideal_sidebar_width(app);
let areas = MainAreas::compute(frame.area(), ideal_sidebar);
let outer = areas.outer;
widgets::header::render(app, frame, outer[0]);
let main_cols = areas.cols;
if areas.show_sidebar {
let sidebar = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(6), Constraint::Length(5), Constraint::Length(5), ])
.split(main_cols[0]);
features::branches::view::render(app, frame, sidebar[0]);
features::stash::view::render(app, frame, sidebar[1]);
features::remotes::view::render(app, frame, sidebar[2]);
}
features::commits::view::render(app, frame, main_cols[1]);
let overlay_rect = Rect {
x: main_cols[2].x,
y: main_cols[2].y,
width: main_cols[2].width,
height: main_cols[2].height + outer[2].height,
};
if app.show_about_panel {
features::about::view::render(app, frame, overlay_rect);
} else if app.show_theme_panel {
features::theme::view::render(app, frame, overlay_rect);
} else if app.show_options_panel {
features::options::view::render(app, frame, overlay_rect);
} else if app.show_editor_panel {
features::editor::view::render(app, frame, main_cols[2]);
} else if app.tab().file_history_path.is_some() {
features::diff::view::render_file_history(app, frame, main_cols[2]);
} else if app.tab().blame_path.is_some() {
features::diff::view::render_blame(app, frame, main_cols[2]);
} else {
features::diff::view::render(app, frame, main_cols[2]);
}
if !app.show_theme_panel && !app.show_options_panel && !app.show_about_panel {
features::staging::view::render(app, frame, outer[2]);
} else {
let staging_partial = Rect {
x: outer[2].x,
y: outer[2].y,
width: main_cols[2].x.saturating_sub(outer[2].x),
height: outer[2].height,
};
features::staging::view::render(app, frame, staging_partial);
}
widgets::status_bar::render(app, frame, outer[3]);
}
pub const NARROW_COLS: u16 = 80;
pub const SHORT_ROWS: u16 = 24;
fn ideal_sidebar_width(app: &App) -> u16 {
let longest = app
.tab()
.branches
.iter()
.map(|b| b.name.chars().count())
.max()
.unwrap_or(10);
(longest + 6) as u16
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MainAreas {
pub outer: [Rect; 4],
pub cols: [Rect; 3],
pub show_sidebar: bool,
}
impl MainAreas {
pub fn compute(area: Rect, ideal_sidebar: u16) -> Self {
let short = area.height < SHORT_ROWS;
let (content_pct, staging_pct) = if short { (70, 30) } else { (60, 40) };
let o = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Percentage(content_pct),
Constraint::Percentage(staging_pct),
Constraint::Length(1),
])
.split(area);
let show_sidebar = area.width >= NARROW_COLS;
let max_sidebar = (o[1].width * 30 / 100).clamp(22, 50);
let sidebar_w = if show_sidebar {
ideal_sidebar.min(max_sidebar).max(22)
} else {
0
};
let commit_pct = if show_sidebar { 40 } else { 50 };
let c = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(sidebar_w),
Constraint::Percentage(commit_pct),
Constraint::Min(20),
])
.split(o[1]);
Self {
outer: [o[0], o[1], o[2], o[3]],
cols: [c[0], c[1], c[2]],
show_sidebar,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn area(w: u16, h: u16) -> Rect {
Rect::new(0, 0, w, h)
}
#[test]
fn wide_terminal_shows_sidebar_within_bounds() {
let a = MainAreas::compute(area(200, 50), 30);
assert!(a.show_sidebar);
assert_eq!(a.cols[0].width, 30);
}
#[test]
fn sidebar_width_clamped_to_50() {
let a = MainAreas::compute(area(300, 50), 120);
assert_eq!(a.cols[0].width, 50);
}
#[test]
fn sidebar_min_22() {
let a = MainAreas::compute(area(120, 50), 5);
assert_eq!(a.cols[0].width, 22);
}
#[test]
fn narrow_terminal_hides_sidebar() {
let a = MainAreas::compute(area(NARROW_COLS - 1, 40), 30);
assert!(!a.show_sidebar);
assert_eq!(a.cols[0].width, 0);
assert!(MainAreas::compute(area(NARROW_COLS, 40), 30).show_sidebar);
}
#[test]
fn columns_cover_content_width() {
for w in [40u16, 79, 80, 120, 250] {
let a = MainAreas::compute(area(w, 40), 30);
let total: u16 = a.cols.iter().map(|r| r.width).sum();
assert_eq!(total, w, "width {w}");
assert!(a.cols[2].width > 0, "diff visible at {w}");
}
}
#[test]
fn short_terminal_shrinks_staging() {
let tall = MainAreas::compute(area(120, 40), 30);
let short = MainAreas::compute(area(120, SHORT_ROWS - 1), 30);
assert!(
short.outer[2].height * tall.outer[1].height
< tall.outer[2].height * short.outer[1].height * 2
);
assert!(short.outer[1].height > short.outer[2].height);
}
#[test]
fn tiny_terminal_does_not_panic() {
let a = MainAreas::compute(area(1, 1), 30);
assert_eq!(a.outer.len(), 4);
}
}