1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
//! UI rendering: one `draw` function composes all panels for a single frame.
pub mod comment_composer;
pub mod confirm;
pub mod copy_mode;
pub mod dashboard;
pub mod diff;
pub mod first_run;
pub mod glyphs;
pub mod help;
pub mod issue_detail;
pub mod markdown;
pub mod pr_detail;
pub mod repo_picker;
pub mod status_bar;
pub mod tab_bar;
pub mod tabs;
pub mod theme_picker;
pub mod util;
use crate::app::{App, Focus};
use ratatui::{
Frame,
layout::{Constraint, Direction, Layout},
style::Style,
widgets::Block,
};
/// Render the full application UI for one frame.
///
/// Layout (top to bottom):
/// 1. Tab bar (1 row, only when tabs are open)
/// 2. Main content area (fills remaining height)
/// 3. Status bar (1 row)
pub fn draw(f: &mut Frame, app: &App) {
let area = f.area();
// Paint every cell with the theme background before any widget renders.
// Without this, cells not covered by a widget retain the terminal's default
// background, making light themes look broken on dark terminals.
f.render_widget(Block::default().style(Style::default().bg(app.palette.background)), area);
let has_tabs = !app.tabs.is_empty();
let tab_bar_height: u16 = u16::from(has_tabs);
let outer = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(tab_bar_height),
Constraint::Min(1),
Constraint::Length(1),
])
.split(area);
// ── Tab bar ───────────────────────────────────────────────────────────────
if has_tabs {
tab_bar::draw(f, app, outer[0]);
}
// ── Main content area ─────────────────────────────────────────────────────
let content_area = outer[1];
// Route to the appropriate panel based on current focus.
// RepoPicker / Confirm / Composer focus states draw the dashboard beneath them;
// the overlay is rendered after the main content area.
match app.focus {
Focus::Detail | Focus::Composer => {
// If PR detail is populated (or being fetched/errored), render it.
// If issue detail is populated instead, render the issue detail.
// Fall back to dashboard if neither is populated and no fetch is active.
if app.pr_detail.is_some()
|| (app.detail_fetching && app.issue_detail.is_none())
|| (app.detail_error.is_some() && app.issue_detail.is_none())
{
pr_detail::draw(f, app, content_area);
} else if app.issue_detail.is_some()
|| app.detail_fetching
|| app.detail_error.is_some()
{
issue_detail::draw(f, app, content_area);
} else {
// Defensive fallback: both are None and no active fetch.
dashboard::draw(f, app, content_area);
}
}
_ => {
dashboard::draw(f, app, content_area);
}
}
// ── Status bar ────────────────────────────────────────────────────────────
status_bar::draw(f, app, app.flash.as_ref(), outer[2]);
// ── Overlays (drawn last so they float above everything) ──────────────────
if app.show_help {
help::draw(f, app);
}
if app.focus == Focus::FirstRun {
first_run::draw(f, app);
}
if app.focus == Focus::RepoPicker {
repo_picker::draw(f, app);
}
if app.focus == Focus::Confirm && app.confirm.is_some() {
confirm::draw(f, app);
}
if app.focus == Focus::Composer && app.composer.is_some() {
comment_composer::draw(f, app);
}
if app.focus == Focus::ThemePicker {
theme_picker::draw(f, app);
}
}