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