diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 9e57092d..ca46b4c8 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -702,6 +702,7 @@ pub fn open_context_menu(app: &mut FigApp, pos: Pos2, deep: bool) { } } app.ui.context_menu = Some((pos, app.session.selection())); + app.ui.context_menu_shown = false; } fn stroke(c: Color32, w: f32) -> Stroke { diff --git a/crates/ui-egui/src/guides.rs b/crates/ui-egui/src/guides.rs index 69ab8ee8..befc9b53 100644 --- a/crates/ui-egui/src/guides.rs +++ b/crates/ui-egui/src/guides.rs @@ -268,6 +268,7 @@ pub fn input(app: &mut FigApp, ctx: &egui::Context, pos: Pos2) -> bool { app.run("guide.select", owned(g.owner, page, json!({"index": g.index}))); if secondary { app.ui.context_menu = Some((pos, Vec::new())); + app.ui.context_menu_shown = false; } else { ctx.data_mut(|d| { d.insert_temp(drag_id(), GuideDrag::Move { owner: g.owner, index: g.index, vertical: g.vertical, copy: alt, frame }) diff --git a/crates/ui-egui/src/layers.rs b/crates/ui-egui/src/layers.rs index ffb74aef..c84db661 100644 --- a/crates/ui-egui/src/layers.rs +++ b/crates/ui-egui/src/layers.rs @@ -991,6 +991,7 @@ fn layers(app: &mut FigApp, ui: &mut Ui) { } let pos = ui.input(|i| i.pointer.hover_pos()).unwrap_or(r.center()); app.ui.context_menu = Some((pos, vec![row.id])); + app.ui.context_menu_shown = false; } // Drag to reorder. if resp.drag_started() && !crate::menus::dev_mode_refuses(app, "node.reparent") { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index da40bf0c..21c47a95 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -528,6 +528,8 @@ pub struct UiState { pub palette_hi: usize, /// The palette ignores one outside click after opening (the click that opened it). pub palette_armed: bool, + /// The context menu has been drawn at least once (its opening frame's press is spared). + pub context_menu_shown: bool, /// This frame's Actions toolbar button rect: presses on it toggle, never dismiss. pub actions_rect: Option, /// The Variables editor, when open. @@ -707,6 +709,7 @@ impl Default for UiState { palette_tab: menus::PaletteTab::All, palette_hi: 0, palette_armed: false, + context_menu_shown: false, actions_rect: None, variables: None, about: false, diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index c8d0159b..89b6cd60 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -978,11 +978,15 @@ pub fn popups(app: &mut FigApp, ctx: &egui::Context) { } }); }); - let clicked_outside = ctx.input(|i| i.pointer.any_pressed()) && !area.response.hovered() && !over_menu(ctx); + // The press of a tap that opened the menu (press and release in one frame) is spared. + let fresh = !std::mem::replace(&mut app.ui.context_menu_shown, true); + let clicked_outside = !fresh && ctx.input(|i| i.pointer.any_pressed()) && !area.response.hovered() && !over_menu(ctx); if close || clicked_outside || ctx.input(|i| i.key_pressed(egui::Key::Escape)) { app.ui.context_menu = None; forget_tree(ctx, ctx_menu_id()); } + } else { + app.ui.context_menu_shown = false; } crate::colorpicker::show(app, ctx); palette(app, ctx); diff --git a/crates/ui-egui/src/palette_tests.rs b/crates/ui-egui/src/palette_tests.rs index cfe9c1cd..b9827867 100644 --- a/crates/ui-egui/src/palette_tests.rs +++ b/crates/ui-egui/src/palette_tests.rs @@ -161,3 +161,26 @@ fn arrows_move_the_highlight_and_enter_runs_it() { assert!(u.app.ui.palette.is_none(), "Enter runs a row and closes the palette"); assert_eq!(u.app.session.prefs.recent_actions.first(), Some(&want), "the highlighted row ran"); } + +/// A trackpad tap sends the press and the release in one frame; the press must not close a +/// context menu the release just opened, but a later outside press still does. +#[test] +fn context_menu_survives_a_same_frame_tap() { + let mut u = Ui::new(Session::new()); + let p = Pos2::new(1000.0, 600.0); + let b = |pressed| Event::PointerButton { pos: p, button: egui::PointerButton::Secondary, pressed, modifiers: Default::default() }; + u.frame(vec![Event::PointerMoved(p), b(true), b(false)]); + assert!(u.app.ui.context_menu.is_some(), "the opening tap's press is spared"); + u.frame(vec![]); + assert!(u.app.ui.context_menu.is_some(), "the menu stays open"); + // A second tap elsewhere while the menu is open replaces it and survives too. + let q = Pos2::new(600.0, 400.0); + let b2 = |pressed| Event::PointerButton { pos: q, button: egui::PointerButton::Secondary, pressed, modifiers: Default::default() }; + u.frame(vec![Event::PointerMoved(q), b2(true), b2(false)]); + u.frame(vec![]); + assert!(u.app.ui.context_menu.is_some(), "a repeat tap keeps the replaced menu open"); + let far = Pos2::new(100.0, 100.0); + u.button(far, true); + u.button(far, false); + assert!(u.app.ui.context_menu.is_none(), "a later outside press closes the menu"); +}