Skip to main content

cli_tutor/ui/
layout.rs

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
11// content_display.LAYOUT.1-5
12pub fn render(app: &App, frame: &mut Frame) {
13    let area = frame.area();
14
15    // content_display.LAYOUT.4 — enforce minimum terminal size
16    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), // header
25            Constraint::Min(0),    // main content
26            Constraint::Length(1), // footer
27        ])
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    // content_display.LAYOUT.2
41    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    // content_display.LAYOUT.1 — 25% / 75% split
83    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    // content_display.LAYOUT.3
94    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
109// content_display.LAYOUT.4
110fn 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}