From 5d2b0bc06387b4793e88bbcc9445f5b419be1498 Mon Sep 17 00:00:00 2001 From: Eric Kaden <608341+EricKaden@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:22:21 -0400 Subject: [PATCH] M6: Escape in an open drop-down closes only its list, and a note on the list's fixed colors --- crates/ui-egui/src/canvas.rs | 13 ++++++++----- crates/ui-egui/src/forms_ui.rs | 2 ++ crates/ui-egui/tests/forms.rs | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 4d534380a5..3c485620df 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2519,8 +2519,13 @@ pub fn shortcuts(view: &mut DocView, ctx: &egui::Context, speed: crate::scroll_s view.view_history(true); } let key = |k| (arrows || !matches!(k, Key::ArrowLeft | Key::ArrowRight | Key::ArrowUp | Key::ArrowDown)) && ctx.input(|i| i.key_pressed(k)); + // An open form field (a drop-down's or a list box's items) takes the arrows, Page Up / Page + // Down, Home, End and Escape. A focused check box or radio button uses none of them, so the + // page keys still work there. + let field_open = view.forms.focus.is_some() && form_box.is_none(); // 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) { + // So does an open field, whose list closes. + if key(Key::Escape) && !egui::Popup::is_any_open(ctx) && !field_open { if view.camera.is_some() || view.camera_done.is_some() { view.pending_action = Some(ViewAction::CancelCamera); } else if view.selection.is_some() { @@ -2529,10 +2534,8 @@ pub fn shortcuts(view: &mut DocView, ctx: &egui::Context, speed: crate::scroll_s view.find = None; } } - // An open form field takes the arrows, Page Up / Page Down, Home and End (a drop-down's or a - // list box's items), so the page under it stays put. A focused check box or radio button - // uses none of them, so the page keys still work there. - if view.forms.focus.is_some() && form_box.is_none() { + // The open field's keys leave the page under it where it is. + if field_open { return; } if key(Key::Home) { diff --git a/crates/ui-egui/src/forms_ui.rs b/crates/ui-egui/src/forms_ui.rs index cc5e56886d..8dda436955 100644 --- a/crates/ui-egui/src/forms_ui.rs +++ b/crates/ui-egui/src/forms_ui.rs @@ -899,6 +899,8 @@ fn choice_list( let resp = egui::Area::new(egui::Id::new(("form-choices", view))).order(egui::Order::Foreground).fixed_pos(rect.left_bottom()).show(ctx, |ui| { let (outer, area) = ui.allocate_exact_size(vec2(width, shown as f32 * row + 2.0), egui::Sense::hover()); let p = ui.painter(); + // The list is drawn as the form's own, a white list in the field's font with a blue + // highlight, in light and dark themes alike, so its colors are fixed, not the app's tokens. p.rect_filled(outer, CornerRadius::ZERO, Color32::WHITE); // The wheel scrolls the list, a row at a time, and not the page under it. if area.contains_pointer() && n > shown { diff --git a/crates/ui-egui/tests/forms.rs b/crates/ui-egui/tests/forms.rs index 2a5cf8c24b..3871c46e05 100644 --- a/crates/ui-egui/tests/forms.rs +++ b/crates/ui-egui/tests/forms.rs @@ -1525,6 +1525,24 @@ fn the_mouse_wheel_scrolls_a_long_drop_down_list_and_not_the_page() { assert_eq!(h.state().views[0].page_screen_rect(0), page, "the page stayed where it was"); } +/// Escape in an open drop-down closes its list and nothing else: the find bar, which Escape +/// closes when no field is open, stays open. +#[test] +fn escape_closes_an_open_drop_downs_list_and_leaves_the_page_alone() { + let mut h = harness(); + long_country(&mut h); + h.state_mut().views[0].open_find(); + h.run_steps(3); + assert!(h.state().views[0].find.is_some()); + click_field(&mut h, "country", 0); + assert!(egui_kittest::kittest::Queryable::query_by_label(&h, "0000").is_some(), "the list is open"); + h.key_press(egui::Key::Escape); + h.run_steps(3); + assert!(egui_kittest::kittest::Queryable::query_by_label(&h, "0000").is_none(), "Escape closed the list"); + assert!(h.state().views[0].forms.focus.is_none(), "and the field"); + assert!(h.state().views[0].find.is_some(), "the find bar stays open"); +} + /// An auto-sized drop-down's list draws its items at the size the field shows its value at, so a /// long chosen item fits the list as it fits the field instead of growing past it. #[test]