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