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