diff --git a/crates/gpui_web/Cargo.toml b/crates/gpui_web/Cargo.toml index dbb110597c7b850c28cde99ed573eab8264a18f7..5980fa5e855214e1d240dcaaacd59ae4bb6f3537 100644 --- a/crates/gpui_web/Cargo.toml +++ b/crates/gpui_web/Cargo.toml @@ -35,6 +35,7 @@ raw-window-handle = "0.6" wasm_thread = { version = "0.3", features = ["es_modules"], optional = true } web-sys = { version = "0.3", features = [ "console", + "CompositionEvent", "CssStyleDeclaration", "DataTransfer", "Document", diff --git a/crates/gpui_web/src/events.rs b/crates/gpui_web/src/events.rs index 5f6d8527e70a3778a46a11e00758e822790e742f..e93534fbe88238118ce0a1e819aec5ff3c3d201a 100644 --- a/crates/gpui_web/src/events.rs +++ b/crates/gpui_web/src/events.rs @@ -1,10 +1,10 @@ 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, + Capslock, DispatchEventResult, 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::*; @@ -64,6 +64,9 @@ impl WebWindowInner { self.register_dragleave(), self.register_key_down(), self.register_key_up(), + self.register_composition_start(), + self.register_composition_update(), + self.register_composition_end(), self.register_focus(), self.register_blur(), self.register_pointer_enter(), @@ -87,6 +90,18 @@ impl WebWindowInner { closure } + fn listen_input( + self: &Rc, + event_name: &str, + handler: impl FnMut(JsValue) + 'static, + ) -> Closure { + let closure = Closure::::new(handler); + self.input_element + .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( @@ -109,11 +124,9 @@ impl WebWindowInner { closure } - fn dispatch_input(&self, input: PlatformInput) { + fn dispatch_input(&self, input: PlatformInput) -> Option { let mut borrowed = self.callbacks.borrow_mut(); - if let Some(ref mut callback) = borrowed.input { - callback(input); - } + borrowed.input.as_mut().map(|callback| callback(input)) } fn register_pointer_down(self: &Rc) -> Closure { @@ -121,7 +134,7 @@ impl WebWindowInner { self.listen("pointerdown", move |event: JsValue| { let event: web_sys::PointerEvent = event.unchecked_into(); event.prevent_default(); - this.canvas.focus().ok(); + this.input_element.focus().ok(); let button = dom_mouse_button_to_gpui(event.button()); let position = pointer_position_in_element(&event); @@ -315,7 +328,7 @@ impl WebWindowInner { fn register_key_down(self: &Rc) -> Closure { let this = Rc::clone(self); - self.listen("keydown", move |event: JsValue| { + self.listen_input("keydown", move |event: JsValue| { let event: web_sys::KeyboardEvent = event.unchecked_into(); let modifiers = modifiers_from_keyboard_event(&event, this.is_mac); @@ -346,20 +359,38 @@ impl WebWindowInner { let keystroke = Keystroke { modifiers, key, - key_char, + key_char: key_char.clone(), }; - this.dispatch_input(PlatformInput::KeyDown(KeyDownEvent { + let result = this.dispatch_input(PlatformInput::KeyDown(KeyDownEvent { keystroke, is_held, prefer_character_input: false, })); + + if let Some(result) = result { + if !result.propagate { + return; + } + } + + if this.is_composing.get() || event.is_composing() { + return; + } + + if modifiers.is_subset_of(&Modifiers::shift()) { + if let Some(text) = key_char { + this.with_input_handler(|handler| { + handler.replace_text_in_range(None, &text); + }); + } + } }) } fn register_key_up(self: &Rc) -> Closure { let this = Rc::clone(self); - self.listen("keyup", move |event: JsValue| { + self.listen_input("keyup", move |event: JsValue| { let event: web_sys::KeyboardEvent = event.unchecked_into(); let modifiers = modifiers_from_keyboard_event(&event, this.is_mac); @@ -396,9 +427,42 @@ impl WebWindowInner { }) } + fn register_composition_start(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen_input("compositionstart", move |_event: JsValue| { + this.is_composing.set(true); + }) + } + + fn register_composition_update(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen_input("compositionupdate", move |event: JsValue| { + let event: web_sys::CompositionEvent = event.unchecked_into(); + let data = event.data().unwrap_or_default(); + this.is_composing.set(true); + this.with_input_handler(|handler| { + handler.replace_and_mark_text_in_range(None, &data, None); + }); + }) + } + + fn register_composition_end(self: &Rc) -> Closure { + let this = Rc::clone(self); + self.listen_input("compositionend", move |event: JsValue| { + let event: web_sys::CompositionEvent = event.unchecked_into(); + let data = event.data().unwrap_or_default(); + this.is_composing.set(false); + this.with_input_handler(|handler| { + handler.replace_text_in_range(None, &data); + handler.unmark_text(); + }); + this.input_element.set_value(""); + }) + } + fn register_focus(self: &Rc) -> Closure { let this = Rc::clone(self); - self.listen("focus", move |_event: JsValue| { + self.listen_input("focus", move |_event: JsValue| { { let mut state = this.state.borrow_mut(); state.is_active = true; @@ -412,7 +476,7 @@ impl WebWindowInner { fn register_blur(self: &Rc) -> Closure { let this = Rc::clone(self); - self.listen("blur", move |_event: JsValue| { + self.listen_input("blur", move |_event: JsValue| { { let mut state = this.state.borrow_mut(); state.is_active = false; @@ -556,7 +620,10 @@ pub(crate) fn is_mac_platform(browser_window: &web_sys::Window) -> bool { } fn is_modifier_only_key(key: &str) -> bool { - matches!(key, "control" | "alt" | "shift" | "platform" | "capslock") + matches!( + key, + "control" | "alt" | "shift" | "platform" | "capslock" | "compose" | "process" + ) } fn compute_key_char( diff --git a/crates/gpui_web/src/window.rs b/crates/gpui_web/src/window.rs index ab6d6fc857dfd092ea7e3c5d2dcb46f9ddc96cfb..5a2e564367033f76b0aa12c2d29d7f098d7eeb7a 100644 --- a/crates/gpui_web/src/window.rs +++ b/crates/gpui_web/src/window.rs @@ -45,6 +45,7 @@ pub(crate) struct WebWindowMutableState { pub(crate) struct WebWindowInner { pub(crate) browser_window: web_sys::Window, pub(crate) canvas: web_sys::HtmlCanvasElement, + pub(crate) input_element: web_sys::HtmlInputElement, pub(crate) has_device_pixel_support: bool, pub(crate) is_mac: bool, pub(crate) state: RefCell, @@ -53,6 +54,7 @@ pub(crate) struct WebWindowInner { pub(crate) pressed_button: Cell>, pub(crate) last_physical_size: Cell<(u32, u32)>, pub(crate) notify_scale: Cell, + pub(crate) is_composing: Cell, mql_handle: RefCell>, pending_physical_size: Cell>, } @@ -89,7 +91,7 @@ impl WebWindow { let max_texture_dimension = context.device.limits().max_texture_dimension_2d; let has_device_pixel_support = check_device_pixel_support(); - canvas.set_tab_index(0); + canvas.set_tab_index(-1); let style = canvas.style(); style @@ -114,7 +116,21 @@ impl WebWindow { body.append_child(&canvas) .map_err(|e| anyhow::anyhow!("Failed to append canvas to body: {e:?}"))?; - canvas.focus().ok(); + let input_element: web_sys::HtmlInputElement = document + .create_element("input") + .map_err(|e| anyhow::anyhow!("Failed to create input element: {e:?}"))? + .dyn_into() + .map_err(|e| anyhow::anyhow!("Created element is not an input: {e:?}"))?; + let input_style = input_element.style(); + input_style.set_property("position", "fixed").ok(); + input_style.set_property("top", "0").ok(); + input_style.set_property("left", "0").ok(); + input_style.set_property("width", "1px").ok(); + input_style.set_property("height", "1px").ok(); + input_style.set_property("opacity", "0").ok(); + body.append_child(&input_element) + .map_err(|e| anyhow::anyhow!("Failed to append input to body: {e:?}"))?; + input_element.focus().ok(); let device_size = Size { width: DevicePixels(0), @@ -155,6 +171,7 @@ impl WebWindow { let inner = Rc::new(WebWindowInner { browser_window, canvas, + input_element, has_device_pixel_support, is_mac, state: RefCell::new(mutable_state), @@ -163,6 +180,7 @@ impl WebWindow { pressed_button: Cell::new(None), last_physical_size: Cell::new((0, 0)), notify_scale: Cell::new(false), + is_composing: Cell::new(false), mql_handle: RefCell::new(None), pending_physical_size: Cell::new(None), }); @@ -389,6 +407,16 @@ impl WebWindowInner { Some(closure) } + pub(crate) fn with_input_handler( + &self, + f: impl FnOnce(&mut PlatformInputHandler) -> R, + ) -> Option { + let mut handler = self.state.borrow_mut().input_handler.take()?; + let result = f(&mut handler); + self.state.borrow_mut().input_handler = Some(handler); + Some(result) + } + pub(crate) fn register_appearance_change( self: &Rc, ) -> Option> {