events.rs

  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}