winit/platform_impl/windows/
ime.rs

1use 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            // IME haven't split words and select any clause yet, so trying to retrieve normal
62            // cursor.
63            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}