1use crate::app::{App, ContentView};
2use crate::ui::{command_list, content_pane, help_overlay};
3use ratatui::{
4 layout::{Constraint, Direction, Layout, Rect},
5 style::{Color, Modifier, Style},
6 text::{Line, Span},
7 widgets::{Block, Borders, Paragraph},
8 Frame,
9};
10
11pub fn render(app: &App, frame: &mut Frame) {
13 let area = frame.area();
14
15 if area.width < 80 || area.height < 24 {
17 render_resize_prompt(frame, area);
18 return;
19 }
20
21 let rows = Layout::default()
22 .direction(Direction::Vertical)
23 .constraints([
24 Constraint::Length(1), Constraint::Min(0), Constraint::Length(1), ])
28 .split(area);
29
30 render_header(app, frame, rows[0]);
31 render_main(app, frame, rows[1]);
32 render_footer(app, frame, rows[2]);
33
34 if app.show_help {
35 help_overlay::render(app, frame, area);
36 }
37}
38
39fn render_header(app: &App, frame: &mut Frame, area: Rect) {
40 let module = app.current_module();
42 let ex_count = app.exercise_count();
43 let progress_text = if ex_count > 0 {
44 let completed = app
45 .module_progress()
46 .map(|p| p.completed.len())
47 .unwrap_or(0);
48 format!(
49 " {} — {} ({}/{})",
50 module.module.name, module.module.description, completed, ex_count
51 )
52 } else {
53 format!(" {} — {}", module.module.name, module.module.description)
54 };
55
56 let view_label = match app.current_view {
57 ContentView::Intro => "[Intro]",
58 ContentView::Examples => "[Examples]",
59 ContentView::Exercise => "[Exercise]",
60 };
61
62 let title = Line::from(vec![
63 Span::styled(
64 " CLI Tutor ",
65 Style::default()
66 .fg(Color::Cyan)
67 .add_modifier(Modifier::BOLD),
68 ),
69 Span::styled(&progress_text, Style::default().fg(Color::White)),
70 Span::styled(" ", Style::default()),
71 Span::styled(view_label, Style::default().fg(Color::Yellow)),
72 Span::styled(" [?] Help", Style::default().fg(Color::DarkGray)),
73 ]);
74
75 frame.render_widget(
76 Paragraph::new(title).style(Style::default().bg(Color::DarkGray)),
77 area,
78 );
79}
80
81fn render_main(app: &App, frame: &mut Frame, area: Rect) {
82 let cols = Layout::default()
84 .direction(Direction::Horizontal)
85 .constraints([Constraint::Percentage(25), Constraint::Percentage(75)])
86 .split(area);
87
88 command_list::render(app, frame, cols[0]);
89 content_pane::render(app, frame, cols[1]);
90}
91
92fn render_footer(app: &App, frame: &mut Frame, area: Rect) {
93 let hints = match app.current_view {
95 ContentView::Intro | ContentView::Examples => {
96 " ↑↓: Module Tab: Next view PgUp/PgDn: Scroll q: Quit"
97 }
98 ContentView::Exercise => {
99 " Enter: Submit ^T: Hint ^S: Solution ^F: Files ^R: Reset ^N/^P: Next/Prev Esc: Back"
100 }
101 };
102
103 frame.render_widget(
104 Paragraph::new(hints).style(Style::default().bg(Color::DarkGray).fg(Color::White)),
105 area,
106 );
107}
108
109fn render_resize_prompt(frame: &mut Frame, area: Rect) {
111 let msg = Paragraph::new("Please resize your terminal (min 80×24)")
112 .style(Style::default().fg(Color::Yellow))
113 .block(Block::default().borders(Borders::ALL));
114 let center = centered_rect(50, 20, area);
115 frame.render_widget(msg, center);
116}
117
118fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
119 let layout = Layout::default()
120 .direction(Direction::Vertical)
121 .constraints([
122 Constraint::Percentage((100 - percent_y) / 2),
123 Constraint::Percentage(percent_y),
124 Constraint::Percentage((100 - percent_y) / 2),
125 ])
126 .split(r);
127 Layout::default()
128 .direction(Direction::Horizontal)
129 .constraints([
130 Constraint::Percentage((100 - percent_x) / 2),
131 Constraint::Percentage(percent_x),
132 Constraint::Percentage((100 - percent_x) / 2),
133 ])
134 .split(layout[1])[1]
135}