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