T06b forms selection: checkbox, radio, cards, switch, select, combobox, multi select, cascader, transfer list, list box, chips, rating, sliders, knob, stepper
This commit is contained in:
36 files changed
+5170
-198
No files matched your search
@@ -0,0 +1,313 @@
|
||||
use gpui::{
|
||||
AppContext as _, Context, Entity, IntoElement, KeyUpEvent, Keystroke, Render, SharedString,
|
||||
TestAppContext, VisualTestContext, Window,
|
||||
};
|
||||
|
||||
use super::setup;
|
||||
use crate::forms::{
|
||||
Checkbox, Choice, Combobox, MultiSelect, RadioGroup, Rating, Select, Slider, TextInput,
|
||||
};
|
||||
|
||||
fn abc(middle_off: bool) -> [Choice; 3] {
|
||||
let middle = Choice::new("b", "B");
|
||||
[
|
||||
Choice::new("a", "A"),
|
||||
if middle_off {
|
||||
middle.disabled()
|
||||
} else {
|
||||
middle
|
||||
},
|
||||
Choice::new("c", "C"),
|
||||
]
|
||||
}
|
||||
|
||||
struct Radios {
|
||||
chosen: SharedString,
|
||||
}
|
||||
|
||||
fn release(key: &str, cx: &mut VisualTestContext) {
|
||||
cx.simulate_event(KeyUpEvent {
|
||||
keystroke: Keystroke::parse(key).unwrap(),
|
||||
});
|
||||
}
|
||||
|
||||
impl Render for Radios {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
RadioGroup::new("radios", abc(true))
|
||||
.selected(self.chosen.clone())
|
||||
.on_change(move |value, _, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.chosen = value.clone();
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn radio_arrows_skip_disabled_choices_and_wrap(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|_, _| Radios { chosen: "a".into() });
|
||||
cx.update(|window, _| window.focus_next());
|
||||
cx.simulate_keystrokes("down");
|
||||
assert_eq!(view.read_with(cx, |view, _| view.chosen.clone()), "c");
|
||||
cx.simulate_keystrokes("down");
|
||||
assert_eq!(view.read_with(cx, |view, _| view.chosen.clone()), "a");
|
||||
}
|
||||
|
||||
struct Picker {
|
||||
chosen: SharedString,
|
||||
}
|
||||
|
||||
impl Render for Picker {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
Select::new("select", abc(false))
|
||||
.selected(self.chosen.clone())
|
||||
.on_change(move |value, _, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.chosen = value.clone();
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn select_opens_moves_and_picks_from_the_keyboard(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|_, _| Picker { chosen: "a".into() });
|
||||
cx.update(|window, _| window.focus_next());
|
||||
cx.simulate_keystrokes("down down enter");
|
||||
assert_eq!(view.read_with(cx, |view, _| view.chosen.clone()), "b");
|
||||
}
|
||||
|
||||
struct Level {
|
||||
value: f64,
|
||||
}
|
||||
|
||||
impl Render for Level {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
Slider::new("level", self.value).on_change(move |value, _, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.value = value;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn slider_keys_step_jump_and_reach_the_end(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|_, _| Level { value: 10.0 });
|
||||
cx.update(|window, _| window.focus_next());
|
||||
cx.simulate_keystrokes("right pageup");
|
||||
assert_eq!(view.read_with(cx, |view, _| view.value), 21.0);
|
||||
cx.simulate_keystrokes("end");
|
||||
assert_eq!(view.read_with(cx, |view, _| view.value), 100.0);
|
||||
}
|
||||
|
||||
struct Agree {
|
||||
on: bool,
|
||||
}
|
||||
|
||||
impl Render for Agree {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
Checkbox::new("agree", self.on)
|
||||
.label("Agree")
|
||||
.on_change(move |on, _, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.on = on;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn space_toggles_a_focused_checkbox(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|_, _| Agree { on: false });
|
||||
cx.update(|window, _| window.focus_next());
|
||||
cx.simulate_keystrokes("space");
|
||||
cx.simulate_event(KeyUpEvent {
|
||||
keystroke: Keystroke::parse("space").unwrap(),
|
||||
});
|
||||
assert!(view.read_with(cx, |view, _| view.on));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_disabled_choice_still_leaves_the_group_reachable(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|_, _| Radios { chosen: "b".into() });
|
||||
cx.update(|window, _| window.focus_next());
|
||||
cx.simulate_keystrokes("down");
|
||||
assert_eq!(view.read_with(cx, |view, _| view.chosen.clone()), "c");
|
||||
}
|
||||
|
||||
struct Several {
|
||||
chosen: Vec<SharedString>,
|
||||
}
|
||||
|
||||
impl Render for Several {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
MultiSelect::new("several", abc(false))
|
||||
.selected(self.chosen.clone())
|
||||
.on_change(move |next, _, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.chosen = next.to_vec();
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_full_enter_press_opens_a_multi_select_and_keeps_it_open(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|_, _| Several { chosen: Vec::new() });
|
||||
cx.update(|window, _| window.focus_next());
|
||||
cx.simulate_keystrokes("enter");
|
||||
release("enter", cx);
|
||||
cx.simulate_keystrokes("enter");
|
||||
assert_eq!(view.read_with(cx, |view, _| view.chosen.clone()), ["a"]);
|
||||
}
|
||||
|
||||
struct Typed {
|
||||
state: Entity<TextInput>,
|
||||
chosen: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl Render for Typed {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
let mut combo = Combobox::new(
|
||||
"typed",
|
||||
&self.state,
|
||||
[
|
||||
Choice::new("a", "A").disabled(),
|
||||
Choice::new("b", "B"),
|
||||
Choice::new("c", "C"),
|
||||
],
|
||||
)
|
||||
.on_change(move |value, _, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.chosen = Some(value.clone());
|
||||
cx.notify();
|
||||
})
|
||||
});
|
||||
if let Some(value) = self.chosen.clone() {
|
||||
combo = combo.selected(value);
|
||||
}
|
||||
combo
|
||||
}
|
||||
}
|
||||
|
||||
fn typed<'a>(
|
||||
chosen: Option<&str>,
|
||||
cx: &'a mut TestAppContext,
|
||||
) -> (Entity<Typed>, &'a mut VisualTestContext) {
|
||||
setup(cx);
|
||||
let chosen = chosen.map(|value| SharedString::from(value.to_string()));
|
||||
cx.add_window_view(move |window, cx| Typed {
|
||||
state: cx.new(|cx| TextInput::new(window, cx)),
|
||||
chosen,
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn enter_in_a_combobox_skips_a_disabled_first_row(cx: &mut TestAppContext) {
|
||||
let (view, cx) = typed(None, cx);
|
||||
cx.update(|window, _| window.focus_next());
|
||||
cx.simulate_keystrokes("enter");
|
||||
assert_eq!(
|
||||
view.read_with(cx, |view, _| view.chosen.clone()),
|
||||
Some("b".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_combobox_shows_the_value_its_owner_sets(cx: &mut TestAppContext) {
|
||||
let (view, cx) = typed(Some("b"), cx);
|
||||
let state = view.read_with(cx, |view, _| view.state.clone());
|
||||
assert_eq!(
|
||||
state.read_with(cx, |input, _| input.text().to_string()),
|
||||
"B"
|
||||
);
|
||||
view.update(cx, |view, cx| {
|
||||
view.chosen = Some("c".into());
|
||||
cx.notify();
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
state.read_with(cx, |input, _| input.text().to_string()),
|
||||
"C"
|
||||
);
|
||||
}
|
||||
|
||||
struct Stars {
|
||||
value: u8,
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Render for Stars {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
Rating::new("stars", self.value)
|
||||
.disabled(self.disabled)
|
||||
.on_change(move |value, _, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.value = value;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_disabled_rating_ignores_the_arrows(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|_, _| Stars {
|
||||
value: 2,
|
||||
disabled: false,
|
||||
});
|
||||
cx.update(|window, _| window.focus_next());
|
||||
view.update(cx, |view, cx| {
|
||||
view.disabled = true;
|
||||
cx.notify();
|
||||
});
|
||||
cx.simulate_keystrokes("right");
|
||||
assert_eq!(view.read_with(cx, |view, _| view.value), 2);
|
||||
}
|
||||
|
||||
struct Free {
|
||||
state: Entity<TextInput>,
|
||||
}
|
||||
|
||||
impl Render for Free {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
Combobox::new("free", &self.state, abc(false)).free()
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_free_combobox_keeps_the_text_it_mounts_with(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|window, cx| Free {
|
||||
state: cx.new(|cx| {
|
||||
let mut input = TextInput::new(window, cx);
|
||||
input.set_text("custom label", cx);
|
||||
input
|
||||
}),
|
||||
});
|
||||
let state = view.read_with(cx, |view, _| view.state.clone());
|
||||
assert_eq!(
|
||||
state.read_with(cx, |input, _| input.text().to_string()),
|
||||
"custom label"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
mod choices;
|
||||
|
||||
use gpui::{
|
||||
AppContext as _, Context, Entity, EntityInputHandler, IntoElement, KeyUpEvent, Keystroke,
|
||||
ParentElement, Render, TestAppContext, VisualTestContext, Window, div,
|
||||
};
|
||||
|
||||
use super::{Input, MaskedInput, NumberInput, PinInput, TextInput, bind_keys};
|
||||
use crate::theme::Theme;
|
||||
|
||||
struct Fields {
|
||||
fields: Vec<Entity<TextInput>>,
|
||||
clearable: bool,
|
||||
}
|
||||
|
||||
impl Render for Fields {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div().children(self.fields.iter().map(|field| {
|
||||
let input = Input::new(field);
|
||||
if self.clearable {
|
||||
input.clearable()
|
||||
} else {
|
||||
input
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn setup(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
Theme::init(cx);
|
||||
bind_keys(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn open(
|
||||
count: usize,
|
||||
clearable: bool,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Vec<Entity<TextInput>>, &mut VisualTestContext) {
|
||||
open_with(count, clearable, |input| input, cx)
|
||||
}
|
||||
|
||||
fn open_with(
|
||||
count: usize,
|
||||
clearable: bool,
|
||||
build: impl Fn(TextInput) -> TextInput + 'static,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Vec<Entity<TextInput>>, &mut VisualTestContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|window, cx| Fields {
|
||||
fields: (0..count)
|
||||
.map(|_| cx.new(|cx| build(TextInput::new(window, cx))))
|
||||
.collect(),
|
||||
clearable,
|
||||
});
|
||||
let fields = view.read_with(cx, |view, _| view.fields.clone());
|
||||
(fields, cx)
|
||||
}
|
||||
|
||||
fn text(field: &Entity<TextInput>, cx: &mut VisualTestContext) -> String {
|
||||
field.read_with(cx, |input, _| input.text().to_string())
|
||||
}
|
||||
|
||||
fn focus(field: &Entity<TextInput>, cx: &mut VisualTestContext) {
|
||||
cx.update(|window, cx| window.focus(&field.read(cx).focus().clone()));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn tab_reaches_fields_and_skips_disabled_ones(cx: &mut TestAppContext) {
|
||||
let (fields, cx) = open(3, false, cx);
|
||||
fields[1].update(cx, |input, cx| input.set_disabled(true, cx));
|
||||
cx.run_until_parked();
|
||||
let mut order = Vec::new();
|
||||
for _ in 0..2 {
|
||||
order.push(cx.update(|window, cx| {
|
||||
window.focus_next();
|
||||
fields
|
||||
.iter()
|
||||
.position(|field| field.read(cx).focus().is_focused(window))
|
||||
}));
|
||||
}
|
||||
assert_eq!(order, [Some(0), Some(2)]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn enter_on_a_clear_button_clears_its_own_field(cx: &mut TestAppContext) {
|
||||
let (fields, cx) = open(2, true, cx);
|
||||
for field in &fields {
|
||||
field.update(cx, |input, cx| input.set_text("kept", cx));
|
||||
}
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, _| {
|
||||
window.focus_next();
|
||||
window.focus_next();
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.simulate_event(KeyUpEvent {
|
||||
keystroke: Keystroke::parse("enter").unwrap(),
|
||||
});
|
||||
assert_eq!([text(&fields[0], cx), text(&fields[1], cx)], ["", "kept"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_composition_undoes_in_one_step(cx: &mut TestAppContext) {
|
||||
let (fields, cx) = open(1, false, cx);
|
||||
let field = &fields[0];
|
||||
focus(field, cx);
|
||||
cx.update(|window, cx| {
|
||||
field.update(cx, |input, cx| {
|
||||
input.replace_and_mark_text_in_range(None, "n", None, window, cx);
|
||||
input.replace_and_mark_text_in_range(None, "ni", None, window, cx);
|
||||
input.replace_text_in_range(None, "你", window, cx);
|
||||
})
|
||||
});
|
||||
assert_eq!(text(field, cx), "你");
|
||||
cx.simulate_keystrokes("cmd-z");
|
||||
assert_eq!(text(field, cx), "");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_committed_composition_obeys_the_filter_and_length(cx: &mut TestAppContext) {
|
||||
let (fields, cx) = open_with(
|
||||
1,
|
||||
false,
|
||||
|input| input.filter(|ch| ch.is_ascii_digit()).max_len(1),
|
||||
cx,
|
||||
);
|
||||
let field = &fields[0];
|
||||
focus(field, cx);
|
||||
cx.update(|window, cx| {
|
||||
field.update(cx, |input, cx| {
|
||||
input.replace_and_mark_text_in_range(None, "12a", None, window, cx);
|
||||
input.unmark_text(window, cx);
|
||||
})
|
||||
});
|
||||
assert_eq!(text(field, cx), "1");
|
||||
cx.simulate_keystrokes("cmd-z");
|
||||
assert_eq!(text(field, cx), "");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn undo_leaves_a_disabled_field_alone(cx: &mut TestAppContext) {
|
||||
let (fields, cx) = open(1, false, cx);
|
||||
let field = &fields[0];
|
||||
focus(field, cx);
|
||||
cx.simulate_input("a");
|
||||
field.update(cx, |input, cx| input.set_disabled(true, cx));
|
||||
cx.simulate_keystrokes("cmd-z");
|
||||
assert_eq!(text(field, cx), "a");
|
||||
}
|
||||
|
||||
struct Stepped {
|
||||
value: f64,
|
||||
}
|
||||
|
||||
impl Render for Stepped {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
NumberInput::new("stepped", self.value).on_change(move |value, _, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.value = value;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn stepping_starts_from_the_typed_number(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|_, _| Stepped { value: 4.0 });
|
||||
cx.update(|window, _| window.focus_next());
|
||||
cx.simulate_keystrokes("cmd-a");
|
||||
cx.simulate_input("20");
|
||||
cx.simulate_keystrokes("up");
|
||||
assert_eq!(view.read_with(cx, |view, _| view.value), 21.0);
|
||||
}
|
||||
|
||||
struct Pin {
|
||||
code: String,
|
||||
}
|
||||
|
||||
impl Render for Pin {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
PinInput::new("pin", 4).on_complete(move |code, _, cx| {
|
||||
let code = code.to_string();
|
||||
view.update(cx, |view, _| view.code = code);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn select_all_then_typing_replaces_the_pin(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|_, _| Pin {
|
||||
code: String::new(),
|
||||
});
|
||||
cx.update(|window, _| window.focus_next());
|
||||
cx.simulate_input("4821");
|
||||
cx.simulate_keystrokes("cmd-a");
|
||||
cx.simulate_input("1234");
|
||||
assert_eq!(view.read_with(cx, |view, _| view.code.clone()), "1234");
|
||||
}
|
||||
|
||||
struct Masked {
|
||||
state: Entity<TextInput>,
|
||||
mask: &'static str,
|
||||
}
|
||||
|
||||
impl Render for Masked {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
MaskedInput::new("masked", &self.state, self.mask)
|
||||
}
|
||||
}
|
||||
|
||||
fn masked<'a>(
|
||||
mask: &'static str,
|
||||
cx: &'a mut TestAppContext,
|
||||
) -> (Entity<TextInput>, &'a mut VisualTestContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|window, cx| Masked {
|
||||
state: cx.new(|cx| TextInput::new(window, cx)),
|
||||
mask,
|
||||
});
|
||||
let state = view.read_with(cx, |view, _| view.state.clone());
|
||||
focus(&state, cx);
|
||||
(state, cx)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn select_all_then_typing_refits_a_masked_field(cx: &mut TestAppContext) {
|
||||
let (state, cx) = masked("1-999", cx);
|
||||
cx.simulate_input("123");
|
||||
assert_eq!(text(&state, cx), "1-123");
|
||||
cx.simulate_keystrokes("cmd-a");
|
||||
cx.simulate_input("1");
|
||||
assert_eq!(text(&state, cx), "1-1");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_composition_inside_a_masked_field_fits_from_where_it_began(cx: &mut TestAppContext) {
|
||||
let (state, cx) = masked("(999) 999", cx);
|
||||
cx.simulate_input("123456");
|
||||
assert_eq!(text(&state, cx), "(123) 456");
|
||||
cx.update(|window, cx| {
|
||||
state.update(cx, |input, cx| {
|
||||
input.select(2..2, cx);
|
||||
input.replace_and_mark_text_in_range(None, "9", None, window, cx);
|
||||
input.replace_text_in_range(None, "9", window, cx);
|
||||
})
|
||||
});
|
||||
assert_eq!(text(&state, cx), "(192) 345");
|
||||
}
|
||||
Reference in new issue
Block a user