mod common;
use std::cell::RefCell;
use std::rc::Rc;
use std::time::{Duration, Instant};
use common::Harness;
use guido::prelude::*;
use guido::reactive::clipboard::{
clear_system_clipboard, clipboard_paste, primary_copy, take_clipboard_change,
};
use guido::reactive::focus::{clear_focus, focus_path, request_focus};
use guido::renderer::{DrawCommand, RenderNode};
use guido::tree::WidgetId;
const WIDTH: f32 = 200.0;
const HEIGHT: f32 = 40.0;
const SENTENCE: &str = "the quick brown fox jumps over the lazy dog";
const SCROLL_PADDING_BOTH_ENDS: f32 = 4.0;
struct Field {
harness: Harness,
value: RwSignal<String>,
now: Instant,
}
impl Field {
fn focused(text: &str) -> Self {
let value = create_signal(text.to_owned());
Self::focused_around(value, text_input(value))
}
fn focused_around(value: RwSignal<String>, input: TextInput) -> Self {
let field = Self::around(value, input);
request_focus(&field.harness.tree, field.harness.root);
field
}
fn unfocused(text: &str) -> Self {
let value = create_signal(text.to_owned());
Self::around(value, text_input(value))
}
fn wrapped(value: RwSignal<String>, root: impl Widget + 'static) -> Self {
let field = Self::around(value, root);
request_focus(&field.harness.tree, field.input());
field
}
fn input(&self) -> WidgetId {
self.harness
.tree
.get_children(self.harness.root)
.last()
.copied()
.unwrap_or(self.harness.root)
}
fn around(value: RwSignal<String>, input: impl Widget + 'static) -> Self {
clear_focus();
clear_system_clipboard();
let _ = take_clipboard_change();
Self {
harness: Harness::laid_out(input, WIDTH, HEIGHT),
value,
now: Instant::now(),
}
}
fn press(&mut self, key: Key, modifiers: Modifiers) -> EventResponse {
self.now += Duration::from_millis(1);
self.harness
.send_at(Event::KeyDown { key, modifiers }, self.now)
}
fn key(&mut self, key: Key) -> EventResponse {
self.press(key, Modifiers::default())
}
fn shift(&mut self, key: Key) {
self.press(
key,
Modifiers {
shift: true,
..Default::default()
},
);
}
fn ctrl(&mut self, c: char) {
self.press(
Key::Char(c),
Modifiers {
ctrl: true,
..Default::default()
},
);
}
fn ctrl_shift(&mut self, c: char) {
self.press(
Key::Char(c),
Modifiers {
ctrl: true,
shift: true,
..Default::default()
},
);
}
fn middle_click(&mut self, x: f32, y: f32) {
self.now += Duration::from_millis(1);
self.harness
.send_at(Event::mouse_down(x, y, MouseButton::Middle), self.now);
}
fn type_text(&mut self, text: &str) {
for c in text.chars() {
self.key(Key::Char(c));
}
}
fn pause(&mut self) {
self.now += Duration::from_millis(1000);
}
fn wait(&mut self, ms: u64) {
self.now += Duration::from_millis(ms);
self.harness.advance(self.now);
}
fn text(&self) -> String {
self.value.get_untracked()
}
fn caret_x(&mut self) -> Option<f32> {
self.caret().map(|c| c.x)
}
fn text_colour(&mut self) -> Option<Color> {
fn find(node: &RenderNode) -> Option<Color> {
for cmd in &node.commands {
if let DrawCommand::Text { color, .. } = &**cmd {
return Some(*color);
}
}
node.children.iter().find_map(|child| find(child))
}
self.harness.lay_out(WIDTH, HEIGHT);
find(&self.harness.paint())
}
fn caret(&mut self) -> Option<Rect> {
let painted = self.harness.painted_rects();
assert!(
painted.len() <= 1,
"a field paints a selection highlight and a caret and nothing else, \
and the highlight comes first — so this answers for the caret only \
where there is no selection: {painted:?}"
);
painted.into_iter().next()
}
}
fn assert_caret_at(field: &mut Field, expected: f32, why: &str) {
let caret = field.caret().expect("a focused field draws its caret");
assert!(
(caret.x - expected).abs() < 0.5,
"{why}: expected the caret at {expected}, found it at {}",
caret.x
);
}
#[test]
fn an_unfocused_field_does_not_hear_the_keyboard() {
let mut field = Field::unfocused("");
let response = field.key(Key::Char('a'));
assert_eq!(response, EventResponse::Ignored);
assert_eq!(
field.text(),
"",
"a field nobody focused took a keystroke meant for whoever holds the \
keyboard"
);
}
#[test]
fn typing_puts_the_characters_in() {
let mut field = Field::focused("");
field.type_text("hi");
assert_eq!(field.text(), "hi");
}
#[test]
fn enter_hands_the_text_as_it_stands_to_on_submit() {
let submitted: Rc<RefCell<Vec<String>>> = Rc::default();
let sink = Rc::clone(&submitted);
let value = create_signal(String::new());
let mut field = Field::focused_around(
value,
text_input(value).on_submit(move |text| sink.borrow_mut().push(text.to_owned())),
);
field.type_text("hi");
field.key(Key::Enter);
assert_eq!(
submitted.borrow().as_slice(),
["hi".to_owned()],
"Enter has to hand over what is in the field now, not what it held when \
the callback was declared"
);
}
#[test]
fn backspace_takes_the_character_before_the_cursor_and_leaves_it_there() {
let mut field = Field::focused("");
field.type_text("hit");
field.key(Key::Backspace);
assert_eq!(field.text(), "hi");
field.key(Key::Left);
field.type_text("Z");
assert_eq!(
field.text(),
"hZi",
"backspace leaves the cursor where the character it removed was"
);
}
#[test]
fn delete_takes_the_character_after_it() {
let mut field = Field::focused("hit");
field.key(Key::Delete);
assert_eq!(field.text(), "it");
}
#[test]
fn backspace_at_the_start_and_delete_at_the_end_do_nothing() {
let mut field = Field::focused("hi");
field.key(Key::Backspace);
assert_eq!(field.text(), "hi");
field.key(Key::End);
field.key(Key::Delete);
assert_eq!(field.text(), "hi");
}
#[test]
fn right_moves_the_cursor_forward_one_character() {
let mut field = Field::focused("ac");
field.key(Key::Right);
field.type_text("b");
assert_eq!(field.text(), "abc");
}
#[test]
fn left_moves_the_cursor_backward_one_character() {
let mut field = Field::focused("bcd");
field.key(Key::End);
field.key(Key::Left);
field.type_text("X");
assert_eq!(field.text(), "bcXd");
}
#[test]
fn home_and_end_go_to_the_ends() {
let mut field = Field::focused("bcd");
field.key(Key::End);
field.type_text("!");
assert_eq!(field.text(), "bcd!");
field.key(Key::Home);
field.type_text("?");
assert_eq!(field.text(), "?bcd!");
}
#[test]
fn shift_and_an_arrow_extend_the_selection() {
let mut field = Field::focused("abcd");
field.shift(Key::Right);
field.shift(Key::Right);
field.type_text("X");
assert_eq!(
field.text(),
"Xcd",
"typing over a selection replaces it, so what was replaced says how far \
the selection reached"
);
}
#[test]
fn shift_and_home_select_back_to_the_start() {
let mut field = Field::focused("abcd");
field.key(Key::End);
field.shift(Key::Home);
field.type_text("X");
assert_eq!(field.text(), "X");
}
#[test]
fn end_without_shift_ends_the_selection() {
let mut field = Field::focused("abcd");
field.shift(Key::Right);
field.shift(Key::Right);
field.key(Key::End);
field.type_text("X");
assert_eq!(
field.text(),
"abcdX",
"an edge without shift collapses what was selected, so what follows \
inserts rather than replaces"
);
}
#[test]
fn left_on_a_selection_goes_to_its_start_rather_than_stepping_past_it() {
let mut field = Field::focused("abcd");
field.ctrl('a');
field.key(Key::Left);
field.type_text("X");
assert_eq!(
field.text(),
"Xabcd",
"Left on a selection goes to its start, not one character back from the \
cursor"
);
}
#[test]
fn right_on_a_selection_goes_to_its_end_rather_than_stepping_past_it() {
let mut field = Field::focused("abcd");
field.shift(Key::Right);
field.shift(Key::Right);
field.key(Key::Right);
field.type_text("Y");
assert_eq!(
field.text(),
"abYcd",
"Right on a selection goes to its end, not one character on from the \
cursor"
);
}
#[test]
fn ctrl_a_selects_all_of_it_from_wherever_the_cursor_is() {
let mut field = Field::focused("abcd");
field.key(Key::End);
field.ctrl('a');
field.type_text("X");
assert_eq!(field.text(), "X");
}
#[test]
fn ctrl_x_cuts_the_selection_to_the_clipboard() {
let mut field = Field::focused("abcd");
field.ctrl('a');
field.ctrl('x');
assert_eq!(field.text(), "");
assert_eq!(clipboard_paste().as_deref(), Some("abcd"));
}
#[test]
fn a_cut_leaves_the_cursor_where_the_selection_was() {
let mut field = Field::focused("abcd");
field.shift(Key::Right);
field.shift(Key::Right);
field.ctrl('x');
assert_eq!(field.text(), "cd");
field.type_text("X");
assert_eq!(
field.text(),
"Xcd",
"what is typed after a cut belongs where the cut text was, not wherever \
the cursor was left"
);
}
#[test]
fn ctrl_z_puts_back_what_was_typed_and_ctrl_y_takes_it_away_again() {
let mut field = Field::focused("");
field.type_text("ab");
field.pause();
field.type_text("c");
assert_eq!(field.text(), "abc");
field.ctrl('z');
assert_eq!(
field.text(),
"ab",
"the second thought is undone on its own; the first is a separate entry \
because a second passed in between"
);
field.ctrl('z');
assert_eq!(field.text(), "");
field.ctrl('y');
assert_eq!(field.text(), "ab");
field.ctrl_shift('z');
assert_eq!(field.text(), "abc", "Ctrl+Shift+Z is the other redo");
}
#[test]
fn a_delete_is_undoable_too() {
let mut field = Field::focused("hi");
field.key(Key::End);
field.pause();
field.key(Key::Backspace);
assert_eq!(field.text(), "h");
field.ctrl('z');
assert_eq!(field.text(), "hi");
}
#[test]
fn typing_after_an_undo_drops_what_was_undone() {
let mut field = Field::focused("");
field.type_text("ab");
field.pause();
field.type_text("c");
field.ctrl('z');
assert_eq!(field.text(), "ab");
field.type_text("X");
field.ctrl('y');
assert_eq!(
field.text(),
"abX",
"there is nothing to redo once the undone version has been typed over"
);
}
#[test]
fn typing_restarts_the_blink_rather_than_letting_the_caret_stay_dark() {
let mut field = Field::focused("");
assert!(field.caret().is_some(), "a focused field shows a caret");
field.wait(600);
assert_eq!(
field.caret(),
None,
"a blink period on, the caret is in its dark half"
);
field.wait(400);
field.type_text("a");
assert!(
field.caret().is_some(),
"the caret has to come back the moment a key is pressed, or typing \
happens somewhere the user cannot see"
);
field.wait(400);
assert!(
field.caret().is_some(),
"the blink resumed from the last toggle instead of from the keystroke"
);
}
#[test]
fn the_caret_stays_inside_the_field_when_the_text_runs_past_its_end() {
let mut field = Field::focused(SENTENCE);
field.key(Key::End);
assert_caret_at(
&mut field,
WIDTH - SCROLL_PADDING_BOTH_ENDS,
"a caret pushed off the right edge should be scrolled back to just \
inside it",
);
}
#[test]
fn the_view_follows_the_caret_no_further_than_it_has_to() {
let mut field = Field::focused(SENTENCE);
field.key(Key::End);
let settled = field.caret().expect("a focused field draws its caret").x;
field.type_text(" and again");
assert_caret_at(
&mut field,
settled,
"typing at the right edge should move the text, not the caret",
);
}
#[test]
fn a_step_back_inside_the_viewport_moves_the_caret_and_not_the_view() {
let mut field = Field::focused(SENTENCE);
field.key(Key::End);
let at_the_edge = field.caret().expect("a focused field draws its caret").x;
field.key(Key::Left);
let caret = field.caret().expect("a focused field draws its caret");
assert!(
caret.x < at_the_edge - 1.0,
"one character back from the right edge is still inside the viewport, so \
the caret should have moved left from {at_the_edge} and is at {}",
caret.x
);
}
#[test]
fn walking_back_through_a_scrolled_field_never_puts_the_caret_outside_it() {
let mut field = Field::focused(SENTENCE);
field.key(Key::End);
for step in 1..=SENTENCE.chars().count() {
field.key(Key::Left);
let caret = field.caret().expect("a focused field draws its caret");
assert!(
caret.x >= -0.5 && caret.x <= WIDTH + 0.5,
"{step} characters back from the end, the caret is at {} — outside a \
field {WIDTH} wide",
caret.x
);
}
}
#[test]
fn the_view_comes_back_to_the_start_when_the_cursor_does() {
let mut field = Field::focused(SENTENCE);
field.key(Key::End);
field.key(Key::Home);
assert_caret_at(
&mut field,
0.0,
"a cursor at the start of the text belongs at the start of the field",
);
}
#[test]
fn a_field_below_a_disabled_container_does_not_hear_the_keyboard() {
let value = create_signal("hi".to_owned());
let mut field = Field::wrapped(
value,
container()
.width(WIDTH)
.height(HEIGHT)
.enabled(false)
.child(text_input(value)),
);
let response = field.key(Key::Char('x'));
assert_eq!(response, EventResponse::Ignored);
assert_eq!(
field.text(),
"hi",
"a field under a disabled container took a keystroke"
);
}
#[test]
fn a_disabled_container_stops_claiming_the_focus_below_it() {
let value = create_signal(String::new());
let enabled = create_signal(true);
let field = Field::wrapped(
value,
container()
.width(WIDTH)
.height(HEIGHT)
.enabled(enabled)
.child(text_input(value)),
);
let control = field
.harness
.tree
.nearest_control(field.input())
.expect("declaring `enabled` makes a container an interaction unit");
assert!(control.has_focus(), "the field below it holds the keyboard");
enabled.set(false);
assert!(
!control.has_focus(),
"a disabled unit is not where the keyboard is aimed"
);
}
#[test]
fn a_read_only_field_refuses_every_edit() {
let value = create_signal("hi".to_owned());
let readonly = create_signal(true);
let mut field = Field::wrapped(
value,
container()
.width(WIDTH)
.height(HEIGHT)
.child(text_input(value).readonly(readonly)),
);
for key in [Key::Char('x'), Key::Backspace, Key::Delete] {
assert_eq!(
field.key(key),
EventResponse::Ignored,
"a key it will not use is a key somebody else may"
);
}
for c in ['x', 'v', 'z', 'y'] {
assert_eq!(
field.press(
Key::Char(c),
Modifiers {
ctrl: true,
..Default::default()
}
),
EventResponse::Ignored,
"ctrl+{c} edits, so it is refused too"
);
}
assert_eq!(
field.press(
Key::Char('a'),
Modifiers {
ctrl: true,
..Default::default()
}
),
EventResponse::Handled
);
assert_eq!(field.text(), "hi", "not one of those may change the text");
readonly.set(false);
field.key(Key::End);
field.key(Key::Char('x'));
assert_eq!(field.text(), "hix", "and it takes them again when it may");
}
#[test]
fn a_read_only_field_refuses_a_middle_click_paste() {
let value = create_signal("hi".to_owned());
let readonly = create_signal(true);
let mut field = Field::wrapped(
value,
container()
.width(WIDTH)
.height(HEIGHT)
.child(text_input(value).readonly(readonly)),
);
primary_copy("pasted");
field.middle_click(4.0, 5.0);
assert_eq!(field.text(), "hi", "the primary selection got in");
readonly.set(false);
field.middle_click(4.0, 5.0);
assert!(
field.text().contains("pasted"),
"and the same click works when the field is not read-only: {}",
field.text()
);
}
#[test]
fn a_field_styles_itself_from_its_control() {
const CALM: Color = Color::rgb(0.5, 0.5, 0.5);
const LIT: Color = Color::rgb(1.0, 1.0, 1.0);
let value = create_signal("hi".to_owned());
let enabled = create_signal(true);
let mut field = Field::wrapped(
value,
container()
.width(WIDTH)
.height(HEIGHT)
.enabled(enabled)
.child(text_input(value).color(CALM).when_focused(|s| s.color(LIT))),
);
assert_eq!(
field.text_colour(),
Some(LIT),
"the field holds the focus, so its focused layer applies"
);
enabled.set(false);
assert_eq!(
field.text_colour(),
Some(CALM),
"a disabled unit is not where the keyboard is aimed, so the layer lifts"
);
}
#[test]
fn a_read_only_field_still_submits() {
let value = create_signal("hi".to_owned());
let submits = Rc::new(RefCell::new(Vec::new()));
let sink = submits.clone();
let mut field = Field::wrapped(
value,
container().width(WIDTH).height(HEIGHT).child(
text_input(value)
.readonly(true)
.on_submit(move |text| sink.borrow_mut().push(text.to_owned())),
),
);
assert_eq!(field.key(Key::Enter), EventResponse::Handled);
assert_eq!(submits.borrow().as_slice(), ["hi"]);
}
#[test]
fn a_read_only_field_keeps_the_focus_and_the_ring() {
let value = create_signal("hi".to_owned());
let readonly = create_signal(false);
let field = Field::wrapped(
value,
container()
.width(WIDTH)
.height(HEIGHT)
.control()
.child(text_input(value).readonly(readonly)),
);
let control = field.harness.tree.nearest_control(field.input()).unwrap();
let before = focus_path();
assert!(before.contains(field.input()));
assert!(control.has_focus());
readonly.set(true);
assert_eq!(before, focus_path(), "the focus path is untouched");
assert!(
control.has_focus(),
"read-only is not disabled: the ring stays"
);
}
#[test]
fn a_read_only_field_still_moves_its_caret() {
let value = create_signal(SENTENCE.to_owned());
let mut field = Field::wrapped(
value,
container()
.width(WIDTH)
.height(HEIGHT)
.child(text_input(value).readonly(true)),
);
assert_eq!(field.key(Key::End), EventResponse::Handled);
assert_ne!(
field.caret_x(),
Some(0.0),
"End moved the caret off the start"
);
assert_eq!(field.key(Key::Home), EventResponse::Handled);
}
fn after_a_drag(leave: bool, then_move_to: Option<f32>) -> String {
let value = create_signal(SENTENCE.to_owned());
let mut field = Field::focused_around(value, text_input(value));
field.now += Duration::from_millis(1);
field
.harness
.send_at(Event::mouse_down(4.0, 5.0, MouseButton::Left), field.now);
field.now += Duration::from_millis(1);
field
.harness
.send_at(Event::mouse_move(40.0, 5.0), field.now);
if leave {
field.now += Duration::from_millis(1);
field.harness.send_at(Event::MouseLeave, field.now);
}
if let Some(x) = then_move_to {
field.now += Duration::from_millis(1);
field.harness.send_at(Event::mouse_move(x, 5.0), field.now);
}
field.key(Key::Char('X'));
field.text()
}
#[test]
fn a_drag_does_not_outlive_the_pointer_leaving_the_surface() {
let value = create_signal(SENTENCE.to_owned());
let mut field = Field::focused_around(value, text_input(value));
field.now += Duration::from_millis(1);
field
.harness
.send_at(Event::mouse_down(4.0, 5.0, MouseButton::Left), field.now);
field.now += Duration::from_millis(1);
assert_eq!(
field
.harness
.send_at(Event::mouse_move(40.0, 5.0), field.now),
EventResponse::Handled,
"the drag is live and extending the selection"
);
field.now += Duration::from_millis(1);
field.harness.send_at(Event::MouseLeave, field.now);
field.now += Duration::from_millis(1);
assert_eq!(
field
.harness
.send_at(Event::mouse_move(150.0, 5.0), field.now),
EventResponse::Ignored,
"the leave ended the drag, so a later move is not the field's — and \
150 is inside the field, so only a live drag could have claimed it"
);
let left_then_moved = after_a_drag(true, Some(150.0));
assert_eq!(
left_then_moved,
after_a_drag(true, None),
"the move after the leave selected nothing further"
);
assert_ne!(
left_then_moved,
after_a_drag(false, Some(150.0)),
"and it would have, had the drag still been live"
);
}