add select component built on menu
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
Context, IntoElement, MouseButton, ParentElement as _, Render, Styled as _, TestAppContext,
|
||||
VisualTestContext, Window, div, point, px,
|
||||
};
|
||||
use gpui_native_core::select::Select;
|
||||
use gpui_native_theme::Theme;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Strategy {
|
||||
Curated,
|
||||
Uncensored,
|
||||
}
|
||||
|
||||
const STRATEGIES: [(Strategy, &str); 2] = [
|
||||
(Strategy::Curated, "Curated"),
|
||||
(Strategy::Uncensored, "Uncensored"),
|
||||
];
|
||||
|
||||
type Changes = Rc<RefCell<Vec<Option<Strategy>>>>;
|
||||
|
||||
fn draw(visual: &mut VisualTestContext) {
|
||||
visual.update(|window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn click(visual: &mut VisualTestContext, position: gpui::Point<gpui::Pixels>) {
|
||||
visual.simulate_mouse_down(position, MouseButton::Left, gpui::Modifiers::default());
|
||||
visual.simulate_mouse_up(position, MouseButton::Left, gpui::Modifiers::default());
|
||||
}
|
||||
|
||||
struct Harness {
|
||||
strategy: Option<Strategy>,
|
||||
changes: Changes,
|
||||
}
|
||||
|
||||
impl Render for Harness {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let changes = self.changes.clone();
|
||||
let view = cx.entity();
|
||||
div().size_full().p_4().child(
|
||||
Select::new("strategy")
|
||||
.options(STRATEGIES)
|
||||
.selected(self.strategy)
|
||||
.on_change(move |next, _window, cx| {
|
||||
changes.borrow_mut().push(next);
|
||||
view.update(cx, |harness, cx| {
|
||||
harness.strategy = next;
|
||||
cx.notify();
|
||||
});
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// One select: trigger band is `16..46` vertically (`p_4` + the 30 px field).
|
||||
fn select_window() -> (TestAppContext, VisualTestContext, Changes) {
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(Theme::init);
|
||||
let changes = Rc::new(RefCell::new(Vec::new()));
|
||||
let harness_changes = changes.clone();
|
||||
let window = cx.add_window(move |_, _| Harness {
|
||||
strategy: None,
|
||||
changes: harness_changes,
|
||||
});
|
||||
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
||||
draw(&mut visual);
|
||||
(cx, visual, changes)
|
||||
}
|
||||
|
||||
struct Pair {
|
||||
first: Option<Strategy>,
|
||||
second: Option<Strategy>,
|
||||
first_changes: Changes,
|
||||
second_changes: Changes,
|
||||
}
|
||||
|
||||
impl Render for Pair {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
let first_changes = self.first_changes.clone();
|
||||
let second_changes = self.second_changes.clone();
|
||||
let first_view = view.clone();
|
||||
let second_view = view.clone();
|
||||
|
||||
div().size_full().p_4().child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
Select::new("first")
|
||||
.options(STRATEGIES)
|
||||
.selected(self.first)
|
||||
.on_change(move |next, _window, cx| {
|
||||
first_changes.borrow_mut().push(next);
|
||||
first_view.update(cx, |pair, cx| {
|
||||
pair.first = next;
|
||||
cx.notify();
|
||||
});
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Select::new("second")
|
||||
.options(STRATEGIES)
|
||||
.selected(self.second)
|
||||
.on_change(move |next, _window, cx| {
|
||||
second_changes.borrow_mut().push(next);
|
||||
second_view.update(cx, |pair, cx| {
|
||||
pair.second = next;
|
||||
cx.notify();
|
||||
});
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Two selects: bands `16..46` and `54..84` (the 8 px gap between).
|
||||
fn pair_window() -> (TestAppContext, VisualTestContext, Changes, Changes) {
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(Theme::init);
|
||||
let first_changes = Rc::new(RefCell::new(Vec::new()));
|
||||
let second_changes = Rc::new(RefCell::new(Vec::new()));
|
||||
let first = first_changes.clone();
|
||||
let second = second_changes.clone();
|
||||
let window = cx.add_window(move |_, _| Pair {
|
||||
first: None,
|
||||
second: None,
|
||||
first_changes: first,
|
||||
second_changes: second,
|
||||
});
|
||||
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
||||
draw(&mut visual);
|
||||
(cx, visual, first_changes, second_changes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn choosing_an_option_reports_the_value_and_closes() {
|
||||
let (_cx, mut visual, changes) = select_window();
|
||||
|
||||
// The trigger opens the popup; the panel grabs focus, so the first option
|
||||
// is keyboard-highlighted and Enter activates it.
|
||||
click(&mut visual, point(px(100.), px(31.)));
|
||||
draw(&mut visual);
|
||||
visual.simulate_keystrokes("enter");
|
||||
draw(&mut visual);
|
||||
assert_eq!(*changes.borrow(), vec![Some(Strategy::Curated)]);
|
||||
|
||||
// The popup closed with the choice, so another Enter reports nothing.
|
||||
visual.simulate_keystrokes("enter");
|
||||
draw(&mut visual);
|
||||
assert_eq!(*changes.borrow(), vec![Some(Strategy::Curated)]);
|
||||
|
||||
// The trigger reopens; the second option is two Downs away.
|
||||
click(&mut visual, point(px(100.), px(31.)));
|
||||
draw(&mut visual);
|
||||
visual.simulate_keystrokes("down");
|
||||
visual.simulate_keystrokes("enter");
|
||||
draw(&mut visual);
|
||||
assert_eq!(
|
||||
*changes.borrow(),
|
||||
vec![Some(Strategy::Curated), Some(Strategy::Uncensored)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_controlled_value_round_trips_through_the_owner() {
|
||||
let (_cx, mut visual, changes) = select_window();
|
||||
|
||||
click(&mut visual, point(px(100.), px(31.)));
|
||||
draw(&mut visual);
|
||||
visual.simulate_keystrokes("down");
|
||||
visual.simulate_keystrokes("enter");
|
||||
draw(&mut visual);
|
||||
|
||||
// The harness stored the reported value, so the select now holds it.
|
||||
click(&mut visual, point(px(100.), px(31.)));
|
||||
draw(&mut visual);
|
||||
visual.simulate_keystrokes("enter");
|
||||
draw(&mut visual);
|
||||
assert_eq!(
|
||||
*changes.borrow(),
|
||||
vec![Some(Strategy::Uncensored), Some(Strategy::Uncensored)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_selects_keep_their_popups_independent() {
|
||||
let (_cx, mut visual, first_changes, second_changes) = pair_window();
|
||||
|
||||
// Opening the second select never touches the first.
|
||||
click(&mut visual, point(px(100.), px(69.)));
|
||||
draw(&mut visual);
|
||||
visual.simulate_keystrokes("enter");
|
||||
draw(&mut visual);
|
||||
|
||||
assert_eq!(*first_changes.borrow(), vec![]);
|
||||
assert_eq!(*second_changes.borrow(), vec![Some(Strategy::Curated)]);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
Context, IntoElement, MouseButton, ParentElement as _, Render, Styled as _, TestAppContext,
|
||||
VisualTestContext, Window, div, point, px,
|
||||
};
|
||||
use gpui_native_core::select::Select;
|
||||
use gpui_native_theme::Theme;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Level {
|
||||
Low,
|
||||
Mid,
|
||||
High,
|
||||
}
|
||||
|
||||
const LEVELS: [(Level, &str); 3] = [
|
||||
(Level::Low, "Low"),
|
||||
(Level::Mid, "Mid"),
|
||||
(Level::High, "High"),
|
||||
];
|
||||
|
||||
type Changes = Rc<RefCell<Vec<Option<Level>>>>;
|
||||
|
||||
struct Harness {
|
||||
value: Option<Level>,
|
||||
changes: Changes,
|
||||
}
|
||||
|
||||
impl Render for Harness {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let changes = self.changes.clone();
|
||||
let view = cx.entity();
|
||||
div().size_full().p_4().child(
|
||||
Select::new("level")
|
||||
.options(LEVELS)
|
||||
.selected(self.value)
|
||||
.on_change(move |next, _window, cx| {
|
||||
changes.borrow_mut().push(next);
|
||||
view.update(cx, |harness, cx| {
|
||||
harness.value = next;
|
||||
cx.notify();
|
||||
});
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn level_window() -> (TestAppContext, VisualTestContext, Changes) {
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(Theme::init);
|
||||
let changes = Rc::new(RefCell::new(Vec::new()));
|
||||
let harness_changes = changes.clone();
|
||||
let window = cx.add_window(move |_, _| Harness {
|
||||
value: None,
|
||||
changes: harness_changes,
|
||||
});
|
||||
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
||||
visual.update(|window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
});
|
||||
(cx, visual, changes)
|
||||
}
|
||||
|
||||
fn click(visual: &mut VisualTestContext, position: gpui::Point<gpui::Pixels>) {
|
||||
visual.simulate_mouse_down(position, MouseButton::Left, gpui::Modifiers::default());
|
||||
visual.simulate_mouse_up(position, MouseButton::Left, gpui::Modifiers::default());
|
||||
}
|
||||
|
||||
/// The trigger band is `16..46`; the panel hangs 4 px below it with a 4 px
|
||||
/// inner padding, so the three rows ride `~58..90`, `~90..122`, `~122..154`.
|
||||
/// Every option must answer a pointer press — a popup that clips its last
|
||||
/// item never does.
|
||||
#[test]
|
||||
fn every_option_row_answers_the_pointer() {
|
||||
let (_cx, mut visual, changes) = level_window();
|
||||
|
||||
let expected = [
|
||||
(px(74.), Level::Low),
|
||||
(px(106.), Level::Mid),
|
||||
(px(138.), Level::High),
|
||||
];
|
||||
for (y, value) in expected {
|
||||
click(&mut visual, point(px(100.), px(31.)));
|
||||
visual.update(|window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
});
|
||||
click(&mut visual, point(px(100.), y));
|
||||
visual.update(|window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
});
|
||||
assert_eq!(
|
||||
changes.borrow().last(),
|
||||
Some(&Some(value)),
|
||||
"pressing {value:?}'s row must choose it"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user