Skip to content

Adopt native Liquid Glass across the chrome (macOS 26) #31

Closed

Description

@arzafran

Why we don't have Liquid Glass today

macOS 26 upgrades apps to Liquid Glass largely for free — if they use the system's building blocks. We don't, on four counts, and each one independently blocks the material:

  1. None of our chrome is a system control. Auto-adoption applies to NSToolbar, standard buttons, sidebars, titlebars. Our toolbar, tab pills, ⌘K palette, and popovers are all hand-built NSViews drawing their own backgrounds — there is nothing for the OS to upgrade. To the system, our chrome is opaque custom content.
  2. Our "glass" is the previous material system. The backdrop is NSVisualEffectView with HUDWindow material (src/bro_mac.mm:258) plus hand-tuned scrims — that's static blur from the pre-26 API family. Liquid Glass is a different material with a different API (NSGlassEffectView); visual-effect views do not become glass, ever. Blur samples what's behind it once; glass refracts and responds — they're not the same thing wearing different settings.
  3. The appearance is pinned dark by fiat (NSApp.appearance = DarkAqua, src/bro_mac.mm:1313) with hardcoded colors throughout. Liquid Glass is adaptive by nature; a forced appearance and literal-white text fight it at the root. This exact combination already produced the invisible-URL-text bug — the material system re-resolves colors in ways our hand-rolled setup doesn't anticipate.
  4. The build may not even link the 26 SDK — and the new look is gated on linking against it (unverified; phase 1 checks).

So: the fix isn't a setting, it's swapping our hand-rolled material layer for the system's — surface by surface. The good news is the app's structure makes that unusually cheap: every chrome surface is already a discrete view, and our concentric-corner system maps 1:1 onto the glass API's corner model. The web content stays opaque throughout — glass is the control layer, never the content layer.

The payoff move: tab pills placed in one NSGlassEffectContainerView merge and separate fluidly as they approach during drag/reorder — the system does it, we currently fake none of it.

Verified API surface (read from the local macOS 26 SDK headers on this machine, not docs-from-memory)

  • NSGlassEffectView (macOS 26.0+): contentView, cornerRadius, tintColor, style — NSGlassEffectViewStyleRegular / ...Clear
  • NSGlassEffectContainerView (macOS 26.0+): contentView, spacing — merges nearby glass descendants, batches rendering
  • SDK present at /Library/Developer/CommandLineTools/SDKs/MacOSX26.sdk (Xcode 26.3 installed)

Design decisions reserved for @ragojose

  1. Regular vs Clear per surface — Clear reads lighter but risks legibility over busy pages; likely Regular for the chrome row, decide per overlay.
  2. Tint — tintColor per surface or fully neutral; interacts with the theme work below.
  3. Pill merge behavior — embrace the merge (container spacing > 0) or keep pills discrete (spacing 0, still batched).
  4. Light mode look — dark-only ends when the forced appearance goes; light mode becomes real and needs a designer's eye, not a code default.

Implementation phases (bounded, each independently shippable — for the agent)

Phase 0 — themes (hard prerequisite). Blocker 3 above.

  • Remove NSApp.appearance = DarkAqua (src/bro_mac.mm:1313); add a System/Light/Dark preference (default System), persisted via the existing bro_persist pattern, menu under the app menu
  • Sweep hardcoded chrome colors to dynamic/semantic colors: src/bro_toolbar.mm (multiple whiteColor/labelColor sites — see PR Fix invisible idle address text in the glass chrome #23/Rebuild browse input and refine native browser chrome #24 history for the trap), src/bro_tabstrip.mm, src/bro_palette.mm, src/bro_downloads.mm
  • Verify: build clean; launch in both appearances; URL text visible idle AND after edit+Return in both (the exact regression path from the invisible-text bug)

Phase 1 — SDK + gating. Blocker 4.

  • Pin CMAKE_OSX_SYSROOT to the 26 SDK in CMakeLists.txt; first verify what the default currently resolves to
  • Keep LSMinimumSystemVersion 12.0: every glass surface behind if (@available(macOS 26.0, *)) with the current NSVisualEffectView path as the else-branch
  • Verify: build clean; fallback path still launches; no new warnings

Phase 2 — surface swaps (blockers 1–2; one commit each, in this order).

  1. Window backdrop: src/bro_mac.mm:258 → NSGlassEffectView — blank tabs and the screenshot-mode gutter become real glass with zero further work
  2. Chrome row → glass, replacing the hand-rolled treatment
  3. Tab strip: pills inside one NSGlassEffectContainerView (src/bro_tabstrip.mm)
  4. Overlays, one each: ⌘K palette (src/bro_palette.mm), downloads popover (src/bro_downloads.mm), hover card, zoom HUD
  • Verify per commit: build clean; AX smoke (launch, new tab, toggle screenshot mode); both appearances eyeballed

Phase 3 — corner handoff. Where a surface is glass-wrapped, hand its radius to NSGlassEffectView.cornerRadius derived from src/bro_geometry.h's concentric rule; bro_geometry stays authoritative for non-glass surfaces.

Risks to test in the first afternoon, before committing to the full sweep

  • Compositor stacking: CEF renders its own GPU surface; glass sampling around it is the perf unknown. Test: chrome-row glass over a WebGL-heavy page (e.g. wakawaka.world), watch for dropped frames during tab drag
  • Legibility: Clear style over dark pages vs the 8pt screenshot-mode gutter — pick styles per-surface with real pages, not blank tabs

Context for the agent

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions