add multiline support to input
This commit is contained in:
+761
-262
File diff suppressed because it is too large
Load Diff
+174
-1
@@ -4,7 +4,8 @@ use gpui::{
|
||||
TestAppContext, VisualTestContext, Window, div, point, px,
|
||||
};
|
||||
use gpui_native_core::input::{
|
||||
INPUT_CONTEXT, Input, InputBindings, InputState, InputStateEvent, Submit, bind_input_keys,
|
||||
INPUT_CONTEXT, Input, InputBindings, InputState, InputStateEvent, Submit, SubmitOn,
|
||||
bind_input_keys,
|
||||
};
|
||||
use gpui_native_theme::Theme;
|
||||
use std::cell::RefCell;
|
||||
@@ -599,6 +600,34 @@ fn password_window(initial: &str) -> (TestAppContext, Entity<InputState>, Visual
|
||||
(cx, state, visual)
|
||||
}
|
||||
|
||||
fn multiline_window(initial: &str) -> (TestAppContext, Entity<InputState>, VisualTestContext) {
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(|cx| {
|
||||
Theme::init(cx);
|
||||
bind_input_keys(cx, InputBindings::default());
|
||||
});
|
||||
let state = cx.update(InputState::new);
|
||||
cx.update(|cx| {
|
||||
state.update(cx, |state, cx| state.set_multiline(true, cx));
|
||||
});
|
||||
let state_for_window = state.clone();
|
||||
let window = cx.add_window(move |_, _| Harness {
|
||||
state: state_for_window,
|
||||
disabled: false,
|
||||
});
|
||||
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
||||
if !initial.is_empty() {
|
||||
// Seeding after the mode flips so newlines survive the sanitizer.
|
||||
visual.update(|window, cx| {
|
||||
state.update(cx, |state, cx| state.set_value(initial, window, cx));
|
||||
});
|
||||
}
|
||||
visual.update(|window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
});
|
||||
(cx, state, visual)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn password_fields_keep_the_real_buffer_while_masked() {
|
||||
let (cx, state, mut visual) = password_window("");
|
||||
@@ -733,3 +762,147 @@ fn the_reveal_toggle_flips_masking() {
|
||||
let revealed = cx.update(|cx| state.read(cx).is_revealed());
|
||||
assert!(!revealed, "the toggle should mask the field again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_caret_can_rest_on_a_line_break() {
|
||||
let (cx, state, mut visual) = multiline_window("ab\ncdef");
|
||||
focus(&mut visual, &state);
|
||||
|
||||
// Five lefts land the caret on the line break at the end of row 1 —
|
||||
// an offset between the two rows — and painting must not panic there.
|
||||
visual.simulate_keystrokes("left left left left left");
|
||||
visual.update(|window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
});
|
||||
visual.simulate_keystrokes("x");
|
||||
assert_eq!(value(&cx, &state), "abx\ncdef");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_fields_keep_newlines() {
|
||||
let (cx, state, mut visual) = multiline_window("");
|
||||
focus(&mut visual, &state);
|
||||
visual.update(|window, cx| {
|
||||
state.update(cx, |state, cx| state.set_value("a\nb", window, cx));
|
||||
});
|
||||
assert_eq!(value(&cx, &state), "a\nb");
|
||||
|
||||
// Paste keeps line breaks too.
|
||||
cx.update(|cx| {
|
||||
cx.write_to_clipboard(gpui::ClipboardItem::new_string("x\ny".to_string()));
|
||||
});
|
||||
visual.simulate_keystrokes("cmd-v");
|
||||
assert_eq!(value(&cx, &state), "a\nbx\ny");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_inserts_a_newline_and_modifier_enter_submits() {
|
||||
let (mut cx, state, mut visual) = multiline_window("");
|
||||
let (sink, _subscription) = record_events(&mut cx, &state);
|
||||
focus(&mut visual, &state);
|
||||
|
||||
visual.simulate_keystrokes("a b");
|
||||
visual.simulate_keystrokes("enter");
|
||||
assert_eq!(value(&cx, &state), "ab\n");
|
||||
assert_eq!(count(&sink, &InputStateEvent::Submitted), 0);
|
||||
|
||||
visual.simulate_keystrokes("cmd-enter");
|
||||
assert_eq!(count(&sink, &InputStateEvent::Submitted), 1);
|
||||
assert_eq!(value(&cx, &state), "ab\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_on_enter_swaps_the_keys() {
|
||||
let (mut cx, state, mut visual) = multiline_window("");
|
||||
let (sink, _subscription) = record_events(&mut cx, &state);
|
||||
visual.update(|_, cx| {
|
||||
state.update(cx, |state, cx| state.set_submit_on(SubmitOn::Enter, cx));
|
||||
});
|
||||
focus(&mut visual, &state);
|
||||
|
||||
// Enter submits; Shift + Enter breaks the line.
|
||||
visual.simulate_keystrokes("enter");
|
||||
assert_eq!(count(&sink, &InputStateEvent::Submitted), 1);
|
||||
assert_eq!(value(&cx, &state), "");
|
||||
visual.simulate_keystrokes("shift-enter");
|
||||
assert_eq!(value(&cx, &state), "\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn up_and_down_move_between_rows() {
|
||||
let (cx, state, mut visual) = multiline_window("ab\ncdef");
|
||||
focus(&mut visual, &state);
|
||||
|
||||
// From the end of row 1, shift-up reaches the end of the shorter row 0.
|
||||
visual.simulate_keystrokes("shift-up");
|
||||
visual.simulate_keystrokes("backspace");
|
||||
assert_eq!(value(&cx, &state), "ab");
|
||||
|
||||
// From the start of row 0, shift-down reaches the start of row 1.
|
||||
visual.update(|window, cx| {
|
||||
state.update(cx, |state, cx| state.set_value("ab\ncdef", window, cx));
|
||||
});
|
||||
visual.simulate_keystrokes("home");
|
||||
visual.simulate_keystrokes("shift-down");
|
||||
visual.simulate_keystrokes("backspace");
|
||||
assert_eq!(value(&cx, &state), "cdef");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_line_fields_ignore_up_and_down() {
|
||||
let (cx, state, mut visual) = input_window("ab", false);
|
||||
focus(&mut visual, &state);
|
||||
visual.simulate_keystrokes("up down");
|
||||
visual.simulate_keystrokes("x");
|
||||
assert_eq!(value(&cx, &state), "abx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn triple_click_selects_the_clicked_row() {
|
||||
let (cx, state, mut visual) = multiline_window("ab\ncdef");
|
||||
focus(&mut visual, &state);
|
||||
|
||||
// The second row's center sits one line height below the first's.
|
||||
let at = point(px(30.), px(44.));
|
||||
visual.simulate_event(MouseDownEvent {
|
||||
position: at,
|
||||
modifiers: Modifiers::default(),
|
||||
button: MouseButton::Left,
|
||||
click_count: 3,
|
||||
first_mouse: false,
|
||||
});
|
||||
visual.simulate_event(MouseUpEvent {
|
||||
position: at,
|
||||
modifiers: Modifiers::default(),
|
||||
button: MouseButton::Left,
|
||||
click_count: 3,
|
||||
});
|
||||
visual.simulate_keystrokes("cmd-c");
|
||||
let clipboard = cx.update(|cx| {
|
||||
cx.read_from_clipboard()
|
||||
.and_then(|item| item.text())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
assert_eq!(clipboard, "cdef");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_scroll_keeps_the_caret_in_view() {
|
||||
let (_cx, state, mut visual) = multiline_window("l1\nl2\nl3\nl4\nl5\nl6");
|
||||
focus(&mut visual, &state);
|
||||
visual.update(|window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
});
|
||||
|
||||
// The field shows four of the six rows and the caret ends on the last,
|
||||
// so the view scrolls down and row 1 sits above the field's top.
|
||||
let first_row_top = visual
|
||||
.update(|window, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.bounds_for_range(0..1, Default::default(), window, cx)
|
||||
})
|
||||
})
|
||||
.map(|bounds| bounds.top())
|
||||
.unwrap_or_default();
|
||||
assert!(first_row_top < px(0.), "first row top: {first_row_top:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user