use std::rc::Rc; use gpui::{ Capslock, ExternalPaths, FileDropEvent, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformInput, Point, ScrollDelta, ScrollWheelEvent, TouchPhase, point, px, }; use smallvec::smallvec; use wasm_bindgen::prelude::*; use crate::window::WebWindowInner; pub struct WebEventListeners { #[allow(dead_code)] closures: Vec>, } pub(crate) struct ClickState { last_position: Point, last_time: f64, current_count: usize, } impl Default for ClickState { fn default() -> Self { Self { last_position: Point::default(), last_time: 0.0, current_count: 0, } } } impl ClickState { fn register_click(&mut self, position: Point, time: f64) -> usize { let distance = ((f32::from(position.x) - f32::from(self.last_position.x)).powi(2) + (f32::from(position.y) - f32::from(self.last_position.y)).powi(2)) .sqrt(); if (time - self.last_time) < 400.0 && distance < 5.0 { self.current_count += 1; } else { self.current_count = 1; } self.last_position = position; self.last_time = time; self.current_count } } impl WebWindowInner { pub fn register_event_listeners(self: &Rc) -> WebEventListeners { let mut closures = vec![ self.register_pointer_down(), self.register_pointer_up(), self.register_pointer_move(), self.register_pointer_leave(), self.register_wheel(), self.register_context_menu(), self.register_dragover(), self.register_drop(), self.register_dragleave(), self.register_key_down(), self.register_key_up(), self.register_focus(), self.register_blur(), self.register_pointer_enter(), self.register_pointer_leave_hover(), ]; closures.extend(self.register_visibility_change()); closures.extend(self.register_appearance_change()); WebEventListeners { closures } } fn listen( self: &Rc, event_name: &str, handler: impl FnMut(JsValue) + 'static, ) -> Closure { let closure = Closure::::new(handler); self.canvas .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) .ok(); closure } /// Registers a listener with `{passive: false}` so that `preventDefault()` works. /// Needed for events like `wheel` which are passive by default in modern browsers. fn listen_non_passive( self: &Rc, event_name: &str, handler: impl FnMut(JsValue) + 'static, ) -> Closure { let closure = Closure::::new(handler); let canvas_js: &JsValue = self.canvas.as_ref(); let callback_js: &JsValue = closure.as_ref(); let options = js_sys::Object::new(); js_sys::Reflect::set(&options, &"passive".into(), &false.into()).ok(); if let Ok(add_fn_val) = js_sys::Reflect::get(canvas_js, &"addEventListener".into()) { if let Ok(add_fn) = add_fn_val.dyn_into::() { add_fn .call3(canvas_js, &event_name.into(), callback_js, &options) .ok(); } } closure } fn dispatch_input(&self, input: PlatformInput) { let mut borrowed = self.callbacks.borrow_mut(); if let Some(ref mut callback) = borrowed.input { callback(input); } } fn register_pointer_down(self: &Rc) -> Closure { let this = Rc::clone(self); self.listen("pointerdown", move |event: JsValue| { let event: web_sys::PointerEvent = event.unchecked_into(); event.prevent_default(); this.canvas.focus().ok(); let button = dom_mouse_button_to_gpui(event.button()); let position = pointer_position_in_element(&event); let modifiers = modifiers_from_mouse_event(&event, this.is_mac); let time = js_sys::Date::now(); this.pressed_button.set(Some(button)); let click_count = this.click_state.borrow_mut().register_click(position, time); { let mut current_state = this.state.borrow_mut(); current_state.mouse_position = position; current_state.modifiers = modifiers; } this.dispatch_input(PlatformInput::MouseDown(MouseDownEvent { button, position, modifiers, click_count, first_mouse: false, })); }) } fn register_pointer_up(self: &Rc) -> Closure { let this = Rc::clone(self); self.listen("pointerup", move |event: JsValue| { let event: web_sys::PointerEvent = event.unchecked_into(); event.prevent_default(); let button = dom_mouse_button_to_gpui(event.button()); let position = pointer_position_in_element(&event); let modifiers = modifiers_from_mouse_event(&event, this.is_mac); this.pressed_button.set(None); let click_count = this.click_state.borrow().current_count; { let mut current_state = this.state.borrow_mut(); current_state.mouse_position = position; current_state.modifiers = modifiers; } this.dispatch_input(PlatformInput::MouseUp(MouseUpEvent { button, position, modifiers, click_count, })); }) } fn register_pointer_move(self: &Rc) -> Closure { let this = Rc::clone(self); self.listen("pointermove", move |event: JsValue| { let event: web_sys::PointerEvent = event.unchecked_into(); event.prevent_default(); let position = pointer_position_in_element(&event); let modifiers = modifiers_from_mouse_event(&event, this.is_mac); let current_pressed = this.pressed_button.get(); { let mut current_state = this.state.borrow_mut(); current_state.mouse_position = position; current_state.modifiers = modifiers; } this.dispatch_input(PlatformInput::MouseMove(MouseMoveEvent { position, pressed_button: current_pressed, modifiers, })); }) } fn register_pointer_leave(self: &Rc) -> Closure { let this = Rc::clone(self); self.listen("pointerleave", move |event: JsValue| { let event: web_sys::PointerEvent = event.unchecked_into(); let position = pointer_position_in_element(&event); let modifiers = modifiers_from_mouse_event(&event, this.is_mac); let current_pressed = this.pressed_button.get(); { let mut current_state = this.state.borrow_mut(); current_state.mouse_position = position; current_state.modifiers = modifiers; } this.dispatch_input(PlatformInput::MouseExited(MouseExitEvent { position, pressed_button: current_pressed, modifiers, })); }) } fn register_wheel(self: &Rc) -> Closure { let this = Rc::clone(self); self.listen_non_passive("wheel", move |event: JsValue| { let event: web_sys::WheelEvent = event.unchecked_into(); event.prevent_default(); let mouse_event: &web_sys::MouseEvent = event.as_ref(); let position = mouse_position_in_element(mouse_event); let modifiers = modifiers_from_wheel_event(mouse_event, this.is_mac); let delta_mode = event.delta_mode(); let delta = if delta_mode == 1 { ScrollDelta::Lines(point(-event.delta_x() as f32, -event.delta_y() as f32)) } else { ScrollDelta::Pixels(point( px(-event.delta_x() as f32), px(-event.delta_y() as f32), )) }; { let mut current_state = this.state.borrow_mut(); current_state.modifiers = modifiers; } this.dispatch_input(PlatformInput::ScrollWheel(ScrollWheelEvent { position, delta, modifiers, touch_phase: TouchPhase::Moved, })); }) } fn register_context_menu(self: &Rc) -> Closure { self.listen("contextmenu", move |event: JsValue| { let event: web_sys::Event = event.unchecked_into(); event.prevent_default(); }) } fn register_dragover(self: &Rc) -> Closure { let this = Rc::clone(self); self.listen("dragover", move |event: JsValue| { let event: web_sys::DragEvent = event.unchecked_into(); event.prevent_default(); let mouse_event: &web_sys::MouseEvent = event.as_ref(); let position = mouse_position_in_element(mouse_event); { let mut current_state = this.state.borrow_mut(); current_state.mouse_position = position; } this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Pending { position })); }) } fn register_drop(self: &Rc) -> Closure { let this = Rc::clone(self); self.listen("drop", move |event: JsValue| { let event: web_sys::DragEvent = event.unchecked_into(); event.prevent_default(); let mouse_event: &web_sys::MouseEvent = event.as_ref(); let position = mouse_position_in_element(mouse_event); { let mut current_state = this.state.borrow_mut(); current_state.mouse_position = position; } let paths = extract_file_paths_from_drag(&event); this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Entered { position, paths: ExternalPaths(paths), })); this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Submit { position })); }) } fn register_dragleave(self: &Rc) -> Closure { let this = Rc::clone(self); self.listen("dragleave", move |_event: JsValue| { this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Exited)); }) } fn register_key_down(self: &Rc) -> Closure { let this = Rc::clone(self); self.listen("keydown", move |event: JsValue| { let event: web_sys::KeyboardEvent = event.unchecked_into(); let modifiers = modifiers_from_keyboard_event(&event, this.is_mac); let capslock = capslock_from_keyboard_event(&event); { let mut current_state = this.state.borrow_mut(); current_state.modifiers = modifiers; current_state.capslock = capslock; } this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent { modifiers, capslock, })); let key = dom_key_to_gpui_key(&event); if is_modifier_only_key(&key) { return; } event.prevent_default(); let is_held = event.repeat(); let key_char = compute_key_char(&event, &key, &modifiers); let keystroke = Keystroke { modifiers, key, key_char, }; this.dispatch_input(PlatformInput::KeyDown(KeyDownEvent { keystroke, is_held, prefer_character_input: false, })); }) } fn register_key_up(self: &Rc) -> Closure { let this = Rc::clone(self); self.listen("keyup", move |event: JsValue| { let event: web_sys::KeyboardEvent = event.unchecked_into(); let modifiers = modifiers_from_keyboard_event(&event, this.is_mac); let capslock = capslock_from_keyboard_event(&event); { let mut current_state = this.state.borrow_mut(); current_state.modifiers = modifiers; current_state.capslock = capslock; } this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent { modifiers, capslock, })); let key = dom_key_to_gpui_key(&event); if is_modifier_only_key(&key) { return; } event.prevent_default(); let key_char = compute_key_char(&event, &key, &modifiers); let keystroke = Keystroke { modifiers, key, key_char, }; this.dispatch_input(PlatformInput::KeyUp(KeyUpEvent { keystroke })); }) } fn register_focus(self: &Rc) -> Closure { let this = Rc::clone(self); self.listen("focus", move |_event: JsValue| { { let mut state = this.state.borrow_mut(); state.is_active = true; } let mut callbacks = this.callbacks.borrow_mut(); if let Some(ref mut callback) = callbacks.active_status_change { callback(true); } }) } fn register_blur(self: &Rc) -> Closure { let this = Rc::clone(self); self.listen("blur", move |_event: JsValue| { { let mut state = this.state.borrow_mut(); state.is_active = false; } let mut callbacks = this.callbacks.borrow_mut(); if let Some(ref mut callback) = callbacks.active_status_change { callback(false); } }) } fn register_pointer_enter(self: &Rc) -> Closure { let this = Rc::clone(self); self.listen("pointerenter", move |_event: JsValue| { { let mut state = this.state.borrow_mut(); state.is_hovered = true; } let mut callbacks = this.callbacks.borrow_mut(); if let Some(ref mut callback) = callbacks.hover_status_change { callback(true); } }) } fn register_pointer_leave_hover(self: &Rc) -> Closure { let this = Rc::clone(self); self.listen("pointerleave", move |_event: JsValue| { { let mut state = this.state.borrow_mut(); state.is_hovered = false; } let mut callbacks = this.callbacks.borrow_mut(); if let Some(ref mut callback) = callbacks.hover_status_change { callback(false); } }) } } fn dom_key_to_gpui_key(event: &web_sys::KeyboardEvent) -> String { let key = event.key(); match key.as_str() { "Enter" => "enter".to_string(), "Backspace" => "backspace".to_string(), "Tab" => "tab".to_string(), "Escape" => "escape".to_string(), "Delete" => "delete".to_string(), " " => "space".to_string(), "ArrowLeft" => "left".to_string(), "ArrowRight" => "right".to_string(), "ArrowUp" => "up".to_string(), "ArrowDown" => "down".to_string(), "Home" => "home".to_string(), "End" => "end".to_string(), "PageUp" => "pageup".to_string(), "PageDown" => "pagedown".to_string(), "Insert" => "insert".to_string(), "Control" => "control".to_string(), "Alt" => "alt".to_string(), "Shift" => "shift".to_string(), "Meta" => "platform".to_string(), "CapsLock" => "capslock".to_string(), other => { if let Some(rest) = other.strip_prefix('F') { if let Ok(number) = rest.parse::() { if (1..=35).contains(&number) { return format!("f{number}"); } } } other.to_lowercase() } } } fn dom_mouse_button_to_gpui(button: i16) -> MouseButton { match button { 0 => MouseButton::Left, 1 => MouseButton::Middle, 2 => MouseButton::Right, 3 => MouseButton::Navigate(NavigationDirection::Back), 4 => MouseButton::Navigate(NavigationDirection::Forward), _ => MouseButton::Left, } } fn modifiers_from_keyboard_event(event: &web_sys::KeyboardEvent, _is_mac: bool) -> Modifiers { Modifiers { control: event.ctrl_key(), alt: event.alt_key(), shift: event.shift_key(), platform: event.meta_key(), function: false, } } fn modifiers_from_mouse_event(event: &web_sys::PointerEvent, _is_mac: bool) -> Modifiers { let mouse_event: &web_sys::MouseEvent = event.as_ref(); Modifiers { control: mouse_event.ctrl_key(), alt: mouse_event.alt_key(), shift: mouse_event.shift_key(), platform: mouse_event.meta_key(), function: false, } } fn modifiers_from_wheel_event(event: &web_sys::MouseEvent, _is_mac: bool) -> Modifiers { Modifiers { control: event.ctrl_key(), alt: event.alt_key(), shift: event.shift_key(), platform: event.meta_key(), function: false, } } fn capslock_from_keyboard_event(event: &web_sys::KeyboardEvent) -> Capslock { Capslock { on: event.get_modifier_state("CapsLock"), } } pub(crate) fn is_mac_platform(browser_window: &web_sys::Window) -> bool { let navigator = browser_window.navigator(); #[allow(deprecated)] // navigator.platform() is deprecated but navigator.userAgentData is not widely available yet if let Ok(platform) = navigator.platform() { if platform.contains("Mac") { return true; } } if let Ok(user_agent) = navigator.user_agent() { return user_agent.contains("Mac"); } false } fn is_modifier_only_key(key: &str) -> bool { matches!(key, "control" | "alt" | "shift" | "platform" | "capslock") } fn compute_key_char( event: &web_sys::KeyboardEvent, gpui_key: &str, modifiers: &Modifiers, ) -> Option { if modifiers.platform || modifiers.control { return None; } if is_modifier_only_key(gpui_key) { return None; } if gpui_key == "space" { return Some(" ".to_string()); } let raw_key = event.key(); if raw_key.len() == 1 { return Some(raw_key); } None } fn pointer_position_in_element(event: &web_sys::PointerEvent) -> Point { let mouse_event: &web_sys::MouseEvent = event.as_ref(); mouse_position_in_element(mouse_event) } fn mouse_position_in_element(event: &web_sys::MouseEvent) -> Point { // offset_x/offset_y give position relative to the target element's padding edge point(px(event.offset_x() as f32), px(event.offset_y() as f32)) } fn extract_file_paths_from_drag( event: &web_sys::DragEvent, ) -> smallvec::SmallVec<[std::path::PathBuf; 2]> { let mut paths = smallvec![]; let Some(data_transfer) = event.data_transfer() else { return paths; }; let file_list = data_transfer.files(); let Some(files) = file_list else { return paths; }; for index in 0..files.length() { if let Some(file) = files.get(index) { paths.push(std::path::PathBuf::from(file.name())); } } paths }