1use std::{
2 cell::RefCell,
3 fmt::{Debug, Formatter},
4 rc::Rc,
5};
6
7use compact_str::CompactString;
8use wasm_bindgen::{closure::Closure, JsCast};
9use wasm_bindgen_futures::spawn_local;
10use web_sys::console;
11
12use crate::{
13 gl::{SelectionTracker, TerminalDimensions},
14 select, Error, SelectionMode, TerminalGrid,
15};
16
17pub(super) type MouseEventCallback = Box<dyn FnMut(TerminalMouseEvent, &TerminalGrid) + 'static>;
18type EventHandler = Rc<RefCell<dyn FnMut(TerminalMouseEvent, &TerminalGrid) + 'static>>;
19
20pub struct TerminalMouseHandler {
26 canvas: web_sys::HtmlCanvasElement,
27 on_mouse_down: Closure<dyn FnMut(web_sys::MouseEvent)>,
28 on_mouse_up: Closure<dyn FnMut(web_sys::MouseEvent)>,
29 on_mouse_move: Closure<dyn FnMut(web_sys::MouseEvent)>,
30 terminal_dimensions: crate::gl::TerminalDimensions,
31 pub(crate) default_input_handler: Option<DefaultSelectionHandler>,
32}
33
34#[derive(Debug, Clone, Copy)]
39pub struct TerminalMouseEvent {
40 pub event_type: MouseEventType,
42 pub col: u16,
44 pub row: u16,
46 pub button: i16,
48 pub ctrl_key: bool,
50 pub shift_key: bool,
52 pub alt_key: bool,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq)]
58pub enum MouseEventType {
59 MouseDown,
61 MouseUp,
63 MouseMove,
65}
66
67impl TerminalMouseHandler {
68 pub(crate) fn new<F>(
81 canvas: &web_sys::HtmlCanvasElement,
82 grid: Rc<RefCell<TerminalGrid>>,
83 event_handler: F,
84 ) -> Result<Self, Error>
85 where
86 F: FnMut(TerminalMouseEvent, &TerminalGrid) + 'static,
87 {
88 Self::new_internal(canvas, grid, Box::new(event_handler))
89 }
90
91 fn new_internal(
92 canvas: &web_sys::HtmlCanvasElement,
93 grid: Rc<RefCell<TerminalGrid>>,
94 event_handler: MouseEventCallback,
95 ) -> Result<Self, Error> {
96 let shared_handler = Rc::new(RefCell::new(event_handler));
98
99 let (cell_width, cell_height) = grid.borrow().cell_size();
101 let (cols, rows) = grid.borrow().terminal_size();
102 let terminal_dimensions = TerminalDimensions::new(cols, rows);
103
104 let dimensions_ref = terminal_dimensions.clone_ref();
106 let pixel_to_cell = move |event: &web_sys::MouseEvent| -> Option<(u16, u16)> {
107 let x = event.offset_x() as f32;
108 let y = event.offset_y() as f32;
109
110 let col = (x / cell_width as f32).floor() as u16;
111 let row = (y / cell_height as f32).floor() as u16;
112
113 let (max_cols, max_rows) = *dimensions_ref.borrow();
114 if col < max_cols && row < max_rows {
115 Some((col, row))
116 } else {
117 None
118 }
119 };
120
121 use MouseEventType::*;
123 let on_mouse_down = create_mouse_event_closure(
124 MouseDown,
125 grid.clone(),
126 shared_handler.clone(),
127 pixel_to_cell.clone(),
128 );
129 let on_mouse_up = create_mouse_event_closure(
130 MouseUp,
131 grid.clone(),
132 shared_handler.clone(),
133 pixel_to_cell.clone(),
134 );
135 let on_mouse_move =
136 create_mouse_event_closure(MouseMove, grid.clone(), shared_handler, pixel_to_cell);
137
138 canvas
140 .add_event_listener_with_callback("mousedown", on_mouse_down.as_ref().unchecked_ref())
141 .map_err(|_| Error::Callback("Failed to add mousedown listener".into()))?;
142 canvas
143 .add_event_listener_with_callback("mouseup", on_mouse_up.as_ref().unchecked_ref())
144 .map_err(|_| Error::Callback("Failed to add mouseup listener".into()))?;
145 canvas
146 .add_event_listener_with_callback("mousemove", on_mouse_move.as_ref().unchecked_ref())
147 .map_err(|_| Error::Callback("Failed to add mousemove listener".into()))?;
148
149 Ok(Self {
150 canvas: canvas.clone(),
151 on_mouse_down,
152 on_mouse_up,
153 on_mouse_move,
154 terminal_dimensions,
155 default_input_handler: None,
156 })
157 }
158
159 pub(crate) fn update_dimensions(&self, cols: u16, rows: u16) {
164 self.terminal_dimensions.set(cols, rows);
165 }
166
167 pub(crate) fn cleanup(&self) {
172 let _ = self.canvas.remove_event_listener_with_callback(
173 "mousedown",
174 self.on_mouse_down.as_ref().unchecked_ref(),
175 );
176 let _ = self.canvas.remove_event_listener_with_callback(
177 "mouseup",
178 self.on_mouse_up.as_ref().unchecked_ref(),
179 );
180 let _ = self.canvas.remove_event_listener_with_callback(
181 "mousemove",
182 self.on_mouse_move.as_ref().unchecked_ref(),
183 );
184 }
185}
186
187pub(crate) struct DefaultSelectionHandler {
193 selection_state: Rc<RefCell<SelectionState>>,
194 grid: Rc<RefCell<TerminalGrid>>,
195 query_mode: SelectionMode,
196 trim_trailing_whitespace: bool,
197}
198
199impl DefaultSelectionHandler {
200 pub(crate) fn new(
207 grid: Rc<RefCell<TerminalGrid>>,
208 query_mode: SelectionMode,
209 trim_trailing_whitespace: bool,
210 ) -> Self {
211 Self {
212 selection_state: Rc::new(RefCell::new(SelectionState::new())),
213 grid,
214 query_mode,
215 trim_trailing_whitespace,
216 }
217 }
218
219 pub fn create_event_handler(&self, active_selection: SelectionTracker) -> MouseEventCallback {
224 let selection_state = self.selection_state.clone();
225 let query_mode = self.query_mode;
226 let trim_trailing = self.trim_trailing_whitespace;
227
228 Box::new(move |event: TerminalMouseEvent, grid: &TerminalGrid| {
229 let mut state = selection_state.borrow_mut();
230
231 match event.event_type {
232 MouseEventType::MouseDown if event.button == 0 => {
234 if state.is_complete() {
236 state.maybe_selecting(event.col, event.row);
240 } else {
241 state.begin_selection(event.col, event.row);
243 }
244
245 let query = select(query_mode)
246 .start((event.col, event.row))
247 .trim_trailing_whitespace(trim_trailing);
248
249 active_selection.set_query(query);
250 },
251 MouseEventType::MouseMove if state.is_selecting() => {
252 state.update_selection(event.col, event.row);
253 active_selection.update_selection_end((event.col, event.row));
254 },
255 MouseEventType::MouseUp if event.button == 0 => {
256 if let Some((_start, _end)) = state.complete_selection(event.col, event.row) {
260 active_selection.update_selection_end((event.col, event.row));
261 let selected_text = grid.get_text(active_selection.query());
262 copy_to_clipboard(selected_text);
263 } else {
264 state.clear();
265 active_selection.clear();
266 }
267 },
268 _ => {}, }
270 })
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq)]
280enum SelectionState {
281 Idle,
283 Selecting {
285 start: (u16, u16),
286 current: Option<(u16, u16)>,
287 },
288 MaybeSelecting { start: (u16, u16) },
290 Complete { start: (u16, u16), end: (u16, u16) },
292}
293
294impl SelectionState {
295 fn new() -> Self {
297 SelectionState::Idle
298 }
299
300 fn begin_selection(&mut self, col: u16, row: u16) {
302 *self = SelectionState::Selecting { start: (col, row), current: None };
303 }
304
305 fn update_selection(&mut self, col: u16, row: u16) {
307 use SelectionState::*;
308
309 match self {
310 Selecting { current, .. } => {
311 *current = Some((col, row));
312 },
313 MaybeSelecting { start } => {
314 if (col, row) != *start {
315 *self = Selecting { start: *start, current: Some((col, row)) };
316 }
317 },
318 _ => {},
319 }
320 }
321
322 fn is_selecting(&self) -> bool {
324 use SelectionState::*;
325 matches!(self, Selecting { .. } | MaybeSelecting { .. })
326 }
327
328 fn complete_selection(&mut self, col: u16, row: u16) -> Option<((u16, u16), (u16, u16))> {
332 match self {
333 SelectionState::Selecting { start, .. } => {
334 let result = Some((*start, (col, row)));
335 *self = SelectionState::Complete { start: *start, end: (col, row) };
336 result
337 },
338 _ => None,
339 }
340 }
341
342 fn clear(&mut self) {
344 *self = SelectionState::Idle;
345 }
346
347 fn maybe_selecting(&mut self, col: u16, row: u16) {
349 *self = SelectionState::MaybeSelecting { start: (col, row) };
350 }
351
352 fn is_complete(&self) -> bool {
354 matches!(self, SelectionState::Complete { .. })
355 }
356}
357
358fn create_mouse_event_closure(
362 event_type: MouseEventType,
363 grid: Rc<RefCell<TerminalGrid>>,
364 event_handler: EventHandler,
365 pixel_to_cell: impl Fn(&web_sys::MouseEvent) -> Option<(u16, u16)> + 'static,
366) -> Closure<dyn FnMut(web_sys::MouseEvent)> {
367 Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
368 if let Some((col, row)) = pixel_to_cell(&event) {
369 let terminal_event = TerminalMouseEvent {
370 event_type,
371 col,
372 row,
373 button: event.button(),
374 ctrl_key: event.ctrl_key(),
375 shift_key: event.shift_key(),
376 alt_key: event.alt_key(),
377 };
378 let grid_ref = grid.borrow();
379 event_handler.borrow_mut()(terminal_event, &grid_ref);
380 }
381 }) as Box<dyn FnMut(_)>)
382}
383
384fn copy_to_clipboard(text: CompactString) {
389 console::log_1(&format!("Copying {} characters to clipboard", text.len()).into());
390
391 spawn_local(async move {
392 if let Some(window) = web_sys::window() {
393 let clipboard = window.navigator().clipboard();
394 match wasm_bindgen_futures::JsFuture::from(clipboard.write_text(&text)).await {
395 Ok(_) => {
396 console::log_1(
397 &format!("Successfully copied {} characters", text.chars().count()).into(),
398 );
399 },
400 Err(err) => {
401 console::error_1(&format!("Failed to copy to clipboard: {err:?}").into());
402 },
403 }
404 }
405 });
406}
407
408impl Drop for TerminalMouseHandler {
409 fn drop(&mut self) {
410 self.cleanup();
411 }
412}
413
414impl Debug for TerminalMouseHandler {
415 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416 let (cols, rows) = self.terminal_dimensions.get();
417 write!(f, "TerminalMouseHandler {{ dimensions: {cols}x{rows} }}")
418 }
419}
420
421impl Debug for DefaultSelectionHandler {
422 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
423 let (cols, rows) = self.grid.borrow().terminal_size();
424 write!(
425 f,
426 "DefaultSelectionHandler {{ mode: {:?}, trim_whitespace: {}, grid: {}x{} }}",
427 self.query_mode, self.trim_trailing_whitespace, cols, rows
428 )
429 }
430}