From d2379bc9119f62db6c91536da823737b6f17301e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 26 Sep 2026 23:45:22 -0400 Subject: [PATCH] =?UTF-8?q?Mail=20=C2=B7=20a=20focused=20star=20keeps=20Sp?= =?UTF-8?q?ace=20and=20Enter;=20so=20does=20Unpin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A button in a SelectableList row now stops Space and Enter at its box on key down, so the list no longer toggles its selection before the button acts. The mail star and the pinned messages' Unpin, which had the same fault, both do. --- AGENTS.md | 2 +- src/mail/list.rs | 8 ++++++++ src/mail/tests/mod.rs | 10 ++++++++++ src/messaging/pins.rs | 8 ++++++++ src/messaging/tests/mod.rs | 11 +++++++++++ 5 files changed, 38 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index e39f9a8..d1eb36e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,7 +73,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - gpui focuses the nearest focusable ancestor on mouse down, and apps wrap everything in a `FocusScope`. A press that must leave focus alone calls `prevent_default`, as buttons and context regions do. - gpui runs the click of every pressed element under the pointer, a row's with its button's. A pressable row that holds buttons stops their press at their box on mouse down (`files::transfer`), as `overlays::media_button` stops its own. - gpui draws a window's first frame inside `open_window`. State the first frame reads, such as `cx.set_menus`, is set before it opens. -- gpui clicks a focused element when Enter or Space is released. An overlay that hands focus back picks on release too; a pick on press returns focus first, and the release clicks the opener again. An edit in place that hands focus back keeps its text on Enter's release (`forms::InlineEdit`). The press itself bubbles, so a container that takes Space leaves a focused button its own: `overlays::media_button` stops it. +- gpui clicks a focused element when Enter or Space is released. An overlay that hands focus back picks on release too; a pick on press returns focus first, and the release clicks the opener again. An edit in place that hands focus back keeps its text on Enter's release (`forms::InlineEdit`). The press itself bubbles, so a container that takes Space leaves a focused button its own: `overlays::media_button` stops it, and a button in a `SelectableList` row stops Space and Enter at its box (`mail::MailItem`'s star, `messaging::PinnedMessages`' Unpin). - A focus handle a component hands back lives across all its modes: take it with `tab_stop` before any early return, or keyed state drops it while the other mode draws and the handed-back focus lands nowhere. - A dialog goes through `overlays::Dialog`: scrim, focus trap, Escape and focus return live there. Its close runs on every way out, so a dialog with two answers marks the chosen one before it closes (`messaging::IncomingCallDialog`). AlertDialog, ConfirmDialog and PromptDialog build on it; a fullscreen dialog fills the window below its title bar. A dialog's card fits the window, a title bar's height clear at each end; its body scrolls and its actions stay. - Controls on a dark layer over pictures are `overlays::media_button`, light icons that dim when they have nowhere to go, for the lightbox and a video player's bar alike. diff --git a/src/mail/list.rs b/src/mail/list.rs index 2513274..386bf65 100644 --- a/src/mail/list.rs +++ b/src/mail/list.rs @@ -166,6 +166,14 @@ impl MailItem { window.prevent_default(); cx.stop_propagation(); }) + .on_key_down(|event, _, cx| { + let key = event.keystroke.key.as_str(); + if matches!(key, "space" | "enter") + && !event.keystroke.modifiers.modified() + { + cx.stop_propagation(); + } + }) .on_click(move |_, window, cx| { log::info!("mail {key}: star {}", !starred); set(!starred, window, cx) diff --git a/src/mail/tests/mod.rs b/src/mail/tests/mod.rs index 91c0626..8a29980 100644 --- a/src/mail/tests/mod.rs +++ b/src/mail/tests/mod.rs @@ -187,6 +187,16 @@ fn a_star_toggles_and_leaves_the_selection(cx: &mut TestAppContext) { assert_eq!(heard(&host, cx), ["star m3 false"]); } +#[gpui::test] +fn space_on_a_star_stars_and_leaves_the_selection(cx: &mut TestAppContext) { + let (host, cx) = mailing(mails, cx); + tab_to(2, cx); + press("space", cx); + tab_to(2, cx); + press("enter", cx); + assert_eq!(heard(&host, cx), ["star m1 true", "star m1 true"]); +} + #[gpui::test] fn no_mail_says_so_and_takes_no_tab_stop(cx: &mut TestAppContext) { let (_, cx) = mailing( diff --git a/src/messaging/pins.rs b/src/messaging/pins.rs index db94a4d..e5ce17d 100644 --- a/src/messaging/pins.rs +++ b/src/messaging/pins.rs @@ -90,6 +90,14 @@ impl RenderOnce for PinnedMessages { div() .debug_selector(move || format!("unpin {named}")) .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_key_down(|event, _, cx| { + let key = event.keystroke.key.as_str(); + if matches!(key, "space" | "enter") + && !event.keystroke.modifiers.modified() + { + cx.stop_propagation(); + } + }) .child( IconButton::new( (self.id.clone(), format!("unpin-{key}")), diff --git a/src/messaging/tests/mod.rs b/src/messaging/tests/mod.rs index a639867..88cb1aa 100644 --- a/src/messaging/tests/mod.rs +++ b/src/messaging/tests/mod.rs @@ -221,6 +221,17 @@ fn a_press_on_unpin_unpins_and_opens_nothing(cx: &mut TestAppContext) { ); } +#[gpui::test] +fn space_on_unpin_unpins_and_opens_nothing(cx: &mut TestAppContext) { + let (host, cx) = pinning(cx); + tab_to(2, cx); + press("space", cx); + assert_eq!( + host.read_with(cx, |host, _| host.asked.clone()), + ["unpin newer"] + ); +} + #[test] #[should_panic(expected = "pin a twice")] fn a_message_is_pinned_once() {