Add new Ime event for desktop platforms
This commit brings new Ime event to account for preedit state of input method, also adding `Window::set_ime_allowed` to toggle IME input on the particular window. This commit implements API as designed in #1497 for desktop platforms. Co-authored-by: Artur Kovacs <kovacs.artur.barnabas@gmail.com> Co-authored-by: Markus Siglreithmaier <m.siglreith@gmail.com> Co-authored-by: Murarth <murarth@gmail.com> Co-authored-by: Yusuke Kominami <yukke.konan@gmail.com> Co-authored-by: moko256 <koutaro.mo@gmail.com>
This commit is contained in:
parent
b4175c1454
commit
f04fa5d54f
30 changed files with 1346 additions and 311 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
150
src/platform_impl/windows/ime.rs
Normal file
150
src/platform_impl/windows/ime.rs
Normal 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) };
|
||||
}
|
||||
}
|
||||
|
|
@ -154,6 +154,7 @@ mod drop_handler;
|
|||
mod event;
|
||||
mod event_loop;
|
||||
mod icon;
|
||||
mod ime;
|
||||
mod monitor;
|
||||
mod raw_input;
|
||||
mod window;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue