summaryrefslogtreecommitdiff
path: root/src/scenes/pause.rs
blob: 41cff436a9d6bf46917e680e031e0005f34d9903 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//! 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<String>,
}

impl Pause {
    pub fn new(here: Option<&'static str>, note: Option<String>) -> 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<Choice> {
        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<Choice> {
        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));
    }
}