CmuxNext: sidebar rows tile so hover moves row to row with no flicker
Summary
Leo reported four sidebar workspace-row papercuts. This PR fixes all four:
- Rows tile for hit testing. Rows sat
rowSpacing(2 pt) apart, and that spacing hit nothing. Moving the pointer down the list dropped the hover in every gap, so it flickered row to row.SidebarLayout.row(at:)now splits the spacing between two consecutive rows, half to each, so their hit areas meet. The hover moves from one row to the next on the same pointer step, and the fills cross-fade (MotionFade.hover). The pill stays inset inside the row.- Clicks, the context menu and middle-click use the same hit test, so the 2 pt slivers now hit the nearer row.
- Section gaps, a group's bottom padding and the drag drop gap are wider than the spacing and still hit nothing. Double-clicking empty space still makes a workspace there.
- The hover card waits for a dwell. The workspace card used to show at once on every row the pointer passed. It now waits about 1 s, like a native tooltip, via the new tunable
motion.hoverCard.dwell(default 1 s, with the other Motion hover tunables).- A sweep shows no card, and leaving a row cancels its pending card.
- Everything else about the card is unchanged: once a card shows, moving to another row slides it over, its fade uses the existing tokens, and every row still gets a card.
- One gutter.
SidebarStyle.horizontalInsetnow sets four distances: the pill's inset from the list edges, the text's inset from the pill's leading edge, the trailing marks' inset from its trailing edge, and the band divider's inset. These match on selected and unselected rows.- Before, the text sat 8 pt in while the trailing marks sat 6 pt in. The title now starts at the inset, with label cells sitting back by their cell padding (
SidebarStyle.labelCellPadding), and it runs to the trailing inset. - Section, folder and region header names start where row titles do.
- The divider above the list is inset like the pills, so its ends line up with theirs. This changes the divider: it was full width.
- Before, the text sat 8 pt in while the trailing marks sat 6 pt in. The title now starts at the inset, with label cells sitting back by their cell padding (
- Count badge. I kept the chip rather than switching to plain muted text. Next to the status glyph, a bare digit reads as part of the title or the glyph, and the fill marks it as a count.
- It's now a quiet small-radius rect (
Metrics.chipCornerRadius, not a capsule) on the theme's badge fill, with no hairline. - Its height matches the status glyph through one new CmuxNextDesign token,
Metrics.statusMarkSize. - Its digits are tabular system digits instead of the monospaced face, optically centered by cap height on the pixel grid.
- It's now a quiet small-radius rect (
The row's X and options controls are unchanged. They show with the row's hover fill, as in Edge and Codex.
Overlap with other lanes:
- No open op-next-icons PR touches the status glyph.
gh pr list --search "status glyph"and theop-next-icons/branches show only #20213, which doesn't touch the sidebar orStatusIndicator*. - I didn't edit the glyph's files.
SidebarStyle.indicatorSizenow readsMetrics.statusMarkSize, which has the same value as before (smallIconSize - space1), so the glyph keeps its size. - #20121's
SidebarSide+Pins.swiftandWindowRootView+SidebarSide.swiftare untouched. All inset changes are inside the sidebar row, list and band views.
Testing
I added three behavior suites in CmuxNextAppTests that drive a real SidebarListView or SidebarView in a window:
SidebarRowHoverGapTests:- A 0.25 pt probe finds no dead band between consecutive rows.
- A 1 pt pointer sweep hovers exactly one row at every step, and at each change both fills animate.
SidebarHoverCardDwellTests:- A sweep across 5 rows at 1 pt every 2 ms, ending outside the sidebar, shows zero cards (checked again after 1.2 s).
- A 1.2 s rest on one row shows exactly that row's card, and no card shows at 300 ms.
SidebarRowGutterTests:- On selected and unselected rows, the leading inset equals the trailing inset and equals the pill inset.
- The pill's edges match the pinned section header's leading edge and the band divider's leading and trailing edges.
- The badge height equals the status glyph's, and its digits are tabular and not in a fixed-pitch face.
Fleet runs, each cmux-ci run --class light --script scripts/ci/run-swift-testing-suites.sh --arg Packages/macOS/CmuxNext:
- Red on
7b2a4804111c(--red): both hover suites fail.- Rows: a 2.25 pt dead band, and 0 rows hovered in the gaps.
- Cards: the sweep showed 5 cards, and the dwell test showed one before 300 ms.
- Red on
aa4c0416d921(--red):SidebarRowGutterTestsfails.- Text 8 pt in vs marks 6 pt in.
- Pill 6 to 234 vs divider 0 to 240.
- Badge 14 pt vs glyph 10 pt, in a fixed-pitch font.
- Green on
171d134c43d6: both hover suites pass. - Green on the head
26961ff1d96d: all three suites pass.
Every run also fails CmuxNextDaemonTests.DaemonCapabilityExportTests/exportIsFresh, including the test-only red commits. This PR doesn't touch it, so it's a base red.
Guards on the head (merge base 5f5eefbd72fc):
check-concurrency.sh,check-motion.sh,check-theme-scope.shandcheck-system-blue.pypass.crash_ratchet.pypasses.lint-ios-package-conventions.shpasses.check-app-size.sh --basepasses: CmuxNextApp is untouched at 113798 lines.check-no-godfiles.sh --basepasses, with notes only for entries that were already over budget on the base and that this change didn't grow.WorkspaceRowView.swiftstays at 399 lines.
Localization: the only new strings are the Debug Settings tunable's title and help text, which are English-only like every other tunable. No user-facing strings changed.
Proof: captures don't gate this change. What's visible: the hover moves row to row with no off frame; no hover card appears during a sweep; row text and trailing marks sit the same distance in from the pill; and the count badge is a glyph-height quiet rect.
Changelog
Fixed: Moving the pointer down the sidebar no longer flickers the hover highlight between rows.
🤖 Generated with Claude Code
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Fixes four sidebar workspace-row papercuts: hover no longer flickers between rows, the workspace hover card waits for a native-tooltip dwell, row content and marks share one gutter, and the count badge matches the status glyph.
Behavior changes
- Rows' hit areas now tile: the spacing between two rows is split half to each, so hovering moves row to row on the same pointer step and clicks in the gaps hit the nearer row. Section gaps, group padding, and the drop gap still hit nothing.
- The workspace hover card waits about 1 s (new tunable
motion.hoverCard.dwell) before showing; sweeping the list shows no cards, and leaving a row cancels its pending card.
Visual changes
SidebarStyle.horizontalInsetnow sets the pill, title, trailing marks, and band divider insets to one gutter; the divider is now inset (it was full width), and a title without marks runs to the trailing inset.- The count badge is now a quiet small-radius rect as tall as the status glyph (
Metrics.statusMarkSize), with tabular system digits instead of a taller monospaced chip.
Three new test suites cover the hit-area tiling, hover-card dwell, and gutter/badge metrics.