use std::{
cell::RefCell,
fmt::{Debug, Formatter},
rc::Rc,
};
use bitflags::bitflags;
use wasm_bindgen::{JsCast, closure::Closure};
use crate::{Error, SelectionMode, TerminalGrid, gl::SelectionTracker, select};
pub type MouseEventCallback = Box<dyn FnMut(TerminalMouseEvent, &TerminalGrid) + 'static>;
type EventHandler = Rc<RefCell<dyn FnMut(TerminalMouseEvent, &TerminalGrid) + 'static>>;
const MOUSE_EVENTS: &[&str] =
&["mousedown", "mouseup", "mousemove", "click", "mouseenter", "mouseleave"];
pub struct TerminalMouseHandler {
canvas: web_sys::HtmlCanvasElement,
on_mouse_event: Closure<dyn FnMut(web_sys::MouseEvent)>,
pub(crate) default_input_handler: Option<DefaultSelectionHandler>,
}
#[derive(Debug, Clone, Copy)]
pub struct TerminalMouseEvent {
pub event_type: MouseEventType,
pub col: u16,
pub row: u16,
button: i16,
modifier_keys: ModifierKeys,
}
impl TerminalMouseEvent {
pub fn button(&self) -> i16 {
self.button
}
pub fn ctrl_key(&self) -> bool {
self.modifier_keys.contains(ModifierKeys::CONTROL)
}
pub fn shift_key(&self) -> bool {
self.modifier_keys.contains(ModifierKeys::SHIFT)
}
pub fn alt_key(&self) -> bool {
self.modifier_keys.contains(ModifierKeys::ALT)
}
pub fn meta_key(&self) -> bool {
self.modifier_keys.contains(ModifierKeys::META)
}
pub fn has_exact_modifiers(&self, mods: ModifierKeys) -> bool {
self.modifier_keys == mods
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum MouseEventType {
MouseDown = 0,
MouseUp = 1,
MouseMove = 2,
Click = 3,
MouseEnter = 4,
MouseLeave = 5,
}
impl MouseEventType {
fn from_event_type(event_type: &str) -> Option<Self> {
match event_type {
"mousedown" => Some(Self::MouseDown),
"mouseup" => Some(Self::MouseUp),
"mousemove" => Some(Self::MouseMove),
"click" => Some(Self::Click),
"mouseenter" => Some(Self::MouseEnter),
"mouseleave" => Some(Self::MouseLeave),
_ => None,
}
}
}
#[derive(Clone, Debug, Copy, Default)]
pub struct MouseSelectOptions {
selection_mode: SelectionMode,
require_modifier_keys: ModifierKeys,
trim_trailing_whitespace: bool,
}
impl MouseSelectOptions {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn selection_mode(mut self, mode: SelectionMode) -> Self {
self.selection_mode = mode;
self
}
#[must_use]
pub fn require_modifier_keys(mut self, require_modifier_keys: ModifierKeys) -> Self {
self.require_modifier_keys = require_modifier_keys;
self
}
#[must_use]
pub fn trim_trailing_whitespace(mut self, trim: bool) -> Self {
self.trim_trailing_whitespace = trim;
self
}
}
impl TerminalMouseHandler {
pub fn new<F>(
canvas: &web_sys::HtmlCanvasElement,
grid: Rc<RefCell<TerminalGrid>>,
event_handler: F,
) -> Result<Self, Error>
where
F: FnMut(TerminalMouseEvent, &TerminalGrid) + 'static,
{
Self::new_internal(canvas, grid, Box::new(event_handler))
}
fn new_internal(
canvas: &web_sys::HtmlCanvasElement,
grid: Rc<RefCell<TerminalGrid>>,
event_handler: MouseEventCallback,
) -> Result<Self, Error> {
let shared_handler = Rc::new(RefCell::new(event_handler));
let grid_for_coords = grid.clone();
let pixel_to_cell = move |event: &web_sys::MouseEvent| -> Option<(u16, u16)> {
let g = grid_for_coords.try_borrow().ok()?;
let ts = g.terminal_size();
let (cell_width, cell_height) = g.css_cell_size();
let col = (event.offset_x() as f32 / cell_width).floor() as u16;
let row = (event.offset_y() as f32 / cell_height).floor() as u16;
if col < ts.cols && row < ts.rows { Some((col, row)) } else { None }
};
let on_mouse_event =
create_mouse_event_closure(grid.clone(), shared_handler, pixel_to_cell);
for event_type in MOUSE_EVENTS {
canvas
.add_event_listener_with_callback(
event_type,
on_mouse_event.as_ref().unchecked_ref(),
)
.map_err(|_| Error::Callback(format!("Failed to add {event_type} listener")))?;
}
Ok(Self {
canvas: canvas.clone(),
on_mouse_event,
default_input_handler: None,
})
}
pub fn cleanup(&self) {
for event_type in MOUSE_EVENTS {
let _ = self.canvas.remove_event_listener_with_callback(
event_type,
self.on_mouse_event.as_ref().unchecked_ref(),
);
}
}
}
pub(crate) struct DefaultSelectionHandler {
selection_state: Rc<RefCell<SelectionState>>,
grid: Rc<RefCell<TerminalGrid>>,
options: MouseSelectOptions,
}
impl DefaultSelectionHandler {
pub(crate) fn new(grid: Rc<RefCell<TerminalGrid>>, options: MouseSelectOptions) -> Self {
Self {
grid,
selection_state: Rc::new(RefCell::new(SelectionState::Idle)),
options,
}
}
pub fn create_event_handler(&self, active_selection: SelectionTracker) -> MouseEventCallback {
let selection_state = self.selection_state.clone();
let query_mode = self.options.selection_mode;
let trim_trailing = self.options.trim_trailing_whitespace;
let require_modifier_keys = self.options.require_modifier_keys;
Box::new(move |event: TerminalMouseEvent, grid: &TerminalGrid| {
let mut state = selection_state.borrow_mut();
match event.event_type {
MouseEventType::MouseDown if event.button == 0 => {
if !event.has_exact_modifiers(require_modifier_keys) {
return;
}
if state.is_complete() {
state.maybe_selecting(event.col, event.row);
} else if state.is_idle() {
state.begin_selection(event.col, event.row);
}
let query = select(query_mode)
.start((event.col, event.row))
.trim_trailing_whitespace(trim_trailing);
active_selection.set_query(query);
},
MouseEventType::MouseMove if state.is_selecting() => {
state.update_selection(event.col, event.row);
active_selection.update_selection_end((event.col, event.row));
},
MouseEventType::MouseUp if event.button == 0 => {
if let Some((_start, _end)) = state.complete_selection(event.col, event.row) {
active_selection.update_selection_end((event.col, event.row));
let query = active_selection.query();
active_selection.set_content_hash(grid.hash_cells(query));
let selected_text = grid.get_text(active_selection.query());
crate::js::copy_to_clipboard(selected_text);
} else {
state.clear();
active_selection.clear();
}
},
_ => {}, }
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum SelectionState {
Idle,
Selecting { start: (u16, u16), current: Option<(u16, u16)> },
MaybeSelecting { start: (u16, u16) },
Complete { start: (u16, u16), end: (u16, u16) },
}
impl SelectionState {
fn begin_selection(&mut self, col: u16, row: u16) {
debug_assert!(matches!(self, SelectionState::Idle));
*self = SelectionState::Selecting { start: (col, row), current: None };
}
fn update_selection(&mut self, col: u16, row: u16) {
use SelectionState::*;
match self {
Selecting { current, .. } => {
*current = Some((col, row));
},
MaybeSelecting { start } => {
if (col, row) != *start {
*self = Selecting { start: *start, current: Some((col, row)) };
}
},
_ => {},
}
}
fn complete_selection(&mut self, col: u16, row: u16) -> Option<((u16, u16), (u16, u16))> {
match self {
SelectionState::Selecting { start, .. } => {
let result = Some((*start, (col, row)));
*self = SelectionState::Complete { start: *start, end: (col, row) };
result
},
_ => None,
}
}
fn clear(&mut self) {
*self = SelectionState::Idle;
}
fn is_selecting(&self) -> bool {
matches!(
self,
SelectionState::Selecting { .. } | SelectionState::MaybeSelecting { .. }
)
}
fn is_idle(&self) -> bool {
matches!(self, SelectionState::Idle)
}
fn maybe_selecting(&mut self, col: u16, row: u16) {
*self = SelectionState::MaybeSelecting { start: (col, row) };
}
fn is_complete(&self) -> bool {
matches!(self, SelectionState::Complete { .. })
}
}
fn create_mouse_event_closure(
grid: Rc<RefCell<TerminalGrid>>,
event_handler: EventHandler,
pixel_to_cell: impl Fn(&web_sys::MouseEvent) -> Option<(u16, u16)> + 'static,
) -> Closure<dyn FnMut(web_sys::MouseEvent)> {
Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
let Some(event_type) = MouseEventType::from_event_type(&event.type_()) else {
return;
};
let (col, row) = match event_type {
MouseEventType::MouseEnter | MouseEventType::MouseLeave => {
(0, 0)
},
_ => {
match pixel_to_cell(&event) {
Some(coords) => coords,
None => return,
}
},
};
let modifiers = {
let mut mods = ModifierKeys::empty();
if event.ctrl_key() {
mods |= ModifierKeys::CONTROL;
}
if event.shift_key() {
mods |= ModifierKeys::SHIFT;
}
if event.alt_key() {
mods |= ModifierKeys::ALT;
}
if event.meta_key() {
mods |= ModifierKeys::META;
}
mods
};
let terminal_event = TerminalMouseEvent {
event_type,
col,
row,
button: event.button(),
modifier_keys: modifiers,
};
let grid_ref = grid.borrow();
event_handler.borrow_mut()(terminal_event, &grid_ref);
}) as Box<dyn FnMut(_)>)
}
impl Drop for TerminalMouseHandler {
fn drop(&mut self) {
self.cleanup();
}
}
impl Debug for TerminalMouseHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TerminalMouseHandler").finish()
}
}
impl Debug for DefaultSelectionHandler {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let ts = self.grid.borrow().terminal_size();
write!(
f,
"DefaultSelectionHandler {{ options: {:?}, grid: {}x{} }}",
self.options, ts.cols, ts.rows
)
}
}
bitflags! {
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
pub struct ModifierKeys : u8 {
const CONTROL = 0b0000_0001;
const SHIFT = 0b0000_0010;
const ALT = 0b0000_0100;
const META = 0b0000_1000;
}
}