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 => " ↑↓ Scroll  Tab: Next view  ↑↓ Module  q: Quit",
96        ContentView::Examples => " ↑↓ Scroll  Tab: Next view  ↑↓ Module  q: Quit",
97        ContentView::Exercise => {
98            " Enter: Submit  h: Hint  s: Solution  f: Files  r: Reset  Tab: View  q: Quit"
99        }
100    };
101
102    frame.render_widget(
103        Paragraph::new(hints).style(Style::default().bg(Color::DarkGray).fg(Color::White)),
104        area,
105    );
106}
107
108// content_display.LAYOUT.4
109fn render_resize_prompt(frame: &mut Frame, area: Rect) {
110    let msg = Paragraph::new("Please resize your terminal (min 80×24)")
111        .style(Style::default().fg(Color::Yellow))
112        .block(Block::default().borders(Borders::ALL));
113    let center = centered_rect(50, 20, area);
114    frame.render_widget(msg, center);
115}
116
117fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
118    let layout = Layout::default()
119        .direction(Direction::Vertical)
120        .constraints([
121            Constraint::Percentage((100 - percent_y) / 2),
122            Constraint::Percentage(percent_y),
123            Constraint::Percentage((100 - percent_y) / 2),
124        ])
125        .split(r);
126    Layout::default()
127        .direction(Direction::Horizontal)
128        .constraints([
129            Constraint::Percentage((100 - percent_x) / 2),
130            Constraint::Percentage(percent_x),
131            Constraint::Percentage((100 - percent_x) / 2),
132        ])
133        .split(layout[1])[1]
134}