Skip to main content

kimun_notes/components/dialogs/
modal_shell.rs

1use ratatui::crossterm::event::{KeyCode, KeyEvent, MouseEvent};
2use ratatui::layout::Rect;
3
4use crate::components::clickable::is_press_outside;
5use crate::components::hint_row::HintRow;
6
7/// Pointer plumbing shared by the dismissable popups: it remembers the popup's
8/// rect from the last render, and turns the two universal clicks into the key
9/// the dialog already handles — a press outside is `Esc`, a press on a hint
10/// chip is that chip's key. Dialogs that must not be dismissed by a stray click
11/// (delete, rename, anything holding typed input) simply don't use it.
12#[derive(Debug, Default)]
13pub struct ModalShell {
14    popup_rect: Rect,
15}
16
17impl ModalShell {
18    /// Record the popup's outer rect; call once per render.
19    pub fn set(&mut self, rect: Rect) {
20        self.popup_rect = rect;
21    }
22
23    /// The key a mouse event stands for: `Esc` for a press outside the popup,
24    /// else the key of the hint chip under a left press (first row in `hints`
25    /// wins). `None` leaves the event for the dialog's own targets.
26    pub fn pointer_key(&self, m: &MouseEvent, hints: &[&HintRow]) -> Option<KeyEvent> {
27        if is_press_outside(m, self.popup_rect) {
28            return Some(KeyEvent::from(KeyCode::Esc));
29        }
30        hints.iter().find_map(|h| h.hit(m))
31    }
32}
33
34#[cfg(test)]
35mod tests {
36    use super::*;
37    use ratatui::crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
38
39    fn press(col: u16, row: u16) -> MouseEvent {
40        MouseEvent {
41            kind: MouseEventKind::Down(MouseButton::Left),
42            column: col,
43            row,
44            modifiers: KeyModifiers::NONE,
45        }
46    }
47
48    #[test]
49    fn outside_press_is_esc_inside_is_none() {
50        let mut shell = ModalShell::default();
51        assert_eq!(
52            shell.pointer_key(&press(0, 0), &[]),
53            None,
54            "not rendered yet"
55        );
56        shell.set(Rect::new(10, 5, 20, 8));
57        assert_eq!(
58            shell.pointer_key(&press(0, 0), &[]).map(|k| k.code),
59            Some(KeyCode::Esc)
60        );
61        assert_eq!(shell.pointer_key(&press(12, 6), &[]), None);
62    }
63
64    #[test]
65    fn hint_chip_press_yields_its_key_and_outside_wins() {
66        use ratatui::style::Style;
67        use ratatui::{Terminal, backend::TestBackend};
68        let theme = crate::settings::themes::Theme::gruvbox_dark();
69        let mut hints = HintRow::new(&[(KeyCode::Esc, "Esc", "Close")]);
70        let mut t = Terminal::new(TestBackend::new(40, 5)).unwrap();
71        t.draw(|f| hints.render(f, Rect::new(10, 2, 20, 1), Style::default(), &theme))
72            .unwrap();
73        let (col, row) =
74            crate::test_support::find_text(t.backend().buffer(), "Close").expect("chip drawn");
75        let mut shell = ModalShell::default();
76        shell.set(Rect::new(8, 1, 30, 3));
77        let key = |m| shell.pointer_key(&m, &[&hints]).map(|k| k.code);
78        assert_eq!(key(press(col, row)), Some(KeyCode::Esc), "chip key");
79        assert_eq!(key(press(0, 0)), Some(KeyCode::Esc), "outside is Esc");
80        assert_eq!(
81            key(press(col, row.saturating_sub(1))),
82            None,
83            "inside, off chip"
84        );
85    }
86}