Skip to content

polish(win32): show focus rings only for keyboard focus and tidy the chrome - #247

Merged
amanthanvi merged 2 commits into
mainfrom
polish/win32-chrome
Sep 16, 2026
Merged

amanthanvi merged 2 commits into
mainfrom
polish/win32-chrome

Conversation

@amanthanvi

@amanthanvi amanthanvi commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Summary

A polish pass over the Win32 chrome and the Settings window, driven by the
things that have been visibly janky for a while: the blue outline that hung
around tabs and buttons, the near-white bevels on dark Settings fields, the
clipped text in the command palette query box, and a few smaller rough edges
that turned up once the harness was pointed at them. No new features; every
change is a refinement of the incumbent look.

What changed and why

Focus rings follow the keyboard, not the pointer. Every owner-drawn
button in the host and in Settings painted its focus ring straight off
GetFocus(). Native Windows controls do not: the OS keeps UISF_HIDEFOCUS
set until the dialog manager sees Tab or an arrow key, and a mouse click sets
it again (measured: WM_QUERYUISTATE returns 0x3 on a fresh host and a
fresh Settings window). So a tab you had just clicked, the [▾] whose menu
you had just dismissed, and the section button that opens Settings all wore
a ring nobody asked for. src/apprt/win32/focus_cue.zig is the pure policy
(rings in keyboard mode only; High Contrast keeps them on pointer focus;
only navigation keys reveal them). The host tracks the mode from its chrome
button procs and from the cycle_focus_region keybind, which calls
SetFocus without any key reaching the control. Settings tracks it from the
App pump ahead of IsDialogMessageW, because the dialog manager consumes
Tab and the arrows and never dispatches them to the control that gains
focus.

[▾] no longer keeps focus after its menu is dismissed. The BUTTON class
takes focus on the click that opens the menu, and nothing gave it back when
the menu closed without a command, so the next keystroke went to the
chevron rather than the shell. Focus now returns through the same
activation-target rule the window already uses.

Modal prompt buttons keep their ring. The confirm prompt's Allow /
Cancel are the one place the pointer gate does not apply: focus lands on
Allow programmatically and Enter presses whatever holds focus, so the ring
is the only mark of what Enter will do, the same reason native dialogs keep
their default button visibly marked.

The * active-tab marker stays. An earlier revision of this branch
dropped it as a third cue next to the raised button and the underline, but
docs/accessibility-matrix.md records that NVDA reads the BUTTON-class tab
items through MSAA window text and never speaks the UIA selection state, so
the asterisk is the only spoken cue for the active tab. Reverted.

No accent strip under DWM's accent border. The 2 px strip painted on row
0 of the integrated titlebar sat directly under the 1 px accent border DWM
already draws and read as a blue rim around the whole window. It is now
painted only for the separate tab row (Win10, or decorations without the
integrated titlebar), where it sits below the system caption.

Command palette query text is no longer clipped. The EDIT child was
12 px tall inside a 24 px row at 96 DPI under a font whose GDI cell is
19 px, and the EDIT class clips glyphs to its client rect, so every letter
was cut at the waist. The vertical inset is now 2 px, which keeps the
frame's stroke visible around the child at 96, 144, 192 and 288 DPI.

Settings edit fields match the combo boxes. They used
WS_EX_CLIENTEDGE, a hard-coded 3-D bevel that DarkMode_Explorer never
themes, so every dark-theme field wore a near-white 2 px frame. A themed
WS_BORDER EDIT draws the same 1 px frame the combo boxes beside it draw,
in both themes.

Palette hint copy. "Open the config file.." is now "Open the config
file." (a full stop was appended to descriptions that already end in one).

How verified

Built with zig build -Demit-exe=true and driven headlessly at 150% DPI on
Windows 11 26200 with a Python + ctypes harness (EnumChildWindows,
GetGUIThreadInfo for focus, screen BitBlt for pixels, WM_COMMAND for
tabs and the palette).

Before / after, same steps:

