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

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