T08b menus: menu bar over the native menus, searchable menu, pie menu; megamenu points to the navigation menu; menus follow their hosts
This commit is contained in:
18 files changed
+1516
-261
No files matched your search
@@ -0,0 +1,379 @@
|
||||
use gpui::{
|
||||
App, Context, FocusHandle, InteractiveElement, IntoElement, KeyUpEvent, Keystroke, Modifiers,
|
||||
MouseButton, ParentElement, Render, SharedString, Styled, TestAppContext, VisualTestContext,
|
||||
Window, div, point, prelude::*, px,
|
||||
};
|
||||
|
||||
mod more;
|
||||
|
||||
use super::{ContextMenu, DropdownMenu, Menu, MenuItem, SearchableMenu};
|
||||
use crate::{primitives::FocusScope, theme::Theme};
|
||||
|
||||
/// What the rows did, and the state the check and radio rows show.
|
||||
struct Desk {
|
||||
root: FocusHandle,
|
||||
ran: Vec<SharedString>,
|
||||
wrap: bool,
|
||||
theme: SharedString,
|
||||
view_rows: bool,
|
||||
enters_above: usize,
|
||||
shift: bool,
|
||||
}
|
||||
|
||||
impl Desk {
|
||||
fn menu(&self, cx: &mut Context<Self>) -> Menu {
|
||||
let run = |name: &'static str| {
|
||||
let view = cx.entity();
|
||||
move |_: &mut Window, cx: &mut App| {
|
||||
view.update(cx, |desk, cx| {
|
||||
desk.ran.push(name.into());
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
};
|
||||
let (wrap, light, dark) = (cx.entity(), cx.entity(), cx.entity());
|
||||
let theme = Menu::new()
|
||||
.item(
|
||||
MenuItem::radio("Light", self.theme == "light")
|
||||
.on_click(move |_, cx| light.update(cx, |desk, _| desk.theme = "light".into())),
|
||||
)
|
||||
.item(
|
||||
MenuItem::radio("Dark", self.theme == "dark")
|
||||
.on_click(move |_, cx| dark.update(cx, |desk, _| desk.theme = "dark".into())),
|
||||
);
|
||||
let edit = Menu::new()
|
||||
.item(
|
||||
MenuItem::new("Cut")
|
||||
.keys("secondary-x")
|
||||
.on_click(run("cut")),
|
||||
)
|
||||
.item(MenuItem::new("Copy").disabled(true).on_click(run("copy")))
|
||||
.item(MenuItem::new("Paste").on_click(run("paste")));
|
||||
if !self.view_rows {
|
||||
return edit;
|
||||
}
|
||||
edit.group(
|
||||
"View",
|
||||
[
|
||||
MenuItem::check("Word wrap", self.wrap)
|
||||
.on_click(move |_, cx| wrap.update(cx, |desk, _| desk.wrap = !desk.wrap)),
|
||||
MenuItem::submenu("Theme", theme),
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Desk {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let menu = self.menu(cx);
|
||||
let above = cx.entity();
|
||||
div()
|
||||
.size_full()
|
||||
.on_key_down(move |event, _, cx| {
|
||||
if event.keystroke.key == "enter" {
|
||||
above.update(cx, |desk, _| desk.enters_above += 1);
|
||||
}
|
||||
})
|
||||
.child(
|
||||
FocusScope::new(&self.root)
|
||||
.size_full()
|
||||
.when(self.shift, |scope| scope.child(div().h(px(80.0))))
|
||||
.child(DropdownMenu::new("edit", "Edit", menu.clone()))
|
||||
.child(
|
||||
ContextMenu::new("area", menu.clone())
|
||||
.child(div().w(px(200.0)).h(px(120.0))),
|
||||
)
|
||||
.child(SearchableMenu::new("find", "Find", menu)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn settle(cx: &mut VisualTestContext) {
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, _| window.refresh());
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
/// Presses and releases `key`, with a frame in between as on a real keyboard.
|
||||
pub(super) fn press(key: &str, cx: &mut VisualTestContext) {
|
||||
cx.simulate_keystrokes(key);
|
||||
settle(cx);
|
||||
cx.simulate_event(KeyUpEvent {
|
||||
keystroke: Keystroke::parse(key).expect("a key"),
|
||||
});
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
fn desk(cx: &mut TestAppContext) -> (gpui::Entity<Desk>, &mut VisualTestContext) {
|
||||
cx.update(|cx| {
|
||||
Theme::init(cx);
|
||||
crate::forms::bind_keys(cx);
|
||||
});
|
||||
let (view, cx) = cx.add_window_view(|_, cx| Desk {
|
||||
root: cx.focus_handle(),
|
||||
ran: Vec::new(),
|
||||
wrap: false,
|
||||
theme: "light".into(),
|
||||
view_rows: true,
|
||||
enters_above: 0,
|
||||
shift: false,
|
||||
});
|
||||
settle(cx);
|
||||
(view, cx)
|
||||
}
|
||||
|
||||
fn ran(view: &gpui::Entity<Desk>, cx: &mut VisualTestContext) -> Vec<SharedString> {
|
||||
view.read_with(cx, |desk, _| desk.ran.clone())
|
||||
}
|
||||
|
||||
fn click(x: f32, y: f32, cx: &mut VisualTestContext) {
|
||||
let at = point(px(x), px(y));
|
||||
cx.simulate_mouse_move(at, None, Modifiers::none());
|
||||
cx.simulate_click(at, Modifiers::none());
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_keyboard_opens_walks_past_disabled_rows_and_runs(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
cx.update(|window, _| window.focus_next());
|
||||
press("enter", cx);
|
||||
press("down", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["paste"]);
|
||||
press("down", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["paste"], "the release left the menu shut");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn submenus_open_right_and_close_left(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
cx.update(|window, _| window.focus_next());
|
||||
press("enter", cx);
|
||||
press("up", cx);
|
||||
press("right", cx);
|
||||
press("down", cx);
|
||||
press("left", cx);
|
||||
press("right", cx);
|
||||
press("down", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(view.read_with(cx, |desk, _| desk.theme.clone()), "dark");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_check_row_reports_to_its_owner(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
cx.update(|window, _| window.focus_next());
|
||||
press("enter", cx);
|
||||
press("up", cx);
|
||||
press("up", cx);
|
||||
press("enter", cx);
|
||||
assert!(view.read_with(cx, |desk, _| desk.wrap));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn escape_backs_out_one_level_then_closes(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
cx.update(|window, _| window.focus_next());
|
||||
press("enter", cx);
|
||||
press("up", cx);
|
||||
press("right", cx);
|
||||
press("escape", cx);
|
||||
press("up", cx);
|
||||
press("enter", cx);
|
||||
assert!(
|
||||
view.read_with(cx, |desk, _| desk.wrap),
|
||||
"one escape left the root open"
|
||||
);
|
||||
press("enter", cx);
|
||||
press("escape", cx);
|
||||
press("down", cx);
|
||||
press("enter", cx);
|
||||
assert!(
|
||||
ran(&view, cx).is_empty(),
|
||||
"the second escape closed the menu"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_right_click_opens_the_context_menu_where_it_lands(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
let at = point(px(60.0), px(80.0));
|
||||
cx.simulate_mouse_move(at, None, Modifiers::none());
|
||||
cx.simulate_mouse_down(at, MouseButton::Right, Modifiers::none());
|
||||
cx.simulate_mouse_up(at, MouseButton::Right, Modifiers::none());
|
||||
settle(cx);
|
||||
press("down", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["cut"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_trigger_toggles_and_a_press_outside_closes(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
click(12.0, 12.0, cx);
|
||||
click(12.0, 12.0, cx);
|
||||
press("down", cx);
|
||||
press("enter", cx);
|
||||
assert!(
|
||||
ran(&view, cx).is_empty(),
|
||||
"the second press on the trigger closed it"
|
||||
);
|
||||
click(12.0, 12.0, cx);
|
||||
click(600.0, 400.0, cx);
|
||||
press("down", cx);
|
||||
press("enter", cx);
|
||||
assert!(ran(&view, cx).is_empty(), "the press outside closed it");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn rows_the_owner_removes_leave_the_menu_usable(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
cx.update(|window, _| window.focus_next());
|
||||
press("enter", cx);
|
||||
press("up", cx);
|
||||
view.update(cx, |desk, cx| {
|
||||
desk.view_rows = false;
|
||||
cx.notify();
|
||||
});
|
||||
settle(cx);
|
||||
press("enter", cx);
|
||||
assert!(ran(&view, cx).is_empty(), "the removed row ran");
|
||||
press("down", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["cut"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn closing_from_the_trigger_hands_focus_back(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
cx.update(|window, _| window.focus_next());
|
||||
press("enter", cx);
|
||||
click(12.0, 12.0, cx);
|
||||
press("enter", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["cut"], "focus came back to the trigger");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn confirm_keys_stay_inside_the_menu(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
click(12.0, 12.0, cx);
|
||||
press("down", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["cut"]);
|
||||
assert_eq!(view.read_with(cx, |desk, _| desk.enters_above), 0);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn modified_confirm_keys_do_not_pick(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
cx.update(|window, _| window.focus_next());
|
||||
press("enter", cx);
|
||||
press("ctrl-enter", cx);
|
||||
press("shift-space", cx);
|
||||
assert!(ran(&view, cx).is_empty(), "a modified key picked");
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["cut"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_second_right_click_moves_the_context_menu(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
cx.update(|window, _| window.focus_next());
|
||||
for at in [point(px(150.0), px(120.0)), point(px(20.0), px(40.0))] {
|
||||
cx.simulate_mouse_move(at, None, Modifiers::none());
|
||||
cx.simulate_mouse_down(at, MouseButton::Right, Modifiers::none());
|
||||
cx.simulate_mouse_up(at, MouseButton::Right, Modifiers::none());
|
||||
settle(cx);
|
||||
}
|
||||
press("down", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(
|
||||
ran(&view, cx),
|
||||
["cut"],
|
||||
"the menu stayed open at the second click"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_left_press_in_the_region_closes_the_context_menu(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
let at = point(px(150.0), px(120.0));
|
||||
cx.simulate_mouse_move(at, None, Modifiers::none());
|
||||
cx.simulate_mouse_down(at, MouseButton::Right, Modifiers::none());
|
||||
cx.simulate_mouse_up(at, MouseButton::Right, Modifiers::none());
|
||||
settle(cx);
|
||||
click(20.0, 40.0, cx);
|
||||
press("down", cx);
|
||||
press("enter", cx);
|
||||
assert!(ran(&view, cx).is_empty(), "the left press closed the menu");
|
||||
}
|
||||
|
||||
fn open_find(cx: &mut VisualTestContext) {
|
||||
cx.update(|window, _| {
|
||||
window.focus_next();
|
||||
window.focus_next();
|
||||
});
|
||||
press("enter", cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_filter_runs_the_best_fit(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
open_find(cx);
|
||||
cx.simulate_input("pa");
|
||||
settle(cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["paste"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn space_types_into_the_filter(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
open_find(cx);
|
||||
cx.simulate_input("pa");
|
||||
settle(cx);
|
||||
press("space", cx);
|
||||
assert!(ran(&view, cx).is_empty(), "space picked a row");
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["paste"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn arrows_walk_rows_from_the_filter(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
open_find(cx);
|
||||
press("down", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["paste"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn an_open_dropdown_follows_its_host(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
click(12.0, 12.0, cx);
|
||||
view.update(cx, |desk, cx| {
|
||||
desk.shift = true;
|
||||
cx.notify();
|
||||
});
|
||||
settle(cx);
|
||||
settle(cx);
|
||||
cx.simulate_mouse_move(point(px(40.0), px(131.0)), None, Modifiers::none());
|
||||
settle(cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["cut"], "the panel moved with its button");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn clearing_the_filter_marks_the_first_row_again(cx: &mut TestAppContext) {
|
||||
let (view, cx) = desk(cx);
|
||||
open_find(cx);
|
||||
cx.simulate_input("t");
|
||||
settle(cx);
|
||||
press("down", cx);
|
||||
press("backspace", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["cut"]);
|
||||
}
|
||||
Reference in new issue
Block a user