Skip to main content

gitkraft_tui/
layout.rs

1use ratatui::layout::{Constraint, Direction, Layout};
2use ratatui::Frame;
3
4use crate::app::{App, AppScreen};
5use crate::features;
6use crate::widgets;
7
8/// Main render entry point — called once per frame from the event loop.
9pub fn render(app: &mut App, frame: &mut Frame) {
10    match app.screen {
11        AppScreen::Welcome => {
12            features::repo::view::render(&*app, frame, frame.area());
13        }
14        AppScreen::DirBrowser => {
15            features::repo::view::render_browser(app, frame, frame.area());
16        }
17        AppScreen::Main => {
18            render_main(app, frame);
19        }
20    }
21}
22
23/// Render the full Main screen layout with header, content columns, staging
24/// area, and status bar.
25fn render_main(app: &mut App, frame: &mut Frame) {
26    // -- Outer vertical split --
27    //  [0] Header bar          — 3 rows
28    //  [1] Main content area   — flexible
29    //  [2] Staging area        — 12 rows
30    //  [3] Status bar          — 1 row
31    let outer = Layout::default()
32        .direction(Direction::Vertical)
33        .constraints([
34            Constraint::Length(3),
35            Constraint::Min(10),
36            Constraint::Length(12),
37            Constraint::Length(1),
38        ])
39        .split(frame.area());
40
41    // Header
42    widgets::header::render(app, frame, outer[0]);
43
44    // -- Main content: three columns --
45    //  [0] Sidebar (branches + stashes + remotes) — dynamic width
46    //  [1] Commit log                             — 40 %
47    //  [2] Diff view                              — remainder
48
49    // Compute sidebar width from the longest branch name + padding for
50    // the indicator icon, highlight symbol, and borders (~6 chars overhead).
51    let longest_branch = app
52        .tab()
53        .branches
54        .iter()
55        .map(|b| b.name.chars().count())
56        .max()
57        .unwrap_or(10);
58    // +6 for: 2 (border) + 2 (highlight "▶ ") + 2 (prefix "* " or "⇄ ")
59    let ideal_sidebar = (longest_branch + 6) as u16;
60    let term_width = outer[1].width;
61    // Sidebar gets up to 30% of terminal width, clamped to [22, 50]
62    let max_sidebar = (term_width * 30 / 100).clamp(22, 50);
63    let sidebar_width = ideal_sidebar.min(max_sidebar).max(22);
64
65    let main_cols = Layout::default()
66        .direction(Direction::Horizontal)
67        .constraints([
68            Constraint::Length(sidebar_width),
69            Constraint::Percentage(40),
70            Constraint::Min(20),
71        ])
72        .split(outer[1]);
73
74    // The sidebar is itself split vertically: branches get the lion's share,
75    // with stashes and remotes at the bottom.
76    let sidebar = Layout::default()
77        .direction(Direction::Vertical)
78        .constraints([
79            Constraint::Min(6),    // branches
80            Constraint::Length(5), // stashes
81            Constraint::Length(5), // remotes
82        ])
83        .split(main_cols[0]);
84
85    features::branches::view::render(app, frame, sidebar[0]);
86    features::stash::view::render(app, frame, sidebar[1]);
87    features::remotes::view::render(app, frame, sidebar[2]);
88
89    // Commit log
90    features::commits::view::render(app, frame, main_cols[1]);
91
92    // Diff view OR theme panel OR options panel
93    if app.show_theme_panel {
94        features::theme::view::render(app, frame, main_cols[2]);
95    } else if app.show_options_panel {
96        features::options::view::render(app, frame, main_cols[2]);
97    } else if app.show_editor_panel {
98        features::editor::view::render(app, frame, main_cols[2]);
99    } else {
100        features::diff::view::render(app, frame, main_cols[2]);
101    }
102
103    // Staging area
104    features::staging::view::render(app, frame, outer[2]);
105
106    // Status bar
107    widgets::status_bar::render(app, frame, outer[3]);
108}