//! Mouse reporting: what a program asked to be told, and how a report is //! spelled on the wire. //! //! The grid holds the two modes and does the encoding; deciding that the //! pointer did something is the binary's job. use crate::Grid; /// How much of the mouse a program has asked to be told about. /// /// Strictly increasing: each level includes everything below it, which is why /// one field holds all of them rather than a flag per DECSET number. Setting /// any level replaces the previous one, matching xterm — the modes are not /// composable there either, however much the separate numbers suggest it. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] pub enum MouseTracking { /// The pointer belongs to the user: shop selects text with it. #[default] Off, /// DECSET 9, X10 compatibility. Presses only, and no modifier bits. Press, /// DECSET 1000. Presses and releases. Click, /// DECSET 1002. Adds motion, but only while a button is held. Drag, /// DECSET 1003. Adds motion with no button down, which is a report per /// cell crossed for as long as the pointer is over the window. Motion, } /// How a mouse report is spelled on the wire. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub enum MouseEncoding { /// The original `CSI M Cb Cx Cy`, each field a byte biased by 32. /// /// Two consequences worth knowing, and both are why 1006 exists: a /// coordinate past 223 has no byte to land in and is dropped, and a /// release does not say which button was let go. #[default] X10, /// DECSET 1006. `CSI < b ; x ; y M` for a press, `m` for a release — /// decimal, so no coordinate ceiling, and the release keeps its button. Sgr, } /// Which button a mouse report is about. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum MouseButton { Left, Middle, Right, WheelUp, WheelDown, /// Motion with nothing held. Only [`MouseTracking::Motion`] asks for it. None, } impl MouseButton { /// The low bits the wire spells this button with. Wheel buttons set 64, /// which is the bit that distinguishes them from a real press. fn code(self) -> u8 { match self { Self::Left => 0, Self::Middle => 1, Self::Right => 2, Self::WheelUp => 64, Self::WheelDown => 65, // The same 3 a release uses. Unambiguous in context: this one // always arrives with the motion bit set. Self::None => 3, } } fn is_wheel(self) -> bool { matches!(self, Self::WheelUp | Self::WheelDown) } } /// What the pointer did. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum MouseAction { Press, Release, /// The pointer crossed into another cell. Whether a button is held is read /// from the report's button, not from here. Motion, } /// Modifiers held while the pointer did it. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct MouseMods { pub shift: bool, pub alt: bool, pub ctrl: bool, } impl MouseMods { fn bits(self) -> u8 { u8::from(self.shift) * 4 + u8::from(self.alt) * 8 + u8::from(self.ctrl) * 16 } } /// One thing the pointer did, in grid coordinates, ready to be encoded. /// /// Cells, 0-based, as the rest of this crate counts them. The +1 the wire /// wants is applied at encoding time and nowhere else. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct MouseReport { pub button: MouseButton, pub action: MouseAction, pub col: u16, pub row: u16, pub mods: MouseMods, } /// The largest coordinate X10's byte-per-field encoding can carry. /// /// A field is `32 + 1 + n` in one byte, so n stops at 222. Past that the /// report is dropped rather than truncated: a wrong coordinate tells the /// program the click was somewhere it wasn't, and a missing one tells it /// nothing, which is the smaller lie. const X10_COORD_MAX: u16 = 222; impl Grid { /// How much of the mouse the program has asked for. [`MouseTracking::Off`] /// means the pointer is the user's, for selecting text. pub fn mouse_tracking(&self) -> MouseTracking { self.mouse_tracking } /// Which spelling a mouse report should use. pub fn mouse_encoding(&self) -> MouseEncoding { self.mouse_encoding } /// The bytes this pointer event owes the program, or `None` when the /// program did not ask for it. /// /// Filtering lives here rather than at the call site because the levels /// are what decides it, and the levels are this crate's business. A /// caller reports everything the pointer does and lets the answer decide. pub fn encode_mouse(&self, r: MouseReport) -> Option> { if self.mouse_tracking == MouseTracking::Off || !self.mouse_coords_fit(r.col, r.row) { return None; } // The wheel has no release and no drag; it is a press or it is // nothing, at every level that reports the mouse at all. if r.button.is_wheel() { return (r.action == MouseAction::Press).then(|| self.spell_mouse(r)); } let wanted = match r.action { MouseAction::Press => true, MouseAction::Release => self.mouse_tracking >= MouseTracking::Click, MouseAction::Motion if r.button == MouseButton::None => { self.mouse_tracking == MouseTracking::Motion } MouseAction::Motion => self.mouse_tracking >= MouseTracking::Drag, }; wanted.then(|| self.spell_mouse(r)) } fn spell_mouse(&self, r: MouseReport) -> Vec { let mut cb = r.button.code(); if r.action == MouseAction::Motion { cb += 32; } // X10 compatibility mode predates modifier reporting, and a program // that asked for it is parsing three fixed bytes. if self.mouse_tracking != MouseTracking::Press { cb += r.mods.bits(); } match self.mouse_encoding { MouseEncoding::Sgr => { let end = if r.action == MouseAction::Release { 'm' } else { 'M' }; format!("\x1b[<{};{};{}{end}", cb, r.col + 1, r.row + 1).into_bytes() } MouseEncoding::X10 => { // The button a release let go of has nowhere to be spelled // here; 3 is "some button came up" and it is all the program // gets. This is the limitation 1006 exists to lift. if r.action == MouseAction::Release { cb = 3 + if self.mouse_tracking == MouseTracking::Press { 0 } else { r.mods.bits() }; } let mut out = vec![0x1b, b'[', b'M', 32 + cb]; out.push(32 + 1 + r.col as u8); out.push(32 + 1 + r.row as u8); out } } } /// Whether a report at these coordinates can be spelled at all. /// /// Only X10 can fail, and only past its byte ceiling. Checked separately /// from encoding so a caller can drop the event before doing the work. pub fn mouse_coords_fit(&self, col: u16, row: u16) -> bool { self.mouse_encoding == MouseEncoding::Sgr || (col <= X10_COORD_MAX && row <= X10_COORD_MAX) } } #[cfg(test)] mod tests { use crate::testutil::feed; use crate::*; #[test] fn no_mouse_is_reported_until_a_program_asks() { let g = Grid::new(20, 10); assert_eq!(g.mouse_tracking(), MouseTracking::Off); assert_eq!(g.encode_mouse(press(2, 3)), None); } #[test] fn sgr_spells_a_press_and_a_release_differently() { let mut g = Grid::new(20, 10); feed(&mut g, b"\x1b[?1000h\x1b[?1006h"); assert_eq!(bytes(&g, press(2, 3)), "\x1b[<0;3;4M"); let mut up = press(2, 3); up.action = MouseAction::Release; // The button survives the release, which is what 1006 is for. assert_eq!(bytes(&g, up), "\x1b[<0;3;4m"); } #[test] fn x10_biases_every_field_by_thirty_two() { let mut g = Grid::new(20, 10); feed(&mut g, b"\x1b[?1000h"); assert_eq!(g.encode_mouse(press(2, 3)).unwrap(), b"\x1b[M\x20\x23\x24"); } #[test] fn x10_drops_a_coordinate_it_cannot_spell() { let mut g = Grid::new(400, 400); feed(&mut g, b"\x1b[?1000h"); assert_eq!(g.encode_mouse(press(300, 3)), None, "truncated instead"); feed(&mut g, b"\x1b[?1006h"); assert_eq!(bytes(&g, press(300, 3)), "\x1b[<0;301;4M"); } #[test] fn click_tracking_reports_buttons_but_not_movement() { let mut g = Grid::new(20, 10); feed(&mut g, b"\x1b[?1000h\x1b[?1006h"); let mut drag = press(2, 3); drag.action = MouseAction::Motion; assert_eq!(g.encode_mouse(drag), None); feed(&mut g, b"\x1b[?1002h"); assert_eq!(bytes(&g, drag), "\x1b[<32;3;4M"); } #[test] fn only_the_any_motion_level_reports_a_pointer_with_nothing_held() { let mut g = Grid::new(20, 10); feed(&mut g, b"\x1b[?1002h\x1b[?1006h"); let hover = MouseReport { button: MouseButton::None, action: MouseAction::Motion, col: 2, row: 3, mods: MouseMods::default(), }; assert_eq!(g.encode_mouse(hover), None); feed(&mut g, b"\x1b[?1003h"); assert_eq!(bytes(&g, hover), "\x1b[<35;3;4M"); } #[test] fn clearing_a_level_hands_the_pointer_back_to_the_user() { // Not "drop to the next level down": a program clearing 1002 is done // with the mouse, and shop takes the pointer back for selection. let mut g = Grid::new(20, 10); feed(&mut g, b"\x1b[?1002h\x1b[?1002l"); assert_eq!(g.mouse_tracking(), MouseTracking::Off); } #[test] fn clearing_a_level_nobody_set_leaves_the_live_one_alone() { let mut g = Grid::new(20, 10); feed(&mut g, b"\x1b[?1003h\x1b[?1000l"); assert_eq!(g.mouse_tracking(), MouseTracking::Motion); } #[test] fn modifiers_ride_along_except_in_x10_compatibility() { let mut g = Grid::new(20, 10); feed(&mut g, b"\x1b[?1000h\x1b[?1006h"); let mut m = press(2, 3); m.mods = MouseMods { ctrl: true, ..MouseMods::default() }; assert_eq!(bytes(&g, m), "\x1b[<16;3;4M"); // Mode 9 predates modifier reporting and its readers parse fixed // fields, so the bits stay off there. feed(&mut g, b"\x1b[?1000l\x1b[?9h"); assert_eq!(bytes(&g, m), "\x1b[<0;3;4M"); } #[test] fn the_wheel_is_a_press_with_no_release() { let mut g = Grid::new(20, 10); feed(&mut g, b"\x1b[?1000h\x1b[?1006h"); let mut w = press(2, 3); w.button = MouseButton::WheelUp; assert_eq!(bytes(&g, w), "\x1b[<64;3;4M"); w.action = MouseAction::Release; assert_eq!(g.encode_mouse(w), None); } #[test] fn x10_compatibility_reports_the_press_and_stays_quiet_after() { let mut g = Grid::new(20, 10); feed(&mut g, b"\x1b[?9h\x1b[?1006h"); assert!(g.encode_mouse(press(2, 3)).is_some()); let mut up = press(2, 3); up.action = MouseAction::Release; assert_eq!(g.encode_mouse(up), None); } fn press(col: u16, row: u16) -> MouseReport { MouseReport { button: MouseButton::Left, action: MouseAction::Press, col, row, mods: MouseMods::default(), } } fn bytes(g: &Grid, r: MouseReport) -> String { String::from_utf8(g.encode_mouse(r).expect("nothing to send")).unwrap() } }