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}