From ddc3f02203f1216104a3bb8165a325fccf8db810 Mon Sep 17 00:00:00 2001 From: Aforno Date: Sat, 29 Aug 2026 01:21:44 +0100 Subject: [PATCH] fix(desktop): keep notch panel frame under AppKit control - host SwiftUI in a fixed AppKit content view so layout animation cannot resize the panel - drop hosting safe-area insets and always disable SwiftUI sizing options --- .../Utilities/NotchPanelController.swift | 18 ++++++++++++++---- .../NotchPanelControllerTests.swift | 11 +++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/Sources/AgentsNotch/Utilities/NotchPanelController.swift b/Sources/AgentsNotch/Utilities/NotchPanelController.swift index acac3a6..730f627 100644 --- a/Sources/AgentsNotch/Utilities/NotchPanelController.swift +++ b/Sources/AgentsNotch/Utilities/NotchPanelController.swift @@ -1,6 +1,10 @@ import AppKit import SwiftUI +/// Keeps AppKit in sole control of the panel frame while the hosted SwiftUI +/// hierarchy animates its own layout inside these fixed bounds. +final class NotchPanelContentView: NSView {} + @MainActor final class NotchPanelController: NSWindowController { private let runtime: AppRuntime @@ -143,12 +147,18 @@ final class NotchPanelController: NSWindowController { let hostingView = NSHostingView(rootView: rootView) hostingView.wantsLayer = true hostingView.layer?.backgroundColor = NSColor.clear.cgColor + hostingView.safeAreaRegions = [] // Don't size the window from SwiftUI intrinsic content — the panel // frame is the single source of truth for bounds during animation. - if #available(macOS 13.0, *) { - hostingView.sizingOptions = [] - } - panel.contentView = hostingView + hostingView.sizingOptions = [] + + let contentView = NotchPanelContentView(frame: panel.contentLayoutRect) + contentView.wantsLayer = true + contentView.layer?.backgroundColor = NSColor.clear.cgColor + hostingView.frame = contentView.bounds + hostingView.autoresizingMask = [.width, .height] + contentView.addSubview(hostingView) + panel.contentView = contentView } private func observeDisplayChanges() { diff --git a/Tests/AgentsNotchTests/NotchPanelControllerTests.swift b/Tests/AgentsNotchTests/NotchPanelControllerTests.swift index 2c98c1a..5f816a0 100644 --- a/Tests/AgentsNotchTests/NotchPanelControllerTests.swift +++ b/Tests/AgentsNotchTests/NotchPanelControllerTests.swift @@ -2,6 +2,17 @@ import XCTest final class NotchPanelControllerTests: XCTestCase { + @MainActor + func testPanelKeepsHostingViewBehindAppKitSizingBoundary() throws { + let controller = NotchPanelController(runtime: AppRuntime(monitorProviders: false)) + let contentView = try XCTUnwrap(controller.window?.contentView) + + XCTAssertTrue(contentView is NotchPanelContentView) + XCTAssertEqual(contentView.subviews.count, 1) + XCTAssertEqual(contentView.subviews[0].autoresizingMask, [.width, .height]) + XCTAssertEqual(contentView.subviews[0].frame, contentView.bounds) + } + @MainActor func testSurfaceAvailabilityCallbackTracksPreferenceChanges() { let defaults = UserDefaults.standard