winit/platform_impl/windows/
ime.rs1use std::ffi::{c_void, OsString};
2use std::os::windows::prelude::OsStringExt;
3use std::ptr::null_mut;
4
5use windows_sys::Win32::Foundation::{POINT, RECT};
6use windows_sys::Win32::Globalization::HIMC;
7use windows_sys::Win32::UI::Input::Ime::{
8 ImmAssociateContextEx, ImmGetCompositionStringW, ImmGetContext, ImmReleaseContext,
9 ImmSetCandidateWindow, ImmSetCompositionWindow, ATTR_TARGET_CONVERTED,
10 ATTR_TARGET_NOTCONVERTED, CANDIDATEFORM, CFS_EXCLUDE, CFS_POINT, COMPOSITIONFORM, GCS_COMPATTR,
11 GCS_COMPSTR, GCS_CURSORPOS, GCS_RESULTSTR, IACE_CHILDREN, IACE_DEFAULT,
12};
13use windows_sys::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_IMMENABLED};
14
15use crate::dpi::{Position, Size};
16use crate::platform::windows::HWND;
17
18pub struct ImeContext {
19 hwnd: HWND,
20 himc: HIMC,
21}
22
23impl ImeContext {
24 pub unsafe fn current(hwnd: HWND) -> Self {
25 let himc = unsafe { ImmGetContext(hwnd) };
26 ImeContext { hwnd, himc }
27 }
28
29 pub unsafe fn get_composing_text_and_cursor(
30 &self,
31 ) -> Option<(String, Option<usize>, Option<usize>)> {
32 let text = unsafe { self.get_composition_string(GCS_COMPSTR) }?;
33 let attrs = unsafe { self.get_composition_data(GCS_COMPATTR) }.unwrap_or_default();
34
35 let mut first = None;
36 let mut last = None;
37 let mut boundary_before_char = 0;
38 let mut attr_idx = 0;
39
40 for chr in text.chars() {
41 let Some(attr) = attrs.get(attr_idx).copied() else {
42 break;
43 };
44
45 let char_is_targeted =
46 attr as u32 == ATTR_TARGET_CONVERTED || attr as u32 == ATTR_TARGET_NOTCONVERTED;
47
48 if first.is_none() && char_is_targeted {
49 first = Some(boundary_before_char);
50 } else if first.is_some() && last.is_none() && !char_is_targeted {
51 last = Some(boundary_before_char);
52 }
53
54 boundary_before_char += chr.len_utf8();
55 attr_idx += chr.len_utf16();
56 }
57
58 if first.is_some() && last.is_none() {
59 last = Some(text.len());
60 } else if first.is_none() {
61 let cursor = unsafe { self.get_composition_cursor(&text) };
64 first = cursor;
65 last = cursor;
66 }
67
68 Some((text, first, last))
69 }
70
71 pub unsafe fn get_composed_text(&self) -> Option<String> {
72 unsafe { self.get_composition_string(GCS_RESULTSTR) }
73 }
74
75 unsafe fn get_composition_cursor(&self, text: &str) -> Option<usize> {
76 let cursor = unsafe { ImmGetCompositionStringW(self.himc, GCS_CURSORPOS, null_mut(), 0) };
77 (cursor >= 0).then(|| text.chars().take(cursor as _).map(|c| c.len_utf8()).sum())
78 }
79
80 unsafe fn get_composition_string(&self, gcs_mode: u32) -> Option<String> {
81 let data = unsafe { self.get_composition_data(gcs_mode) }?;
82 let (prefix, shorts, suffix) = unsafe { data.align_to::<u16>() };
83 if prefix.is_empty() && suffix.is_empty() {
84 OsString::from_wide(shorts).into_string().ok()
85 } else {
86 None
87 }
88 }
89
90 unsafe fn get_composition_data(&self, gcs_mode: u32) -> Option<Vec<u8>> {
91 let size = match unsafe { ImmGetCompositionStringW(self.himc, gcs_mode, null_mut(), 0) } {
92 0 => return Some(Vec::new()),
93 size if size < 0 => return None,
94 size => size,
95 };
96
97 let mut buf = Vec::<u8>::with_capacity(size as _);
98 let size = unsafe {
99 ImmGetCompositionStringW(
100 self.himc,
101 gcs_mode,
102 buf.as_mut_ptr() as *mut c_void,
103 size as _,
104 )
105 };
106
107 if size < 0 {
108 None
109 } else {
110 unsafe { buf.set_len(size as _) };
111 Some(buf)
112 }
113 }
114
115 pub unsafe fn set_ime_cursor_area(&self, spot: Position, size: Size, scale_factor: f64) {
116 if !unsafe { ImeContext::system_has_ime() } {
117 return;
118 }
119
120 let (x, y) = spot.to_physical::<i32>(scale_factor).into();
121 let (width, height): (i32, i32) = size.to_physical::<i32>(scale_factor).into();
122 let rc_area = RECT { left: x, top: y, right: x + width, bottom: y + height };
123 let candidate_form = CANDIDATEFORM {
124 dwIndex: 0,
125 dwStyle: CFS_EXCLUDE,
126 ptCurrentPos: POINT { x, y },
127 rcArea: rc_area,
128 };
129 let composition_form = COMPOSITIONFORM {
130 dwStyle: CFS_POINT,
131 ptCurrentPos: POINT { x, y: y + height },
132 rcArea: rc_area,
133 };
134
135 unsafe {
136 ImmSetCompositionWindow(self.himc, &composition_form);
137 ImmSetCandidateWindow(self.himc, &candidate_form);
138 }
139 }
140
141 pub unsafe fn set_ime_allowed(hwnd: HWND, allowed: bool) {
142 if !unsafe { ImeContext::system_has_ime() } {
143 return;
144 }
145
146 if allowed {
147 unsafe { ImmAssociateContextEx(hwnd, 0, IACE_DEFAULT) };
148 } else {
149 unsafe { ImmAssociateContextEx(hwnd, 0, IACE_CHILDREN) };
150 }
151 }
152
153 unsafe fn system_has_ime() -> bool {
154 unsafe { GetSystemMetrics(SM_IMMENABLED) != 0 }
155 }
156}
157
158impl Drop for ImeContext {
159 fn drop(&mut self) {
160 unsafe { ImmReleaseContext(self.hwnd, self.himc) };
161 }
162}