use std::cell::RefCell;
use std::rc::Rc;
macro_rules! ensure {
($cond:expr, $($arg:tt)*) => {
if !$cond {
return Err(format!($($arg)*));
}
};
}
macro_rules! ensure_eq {
($left:expr, $right:expr, $($arg:tt)*) => {{
let left = $left;
let right = $right;
if left != right {
return Err(format!(
"{} (left: {:?}, right: {:?})",
format!($($arg)*),
left,
right
));
}
}};
}
type Case = (&'static str, fn() -> Result<(), String>);
use slate_framework::app_state::AppState;
use slate_framework::app_state::window_state::WindowState;
use slate_framework::element::AnyElement;
use slate_framework::elements::Div;
use slate_framework::elements::text_area::{
build_ime_handlers_for_test, build_key_down_handler_for_test, build_text_input_handler_for_test,
};
use slate_framework::event::KeyHandlers;
use slate_framework::executor::{Executor, RedrawRequester};
use slate_framework::focus::FocusableEntry;
use slate_framework::ime::ImeState;
use slate_framework::text_system::TextSystem;
use slate_framework::types::ElementId;
use slate_framework::view::{IntoAny, View};
use slate_framework::{Key, KeyCode, Modifiers, NamedKey};
use slate_platform::{DefaultPlatform, Platform, Window, WindowId, WindowOptions, wake_run_loop};
use slate_reactive::{Runtime, Signal};
#[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-textarea-editing-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)
}
struct Harness {
state: Rc<AppState>,
ime: Rc<RefCell<ImeState>>,
value: Signal<String>,
window: WindowId,
_rt: std::sync::Arc<Runtime>,
}
fn harness() -> Harness {
slate_platform::clipboard::install_clipboard_override_for_test();
let (state, win) = make_state();
let elem = ElementId::from_raw(20);
state.register_focusable_for_test(
win,
FocusableEntry {
id: elem,
tab_index: 0,
focus_ring: true,
},
);
state.set_focus_for_test(win, elem);
let ime = state.register_ime_state_for_test(win, elem);
state.republish_ime_cache_for_test(win);
let rt = Runtime::new();
let value = Signal::new(rt.clone(), String::new());
state.install_element_key_handlers_for_test(
win,
elem,
KeyHandlers {
on_key_down: Some(build_key_down_handler_for_test(value.clone())),
on_key_up: None,
on_text_input: Some(build_text_input_handler_for_test(value.clone())),
},
);
state.install_element_ime_handlers_for_test(
win,
elem,
build_ime_handlers_for_test(value.clone()),
);
Harness {
state,
ime,
value,
window: win,
_rt: rt,
}
}
impl Harness {
fn typ(&self, s: &str) {
self.state
.dispatch_text_input_for_test(self.window, s.to_string());
}
fn key(&self, code: KeyCode, key: Key, mods: Modifiers) {
self.state
.dispatch_key_down_for_test(self.window, code, key, mods, false);
}
fn enter(&self) {
self.key(
KeyCode::Enter,
Key::Named(NamedKey::Enter),
Modifiers::default(),
);
}
fn shift_left(&self) {
self.key(
KeyCode::ArrowLeft,
Key::Named(NamedKey::ArrowLeft),
Modifiers {
shift: true,
..Default::default()
},
);
}
fn arrow_right(&self) {
self.key(
KeyCode::ArrowRight,
Key::Named(NamedKey::ArrowRight),
Modifiers::default(),
);
}
fn command(&self, code: KeyCode, ch: &str) {
let mods = if cfg!(target_os = "macos") {
Modifiers {
meta: true,
..Default::default()
}
} else {
Modifiers {
ctrl: true,
..Default::default()
}
};
self.key(code, Key::Character(ch.into()), mods);
}
fn text(&self) -> String {
self.value.get_untracked()
}
fn caret(&self) -> usize {
self.ime.borrow().caret
}
}
fn reseed_layout(h: &Harness, text: &str) {
let mut ts = TextSystem::new().expect("create TextSystem");
let font = ts
.load_font_from_bytes(slate_text::TEST_FONT, 14.0, 1.0)
.expect("load font");
let doc = ts.shape_document(&font, text).expect("shape");
let layout = slate_text::wrap_document(&doc, 1000.0);
h.ime.borrow_mut().last_layout = Some(Rc::new(layout));
}
fn check_type_two_lines_select_replace_then_undo() -> Result<(), String> {
let h = harness();
h.typ("a");
h.typ("b");
h.enter();
h.typ("c");
h.typ("d");
ensure_eq!(
h.text(),
"ab\ncd",
"Enter inserts a newline between the runs"
);
ensure_eq!(h.caret(), 5, "caret after typing 'ab\\ncd'");
h.shift_left();
h.shift_left();
ensure_eq!(
h.ime.borrow().selection_anchor,
Some(5),
"selection anchor at byte 5"
);
ensure_eq!(h.caret(), 3, "selection spans bytes 3..5 ('cd')");
h.typ("X");
ensure_eq!(h.text(), "ab\nX", "typing replaces the selection");
ensure_eq!(h.caret(), 4, "caret after replacing selection with 'X'");
h.command(KeyCode::KeyZ, "z");
ensure_eq!(h.text(), "ab\ncd", "undo reverts the replace");
Ok(())
}
fn check_copy_multiline_selection_and_paste_preserves_newlines() -> Result<(), String> {
let h = harness();
h.typ("a");
h.typ("b");
h.enter();
h.typ("c");
h.typ("d");
ensure_eq!(h.text(), "ab\ncd", "seeded buffer");
for _ in 0..5 {
h.shift_left();
}
ensure_eq!(h.caret(), 0, "caret at document start after selecting all");
ensure_eq!(
h.ime.borrow().selection_anchor,
Some(5),
"anchor at document end"
);
h.command(KeyCode::KeyC, "c");
h.arrow_right(); ensure_eq!(h.caret(), 5, "ArrowRight collapses to byte 5");
ensure_eq!(
h.ime.borrow().selection_anchor,
None,
"selection cleared on collapse"
);
h.command(KeyCode::KeyV, "v");
ensure_eq!(
h.text(),
"ab\ncdab\ncd",
"multi-line paste keeps the '\\n' (multiline = true)"
);
Ok(())
}
fn check_ime_commit_inserts_at_caret_mid_document() -> Result<(), String> {
let h = harness();
{
let mut s = h.ime.borrow_mut();
s.text = "ad".to_string();
s.caret = 1;
s.seed_undo_baseline();
}
h.value.set("ad".to_string());
h.state
.dispatch_ime_preedit_for_test(h.window, "b".into(), 1, None);
{
let s = h.ime.borrow();
ensure_eq!(s.text.as_str(), "ad", "preedit leaves the buffer untouched");
ensure!(
s.preedit.is_some(),
"preedit is recorded during composition"
);
}
h.state.dispatch_ime_commit_for_test(h.window, "b".into());
ensure_eq!(h.text(), "abd", "commit inserts at the caret mid-document");
ensure_eq!(h.caret(), 2, "caret advances past the committed text");
ensure!(
h.ime.borrow().preedit.is_none(),
"commit clears the preedit"
);
Ok(())
}
fn check_enter_inserts_exactly_one_newline() -> Result<(), String> {
let h = harness();
h.enter();
h.typ("\n");
ensure_eq!(h.text(), "\n", "one Enter press yields exactly one newline");
ensure_eq!(h.caret(), 1, "caret after the single newline");
Ok(())
}
fn check_enter_inserts_exactly_one_newline_carriage_return() -> Result<(), String> {
let h = harness();
h.enter();
h.typ("\r");
ensure_eq!(h.text(), "\n", "bare \\r from text input is also filtered");
ensure_eq!(h.caret(), 1, "caret after the single newline");
Ok(())
}
fn check_end_key_after_typing_is_visual_line_relative() -> Result<(), String> {
let h = harness();
h.typ("a");
h.typ("b");
h.enter();
h.typ("c");
h.typ("d");
reseed_layout(&h, "ab\ncd");
h.ime.borrow_mut().caret = 0;
h.key(
KeyCode::End,
Key::Named(NamedKey::End),
Modifiers::default(),
);
ensure_eq!(
h.caret(),
2,
"End lands at line0 end ('ab'), not document end"
);
Ok(())
}
fn main() {
let cases: &[Case] = &[
(
"type_two_lines_select_replace_then_undo",
check_type_two_lines_select_replace_then_undo,
),
(
"copy_multiline_selection_and_paste_preserves_newlines",
check_copy_multiline_selection_and_paste_preserves_newlines,
),
(
"ime_commit_inserts_at_caret_mid_document",
check_ime_commit_inserts_at_caret_mid_document,
),
(
"enter_inserts_exactly_one_newline",
check_enter_inserts_exactly_one_newline,
),
(
"enter_inserts_exactly_one_newline_carriage_return",
check_enter_inserts_exactly_one_newline_carriage_return,
),
(
"end_key_after_typing_is_visual_line_relative",
check_end_key_after_typing_is_visual_line_relative,
),
];
let mut failed = 0;
for (name, f) in cases {
match f() {
Ok(()) => println!("ok - {name}"),
Err(e) => {
eprintln!("FAIL - {name}: {e}");
failed += 1;
}
}
}
if failed > 0 {
eprintln!("\n{failed} case(s) failed");
std::process::exit(1);
}
println!("\nall {} case(s) passed", cases.len());
}