From 05bd22b7f0022e24b12355f6cd2bb8d7bafffd93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 28 Sep 2026 20:54:25 -0400 Subject: [PATCH] =?UTF-8?q?Port=20=C2=B7=20Calendar:=20a=20real=20exit=20c?= =?UTF-8?q?lears=20the=20preview=20after=20a=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The calendar's grid clears its preview on leave, and ignores a leave that follows a key. gpui records the exit a key makes before the handler runs, so the guard swallowed it and a real exit afterwards had no change left to report: the stale day stayed the preview. The grid now listens independently of input, while its days keep the default, so a key is no leave and a real exit is. A test leaves the grid after an arrow, a letter and a month key. shaped()'s doc goes back to its one line; AGENTS.md keeps the why. --- AGENTS.md | 2 +- TASKS.md | 1 + src/chat/media.rs | 2 +- src/forms/date/calendar.rs | 7 ++++--- src/forms/tests/dates.rs | 21 +++++++++++++++++++++ 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 916441e..6f8ccbe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,7 +53,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - `hover()` needs an element id. - gpui applies hover and group-hover styles once an element has a hitbox, in prepaint and paint, but text takes its color at layout, so a hover cannot recolor text. What shows on hover rests `invisible()` and turns `visible()` through `group_hover` (`chat::ConversationItem`, `messaging::ChatMessage`). - gpui 0.2.2 panics on a deferred draw inside another. Whatever draws over the page goes through `primitives::raise`: at the top it defers; inside something raised it lays out on its own and draws last there, so a list opened in a dialog lies over the dialog. A raise takes its owner's id and draws under it, so its keyed state and focus hold while other raises come and go. `scripts/check.sh` refuses gpui's `deferred` anywhere else. -- gpui hides hover after a key press until the mouse moves. A hover that holds or shows something (the toast stack, a carousel, tooltips, hover cards, charts, scroll activity) sets `HoverListenerMode::InputModalityIndependent`, so typing under a resting pointer keeps it. A hover preview the keyboard also moves keeps the default, and its leave ignores a key press through `last_input_was_keyboard` (`forms::Calendar`). +- gpui hides hover after a key press until the mouse moves. A hover that holds or shows something (the toast stack, a carousel, tooltips, hover cards, charts, scroll activity) sets `HoverListenerMode::InputModalityIndependent`, so typing under a resting pointer keeps it. A hover preview the keyboard also moves keeps the default on its items; the box that clears it on leave listens independently, so a real exit still reports, and ignores a leave that follows a key through `last_input_was_keyboard` (`forms::Calendar`). - `occlude()` blocks the pointer for everything painted before it, its own ancestors too. Put it on the outermost box that should stop the pointer, as the toast stack does, or the ancestors' hover never fires. - A drag that pans, turns or slides goes through gpui's drag, whose moves reach the owner in or out of its box; `on_mouse_move` stops at the box's edge. gpui sends a drag's moves to every listener of its type, so each payload carries its owner's `EntityId` and handlers check it. - gpui 0.2.2 paints pictures without a transform, so a picture turns by its pixels: `media::turn` turns decoded frames by quarter turns off the main thread, an asset keyed by the picture and the turn. An svg turns to any angle through its `Transformation`, so a control that turns shows one (`interaction::Rotatable`). diff --git a/TASKS.md b/TASKS.md index f255a01..139af78 100644 --- a/TASKS.md +++ b/TASKS.md @@ -476,3 +476,4 @@ Per-component lines live in `tasks/`. Tags there: | T44a | 1 | FAIL | twelve roots in nine components still said w_full: a root held in a variable, a match arm or an animation passed the sweep | | T44a | 2 | FAIL | roots built in render(mut self), returned by shared helpers or passed through them still said w_full: fields, charts, state views, Steps, previews; five measured at content width in a padded card | | T44a | 3 | PASS | the brief counted 43 components in the new tests; they hold 41 | +| T44b | 1 | FAIL | after a key, leaving the calendar grid kept a stale preview, since the guard swallowed the exit gpui records for the key; the shaped() doc grew to 40 words | diff --git a/src/chat/media.rs b/src/chat/media.rs index 2c30b61..e8c032a 100644 --- a/src/chat/media.rs +++ b/src/chat/media.rs @@ -15,7 +15,7 @@ use crate::{ typography::{Ellipsis, format::file_size}, }; -/// A box in a picture's own shape: as wide as the picture, but no wider than `widest` or its container. Hand it to a flex column inside a block: taffy 0.13 sizes the column by its width before its cap. +/// A box in a picture's own shape: as wide as the picture, but no wider than `widest` or its container. pub(crate) fn shaped(width: f32, height: f32, widest: Pixels) -> Div { assert!( width > 0.0 && height > 0.0, diff --git a/src/forms/date/calendar.rs b/src/forms/date/calendar.rs index 7e4f4b9..242eee8 100644 --- a/src/forms/date/calendar.rs +++ b/src/forms/date/calendar.rs @@ -1,9 +1,9 @@ use std::rc::Rc; use gpui::{ - Animation, AnimationExt, App, ElementId, FocusHandle, FontWeight, InteractiveElement, - IntoElement, ParentElement, RenderOnce, StatefulInteractiveElement, Styled, Window, div, - prelude::*, transparent_black, + Animation, AnimationExt, App, ElementId, FocusHandle, FontWeight, HoverListenerMode, + InteractiveElement, IntoElement, ParentElement, RenderOnce, StatefulInteractiveElement, Styled, + Window, div, prelude::*, transparent_black, }; use jiff::{ToSpan, civil::Date}; @@ -328,6 +328,7 @@ impl RenderOnce for Calendar { .grid() .grid_cols(7) .gap_0p5() + .hover_listener_mode(HoverListenerMode::InputModalityIndependent) .on_hover(move |over, window, cx| { if !*over && !window.last_input_was_keyboard() { leave.update(cx, |hover, cx| { diff --git a/src/forms/tests/dates.rs b/src/forms/tests/dates.rs index c69d715..8add533 100644 --- a/src/forms/tests/dates.rs +++ b/src/forms/tests/dates.rs @@ -236,3 +236,24 @@ fn keys_keep_the_preview_under_a_resting_pointer(cx: &mut TestAppContext) { "the keyboard's day stays the preview" ); } + +#[gpui::test] +fn leaving_the_grid_after_a_key_clears_the_preview(cx: &mut TestAppContext) { + setup(cx); + for key in ["right", "a", "pagedown"] { + let (view, cx) = cx.add_window_view(|_, _| Watched { seen: Vec::new() }); + cx.update(|window, cx| window.focus_next(cx)); + cx.simulate_mouse_move(point(px(120.0), px(200.0)), None, Modifiers::none()); + cx.simulate_keystrokes(key); + cx.update(|window, _| window.refresh()); + cx.run_until_parked(); + cx.simulate_mouse_move(point(px(700.0), px(480.0)), None, Modifiers::none()); + cx.update(|window, _| window.refresh()); + cx.run_until_parked(); + assert_eq!( + view.read_with(cx, |view, _| view.seen.last().copied().flatten()), + None, + "after {key}, leaving the grid clears its preview" + ); + } +}