//! The menu `esc` opens over a game screen: keep playing, do what `esc` //! used to do there (abandon ship, back to your ship), or save and quit to //! the title. It opens on keep playing, and `esc` again closes it: quitting //! is always something you choose. //! //! Like the inventory, it's a panel a scene holds and draws over itself, //! not a scene of its own: the scene beneath stays in view, and stops while //! the menu's open. use ratatui::Frame; use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::style::{Modifier, Style, Stylize}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Clear, Padding, Paragraph, Wrap}; use super::keys::{self, ACCENT, DIM}; /// What you chose. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Choice { /// Save, and go back to the title screen. Quit, /// Whatever `esc` does here besides: abandon ship, leave the building. Here, /// Close the menu and carry on. Resume, } pub struct Pause { choice: usize, /// What else you can do here, if there's anything: "Abandon ship". here: Option<&'static str>, /// A word about where you are, or which way to go. note: Option, } impl Pause { pub fn new(here: Option<&'static str>, note: Option) -> Pause { Pause { choice: 0, here, note } } fn choices(&self) -> Vec<(Choice, &'static str)> { let mut choices = vec![(Choice::Resume, "Keep playing")]; if let Some(here) = self.here { choices.push((Choice::Here, here)); } choices.push((Choice::Quit, "Save and quit to the title")); choices } /// Takes a key. Says what you chose, if you chose anything. A key held /// down doesn't count again: holding `esc` mustn't flicker the menu. pub fn key(&mut self, key: KeyEvent) -> Option { if key.kind == KeyEventKind::Repeat { return None; } let choices = self.choices(); let last = choices.len() - 1; match key.code { KeyCode::Esc | KeyCode::Char('q') => Some(Choice::Resume), KeyCode::Down | KeyCode::Char('j') | KeyCode::Tab => { self.choice = (self.choice + 1).min(last); None } KeyCode::Up | KeyCode::Char('k') | KeyCode::BackTab => { self.choice = self.choice.saturating_sub(1); None } KeyCode::Char('g') | KeyCode::Home => { self.choice = 0; None } KeyCode::Char('G') | KeyCode::End => { self.choice = last; None } KeyCode::Enter | KeyCode::Char(' ' | 'l') => Some(choices[self.choice.min(last)].0), KeyCode::Char(c @ '1'..='9') => { choices.get(c as usize - '1' as usize).map(|&(choice, _)| choice) } _ => None, } } /// Draws the menu in the middle of `area`. pub fn render(&self, frame: &mut Frame, area: Rect) { let mut lines = Vec::new(); if let Some(note) = &self.note { lines.push(Line::from(note.clone()).fg(DIM)); lines.push(Line::default()); } for (i, (_, what)) in self.choices().into_iter().enumerate() { let chosen = i == self.choice; let marker = if chosen { "› " } else { " " }; let line = Line::from(vec![ Span::raw(format!("{} ", i + 1)).fg(ACCENT), Span::raw(marker).fg(ACCENT), Span::raw(what), ]); lines.push(if chosen { line.patch_style(Style::new().add_modifier(Modifier::REVERSED)) } else { line }); } lines.push(Line::default()); lines.push(keys::line(&[("↑↓ enter", "choose"), ("esc", "keep playing")])); let [popup] = Layout::horizontal([Constraint::Max(48)]).flex(Flex::Center).areas(area); let width = usize::from(popup.width.saturating_sub(4)).max(1); let rows: usize = lines.iter().map(|l| l.width().div_ceil(width).max(1)).sum(); let height = u16::try_from(rows + 2).unwrap_or(u16::MAX); let [popup] = Layout::vertical([Constraint::Length(height)]).flex(Flex::Center).areas(popup); let block = Block::bordered() .border_style(Style::new().fg(ACCENT)) .title(Line::from(" PAUSED ").fg(ACCENT).bold()) .padding(Padding::horizontal(1)); frame.render_widget(Clear, popup); frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: true }).block(block), popup); } } #[cfg(test)] mod tests { use ratatui::crossterm::event::KeyModifiers; use super::*; fn press(pause: &mut Pause, code: KeyCode) -> Option { pause.key(KeyEvent::new(code, KeyModifiers::NONE)) } #[test] fn esc_again_closes_the_menu_and_holding_it_does_nothing() { let mut pause = Pause::new(Some("Abandon ship"), None); let held = KeyEvent::new_with_kind(KeyCode::Esc, KeyModifiers::NONE, KeyEventKind::Repeat); assert_eq!(pause.key(held), None); assert_eq!(press(&mut pause, KeyCode::Esc), Some(Choice::Resume)); assert_eq!(press(&mut pause, KeyCode::Char('j')), None); assert_eq!(press(&mut pause, KeyCode::Esc), Some(Choice::Resume), "wherever the cursor is"); } #[test] fn you_can_pick_what_else_esc_does_or_carry_on() { let mut pause = Pause::new(Some("Abandon ship"), None); assert_eq!(press(&mut pause, KeyCode::Enter), Some(Choice::Resume), "it opens on that"); assert_eq!(press(&mut pause, KeyCode::Char('j')), None); assert_eq!(press(&mut pause, KeyCode::Enter), Some(Choice::Here)); assert_eq!(press(&mut pause, KeyCode::Char('G')), None); assert_eq!(press(&mut pause, KeyCode::Enter), Some(Choice::Quit)); assert_eq!(press(&mut pause, KeyCode::Char('1')), Some(Choice::Resume)); assert_eq!(press(&mut pause, KeyCode::Char('3')), Some(Choice::Quit)); assert_eq!(press(&mut pause, KeyCode::Char('q')), Some(Choice::Resume)); assert_eq!(press(&mut pause, KeyCode::Char('x')), None, "anything else does nothing"); let mut pause = Pause::new(None, None); assert_eq!(press(&mut pause, KeyCode::Char('2')), Some(Choice::Quit)); } }