use crate::compat::{lock, Mutex, String, ToString};
use crate::core::ObjectId;
use crate::platform::ime::{ImeBridge, ImeCandidatePosition, ImeComposition};
#[cfg(target_os = "windows")]
mod tsf {
use core::ffi::c_void;
pub const S_OK: i32 = 0;
pub const E_NOINTERFACE: i32 = 0x8000_4002u32 as i32;
pub const CLSCTX_INPROC_SERVER: u32 = 0x1;
pub const TF_POPF_ALL: u32 = 0x1;
pub const TF_CES_CURRENTINPUTTHREAD: i32 = 0x2;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct Guid {
pub data1: u32,
pub data2: u16,
pub data3: u16,
pub data4: [u8; 8],
}
macro_rules! guid {
($d1:expr, $d2:expr, $d3:expr, $b4:expr, $b5:expr, $b6:expr, $b7:expr, $b8:expr, $b9:expr, $b10:expr, $b11:expr) => {
Guid {
data1: $d1,
data2: $d2,
data3: $d3,
data4: [$b4, $b5, $b6, $b7, $b8, $b9, $b10, $b11],
}
};
}
pub const CLSID_TF_THREAD_MGR: Guid =
guid!(0x529A_9E6B, 0x6587, 0x4F23, 0xAB, 0x9E, 0x9C, 0x7D, 0x68, 0x3E, 0x3C, 0x50);
pub const IID_ITF_THREAD_MGR: Guid =
guid!(0xAA80_E801, 0x2021, 0x11D2, 0x93, 0xE0, 0x00, 0x60, 0xB0, 0x67, 0xB8, 0x6E);
pub const IID_ITF_THREAD_MGR_EVENT_SINK: Guid =
guid!(0xAA80_E80E, 0x2021, 0x11D2, 0x93, 0xE0, 0x00, 0x60, 0xB0, 0x67, 0xB8, 0x6E);
pub const IID_ITF_TEXT_EDIT_SINK: Guid =
guid!(0x8127_D901, 0xF856, 0x4E52, 0x81, 0x4F, 0x6D, 0x6C, 0xC3, 0xB1, 0xC6, 0xA1);
#[repr(C)]
pub struct IUnknown {
pub lp_vtbl: *const c_void,
}
#[repr(C)]
pub struct IUnknownVtbl {
pub query_interface:
unsafe extern "system" fn(*mut IUnknown, *const Guid, *mut *mut c_void) -> i32,
pub add_ref: unsafe extern "system" fn(*mut IUnknown) -> u32,
pub release: unsafe extern "system" fn(*mut IUnknown) -> u32,
}
#[repr(C)]
pub struct ITfThreadMgr {
pub lp_vtbl: *const ITfThreadMgrVtbl,
}
#[repr(C)]
pub struct ITfThreadMgrVtbl {
pub base: IUnknownVtbl,
pub activate: unsafe extern "system" fn(*mut ITfThreadMgr, *mut u32) -> i32,
pub deactivate: unsafe extern "system" fn(*mut ITfThreadMgr) -> i32,
pub create_document_mgr:
unsafe extern "system" fn(*mut ITfThreadMgr, *mut *mut ITfDocumentMgr) -> i32,
pub enum_document_mgrs: usize,
pub get_focus:
unsafe extern "system" fn(*mut ITfThreadMgr, *mut *mut ITfDocumentMgr) -> i32,
pub set_focus: unsafe extern "system" fn(*mut ITfThreadMgr, *mut ITfDocumentMgr) -> i32,
pub associate_focus: usize,
pub is_thread_focus: usize,
pub get_function_provider: usize,
pub enum_function_providers: usize,
pub get_global_compartment:
unsafe extern "system" fn(*mut ITfThreadMgr, *mut *mut c_void) -> i32,
pub create_context: unsafe extern "system" fn(
*mut ITfThreadMgr,
u32,
u32,
*mut IUnknown,
*mut *mut ITfContext,
) -> i32,
}
#[repr(C)]
pub struct ITfDocumentMgr {
pub lp_vtbl: *const ITfDocumentMgrVtbl,
}
#[repr(C)]
pub struct ITfDocumentMgrVtbl {
pub base: IUnknownVtbl,
pub create_context: unsafe extern "system" fn(
*mut ITfDocumentMgr,
u32,
u32,
*mut IUnknown,
*mut *mut ITfContext,
) -> i32,
pub push: unsafe extern "system" fn(*mut ITfDocumentMgr, *mut ITfContext) -> i32,
pub pop: unsafe extern "system" fn(*mut ITfDocumentMgr, u32) -> i32,
pub get_top: unsafe extern "system" fn(*mut ITfDocumentMgr, *mut *mut ITfContext) -> i32,
pub get_base: unsafe extern "system" fn(*mut ITfDocumentMgr, *mut *mut ITfContext) -> i32,
pub enum_contexts: usize,
}
#[repr(C)]
pub struct ITfContext {
pub lp_vtbl: *const IUnknownVtbl,
}
#[repr(C)]
pub struct ITfThreadMgrEventSink {
pub lp_vtbl: *const ITfThreadMgrEventSinkVtbl,
}
#[repr(C)]
pub struct ITfThreadMgrEventSinkVtbl {
pub base: IUnknownVtbl,
pub init_document_mgr:
unsafe extern "system" fn(*mut ITfThreadMgrEventSink, *mut ITfDocumentMgr) -> i32,
pub uninit_document_mgr:
unsafe extern "system" fn(*mut ITfThreadMgrEventSink, *mut ITfDocumentMgr) -> i32,
pub set_focus: unsafe extern "system" fn(
*mut ITfThreadMgrEventSink,
*mut ITfDocumentMgr,
*mut ITfDocumentMgr,
) -> i32,
pub push_context:
unsafe extern "system" fn(*mut ITfThreadMgrEventSink, *mut ITfContext) -> i32,
pub pop_context:
unsafe extern "system" fn(*mut ITfThreadMgrEventSink, *mut ITfContext) -> i32,
}
#[repr(C)]
pub struct ITfTextEditSink {
pub lp_vtbl: *const ITfTextEditSinkVtbl,
}
#[repr(C)]
pub struct ITfTextEditSinkVtbl {
pub base: IUnknownVtbl,
pub on_end_edit: unsafe extern "system" fn(
*mut ITfTextEditSink,
*mut ITfContext,
u32,
*mut c_void,
) -> i32,
}
use super::WindowsImeBridge;
pub static THREAD_MGR_EVENT_SINK_VTBL: ITfThreadMgrEventSinkVtbl = ITfThreadMgrEventSinkVtbl {
base: IUnknownVtbl {
query_interface: comlink_query_interface,
add_ref: comlink_add_ref,
release: comlink_release,
},
init_document_mgr: sink_init_document_mgr,
uninit_document_mgr: sink_uninit_document_mgr,
set_focus: sink_set_focus,
push_context: sink_push_context,
pop_context: sink_pop_context,
};
pub static TEXT_EDIT_SINK_VTBL: ITfTextEditSinkVtbl = ITfTextEditSinkVtbl {
base: IUnknownVtbl {
query_interface: comlink_query_interface,
add_ref: comlink_add_ref,
release: comlink_release,
},
on_end_edit: sink_on_end_edit,
};
#[repr(C)]
pub struct ComLink {
pub lp_vtbl: *const c_void,
pub ref_count: core::sync::atomic::AtomicU32,
pub bridge: *const WindowsImeBridge,
}
unsafe impl Send for ComLink {}
unsafe impl Sync for ComLink {}
unsafe extern "system" fn comlink_query_interface(
this: *mut IUnknown,
riid: *const Guid,
ppv: *mut *mut c_void,
) -> i32 {
if ppv.is_null() {
return E_NOINTERFACE;
}
let comlink = this as *mut ComLink;
let iid = &*riid;
if guid_eq(iid, &IID_ITF_THREAD_MGR_EVENT_SINK) {
(*comlink).lp_vtbl = &THREAD_MGR_EVENT_SINK_VTBL as *const _ as *const c_void;
(*comlink).add_ref();
*ppv = comlink as *mut c_void;
S_OK
} else if guid_eq(iid, &IID_ITF_TEXT_EDIT_SINK) {
(*comlink).lp_vtbl = &TEXT_EDIT_SINK_VTBL as *const _ as *const c_void;
(*comlink).add_ref();
*ppv = comlink as *mut c_void;
S_OK
} else {
*ppv = core::ptr::null_mut();
E_NOINTERFACE
}
}
unsafe extern "system" fn comlink_add_ref(this: *mut IUnknown) -> u32 {
let comlink = this as *mut ComLink;
(*comlink).add_ref()
}
unsafe extern "system" fn comlink_release(this: *mut IUnknown) -> u32 {
let comlink = this as *mut ComLink;
let prev = (*comlink).ref_count.fetch_sub(1, core::sync::atomic::Ordering::AcqRel);
if prev == 1 {
drop(Box::from_raw(comlink));
0
} else {
prev - 1
}
}
impl ComLink {
fn add_ref(&self) -> u32 {
self.ref_count.fetch_add(1, core::sync::atomic::Ordering::AcqRel) + 1
}
}
fn guid_eq(a: &Guid, b: &Guid) -> bool {
a.data1 == b.data1 && a.data2 == b.data2 && a.data3 == b.data3 && a.data4 == b.data4
}
unsafe extern "system" fn sink_init_document_mgr(
_this: *mut ITfThreadMgrEventSink,
_pdim: *mut ITfDocumentMgr,
) -> i32 {
S_OK
}
unsafe extern "system" fn sink_uninit_document_mgr(
_this: *mut ITfThreadMgrEventSink,
_pdim: *mut ITfDocumentMgr,
) -> i32 {
S_OK
}
unsafe extern "system" fn sink_set_focus(
_this: *mut ITfThreadMgrEventSink,
_pdim_focus: *mut ITfDocumentMgr,
_pdim_prev_focus: *mut ITfDocumentMgr,
) -> i32 {
S_OK
}
unsafe extern "system" fn sink_push_context(
_this: *mut ITfThreadMgrEventSink,
_pic: *mut ITfContext,
) -> i32 {
S_OK
}
unsafe extern "system" fn sink_pop_context(
_this: *mut ITfThreadMgrEventSink,
_pic: *mut ITfContext,
) -> i32 {
S_OK
}
unsafe extern "system" fn sink_on_end_edit(
_this: *mut ITfTextEditSink,
_pic: *mut ITfContext,
_ec: u32,
_prec: *mut c_void,
) -> i32 {
S_OK
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TsfCreateError {
CoCreate(i32),
Activate(i32),
Context(i32),
}
pub struct TsfConnection {
pub thread_mgr: *mut c_void,
pub doc_mgr: *mut c_void,
pub context: *mut c_void,
pub sink: *mut ComLink,
pub text_edit_cookie: u32,
pub thread_mgr_cookie: u32,
}
pub fn create_thread_mgr(
bridge: *const WindowsImeBridge,
) -> Result<TsfConnection, TsfCreateError> {
unsafe extern "system" {
fn CoCreateInstance(
rclsid: *const Guid,
p_unk_outer: *mut c_void,
cls_context: u32,
riid: *const Guid,
ppv: *mut *mut c_void,
) -> i32;
}
unsafe {
let mut thread_mgr: *mut c_void = core::ptr::null_mut();
let hr = CoCreateInstance(
&CLSID_TF_THREAD_MGR,
core::ptr::null_mut(),
CLSCTX_INPROC_SERVER,
&IID_ITF_THREAD_MGR,
&mut thread_mgr,
);
if hr < 0 || thread_mgr.is_null() {
return Err(TsfCreateError::CoCreate(hr));
}
let mgr = thread_mgr as *mut ITfThreadMgr;
let mut client_id: u32 = 0;
let activate = (*(*mgr).lp_vtbl).activate;
let hr = activate(mgr, &mut client_id);
if hr < 0 {
release(mgr as *mut IUnknown);
return Err(TsfCreateError::Activate(hr));
}
let mut doc_mgr: *mut ITfDocumentMgr = core::ptr::null_mut();
let create_doc_mgr = (*(*mgr).lp_vtbl).create_document_mgr;
let hr = create_doc_mgr(mgr, &mut doc_mgr);
if hr < 0 || doc_mgr.is_null() {
release(mgr as *mut IUnknown);
return Err(TsfCreateError::Context(hr));
}
let mut context: *mut ITfContext = core::ptr::null_mut();
let create_ctx = (*(*doc_mgr).lp_vtbl).create_context;
let hr = create_ctx(
doc_mgr,
client_id,
TF_CES_CURRENTINPUTTHREAD as u32,
core::ptr::null_mut(),
&mut context,
);
if hr < 0 || context.is_null() {
release(doc_mgr as *mut IUnknown);
release(mgr as *mut IUnknown);
return Err(TsfCreateError::Context(hr));
}
let push = (*(*doc_mgr).lp_vtbl).push;
let hr = push(doc_mgr, context);
if hr < 0 {
release(context as *mut IUnknown);
release(doc_mgr as *mut IUnknown);
release(mgr as *mut IUnknown);
return Err(TsfCreateError::Context(hr));
}
let sink = Box::into_raw(Box::new(ComLink {
lp_vtbl: &TEXT_EDIT_SINK_VTBL as *const _ as *const c_void,
ref_count: core::sync::atomic::AtomicU32::new(1),
bridge,
}));
let mut text_edit_cookie: u32 = 0;
let hr = advise_sink(
context as *mut IUnknown,
sink as *mut IUnknown,
&IID_ITF_TEXT_EDIT_SINK,
&mut text_edit_cookie,
);
if hr < 0 {
drop(Box::from_raw(sink));
pop_all(doc_mgr);
release(context as *mut IUnknown);
release(doc_mgr as *mut IUnknown);
release(mgr as *mut IUnknown);
return Err(TsfCreateError::Context(hr));
}
let mut thread_mgr_cookie: u32 = 0;
if advise_sink(
mgr as *mut IUnknown,
sink as *mut IUnknown,
&IID_ITF_THREAD_MGR_EVENT_SINK,
&mut thread_mgr_cookie,
) < 0
{
thread_mgr_cookie = 0;
}
log::debug!(
"[Windows IME] TSF client {client_id} connected: text-edit cookie {} on the \
context, thread-mgr cookie {} on the thread manager",
text_edit_cookie,
thread_mgr_cookie,
);
Ok(TsfConnection {
thread_mgr: mgr as *mut c_void,
doc_mgr: doc_mgr as *mut c_void,
context: context as *mut c_void,
sink,
text_edit_cookie,
thread_mgr_cookie,
})
}
}
pub fn destroy_thread_mgr(conn: TsfConnection) {
unsafe {
let context = conn.context as *mut ITfContext;
let doc_mgr = conn.doc_mgr as *mut ITfDocumentMgr;
let mgr = conn.thread_mgr as *mut ITfThreadMgr;
if !context.is_null() && conn.text_edit_cookie != 0 {
unadvise_sink(context as *mut IUnknown, conn.text_edit_cookie);
}
if !mgr.is_null() && conn.thread_mgr_cookie != 0 {
unadvise_sink(mgr as *mut IUnknown, conn.thread_mgr_cookie);
}
if !conn.sink.is_null() {
comlink_release(conn.sink as *mut IUnknown);
}
if !doc_mgr.is_null() {
pop_all(doc_mgr);
release(doc_mgr as *mut IUnknown);
}
if !context.is_null() {
release(context as *mut IUnknown);
}
if !mgr.is_null() {
let deactivate = (*(*mgr).lp_vtbl).deactivate;
deactivate(mgr);
release(mgr as *mut IUnknown);
}
}
}
unsafe fn advise_sink(
source_object: *mut IUnknown,
sink: *mut IUnknown,
iid: &Guid,
cookie: *mut u32,
) -> i32 {
#[repr(C)]
struct ITfSourceVtbl {
base: IUnknownVtbl,
advise_sink: unsafe extern "system" fn(
*mut IUnknown,
*const Guid,
*mut IUnknown,
*mut u32,
) -> i32,
unadvise_sink: usize,
detach_sink: usize,
}
let mut source: *mut c_void = core::ptr::null_mut();
let hr = query_itf_source(source_object, &mut source);
if hr < 0 || source.is_null() {
return hr;
}
let src = source as *mut IUnknown;
let advise = (*(*src).lp_vtbl.cast::<ITfSourceVtbl>()).advise_sink;
let hr = advise(src, iid, sink, cookie);
release(src);
hr
}
unsafe fn unadvise_sink(source_object: *mut IUnknown, cookie: u32) {
#[repr(C)]
struct ITfSourceVtbl {
base: IUnknownVtbl,
advise_sink: usize,
unadvise_sink: unsafe extern "system" fn(*mut IUnknown, u32) -> i32,
detach_sink: usize,
}
let mut source: *mut c_void = core::ptr::null_mut();
if query_itf_source(source_object, &mut source) < 0 || source.is_null() {
return;
}
let src = source as *mut IUnknown;
let unadvise = (*(*src).lp_vtbl.cast::<ITfSourceVtbl>()).unadvise_sink;
unadvise(src, cookie);
release(src);
}
unsafe fn query_itf_source(source_object: *mut IUnknown, out: *mut *mut c_void) -> i32 {
const IID_ITF_SOURCE: Guid =
guid!(0x4EA4_8A35, 0x60AE, 0x446F, 0x8F, 0xD6, 0xE6, 0xA8, 0xD8, 0x24, 0x59, 0xF7);
let qi = (*(*source_object).lp_vtbl.cast::<IUnknownVtbl>()).query_interface;
qi(source_object, &IID_ITF_SOURCE, out)
}
unsafe fn pop_all(doc_mgr: *mut ITfDocumentMgr) {
let pop = (*(*doc_mgr).lp_vtbl).pop;
pop(doc_mgr, TF_POPF_ALL);
}
unsafe fn release(iface: *mut IUnknown) {
if iface.is_null() {
return;
}
let release_fn = (*(*iface).lp_vtbl.cast::<IUnknownVtbl>()).release;
release_fn(iface);
}
}
#[cfg(target_os = "windows")]
fn probe_native_tsf(bridge: *const WindowsImeBridge) -> bool {
match tsf::create_thread_mgr(bridge) {
Ok(conn) => {
tsf::destroy_thread_mgr(conn);
true
}
Err(result) => {
log::debug!("[Windows IME] TSF creation failed: {result:?}");
false
}
}
}
#[cfg(not(target_os = "windows"))]
fn probe_native_tsf(_bridge: *const WindowsImeBridge) -> bool {
false
}
pub struct WindowsImeBridge {
focused_widget: Mutex<Option<ObjectId>>,
marked_text: Mutex<String>,
composition_start: Mutex<usize>,
cursor_pos: Mutex<usize>,
cursor_rect: Mutex<(i32, i32, u32, u32)>,
candidate_position: Mutex<ImeCandidatePosition>,
native_ime_available: Mutex<bool>,
}
crate::impl_default_via_new!(WindowsImeBridge);
impl WindowsImeBridge {
pub fn new() -> Self {
let bridge = Self {
focused_widget: Mutex::new(None),
marked_text: Mutex::new(String::new()),
composition_start: Mutex::new(0),
cursor_pos: Mutex::new(0),
cursor_rect: Mutex::new((0, 0, 0, 0)),
candidate_position: Mutex::new(ImeCandidatePosition { x: 0, y: 0 }),
native_ime_available: Mutex::new(false),
};
let native = bridge.probe();
*lock(&bridge.native_ime_available) = native;
bridge
}
fn probe(&self) -> bool {
let active = probe_native_tsf(self as *const Self);
if active {
log::info!("[Windows IME] TSF thread manager created and activated");
} else {
log::debug!(
"[Windows IME] TSF unavailable; tracking focus and composition in memory only"
);
}
active
}
pub fn refresh_native_availability(&self) -> bool {
let native = self.probe();
*lock(&self.native_ime_available) = native;
native
}
pub fn has_native_ime(&self) -> bool {
*lock(&self.native_ime_available)
}
pub fn set_cursor_rect(&self, x: i32, y: i32, w: u32, h: u32) {
*lock(&self.cursor_rect) = (x, y, w, h);
}
pub fn process_key_event(
&self,
key_code: u32,
modifiers: u32,
pressed: bool,
) -> Option<String> {
log::debug!(
"[Windows IME] process_key_event: key={}, mods={:#x}, pressed={}",
key_code,
modifiers,
pressed,
);
if self.has_marked_text() {
return None;
}
if !pressed {
return None;
}
if (0x20..=0x7e).contains(&key_code) {
let ch = char::from_u32(key_code)?;
let final_char = if modifiers & 0x02 != 0 { ch.to_ascii_uppercase() } else { ch };
return Some(final_char.to_string());
}
if key_code == 0x0d || key_code == 0x03 {
return Some("\n".to_string());
}
if key_code == 0x09 {
return Some("\t".to_string());
}
None
}
pub fn set_marked_text(&self, text: &str, sel_start: i32, sel_end: i32) {
log::debug!("[Windows IME] set_marked_text: '{}'", text);
let len = text.len();
let cursor = if sel_start >= 0 && sel_end >= 0 {
let end = sel_end as usize;
end.min(len)
} else {
len
};
*lock(&self.marked_text) = text.to_string();
*lock(&self.composition_start) = 0;
*lock(&self.cursor_pos) = cursor;
}
pub fn get_marked_text(&self) -> Option<String> {
let text = lock(&self.marked_text);
if text.is_empty() {
None
} else {
Some(text.clone())
}
}
pub fn has_marked_text(&self) -> bool {
!lock(&self.marked_text).is_empty()
}
pub fn discard_marked_text(&self) {
log::debug!("[Windows IME] discard_marked_text");
*lock(&self.marked_text) = String::new();
*lock(&self.composition_start) = 0;
*lock(&self.cursor_pos) = 0;
}
fn clear_composition(&self) {
*lock(&self.marked_text) = String::new();
*lock(&self.composition_start) = 0;
*lock(&self.cursor_pos) = 0;
}
}
impl ImeBridge for WindowsImeBridge {
fn focus_in(&self, widget_id: ObjectId) {
*lock(&self.focused_widget) = Some(widget_id);
log::info!("[Windows IME] focus_in: widget={}", widget_id);
}
fn focus_out(&self, widget_id: ObjectId) {
*lock(&self.focused_widget) = None;
self.clear_composition();
log::info!("[Windows IME] focus_out: widget={}", widget_id);
}
fn commit_text(&self, text: &str) {
log::info!("[Windows IME] commit_text: '{}'", text);
self.clear_composition();
#[cfg(not(alloc_frugal))]
{
let focused = *lock(&self.focused_widget);
if let Some(widget_id) = focused {
if !crate::platform::ime::deliver_commit(widget_id, text) {
log::debug!(
"[Windows IME] commit_text: widget {widget_id} is no longer mounted; the \
commit was not delivered"
);
}
} else {
log::debug!("[Windows IME] commit_text with no focused widget; nothing to deliver");
}
}
}
fn set_composition(&self, composition: &ImeComposition) {
log::debug!("[Windows IME] set_composition: '{}'", composition.text);
let text = &composition.text;
let len = text.len();
*lock(&self.marked_text) = text.to_string();
*lock(&self.composition_start) = 0;
let cursor = composition.cursor_position.min(len);
*lock(&self.cursor_pos) = cursor;
}
fn set_candidate_window_position(&self, position: ImeCandidatePosition) {
log::debug!(
"[Windows IME] set_candidate_window_position: ({}, {})",
position.x,
position.y,
);
*lock(&self.candidate_position) = position;
}
fn is_active(&self) -> bool {
self.has_native_ime() && lock(&self.focused_widget).is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::platform::ime::ImeComposition;
#[test]
fn test_focus_in_out() {
let bridge = WindowsImeBridge::new();
assert!(!bridge.is_active());
assert!(lock(&bridge.focused_widget).is_none());
bridge.focus_in(42);
assert_eq!(*lock(&bridge.focused_widget), Some(42));
assert_eq!(
bridge.is_active(),
bridge.has_native_ime(),
"activity must mirror whether a real TSF thread manager exists"
);
bridge.focus_out(42);
assert!(!bridge.is_active());
assert!(lock(&bridge.focused_widget).is_none());
}
#[test]
fn test_commit_text_clears_composition() {
let bridge = WindowsImeBridge::new();
bridge.set_marked_text("hello", 5, 5);
assert!(bridge.has_marked_text());
bridge.commit_text("hello");
assert!(!bridge.has_marked_text());
assert_eq!(bridge.get_marked_text(), None);
}
#[test]
fn test_commit_text_via_trait() {
let bridge = WindowsImeBridge::new();
bridge.set_marked_text("你好", 6, 6);
assert!(bridge.has_marked_text());
ImeBridge::commit_text(&bridge, "你好");
assert!(!bridge.has_marked_text());
}
#[test]
fn test_set_composition_trait() {
let bridge = WindowsImeBridge::new();
let comp = ImeComposition {
text: "composing".to_string(),
cursor_position: 5,
selection_length: 0,
};
bridge.set_composition(&comp);
assert!(bridge.has_marked_text());
assert_eq!(bridge.get_marked_text(), Some("composing".to_string()));
assert_eq!(*lock(&bridge.cursor_pos), 5);
}
#[test]
fn test_set_composition_empty_clears() {
let bridge = WindowsImeBridge::new();
bridge.set_marked_text("something", 5, 0);
let empty = ImeComposition::default();
bridge.set_composition(&empty);
assert!(!bridge.has_marked_text());
}
#[test]
fn test_discard_marked_text() {
let bridge = WindowsImeBridge::new();
bridge.set_marked_text("你好世界", 4, 8);
assert!(bridge.has_marked_text());
assert_eq!(*lock(&bridge.cursor_pos), 8);
bridge.discard_marked_text();
assert!(!bridge.has_marked_text());
assert_eq!(bridge.get_marked_text(), None);
assert_eq!(*lock(&bridge.cursor_pos), 0);
}
#[test]
fn test_set_marked_text_negative_sel_defaults_cursor_at_end() {
let bridge = WindowsImeBridge::new();
bridge.set_marked_text("test", -1, -1);
assert_eq!(*lock(&bridge.cursor_pos), 4);
}
#[test]
fn test_is_active_after_events() {
let bridge = WindowsImeBridge::new();
assert!(!bridge.is_active());
bridge.focus_in(1);
assert_eq!(bridge.is_active(), bridge.has_native_ime());
bridge.focus_out(1);
assert!(!bridge.is_active());
}
#[test]
fn test_process_key_event_no_composition() {
let bridge = WindowsImeBridge::new();
let result = bridge.process_key_event(0x61, 0x02, true);
assert_eq!(result, Some("A".to_string()));
let result = bridge.process_key_event(0x61, 0x00, true);
assert_eq!(result, Some("a".to_string()));
let result = bridge.process_key_event(0x0d, 0x00, true);
assert_eq!(result, Some("\n".to_string()));
let result = bridge.process_key_event(0x1b, 0x00, true);
assert_eq!(result, None);
}
#[test]
fn test_process_key_event_during_composition() {
let bridge = WindowsImeBridge::new();
bridge.set_marked_text(" composing", 10, 0);
let result = bridge.process_key_event(0x61, 0x00, true);
assert_eq!(result, None);
}
#[test]
fn test_process_key_event_released() {
let bridge = WindowsImeBridge::new();
let result = bridge.process_key_event(0x61, 0x00, false);
assert_eq!(result, None);
}
#[test]
fn test_set_candidate_window_position() {
let bridge = WindowsImeBridge::new();
bridge.set_candidate_window_position(ImeCandidatePosition { x: 50, y: 75 });
assert_eq!(*lock(&bridge.candidate_position), ImeCandidatePosition { x: 50, y: 75 });
}
#[test]
fn test_focus_out_discards_composition() {
let bridge = WindowsImeBridge::new();
bridge.set_marked_text("pending", 7, 0);
assert!(bridge.has_marked_text());
bridge.focus_out(1);
assert!(!bridge.has_marked_text());
assert!(!bridge.is_active());
}
#[test]
fn test_set_cursor_rect() {
let bridge = WindowsImeBridge::new();
bridge.set_cursor_rect(0, 0, 200, 20);
assert_eq!(*lock(&bridge.cursor_rect), (0, 0, 200, 20));
}
#[test]
fn test_native_availability_is_a_real_os_probe_and_agrees_with_is_active() {
let bridge = WindowsImeBridge::new();
assert_eq!(
bridge.has_native_ime(),
bridge.probe(),
"the recorded flag must be what a real TSF creation returns, not a symbol lookup"
);
assert_eq!(bridge.refresh_native_availability(), bridge.has_native_ime());
assert!(!bridge.is_active(), "no widget holds focus, so nothing is active");
assert!(!bridge.has_marked_text());
}
#[cfg(not(target_os = "windows"))]
#[test]
fn non_windows_reports_tsf_unavailable() {
let bridge = WindowsImeBridge::new();
assert!(!bridge.has_native_ime(), "no TSF exists off Windows");
assert!(!bridge.is_active(), "no TSF connection, so nothing is active");
bridge.focus_in(7);
assert!(
!bridge.is_active(),
"focus cannot manufacture a TSF connection that was never created"
);
bridge.set_marked_text("にほん", 9, 0);
assert!(bridge.has_marked_text());
assert_eq!(bridge.get_marked_text().as_deref(), Some("にほん"));
}
#[cfg(not(alloc_frugal))]
#[test]
fn commit_text_releases_the_focus_lock_before_delivery() {
use crate::event::{Event, EventHandler};
use std::rc::Rc;
let bridge = Rc::new(WindowsImeBridge::new());
let reentered = Rc::new(core::cell::Cell::new(false));
let id_cell = Rc::new(core::cell::Cell::new(0u64));
struct Reentrant {
bridge: Rc<WindowsImeBridge>,
id: Rc<core::cell::Cell<u64>>,
reentered: Rc<core::cell::Cell<bool>>,
base: crate::widget::BaseWidget,
}
impl crate::widget::Widget for Reentrant {
fn base(&self) -> &crate::widget::BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut crate::widget::BaseWidget {
&mut self.base
}
}
impl EventHandler for Reentrant {
fn handle_event(&mut self, event: &Event) {
if matches!(event, Event::ImeCommit { .. }) {
assert!(
crate::compat::try_lock(&self.bridge.focused_widget).is_some(),
"commit delivery must not hold the focused_widget lock"
);
self.bridge.focus_out(self.id.get());
self.reentered.set(true);
}
}
}
let id = crate::widget::runtime::register(Box::new(Reentrant {
bridge: Rc::clone(&bridge),
id: Rc::clone(&id_cell),
reentered: Rc::clone(&reentered),
base: crate::widget::BaseWidget::new(
crate::widget::WidgetKind::LineEdit,
crate::core::Rect::new(0, 0, 10, 10),
"reentrant",
),
}))
.expect("mount the reentrant widget");
id_cell.set(id);
bridge.focus_in(id);
bridge.commit_text("hi");
assert!(reentered.get(), "the commit handler must have run");
assert!(
lock(&bridge.focused_widget).is_none(),
"focus_out must have re-entered and cleared"
);
crate::widget::runtime::unregister(id);
}
}
#[cfg(all(test, target_os = "windows"))]
mod tsf_layout_tests {
use super::tsf::*;
use core::mem::{align_of, offset_of, size_of};
#[test]
fn vtable_slots_are_pointer_sized() {
assert_eq!(size_of::<usize>(), size_of::<*const core::ffi::c_void>());
assert_eq!(align_of::<usize>(), align_of::<*const core::ffi::c_void>());
}
#[test]
fn iunknown_vtbl_is_three_pointers() {
assert_eq!(size_of::<IUnknownVtbl>(), 3 * size_of::<usize>());
}
#[test]
fn thread_mgr_slots_match_the_abi() {
let p = size_of::<usize>();
assert_eq!(offset_of!(ITfThreadMgrVtbl, activate), 3 * p);
assert_eq!(offset_of!(ITfThreadMgrVtbl, deactivate), 4 * p);
assert_eq!(offset_of!(ITfThreadMgrVtbl, create_document_mgr), 5 * p);
assert_eq!(offset_of!(ITfThreadMgrVtbl, create_context), 14 * p);
assert_eq!(offset_of!(ITfThreadMgrVtbl, create_context), size_of::<ITfThreadMgrVtbl>() - p);
}
#[test]
fn document_mgr_slots_match_the_abi() {
let p = size_of::<usize>();
assert_eq!(offset_of!(ITfDocumentMgrVtbl, create_context), 3 * p);
assert_eq!(offset_of!(ITfDocumentMgrVtbl, push), 4 * p);
assert_eq!(offset_of!(ITfDocumentMgrVtbl, pop), 5 * p);
}
#[test]
fn sink_vtables_begin_with_iunknown() {
assert_eq!(offset_of!(ITfThreadMgrEventSinkVtbl, base), 0);
assert_eq!(offset_of!(ITfTextEditSinkVtbl, base), 0);
assert_eq!(offset_of!(ITfTextEditSinkVtbl, on_end_edit), 3 * size_of::<usize>());
}
#[test]
fn clsid_and_iids_match_msctf_h() {
assert_eq!(CLSID_TF_THREAD_MGR.data1, 0x529A_9E6B);
assert_eq!(CLSID_TF_THREAD_MGR.data2, 0x6587);
assert_eq!(CLSID_TF_THREAD_MGR.data3, 0x4F23);
assert_eq!(CLSID_TF_THREAD_MGR.data4, [0xAB, 0x9E, 0x9C, 0x7D, 0x68, 0x3E, 0x3C, 0x50]);
assert_eq!(IID_ITF_THREAD_MGR.data1, 0xAA80_E801);
assert_eq!(IID_ITF_THREAD_MGR.data2, 0x2021);
assert_eq!(IID_ITF_TEXT_EDIT_SINK.data1, 0x8127_D901);
assert_eq!(IID_ITF_THREAD_MGR_EVENT_SINK.data1, 0xAA80_E80E);
}
#[test]
fn comlink_starts_with_the_vtable_pointer() {
assert_eq!(offset_of!(ComLink, lp_vtbl), 0);
}
}