use kui_core::testing::{press, release};
use kui_core::{
ClipboardMarks, Core, InputEvent, Key, KeyCode, KeyMods, KeyPress, MenuAction, NodeSpec, Role,
Size, TextStyle, UiEvent, Value, Vec2,
};
const LH: f32 = 20.0;
const GUTTER: f32 = 30.0;
fn mono() -> TextStyle {
TextStyle::new(14.0).mono().line_height(LH)
}
fn frame(core: &mut Core, lines: &[&str]) -> Key {
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill().pad(10.0));
let sink = ui.with_keyed(
"editor",
NodeSpec::row()
.fill()
.key_sink()
.on_drag("sel")
.on_click(Value::map([("kind", Value::str("hit"))]))
.role(Role::MultilineTextInput),
|ui| {
ui.with(NodeSpec::column().width(GUTTER).role(Role::None), |ui| {
for (n, _) in lines.iter().enumerate() {
ui.text_in(NodeSpec::row().height(LH), &format!("{}", n + 1), mono());
}
});
ui.with(NodeSpec::column().grow_width(), |ui| {
for line in lines {
ui.with(NodeSpec::row().height(LH).role(Role::Line), |ui| {
let (a, b) = line.split_at(line.len() / 2);
ui.text(a, mono());
ui.text(b, mono());
});
}
});
},
);
ui.take_key_focus(sink);
ui.finish();
sink
}
fn field(ev: &UiEvent, name: &str) -> Option<Value> {
ev.payload.get(name).cloned()
}
fn field_of(ev: &UiEvent, name: &str) -> Option<Value> {
field(ev, name)
}
fn kind(ev: &UiEvent) -> Option<String> {
ev.payload.get_str("kind").map(str::to_string)
}
#[test]
fn a_press_in_a_sink_says_which_line_and_where() {
let mut core = Core::new();
let sink = frame(&mut core, &["hello world", "second line", "third"]);
let w = core.measure_text("M", &mono(), None).width;
let at = Vec2::new(10.0 + GUTTER + 6.2 * w, 10.0 + LH + 5.0);
let evs = press(&mut core, at);
let start = evs
.iter()
.find(|e| kind(e).as_deref() == Some("drag"))
.expect("the drag started");
assert_eq!(start.key, sink);
assert_eq!(field(start, "line"), Some(Value::Int(1)));
assert_eq!(field(start, "byte"), Some(Value::Int(6)));
assert_eq!(field(start, "clicks"), Some(Value::Int(1)));
let evs = core.handle_input(InputEvent::CursorMoved(Vec2::new(
10.0 + GUTTER + 2.8 * w,
10.0 + 2.0 * LH + 5.0,
)));
let mv = evs
.iter()
.find(|e| field(e, "phase") == Some(Value::str("move")))
.expect("a move");
assert_eq!(field(mv, "line"), Some(Value::Int(2)));
assert_eq!(field(mv, "byte"), Some(Value::Int(3)));
let evs = release(&mut core);
let end = evs
.iter()
.find(|e| field(e, "phase") == Some(Value::str("end")))
.expect("the drag ended");
assert_eq!(field(end, "line"), Some(Value::Int(2)));
assert_eq!(field(end, "clicks"), Some(Value::Int(1)));
}
#[test]
fn a_click_carries_the_press_count_and_the_gutter_is_the_line_beside_it() {
let mut core = Core::new();
frame(&mut core, &["hello world", "second line"]);
let at = Vec2::new(10.0 + 5.0, 10.0 + LH + 2.0);
core.handle_input(InputEvent::CursorMoved(at));
core.handle_input(InputEvent::mouse_down(2));
let evs = core.handle_input(InputEvent::mouse_up());
let click = evs
.iter()
.find(|e| kind(e).as_deref() == Some("hit"))
.expect("the click arrived");
assert_eq!(field(click, "line"), Some(Value::Int(1)));
assert_eq!(field(click, "byte"), Some(Value::Int(0)));
assert_eq!(field(click, "clicks"), Some(Value::Int(2)));
}
#[test]
fn below_the_last_line_is_the_last_line_and_a_sink_without_lines_adds_nothing() {
let mut core = Core::new();
frame(&mut core, &["only"]);
let evs = press(&mut core, Vec2::new(10.0 + GUTTER + 200.0, 250.0));
let start = evs
.iter()
.find(|e| kind(e).as_deref() == Some("drag"))
.expect("the drag started");
assert_eq!(field(start, "line"), Some(Value::Int(0)));
assert_eq!(field(start, "byte"), Some(Value::Int(4)));
release(&mut core);
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill());
let sink = ui.text_in_keyed(
"plain",
NodeSpec::column().fill().key_sink().on_drag("d"),
"no lines here",
mono(),
);
ui.take_key_focus(sink);
ui.finish();
let evs = press(&mut core, Vec2::new(50.0, 50.0));
let start = evs
.iter()
.find(|e| kind(e).as_deref() == Some("drag"))
.expect("the drag started");
assert_eq!(field(start, "line"), None);
assert_eq!(field(start, "byte"), None);
assert_eq!(field(start, "clicks"), None);
release(&mut core);
}
#[test]
fn a_click_without_a_press_gains_nothing() {
use kui_core::{AccessAction, AccessRequest};
let mut core = Core::new();
let sink = frame(&mut core, &["hello world", "second"]);
core.handle_input(InputEvent::CursorMoved(Vec2::new(
10.0 + GUTTER + 20.0,
10.0 + LH + 5.0,
)));
core.handle_input(InputEvent::mouse_down(2));
core.handle_input(InputEvent::mouse_up());
let evs = core.handle_input(InputEvent::Access(AccessRequest::new(
sink,
AccessAction::Click,
)));
let click = evs
.iter()
.find(|e| kind(e).as_deref() == Some("hit"))
.expect("the click arrived");
assert_eq!(field(click, "line"), None, "{:?}", click.payload);
assert_eq!(field(click, "byte"), None);
assert_eq!(field(click, "clicks"), None);
}
#[test]
fn a_key_on_the_sink_gains_nothing() {
use kui_core::{KeyCode, KeyMods, KeyPress};
let mut core = Core::new();
frame(&mut core, &["hello"]);
let evs = core.handle_input(InputEvent::KeyDown(KeyPress::new(
KeyCode::Char('j'),
KeyMods::default(),
)));
let key = evs
.iter()
.find(|e| kind(e).as_deref() == Some("key"))
.expect("the key arrived");
assert_eq!(field(key, "line"), None);
assert_eq!(field(key, "clicks"), None);
}
#[test]
fn an_editors_changed_under_the_sink_gains_nothing() {
let mut core = Core::new();
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill().pad(10.0));
let mut field = Key::ROOT;
ui.with_keyed("shell", NodeSpec::column().fill().key_sink(), |ui| {
ui.text_in(
NodeSpec::row().height(LH).role(Role::Line),
"a line",
mono(),
);
field = kui_core::widgets::text_input(ui, "minibuffer", "");
});
ui.finish();
core.handle_input(InputEvent::CursorMoved(Vec2::new(50.0, 10.0 + 5.0)));
core.handle_input(InputEvent::mouse_down(1));
core.handle_input(InputEvent::mouse_up());
core.set_focus(Some(field));
let evs = core.handle_input(InputEvent::Text("x".into()));
let changed = evs
.iter()
.find(|e| kind(e).as_deref() == Some("changed"))
.expect("the editor changed");
assert_eq!(changed.key, field);
assert_eq!(field_of(changed, "line"), None, "{:?}", changed.payload);
assert_eq!(field_of(changed, "byte"), None);
assert_eq!(field_of(changed, "clicks"), None);
}
#[test]
fn a_click_payload_named_like_a_core_event_still_gains_its_line() {
let mut core = Core::new();
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill().pad(10.0));
let sink = ui.with_keyed(
"editor",
NodeSpec::column()
.fill()
.key_sink()
.on_click(Value::map([("kind", Value::str("click"))])),
|ui| {
for line in ["one", "two"] {
ui.text_in(NodeSpec::row().height(LH).role(Role::Line), line, mono());
}
},
);
ui.take_key_focus(sink);
ui.finish();
core.handle_input(InputEvent::CursorMoved(Vec2::new(12.0, 10.0 + LH + 5.0)));
core.handle_input(InputEvent::mouse_down(1));
let evs = core.handle_input(InputEvent::mouse_up());
let click = evs
.iter()
.find(|e| kind(e).as_deref() == Some("click"))
.expect("the click arrived");
assert_eq!(
field_of(click, "line"),
Some(Value::Int(1)),
"{:?}",
click.payload
);
assert_eq!(field_of(click, "clicks"), Some(Value::Int(1)));
}
#[test]
fn a_click_payload_named_button_still_gains_its_count() {
let mut core = Core::new();
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill().pad(10.0));
let sink = ui.with_keyed(
"editor",
NodeSpec::column().fill().key_sink().on_click(Value::map([
("kind", Value::str("button")),
("id", Value::Int(3)),
])),
|ui| {
for line in ["one", "two"] {
ui.text_in(NodeSpec::row().height(LH).role(Role::Line), line, mono());
}
},
);
ui.take_key_focus(sink);
ui.finish();
core.handle_input(InputEvent::CursorMoved(Vec2::new(12.0, 10.0 + LH + 5.0)));
core.handle_input(InputEvent::mouse_down(1));
let evs = core.handle_input(InputEvent::mouse_up());
let click = evs
.iter()
.find(|e| kind(e).as_deref() == Some("button"))
.expect("the click arrived");
assert_eq!(
field_of(click, "line"),
Some(Value::Int(1)),
"{:?}",
click.payload
);
assert_eq!(field_of(click, "clicks"), Some(Value::Int(1)));
}
#[test]
fn the_sink_yanks_to_the_clipboard_and_a_paste_comes_back_as_text() {
let mut core = Core::new();
let sink = frame(&mut core, &["hello"]);
assert!(core.take_menu_actions().is_empty());
core.set_clipboard("yanked", None);
core.request_paste();
assert_eq!(
core.take_menu_actions(),
vec![
MenuAction::SetClipboard {
text: "yanked".into(),
html: None
},
MenuAction::Paste
]
);
assert!(core.take_menu_actions().is_empty());
let evs = core.handle_input(InputEvent::Commit("from the clipboard".into()));
let text = evs
.iter()
.find(|e| kind(e).as_deref() == Some("text"))
.expect("the paste arrived");
assert_eq!(text.key, sink);
assert_eq!(field(text, "text"), Some(Value::str("from the clipboard")));
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.set_clipboard("from the view", Some("<b>from the view</b>".into()));
ui.finish();
assert_eq!(
core.take_menu_actions(),
vec![MenuAction::SetClipboard {
text: "from the view".into(),
html: Some("<b>from the view</b>".into())
}]
);
}
#[test]
fn a_root_sink_hears_a_paste_with_nothing_focused() {
let mut core = Core::new();
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill().on_key("shell"));
ui.text("nothing focusable", mono());
ui.finish();
assert_eq!(core.focus(), None);
let evs = core.handle_input(InputEvent::Commit("pasted".into()));
let text = evs
.iter()
.find(|e| kind(e).as_deref() == Some("text"))
.expect("the root sink heard it");
assert_eq!(text.key, Key::ROOT);
assert_eq!(field(text, "tag"), Some(Value::str("shell")));
}
#[test]
fn a_second_paste_ask_is_dropped_until_the_first_is_answered() {
let mut core = Core::new();
frame(&mut core, &["hello"]);
assert!(!core.awaiting_paste());
core.request_paste();
core.request_paste();
core.request_paste();
assert!(core.awaiting_paste());
assert_eq!(core.take_menu_actions(), vec![MenuAction::Paste], "one ask");
core.request_paste();
assert!(core.take_menu_actions().is_empty());
assert!(core.awaiting_paste());
let evs = core.handle_input(InputEvent::Commit(String::new()));
assert_eq!(evs.len(), 1);
assert!(!core.awaiting_paste());
core.request_paste();
assert_eq!(
core.take_menu_actions(),
vec![MenuAction::Paste],
"the next ask goes through"
);
core.handle_input(InputEvent::Commit("pasted".into()));
assert!(!core.awaiting_paste());
}
#[test]
fn a_paste_carries_the_pasteboards_markers_to_the_sink() {
let mut core = Core::new();
let sink = frame(&mut core, &["hello"]);
core.request_paste();
assert_eq!(core.take_menu_actions(), vec![MenuAction::Paste]);
let evs = core.handle_input(InputEvent::Paste {
text: "hunter2".into(),
marks: ClipboardMarks::SECRET,
});
assert!(!core.awaiting_paste(), "a paste is the answer");
let text = evs
.iter()
.find(|e| kind(e).as_deref() == Some("text"))
.expect("the paste arrived");
assert_eq!(text.key, sink);
assert_eq!(field(text, "text"), Some(Value::str("hunter2")));
assert_eq!(field(text, "concealed"), Some(Value::Bool(true)));
assert_eq!(field(text, "transient"), Some(Value::Bool(true)));
let evs = core.handle_input(InputEvent::Paste {
text: "brief".into(),
marks: ClipboardMarks {
concealed: false,
transient: true,
},
});
let text = evs
.iter()
.find(|e| kind(e).as_deref() == Some("text"))
.unwrap();
assert_eq!(field(text, "concealed"), None, "absent, never false");
assert_eq!(field(text, "transient"), Some(Value::Bool(true)));
for ev in [
InputEvent::Paste {
text: "plain".into(),
marks: ClipboardMarks::default(),
},
InputEvent::Commit("plain".into()),
] {
let evs = core.handle_input(ev);
let text = evs
.iter()
.find(|e| kind(e).as_deref() == Some("text"))
.unwrap();
assert_eq!(field(text, "concealed"), None);
assert_eq!(field(text, "transient"), None);
}
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.set_clipboard_secret("s3cret");
ui.finish();
assert_eq!(
core.take_menu_actions(),
vec![MenuAction::SetClipboardSecret {
text: "s3cret".into()
}]
);
for bits in 0..4 {
assert_eq!(ClipboardMarks::from_bits(bits).bits(), bits);
}
assert_eq!(ClipboardMarks::SECRET.bits(), 3);
assert!(ClipboardMarks::default().is_empty());
}
#[test]
fn a_sink_hears_the_paste_chord_and_never_a_bare_text() {
let mut core = Core::new();
let sink = frame(&mut core, &["hello"]);
let cmd_v = KeyPress::new(KeyCode::Char('v'), KeyMods::NONE.with_super());
let evs = core.handle_input(InputEvent::KeyDown(cmd_v));
assert!(
evs.iter()
.any(|e| e.key == sink && kind(e).as_deref() == Some("key")),
"the sink hears the chord: {evs:?}"
);
let evs = core.handle_input(InputEvent::Text("hunter2".into()));
assert!(
!evs.iter()
.any(|e| e.key == sink && kind(e).as_deref() == Some("text")),
"a bare Text is not the sink's: {evs:?}"
);
}
#[test]
fn a_button_press_in_a_sink_reads_its_line_and_byte() {
use kui_core::{Buttons, MouseButton};
let mut core = Core::new();
let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
ui.configure_root(NodeSpec::column().fill().pad(10.0));
let sink = ui.with_keyed(
"editor",
NodeSpec::column()
.fill()
.key_sink()
.on_button("btn")
.buttons(Buttons::MIDDLE),
|ui| {
for line in ["one", "two"] {
ui.text_in(NodeSpec::row().height(LH).role(Role::Line), line, mono());
}
},
);
ui.take_key_focus(sink);
ui.finish();
let w = core.measure_text("M", &mono(), None).width;
core.handle_input(InputEvent::CursorMoved(Vec2::new(
10.0 + 2.2 * w,
10.0 + LH + 5.0,
)));
let evs = core.handle_input(InputEvent::MouseDown {
button: MouseButton::Middle,
clicks: 1,
});
let b = evs.iter().find_map(|e| e.button()).expect("the press");
assert_eq!((b.line, b.byte), (Some(1), Some(2)), "{evs:?}");
}