Skip to main content

max / shop

11.7 KB · 342 lines History Blame Raw
1 //! Mouse reporting: what a program asked to be told, and how a report is
2 //! spelled on the wire.
3 //!
4 //! The grid holds the two modes and does the encoding; deciding that the
5 //! pointer did something is the binary's job.
6
7 use crate::Grid;
8
9 /// How much of the mouse a program has asked to be told about.
10 ///
11 /// Strictly increasing: each level includes everything below it, which is why
12 /// one field holds all of them rather than a flag per DECSET number. Setting
13 /// any level replaces the previous one, matching xterm — the modes are not
14 /// composable there either, however much the separate numbers suggest it.
15 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
16 pub enum MouseTracking {
17 /// The pointer belongs to the user: shop selects text with it.
18 #[default]
19 Off,
20 /// DECSET 9, X10 compatibility. Presses only, and no modifier bits.
21 Press,
22 /// DECSET 1000. Presses and releases.
23 Click,
24 /// DECSET 1002. Adds motion, but only while a button is held.
25 Drag,
26 /// DECSET 1003. Adds motion with no button down, which is a report per
27 /// cell crossed for as long as the pointer is over the window.
28 Motion,
29 }
30
31 /// How a mouse report is spelled on the wire.
32 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
33 pub enum MouseEncoding {
34 /// The original `CSI M Cb Cx Cy`, each field a byte biased by 32.
35 ///
36 /// Two consequences worth knowing, and both are why 1006 exists: a
37 /// coordinate past 223 has no byte to land in and is dropped, and a
38 /// release does not say which button was let go.
39 #[default]
40 X10,
41 /// DECSET 1006. `CSI < b ; x ; y M` for a press, `m` for a release —
42 /// decimal, so no coordinate ceiling, and the release keeps its button.
43 Sgr,
44 }
45
46 /// Which button a mouse report is about.
47 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
48 pub enum MouseButton {
49 Left,
50 Middle,
51 Right,
52 WheelUp,
53 WheelDown,
54 /// Motion with nothing held. Only [`MouseTracking::Motion`] asks for it.
55 None,
56 }
57
58 impl MouseButton {
59 /// The low bits the wire spells this button with. Wheel buttons set 64,
60 /// which is the bit that distinguishes them from a real press.
61 fn code(self) -> u8 {
62 match self {
63 Self::Left => 0,
64 Self::Middle => 1,
65 Self::Right => 2,
66 Self::WheelUp => 64,
67 Self::WheelDown => 65,
68 // The same 3 a release uses. Unambiguous in context: this one
69 // always arrives with the motion bit set.
70 Self::None => 3,
71 }
72 }
73
74 fn is_wheel(self) -> bool {
75 matches!(self, Self::WheelUp | Self::WheelDown)
76 }
77 }
78
79 /// What the pointer did.
80 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
81 pub enum MouseAction {
82 Press,
83 Release,
84 /// The pointer crossed into another cell. Whether a button is held is read
85 /// from the report's button, not from here.
86 Motion,
87 }
88
89 /// Modifiers held while the pointer did it.
90 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
91 pub struct MouseMods {
92 pub shift: bool,
93 pub alt: bool,
94 pub ctrl: bool,
95 }
96
97 impl MouseMods {
98 fn bits(self) -> u8 {
99 u8::from(self.shift) * 4 + u8::from(self.alt) * 8 + u8::from(self.ctrl) * 16
100 }
101 }
102
103 /// One thing the pointer did, in grid coordinates, ready to be encoded.
104 ///
105 /// Cells, 0-based, as the rest of this crate counts them. The +1 the wire
106 /// wants is applied at encoding time and nowhere else.
107 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
108 pub struct MouseReport {
109 pub button: MouseButton,
110 pub action: MouseAction,
111 pub col: u16,
112 pub row: u16,
113 pub mods: MouseMods,
114 }
115
116 /// The largest coordinate X10's byte-per-field encoding can carry.
117 ///
118 /// A field is `32 + 1 + n` in one byte, so n stops at 222. Past that the
119 /// report is dropped rather than truncated: a wrong coordinate tells the
120 /// program the click was somewhere it wasn't, and a missing one tells it
121 /// nothing, which is the smaller lie.
122 const X10_COORD_MAX: u16 = 222;
123
124 impl Grid {
125 /// How much of the mouse the program has asked for. [`MouseTracking::Off`]
126 /// means the pointer is the user's, for selecting text.
127 pub fn mouse_tracking(&self) -> MouseTracking {
128 self.mouse_tracking
129 }
130
131 /// Which spelling a mouse report should use.
132 pub fn mouse_encoding(&self) -> MouseEncoding {
133 self.mouse_encoding
134 }
135
136 /// The bytes this pointer event owes the program, or `None` when the
137 /// program did not ask for it.
138 ///
139 /// Filtering lives here rather than at the call site because the levels
140 /// are what decides it, and the levels are this crate's business. A
141 /// caller reports everything the pointer does and lets the answer decide.
142 pub fn encode_mouse(&self, r: MouseReport) -> Option<Vec<u8>> {
143 if self.mouse_tracking == MouseTracking::Off || !self.mouse_coords_fit(r.col, r.row) {
144 return None;
145 }
146 // The wheel has no release and no drag; it is a press or it is
147 // nothing, at every level that reports the mouse at all.
148 if r.button.is_wheel() {
149 return (r.action == MouseAction::Press).then(|| self.spell_mouse(r));
150 }
151 let wanted = match r.action {
152 MouseAction::Press => true,
153 MouseAction::Release => self.mouse_tracking >= MouseTracking::Click,
154 MouseAction::Motion if r.button == MouseButton::None => {
155 self.mouse_tracking == MouseTracking::Motion
156 }
157 MouseAction::Motion => self.mouse_tracking >= MouseTracking::Drag,
158 };
159 wanted.then(|| self.spell_mouse(r))
160 }
161
162 fn spell_mouse(&self, r: MouseReport) -> Vec<u8> {
163 let mut cb = r.button.code();
164 if r.action == MouseAction::Motion {
165 cb += 32;
166 }
167 // X10 compatibility mode predates modifier reporting, and a program
168 // that asked for it is parsing three fixed bytes.
169 if self.mouse_tracking != MouseTracking::Press {
170 cb += r.mods.bits();
171 }
172 match self.mouse_encoding {
173 MouseEncoding::Sgr => {
174 let end = if r.action == MouseAction::Release {
175 'm'
176 } else {
177 'M'
178 };
179 format!("\x1b[<{};{};{}{end}", cb, r.col + 1, r.row + 1).into_bytes()
180 }
181 MouseEncoding::X10 => {
182 // The button a release let go of has nowhere to be spelled
183 // here; 3 is "some button came up" and it is all the program
184 // gets. This is the limitation 1006 exists to lift.
185 if r.action == MouseAction::Release {
186 cb = 3 + if self.mouse_tracking == MouseTracking::Press {
187 0
188 } else {
189 r.mods.bits()
190 };
191 }
192 let mut out = vec![0x1b, b'[', b'M', 32 + cb];
193 out.push(32 + 1 + r.col as u8);
194 out.push(32 + 1 + r.row as u8);
195 out
196 }
197 }
198 }
199
200 /// Whether a report at these coordinates can be spelled at all.
201 ///
202 /// Only X10 can fail, and only past its byte ceiling. Checked separately
203 /// from encoding so a caller can drop the event before doing the work.
204 pub fn mouse_coords_fit(&self, col: u16, row: u16) -> bool {
205 self.mouse_encoding == MouseEncoding::Sgr || (col <= X10_COORD_MAX && row <= X10_COORD_MAX)
206 }
207 }
208
209 #[cfg(test)]
210 mod tests {
211 use crate::testutil::feed;
212 use crate::*;
213
214 #[test]
215 fn no_mouse_is_reported_until_a_program_asks() {
216 let g = Grid::new(20, 10);
217 assert_eq!(g.mouse_tracking(), MouseTracking::Off);
218 assert_eq!(g.encode_mouse(press(2, 3)), None);
219 }
220
221 #[test]
222 fn sgr_spells_a_press_and_a_release_differently() {
223 let mut g = Grid::new(20, 10);
224 feed(&mut g, b"\x1b[?1000h\x1b[?1006h");
225 assert_eq!(bytes(&g, press(2, 3)), "\x1b[<0;3;4M");
226 let mut up = press(2, 3);
227 up.action = MouseAction::Release;
228 // The button survives the release, which is what 1006 is for.
229 assert_eq!(bytes(&g, up), "\x1b[<0;3;4m");
230 }
231
232 #[test]
233 fn x10_biases_every_field_by_thirty_two() {
234 let mut g = Grid::new(20, 10);
235 feed(&mut g, b"\x1b[?1000h");
236 assert_eq!(g.encode_mouse(press(2, 3)).unwrap(), b"\x1b[M\x20\x23\x24");
237 }
238
239 #[test]
240 fn x10_drops_a_coordinate_it_cannot_spell() {
241 let mut g = Grid::new(400, 400);
242 feed(&mut g, b"\x1b[?1000h");
243 assert_eq!(g.encode_mouse(press(300, 3)), None, "truncated instead");
244 feed(&mut g, b"\x1b[?1006h");
245 assert_eq!(bytes(&g, press(300, 3)), "\x1b[<0;301;4M");
246 }
247
248 #[test]
249 fn click_tracking_reports_buttons_but_not_movement() {
250 let mut g = Grid::new(20, 10);
251 feed(&mut g, b"\x1b[?1000h\x1b[?1006h");
252 let mut drag = press(2, 3);
253 drag.action = MouseAction::Motion;
254 assert_eq!(g.encode_mouse(drag), None);
255 feed(&mut g, b"\x1b[?1002h");
256 assert_eq!(bytes(&g, drag), "\x1b[<32;3;4M");
257 }
258
259 #[test]
260 fn only_the_any_motion_level_reports_a_pointer_with_nothing_held() {
261 let mut g = Grid::new(20, 10);
262 feed(&mut g, b"\x1b[?1002h\x1b[?1006h");
263 let hover = MouseReport {
264 button: MouseButton::None,
265 action: MouseAction::Motion,
266 col: 2,
267 row: 3,
268 mods: MouseMods::default(),
269 };
270 assert_eq!(g.encode_mouse(hover), None);
271 feed(&mut g, b"\x1b[?1003h");
272 assert_eq!(bytes(&g, hover), "\x1b[<35;3;4M");
273 }
274
275 #[test]
276 fn clearing_a_level_hands_the_pointer_back_to_the_user() {
277 // Not "drop to the next level down": a program clearing 1002 is done
278 // with the mouse, and shop takes the pointer back for selection.
279 let mut g = Grid::new(20, 10);
280 feed(&mut g, b"\x1b[?1002h\x1b[?1002l");
281 assert_eq!(g.mouse_tracking(), MouseTracking::Off);
282 }
283
284 #[test]
285 fn clearing_a_level_nobody_set_leaves_the_live_one_alone() {
286 let mut g = Grid::new(20, 10);
287 feed(&mut g, b"\x1b[?1003h\x1b[?1000l");
288 assert_eq!(g.mouse_tracking(), MouseTracking::Motion);
289 }
290
291 #[test]
292 fn modifiers_ride_along_except_in_x10_compatibility() {
293 let mut g = Grid::new(20, 10);
294 feed(&mut g, b"\x1b[?1000h\x1b[?1006h");
295 let mut m = press(2, 3);
296 m.mods = MouseMods {
297 ctrl: true,
298 ..MouseMods::default()
299 };
300 assert_eq!(bytes(&g, m), "\x1b[<16;3;4M");
301 // Mode 9 predates modifier reporting and its readers parse fixed
302 // fields, so the bits stay off there.
303 feed(&mut g, b"\x1b[?1000l\x1b[?9h");
304 assert_eq!(bytes(&g, m), "\x1b[<0;3;4M");
305 }
306
307 #[test]
308 fn the_wheel_is_a_press_with_no_release() {
309 let mut g = Grid::new(20, 10);
310 feed(&mut g, b"\x1b[?1000h\x1b[?1006h");
311 let mut w = press(2, 3);
312 w.button = MouseButton::WheelUp;
313 assert_eq!(bytes(&g, w), "\x1b[<64;3;4M");
314 w.action = MouseAction::Release;
315 assert_eq!(g.encode_mouse(w), None);
316 }
317
318 #[test]
319 fn x10_compatibility_reports_the_press_and_stays_quiet_after() {
320 let mut g = Grid::new(20, 10);
321 feed(&mut g, b"\x1b[?9h\x1b[?1006h");
322 assert!(g.encode_mouse(press(2, 3)).is_some());
323 let mut up = press(2, 3);
324 up.action = MouseAction::Release;
325 assert_eq!(g.encode_mouse(up), None);
326 }
327
328 fn press(col: u16, row: u16) -> MouseReport {
329 MouseReport {
330 button: MouseButton::Left,
331 action: MouseAction::Press,
332 col,
333 row,
334 mods: MouseMods::default(),
335 }
336 }
337
338 fn bytes(g: &Grid, r: MouseReport) -> String {
339 String::from_utf8(g.encode_mouse(r).expect("nothing to send")).unwrap()
340 }
341 }
342