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
//! The [`Modal`] trait, its render context, and dispatch outcomes. See
//! [`super::ModalStack`] for ownership and dispatch.
use std::any::Any;
use std::time::Instant;
use crossterm::event::KeyEvent;
use ratatui::layout::Rect;
use ratatui::Frame;
use crate::app::App;
use crate::config::{Config, Theme};
pub use crate::ui::ModalKind;
/// Hit-test for a cached `esc` close-affordance rect.
pub fn esc_rect_hit(esc_rect: Option<ratatui::layout::Rect>, col: u16, row: u16) -> bool {
match esc_rect {
Some(r) => col >= r.x && col < r.x + r.width && row >= r.y && row < r.y + r.height,
None => false,
}
}
/// `Close` when `(col, row)` lands inside `esc_rect`, else `Continue`.
pub fn close_if_esc_clicked(
esc_rect: Option<ratatui::layout::Rect>,
col: u16,
row: u16,
) -> ModalOutcome {
if esc_rect_hit(esc_rect, col, row) {
ModalOutcome::Close
} else {
ModalOutcome::Continue
}
}
/// Read-only context handed to [`Modal::render`].
pub struct ModalRenderCtx<'a> {
pub theme: &'a Theme,
pub config: &'a Config,
pub cursor_visible: bool,
}
/// Outcome of dispatching input to a modal. The dispatcher pops the modal before invoking
/// the handler; `Continue*` re-pushes it, `Close*` drops it, and the `*And` callbacks run
/// afterwards against the now-unborrowed `App`.
pub enum ModalOutcome {
Continue,
/// Stay open; the callback runs after the modal is pushed back (e.g. to open another
/// modal on top).
ContinueAnd(Box<dyn FnOnce(&mut App)>),
Close,
CloseAnd(Box<dyn FnOnce(&mut App)>),
}
/// A popup or overlay on top of the editor view. The topmost modal on the
/// [`super::ModalStack`] absorbs all keyboard and wheel input and renders last.
pub trait Modal {
fn render(&mut self, frame: &mut Frame<'_>, area: Rect, ctx: &ModalRenderCtx<'_>);
/// `doc_height` / `doc_width` serve overlays that dispatch `Action`s through the same
/// pipeline as direct keystrokes.
fn handle_key(
&mut self,
key: KeyEvent,
app: &mut App,
doc_height: usize,
doc_width: usize,
) -> ModalOutcome;
/// Apply a bracketed paste. `text` is raw; the modal sanitizes it
/// ([`crate::ui::sanitize_paste`]). Default: ignore.
fn handle_paste(&mut self, _text: &str) -> ModalOutcome {
ModalOutcome::Continue
}
fn handle_wheel(&mut self, _delta: i32) {}
/// Left click at terminal `(col, row)`. Default: ignore.
fn handle_click(&mut self, _col: u16, _row: u16, _app: &mut App) -> ModalOutcome {
ModalOutcome::Continue
}
/// Visual urgency; drives the title color. Must read the same field the render path
/// uses so the two can't drift.
#[allow(dead_code)]
fn kind(&self) -> ModalKind {
ModalKind::Normal
}
/// Whether `Esc` / the `esc` button may dismiss this modal; `false` forces a footer
/// button. Same single-field rule as [`Self::kind`].
#[allow(dead_code)]
fn dismissable(&self) -> bool {
true
}
/// When this modal next needs a redraw for time-driven content (spinner, rotating
/// tagline); aggregated by [`super::ModalStack::next_deadline`].
fn next_deadline(&self) -> Option<Instant> {
None
}
/// Always the trivial `{ self }`; no default because an `Any` supertrait would force
/// `'static` on every implementor.
fn as_any(&self) -> &dyn Any;
/// Mutable counterpart to [`Self::as_any`], for [`super::ModalStack::find_first_mut`].
fn as_any_mut(&mut self) -> &mut dyn Any;
}