From 679cafdc511ebbba1ff60d061cf02680acdfae54 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Sun, 8 Feb 2026 20:21:27 -0800 Subject: [PATCH] Fix update pill constraint feedback loop The pill never appeared because: 1. SwiftUI .frame(width:0, height:0) when idle poisoned fittingSize 2. AppKit constraints locked at 0x0 prevented expansion on state change 3. fittingSize always returned 0 due to active 0x0 constraints Fix: Remove zero-frame from SwiftUI (always render at natural size, use opacity only). Deactivate constraints before measuring fittingSize so they don't clamp the measurement. Pass visibility to sizeToolbarItem to set constraints to zero when idle or natural size when active. --- CHANGELOG.md | 5 +++++ GhosttyTabs.xcodeproj/project.pbxproj | 16 ++++++++-------- Sources/Update/UpdatePill.swift | 2 -- Sources/WindowToolbarController.swift | 22 +++++++++++++++++----- docs-site/content/docs/changelog.mdx | 5 +++++ 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93ca8eca..1e7f40d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to cmuxterm are documented here. +## [1.19.4] - 2026-02-09 + +### Fixed +- Update pill never appearing due to constraint/sizing feedback loop + ## [1.19.3] - 2026-02-09 ### Fixed diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index e0116a27..af21702e 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -538,7 +538,7 @@ CODE_SIGN_ENTITLEMENTS = ""; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 28; + CURRENT_PROJECT_VERSION = 29; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; @@ -554,7 +554,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.19.3; + MARKETING_VERSION = 1.19.4; OTHER_LDFLAGS = ( "-lc++", "-framework", @@ -583,7 +583,7 @@ CODE_SIGN_ENTITLEMENTS = ""; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 28; + CURRENT_PROJECT_VERSION = 29; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; @@ -599,7 +599,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.19.3; + MARKETING_VERSION = 1.19.4; OTHER_LDFLAGS = ( "-lc++", "-framework", @@ -652,10 +652,10 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 28; + CURRENT_PROJECT_VERSION = 29; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.19.3; + MARKETING_VERSION = 1.19.4; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.appuitests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -669,10 +669,10 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 28; + CURRENT_PROJECT_VERSION = 29; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.19.3; + MARKETING_VERSION = 1.19.4; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.appuitests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Sources/Update/UpdatePill.swift b/Sources/Update/UpdatePill.swift index ace5eb9b..a3ef524a 100644 --- a/Sources/Update/UpdatePill.swift +++ b/Sources/Update/UpdatePill.swift @@ -21,8 +21,6 @@ struct UpdatePill: View { UpdatePopoverView(model: model) } .opacity(visible ? 1 : 0) - .frame(width: visible ? nil : 0, height: visible ? nil : 0) - .clipped() } @ViewBuilder diff --git a/Sources/WindowToolbarController.swift b/Sources/WindowToolbarController.swift index c340ce88..dd7e5cc3 100644 --- a/Sources/WindowToolbarController.swift +++ b/Sources/WindowToolbarController.swift @@ -128,16 +128,17 @@ final class WindowToolbarController: NSObject, NSToolbarDelegate { let view = NonDraggableHostingView(rootView: UpdatePill(model: updateViewModel)) let key = ObjectIdentifier(toolbar) item.view = view - sizeToolbarItem(for: key, hostingView: view) + let visible = !updateViewModel.effectiveState.isIdle + sizeToolbarItem(for: key, hostingView: view, visible: visible) updateSizeCancellables[key]?.cancel() updateSizeCancellables[key] = updateViewModel.$state .receive(on: DispatchQueue.main) - .sink { [weak self, weak view] _ in + .sink { [weak self, weak view] newState in // @Published fires on willSet, so SwiftUI hasn't processed the // new state yet. Defer measurement to the next run loop cycle. DispatchQueue.main.async { [weak self, weak view] in guard let self, let view else { return } - self.sizeToolbarItem(for: key, hostingView: view) + self.sizeToolbarItem(for: key, hostingView: view, visible: !newState.isIdle) } } return item @@ -146,10 +147,19 @@ final class WindowToolbarController: NSObject, NSToolbarDelegate { return nil } - private func sizeToolbarItem(for key: ObjectIdentifier, hostingView: NSView) { + private func sizeToolbarItem(for key: ObjectIdentifier, hostingView: NSView, visible: Bool) { + // Deactivate existing constraints before measuring so they don't + // clamp fittingSize to zero (chicken-and-egg sizing problem). + if let constraints = updateViewConstraints[key] { + constraints.width.isActive = false + constraints.height.isActive = false + } + hostingView.invalidateIntrinsicContentSize() hostingView.layoutSubtreeIfNeeded() - let size = hostingView.fittingSize + let naturalSize = hostingView.fittingSize + let size = visible ? naturalSize : .zero + hostingView.setFrameSize(size) hostingView.setContentHuggingPriority(.required, for: .horizontal) hostingView.setContentHuggingPriority(.required, for: .vertical) @@ -157,6 +167,8 @@ final class WindowToolbarController: NSObject, NSToolbarDelegate { if let constraints = updateViewConstraints[key] { constraints.width.constant = size.width constraints.height.constant = size.height + constraints.width.isActive = true + constraints.height.isActive = true } else { let width = hostingView.widthAnchor.constraint(equalToConstant: size.width) let height = hostingView.heightAnchor.constraint(equalToConstant: size.height) diff --git a/docs-site/content/docs/changelog.mdx b/docs-site/content/docs/changelog.mdx index 3e575faa..c74f5ade 100644 --- a/docs-site/content/docs/changelog.mdx +++ b/docs-site/content/docs/changelog.mdx @@ -5,6 +5,11 @@ description: Release notes and version history for cmuxterm All notable changes to cmuxterm are documented here. +## [1.19.4] - 2026-02-09 + +### Fixed +- Update pill never appearing due to constraint/sizing feedback loop + ## [1.19.3] - 2026-02-09 ### Fixed