Move help to a separate dialog box
This commit is contained in:
125
src/tui.rs
125
src/tui.rs
@@ -7,7 +7,7 @@ use crossterm::terminal::{
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Wrap};
|
||||
use ratatui::{Frame, Terminal};
|
||||
use std::io::{stdout, Stdout};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -324,6 +324,7 @@ fn main_loop(
|
||||
let (mut screen, mut log) = initial_state(prog, ®istry);
|
||||
let mut focus = default_focus(&screen);
|
||||
let mut editor = Editor::new();
|
||||
let mut help_open = false;
|
||||
|
||||
// Resume case: if the bottom of the log is a LevelPrompt, animate it
|
||||
// from scratch so resumed sessions feel the same as fresh ones.
|
||||
@@ -336,7 +337,15 @@ fn main_loop(
|
||||
|
||||
loop {
|
||||
terminal.draw(|frame| {
|
||||
render(frame, &screen, focus, &editor, &log, typewriter.as_ref());
|
||||
render(
|
||||
frame,
|
||||
&screen,
|
||||
focus,
|
||||
&editor,
|
||||
&log,
|
||||
typewriter.as_ref(),
|
||||
help_open,
|
||||
);
|
||||
})?;
|
||||
|
||||
let poll_timeout = typewriter
|
||||
@@ -357,6 +366,7 @@ fn main_loop(
|
||||
&mut focus,
|
||||
&mut editor,
|
||||
&mut log,
|
||||
&mut help_open,
|
||||
key,
|
||||
prog,
|
||||
®istry,
|
||||
@@ -403,17 +413,32 @@ fn step(
|
||||
focus: &mut Focus,
|
||||
editor: &mut Editor,
|
||||
log: &mut HistoryLog,
|
||||
help_open: &mut bool,
|
||||
key: KeyEvent,
|
||||
prog: &mut Progress,
|
||||
registry: &[Box<dyn Level>],
|
||||
) -> Result<bool> {
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
|
||||
// Global: Ctrl-Q quits from anywhere.
|
||||
// Global: Ctrl-Q quits from anywhere, including the help dialog.
|
||||
if ctrl && key.code == KeyCode::Char('q') {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Help: Ctrl-H opens (idempotent).
|
||||
if ctrl && key.code == KeyCode::Char('h') {
|
||||
*help_open = true;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// While help is open, only Esc closes it; everything else is swallowed.
|
||||
if *help_open {
|
||||
if key.code == KeyCode::Esc {
|
||||
*help_open = false;
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Global: Ctrl-X grades while in Game.
|
||||
if ctrl && key.code == KeyCode::Char('x') && matches!(screen, Screen::Game) {
|
||||
grade(editor.text(), prog, registry, log)?;
|
||||
@@ -582,6 +607,7 @@ fn render(
|
||||
editor: &Editor,
|
||||
log: &HistoryLog,
|
||||
typewriter: Option<&Typewriter>,
|
||||
help_open: bool,
|
||||
) {
|
||||
// Outer layout: two-column area on top, 1-row status bar across the
|
||||
// full width below.
|
||||
@@ -609,29 +635,98 @@ fn render(
|
||||
render_editor_inactive(frame, right);
|
||||
}
|
||||
|
||||
render_status_bar(frame, status, screen, focus);
|
||||
render_status_bar(frame, status, screen, focus, help_open);
|
||||
|
||||
// Help dialog overlay — drawn last so it sits on top of everything.
|
||||
if help_open {
|
||||
let area = centered_rect(70, 60, frame.size());
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(help_widget(), area);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_status_bar(frame: &mut Frame, area: Rect, screen: &Screen, focus: Focus) {
|
||||
/// Center a rectangle of `pct_x` × `pct_y` percent inside `area`.
|
||||
fn centered_rect(pct_x: u16, pct_y: u16, area: Rect) -> Rect {
|
||||
let vertical = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - pct_y) / 2),
|
||||
Constraint::Percentage(pct_y),
|
||||
Constraint::Percentage((100 - pct_y) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - pct_x) / 2),
|
||||
Constraint::Percentage(pct_x),
|
||||
Constraint::Percentage((100 - pct_x) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(vertical[1])[1]
|
||||
}
|
||||
|
||||
fn help_widget<'a>() -> Paragraph<'a> {
|
||||
let body = "\n\
|
||||
⌨ Controls\n\
|
||||
\n\
|
||||
[Tab] swap focus between log and editor\n\
|
||||
[Ctrl-X] grade your YAML\n\
|
||||
[Ctrl-H] open this help\n\
|
||||
[Esc] close help · or focus log when editing\n\
|
||||
[↑/↓] scroll the log (when log is focused)\n\
|
||||
[PgUp/PgDn] scroll faster\n\
|
||||
[r] reset progress\n\
|
||||
[q] / [Ctrl-Q] quit\n\
|
||||
\n\
|
||||
Press [Esc] to close."
|
||||
.to_string();
|
||||
Paragraph::new(body)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" 📜 Controls "),
|
||||
)
|
||||
.wrap(Wrap { trim: false })
|
||||
}
|
||||
|
||||
fn render_status_bar(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
screen: &Screen,
|
||||
focus: Focus,
|
||||
help_open: bool,
|
||||
) {
|
||||
let style = Style::default().fg(Color::White).bg(Color::DarkGray);
|
||||
let p = Paragraph::new(status_text(screen, focus)).style(style);
|
||||
let p = Paragraph::new(status_text(screen, focus, help_open)).style(style);
|
||||
frame.render_widget(p, area);
|
||||
}
|
||||
|
||||
fn status_text(screen: &Screen, focus: Focus) -> String {
|
||||
fn status_text(screen: &Screen, focus: Focus, help_open: bool) -> String {
|
||||
if help_open {
|
||||
return " [Esc] close help · [Ctrl-Q] quit".into();
|
||||
}
|
||||
match (screen, focus) {
|
||||
(Screen::Welcome, _) => {
|
||||
" [Enter] begin · [↑/↓] scroll · [PgUp/PgDn] page · [q] quit".into()
|
||||
" [Enter] begin · [↑/↓] scroll · [PgUp/PgDn] page · [Ctrl-H] help · [q] quit".into()
|
||||
}
|
||||
(Screen::Game, Focus::Editor) => {
|
||||
" [Ctrl-X] grade · [Tab] focus log · [Esc] focus log · [Ctrl-Q] quit".into()
|
||||
" [Ctrl-X] grade · [Tab] focus log · [Esc] focus log · [Ctrl-H] help · [Ctrl-Q] quit"
|
||||
.into()
|
||||
}
|
||||
(Screen::Game, Focus::Game) => {
|
||||
" [Ctrl-X] grade · [Tab] focus editor · [↑/↓] scroll · [r] reset · [q] quit".into()
|
||||
" [Ctrl-X] grade · [Tab] focus editor · [↑/↓] scroll · [r] reset · [Ctrl-H] help · [q] quit"
|
||||
.into()
|
||||
}
|
||||
(Screen::ResetConfirm, _) => " [y] confirm wipe · any other key cancels".into(),
|
||||
(Screen::Completed, _) => {
|
||||
" [↑/↓] scroll · [PgUp/PgDn] page · [r] reset · [q] quit".into()
|
||||
" [↑/↓] scroll · [PgUp/PgDn] page · [r] reset · [Ctrl-H] help · [q] quit".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -723,13 +818,7 @@ fn entry_lines(entry: &LogEntry) -> Vec<String> {
|
||||
" Score ≥ 95 % → 🥇 Gold · ≥ 80 % → 🥈 Silver · ≥ 70 % → 🥉 Bronze.".to_string(),
|
||||
" Below 70 % means retry — refine your YAML and press Ctrl-X again.".to_string(),
|
||||
String::new(),
|
||||
" ⌨ Controls".to_string(),
|
||||
" [Tab] swap focus between log and editor".to_string(),
|
||||
" [Ctrl-X] grade your YAML".to_string(),
|
||||
" [↑/↓] scroll the log (when log is focused)".to_string(),
|
||||
" [PgUp/PgDn] scroll faster".to_string(),
|
||||
" [r] reset progress".to_string(),
|
||||
" [q] / [Ctrl-Q] quit".to_string(),
|
||||
" Press [Ctrl-H] any time for the full controls.".to_string(),
|
||||
],
|
||||
LogEntry::LevelPrompt {
|
||||
level_id,
|
||||
|
||||
Reference in New Issue
Block a user