From f04fa5d54f4ec10cdb6d084deeb79d3e6d27ae67 Mon Sep 17 00:00:00 2001 From: Kirill Chibisov Date: Sat, 7 May 2022 05:29:25 +0300 Subject: [PATCH] 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 Co-authored-by: Markus Siglreithmaier Co-authored-by: Murarth Co-authored-by: Yusuke Kominami Co-authored-by: moko256 --- CHANGELOG.md | 3 + Cargo.toml | 4 +- examples/ime.rs | 97 +++++ examples/set_ime_position.rs | 53 --- src/event.rs | 87 ++++- src/platform_impl/android/mod.rs | 2 + src/platform_impl/ios/window.rs | 4 + src/platform_impl/linux/mod.rs | 5 + .../linux/wayland/event_loop/mod.rs | 3 +- .../linux/wayland/seat/text_input/handlers.rs | 49 ++- .../linux/wayland/seat/text_input/mod.rs | 27 +- src/platform_impl/linux/wayland/window/mod.rs | 7 +- .../linux/wayland/window/shim.rs | 40 +- .../linux/x11/event_processor.rs | 80 +++- src/platform_impl/linux/x11/ime/callbacks.rs | 13 +- src/platform_impl/linux/x11/ime/context.rs | 354 ++++++++++++++---- src/platform_impl/linux/x11/ime/inner.rs | 9 +- src/platform_impl/linux/x11/ime/mod.rs | 78 +++- src/platform_impl/linux/x11/mod.rs | 7 +- src/platform_impl/linux/x11/window.rs | 28 +- src/platform_impl/macos/util/mod.rs | 21 +- src/platform_impl/macos/view.rs | 314 +++++++++++----- src/platform_impl/macos/window.rs | 9 +- src/platform_impl/web/window.rs | 5 + src/platform_impl/windows/event_loop.rs | 122 +++++- src/platform_impl/windows/ime.rs | 150 ++++++++ src/platform_impl/windows/mod.rs | 1 + src/platform_impl/windows/window.rs | 35 +- src/platform_impl/windows/window_state.rs | 13 + src/window.rs | 37 ++ 30 files changed, 1346 insertions(+), 311 deletions(-) create mode 100644 examples/ime.rs delete mode 100644 examples/set_ime_position.rs create mode 100644 src/platform_impl/windows/ime.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 589642da..aa457108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,9 @@ And please only add new entries to the top of this list, right below the `# Unre - **Breaking:** Replaced `Window::with_app_id` and `Window::with_class` with `Window::with_name` on `WindowBuilderExtUnix`. - On Wayland and X11, fix window not resizing with `Window::set_inner_size` after calling `Window:set_resizable(false)`. - On Windows, fix wrong fullscreen monitors being recognized when handling WM_WINDOWPOSCHANGING messages +- **Breaking:** Added new `WindowEvent::Ime` supported on desktop platforms. +- Added `Window::set_ime_allowed` supported on desktop platforms. +- **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. # 0.26.1 (2022-01-05) diff --git a/Cargo.toml b/Cargo.toml index e52ebf3f..d6ee8b53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,8 +87,8 @@ features = [ ] [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] -wayland-client = { version = "0.29", default_features = false, features = ["use_system_lib"], optional = true } -wayland-protocols = { version = "0.29", features = [ "staging_protocols"], optional = true } +wayland-client = { version = "0.29.4", default_features = false, features = ["use_system_lib"], optional = true } +wayland-protocols = { version = "0.29.4", features = [ "staging_protocols"], optional = true } sctk = { package = "smithay-client-toolkit", version = "0.15.4", default_features = false, features = ["calloop"], optional = true } mio = { version = "0.8", features = ["os-ext"], optional = true } x11-dl = { version = "2.18.5", optional = true } diff --git a/examples/ime.rs b/examples/ime.rs new file mode 100644 index 00000000..5f38447a --- /dev/null +++ b/examples/ime.rs @@ -0,0 +1,97 @@ +use log::LevelFilter; +use simple_logger::SimpleLogger; +use winit::{ + dpi::PhysicalPosition, + event::{ElementState, Event, Ime, VirtualKeyCode, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +fn main() { + SimpleLogger::new() + .with_level(LevelFilter::Trace) + .init() + .unwrap(); + + println!("IME position will system default"); + println!("Click to set IME position to cursor's"); + println!("Press F2 to toggle IME. See the documentation of `set_ime_allowed` for more info"); + + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_inner_size(winit::dpi::LogicalSize::new(256f64, 128f64)) + .build(&event_loop) + .unwrap(); + + let mut ime_allowed = true; + window.set_ime_allowed(ime_allowed); + + let mut may_show_ime = false; + let mut cursor_position = PhysicalPosition::new(0.0, 0.0); + let mut ime_pos = PhysicalPosition::new(0.0, 0.0); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + Event::WindowEvent { + event: WindowEvent::CursorMoved { position, .. }, + .. + } => { + cursor_position = position; + } + Event::WindowEvent { + event: + WindowEvent::MouseInput { + state: ElementState::Released, + .. + }, + .. + } => { + println!( + "Setting ime position to {}, {}", + cursor_position.x, cursor_position.y + ); + ime_pos = cursor_position; + if may_show_ime { + window.set_ime_position(ime_pos); + } + } + Event::WindowEvent { + event: WindowEvent::Ime(event), + .. + } => { + println!("{:?}", event); + may_show_ime = event != Ime::Disabled; + if may_show_ime { + window.set_ime_position(ime_pos); + } + } + Event::WindowEvent { + event: WindowEvent::ReceivedCharacter(ch), + .. + } => { + println!("ch: {:?}", ch); + } + Event::WindowEvent { + event: WindowEvent::KeyboardInput { input, .. }, + .. + } => { + println!("key: {:?}", input); + + if input.state == ElementState::Pressed + && input.virtual_keycode == Some(VirtualKeyCode::F2) + { + ime_allowed = !ime_allowed; + window.set_ime_allowed(ime_allowed); + println!("\nIME: {}\n", ime_allowed); + } + } + _ => (), + } + }); +} diff --git a/examples/set_ime_position.rs b/examples/set_ime_position.rs deleted file mode 100644 index 5f96a6fe..00000000 --- a/examples/set_ime_position.rs +++ /dev/null @@ -1,53 +0,0 @@ -use simple_logger::SimpleLogger; -use winit::{ - dpi::PhysicalPosition, - event::{ElementState, Event, WindowEvent}, - event_loop::EventLoop, - window::WindowBuilder, -}; - -fn main() { - SimpleLogger::new().init().unwrap(); - let event_loop = EventLoop::new(); - - let window = WindowBuilder::new().build(&event_loop).unwrap(); - window.set_title("A fantastic window!"); - - println!("Ime position will system default"); - println!("Click to set ime position to cursor's"); - - let mut cursor_position = PhysicalPosition::new(0.0, 0.0); - event_loop.run(move |event, _, control_flow| { - control_flow.set_wait(); - - match event { - Event::WindowEvent { - event: WindowEvent::CursorMoved { position, .. }, - .. - } => { - cursor_position = position; - } - Event::WindowEvent { - event: - WindowEvent::MouseInput { - state: ElementState::Released, - .. - }, - .. - } => { - println!( - "Setting ime position to {}, {}", - cursor_position.x, cursor_position.y - ); - window.set_ime_position(cursor_position); - } - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => { - control_flow.set_exit(); - } - _ => (), - } - }); -} diff --git a/src/event.rs b/src/event.rs index 5afb0ebf..65e556a2 100644 --- a/src/event.rs +++ b/src/event.rs @@ -36,6 +36,8 @@ use instant::Instant; use std::path::PathBuf; +#[cfg(doc)] +use crate::window::Window; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, platform_impl, @@ -93,8 +95,7 @@ pub enum Event<'a, T: 'static> { /// This gets triggered in two scenarios: /// - The OS has performed an operation that's invalidated the window's contents (such as /// resizing the window). - /// - The application has explicitly requested a redraw via - /// [`Window::request_redraw`](crate::window::Window::request_redraw). + /// - The application has explicitly requested a redraw via [`Window::request_redraw`]. /// /// During each iteration of the event loop, Winit will aggregate duplicate redraw requests /// into a single event, to help avoid duplicating rendering work. @@ -206,7 +207,7 @@ pub enum StartCause { Init, } -/// Describes an event from a `Window`. +/// Describes an event from a [`Window`]. #[derive(Debug, PartialEq)] pub enum WindowEvent<'a> { /// The size of the window has changed. Contains the client area's new dimensions. @@ -240,6 +241,8 @@ pub enum WindowEvent<'a> { HoveredFileCancelled, /// The window received a unicode character. + /// + /// See also the [`Ime`](Self::Ime) event for more complex character sequences. ReceivedCharacter(char), /// The window gained or lost focus. @@ -270,6 +273,14 @@ pub enum WindowEvent<'a> { /// issue, and it should get fixed - but it's the current state of the API. ModifiersChanged(ModifiersState), + /// An event from input method. + /// + /// **Note :** You have to explicitly enable this event using [`Window::set_ime_allowed`]. + /// + /// Platform-specific behavior: + /// - **iOS / Android / Web :** Unsupported. + Ime(Ime), + /// The cursor has moved on the window. CursorMoved { device_id: DeviceId, @@ -376,7 +387,7 @@ impl Clone for WindowEvent<'static> { input: *input, is_synthetic: *is_synthetic, }, - + Ime(preedit_state) => Ime(preedit_state.clone()), ModifiersChanged(modifiers) => ModifiersChanged(*modifiers), #[allow(deprecated)] CursorMoved { @@ -468,6 +479,7 @@ impl<'a> WindowEvent<'a> { is_synthetic, }), ModifiersChanged(modifiers) => Some(ModifiersChanged(modifiers)), + Ime(event) => Some(Ime(event)), #[allow(deprecated)] CursorMoved { device_id, @@ -627,6 +639,73 @@ pub struct KeyboardInput { pub modifiers: ModifiersState, } +/// Describes [input method](https://en.wikipedia.org/wiki/Input_method) events. +/// +/// This is also called a "composition event". +/// +/// Most keypresses using a latin-like keyboard layout simply generate a [`WindowEvent::ReceivedCharacter`]. +/// However, one couldn't possibly have a key for every single unicode character that the user might want to type +/// - so the solution operating systems employ is to allow the user to type these using _a sequence of keypresses_ instead. +/// +/// A prominent example of this is accents - many keyboard layouts allow you to first click the "accent key", and then +/// the character you want to apply the accent to. This will generate the following event sequence: +/// ```ignore +/// // Press "`" key +/// Ime::Preedit("`", Some(0), Some(0)) +/// // Press "E" key +/// Ime::Commit("é") +/// ``` +/// +/// Additionally, certain input devices are configured to display a candidate box that allow the user to select the +/// desired character interactively. (To properly position this box, you must use [`Window::set_ime_position`].) +/// +/// An example of a keyboard layout which uses candidate boxes is pinyin. On a latin keybaord the following event +/// sequence could be obtained: +/// ```ignore +/// // Press "A" key +/// Ime::Preedit("a", Some(1), Some(1)) +/// // Press "B" key +/// Ime::Preedit("a b", Some(3), Some(3)) +/// // Press left arrow key +/// Ime::Preedit("a b", Some(1), Some(1)) +/// // Press space key +/// Ime::Preedit("啊b", Some(3), Some(3)) +/// // Press space key +/// Ime::Commit("啊不") +/// ``` +/// +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Ime { + /// Notifies when the IME was enabled. + /// + /// After getting this event you could receive [`Preedit`](Self::Preedit) and + /// [`Commit`](Self::Commit) events. You should also start performing IME related requests + /// like [`Window::set_ime_position`]. + Enabled, + + /// Notifies when a new composing text should be set at the cursor position. + /// + /// The value represents a pair of the preedit string and the cursor begin position and end + /// position. When it's `None`, the cursor should be hidden. + /// + /// The cursor position is byte-wise indexed. + Preedit(String, Option<(usize, usize)>), + + /// Notifies when text should be inserted into the editor widget. + /// + /// Any pending [`Preedit`](Self::Preedit) must be cleared. + Commit(String), + + /// Notifies when the IME was disabled. + /// + /// After receiving this event you won't get any more [`Preedit`](Self::Preedit) or + /// [`Commit`](Self::Commit) events until the next [`Enabled`](Self::Enabled) event. You can + /// also stop issuing IME related requests like [`Window::set_ime_position`] and clear pending + /// preedit text. + Disabled, +} + /// Describes touch-screen input state. #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 9457894e..27d9984b 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -748,6 +748,8 @@ impl Window { pub fn set_ime_position(&self, _position: Position) {} + pub fn set_ime_allowed(&self, _allowed: bool) {} + pub fn focus_window(&self) {} pub fn request_user_attention(&self, _request_type: Option) {} diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index 81c225ef..5df7734a 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -291,6 +291,10 @@ impl Inner { warn!("`Window::set_ime_position` is ignored on iOS") } + pub fn set_ime_allowed(&self, _allowed: bool) { + warn!("`Window::set_ime_allowed` is ignored on iOS") + } + pub fn focus_window(&self) { warn!("`Window::set_focus` is ignored on iOS") } diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 78765756..cee7f94d 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -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 { diff --git a/src/platform_impl/linux/wayland/event_loop/mod.rs b/src/platform_impl/linux/wayland/event_loop/mod.rs index 0a69365b..84dcb9fa 100644 --- a/src/platform_impl/linux/wayland/event_loop/mod.rs +++ b/src/platform_impl/linux/wayland/event_loop/mod.rs @@ -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 { diff --git a/src/platform_impl/linux/wayland/seat/text_input/handlers.rs b/src/platform_impl/linux/wayland/seat/text_input/handlers.rs index 4ba13d67..8f05bb60 100644 --- a/src/platform_impl/linux/wayland/seat/text_input/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/text_input/handlers.rs @@ -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); } } _ => (), diff --git a/src/platform_impl/linux/wayland/seat/text_input/mod.rs b/src/platform_impl/linux/wayland/seat/text_input/mod.rs index 77f4ff08..52ec94af 100644 --- a/src/platform_impl/linux/wayland/seat/text_input/mod.rs +++ b/src/platform_impl/linux/wayland/seat/text_input/mod.rs @@ -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, - /// Pending string to commit. - commit_string: Option, + /// Pending commit event which will be dispatched on `text_input_v3::Done`. + pending_commit: Option, + + /// Pending preedit event which will be dispatched on `text_input_v3::Done`. + pending_preedit: Option, +} + +struct Preedit { + text: String, + cursor_begin: Option, + cursor_end: Option, } impl TextInputInner { fn new() -> Self { Self { target_window_id: None, - commit_string: None, + pending_commit: None, + pending_preedit: None, } } } diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index 4cff918f..04ca0189 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -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] diff --git a/src/platform_impl/linux/wayland/window/shim.rs b/src/platform_impl/linux/wayland/window/shim.rs index 90695be5..7359a300 100644 --- a/src/platform_impl/linux/wayland/window/shim.rs +++ b/src/platform_impl/linux/wayland/window/shim.rs @@ -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), /// Set IME window position. - IMEPosition(LogicalPosition), + ImePosition(LogicalPosition), + + /// 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, + /// Allow IME events for that window. + pub ime_allowed: Cell, + /// Visible cursor or not. cursor_visible: Cell, @@ -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); } diff --git a/src/platform_impl/linux/x11/event_processor.rs b/src/platform_impl/linux/x11/event_processor.rs index 77cc5530..0db198bb 100644 --- a/src/platform_impl/linux/x11/event_processor.rs +++ b/src/platform_impl/linux/x11/event_processor.rs @@ -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 { 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>, pub(super) xi2ext: XExtension, @@ -37,6 +40,7 @@ pub(super) struct EventProcessor { pub(super) first_touch: Option, // Currently focused window belonging to this process pub(super) active_window: Option, + pub(super) is_composing: bool, } impl EventProcessor { @@ -567,7 +571,7 @@ impl EventProcessor { // 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 EventProcessor { 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 EventProcessor { } } - 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(_) => (), } } diff --git a/src/platform_impl/linux/x11/ime/callbacks.rs b/src/platform_impl/linux/x11/ime/callbacks.rs index 7b599274..a957c23a 100644 --- a/src/platform_impl/linux/x11/ime/callbacks.rs +++ b/src/platform_impl/linux/x11/ime/callbacks.rs @@ -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); } diff --git a/src/platform_impl/linux/x11/ime/context.rs b/src/platform_impl/linux/x11/ime/context.rs index 8d580820..2fdfc6f3 100644 --- a/src/platform_impl/linux/x11/ime/context.rs +++ b/src/platform_impl/linux/x11/ime/context.rs @@ -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, - 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, 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, + 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, } impl ImeContext { @@ -44,66 +199,111 @@ impl ImeContext { im: ffi::XIM, window: ffi::Window, ic_spot: Option, + is_allowed: bool, + event_sender: ImeEventSender, ) -> Result { - 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, + im: ffi::XIM, + window: ffi::Window, + ) -> Option { + 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, im: ffi::XIM, window: ffi::Window, + client_data: ffi::XPointer, ) -> Option { - 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, - im: ffi::XIM, - window: ffi::Window, - ic_spot: ffi::XPoint, - ) -> Option { - 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) -> 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, 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::<()>(), ); } diff --git a/src/platform_impl/linux/x11/ime/inner.rs b/src/platform_impl/linux/x11/ime/inner.rs index 97dcf7af..58b558bc 100644 --- a/src/platform_impl/linux/x11/ime/inner.rs +++ b/src/platform_impl/linux/x11/ime/inner.rs @@ -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, im: ffi::XIM) -> Result<(), XError> { (xconn.xlib.XCloseIM)(im); @@ -22,6 +23,7 @@ pub struct ImeInner { pub contexts: HashMap>, // 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, potential_input_methods: PotentialInputMethods) -> Self { + pub fn new( + xconn: Arc, + 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, } diff --git a/src/platform_impl/linux/x11/ime/mod.rs b/src/platform_impl/linux/x11/ime/mod.rs index b95da711..4125c0ac 100644 --- a/src/platform_impl/linux/x11/ime/mod.rs +++ b/src/platform_impl/linux/x11/ime/mod.rs @@ -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; +pub type ImeSender = Sender; +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) -> Result { + pub fn new( + xconn: Arc, + event_sender: ImeEventSender, + ) -> Result { 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 { + pub fn create_context( + &mut self, + window: ffi::Window, + with_preedit: bool, + ) -> Result { 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 { diff --git a/src/platform_impl/linux/x11/mod.rs b/src/platform_impl/linux/x11/mod.rs index e0a2995e..491e37d6 100644 --- a/src/platform_impl/linux/x11/mod.rs +++ b/src/platform_impl/linux/x11/mod.rs @@ -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 EventLoop { .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 EventLoop { } } 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 EventLoop { 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 diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index e3707960..fb2047cc 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -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::(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] diff --git a/src/platform_impl/macos/util/mod.rs b/src/platform_impl/macos/util/mod.rs index 7747a57b..75ff703a 100644 --- a/src/platform_impl/macos/util/mod.rs +++ b/src/platform_impl/macos/util/mod.rs @@ -4,6 +4,7 @@ mod cursor; pub use self::{cursor::*, r#async::*}; use std::ops::{BitAnd, Deref}; +use std::os::raw::c_uchar; use cocoa::{ appkit::{NSApp, NSWindowStyleMask}, @@ -11,7 +12,7 @@ use cocoa::{ foundation::{NSPoint, NSRect, NSString, NSUInteger}, }; use core_graphics::display::CGDisplay; -use objc::runtime::{Class, Object}; +use objc::runtime::{Class, Object, BOOL, NO}; use crate::dpi::LogicalPosition; use crate::platform_impl::platform::ffi; @@ -165,3 +166,21 @@ pub unsafe fn toggle_style_mask(window: id, view: id, mask: NSWindowStyleMask, o // If we don't do this, key handling will break. Therefore, never call `setStyleMask` directly! window.makeFirstResponder_(view); } + +/// For invalid utf8 sequences potentially returned by `UTF8String`, +/// it behaves identically to `String::from_utf8_lossy` +/// +/// Safety: Assumes that `string` is an instance of `NSAttributedString` or `NSString` +pub unsafe fn id_to_string_lossy(string: id) -> String { + let has_attr: BOOL = msg_send![string, isKindOfClass: class!(NSAttributedString)]; + let characters = if has_attr != NO { + // This is a *mut NSAttributedString + msg_send![string, string] + } else { + // This is already a *mut NSString + string + }; + let utf8_sequence = + std::slice::from_raw_parts(characters.UTF8String() as *const c_uchar, characters.len()); + String::from_utf8_lossy(utf8_sequence).into_owned() +} diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index 2d879d58..4845fb9d 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -3,7 +3,10 @@ use std::{ collections::VecDeque, os::raw::*, ptr, slice, str, - sync::{Arc, Mutex, Weak}, + sync::{ + atomic::{compiler_fence, Ordering}, + Arc, Mutex, Weak, + }, }; use cocoa::{ @@ -19,7 +22,7 @@ use objc::{ use crate::{ dpi::LogicalPosition, event::{ - DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, MouseButton, + DeviceEvent, ElementState, Event, Ime, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode, WindowEvent, }, platform_impl::platform::{ @@ -29,7 +32,7 @@ use crate::{ scancode_to_keycode, EventWrapper, }, ffi::*, - util::{self, IdRef}, + util::{self, id_to_string_lossy, IdRef}, window::get_window_id, DEVICE_ID, }, @@ -50,20 +53,42 @@ impl Default for CursorState { } } +#[derive(Eq, PartialEq)] +enum ImeState { + Disabled, + Enabled, + Preedit, +} + pub(super) struct ViewState { ns_window: id, pub cursor_state: Arc>, - /// The position of the candidate window. ime_position: LogicalPosition, - raw_characters: Option, pub(super) modifiers: ModifiersState, tracking_rect: Option, + ime_state: ImeState, + input_source: String, + + /// True iff the application wants IME events. + /// + /// Can be set using `set_ime_allowed` + ime_allowed: bool, + + /// True if the current key event should be forwarded + /// to the application, even during IME + forward_key_to_app: bool, } impl ViewState { fn get_scale_factor(&self) -> f64 { (unsafe { NSWindow::backingScaleFactor(self.ns_window) }) as f64 } + fn is_ime_enabled(&self) -> bool { + match self.ime_state { + ImeState::Disabled => false, + _ => true, + } + } } pub fn new_view(ns_window: id) -> (IdRef, Weak>) { @@ -72,11 +97,13 @@ pub fn new_view(ns_window: id) -> (IdRef, Weak>) { let state = ViewState { ns_window, cursor_state, - // By default, open the candidate window in the top left corner ime_position: LogicalPosition::new(0.0, 0.0), - raw_characters: None, modifiers: Default::default(), tracking_rect: None, + ime_state: ImeState::Disabled, + input_source: String::new(), + ime_allowed: false, + forward_key_to_app: false, }; unsafe { // This is free'd in `dealloc` @@ -97,6 +124,33 @@ pub unsafe fn set_ime_position(ns_view: id, position: LogicalPosition) { let _: () = msg_send![input_context, invalidateCharacterCoordinates]; } +pub unsafe fn set_ime_allowed(ns_view: id, ime_allowed: bool) { + let state_ptr: *mut c_void = *(*ns_view).get_mut_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + if state.ime_allowed == ime_allowed { + return; + } + state.ime_allowed = ime_allowed; + if state.ime_allowed { + return; + } + let marked_text_ref: &mut id = (*ns_view).get_mut_ivar("markedText"); + + // Clear markedText + let _: () = msg_send![*marked_text_ref, release]; + let marked_text = + ::init(NSMutableAttributedString::alloc(nil)); + *marked_text_ref = marked_text; + + if state.ime_state != ImeState::Disabled { + state.ime_state = ImeState::Disabled; + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Ime(Ime::Disabled), + })); + } +} + struct ViewClass(*const Class); unsafe impl Send for ViewClass {} unsafe impl Sync for ViewClass {} @@ -130,6 +184,9 @@ lazy_static! { sel!(resetCursorRects), reset_cursor_rects as extern "C" fn(&Object, Sel), ); + + // ------------------------------------------------------------------ + // NSTextInputClient decl.add_method( sel!(hasMarkedText), has_marked_text as extern "C" fn(&Object, Sel) -> BOOL, @@ -173,6 +230,8 @@ lazy_static! { sel!(doCommandBySelector:), do_command_by_selector as extern "C" fn(&Object, Sel, Sel), ); + // ------------------------------------------------------------------ + decl.add_method(sel!(keyDown:), key_down as extern "C" fn(&Object, Sel, id)); decl.add_method(sel!(keyUp:), key_up as extern "C" fn(&Object, Sel, id)); decl.add_method( @@ -266,9 +325,9 @@ lazy_static! { extern "C" fn dealloc(this: &Object, _sel: Sel) { unsafe { - let state: *mut c_void = *this.get_ivar("winitState"); let marked_text: id = *this.get_ivar("markedText"); let _: () = msg_send![marked_text, release]; + let state: *mut c_void = *this.get_ivar("winitState"); Box::from_raw(state as *mut ViewState); } } @@ -285,15 +344,19 @@ extern "C" fn init_with_winit(this: &Object, _sel: Sel, state: *mut c_void) -> i let notification_center: &Object = msg_send![class!(NSNotificationCenter), defaultCenter]; - let notification_name = + // About frame change + let frame_did_change_notification_name = IdRef::new(NSString::alloc(nil).init_str("NSViewFrameDidChangeNotification")); let _: () = msg_send![ notification_center, addObserver: this selector: sel!(frameDidChange:) - name: notification_name + name: frame_did_change_notification_name object: this ]; + + let winit_state = &mut *(state as *mut ViewState); + winit_state.input_source = current_input_source(this); } this } @@ -402,7 +465,7 @@ extern "C" fn marked_range(this: &Object, _sel: Sel) -> NSRange { let marked_text: id = *this.get_ivar("markedText"); let length = marked_text.length(); if length > 0 { - NSRange::new(0, length - 1) + NSRange::new(0, length) } else { util::EMPTY_RANGE } @@ -414,6 +477,13 @@ extern "C" fn selected_range(_this: &Object, _sel: Sel) -> NSRange { util::EMPTY_RANGE } +/// Safety: Assumes that `view` is an instance of `VIEW_CLASS` from winit. +unsafe fn current_input_source(view: *const Object) -> String { + let input_context: id = msg_send![view, inputContext]; + let input_source: id = msg_send![input_context, selectedKeyboardInputSource]; + id_to_string_lossy(input_source) +} + extern "C" fn set_marked_text( this: &mut Object, _sel: Sel, @@ -423,7 +493,10 @@ extern "C" fn set_marked_text( ) { trace_scope!("setMarkedText:selectedRange:replacementRange:"); unsafe { + // Get pre-edit text let marked_text_ref: &mut id = this.get_mut_ivar("markedText"); + + // Update markedText let _: () = msg_send![(*marked_text_ref), release]; let marked_text = NSMutableAttributedString::alloc(nil); let has_attr: BOOL = msg_send![string, isKindOfClass: class!(NSAttributedString)]; @@ -433,6 +506,33 @@ extern "C" fn set_marked_text( marked_text.initWithString(string); }; *marked_text_ref = marked_text; + + // Update ViewState with new marked text + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + let preedit_string = id_to_string_lossy(string); + + // Notify IME is active if application still doesn't know it. + if state.ime_state == ImeState::Disabled { + state.input_source = current_input_source(this); + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Ime(Ime::Enabled), + })); + } + + let cursor_start = preedit_string.len(); + let cursor_end = preedit_string.len(); + state.ime_state = ImeState::Preedit; + + // Send WindowEvent for updating marked text + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Ime(Ime::Preedit( + preedit_string, + Some((cursor_start, cursor_end)), + )), + })); } } @@ -446,6 +546,19 @@ extern "C" fn unmark_text(this: &Object, _sel: Sel) { let _: () = msg_send![s, release]; let input_context: id = msg_send![this, inputContext]; let _: () = msg_send![input_context, discardMarkedText]; + + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Ime(Ime::Preedit(String::new(), Some((0, 0)))), + })); + if state.is_ime_enabled() { + // Leave the Preedit state + state.ime_state = ImeState::Enabled; + } else { + warn!("Expected to have IME enabled when receiving unmarkText"); + } } } @@ -499,35 +612,24 @@ extern "C" fn insert_text(this: &Object, _sel: Sel, string: id, _replacement_ran let state_ptr: *mut c_void = *this.get_ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); - let has_attr: BOOL = msg_send![string, isKindOfClass: class!(NSAttributedString)]; - let characters = if has_attr != NO { - // This is a *mut NSAttributedString - msg_send![string, string] - } else { - // This is already a *mut NSString - string - }; + let string = id_to_string_lossy(string); - let slice = - slice::from_raw_parts(characters.UTF8String() as *const c_uchar, characters.len()); - let string = str::from_utf8_unchecked(slice); + let is_control = string.chars().next().map_or(false, |c| c.is_control()); // We don't need this now, but it's here if that changes. //let event: id = msg_send![NSApp(), currentEvent]; - let mut events = VecDeque::with_capacity(characters.len()); - for character in string.chars().filter(|c| !is_corporate_character(*c)) { - events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { + if state.is_ime_enabled() && !is_control { + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::ReceivedCharacter(character), + event: WindowEvent::Ime(Ime::Commit(string)), })); + state.ime_state = ImeState::Enabled; } - - AppState::queue_events(events); } } -extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, command: Sel) { +extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, _command: Sel) { trace_scope!("doCommandBySelector:"); // Basically, we're sent this message whenever a keyboard event that doesn't generate a "human readable" character // happens, i.e. newlines, tabs, and Ctrl+C. @@ -535,31 +637,15 @@ extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, command: Sel) { let state_ptr: *mut c_void = *this.get_ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); - let mut events = VecDeque::with_capacity(1); - if command == sel!(insertNewline:) { - // The `else` condition would emit the same character, but I'm keeping this here both... - // 1) as a reminder for how `doCommandBySelector` works - // 2) to make our use of carriage return explicit - events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::ReceivedCharacter('\r'), - })); - } else { - let raw_characters = state.raw_characters.take(); - if let Some(raw_characters) = raw_characters { - for character in raw_characters - .chars() - .filter(|c| !is_corporate_character(*c)) - { - events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::ReceivedCharacter(character), - })); - } - } - }; + state.forward_key_to_app = true; - AppState::queue_events(events); + let has_marked_text: BOOL = msg_send![this, hasMarkedText]; + if has_marked_text == NO { + if state.ime_state == ImeState::Preedit { + // Leave preedit so that we also report the keyup for this key + state.ime_state = ImeState::Enabled; + } + } } } @@ -637,54 +723,71 @@ extern "C" fn key_down(this: &Object, _sel: Sel, event: id) { let state_ptr: *mut c_void = *this.get_ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); let window_id = WindowId(get_window_id(state.ns_window)); - let characters = get_characters(event, false); - state.raw_characters = Some(characters.clone()); + let input_source = current_input_source(this); + if state.input_source != input_source && state.is_ime_enabled() { + state.ime_state = ImeState::Disabled; + state.input_source = input_source; + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Ime(Ime::Disabled), + })); + } + let was_in_preedit = state.ime_state == ImeState::Preedit; + + let characters = get_characters(event, false); + state.forward_key_to_app = false; + + // The `interpretKeyEvents` function might call + // `setMarkedText`, `insertText`, and `doCommandBySelector`. + // It's important that we call this before queuing the KeyboardInput, because + // we must send the `KeyboardInput` event during IME if it triggered + // `doCommandBySelector`. (doCommandBySelector means that the keyboard input + // is not handled by IME and should be handled by the application) + if state.ime_allowed { + let events_for_nsview: id = msg_send![class!(NSArray), arrayWithObject: event]; + let _: () = msg_send![this, interpretKeyEvents: events_for_nsview]; + + // Using a compiler fence because `interpretKeyEvents` might call + // into functions that modify the `ViewState`, but the compiler + // doesn't know this. Without the fence, the compiler may think that + // some of the reads (eg `state.ime_state`) that happen after this + // point are not needed. + compiler_fence(Ordering::SeqCst); + } + + let now_in_preedit = state.ime_state == ImeState::Preedit; let scancode = get_scancode(event) as u32; let virtual_keycode = retrieve_keycode(event); - let is_repeat: BOOL = msg_send![event, isARepeat]; - update_potentially_stale_modifiers(state, event); - #[allow(deprecated)] - let window_event = Event::WindowEvent { - window_id, - event: WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: ElementState::Pressed, - scancode, - virtual_keycode, - modifiers: event_mods(event), + let preedit_related = was_in_preedit || now_in_preedit; + if !preedit_related || state.forward_key_to_app || !state.ime_allowed { + #[allow(deprecated)] + let window_event = Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state: ElementState::Pressed, + scancode, + virtual_keycode, + modifiers: event_mods(event), + }, + is_synthetic: false, }, - is_synthetic: false, - }, - }; + }; - let pass_along = { AppState::queue_event(EventWrapper::StaticEvent(window_event)); - // Emit `ReceivedCharacter` for key repeats - if is_repeat != NO { - for character in characters.chars().filter(|c| !is_corporate_character(*c)) { - AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { - window_id, - event: WindowEvent::ReceivedCharacter(character), - })); - } - false - } else { - true - } - }; - if pass_along { - // Some keys (and only *some*, with no known reason) don't trigger `insertText`, while others do... - // So, we don't give repeats the opportunity to trigger that, since otherwise our hack will cause some - // keys to generate twice as many characters. - let array: id = msg_send![class!(NSArray), arrayWithObject: event]; - let _: () = msg_send![this, interpretKeyEvents: array]; + for character in characters.chars().filter(|c| !is_corporate_character(*c)) { + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id, + event: WindowEvent::ReceivedCharacter(character), + })); + } } } } @@ -700,22 +803,25 @@ extern "C" fn key_up(this: &Object, _sel: Sel, event: id) { update_potentially_stale_modifiers(state, event); - #[allow(deprecated)] - let window_event = Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: ElementState::Released, - scancode, - virtual_keycode, - modifiers: event_mods(event), + // We want to send keyboard input when we are not currently in preedit + if state.ime_state != ImeState::Preedit { + #[allow(deprecated)] + let window_event = Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state: ElementState::Released, + scancode, + virtual_keycode, + modifiers: event_mods(event), + }, + is_synthetic: false, }, - is_synthetic: false, - }, - }; + }; - AppState::queue_event(EventWrapper::StaticEvent(window_event)); + AppState::queue_event(EventWrapper::StaticEvent(window_event)); + } } } diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index a4b827c7..d4856cb9 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -461,7 +461,7 @@ impl UnownedWindow { if maximized { window.set_maximized(maximized); } - + trace!("Done unowned window::new"); Ok((window, delegate)) } @@ -1054,6 +1054,13 @@ impl UnownedWindow { unsafe { view::set_ime_position(*self.ns_view, logical_spot) }; } + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + unsafe { + view::set_ime_allowed(*self.ns_view, allowed); + } + } + #[inline] pub fn focus_window(&self) { let is_minimized: BOOL = unsafe { msg_send![*self.ns_window, isMiniaturized] }; diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 886514ca..6f5201b2 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -302,6 +302,11 @@ impl Window { // Currently a no-op as it does not seem there is good support for this on web } + #[inline] + pub fn set_ime_allowed(&self, _allowed: bool) { + // Currently not implemented + } + #[inline] pub fn focus_window(&self) { // Currently a no-op as it does not seem there is good support for this on web diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index 36b44177..5e8db28a 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -34,6 +34,7 @@ use windows_sys::Win32::{ UI::{ Controls::{HOVER_DEFAULT, WM_MOUSELEAVE}, Input::{ + Ime::{GCS_COMPSTR, GCS_RESULTSTR, ISC_SHOWUICOMPOSITIONWINDOW}, KeyboardAndMouse::{ MapVirtualKeyA, ReleaseCapture, SetCapture, TrackMouseEvent, TME_LEAVE, TRACKMOUSEEVENT, VK_F4, @@ -59,21 +60,23 @@ use windows_sys::Win32::{ SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, WM_CAPTURECHANGED, WM_CHAR, WM_CLOSE, WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_DROPFILES, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, - WM_GETMINMAXINFO, WM_INPUT, WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, - WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEHWHEEL, - WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, - WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, - WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SYSCHAR, WM_SYSCOMMAND, - WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, - WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, - WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE, + WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT, + WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, WM_KEYUP, + WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, + WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCCREATE, WM_NCDESTROY, + WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, + WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, + WM_SYSCHAR, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, + WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED, + WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, + WS_VISIBLE, }, }, }; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, - event::{DeviceEvent, Event, Force, KeyboardInput, Touch, TouchPhase, WindowEvent}, + event::{DeviceEvent, Event, Force, Ime, KeyboardInput, Touch, TouchPhase, WindowEvent}, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, monitor::MonitorHandle as RootMonitorHandle, platform_impl::platform::{ @@ -81,10 +84,11 @@ use crate::{ dpi::{become_dpi_aware, dpi_to_scale_factor}, drop_handler::FileDropHandler, event::{self, handle_extended_keys, process_key_params, vkey_to_winit_vkey}, + ime::ImeContext, monitor::{self, MonitorHandle}, raw_input, util, window::InitData, - window_state::{CursorFlags, WindowFlags, WindowState}, + window_state::{CursorFlags, ImeState, WindowFlags, WindowState}, wrap_device_id, WindowId, DEVICE_ID, }, window::{Fullscreen, WindowId as RootWindowId}, @@ -1128,6 +1132,104 @@ unsafe fn public_window_callback_inner( 0 } + WM_IME_STARTCOMPOSITION => { + let ime_allowed = userdata.window_state.lock().ime_allowed; + if ime_allowed { + userdata.window_state.lock().ime_state = ImeState::Enabled; + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Enabled), + }); + } + + DefWindowProcW(window, msg, wparam, lparam) + } + + WM_IME_COMPOSITION => { + let ime_allowed_and_composing = { + let w = userdata.window_state.lock(); + w.ime_allowed && w.ime_state != ImeState::Disabled + }; + // Windows Hangul IME sends WM_IME_COMPOSITION after WM_IME_ENDCOMPOSITION, so + // check whether composing. + if ime_allowed_and_composing { + let ime_context = ImeContext::current(window); + + if lparam == 0 { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }); + } + + // Google Japanese Input and ATOK have both flags, so + // first, receive composing result if exist. + if (lparam as u32 & GCS_RESULTSTR) != 0 { + if let Some(text) = ime_context.get_composed_text() { + userdata.window_state.lock().ime_state = ImeState::Enabled; + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Commit(text)), + }); + } + } + + // Next, receive preedit range for next composing if exist. + if (lparam as u32 & GCS_COMPSTR) != 0 { + if let Some((text, first, last)) = ime_context.get_composing_text_and_cursor() { + userdata.window_state.lock().ime_state = ImeState::Preedit; + let cursor_range = first.map(|f| (f, last.unwrap_or(f))); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(text, cursor_range)), + }); + } + } + } + + // Not calling DefWindowProc to hide composing text drawn by IME. + 0 + } + + WM_IME_ENDCOMPOSITION => { + let ime_allowed_or_composing = { + let w = userdata.window_state.lock(); + w.ime_allowed || w.ime_state != ImeState::Disabled + }; + if ime_allowed_or_composing { + if userdata.window_state.lock().ime_state == ImeState::Preedit { + // Windows Hangul IME sends WM_IME_COMPOSITION after WM_IME_ENDCOMPOSITION, so + // trying receiving composing result and commit if exists. + let ime_context = ImeContext::current(window); + if let Some(text) = ime_context.get_composed_text() { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Commit(text)), + }); + } + } + + userdata.window_state.lock().ime_state = ImeState::Disabled; + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Disabled), + }); + } + + DefWindowProcW(window, msg, wparam, lparam) + } + + WM_IME_SETCONTEXT => { + // Hide composing text drawn by IME. + let wparam = wparam & (!ISC_SHOWUICOMPOSITIONWINDOW as usize); + + DefWindowProcW(window, msg, wparam, lparam) + } + // this is necessary for us to maintain minimize/restore state WM_SYSCOMMAND => { if wparam == SC_RESTORE as usize { diff --git a/src/platform_impl/windows/ime.rs b/src/platform_impl/windows/ime.rs new file mode 100644 index 00000000..d6c5c79b --- /dev/null +++ b/src/platform_impl/windows/ime.rs @@ -0,0 +1,150 @@ +use std::{ + ffi::{c_void, OsString}, + mem::zeroed, + os::windows::prelude::OsStringExt, + ptr::null_mut, +}; + +use windows_sys::Win32::{ + Foundation::POINT, + Globalization::HIMC, + UI::{ + Input::Ime::{ + ImmAssociateContextEx, ImmGetCompositionStringW, ImmGetContext, ImmReleaseContext, + ImmSetCandidateWindow, ATTR_TARGET_CONVERTED, ATTR_TARGET_NOTCONVERTED, CANDIDATEFORM, + CFS_EXCLUDE, GCS_COMPATTR, GCS_COMPSTR, GCS_CURSORPOS, GCS_RESULTSTR, IACE_CHILDREN, + IACE_DEFAULT, + }, + WindowsAndMessaging::{GetSystemMetrics, SM_IMMENABLED}, + }, +}; + +use crate::{dpi::Position, platform::windows::HWND}; + +pub struct ImeContext { + hwnd: HWND, + himc: HIMC, +} + +impl ImeContext { + pub unsafe fn current(hwnd: HWND) -> Self { + let himc = ImmGetContext(hwnd); + ImeContext { hwnd, himc } + } + + pub unsafe fn get_composing_text_and_cursor( + &self, + ) -> Option<(String, Option, Option)> { + let text = self.get_composition_string(GCS_COMPSTR)?; + let attrs = self.get_composition_data(GCS_COMPATTR).unwrap_or_default(); + + let mut first = None; + let mut last = None; + let mut boundary_before_char = 0; + + for (attr, chr) in attrs.into_iter().zip(text.chars()) { + let char_is_targetted = + attr as u32 == ATTR_TARGET_CONVERTED || attr as u32 == ATTR_TARGET_NOTCONVERTED; + + if first.is_none() && char_is_targetted { + first = Some(boundary_before_char); + } else if first.is_some() && last.is_none() && !char_is_targetted { + last = Some(boundary_before_char); + } + + boundary_before_char += chr.len_utf8(); + } + + if first.is_some() && last.is_none() { + last = Some(text.len()); + } else if first.is_none() { + // IME haven't split words and select any clause yet, so trying to retrieve normal cursor. + let cursor = self.get_composition_cursor(&text); + first = cursor; + last = cursor; + } + + Some((text, first, last)) + } + + pub unsafe fn get_composed_text(&self) -> Option { + self.get_composition_string(GCS_RESULTSTR) + } + + unsafe fn get_composition_cursor(&self, text: &str) -> Option { + let cursor = ImmGetCompositionStringW(self.himc, GCS_CURSORPOS, null_mut(), 0); + (cursor >= 0).then(|| text.chars().take(cursor as _).map(|c| c.len_utf8()).sum()) + } + + unsafe fn get_composition_string(&self, gcs_mode: u32) -> Option { + let data = self.get_composition_data(gcs_mode)?; + let (prefix, shorts, suffix) = data.align_to::(); + if prefix.is_empty() && suffix.is_empty() { + OsString::from_wide(&shorts).into_string().ok() + } else { + None + } + } + + unsafe fn get_composition_data(&self, gcs_mode: u32) -> Option> { + let size = ImmGetCompositionStringW(self.himc, gcs_mode, null_mut(), 0); + if size < 0 { + return None; + } else if size == 0 { + return Some(Vec::new()); + } + + let mut buf = Vec::::with_capacity(size as _); + let size = ImmGetCompositionStringW( + self.himc, + gcs_mode, + buf.as_mut_ptr() as *mut c_void, + size as _, + ); + + if size < 0 { + None + } else { + buf.set_len(size as _); + Some(buf) + } + } + + pub unsafe fn set_ime_position(&self, spot: Position, scale_factor: f64) { + if !ImeContext::system_has_ime() { + return; + } + + let (x, y) = spot.to_physical::(scale_factor).into(); + let candidate_form = CANDIDATEFORM { + dwIndex: 0, + dwStyle: CFS_EXCLUDE, + ptCurrentPos: POINT { x, y }, + rcArea: zeroed(), + }; + + ImmSetCandidateWindow(self.himc, &candidate_form); + } + + pub unsafe fn set_ime_allowed(hwnd: HWND, allowed: bool) { + if !ImeContext::system_has_ime() { + return; + } + + if allowed { + ImmAssociateContextEx(hwnd, 0, IACE_DEFAULT); + } else { + ImmAssociateContextEx(hwnd, 0, IACE_CHILDREN); + } + } + + unsafe fn system_has_ime() -> bool { + return GetSystemMetrics(SM_IMMENABLED) != 0; + } +} + +impl Drop for ImeContext { + fn drop(&mut self) { + unsafe { ImmReleaseContext(self.hwnd, self.himc) }; + } +} diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 56c83ce7..5c3a01b9 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -154,6 +154,7 @@ mod drop_handler; mod event; mod event_loop; mod icon; +mod ime; mod monitor; mod raw_input; mod window; diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index a5febfe8..60179ece 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -31,10 +31,6 @@ use windows_sys::Win32::{ }, UI::{ Input::{ - Ime::{ - ImmGetContext, ImmReleaseContext, ImmSetCompositionWindow, CFS_POINT, - COMPOSITIONFORM, - }, KeyboardAndMouse::{ EnableWindow, GetActiveWindow, MapVirtualKeyW, ReleaseCapture, SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP, @@ -49,8 +45,8 @@ use windows_sys::Win32::{ SetWindowPlacement, SetWindowPos, SetWindowTextW, CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, FLASHWINFO, FLASHW_ALL, FLASHW_STOP, FLASHW_TIMERNOFG, FLASHW_TRAY, GWLP_HINSTANCE, HTCAPTION, MAPVK_VK_TO_VSC, NID_READY, PM_NOREMOVE, SM_DIGITIZER, - SM_IMMENABLED, SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER, - WM_NCLBUTTONDOWN, WNDCLASSEXW, + SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER, WM_NCLBUTTONDOWN, + WNDCLASSEXW, }, }, }; @@ -69,6 +65,7 @@ use crate::{ drop_handler::FileDropHandler, event_loop::{self, EventLoopWindowTarget, DESTROY_MSG_ID}, icon::{self, IconType}, + ime::ImeContext, monitor, util, window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState}, Parent, PlatformSpecificWindowBuilderAttributes, WindowId, @@ -626,25 +623,19 @@ impl Window { self.window_state.lock().taskbar_icon = taskbar_icon; } - pub(crate) fn set_ime_position_physical(&self, x: i32, y: i32) { - if unsafe { GetSystemMetrics(SM_IMMENABLED) } != 0 { - let composition_form = COMPOSITIONFORM { - dwStyle: CFS_POINT, - ptCurrentPos: POINT { x, y }, - rcArea: unsafe { mem::zeroed() }, - }; - unsafe { - let himc = ImmGetContext(self.hwnd()); - ImmSetCompositionWindow(himc, &composition_form); - ImmReleaseContext(self.hwnd(), himc); - } + #[inline] + pub fn set_ime_position(&self, spot: Position) { + unsafe { + ImeContext::current(self.hwnd()).set_ime_position(spot, self.scale_factor()); } } #[inline] - pub fn set_ime_position(&self, spot: Position) { - let (x, y) = spot.to_physical::(self.scale_factor()).into(); - self.set_ime_position_physical(x, y); + pub fn set_ime_allowed(&self, allowed: bool) { + self.window_state.lock().ime_allowed = allowed; + unsafe { + ImeContext::set_ime_allowed(self.hwnd(), allowed); + } } #[inline] @@ -798,6 +789,8 @@ impl<'a, T: 'static> InitData<'a, T> { enable_non_client_dpi_scaling(window); + ImeContext::set_ime_allowed(window, false); + Window { window: WindowWrapper(window), window_state, diff --git a/src/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs index 9122649b..ed71204f 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -42,6 +42,9 @@ pub struct WindowState { pub preferred_theme: Option, pub high_surrogate: Option, pub window_flags: WindowFlags, + + pub ime_state: ImeState, + pub ime_allowed: bool, } #[derive(Clone)] @@ -101,6 +104,13 @@ bitflags! { } } +#[derive(Eq, PartialEq)] +pub enum ImeState { + Disabled, + Enabled, + Preedit, +} + impl WindowState { pub fn new( attributes: &WindowAttributes, @@ -132,6 +142,9 @@ impl WindowState { preferred_theme, high_surrogate: None, window_flags: WindowFlags::empty(), + + ime_state: ImeState::Disabled, + ime_allowed: false, } } diff --git a/src/window.rs b/src/window.rs index a1f11070..96189872 100644 --- a/src/window.rs +++ b/src/window.rs @@ -814,6 +814,13 @@ impl Window { /// Sets location of IME candidate box in client area coordinates relative to the top left. /// + /// This is the window / popup / overlay that allows you to select the desired characters. + /// The look of this box may differ between input devices, even on the same platform. + /// + /// (Apple's official term is "candidate window", see their [chinese] and [japanese] guides). + /// + /// ## Example + /// /// ```no_run /// # use winit::dpi::{LogicalPosition, PhysicalPosition}; /// # use winit::event_loop::EventLoop; @@ -830,11 +837,41 @@ impl Window { /// ## Platform-specific /// /// - **iOS / Android / Web:** Unsupported. + /// + /// [chinese]: https://support.apple.com/guide/chinese-input-method/use-the-candidate-window-cim12992/104/mac/12.0 + /// [japanese]: https://support.apple.com/guide/japanese-input-method/use-the-candidate-window-jpim10262/6.3/mac/12.0 #[inline] pub fn set_ime_position>(&self, position: P) { self.window.set_ime_position(position.into()) } + /// Sets whether the window should get IME events + /// + /// When IME is allowed, the window will receive [`Ime`] events, and during the + /// preedit phase the window will NOT get [`KeyboardInput`] or + /// [`ReceivedCharacter`] events. The window should allow IME while it is + /// expecting text input. + /// + /// When IME is not allowed, the window won't receive [`Ime`] events, and will + /// receive [`KeyboardInput`] events for every keypress instead. Without + /// allowing IME, the window will also get [`ReceivedCharacter`] events for + /// certain keyboard input. Not allowing IME is useful for games for example. + /// + /// IME is **not** allowed by default. + /// + /// ## Platform-specific + /// + /// - **macOS:** IME must be enabled to receive text-input where dead-key sequences are combined. + /// - ** iOS / Android / Web :** Unsupported. + /// + /// [`Ime`]: crate::event::WindowEvent::Ime + /// [`KeyboardInput`]: crate::event::WindowEvent::KeyboardInput + /// [`ReceivedCharacter`]: crate::event::WindowEvent::ReceivedCharacter + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + self.window.set_ime_allowed(allowed); + } + /// Brings the window to the front and sets input focus. Has no effect if the window is /// already in focus, minimized, or not visible. ///