diff --git a/ROADMAP.md b/ROADMAP.md index b1283e775..cc88faa6b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -172,6 +172,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md ## Progress log +- **2026-10-20 (M3, #3535):** Escape with a menu open closes the menu and keeps the text selection; the next Escape clears it, as before. One keypress used to do both. A synthetic UI regression checks it. Estimates unchanged. + - **2026-10-19 (M14, #3373):** The main menu and its long submenus scroll in short windows, keeping their commands reachable. Estimates unchanged. - **2026-10-19 (M3, #3387):** Save-prompt shortcuts yield to later consent and utility dialogs; a synthetic UI regression checks that an edited document stays open. Estimates unchanged. diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index eb9244a28..d83708aa9 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2285,7 +2285,8 @@ pub fn shortcuts(view: &mut DocView, ctx: &egui::Context, speed: crate::scroll_s view.view_history(true); } let key = |k| ctx.input(|i| i.key_pressed(k)); - if key(Key::Escape) { + // An open menu takes Escape first and closes; the selection stays for the next one (#3535). + if key(Key::Escape) && !egui::Popup::is_any_open(ctx) { if view.camera.is_some() || view.camera_done.is_some() { view.pending_action = Some(ViewAction::CancelCamera); } else if view.selection.is_some() { diff --git a/crates/ui-egui/tests/ui_group/text_select.rs b/crates/ui-egui/tests/ui_group/text_select.rs index 9ed4e9d5d..d3d40da8d 100644 --- a/crates/ui-egui/tests/ui_group/text_select.rs +++ b/crates/ui-egui/tests/ui_group/text_select.rs @@ -226,6 +226,27 @@ fn selected_text_copy_edit_and_context_menus() { assert_eq!(h.state().views[0].selected_text().as_deref(), Some("quick")); } +#[test] +fn escape_closes_an_open_menu_before_it_clears_the_selection() { + // #3535: one Escape closed the main menu and dropped the selection with it. + use egui_kittest::kittest::Queryable; + let (mut h, at) = harness(|_| {}); + click(&mut h, at); + click(&mut h, at); + h.run_steps(2); + assert_eq!(h.state().views[0].selected_text().as_deref(), Some("quick")); + h.get_by_label("Menu").click(); + h.run_steps(2); + assert!(egui::Popup::is_any_open(&h.ctx), "the menu is open"); + h.key_press(egui::Key::Escape); + h.run_steps(2); + assert!(!egui::Popup::is_any_open(&h.ctx), "Escape closes the menu"); + assert_eq!(h.state().views[0].selected_text().as_deref(), Some("quick"), "and keeps the selection"); + h.key_press(egui::Key::Escape); + h.run_steps(2); + assert_eq!(h.state().views[0].selected_text(), None, "a second Escape clears it"); +} + #[test] fn selected_text_copy_command_requires_selection() { let (mut h, at) = harness(|_| {});