fix: wrong popup shadow rendering after #652 and #566#707
Conversation
| for backingView in [window.contentView, window.contentView?.superview].compactMap({ $0 }) { | ||
| backingView.wantsLayer = true | ||
| backingView.layer?.backgroundColor = NSColor.clear.cgColor | ||
| backingView.layer?.borderWidth = 0 | ||
| backingView.layer?.shadowOpacity = 0 | ||
| backingView.layer?.masksToBounds = false | ||
| } |
There was a problem hiding this comment.
Modifying
contentView?.superview (NSThemeFrame) is fragile internal API
window.contentView?.superview resolves to AppKit's private NSThemeFrame. While compactMap safely handles a nil superview, setting wantsLayer, borderWidth, shadowOpacity, and masksToBounds directly on that internal view relies on an undocumented, stable view-hierarchy assumption. A future macOS 26 point release could introduce an intermediate wrapper between NSWindow and its contentView, silently bypassing these property writes and re-exposing the double-outline. Consider logging a warning when contentView?.superview is the class name you expect so regressions surface early rather than silently.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| window.invalidateShadow() | ||
| } | ||
| } |
There was a problem hiding this comment.
window.invalidateShadow() is a no-op here because window.hasShadow was set to false on the line above — there is no native Cocoa shadow left for the window manager to redraw. The shadow is owned entirely by SwiftUI's .shadow() modifier on the fill rectangle. Remove or replace with an explanatory comment to avoid implying it has an effect.
| window.invalidateShadow() | |
| } | |
| } | |
| // Note: invalidateShadow() is intentionally omitted here — hasShadow is false so the | |
| // native window manager shadow is disabled, and the visible shadow is owned by the | |
| // SwiftUI .shadow() modifier on the fill rectangle. | |
| } | |
| } |
Summary
This fixes the shadow and double-outline regression introduced by the interaction between the fixes for #566 and #652 on macOS 26. The menu bar popover now binds to and configures the underlying host
NSWindow, ensuring the native host window remains visually transparent while a single SwiftUI-rendered panel surface owns the visible background, border, and shadow.Validation
Linked issues
Fixes #704
Risk / rollout notes
NSWindowused byMenuBarExtra(.window).Solves #704
Greptile Summary
This PR fixes a shadow and double-outline regression on macOS 26 where the
MenuBarExtra(.window)popover rendered both the native host window's rounded chrome and the SwiftUI fill layer as separate visible surfaces. The fix clears the nativeNSWindowchrome via a newMenuBarWindowChromeConfiguratorhelper and transfers visual ownership of the background, border, and shadow entirely to a SwiftUI-renderedRoundedRectangle.MenuBarWindowChromeConfigurator(macOS 26 only): setswindow.isOpaque = false,backgroundColor = .clear,hasShadow = false, and strips layer chrome fromcontentViewand its superview so the host window is fully transparent.MenuBarWindowBackgroundModifier(macOS 26 branch): adds a SwiftUI.shadow()and.overlaystroke border to the fill rect, making this SwiftUI layer the sole visible rounded surface.MenuBarPopoverDismisserBinder: gains anonWindowBindcallback soMenuBarViewcan invokeconfigureMenuBarWindowIfNeededthe moment the host window becomes available.Confidence Score: 4/5
Safe to merge for the targeted macOS 26 regression; older macOS paths are untouched.
The macOS 26 fix is carefully gated and the changed logic is well-reasoned. The two areas that warrant a second look are the
window.invalidateShadow()call (harmless dead code afterhasShadow = false) and the direct manipulation ofcontentView?.superview— AppKit's internal NSThemeFrame — which is undocumented and could quietly stop working in a future macOS 26 point release if the view hierarchy gains an intermediate layer.MenuBarView.swift — specifically the
MenuBarWindowChromeConfigurator.configuremethod and its access tocontentView?.superview.Important Files Changed
MenuBarWindowChromeConfigurator(macOS 26 only) that strips the host NSWindow's native chrome and shadow, then draws a single SwiftUI-rendered rounded panel with its own fill, border, and shadow. Two minor concerns:invalidateShadow()is a no-op afterhasShadow = false, and accessingcontentView?.superview(NSThemeFrame) is fragile undocumented API.MenuBarPopoverDismisserBinderwith anonWindowBindcallback (defaulting to a no-op) so callers can react to the host window becoming available. The callback is correctly threaded throughmakeNSViewandupdateNSView, and fires inviewDidMoveToWindowguarded behind a non-nil window check. Straightforward and safe.Sequence Diagram
sequenceDiagram participant SwiftUI participant Binder as MenuBarPopoverDismisserBinder participant WBV as WindowBindingView participant Configurator as MenuBarWindowChromeConfigurator participant NSWin as NSWindow SwiftUI->>Binder: makeNSView(context:) Binder->>WBV: create view, set onWindowBind SwiftUI->>WBV: add to window hierarchy WBV->>WBV: viewDidMoveToWindow() WBV->>Configurator: onWindowBind(window) Configurator->>NSWin: "isOpaque = false" Configurator->>NSWin: "backgroundColor = .clear" Configurator->>NSWin: "hasShadow = false" Configurator->>NSWin: contentView layer — clear bg, no border, no shadow Configurator->>NSWin: contentView.superview layer — clear bg, no border, no shadow Note over SwiftUI,NSWin: SwiftUI RoundedRectangle now owns fill + shadow + borderReviews (1): Last reviewed commit: "fix(MenuBar): enhance popover window con..." | Re-trigger Greptile