Add new Ime event for desktop platforms
This commit brings new Ime event to account for preedit state of input method, also adding `Window::set_ime_allowed` to toggle IME input on the particular window. This commit implements API as designed in #1497 for desktop platforms. Co-authored-by: Artur Kovacs <kovacs.artur.barnabas@gmail.com> Co-authored-by: Markus Siglreithmaier <m.siglreith@gmail.com> Co-authored-by: Murarth <murarth@gmail.com> Co-authored-by: Yusuke Kominami <yukke.konan@gmail.com> Co-authored-by: moko256 <koutaro.mo@gmail.com>
This commit is contained in:
parent
b4175c1454
commit
f04fa5d54f
30 changed files with 1346 additions and 311 deletions
|
|
@ -477,6 +477,11 @@ impl Window {
|
|||
x11_or_wayland!(match self; Window(w) => w.set_ime_position(position))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_ime_allowed(&self, allowed: bool) {
|
||||
x11_or_wayland!(match self; Window(w) => w.set_ime_allowed(allowed))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn focus_window(&self) {
|
||||
match self {
|
||||
|
|
|
|||
|
|
@ -32,10 +32,9 @@ mod sink;
|
|||
mod state;
|
||||
|
||||
pub use proxy::EventLoopProxy;
|
||||
pub use sink::EventSink;
|
||||
pub use state::WinitState;
|
||||
|
||||
use sink::EventSink;
|
||||
|
||||
type WinitDispatcher = calloop::Dispatcher<'static, WaylandSource, WinitState>;
|
||||
|
||||
pub struct EventLoopWindowTarget<T> {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ use sctk::reexports::protocols::unstable::text_input::v3::client::zwp_text_input
|
|||
Event as TextInputEvent, ZwpTextInputV3,
|
||||
};
|
||||
|
||||
use crate::event::WindowEvent;
|
||||
use crate::event::{Ime, WindowEvent};
|
||||
use crate::platform_impl::wayland;
|
||||
use crate::platform_impl::wayland::event_loop::WinitState;
|
||||
|
||||
use super::{TextInputHandler, TextInputInner};
|
||||
use super::{Preedit, TextInputHandler, TextInputInner};
|
||||
|
||||
#[inline]
|
||||
pub(super) fn handle_text_input(
|
||||
|
|
@ -30,8 +30,11 @@ pub(super) fn handle_text_input(
|
|||
inner.target_window_id = Some(window_id);
|
||||
|
||||
// Enable text input on that surface.
|
||||
text_input.enable();
|
||||
text_input.commit();
|
||||
if window_handle.ime_allowed.get() {
|
||||
text_input.enable();
|
||||
text_input.commit();
|
||||
event_sink.push_window_event(WindowEvent::Ime(Ime::Enabled), window_id);
|
||||
}
|
||||
|
||||
// Notify a window we're currently over about text input handler.
|
||||
let text_input_handler = TextInputHandler {
|
||||
|
|
@ -58,19 +61,45 @@ pub(super) fn handle_text_input(
|
|||
text_input: text_input.detach(),
|
||||
};
|
||||
window_handle.text_input_left(text_input_handler);
|
||||
event_sink.push_window_event(WindowEvent::Ime(Ime::Disabled), window_id);
|
||||
}
|
||||
TextInputEvent::PreeditString {
|
||||
text,
|
||||
cursor_begin,
|
||||
cursor_end,
|
||||
} => {
|
||||
let cursor_begin = usize::try_from(cursor_begin).ok();
|
||||
let cursor_end = usize::try_from(cursor_end).ok();
|
||||
let text = text.unwrap_or_default();
|
||||
inner.pending_preedit = Some(Preedit {
|
||||
text,
|
||||
cursor_begin,
|
||||
cursor_end,
|
||||
});
|
||||
}
|
||||
TextInputEvent::CommitString { text } => {
|
||||
// Update currenly commited string.
|
||||
inner.commit_string = text;
|
||||
// Update currenly commited string and reset previous preedit.
|
||||
inner.pending_preedit = None;
|
||||
inner.pending_commit = Some(text.unwrap_or_default());
|
||||
}
|
||||
TextInputEvent::Done { .. } => {
|
||||
let (window_id, text) = match (inner.target_window_id, inner.commit_string.take()) {
|
||||
(Some(window_id), Some(text)) => (window_id, text),
|
||||
let window_id = match inner.target_window_id {
|
||||
Some(window_id) => window_id,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
for ch in text.chars() {
|
||||
event_sink.push_window_event(WindowEvent::ReceivedCharacter(ch), window_id);
|
||||
if let Some(text) = inner.pending_commit.take() {
|
||||
event_sink.push_window_event(WindowEvent::Ime(Ime::Commit(text)), window_id);
|
||||
}
|
||||
|
||||
// Push preedit string we've got after latest commit.
|
||||
if let Some(preedit) = inner.pending_preedit.take() {
|
||||
let cursor_range = preedit
|
||||
.cursor_begin
|
||||
.map(|b| (b, preedit.cursor_end.unwrap_or(b)));
|
||||
|
||||
let event = Ime::Preedit(preedit.text, cursor_range);
|
||||
event_sink.push_window_event(WindowEvent::Ime(event), window_id);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,17 @@ impl TextInputHandler {
|
|||
self.text_input.set_cursor_rectangle(x, y, 0, 0);
|
||||
self.text_input.commit();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_input_allowed(&self, allowed: bool) {
|
||||
if allowed {
|
||||
self.text_input.enable();
|
||||
} else {
|
||||
self.text_input.disable();
|
||||
}
|
||||
|
||||
self.text_input.commit();
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around text input to automatically destroy the object on `Drop`.
|
||||
|
|
@ -52,15 +63,25 @@ struct TextInputInner {
|
|||
/// Currently focused surface.
|
||||
target_window_id: Option<WindowId>,
|
||||
|
||||
/// Pending string to commit.
|
||||
commit_string: Option<String>,
|
||||
/// Pending commit event which will be dispatched on `text_input_v3::Done`.
|
||||
pending_commit: Option<String>,
|
||||
|
||||
/// Pending preedit event which will be dispatched on `text_input_v3::Done`.
|
||||
pending_preedit: Option<Preedit>,
|
||||
}
|
||||
|
||||
struct Preedit {
|
||||
text: String,
|
||||
cursor_begin: Option<usize>,
|
||||
cursor_end: Option<usize>,
|
||||
}
|
||||
|
||||
impl TextInputInner {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
target_window_id: None,
|
||||
commit_string: None,
|
||||
pending_commit: None,
|
||||
pending_preedit: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -496,7 +496,12 @@ impl Window {
|
|||
pub fn set_ime_position(&self, position: Position) {
|
||||
let scale_factor = self.scale_factor() as f64;
|
||||
let position = position.to_logical(scale_factor);
|
||||
self.send_request(WindowRequest::IMEPosition(position));
|
||||
self.send_request(WindowRequest::ImePosition(position));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_ime_allowed(&self, allowed: bool) {
|
||||
self.send_request(WindowRequest::AllowIme(allowed));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ use sctk::window::{Decorations, FallbackFrame, Window};
|
|||
|
||||
use crate::dpi::{LogicalPosition, LogicalSize};
|
||||
|
||||
use crate::event::WindowEvent;
|
||||
use crate::event::{Ime, WindowEvent};
|
||||
use crate::platform_impl::wayland;
|
||||
use crate::platform_impl::wayland::env::WinitEnv;
|
||||
use crate::platform_impl::wayland::event_loop::WinitState;
|
||||
use crate::platform_impl::wayland::event_loop::{EventSink, WinitState};
|
||||
use crate::platform_impl::wayland::seat::pointer::WinitPointer;
|
||||
use crate::platform_impl::wayland::seat::text_input::TextInputHandler;
|
||||
use crate::platform_impl::wayland::WindowId;
|
||||
|
|
@ -69,7 +69,10 @@ pub enum WindowRequest {
|
|||
FrameSize(LogicalSize<u32>),
|
||||
|
||||
/// Set IME window position.
|
||||
IMEPosition(LogicalPosition<u32>),
|
||||
ImePosition(LogicalPosition<u32>),
|
||||
|
||||
/// Enable IME on the given window.
|
||||
AllowIme(bool),
|
||||
|
||||
/// Request Attention.
|
||||
///
|
||||
|
|
@ -157,6 +160,9 @@ pub struct WindowHandle {
|
|||
/// Whether the window is resizable.
|
||||
pub is_resizable: Cell<bool>,
|
||||
|
||||
/// Allow IME events for that window.
|
||||
pub ime_allowed: Cell<bool>,
|
||||
|
||||
/// Visible cursor or not.
|
||||
cursor_visible: Cell<bool>,
|
||||
|
||||
|
|
@ -204,6 +210,7 @@ impl WindowHandle {
|
|||
xdg_activation,
|
||||
attention_requested: Cell::new(false),
|
||||
compositor,
|
||||
ime_allowed: Cell::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -333,6 +340,27 @@ impl WindowHandle {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_ime_allowed(&self, allowed: bool, event_sink: &mut EventSink) {
|
||||
if self.ime_allowed.get() == allowed {
|
||||
return;
|
||||
}
|
||||
|
||||
self.ime_allowed.replace(allowed);
|
||||
let window_id = wayland::make_wid(self.window.surface());
|
||||
|
||||
for text_input in self.text_inputs.iter() {
|
||||
text_input.set_input_allowed(allowed);
|
||||
}
|
||||
|
||||
let event = if allowed {
|
||||
WindowEvent::Ime(Ime::Enabled)
|
||||
} else {
|
||||
WindowEvent::Ime(Ime::Disabled)
|
||||
};
|
||||
|
||||
event_sink.push_window_event(event, window_id);
|
||||
}
|
||||
|
||||
pub fn set_cursor_visible(&self, visible: bool) {
|
||||
self.cursor_visible.replace(visible);
|
||||
let cursor_icon = match visible {
|
||||
|
|
@ -387,9 +415,13 @@ pub fn handle_window_requests(winit_state: &mut WinitState) {
|
|||
WindowRequest::NewCursorIcon(cursor_icon) => {
|
||||
window_handle.set_cursor_icon(cursor_icon);
|
||||
}
|
||||
WindowRequest::IMEPosition(position) => {
|
||||
WindowRequest::ImePosition(position) => {
|
||||
window_handle.set_ime_position(position);
|
||||
}
|
||||
WindowRequest::AllowIme(allow) => {
|
||||
let event_sink = &mut winit_state.event_sink;
|
||||
window_handle.set_ime_allowed(allow, event_sink);
|
||||
}
|
||||
WindowRequest::GrabCursor(grab) => {
|
||||
window_handle.set_cursor_grab(grab);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@ use super::{
|
|||
|
||||
use util::modifiers::{ModifierKeyState, ModifierKeymap};
|
||||
|
||||
use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventReceiver, ImeRequest};
|
||||
use crate::{
|
||||
dpi::{PhysicalPosition, PhysicalSize},
|
||||
event::{
|
||||
DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, TouchPhase, WindowEvent,
|
||||
DeviceEvent, ElementState, Event, Ime, KeyboardInput, ModifiersState, TouchPhase,
|
||||
WindowEvent,
|
||||
},
|
||||
event_loop::EventLoopWindowTarget as RootELW,
|
||||
};
|
||||
|
|
@ -26,6 +28,7 @@ const KEYCODE_OFFSET: u8 = 8;
|
|||
pub(super) struct EventProcessor<T: 'static> {
|
||||
pub(super) dnd: Dnd,
|
||||
pub(super) ime_receiver: ImeReceiver,
|
||||
pub(super) ime_event_receiver: ImeEventReceiver,
|
||||
pub(super) randr_event_offset: c_int,
|
||||
pub(super) devices: RefCell<HashMap<DeviceId, Device>>,
|
||||
pub(super) xi2ext: XExtension,
|
||||
|
|
@ -37,6 +40,7 @@ pub(super) struct EventProcessor<T: 'static> {
|
|||
pub(super) first_touch: Option<u64>,
|
||||
// Currently focused window belonging to this process
|
||||
pub(super) active_window: Option<ffi::Window>,
|
||||
pub(super) is_composing: bool,
|
||||
}
|
||||
|
||||
impl<T: 'static> EventProcessor<T> {
|
||||
|
|
@ -567,7 +571,7 @@ impl<T: 'static> EventProcessor<T> {
|
|||
|
||||
// When a compose sequence or IME pre-edit is finished, it ends in a KeyPress with
|
||||
// a keycode of 0.
|
||||
if keycode != 0 {
|
||||
if keycode != 0 && !self.is_composing {
|
||||
let scancode = keycode - KEYCODE_OFFSET as u32;
|
||||
let keysym = wt.xconn.lookup_keysym(xkev);
|
||||
let virtual_keycode = events::keysym_to_element(keysym as c_uint);
|
||||
|
|
@ -602,12 +606,25 @@ impl<T: 'static> EventProcessor<T> {
|
|||
return;
|
||||
};
|
||||
|
||||
for chr in written.chars() {
|
||||
// If we're composing right now, send the string we've got from X11 via
|
||||
// Ime::Commit.
|
||||
if self.is_composing && keycode == 0 && !written.is_empty() {
|
||||
let event = Event::WindowEvent {
|
||||
window_id,
|
||||
event: WindowEvent::ReceivedCharacter(chr),
|
||||
event: WindowEvent::Ime(Ime::Commit(written)),
|
||||
};
|
||||
|
||||
self.is_composing = false;
|
||||
callback(event);
|
||||
} else {
|
||||
for chr in written.chars() {
|
||||
let event = Event::WindowEvent {
|
||||
window_id,
|
||||
event: WindowEvent::ReceivedCharacter(chr),
|
||||
};
|
||||
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1223,8 +1240,59 @@ impl<T: 'static> EventProcessor<T> {
|
|||
}
|
||||
}
|
||||
|
||||
if let Ok((window_id, x, y)) = self.ime_receiver.try_recv() {
|
||||
wt.ime.borrow_mut().send_xim_spot(window_id, x, y);
|
||||
// Handle IME requests.
|
||||
if let Ok(request) = self.ime_receiver.try_recv() {
|
||||
let mut ime = wt.ime.borrow_mut();
|
||||
match request {
|
||||
ImeRequest::Position(window_id, x, y) => {
|
||||
ime.send_xim_spot(window_id, x, y);
|
||||
}
|
||||
ImeRequest::Allow(window_id, allowed) => {
|
||||
ime.set_ime_allowed(window_id, allowed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match self.ime_event_receiver.try_recv() {
|
||||
Ok((window, event)) => match event {
|
||||
ImeEvent::Enabled => {
|
||||
callback(Event::WindowEvent {
|
||||
window_id: mkwid(window),
|
||||
event: WindowEvent::Ime(Ime::Enabled),
|
||||
});
|
||||
}
|
||||
ImeEvent::Start => {
|
||||
self.is_composing = true;
|
||||
callback(Event::WindowEvent {
|
||||
window_id: mkwid(window),
|
||||
event: WindowEvent::Ime(Ime::Preedit("".to_owned(), None)),
|
||||
});
|
||||
}
|
||||
ImeEvent::Update(text, position) => {
|
||||
if self.is_composing {
|
||||
callback(Event::WindowEvent {
|
||||
window_id: mkwid(window),
|
||||
event: WindowEvent::Ime(Ime::Preedit(text, Some((position, position)))),
|
||||
});
|
||||
}
|
||||
}
|
||||
ImeEvent::End => {
|
||||
self.is_composing = false;
|
||||
// Issue empty preedit on `Done`.
|
||||
callback(Event::WindowEvent {
|
||||
window_id: mkwid(window),
|
||||
event: WindowEvent::Ime(Ime::Preedit(String::new(), None)),
|
||||
});
|
||||
}
|
||||
ImeEvent::Disabled => {
|
||||
self.is_composing = false;
|
||||
callback(Event::WindowEvent {
|
||||
window_id: mkwid(window),
|
||||
event: WindowEvent::Ime(Ime::Disabled),
|
||||
});
|
||||
}
|
||||
},
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -108,8 +108,19 @@ unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> {
|
|||
let mut new_contexts = HashMap::new();
|
||||
for (window, old_context) in (*inner).contexts.iter() {
|
||||
let spot = old_context.as_ref().map(|old_context| old_context.ic_spot);
|
||||
let is_allowed = old_context
|
||||
.as_ref()
|
||||
.map(|old_context| old_context.is_allowed)
|
||||
.unwrap_or_default();
|
||||
let new_context = {
|
||||
let result = ImeContext::new(xconn, new_im.im, *window, spot);
|
||||
let result = ImeContext::new(
|
||||
xconn,
|
||||
new_im.im,
|
||||
*window,
|
||||
spot,
|
||||
is_allowed,
|
||||
(*inner).event_sender.clone(),
|
||||
);
|
||||
if result.is_err() {
|
||||
let _ = close_im(xconn, new_im.im);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,196 @@
|
|||
use std::{
|
||||
os::raw::{c_short, c_void},
|
||||
ptr,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::mem::transmute;
|
||||
use std::os::raw::c_short;
|
||||
use std::ptr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{ffi, util, XConnection, XError};
|
||||
use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender};
|
||||
use std::ffi::CStr;
|
||||
use x11_dl::xlib::{XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct};
|
||||
|
||||
/// IME creation error.
|
||||
#[derive(Debug)]
|
||||
pub enum ImeContextCreationError {
|
||||
/// Got the error from Xlib.
|
||||
XError(XError),
|
||||
|
||||
/// Got null pointer from Xlib but without exact reason.
|
||||
Null,
|
||||
}
|
||||
|
||||
unsafe fn create_pre_edit_attr<'a>(
|
||||
xconn: &'a Arc<XConnection>,
|
||||
ic_spot: &'a ffi::XPoint,
|
||||
) -> util::XSmartPointer<'a, c_void> {
|
||||
util::XSmartPointer::new(
|
||||
xconn,
|
||||
(xconn.xlib.XVaCreateNestedList)(
|
||||
0,
|
||||
ffi::XNSpotLocation_0.as_ptr() as *const _,
|
||||
ic_spot,
|
||||
ptr::null_mut::<()>(),
|
||||
),
|
||||
)
|
||||
.expect("XVaCreateNestedList returned NULL")
|
||||
/// The callback used by XIM preedit functions.
|
||||
type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer);
|
||||
|
||||
/// Wrapper for creating XIM callbacks.
|
||||
#[inline]
|
||||
fn create_xim_callback(client_data: ffi::XPointer, callback: XIMProcNonnull) -> ffi::XIMCallback {
|
||||
XIMCallback {
|
||||
client_data,
|
||||
callback: Some(callback),
|
||||
}
|
||||
}
|
||||
|
||||
// WARNING: this struct doesn't destroy its XIC resource when dropped.
|
||||
/// The server started preedit.
|
||||
extern "C" fn preedit_start_callback(
|
||||
_xim: ffi::XIM,
|
||||
client_data: ffi::XPointer,
|
||||
_call_data: ffi::XPointer,
|
||||
) -> i32 {
|
||||
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
|
||||
|
||||
client_data.text.clear();
|
||||
client_data.cursor_pos = 0;
|
||||
client_data
|
||||
.event_sender
|
||||
.send((client_data.window, ImeEvent::Start))
|
||||
.expect("failed to send preedit start event");
|
||||
-1
|
||||
}
|
||||
|
||||
/// Done callback is used when the preedit should be hidden.
|
||||
extern "C" fn preedit_done_callback(
|
||||
_xim: ffi::XIM,
|
||||
client_data: ffi::XPointer,
|
||||
_call_data: ffi::XPointer,
|
||||
) {
|
||||
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
|
||||
|
||||
// Drop text buffer and reset cursor position on done.
|
||||
client_data.text = Vec::new();
|
||||
client_data.cursor_pos = 0;
|
||||
|
||||
client_data
|
||||
.event_sender
|
||||
.send((client_data.window, ImeEvent::End))
|
||||
.expect("failed to send preedit end event");
|
||||
}
|
||||
|
||||
fn calc_byte_position(text: &Vec<char>, pos: usize) -> usize {
|
||||
let mut byte_pos = 0;
|
||||
for i in 0..pos {
|
||||
byte_pos += text[i].len_utf8();
|
||||
}
|
||||
byte_pos
|
||||
}
|
||||
|
||||
/// Preedit text information to be drawn inline by the client.
|
||||
extern "C" fn preedit_draw_callback(
|
||||
_xim: ffi::XIM,
|
||||
client_data: ffi::XPointer,
|
||||
call_data: ffi::XPointer,
|
||||
) {
|
||||
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
|
||||
let call_data = unsafe { &mut *(call_data as *mut XIMPreeditDrawCallbackStruct) };
|
||||
client_data.cursor_pos = call_data.caret as usize;
|
||||
|
||||
let chg_range =
|
||||
call_data.chg_first as usize..(call_data.chg_first + call_data.chg_length) as usize;
|
||||
if chg_range.start > client_data.text.len() || chg_range.end > client_data.text.len() {
|
||||
warn!(
|
||||
"invalid chg range: buffer length={}, but chg_first={} chg_lengthg={}",
|
||||
client_data.text.len(),
|
||||
call_data.chg_first,
|
||||
call_data.chg_length
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// NULL indicate text deletion
|
||||
let mut new_chars = if call_data.text.is_null() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let xim_text = unsafe { &mut *(call_data.text) };
|
||||
if xim_text.encoding_is_wchar > 0 {
|
||||
return;
|
||||
}
|
||||
let new_text = unsafe { CStr::from_ptr(xim_text.string.multi_byte) };
|
||||
|
||||
String::from(new_text.to_str().expect("Invalid UTF-8 String from IME"))
|
||||
.chars()
|
||||
.collect()
|
||||
};
|
||||
let mut old_text_tail = client_data.text.split_off(chg_range.end);
|
||||
client_data.text.truncate(chg_range.start);
|
||||
client_data.text.append(&mut new_chars);
|
||||
client_data.text.append(&mut old_text_tail);
|
||||
let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
|
||||
|
||||
client_data
|
||||
.event_sender
|
||||
.send((
|
||||
client_data.window,
|
||||
ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
|
||||
))
|
||||
.expect("failed to send preedit update event");
|
||||
}
|
||||
|
||||
/// Handling of cursor movements in preedit text.
|
||||
extern "C" fn preedit_caret_callback(
|
||||
_xim: ffi::XIM,
|
||||
client_data: ffi::XPointer,
|
||||
call_data: ffi::XPointer,
|
||||
) {
|
||||
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
|
||||
let call_data = unsafe { &mut *(call_data as *mut XIMPreeditCaretCallbackStruct) };
|
||||
|
||||
if call_data.direction == ffi::XIMCaretDirection::XIMAbsolutePosition {
|
||||
client_data.cursor_pos = call_data.position as usize;
|
||||
let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
|
||||
|
||||
client_data
|
||||
.event_sender
|
||||
.send((
|
||||
client_data.window,
|
||||
ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
|
||||
))
|
||||
.expect("failed to send preedit update event");
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct to simplify callback creation and latter passing into Xlib XIM.
|
||||
struct PreeditCallbacks {
|
||||
start_callback: ffi::XIMCallback,
|
||||
done_callback: ffi::XIMCallback,
|
||||
draw_callback: ffi::XIMCallback,
|
||||
caret_callback: ffi::XIMCallback,
|
||||
}
|
||||
|
||||
impl PreeditCallbacks {
|
||||
pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks {
|
||||
let start_callback = create_xim_callback(client_data, unsafe {
|
||||
transmute(preedit_start_callback as usize)
|
||||
});
|
||||
let done_callback = create_xim_callback(client_data, preedit_done_callback);
|
||||
let caret_callback = create_xim_callback(client_data, preedit_caret_callback);
|
||||
let draw_callback = create_xim_callback(client_data, preedit_draw_callback);
|
||||
|
||||
PreeditCallbacks {
|
||||
start_callback,
|
||||
done_callback,
|
||||
caret_callback,
|
||||
draw_callback,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImeContextClientData {
|
||||
window: ffi::Window,
|
||||
event_sender: ImeEventSender,
|
||||
text: Vec<char>,
|
||||
cursor_pos: usize,
|
||||
}
|
||||
|
||||
// XXX: this struct doesn't destroy its XIC resource when dropped.
|
||||
// This is intentional, as it doesn't have enough information to know whether or not the context
|
||||
// still exists on the server. Since `ImeInner` has that awareness, destruction must be handled
|
||||
// through `ImeInner`.
|
||||
#[derive(Debug)]
|
||||
pub struct ImeContext {
|
||||
pub ic: ffi::XIC,
|
||||
pub ic_spot: ffi::XPoint,
|
||||
pub(super) ic: ffi::XIC,
|
||||
pub(super) ic_spot: ffi::XPoint,
|
||||
pub(super) is_allowed: bool,
|
||||
// Since the data is passed shared between X11 XIM callbacks, but couldn't be direclty free from
|
||||
// there we keep the pointer to automatically deallocate it.
|
||||
_client_data: Box<ImeContextClientData>,
|
||||
}
|
||||
|
||||
impl ImeContext {
|
||||
|
|
@ -44,66 +199,111 @@ impl ImeContext {
|
|||
im: ffi::XIM,
|
||||
window: ffi::Window,
|
||||
ic_spot: Option<ffi::XPoint>,
|
||||
is_allowed: bool,
|
||||
event_sender: ImeEventSender,
|
||||
) -> Result<Self, ImeContextCreationError> {
|
||||
let ic = if let Some(ic_spot) = ic_spot {
|
||||
ImeContext::create_ic_with_spot(xconn, im, window, ic_spot)
|
||||
let client_data = Box::into_raw(Box::new(ImeContextClientData {
|
||||
window,
|
||||
event_sender,
|
||||
text: Vec::new(),
|
||||
cursor_pos: 0,
|
||||
}));
|
||||
|
||||
let ic = if is_allowed {
|
||||
ImeContext::create_ic(xconn, im, window, client_data as ffi::XPointer)
|
||||
.ok_or(ImeContextCreationError::Null)?
|
||||
} else {
|
||||
ImeContext::create_ic(xconn, im, window)
|
||||
ImeContext::create_none_ic(xconn, im, window).ok_or(ImeContextCreationError::Null)?
|
||||
};
|
||||
|
||||
let ic = ic.ok_or(ImeContextCreationError::Null)?;
|
||||
xconn
|
||||
.check_errors()
|
||||
.map_err(ImeContextCreationError::XError)?;
|
||||
|
||||
Ok(ImeContext {
|
||||
let mut context = ImeContext {
|
||||
ic,
|
||||
ic_spot: ic_spot.unwrap_or(ffi::XPoint { x: 0, y: 0 }),
|
||||
})
|
||||
ic_spot: ffi::XPoint { x: 0, y: 0 },
|
||||
is_allowed,
|
||||
_client_data: 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)
|
||||
}
|
||||
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
unsafe fn create_none_ic(
|
||||
xconn: &Arc<XConnection>,
|
||||
im: ffi::XIM,
|
||||
window: ffi::Window,
|
||||
) -> Option<ffi::XIC> {
|
||||
let ic = (xconn.xlib.XCreateIC)(
|
||||
im,
|
||||
ffi::XNInputStyle_0.as_ptr() as *const _,
|
||||
ffi::XIMPreeditNone | ffi::XIMStatusNone,
|
||||
ffi::XNClientWindow_0.as_ptr() as *const _,
|
||||
window,
|
||||
ptr::null_mut::<()>(),
|
||||
);
|
||||
|
||||
(!ic.is_null()).then(|| ic)
|
||||
}
|
||||
|
||||
unsafe fn create_ic(
|
||||
xconn: &Arc<XConnection>,
|
||||
im: ffi::XIM,
|
||||
window: ffi::Window,
|
||||
client_data: ffi::XPointer,
|
||||
) -> Option<ffi::XIC> {
|
||||
let ic = (xconn.xlib.XCreateIC)(
|
||||
im,
|
||||
ffi::XNInputStyle_0.as_ptr() as *const _,
|
||||
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
|
||||
ffi::XNClientWindow_0.as_ptr() as *const _,
|
||||
window,
|
||||
ptr::null_mut::<()>(),
|
||||
);
|
||||
if ic.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(ic)
|
||||
}
|
||||
}
|
||||
let preedit_callbacks = PreeditCallbacks::new(client_data);
|
||||
let preedit_attr = util::XSmartPointer::new(
|
||||
xconn,
|
||||
(xconn.xlib.XVaCreateNestedList)(
|
||||
0,
|
||||
ffi::XNPreeditStartCallback_0.as_ptr() as *const _,
|
||||
&(preedit_callbacks.start_callback) as *const _,
|
||||
ffi::XNPreeditDoneCallback_0.as_ptr() as *const _,
|
||||
&(preedit_callbacks.done_callback) as *const _,
|
||||
ffi::XNPreeditCaretCallback_0.as_ptr() as *const _,
|
||||
&(preedit_callbacks.caret_callback) as *const _,
|
||||
ffi::XNPreeditDrawCallback_0.as_ptr() as *const _,
|
||||
&(preedit_callbacks.draw_callback) as *const _,
|
||||
ptr::null_mut::<()>(),
|
||||
),
|
||||
)
|
||||
.expect("XVaCreateNestedList returned NULL");
|
||||
|
||||
unsafe fn create_ic_with_spot(
|
||||
xconn: &Arc<XConnection>,
|
||||
im: ffi::XIM,
|
||||
window: ffi::Window,
|
||||
ic_spot: ffi::XPoint,
|
||||
) -> Option<ffi::XIC> {
|
||||
let pre_edit_attr = create_pre_edit_attr(xconn, &ic_spot);
|
||||
let ic = (xconn.xlib.XCreateIC)(
|
||||
im,
|
||||
ffi::XNInputStyle_0.as_ptr() as *const _,
|
||||
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
|
||||
ffi::XNClientWindow_0.as_ptr() as *const _,
|
||||
window,
|
||||
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
|
||||
pre_edit_attr.ptr,
|
||||
ptr::null_mut::<()>(),
|
||||
);
|
||||
if ic.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(ic)
|
||||
}
|
||||
let ic = {
|
||||
let ic = (xconn.xlib.XCreateIC)(
|
||||
im,
|
||||
ffi::XNInputStyle_0.as_ptr() as *const _,
|
||||
ffi::XIMPreeditCallbacks | ffi::XIMStatusNothing,
|
||||
ffi::XNClientWindow_0.as_ptr() as *const _,
|
||||
window,
|
||||
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
|
||||
preedit_attr.ptr,
|
||||
ptr::null_mut::<()>(),
|
||||
);
|
||||
|
||||
// If we've failed to create IC with preedit callbacks fallback to normal one.
|
||||
if ic.is_null() {
|
||||
(xconn.xlib.XCreateIC)(
|
||||
im,
|
||||
ffi::XNInputStyle_0.as_ptr() as *const _,
|
||||
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
|
||||
ffi::XNClientWindow_0.as_ptr() as *const _,
|
||||
window,
|
||||
ptr::null_mut::<()>(),
|
||||
)
|
||||
} else {
|
||||
ic
|
||||
}
|
||||
};
|
||||
|
||||
(!ic.is_null()).then(|| ic)
|
||||
}
|
||||
|
||||
pub fn focus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
|
||||
|
|
@ -120,18 +320,34 @@ impl ImeContext {
|
|||
xconn.check_errors()
|
||||
}
|
||||
|
||||
// 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 fn set_spot(&mut self, xconn: &Arc<XConnection>, x: c_short, y: c_short) {
|
||||
if self.ic_spot.x == x && self.ic_spot.y == y {
|
||||
if !self.is_allowed || self.ic_spot.x == x && self.ic_spot.y == y {
|
||||
return;
|
||||
}
|
||||
|
||||
self.ic_spot = ffi::XPoint { x, y };
|
||||
|
||||
unsafe {
|
||||
let pre_edit_attr = create_pre_edit_attr(xconn, &self.ic_spot);
|
||||
let preedit_attr = util::XSmartPointer::new(
|
||||
xconn,
|
||||
(xconn.xlib.XVaCreateNestedList)(
|
||||
0,
|
||||
ffi::XNSpotLocation_0.as_ptr(),
|
||||
&self.ic_spot,
|
||||
ptr::null_mut::<()>(),
|
||||
),
|
||||
)
|
||||
.expect("XVaCreateNestedList returned NULL");
|
||||
|
||||
(xconn.xlib.XSetICValues)(
|
||||
self.ic,
|
||||
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
|
||||
pre_edit_attr.ptr,
|
||||
preedit_attr.ptr,
|
||||
ptr::null_mut::<()>(),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use std::{collections::HashMap, mem, ptr, sync::Arc};
|
|||
use super::{ffi, XConnection, XError};
|
||||
|
||||
use super::{context::ImeContext, input_method::PotentialInputMethods};
|
||||
use crate::platform_impl::platform::x11::ime::ImeEventSender;
|
||||
|
||||
pub unsafe fn close_im(xconn: &Arc<XConnection>, im: ffi::XIM) -> Result<(), XError> {
|
||||
(xconn.xlib.XCloseIM)(im);
|
||||
|
|
@ -22,6 +23,7 @@ pub struct ImeInner {
|
|||
pub contexts: HashMap<ffi::Window, Option<ImeContext>>,
|
||||
// WARNING: this is initially zeroed!
|
||||
pub destroy_callback: ffi::XIMCallback,
|
||||
pub event_sender: ImeEventSender,
|
||||
// Indicates whether or not the the input method was destroyed on the server end
|
||||
// (i.e. if ibus/fcitx/etc. was terminated/restarted)
|
||||
pub is_destroyed: bool,
|
||||
|
|
@ -29,13 +31,18 @@ pub struct ImeInner {
|
|||
}
|
||||
|
||||
impl ImeInner {
|
||||
pub fn new(xconn: Arc<XConnection>, potential_input_methods: PotentialInputMethods) -> Self {
|
||||
pub fn new(
|
||||
xconn: Arc<XConnection>,
|
||||
potential_input_methods: PotentialInputMethods,
|
||||
event_sender: ImeEventSender,
|
||||
) -> Self {
|
||||
ImeInner {
|
||||
xconn,
|
||||
im: ptr::null_mut(),
|
||||
potential_input_methods,
|
||||
contexts: HashMap::new(),
|
||||
destroy_callback: unsafe { mem::zeroed() },
|
||||
event_sender,
|
||||
is_destroyed: false,
|
||||
is_fallback: false,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,29 @@ use self::{
|
|||
inner::{close_im, ImeInner},
|
||||
input_method::PotentialInputMethods,
|
||||
};
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum ImeEvent {
|
||||
Enabled,
|
||||
Start,
|
||||
Update(String, usize),
|
||||
End,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
pub type ImeReceiver = Receiver<(ffi::Window, i16, i16)>;
|
||||
pub type ImeSender = Sender<(ffi::Window, i16, i16)>;
|
||||
pub type ImeReceiver = Receiver<ImeRequest>;
|
||||
pub type ImeSender = Sender<ImeRequest>;
|
||||
pub type ImeEventReceiver = Receiver<(ffi::Window, ImeEvent)>;
|
||||
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),
|
||||
|
||||
/// Allow IME input for the given `window_id`.
|
||||
Allow(ffi::Window, bool),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ImeCreationError {
|
||||
|
|
@ -37,11 +57,14 @@ pub struct Ime {
|
|||
}
|
||||
|
||||
impl Ime {
|
||||
pub fn new(xconn: Arc<XConnection>) -> Result<Self, ImeCreationError> {
|
||||
pub fn new(
|
||||
xconn: Arc<XConnection>,
|
||||
event_sender: ImeEventSender,
|
||||
) -> Result<Self, ImeCreationError> {
|
||||
let potential_input_methods = PotentialInputMethods::new(&xconn);
|
||||
|
||||
let (mut inner, client_data) = {
|
||||
let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods));
|
||||
let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods, event_sender));
|
||||
let inner_ptr = Box::into_raw(inner);
|
||||
let client_data = inner_ptr as _;
|
||||
let destroy_callback = ffi::XIMCallback {
|
||||
|
|
@ -88,12 +111,37 @@ impl Ime {
|
|||
// Ok(_) indicates that nothing went wrong internally
|
||||
// Ok(true) indicates that the action was actually performed
|
||||
// Ok(false) indicates that the action is not presently applicable
|
||||
pub fn create_context(&mut self, window: ffi::Window) -> Result<bool, ImeContextCreationError> {
|
||||
pub fn create_context(
|
||||
&mut self,
|
||||
window: ffi::Window,
|
||||
with_preedit: bool,
|
||||
) -> Result<bool, ImeContextCreationError> {
|
||||
let context = if self.is_destroyed() {
|
||||
// Create empty entry in map, so that when IME is rebuilt, this window has a context.
|
||||
None
|
||||
} else {
|
||||
Some(unsafe { ImeContext::new(&self.inner.xconn, self.inner.im, window, None) }?)
|
||||
let event = if with_preedit {
|
||||
ImeEvent::Enabled
|
||||
} else {
|
||||
// There's no IME without preedit.
|
||||
ImeEvent::Disabled
|
||||
};
|
||||
|
||||
self.inner
|
||||
.event_sender
|
||||
.send((window, event))
|
||||
.expect("Failed to send enabled event");
|
||||
|
||||
Some(unsafe {
|
||||
ImeContext::new(
|
||||
&self.inner.xconn,
|
||||
self.inner.im,
|
||||
window,
|
||||
None,
|
||||
with_preedit,
|
||||
self.inner.event_sender.clone(),
|
||||
)
|
||||
}?)
|
||||
};
|
||||
self.inner.contexts.insert(window, context);
|
||||
Ok(!self.is_destroyed())
|
||||
|
|
@ -151,6 +199,24 @@ impl Ime {
|
|||
context.set_spot(&self.xconn, x as _, y as _);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_ime_allowed(&mut self, window: ffi::Window, allowed: bool) {
|
||||
if self.is_destroyed() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
|
||||
if allowed == context.is_allowed {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove context for that window.
|
||||
let _ = self.remove_context(window);
|
||||
|
||||
// Create new context supporting IME input.
|
||||
let _ = self.create_context(window, allowed);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Ime {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ use mio::{unix::SourceFd, Events, Interest, Poll, Token, Waker};
|
|||
use self::{
|
||||
dnd::{Dnd, DndState},
|
||||
event_processor::EventProcessor,
|
||||
ime::{Ime, ImeCreationError, ImeReceiver, ImeSender},
|
||||
ime::{Ime, ImeCreationError, ImeReceiver, ImeRequest, ImeSender},
|
||||
util::modifiers::ModifierKeymap,
|
||||
};
|
||||
use crate::{
|
||||
|
|
@ -144,6 +144,7 @@ impl<T: 'static> EventLoop<T> {
|
|||
.expect("Failed to call XInternAtoms when initializing drag and drop");
|
||||
|
||||
let (ime_sender, ime_receiver) = mpsc::channel();
|
||||
let (ime_event_sender, ime_event_receiver) = mpsc::channel();
|
||||
// Input methods will open successfully without setting the locale, but it won't be
|
||||
// possible to actually commit pre-edit sequences.
|
||||
unsafe {
|
||||
|
|
@ -168,7 +169,7 @@ impl<T: 'static> EventLoop<T> {
|
|||
}
|
||||
}
|
||||
let ime = RefCell::new({
|
||||
let result = Ime::new(Arc::clone(&xconn));
|
||||
let result = Ime::new(Arc::clone(&xconn), ime_event_sender);
|
||||
if let Err(ImeCreationError::OpenFailure(ref state)) = result {
|
||||
panic!("Failed to open input method: {:#?}", state);
|
||||
}
|
||||
|
|
@ -252,12 +253,14 @@ impl<T: 'static> EventLoop<T> {
|
|||
devices: Default::default(),
|
||||
randr_event_offset,
|
||||
ime_receiver,
|
||||
ime_event_receiver,
|
||||
xi2ext,
|
||||
mod_keymap,
|
||||
device_mod_state: Default::default(),
|
||||
num_touch: 0,
|
||||
first_touch: None,
|
||||
active_window: None,
|
||||
is_composing: false,
|
||||
};
|
||||
|
||||
// Register for device hotplug events
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ use crate::{
|
|||
};
|
||||
|
||||
use super::{
|
||||
ffi, util, EventLoopWindowTarget, ImeSender, WakeSender, WindowId, XConnection, XError,
|
||||
ffi, util, EventLoopWindowTarget, ImeRequest, ImeSender, WakeSender, WindowId, XConnection,
|
||||
XError,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -453,7 +454,10 @@ impl UnownedWindow {
|
|||
.queue();
|
||||
|
||||
{
|
||||
let result = event_loop.ime.borrow_mut().create_context(window.xwindow);
|
||||
let result = event_loop
|
||||
.ime
|
||||
.borrow_mut()
|
||||
.create_context(window.xwindow, false);
|
||||
if let Err(err) = result {
|
||||
let e = match err {
|
||||
ImeContextCreationError::XError(err) => OsError::XError(err),
|
||||
|
|
@ -1410,17 +1414,21 @@ impl UnownedWindow {
|
|||
.map_err(|err| ExternalError::Os(os_error!(OsError::XError(err))))
|
||||
}
|
||||
|
||||
pub(crate) fn set_ime_position_physical(&self, x: i32, y: i32) {
|
||||
let _ = self
|
||||
.ime_sender
|
||||
.lock()
|
||||
.send((self.xwindow, x as i16, y as i16));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_ime_position(&self, spot: Position) {
|
||||
let (x, y) = spot.to_physical::<i32>(self.scale_factor()).into();
|
||||
self.set_ime_position_physical(x, y);
|
||||
let _ = self
|
||||
.ime_sender
|
||||
.lock()
|
||||
.send(ImeRequest::Position(self.xwindow, x, y));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_ime_allowed(&self, allowed: bool) {
|
||||
let _ = self
|
||||
.ime_sender
|
||||
.lock()
|
||||
.send(ImeRequest::Allow(self.xwindow, allowed));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue