events.rs

  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}