Skip to content

feat(v3/darwin): prepare AppKit layer for macOS 27 compatibility#5760

Draft
taliesin-ai wants to merge 5 commits into
masterfrom
feat/macos27-appkit-prep
Draft

feat(v3/darwin): prepare AppKit layer for macOS 27 compatibility#5760
taliesin-ai wants to merge 5 commits into
masterfrom
feat/macos27-appkit-prep

Conversation

@taliesin-ai

@taliesin-ai taliesin-ai commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What

Implements the P0 items of the macOS 27 "Golden Gate" AppKit compatibility spec for the v3 native macOS layer. Goal: no regressions in window chrome, native menus, or Liquid Glass when users upgrade to macOS 27 (beta 2 now, GA expected fall 2026).

Update: all 27-only API usage has now been validated against the real macOS 27.0 SDK (Xcode 27.0 beta 2, 27A5209h) — see P0-5 below.

P0-1 Gesture recognizer migration (TN3212)

Audit result: the native layer has no mouseDown:/mouseDragged:/mouseUp: NSResponder overrides and no nextEventMatching: loops. The deprecated input patterns it did have:

  • Two app-global NSEvent local monitors driving invisible-title-bar drag and the JS drag() event cache → replaced with a per-window WailsWindowMouseGestureObserver (an NSGestureRecognizer that forwards mouseDown:/mouseUp: to the window delegate and never claims the gesture, so webview event delivery is untouched). Gesture recognizers are the only input path that receives Sidecar/touch-synthesised events on macOS 27.
  • The system-tray pre-click NSEvent monitor → same observer pattern attached to the status-item button (rightMouseDown: included).
  • showMenu synthesised a fake mouseDown: NSEvent → now [statusItem.button performClick:nil]. (Likely relevant to [v3][macOS 27] Tray menu fails to open/render on click #5752 — tray menu reported broken on 27; the synthetic-event path is exactly what 27 stopped honouring. Verified no regression on 26 via VM + golwg; on-27 confirmation still pending.)
  • Note: the 27 SDK carries no formal API_DEPRECATED on monitors/responder overrides — the migration pressure is TN3212 guidance + Sidecar routing, so no compiler warnings would have flagged this.

P0-2 Menu icon visibility

All icon-bearing menu items funnel through setMenuItemBitmap; it now sets preferredImageVisibility to visible (runtime-guarded — the selector doesn't exist on ≤26, where images are always shown). Confirmed against the 27 SDK headers: NSMenuItemImageVisibilityVisible = 1, API_AVAILABLE(macos(27.0)) — matches the implementation exactly.

P0-3 Liquid Glass

  • Audit: the NSGlassEffectView path already embeds the webview in the glass contentView — the sibling-behind-content anti-pattern only exists in the NSVisualEffectView fallback used on ≤15, where the warning doesn't apply. No refactor needed there.
  • Grouping: the previous groupIdentifier/groupName KVC was a silent no-op (no such properties exist on NSGlassEffectView). Glass windows with a GroupID are now hosted in an NSGlassEffectContainerView (with GroupSpacingspacing). Confirmed against the 27 SDK: container API is unchanged from 26 (contentView + spacing); 27 only adds NSGlassEffectView.effectIsInteractive, which we don't need for parity.
  • Fixes a latent use-after-free: windowRemoveVisualEffects removed the glass view while the webview lived inside it — the hierarchy holds the only retain on the webview (webView property is assign), so re-applying glass deallocated it. The webview is now rescued back to the content view first, and retained across reparenting.

P0-4 Corner radius / concentricity

Reworked after checking the real 27 SDK: NSView.cornerConfiguration is readonly and subclass-overridable, not settable. Frameless windows now use WailsFramelessContentView, which returns a container-concentric configuration (NSViewCornerRadius.containerConcentricRadius via configurationWithRadius:) and applies the system-resolved effectiveCornerRadii to its backing layer in viewDidChangeEffectiveCornerRadii, falling back to the historic 8.0 if the radii resolve to zero. On ≤26 none of this machinery is invoked and the hardcoded 8.0 stands. Glass corner radius was already only set when explicitly configured, so unconfigured windows inherit the new system default automatically.

P0-5 Build / smoke test

  • Clean build of v3/pkg/... + go vet + go test ./pkg/application/ against both SDK 26.5 (Xcode 26.5) and macOS 27.0 SDK (Xcode 27.0 beta 2, 27A5209h).
  • Deprecation triage on the 27 SDK: zero new deprecations. The only warnings in the darwin layer are 10.14-era pre-existing debt (NSFilenamesPboardType in drag-and-drop, legacy NSVisualEffectMaterial values in the pre-26 fallback, NSStatusItem.setAttributedTitle:) — candidates for an unrelated cleanup PR.
  • Scripted smoke app (launch, normal + invisible-title-bar window, frameless window, glass window, grouped glass window, app menu with icon, tray create/destroy, resize/center/focus, clean shutdown) run on macOS 26.4.1: passes when built against either SDK, output byte-identical to a clean-master baseline. The SDK-27-built binary running on 26 also validates the weak-linking/guard story.
  • Separately verified (VM + golwg UI automation): tray menu opens correctly on macOS 26 from this branch — no regression from the performClick: change.

P1 audit (non-blocking, all N/A today)

  • NSControl.Events: no hand-rolled event dispatch on custom NSControl subclasses exists.
  • NSToolbarItemGroupRole/NSSegmentedControlRole(.tabs): no NSToolbar/NSSegmentedControl tab navigation in the runtime.

Spec open questions

  • Q1: no mouse-event overrides exist outside the runtime's native layer (templates/examples clean).
  • Q2: Sidecar/touch has never been an explicitly supported config; this PR makes the input path compatible either way.
  • Q3: glass transparency is system-inherited only; no user-facing control in Wails.
  • Q4: needs a call — this PR targets "safe on beta, no regression on 26".

Remaining before GA

  1. Run on an actual macOS 27 seed (blocked on a 27 VM/box): the 27-only branches — menu-icon visibility, concentric corners, and whether performClick: fixes [v3][macOS 27] Tray menu fails to open/render on click #5752 on 27 — have been header-validated but never executed on a 27 runtime.
  2. Manual feel checks on 26: title-bar drag on frameless/invisible-title-bar windows (go run ./examples/liquid-glass), tray click menus, glass visuals.
  3. Re-run the P0 checklist against each new beta seed (test matrix: 26 baseline → 27 beta → 27 GA).

macOS 27 deprecates NSResponder mouse overrides, synthesised mouse
NSEvents and NSEvent monitors as input paths (TN3212); they also miss
Sidecar/touch-synthesised input. It further hides menu-item images by
default, tightens the default window corner radius, and warns against
managing grouped glass surfaces as loose siblings.

- Replace the app-global NSEvent left-mouse monitors with a per-window
  NSGestureRecognizer observer that forwards mouseDown/mouseUp to the
  window delegate (title-bar drag and JS-initiated drag are unchanged).
  The observer never claims the gesture, so webview event delivery is
  unaffected.
- Replace the system-tray pre-click NSEvent monitor with the same
  observer pattern on the status-item button, and the synthesised
  mouseDown: in showMenu with performClick:.
- Opt menu-item images back in explicitly via preferredImageVisibility
  (runtime-guarded; no-op on macOS <= 26 where the selector is absent).
- Host grouped Liquid Glass surfaces in an NSGlassEffectContainerView
  (with spacing) instead of the non-functional groupIdentifier KVC.
- Rescue the webview before stripping glass views in
  windowRemoveVisualEffects and retain it across reparenting: the view
  hierarchy holds the only retain, so re-applying glass previously
  deallocated the webview (latent use-after-free).
- Adopt NSViewCornerConfiguration containerConcentric for frameless
  windows when available (macOS 27), keeping the 8.0 radius fallback.
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2d1466a7-b5b5-49d7-8921-6789bf5c7bd4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/macos27-appkit-prep

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.

Validated against the macOS 27.0 SDK (Xcode 27.0 beta 2, 27A5209h):

- NSView.cornerConfiguration is readonly and subclass-overridable, not
  settable as previously assumed. Frameless windows now use a
  WailsFramelessContentView that returns a container-concentric
  configuration and applies the system-resolved effectiveCornerRadii to
  its backing layer in viewDidChangeEffectiveCornerRadii, falling back
  to the historic 8.0 when the radii resolve to nothing. Inert on <= 26.
- NSMenuItem.preferredImageVisibility and NSGlassEffectContainerView
  usages confirmed to match the 27 SDK headers exactly; no changes.
- SDK 27 deprecation triage: the only warnings in the darwin layer are
  10.14-era (NSFilenamesPboardType, legacy NSVisualEffectMaterial
  values in the pre-26 fallback, NSStatusItem setAttributedTitle:);
  macOS 27 deprecates nothing new in our usage. NSEvent monitors and
  NSResponder mouse overrides carry no formal API_DEPRECATED in the 27
  SDK - the migration pressure is TN3212 guidance plus Sidecar/touch
  routing.
@taliesin-ai

taliesin-ai commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Visual parity verification: Liquid Glass is unchanged (pixel-identical)

Concern raised: the glass refactor (container grouping + webview rescue) must not lose or alter the existing Liquid Glass effect.

Method: built the examples/liquid-glass demo and a two-window grouped-glass probe (one plain window, one with GroupID+GroupSpacing, fixed coordinates) from both clean master (a84a490) and this branch. Ran all four binaries sequentially in the same disposable macOS 26.3 Tart VM against a static wallpaper, captured full-screen PNGs, and pixel-diffed the frames below the menu bar (menu bar excluded: app name/clock differ by design).

Results:

  • liquid-glass example, master vs branch: meanAbsDiff 0.000, maxDiff 0, no differing pixels (after controlling for run-to-run glass animation sampling — a re-run of the same master binary against itself shows meanAbsDiff 1.749 in the right-edge windows, which fully accounts for any first-pass delta).
  • Grouped-glass probe, master vs branch: pixel-identical below the menu bar (diff bounding box covers only the menu-bar app name + clock). Static wallpaper control regions: 0.000 diff, confirming aligned deterministic captures.
  • All glass styles visually confirmed intact in the captures: Light, Dark, Vibrant, Blue Tint, Sheet Material, plus the grouped window — translucency, tint, corner radius, and content all render.

Note on the grouped path: on macOS 26.3 the NSGlassEffectContainerView branch did execute in the branch build (the class exists on 26), and still renders identically to master's ungrouped glass for a single surface — the container only changes behavior when multiple glass surfaces merge, which matches its documented purpose.

Remaining (unchanged): the same visual pass on an actual macOS 27 seed once a VM/box exists, where the system-wide transparency slider range also needs checking per the spec.

Branch:

shot3-grp-branch shot3-lg-branch

Master:

shot3-grp-master shot3-lg-master

Review feedback: the gesture-recognizer migration and performClick: change
should not alter macOS 26 behaviour. Wrap every input-path change in an
@available(macOS 27.0, *) check so <= 26 keeps its exact original code:

- App-global NSEvent left-mouse monitors: restored on <= 26; the
  per-window WailsWindowMouseGestureObserver is attached only on 27+.
- System tray: the pre-click NSEvent monitor is restored on <= 26; the
  status-item gesture observer is used only on 27+. Controller keeps both
  eventMonitor and gestureObserver; teardown releases whichever is set.
- showMenu: the synthesised mouseDown: NSEvent path is restored on <= 26;
  performClick: is used only on 27+.
- Liquid Glass grouping: the NSGlassEffectContainerView branch runs only
  on 27+; <= 26 keeps the historic bare glass view (the old groupID KVC
  was a no-op anyway, so no behaviour is lost).

Menu-item preferredImageVisibility and the frameless corner configuration
were already inert on <= 26 (guarded by respondsToSelector / an overridden
readonly property the OS never calls), so they are unchanged.

Verified on macOS 26.3 (Tart VM): injected a title-bar drag with CGEvent
on a frameless invisible-title-bar window built from this branch; the
window frame tracked the cursor delta exactly, confirming the <= 26
monitor path still drives dragging.
@taliesin-ai

Copy link
Copy Markdown
Collaborator Author

Fix: macOS 27 input changes are now guarded; macOS ≤26 behaviour is byte-for-byte unchanged

Good catch on the review — the earlier revision replaced the macOS 26 NSEvent monitors with the gesture recognizer on all versions, which changed 26's drag path even though it kept working. That's now corrected: every input-path change is wrapped in @available(macOS 27.0, *), so ≤26 runs its original code verbatim.

Path macOS ≤26 (unchanged) macOS 27+ (new)
Window drag / JS drag cache app-global NSEvent left-mouse monitors (restored) per-window WailsWindowMouseGestureObserver
Tray pre-click NSEvent local monitor (restored) status-item gesture observer
Tray showMenu synthesised mouseDown: NSEvent (restored) performClick:
Grouped Liquid Glass bare glass view (historic; old groupID KVC was a no-op) NSGlassEffectContainerView

preferredImageVisibility and the frameless corner configuration were already inert on ≤26 (guarded by respondsToSelector / an overridden readonly property the OS never invokes before 27), so they needed no change.

Empirical proof drag still works on macOS 26.3

Built a frameless invisible-title-bar window from this branch, ran it in a macOS 26.3 Tart VM, and injected a real title-bar drag with CGEvent (cursor 400,230600,380, delta +200,+150). The window frame tracked the cursor by exactly that delta — before/after screenshots confirm the window moved right-and-down while staying under the cursor. So the ≤26 monitor path drives dragging identically to master.

Rebuilt clean against both the 26.5 and 27.0 SDKs after the change; go test ./pkg/application/ green. Branch also merged up to current master.

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.

[v3][macOS 27] Tray menu fails to open/render on click

2 participants