diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 89202895..3efebe5f 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -731,6 +731,11 @@ struct ContentView: View { updateSidebarResizerBandState() syncTrafficLightInset() } + // The portal stops following SwiftUI frames when the slide ends; sync it to the final one. + .onReceive(NotificationCenter.default.publisher(for: .programaSidebarToggleDidSettle, object: sidebarState)) { _ in + guard let observedWindow else { return } + TerminalWindowPortalRegistry.scheduleExternalGeometrySynchronize(for: observedWindow) + } .onChange(of: sidebarMatchTerminalBackground) { guard sidebarState.isVisible, sidebarBlendMode == SidebarBlendModeOption.withinWindow.rawValue else { return } diff --git a/Sources/Update/UpdateTitlebarAccessory.swift b/Sources/Update/UpdateTitlebarAccessory.swift index 28d84987..42b4bc4c 100644 --- a/Sources/Update/UpdateTitlebarAccessory.swift +++ b/Sources/Update/UpdateTitlebarAccessory.swift @@ -743,6 +743,14 @@ private final class TrafficLightsTrailingEdgeView: NSView { self?.scheduleMeasure() } } + // Measurements are skipped while the sidebar slides, so measure once it settles. + windowObservers.append( + NotificationCenter.default.addObserver( + forName: .programaSidebarToggleDidSettle, object: nil, queue: .main + ) { [weak self] _ in + self?.scheduleMeasure() + } + ) scheduleMeasure() } @@ -764,6 +772,8 @@ private final class TrafficLightsTrailingEdgeView: NSView { DispatchQueue.main.async { [weak self] in guard let self else { return } self.measurePending = false + // Mid-slide frames carry the transition offset; the settle notification re-measures. + guard !TerminalWindowPortalRegistry.isInteractiveGeometryResizeActive else { return } self.onChange?(self.measureZoomButtonMaxX()) } } diff --git a/Sources/VerticalTabsSidebar.swift b/Sources/VerticalTabsSidebar.swift index 0eec2ab9..63f0f574 100644 --- a/Sources/VerticalTabsSidebar.swift +++ b/Sources/VerticalTabsSidebar.swift @@ -6,6 +6,9 @@ import SwiftUI extension Notification.Name { static let programaSidebarVisibilityDidChange = Notification.Name("programaSidebarVisibilityDidChange") + /// Posted with the `SidebarState` as object when its show/hide animation finishes. + static let programaSidebarToggleDidSettle = + Notification.Name("programaSidebarToggleDidSettle") } final class SidebarState: ObservableObject { @@ -16,6 +19,17 @@ final class SidebarState: ObservableObject { } } @Published var persistedWidth: CGFloat + /// Slides whose completion has not run; released in deinit so a window closed + /// mid-slide cannot leave the global portal resize counter raised. + private var pendingSlides = 0 + + deinit { + let pending = pendingSlides + guard pending > 0 else { return } + Task { @MainActor in + for _ in 0..