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