Check Before After
Ring on the active tab at rest (three tabs, tab 2 active) 2 px blue ring none
F6 from the terminal into the strip ring ring (keyboard mode)
Click tab 1 ring on tab 1 no ring; button still has focus
Right arrow after the click ring ring on the tab it lands on
Focus after dismissing the [▾] menu with Escape Button id=1911 terminal surface
Settings on open / after clicking a section ring on the section none
Settings after Down arrow on the rail ring ring
Top 2 rows of the integrated titlebar accent (104,144,212) chrome (32,32,32)
Dark Settings edit frame pixel (254,254,254) (48,48,48)
Palette query "Open Config" glyphs cut at the waist whole glyphs
F6 then Space on [▾], then Escape focus on the chevron focus on the chevron, ring on
Multi-line paste after a pointer action Allow ring Allow ring (not gated)
Settings: Down, close, reopen ring on reopen (mode leaked) no ring

Unit tests added or updated: focus_cue policy (3), overlay edit child
height at four DPIs, palette feedback full stop, Settings section ring with
input mode. Filters
run: focus, label, tab, overlay, settings, palette (each paired
with ConPTY), all passing.

Not covered here: the confirm preview EDIT and UIA_LabeledByPropertyId
gaps from #242, and the Settings "Enable background blur" label from #239,
are still open follow-ups.

AI assistance

Per AI_POLICY.md: this change was authored by an AI agent (Claude Code)
operating the maintainer's account with the maintainer's authorization. The
agent measured the baseline, made the changes, ran the unit tests and the
live before/after captures quoted above, and obtained an independent
adversarial review before the PR was opened. The maintainer reviews the
diff and the captures before merge.

Summary by Sourcery

Polish the Win32 chrome and Settings presentation with input-aware focus cues, cleaner borders and accents, corrected palette text rendering, and improved focus behavior.

Bug Fixes:

  • Prevent focus rings from appearing after pointer interaction while preserving them for keyboard navigation, high-contrast mode, and modal prompt actions.
  • Restore terminal focus when the overflow menu is dismissed without selecting a command.
  • Fix clipped command-palette query text across supported DPI scales and avoid duplicated punctuation in palette hints.
  • Replace dark Settings edit-field bevels with theme-consistent borders.

Enhancements:

  • Refine Win32 chrome visuals by removing the redundant integrated-titlebar accent strip while retaining the separate tab-row accent.
  • Preserve the active-tab asterisk accessibility cue.

Tests:

  • Add coverage for focus-cue policy, DPI-scaled overlay edit sizing, palette feedback punctuation, and Settings section focus behavior.

Summary by CodeRabbit

  • New Features

    • Focus indicators now appear for keyboard navigation while remaining unobtrusive during pointer interaction.
    • High Contrast mode consistently displays focus indicators for focused controls.
    • Overflow menus now return focus to the terminal when dismissed without selecting an action.
  • Bug Fixes

    • Improved query-field sizing to prevent text glyphs from being clipped.
    • Updated settings edit fields to better match the application’s themed controls.
    • Prevented duplicate punctuation in command palette descriptions.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @amanthanvi, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 5 days and 14 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Warning

Review limit reached

Next included review available in 12 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 244a2b66-0832-49d9-be2a-5478d8e2749f

📥 Commits

Reviewing files that changed from the base of the PR and between 3991a14 and bf4a230.

📒 Files selected for processing (2)
  • AGENTS.md
  • src/apprt/win32.zig

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 671ff1c7-f04a-4fd3-80e5-37dea9926b9a

📥 Commits

Reviewing files that changed from the base of the PR and between d29ba22 and 3991a14.

📒 Files selected for processing (6)
  • AGENTS.md
  • src/apprt/win32.zig
  • src/apprt/win32/chrome_layout.zig
  • src/apprt/win32/focus_cue.zig
  • src/apprt/win32/labels.zig
  • src/apprt/win32_settings.zig

Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Changes

