Skip to content

Redesign developer overlay as a Liquid Glass HUD - #115

Merged
kyleve merged 3 commits into
mainfrom
cursor/devtools-hud-redesign
Jul 22, 2026
Merged

Redesign developer overlay as a Liquid Glass HUD#115
kyleve merged 3 commits into
mainfrom
cursor/devtools-hud-redesign

Conversation

@kyleve

@kyleve kyleve commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Reworks the DEBUG-only developer overlay into a HUD-style Liquid Glass surface, and makes the floating window movable, resizable, and aware of the app content behind it. All changes are #if DEBUG and compiled out of release.

What changed

  • Hover button — a circular adaptive .glassEffect disc with a .primary glyph (reads in light and dark) instead of the flat stroked ring.
  • Chrome — one glass panel surface with a slim HUD control strip (close / drag grabber / full-screen). The tools list clears its scroll background so the glass shows through.
  • Full screen — now an inset (~12px) rounded modal over a dimmed, touch-blocking scrim, preserving the .isModal / .screenChanged accessibility behavior.
  • Floating window — draggable by its grabber and resizable from a bottom-trailing grip (top-leading corner pinned), clamped on-screen with a minimum size. Center + size (and resting corner) persist across launches via an injected KeyValueStore, kept local to this DEBUG surface rather than routed through the shipping WherePreferences. In-flight translation lives in the view so the store is written once per gesture (not per frame). The persisted layout is re-clamped to the current container on open, so a window saved in one orientation / size class / device can't reopen off-screen or oversized.
  • Safe area — the window publishes the footprint it occupies (DeveloperOverlayInsetKey), which RootView applies as safeAreaPadding to the app content only, so screens behind the non-modal HUD scroll clear of it; the HUD's own list reserves room for the resize grip. The footprint math is a pure, unit-tested DeveloperOverlayModel.contentInsets(...): an edge docks only when its near edge is within tolerance and the opposite edge is free (so a window spanning an axis — e.g. the near full-width default — claims nothing), and each inset is capped at 80% of the container so a large window can't collapse the content behind it.
  • Strings — adds developer.dragHandle / developer.resizeHandle accessibility labels.

Notable decisions / trade-offs

  • Adaptive glass (follows light/dark) rather than an always-dark HUD, to stay consistent with the rest of the app; a dark clamp or a toggle can come later.
  • One glass surface for the whole panel (rather than glass-on-glass toolbar + background) to avoid double-rendered material.
  • The app-content safeAreaPadding is scoped to the LifecycleContainer only (never the sibling overlay/toast layers) so the overlay's geometry can't feed back on itself. A bottom-docked HUD lifts the tab bar clear of it — intended.
  • Drag/resize gestures are measured in the global coordinate space; since each gesture rides the moving/growing window, a .local translation oscillated and read as the window resetting to its prior size/position.
  • The window contents use .dynamicTypeSize(.small) to reclaim space in the small HUD.

Testing

  • Extended DeveloperOverlayModelTests: default/clamp/move/resize geometry, persistence round-trip via InMemoryKeyValueStore, layout survival across a full-screen toggle, the content-inset footprint (each docking direction, corner, axis-spanning, the cap, the empty-container guard), and the clamp-on-restore path.
  • WhereUITests passes locally (tuist test WhereUITests, iPhone 17 / iOS 26.2) and swiftformat --lint is clean.

kyleve added 3 commits July 20, 2026 19:03
Rework the DEBUG-only developer overlay into a HUD-style Liquid Glass
surface, and make the floating window movable, resizable, and aware of
the app content behind it.

- Hover button: circular adaptive `.glassEffect` disc with a `.primary`
  glyph (reads in light/dark) instead of the flat stroked ring.
- Chrome: one glass panel surface with a slim HUD control strip
  (close / drag grabber / full-screen); the tools list clears its scroll
  background so the glass shows through.
- Full screen: an inset (~12px) rounded modal over a dimmed,
  touch-blocking scrim, preserving the `.isModal` / `.screenChanged`
  accessibility behavior.
- Floating window: draggable by its grabber, resizable from a
  bottom-trailing grip (top-leading pinned), clamped on-screen with a min
  size. Center + size (and resting corner) persist across launches via an
  injected KeyValueStore, kept local to this DEBUG surface. In-flight
  translation lives in the view so the store is written once per gesture.
- Safe area: the window publishes the footprint it occupies
  (DeveloperOverlayInsetKey), which RootView applies as safeAreaPadding to
  the app content only, so screens behind the non-modal HUD scroll clear
  of it; the HUD's own list reserves room for the resize grip.
- Add developer.dragHandle / developer.resizeHandle a11y strings; extend
  DeveloperOverlayModelTests for the layout math, persistence round-trip,
  and layout survival across a full-screen toggle.
- Measure the drag and resize gestures in the global coordinate space.
  They were attached to the moving/growing window, so a `.local`
  translation was relative to a frame moving under the finger, which
  oscillated and read as the window briefly resetting to its prior
  size/position.
- Apply `.dynamicTypeSize(.small)` to the HUD surface so the control bar
  and tools list shrink, reclaiming space in the small window.
