Add new Ime event for desktop platforms

This commit brings new Ime event to account for preedit state of input
method, also adding `Window::set_ime_allowed` to toggle IME input on
the particular window.

This commit implements API as designed in #1497 for desktop platforms.

Co-authored-by: Artur Kovacs <kovacs.artur.barnabas@gmail.com>
Co-authored-by: Markus Siglreithmaier <m.siglreith@gmail.com>
Co-authored-by: Murarth <murarth@gmail.com>
Co-authored-by: Yusuke Kominami <yukke.konan@gmail.com>
Co-authored-by: moko256 <koutaro.mo@gmail.com>
This commit is contained in:
Kirill Chibisov 2022-05-07 05:29:25 +03:00 committed by GitHub
parent b4175c1454
commit f04fa5d54f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1346 additions and 311 deletions

View file

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

View file

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

97
examples/ime.rs Normal file
View file

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

View file

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

View file

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

View file

@ -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<window::UserAttentionType>) {}

View file

@ -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")
}

View file

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

View file

@ -32,10 +32,9 @@ mod sink;
mod state;
pub use proxy::EventLoopProxy;
pub use sink::EventSink;
pub use state::WinitState;
use sink::EventSink;
type WinitDispatcher = calloop::Dispatcher<'static, WaylandSource, WinitState>;
pub struct EventLoopWindowTarget<T> {

View file

@ -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.
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);
}
}
_ => (),

View file

@ -20,6 +20,17 @@ impl TextInputHandler {
self.text_input.set_cursor_rectangle(x, y, 0, 0);
self.text_input.commit();
}
#[inline]
pub fn set_input_allowed(&self, allowed: bool) {
if allowed {
self.text_input.enable();
} else {
self.text_input.disable();
}
self.text_input.commit();
}
}
/// A wrapper around text input to automatically destroy the object on `Drop`.
@ -52,15 +63,25 @@ struct TextInputInner {
/// Currently focused surface.
target_window_id: Option<WindowId>,
/// Pending string to commit.
commit_string: Option<String>,
/// Pending commit event which will be dispatched on `text_input_v3::Done`.
pending_commit: Option<String>,
/// Pending preedit event which will be dispatched on `text_input_v3::Done`.
pending_preedit: Option<Preedit>,
}
struct Preedit {
text: String,
cursor_begin: Option<usize>,
cursor_end: Option<usize>,
}
impl TextInputInner {
fn new() -> Self {
Self {
target_window_id: None,
commit_string: None,
pending_commit: None,
pending_preedit: None,
}
}
}

View file

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

View file

