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}