feat(client): press pulse for the tab bar, window buttons, settings rows and context menu (UI/UX v3 P3b3) - #83
Merged
Merged
Conversation
押下即発火という既存のクリック確定タイミングを前提に、保持型の pressed 状態ではなく一発再生の press pulse を設計した。共有型 PressPulse<Id>、hover の後段での明度合成、4 モデルへの配線点、 コンテキストメニューの新規 press 分岐とその順序上の罠を記録する。
6 タスク(共有型 → 合成ヘルパ → 4 モデルの配線)に分割した。 spec には計画中に実コードを読んで判明した 2 点を反映済み: 描画サイトのうち 3 つは加算レイヤで base fill を持たないこと、 コントラストは既存欠陥を踏むため相対比較で固定すること。
押下即発火という既存のクリック確定タイミングに合わせ、保持型ではなく 一発減衰の press 表現を共有型として追加する。HoverTransition と違い 二段タイマーは不要で、押下ごとに作り直す(非冪等)。
押下時の見た目を1箇所に集約する。明度を下げるだけでは暗いスキームで 知覚できないため、加算レイヤのalphaも同時に強める。9スキーム全部で hoverとpressが区別できることをテストで固定する。
実装中の計測で 1.7 では 9 スキーム中 8 つで press が知覚できないことが 判明したため 2.3 へ引き上げた。コントラストのゲートも、暗いスキームでは hover レイヤが行を明るくするという実測に合わせて絶対値 4.5:1 との 二者択一に改めた。
タブのクリックは押下で確定するため、押下時点で pulse を発火し、 描画側は hover と press の大きい方で fill を作ってから press_fill に通す。
Minimize と Close は押下直後にウィンドウが消えるため実際には見えないが、 3 つとも配線して press 連鎖に例外を作らない。
fill は hover と press の大きい方で良いが、グリフ色は hover 単独のまま にする。仕様上 press は前景色を動かしてはならないため、判定式を window_button_glyph_color に切り出して press を引数から外し、 hover=0 で unhovered 色に一致することをテストで固定した。
hit → WidgetId の対応を settings_panel_hit.rs へ切り出し、hover と press の両方が同じ対応表を使うようにした。focus 済みの行を押した場合も press_fill を通すため、押下フィードバックが消えない。
メニューだけは release で確定するため、押下から離すまで pulse が見える。 既存の分岐順は変えず、press ハンドラの先頭で hovered 済みの index に 対して発火するだけの純粋な追加にとどめた。
P3b3レビューの唯一のInstant指摘に対応。tab/window-button/context-menuの press pulseには専用の回帰テストがあったが、settings-row側のクローズは 間接的にしか検証されておらず、将来のリファクタで無音に壊れうる状態だった。
Coverage reportGenerated by |
7 tasks
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.
UI/UX v3 P3b3 closes the other half of the parent design's "widget hover/press": the moment the user commits to a control. P3b1 gave overlay surfaces an entrance and an exit (#79); P3b2 gave the pointer's hover a cross-fade (#80, #82); this gives the press its feedback, across the same four pointer models.
Design:
docs/superpowers/specs/2026-08-29-p3b3-press-pulse-design.mdPlan:
docs/superpowers/plans/2026-08-29-p3b3-press-pulse.mdWhy a pulse and not a pressed state
Three of the four models commit their action on mouse-down, not on mouse-up:
on_mouse_left_pressedon_mouse_left_pressedon_mouse_left_pressedon_mouse_left_releasedA Fluent-style held "pressed" state therefore has no window to live in on three of them — by the frame the button comes up, the tab has switched, the row has toggled, or the window is gone. Moving those commits to mouse-up would change click semantics across the chrome and is out of scope.
So press is a one-shot pulse: full weight at the press instant, zero 100 ms later (
duration::FASTER, the Fluent token documented as "Button press feedback"), independent of when the button comes up.PressPulse<Id>holds oneTimed, whereHoverTransitionneeds two — that type's second timer exists so a hand-off decays the outgoing item from the weight it actually held, and a press has no hand-off.With animations disabled,
scaled_duration_msreturns 0 and the weight is 0 from the first read, so reduced motion (P3c) means "no press feedback" rather than a stuck pressed appearance.Composition: why
max, and why the alpha boostOnly the tab composes its hover as an opaque
lerp_rgba. The other three draw the hover as an additive layer gated onw > 0.0— they emit no vertices at all at weight zero, so dimming a layer that is not being drawn is invisible. Press has to raise the weight before it dims it.And the dim alone is not enough: the normal click lands on a control that is already fully hovered, where
maxchanges nothing. A brightness multiplier scales HSVv, so on a near-black scheme the step has no absolute room. Measured: at the design's provisionalPRESS_ALPHA_BOOST = 1.7, the pulse was imperceptible on eight of the nine builtin schemes. Shipped value is 2.3, pinned by a test over all nine.Test coverage
animations/press.rs— 5 tests: decay timing, the single-slot replacement, the deliberate non-idempotence (a double-click must pulse twice, unlikeretarget), and theanimations.enabled = falsegate.color_util.rs— identity at weight 0, darken + strengthen at weight 1, alpha clamp, and two nine-scheme gates: press is perceptible on every builtin scheme, and it does not cost legibility (after >= 4.5 || after >= before * 0.90).widgets/draw/mod.rs— a pressed row paints even with no hover (this is what themaxbuys), and a pressed row's fill differs from a merely hovered one.state/mod.rs— one test per model that a decaying pulse keeps the frame loop awake.1031 tests pass;
cargo clippy -- -D warningsandcargo fmt --checkare clean.The defect this phase kept producing
Redefining a site's weight variable as
hover.max(press)silently changes every later use of that variable in the same function. Twice, that later use was a foreground colour — the window-button glyph and the context-menu label — so press moved text colour for 100 ms, which this project forbids. Neither line was edited; their meaning changed underneath them. Both are fixed: the glyph now goes throughwindow_button_glyph_color, whose signature has no press parameter at all, and the menu label reads a separatehover_w. A whole-branch review confirmed there is no third instance.Non-goals, stated so they are not read as omissions
add_px_soft_shadow_sdfdraws shadows for whole surfaces, and there is no per-control elevation in this renderer.snap_layout.rsswallowsWM_NCLBUTTONDOWNand completes onWM_NCLBUTTONUP, so the press never reaches client state. Unlike hover, press has one writer there, not two.Known limitations
docs/plans/ui-ux-modernization-v3.md.Test plan
cargo test -p nexterm-client-gpu— 1031 passedcargo clippy -- -D warningscargo fmt --checkanimations.enabled = false, confirm no press feedback appears at all