From a0464ae83b880899b6fb5a25a2fe0d6447fd487a Mon Sep 17 00:00:00 2001 From: Aaron Muir Hamilton Date: Tue, 11 Mar 2025 14:04:50 -0400 Subject: [PATCH] x11: implement true cursor area with XNArea attribute --- src/changelog/unreleased.md | 1 + .../linux/x11/event_processor.rs | 4 +- src/platform_impl/linux/x11/ime/callbacks.rs | 4 +- src/platform_impl/linux/x11/ime/context.rs | 47 ++++++++++++------- src/platform_impl/linux/x11/ime/mod.rs | 8 ++-- src/platform_impl/linux/x11/window.rs | 16 +++---- src/window.rs | 2 - 7 files changed, 46 insertions(+), 36 deletions(-) diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index 7380df27..7c30116c 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -73,6 +73,7 @@ changelog entry. - Add ability to make non-activating window on macOS using `NSPanel` with `NSWindowStyleMask::NonactivatingPanel`. - On Windows, add `IconExtWindows::from_resource_name`. - Implement `MonitorHandleProvider` for `MonitorHandle` to access common monitor API. +- On X11, set an "area" attribute on XIM input connection to convey the cursor area. ### Changed diff --git a/src/platform_impl/linux/x11/event_processor.rs b/src/platform_impl/linux/x11/event_processor.rs index 4c570280..6b3d82d4 100644 --- a/src/platform_impl/linux/x11/event_processor.rs +++ b/src/platform_impl/linux/x11/event_processor.rs @@ -89,8 +89,8 @@ impl EventProcessor { let ime = ime.get_mut(); match request { - ImeRequest::Position(window_id, x, y) => { - ime.send_xim_spot(window_id, x, y); + ImeRequest::Area(window_id, x, y, w, h) => { + ime.send_xim_area(window_id, x, y, w, h); }, ImeRequest::Allow(window_id, allowed) => { ime.set_ime_allowed(window_id, allowed); diff --git a/src/platform_impl/linux/x11/ime/callbacks.rs b/src/platform_impl/linux/x11/ime/callbacks.rs index 16737aea..d623d281 100644 --- a/src/platform_impl/linux/x11/ime/callbacks.rs +++ b/src/platform_impl/linux/x11/ime/callbacks.rs @@ -116,7 +116,7 @@ unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> { let mut new_contexts = HashMap::new(); for (window, old_context) in unsafe { (*inner).contexts.iter() } { - let spot = old_context.as_ref().map(|old_context| old_context.ic_spot); + let area = old_context.as_ref().map(|old_context| old_context.ic_area); // Check if the IME was allowed on that context. let is_allowed = @@ -128,7 +128,7 @@ unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> { xconn, &new_im, *window, - spot, + area, (*inner).event_sender.clone(), is_allowed, ) diff --git a/src/platform_impl/linux/x11/ime/context.rs b/src/platform_impl/linux/x11/ime/context.rs index b981c999..53a75ac6 100644 --- a/src/platform_impl/linux/x11/ime/context.rs +++ b/src/platform_impl/linux/x11/ime/context.rs @@ -1,6 +1,5 @@ use std::error::Error; use std::ffi::CStr; -use std::os::raw::c_short; use std::sync::Arc; use std::{fmt, mem, ptr}; @@ -196,7 +195,7 @@ struct ImeContextClientData { // through `ImeInner`. pub struct ImeContext { pub(crate) ic: ffi::XIC, - pub(crate) ic_spot: ffi::XPoint, + pub(crate) ic_area: ffi::XRectangle, pub(crate) allowed: bool, // Since the data is passed shared between X11 XIM callbacks, but couldn't be directly free // from there we keep the pointer to automatically deallocate it. @@ -208,7 +207,7 @@ impl ImeContext { xconn: &Arc, im: &InputMethod, window: ffi::Window, - ic_spot: Option, + ic_area: Option, event_sender: ImeEventSender, allowed: bool, ) -> Result { @@ -244,14 +243,14 @@ impl ImeContext { let mut context = ImeContext { ic, - ic_spot: ffi::XPoint { x: 0, y: 0 }, + ic_area: ffi::XRectangle { x: 0, y: 0, width: 0, height: 0 }, allowed, _client_data: unsafe { Box::from_raw(client_data) }, }; - // Set the spot location, if it's present. - if let Some(ic_spot) = ic_spot { - context.set_spot(xconn, ic_spot.x, ic_spot.y) + // Set the preedit cursor area, if it's present. + if let Some(ic_area) = ic_area { + context.set_area(xconn, ic_area.x, ic_area.y, ic_area.width, ic_area.height); } Ok(context) @@ -355,17 +354,31 @@ impl ImeContext { self.allowed } - // Set the spot for preedit text. Setting spot isn't working with libX11 when preedit callbacks - // are being used. Certain IMEs do show selection window, but it's placed in bottom left of the - // window and couldn't be changed. - // - // For me see: https://bugs.freedesktop.org/show_bug.cgi?id=1580. - pub(crate) fn set_spot(&mut self, xconn: &Arc, x: c_short, y: c_short) { - if !self.is_allowed() || self.ic_spot.x == x && self.ic_spot.y == y { + /// Set the spot and area for preedit text. + /// + /// This functionality depends on the libx11 version. + /// - Until libx11 1.8.2, XNSpotLocation was blocked by libx11 in On-The-Spot mode. + /// - Until libx11 1.8.11, XNArea was blocked by libx11 in On-The-Spot mode. + /// + /// Use of this information is discretionary by input method servers, + /// and some may not use it by default, even if they have support. + pub(crate) fn set_area( + &mut self, + xconn: &Arc, + x: i16, + y: i16, + width: u16, + height: u16, + ) { + let ic_area = ffi::XRectangle { x, y, width, height }; + + if !self.is_allowed() || self.ic_area == ic_area { return; } - self.ic_spot = ffi::XPoint { x, y }; + self.ic_area = ic_area; + let ic_spot = + ffi::XPoint { x: x.saturating_add(width as i16), y: y.saturating_add(height as i16) }; unsafe { let preedit_attr = util::memory::XSmartPointer::new( @@ -373,7 +386,9 @@ impl ImeContext { (xconn.xlib.XVaCreateNestedList)( 0, ffi::XNSpotLocation_0.as_ptr(), - &self.ic_spot, + &ic_spot, + ffi::XNArea_0.as_ptr(), + &self.ic_area, ptr::null_mut::<()>(), ), ) diff --git a/src/platform_impl/linux/x11/ime/mod.rs b/src/platform_impl/linux/x11/ime/mod.rs index 650ccf9e..e1c3e36f 100644 --- a/src/platform_impl/linux/x11/ime/mod.rs +++ b/src/platform_impl/linux/x11/ime/mod.rs @@ -36,8 +36,8 @@ pub type ImeEventSender = Sender<(ffi::Window, ImeEvent)>; /// Request to control XIM handler from the window. pub enum ImeRequest { - /// Set IME spot position for given `window_id`. - Position(ffi::Window, i16, i16), + /// Set IME preedit area for given `window_id`. + Area(ffi::Window, i16, i16, u16, u16), /// Allow IME input for the given `window_id`. Allow(ffi::Window, bool), @@ -192,12 +192,12 @@ impl Ime { } } - pub fn send_xim_spot(&mut self, window: ffi::Window, x: i16, y: i16) { + pub fn send_xim_area(&mut self, window: ffi::Window, x: i16, y: i16, w: u16, h: u16) { if self.is_destroyed() { return; } if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) { - context.set_spot(&self.xconn, x as _, y as _); + context.set_area(&self.xconn, x as _, y as _, w as _, h as _); } } diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index ac184981..f9a39c7c 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -2048,17 +2048,13 @@ impl UnownedWindow { #[inline] pub fn set_ime_cursor_area(&self, spot: Position, size: Size) { let PhysicalPosition { x, y } = spot.to_physical::(self.scale_factor()); - let PhysicalSize { width, height } = size.to_physical::(self.scale_factor()); - // We only currently support reporting a caret position via XIM. - // No IM servers currently process preedit area information from XIM clients - // and it is unclear this is even part of the standard protocol. - // Fcitx and iBus both assume that the position reported is at the insertion - // caret, and by default will place the candidate window under and to the - // right of the reported point. - let _ = self.ime_sender.lock().unwrap().send(ImeRequest::Position( + let PhysicalSize { width, height } = size.to_physical::(self.scale_factor()); + let _ = self.ime_sender.lock().unwrap().send(ImeRequest::Area( self.xwindow as ffi::Window, - x.saturating_add(width), - y.saturating_add(height), + x, + y, + width, + height, )); } diff --git a/src/window.rs b/src/window.rs index 8caf32de..4a1cb1ec 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1064,8 +1064,6 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// /// ## Platform-specific /// - /// - **X11:** Area is not supported, only position. The bottom-right corner of the provided - /// area is reported as the position. /// - **iOS / Android / Web / Orbital:** Unsupported. /// /// [chinese]: https://support.apple.com/guide/chinese-input-method/use-the-candidate-window-cim12992/104/mac/12.0