The Windows frontend now tracks keyboard and pointer input modes for focus-ring rendering. Settings and host chrome use the shared focus-cue policy, with high-contrast and modal overlay exceptions. The change also corrects overlay edit sizing, themed edit borders, integrated-titlebar accents, and command-palette punctuation.

Win32 focus and chrome

Layer / File(s) Summary
Focus-cue policy and settings integration
src/apprt/win32/focus_cue.zig, src/apprt/win32_settings.zig, src/apprt/win32.zig, AGENTS.md
Adds shared keyboard-versus-pointer focus rules. Settings input is recorded before IsDialogMessageW, and section-button rings use the shared policy.
Host focus-mode wiring and chrome rendering
src/apprt/win32.zig
Host chrome records input from window procedures, updates ring visibility, marks F6 navigation as keyboard input, preserves overlay rings, and restores terminal focus after pointer menu dismissal.
Chrome control geometry and window styling
src/apprt/win32/chrome_layout.zig, src/apprt/win32_settings.zig, src/apprt/win32.zig
Overlay edit insets preserve the font cell and frame stroke. Settings edits use WS_BORDER, and the tab accent strip is skipped for integrated titlebars.
Command-palette feedback punctuation
src/apprt/win32/labels.zig
Feedback text avoids duplicate sentence punctuation and adds a period when the subtitle has no terminal punctuation.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~30 minutes

Change: Bug fix

Suggested reviewers: nanasess

Merge Risk: ⚪ Minimal · up to 3991a

The Win32 focus and chrome updates have no concrete unresolved merge-blocking risk in the supplied evidence.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the primary Win32 focus-ring change and the related chrome polish.
Description check ✅ Passed The description is detailed and covers the summary, implementation rationale, validation results, tests, risks, and follow-ups. It uses "How verified" instead of the template's "Validation" heading an…
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch polish/win32-chrome
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch polish/win32-chrome

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

This polish pass centralizes keyboard-versus-pointer focus-cue decisions and wires them into both Win32 chrome paths, while correcting menu dismissal focus, tab rendering and repaint behavior, titlebar accents, palette EDIT geometry, Settings field borders, and hint punctuation. Unit coverage was updated for the policy and DPI-sensitive layout changes, with the implementation informed by Windows UI-state and pixel measurements.

Sequence diagram for keyboard-aware Win32 focus rings

sequenceDiagram
    participant User
    participant AppPump
    participant HostOrSettings
    participant focus_cue
    participant OwnerDrawnControl

    User->>AppPump: Tab, arrow, F6, or pointer input
    alt Settings dialog navigation
        AppPump->>HostOrSettings: noteInputMessage(hwnd, message, wParam)
        AppPump->>AppPump: IsDialogMessageW
    else Host chrome input
        HostOrSettings->>HostOrSettings: noteChromeKeyInput or noteChromePointerInput
    end
    HostOrSettings->>focus_cue: keyRevealsRing(vk) or showRing(focused, disabled, mode, high_contrast)
    focus_cue-->>HostOrSettings: keyboard mode, pointer mode, or High Contrast result
    HostOrSettings->>OwnerDrawnControl: InvalidateRect on mode change
    OwnerDrawnControl->>focus_cue: showRing(...)
    focus_cue-->>OwnerDrawnControl: Paint ring only when policy allows
Loading

Flow diagram for dismissed chevron menu focus restoration

flowchart LR
    Click[Click chevron button] --> Menu[TrackPopupMenu]
    Menu -->|command selected| Activation[Existing activation target]
    Menu -->|dismissed with Escape or no command| Check{"GetFocus() == button"}
    Check -->|yes| Restore[refocusHostAfterActivation]
    Check -->|no| Continue[Continue normally]
    Restore --> Shell[Terminal or activation target receives next key]
    Activation --> Shell
Loading

File-Level Changes

