Skip to content

Commit

Permalink
x11: Implement true cursor area with XNArea attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
xorgy committed Mar 8, 2025
1 parent 5cada36 commit b9f504a
Show file tree
Hide file tree
Showing 7 changed files with 48 additions and 35 deletions.
1 change: 1 addition & 0 deletions src/changelog/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ changelog entry.
- `Window::request_inner_size` to `request_surface_size`.
- `Window::set_min_inner_size` to `set_min_surface_size`.
- `Window::set_max_inner_size` to `set_max_surface_size`.
- On X11, set an "area" attribute on XIM input connection to convey the cursor area.

To migrate, you can probably just replace all instances of `inner_size` with `surface_size` in your codebase.
- Every event carrying a `DeviceId` now uses `Option<DeviceId>` instead. A `None` value signifies that the
Expand Down
4 changes: 2 additions & 2 deletions src/platform_impl/linux/x11/event_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/platform_impl/linux/x11/ime/callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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,
)
Expand Down
48 changes: 33 additions & 15 deletions src/platform_impl/linux/x11/ime/context.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -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.
Expand All @@ -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> {
Expand Down Expand Up @@ -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)
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)
Expand Down Expand Up @@ -355,25 +354,44 @@ 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,
) {
if !self.is_allowed()
|| self.ic_area.x == x
&& self.ic_area.y == y
&& self.ic_area.width == width
&& self.ic_area.height == height
{
return;
}

self.ic_spot = ffi::XPoint { x, y };
self.ic_area = ffi::XRectangle { x, y, width, height };
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(
xconn,
(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::<()>(),
),
)
Expand Down
8 changes: 4 additions & 4 deletions src/platform_impl/linux/x11/ime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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 _);
}
}

Expand Down
16 changes: 6 additions & 10 deletions src/platform_impl/linux/x11/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2050,17 +2050,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,
));
}

Expand Down
2 changes: 0 additions & 2 deletions src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit b9f504a

Please sign in to comment.