Skip to main content

bubbletea/
mouse.rs

1//! Mouse input handling.
2//!
3//! This module provides types for representing mouse events including clicks,
4//! scrolls, and motion.
5
6use std::fmt;
7
8/// Mouse event message.
9///
10/// MouseMsg is sent to the program's update function when mouse activity occurs.
11/// Note: Mouse events must be enabled using `Program::with_mouse_cell_motion()`
12/// or `Program::with_mouse_all_motion()`.
13///
14/// # Example
15///
16/// ```rust
17/// use bubbletea::{MouseMsg, MouseButton, MouseAction};
18///
19/// fn handle_mouse(mouse: MouseMsg) {
20///     if mouse.button == MouseButton::Left && mouse.action == MouseAction::Press {
21///         println!("Left click at ({}, {})", mouse.x, mouse.y);
22///     }
23/// }
24/// ```
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct MouseMsg {
27    /// X coordinate (column), 0-indexed.
28    pub x: u16,
29    /// Y coordinate (row), 0-indexed.
30    pub y: u16,
31    /// Whether Shift was held.
32    pub shift: bool,
33    /// Whether Alt was held.
34    pub alt: bool,
35    /// Whether Ctrl was held.
36    pub ctrl: bool,
37    /// The action that occurred.
38    pub action: MouseAction,
39    /// The button involved.
40    pub button: MouseButton,
41}
42
43impl MouseMsg {
44    /// Check if this is a wheel event.
45    pub fn is_wheel(&self) -> bool {
46        matches!(
47            self.button,
48            MouseButton::WheelUp
49                | MouseButton::WheelDown
50                | MouseButton::WheelLeft
51                | MouseButton::WheelRight
52        )
53    }
54}
55
56impl Default for MouseMsg {
57    fn default() -> Self {
58        Self {
59            x: 0,
60            y: 0,
61            shift: false,
62            alt: false,
63            ctrl: false,
64            action: MouseAction::Press,
65            button: MouseButton::None,
66        }
67    }
68}
69
70impl fmt::Display for MouseMsg {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        if self.ctrl {
73            write!(f, "ctrl+")?;
74        }
75        if self.alt {
76            write!(f, "alt+")?;
77        }
78        if self.shift {
79            write!(f, "shift+")?;
80        }
81
82        if self.button == MouseButton::None {
83            if self.action == MouseAction::Motion || self.action == MouseAction::Release {
84                write!(f, "{}", self.action)?;
85            } else {
86                write!(f, "unknown")?;
87            }
88        } else if self.is_wheel() {
89            write!(f, "{}", self.button)?;
90        } else {
91            write!(f, "{}", self.button)?;
92            write!(f, " {}", self.action)?;
93        }
94        Ok(())
95    }
96}
97
98/// Mouse action type.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
100pub enum MouseAction {
101    /// Mouse button pressed.
102    #[default]
103    Press,
104    /// Mouse button released.
105    Release,
106    /// Mouse moved.
107    Motion,
108}
109
110impl fmt::Display for MouseAction {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        let name = match self {
113            MouseAction::Press => "press",
114            MouseAction::Release => "release",
115            MouseAction::Motion => "motion",
116        };
117        write!(f, "{}", name)
118    }
119}
120
121/// Mouse button identifier.
122///
123/// Based on X11 mouse button codes.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
125pub enum MouseButton {
126    /// No button (motion only).
127    #[default]
128    None,
129    /// Left button (button 1).
130    Left,
131    /// Middle button (button 2, scroll wheel click).
132    Middle,
133    /// Right button (button 3).
134    Right,
135    /// Scroll wheel up (button 4).
136    WheelUp,
137    /// Scroll wheel down (button 5).
138    WheelDown,
139    /// Scroll wheel left (button 6).
140    WheelLeft,
141    /// Scroll wheel right (button 7).
142    WheelRight,
143    /// Browser backward (button 8).
144    Backward,
145    /// Browser forward (button 9).
146    Forward,
147    /// Button 10.
148    Button10,
149    /// Button 11.
150    Button11,
151}
152
153impl fmt::Display for MouseButton {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        let name = match self {
156            MouseButton::None => "none",
157            MouseButton::Left => "left",
158            MouseButton::Middle => "middle",
159            MouseButton::Right => "right",
160            MouseButton::WheelUp => "wheel up",
161            MouseButton::WheelDown => "wheel down",
162            MouseButton::WheelLeft => "wheel left",
163            MouseButton::WheelRight => "wheel right",
164            MouseButton::Backward => "backward",
165            MouseButton::Forward => "forward",
166            MouseButton::Button10 => "button 10",
167            MouseButton::Button11 => "button 11",
168        };
169        write!(f, "{}", name)
170    }
171}
172
173/// Convert a crossterm mouse event to our MouseMsg.
174pub fn from_crossterm_mouse(event: crossterm::event::MouseEvent) -> MouseMsg {
175    use crossterm::event::{MouseButton as CtButton, MouseEventKind};
176
177    let action = match event.kind {
178        MouseEventKind::Down(_) => MouseAction::Press,
179        MouseEventKind::Up(_) => MouseAction::Release,
180        MouseEventKind::Drag(_) => MouseAction::Motion,
181        MouseEventKind::Moved => MouseAction::Motion,
182        MouseEventKind::ScrollUp => MouseAction::Press,
183        MouseEventKind::ScrollDown => MouseAction::Press,
184        MouseEventKind::ScrollLeft => MouseAction::Press,
185        MouseEventKind::ScrollRight => MouseAction::Press,
186    };
187
188    let button = match event.kind {
189        MouseEventKind::Down(b) | MouseEventKind::Up(b) | MouseEventKind::Drag(b) => match b {
190            CtButton::Left => MouseButton::Left,
191            CtButton::Right => MouseButton::Right,
192            CtButton::Middle => MouseButton::Middle,
193        },
194        MouseEventKind::ScrollUp => MouseButton::WheelUp,
195        MouseEventKind::ScrollDown => MouseButton::WheelDown,
196        MouseEventKind::ScrollLeft => MouseButton::WheelLeft,
197        MouseEventKind::ScrollRight => MouseButton::WheelRight,
198        MouseEventKind::Moved => MouseButton::None,
199    };
200
201    MouseMsg {
202        x: event.column,
203        y: event.row,
204        shift: event
205            .modifiers
206            .contains(crossterm::event::KeyModifiers::SHIFT),
207        alt: event
208            .modifiers
209            .contains(crossterm::event::KeyModifiers::ALT),
210        ctrl: event
211            .modifiers
212            .contains(crossterm::event::KeyModifiers::CONTROL),
213        action,
214        button,
215    }
216}
217
218/// Errors that can occur while parsing mouse escape sequences.
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum MouseParseError {
221    /// The sequence is not a supported mouse format.
222    UnsupportedSequence,
223    /// The sequence format is invalid.
224    InvalidFormat,
225    /// Numeric fields could not be parsed.
226    InvalidNumber,
227    /// Coordinates underflowed when converting to 0-indexed values.
228    CoordinateUnderflow,
229}
230
231impl fmt::Display for MouseParseError {
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        let msg = match self {
234            MouseParseError::UnsupportedSequence => "unsupported mouse sequence",
235            MouseParseError::InvalidFormat => "invalid mouse sequence format",
236            MouseParseError::InvalidNumber => "invalid numeric value in mouse sequence",
237            MouseParseError::CoordinateUnderflow => "mouse coordinates underflowed",
238        };
239        write!(f, "{}", msg)
240    }
241}
242
243impl std::error::Error for MouseParseError {}
244
245#[derive(Debug, Clone, Copy)]
246struct ParsedMouse {
247    button: MouseButton,
248    action: MouseAction,
249    shift: bool,
250    alt: bool,
251    ctrl: bool,
252}
253
254fn is_wheel_button(button: MouseButton) -> bool {
255    matches!(
256        button,
257        MouseButton::WheelUp
258            | MouseButton::WheelDown
259            | MouseButton::WheelLeft
260            | MouseButton::WheelRight
261    )
262}
263
264fn parse_mouse_button(encoded: u16, is_sgr: bool) -> ParsedMouse {
265    let mut e = encoded;
266    if !is_sgr {
267        e = e.saturating_sub(32);
268    }
269
270    const BIT_SHIFT: u16 = 0b0000_0100;
271    const BIT_ALT: u16 = 0b0000_1000;
272    const BIT_CTRL: u16 = 0b0001_0000;
273    const BIT_MOTION: u16 = 0b0010_0000;
274    const BIT_WHEEL: u16 = 0b0100_0000;
275    const BIT_ADD: u16 = 0b1000_0000;
276    const BITS_MASK: u16 = 0b0000_0011;
277
278    let mut action = MouseAction::Press;
279    let button = if e & BIT_ADD != 0 {
280        match e & BITS_MASK {
281            0 => MouseButton::Backward,
282            1 => MouseButton::Forward,
283            2 => MouseButton::Button10,
284            _ => MouseButton::Button11,
285        }
286    } else if e & BIT_WHEEL != 0 {
287        match e & BITS_MASK {
288            0 => MouseButton::WheelUp,
289            1 => MouseButton::WheelDown,
290            2 => MouseButton::WheelLeft,
291            _ => MouseButton::WheelRight,
292        }
293    } else {
294        match e & BITS_MASK {
295            0 => MouseButton::Left,
296            1 => MouseButton::Middle,
297            2 => MouseButton::Right,
298            _ => {
299                action = MouseAction::Release;
300                MouseButton::None
301            }
302        }
303    };
304
305    if e & BIT_MOTION != 0 && !is_wheel_button(button) {
306        action = MouseAction::Motion;
307    }
308
309    ParsedMouse {
310        button,
311        action,
312        shift: e & BIT_SHIFT != 0,
313        alt: e & BIT_ALT != 0,
314        ctrl: e & BIT_CTRL != 0,
315    }
316}
317
318fn parse_x10_mouse_event(buf: &[u8]) -> Result<MouseMsg, MouseParseError> {
319    if buf.len() < 6 {
320        return Err(MouseParseError::InvalidFormat);
321    }
322
323    let parsed = parse_mouse_button(buf[3] as u16, false);
324
325    let x = i32::from(buf[4]) - 32 - 1;
326    let y = i32::from(buf[5]) - 32 - 1;
327    if x < 0 || y < 0 {
328        return Err(MouseParseError::CoordinateUnderflow);
329    }
330
331    Ok(MouseMsg {
332        x: x as u16,
333        y: y as u16,
334        shift: parsed.shift,
335        alt: parsed.alt,
336        ctrl: parsed.ctrl,
337        action: parsed.action,
338        button: parsed.button,
339    })
340}
341
342fn parse_sgr_mouse_event(buf: &[u8]) -> Result<MouseMsg, MouseParseError> {
343    if !buf.starts_with(b"\x1b[<") {
344        return Err(MouseParseError::InvalidFormat);
345    }
346
347    let mut nums = [0u16; 3];
348    let mut idx = 0usize;
349    let mut current: u16 = 0;
350    let mut has_digit = false;
351    let mut release = false;
352
353    for &b in &buf[3..] {
354        match b {
355            b'0'..=b'9' => {
356                current = current
357                    .checked_mul(10)
358                    .and_then(|v| v.checked_add(u16::from(b - b'0')))
359                    .ok_or(MouseParseError::InvalidNumber)?;
360                has_digit = true;
361            }
362            b';' => {
363                if !has_digit || idx >= nums.len() {
364                    return Err(MouseParseError::InvalidFormat);
365                }
366                nums[idx] = current;
367                idx += 1;
368                current = 0;
369                has_digit = false;
370            }
371            b'M' | b'm' => {
372                if !has_digit || idx != 2 {
373                    return Err(MouseParseError::InvalidFormat);
374                }
375                nums[idx] = current;
376                release = b == b'm';
377                break;
378            }
379            _ => return Err(MouseParseError::InvalidFormat),
380        }
381    }
382
383    let mut parsed = parse_mouse_button(nums[0], true);
384    if release && parsed.action != MouseAction::Motion && !is_wheel_button(parsed.button) {
385        parsed.action = MouseAction::Release;
386    }
387
388    if nums[1] == 0 || nums[2] == 0 {
389        return Err(MouseParseError::CoordinateUnderflow);
390    }
391
392    Ok(MouseMsg {
393        x: nums[1] - 1,
394        y: nums[2] - 1,
395        shift: parsed.shift,
396        alt: parsed.alt,
397        ctrl: parsed.ctrl,
398        action: parsed.action,
399        button: parsed.button,
400    })
401}
402
403/// Parse an ANSI mouse escape sequence into a [`MouseMsg`].
404pub fn parse_mouse_event_sequence(buf: &[u8]) -> Result<MouseMsg, MouseParseError> {
405    if buf.starts_with(b"\x1b[<") {
406        parse_sgr_mouse_event(buf)
407    } else if buf.starts_with(b"\x1b[M") {
408        parse_x10_mouse_event(buf)
409    } else {
410        Err(MouseParseError::UnsupportedSequence)
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417    use proptest::prelude::*;
418
419    fn sgr_sequence_bytes(encoded: u16, x: u16, y: u16, release: bool) -> Vec<u8> {
420        let suffix = if release { 'm' } else { 'M' };
421        format!("\x1b[<{};{};{}{}", encoded, x, y, suffix).into_bytes()
422    }
423
424    fn expected_sgr_mouse(encoded: u16, x: u16, y: u16, release: bool) -> MouseMsg {
425        let mut parsed = parse_mouse_button(encoded, true);
426        if release && parsed.action != MouseAction::Motion && !is_wheel_button(parsed.button) {
427            parsed.action = MouseAction::Release;
428        }
429        MouseMsg {
430            x: x - 1,
431            y: y - 1,
432            shift: parsed.shift,
433            alt: parsed.alt,
434            ctrl: parsed.ctrl,
435            action: parsed.action,
436            button: parsed.button,
437        }
438    }
439
440    fn x10_sequence_bytes(encoded: u8, x: u16, y: u16) -> [u8; 6] {
441        let x_byte = (x + 33) as u8;
442        let y_byte = (y + 33) as u8;
443        [0x1b, b'[', b'M', encoded, x_byte, y_byte]
444    }
445
446    fn expected_x10_mouse(encoded: u8, x: u16, y: u16) -> MouseMsg {
447        let parsed = parse_mouse_button(u16::from(encoded), false);
448        MouseMsg {
449            x,
450            y,
451            shift: parsed.shift,
452            alt: parsed.alt,
453            ctrl: parsed.ctrl,
454            action: parsed.action,
455            button: parsed.button,
456        }
457    }
458
459    #[test]
460    fn test_mouse_msg_display() {
461        let mouse = MouseMsg {
462            x: 10,
463            y: 20,
464            shift: false,
465            alt: false,
466            ctrl: false,
467            action: MouseAction::Press,
468            button: MouseButton::Left,
469        };
470        assert_eq!(mouse.to_string(), "left press");
471
472        let mouse = MouseMsg {
473            x: 10,
474            y: 20,
475            shift: false,
476            alt: false,
477            ctrl: true,
478            action: MouseAction::Press,
479            button: MouseButton::Left,
480        };
481        assert_eq!(mouse.to_string(), "ctrl+left press");
482    }
483
484    #[test]
485    fn test_mouse_is_wheel() {
486        let mouse = MouseMsg {
487            button: MouseButton::WheelUp,
488            ..Default::default()
489        };
490        assert!(mouse.is_wheel());
491
492        let mouse = MouseMsg {
493            button: MouseButton::Left,
494            ..Default::default()
495        };
496        assert!(!mouse.is_wheel());
497    }
498
499    #[test]
500    fn test_mouse_button_display() {
501        assert_eq!(MouseButton::Left.to_string(), "left");
502        assert_eq!(MouseButton::WheelUp.to_string(), "wheel up");
503    }
504
505    #[test]
506    fn test_mouse_action_display() {
507        assert_eq!(MouseAction::Press.to_string(), "press");
508        assert_eq!(MouseAction::Release.to_string(), "release");
509        assert_eq!(MouseAction::Motion.to_string(), "motion");
510    }
511
512    proptest! {
513        #[test]
514        fn prop_parse_sgr_mouse_roundtrip(
515            encoded in 0u16..=255,
516            x in 1u16..=2000,
517            y in 1u16..=2000,
518            release in any::<bool>(),
519        ) {
520            let buf = sgr_sequence_bytes(encoded, x, y, release);
521            let msg = parse_mouse_event_sequence(&buf).unwrap();
522            let expected = expected_sgr_mouse(encoded, x, y, release);
523            prop_assert_eq!(msg, expected);
524        }
525
526        #[test]
527        fn prop_parse_x10_mouse_roundtrip(
528            encoded in 32u8..=255,
529            x in 0u16..=222,
530            y in 0u16..=222,
531        ) {
532            let buf = x10_sequence_bytes(encoded, x, y);
533            let msg = parse_mouse_event_sequence(&buf).unwrap();
534            let expected = expected_x10_mouse(encoded, x, y);
535            prop_assert_eq!(msg, expected);
536        }
537    }
538}