From 01773d65f6de5a76ccb055700abd0a7f54bc5aa3 Mon Sep 17 00:00:00 2001 From: MilkCOCO_ Date: Thu, 16 Apr 2026 00:15:14 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fit:=20windows=E7=9A=84ime=20preedit?= =?UTF-8?q?=E5=92=8Ccommit=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- Cargo.toml | 2 + src/event.rs | 6 ++ src/lib.rs | 9 ++- src/native.rs | 4 +- src/native/linux_x11/xi_input.rs | 6 +- src/native/windows.rs | 110 +++++++++++++++++++++---------- 7 files changed, 101 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index 2f88dbac5..97d204ad6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target **/*.rs.bk -Cargo.lock \ No newline at end of file +Cargo.lock +/.idea \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 224bebe17..2c5bf5314 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,8 @@ winapi = { version = "0.3", features = [ "hidusage", "shellapi", "imm", + "minwindef", + "ntdef" ] } [target.'cfg(target_os = "android")'.dependencies] diff --git a/src/event.rs b/src/event.rs index 82e0a9258..2eb739256 100644 --- a/src/event.rs +++ b/src/event.rs @@ -233,4 +233,10 @@ pub trait EventHandler { /// `ctx.dropped_file_path()`, and for wasm targets the file bytes /// can be requested with `ctx.dropped_file_bytes()`. fn files_dropped_event(&mut self) {} + + /// Get ime preedit text + fn on_ime_preedit(&mut self, _text: &str) {} + + /// Get ime commit text before preedit + fn on_ime_commit(&mut self, _text: Option<&str>) {} } diff --git a/src/lib.rs b/src/lib.rs index e7f9d5004..773b154ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -410,7 +410,9 @@ pub mod window { /// # Arguments /// * `enabled` - `true` to enable IME (for text input), `false` to disable (for game controls) pub fn set_ime_enabled(enabled: bool) { - let d = native_display().lock().unwrap(); + let mut d = native_display().lock().unwrap(); + d.ime_enabled = enabled; + #[cfg(target_os = "android")] { let _ = enabled; // IME control not applicable on Android @@ -423,6 +425,11 @@ pub mod window { .unwrap(); } } + + pub fn is_ime_enabled() -> bool { + let d = native_display().lock().unwrap(); + d.ime_enabled + } #[cfg(target_vendor = "apple")] pub fn apple_gfx_api() -> crate::conf::AppleGfxApi { diff --git a/src/native.rs b/src/native.rs index f38200360..2d06c578b 100644 --- a/src/native.rs +++ b/src/native.rs @@ -12,6 +12,7 @@ pub(crate) struct NativeDisplayData { pub screen_width: i32, pub screen_height: i32, pub screen_position: (u32, u32), + pub ime_enabled: bool, pub dpi_scale: f32, pub high_dpi: bool, pub quit_requested: bool, @@ -23,7 +24,7 @@ pub(crate) struct NativeDisplayData { pub clipboard: Box, pub dropped_files: DroppedFiles, pub blocking_event_loop: bool, - + #[cfg(target_vendor = "apple")] pub view: crate::native::apple::frameworks::ObjcId, #[cfg(target_os = "ios")] @@ -48,6 +49,7 @@ impl NativeDisplayData { screen_width, screen_height, screen_position: (0, 0), + ime_enabled: false, dpi_scale: 1., high_dpi: false, quit_requested: false, diff --git a/src/native/linux_x11/xi_input.rs b/src/native/linux_x11/xi_input.rs index ffb6f9876..1efca7a31 100644 --- a/src/native/linux_x11/xi_input.rs +++ b/src/native/linux_x11/xi_input.rs @@ -124,9 +124,9 @@ impl LibXi { let raw_event = xcookie.data as *mut xi_input::XIRawEvent; // Data returned from Xlib is not guaranteed to be aligned - let ptr = (*raw_event).raw_values as *const u8; - let dx = std::ptr::read_unaligned(ptr as *const f64); - let dy = std::ptr::read_unaligned(ptr.add(1) as *const f64); + let ptr = (*raw_event).raw_values as *const f64; + let dx = std::ptr::read_unaligned(ptr); + let dy = std::ptr::read_unaligned(ptr.add(1)); (self.XFreeEventData)(display, &mut (*xcookie) as *mut _); diff --git a/src/native/windows.rs b/src/native/windows.rs index 01ffe5fa4..5a3c8c1fa 100644 --- a/src/native/windows.rs +++ b/src/native/windows.rs @@ -25,8 +25,13 @@ use winapi::{ }, }; -// IME constants +// IME composition string flags +/// Composition String +const GCS_COMPSTR: DWORD = 0x0008; +/// Result String const GCS_RESULTSTR: DWORD = 0x0800; +const GCS_CURSORPOS: DWORD = 0x0100; +const GCS_DELTASTART: DWORD = 0x0200; // IME message constants const WM_IME_SETCONTEXT: UINT = 0x0281; @@ -168,15 +173,20 @@ impl WindowsDisplay { /// Enable or disable IME for the window. /// When disabled, the IME will not process keyboard input, useful for game controls. + // src/native/windows.rs (WindowsDisplay::set_ime_enabled) fn set_ime_enabled(&mut self, enabled: bool) { unsafe { if enabled { IME_USER_DISABLED.store(false, std::sync::atomic::Ordering::Relaxed); - // Re-associate IME context with the window - ImmAssociateContextEx(self.wnd, std::ptr::null_mut(), IACE_DEFAULT); + let himc = ImmGetContext(self.wnd); + if !himc.is_null() { + // 强制打开并重置 + ImmSetOpenStatus(himc, 1); + ImmAssociateContextEx(self.wnd, himc, IACE_DEFAULT); + ImmReleaseContext(self.wnd, himc); + } } else { IME_USER_DISABLED.store(true, std::sync::atomic::Ordering::Relaxed); - // Disassociate IME context from the window ImmAssociateContextEx(self.wnd, std::ptr::null_mut(), 0); } } @@ -202,6 +212,7 @@ impl WindowsDisplay { self.user_cursor = cursor_icon != CursorIcon::Default; } + fn set_window_size(&mut self, new_width: u32, new_height: u32) { let mut x = 0; let mut y = 0; @@ -590,49 +601,80 @@ unsafe extern "system" fn win32_wndproc( } WM_IME_COMPOSITION => { let flags = lparam as u32; + let himc = ImmGetContext(hwnd); - // Extract and dispatch the result string manually to avoid duplicates - if (flags & GCS_RESULTSTR) != 0 { - let himc = ImmGetContext(hwnd); - if !himc.is_null() { - let len = ImmGetCompositionStringW(himc, GCS_RESULTSTR, std::ptr::null_mut(), 0); + if !himc.is_null() { + let mut should_notify_end = false; + + // Composition String + if (flags & GCS_COMPSTR) != 0 { + let len = ImmGetCompositionStringW(himc, GCS_COMPSTR, std::ptr::null_mut(), 0); if len > 0 { let mut buffer: Vec = vec![0; (len as usize / 2) + 1]; - let actual_len = ImmGetCompositionStringW( - himc, - GCS_RESULTSTR, - buffer.as_mut_ptr() as *mut _, - len as u32 - ); - if actual_len > 0 { - let char_count = actual_len as usize / 2; - let mods = key_mods(); - // Send chars in order - for i in 0..char_count { - let chr = buffer[i]; - if let Some(c) = char::from_u32(chr as u32) { - event_handler.char_event(c, mods, false); - } - } + ImmGetCompositionStringW(himc, GCS_COMPSTR, buffer.as_mut_ptr() as *mut _, len as u32); + let char_count = len as usize / 2; + let preedit_str = String::from_utf16_lossy(&buffer[..char_count]); + + event_handler.on_ime_preedit(&preedit_str); + } else { + should_notify_end = true; + } + } + + + // Check Cursor + if (flags & GCS_CURSORPOS) != 0 || (flags & GCS_DELTASTART) != 0 { + let cursor_pos_len = ImmGetCompositionStringW(himc, GCS_CURSORPOS, std::ptr::null_mut(), 0); + let delta_start_len = ImmGetCompositionStringW(himc, GCS_DELTASTART, std::ptr::null_mut(), 0); + + if cursor_pos_len == 0 && delta_start_len == 0 { + let comp_len = ImmGetCompositionStringW(himc, GCS_COMPSTR, std::ptr::null_mut(), 0); + if comp_len == 0 { + should_notify_end = true; } } - ImmReleaseContext(hwnd, himc); } - return 0; + + // Result String + if (flags & GCS_RESULTSTR) != 0 { + let len = ImmGetCompositionStringW(himc, GCS_RESULTSTR, std::ptr::null_mut(), 0); + if len > 0 { + let mut buffer: Vec = vec![0; (len as usize / 2) + 1]; + ImmGetCompositionStringW(himc, GCS_RESULTSTR, buffer.as_mut_ptr() as *mut _, len as u32); + let char_count = len as usize / 2; + let result_str = String::from_utf16_lossy(&buffer[..char_count]); + event_handler.on_ime_commit(Some(&result_str)); + should_notify_end = false; + } else { + should_notify_end = true; + } + } + + if should_notify_end { + event_handler.on_ime_commit(None); + } + ImmReleaseContext(hwnd, himc); } - // For non-result messages (composition state updates), pass to DefWindowProc - return DefWindowProcW(hwnd, umsg, wparam, lparam); + return 0; } WM_IME_SETCONTEXT => { - let user_disabled = IME_USER_DISABLED.load(std::sync::atomic::Ordering::Relaxed); + // wparam 的高位 (HIWORD) 表示是否请求显示默认窗口 + let fShow = HIWORD(wparam as _) != 0; - // If user explicitly disabled IME, don't auto-restore - if user_disabled { + // 我们想要隐藏默认窗口(因为我们要自己画) + // 所以返回 1 (TRUE) 告诉系统:"是的,我收到了,但我决定不显示默认窗口" + // 注意:这里不需要调用 DefWindowProc,直接返回即可,或者根据需求微调 + + // 关键:如果用户没有禁用 IME,我们强制让系统不要画默认框 + let user_disabled = IME_USER_DISABLED.load(std::sync::atomic::Ordering::Relaxed); + if !user_disabled { + // 返回 1 表示 "我处理了,别画默认框" + return 1; + } else { return 0; } - // Must pass to DefWindowProc to enable IME properly return DefWindowProcW(hwnd, umsg, wparam, lparam); } WM_IME_STARTCOMPOSITION => { @@ -687,6 +729,8 @@ unsafe extern "system" fn win32_wndproc( return DefWindowProcW(hwnd, umsg, wparam, lparam); } WM_INPUTLANGCHANGEREQUEST | WM_INPUTLANGCHANGE => { + event_handler.on_ime_commit(None); + // Pass input language change messages to default handler return DefWindowProcW(hwnd, umsg, wparam, lparam); } From 85bc0ec15a9999bdfc96136cdf8bd2a7bafb2ab7 Mon Sep 17 00:00:00 2001 From: MilkCOCO_ Date: Sun, 19 Apr 2026 00:05:08 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E8=AF=AF=E8=A7=A6=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E7=9A=84=E6=8C=87=E9=92=88=E7=B1=BB=E5=9E=8B=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/native/linux_x11/xi_input.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/native/linux_x11/xi_input.rs b/src/native/linux_x11/xi_input.rs index 1efca7a31..ffb6f9876 100644 --- a/src/native/linux_x11/xi_input.rs +++ b/src/native/linux_x11/xi_input.rs @@ -124,9 +124,9 @@ impl LibXi { let raw_event = xcookie.data as *mut xi_input::XIRawEvent; // Data returned from Xlib is not guaranteed to be aligned - let ptr = (*raw_event).raw_values as *const f64; - let dx = std::ptr::read_unaligned(ptr); - let dy = std::ptr::read_unaligned(ptr.add(1)); + let ptr = (*raw_event).raw_values as *const u8; + let dx = std::ptr::read_unaligned(ptr as *const f64); + let dy = std::ptr::read_unaligned(ptr.add(1) as *const f64); (self.XFreeEventData)(display, &mut (*xcookie) as *mut _); From b77104e8f35754b4093e150ec8c7d1f8f3741725 Mon Sep 17 00:00:00 2001 From: MilkCOCO_ Date: Sun, 19 Apr 2026 00:16:52 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20set=5Fime=5Fenabled=E4=B8=AD?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E7=9A=84=E5=BC=BA=E5=88=B6=E9=87=8D=E8=AE=BE?= =?UTF-8?q?ime=E4=B8=8A=E4=B8=8B=E6=96=87=20=E8=BF=99=E6=AE=B5=E6=98=AFai?= =?UTF-8?q?=E5=86=99=E7=9A=84=E4=B8=8D=E6=80=A8=E6=88=91=EF=BC=81=EF=BC=81?= =?UTF-8?q?qwq=EF=BC=81=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/native/windows.rs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/native/windows.rs b/src/native/windows.rs index 5a3c8c1fa..d8182ff61 100644 --- a/src/native/windows.rs +++ b/src/native/windows.rs @@ -173,20 +173,15 @@ impl WindowsDisplay { /// Enable or disable IME for the window. /// When disabled, the IME will not process keyboard input, useful for game controls. - // src/native/windows.rs (WindowsDisplay::set_ime_enabled) fn set_ime_enabled(&mut self, enabled: bool) { unsafe { if enabled { IME_USER_DISABLED.store(false, std::sync::atomic::Ordering::Relaxed); - let himc = ImmGetContext(self.wnd); - if !himc.is_null() { - // 强制打开并重置 - ImmSetOpenStatus(himc, 1); - ImmAssociateContextEx(self.wnd, himc, IACE_DEFAULT); - ImmReleaseContext(self.wnd, himc); - } + // Re-associate IME context with the window + ImmAssociateContextEx(self.wnd, std::ptr::null_mut(), IACE_DEFAULT); } else { IME_USER_DISABLED.store(true, std::sync::atomic::Ordering::Relaxed); + // Disassociate IME context from the window ImmAssociateContextEx(self.wnd, std::ptr::null_mut(), 0); } } @@ -659,14 +654,8 @@ unsafe extern "system" fn win32_wndproc( return 0; } WM_IME_SETCONTEXT => { - // wparam 的高位 (HIWORD) 表示是否请求显示默认窗口 let fShow = HIWORD(wparam as _) != 0; - - // 我们想要隐藏默认窗口(因为我们要自己画) - // 所以返回 1 (TRUE) 告诉系统:"是的,我收到了,但我决定不显示默认窗口" - // 注意:这里不需要调用 DefWindowProc,直接返回即可,或者根据需求微调 - - // 关键:如果用户没有禁用 IME,我们强制让系统不要画默认框 + let user_disabled = IME_USER_DISABLED.load(std::sync::atomic::Ordering::Relaxed); if !user_disabled { // 返回 1 表示 "我处理了,别画默认框" @@ -841,6 +830,8 @@ unsafe extern "system" fn win32_wndproc( return DefWindowProcW(hwnd, umsg, wparam, lparam); } WM_KILLFOCUS => { + event_handler.on_ime_commit(None); + return DefWindowProcW(hwnd, umsg, wparam, lparam); } _ => {}