Menus · button rows: Menu::buttons lays icon buttons in one row, arrows cross it, a press inside a panel stays the menu's
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8fd3e431db
commit
3c35daa4d8
6 files changed
+356
-48
No files matched your search
+79
-4
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
AnyElement, App, Div, ElementId, Entity, FontWeight, InteractiveElement, IntoElement,
|
||||
MouseButton, ParentElement, Stateful, Styled, div, prelude::*,
|
||||
MouseButton, ParentElement, Role, SharedString, Stateful, Styled, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -10,7 +10,7 @@ use super::{
|
||||
use crate::{
|
||||
forms::{Run, surface},
|
||||
navigation::marked,
|
||||
primitives::{Icon, IconName},
|
||||
primitives::{Icon, IconName, Tooltip},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
|
||||
typography::KbdCombo,
|
||||
};
|
||||
@@ -82,6 +82,50 @@ fn row(item: &MenuItem, ix: usize, current: bool, cx: &App) -> Stateful<Div> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Button `column` of row `ix`, named by its label.
|
||||
fn button(item: &MenuItem, ix: usize, column: usize, current: bool, cx: &App) -> Stateful<Div> {
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let fg = match (item.disabled, current) {
|
||||
(true, _) => colors.fg_disabled,
|
||||
(false, true) => colors.fg,
|
||||
(false, false) => colors.fg_muted,
|
||||
};
|
||||
let (label, keys) = (item.label.clone(), item.keys.clone());
|
||||
let icon = item.icon.expect("a menu button has its icon");
|
||||
div()
|
||||
.id((SharedString::from(format!("button-{ix}")), column))
|
||||
.debug_selector(move || format!("menu-button-{ix}-{column}"))
|
||||
.role(Role::Button)
|
||||
.aria_label(item.label.clone())
|
||||
.flex_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.h(theme.control_height(ControlSize::Md))
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.when(current && !item.disabled, |button| button.bg(colors.hover))
|
||||
.when(!item.disabled, |button| button.cursor_pointer())
|
||||
.child(Icon::new(icon).size(IconSize::Sm).color(fg))
|
||||
.tooltip(Tooltip::rich(move |_, cx| {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(label.clone())
|
||||
.when_some(keys.clone(), |tip, keys| {
|
||||
tip.child(
|
||||
div()
|
||||
.text_color(cx.theme().colors.tooltip_fg.opacity(0.6))
|
||||
.child(KbdCombo::new(&keys)),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn panel(
|
||||
id: &ElementId,
|
||||
menu: &Menu,
|
||||
@@ -111,6 +155,32 @@ pub(super) fn panel(
|
||||
.text_color(colors.fg_subtle)
|
||||
.child(title.clone())
|
||||
.into_any_element(),
|
||||
Entry::Buttons(buttons) => div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.py_0p5()
|
||||
.children(buttons.iter().enumerate().map(|(column, item)| {
|
||||
let current = at == Some(ix) && open.levels[depth].column == column;
|
||||
let (hover, click, close, menu) =
|
||||
(state.clone(), state.clone(), close.clone(), menu.clone());
|
||||
let hover_menu = menu.clone();
|
||||
button(item, ix, column, current, cx).when(!item.disabled, |button| {
|
||||
button
|
||||
.on_mouse_move(move |_, _, cx| {
|
||||
let open = hover.read(cx);
|
||||
let marked = open.levels.get(depth).map(|l| (l.at, l.column));
|
||||
if marked != Some((Some(ix), column)) {
|
||||
mark(&hover, &hover_menu, depth, (ix, column), false, cx);
|
||||
}
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, move |_, window, cx| {
|
||||
window.prevent_default();
|
||||
cx.stop_propagation();
|
||||
choose(&click, &menu, depth, (ix, column), &close, window, cx);
|
||||
})
|
||||
})
|
||||
}))
|
||||
.into_any_element(),
|
||||
Entry::Item(item) => {
|
||||
let marked = at == Some(ix);
|
||||
let sub = matches!(item.kind, Kind::Sub(_));
|
||||
@@ -126,7 +196,7 @@ pub(super) fn panel(
|
||||
};
|
||||
let shown = open.levels.len() > depth + 1;
|
||||
if level.at != Some(ix) || (sub && !shown) {
|
||||
mark(&hover, &hover_menu, depth, ix, true, cx);
|
||||
mark(&hover, &hover_menu, depth, (ix, 0), true, cx);
|
||||
}
|
||||
})
|
||||
.on_mouse_down(
|
||||
@@ -134,7 +204,7 @@ pub(super) fn panel(
|
||||
move |_, window, cx| {
|
||||
window.prevent_default();
|
||||
cx.stop_propagation();
|
||||
choose(&click, &menu, depth, ix, &close, window, cx);
|
||||
choose(&click, &menu, depth, (ix, 0), &close, window, cx);
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -151,6 +221,11 @@ pub(super) fn panel(
|
||||
});
|
||||
surface((id.clone(), format!("panel-{depth}")), cx)
|
||||
.relative()
|
||||
// Presses here are the menu's, not its host's.
|
||||
.on_mouse_down(MouseButton::Left, |_, window, cx| {
|
||||
window.prevent_default();
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.min_w(theme.menu_width())
|
||||
.p_1()
|
||||
.flex()
|
||||
|
||||
+59
-36
@@ -21,7 +21,7 @@ pub(super) fn at_depth<'a>(menu: &'a Menu, levels: &[Level], depth: usize) -> Op
|
||||
levels.get(..depth)?.iter().try_fold(menu, |menu, level| {
|
||||
match level
|
||||
.at
|
||||
.and_then(|ix| menu.item_at(ix))
|
||||
.and_then(|ix| menu.item_at(ix, level.column))
|
||||
.map(|item| &item.kind)
|
||||
{
|
||||
Some(Kind::Sub(sub)) => Some(sub),
|
||||
@@ -34,11 +34,16 @@ pub(super) fn at_depth<'a>(menu: &'a Menu, levels: &[Level], depth: usize) -> Op
|
||||
fn settle(menu: &Menu, levels: &mut Vec<Level>) {
|
||||
let mut here = menu;
|
||||
for depth in 0..levels.len() {
|
||||
let at = levels[depth]
|
||||
let column = levels[depth].column;
|
||||
let spot = levels[depth]
|
||||
.at
|
||||
.filter(|&ix| here.item_at(ix).is_some_and(|item| !item.disabled));
|
||||
levels[depth].at = at;
|
||||
match at.and_then(|ix| here.item_at(ix)).map(|item| &item.kind) {
|
||||
.and_then(|ix| here.settled(ix, column).map(|column| (ix, column)));
|
||||
levels[depth].at = spot.map(|(ix, _)| ix);
|
||||
levels[depth].column = spot.map_or(0, |(_, column)| column);
|
||||
match spot
|
||||
.and_then(|(ix, column)| here.item_at(ix, column))
|
||||
.map(|item| &item.kind)
|
||||
{
|
||||
Some(Kind::Sub(sub)) if depth + 1 < levels.len() => here = sub,
|
||||
_ => {
|
||||
levels.truncate(depth + 1);
|
||||
@@ -48,10 +53,11 @@ fn settle(menu: &Menu, levels: &mut Vec<Level>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// One open level: its marked row, that row's box and its panel's box.
|
||||
/// An open level: marked row, button, and their boxes.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub(super) struct Level {
|
||||
pub(super) at: Option<usize>,
|
||||
pub(super) column: usize,
|
||||
pub(super) row: Bounds<Pixels>,
|
||||
pub(super) panel: Bounds<Pixels>,
|
||||
}
|
||||
@@ -93,10 +99,7 @@ impl Open {
|
||||
let first = menu.step(None, 1).filter(|_| marked);
|
||||
state.update(cx, |open, cx| {
|
||||
open.spot = Some(spot);
|
||||
open.levels = vec![Level {
|
||||
at: first,
|
||||
..Level::default()
|
||||
}];
|
||||
open.levels = vec![first_of(menu, first)];
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
@@ -105,10 +108,7 @@ impl Open {
|
||||
pub(crate) fn restart(state: &Entity<Open>, menu: &Menu, cx: &mut App) {
|
||||
let first = menu.step(None, 1);
|
||||
state.update(cx, |open, cx| {
|
||||
open.levels = vec![Level {
|
||||
at: first,
|
||||
..Level::default()
|
||||
}];
|
||||
open.levels = vec![first_of(menu, first)];
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
@@ -127,12 +127,24 @@ impl Open {
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks row `at` of level `depth` and drops deeper levels; a submenu row opens when `open_sub`.
|
||||
/// A level marking row `at` at its first enabled button.
|
||||
fn first_of(menu: &Menu, at: Option<usize>) -> Level {
|
||||
Level {
|
||||
at,
|
||||
column: match at {
|
||||
Some(ix) => menu.settled(ix, 0).expect("a stepped row runs"),
|
||||
None => 0,
|
||||
},
|
||||
..Level::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks a spot at `depth`; `open_sub` opens its submenu.
|
||||
pub(super) fn mark(
|
||||
state: &Entity<Open>,
|
||||
menu: &Menu,
|
||||
depth: usize,
|
||||
at: usize,
|
||||
(at, column): (usize, usize),
|
||||
open_sub: bool,
|
||||
cx: &mut App,
|
||||
) {
|
||||
@@ -140,12 +152,16 @@ pub(super) fn mark(
|
||||
let Some(here) = at_depth(menu, &open.levels, depth) else {
|
||||
return;
|
||||
};
|
||||
let sub = matches!(here.item_at(at).map(|item| &item.kind), Some(Kind::Sub(_)));
|
||||
let sub = matches!(
|
||||
here.item_at(at, column).map(|item| &item.kind),
|
||||
Some(Kind::Sub(_))
|
||||
);
|
||||
open.levels.truncate(depth + 1);
|
||||
if open.levels[depth].at != Some(at) {
|
||||
open.levels[depth].at = Some(at);
|
||||
open.levels[depth].row = Bounds::default();
|
||||
let level = &mut open.levels[depth];
|
||||
if level.at != Some(at) {
|
||||
level.row = Bounds::default();
|
||||
}
|
||||
(level.at, level.column) = (Some(at), column);
|
||||
if open_sub && sub {
|
||||
open.levels.push(Level::default());
|
||||
}
|
||||
@@ -153,12 +169,12 @@ pub(super) fn mark(
|
||||
});
|
||||
}
|
||||
|
||||
/// Runs a row. A submenu opens with its first row marked; anything else closes the menu first.
|
||||
/// Runs a spot; a submenu opens, others close first.
|
||||
pub(super) fn choose(
|
||||
state: &Entity<Open>,
|
||||
menu: &Menu,
|
||||
depth: usize,
|
||||
at: usize,
|
||||
(at, column): (usize, usize),
|
||||
close: &Run,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -166,23 +182,22 @@ pub(super) fn choose(
|
||||
let levels = &state.read(cx).levels;
|
||||
let Some(item) = at_depth(menu, levels, depth)
|
||||
.filter(|_| levels.len() > depth)
|
||||
.and_then(|here| here.item_at(at))
|
||||
.and_then(|here| here.item_at(at, column))
|
||||
.cloned()
|
||||
else {
|
||||
log::debug!("menu: a press from an older frame found row {at} of level {depth} gone");
|
||||
log::debug!(
|
||||
"menu: a press from an older frame found row {at}:{column} of level {depth} gone"
|
||||
);
|
||||
return;
|
||||
};
|
||||
if item.disabled {
|
||||
return;
|
||||
}
|
||||
if let Kind::Sub(sub) = &item.kind {
|
||||
let first = sub.step(None, 1);
|
||||
mark(state, menu, depth, at, false, cx);
|
||||
let first = first_of(sub, sub.step(None, 1));
|
||||
mark(state, menu, depth, (at, column), false, cx);
|
||||
state.update(cx, |open, cx| {
|
||||
open.levels.push(Level {
|
||||
at: first,
|
||||
..Level::default()
|
||||
});
|
||||
open.levels.push(first);
|
||||
cx.notify();
|
||||
});
|
||||
return;
|
||||
@@ -194,7 +209,7 @@ pub(super) fn choose(
|
||||
}
|
||||
}
|
||||
|
||||
/// Arrows walk the deepest level; Right opens a submenu; Enter and Space choose; Left and Escape back out.
|
||||
/// Arrows, Enter, Space and Escape drive the deepest level.
|
||||
fn keys(
|
||||
menu: &Menu,
|
||||
state: &Entity<Open>,
|
||||
@@ -210,19 +225,27 @@ fn keys(
|
||||
let Some(here) = at_depth(menu, &levels, depth) else {
|
||||
return false;
|
||||
};
|
||||
let at = levels[depth].at;
|
||||
let (at, column) = (levels[depth].at, levels[depth].column);
|
||||
let sub = at
|
||||
.and_then(|at| here.item_at(at))
|
||||
.and_then(|at| here.item_at(at, column))
|
||||
.is_some_and(|item| matches!(item.kind, Kind::Sub(_)));
|
||||
let across = |by| at.and_then(|at| here.across(at, column, by).map(|to| (at, to)));
|
||||
match (key, at) {
|
||||
("down" | "up", _) => {
|
||||
let by = if key == "down" { 1 } else { -1 };
|
||||
if let Some(to) = here.step(at, by) {
|
||||
mark(state, menu, depth, to, false, cx);
|
||||
let column = here.settled(to, 0).expect("a stepped row runs");
|
||||
mark(state, menu, depth, (to, column), false, cx);
|
||||
}
|
||||
}
|
||||
("right", Some(at)) if sub => choose(state, menu, depth, at, close, window, cx),
|
||||
("enter" | "space", Some(at)) => choose(state, menu, depth, at, close, window, cx),
|
||||
("left" | "right", Some(_)) if across(1).is_some() => {
|
||||
let to = across(if key == "right" { 1 } else { -1 }).expect("a row with a button");
|
||||
mark(state, menu, depth, to, false, cx);
|
||||
}
|
||||
("right", Some(at)) if sub => choose(state, menu, depth, (at, column), close, window, cx),
|
||||
("enter" | "space", Some(at)) => {
|
||||
choose(state, menu, depth, (at, column), close, window, cx)
|
||||
}
|
||||
("left" | "escape", _) if depth > 0 => state.update(cx, |open, cx| {
|
||||
open.levels.pop();
|
||||
cx.notify();
|
||||
|
||||
+61
-7
@@ -82,6 +82,8 @@ impl MenuItem {
|
||||
#[derive(Clone)]
|
||||
pub(super) enum Entry {
|
||||
Item(MenuItem),
|
||||
/// Icon buttons side by side; labels name them.
|
||||
Buttons(Vec<MenuItem>),
|
||||
Separator,
|
||||
Heading(SharedString),
|
||||
}
|
||||
@@ -121,9 +123,41 @@ impl Menu {
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn item_at(&self, ix: usize) -> Option<&MenuItem> {
|
||||
/// A row of icon buttons; labels name them.
|
||||
pub fn buttons(mut self, buttons: impl IntoIterator<Item = MenuItem>) -> Self {
|
||||
let buttons: Vec<MenuItem> = buttons.into_iter().collect();
|
||||
assert!(!buttons.is_empty(), "a button row holds buttons");
|
||||
for button in &buttons {
|
||||
assert!(
|
||||
button.icon.is_some() && matches!(button.kind, Kind::Run),
|
||||
"menu button {:?} needs an icon and runs",
|
||||
button.label
|
||||
);
|
||||
}
|
||||
self.entries.push(Entry::Buttons(buttons));
|
||||
self
|
||||
}
|
||||
|
||||
/// Row `ix`, or button `column` of its button row.
|
||||
pub(super) fn item_at(&self, ix: usize, column: usize) -> Option<&MenuItem> {
|
||||
match self.entries.get(ix) {
|
||||
Some(Entry::Item(item)) => Some(item),
|
||||
Some(Entry::Item(item)) if column == 0 => Some(item),
|
||||
Some(Entry::Buttons(buttons)) => buttons.get(column),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Where row `ix` runs nearest `column`; none if disabled.
|
||||
pub(super) fn settled(&self, ix: usize, column: usize) -> Option<usize> {
|
||||
match self.entries.get(ix) {
|
||||
Some(Entry::Item(item)) if !item.disabled => Some(0),
|
||||
Some(Entry::Buttons(buttons)) => {
|
||||
let near = column.min(buttons.len() - 1);
|
||||
match buttons[near].disabled {
|
||||
false => Some(near),
|
||||
true => buttons.iter().position(|button| !button.disabled),
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -134,7 +168,23 @@ impl Menu {
|
||||
let mut at = from.map_or(if by > 0 { -1 } else { count }, |at| at as isize);
|
||||
for _ in 0..count {
|
||||
at = (at + by).rem_euclid(count);
|
||||
if self.item_at(at as usize).is_some_and(|item| !item.disabled) {
|
||||
if self.settled(at as usize, 0).is_some() {
|
||||
return Some(at as usize);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Row `ix`'s next enabled button after `column`, wrapping.
|
||||
pub(super) fn across(&self, ix: usize, column: usize, by: isize) -> Option<usize> {
|
||||
let Some(Entry::Buttons(buttons)) = self.entries.get(ix) else {
|
||||
return None;
|
||||
};
|
||||
let count = buttons.len() as isize;
|
||||
let mut at = column as isize;
|
||||
for _ in 0..count {
|
||||
at = (at + by).rem_euclid(count);
|
||||
if !buttons[at as usize].disabled {
|
||||
return Some(at as usize);
|
||||
}
|
||||
}
|
||||
@@ -149,15 +199,19 @@ impl Menu {
|
||||
let mut found: Vec<(i32, MenuItem)> = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
Entry::Item(item) => fuzzy(query, &item.label).map(|fit| {
|
||||
.flat_map(|entry| match entry {
|
||||
Entry::Item(item) => std::slice::from_ref(item),
|
||||
Entry::Buttons(buttons) => buttons.as_slice(),
|
||||
_ => &[],
|
||||
})
|
||||
.filter_map(|item| {
|
||||
fuzzy(query, &item.label).map(|fit| {
|
||||
let item = MenuItem {
|
||||
hits: fit.hits,
|
||||
..item.clone()
|
||||
};
|
||||
(fit.score, item)
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
found.sort_by(|(a, _), (b, _)| b.cmp(a));
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
use gpui::{
|
||||
App, Context, FocusHandle, IntoElement, Modifiers, MouseButton, ParentElement, Render,
|
||||
SharedString, Styled, TestAppContext, VisualTestContext, Window, div, point, px,
|
||||
};
|
||||
|
||||
use super::{press, settle};
|
||||
use crate::{
|
||||
menus::{ContextMenu, Menu, MenuItem, SearchableMenu},
|
||||
primitives::{FocusScope, IconName},
|
||||
theme::Theme,
|
||||
};
|
||||
|
||||
/// A context menu headed by clipboard buttons.
|
||||
struct Clip {
|
||||
root: FocusHandle,
|
||||
ran: Vec<SharedString>,
|
||||
}
|
||||
|
||||
impl Clip {
|
||||
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, |clip, _| clip.ran.push(name.into()))
|
||||
}
|
||||
};
|
||||
Menu::new()
|
||||
.buttons([
|
||||
MenuItem::new("Cut")
|
||||
.icon(IconName::Scissors)
|
||||
.on_click(run("cut")),
|
||||
MenuItem::new("Copy")
|
||||
.icon(IconName::Copy)
|
||||
.disabled(true)
|
||||
.on_click(run("copy")),
|
||||
MenuItem::new("Paste")
|
||||
.icon(IconName::Clipboard)
|
||||
.keys("secondary-v")
|
||||
.on_click(run("paste")),
|
||||
])
|
||||
.item(MenuItem::new("Select all").on_click(run("select all")))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Clip {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let menu = self.menu(cx);
|
||||
FocusScope::new(&self.root)
|
||||
.size_full()
|
||||
.child(SearchableMenu::new("find", "Find", menu.clone()))
|
||||
.child(ContextMenu::new("area", menu).child(div().w(px(300.0)).h(px(200.0))))
|
||||
}
|
||||
}
|
||||
|
||||
fn clip(cx: &mut TestAppContext) -> (gpui::Entity<Clip>, &mut VisualTestContext) {
|
||||
cx.update(|cx| {
|
||||
Theme::init(cx);
|
||||
crate::forms::bind_keys(cx);
|
||||
});
|
||||
let (view, cx) = cx.add_window_view(|_, cx| Clip {
|
||||
root: cx.focus_handle(),
|
||||
ran: Vec::new(),
|
||||
});
|
||||
settle(cx);
|
||||
(view, cx)
|
||||
}
|
||||
|
||||
fn ran(view: &gpui::Entity<Clip>, cx: &mut VisualTestContext) -> Vec<SharedString> {
|
||||
view.read_with(cx, |clip, _| clip.ran.clone())
|
||||
}
|
||||
|
||||
fn right_click(cx: &mut VisualTestContext) {
|
||||
let at = point(px(60.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);
|
||||
}
|
||||
|
||||
fn press_button(selector: &'static str, cx: &mut VisualTestContext) {
|
||||
let at = cx.debug_bounds(selector).expect("the button").center();
|
||||
cx.simulate_mouse_move(at, None, Modifiers::none());
|
||||
cx.simulate_click(at, Modifiers::none());
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn arrows_walk_a_button_row_past_its_disabled_button(cx: &mut TestAppContext) {
|
||||
let (view, cx) = clip(cx);
|
||||
right_click(cx);
|
||||
press("down", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["cut"], "down marks the row's first button");
|
||||
right_click(cx);
|
||||
press("down", cx);
|
||||
press("right", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["cut", "paste"], "right skips copy");
|
||||
right_click(cx);
|
||||
press("down", cx);
|
||||
press("left", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["cut", "paste", "paste"], "left wraps");
|
||||
right_click(cx);
|
||||
press("down", cx);
|
||||
press("down", cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(
|
||||
ran(&view, cx).last().map(|name| name.as_ref()),
|
||||
Some("select all")
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_pressed_button_runs_and_a_disabled_one_does_not(cx: &mut TestAppContext) {
|
||||
let (view, cx) = clip(cx);
|
||||
right_click(cx);
|
||||
press_button("menu-button-0-1", cx);
|
||||
assert!(ran(&view, cx).is_empty(), "copy is off");
|
||||
press_button("menu-button-0-2", cx);
|
||||
assert_eq!(ran(&view, cx), ["paste"]);
|
||||
assert!(
|
||||
cx.debug_bounds("menu-button-0-0").is_none(),
|
||||
"the menu closed after paste"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_filter_finds_a_button_by_its_name(cx: &mut TestAppContext) {
|
||||
let (view, cx) = clip(cx);
|
||||
cx.update(|window, cx| window.focus_next(cx));
|
||||
press("enter", cx);
|
||||
cx.simulate_input("pas");
|
||||
settle(cx);
|
||||
press("enter", cx);
|
||||
assert_eq!(ran(&view, cx), ["paste"]);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use gpui::{
|
||||
Window, div, point, prelude::*, px,
|
||||
};
|
||||
|
||||
mod buttons;
|
||||
mod more;
|
||||
|
||||
use super::{ContextMenu, DropdownMenu, Menu, MenuItem, SearchableMenu};
|
||||
|
||||
Reference in new issue
Block a user