feat(client): hover cross-fade for the tab bar and window buttons (UI/UX v3 P3b2b) - #82
Merged
Conversation
Records two traps found by measuring, neither of which is in the design: the tab bar's is_hovered also gates whether the tear-out and close buttons are drawn (a behavioural use that must keep the boolean, since a button drawn at weight 0.05 is still clickable), and hovered_window_button has a second write site in the Windows snap-layout event handler. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
a_tab_transition_that_is_never_retargeted_stays_quiet could not fail: it only exercised HoverTransition::default(), already covered by a_fully_idle_state_wants_no_animation_frames, and never called retarget, so it did not exercise the config gate in mouse.rs even by proxy. A test that cannot fail but carries the name of a requirement is worse than no test - it creates the appearance of coverage. The tab_bar.hover_highlight gate is verified by code inspection only. Covering it properly needs an EventHandler test harness this crate does not have, which is out of scope for P3b2b.
…n::target target() has no production caller across all four hover models, so gate it behind #[cfg(test)] instead of #[allow(dead_code)] -- following AnimationManager::tick_by_dt's precedent -- rather than deleting it and losing the retarget-moved-the-target assertion the weight checks alone cannot express. Also closes out P3b2 in the plan doc and adds the hover-model rule to nexterm-client-gpu/CLAUDE.md.
Three accuracy fixes to the phase-tracking steering document: - Add the missing P3b1 entry (shipped via #79, squash 5d6e167, merged to master): the shared SurfaceMotion timer pair and open/close motion for eleven overlay surfaces, including the redacted password-modal ghost. - Fix P3b2, which claimed "shipped via #80" while #80 is still open. A checklist that overstates what is on master is worse than one that lags behind -- it's exactly the failure mode the CONFIGURATION.md reconciliation (#73) exists to prevent. P3b2a is pending review in #80, P3b2b in #81; the item is unchecked since neither half has merged. - Replace the dangling "this branch" reference with #81, matching every other entry's PR-number provenance convention. No code changes.
7 tasks
Coverage reportGenerated by |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Completes P3b2 by giving the last two of this client's four pointer-hover models a cross-fade. Stacked on #80 (P3b2a), which did the settings widget rows and the context menu.
inactive_bgand a brightened version of it; it now lerps over 100 ms, still gated on the existingtab_bar.hover_highlightkey.text_secondaryandtext_primary.No new primitive:
HoverTransition<Id>andcolor_util::lerp_rgbacome from #80 unchanged.Two traps, both worth a reviewer's eye
Neither is visible in the diff's shape, and both were found by measuring rather than by reading the design.
1. The tab bar's hover flag has a non-colour reader.
let is_hovered = state.hovered_tab_id == Some(pane_id);is read twice — once to pick the background colour, and once to gate whether the tear-out[↗]and close[×]buttons are drawn at all. The second is behavioural: those are click targets hit-tested inmouse.rs, so a button drawn at weight 0.05 is one the user can hit while it is vanishing, and one drawn during a fade-out lingers after the pointer has left. That reader keeps the boolean; only the colour consults the weight.2.
hovered_window_buttonhas two write sites. The pointer-motion handler, andUserEvent::SnapMaximizeHover— the Windows snap-layout path, where the OS reports hover on the maximize button. Retargeting only the first leaves that path snapping, and nothing fails to compile. This is the same class of defect #79's whole-branch review caught in an AccessKit path, so both sites are retargeted and a grep confirmed there is no third.Both interpolation kinds, in one builder
P3b2a established that hover needs a scalar consumed at colour-choice time rather than a post-pass, and that the kind of interpolation follows the shape of the hovered appearance. This branch needs both kinds in the same function:
lerp_rgba— the colour moveslerp_rgbaAlpha-scaling the tab background would fade the tab out of the bar instead of into its tint. Lerping the additive fill would assume what sits behind the button and keep painting at weight 0. The Close button's hovered fill is
semantic_error— an unrelated hue rather than a brightening — so its alpha is carried through untouched by the fade.Also in here
HoverTransition::target()is now#[cfg(test)]rather than carrying a permanent#[allow(dead_code)]. It has no production caller but three real test assertions that pin something the weight checks do not — thatretargetactually moved the target.AnimationManager::tick_by_dtin the same module is the precedent.nexterm-client-gpu/CLAUDE.mdgains an "Adding a hover model" rule: own transition, retarget from every handler that writes the logical id, add the aggregate clause, pick the interpolation by shape rather than habit, and check whether the hover flag has a behavioural reader as well as a colour one.A spec requirement this does NOT meet, stated plainly
The design's Testing section asks that
hover_highlight = falsestarting no transition be tested. A test was written for it and then deleted, because it could not fail — it assertedHoverTransition::default()behaviour on a fresh state and never exercised the gate's branch. A test that cannot fail but carries a requirement's name is worse than an acknowledged gap: the next person to touch the gate would trust it.So the gate is verified by code inspection only. Covering it properly needs an
EventHandlertest harness this crate does not have, which is out of scope here and deserves its own decision. The reason is recorded in the commit body of the deletion and in the plan document's verification backlog, not just in a PR description.Test plan
cargo test --workspace— 0 failures (nexterm-client-gpu: 1013 passed)cargo clippy --workspace --all-targets -- -D warnings— cleancargo fmt --check— cleana_fully_idle_state_wants_no_animation_framesstill green — the phase has now added four clauses tohas_active_animation, four more ways for it to regress.scaled_duration_msreturns 0 when animations are off, so both new models snap exactly as they did before.Cargo.lock,nexterm-i18n/locales/,docs/CONFIGURATION.mdorpkg/flatpak/change, so no flatpak sources regeneration and no key-parity risk.Not verified here: no motion in P3b1 or P3b2 has been seen on hardware. Two questions only a display can settle, both now in the plan's backlog — whether the tab bar's
+0.06/+0.06/+0.08brightening is perceptible as a fade at all, and whether the Close button fading tosemantic_errorrather than snapping to it weakens the "this is destructive" signal. That second one is the only place in P3b2 where the hovered appearance is a warning rather than an affordance.Design:
docs/superpowers/specs/2026-08-29-p3b2-hover-crossfade-design.mdPlan:
docs/superpowers/plans/2026-08-29-p3b2b-titlebar-hover.md🤖 Generated with Claude Code