diff --git a/src/menus/menu.rs b/src/menus/menu.rs index 10bbcf0..29d01db 100644 --- a/src/menus/menu.rs +++ b/src/menus/menu.rs @@ -2,7 +2,8 @@ use std::rc::Rc; use gpui::{ Animation, AnimationExt, AnyElement, App, Bounds, ElementId, Entity, InteractiveElement, - IntoElement, ParentElement, Pixels, Point, Styled, Window, anchored, canvas, div, prelude::*, + IntoElement, ParentElement, Pixels, Point, SharedString, Styled, Window, anchored, canvas, div, + prelude::*, }; use super::{ @@ -34,8 +35,12 @@ pub(super) fn at_depth<'a>(menu: &'a Menu, levels: &[Level], depth: usize) -> Op fn settle(menu: &Menu, levels: &mut Vec) { let mut here = menu; for depth in 0..levels.len() { - let column = levels[depth].column; - let at = levels[depth].at.filter(|&ix| here.runs(ix, column)); + let (column, name) = (levels[depth].column, levels[depth].name.clone()); + // A mark holds its item, not just its place. + let at = levels[depth].at.filter(|&ix| { + here.item_at(ix, column) + .is_some_and(|item| !item.disabled && item.label == name) + }); levels[depth].at = at; match at .and_then(|ix| here.item_at(ix, column)) @@ -50,11 +55,12 @@ fn settle(menu: &Menu, levels: &mut Vec) { } } -/// An open level: marked row, button, and their boxes. -#[derive(Clone, Copy, Debug, Default, PartialEq)] +/// An open level: marked row, button, its label, and boxes. +#[derive(Clone, Debug, Default, PartialEq)] pub(super) struct Level { pub(super) at: Option, pub(super) column: usize, + pub(super) name: SharedString, pub(super) row: Bounds, pub(super) panel: Bounds, } @@ -126,12 +132,18 @@ impl Open { /// A level marking row `at` at its first enabled button. fn first_of(menu: &Menu, at: Option) -> Level { + let Some(ix) = at else { + return Level::default(); + }; + let column = menu.first(ix).expect("a stepped row runs"); Level { at, - column: match at { - Some(ix) => menu.first(ix).expect("a stepped row runs"), - None => 0, - }, + column, + name: menu + .item_at(ix, column) + .expect("its first spot") + .label + .clone(), ..Level::default() } } @@ -146,19 +158,21 @@ pub(super) fn mark( cx: &mut App, ) { state.update(cx, |open, cx| { - let Some(here) = at_depth(menu, &open.levels, depth) else { + if open.levels.len() <= depth { + return log::debug!("menu: level {depth} closed before its mark"); + } + let Some(item) = + at_depth(menu, &open.levels, depth).and_then(|here| here.item_at(at, column)) + else { return; }; - let sub = matches!( - here.item_at(at, column).map(|item| &item.kind), - Some(Kind::Sub(_)) - ); + let (sub, name) = (matches!(item.kind, Kind::Sub(_)), item.label.clone()); open.levels.truncate(depth + 1); let level = &mut open.levels[depth]; if level.at != Some(at) { level.row = Bounds::default(); } - (level.at, level.column) = (Some(at), column); + (level.at, level.column, level.name) = (Some(at), column, name); if open_sub && sub { open.levels.push(Level::default()); } diff --git a/src/menus/model.rs b/src/menus/model.rs index aa8cd2e..75216e1 100644 --- a/src/menus/model.rs +++ b/src/menus/model.rs @@ -147,11 +147,6 @@ impl Menu { } } - /// Whether the spot at row `ix`, button `column`, runs. - pub(super) fn runs(&self, ix: usize, column: usize) -> bool { - self.item_at(ix, column).is_some_and(|item| !item.disabled) - } - /// Row `ix`'s first spot that runs. pub(super) fn first(&self, ix: usize) -> Option { match self.entries.get(ix) { diff --git a/src/menus/tests/buttons.rs b/src/menus/tests/buttons.rs index f41cf09..4e1bf5d 100644 --- a/src/menus/tests/buttons.rs +++ b/src/menus/tests/buttons.rs @@ -15,7 +15,7 @@ struct Clip { root: FocusHandle, ran: Vec, paste_off: bool, - shown: usize, + gone: Option<&'static str>, } impl Clip { @@ -41,9 +41,12 @@ impl Clip { .keys("secondary-v") .disabled(self.paste_off) .on_click(run("paste")), + MenuItem::new("Delete") + .icon(IconName::Trash2) + .on_click(run("delete")), ] .into_iter() - .take(self.shown), + .filter(|item| Some(item.label.as_ref()) != self.gone), ) .item(MenuItem::new("Select all").on_click(run("select all"))) } @@ -68,7 +71,7 @@ fn clip(cx: &mut TestAppContext) -> (gpui::Entity, &mut VisualTestContext) root: cx.focus_handle(), ran: Vec::new(), paste_off: false, - shown: 3, + gone: None, }); settle(cx); (view, cx) @@ -109,7 +112,7 @@ fn arrows_walk_a_button_row_past_its_disabled_button(cx: &mut TestAppContext) { press("down", cx); press("left", cx); press("enter", cx); - assert_eq!(ran(&view, cx), ["cut", "paste", "paste"], "left wraps"); + assert_eq!(ran(&view, cx), ["cut", "paste", "delete"], "left wraps"); right_click(cx); press("down", cx); press("down", cx); @@ -134,6 +137,32 @@ fn a_pressed_button_runs_and_a_disabled_one_does_not(cx: &mut TestAppContext) { ); } +#[gpui::test] +fn a_move_after_a_button_closed_the_menu_does_nothing(cx: &mut TestAppContext) { + let (view, cx) = clip(cx); + right_click(cx); + let at = cx.debug_bounds("menu-button-0-2").expect("paste").center(); + // Both events land before any repaint. + cx.update(|window, cx| { + let down = gpui::MouseDownEvent { + position: at, + button: MouseButton::Left, + modifiers: Modifiers::none(), + click_count: 1, + first_mouse: false, + }; + let moved = gpui::MouseMoveEvent { + position: at + point(px(1.0), px(0.0)), + pressed_button: Some(MouseButton::Left), + modifiers: Modifiers::none(), + }; + window.dispatch_event(gpui::PlatformInput::MouseDown(down), cx); + window.dispatch_event(gpui::PlatformInput::MouseMove(moved), cx); + }); + settle(cx); + assert_eq!(ran(&view, cx), ["paste"]); +} + #[gpui::test] fn the_filter_finds_a_button_by_its_name(cx: &mut TestAppContext) { let (view, cx) = clip(cx); @@ -159,7 +188,7 @@ fn arrows_cross_buttons_while_the_filter_holds_focus(cx: &mut TestAppContext) { fn a_marked_button_turned_off_or_gone_runs_nothing(cx: &mut TestAppContext) { for change in [ |clip: &mut Clip| clip.paste_off = true, - |clip: &mut Clip| clip.shown = 1, + |clip: &mut Clip| clip.gone = Some("Paste"), ] { let (view, cx) = clip(cx); right_click(cx);