Skip to main content

beamterm_renderer/
mouse.rs

1//! Mouse input handling for the beamterm terminal renderer.
2//!
3//! This module provides mouse event handling infrastructure for the terminal,
4//! including coordinate conversion from pixel space to terminal grid space,
5//! text selection with automatic clipboard integration, and customizable
6//! event handling.
7//!
8//! # Architecture
9//!
10//! The mouse handling system consists of:
11//! - [`TerminalMouseHandler`] - Main event handler that attaches to a canvas
12//! - [`TerminalMouseEvent`] - Mouse events translated to terminal coordinates
13//! - [`DefaultSelectionHandler`] - Built-in text selection implementation
14//! - Internal state tracking for selection operations
15//!
16//! # Example
17//!
18//! ```rust,no_run
19//! use beamterm_renderer::{Terminal, SelectionMode};
20//! use beamterm_renderer::mouse::{MouseSelectOptions, ModifierKeys};
21//!
22//! // Enable selection with Shift+Click
23//! let terminal = Terminal::builder("#canvas")
24//!     .mouse_selection_handler(
25//!         MouseSelectOptions::new()
26//!             .selection_mode(SelectionMode::Linear)
27//!             .require_modifier_keys(ModifierKeys::SHIFT)
28//!             .trim_trailing_whitespace(true)
29//!     )
30//!     .build().unwrap();
31//!
32//! // Or provide custom mouse handling
33//! let terminal = Terminal::builder("#canvas")
34//!     .mouse_input_handler(|event, grid| {
35//!         println!("Mouse event at ({}, {})", event.col, event.row);
36//!     })
37//!     .build().unwrap();
38//! ```
39
40use std::{
41    cell::RefCell,
42    fmt::{Debug, Formatter},
43    rc::Rc,
44};
45
46use bitflags::bitflags;
47use wasm_bindgen::{JsCast, closure::Closure};
48
49use crate::{Error, SelectionMode, TerminalGrid, gl::SelectionTracker, select};
50
51/// Type alias for boxed mouse event callback functions.
52///
53/// Callbacks are invoked synchronously in the browser's event loop
54pub type MouseEventCallback = Box<dyn FnMut(TerminalMouseEvent, &TerminalGrid) + 'static>;
55
56/// Internal type for shared event handler wrapped in Rc<RefCell>.
57type EventHandler = Rc<RefCell<dyn FnMut(TerminalMouseEvent, &TerminalGrid) + 'static>>;
58
59/// All mouse event types this handler listens to.
60const MOUSE_EVENTS: &[&str] =
61    &["mousedown", "mouseup", "mousemove", "click", "mouseenter", "mouseleave"];
62
63/// Handles mouse input events for a terminal grid.
64///
65/// Converts browser mouse events into terminal grid coordinates and manages
66/// event handlers for mouse interactions. Maintains terminal dimensions for
67/// accurate coordinate mapping.
68pub struct TerminalMouseHandler {
69    /// The canvas element this handler is attached to.
70    canvas: web_sys::HtmlCanvasElement,
71    /// Unified closure for all mouse events.
72    on_mouse_event: Closure<dyn FnMut(web_sys::MouseEvent)>,
73    /// Optional default selection handler.
74    pub(crate) default_input_handler: Option<DefaultSelectionHandler>,
75}
76
77/// Mouse event data with terminal cell coordinates.
78///
79/// Represents a mouse event translated from pixel coordinates to terminal
80/// grid coordinates, including modifier key states.`col` and `row` are 0-based
81/// terminal grid coordinates
82#[derive(Debug, Clone, Copy)]
83pub struct TerminalMouseEvent {
84    /// Type of mouse event (down, up, or move).
85    pub event_type: MouseEventType,
86    /// Column in the terminal grid (0-based).
87    pub col: u16,
88    /// Row in the terminal grid (0-based).
89    pub row: u16,
90    /// Mouse button pressed (0 = left, 1 = middle, 2 = right).
91    button: i16,
92    /// All modifier that were pressed during the event.
93    modifier_keys: ModifierKeys,
94}
95
96impl TerminalMouseEvent {
97    /// Returns the mouse button pressed during the event.
98    #[must_use]
99    pub fn button(&self) -> i16 {
100        self.button
101    }
102
103    /// Returns whether the Ctrl key was pressed during the event.
104    #[must_use]
105    pub fn ctrl_key(&self) -> bool {
106        self.modifier_keys.contains(ModifierKeys::CONTROL)
107    }
108
109    /// Returns whether the Shift key was pressed during the event.
110    #[must_use]
111    pub fn shift_key(&self) -> bool {
112        self.modifier_keys.contains(ModifierKeys::SHIFT)
113    }
114
115    /// Returns whether the Alt key was pressed during the event.
116    #[must_use]
117    pub fn alt_key(&self) -> bool {
118        self.modifier_keys.contains(ModifierKeys::ALT)
119    }
120
121    /// Returns whether the Meta key was pressed during the event.
122    ///
123    /// This is the Command key on macOS or the Windows key on Windows.
124    #[must_use]
125    pub fn meta_key(&self) -> bool {
126        self.modifier_keys.contains(ModifierKeys::META)
127    }
128
129    /// Returns whether the pressed modifiers exactly match the specified set.
130    ///
131    /// Returns `true` only if the modifier keys pressed during the event
132    /// are exactly equal to `mods`—no more, no less.
133    #[must_use]
134    pub fn has_exact_modifiers(&self, mods: ModifierKeys) -> bool {
135        self.modifier_keys == mods
136    }
137}
138
139/// Types of mouse events that can occur.
140#[derive(Debug, Clone, Copy, PartialEq)]
141#[repr(u8)]
142pub enum MouseEventType {
143    /// Mouse button was pressed.
144    MouseDown = 0,
145    /// Mouse button was released.
146    MouseUp = 1,
147    /// Mouse moved while over the terminal.
148    MouseMove = 2,
149    /// Mouse button was clicked (pressed and released).
150    Click = 3,
151    /// Mouse cursor entered the terminal area.
152    MouseEnter = 4,
153    /// Mouse cursor left the terminal area.
154    MouseLeave = 5,
155}
156
157impl MouseEventType {
158    /// Converts a browser event type string to a MouseEventType.
159    fn from_event_type(event_type: &str) -> Option<Self> {
160        match event_type {
161            "mousedown" => Some(Self::MouseDown),
162            "mouseup" => Some(Self::MouseUp),
163            "mousemove" => Some(Self::MouseMove),
164            "click" => Some(Self::Click),
165            "mouseenter" => Some(Self::MouseEnter),
166            "mouseleave" => Some(Self::MouseLeave),
167            _ => None,
168        }
169    }
170}
171
172/// Configuration options for mouse-based text selection.
173///
174/// Use the builder pattern to configure selection behavior:
175///
176/// ```rust,no_run
177/// use beamterm_renderer::mouse::{MouseSelectOptions, ModifierKeys};
178/// use beamterm_renderer::SelectionMode;
179///
180/// let options = MouseSelectOptions::new()
181///     .selection_mode(SelectionMode::Block)
182///     .require_modifier_keys(ModifierKeys::SHIFT)
183///     .trim_trailing_whitespace(true);
184/// ```
185#[derive(Clone, Debug, Copy, Default)]
186pub struct MouseSelectOptions {
187    selection_mode: SelectionMode,
188    require_modifier_keys: ModifierKeys,
189    trim_trailing_whitespace: bool,
190}
191
192impl MouseSelectOptions {
193    /// Creates a new `MouseSelectOptions` with default settings.
194    ///
195    /// Defaults:
196    /// - Selection mode: `Block`
197    /// - Required modifier keys: none
198    /// - Trim trailing whitespace: `false`
199    #[must_use]
200    pub fn new() -> Self {
201        Self::default()
202    }
203
204    /// Sets the selection mode (Linear or Block).
205    ///
206    /// - `Linear`: Selects text following the natural reading order
207    /// - `Block`: Selects a rectangular region of cells
208    #[must_use]
209    pub fn selection_mode(mut self, mode: SelectionMode) -> Self {
210        self.selection_mode = mode;
211        self
212    }
213
214    /// Sets modifier keys that must be held for selection to activate.
215    ///
216    /// When set, mouse selection only begins if the specified modifier
217    /// keys are pressed during the initial click. Use `ModifierKeys::empty()`
218    /// to allow selection without any modifiers (the default).
219    ///
220    /// # Example
221    /// ```rust,no_run
222    /// use beamterm_renderer::mouse::{MouseSelectOptions, ModifierKeys};
223    ///
224    /// // Require Shift+Click to start selection
225    /// let options = MouseSelectOptions::new()
226    ///     .require_modifier_keys(ModifierKeys::SHIFT);
227    ///
228    /// // Require Ctrl+Shift+Click
229    /// let options = MouseSelectOptions::new()
230    ///     .require_modifier_keys(ModifierKeys::CONTROL | ModifierKeys::SHIFT);
231    /// ```
232    #[must_use]
233    pub fn require_modifier_keys(mut self, require_modifier_keys: ModifierKeys) -> Self {
234        self.require_modifier_keys = require_modifier_keys;
235        self
236    }
237
238    /// Sets whether to trim trailing whitespace from selected text.
239    ///
240    /// When enabled, trailing spaces at the end of each line are removed
241    /// from the copied text.
242    #[must_use]
243    pub fn trim_trailing_whitespace(mut self, trim: bool) -> Self {
244        self.trim_trailing_whitespace = trim;
245        self
246    }
247}
248
249impl TerminalMouseHandler {
250    /// Creates a new mouse handler for the given canvas and terminal grid.
251    ///
252    /// Sets up mouse event listeners on the canvas and converts pixel coordinates
253    /// to terminal cell coordinates before invoking the provided event handler.
254    ///
255    /// # Arguments
256    /// * `canvas` - The HTML canvas element to attach mouse listeners to
257    /// * `grid` - The terminal grid for coordinate calculations
258    /// * `event_handler` - Callback invoked for each mouse event
259    ///
260    /// # Errors
261    /// Returns `Error::Callback` if event listeners cannot be attached to the canvas.
262    ///
263    /// # Example
264    /// ```rust,no_run
265    /// use beamterm_renderer::mouse::TerminalMouseHandler;
266    /// use std::{cell::RefCell, rc::Rc};
267    ///
268    /// let canvas = unimplemented!("canvas");
269    /// let grid: Rc<RefCell<()>> = unimplemented!("TerminalGrid");
270    ///
271    /// // In real code, this would be TerminalGrid
272    /// // let handler = TerminalMouseHandler::new(
273    /// //     &canvas,
274    /// //     grid.clone(),
275    /// //     |event, grid| {
276    /// //         println!("Click at ({}, {})", event.col, event.row);
277    /// //     }
278    /// // ).unwrap();
279    /// ```
280    pub fn new<F>(
281        canvas: &web_sys::HtmlCanvasElement,
282        grid: Rc<RefCell<TerminalGrid>>,
283        event_handler: F,
284    ) -> Result<Self, Error>
285    where
286        F: FnMut(TerminalMouseEvent, &TerminalGrid) + 'static,
287    {
288        Self::new_internal(canvas, grid, Box::new(event_handler))
289    }
290
291    /// Internal constructor that accepts a boxed event handler.
292    ///
293    /// # Implementation Details
294    /// - Wraps handler in Rc<RefCell> for sharing between event closures
295    /// - Caches terminal metrics for fast coordinate conversion
296    /// - Creates three closures (one per event type) that share the handler
297    #[allow(clippy::needless_pass_by_value)] // Rc pass-by-value is idiomatic for shared ownership
298    fn new_internal(
299        canvas: &web_sys::HtmlCanvasElement,
300        grid: Rc<RefCell<TerminalGrid>>,
301        event_handler: MouseEventCallback,
302    ) -> Result<Self, Error> {
303        // Wrap the handler in Rc<RefCell> for sharing between closures
304        let shared_handler = Rc::new(RefCell::new(event_handler));
305
306        // Create pixel-to-cell coordinate converter that reads live grid state.
307        // This ensures correct coordinate mapping even after DPR/zoom changes,
308        let grid_for_coords = grid.clone();
309        let pixel_to_cell = move |event: &web_sys::MouseEvent| -> Option<(u16, u16)> {
310            let g = grid_for_coords.try_borrow().ok()?;
311            let ts = g.terminal_size();
312            let (cell_width, cell_height) = g.css_cell_size();
313
314            let col = (event.offset_x() as f32 / cell_width).floor() as u16;
315            let row = (event.offset_y() as f32 / cell_height).floor() as u16;
316
317            if col < ts.cols && row < ts.rows { Some((col, row)) } else { None }
318        };
319
320        // Create unified event handler
321        let on_mouse_event =
322            create_mouse_event_closure(grid.clone(), shared_handler, pixel_to_cell);
323
324        // Attach event listeners for all mouse event types
325        for event_type in MOUSE_EVENTS {
326            canvas
327                .add_event_listener_with_callback(
328                    event_type,
329                    on_mouse_event.as_ref().unchecked_ref(),
330                )
331                .map_err(|_| Error::Callback(format!("Failed to add {event_type} listener")))?;
332        }
333
334        Ok(Self {
335            canvas: canvas.clone(),
336            on_mouse_event,
337            default_input_handler: None,
338        })
339    }
340
341    /// Removes all event listeners from the canvas.
342    ///
343    /// Called automatically on drop. Safe to call multiple times.
344    pub fn cleanup(&self) {
345        for event_type in MOUSE_EVENTS {
346            let _ = self.canvas.remove_event_listener_with_callback(
347                event_type,
348                self.on_mouse_event.as_ref().unchecked_ref(),
349            );
350        }
351    }
352}
353
354/// Default mouse selection handler with clipboard integration.
355///
356/// Provides text selection functionality with automatic clipboard copying
357/// on selection completion. Supports both linear (text flow) and block
358/// (rectangular) selection modes.
359///
360/// # Features
361/// - Click and drag to select text
362/// - Automatic clipboard copy on mouse release
363/// - Configurable selection modes (Linear/Block)
364/// - Optional trailing whitespace trimming
365pub(crate) struct DefaultSelectionHandler {
366    /// Current selection state machine.
367    selection_state: Rc<RefCell<SelectionState>>,
368    /// Terminal grid reference for text extraction.
369    grid: Rc<RefCell<TerminalGrid>>,
370    /// Selection mode (Linear or Block).
371    options: MouseSelectOptions,
372}
373
374impl DefaultSelectionHandler {
375    /// Creates a new selection handler for the given terminal grid.
376    ///
377    /// # Arguments
378    /// * `grid` - Terminal grid for text extraction
379    /// * `query_mode` - Selection mode (Linear follows text flow, Block is rectangular)
380    /// * `trim_trailing_whitespace` - Whether to remove trailing spaces from selected text
381    pub(crate) fn new(grid: Rc<RefCell<TerminalGrid>>, options: MouseSelectOptions) -> Self {
382        Self {
383            grid,
384            selection_state: Rc::new(RefCell::new(SelectionState::Idle)),
385            options,
386        }
387    }
388
389    /// Creates the mouse event handler closure for this selection handler.
390    ///
391    /// Returns a boxed closure that handles mouse events, tracks selection state,
392    /// and copies selected text to the clipboard on completion.
393    ///
394    /// # Arguments
395    /// * `active_selection` - Selection tracker for visual feedback
396    ///
397    /// # Algorithm
398    /// 1. MouseDown: Begin new selection or replace existing
399    /// 2. MouseMove: Update selection end point if selecting
400    /// 3. MouseUp: Complete selection and copy to clipboard
401    ///
402    /// Repeated single-cell clicks cancel selection rather than selecting one cell.
403    pub fn create_event_handler(&self, active_selection: SelectionTracker) -> MouseEventCallback {
404        let selection_state = self.selection_state.clone();
405        let query_mode = self.options.selection_mode;
406        let trim_trailing = self.options.trim_trailing_whitespace;
407        let require_modifier_keys = self.options.require_modifier_keys;
408
409        Box::new(move |event: TerminalMouseEvent, grid: &TerminalGrid| {
410            let mut state = selection_state.borrow_mut();
411
412            // update mouse selection state based on the event type.
413            match event.event_type {
414                MouseEventType::MouseDown if event.button == 0 => {
415                    // if modifier keys are required, ensure they are pressed
416                    if !event.has_exact_modifiers(require_modifier_keys) {
417                        return;
418                    }
419
420                    // note: if there's an existing selection in progress, it
421                    // means that the cursor left the terminal (canvas) area
422                    // while a previous selection was ongoing. if so, we do
423                    // nothing and await the MouseUp event.
424
425                    // mouse down always begins a new *potential* selection
426                    if state.is_complete() {
427                        // the existing (completed) selection is replaced with
428                        // a new selection which will be canceled if the mouse
429                        // up event is fired on the same cell.
430                        state.maybe_selecting(event.col, event.row);
431                    } else if state.is_idle() {
432                        // begins a new selection from a blank state
433                        state.begin_selection(event.col, event.row);
434                    }
435
436                    let query = select(query_mode)
437                        .start((event.col, event.row))
438                        .trim_trailing_whitespace(trim_trailing);
439
440                    active_selection.set_query(query);
441                },
442                MouseEventType::MouseMove if state.is_selecting() => {
443                    state.update_selection(event.col, event.row);
444                    active_selection.update_selection_end((event.col, event.row));
445                },
446                MouseEventType::MouseUp if event.button == 0 => {
447                    // at this point, we're either at:
448                    // a) the user has finished making the selection
449                    // b) the selection was canceled by a click inside a single cell
450                    if let Some((_start, _end)) = state.complete_selection(event.col, event.row) {
451                        active_selection.update_selection_end((event.col, event.row));
452
453                        // hash the selected content and store it with the query;
454                        // this allows us to clear the selection if the content changes
455                        let query = active_selection.query();
456                        active_selection.set_content_hash(grid.hash_cells(query));
457
458                        let selected_text = grid.get_text(active_selection.query());
459                        crate::js::copy_to_clipboard(selected_text);
460                    } else {
461                        state.clear();
462                        active_selection.clear();
463                    }
464                },
465                _ => {}, // ignore non-left button events
466            }
467        })
468    }
469}
470
471/// Internal state machine for tracking mouse selection operations.
472///
473/// Manages the lifecycle of a selection from initial click through dragging
474/// to final release. Handles edge cases like single-cell clicks that should
475/// cancel rather than select.
476///
477/// # State Transitions
478/// ```text
479///        ┌──────────┐
480///    ┌──▶│   Idle   │
481///    │   └────┬─────┘
482///    │        │ begin_selection
483///    │        ▼
484///    │   ┌──────────┐
485///    │   │Selecting │◀────────────┐
486///    │   └────┬─────┘             │
487///    │        │ complete_selection│
488///    │        ▼                   │
489///    │   ┌──────────┐             │
490///    │   │ Complete │             │
491///    │   └────┬─────┘             │
492///    │        │ maybe_selecting   │
493///    │        ▼                   │
494///    │   ┌──────────────┐         │
495///    └───│MaybeSelecting│─────────┘
496/// mouse  └──────────────┘  update_selection
497///   up       
498///          
499/// ```
500#[derive(Debug, Clone, PartialEq, Eq)]
501enum SelectionState {
502    /// No selection in progress.
503    Idle,
504    /// Active selection with start point and current cursor position.
505    Selecting { start: (u16, u16), current: Option<(u16, u16)> },
506    /// MouseDown in a cell while selection exists.
507    /// Will become Selecting on MouseMove or Idle on MouseUp.
508    MaybeSelecting { start: (u16, u16) },
509    /// Selection completed, contains start and end coordinates.
510    Complete { start: (u16, u16), end: (u16, u16) },
511}
512
513impl SelectionState {
514    /// Begins a new selection from idle state.
515    ///
516    /// # Panics
517    /// Panics if called when not in Idle state (debug only).
518    fn begin_selection(&mut self, col: u16, row: u16) {
519        debug_assert!(matches!(self, SelectionState::Idle));
520        *self = SelectionState::Selecting { start: (col, row), current: None };
521    }
522
523    /// Updates selection end point during drag.
524    ///
525    /// Transitions MaybeSelecting to Selecting if needed.
526    fn update_selection(&mut self, col: u16, row: u16) {
527        use SelectionState::*;
528
529        match self {
530            Selecting { current, .. } => {
531                *current = Some((col, row));
532            },
533            MaybeSelecting { start } => {
534                if (col, row) != *start {
535                    *self = Selecting { start: *start, current: Some((col, row)) };
536                }
537            },
538            _ => {},
539        }
540    }
541
542    /// Completes the selection on mouse release.
543    ///
544    /// # Returns
545    /// - `Some((start, end))` if selection completed
546    /// - `None` if selection was canceled (single cell click)
547    fn complete_selection(&mut self, col: u16, row: u16) -> Option<((u16, u16), (u16, u16))> {
548        match self {
549            SelectionState::Selecting { start, .. } => {
550                let result = Some((*start, (col, row)));
551                *self = SelectionState::Complete { start: *start, end: (col, row) };
552                result
553            },
554            _ => None,
555        }
556    }
557
558    /// Clears the selection state back to idle.
559    fn clear(&mut self) {
560        *self = SelectionState::Idle;
561    }
562
563    /// Checks if currently in selecting state.
564    fn is_selecting(&self) -> bool {
565        matches!(
566            self,
567            SelectionState::Selecting { .. } | SelectionState::MaybeSelecting { .. }
568        )
569    }
570
571    fn is_idle(&self) -> bool {
572        matches!(self, SelectionState::Idle)
573    }
574
575    /// Begins potential new selection while one exists.
576    ///
577    /// Used when clicking while a selection is complete.
578    fn maybe_selecting(&mut self, col: u16, row: u16) {
579        *self = SelectionState::MaybeSelecting { start: (col, row) };
580    }
581
582    /// Checks if a selection has been completed.
583    fn is_complete(&self) -> bool {
584        matches!(self, SelectionState::Complete { .. })
585    }
586}
587
588/// Creates a closure that handles browser mouse events and converts them to terminal events.
589///
590/// Wraps the event handler with coordinate conversion and terminal event creation logic.
591/// Handles all mouse event types (mousedown, mouseup, mousemove, click, mouseenter, mouseleave).
592fn create_mouse_event_closure(
593    grid: Rc<RefCell<TerminalGrid>>,
594    event_handler: EventHandler,
595    pixel_to_cell: impl Fn(&web_sys::MouseEvent) -> Option<(u16, u16)> + 'static,
596) -> Closure<dyn FnMut(web_sys::MouseEvent)> {
597    Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
598        // Determine event type from browser event
599        let Some(event_type) = MouseEventType::from_event_type(&event.type_()) else {
600            return;
601        };
602
603        // For enter/exit events, we don't need valid cell coordinates
604        let (col, row) = match event_type {
605            MouseEventType::MouseEnter | MouseEventType::MouseLeave => {
606                // Use (0, 0) for enter/leave events as they may occur outside the grid
607                (0, 0)
608            },
609            _ => {
610                // For other events, require valid cell coordinates
611                match pixel_to_cell(&event) {
612                    Some(coords) => coords,
613                    None => return,
614                }
615            },
616        };
617
618        let modifiers = {
619            let mut mods = ModifierKeys::empty();
620            if event.ctrl_key() {
621                mods |= ModifierKeys::CONTROL;
622            }
623            if event.shift_key() {
624                mods |= ModifierKeys::SHIFT;
625            }
626            if event.alt_key() {
627                mods |= ModifierKeys::ALT;
628            }
629            if event.meta_key() {
630                mods |= ModifierKeys::META;
631            }
632            mods
633        };
634
635        let terminal_event = TerminalMouseEvent {
636            event_type,
637            col,
638            row,
639            button: event.button(),
640            modifier_keys: modifiers,
641        };
642        let grid_ref = grid.borrow();
643        event_handler.borrow_mut()(terminal_event, &grid_ref);
644    }) as Box<dyn FnMut(_)>)
645}
646
647impl Drop for TerminalMouseHandler {
648    /// Automatically removes event listeners when handler is dropped.
649    fn drop(&mut self) {
650        self.cleanup();
651    }
652}
653
654impl Debug for TerminalMouseHandler {
655    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
656        f.debug_struct("TerminalMouseHandler").finish()
657    }
658}
659
660impl Debug for DefaultSelectionHandler {
661    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
662        let ts = self.grid.borrow().terminal_size();
663        write!(
664            f,
665            "DefaultSelectionHandler {{ options: {:?}, grid: {}x{} }}",
666            self.options, ts.cols, ts.rows
667        )
668    }
669}
670
671bitflags! {
672    /// Keyboard modifier keys that can be held during mouse events.
673    ///
674    /// Flags can be combined using bitwise OR:
675    ///
676    /// ```rust,no_run
677    /// use beamterm_renderer::mouse::ModifierKeys;
678    ///
679    /// let ctrl_shift = ModifierKeys::CONTROL | ModifierKeys::SHIFT;
680    /// ```
681    #[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
682    pub struct ModifierKeys : u8 {
683        /// The Control key (Ctrl on Windows/Linux).
684        const CONTROL = 0b0000_0001;
685        /// The Shift key.
686        const SHIFT   = 0b0000_0010;
687        /// The Alt key (Option on macOS).
688        const ALT     = 0b0000_0100;
689        /// The Meta key (Command on macOS, Windows key on Windows).
690        const META    = 0b0000_1000;
691    }
692}