#![cfg(all(target_os = "windows", feature = "test-hooks"))]
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use slate_framework::app_state::window_state::WindowState;
use slate_framework::app_state::{AppSignal, AppState};
use slate_framework::element::AnyElement;
use slate_framework::elements::Div;
use slate_framework::event::{ImeCommitEvent, ImeHandlers, ImeLifecycleEvent, ImePreeditEvent};
use slate_framework::executor::{Executor, RedrawRequester};
use slate_framework::focus::FocusableEntry;
use slate_framework::ime::{ImeState, Preedit};
use slate_framework::types::ElementId;
use slate_framework::view::{IntoAny, View};
use slate_framework::{EventCtx, Key, KeyCode, Modifiers, NamedKey};
use slate_platform::{
DefaultPlatform, PhysicalRect, Platform, Window, WindowId, WindowOptions, wake_run_loop,
};
#[allow(dead_code)]
struct NoopView;
impl View for NoopView {
fn render(&mut self, _cx: &mut slate_framework::RenderCx) -> AnyElement {
Div::new().into_any()
}
}
fn make_state() -> (Rc<AppState>, WindowId) {
let platform = DefaultPlatform::new();
let window = platform.create_window(WindowOptions {
title: "slate-ime-test".into(),
size: (1, 1),
min_size: None,
resizable: false,
visible: false,
position: Some((-32000, -32000)),
});
let redraw_requester = RedrawRequester::new(wake_run_loop);
let executor = Executor::new(redraw_requester.clone());
let runtime = slate_reactive::Runtime::new();
let _ = platform;
let state = Rc::new(AppState::new(
executor,
redraw_requester.clone(),
runtime.clone(),
));
let window_id = window.id();
{
let win_state = WindowState::new(window, runtime);
state.windows.borrow_mut().insert(window_id, win_state);
}
state.register_redraw_requester_for_test(window_id, redraw_requester);
(state, window_id)
}
fn id(n: u64) -> ElementId {
ElementId::from_raw(n)
}
fn entry(n: u64) -> FocusableEntry {
FocusableEntry {
id: id(n),
tab_index: 0,
focus_ring: true,
}
}
#[test]
fn dispatch_ime_preedit_fires_app_handler() {
let (state, win) = make_state();
let fired = Rc::new(Cell::new(0u32));
let f = fired.clone();
state.install_ime_handlers_for_test(
vec![Box::new(move |_e: &ImePreeditEvent, _cx: &mut EventCtx| {
f.set(f.get() + 1)
})],
vec![],
vec![],
vec![],
);
let signal = state.dispatch_ime_preedit_for_test(win, "abc".into(), 3, None);
assert_eq!(fired.get(), 1);
assert!(matches!(signal, AppSignal::RequestRedraw { .. }));
}
#[test]
fn dispatch_ime_preedit_routes_to_focused_element_with_stop_propagation() {
let (state, win) = make_state();
state.register_focusable_for_test(win, entry(1));
state.set_focus_for_test(win, id(1));
let elem_fired = Arc::new(Mutex::new(0u32));
let app_fired = Rc::new(Cell::new(0u32));
let ef = elem_fired.clone();
let af = app_fired.clone();
state.install_element_ime_handlers_for_test(
win,
id(1),
ImeHandlers {
on_ime_preedit: Some(Arc::new(move |_e, cx| {
*ef.lock().unwrap() += 1;
cx.stop_propagation();
})),
..Default::default()
},
);
state.install_ime_handlers_for_test(
vec![Box::new(move |_e: &ImePreeditEvent, _cx: &mut EventCtx| {
af.set(af.get() + 1)
})],
vec![],
vec![],
vec![],
);
state.dispatch_ime_preedit_for_test(win, "hi".into(), 2, None);
assert_eq!(*elem_fired.lock().unwrap(), 1, "element handler must fire");
assert_eq!(
app_fired.get(),
0,
"app handler must not fire after stop_propagation"
);
}
#[test]
fn dispatch_empty_preedit_clears_active_preedit() {
let (state, win) = make_state();
let elem_id = id(3);
state.register_focusable_for_test(win, entry(3));
state.set_focus_for_test(win, elem_id);
let ime_rc = state.register_ime_state_for_test(win, elem_id);
state.install_element_ime_handlers_for_test(
win,
elem_id,
ImeHandlers {
on_ime_preedit: Some(Arc::new(move |e: &ImePreeditEvent, cx: &mut EventCtx| {
let Some(state_rc) = cx.ime_state(elem_id) else {
return;
};
let mut s = state_rc.borrow_mut();
if e.text.is_empty() {
s.preedit = None;
} else {
s.preedit = Some(Preedit {
text: e.text.clone(),
cursor_byte_offset: e.cursor_byte_offset,
selection: e.selection.clone(),
});
}
cx.stop_propagation();
})),
..Default::default()
},
);
state.dispatch_ime_preedit_for_test(win, "ぱ".into(), 3, None);
assert!(
ime_rc.borrow().preedit.is_some(),
"preedit must be set after first composition glyph"
);
state.dispatch_ime_preedit_for_test(win, String::new(), 0, None);
assert!(
ime_rc.borrow().preedit.is_none(),
"empty preedit must clear the stored composition — no glyph may stick"
);
}
#[test]
fn dispatch_ime_commit_with_empty_text_only_clears_preedit() {
let (state, win) = make_state();
let elem_id = id(2);
state.register_focusable_for_test(win, entry(2));
state.set_focus_for_test(win, elem_id);
state.set_ime_state_for_test(
win,
elem_id,
ImeState {
text: String::new(),
caret: 0,
preedit: Some(Preedit {
text: "abc".into(),
cursor_byte_offset: 3,
selection: None,
}),
caret_client_rect: None,
..Default::default()
},
);
let app_called = Rc::new(Cell::new(0u32));
let ac = app_called.clone();
state.install_ime_handlers_for_test(
vec![],
vec![Box::new(move |_e: &ImeCommitEvent, _cx: &mut EventCtx| {
ac.set(ac.get() + 1)
})],
vec![],
vec![],
);
state.dispatch_ime_commit_for_test(win, String::new());
assert_eq!(
app_called.get(),
1,
"app handler must fire for empty commit too"
);
}
#[test]
fn dispatch_ime_commit_with_text_fires_app_handler() {
let (state, win) = make_state();
let captured = Rc::new(RefCell::new(String::new()));
let c = captured.clone();
state.install_ime_handlers_for_test(
vec![],
vec![Box::new(move |e: &ImeCommitEvent, _cx: &mut EventCtx| {
*c.borrow_mut() = e.text.clone()
})],
vec![],
vec![],
);
let signal = state.dispatch_ime_commit_for_test(win, "你好".into());
assert_eq!(&*captured.borrow(), "你好");
assert!(matches!(signal, AppSignal::RequestRedraw { .. }));
}
#[test]
fn dispatch_ime_enabled_fires_app_handler() {
let (state, win) = make_state();
let fired = Rc::new(Cell::new(0u32));
let f = fired.clone();
state.install_ime_handlers_for_test(
vec![],
vec![],
vec![Box::new(
move |_e: &ImeLifecycleEvent, _cx: &mut EventCtx| f.set(f.get() + 1),
)],
vec![],
);
let signal = state.dispatch_ime_enabled_for_test(win);
assert_eq!(fired.get(), 1);
assert!(matches!(signal, AppSignal::RequestRedraw { .. }));
}
#[test]
fn dispatch_ime_disabled_fires_app_handler() {
let (state, win) = make_state();
let fired = Rc::new(Cell::new(0u32));
let f = fired.clone();
state.install_ime_handlers_for_test(
vec![],
vec![],
vec![],
vec![Box::new(
move |_e: &ImeLifecycleEvent, _cx: &mut EventCtx| f.set(f.get() + 1),
)],
);
let signal = state.dispatch_ime_disabled_for_test(win);
assert_eq!(fired.get(), 1);
assert!(matches!(signal, AppSignal::RequestRedraw { .. }));
}
#[test]
fn ime_caret_rect_returns_none_when_no_focused_element() {
let (state, win) = make_state();
let rect = state.ime_caret_rect_query_for_test(win);
assert!(rect.is_none());
}
#[test]
fn ime_caret_rect_returns_cached_value_when_focused() {
let (state, win) = make_state();
let elem_id = id(3);
state.register_focusable_for_test(win, entry(3));
state.set_focus_for_test(win, elem_id);
let expected = PhysicalRect {
x: 10,
y: 20,
width: 2,
height: 16,
};
state.set_ime_state_for_test(
win,
elem_id,
ImeState {
caret_client_rect: Some(expected),
..Default::default()
},
);
let rect = state.ime_caret_rect_query_for_test(win);
assert_eq!(rect, Some(expected));
}
#[test]
fn ime_text_returns_substring_when_in_window() {
let (state, win) = make_state();
let elem_id = id(4);
state.register_focusable_for_test(win, entry(4));
state.set_focus_for_test(win, elem_id);
state.set_ime_state_for_test(
win,
elem_id,
ImeState {
text: "hello world".into(),
caret: 5,
..Default::default()
},
);
let result = state.ime_text_query_for_test(win, 0..5);
assert_eq!(result, Some("hello".to_string()));
}
#[test]
fn tab_during_preedit_synthesises_commit() {
let (state, win) = make_state();
let elem_id = id(5);
state.register_focusable_for_test(win, entry(5));
state.set_focus_for_test(win, elem_id);
let ime_rc = state.register_ime_state_for_test(win, elem_id);
{
let mut s = ime_rc.borrow_mut();
s.preedit = Some(Preedit {
text: "abc".into(),
cursor_byte_offset: 3,
selection: None,
});
}
let user_commit_fired = Rc::new(Cell::new(0u32));
let ucf = user_commit_fired.clone();
state.install_ime_handlers_for_test(
vec![],
vec![Box::new(move |_e: &ImeCommitEvent, _cx: &mut EventCtx| {
ucf.set(ucf.get() + 1)
})],
vec![],
vec![],
);
state.dispatch_key_down_for_test(
win,
KeyCode::Tab,
Key::Named(NamedKey::Tab),
Modifiers::default(),
false,
);
let after = ime_rc.borrow();
assert!(after.preedit.is_none(), "preedit must be cleared after Tab");
assert_eq!(
after.text, "abc",
"preedit text must be committed into ImeState.text"
);
assert_eq!(
user_commit_fired.get(),
0,
"user on_ime_commit handlers must NOT fire for synthetic commits"
);
}