Change Details Files
Gate owner-drawn focus rings on keyboard navigation state while preserving accessibility behavior.
  • Added a shared pure focus-cue policy with High Contrast override and navigation-key classification.
  • Tracked pointer versus keyboard input independently in the host and Settings, including input consumed by the dialog manager and direct focus-region cycling.
  • Applied the policy to host chrome and Settings section rendering, with focused-control invalidation on mode changes.
src/apprt/win32/focus_cue.zig
src/apprt/win32.zig
src/apprt/win32_settings.zig
Correct focus and activation behavior in the host chrome.
  • Restore terminal focus when the overflow menu is dismissed without a command.
  • Remove the active-tab text marker and invalidate tab buttons directly when activation changes.
  • Limit the tab accent strip to non-integrated titlebar layouts.
src/apprt/win32.zig
src/apprt/win32/labels.zig
Refine Win32 control geometry and theming for overlays and Settings.
  • Reduce the command-palette EDIT child’s vertical inset so glyphs fit across supported DPIs.
  • Replace Settings EDIT client-edge bevels with themed one-pixel borders matching combo boxes.
src/apprt/win32.zig
src/apprt/win32/chrome_layout.zig
src/apprt/win32_settings.zig
Correct command-palette prose punctuation and update focused unit coverage.
  • Append sentence punctuation only when a description does not already end in punctuation.
  • Update tab-label, budget, tooltip, focus-cue, and overlay geometry tests for the revised behavior.
src/apprt/win32/labels.zig
src/apprt/win32/focus_cue.zig
src/apprt/win32/chrome_layout.zig
src/apprt/win32_settings.zig
Document the measured Win32 rendering and input pitfalls for future maintenance.
  • Record the focus-cue, control styling, overlay geometry, and titlebar-accent findings in the self-correction log.
AGENTS.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@greptile-apps

greptile-apps Bot commented Sep 16, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no actionable new defects or outstanding previous findings remain.

Summary

This PR polishes Win32 focus cues and native chrome presentation without adding features.

  • Tracks pointer-versus-keyboard focus for owner-drawn host and Settings controls, with High Contrast and modal-prompt exceptions.
  • Restores terminal focus after pointer-opened overflow menus are dismissed.
  • Corrects command-palette field sizing and feedback punctuation.
  • Replaces bright Settings EDIT bevels with themed borders and removes the redundant integrated-titlebar accent strip.
  • Resets Settings focus state across HWND recreation.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
    I[Win32 input message] --> T{Input type}
    T -->|Navigation key| K[Keyboard focus mode]
    T -->|Pointer press| P[Pointer focus mode]
    K --> R[Focused owner-drawn control shows ring]
    P --> H[Focused owner-drawn control hides ring]
    C{High Contrast?} -->|Yes| R
    M[Modal prompt default button] --> R
Loading

Reviews (3) · Last reviewed commit: "polish(win32): keep the active-tab marke..."

Comment thread src/apprt/win32_settings.zig
…chrome

Every owner-drawn button in the host and in the settings window painted
its focus ring straight off `GetFocus()`. Native controls do not: the OS
keeps `UISF_HIDEFOCUS` set (measured `WM_QUERYUISTATE` = 0x3 on a fresh
host and settings window) until the dialog manager sees Tab or an arrow
key, and a mouse click sets it again. So a tab that had just been
clicked, the `[▾]` whose menu had just been dismissed, and the section
button that opens Settings all wore a blue ring nobody asked for.

* `win32/focus_cue.zig` is the pure policy: rings show only in keyboard
  mode, High Contrast keeps them on pointer focus, and only navigation
  keys (Tab, Enter, Space, Escape, arrows, Home/End/PgUp/PgDn, F6) flip
  the mode back. The host tracks the mode from its chrome button procs,
  the overlay query edit and the docked search edit, and from the
  `cycle_focus_region` keybind, which calls `SetFocus` without any key
  reaching the control. The settings window tracks it from the App
  pump, ahead of `IsDialogMessageW`, because the dialog manager consumes
  the navigation keys and never dispatches them to the control that
  gains focus.