@ -12,10 +12,10 @@ use sctk::window::{Decorations, FallbackFrame, Window};
use crate::dpi::{LogicalPosition, LogicalSize};
use crate::event::WindowEvent;
use crate::event::{Ime, WindowEvent};
use crate::platform_impl::wayland;
use crate::platform_impl::wayland::env::WinitEnv;
use crate::platform_impl::wayland::event_loop::WinitState;
use crate::platform_impl::wayland::event_loop::{EventSink, WinitState};
use crate::platform_impl::wayland::seat::pointer::WinitPointer;
use crate::platform_impl::wayland::seat::text_input::TextInputHandler;
use crate::platform_impl::wayland::WindowId;
@ -69,7 +69,10 @@ pub enum WindowRequest {
FrameSize(LogicalSize<u32>),
/// Set IME window position.
IMEPosition(LogicalPosition<u32>),
ImePosition(LogicalPosition<u32>),
/// Enable IME on the given window.
AllowIme(bool),
/// Request Attention.
///
@ -157,6 +160,9 @@ pub struct WindowHandle {
/// Whether the window is resizable.
pub is_resizable: Cell<bool>,
/// Allow IME events for that window.
pub ime_allowed: Cell<bool>,
/// Visible cursor or not.
cursor_visible: Cell<bool>,
@ -204,6 +210,7 @@ impl WindowHandle {
xdg_activation,
attention_requested: Cell::new(false),
compositor,
ime_allowed: Cell::new(false),
}
}
@ -333,6 +340,27 @@ impl WindowHandle {
}
}
pub fn set_ime_allowed(&self, allowed: bool, event_sink: &mut EventSink) {
if self.ime_allowed.get() == allowed {
return;
}
self.ime_allowed.replace(allowed);
let window_id = wayland::make_wid(self.window.surface());
for text_input in self.text_inputs.iter() {
text_input.set_input_allowed(allowed);
}
let event = if allowed {
WindowEvent::Ime(Ime::Enabled)
} else {
WindowEvent::Ime(Ime::Disabled)
};
event_sink.push_window_event(event, window_id);
}
pub fn set_cursor_visible(&self, visible: bool) {
self.cursor_visible.replace(visible);
let cursor_icon = match visible {
@ -387,9 +415,13 @@ pub fn handle_window_requests(winit_state: &mut WinitState) {
WindowRequest::NewCursorIcon(cursor_icon) => {
window_handle.set_cursor_icon(cursor_icon);
}
WindowRequest::IMEPosition(position) => {
WindowRequest::ImePosition(position) => {
window_handle.set_ime_position(position);
}
WindowRequest::AllowIme(allow) => {
let event_sink = &mut winit_state.event_sink;
window_handle.set_ime_allowed(allow, event_sink);
}
WindowRequest::GrabCursor(grab) => {
window_handle.set_cursor_grab(grab);
}

View file

@ -12,10 +12,12 @@ use super::{
use util::modifiers::{ModifierKeyState, ModifierKeymap};
use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventReceiver, ImeRequest};
use crate::{
dpi::{PhysicalPosition, PhysicalSize},
event::{
DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, TouchPhase, WindowEvent,
DeviceEvent, ElementState, Event, Ime, KeyboardInput, ModifiersState, TouchPhase,
WindowEvent,
},
event_loop::EventLoopWindowTarget as RootELW,
};
@ -26,6 +28,7 @@ const KEYCODE_OFFSET: u8 = 8;
pub(super) struct EventProcessor<T: 'static> {
pub(super) dnd: Dnd,
pub(super) ime_receiver: ImeReceiver,
pub(super) ime_event_receiver: ImeEventReceiver,
pub(super) randr_event_offset: c_int,
pub(super) devices: RefCell<HashMap<DeviceId, Device>>,
pub(super) xi2ext: XExtension,
@ -37,6 +40,7 @@ pub(super) struct EventProcessor<T: 'static> {
pub(super) first_touch: Option<u64>,
// Currently focused window belonging to this process
pub(super) active_window: Option<ffi::Window>,
pub(super) is_composing: bool,
}
impl<T: 'static> EventProcessor<T> {
@ -567,7 +571,7 @@ impl<T: 'static> EventProcessor<T> {
// When a compose sequence or IME pre-edit is finished, it ends in a KeyPress with
// a keycode of 0.
if keycode != 0 {
if keycode != 0 && !self.is_composing {
let scancode = keycode - KEYCODE_OFFSET as u32;
let keysym = wt.xconn.lookup_keysym(xkev);
let virtual_keycode = events::keysym_to_element(keysym as c_uint);
@ -602,15 +606,28 @@ impl<T: 'static> EventProcessor<T> {
return;
};
// 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::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);
}
}
}
}
ffi::GenericEvent => {
let guard = if let Some(e) = GenericEventCookie::from_event(&wt.xconn, *xev) {
@ -1223,8 +1240,59 @@ impl<T: 'static> EventProcessor<T> {
}
}
if let Ok((window_id, x, y)) = self.ime_receiver.try_recv() {
wt.ime.borrow_mut().send_xim_spot(window_id, x, y);
// Handle IME requests.
if let Ok(request) = self.ime_receiver.try_recv() {
let mut ime = wt.ime.borrow_mut();
match request {
ImeRequest::Position(window_id, x, y) => {
ime.send_xim_spot(window_id, x, y);
}
ImeRequest::Allow(window_id, allowed) => {
ime.set_ime_allowed(window_id, allowed);
}
}
}
match self.ime_event_receiver.try_recv() {
Ok((window, event)) => match event {
ImeEvent::Enabled => {
callback(Event::WindowEvent {
window_id: mkwid(window),
event: WindowEvent::Ime(Ime::Enabled),
});
}
ImeEvent::Start => {
self.is_composing = true;
callback(Event::WindowEvent {
window_id: mkwid(window),
event: WindowEvent::Ime(Ime::Preedit("".to_owned(), None)),
});
}
ImeEvent::Update(text, position) => {
if self.is_composing {
callback(Event::WindowEvent {
window_id: mkwid(window),
event: WindowEvent::Ime(Ime::Preedit(text, Some((position, position)))),
});
}
}
ImeEvent::End => {
self.is_composing = false;
// Issue empty preedit on `Done`.
callback(Event::WindowEvent {
window_id: mkwid(window),
event: WindowEvent::Ime(Ime::Preedit(String::new(), None)),
});
}
ImeEvent::Disabled => {
self.is_composing = false;
callback(Event::WindowEvent {
window_id: mkwid(window),
event: WindowEvent::Ime(Ime::Disabled),
});
}
},
Err(_) => (),
}
}

