1use std::rc::Rc;
2
3use gpui::{
4 Capslock, DispatchEventResult, ExternalPaths, FileDropEvent, KeyDownEvent, KeyUpEvent,
5 Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent,
6 MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformInput, Point, ScrollDelta,
7 ScrollWheelEvent, TouchPhase, point, px,
8};
9use smallvec::smallvec;
10use wasm_bindgen::prelude::*;
11
12use crate::window::WebWindowInner;
13
14pub struct WebEventListeners {
15 #[allow(dead_code)]
16 closures: Vec<Closure<dyn FnMut(JsValue)>>,
17}
18
19pub(crate) struct ClickState {
20 last_position: Point<Pixels>,
21 last_time: f64,
22 current_count: usize,
23}
24
25impl Default for ClickState {
26 fn default() -> Self {
27 Self {
28 last_position: Point::default(),
29 last_time: 0.0,
30 current_count: 0,
31 }
32 }
33}
34
35impl ClickState {
36 fn register_click(&mut self, position: Point<Pixels>, time: f64) -> usize {
37 let distance = ((f32::from(position.x) - f32::from(self.last_position.x)).powi(2)
38 + (f32::from(position.y) - f32::from(self.last_position.y)).powi(2))
39 .sqrt();
40
41 if (time - self.last_time) < 400.0 && distance < 5.0 {
42 self.current_count += 1;
43 } else {
44 self.current_count = 1;
45 }
46
47 self.last_position = position;
48 self.last_time = time;
49 self.current_count
50 }
51}
52
53impl WebWindowInner {
54 pub fn register_event_listeners(self: &Rc<Self>) -> WebEventListeners {
55 let mut closures = vec![
56 self.register_pointer_down(),
57 self.register_pointer_up(),
58 self.register_pointer_move(),
59 self.register_pointer_leave(),
60 self.register_wheel(),
61 self.register_context_menu(),
62 self.register_dragover(),
63 self.register_drop(),
64 self.register_dragleave(),
65 self.register_key_down(),
66 self.register_key_up(),
67 self.register_composition_start(),
68 self.register_composition_update(),
69 self.register_composition_end(),
70 self.register_focus(),
71 self.register_blur(),
72 self.register_pointer_enter(),
73 self.register_pointer_leave_hover(),
74 ];
75 closures.extend(self.register_visibility_change());
76 closures.extend(self.register_appearance_change());
77
78 WebEventListeners { closures }
79 }
80
81 fn listen(
82 self: &Rc<Self>,
83 event_name: &str,
84 handler: impl FnMut(JsValue) + 'static,
85 ) -> Closure<dyn FnMut(JsValue)> {
86 let closure = Closure::<dyn FnMut(JsValue)>::new(handler);
87 self.canvas
88 .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())
89 .ok();
90 closure
91 }
92
93 fn listen_input(
94 self: &Rc<Self>,
95 event_name: &str,
96 handler: impl FnMut(JsValue) + 'static,
97 ) -> Closure<dyn FnMut(JsValue)> {
98 let closure = Closure::<dyn FnMut(JsValue)>::new(handler);
99 self.input_element
100 .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())
101 .ok();
102 closure
103 }
104
105 /// Registers a listener with `{passive: false}` so that `preventDefault()` works.
106 /// Needed for events like `wheel` which are passive by default in modern browsers.
107 fn listen_non_passive(
108 self: &Rc<Self>,
109 event_name: &str,
110 handler: impl FnMut(JsValue) + 'static,
111 ) -> Closure<dyn FnMut(JsValue)> {
112 let closure = Closure::<dyn FnMut(JsValue)>::new(handler);
113 let canvas_js: &JsValue = self.canvas.as_ref();
114 let callback_js: &JsValue = closure.as_ref();
115 let options = js_sys::Object::new();
116 js_sys::Reflect::set(&options, &"passive".into(), &false.into()).ok();
117 if let Ok(add_fn_val) = js_sys::Reflect::get(canvas_js, &"addEventListener".into()) {
118 if let Ok(add_fn) = add_fn_val.dyn_into::<js_sys::Function>() {
119 add_fn
120 .call3(canvas_js, &event_name.into(), callback_js, &options)
121 .ok();
122 }
123 }
124 closure
125 }
126
127 fn dispatch_input(&self, input: PlatformInput) -> Option<DispatchEventResult> {
128 let mut borrowed = self.callbacks.borrow_mut();
129 borrowed.input.as_mut().map(|callback| callback(input))
130 }
131
132 fn register_pointer_down(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
133 let this = Rc::clone(self);
134 self.listen("pointerdown", move |event: JsValue| {
135 let event: web_sys::PointerEvent = event.unchecked_into();
136 event.prevent_default();
137 this.input_element.focus().ok();
138
139 let button = dom_mouse_button_to_gpui(event.button());
140 let position = pointer_position_in_element(&event);
141 let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
142 let time = js_sys::Date::now();
143
144 this.pressed_button.set(Some(button));
145 let click_count = this.click_state.borrow_mut().register_click(position, time);
146
147 {
148 let mut current_state = this.state.borrow_mut();
149 current_state.mouse_position = position;
150 current_state.modifiers = modifiers;
151 }
152
153 this.dispatch_input(PlatformInput::MouseDown(MouseDownEvent {
154 button,
155 position,
156 modifiers,
157 click_count,
158 first_mouse: false,
159 }));
160 })
161 }
162
163 fn register_pointer_up(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
164 let this = Rc::clone(self);
165 self.listen("pointerup", move |event: JsValue| {
166 let event: web_sys::PointerEvent = event.unchecked_into();
167 event.prevent_default();
168
169 let button = dom_mouse_button_to_gpui(event.button());
170 let position = pointer_position_in_element(&event);
171 let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
172
173 this.pressed_button.set(None);
174 let click_count = this.click_state.borrow().current_count;
175
176 {
177 let mut current_state = this.state.borrow_mut();
178 current_state.mouse_position = position;
179 current_state.modifiers = modifiers;
180 }
181
182 this.dispatch_input(PlatformInput::MouseUp(MouseUpEvent {
183 button,
184 position,
185 modifiers,
186 click_count,
187 }));
188 })
189 }
190
191 fn register_pointer_move(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
192 let this = Rc::clone(self);
193 self.listen("pointermove", move |event: JsValue| {
194 let event: web_sys::PointerEvent = event.unchecked_into();
195 event.prevent_default();
196
197 let position = pointer_position_in_element(&event);
198 let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
199 let current_pressed = this.pressed_button.get();
200
201 {
202 let mut current_state = this.state.borrow_mut();
203 current_state.mouse_position = position;
204 current_state.modifiers = modifiers;
205 }
206
207 this.dispatch_input(PlatformInput::MouseMove(MouseMoveEvent {
208 position,
209 pressed_button: current_pressed,
210 modifiers,
211 }));
212 })
213 }
214
215 fn register_pointer_leave(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
216 let this = Rc::clone(self);
217 self.listen("pointerleave", move |event: JsValue| {
218 let event: web_sys::PointerEvent = event.unchecked_into();
219
220 let position = pointer_position_in_element(&event);
221 let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
222 let current_pressed = this.pressed_button.get();
223
224 {
225 let mut current_state = this.state.borrow_mut();
226 current_state.mouse_position = position;
227 current_state.modifiers = modifiers;
228 }
229
230 this.dispatch_input(PlatformInput::MouseExited(MouseExitEvent {
231 position,
232 pressed_button: current_pressed,
233 modifiers,
234 }));
235 })
236 }
237
238 fn register_wheel(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
239 let this = Rc::clone(self);
240 self.listen_non_passive("wheel", move |event: JsValue| {
241 let event: web_sys::WheelEvent = event.unchecked_into();
242 event.prevent_default();
243
244 let mouse_event: &web_sys::MouseEvent = event.as_ref();
245 let position = mouse_position_in_element(mouse_event);
246 let modifiers = modifiers_from_wheel_event(mouse_event, this.is_mac);
247
248 let delta_mode = event.delta_mode();
249 let delta = if delta_mode == 1 {
250 ScrollDelta::Lines(point(-event.delta_x() as f32, -event.delta_y() as f32))
251 } else {
252 ScrollDelta::Pixels(point(
253 px(-event.delta_x() as f32),
254 px(-event.delta_y() as f32),
255 ))
256 };
257
258 {
259 let mut current_state = this.state.borrow_mut();
260 current_state.modifiers = modifiers;
261 }
262
263 this.dispatch_input(PlatformInput::ScrollWheel(ScrollWheelEvent {
264 position,
265 delta,
266 modifiers,
267 touch_phase: TouchPhase::Moved,
268 }));
269 })
270 }
271
272 fn register_context_menu(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
273 self.listen("contextmenu", move |event: JsValue| {
274 let event: web_sys::Event = event.unchecked_into();
275 event.prevent_default();
276 })
277 }
278
279 fn register_dragover(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
280 let this = Rc::clone(self);
281 self.listen("dragover", move |event: JsValue| {
282 let event: web_sys::DragEvent = event.unchecked_into();
283 event.prevent_default();
284
285 let mouse_event: &web_sys::MouseEvent = event.as_ref();
286 let position = mouse_position_in_element(mouse_event);
287
288 {
289 let mut current_state = this.state.borrow_mut();
290 current_state.mouse_position = position;
291 }
292
293 this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Pending { position }));
294 })
295 }
296
297 fn register_drop(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
298 let this = Rc::clone(self);
299 self.listen("drop", move |event: JsValue| {
300 let event: web_sys::DragEvent = event.unchecked_into();
301 event.prevent_default();
302
303 let mouse_event: &web_sys::MouseEvent = event.as_ref();
304 let position = mouse_position_in_element(mouse_event);
305
306 {
307 let mut current_state = this.state.borrow_mut();
308 current_state.mouse_position = position;
309 }
310
311 let paths = extract_file_paths_from_drag(&event);
312
313 this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Entered {
314 position,
315 paths: ExternalPaths(paths),
316 }));
317
318 this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Submit { position }));
319 })
320 }
321
322 fn register_dragleave(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
323 let this = Rc::clone(self);
324 self.listen("dragleave", move |_event: JsValue| {
325 this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Exited));
326 })
327 }
328
329 fn register_key_down(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
330 let this = Rc::clone(self);
331 self.listen_input("keydown", move |event: JsValue| {
332 let event: web_sys::KeyboardEvent = event.unchecked_into();
333
334 let modifiers = modifiers_from_keyboard_event(&event, this.is_mac);
335 let capslock = capslock_from_keyboard_event(&event);
336
337 {
338 let mut current_state = this.state.borrow_mut();
339 current_state.modifiers = modifiers;
340 current_state.capslock = capslock;
341 }
342
343 this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
344 modifiers,
345 capslock,
346 }));
347
348 let key = dom_key_to_gpui_key(&event);
349
350 if is_modifier_only_key(&key) {
351 return;
352 }
353
354 event.prevent_default();
355
356 let is_held = event.repeat();
357 let key_char = compute_key_char(&event, &key, &modifiers);
358
359 let keystroke = Keystroke {
360 modifiers,
361 key,
362 key_char: key_char.clone(),
363 };
364
365 let result = this.dispatch_input(PlatformInput::KeyDown(KeyDownEvent {
366 keystroke,
367 is_held,
368 prefer_character_input: false,
369 }));
370
371 if let Some(result) = result {
372 if !result.propagate {
373 return;
374 }
375 }
376
377 if this.is_composing.get() || event.is_composing() {
378 return;
379 }
380
381 if modifiers.is_subset_of(&Modifiers::shift()) {
382 if let Some(text) = key_char {
383 this.with_input_handler(|handler| {
384 handler.replace_text_in_range(None, &text);
385 });
386 }
387 }
388 })
389 }
390
391 fn register_key_up(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
392 let this = Rc::clone(self);
393 self.listen_input("keyup", move |event: JsValue| {
394 let event: web_sys::KeyboardEvent = event.unchecked_into();
395
396 let modifiers = modifiers_from_keyboard_event(&event, this.is_mac);
397 let capslock = capslock_from_keyboard_event(&event);
398
399 {
400 let mut current_state = this.state.borrow_mut();
401 current_state.modifiers = modifiers;
402 current_state.capslock = capslock;
403 }
404
405 this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
406 modifiers,
407 capslock,
408 }));
409
410 let key = dom_key_to_gpui_key(&event);
411
412 if is_modifier_only_key(&key) {
413 return;
414 }
415
416 event.prevent_default();
417
418 let key_char = compute_key_char(&event, &key, &modifiers);
419
420 let keystroke = Keystroke {
421 modifiers,
422 key,
423 key_char,
424 };
425
426 this.dispatch_input(PlatformInput::KeyUp(KeyUpEvent { keystroke }));
427 })
428 }
429
430 fn register_composition_start(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
431 let this = Rc::clone(self);
432 self.listen_input("compositionstart", move |_event: JsValue| {
433 this.is_composing.set(true);
434 })
435 }
436
437 fn register_composition_update(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
438 let this = Rc::clone(self);
439 self.listen_input("compositionupdate", move |event: JsValue| {
440 let event: web_sys::CompositionEvent = event.unchecked_into();
441 let data = event.data().unwrap_or_default();
442 this.is_composing.set(true);
443 this.with_input_handler(|handler| {
444 handler.replace_and_mark_text_in_range(None, &data, None);
445 });
446 })
447 }
448
449 fn register_composition_end(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
450 let this = Rc::clone(self);
451 self.listen_input("compositionend", move |event: JsValue| {
452 let event: web_sys::CompositionEvent = event.unchecked_into();
453 let data = event.data().unwrap_or_default();
454 this.is_composing.set(false);
455 this.with_input_handler(|handler| {
456 handler.replace_text_in_range(None, &data);
457 handler.unmark_text();
458 });
459 this.input_element.set_value("");
460 })
461 }
462
463 fn register_focus(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
464 let this = Rc::clone(self);
465 self.listen_input("focus", move |_event: JsValue| {
466 {
467 let mut state = this.state.borrow_mut();
468 state.is_active = true;
469 }
470 let mut callbacks = this.callbacks.borrow_mut();
471 if let Some(ref mut callback) = callbacks.active_status_change {
472 callback(true);
473 }
474 })
475 }
476
477 fn register_blur(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
478 let this = Rc::clone(self);
479 self.listen_input("blur", move |_event: JsValue| {
480 {
481 let mut state = this.state.borrow_mut();
482 state.is_active = false;
483 }
484 let mut callbacks = this.callbacks.borrow_mut();
485 if let Some(ref mut callback) = callbacks.active_status_change {
486 callback(false);
487 }
488 })
489 }
490
491 fn register_pointer_enter(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
492 let this = Rc::clone(self);
493 self.listen("pointerenter", move |_event: JsValue| {
494 {
495 let mut state = this.state.borrow_mut();
496 state.is_hovered = true;
497 }
498 let mut callbacks = this.callbacks.borrow_mut();
499 if let Some(ref mut callback) = callbacks.hover_status_change {
500 callback(true);
501 }
502 })
503 }
504
505 fn register_pointer_leave_hover(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
506 let this = Rc::clone(self);
507 self.listen("pointerleave", move |_event: JsValue| {
508 {
509 let mut state = this.state.borrow_mut();
510 state.is_hovered = false;
511 }
512 let mut callbacks = this.callbacks.borrow_mut();
513 if let Some(ref mut callback) = callbacks.hover_status_change {
514 callback(false);
515 }
516 })
517 }
518}
519
520fn dom_key_to_gpui_key(event: &web_sys::KeyboardEvent) -> String {
521 let key = event.key();
522 match key.as_str() {
523 "Enter" => "enter".to_string(),
524 "Backspace" => "backspace".to_string(),
525 "Tab" => "tab".to_string(),
526 "Escape" => "escape".to_string(),
527 "Delete" => "delete".to_string(),
528 " " => "space".to_string(),
529 "ArrowLeft" => "left".to_string(),
530 "ArrowRight" => "right".to_string(),
531 "ArrowUp" => "up".to_string(),
532 "ArrowDown" => "down".to_string(),
533 "Home" => "home".to_string(),
534 "End" => "end".to_string(),
535 "PageUp" => "pageup".to_string(),
536 "PageDown" => "pagedown".to_string(),
537 "Insert" => "insert".to_string(),
538 "Control" => "control".to_string(),
539 "Alt" => "alt".to_string(),
540 "Shift" => "shift".to_string(),
541 "Meta" => "platform".to_string(),
542 "CapsLock" => "capslock".to_string(),
543 other => {
544 if let Some(rest) = other.strip_prefix('F') {
545 if let Ok(number) = rest.parse::<u8>() {
546 if (1..=35).contains(&number) {
547 return format!("f{number}");
548 }
549 }
550 }
551 other.to_lowercase()
552 }
553 }
554}
555
556fn dom_mouse_button_to_gpui(button: i16) -> MouseButton {
557 match button {
558 0 => MouseButton::Left,
559 1 => MouseButton::Middle,
560 2 => MouseButton::Right,
561 3 => MouseButton::Navigate(NavigationDirection::Back),
562 4 => MouseButton::Navigate(NavigationDirection::Forward),
563 _ => MouseButton::Left,
564 }
565}
566
567fn modifiers_from_keyboard_event(event: &web_sys::KeyboardEvent, _is_mac: bool) -> Modifiers {
568 Modifiers {
569 control: event.ctrl_key(),
570 alt: event.alt_key(),
571 shift: event.shift_key(),
572 platform: event.meta_key(),
573 function: false,
574 }
575}
576
577fn modifiers_from_mouse_event(event: &web_sys::PointerEvent, _is_mac: bool) -> Modifiers {
578 let mouse_event: &web_sys::MouseEvent = event.as_ref();
579 Modifiers {
580 control: mouse_event.ctrl_key(),
581 alt: mouse_event.alt_key(),
582 shift: mouse_event.shift_key(),
583 platform: mouse_event.meta_key(),
584 function: false,
585 }
586}
587
588fn modifiers_from_wheel_event(event: &web_sys::MouseEvent, _is_mac: bool) -> Modifiers {
589 Modifiers {
590 control: event.ctrl_key(),
591 alt: event.alt_key(),
592 shift: event.shift_key(),
593 platform: event.meta_key(),
594 function: false,
595 }
596}
597
598fn capslock_from_keyboard_event(event: &web_sys::KeyboardEvent) -> Capslock {
599 Capslock {
600 on: event.get_modifier_state("CapsLock"),
601 }
602}
603
604pub(crate) fn is_mac_platform(browser_window: &web_sys::Window) -> bool {
605 let navigator = browser_window.navigator();
606
607 #[allow(deprecated)]
608 // navigator.platform() is deprecated but navigator.userAgentData is not widely available yet
609 if let Ok(platform) = navigator.platform() {
610 if platform.contains("Mac") {
611 return true;
612 }
613 }
614
615 if let Ok(user_agent) = navigator.user_agent() {
616 return user_agent.contains("Mac");
617 }
618
619 false
620}
621
622fn is_modifier_only_key(key: &str) -> bool {
623 matches!(
624 key,
625 "control" | "alt" | "shift" | "platform" | "capslock" | "compose" | "process"
626 )
627}
628
629fn compute_key_char(
630 event: &web_sys::KeyboardEvent,
631 gpui_key: &str,
632 modifiers: &Modifiers,
633) -> Option<String> {
634 if modifiers.platform || modifiers.control {
635 return None;
636 }
637
638 if is_modifier_only_key(gpui_key) {
639 return None;
640 }
641
642 if gpui_key == "space" {
643 return Some(" ".to_string());
644 }
645
646 let raw_key = event.key();
647
648 if raw_key.len() == 1 {
649 return Some(raw_key);
650 }
651
652 None
653}
654
655fn pointer_position_in_element(event: &web_sys::PointerEvent) -> Point<Pixels> {
656 let mouse_event: &web_sys::MouseEvent = event.as_ref();
657 mouse_position_in_element(mouse_event)
658}
659
660fn mouse_position_in_element(event: &web_sys::MouseEvent) -> Point<Pixels> {
661 // offset_x/offset_y give position relative to the target element's padding edge
662 point(px(event.offset_x() as f32), px(event.offset_y() as f32))
663}
664
665fn extract_file_paths_from_drag(
666 event: &web_sys::DragEvent,
667) -> smallvec::SmallVec<[std::path::PathBuf; 2]> {
668 let mut paths = smallvec![];
669 let Some(data_transfer) = event.data_transfer() else {
670 return paths;
671 };
672 let file_list = data_transfer.files();
673 let Some(files) = file_list else {
674 return paths;
675 };
676 for index in 0..files.length() {
677 if let Some(file) = files.get(index) {
678 paths.push(std::path::PathBuf::from(file.name()));
679 }
680 }
681 paths
682}