use std::{ ffi::{c_void, OsString}, os::windows::prelude::OsStringExt, ptr::null_mut, }; use windows_sys::Win32::{ Foundation::{POINT, RECT}, 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, Size}, platform::windows::HWND, }; pub struct ImeContext { hwnd: HWND, himc: HIMC, } impl ImeContext { pub unsafe fn current(hwnd: HWND) -> Self { let himc = unsafe { ImmGetContext(hwnd) }; ImeContext { hwnd, himc } } pub unsafe fn get_composing_text_and_cursor( &self, ) -> Option<(String, Option, Option)> { let text = unsafe { self.get_composition_string(GCS_COMPSTR) }?; let attrs = unsafe { 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 = unsafe { self.get_composition_cursor(&text) }; first = cursor; last = cursor; } Some((text, first, last)) } pub unsafe fn get_composed_text(&self) -> Option { unsafe { self.get_composition_string(GCS_RESULTSTR) } } unsafe fn get_composition_cursor(&self, text: &str) -> Option { let cursor = unsafe { 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 { let data = unsafe { self.get_composition_data(gcs_mode) }?; let (prefix, shorts, suffix) = unsafe { data.align_to::() }; 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> { let size = match unsafe { ImmGetCompositionStringW(self.himc, gcs_mode, null_mut(), 0) } { 0 => return Some(Vec::new()), size if size < 0 => return None, size => size, }; let mut buf = Vec::::with_capacity(size as _); let size = unsafe { ImmGetCompositionStringW( self.himc, gcs_mode, buf.as_mut_ptr() as *mut c_void, size as _, ) }; if size < 0 { None } else { unsafe { buf.set_len(size as _) }; Some(buf) } } pub unsafe fn set_ime_cursor_area(&self, spot: Position, size: Size, scale_factor: f64) { if !unsafe { ImeContext::system_has_ime() } { return; } let (x, y) = spot.to_physical::(scale_factor).into(); let (width, height): (i32, i32) = size.to_physical::(scale_factor).into(); let rc_area = RECT { left: x, top: y, right: x + width, bottom: y - height, }; let candidate_form = CANDIDATEFORM { dwIndex: 0, dwStyle: CFS_EXCLUDE, ptCurrentPos: POINT { x, y }, rcArea: rc_area, }; unsafe { ImmSetCandidateWindow(self.himc, &candidate_form) }; } pub unsafe fn set_ime_allowed(hwnd: HWND, allowed: bool) { if !unsafe { ImeContext::system_has_ime() } { return; } if allowed { unsafe { ImmAssociateContextEx(hwnd, 0, IACE_DEFAULT) }; } else { unsafe { ImmAssociateContextEx(hwnd, 0, IACE_CHILDREN) }; } } unsafe fn system_has_ime() -> bool { unsafe { GetSystemMetrics(SM_IMMENABLED) != 0 } } } impl Drop for ImeContext { fn drop(&mut self) { unsafe { ImmReleaseContext(self.hwnd, self.himc) }; } }