View file

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

View file

@ -1,41 +1,196 @@
use std::{
os::raw::{c_short, c_void},
ptr,
sync::Arc,
};
use std::mem::transmute;
use std::os::raw::c_short;
use std::ptr;
use std::sync::Arc;
use super::{ffi, util, XConnection, XError};
use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender};
use std::ffi::CStr;
use x11_dl::xlib::{XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct};
/// IME creation error.
#[derive(Debug)]
pub enum ImeContextCreationError {
/// Got the error from Xlib.
XError(XError),
/// Got null pointer from Xlib but without exact reason.
Null,
}
unsafe fn create_pre_edit_attr<'a>(
xconn: &'a Arc<XConnection>,
ic_spot: &'a ffi::XPoint,
) -> util::XSmartPointer<'a, c_void> {
util::XSmartPointer::new(
xconn,
(xconn.xlib.XVaCreateNestedList)(
0,
ffi::XNSpotLocation_0.as_ptr() as *const _,
ic_spot,
ptr::null_mut::<()>(),
),
)
.expect("XVaCreateNestedList returned NULL")
/// The callback used by XIM preedit functions.
type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer);
/// Wrapper for creating XIM callbacks.
#[inline]
fn create_xim_callback(client_data: ffi::XPointer, callback: XIMProcNonnull) -> ffi::XIMCallback {
XIMCallback {
client_data,
callback: Some(callback),
}
}
// WARNING: this struct doesn't destroy its XIC resource when dropped.
/// The server started preedit.
extern "C" fn preedit_start_callback(
_xim: ffi::XIM,
client_data: ffi::XPointer,
_call_data: ffi::XPointer,
) -> i32 {
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
client_data.text.clear();
client_data.cursor_pos = 0;
client_data
.event_sender
.send((client_data.window, ImeEvent::Start))
.expect("failed to send preedit start event");
-1
}
/// Done callback is used when the preedit should be hidden.
extern "C" fn preedit_done_callback(
_xim: ffi::XIM,
client_data: ffi::XPointer,
_call_data: ffi::XPointer,
) {
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
// Drop text buffer and reset cursor position on done.
client_data.text = Vec::new();
client_data.cursor_pos = 0;
client_data
.event_sender
.send((client_data.window, ImeEvent::End))
.expect("failed to send preedit end event");
}
fn calc_byte_position(text: &Vec<char>, pos: usize) -> usize {
let mut byte_pos = 0;
for i in 0..pos {
byte_pos += text[i].len_utf8();
}
byte_pos
}
/// Preedit text information to be drawn inline by the client.
extern "C" fn preedit_draw_callback(
_xim: ffi::XIM,
client_data: ffi::XPointer,
call_data: ffi::XPointer,
) {
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
let call_data = unsafe { &mut *(call_data as *mut XIMPreeditDrawCallbackStruct) };
client_data.cursor_pos = call_data.caret as usize;
let chg_range =
call_data.chg_first as usize..(call_data.chg_first + call_data.chg_length) as usize;
if chg_range.start > client_data.text.len() || chg_range.end > client_data.text.len() {
warn!(
"invalid chg range: buffer length={}, but chg_first={} chg_lengthg={}",
client_data.text.len(),
call_data.chg_first,
call_data.chg_length
);
return;
}
// NULL indicate text deletion
let mut new_chars = if call_data.text.is_null() {
Vec::new()
} else {
let xim_text = unsafe { &mut *(call_data.text) };
if xim_text.encoding_is_wchar > 0 {
return;
}
let new_text = unsafe { CStr::from_ptr(xim_text.string.multi_byte) };
String::from(new_text.to_str().expect("Invalid UTF-8 String from IME"))
.chars()
.collect()
};
let mut old_text_tail = client_data.text.split_off(chg_range.end);
client_data.text.truncate(chg_range.start);
client_data.text.append(&mut new_chars);
client_data.text.append(&mut old_text_tail);
let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
client_data
.event_sender
.send((
client_data.window,
ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
))
.expect("failed to send preedit update event");
}
/// Handling of cursor movements in preedit text.
extern "C" fn preedit_caret_callback(
_xim: ffi::XIM,
client_data: ffi::XPointer,
call_data: ffi::XPointer,
) {
let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) };
let call_data = unsafe { &mut *(call_data as *mut XIMPreeditCaretCallbackStruct) };
if call_data.direction == ffi::XIMCaretDirection::XIMAbsolutePosition {
client_data.cursor_pos = call_data.position as usize;
let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos);
client_data
.event_sender
.send((
client_data.window,
ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos),
))
.expect("failed to send preedit update event");
}
}
/// Struct to simplify callback creation and latter passing into Xlib XIM.
struct PreeditCallbacks {
start_callback: ffi::XIMCallback,
done_callback: ffi::XIMCallback,
draw_callback: ffi::XIMCallback,
caret_callback: ffi::XIMCallback,
}
impl PreeditCallbacks {
pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks {
let start_callback = create_xim_callback(client_data, unsafe {
transmute(preedit_start_callback as usize)
});
let done_callback = create_xim_callback(client_data, preedit_done_callback);
let caret_callback = create_xim_callback(client_data, preedit_caret_callback);
let draw_callback = create_xim_callback(client_data, preedit_draw_callback);
PreeditCallbacks {
start_callback,
done_callback,
caret_callback,
draw_callback,
}
}
}
struct ImeContextClientData {
window: ffi::Window,
event_sender: ImeEventSender,
text: Vec<char>,
cursor_pos: usize,
}
// XXX: this struct doesn't destroy its XIC resource when dropped.
// This is intentional, as it doesn't have enough information to know whether or not the context
// still exists on the server. Since `ImeInner` has that awareness, destruction must be handled
// through `ImeInner`.
#[derive(Debug)]
pub struct ImeContext {
pub ic: ffi::XIC,
pub ic_spot: ffi::XPoint,
pub(super) ic: ffi::XIC,
pub(super) ic_spot: ffi::XPoint,
pub(super) is_allowed: bool,
// Since the data is passed shared between X11 XIM callbacks, but couldn't be direclty free from
// there we keep the pointer to automatically deallocate it.
_client_data: Box<ImeContextClientData>,
}
impl ImeContext {
@ -44,66 +199,111 @@ impl ImeContext {
im: ffi::XIM,
window: ffi::Window,
ic_spot: Option<ffi::XPoint>,
is_allowed: bool,
event_sender: ImeEventSender,
) -> Result<Self, ImeContextCreationError> {
let ic = if let Some(ic_spot) = ic_spot {
ImeContext::create_ic_with_spot(xconn, im, window, ic_spot)
let client_data = Box::into_raw(Box::new(ImeContextClientData {
window,
event_sender,
text: Vec::new(),
cursor_pos: 0,
}));
let ic = if is_allowed {
ImeContext::create_ic(xconn, im, window, client_data as ffi::XPointer)
.ok_or(ImeContextCreationError::Null)?
} else {
ImeContext::create_ic(xconn, im, window)
ImeContext::create_none_ic(xconn, im, window).ok_or(ImeContextCreationError::Null)?
};
let ic = ic.ok_or(ImeContextCreationError::Null)?;
xconn
.check_errors()
.map_err(ImeContextCreationError::XError)?;
Ok(ImeContext {
let mut context = ImeContext {
ic,
ic_spot: ic_spot.unwrap_or(ffi::XPoint { x: 0, y: 0 }),
})
ic_spot: ffi::XPoint { x: 0, y: 0 },
is_allowed,
_client_data: Box::from_raw(client_data),
};
// Set the spot location, if it's present.
if let Some(ic_spot) = ic_spot {
context.set_spot(xconn, ic_spot.x, ic_spot.y)
}
Ok(context)
}
unsafe fn create_none_ic(
xconn: &Arc<XConnection>,
im: ffi::XIM,
window: ffi::Window,
) -> Option<ffi::XIC> {
let ic = (xconn.xlib.XCreateIC)(
im,
ffi::XNInputStyle_0.as_ptr() as *const _,
ffi::XIMPreeditNone | ffi::XIMStatusNone,
ffi::XNClientWindow_0.as_ptr() as *const _,
window,
ptr::null_mut::<()>(),
);
(!ic.is_null()).then(|| ic)
}
unsafe fn create_ic(
xconn: &Arc<XConnection>,
im: ffi::XIM,
window: ffi::Window,
client_data: ffi::XPointer,
) -> Option<ffi::XIC> {
let ic = (xconn.xlib.XCreateIC)(
im,
ffi::XNInputStyle_0.as_ptr() as *const _,
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
ffi::XNClientWindow_0.as_ptr() as *const _,
window,
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::<()>(),
);
if ic.is_null() {
None
} else {
Some(ic)
}
}
),
)
.expect("XVaCreateNestedList returned NULL");
unsafe fn create_ic_with_spot(
xconn: &Arc<XConnection>,
im: ffi::XIM,
window: ffi::Window,
ic_spot: ffi::XPoint,
) -> Option<ffi::XIC> {
let pre_edit_attr = create_pre_edit_attr(xconn, &ic_spot);
let ic = {
let ic = (xconn.xlib.XCreateIC)(
im,
ffi::XNInputStyle_0.as_ptr() as *const _,
ffi::XIMPreeditNothing | ffi::XIMStatusNothing,
ffi::XIMPreeditCallbacks | ffi::XIMStatusNothing,
ffi::XNClientWindow_0.as_ptr() as *const _,
window,
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
pre_edit_attr.ptr,
preedit_attr.ptr,
ptr::null_mut::<()>(),
);
// If we've failed to create IC with preedit callbacks fallback to normal one.
if ic.is_null() {
None
(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 {
Some(ic)
ic
}
};
(!ic.is_null()).then(|| ic)
}
pub fn focus(&self, xconn: &Arc<XConnection>) -> Result<(), XError> {
@ -120,18 +320,34 @@ impl ImeContext {
xconn.check_errors()
}
// Set the spot for preedit text. Setting spot isn't working with libX11 when preedit callbacks
// are being used. Certain IMEs do show selection window, but it's placed in bottom left of the
// window and couldn't be changed.
//
// For me see: https://bugs.freedesktop.org/show_bug.cgi?id=1580.
pub fn set_spot(&mut self, xconn: &Arc<XConnection>, x: c_short, y: c_short) {
if self.ic_spot.x == x && self.ic_spot.y == y {
if !self.is_allowed || self.ic_spot.x == x && self.ic_spot.y == y {
return;
}
self.ic_spot = ffi::XPoint { x, y };
unsafe {
let pre_edit_attr = create_pre_edit_attr(xconn, &self.ic_spot);
let preedit_attr = util::XSmartPointer::new(
xconn,
(xconn.xlib.XVaCreateNestedList)(
0,
ffi::XNSpotLocation_0.as_ptr(),
&self.ic_spot,
ptr::null_mut::<()>(),
),
)
.expect("XVaCreateNestedList returned NULL");
(xconn.xlib.XSetICValues)(
self.ic,
ffi::XNPreeditAttributes_0.as_ptr() as *const _,
pre_edit_attr.ptr,
preedit_attr.ptr,
ptr::null_mut::<()>(),
);
}

View file

@ -3,6 +3,7 @@ use std::{collections::HashMap, mem, ptr, sync::Arc};
use super::{ffi, XConnection, XError};
use super::{context::ImeContext, input_method::PotentialInputMethods};
use crate::platform_impl::platform::x11::ime::ImeEventSender;
pub unsafe fn close_im(xconn: &Arc<XConnection>, im: ffi::XIM) -> Result<(), XError> {
(xconn.xlib.XCloseIM)(im);
@ -22,6 +23,7 @@ pub struct ImeInner {
pub contexts: HashMap<ffi::Window, Option<ImeContext>>,
// WARNING: this is initially zeroed!
pub destroy_callback: ffi::XIMCallback,
pub event_sender: ImeEventSender,
// Indicates whether or not the the input method was destroyed on the server end
// (i.e. if ibus/fcitx/etc. was terminated/restarted)
pub is_destroyed: bool,
@ -29,13 +31,18 @@ pub struct ImeInner {
}
impl ImeInner {
pub fn new(xconn: Arc<XConnection>, potential_input_methods: PotentialInputMethods) -> Self {
pub fn new(
xconn: Arc<XConnection>,
potential_input_methods: PotentialInputMethods,
event_sender: ImeEventSender,
) -> Self {
ImeInner {
xconn,
im: ptr::null_mut(),
potential_input_methods,
contexts: HashMap::new(),
destroy_callback: unsafe { mem::zeroed() },
event_sender,
is_destroyed: false,
is_fallback: false,
}

View file

@ -19,9 +19,29 @@ use self::{
inner::{close_im, ImeInner},
input_method::PotentialInputMethods,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ImeEvent {
Enabled,
Start,
Update(String, usize),
End,
Disabled,
}
pub type ImeReceiver = Receiver<(ffi::Window, i16, i16)>;
pub type ImeSender = Sender<(ffi::Window, i16, i16)>;
pub type ImeReceiver = Receiver<ImeRequest>;
pub type ImeSender = Sender<ImeRequest>;
pub type ImeEventReceiver = Receiver<(ffi::Window, ImeEvent)>;
pub type ImeEventSender = Sender<(ffi::Window, ImeEvent)>;
/// Request to control XIM handler from the window.
pub enum ImeRequest {
/// Set IME spot position for given `window_id`.
Position(ffi::Window, i16, i16),
/// Allow IME input for the given `window_id`.
Allow(ffi::Window, bool),
}
#[derive(Debug)]
pub enum ImeCreationError {
@ -37,11 +57,14 @@ pub struct Ime {
}
impl Ime {
pub fn new(xconn: Arc<XConnection>) -> Result<Self, ImeCreationError> {
pub fn new(
xconn: Arc<XConnection>,
event_sender: ImeEventSender,
) -> Result<Self, ImeCreationError> {
let potential_input_methods = PotentialInputMethods::new(&xconn);
let (mut inner, client_data) = {
let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods));
let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods, event_sender));
let inner_ptr = Box::into_raw(inner);
let client_data = inner_ptr as _;
let destroy_callback = ffi::XIMCallback {
@ -88,12 +111,37 @@ impl Ime {
// Ok(_) indicates that nothing went wrong internally
// Ok(true) indicates that the action was actually performed
// Ok(false) indicates that the action is not presently applicable
pub fn create_context(&mut self, window: ffi::Window) -> Result<bool, ImeContextCreationError> {
pub fn create_context(
&mut self,
window: ffi::Window,
with_preedit: bool,
) -> Result<bool, ImeContextCreationError> {
let context = if self.is_destroyed() {
// Create empty entry in map, so that when IME is rebuilt, this window has a context.
None
} else {
Some(unsafe { ImeContext::new(&self.inner.xconn, self.inner.im, window, None) }?)
let event = if with_preedit {
ImeEvent::Enabled
} else {
// There's no IME without preedit.
ImeEvent::Disabled
};
self.inner
.event_sender
.send((window, event))
.expect("Failed to send enabled event");
Some(unsafe {
ImeContext::new(
&self.inner.xconn,
self.inner.im,
window,
None,
with_preedit,
self.inner.event_sender.clone(),
)
}?)
};
self.inner.contexts.insert(window, context);
Ok(!self.is_destroyed())
@ -151,6 +199,24 @@ impl Ime {
context.set_spot(&self.xconn, x as _, y as _);
}
}
pub fn set_ime_allowed(&mut self, window: ffi::Window, allowed: bool) {
if self.is_destroyed() {
return;
}
if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) {
if allowed == context.is_allowed {
return;
}
}
// Remove context for that window.
let _ = self.remove_context(window);
// Create new context supporting IME input.
let _ = self.create_context(window, allowed);
}
}
impl Drop for Ime {

View file

@ -44,7 +44,7 @@ use mio::{unix::SourceFd, Events, Interest, Poll, Token, Waker};
use self::{
dnd::{Dnd, DndState},
event_processor::EventProcessor,
ime::{Ime, ImeCreationError, ImeReceiver, ImeSender},
ime::{Ime, ImeCreationError, ImeReceiver, ImeRequest, ImeSender},
util::modifiers::ModifierKeymap,
};
use crate::{
@ -144,6 +144,7 @@ impl<T: 'static> EventLoop<T> {
.expect("Failed to call XInternAtoms when initializing drag and drop");
let (ime_sender, ime_receiver) = mpsc::channel();
let (ime_event_sender, ime_event_receiver) = mpsc::channel();
// Input methods will open successfully without setting the locale, but it won't be
// possible to actually commit pre-edit sequences.
unsafe {
@ -168,7 +169,7 @@ impl<T: 'static> EventLoop<T> {
}
}
let ime = RefCell::new({
let result = Ime::new(Arc::clone(&xconn));
let result = Ime::new(Arc::clone(&xconn), ime_event_sender);
if let Err(ImeCreationError::OpenFailure(ref state)) = result {
panic!("Failed to open input method: {:#?}", state);
}
@ -252,12 +253,14 @@ impl<T: 'static> EventLoop<T> {
devices: Default::default(),
randr_event_offset,
ime_receiver,
ime_event_receiver,
xi2ext,
mod_keymap,
device_mod_state: Default::default(),
num_touch: 0,
first_touch: None,
active_window: None,
is_composing: false,
};
// Register for device hotplug events

View file

@ -26,7 +26,8 @@ use crate::{
};
use super::{
ffi, util, EventLoopWindowTarget, ImeSender, WakeSender, WindowId, XConnection, XError,
ffi, util, EventLoopWindowTarget, ImeRequest, ImeSender, WakeSender, WindowId, XConnection,
XError,
};
#[derive(Debug)]
@ -453,7 +454,10 @@ impl UnownedWindow {
.queue();
{
let result = event_loop.ime.borrow_mut().create_context(window.xwindow);
let result = event_loop
.ime
.borrow_mut()
.create_context(window.xwindow, false);
if let Err(err) = result {
let e = match err {
ImeContextCreationError::XError(err) => OsError::XError(err),
@ -1410,17 +1414,21 @@ impl UnownedWindow {
.map_err(|err| ExternalError::Os(os_error!(OsError::XError(err))))
}
pub(crate) fn set_ime_position_physical(&self, x: i32, y: i32) {
let _ = self
.ime_sender
.lock()
.send((self.xwindow, x as i16, y as i16));
}
#[inline]
pub fn set_ime_position(&self, spot: Position) {
let (x, y) = spot.to_physical::<i32>(self.scale_factor()).into();
self.set_ime_position_physical(x, y);
let _ = self
.ime_sender
.lock()
.send(ImeRequest::Position(self.xwindow, x, y));
}
#[inline]
pub fn set_ime_allowed(&self, allowed: bool) {
let _ = self
.ime_sender
.lock()
.send(ImeRequest::Allow(self.xwindow, allowed));
}
#[inline]

View file

@ -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()
}

View file

@ -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<Mutex<CursorState>>,
/// The position of the candidate window.
ime_position: LogicalPosition<f64>,
raw_characters: Option<String>,
pub(super) modifiers: ModifiersState,
tracking_rect: Option<NSInteger>,
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<Mutex<CursorState>>) {
@ -72,11 +97,13 @@ pub fn new_view(ns_window: id) -> (IdRef, Weak<Mutex<CursorState>>) {
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<f64>) {
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 =
<id as NSMutableAttributedString>::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,17 +723,48 @@ 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);
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,
@ -663,28 +780,14 @@ extern "C" fn key_down(this: &Object, _sel: Sel, event: id) {
},
};
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];
}
}
}
@ -700,6 +803,8 @@ extern "C" fn key_up(this: &Object, _sel: Sel, event: id) {
update_potentially_stale_modifiers(state, 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)),
@ -718,6 +823,7 @@ extern "C" fn key_up(this: &Object, _sel: Sel, event: id) {
AppState::queue_event(EventWrapper::StaticEvent(window_event));
}
}
}
extern "C" fn flags_changed(this: &Object, _sel: Sel, event: id) {
trace_scope!("flagsChanged:");

View file

@ -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] };

View file

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

View file

@ -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<T: 'static>(
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 {

View file

@ -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<usize>, Option<usize>)> {
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<String> {
self.get_composition_string(GCS_RESULTSTR)
}
unsafe fn get_composition_cursor(&self, text: &str) -> Option<usize> {
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<String> {
let data = self.get_composition_data(gcs_mode)?;
let (prefix, shorts, suffix) = data.align_to::<u16>();
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<Vec<u8>> {
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::<u8>::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::<i32>(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) };
}
}

View file

@ -154,6 +154,7 @@ mod drop_handler;
mod event;
mod event_loop;
mod icon;
mod ime;
mod monitor;
mod raw_input;
mod window;

View file

@ -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() },
};
#[inline]
pub fn set_ime_position(&self, spot: Position) {
unsafe {
let himc = ImmGetContext(self.hwnd());
ImmSetCompositionWindow(himc, &composition_form);
ImmReleaseContext(self.hwnd(), himc);
}
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::<i32>(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,

View file

@ -42,6 +42,9 @@ pub struct WindowState {
pub preferred_theme: Option<Theme>,
pub high_surrogate: Option<u16>,
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,
}
}

View file

@ -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<P: Into<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.
///