use std::rc::Rc;
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
));
}
}};
}
macro_rules! ensure {
($cond:expr, $($arg:tt)*) => {
match $cond {
true => {}
false => return Err(format!($($arg)*)),
}
};
}
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_mouse_handlers_for_test;
use slate_framework::event::{Modifiers, MouseButton};
use slate_framework::executor::{Executor, RedrawRequester};
use slate_framework::focus::FocusableEntry;
use slate_framework::hit_test::HitRegion;
use slate_framework::ime::ImeState;
use slate_framework::text_system::TextSystem;
use slate_framework::types::{Bounds, ElementId, Point, Size};
use slate_framework::view::{IntoAny, View};
use slate_platform::{DefaultPlatform, 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-textarea-mouse-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 three_line_layout() -> slate_text::MultilineLayout {
let mut text_system = TextSystem::new().expect("create TextSystem");
let font = text_system
.load_font_from_bytes(slate_text::TEST_FONT, 14.0, 1.0)
.expect("load bundled font");
let doc = text_system
.shape_document(&font, "alpha\nbeta\ngamma")
.expect("shape document");
slate_text::wrap_document(&doc, 1000.0)
}
fn setup() -> (Rc<AppState>, WindowId, Rc<std::cell::RefCell<ImeState>>) {
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_rc = state.register_ime_state_for_test(win, elem);
let layout = Rc::new(three_line_layout());
{
let mut s = ime_rc.borrow_mut();
s.text = "alpha\nbeta\ngamma".to_string();
s.caret = 0;
s.last_layout = Some(layout.clone());
s.paint_origin_x = 0.0;
s.paint_origin_y = 0.0;
}
state.republish_ime_cache_for_test(win);
state.install_element_mouse_handlers_for_test(win, elem, build_mouse_handlers_for_test());
state.push_hit_region_for_test(
win,
HitRegion::new(
elem,
Bounds {
origin: Point::new(0.0, 0.0),
size: Size::new(1000.0, layout.total_height_lpx),
},
0,
),
);
(state, win, ime_rc)
}
fn click(state: &Rc<AppState>, win: WindowId, x: f32, y: f32) {
state.dispatch_mouse_down_for_test(win, (x, y), MouseButton::Left, Modifiers::default());
}
fn check_single_click_places_collapsed_caret() -> Result<(), String> {
let (state, win, ime_rc) = setup();
click(&state, win, 12.0, 4.0);
let s = ime_rc.borrow();
ensure_eq!(s.click_count, 1, "single click count");
ensure_eq!(
Some(s.caret),
s.selection_anchor,
"single click is collapsed"
);
ensure!(s.caret < 5, "caret lands inside 'alpha' (bytes 0..5)");
Ok(())
}
fn check_double_click_selects_the_word() -> Result<(), String> {
let (state, win, ime_rc) = setup();
click(&state, win, 12.0, 4.0);
click(&state, win, 12.0, 4.0);
let s = ime_rc.borrow();
ensure_eq!(s.click_count, 2, "second click is a double");
ensure_eq!(s.selection_anchor, Some(0), "word anchor at 'alpha' start");
ensure_eq!(s.caret, 5, "word caret at 'alpha' end");
Ok(())
}
fn check_triple_click_selects_the_visual_line() -> Result<(), String> {
let (state, win, ime_rc) = setup();
click(&state, win, 12.0, 4.0);
click(&state, win, 12.0, 4.0);
click(&state, win, 12.0, 4.0);
let s = ime_rc.borrow();
ensure_eq!(s.click_count, 3, "third click is a triple");
ensure_eq!(s.selection_anchor, Some(0), "line anchor at line0 start");
ensure_eq!(s.caret, 6, "line caret at line0 end (past the '\\n')");
Ok(())
}
fn check_fourth_click_wraps_back_to_caret() -> Result<(), String> {
let (state, win, ime_rc) = setup();
for _ in 0..4 {
click(&state, win, 12.0, 4.0);
}
let s = ime_rc.borrow();
ensure_eq!(s.click_count, 1, "fourth click wraps to single");
ensure_eq!(
Some(s.caret),
s.selection_anchor,
"wrapped click is collapsed"
);
Ok(())
}
fn check_multi_click_run_survives_capture_release() -> Result<(), String> {
let (state, win, ime_rc) = setup();
click(&state, win, 12.0, 4.0);
state.dispatch_mouse_up_for_test(win, (12.0, 4.0), MouseButton::Left, Modifiers::default());
click(&state, win, 12.0, 4.0);
let s = ime_rc.borrow();
ensure_eq!(
s.click_count,
2,
"down→up→down preserves the multi-click counter"
);
ensure_eq!(s.selection_anchor, Some(0), "word anchor at 'alpha' start");
ensure_eq!(s.caret, 5, "word caret at 'alpha' end");
Ok(())
}
fn check_real_capture_loss_still_resets_multi_click_run() -> Result<(), String> {
let (state, win, ime_rc) = setup();
click(&state, win, 12.0, 4.0);
state.dispatch_mouse_up_for_test(win, (12.0, 4.0), MouseButton::Left, Modifiers::default());
let _ = state.dispatch_capture_lost_for_test(win);
click(&state, win, 12.0, 4.0);
let s = ime_rc.borrow();
ensure_eq!(
s.click_count,
1,
"real CaptureLost resets the multi-click counter"
);
ensure_eq!(
Some(s.caret),
s.selection_anchor,
"single click is collapsed"
);
Ok(())
}
fn check_double_click_on_second_line_selects_that_word() -> Result<(), String> {
let (state, win, ime_rc) = setup();
let y = ime_rc
.borrow()
.last_layout
.as_ref()
.unwrap()
.line_height_lpx
* 1.5;
click(&state, win, 8.0, y);
click(&state, win, 8.0, y);
let s = ime_rc.borrow();
ensure_eq!(s.click_count, 2, "double click count on line1");
ensure_eq!(s.selection_anchor, Some(6), "word anchor at 'beta' start");
ensure_eq!(s.caret, 10, "word caret at 'beta' end");
Ok(())
}
fn main() {
let cases: &[Case] = &[
(
"single_click_places_collapsed_caret",
check_single_click_places_collapsed_caret,
),
(
"double_click_selects_the_word",
check_double_click_selects_the_word,
),
(
"triple_click_selects_the_visual_line",
check_triple_click_selects_the_visual_line,
),
(
"fourth_click_wraps_back_to_caret",
check_fourth_click_wraps_back_to_caret,
),
(
"multi_click_run_survives_capture_release",
check_multi_click_run_survives_capture_release,
),
(
"real_capture_loss_still_resets_multi_click_run",
check_real_capture_loss_still_resets_multi_click_run,
),
(
"double_click_on_second_line_selects_that_word",
check_double_click_on_second_line_selects_that_word,
),
];
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());
}