* Dismissing the `[▾]` menu without choosing a command left keyboard
  focus on the chevron, so the next keystroke went to the button, not
  the shell. Focus now returns to the terminal (or the overlay) through
  the same activation-target rule the window already uses.
* The tab label no longer carries the `* ` active marker. The raised
  button and the accent underline already say which tab is active, UIA
  says it through `SelectionItem.IsSelected`, and the marker cost every
  active title two cells and made labels jump on every switch. Tab
  activation now invalidates the two buttons directly, since the label
  text no longer changes.
* The 2 px accent strip on row 0 of the integrated titlebar sat directly
  under DWM's own 1 px accent border and read as a blue rim around the
  whole window. It is now painted only for the separate tab row, where
  it sits below the system caption.
* The command palette query EDIT was 12 px tall inside a 24 px row at
  96 DPI, under a font whose GDI cell is 19 px, so the EDIT class
  clipped every glyph at the waist. The vertical inset is 2 px, which
  keeps the frame's stroke visible around the child at 96..288 DPI.
* Settings edit fields used `WS_EX_CLIENTEDGE`, a hard-coded 3-D bevel
  that `DarkMode_Explorer` never themes, so every dark-theme field wore
  a near-white 2 px frame. A themed `WS_BORDER` EDIT draws the same 1 px
  frame the combo boxes beside it draw, in both themes.
* The palette hint line rendered "Open the config file.." because a
  full stop was appended to descriptions that already end in one.

Verified live at 150% DPI on Windows 11 26200 with a screen-BitBlt
harness (child enumeration plus GetGUIThreadInfo focus): F6 into the
strip paints the ring on the active tab; clicking a tab focuses it with
no ring; Right arrow brings the ring back on the tab it lands on; the
Settings rail shows no ring on open or after a click and shows it after
a Down arrow; focus after dismissing the `[▾]` menu is the terminal
surface; the query box shows whole glyphs; the dark settings fields have
a 1 px (48,48,48) frame instead of (254,254,254).
Follow-ups from an independent review of the focus-cue change.

* The `* ` active marker stays in the tab label. The UIA tree does say
  which tab is selected, but `docs/accessibility-matrix.md` records that
  NVDA reads the BUTTON-class tab items through MSAA window text and
  never speaks the selection state, so the asterisk is the only spoken
  cue for the active tab. The flagship contract suite also pins the test
  that proves it. Reverted to the label builder on main.
* The confirm prompt's Allow / Cancel ring is not gated on the input
  mode. Focus lands on Allow programmatically and Enter presses whatever
  holds focus, so the ring is the only mark of what Enter will do; native
  dialogs keep their default button visibly marked under
  `UISF_HIDEFOCUS` for the same reason.
* Escape on a `[▾]` menu that the keyboard opened (F6, Space) leaves focus
  on the chevron, as every menu does; only a pointer-opened menu hands
  focus back to the terminal.
* The tab-overview banner's F6 and the strip's own Delete / F2 / Apps
  verbs count as navigation, so the control they leave focus on paints
  its ring. A pointer press on the terminal surface returns the mode to
  pointer.
* The settings window resets its mode when its HWND is torn down, so a
  window reopened by the mouse does not inherit the keyboard mode from
  the previous one, and the `WM_SETTINGS_PROOF_LIVE_FOCUS_RING` proof
  reports whether a ring is painted rather than whether the button has
  focus.

Verified live: F6 then Space on the chevron, Escape -> focus stays on
`Button id=1911` with its ring; click then Escape -> focus on the
terminal surface; a multi-line paste started from a palette row click
-> Allow holds focus with its ring; Settings Down, close, reopen ->
Terminal selected with no ring. The four `buildTabButtonLabel` contract
names resolve to exactly one declaration each.
@amanthanvi
amanthanvi merged commit 669ed92 into main Sep 16, 2026
7 checks passed
@amanthanvi
amanthanvi deleted the polish/win32-chrome branch September 16, 2026 03:28
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