use crossterm::event::{
Event as CtEvent, KeyModifiers, MouseButton, MouseEvent as CtMouseEvent, MouseEventKind,
};
use rdom_core::{NodeId, Position, Selection};
use crate::TuiDom;
use crate::layout::{Display, Size};
use crate::render::{LayoutExt, Rect};
use crate::runtime::router::Router;
use crate::runtime::selection::multiclick::{expand_to_line, expand_to_word};
use crate::style::{CascadeExt, Stylesheet, TuiStyle};
fn prepare(dom: &mut TuiDom, sheet: &Stylesheet, viewport: Rect) {
dom.cascade(sheet);
dom.layout_dom(viewport);
}
fn paragraph(text: &str, width: u16) -> (TuiDom, NodeId, NodeId) {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node(text);
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(width)),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, width + 10, 10));
(dom, p, t)
}
fn down_at(x: u16, y: u16) -> CtMouseEvent {
CtMouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: x,
row: y,
modifiers: KeyModifiers::empty(),
}
}
#[test]
fn double_click_word_selects_the_enclosing_word() {
let (mut dom, _p, t) = paragraph("hello world", 40);
dom.set_selection(Some(Selection::caret(Position::new(t, 2))));
let changed = expand_to_word(&mut dom);
assert!(changed);
let sel = dom.selection().unwrap();
assert_eq!(sel.anchor, Position::new(t, 0));
assert_eq!(sel.focus, Position::new(t, 5));
}
#[test]
fn double_click_on_second_word_selects_it() {
let (mut dom, _p, t) = paragraph("hello world", 40);
dom.set_selection(Some(Selection::caret(Position::new(t, 8))));
expand_to_word(&mut dom);
let sel = dom.selection().unwrap();
assert_eq!(sel.anchor, Position::new(t, 6));
assert_eq!(sel.focus, Position::new(t, 11));
}
#[test]
fn double_click_on_whitespace_selects_the_whitespace_run() {
let (mut dom, _p, t) = paragraph("hello world", 40);
dom.set_selection(Some(Selection::caret(Position::new(t, 5))));
expand_to_word(&mut dom);
let sel = dom.selection().unwrap();
assert_eq!(sel.anchor, Position::new(t, 5));
assert_eq!(sel.focus, Position::new(t, 6));
}
#[test]
fn double_click_on_cjk_character_selects_that_character() {
let (mut dom, _p, t) = paragraph("中文", 40);
dom.set_selection(Some(Selection::caret(Position::new(t, 3))));
expand_to_word(&mut dom);
let sel = dom.selection().unwrap();
assert_eq!(sel.anchor, Position::new(t, 3));
assert_eq!(sel.focus, Position::new(t, 6));
}
#[test]
fn double_click_at_end_of_text_snaps_to_last_word() {
let (mut dom, _p, t) = paragraph("hello", 40);
dom.set_selection(Some(Selection::caret(Position::new(t, 5))));
expand_to_word(&mut dom);
let sel = dom.selection().unwrap();
assert_eq!(sel.anchor, Position::new(t, 0));
assert_eq!(sel.focus, Position::new(t, 5));
}
#[test]
fn expand_to_word_without_selection_returns_false() {
let (mut dom, _p, _t) = paragraph("hi", 40);
assert!(!expand_to_word(&mut dom));
}
#[test]
fn expand_to_word_is_idempotent() {
let (mut dom, _p, t) = paragraph("hello", 40);
dom.set_selection(Some(Selection::new(
Position::new(t, 0),
Position::new(t, 5),
)));
assert!(!expand_to_word(&mut dom));
}
#[test]
fn triple_click_line_selects_the_whole_ifc_line() {
let (mut dom, _p, t) = paragraph("hello world", 40);
dom.set_selection(Some(Selection::caret(Position::new(t, 3))));
let changed = expand_to_line(&mut dom);
assert!(changed);
let sel = dom.selection().unwrap();
assert_eq!(sel.anchor, Position::new(t, 0));
assert_eq!(sel.focus, Position::new(t, 11));
}
#[test]
fn triple_click_on_wrapped_second_line_selects_only_that_line() {
let (mut dom, _p, t) = paragraph("hello world", 6);
dom.set_selection(Some(Selection::caret(Position::new(t, 8))));
let changed = expand_to_line(&mut dom);
assert!(changed);
let sel = dom.selection().unwrap();
assert_eq!(sel.anchor, Position::new(t, 6));
assert_eq!(sel.focus, Position::new(t, 11));
}
#[test]
fn triple_click_first_line_of_wrapped_paragraph_selects_that_line() {
let (mut dom, _p, t) = paragraph("hello world", 6);
dom.set_selection(Some(Selection::caret(Position::new(t, 2))));
expand_to_line(&mut dom);
let sel = dom.selection().unwrap();
assert_eq!(sel.anchor, Position::new(t, 0));
assert_eq!(sel.focus, Position::new(t, 5));
}
#[test]
fn expand_to_line_without_selection_returns_false() {
let (mut dom, _p, _t) = paragraph("hi", 40);
assert!(!expand_to_line(&mut dom));
}
#[test]
fn two_fast_downs_promote_selection_to_word() {
let (mut dom, _p, t) = paragraph("hello world", 40);
let mut router = Router::new();
router.route(&mut dom, CtEvent::Mouse(down_at(2, 0)));
let first = dom.selection().unwrap();
assert!(first.is_collapsed());
router.route(&mut dom, CtEvent::Mouse(down_at(2, 0)));
let sel = dom.selection().unwrap();
assert_eq!(sel.anchor, Position::new(t, 0));
assert_eq!(sel.focus, Position::new(t, 5));
}
#[test]
fn three_fast_downs_promote_selection_to_line() {
let (mut dom, _p, t) = paragraph("hello world", 40);
let mut router = Router::new();
router.route(&mut dom, CtEvent::Mouse(down_at(2, 0)));
router.route(&mut dom, CtEvent::Mouse(down_at(2, 0)));
router.route(&mut dom, CtEvent::Mouse(down_at(2, 0)));
let sel = dom.selection().unwrap();
assert_eq!(sel.anchor, Position::new(t, 0));
assert_eq!(sel.focus, Position::new(t, 11));
}
#[test]
fn fourth_click_resets_to_caret_selection() {
let (mut dom, _p, _t) = paragraph("hello world", 40);
let mut router = Router::new();
router.route(&mut dom, CtEvent::Mouse(down_at(2, 0)));
router.route(&mut dom, CtEvent::Mouse(down_at(2, 0)));
router.route(&mut dom, CtEvent::Mouse(down_at(2, 0)));
router.route(&mut dom, CtEvent::Mouse(down_at(2, 0)));
assert!(dom.selection().unwrap().is_collapsed());
}
#[test]
fn clicks_on_different_positions_do_not_promote() {
let (mut dom, _p, _t) = paragraph("hello world", 40);
let mut router = Router::new();
router.route(&mut dom, CtEvent::Mouse(down_at(2, 0)));
router.route(&mut dom, CtEvent::Mouse(down_at(8, 0)));
assert!(dom.selection().unwrap().is_collapsed());
}
#[test]
fn reset_clears_multi_click_state() {
let (mut dom, _p, _t) = paragraph("hello world", 40);
let mut router = Router::new();
router.route(&mut dom, CtEvent::Mouse(down_at(2, 0)));
router.reset();
router.route(&mut dom, CtEvent::Mouse(down_at(2, 0)));
assert!(dom.selection().unwrap().is_collapsed());
}