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
//! Mouse hit-testing.
//!
//! ratatui is immediate-mode, so there is no retained widget tree to hit-test
//! against. Instead each renderer pushes the `Rect`s it drew, paired with what
//! they mean, into a per-frame registry; a click walks that registry in
//! reverse (topmost/most-recently-drawn wins, which puts modals above the
//! rail) and the first containing rect decides the action.
use ratatui::layout::Rect;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HitTarget {
/// The management column's empty space (scroll wheel target).
Cards,
/// One position on one row of the management column: `col` 0 is the
/// label, then each button — the same positions the arrows walk.
CardRow {
row: usize,
col: usize,
},
/// The stream (scroll wheel target).
Stream,
/// One visible stream entry, by index into the visible list.
StreamRow(u64),
ChatInput,
/// A clickable segment of the bottom status bar.
StatusSegment(SegmentId),
/// Anywhere inside the active modal (swallows clicks so they do not reach
/// the panes beneath).
ModalBody,
ModalRow(usize),
ModalButton(ModalButtonId),
/// A labelled button inside a modal.
ModalActionButton(ModalAction),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SegmentId {
Instances,
Waiting,
Model,
Backend,
LogLevel,
WebSearch,
Handler,
Scripting,
Usage,
Help,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModalButtonId {
Confirm,
Cancel,
}
/// A labelled action inside a modal. These are focusable with Tab and
/// clickable — the editors should not depend on remembering shortcuts.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModalAction {
RoutingAdd,
RoutingEdit,
RoutingDelete,
RoutingMoveUp,
RoutingMoveDown,
RoutingSave,
RoutingCancel,
DraftSave,
DraftCancel,
/// One segment of the handler editor's kind control.
DraftKind(crate::tui::modal::routing::HandlerKind),
FormApply,
FormCancel,
/// Show the Wireshark command for the instance the form describes — before
/// it exists, so the capture can be running when it starts.
FormWireshark,
/// Open the composer on the pending intercept: pick an action, fill its
/// fields, send — that answers it.
InterceptCompose,
/// Answer with zero actions: acknowledge, send nothing.
InterceptSend,
/// Refuse the request: the peer gets the fail-closed reply now.
InterceptDismiss,
/// Send the composed action through the client (the composer's button).
ComposerSend,
/// Toggle the composer between fields and raw JSON.
ComposerRaw,
/// Back out of the composer's field form to the action list.
ComposerBack,
/// Accept the text editor's content.
EditorAccept,
/// Discard the text editor's content (same as Esc).
EditorCancel,
/// The confirm dialog's two answers.
ConfirmYes,
ConfirmNo,
}
impl ModalAction {
pub fn label(&self) -> &'static str {
match self {
ModalAction::RoutingAdd => "[ Add ]",
ModalAction::RoutingEdit => "[ Edit ]",
ModalAction::RoutingDelete => "[ Delete ]",
ModalAction::RoutingMoveUp => "[ Move up ]",
ModalAction::RoutingMoveDown => "[ Move down ]",
ModalAction::RoutingSave => "[ Save ]",
ModalAction::RoutingCancel => "[ Cancel ]",
ModalAction::DraftSave => "[ Save response ]",
ModalAction::DraftCancel => "[ Cancel ]",
ModalAction::DraftKind(kind) => kind.label(),
ModalAction::FormApply => "[ Apply ]",
ModalAction::FormCancel => "[ Cancel ]",
ModalAction::FormWireshark => "[ View in Wireshark ]",
ModalAction::InterceptCompose => "[ Compose answer… ]",
ModalAction::InterceptSend => "[ Answer with nothing ]",
ModalAction::InterceptDismiss => "[ Fail closed ]",
ModalAction::ComposerSend => "[ Send ]",
ModalAction::ComposerRaw => "[ Raw JSON ]",
ModalAction::ComposerBack => "[ Back ]",
ModalAction::EditorAccept => "[ Accept ]",
ModalAction::EditorCancel => "[ Cancel ]",
ModalAction::ConfirmYes => "[ Yes ]",
ModalAction::ConfirmNo => "[ No ]",
}
}
}
/// Per-frame registry of drawn regions.
#[derive(Default)]
pub struct HitRegistry {
regions: Vec<(Rect, HitTarget)>,
}
impl HitRegistry {
pub fn clear(&mut self) {
self.regions.clear();
}
pub fn push(&mut self, area: Rect, target: HitTarget) {
self.regions.push((area, target));
}
/// The topmost target containing this cell, if any.
pub fn hit(&self, column: u16, row: u16) -> Option<&HitTarget> {
self.regions
.iter()
.rev()
.find(|(rect, _)| {
column >= rect.x
&& column < rect.x.saturating_add(rect.width)
&& row >= rect.y
&& row < rect.y.saturating_add(rect.height)
})
.map(|(_, target)| target)
}
}