-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Adopt macOS 26 Liquid Glass design #2647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
850d29c
9db1c3a
9eb5e7d
c89a423
4d913ed
894ec52
dcffaf9
58cbc7b
2024e4d
cdf6eb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,20 +22,37 @@ func cmuxJavaScriptStringLiteral(_ value: String?) -> String? { | |
|
|
||
| final class MainWindowHostingView<Content: View>: NSHostingView<Content> { | ||
| private let zeroSafeAreaLayoutGuide = NSLayoutGuide() | ||
| private let usesSystemSafeArea: Bool | ||
|
|
||
| override var safeAreaInsets: NSEdgeInsets { NSEdgeInsetsZero } | ||
| override var safeAreaRect: NSRect { bounds } | ||
| override var safeAreaLayoutGuide: NSLayoutGuide { zeroSafeAreaLayoutGuide } | ||
| override var safeAreaInsets: NSEdgeInsets { | ||
| usesSystemSafeArea ? super.safeAreaInsets : NSEdgeInsetsZero | ||
| } | ||
| override var safeAreaRect: NSRect { | ||
| usesSystemSafeArea ? super.safeAreaRect : bounds | ||
| } | ||
| override var safeAreaLayoutGuide: NSLayoutGuide { | ||
| usesSystemSafeArea ? super.safeAreaLayoutGuide : zeroSafeAreaLayoutGuide | ||
| } | ||
|
|
||
| required init(rootView: Content) { | ||
| if #available(macOS 26.0, *) { | ||
| // On macOS 26, use system safe area so: | ||
| // - Sidebar (.ignoresSafeArea) extends under the glass titlebar | ||
| // - Terminal content respects the titlebar and stays below it | ||
| self.usesSystemSafeArea = true | ||
| } else { | ||
| self.usesSystemSafeArea = false | ||
| } | ||
| super.init(rootView: rootView) | ||
| addLayoutGuide(zeroSafeAreaLayoutGuide) | ||
| NSLayoutConstraint.activate([ | ||
| zeroSafeAreaLayoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor), | ||
| zeroSafeAreaLayoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor), | ||
| zeroSafeAreaLayoutGuide.topAnchor.constraint(equalTo: topAnchor), | ||
| zeroSafeAreaLayoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor), | ||
| ]) | ||
| if !usesSystemSafeArea { | ||
| addLayoutGuide(zeroSafeAreaLayoutGuide) | ||
| NSLayoutConstraint.activate([ | ||
| zeroSafeAreaLayoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor), | ||
| zeroSafeAreaLayoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor), | ||
| zeroSafeAreaLayoutGuide.topAnchor.constraint(equalTo: topAnchor), | ||
| zeroSafeAreaLayoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor), | ||
| ]) | ||
| } | ||
| } | ||
|
|
||
| @available(*, unavailable) | ||
|
|
@@ -1002,6 +1019,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent | |
| syncMenuBarExtraVisibility() | ||
| updateController.startUpdaterIfNeeded() | ||
| } | ||
| // Start the titlebar accessory controller on all versions so the | ||
| // notifications popover infrastructure is available. On macOS 26 | ||
| // the visual titlebar items come from SwiftUI .toolbar, but the | ||
| // popover is still managed by the accessory controller. | ||
| titlebarAccessoryController.start() | ||
| windowDecorationsController.start() | ||
| installMainWindowKeyObserver() | ||
|
|
@@ -5552,10 +5573,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent | |
| window.collectionBehavior.insert(.fullScreenDisallowsTiling) | ||
| } | ||
| window.title = "" | ||
| window.titleVisibility = .hidden | ||
| window.titlebarAppearsTransparent = true | ||
| if #available(macOS 26.0, *) { | ||
| // On macOS 26+, let the system render the native glass titlebar | ||
| window.titleVisibility = .hidden | ||
| window.titlebarAppearsTransparent = false | ||
| } else { | ||
| window.titleVisibility = .hidden | ||
| window.titlebarAppearsTransparent = true | ||
| } | ||
| window.isMovableByWindowBackground = false | ||
| window.isMovable = false | ||
| if #available(macOS 26.0, *) { | ||
| window.isMovable = true | ||
| } else { | ||
| window.isMovable = false | ||
| } | ||
| let restoredFrame = resolvedWindowFrame(from: sessionWindowSnapshot) | ||
| if let restoredFrame { | ||
| window.setFrame(restoredFrame, display: false) | ||
|
|
@@ -8588,6 +8619,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent | |
| #endif | ||
|
|
||
| func attachUpdateAccessory(to window: NSWindow) { | ||
| if #available(macOS 26.0, *) { | ||
| // On macOS 26, toolbar buttons are native SwiftUI .toolbar items | ||
| // in the NavigationSplitView. Skip attaching the old titlebar | ||
| // accessory views, but the controller is already started (for | ||
| // notifications popover support). | ||
| return | ||
| } | ||
| titlebarAccessoryController.start() | ||
| titlebarAccessoryController.attach(to: window) | ||
| } | ||
|
|
@@ -8596,7 +8634,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent | |
| windowDecorationsController.apply(to: window) | ||
| } | ||
|
|
||
| /// Notification posted on macOS 26 to toggle the SwiftUI notifications popover. | ||
| static let toggleNotificationsPopoverNotification = Notification.Name("cmux.toggleNotificationsPopover") | ||
|
|
||
| func toggleNotificationsPopover(animated: Bool = true, anchorView: NSView? = nil) { | ||
| if #available(macOS 26.0, *) { | ||
| // Scope the broadcast to the owning window so a single bell-button | ||
| // tap or menu command only toggles the popover in the intended | ||
| // window — not in every cmux window at once. | ||
| let targetWindow = anchorView?.window ?? NSApp.keyWindow | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| let targetWindowId = targetWindow.flatMap { mainWindowContexts[ObjectIdentifier($0)]?.windowId } | ||
| var userInfo: [AnyHashable: Any] = [:] | ||
| if let targetWindowId { | ||
| userInfo["windowId"] = targetWindowId | ||
| } | ||
| NotificationCenter.default.post( | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| name: Self.toggleNotificationsPopoverNotification, | ||
| object: targetWindow, | ||
| userInfo: userInfo.isEmpty ? nil : userInfo | ||
| ) | ||
| return | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| titlebarAccessoryController.toggleNotificationsPopover(animated: animated, anchorView: anchorView) | ||
|
Comment on lines
+8637
to
8676
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Scope and complete the macOS 26 notifications presenter. This bridge only emits a global toggle and drops the originating window. The SwiftUI side therefore has no deterministic target window, and the explicit show/dismiss/state entry points in this file ( 🛠️ Suggested direction /// Notification posted on macOS 26 to toggle the SwiftUI notifications popover.
static let toggleNotificationsPopoverNotification = Notification.Name("cmux.toggleNotificationsPopover")
+ static let showNotificationsPopoverNotification = Notification.Name("cmux.showNotificationsPopover")
+ static let dismissNotificationsPopoverNotification = Notification.Name("cmux.dismissNotificationsPopover")
+ static let notificationsPopoverWindowIdUserInfoKey = "windowId"
func toggleNotificationsPopover(animated: Bool = true, anchorView: NSView? = nil) {
if `#available`(macOS 26.0, *) {
- NotificationCenter.default.post(name: Self.toggleNotificationsPopoverNotification, object: nil)
+ guard
+ let window = anchorView?.window ?? NSApp.keyWindow ?? NSApp.mainWindow,
+ let windowId = mainWindowId(for: window)
+ else { return }
+ NotificationCenter.default.post(
+ name: Self.toggleNotificationsPopoverNotification,
+ object: window,
+ userInfo: [Self.notificationsPopoverWindowIdUserInfoKey: windowId]
+ )
return
}
titlebarAccessoryController.toggleNotificationsPopover(animated: animated, anchorView: anchorView)
}Then route 🤖 Prompt for AI Agents
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Partially addressed in afb1af9 — the toggle broadcast now scopes to the owning window via There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Plan:
Proposed changes AppDelegate.swift
@@
/// Notification posted on macOS 26 to toggle the SwiftUI notifications popover.
static let toggleNotificationsPopoverNotification = Notification.Name("cmux.toggleNotificationsPopover")
+ /// Explicit show on macOS 26 (used by menu bar path).
+ static let showNotificationsPopoverNotification = Notification.Name("cmux.showNotificationsPopover")
+ /// Explicit dismiss on macOS 26 (used by Escape/menu bar and imperative callers).
+ static let dismissNotificationsPopoverNotification = Notification.Name("cmux.dismissNotificationsPopover")
+ /// SwiftUI presenter reports state back to AppDelegate on any present/dismiss.
+ static let notificationsPopoverVisibilityDidChangeNotification = Notification.Name("cmux.notificationsPopoverVisibilityDidChange")
+ /// userInfo keys shared across all of the above.
+ static let notificationsPopoverWindowIdUserInfoKey = "windowId"
+ static let notificationsPopoverVisibleUserInfoKey = "visible"
+
+ // Track popover visibility by window for isShown()/dismiss return value on macOS 26.
+ private var notificationsPopoverVisibleByWindowId: [UUID: Bool] = [:]
+ private var notificationsPopoverVisibilityObserver: NSObjectProtocol?
@@
func applicationDidFinishLaunching(_ notification: Notification) {
…
titlebarAccessoryController.start()
windowDecorationsController.start()
+ // Track SwiftUI-presented popover visibility on macOS 26.
+ installNotificationsPopoverVisibilityObserver()
…
}
@@
+ private func installNotificationsPopoverVisibilityObserver() {
+ guard notificationsPopoverVisibilityObserver == nil else { return }
+ notificationsPopoverVisibilityObserver = NotificationCenter.default.addObserver(
+ forName: Self.notificationsPopoverVisibilityDidChangeNotification,
+ object: nil,
+ queue: .main
+ ) { [weak self] note in
+ guard let self else { return }
+ guard let userInfo = note.userInfo,
+ let windowId = userInfo[Self.notificationsPopoverWindowIdUserInfoKey] as? UUID,
+ let visible = userInfo[Self.notificationsPopoverVisibleUserInfoKey] as? Bool else {
+ return
+ }
+ self.notificationsPopoverVisibleByWindowId[windowId] = visible
+ }
+ }
@@
- func toggleNotificationsPopover(animated: Bool = true, anchorView: NSView? = nil) {
- if `#available`(macOS 26.0, *) {
- // Scope the broadcast to the owning window so a single bell-button
- // tap or menu command only toggles the popover in the intended
- // window — not in every cmux window at once.
- let targetWindow = anchorView?.window ?? NSApp.keyWindow
- let targetWindowId = targetWindow.flatMap { mainWindowContexts[ObjectIdentifier($0)]?.windowId }
- var userInfo: [AnyHashable: Any] = [:]
- if let targetWindowId {
- userInfo["windowId"] = targetWindowId
- }
- NotificationCenter.default.post(
- name: Self.toggleNotificationsPopoverNotification,
- object: targetWindow,
- userInfo: userInfo.isEmpty ? nil : userInfo
- )
- return
- }
- titlebarAccessoryController.toggleNotificationsPopover(animated: animated, anchorView: anchorView)
+ func toggleNotificationsPopover(animated: Bool = true, anchorView: NSView? = nil) {
+ if `#available`(macOS 26.0, *) {
+ let targetWindow = anchorView?.window ?? NSApp.keyWindow ?? NSApp.mainWindow
+ guard let window = targetWindow, let windowId = mainWindowId(for: window) else { return }
+ NotificationCenter.default.post(
+ name: Self.toggleNotificationsPopoverNotification,
+ object: window,
+ userInfo: [Self.notificationsPopoverWindowIdUserInfoKey: windowId]
+ )
+ return
+ }
+ titlebarAccessoryController.toggleNotificationsPopover(animated: animated, anchorView: anchorView)
}
@@
- func showNotificationsPopoverFromMenuBar() {
+ func showNotificationsPopoverFromMenuBar() {
let context: MainWindowContext? = {
if let keyWindow = NSApp.keyWindow,
let keyContext = contextForMainTerminalWindow(keyWindow) {
return keyContext
}
if let first = mainWindowContexts.values.first {
return first
}
let windowId = createMainWindow()
return mainWindowContexts.values.first(where: { $0.windowId == windowId })
}()
- if let context,
- let window = context.window ?? windowForMainWindowId(context.windowId) {
- setActiveMainWindow(window)
- bringToFront(window)
- }
-
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
- self?.titlebarAccessoryController.showNotificationsPopover(animated: false)
- }
+ if let context, let window = context.window ?? windowForMainWindowId(context.windowId) {
+ setActiveMainWindow(window)
+ bringToFront(window)
+ if `#available`(macOS 26.0, *) {
+ NotificationCenter.default.post(
+ name: Self.showNotificationsPopoverNotification,
+ object: window,
+ userInfo: [Self.notificationsPopoverWindowIdUserInfoKey: context.windowId]
+ )
+ return
+ }
+ }
+ // Legacy path (< macOS 26)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
+ self?.titlebarAccessoryController.showNotificationsPopover(animated: false)
+ }
}
@@
- `@discardableResult`
- func dismissNotificationsPopoverIfShown() -> Bool {
- titlebarAccessoryController.dismissNotificationsPopoverIfShown()
- }
+ `@discardableResult`
+ func dismissNotificationsPopoverIfShown() -> Bool {
+ if `#available`(macOS 26.0, *) {
+ let window = NSApp.keyWindow ?? NSApp.mainWindow
+ guard let w = window, let windowId = mainWindowId(for: w) else { return false }
+ let wasVisible = notificationsPopoverVisibleByWindowId[windowId] == true
+ NotificationCenter.default.post(
+ name: Self.dismissNotificationsPopoverNotification,
+ object: w,
+ userInfo: [Self.notificationsPopoverWindowIdUserInfoKey: windowId]
+ )
+ // Optimistically flip to false; ContentView will confirm via visibilityDidChange.
+ notificationsPopoverVisibleByWindowId[windowId] = false
+ return wasVisible
+ }
+ return titlebarAccessoryController.dismissNotificationsPopoverIfShown()
+ }
@@
- func isNotificationsPopoverShown() -> Bool {
- titlebarAccessoryController.isNotificationsPopoverShown()
- }
+ func isNotificationsPopoverShown() -> Bool {
+ if `#available`(macOS 26.0, *) {
+ let window = NSApp.keyWindow ?? NSApp.mainWindow
+ guard let w = window, let windowId = mainWindowId(for: w) else { return false }
+ return notificationsPopoverVisibleByWindowId[windowId] == true
+ }
+ return titlebarAccessoryController.isNotificationsPopoverShown()
+ }
@@
private func unregisterMainWindow(_ window: NSWindow) {
…
- commandPaletteSnapshotByWindowId.removeValue(forKey: removed.windowId)
+ commandPaletteSnapshotByWindowId.removeValue(forKey: removed.windowId)
+ notificationsPopoverVisibleByWindowId.removeValue(forKey: removed.windowId)
…
}ContentView.swift (SwiftUI presenter)
// In onAppear/onReceive setup for notifications presenter on macOS 26:
if `#available`(macOS 26.0, *) {
let center = NotificationCenter.default
notificationObservers.append(
center.addObserver(forName: AppDelegate.toggleNotificationsPopoverNotification, object: nil, queue: .main) { note in
guard let targetId = (note.userInfo?[AppDelegate.notificationsPopoverWindowIdUserInfoKey] as? UUID),
targetId == self.windowId else { return }
isNotificationsPopoverPresented.toggle()
}
)
notificationObservers.append(
center.addObserver(forName: AppDelegate.showNotificationsPopoverNotification, object: nil, queue: .main) { note in
guard let targetId = (note.userInfo?[AppDelegate.notificationsPopoverWindowIdUserInfoKey] as? UUID),
targetId == self.windowId else { return }
isNotificationsPopoverPresented = true
}
)
notificationObservers.append(
center.addObserver(forName: AppDelegate.dismissNotificationsPopoverNotification, object: nil, queue: .main) { note in
guard let targetId = (note.userInfo?[AppDelegate.notificationsPopoverWindowIdUserInfoKey] as? UUID),
targetId == self.windowId else { return }
isNotificationsPopoverPresented = false
}
)
}
// Whenever `@State` changes (e.g., .popover presenter or onChange):
.onChange(of: isNotificationsPopoverPresented) { newValue in
if `#available`(macOS 26.0, *) {
NotificationCenter.default.post(
name: AppDelegate.notificationsPopoverVisibilityDidChangeNotification,
object: nil,
userInfo: [
AppDelegate.notificationsPopoverWindowIdUserInfoKey: windowId,
AppDelegate.notificationsPopoverVisibleUserInfoKey: newValue
]
)
}
}Result
Would you like me to push a follow-up commit on this PR with the above changes, or open a tracking issue and land it separately? ✏️ Learnings added
🧠 Learnings used |
||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
attachUpdateAccessory(to:)returns early without callingtitlebarAccessoryController.start()or.attach(to:), andapplicationDidFinishLaunchingalso skips.start()on macOS 26 (~line 2601). As a resultcontrolsControllers.allObjectsis always empty. When the SwiftUI toolbar bell callsAppDelegate.shared?.toggleNotificationsPopover(animated: true),UpdateTitlebarAccessoryController.toggleNotificationsPopoverbails immediately onguard !controllers.isEmpty else { return }— clicking the bell silently does nothing.Fix: either call
titlebarAccessoryController.start()(withoutattach) so the popover infrastructure is initialized, or route the bell action through a macOS-26-specific popover path that does not depend oncontrolsControllers.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
titlebarAccessoryController.start()is already called unconditionally at app launch (AppDelegate.swift:2605), socontrolsControllersis populated on macOS 26 too. On 26, the SwiftUI toolbar's bell button toggles local@Statedirectly, and menu/shortcut paths go throughtoggleNotificationsPopover→ NotificationCenter broadcast. The broadcast is now window-scoped in afb1af9.