Skip to main content

netget/tui/
hit.rs

1//! Mouse hit-testing.
2//!
3//! ratatui is immediate-mode, so there is no retained widget tree to hit-test
4//! against. Instead each renderer pushes the `Rect`s it drew, paired with what
5//! they mean, into a per-frame registry; a click walks that registry in
6//! reverse (topmost/most-recently-drawn wins, which puts modals above the
7//! rail) and the first containing rect decides the action.
8
9use ratatui::layout::Rect;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum HitTarget {
13    /// The management column's empty space (scroll wheel target).
14    Cards,
15    /// One position on one row of the management column: `col` 0 is the
16    /// label, then each button — the same positions the arrows walk.
17    CardRow {
18        row: usize,
19        col: usize,
20    },
21    /// The stream (scroll wheel target).
22    Stream,
23    /// One visible stream entry, by index into the visible list.
24    StreamRow(u64),
25    ChatInput,
26    /// A clickable segment of the bottom status bar.
27    StatusSegment(SegmentId),
28    /// Anywhere inside the active modal (swallows clicks so they do not reach
29    /// the panes beneath).
30    ModalBody,
31    ModalRow(usize),
32    ModalButton(ModalButtonId),
33    /// A labelled button inside a modal.
34    ModalActionButton(ModalAction),
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum SegmentId {
39    Instances,
40    Waiting,
41    Model,
42    Backend,
43    LogLevel,
44    WebSearch,
45    Handler,
46    Scripting,
47    Usage,
48    Help,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum ModalButtonId {
53    Confirm,
54    Cancel,
55}
56
57/// A labelled action inside a modal. These are focusable with Tab and
58/// clickable — the editors should not depend on remembering shortcuts.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum ModalAction {
61    RoutingAdd,
62    RoutingEdit,
63    RoutingDelete,
64    RoutingMoveUp,
65    RoutingMoveDown,
66    RoutingSave,
67    RoutingCancel,
68    DraftSave,
69    DraftCancel,
70    /// One segment of the handler editor's kind control.
71    DraftKind(crate::tui::modal::routing::HandlerKind),
72    FormApply,
73    FormCancel,
74    /// Show the Wireshark command for the instance the form describes — before
75    /// it exists, so the capture can be running when it starts.
76    FormWireshark,
77    /// Open the composer on the pending intercept: pick an action, fill its
78    /// fields, send — that answers it.
79    InterceptCompose,
80    /// Answer with zero actions: acknowledge, send nothing.
81    InterceptSend,
82    /// Refuse the request: the peer gets the fail-closed reply now.
83    InterceptDismiss,
84    /// Send the composed action through the client (the composer's button).
85    ComposerSend,
86    /// Toggle the composer between fields and raw JSON.
87    ComposerRaw,
88    /// Back out of the composer's field form to the action list.
89    ComposerBack,
90    /// Accept the text editor's content.
91    EditorAccept,
92    /// Discard the text editor's content (same as Esc).
93    EditorCancel,
94    /// The confirm dialog's two answers.
95    ConfirmYes,
96    ConfirmNo,
97}
98
99impl ModalAction {
100    pub fn label(&self) -> &'static str {
101        match self {
102            ModalAction::RoutingAdd => "[ Add ]",
103            ModalAction::RoutingEdit => "[ Edit ]",
104            ModalAction::RoutingDelete => "[ Delete ]",
105            ModalAction::RoutingMoveUp => "[ Move up ]",
106            ModalAction::RoutingMoveDown => "[ Move down ]",
107            ModalAction::RoutingSave => "[ Save ]",
108            ModalAction::RoutingCancel => "[ Cancel ]",
109            ModalAction::DraftSave => "[ Save response ]",
110            ModalAction::DraftCancel => "[ Cancel ]",
111            ModalAction::DraftKind(kind) => kind.label(),
112            ModalAction::FormApply => "[ Apply ]",
113            ModalAction::FormCancel => "[ Cancel ]",
114            ModalAction::FormWireshark => "[ View in Wireshark ]",
115            ModalAction::InterceptCompose => "[ Compose answer… ]",
116            ModalAction::InterceptSend => "[ Answer with nothing ]",
117            ModalAction::InterceptDismiss => "[ Fail closed ]",
118            ModalAction::ComposerSend => "[ Send ]",
119            ModalAction::ComposerRaw => "[ Raw JSON ]",
120            ModalAction::ComposerBack => "[ Back ]",
121            ModalAction::EditorAccept => "[ Accept ]",
122            ModalAction::EditorCancel => "[ Cancel ]",
123            ModalAction::ConfirmYes => "[ Yes ]",
124            ModalAction::ConfirmNo => "[ No ]",
125        }
126    }
127}
128
129/// Per-frame registry of drawn regions.
130#[derive(Default)]
131pub struct HitRegistry {
132    regions: Vec<(Rect, HitTarget)>,
133}
134
135impl HitRegistry {
136    pub fn clear(&mut self) {
137        self.regions.clear();
138    }
139
140    pub fn push(&mut self, area: Rect, target: HitTarget) {
141        self.regions.push((area, target));
142    }
143
144    /// The topmost target containing this cell, if any.
145    pub fn hit(&self, column: u16, row: u16) -> Option<&HitTarget> {
146        self.regions
147            .iter()
148            .rev()
149            .find(|(rect, _)| {
150                column >= rect.x
151                    && column < rect.x.saturating_add(rect.width)
152                    && row >= rect.y
153                    && row < rect.y.saturating_add(rect.height)
154            })
155            .map(|(_, target)| target)
156    }
157}