Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a90407a

Browse files
kchibisovArturKovacsmurarthkomi1230moko256
committedApr 14, 2022
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: Murarth <murarth@gmail.com> Co-authored-by: Yusuke Kominami <yukke.konan@gmail.com> Co-authored-by: moko256 <koutaro.mo@gmail.com>
1 parent ab1f636 commit a90407a

30 files changed

+1241
-279
lines changed
 

‎CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ And please only add new entries to the top of this list, right below the `# Unre
3232
- On Wayland, fix polling during consecutive `EventLoop::run_return` invocations.
3333
- On Windows, fix race issue creating fullscreen windows with `WindowBuilder::with_fullscreen`
3434
- On Android, `virtual_keycode` for `KeyboardInput` events is now filled in where a suitable match is found.
35+
- **Breaking:** Added new `WindowEvent::IME` supported on desktop platforms.
36+
- Added `Window::set_ime_allowed` supported on desktop platforms.
37+
- **Breaking:** IME input on desktop platforms won't be received unless it's explicitly allowed via `Window::set_ime_allowed` and new `WindowEvent::IME` events are handled.
3538

3639
# 0.26.1 (2022-01-05)
3740

‎Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ features = [
8787
]
8888

8989
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies]
90-
wayland-client = { version = "0.29", default_features = false, features = ["use_system_lib"], optional = true }
91-
wayland-protocols = { version = "0.29", features = [ "staging_protocols"], optional = true }
90+
wayland-client = { version = "0.29.4", default_features = false, features = ["use_system_lib"], optional = true }
91+
wayland-protocols = { version = "0.29.4", features = [ "staging_protocols"], optional = true }
9292
sctk = { package = "smithay-client-toolkit", version = "0.15.1", default_features = false, features = ["calloop"], optional = true }
9393
mio = { version = "0.8", features = ["os-ext"], optional = true }
9494
x11-dl = { version = "2.18.5", optional = true }

‎examples/ime.rs

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
use log::LevelFilter;
2+
use simple_logger::SimpleLogger;
3+
use winit::{
4+
dpi::PhysicalPosition,
5+
event::{ElementState, Event, VirtualKeyCode, WindowEvent, IME},
6+
event_loop::{ControlFlow, EventLoop},
7+
window::WindowBuilder,
8+
};
9+
10+
fn main() {
11+
SimpleLogger::new()
12+
.with_level(LevelFilter::Trace)
13+
.init()
14+
.unwrap();
15+
16+
println!("Ime position will system default");
17+
println!("Click to set ime position to cursor's");
18+
println!("Press F2 to toggle IME. See the documentation of `set_ime_allowed` for more info");
19+
20+
let event_loop = EventLoop::new();
21+
22+
let window = WindowBuilder::new()
23+
.with_inner_size(winit::dpi::LogicalSize::new(256f64, 128f64))
24+
.build(&event_loop)
25+
.unwrap();
26+
27+
let mut ime_allowed = true;
28+
window.set_ime_allowed(ime_allowed);
29+
30+
let mut may_show_ime = false;
31+
let mut cursor_position = PhysicalPosition::new(0.0, 0.0);
32+
let mut ime_pos = PhysicalPosition::new(0.0, 0.0);
33+
34+
event_loop.run(move |event, _, control_flow| {
35+
*control_flow = ControlFlow::Wait;
36+
match event {
37+
Event::WindowEvent {
38+
event: WindowEvent::CloseRequested,
39+
..
40+
} => *control_flow = ControlFlow::Exit,
41+
Event::WindowEvent {
42+
event: WindowEvent::CursorMoved { position, .. },
43+
..
44+
} => {
45+
cursor_position = position;
46+
}
47+
Event::WindowEvent {
48+
event:
49+
WindowEvent::MouseInput {
50+
state: ElementState::Released,
51+
..
52+
},
53+
..
54+
} => {
55+
println!(
56+
"Setting ime position to {}, {}",
57+
cursor_position.x, cursor_position.y
58+
);
59+
ime_pos = cursor_position;
60+
if may_show_ime {
61+
window.set_ime_position(ime_pos);
62+
}
63+
}
64+
Event::WindowEvent {
65+
event: WindowEvent::IME(event),
66+
..
67+
} => {
68+
println!("{:?}", event);
69+
may_show_ime = event != IME::Disabled;
70+
if may_show_ime {
71+
window.set_ime_position(ime_pos);
72+
}
73+
}
74+
Event::WindowEvent {
75+
event: WindowEvent::ReceivedCharacter(ch),
76+
..
77+
} => {
78+
println!("ch: {:?}", ch);
79+
}
80+
Event::WindowEvent {
81+
event: WindowEvent::KeyboardInput { input, .. },
82+
..
83+
} => {
84+
println!("key: {:?}", input);
85+
86+
if input.state == ElementState::Pressed
87+
&& input.virtual_keycode == Some(VirtualKeyCode::F2)
88+
{
89+
ime_allowed = !ime_allowed;
90+
window.set_ime_allowed(ime_allowed);
91+
println!("\nIME: {}\n", ime_allowed);
92+
}
93+
}
94+
_ => (),
95+
}
96+
});
97+
}

‎examples/set_ime_position.rs

-53
This file was deleted.

‎src/event.rs

+30-1
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,12 @@ pub enum WindowEvent<'a> {
270270
/// issue, and it should get fixed - but it's the current state of the API.
271271
ModifiersChanged(ModifiersState),
272272

273+
/// An event from input method.
274+
///
275+
/// Platform-specific behavior:
276+
/// - **iOS / Android / Web :** Unsupported.
277+
IME(IME),
278+
273279
/// The cursor has moved on the window.
274280
CursorMoved {
275281
device_id: DeviceId,
@@ -376,7 +382,7 @@ impl Clone for WindowEvent<'static> {
376382
input: *input,
377383
is_synthetic: *is_synthetic,
378384
},
379-
385+
IME(preedit_state) => IME(preedit_state.clone()),
380386
ModifiersChanged(modifiers) => ModifiersChanged(*modifiers),
381387
#[allow(deprecated)]
382388
CursorMoved {
@@ -468,6 +474,7 @@ impl<'a> WindowEvent<'a> {
468474
is_synthetic,
469475
}),
470476
ModifiersChanged(modifiers) => Some(ModifiersChanged(modifiers)),
477+
IME(event) => Some(IME(event)),
471478
#[allow(deprecated)]
472479
CursorMoved {
473480
device_id,
@@ -627,6 +634,28 @@ pub struct KeyboardInput {
627634
pub modifiers: ModifiersState,
628635
}
629636

637+
/// Describes an event from input method.
638+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
639+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
640+
pub enum IME {
641+
/// Notifies when the IME was enabled.
642+
Enabled,
643+
644+
/// Notifies when a new composing text should be set at the cursor position.
645+
///
646+
/// The value represents a pair of the preedit string and the cursor begin position and end
647+
/// position. When both indices are `None`, the cursor should be hidden.
648+
///
649+
/// The cursor position is byte-wise indexed.
650+
Preedit(String, Option<usize>, Option<usize>),
651+
652+
/// Notifies when text should be inserted into the editor widget.
653+
Commit(String),
654+
655+
/// Notifies when the IME was disabled.
656+
Disabled,
657+
}
658+
630659
/// Describes touch-screen input state.
631660
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
632661
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]

‎src/platform_impl/android/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,8 @@ impl Window {
748748

749749
pub fn set_ime_position(&self, _position: Position) {}
750750

751+
pub fn set_ime_allowed(&self, _allowed: bool) {}
752+
751753
pub fn focus_window(&self) {}
752754

753755
pub fn request_user_attention(&self, _request_type: Option<window::UserAttentionType>) {}

‎src/platform_impl/ios/window.rs

+4
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,10 @@ impl Inner {
287287
warn!("`Window::set_ime_position` is ignored on iOS")
288288
}
289289

290+
pub fn set_ime_allowed(&self, _allowed: bool) {
291+
warn!("`Window::set_ime_allowed` is ignored on iOS")
292+
}
293+
290294
pub fn focus_window(&self) {
291295
warn!("`Window::set_focus` is ignored on iOS")
292296
}

‎src/platform_impl/linux/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,11 @@ impl Window {
466466
x11_or_wayland!(match self; Window(w) => w.set_ime_position(position))
467467
}
468468

469+
#[inline]
470+
pub fn set_ime_allowed(&self, allowed: bool) {
471+
x11_or_wayland!(match self; Window(w) => w.set_ime_allowed(allowed))
472+
}
473+
469474
#[inline]
470475
pub fn focus_window(&self) {
471476
match self {

‎src/platform_impl/linux/wayland/event_loop/mod.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@ mod sink;
3232
mod state;
3333

3434
pub use proxy::EventLoopProxy;
35+
pub use sink::EventSink;
3536
pub use state::WinitState;
3637

37-
use sink::EventSink;
38-
3938
type WinitDispatcher = calloop::Dispatcher<'static, WaylandSource, WinitState>;
4039

4140
pub struct EventLoopWindowTarget<T> {

‎src/platform_impl/linux/wayland/seat/text_input/handlers.rs

+35-10
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ use sctk::reexports::protocols::unstable::text_input::v3::client::zwp_text_input
55
Event as TextInputEvent, ZwpTextInputV3,
66
};
77

8-
use crate::event::WindowEvent;
8+
use crate::event::{WindowEvent, IME};
99
use crate::platform_impl::wayland;
1010
use crate::platform_impl::wayland::event_loop::WinitState;
1111

12-
use super::{TextInputHandler, TextInputInner};
12+
use super::{Preedit, TextInputHandler, TextInputInner};
1313

1414
#[inline]
1515
pub(super) fn handle_text_input(
@@ -30,8 +30,11 @@ pub(super) fn handle_text_input(
3030
inner.target_window_id = Some(window_id);
3131

3232
// Enable text input on that surface.
33-
text_input.enable();
34-
text_input.commit();
33+
if window_handle.ime_allowed.get() {
34+
text_input.enable();
35+
text_input.commit();
36+
event_sink.push_window_event(WindowEvent::IME(IME::Enabled), window_id);
37+
}
3538

3639
// Notify a window we're currently over about text input handler.
3740
let text_input_handler = TextInputHandler {
@@ -58,19 +61,41 @@ pub(super) fn handle_text_input(
5861
text_input: text_input.detach(),
5962
};
6063
window_handle.text_input_left(text_input_handler);
64+
event_sink.push_window_event(WindowEvent::IME(IME::Disabled), window_id);
65+
}
66+
TextInputEvent::PreeditString {
67+
text,
68+
cursor_begin,
69+
cursor_end,
70+
} => {
71+
let cursor_begin = usize::try_from(cursor_begin).ok();
72+
let cursor_end = usize::try_from(cursor_end).ok();
73+
let text = text.unwrap_or_default();
74+
inner.pending_preedit = Some(Preedit {
75+
text,
76+
cursor_begin,
77+
cursor_end,
78+
});
6179
}
6280
TextInputEvent::CommitString { text } => {
63-
// Update currenly commited string.
64-
inner.commit_string = text;
81+
// Update currenly commited string and reset previous preedit.
82+
inner.pending_preedit = None;
83+
inner.pending_commit = Some(text.unwrap_or_default());
6584
}
6685
TextInputEvent::Done { .. } => {
67-
let (window_id, text) = match (inner.target_window_id, inner.commit_string.take()) {
68-
(Some(window_id), Some(text)) => (window_id, text),
86+
let window_id = match inner.target_window_id {
87+
Some(window_id) => window_id,
6988
_ => return,
7089
};
7190

72-
for ch in text.chars() {
73-
event_sink.push_window_event(WindowEvent::ReceivedCharacter(ch), window_id);
91+
if let Some(text) = inner.pending_commit.take() {
92+
event_sink.push_window_event(WindowEvent::IME(IME::Commit(text)), window_id);
93+
}
94+
95+
// Push preedit string we've got after latest commit.
96+
if let Some(preedit) = inner.pending_preedit.take() {
97+
let event = IME::Preedit(preedit.text, preedit.cursor_begin, preedit.cursor_end);
98+
event_sink.push_window_event(WindowEvent::IME(event), window_id);
7499
}
75100
}
76101
_ => (),

‎src/platform_impl/linux/wayland/seat/text_input/mod.rs

+24-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ impl TextInputHandler {
2020
self.text_input.set_cursor_rectangle(x, y, 0, 0);
2121
self.text_input.commit();
2222
}
23+
24+
#[inline]
25+
pub fn set_input_allowed(&self, allowed: bool) {
26+
if allowed {
27+
self.text_input.enable();
28+
} else {
29+
self.text_input.disable();
30+
}
31+
32+
self.text_input.commit();
33+
}
2334
}
2435

2536
/// A wrapper around text input to automatically destroy the object on `Drop`.
@@ -52,15 +63,25 @@ struct TextInputInner {
5263
/// Currently focused surface.
5364
target_window_id: Option<WindowId>,
5465

55-
/// Pending string to commit.
56-
commit_string: Option<String>,
66+
/// Pending commit event which will be dispatched on `text_input_v3::Done`.
67+
pending_commit: Option<String>,
68+
69+
/// Pending preedit event which will be dispatched on `text_input_v3::Done`.
70+
pending_preedit: Option<Preedit>,
71+
}
72+
73+
struct Preedit {
74+
text: String,
75+
cursor_begin: Option<usize>,
76+
cursor_end: Option<usize>,
5777
}
5878

5979
impl TextInputInner {
6080
fn new() -> Self {
6181
Self {
6282
target_window_id: None,
63-
commit_string: None,
83+
pending_commit: None,
84+
pending_preedit: None,
6485
}
6586
}
6687
}

‎src/platform_impl/linux/wayland/window/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,11 @@ impl Window {
475475
self.send_request(WindowRequest::IMEPosition(position));
476476
}
477477

478+
#[inline]
479+
pub fn set_ime_allowed(&self, allowed: bool) {
480+
self.send_request(WindowRequest::AllowIME(allowed));
481+
}
482+
478483
#[inline]
479484
pub fn display(&self) -> &Display {
480485
&self.display

‎src/platform_impl/linux/wayland/window/shim.rs

+34-2
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ use sctk::window::{Decorations, FallbackFrame, Window};
1111

1212
use crate::dpi::{LogicalPosition, LogicalSize};
1313

14-
use crate::event::WindowEvent;
14+
use crate::event::{WindowEvent, IME};
1515
use crate::platform_impl::wayland;
1616
use crate::platform_impl::wayland::env::WinitEnv;
17-
use crate::platform_impl::wayland::event_loop::WinitState;
17+
use crate::platform_impl::wayland::event_loop::{EventSink, WinitState};
1818
use crate::platform_impl::wayland::seat::pointer::WinitPointer;
1919
use crate::platform_impl::wayland::seat::text_input::TextInputHandler;
2020
use crate::platform_impl::wayland::WindowId;
@@ -70,6 +70,9 @@ pub enum WindowRequest {
7070
/// Set IME window position.
7171
IMEPosition(LogicalPosition<u32>),
7272

73+
/// Enable IME on the given window.
74+
AllowIME(bool),
75+
7376
/// Request Attention.
7477
///
7578
/// `None` unsets the attention request.
@@ -150,6 +153,9 @@ pub struct WindowHandle {
150153
/// Current cursor icon.
151154
pub cursor_icon: Cell<CursorIcon>,
152155

156+
/// Allow IME events for that window.
157+
pub ime_allowed: Cell<bool>,
158+
153159
/// Visible cursor or not.
154160
cursor_visible: Cell<bool>,
155161

@@ -189,6 +195,7 @@ impl WindowHandle {
189195
text_inputs: Vec::new(),
190196
xdg_activation,
191197
attention_requested: Cell::new(false),
198+
ime_allowed: Cell::new(false),
192199
}
193200
}
194201

@@ -304,6 +311,27 @@ impl WindowHandle {
304311
}
305312
}
306313

314+
pub fn set_ime_allowed(&self, allowed: bool, event_sink: &mut EventSink) {
315+
if self.ime_allowed.get() == allowed {
316+
return;
317+
}
318+
319+
self.ime_allowed.replace(allowed);
320+
let window_id = wayland::make_wid(self.window.surface());
321+
322+
for text_input in self.text_inputs.iter() {
323+
text_input.set_input_allowed(allowed);
324+
}
325+
326+
let event = if allowed {
327+
WindowEvent::IME(IME::Enabled)
328+
} else {
329+
WindowEvent::IME(IME::Disabled)
330+
};
331+
332+
event_sink.push_window_event(event, window_id);
333+
}
334+
307335
pub fn set_cursor_visible(&self, visible: bool) {
308336
self.cursor_visible.replace(visible);
309337
let cursor_icon = match visible {
@@ -361,6 +389,10 @@ pub fn handle_window_requests(winit_state: &mut WinitState) {
361389
WindowRequest::IMEPosition(position) => {
362390
window_handle.set_ime_position(position);
363391
}
392+
WindowRequest::AllowIME(allow) => {
393+
let event_sink = &mut winit_state.event_sink;
394+
window_handle.set_ime_allowed(allow, event_sink);
395+
}
364396
WindowRequest::GrabCursor(grab) => {
365397
window_handle.set_cursor_grab(grab);
366398
}

‎src/platform_impl/linux/x11/event_processor.rs

+67-2
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ use super::{
1212

1313
use util::modifiers::{ModifierKeyState, ModifierKeymap};
1414

15+
use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventReceiver, ImeRequest};
1516
use crate::{
1617
dpi::{PhysicalPosition, PhysicalSize},
1718
event::{
1819
DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, TouchPhase, WindowEvent,
20+
IME,
1921
},
2022
event_loop::EventLoopWindowTarget as RootELW,
2123
};
@@ -26,6 +28,7 @@ const KEYCODE_OFFSET: u8 = 8;
2628
pub(super) struct EventProcessor<T: 'static> {
2729
pub(super) dnd: Dnd,
2830
pub(super) ime_receiver: ImeReceiver,
31+
pub(super) ime_event_receiver: ImeEventReceiver,
2932
pub(super) randr_event_offset: c_int,
3033
pub(super) devices: RefCell<HashMap<DeviceId, Device>>,
3134
pub(super) xi2ext: XExtension,
@@ -37,6 +40,8 @@ pub(super) struct EventProcessor<T: 'static> {
3740
pub(super) first_touch: Option<u64>,
3841
// Currently focused window belonging to this process
3942
pub(super) active_window: Option<ffi::Window>,
43+
pub(super) is_composing: bool,
44+
pub(super) composed_text: Option<String>,
4045
}
4146

4247
impl<T: 'static> EventProcessor<T> {
@@ -609,6 +614,10 @@ impl<T: 'static> EventProcessor<T> {
609614
};
610615
callback(event);
611616
}
617+
if self.is_composing && !written.is_empty() {
618+
self.composed_text = Some(written);
619+
self.is_composing = false;
620+
}
612621
}
613622
}
614623

@@ -1223,8 +1232,64 @@ impl<T: 'static> EventProcessor<T> {
12231232
}
12241233
}
12251234

1226-
if let Ok((window_id, x, y)) = self.ime_receiver.try_recv() {
1227-
wt.ime.borrow_mut().send_xim_spot(window_id, x, y);
1235+
// Handle IME requests.
1236+
if let Ok(request) = self.ime_receiver.try_recv() {
1237+
let mut ime = wt.ime.borrow_mut();
1238+
match request {
1239+
ImeRequest::Position(window_id, x, y) => {
1240+
ime.send_xim_spot(window_id, x, y);
1241+
}
1242+
ImeRequest::AllowIME(window_id, allowed) => {
1243+
ime.set_ime_allowed(window_id, allowed);
1244+
}
1245+
}
1246+
}
1247+
1248+
match self.ime_event_receiver.try_recv() {
1249+
Ok((window, event)) => match event {
1250+
ImeEvent::Enabled => {
1251+
callback(Event::WindowEvent {
1252+
window_id: mkwid(window),
1253+
event: WindowEvent::IME(IME::Enabled),
1254+
});
1255+
}
1256+
ImeEvent::Start => {
1257+
self.is_composing = true;
1258+
self.composed_text = None;
1259+
callback(Event::WindowEvent {
1260+
window_id: mkwid(window),
1261+
event: WindowEvent::IME(IME::Preedit("".to_owned(), None, None)),
1262+
});
1263+
}
1264+
ImeEvent::Update(text, position) => {
1265+
if self.is_composing {
1266+
callback(Event::WindowEvent {
1267+
window_id: mkwid(window),
1268+
event: WindowEvent::IME(IME::Preedit(
1269+
text,
1270+
Some(position),
1271+
Some(position),
1272+
)),
1273+
});
1274+
}
1275+
}
1276+
ImeEvent::End => {
1277+
self.is_composing = false;
1278+
callback(Event::WindowEvent {
1279+
window_id: mkwid(window),
1280+
event: WindowEvent::IME(IME::Commit(
1281+
self.composed_text.take().unwrap_or("".to_owned()),
1282+
)),
1283+
});
1284+
}
1285+
ImeEvent::Disabled => {
1286+
callback(Event::WindowEvent {
1287+
window_id: mkwid(window),
1288+
event: WindowEvent::IME(IME::Disabled),
1289+
});
1290+
}
1291+
},
1292+
Err(_) => (),
12281293
}
12291294
}
12301295

‎src/platform_impl/linux/x11/ime/callbacks.rs

+12-1
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,19 @@ unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> {
108108
let mut new_contexts = HashMap::new();
109109
for (window, old_context) in (*inner).contexts.iter() {
110110
let spot = old_context.as_ref().map(|old_context| old_context.ic_spot);
111+
let is_allowed = old_context
112+
.as_ref()
113+
.map(|old_context| old_context.is_allowed)
114+
.unwrap_or_default();
111115
let new_context = {
112-
let result = ImeContext::new(xconn, new_im.im, *window, spot);
116+
let result = ImeContext::new(
117+
xconn,
118+
new_im.im,
119+
*window,
120+
spot,
121+
is_allowed,
122+
(*inner).event_sender.clone(),
123+
);
113124
if result.is_err() {
114125
let _ = close_im(xconn, new_im.im);
115126
}

‎src/platform_impl/linux/x11/ime/context.rs

+268-58
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,190 @@
1-
use std::{
2-
os::raw::{c_short, c_void},
3-
ptr,
4-
sync::Arc,
5-
};
1+
use std::mem::transmute;
2+
use std::os::raw::c_short;
3+
use std::ptr;
4+
use std::sync::Arc;
65

76
use super::{ffi, util, XConnection, XError};
7+
use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender};
8+
use std::ffi::CStr;
9+
use x11_dl::xlib::{XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct};
810

11+
/// IME creation error.
912
#[derive(Debug)]
1013
pub enum ImeContextCreationError {
14+
/// Got the error from Xlib.
1115
XError(XError),
16+
17+
/// Got null pointer from Xlib but without exact reason.
1218
Null,
1319
}
1420

15-
unsafe fn create_pre_edit_attr<'a>(
16-
xconn: &'a Arc<XConnection>,
17-
ic_spot: &'a ffi::XPoint,
18-
) -> util::XSmartPointer<'a, c_void> {
19-
util::XSmartPointer::new(
20-
xconn,
21-
(xconn.xlib.XVaCreateNestedList)(
22-
0,
23-
ffi::XNSpotLocation_0.as_ptr() as *const _,
24-
ic_spot,
25-
ptr::null_mut::<()>(),
26-
),
27-
)
28-
.expect("XVaCreateNestedList returned NULL")
21+
/// The callback used by XIM preedit functions.
22+
type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer);
23+
24+
/// Wrapper for creating XIM callbacks.
25+
#[inline]
26+
fn create_xim_callback(client_data: ffi::XPointer, callback: XIMProcNonnull) -> ffi::XIMCallback {
27+
XIMCallback {
28+
client_data,
29+
callback: Some(callback),
30+
}
31+
}
32+
33+
/// The server started preedit.
34+
extern "C" fn preedit_start_callback(
35+
_xim: ffi::XIM,
36+
client_data: ffi::XPointer,
37+
_call_data: ffi::XPointer,
38+
) -> i32 {
39+
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
40+
41+
client_data.text.clear();
42+
client_data.cursor_pos = 0;
43+
client_data
44+
.event_sender
45+
.send((client_data.window, ImeEvent::Start))
46+
.expect("failed to send preedit start event");
47+
-1
48+
}
49+
50+
/// Done callback is called when the user finished preedit and the text is about to be inserted into
51+
/// underlying widget.
52+
extern "C" fn preedit_done_callback(
53+
_xim: ffi::XIM,
54+
client_data: ffi::XPointer,
55+
_call_data: ffi::XPointer,
56+
) {
57+
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
58+
59+
client_data
60+
.event_sender
61+
.send((client_data.window, ImeEvent::End))
62+
.expect("failed to send preedit end event");
63+
}
64+
65+
fn calc_byte_position(text: &Vec<char>, pos: usize) -> usize {
66+
let mut byte_pos = 0;
67+
for i in 0..pos {
68+
byte_pos += text[i].len_utf8();
69+
}
70+
byte_pos
71+
}
72+
73+
/// Preedit text information to be drawn inline by the client.
74+
extern "C" fn preedit_draw_callback(
75+
_xim: ffi::XIM,
76+
client_data: ffi::XPointer,
77+
call_data: ffi::XPointer,
78+
) {
79+
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
80+
let call_data = unsafe { &mut *(call_data as *mut XIMPreeditDrawCallbackStruct) };
81+
client_data.cursor_pos = call_data.caret as usize;
82+
83+
let chg_range =
84+
call_data.chg_first as usize..(call_data.chg_first + call_data.chg_length) as usize;
85+
if chg_range.start > client_data.text.len() || chg_range.end > client_data.text.len() {
86+
warn!(
87+
"invalid chg range: buffer length={}, but chg_first={} chg_lengthg={}",
88+
client_data.text.len(),
89+
call_data.chg_first,
90+
call_data.chg_length
91+
);
92+
return;
93+
}
94+
95+
// NULL indicate text deletion
96+
let mut new_chars = if call_data.text.is_null() {
97+
Vec::new()
98+
} else {
99+
let xim_text = unsafe { &mut *(call_data.text) };
100+
if xim_text.encoding_is_wchar > 0 {
101+
return;
102+
}
103+
let new_text = unsafe { CStr::from_ptr(xim_text.string.multi_byte) };
104+
105+
String::from(new_text.to_str().expect("Invalid UTF-8 String from IME"))
106+
.chars()
107+
.collect()
108+
};
109+
let mut old_text_tail = client_data.text.split_off(chg_range.end);
110+
client_data.text.truncate(chg_range.start);
111+
client_data.text.append(&mut new_chars);
112+
client_data.text.append(&mut old_text_tail);
113+
let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
114+
115+
client_data
116+
.event_sender
117+
.send((
118+
client_data.window,
119+
ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
120+
))
121+
.expect("failed to send preedit update event");
122+
}
123+
124+
/// Handling of cursor movements in preedit text.
125+
extern "C" fn preedit_caret_callback(
126+
_xim: ffi::XIM,
127+
client_data: ffi::XPointer,
128+
call_data: ffi::XPointer,
129+
) {
130+
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
131+
let call_data = unsafe { &mut *(call_data as *mut XIMPreeditCaretCallbackStruct) };
132+
client_data.cursor_pos = call_data.position as usize;
133+
let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
134+
135+
client_data
136+
.event_sender
137+
.send((
138+
client_data.window,
139+
ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
140+
))
141+
.expect("failed to send preedit update event");
142+
}
143+
144+
/// Struct to simplify callback creation and latter passing into Xlib XIM.
145+
struct PreeditCallbacks {
146+
start_callback: ffi::XIMCallback,
147+
done_callback: ffi::XIMCallback,
148+
draw_callback: ffi::XIMCallback,
149+
caret_callback: ffi::XIMCallback,
150+
}
151+
152+
impl PreeditCallbacks {
153+
pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks {
154+
let start_callback = create_xim_callback(client_data, unsafe {
155+
transmute(preedit_start_callback as usize)
156+
});
157+
let done_callback = create_xim_callback(client_data, preedit_done_callback);
158+
let caret_callback = create_xim_callback(client_data, preedit_caret_callback);
159+
let draw_callback = create_xim_callback(client_data, preedit_draw_callback);
160+
161+
PreeditCallbacks {
162+
start_callback,
163+
done_callback,
164+
caret_callback,
165+
draw_callback,
166+
}
167+
}
29168
}
30169

31-
// WARNING: this struct doesn't destroy its XIC resource when dropped.
170+
struct ImeContextClientData {
171+
window: ffi::Window,
172+
event_sender: ImeEventSender,
173+
text: Vec<char>,
174+
cursor_pos: usize,
175+
}
176+
177+
// XXX: this struct doesn't destroy its XIC resource when dropped.
32178
// This is intentional, as it doesn't have enough information to know whether or not the context
33179
// still exists on the server. Since `ImeInner` has that awareness, destruction must be handled
34180
// through `ImeInner`.
35-
#[derive(Debug)]
36181
pub struct ImeContext {
37-
pub ic: ffi::XIC,
38-
pub ic_spot: ffi::XPoint,
182+
pub(super) ic: ffi::XIC,
183+
pub(super) ic_spot: ffi::XPoint,
184+
pub(super) is_allowed: bool,
185+
// Since the data is passed shared between X11 XIM callbacks, but couldn't be direclty free from
186+
// there we keep the pointer to automatically deallocate it.
187+
_client_data: Box<ImeContextClientData>,
39188
}
40189

41190
impl ImeContext {
@@ -44,66 +193,111 @@ impl ImeContext {
44193
im: ffi::XIM,
45194
window: ffi::Window,
46195
ic_spot: Option<ffi::XPoint>,
196+
is_allowed: bool,
197+
event_sender: ImeEventSender,
47198
) -> Result<Self, ImeContextCreationError> {
48-
let ic = if let Some(ic_spot) = ic_spot {
49-
ImeContext::create_ic_with_spot(xconn, im, window, ic_spot)
199+
let client_data = Box::into_raw(Box::new(ImeContextClientData {
200+
window,
201+
event_sender,
202+
text: Vec::new(),
203+
cursor_pos: 0,
204+
}));
205+
206+
let ic = if is_allowed {
207+
ImeContext::create_ic(xconn, im, window, client_data as ffi::XPointer)
208+
.ok_or(ImeContextCreationError::Null)?
50209
} else {
51-
ImeContext::create_ic(xconn, im, window)
210+
ImeContext::create_none_ic(xconn, im, window).ok_or(ImeContextCreationError::Null)?
52211
};
53212

54-
let ic = ic.ok_or(ImeContextCreationError::Null)?;
55213
xconn
56214
.check_errors()
57215
.map_err(ImeContextCreationError::XError)?;
58216

59-
Ok(ImeContext {
217+
let mut context = ImeContext {
60218
ic,
61-
ic_spot: ic_spot.unwrap_or(ffi::XPoint { x: 0, y: 0 }),
62-
})
219+
ic_spot: ffi::XPoint { x: 0, y: 0 },
220+
is_allowed,
221+
_client_data: Box::from_raw(client_data),
222+
};
223+
224+
// Set the spot location, if it's present.
225+
if let Some(ic_spot) = ic_spot {
226+
context.set_spot(xconn, ic_spot.x, ic_spot.y)
227+
}
228+
229+
Ok(context)
63230
}
64231

65-
unsafe fn create_ic(
232+
unsafe fn create_none_ic(
66233
xconn: &Arc<XConnection>,
67234
im: ffi::XIM,
68235
window: ffi::Window,
69236
) -> Option<ffi::XIC> {
70237
let ic = (xconn.xlib.XCreateIC)(
71238
im,
72239
ffi::XNInputStyle_0.as_ptr() as *const _,
73-
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
240+
ffi::XIMPreeditNone | ffi::XIMStatusNone,
74241
ffi::XNClientWindow_0.as_ptr() as *const _,
75242
window,
76243
ptr::null_mut::<()>(),
77244
);
78-
if ic.is_null() {
79-
None
80-
} else {
81-
Some(ic)
82-
}
245+
246+
(!ic.is_null()).then(|| ic)
83247
}
84248

85-
unsafe fn create_ic_with_spot(
249+
unsafe fn create_ic(
86250
xconn: &Arc<XConnection>,
87251
im: ffi::XIM,
88252
window: ffi::Window,
89-
ic_spot: ffi::XPoint,
253+
client_data: ffi::XPointer,
90254
) -> Option<ffi::XIC> {
91-
let pre_edit_attr = create_pre_edit_attr(xconn, &ic_spot);
92-
let ic = (xconn.xlib.XCreateIC)(
93-
im,
94-
ffi::XNInputStyle_0.as_ptr() as *const _,
95-
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
96-
ffi::XNClientWindow_0.as_ptr() as *const _,
97-
window,
98-
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
99-
pre_edit_attr.ptr,
100-
ptr::null_mut::<()>(),
101-
);
102-
if ic.is_null() {
103-
None
104-
} else {
105-
Some(ic)
106-
}
255+
let preedit_callbacks = PreeditCallbacks::new(client_data);
256+
let preedit_attr = util::XSmartPointer::new(
257+
xconn,
258+
(xconn.xlib.XVaCreateNestedList)(
259+
0,
260+
ffi::XNPreeditStartCallback_0.as_ptr() as *const _,
261+
&(preedit_callbacks.start_callback) as *const _,
262+
ffi::XNPreeditDoneCallback_0.as_ptr() as *const _,
263+
&(preedit_callbacks.done_callback) as *const _,
264+
ffi::XNPreeditCaretCallback_0.as_ptr() as *const _,
265+
&(preedit_callbacks.caret_callback) as *const _,
266+
ffi::XNPreeditDrawCallback_0.as_ptr() as *const _,
267+
&(preedit_callbacks.draw_callback) as *const _,
268+
ptr::null_mut::<()>(),
269+
),
270+
)
271+
.expect("XVaCreateNestedList returned NULL");
272+
273+
let ic = {
274+
let ic = (xconn.xlib.XCreateIC)(
275+
im,
276+
ffi::XNInputStyle_0.as_ptr() as *const _,
277+
ffi::XIMPreeditCallbacks | ffi::XIMStatusNothing,
278+
ffi::XNClientWindow_0.as_ptr() as *const _,
279+
window,
280+
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
281+
preedit_attr.ptr,
282+
ptr::null_mut::<()>(),
283+
);
284+
285+
// If we've failed to create IC with preedit callbacks fallback to normal one.
286+
if ic.is_null() {
287+
(xconn.xlib.XCreateIC)(
288+
im,
289+
ffi::XNInputStyle_0.as_ptr() as *const _,
290+
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
291+
ffi::XNClientWindow_0.as_ptr() as *const _,
292+
window,
293+
ptr::null_mut::<()>(),
294+
)
295+
} else {
296+
ic
297+
}
298+
};
299+
300+
(!ic.is_null()).then(|| ic)
107301
}
108302

109303
pub fn focus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
@@ -120,18 +314,34 @@ impl ImeContext {
120314
xconn.check_errors()
121315
}
122316

317+
// Set the spot for preedit text. Setting spot isn't working with libX11 when preedit callbacks
318+
// are being used. Certain IMEs do show selection window, but it's placed in bottom left of the
319+
// window and couldn't be changed.
320+
//
321+
// For me see: https://bugs.freedesktop.org/show_bug.cgi?id=1580.
123322
pub fn set_spot(&mut self, xconn: &Arc<XConnection>, x: c_short, y: c_short) {
124-
if self.ic_spot.x == x && self.ic_spot.y == y {
323+
if !self.is_allowed || self.ic_spot.x == x && self.ic_spot.y == y {
125324
return;
126325
}
326+
127327
self.ic_spot = ffi::XPoint { x, y };
128328

129329
unsafe {
130-
let pre_edit_attr = create_pre_edit_attr(xconn, &self.ic_spot);
330+
let preedit_attr = util::XSmartPointer::new(
331+
xconn,
332+
(xconn.xlib.XVaCreateNestedList)(
333+
0,
334+
ffi::XNSpotLocation_0.as_ptr(),
335+
&self.ic_spot,
336+
ptr::null_mut::<()>(),
337+
),
338+
)
339+
.expect("XVaCreateNestedList returned NULL");
340+
131341
(xconn.xlib.XSetICValues)(
132342
self.ic,
133343
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
134-
pre_edit_attr.ptr,
344+
preedit_attr.ptr,
135345
ptr::null_mut::<()>(),
136346
);
137347
}

‎src/platform_impl/linux/x11/ime/inner.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::{collections::HashMap, mem, ptr, sync::Arc};
33
use super::{ffi, XConnection, XError};
44

55
use super::{context::ImeContext, input_method::PotentialInputMethods};
6+
use crate::platform_impl::platform::x11::ime::ImeEventSender;
67

78
pub unsafe fn close_im(xconn: &Arc<XConnection>, im: ffi::XIM) -> Result<(), XError> {
89
(xconn.xlib.XCloseIM)(im);
@@ -22,20 +23,26 @@ pub struct ImeInner {
2223
pub contexts: HashMap<ffi::Window, Option<ImeContext>>,
2324
// WARNING: this is initially zeroed!
2425
pub destroy_callback: ffi::XIMCallback,
26+
pub event_sender: ImeEventSender,
2527
// Indicates whether or not the the input method was destroyed on the server end
2628
// (i.e. if ibus/fcitx/etc. was terminated/restarted)
2729
pub is_destroyed: bool,
2830
pub is_fallback: bool,
2931
}
3032

3133
impl ImeInner {
32-
pub fn new(xconn: Arc<XConnection>, potential_input_methods: PotentialInputMethods) -> Self {
34+
pub fn new(
35+
xconn: Arc<XConnection>,
36+
potential_input_methods: PotentialInputMethods,
37+
event_sender: ImeEventSender,
38+
) -> Self {
3339
ImeInner {
3440
xconn,
3541
im: ptr::null_mut(),
3642
potential_input_methods,
3743
contexts: HashMap::new(),
3844
destroy_callback: unsafe { mem::zeroed() },
45+
event_sender,
3946
is_destroyed: false,
4047
is_fallback: false,
4148
}

‎src/platform_impl/linux/x11/ime/mod.rs

+72-6
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,29 @@ use self::{
1919
inner::{close_im, ImeInner},
2020
input_method::PotentialInputMethods,
2121
};
22+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
24+
pub enum ImeEvent {
25+
Enabled,
26+
Start,
27+
Update(String, usize),
28+
End,
29+
Disabled,
30+
}
31+
32+
pub type ImeReceiver = Receiver<ImeRequest>;
33+
pub type ImeSender = Sender<ImeRequest>;
34+
pub type ImeEventReceiver = Receiver<(ffi::Window, ImeEvent)>;
35+
pub type ImeEventSender = Sender<(ffi::Window, ImeEvent)>;
2236

23-
pub type ImeReceiver = Receiver<(ffi::Window, i16, i16)>;
24-
pub type ImeSender = Sender<(ffi::Window, i16, i16)>;
37+
/// Request to control XIM handler from the window.
38+
pub enum ImeRequest {
39+
/// Set IME spot position for given `window_id`.
40+
Position(ffi::Window, i16, i16),
41+
42+
/// Allow IME input for the given `window_id`.
43+
AllowIME(ffi::Window, bool),
44+
}
2545

2646
#[derive(Debug)]
2747
pub enum ImeCreationError {
@@ -37,11 +57,14 @@ pub struct Ime {
3757
}
3858

3959
impl Ime {
40-
pub fn new(xconn: Arc<XConnection>) -> Result<Self, ImeCreationError> {
60+
pub fn new(
61+
xconn: Arc<XConnection>,
62+
event_sender: ImeEventSender,
63+
) -> Result<Self, ImeCreationError> {
4164
let potential_input_methods = PotentialInputMethods::new(&xconn);
4265

4366
let (mut inner, client_data) = {
44-
let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods));
67+
let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods, event_sender));
4568
let inner_ptr = Box::into_raw(inner);
4669
let client_data = inner_ptr as _;
4770
let destroy_callback = ffi::XIMCallback {
@@ -88,12 +111,37 @@ impl Ime {
88111
// Ok(_) indicates that nothing went wrong internally
89112
// Ok(true) indicates that the action was actually performed
90113
// Ok(false) indicates that the action is not presently applicable
91-
pub fn create_context(&mut self, window: ffi::Window) -> Result<bool, ImeContextCreationError> {
114+
pub fn create_context(
115+
&mut self,
116+
window: ffi::Window,
117+
with_preedit: bool,
118+
) -> Result<bool, ImeContextCreationError> {
92119
let context = if self.is_destroyed() {
93120
// Create empty entry in map, so that when IME is rebuilt, this window has a context.
94121
None
95122
} else {
96-
Some(unsafe { ImeContext::new(&self.inner.xconn, self.inner.im, window, None) }?)
123+
let event = if with_preedit {
124+
ImeEvent::Enabled
125+
} else {
126+
// There's no IME without preedit.
127+
ImeEvent::Disabled
128+
};
129+
130+
self.inner
131+
.event_sender
132+
.send((window, event))
133+
.expect("Failed to send enabled event");
134+
135+
Some(unsafe {
136+
ImeContext::new(
137+
&self.inner.xconn,
138+
self.inner.im,
139+
window,
140+
None,
141+
with_preedit,
142+
self.inner.event_sender.clone(),
143+
)
144+
}?)
97145
};
98146
self.inner.contexts.insert(window, context);
99147
Ok(!self.is_destroyed())
@@ -151,6 +199,24 @@ impl Ime {
151199
context.set_spot(&self.xconn, x as _, y as _);
152200
}
153201
}
202+
203+
pub fn set_ime_allowed(&mut self, window: ffi::Window, allowed: bool) {
204+
if self.is_destroyed() {
205+
return;
206+
}
207+
208+
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
209+
if allowed == context.is_allowed {
210+
return;
211+
}
212+
}
213+
214+
// Remove context for that window.
215+
let _ = self.remove_context(window);
216+
217+
// Create new context supporting IME input.
218+
let _ = self.create_context(window, allowed);
219+
}
154220
}
155221

156222
impl Drop for Ime {

‎src/platform_impl/linux/x11/mod.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ use mio::{unix::SourceFd, Events, Interest, Poll, Token, Waker};
4444
use self::{
4545
dnd::{Dnd, DndState},
4646
event_processor::EventProcessor,
47-
ime::{Ime, ImeCreationError, ImeReceiver, ImeSender},
47+
ime::{Ime, ImeCreationError, ImeReceiver, ImeRequest, ImeSender},
4848
util::modifiers::ModifierKeymap,
4949
};
5050
use crate::{
@@ -144,6 +144,7 @@ impl<T: 'static> EventLoop<T> {
144144
.expect("Failed to call XInternAtoms when initializing drag and drop");
145145

146146
let (ime_sender, ime_receiver) = mpsc::channel();
147+
let (ime_event_sender, ime_event_receiver) = mpsc::channel();
147148
// Input methods will open successfully without setting the locale, but it won't be
148149
// possible to actually commit pre-edit sequences.
149150
unsafe {
@@ -168,7 +169,7 @@ impl<T: 'static> EventLoop<T> {
168169
}
169170
}
170171
let ime = RefCell::new({
171-
let result = Ime::new(Arc::clone(&xconn));
172+
let result = Ime::new(Arc::clone(&xconn), ime_event_sender);
172173
if let Err(ImeCreationError::OpenFailure(ref state)) = result {
173174
panic!("Failed to open input method: {:#?}", state);
174175
}
@@ -252,12 +253,15 @@ impl<T: 'static> EventLoop<T> {
252253
devices: Default::default(),
253254
randr_event_offset,
254255
ime_receiver,
256+
ime_event_receiver,
255257
xi2ext,
256258
mod_keymap,
257259
device_mod_state: Default::default(),
258260
num_touch: 0,
259261
first_touch: None,
260262
active_window: None,
263+
is_composing: false,
264+
composed_text: None,
261265
};
262266

263267
// Register for device hotplug events

‎src/platform_impl/linux/x11/window.rs

+15-7
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ use crate::{
2626
};
2727

2828
use super::{
29-
ffi, util, EventLoopWindowTarget, ImeSender, WakeSender, WindowId, XConnection, XError,
29+
ffi, util, EventLoopWindowTarget, ImeRequest, ImeSender, WakeSender, WindowId, XConnection,
30+
XError,
3031
};
3132

3233
#[derive(Debug)]
@@ -449,7 +450,10 @@ impl UnownedWindow {
449450
.queue();
450451

451452
{
452-
let result = event_loop.ime.borrow_mut().create_context(window.xwindow);
453+
let result = event_loop
454+
.ime
455+
.borrow_mut()
456+
.create_context(window.xwindow, false);
453457
if let Err(err) = result {
454458
let e = match err {
455459
ImeContextCreationError::XError(err) => OsError::XError(err),
@@ -1392,17 +1396,21 @@ impl UnownedWindow {
13921396
.map_err(|err| ExternalError::Os(os_error!(OsError::XError(err))))
13931397
}
13941398

1395-
pub(crate) fn set_ime_position_physical(&self, x: i32, y: i32) {
1399+
#[inline]
1400+
pub fn set_ime_position(&self, spot: Position) {
1401+
let (x, y) = spot.to_physical::<i32>(self.scale_factor()).into();
13961402
let _ = self
13971403
.ime_sender
13981404
.lock()
1399-
.send((self.xwindow, x as i16, y as i16));
1405+
.send(ImeRequest::Position(self.xwindow, x, y));
14001406
}
14011407

14021408
#[inline]
1403-
pub fn set_ime_position(&self, spot: Position) {
1404-
let (x, y) = spot.to_physical::<i32>(self.scale_factor()).into();
1405-
self.set_ime_position_physical(x, y);
1409+
pub fn set_ime_allowed(&self, allowed: bool) {
1410+
let _ = self
1411+
.ime_sender
1412+
.lock()
1413+
.send(ImeRequest::AllowIME(self.xwindow, allowed));
14061414
}
14071415

14081416
#[inline]

‎src/platform_impl/macos/util/mod.rs

+20-1
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ mod cursor;
44
pub use self::{cursor::*, r#async::*};
55

66
use std::ops::{BitAnd, Deref};
7+
use std::os::raw::c_uchar;
78

89
use cocoa::{
910
appkit::{NSApp, NSWindowStyleMask},
1011
base::{id, nil},
1112
foundation::{NSPoint, NSRect, NSString, NSUInteger},
1213
};
1314
use core_graphics::display::CGDisplay;
14-
use objc::runtime::{Class, Object};
15+
use objc::runtime::{Class, Object, BOOL, NO};
1516

1617
use crate::dpi::LogicalPosition;
1718
use crate::platform_impl::platform::ffi;
@@ -165,3 +166,21 @@ pub unsafe fn toggle_style_mask(window: id, view: id, mask: NSWindowStyleMask, o
165166
// If we don't do this, key handling will break. Therefore, never call `setStyleMask` directly!
166167
window.makeFirstResponder_(view);
167168
}
169+
170+
/// For invalid utf8 sequences potentially returned by `UTF8String`,
171+
/// it behaves identically to `String::from_utf8_lossy`
172+
///
173+
/// Safety: Assumes that `string` is an instance of `NSAttributedString` or `NSString`
174+
pub unsafe fn id_to_string_lossy(string: id) -> String {
175+
let has_attr: BOOL = msg_send![string, isKindOfClass: class!(NSAttributedString)];
176+
let characters = if has_attr != NO {
177+
// This is a *mut NSAttributedString
178+
msg_send![string, string]
179+
} else {
180+
// This is already a *mut NSString
181+
string
182+
};
183+
let utf8_sequence =
184+
std::slice::from_raw_parts(characters.UTF8String() as *const c_uchar, characters.len());
185+
String::from_utf8_lossy(utf8_sequence).into_owned()
186+
}

‎src/platform_impl/macos/view.rs

+211-104
Large diffs are not rendered by default.

‎src/platform_impl/macos/window.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ impl UnownedWindow {
461461
if maximized {
462462
window.set_maximized(maximized);
463463
}
464-
464+
trace!("Done unowned window::new");
465465
Ok((window, delegate))
466466
}
467467

@@ -1045,6 +1045,13 @@ impl UnownedWindow {
10451045
unsafe { view::set_ime_position(*self.ns_view, logical_spot) };
10461046
}
10471047

1048+
#[inline]
1049+
pub fn set_ime_allowed(&self, allowed: bool) {
1050+
unsafe {
1051+
view::set_ime_allowed(*self.ns_view, allowed);
1052+
}
1053+
}
1054+
10481055
#[inline]
10491056
pub fn focus_window(&self) {
10501057
let is_minimized: BOOL = unsafe { msg_send![*self.ns_window, isMiniaturized] };

‎src/platform_impl/web/window.rs

+5
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,11 @@ impl Window {
297297
// Currently a no-op as it does not seem there is good support for this on web
298298
}
299299

300+
#[inline]
301+
pub fn set_ime_allowed(&self, _allowed: bool) {
302+
// Currently not implemented
303+
}
304+
300305
#[inline]
301306
pub fn focus_window(&self) {
302307
// Currently a no-op as it does not seem there is good support for this on web

‎src/platform_impl/windows/event_loop.rs

+102-2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use windows_sys::Win32::{
3434
UI::{
3535
Controls::{HOVER_DEFAULT, WM_MOUSELEAVE},
3636
Input::{
37+
Ime::{GCS_COMPSTR, GCS_RESULTSTR, ISC_SHOWUICOMPOSITIONWINDOW},
3738
KeyboardAndMouse::{
3839
MapVirtualKeyA, ReleaseCapture, SetCapture, TrackMouseEvent, TME_LEAVE,
3940
TRACKMOUSEEVENT, VK_F4,
@@ -59,6 +60,7 @@ use windows_sys::Win32::{
5960
SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOZORDER,
6061
WHEEL_DELTA, WINDOWPOS, WM_CAPTURECHANGED, WM_CHAR, WM_CLOSE, WM_CREATE, WM_DESTROY,
6162
WM_DPICHANGED, WM_DROPFILES, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO,
63+
WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION,
6264
WM_INPUT, WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN,
6365
WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEHWHEEL, WM_MOUSEMOVE,
6466
WM_MOUSEWHEEL, WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN,
@@ -73,18 +75,19 @@ use windows_sys::Win32::{
7375

7476
use crate::{
7577
dpi::{PhysicalPosition, PhysicalSize},
76-
event::{DeviceEvent, Event, Force, KeyboardInput, Touch, TouchPhase, WindowEvent},
78+
event::{DeviceEvent, Event, Force, KeyboardInput, Touch, TouchPhase, WindowEvent, IME},
7779
event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW},
7880
monitor::MonitorHandle as RootMonitorHandle,
7981
platform_impl::platform::{
8082
dark_mode::try_theme,
8183
dpi::{become_dpi_aware, dpi_to_scale_factor},
8284
drop_handler::FileDropHandler,
8385
event::{self, handle_extended_keys, process_key_params, vkey_to_winit_vkey},
86+
ime::ImeContext,
8487
monitor::{self, MonitorHandle},
8588
raw_input, util,
8689
window::InitData,
87-
window_state::{CursorFlags, WindowFlags, WindowState},
90+
window_state::{CursorFlags, ImeState, WindowFlags, WindowState},
8891
wrap_device_id, WindowId, DEVICE_ID,
8992
},
9093
window::{Fullscreen, WindowId as RootWindowId},
@@ -1095,6 +1098,103 @@ unsafe fn public_window_callback_inner<T: 'static>(
10951098
0
10961099
}
10971100

1101+
WM_IME_STARTCOMPOSITION => {
1102+
let ime_allowed = userdata.window_state.lock().ime_allowed;
1103+
if ime_allowed {
1104+
userdata.window_state.lock().ime_state = ImeState::Enabled;
1105+
1106+
userdata.send_event(Event::WindowEvent {
1107+
window_id: RootWindowId(WindowId(window)),
1108+
event: WindowEvent::IME(IME::Enabled),
1109+
});
1110+
}
1111+
1112+
DefWindowProcW(window, msg, wparam, lparam)
1113+
}
1114+
1115+
WM_IME_COMPOSITION => {
1116+
let ime_allowed_and_composing = {
1117+
let w = userdata.window_state.lock();
1118+
w.ime_allowed && w.ime_state != ImeState::Disabled
1119+
};
1120+
// Windows Hangul IME sends WM_IME_COMPOSITION after WM_IME_ENDCOMPOSITION, so
1121+
// check whether composing.
1122+
if ime_allowed_and_composing {
1123+
let ime_context = ImeContext::current(window);
1124+
1125+
if lparam == 0 {
1126+
userdata.send_event(Event::WindowEvent {
1127+
window_id: RootWindowId(WindowId(window)),
1128+
event: WindowEvent::IME(IME::Preedit(String::new(), None, None)),
1129+
});
1130+
}
1131+
1132+
// Google Japanese Input and ATOK have both flags, so
1133+
// first, receive composing result if exist.
1134+
if (lparam as u32 & GCS_RESULTSTR) != 0 {
1135+
if let Some(text) = ime_context.get_composed_text() {
1136+
userdata.window_state.lock().ime_state = ImeState::Enabled;
1137+
1138+
userdata.send_event(Event::WindowEvent {
1139+
window_id: RootWindowId(WindowId(window)),
1140+
event: WindowEvent::IME(IME::Commit(text)),
1141+
});
1142+
}
1143+
}
1144+
1145+
// Next, receive preedit range for next composing if exist.
1146+
if (lparam as u32 & GCS_COMPSTR) != 0 {
1147+
if let Some((text, first, last)) = ime_context.get_composing_text_and_cursor() {
1148+
userdata.window_state.lock().ime_state = ImeState::Preedit;
1149+
1150+
userdata.send_event(Event::WindowEvent {
1151+
window_id: RootWindowId(WindowId(window)),
1152+
event: WindowEvent::IME(IME::Preedit(text, first, last)),
1153+
});
1154+
}
1155+
}
1156+
}
1157+
1158+
// Not calling DefWindowProc to hide composing text drawn by IME.
1159+
0
1160+
}
1161+
1162+
WM_IME_ENDCOMPOSITION => {
1163+
let ime_allowed_or_composing = {
1164+
let w = userdata.window_state.lock();
1165+
w.ime_allowed || w.ime_state != ImeState::Disabled
1166+
};
1167+
if ime_allowed_or_composing {
1168+
if userdata.window_state.lock().ime_state == ImeState::Preedit {
1169+
// Windows Hangul IME sends WM_IME_COMPOSITION after WM_IME_ENDCOMPOSITION, so
1170+
// trying receiving composing result and commit if exists.
1171+
let ime_context = ImeContext::current(window);
1172+
if let Some(text) = ime_context.get_composed_text() {
1173+
userdata.send_event(Event::WindowEvent {
1174+
window_id: RootWindowId(WindowId(window)),
1175+
event: WindowEvent::IME(IME::Commit(text)),
1176+
});
1177+
}
1178+
}
1179+
1180+
userdata.window_state.lock().ime_state = ImeState::Disabled;
1181+
1182+
userdata.send_event(Event::WindowEvent {
1183+
window_id: RootWindowId(WindowId(window)),
1184+
event: WindowEvent::IME(IME::Disabled),
1185+
});
1186+
}
1187+
1188+
DefWindowProcW(window, msg, wparam, lparam)
1189+
}
1190+
1191+
WM_IME_SETCONTEXT => {
1192+
// Hide composing text drawn by IME.
1193+
let wparam = wparam & (!ISC_SHOWUICOMPOSITIONWINDOW as usize);
1194+
1195+
DefWindowProcW(window, msg, wparam, lparam)
1196+
}
1197+
10981198
// this is necessary for us to maintain minimize/restore state
10991199
WM_SYSCOMMAND => {
11001200
if wparam == SC_RESTORE as usize {

‎src/platform_impl/windows/ime.rs

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
use std::{
2+
ffi::{c_void, OsString},
3+
mem::zeroed,
4+
os::windows::prelude::OsStringExt,
5+
ptr::null_mut,
6+
};
7+
8+
use windows_sys::Win32::{
9+
Foundation::POINT,
10+
Globalization::HIMC,
11+
UI::{
12+
Input::Ime::{
13+
ImmAssociateContextEx, ImmGetCompositionStringW, ImmGetContext, ImmReleaseContext,
14+
ImmSetCandidateWindow, ATTR_TARGET_CONVERTED, ATTR_TARGET_NOTCONVERTED, CANDIDATEFORM,
15+
CFS_EXCLUDE, GCS_COMPATTR, GCS_COMPSTR, GCS_CURSORPOS, GCS_RESULTSTR, IACE_CHILDREN,
16+
IACE_DEFAULT,
17+
},
18+
WindowsAndMessaging::{GetSystemMetrics, SM_IMMENABLED},
19+
},
20+
};
21+
22+
use crate::{dpi::Position, platform::windows::HWND};
23+
24+
pub struct ImeContext {
25+
hwnd: HWND,
26+
himc: HIMC,
27+
}
28+
29+
impl ImeContext {
30+
pub unsafe fn current(hwnd: HWND) -> Self {
31+
let himc = ImmGetContext(hwnd);
32+
ImeContext { hwnd, himc }
33+
}
34+
35+
pub unsafe fn get_composing_text_and_cursor(
36+
&self,
37+
) -> Option<(String, Option<usize>, Option<usize>)> {
38+
let text = self.get_composition_string(GCS_COMPSTR)?;
39+
let attrs = self.get_composition_data(GCS_COMPATTR).unwrap_or_default();
40+
41+
let mut first = None;
42+
let mut last = None;
43+
let mut boundary_before_char = 0;
44+
45+
for (attr, chr) in attrs.into_iter().zip(text.chars()) {
46+
let char_is_targetted =
47+
attr as u32 == ATTR_TARGET_CONVERTED || attr as u32 == ATTR_TARGET_NOTCONVERTED;
48+
49+
if first.is_none() && char_is_targetted {
50+
first = Some(boundary_before_char);
51+
} else if first.is_some() && last.is_none() && !char_is_targetted {
52+
last = Some(boundary_before_char);
53+
}
54+
55+
boundary_before_char += chr.len_utf8();
56+
}
57+
58+
if first.is_some() && last.is_none() {
59+
last = Some(text.len());
60+
} else if first.is_none() {
61+
// IME haven't split words and select any clause yet, so trying to retrieve normal cursor.
62+
let cursor = self.get_composition_cursor(&text);
63+
first = cursor;
64+
last = cursor;
65+
}
66+
67+
Some((text, first, last))
68+
}
69+
70+
pub unsafe fn get_composed_text(&self) -> Option<String> {
71+
self.get_composition_string(GCS_RESULTSTR)
72+
}
73+
74+
unsafe fn get_composition_cursor(&self, text: &str) -> Option<usize> {
75+
let cursor = ImmGetCompositionStringW(self.himc, GCS_CURSORPOS, null_mut(), 0);
76+
(cursor >= 0).then(|| text.chars().take(cursor as _).map(|c| c.len_utf8()).sum())
77+
}
78+
79+
unsafe fn get_composition_string(&self, gcs_mode: u32) -> Option<String> {
80+
let data = self.get_composition_data(gcs_mode)?;
81+
let (prefix, shorts, suffix) = data.align_to::<u16>();
82+
if prefix.is_empty() && suffix.is_empty() {
83+
OsString::from_wide(&shorts).into_string().ok()
84+
} else {
85+
None
86+
}
87+
}
88+
89+
unsafe fn get_composition_data(&self, gcs_mode: u32) -> Option<Vec<u8>> {
90+
let size = ImmGetCompositionStringW(self.himc, gcs_mode, null_mut(), 0);
91+
if size < 0 {
92+
return None;
93+
} else if size == 0 {
94+
return Some(Vec::new());
95+
}
96+
97+
let mut buf = Vec::<u8>::with_capacity(size as _);
98+
let size = ImmGetCompositionStringW(
99+
self.himc,
100+
gcs_mode,
101+
buf.as_mut_ptr() as *mut c_void,
102+
size as _,
103+
);
104+
105+
if size < 0 {
106+
None
107+
} else {
108+
buf.set_len(size as _);
109+
Some(buf)
110+
}
111+
}
112+
113+
pub unsafe fn set_ime_position(&self, spot: Position, scale_factor: f64) {
114+
if !ImeContext::system_has_ime() {
115+
return;
116+
}
117+
118+
let (x, y) = spot.to_physical::<i32>(scale_factor).into();
119+
let candidate_form = CANDIDATEFORM {
120+
dwIndex: 0,
121+
dwStyle: CFS_EXCLUDE,
122+
ptCurrentPos: POINT { x, y },
123+
rcArea: zeroed(),
124+
};
125+
126+
ImmSetCandidateWindow(self.himc, &candidate_form);
127+
}
128+
129+
pub unsafe fn set_ime_allowed(hwnd: HWND, allowed: bool) {
130+
if !ImeContext::system_has_ime() {
131+
return;
132+
}
133+
134+
if allowed {
135+
ImmAssociateContextEx(hwnd, 0, IACE_DEFAULT);
136+
} else {
137+
ImmAssociateContextEx(hwnd, 0, IACE_CHILDREN);
138+
}
139+
}
140+
141+
unsafe fn system_has_ime() -> bool {
142+
return GetSystemMetrics(SM_IMMENABLED) != 0;
143+
}
144+
}
145+
146+
impl Drop for ImeContext {
147+
fn drop(&mut self) {
148+
unsafe { ImmReleaseContext(self.hwnd, self.himc) };
149+
}
150+
}

‎src/platform_impl/windows/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ mod drop_handler;
154154
mod event;
155155
mod event_loop;
156156
mod icon;
157+
mod ime;
157158
mod monitor;
158159
mod raw_input;
159160
mod window;

‎src/platform_impl/windows/window.rs

+14-21
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@ use windows_sys::Win32::{
3131
},
3232
UI::{
3333
Input::{
34-
Ime::{
35-
ImmGetContext, ImmReleaseContext, ImmSetCompositionWindow, CFS_POINT,
36-
COMPOSITIONFORM,
37-
},
3834
KeyboardAndMouse::{
3935
EnableWindow, GetActiveWindow, MapVirtualKeyW, ReleaseCapture, SendInput, INPUT,
4036
INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP,
@@ -49,8 +45,8 @@ use windows_sys::Win32::{
4945
SetWindowPlacement, SetWindowPos, SetWindowTextW, CS_HREDRAW, CS_VREDRAW,
5046
CW_USEDEFAULT, FLASHWINFO, FLASHW_ALL, FLASHW_STOP, FLASHW_TIMERNOFG, FLASHW_TRAY,
5147
GWLP_HINSTANCE, HTCAPTION, MAPVK_VK_TO_VSC, NID_READY, PM_NOREMOVE, SM_DIGITIZER,
52-
SM_IMMENABLED, SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER,
53-
WM_NCLBUTTONDOWN, WNDCLASSEXW,
48+
SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER, WM_NCLBUTTONDOWN,
49+
WNDCLASSEXW,
5450
},
5551
},
5652
};
@@ -69,6 +65,7 @@ use crate::{
6965
drop_handler::FileDropHandler,
7066
event_loop::{self, EventLoopWindowTarget, DESTROY_MSG_ID},
7167
icon::{self, IconType},
68+
ime::ImeContext,
7269
monitor, util,
7370
window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState},
7471
Parent, PlatformSpecificWindowBuilderAttributes, WindowId,
@@ -613,25 +610,19 @@ impl Window {
613610
self.window_state.lock().taskbar_icon = taskbar_icon;
614611
}
615612

616-
pub(crate) fn set_ime_position_physical(&self, x: i32, y: i32) {
617-
if unsafe { GetSystemMetrics(SM_IMMENABLED) } != 0 {
618-
let composition_form = COMPOSITIONFORM {
619-
dwStyle: CFS_POINT,
620-
ptCurrentPos: POINT { x, y },
621-
rcArea: unsafe { mem::zeroed() },
622-
};
623-
unsafe {
624-
let himc = ImmGetContext(self.hwnd());
625-
ImmSetCompositionWindow(himc, &composition_form);
626-
ImmReleaseContext(self.hwnd(), himc);
627-
}
613+
#[inline]
614+
pub fn set_ime_position(&self, spot: Position) {
615+
unsafe {
616+
ImeContext::current(self.hwnd()).set_ime_position(spot, self.scale_factor());
628617
}
629618
}
630619

631620
#[inline]
632-
pub fn set_ime_position(&self, spot: Position) {
633-
let (x, y) = spot.to_physical::<i32>(self.scale_factor()).into();
634-
self.set_ime_position_physical(x, y);
621+
pub fn set_ime_allowed(&self, allowed: bool) {
622+
self.window_state.lock().ime_allowed = allowed;
623+
unsafe {
624+
ImeContext::set_ime_allowed(self.hwnd(), allowed);
625+
}
635626
}
636627

637628
#[inline]
@@ -785,6 +776,8 @@ impl<'a, T: 'static> InitData<'a, T> {
785776

786777
enable_non_client_dpi_scaling(window);
787778

779+
ImeContext::set_ime_allowed(window, false);
780+
788781
Window {
789782
window: WindowWrapper(window),
790783
window_state,

‎src/platform_impl/windows/window_state.rs

+13
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ pub struct WindowState {
4242
pub preferred_theme: Option<Theme>,
4343
pub high_surrogate: Option<u16>,
4444
pub window_flags: WindowFlags,
45+
46+
pub ime_state: ImeState,
47+
pub ime_allowed: bool,
4548
}
4649

4750
#[derive(Clone)]
@@ -99,6 +102,13 @@ bitflags! {
99102
}
100103
}
101104

105+
#[derive(Eq, PartialEq)]
106+
pub enum ImeState {
107+
Disabled,
108+
Enabled,
109+
Preedit,
110+
}
111+
102112
impl WindowState {
103113
pub fn new(
104114
attributes: &WindowAttributes,
@@ -130,6 +140,9 @@ impl WindowState {
130140
preferred_theme,
131141
high_surrogate: None,
132142
window_flags: WindowFlags::empty(),
143+
144+
ime_state: ImeState::Disabled,
145+
ime_allowed: false,
133146
}
134147
}
135148

‎src/window.rs

+27
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,33 @@ impl Window {
834834
self.window.set_ime_position(position.into())
835835
}
836836

837+
/// Sets whether the window should get IME events
838+
///
839+
/// When IME is allowed, the window will receive [`IME`] events, and during the
840+
/// preedit phase the window will NOT get [`KeyboardInput`] or
841+
/// [`ReceivedCharacter`] events. The window should allow IME while it is
842+
/// expecting text input.
843+
///
844+
/// When IME is not allowed, the window won't receive [`IME`] events, and will
845+
/// receive [`KeyboardInput`] events for every keypress instead. Without
846+
/// allowing IME, the window will also get [`ReceivedCharacter`] events for
847+
/// certain keyboard input. Not allowing IME is useful for games for example.
848+
///
849+
/// IME is **not** allowed by default.
850+
///
851+
/// ## Platform-specific
852+
///
853+
/// - **macOS:** IME must be enabled to receive text-input where dead-key sequences are combined.
854+
/// - ** iOS / Android / Web :** Unsupported.
855+
///
856+
/// [`IME`]: crate::event::WindowEvent::IME
857+
/// [`KeyboardInput`]: crate::event::WindowEvent::KeyboardInput
858+
/// [`ReceivedCharacter`]: crate::event::WindowEvent::ReceivedCharacter
859+
#[inline]
860+
pub fn set_ime_allowed(&self, allowed: bool) {
861+
self.window.set_ime_allowed(allowed);
862+
}
863+
837864
/// Brings the window to the front and sets input focus. Has no effect if the window is
838865
/// already in focus, minimized, or not visible.
839866
///

0 commit comments

Comments
 (0)
Please sign in to comment.