diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index efe96e552b..b6152807e6 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -71,7 +71,8 @@ use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSour use crate::platform::linux::LinuxClient; use crate::platform::PlatformWindow; use crate::{ - point, px, FileDropEvent, ForegroundExecutor, MouseExitEvent, WindowAppearance, SCROLL_LINES, + point, px, Bounds, FileDropEvent, ForegroundExecutor, MouseExitEvent, WindowAppearance, + SCROLL_LINES, }; use crate::{ AnyWindowHandle, CursorStyle, DisplayId, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, @@ -151,6 +152,7 @@ pub(crate) struct WaylandClientState { data_device: Option, text_input: Option, pre_edit_text: Option, + composing: bool, // Surface to Window mapping windows: HashMap, // Output to scale mapping @@ -218,6 +220,41 @@ impl WaylandClientStatePtr { self.0.upgrade().unwrap().borrow().serial_tracker.get(kind) } + pub fn enable_ime(&self) { + let client = self.get_client(); + let mut state = client.borrow_mut(); + let Some(mut text_input) = state.text_input.take() else { + return; + }; + + text_input.enable(); + text_input.set_content_type(ContentHint::None, ContentPurpose::Normal); + if let Some(window) = state.keyboard_focused_window.clone() { + drop(state); + if let Some(area) = window.get_ime_area() { + text_input.set_cursor_rectangle( + area.origin.x.0 as i32, + area.origin.y.0 as i32, + area.size.width.0 as i32, + area.size.height.0 as i32, + ); + } + state = client.borrow_mut(); + } + text_input.commit(); + state.text_input = Some(text_input); + } + + pub fn disable_ime(&self) { + let client = self.get_client(); + let mut state = client.borrow_mut(); + state.composing = false; + if let Some(text_input) = &state.text_input { + text_input.disable(); + text_input.commit(); + } + } + pub fn drop_window(&self, surface_id: &ObjectId) { let mut client = self.get_client(); let mut state = client.borrow_mut(); @@ -372,6 +409,7 @@ impl WaylandClient { data_device, text_input: None, pre_edit_text: None, + composing: false, output_scales: outputs, windows: HashMap::default(), common, @@ -1050,34 +1088,35 @@ impl Dispatch for WaylandClientStatePtr { let mut state = client.borrow_mut(); match event { zwp_text_input_v3::Event::Enter { surface } => { - text_input.enable(); - text_input.set_content_type(ContentHint::None, ContentPurpose::Normal); - - if let Some(window) = state.keyboard_focused_window.clone() { - drop(state); - if let Some(area) = window.get_ime_area() { - text_input.set_cursor_rectangle( - area.origin.x.0 as i32, - area.origin.y.0 as i32, - area.size.width.0 as i32, - area.size.height.0 as i32, - ); - } - } - text_input.commit(); + drop(state); + this.enable_ime(); } zwp_text_input_v3::Event::Leave { surface } => { - text_input.disable(); - text_input.commit(); + drop(state); + this.disable_ime(); } zwp_text_input_v3::Event::CommitString { text } => { + state.composing = false; let Some(window) = state.keyboard_focused_window.clone() else { return; }; if let Some(commit_text) = text { drop(state); - window.handle_ime(ImeInput::InsertText(commit_text)); + // IBus Intercepts keys like `a`, `b`, but those keys are needed for vim mode. + // We should only send ASCII characters to Zed, otherwise a user could remap a letter like `か` or `相`. + if commit_text.len() == 1 { + window.handle_input(PlatformInput::KeyDown(KeyDownEvent { + keystroke: Keystroke { + modifiers: Modifiers::default(), + key: commit_text.clone(), + ime_key: Some(commit_text), + }, + is_held: false, + })); + } else { + window.handle_ime(ImeInput::InsertText(commit_text)); + } } } zwp_text_input_v3::Event::PreeditString { @@ -1085,6 +1124,7 @@ impl Dispatch for WaylandClientStatePtr { cursor_begin, cursor_end, } => { + state.composing = true; state.pre_edit_text = text; } zwp_text_input_v3::Event::Done { serial } => { @@ -1238,15 +1278,23 @@ impl Dispatch for WaylandClientStatePtr { } match button_state { wl_pointer::ButtonState::Pressed => { - if let (Some(window), Some(text), Some(compose_state)) = ( - state.keyboard_focused_window.clone(), - state.pre_edit_text.take(), - state.compose_state.as_mut(), - ) { - compose_state.reset(); - drop(state); - window.handle_ime(ImeInput::InsertText(text)); - state = client.borrow_mut(); + if let Some(window) = state.keyboard_focused_window.clone() { + if state.composing && state.text_input.is_some() { + let text_input = state.text_input.as_ref().unwrap(); + drop(state); + // text_input_v3 don't have something like a reset function + this.disable_ime(); + this.enable_ime(); + window.handle_ime(ImeInput::UnmarkText); + state = client.borrow_mut(); + } else if let (Some(text), Some(compose)) = + (state.pre_edit_text.take(), state.compose_state.as_mut()) + { + compose.reset(); + drop(state); + window.handle_ime(ImeInput::InsertText(text)); + state = client.borrow_mut(); + } } let click_elapsed = state.click.last_click.elapsed(); diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index cf6a016e8f..b1f2c5f34b 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -170,6 +170,7 @@ pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr); pub enum ImeInput { InsertText(String), SetMarkedText(String), + UnmarkText, DeleteText, } @@ -448,6 +449,9 @@ impl WaylandWindowStatePtr { ImeInput::SetMarkedText(text) => { input_handler.replace_and_mark_text_in_range(None, &text, None); } + ImeInput::UnmarkText => { + input_handler.unmark_text(); + } ImeInput::DeleteText => { if let Some(marked) = input_handler.marked_text_range() { input_handler.replace_text_in_range(Some(marked), ""); diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 7b335bba3e..b61a07114f 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -25,6 +25,7 @@ use x11rb::protocol::{randr, render, xinput, xkb, xproto, Event}; use x11rb::resource_manager::Database; use x11rb::xcb_ffi::XCBConnection; use xim::{x11rb::X11rbClient, Client}; +use xim::{AHashMap, AttributeName, ClientHandler, InputStyle}; use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION}; use xkbcommon::xkb as xkbc; @@ -113,6 +114,7 @@ pub struct X11ClientState { pub(crate) compose_state: xkbc::compose::State, pub(crate) pre_edit_text: Option, + pub(crate) composing: bool, pub(crate) cursor_handle: cursor::Handle, pub(crate) cursor_styles: HashMap, pub(crate) cursor_cache: HashMap, @@ -393,6 +395,7 @@ impl X11Client { compose_state: compose_state, pre_edit_text: None, + composing: false, cursor_handle, cursor_styles: HashMap::default(), @@ -407,6 +410,58 @@ impl X11Client { }))) } + pub fn enable_ime(&self) { + let mut state = self.0.borrow_mut(); + if state.ximc.is_none() { + return; + } + + let mut ximc = state.ximc.take().unwrap(); + let mut xim_handler = state.xim_handler.take().unwrap(); + let mut ic_attributes = ximc + .build_ic_attributes() + .push( + AttributeName::InputStyle, + InputStyle::PREEDIT_CALLBACKS + | InputStyle::STATUS_NOTHING + | InputStyle::PREEDIT_NONE, + ) + .push(AttributeName::ClientWindow, xim_handler.window) + .push(AttributeName::FocusWindow, xim_handler.window); + + let window_id = state.focused_window; + drop(state); + if let Some(window_id) = window_id { + let window = self.get_window(window_id).unwrap(); + if let Some(area) = window.get_ime_area() { + ic_attributes = + ic_attributes.nested_list(xim::AttributeName::PreeditAttributes, |b| { + b.push( + xim::AttributeName::SpotLocation, + xim::Point { + x: u32::from(area.origin.x + area.size.width) as i16, + y: u32::from(area.origin.y + area.size.height) as i16, + }, + ); + }); + } + } + ximc.create_ic(xim_handler.im_id, ic_attributes.build()); + state = self.0.borrow_mut(); + state.xim_handler = Some(xim_handler); + state.ximc = Some(ximc); + } + + pub fn disable_ime(&self) { + let mut state = self.0.borrow_mut(); + state.composing = false; + if let Some(mut ximc) = state.ximc.take() { + let xim_handler = state.xim_handler.as_ref().unwrap(); + ximc.destroy_ic(xim_handler.im_id, xim_handler.ic_id); + state.ximc = Some(ximc); + } + } + fn get_window(&self, win: xproto::Window) -> Option { let state = self.0.borrow(); state @@ -452,12 +507,21 @@ impl X11Client { Event::FocusIn(event) => { let window = self.get_window(event.event)?; window.set_focused(true); - self.0.borrow_mut().focused_window = Some(event.event); + let mut state = self.0.borrow_mut(); + state.focused_window = Some(event.event); + drop(state); + self.enable_ime(); } Event::FocusOut(event) => { let window = self.get_window(event.event)?; window.set_focused(false); - self.0.borrow_mut().focused_window = None; + let mut state = self.0.borrow_mut(); + state.focused_window = None; + state.compose_state.reset(); + state.pre_edit_text.take(); + drop(state); + self.disable_ime(); + window.handle_ime_delete(); } Event::XkbStateNotify(event) => { let mut state = self.0.borrow_mut(); @@ -558,6 +622,19 @@ impl X11Client { px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), ); + + if state.composing && state.ximc.is_some() { + drop(state); + self.disable_ime(); + self.enable_ime(); + window.handle_ime_unmark(); + state = self.0.borrow_mut(); + } else if let Some(text) = state.pre_edit_text.take() { + state.compose_state.reset(); + drop(state); + window.handle_ime_commit(text); + state = self.0.borrow_mut(); + } if let Some(button) = button_of_key(event.detail.try_into().unwrap()) { let click_elapsed = state.last_click.elapsed(); @@ -739,6 +816,9 @@ impl X11Client { fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> { let window = self.get_window(window).unwrap(); + let mut state = self.0.borrow_mut(); + state.composing = false; + drop(state); window.handle_ime_commit(text); Some(()) @@ -751,6 +831,7 @@ impl X11Client { let mut state = self.0.borrow_mut(); let mut ximc = state.ximc.take().unwrap(); let mut xim_handler = state.xim_handler.take().unwrap(); + state.composing = true; drop(state); if let Some(area) = window.get_ime_area() { diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 268b3b7980..1e628f2806 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -579,6 +579,28 @@ impl X11WindowStatePtr { } } + pub fn handle_ime_unmark(&self) { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.unmark_text(); + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } + + pub fn handle_ime_delete(&self) { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + if let Some(marked) = input_handler.marked_text_range() { + input_handler.replace_text_in_range(Some(marked), ""); + } + let mut state = self.state.borrow_mut(); + state.input_handler = Some(input_handler); + } + } + pub fn get_ime_area(&self) -> Option> { let mut state = self.state.borrow_mut(); let mut bounds: Option> = None;