x11: implement true cursor area with XNArea attribute

This commit is contained in:
Aaron Muir Hamilton 2025-03-11 14:04:50 -04:00 committed by GitHub
parent 16d5f46db1
commit a0464ae83b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 46 additions and 36 deletions

View file

@ -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

View file

@ -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);

View file

@ -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,
)

View file

@ -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<XConnection>,
im: &InputMethod,
window: ffi::Window,
ic_spot: Option<ffi::XPoint>,
ic_area: Option<ffi::XRectangle>,
event_sender: ImeEventSender,
allowed: bool,
) -> Result<Self, ImeContextCreationError> {
@ -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<XConnection>, 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<XConnection>,
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::<()>(),
),
)

View file

@ -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 _);
}
}

View file

@ -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::<i16>(self.scale_factor());
let PhysicalSize { width, height } = size.to_physical::<i16>(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::<u16>(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,
));
}

View file

@ -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