Skip to content

feat(client): motion and self-dismissal for the InfoBar stack (UI/UX v3 P6d) - #95

Merged
mizu-jun merged 1 commit into
masterfrom
p6d-infobar-motion
Aug 29, 2026
Merged

feat(client): motion and self-dismissal for the InfoBar stack (UI/UX v3 P6d)#95
mizu-jun merged 1 commit into
masterfrom
p6d-infobar-motion

Conversation

@mizu-jun

Copy link
Copy Markdown
Owner

Problem

The stack shipped in P6a–P6c already declared a Timed entrance, an Option<Timed> exit and an expires_at deadline — and nothing read any of them. infobar.rs carried a file-level #![allow(dead_code)] saying exactly that. So the three banners were still the only overlays in the product that pop in and out on a frame boundary, and the roadmap's word "auto-dismissing" described none of them.

Solution

  • Motion is per bar, not per stack. apply_surface_fade is called inside the builder's loop, so one bar can fade out while the one under it is still arriving. InfoBar::visibility() is one expression (1 - exit.progress, else entrance.progress), and the exit resumes from what is on screen via Timed::resuming_at — a bar dismissed mid-entrance does not jump to opaque first.
  • Dismissal is not deletion. ClientState::dismiss_info_bar starts the exit, retire_info_bars drops the bar once it has finished drawing out, and expire_info_bars applies the info severity's 20 s INFO_BAR_TTL (D3 — the warning and error severities never expire). Both run from the tick in lifecycle.rs, next to the existing retire calls.
  • Esc, Enter and a successful connect all dismiss. Slot replacement stays abrupt: the replacing bar is drawn in the same place on the same frame, and cross-fading would only smear the two messages together.
  • A dismissed bar is out of everything but the renderer at once — it takes no key, holds no slot against a repeat of the same message, and leaves the AccessKit tree and the tree hash immediately rather than announcing a bar the user just closed.
  • infobar::top_live / top_dismissible replace StackLayout::top, so the keyboard path stops re-deriving the stack's order itself.

push_info_bar and apply_server_message now take &AnimationsConfig: the entrance duration is the user's setting, not a constant.

Gates

  • G-idlehas_active_animation gains one clause and goes quiet once every bar settles. A bar merely counting down its 20 s deadline asks for no frames; covered by only_a_moving_bar_asks_for_frames.
  • Reduced motion — with animations off every Timed is born finished, so a bar appears and leaves without a single extra frame (motion_off_makes_a_bar_appear_and_leave_without_a_single_extra_frame).
  • G-a11y / G-hash — still exhaustive over the enum; a new test pins that a dismissed bar leaves both the nodes and the hash before its exit finishes.
  • G-single, G-cap, G-order, G-i18n unchanged from P6a–P6c.

Verification

  • cargo fmt --check — clean
  • cargo clippy --workspace --all-targets -- -D warnings — no warnings
  • cargo test --workspace — 28 suites pass (1111 in nexterm-client-gpu), 10 new tests
  • Not covered: appearance. Per the spec's §7 this joins the P4/P5 on-device backlog; the thing worth looking at is three bars stacked in a small window, where the cap and the count suffix are all that stand between the stack and the terminal content.

Closes P6 (P6a #92, P6b #93, P6c #94, P6d here).

🤖 Generated with Claude Code

…(UI/UX v3 P6d)

The stack shipped in P6a-P6c had a `Timed` entrance, an `Option<Timed>` exit
and an `expires_at` field that nothing ever read - the module carried a
file-level `#![allow(dead_code)]` saying so. P6d wires all three and removes
the allow.

- Motion is per bar, not per stack: `apply_surface_fade` is called inside the
  builder's loop, so one bar can fade out while the one under it arrives.
- Dismissal never deletes. `dismiss_info_bar` starts an exit resuming from
  what is on screen, `retire_info_bars` drops the bar once it finishes, and
  `expire_info_bars` applies the info severity's 20 s TTL (D3). Both run from
  the tick in `lifecycle.rs`, next to the other `retire` calls.
- `Esc`, `Enter` and a successful connect all dismiss rather than remove. Slot
  replacement stays abrupt: the replacing bar is drawn in the same place on the
  same frame, and cross-fading would smear the two messages together.
- A dismissed bar is out of everything but the renderer at once - it takes no
  key, holds no slot against a repeat, and leaves the AccessKit tree and the
  tree hash immediately rather than announcing a bar the user just closed.
- `top_live` / `top_dismissible` replace `StackLayout::top` so the keyboard
  path stops re-deriving the stack's order itself.

G-idle: `has_active_animation` gains one clause and goes quiet once every bar
settles, so a bar counting down its deadline asks for no frames. With
animations off every `Timed` is born finished, which is the reduced-motion
path: bars appear and leave without a single extra frame.

`push_info_bar` and `apply_server_message` now take `&AnimationsConfig`, since
the entrance duration is the user's setting.

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>
@github-actions

Copy link
Copy Markdown

Coverage report


Generated by cargo llvm-cov (workspace minus nexterm-client-gpu and nexterm-i18n).

@mizu-jun
mizu-jun merged commit 3ecb37e into master Aug 29, 2026
13 checks passed
@mizu-jun
mizu-jun deleted the p6d-infobar-motion branch August 29, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant