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