Address review findings on the developer HUD:

- Clamp the persisted floating layout to the *current* container before
  use (shared `currentBase`, used by both display and the gesture-end
  commit). Persisted geometry was only clamped for the container active
  when written, so after a rotation, size-class change, or launch on a
  different device the window could open off-screen or oversized.
- Extract the footprint math into a pure, testable
  `DeveloperOverlayModel.contentInsets(for:in:edgeTolerance:)`. An edge
  now docks only when its near edge is within tolerance *and* the opposite
  edge is free, so a window spanning an axis (e.g. the near full-width
  default) no longer wrongly claims a large inset, and a window docks at
  most one edge per axis. Each inset is capped at
  `Layout.maxContentInsetFraction` (0.8) of the container so a large
  window can't collapse the content behind it.
- Add DeveloperOverlayModelTests for the inset directions, spanning, the
  cap, the empty-container guard, and the clamp-on-restore path.
@kyleve
kyleve enabled auto-merge (squash) July 22, 2026 16:48
@kyleve
kyleve merged commit e71a57d into main Jul 22, 2026
2 checks passed
kyleve pushed a commit that referenced this pull request Jul 26, 2026
Weekly read-only audit refresh. Every open July 19 finding was re-verified against current source, and the week's new surface was reviewed: the Periscope migration (#94), Settings drill-in (#111), developer HUD (#115), navigation restructure (#119), log-viewer tooling (#107), String Catalog symbols (#124), the Gregorian-calendar pass (`fe99dde`), preview coverage (`52f0136`), Bumper Bowling (#127), and catalog serialization (#135). No source changed.

## Structural changes to the audit

- **`LogKit` and `LogViewerUI` are gone** — Periscope replaced them, so their sections are removed and the inventory drops to **14 SPM library targets**. Counts refreshed to ~359 source / ~198 test files (WhereUI 84 → 113, WhereCore 70 → 87, PeriscopeTools 15 → 24, RegionKit 9 → 13).
- New section for **Bumper Bowling**, the repo-owned architecture lint that landed this week.

## Two new high findings

1. **`where.gregorian_calendar` enforces nothing.** The rule filters `MemberAccessExprSyntax` on `base == "Calendar"`, so it matches a spelled-out `Calendar.current` but not the implicit-member form (`calendar: Calendar = .current`, `in: .current`) — which, after the Gregorian call-site pass, is the only form left in the tree. CI hard-gates `bumper lint` at `severity: .error` and is green, which confirms it finds nothing while seven production sites drift. `.bumper/RULES.md` also claims three calendar violations and some preview-coverage violations are "left visible during this bootstrap"; both claims are stale.
2. **`CalendarDay.displayDate` resolves through `Calendar.current`.** `startOfDay(in:)` interprets the day's Gregorian Y-M-D as *that* calendar's components, so on a Buddhist-era device every day label flowing through the helper — relabel, logged days, resolution details, the region drill-in — renders a date ~543 years off. `DateRangeFormatting.abbreviated` and `PresenceTimeline.stints` also default to `.current`.

The three highs carried from July 19 are all still open: daily-summary staleness, the WhereUI tracking-toggle race, and the LifecycleKit terminal-phase race (now described precisely — `runSteps`' cancellation check sits at the *top* of the loop, so a cancel during the **final** step's `minVisible` hold is never observed before it returns `.completed`).

## Retired as verified fixed

The Where app `README.md`; the cold-launch reason misclassification (`.undetermined` + `completedStepIDs`); `#Preview` coverage across WhereUI/WhereWidgets; the hand-maintained string-key facades; the in-app SwiftData browser; the widget post-midnight snapshot policy (now documented as intentional degradation); and `SharedItemLoader` warning logs.

## TODOs.md

**`Where/TODOs.md`** — retires the `WhereModel` break-up (now 185 lines of process-lifetime state), the SwiftData browser, the `@_spi(Testing)` migration, and two items whose subject no longer exists (the `WhereModel` "controller" guard, hoisting `Calendar.current` onto the controller). Files this week's findings, pins the exact remaining `waitForOneRunloop` call sites, and promotes soft-deleting untracked regions now that the shipped picker reaches the hard delete.

**`Shared/Periscope/TODOs.md`** — adds a P1 (the orphan sweep closing spans whose `survivesRelaunch` policy couldn't be decoded, silently) and three P2s (`LogInspectorModel` re-querying full subtrees per commit, `SpanHistoryModel` swallowing an `SpanEnded` decode failure, and a test pinning open-span containment). Sharpens the two density items with the user-visible consequence.

**Root `AGENTS.md`** — its description of the audit cited the LogKit/LogViewerUI sections as an example of stale content surviving a refresh; this refresh removed them, so the caveat is rewritten to keep the useful half without an example that goes stale again.

## Verification

Static analysis only — the Cloud agent runs Linux, so no `tuist test`, `bumper lint`, or simulator runs. CI status on `main` was read via `gh` (green) which is what lets the "the Gregorian rule finds nothing" conclusion stand. Changes are markdown-only, so `./swiftformat --lint` and `./xcstrings --lint` are unaffected.
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