From db3f7ea838cddc2e4b60d5be99df1177b0949e3e Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 14 Aug 2026 13:04:37 -0300 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20invert=20glass=20layout=20?= =?UTF-8?q?=E2=80=94=20Aside-style=20backdrop,=20opaque=20content=20cards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The window backdrop is now the sidebar's Liquid Glass surface: a full-window NSGlassEffectView underlay in the theme frame below the contentView, sampling the desktop through the standard transparent-window compositing path (no contentView replacement, no custom corner masks — AppKit owns the frame, which removes the transparent-corner class of bugs at the source). Terminal and browser panes are opaque elevated cards (12pt continuous radius, 8pt inset from edges and sidebar). The 0.82 glass opacity clamp is gone — the card is fully opaque by design. The sidebar renders flat on the backdrop with the system appearance, fixing the forced-dark sidebar in light mode; only pane pills keep terminal-derived contrast. Pane chrome portal now searches the theme frame for its host, minimal mode no longer pulls content under the titlebar in card layout, and a debug.viewtree socket command dumps the window's AppKit tree (frames, layer colors, effect-view state) for chrome-layering diagnosis. --- Sources/ContentView.swift | 15 ++ Sources/GhosttySurfaceScrollView.swift | 7 + Sources/GhosttyTerminalSupport.swift | 11 +- Sources/ProgramaGlassSettings.swift | 9 +- Sources/SidebarVisuals.swift | 81 ++------ Sources/TabItemView.swift | 6 +- Sources/TerminalController+Debug.swift | 41 ++++ Sources/TerminalController.swift | 2 + Sources/VerticalTabsSidebar.swift | 27 ++- Sources/WindowBrowserSlotView.swift | 6 + Sources/WindowChrome.swift | 271 ++++++++----------------- Sources/WindowPaneChromePortal.swift | 15 +- Sources/WorkspaceContentView.swift | 11 +- 13 files changed, 226 insertions(+), 276 deletions(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index ce920cd5..7567f3ce 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -664,6 +664,10 @@ struct ContentView: View { private var effectiveTitlebarPadding: CGFloat { if isMinimalMode { + // Inverted card layout: the card inset + pane tab strip own the top; + // pulling content up under the (hidden) titlebar overflows the card + // past the window edge. + if cardInsetAmount > 0 { return 0 } return isFullScreen ? 0 : -titlebarPadding } return titlebarPadding @@ -918,6 +922,15 @@ struct ContentView: View { return dir.isEmpty ? nil : dir } + @Environment(\.accessibilityReduceTransparency) private var accessibilityReduceTransparency + + /// Inverted glass layout: the terminal region floats as a rounded card over + /// the window's glass backdrop, inset from the edges and the sidebar. + private var cardInsetAmount: CGFloat { + WindowGlassEffect.isAvailable && !accessibilityReduceTransparency + ? WindowGlassEffect.contentCardInset : 0 + } + private var contentAndSidebarLayout: AnyView { let layout: AnyView // When matching terminal background, use HStack so both sidebar and terminal @@ -930,6 +943,7 @@ struct ContentView: View { layout = AnyView( ZStack(alignment: .leading) { terminalContentWithSidebarDropOverlay + .padding(cardInsetAmount) .padding(.leading, sidebarState.isVisible ? sidebarWidth : 0) if sidebarState.isVisible { sidebarView @@ -944,6 +958,7 @@ struct ContentView: View { sidebarView } terminalContentWithSidebarDropOverlay + .padding(cardInsetAmount) } ) } diff --git a/Sources/GhosttySurfaceScrollView.swift b/Sources/GhosttySurfaceScrollView.swift index 2d931607..ce2aceb1 100644 --- a/Sources/GhosttySurfaceScrollView.swift +++ b/Sources/GhosttySurfaceScrollView.swift @@ -416,6 +416,13 @@ final class GhosttySurfaceScrollView: NSView { super.init(frame: .zero) wantsLayer = true layer?.masksToBounds = true + // Inverted glass layout: each pane is an elevated card over the window + // backdrop. The layer already masks to bounds, so rounding it clips the + // Metal surface to the card shape for free. + if WindowGlassEffect.isAvailable { + layer?.cornerRadius = WindowGlassEffect.contentCardCornerRadius + layer?.cornerCurve = .continuous + } backgroundView.wantsLayer = true let initialTerminalBackground = GhosttyApp.shared.defaultBackgroundColor diff --git a/Sources/GhosttyTerminalSupport.swift b/Sources/GhosttyTerminalSupport.swift index b5e4e577..8ecb8ab6 100644 --- a/Sources/GhosttyTerminalSupport.swift +++ b/Sources/GhosttyTerminalSupport.swift @@ -23,15 +23,15 @@ func ghostty_surface_select_cursor_cell_compat(_ surface: ghostty_surface_t) -> func cmuxShouldApplyWindowGlass( sidebarBlendMode _: String, bgGlassEnabled: Bool, - glassEffectAvailable _: Bool, + glassEffectAvailable: Bool, performanceOverride: Bool? = nil ) -> Bool { if let performanceOverride { return performanceOverride } - // Window glass is independent from the sidebar material and blend mode. Native - // NSGlassEffectView vs NSVisualEffectView fallback is chosen in WindowGlassEffect.apply. - return bgGlassEnabled + // Inverted layout: on macOS 26 the glass backdrop is the stock window + // treatment, not an opt-in. Pre-26 keeps the legacy opt-in flag. + return glassEffectAvailable || bgGlassEnabled } func cmuxShouldUseTransparentBackgroundWindow() -> Bool { @@ -47,6 +47,9 @@ func cmuxShouldUseTransparentBackgroundWindow() -> Bool { } func cmuxShouldUseClearWindowBackground(for opacity: Double) -> Bool { + // The glass backdrop samples BEHIND the window, which requires a + // non-opaque window — same compositing the translucent-terminal mode has + // always used (standard frame, no custom masks, so no corner artifacts). cmuxShouldUseTransparentBackgroundWindow() || opacity < 0.999 } diff --git a/Sources/ProgramaGlassSettings.swift b/Sources/ProgramaGlassSettings.swift index 2a5955aa..24877e88 100644 --- a/Sources/ProgramaGlassSettings.swift +++ b/Sources/ProgramaGlassSettings.swift @@ -87,10 +87,13 @@ enum ProgramaGlassSettings { /// backgrounds retain enough tint for readable text while allowing the glass to show through. static func effectiveTerminalBackgroundOpacity( configuredOpacity: Double, - windowGlassEnabled: Bool + windowGlassEnabled _: Bool ) -> Double { - let clampedOpacity = min(1.0, max(0.0, configuredOpacity)) - return windowGlassEnabled ? min(clampedOpacity, 0.82) : clampedOpacity + // Inverted backdrop layout: the terminal pane is an opaque elevated card; + // the glass material lives behind it in the sidebar backdrop. The old + // 0.82 glass clamp forced a translucent terminal, which in turn forced a + // transparent window and killed the backdrop's window-server blur. + min(1.0, max(0.0, configuredOpacity)) } static let sidebarMigrationVersionKey = "sidebarAppearanceDefaultsVersion" diff --git a/Sources/SidebarVisuals.swift b/Sources/SidebarVisuals.swift index 5497022d..af7a7715 100644 --- a/Sources/SidebarVisuals.swift +++ b/Sources/SidebarVisuals.swift @@ -524,11 +524,11 @@ struct SidebarTerminalColorScheme: ViewModifier { @Environment(\.accessibilityReduceTransparency) private var accessibilityReduceTransparency @State private var scheme: ColorScheme = SidebarTerminalAppearance.colorScheme() - /// The native glass sidebar floats over the terminal-colored base plane, so its - /// content must always resolve contrast against the terminal, not the app scheme. + /// Inverted layout: the sidebar sits on the system-appearance window glass, so + /// it follows the system scheme. Only the explicit terminal-background option + /// still borrows the terminal's luminance. private var followsTerminal: Bool { - matchTerminalBackground || - (WindowGlassEffect.isAvailable && !accessibilityReduceTransparency) + matchTerminalBackground } func body(content: Content) -> some View { @@ -1237,13 +1237,13 @@ private struct SidebarVisualEffectBackground: NSViewRepresentable { } } -/// Hosts the complete interactive sidebar inside the native glass content host. AppKit does -/// not guarantee the z-order or rendering of controls added as arbitrary glass siblings. -private struct SidebarNativeGlassContentHost: NSViewRepresentable { +/// Hosts the complete interactive sidebar directly on the window's glass backdrop +/// (inverted, Aside-style layout). No inner glass surface: the window contentView +/// glass provides the material, so this host is a transparent AppKit container +/// whose only job is zeroing the window safe area — the sidebar owns its own +/// titlebar-like header row. +private struct SidebarBackdropContentHost: NSViewRepresentable { let content: Content - let tintColor: NSColor? - let cornerRadius: CGFloat - let appearance: NSAppearance? final class Coordinator { let hostingView: NSHostingView @@ -1253,7 +1253,7 @@ private struct SidebarNativeGlassContentHost: NSViewRepresentable hostingView.autoresizingMask = [.width, .height] hostingView.wantsLayer = true hostingView.layer?.backgroundColor = NSColor.clear.cgColor - // The panel owns its own titlebar-like header row; the window safe + // The sidebar owns its own titlebar-like header row; the window safe // area must not add a second inset on top of it. hostingView.safeAreaRegions = [] } @@ -1264,53 +1264,12 @@ private struct SidebarNativeGlassContentHost: NSViewRepresentable } func makeNSView(context: Context) -> NSView { - let hostingView = context.coordinator.hostingView - - #if compiler(>=6.2) - if #available(macOS 26.0, *) { - let glass = NSGlassEffectView(frame: .zero) - glass.autoresizingMask = [.width, .height] - glass.wantsLayer = true - glass.style = .regular - glass.tintColor = tintColor - glass.appearance = appearance - applyCornerMask(to: glass) - hostingView.frame = glass.bounds - glass.contentView = hostingView - return glass - } - #endif - - return hostingView + context.coordinator.hostingView } func updateNSView(_ nsView: NSView, context: Context) { context.coordinator.hostingView.rootView = content - - #if compiler(>=6.2) - if #available(macOS 26.0, *), let glass = nsView as? NSGlassEffectView { - glass.tintColor = tintColor - glass.appearance = appearance - applyCornerMask(to: glass) - } - #endif - } - - #if compiler(>=6.2) - @available(macOS 26.0, *) - private func applyCornerMask(to glass: NSGlassEffectView) { - glass.cornerRadius = cornerRadius - glass.layer?.cornerRadius = cornerRadius - glass.layer?.cornerCurve = .continuous - glass.layer?.maskedCorners = [ - .layerMinXMinYCorner, - .layerMaxXMinYCorner, - .layerMinXMaxYCorner, - .layerMaxXMaxYCorner, - ] - glass.layer?.masksToBounds = cornerRadius > 0 } - #endif } /// Owns the sidebar as a distinct native glass surface above the terminal-colored base plane. @@ -1332,16 +1291,12 @@ struct SidebarSurface: View { var body: some View { Group { if usesLocalNativeGlass { - // One radius, one mask: the glass host owns the corner treatment. - // The sidebar floats over the terminal-colored plane, so its glass and - // content must resolve against the terminal scheme, not the app scheme. - SidebarNativeGlassContentHost( - content: content.environment(\.colorScheme, terminalScheme), - tintColor: resolvedTintColor, - cornerRadius: standaloneCornerRadius, - appearance: NSAppearance(named: terminalScheme == .dark ? .darkAqua : .aqua) - ) - .frame(maxWidth: .infinity, maxHeight: .infinity) + // Inverted layout: the sidebar sits directly on the window's glass + // backdrop, which follows the SYSTEM appearance (light sidebar in + // light mode) — only the content card and its pills stay + // terminal-toned. No inner glass, no clip: window-level material. + SidebarBackdropContentHost(content: content) + .frame(maxWidth: .infinity, maxHeight: .infinity) } else { ZStack { SidebarBackdrop() diff --git a/Sources/TabItemView.swift b/Sources/TabItemView.swift index 932657cd..c8551158 100644 --- a/Sources/TabItemView.swift +++ b/Sources/TabItemView.swift @@ -709,10 +709,10 @@ struct TabItemView: View, Equatable { .padding(.horizontal, 10) .padding(.vertical, 8) .background( - RoundedRectangle(cornerRadius: 6) + RoundedRectangle(cornerRadius: 8, style: .continuous) .fill(backgroundColor) .overlay { - RoundedRectangle(cornerRadius: 6) + RoundedRectangle(cornerRadius: 8, style: .continuous) .strokeBorder(activeBorderColor, lineWidth: activeBorderLineWidth) } .overlay(alignment: .leading) { @@ -1168,7 +1168,7 @@ struct TabItemView: View, Equatable { } // Default: quiet neutral selection that harmonizes with the glass sidebar // instead of a saturated accent card. - return NSColor.labelColor.withAlphaComponent(0.14) + return NSColor.quaternaryLabelColor } private var backgroundColor: Color { diff --git a/Sources/TerminalController+Debug.swift b/Sources/TerminalController+Debug.swift index 806d897a..0ac18b0c 100644 --- a/Sources/TerminalController+Debug.swift +++ b/Sources/TerminalController+Debug.swift @@ -478,6 +478,47 @@ extension TerminalController { return .ok(payload) } + /// Dumps the key window's AppKit view tree with frames, visibility, and any + /// opaque layer background — chrome-layering bugs (a stray view painting + /// over content) are otherwise invisible to log-based diagnosis. + func v2DebugViewTree() -> V2CallResult { + let lines: [String] = v2MainSync { + guard let window = NSApp.keyWindow ?? NSApp.windows.first(where: { $0.isVisible && $0.contentView != nil }) else { + return [] + } + var out: [String] = [] + out.append( + "WINDOW isOpaque=\(window.isOpaque) bg=\(window.backgroundColor.hexString())@\(String(format: "%.3f", window.backgroundColor.alphaComponent)) " + + "appearance=\(window.effectiveAppearance.name.rawValue)" + ) + func walk(_ view: NSView, depth: Int) { + let frame = view.frame + var line = String(repeating: " ", count: depth) + line += String(describing: type(of: view)).prefix(48) + line += String(format: " (%.0f,%.0f %.0fx%.0f)", frame.origin.x, frame.origin.y, frame.width, frame.height) + if view.isHidden { line += " HIDDEN" } + if let bg = view.layer?.backgroundColor, let color = NSColor(cgColor: bg), color.alphaComponent > 0.01 { + line += " bg=\(color.hexString())@\(String(format: "%.2f", color.alphaComponent))" + } + if view.layer?.cornerRadius ?? 0 > 0 { + line += " r=\(Int(view.layer?.cornerRadius ?? 0))" + } + if let effect = view as? NSVisualEffectView { + line += " material=\(effect.material.rawValue) blend=\(effect.blendingMode.rawValue) " + + "state=\(effect.state.rawValue) alpha=\(String(format: "%.2f", effect.alphaValue)) " + + "emphasized=\(effect.isEmphasized)" + } + out.append(line) + for child in view.subviews { walk(child, depth: depth + 1) } + } + if let root = window.contentView?.superview ?? window.contentView { + walk(root, depth: 0) + } + return out + } + return .ok(["tree": lines]) + } + func v2DebugBonsplitUnderflowCount() -> V2CallResult { let resp = bonsplitUnderflowCount() guard resp.hasPrefix("OK ") else { return .err(code: "internal_error", message: resp, data: nil) } diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index bb97ca76..b342123a 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -2069,6 +2069,8 @@ class TerminalController { return v2Result(id: id, self.v2DebugLayout()) case "debug.portal.stats": return v2Result(id: id, self.v2DebugPortalStats()) + case "debug.viewtree": + return v2Result(id: id, self.v2DebugViewTree()) case "debug.bonsplit_underflow.count": return v2Result(id: id, self.v2DebugBonsplitUnderflowCount()) case "debug.bonsplit_underflow.reset": diff --git a/Sources/VerticalTabsSidebar.swift b/Sources/VerticalTabsSidebar.swift index fef7562b..f0868eb8 100644 --- a/Sources/VerticalTabsSidebar.swift +++ b/Sources/VerticalTabsSidebar.swift @@ -182,6 +182,12 @@ struct VerticalTabsSidebar: View { /// Content clearance inside the glass panel for the traffic lights and the /// always-visible titlebar controls that share the panel's top strip. private let trafficLightPadding: CGFloat = WindowGlassEffect.sidebarHeaderHeight + @Environment(\.accessibilityReduceTransparency) private var accessibilityReduceTransparency + + /// Inverted layout: sidebar rendered directly on the window glass backdrop. + private var usesBackdropSidebar: Bool { + WindowGlassEffect.isAvailable && !accessibilityReduceTransparency + } private let tabRowSpacing: CGFloat = 2 private var isMinimalMode: Bool { @@ -225,6 +231,9 @@ struct VerticalTabsSidebar: View { HiddenTitlebarSidebarControlsView(notificationStore: notificationStore) } .frame(height: trafficLightPadding) + // Flush sidebar (no panel inset): keep the header content on the + // 25pt traffic-light midline the decorations controller targets. + .padding(.top, usesBackdropSidebar ? WindowGlassEffect.sidebarPanelInset : 0) .contentShape(Rectangle()) .background( WindowDragHandleView() @@ -332,14 +341,16 @@ struct VerticalTabsSidebar: View { .accessibilityIdentifier("Sidebar") ZStack { - SidebarTerminalBasePlane() - .ignoresSafeArea() - - // Maps-style: the panel includes the traffic lights and simply pads - // its content below them; insets stay uniform so the panel radius is - // concentric with the window corner on all four sides. - SidebarSurface(content: sidebarContent) - .padding(WindowGlassEffect.sidebarPanelInset) + if usesBackdropSidebar { + // Inverted (Aside-style): the sidebar sits flush on the window's + // glass backdrop — no base plane, no panel inset. + SidebarSurface(content: sidebarContent) + } else { + SidebarTerminalBasePlane() + .ignoresSafeArea() + SidebarSurface(content: sidebarContent) + .padding(WindowGlassEffect.sidebarPanelInset) + } } .frame(maxWidth: .infinity, maxHeight: .infinity) .ignoresSafeArea() diff --git a/Sources/WindowBrowserSlotView.swift b/Sources/WindowBrowserSlotView.swift index 5ec81f3a..97459769 100644 --- a/Sources/WindowBrowserSlotView.swift +++ b/Sources/WindowBrowserSlotView.swift @@ -53,6 +53,12 @@ final class WindowBrowserSlotView: NSView { super.init(frame: frameRect) wantsLayer = true layer?.masksToBounds = true + // Inverted glass layout: browser panes are elevated cards like terminal + // panes (GhosttySurfaceScrollView carries the same radius). + if WindowGlassEffect.isAvailable { + layer?.cornerRadius = WindowGlassEffect.contentCardCornerRadius + layer?.cornerCurve = .continuous + } translatesAutoresizingMaskIntoConstraints = true autoresizingMask = [] diff --git a/Sources/WindowChrome.swift b/Sources/WindowChrome.swift index a54facde..986e5d36 100644 --- a/Sources/WindowChrome.swift +++ b/Sources/WindowChrome.swift @@ -52,193 +52,106 @@ enum WindowGlassEffect { /// Vertical midline of the header row measured from the window top. static var sidebarHeaderCenterFromWindowTop: CGFloat { sidebarPanelInset + sidebarHeaderHeight / 2 } - /// The stock tint (#000000 @ 0.03) is effectively clear, which lets the - /// desktop color wash through every translucent region and corner. Stock - /// resolves to a terminal-toned grounding tint; explicit user tints win. - static func resolvedWindowTint(hex: String, opacity: Double) -> NSColor { + /// Inverted (Aside-style) layout: the window backdrop is the sidebar's + /// material, sampling the desktop and following the system appearance. + /// Stock (#000000 @ 0.03) means untinted system glass; explicit user + /// tints still win. + static func resolvedWindowTint(hex: String, opacity: Double) -> NSColor? { let normalized = hex.trimmingCharacters(in: .whitespacesAndNewlines) .replacingOccurrences(of: "#", with: "").uppercased() let isStock = normalized == "000000" && abs(opacity - 0.03) < 0.001 if isStock { - return GhosttyBackgroundTheme.currentColor().withAlphaComponent(0.55) + return nil } return (NSColor(hex: hex) ?? .black).withAlphaComponent(opacity) } - private static var fullScreenObserverKey: UInt8 = 0 - - @available(macOS 26.0, *) - private static func applyWindowCornerMask(to glassView: NSView, rounded: Bool) { - glassView.layer?.cornerRadius = rounded ? windowCornerRadius : 0 - glassView.layer?.cornerCurve = .continuous - glassView.layer?.masksToBounds = rounded - } - - /// The transparent titlebar still hosts a material backdrop that peeks out - /// in the crescent between our large corner mask and the legacy frame shape - /// at the two top corners — a lighter glitchy wedge. Hide the material; the - /// buttons and accessories are separate views and stay visible. - private static func hideTitlebarBackdrop(in window: NSWindow) { - guard let frameView = window.contentView?.superview else { return } - for child in frameView.subviews - where String(describing: type(of: child)).contains("NSTitlebarContainerView") { - for titlebarChild in descendants(of: child) - where titlebarChild is NSVisualEffectView { - titlebarChild.isHidden = true - } - } - } - - private static func descendants(of view: NSView) -> [NSView] { - view.subviews.flatMap { [$0] + descendants(of: $0) } - } - - @available(macOS 26.0, *) - private static func installFullScreenRadiusObservers(for window: NSWindow, glassView: NSView) { - let center = NotificationCenter.default - let tokens: [NSObjectProtocol] = [ - center.addObserver( - forName: NSWindow.willEnterFullScreenNotification, object: window, queue: .main - ) { [weak glassView] _ in - guard let glassView else { return } - applyWindowCornerMask(to: glassView, rounded: false) - }, - center.addObserver( - forName: NSWindow.willExitFullScreenNotification, object: window, queue: .main - ) { [weak glassView] _ in - guard let glassView else { return } - applyWindowCornerMask(to: glassView, rounded: true) - }, - // Shadow must re-derive from content alpha after size/shape changes. - center.addObserver( - forName: NSWindow.didEndLiveResizeNotification, object: window, queue: .main - ) { [weak window] _ in - window?.invalidateShadow() - }, - center.addObserver( - forName: NSWindow.didExitFullScreenNotification, object: window, queue: .main - ) { [weak window] _ in - DispatchQueue.main.async { window?.invalidateShadow() } - }, - ] - objc_setAssociatedObject(window, &fullScreenObserverKey, tokens, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } + /// Corner radius of the elevated content card (terminal/browser panes). + static let contentCardCornerRadius: CGFloat = 12 + /// Radius for floating glass controls: tab pills, icon capsule clusters. + static let controlCornerRadius: CGFloat = 10 + /// Gap between the content card and the window edges / sidebar. + static let contentCardInset: CGFloat = 8 + /// Inverted (Aside-style) backdrop. The window stays a completely standard + /// opaque AppKit window — system corner radius, system shadow, system frame. + /// The sidebar material is an NSVisualEffectView underlay that AppKit rounds + /// to the window shape itself, exactly like every native sidebar. No custom + /// corner mask, no non-opaque window, no contentView replacement: those were + /// the source of every "transparent corner" artifact, so they are gone, not + /// patched. static func apply(to window: NSWindow, tintColor: NSColor? = nil) { guard let originalContentView = window.contentView else { return } - // Check if we already applied glass (avoid re-wrapping) - if let existingGlass = objc_getAssociatedObject(window, &glassViewKey) as? NSView { - // Already applied, just update the tint - updateTint(on: existingGlass, color: tintColor, window: window) + if let existing = objc_getAssociatedObject(window, &glassViewKey) as? NSView { + updateTint(on: existing, color: tintColor, window: window) return } + let bounds = originalContentView.bounds + let backdrop: NSView #if compiler(>=6.2) if #available(macOS 26.0, *) { - applyGlass(to: window, originalContentView: originalContentView, tintColor: tintColor) - return + // Liquid Glass backdrop: samples the desktop through the transparent + // window and blurs it — the mechanism proven to work on macOS 26. + let glass = NSGlassEffectView(frame: bounds) + glass.style = .regular + glass.cornerRadius = 0 + glass.tintColor = tintColor + backdrop = glass + } else { + backdrop = Self.makeVisualEffectBackdrop(frame: bounds) } + #else + backdrop = Self.makeVisualEffectBackdrop(frame: bounds) #endif - applyVisualEffectFallback(to: window, originalContentView: originalContentView, tintColor: tintColor) - } - - #if compiler(>=6.2) - @available(macOS 26.0, *) - private static func applyGlass(to window: NSWindow, originalContentView: NSView, tintColor: NSColor?) { - let glassView = NSGlassEffectView(frame: originalContentView.bounds) - glassView.wantsLayer = true - // Match the modern large window radius (Maps-style); the sidebar panel's - // radius is derived as this minus its inset to stay concentric. Shape via - // a plain layer mask, not NSGlassEffectView.cornerRadius: the glass draws - // a specular rim at its own rounded boundary, which reads as a ghost - // border against dark content. A layer cut has no rim. - glassView.cornerRadius = 0 - // Opaque terminal-colored backing: backdrop sampling bleeds the desktop - // into a rim wherever fills are translucent, which reads as a glitchy - // inconsistent border. In-window glass elements are unaffected — they - // sample window content, not the desktop. - glassView.layer?.backgroundColor = - GhosttyBackgroundTheme.currentColor().withAlphaComponent(1.0).cgColor - applyWindowCornerMask(to: glassView, rounded: !window.styleMask.contains(.fullScreen)) - installFullScreenRadiusObservers(for: window, glassView: glassView) - // The system window backdrop keeps its own smaller-radius shape; between - // it and our larger mask it peeks out as a ghost arc in each corner. - // A clear window lets the shadow and edge follow the masked shape only. - window.isOpaque = false - window.backgroundColor = .clear - // A non-opaque window's shadow only re-derives from content alpha on - // explicit invalidation; without it the old square-ish shadow rings the - // corner crescents and they read as transparent holes. - DispatchQueue.main.async { [weak window] in - guard let window else { return } - hideTitlebarBackdrop(in: window) - window.invalidateShadow() - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak window] in - guard let window else { return } - hideTitlebarBackdrop(in: window) - window.invalidateShadow() - } - glassView.tintColor = tintColor - glassView.autoresizingMask = [.width, .height] - - // NSGlassEffectView is a full replacement for the contentView. - objc_setAssociatedObject(window, &originalContentViewKey, originalContentView, .OBJC_ASSOCIATION_RETAIN) - window.contentView = glassView - - // AppKit only guarantees correct control placement when the controls are owned by - // NSGlassEffectView.contentView. Portal-hosted terminal surfaces remain siblings below - // this host, where they continue to provide the source pixels sampled by the effect. - originalContentView.frame = glassView.bounds - originalContentView.autoresizingMask = [.width, .height] - originalContentView.wantsLayer = true - originalContentView.layer?.backgroundColor = NSColor.clear.cgColor - glassView.contentView = originalContentView - - objc_setAssociatedObject(window, &glassViewKey, glassView, .OBJC_ASSOCIATION_RETAIN) - } - #endif - private static func applyVisualEffectFallback(to window: NSWindow, originalContentView: NSView, tintColor: NSColor?) { - let bounds = originalContentView.bounds - let glassView = NSVisualEffectView(frame: bounds) - glassView.blendingMode = .behindWindow - // Favor a lighter fallback so behind-window glass reads more transparent. - glassView.material = .underWindowBackground - glassView.state = .active - glassView.wantsLayer = true - glassView.autoresizingMask = [.width, .height] - - // For the NSVisualEffectView fallback, do NOT replace window.contentView. - // Replacing contentView can break traffic light rendering with - // `.fullSizeContentView` + `titlebarAppearsTransparent`. - glassView.translatesAutoresizingMaskIntoConstraints = false - originalContentView.addSubview(glassView, positioned: .below, relativeTo: nil) + // Never replace window.contentView — that breaks traffic-light rendering + // with `.fullSizeContentView` + `titlebarAppearsTransparent`, and it is + // what forced the old custom-mask architecture. The backdrop also cannot + // be a SUBVIEW of the hosting view: NSHostingView draws pure-SwiftUI + // content into its own layer, and any subview — even positioned .below — + // sits above that drawing, occluding the sidebar. Install it as a theme- + // frame sibling below the contentView instead (the terminal portal's + // proven pattern), pinned to the contentView's geometry. + guard let themeFrame = originalContentView.superview else { return } + backdrop.translatesAutoresizingMaskIntoConstraints = false + themeFrame.addSubview(backdrop, positioned: .below, relativeTo: originalContentView) NSLayoutConstraint.activate([ - glassView.topAnchor.constraint(equalTo: originalContentView.topAnchor), - glassView.bottomAnchor.constraint(equalTo: originalContentView.bottomAnchor), - glassView.leadingAnchor.constraint(equalTo: originalContentView.leadingAnchor), - glassView.trailingAnchor.constraint(equalTo: originalContentView.trailingAnchor) + backdrop.topAnchor.constraint(equalTo: originalContentView.topAnchor), + backdrop.bottomAnchor.constraint(equalTo: originalContentView.bottomAnchor), + backdrop.leadingAnchor.constraint(equalTo: originalContentView.leadingAnchor), + backdrop.trailingAnchor.constraint(equalTo: originalContentView.trailingAnchor) ]) - // Add tint overlay between glass and content - if let tintColor { - let tintOverlay = NSView(frame: bounds) - tintOverlay.translatesAutoresizingMaskIntoConstraints = false - tintOverlay.wantsLayer = true - tintOverlay.layer?.backgroundColor = tintColor.cgColor - glassView.addSubview(tintOverlay) - NSLayoutConstraint.activate([ - tintOverlay.topAnchor.constraint(equalTo: glassView.topAnchor), - tintOverlay.bottomAnchor.constraint(equalTo: glassView.bottomAnchor), - tintOverlay.leadingAnchor.constraint(equalTo: glassView.leadingAnchor), - tintOverlay.trailingAnchor.constraint(equalTo: glassView.trailingAnchor) - ]) - objc_setAssociatedObject(window, &tintOverlayKey, tintOverlay, .OBJC_ASSOCIATION_RETAIN) + if let tintColor, !isGlassEffectView(backdrop) { + installTintOverlay(on: backdrop, color: tintColor, window: window) } - objc_setAssociatedObject(window, &glassViewKey, glassView, .OBJC_ASSOCIATION_RETAIN) + objc_setAssociatedObject(window, &glassViewKey, backdrop, .OBJC_ASSOCIATION_RETAIN) + } + + private static func makeVisualEffectBackdrop(frame: NSRect) -> NSVisualEffectView { + let view = NSVisualEffectView(frame: frame) + view.blendingMode = .behindWindow + view.material = .sidebar + view.state = .active + return view + } + + private static func installTintOverlay(on backdrop: NSView, color: NSColor, window: NSWindow) { + let tintOverlay = NSView(frame: backdrop.bounds) + tintOverlay.translatesAutoresizingMaskIntoConstraints = false + tintOverlay.wantsLayer = true + tintOverlay.layer?.backgroundColor = color.cgColor + backdrop.addSubview(tintOverlay) + NSLayoutConstraint.activate([ + tintOverlay.topAnchor.constraint(equalTo: backdrop.topAnchor), + tintOverlay.bottomAnchor.constraint(equalTo: backdrop.bottomAnchor), + tintOverlay.leadingAnchor.constraint(equalTo: backdrop.leadingAnchor), + tintOverlay.trailingAnchor.constraint(equalTo: backdrop.trailingAnchor) + ]) + objc_setAssociatedObject(window, &tintOverlayKey, tintOverlay, .OBJC_ASSOCIATION_RETAIN) } /// Update the tint color on an existing glass effect @@ -247,47 +160,25 @@ enum WindowGlassEffect { updateTint(on: glassView, color: color, window: window) } - private static func updateTint(on glassView: NSView, color: NSColor?, window: NSWindow) { + private static func updateTint(on backdrop: NSView, color: NSColor?, window: NSWindow) { #if compiler(>=6.2) - if #available(macOS 26.0, *), let glass = glassView as? NSGlassEffectView { + if #available(macOS 26.0, *), let glass = backdrop as? NSGlassEffectView { glass.tintColor = color - // Keep the opaque backing in step with the terminal theme. - glass.layer?.backgroundColor = - GhosttyBackgroundTheme.currentColor().withAlphaComponent(1.0).cgColor return } #endif - // For NSVisualEffectView fallback, update the tint overlay if let tintOverlay = objc_getAssociatedObject(window, &tintOverlayKey) as? NSView { tintOverlay.layer?.backgroundColor = color?.cgColor + } else if let color { + installTintOverlay(on: backdrop, color: color, window: window) } } static func remove(from window: NSWindow) { - guard let glassView = objc_getAssociatedObject(window, &glassViewKey) as? NSView else { + guard let backdrop = objc_getAssociatedObject(window, &glassViewKey) as? NSView else { return } - - if isGlassEffectView(glassView) { - if let originalContentView = objc_getAssociatedObject(window, &originalContentViewKey) as? NSView { - #if compiler(>=6.2) - if #available(macOS 26.0, *), let glass = glassView as? NSGlassEffectView { - glass.contentView = nil - } - #endif - originalContentView.removeFromSuperview() - originalContentView.autoresizingMask = [.width, .height] - originalContentView.frame = glassView.bounds - window.contentView = originalContentView - } - } else { - glassView.removeFromSuperview() - } - - if let tokens = objc_getAssociatedObject(window, &fullScreenObserverKey) as? [NSObjectProtocol] { - for token in tokens { NotificationCenter.default.removeObserver(token) } - } - objc_setAssociatedObject(window, &fullScreenObserverKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + backdrop.removeFromSuperview() objc_setAssociatedObject(window, &glassViewKey, nil, .OBJC_ASSOCIATION_RETAIN) objc_setAssociatedObject(window, &originalContentViewKey, nil, .OBJC_ASSOCIATION_RETAIN) objc_setAssociatedObject(window, &tintOverlayKey, nil, .OBJC_ASSOCIATION_RETAIN) diff --git a/Sources/WindowPaneChromePortal.swift b/Sources/WindowPaneChromePortal.swift index b401669d..a836baa7 100644 --- a/Sources/WindowPaneChromePortal.swift +++ b/Sources/WindowPaneChromePortal.swift @@ -277,10 +277,13 @@ final class WindowPaneChromePortalRegistry: NSObject, BonsplitPaneChromePortalBr // Called from every descriptor update and geometry pass; a full-window // recursive search each time is the dominant cost, so cache the host and // re-find only when it leaves the window. + // The terminal portal installs its host in the theme frame (the + // contentView's superview), so the search must start there. + let searchRoot = window.contentView?.superview ?? window.contentView let terminalHost: WindowTerminalHostView if let cached = cachedTerminalHost, cached.window === window { terminalHost = cached - } else if let found = findTerminalHost(in: window.contentView) { + } else if let found = findTerminalHost(in: searchRoot) { cachedTerminalHost = found terminalHost = found } else { @@ -578,7 +581,7 @@ private final class GlassIconClusterView: NSView { super.init(frame: .zero) #if compiler(>=6.2) glass.style = .regular - glass.cornerRadius = 14 + glass.cornerRadius = WindowGlassEffect.controlCornerRadius glass.contentView = container #endif addSubview(glass) @@ -659,7 +662,7 @@ private final class NativeGlassTabPillView: NSView, NSDraggingSource { super.init(frame: frameRect) #if compiler(>=6.2) glass.style = .regular - glass.cornerRadius = 14 + glass.cornerRadius = WindowGlassEffect.controlCornerRadius glass.contentView = control #endif addSubview(glass) @@ -836,7 +839,11 @@ private final class NativeTabPillControl: NSControl, NSMenuDelegate, NSDraggingS override var focusRingMaskBounds: NSRect { bounds } override func drawFocusRingMask() { - NSBezierPath(roundedRect: bounds, xRadius: 14, yRadius: 14).fill() + NSBezierPath( + roundedRect: bounds, + xRadius: WindowGlassEffect.controlCornerRadius, + yRadius: WindowGlassEffect.controlCornerRadius + ).fill() } override func keyDown(with event: NSEvent) { diff --git a/Sources/WorkspaceContentView.swift b/Sources/WorkspaceContentView.swift index 81577e70..6cca71a3 100644 --- a/Sources/WorkspaceContentView.swift +++ b/Sources/WorkspaceContentView.swift @@ -241,12 +241,18 @@ struct WorkspaceContentView: View { @AppStorage(ProgramaGlassSettings.overlaysEnabledKey) private var overlayLiquidGlassEnabled = false @Environment(\.colorScheme) private var colorScheme + @Environment(\.accessibilityReduceTransparency) private var accessibilityReduceTransparency @EnvironmentObject var notificationStore: TerminalNotificationStore private var isMinimalMode: Bool { WorkspacePresentationModeSettings.mode(for: workspacePresentationMode) == .minimal } + /// Inverted glass layout: content floats as an inset card (see ContentView). + private var usesCardLayout: Bool { + WindowGlassEffect.isAvailable && !accessibilityReduceTransparency + } + static func panelVisibleInUI( isWorkspaceVisible: Bool, isSelectedInPane: Bool, @@ -389,10 +395,13 @@ struct WorkspaceContentView: View { } Group { - if isMinimalMode && !isFullScreen { + if isMinimalMode && !isFullScreen && !usesCardLayout { bonsplitView .ignoresSafeArea(.container, edges: .top) } else { + // Card layout: the inset card owns its geometry — expanding + // through the top safe area would push the card past the + // window edge. bonsplitView } } From 4c987e11d6c2915ba75150eeee8867507c27d686 Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 14 Aug 2026 13:15:43 -0300 Subject: [PATCH 02/13] polish: round the card's pane background, sidebar + chrome spacing grid The square corners were bonsplit's pane background painting the full pane rect; it is now clipped to the card shape. Sidebar paddings land on a 4pt grid (edges on 8): workspace rows, footer, quota rows; the off-grid scroll spacer is gone. Control clusters use 8pt top offset and gaps. --- Sources/SidebarQuotaFooter.swift | 5 +++-- Sources/SidebarVisuals.swift | 14 +++++++------- Sources/VerticalTabsSidebar.swift | 3 --- Sources/WindowPaneChromePortal.swift | 5 +++-- Sources/WorkspaceContentView.swift | 6 ++++++ 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Sources/SidebarQuotaFooter.swift b/Sources/SidebarQuotaFooter.swift index 79bc9abb..773cf730 100644 --- a/Sources/SidebarQuotaFooter.swift +++ b/Sources/SidebarQuotaFooter.swift @@ -20,8 +20,9 @@ struct SidebarQuotaFooter: View { window: snapshot.sevenDay ) } - .padding(.horizontal, 12) - .padding(.vertical, 6) + // Sidebar spacing grid: 4pt base, edges on 8. + .padding(.horizontal, 8) + .padding(.vertical, 4) } else { EmptyView() } diff --git a/Sources/SidebarVisuals.swift b/Sources/SidebarVisuals.swift index af7a7715..bd071891 100644 --- a/Sources/SidebarVisuals.swift +++ b/Sources/SidebarVisuals.swift @@ -22,10 +22,10 @@ struct SidebarFooter: View { #if DEBUG SidebarDevFooter(updateViewModel: updateViewModel, onSendFeedback: onSendFeedback) #else + // Sidebar spacing grid: 4pt base, edges on 8. SidebarFooterButtons(updateViewModel: updateViewModel, onSendFeedback: onSendFeedback) - .padding(.leading, 6) - .padding(.trailing, 10) - .padding(.bottom, 6) + .padding(.horizontal, 8) + .padding(.bottom, 8) #endif } } @@ -487,7 +487,7 @@ private struct SidebarDevFooter: View { private var showSidebarDevBuildBanner = DevBuildBannerDebugSettings.defaultShowSidebarBanner var body: some View { - VStack(alignment: .leading, spacing: 6) { + VStack(alignment: .leading, spacing: 4) { SidebarFooterButtons(updateViewModel: updateViewModel, onSendFeedback: onSendFeedback) if showSidebarDevBuildBanner { Text(String(localized: "debug.devBuildBanner.title", defaultValue: "THIS IS A DEV BUILD")) @@ -495,9 +495,9 @@ private struct SidebarDevFooter: View { .foregroundColor(.red) } } - .padding(.leading, 6) - .padding(.trailing, 10) - .padding(.bottom, 6) + // Sidebar spacing grid: 4pt base, edges on 8. + .padding(.horizontal, 8) + .padding(.bottom, 8) } } #endif diff --git a/Sources/VerticalTabsSidebar.swift b/Sources/VerticalTabsSidebar.swift index f0868eb8..92051ec3 100644 --- a/Sources/VerticalTabsSidebar.swift +++ b/Sources/VerticalTabsSidebar.swift @@ -243,9 +243,6 @@ struct VerticalTabsSidebar: View { GeometryReader { proxy in ScrollView { VStack(spacing: 0) { - Spacer() - .frame(height: 6) - // Workspaces are bounded, so prefer a non-lazy stack here. // LazyVStack + drag-state invalidations can recurse through layout. VStack(spacing: tabRowSpacing) { diff --git a/Sources/WindowPaneChromePortal.swift b/Sources/WindowPaneChromePortal.swift index a836baa7..e50b771c 100644 --- a/Sources/WindowPaneChromePortal.swift +++ b/Sources/WindowPaneChromePortal.swift @@ -118,7 +118,8 @@ final class WindowPaneChromePortalRegistry: NSObject, BonsplitPaneChromePortalBr return } let barHeight: CGFloat = 28 - var y = hostView.bounds.maxY - barHeight - 5 + // Chrome spacing grid: 4pt base, edges on 8. + var y = hostView.bounds.maxY - barHeight - 8 newTabCluster.setActions([active.onNewTab, active.onNewBrowserTab]) newTabCluster.isHidden = false @@ -129,7 +130,7 @@ final class WindowPaneChromePortalRegistry: NSObject, BonsplitPaneChromePortalBr width: newTabCluster.preferredWidth, height: barHeight ) - y -= barHeight + 7 + y -= barHeight + 8 // Cap workspace splits at a 2x2-equivalent depth; deeper trees degenerate // into slivers. Checked here (live pane count) rather than at publish time, diff --git a/Sources/WorkspaceContentView.swift b/Sources/WorkspaceContentView.swift index 6cca71a3..7357cf91 100644 --- a/Sources/WorkspaceContentView.swift +++ b/Sources/WorkspaceContentView.swift @@ -405,6 +405,12 @@ struct WorkspaceContentView: View { bonsplitView } } + // The card's visible body is bonsplit's pane background; clip it to the + // card shape (portal-hosted surfaces above carry the same radius). + .clipShape(RoundedRectangle( + cornerRadius: usesCardLayout ? WindowGlassEffect.contentCardCornerRadius : 0, + style: .continuous + )) .background( WindowAccessor(dedupeByWindow: false) { window in if #available(macOS 26.0, *) { From 2755722daa8dd03a0aa3a72e33979270e40328ad Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 14 Aug 2026 13:19:22 -0300 Subject: [PATCH 03/13] =?UTF-8?q?polish:=20single-row=20strip=20=E2=80=94?= =?UTF-8?q?=20pills=20left,=20control=20capsules=20side=20by=20side=20righ?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both capsules share the tab strip's midline instead of stacking below the window top; the tab bar reserves their width so pills compress rather than slide under; capsules take the resting-pill surface so the strip's three backgrounds read as one material family. --- Sources/WindowPaneChromePortal.swift | 39 ++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/Sources/WindowPaneChromePortal.swift b/Sources/WindowPaneChromePortal.swift index e50b771c..b8a238fb 100644 --- a/Sources/WindowPaneChromePortal.swift +++ b/Sources/WindowPaneChromePortal.swift @@ -105,32 +105,39 @@ final class WindowPaneChromePortalRegistry: NSObject, BonsplitPaneChromePortalBr scheduleSettledSynchronize() } - /// Workspace-level controls follow the focused visible pane, stacked down the - /// right edge like Maps' control pills: new-tab capsule first, splits below. + /// Workspace-level controls follow the focused visible pane, sharing its tab + /// strip: pills on the left, both capsules side by side on the right, all + /// centered on the strip's midline. The pane's tab bar reserves trailing + /// space so pills never run under the controls. private func updateClusters() { // Anchors can move to another window (workspace drag-out); their stale // descriptors must not steer this window's controls. let inWindow = descriptors.values.filter { $0.anchorView?.window === window } let visible = inWindow.filter(\.isVisible) - guard let active = visible.first(where: { $0.isFocused }) ?? visible.first else { + guard let active = visible.first(where: { $0.isFocused }) ?? visible.first, + let anchor = active.anchorView else { newTabCluster.isHidden = true splitCluster.isHidden = true + for bar in bars.values { bar.trailingReservedWidth = 0 } return } let barHeight: CGFloat = 28 // Chrome spacing grid: 4pt base, edges on 8. - var y = hostView.bounds.maxY - barHeight - 8 + let gap: CGFloat = 8 + let strip = hostView.convert(anchor.bounds, from: anchor) + let y = strip.midY - barHeight / 2 newTabCluster.setActions([active.onNewTab, active.onNewBrowserTab]) newTabCluster.isHidden = false hostView.addSubview(newTabCluster) // keep above pane bars + let newTabX = strip.maxX - gap - newTabCluster.preferredWidth newTabCluster.frame = NSRect( - x: hostView.bounds.maxX - newTabCluster.preferredWidth - 8, + x: newTabX, y: y, width: newTabCluster.preferredWidth, height: barHeight ) - y -= barHeight + 8 + var reserved = gap + newTabCluster.preferredWidth // Cap workspace splits at a 2x2-equivalent depth; deeper trees degenerate // into slivers. Checked here (live pane count) rather than at publish time, @@ -151,14 +158,19 @@ final class WindowPaneChromePortalRegistry: NSObject, BonsplitPaneChromePortalBr splitCluster.isHidden = false hostView.addSubview(splitCluster) splitCluster.frame = NSRect( - x: hostView.bounds.maxX - splitCluster.preferredWidth - 8, + x: newTabX - gap - splitCluster.preferredWidth, y: y, width: splitCluster.preferredWidth, height: barHeight ) + reserved += gap + splitCluster.preferredWidth } else { splitCluster.isHidden = true } + + for (paneID, bar) in bars { + bar.trailingReservedWidth = paneID == active.paneID ? reserved : 0 + } } func removePaneChrome(for paneID: PaneID, anchorView: NSView) { @@ -472,6 +484,12 @@ private final class NativePaneTabBarView: NSView { private var pillViews: [TabID: NativeGlassTabPillView] = [:] private var descriptor: BonsplitPaneChromeDescriptor? + /// Space kept clear at the trailing edge for the workspace control capsules + /// that share this strip. + var trailingReservedWidth: CGFloat = 0 { + didSet { if oldValue != trailingReservedWidth { needsLayout = true } } + } + override init(frame frameRect: NSRect) { super.init(frame: frameRect) wantsLayer = true @@ -493,7 +511,9 @@ private final class NativePaneTabBarView: NSView { override func layout() { super.layout() - scrollView.frame = bounds.insetBy(dx: 8, dy: 5) + var scrollFrame = bounds.insetBy(dx: 8, dy: 5) + scrollFrame.size.width = max(0, scrollFrame.width - trailingReservedWidth) + scrollView.frame = scrollFrame layoutPills() // Early zero-sized layout passes can leave the clip view scrolled to a // negative vertical origin, which parks the whole tab row outside the @@ -601,6 +621,9 @@ private final class GlassIconClusterView: NSView { } actions = Array(repeating: {}, count: symbols.count) defaultTooltips = symbols.map(\.tooltip) + // Same resting surface as an unselected tab pill, so the strip's three + // capsules read as one material family. + alphaValue = 0.82 } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } From fb352b28e5d3e4e04b3938bfa2dcece26977b771 Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 14 Aug 2026 13:29:10 -0300 Subject: [PATCH 04/13] polish: strip rhythm on the grid, one surface tone across pills and capsules Strip is 44pt (28pt pill + 8/8); selected pills and control capsules share labelColor@0.12 at full presence, quiet pills stay untinted glass. --- Sources/WindowPaneChromePortal.swift | 18 +++++++++++------- .../Internal/Views/TabBarGlassSurfaces.swift | 5 +++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Sources/WindowPaneChromePortal.swift b/Sources/WindowPaneChromePortal.swift index b8a238fb..a6f86d24 100644 --- a/Sources/WindowPaneChromePortal.swift +++ b/Sources/WindowPaneChromePortal.swift @@ -511,7 +511,7 @@ private final class NativePaneTabBarView: NSView { override func layout() { super.layout() - var scrollFrame = bounds.insetBy(dx: 8, dy: 5) + var scrollFrame = bounds.insetBy(dx: 8, dy: 8) scrollFrame.size.width = max(0, scrollFrame.width - trailingReservedWidth) scrollView.frame = scrollFrame layoutPills() @@ -603,6 +603,8 @@ private final class GlassIconClusterView: NSView { #if compiler(>=6.2) glass.style = .regular glass.cornerRadius = WindowGlassEffect.controlCornerRadius + // Same surface tone as the selected pill (see applySurfaceState). + glass.tintColor = NSColor.labelColor.withAlphaComponent(0.12) glass.contentView = container #endif addSubview(glass) @@ -621,9 +623,9 @@ private final class GlassIconClusterView: NSView { } actions = Array(repeating: {}, count: symbols.count) defaultTooltips = symbols.map(\.tooltip) - // Same resting surface as an unselected tab pill, so the strip's three - // capsules read as one material family. - alphaValue = 0.82 + // Full-presence like the selected pill; the shared tint above keeps the + // strip's three capsule surfaces in one material family. + alphaValue = 1.0 } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -735,15 +737,17 @@ private final class NativeGlassTabPillView: NSView, NSDraggingSource { /// Accent fills fight the native material; tint with the text primary. private func applySurfaceState() { #if compiler(>=6.2) + // One shared surface tone across the strip: selected pills and the + // control capsules use labelColor@0.12; quiet pills stay untinted. if isSelected { - glass.tintColor = NSColor.labelColor.withAlphaComponent(0.16) + glass.tintColor = NSColor.labelColor.withAlphaComponent(0.12) } else if isHovered { - glass.tintColor = NSColor.labelColor.withAlphaComponent(0.08) + glass.tintColor = NSColor.labelColor.withAlphaComponent(0.06) } else { glass.tintColor = nil } #endif - alphaValue = isSelected ? 1.0 : (isHovered ? 0.94 : 0.82) + alphaValue = isSelected ? 1.0 : (isHovered ? 0.94 : 0.85) } func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation { diff --git a/vendor/bonsplit/Sources/Bonsplit/Internal/Views/TabBarGlassSurfaces.swift b/vendor/bonsplit/Sources/Bonsplit/Internal/Views/TabBarGlassSurfaces.swift index 926e09ed..e3986b33 100644 --- a/vendor/bonsplit/Sources/Bonsplit/Internal/Views/TabBarGlassSurfaces.swift +++ b/vendor/bonsplit/Sources/Bonsplit/Internal/Views/TabBarGlassSurfaces.swift @@ -2,8 +2,9 @@ import AppKit import SwiftUI enum TabBarGlassStyling { - static let verticalInset: CGFloat = 4 - static let barHeight: CGFloat = TabBarMetrics.tabHeight + (verticalInset * 2) + // Chrome spacing grid: 8pt above and below the 28pt pill row. + static let verticalInset: CGFloat = 8 + static let barHeight: CGFloat = 28 + (verticalInset * 2) static var isAvailable: Bool { #if compiler(>=6.2) From aae2c0455eb0ba605230f07581ff3e63b2aff78a Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 14 Aug 2026 13:35:55 -0300 Subject: [PATCH 05/13] feat: per-pane new-tab button in the strip, immediate chrome sync in live resize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Safari-style + capsule follows the last pill (scrolls with tabs, pills compress around its reserved seat) wired to the pane's new-terminal-tab action, with EN/JA strings. Chrome geometry syncs immediately during live resize instead of coalescing — the one-turn lag read as icons trailing the window edge. Pill gap joins the 8pt grid. --- Resources/Localizable.xcstrings | 17 +++++++++++++++++ Sources/WindowPaneChromePortal.swift | 22 +++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 60cd507e..60c9ef17 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -13456,6 +13456,23 @@ } } } + }, + "tabBar.newTab": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New Tab" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "新規タブ" + } + } + } } }, "version": "1.0" diff --git a/Sources/WindowPaneChromePortal.swift b/Sources/WindowPaneChromePortal.swift index a6f86d24..af4e7460 100644 --- a/Sources/WindowPaneChromePortal.swift +++ b/Sources/WindowPaneChromePortal.swift @@ -376,6 +376,12 @@ final class WindowPaneChromePortalRegistry: NSObject, BonsplitPaneChromePortalBr /// Coalesces ancestor-resize storms (divider drags, collapse animations) into /// one geometry pass per runloop turn. private func scheduleSynchronizeAll() { + // Live resize: a one-turn lag reads as chrome smearing behind the + // window edge — sync immediately (same trade the terminal portal makes). + if window?.inLiveResize == true { + synchronizeAll() + return + } guard !syncAllScheduled else { return } syncAllScheduled = true DispatchQueue.main.async { [weak self] in @@ -483,6 +489,10 @@ private final class NativePaneTabBarView: NSView { private let documentView = FlippedDocumentView(frame: .zero) private var pillViews: [TabID: NativeGlassTabPillView] = [:] private var descriptor: BonsplitPaneChromeDescriptor? + /// Safari-style "+" after the last pill; scrolls with the tabs. + private let newTabButton = GlassIconClusterView(symbols: [ + (name: "plus", tooltip: String(localized: "tabBar.newTab", defaultValue: "New Tab")), + ]) /// Space kept clear at the trailing edge for the workspace control capsules /// that share this strip. @@ -505,6 +515,7 @@ private final class NativePaneTabBarView: NSView { scrollView.verticalScrollElasticity = .none scrollView.documentView = documentView addSubview(scrollView) + documentView.addSubview(newTabButton) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -547,6 +558,7 @@ private final class NativePaneTabBarView: NSView { dragState: { [weak descriptor] active in descriptor?.onDragStateChanged(tab.id, active) } ) } + newTabButton.setActions([descriptor.onNewTab]) // Title changes arrive outside AppKit's layout cadence; needsLayout // reruns layoutPills() in the next pass (widths track intrinsic size). needsLayout = true @@ -554,16 +566,18 @@ private final class NativePaneTabBarView: NSView { private func layoutPills() { guard let descriptor else { return } - let gap: CGFloat = 7 + let gap: CGFloat = 8 let minPillWidth: CGFloat = 78 let leadingInset = max(0, descriptor.leadingInset) let height = max(28, scrollView.contentSize.height) let pills = descriptor.tabs.compactMap { pillViews[$0.id] } + let plusWidth = newTabButton.preferredWidth // Natural width per pill; only compress (which is what introduces - // truncation) once the row genuinely runs out of space. + // truncation) once the row genuinely runs out of space. The "+" always + // keeps its seat at the end of the row. var widths = pills.map { min(220, max(minPillWidth, $0.preferredWidth)) } - let available = scrollView.contentSize.width - leadingInset + let available = scrollView.contentSize.width - leadingInset - (gap + plusWidth) let naturalTotal = widths.reduce(0, +) + gap * CGFloat(max(0, widths.count - 1)) if naturalTotal > available, !widths.isEmpty { let evenWidth = (available - gap * CGFloat(widths.count - 1)) / CGFloat(widths.count) @@ -577,6 +591,8 @@ private final class NativePaneTabBarView: NSView { pill.layoutSubtreeIfNeeded() x += width + gap } + newTabButton.frame = NSRect(x: x, y: 0, width: plusWidth, height: height) + x += plusWidth + gap documentView.frame = NSRect(x: 0, y: 0, width: max(x, scrollView.contentSize.width), height: height) } } From 5a5433305ca2b7b0be7eddaadf0707500c726b26 Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 14 Aug 2026 13:54:35 -0300 Subject: [PATCH 06/13] polish: bare new-tab glyph, drop the dead titlebar band inside the card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The + is a plain 28pt ghost button, not a capsule peer of the pills. With the sidebar visible the card layout no longer reserves the legacy 32pt titlebar band (and skips the invisible titlebar overlay) — the strip owns the card top. Sidebar-hidden and fullscreen keep the band for the titlebar accessory controls. --- Sources/ContentView.swift | 11 +++++++---- Sources/WindowPaneChromePortal.swift | 25 +++++++++++++++++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 7567f3ce..9eb55111 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -663,10 +663,11 @@ struct ContentView: View { } private var effectiveTitlebarPadding: CGFloat { + // Inverted card layout with the sidebar visible: the strip owns the + // card's top — the legacy 32pt titlebar band is dead space there. With + // the sidebar hidden the titlebar accessory controls still need it. + if cardInsetAmount > 0 && sidebarState.isVisible { return 0 } if isMinimalMode { - // Inverted card layout: the card inset + pane tab strip own the top; - // pulling content up under the (hidden) titlebar overflows the card - // past the window edge. if cardInsetAmount > 0 { return 0 } return isFullScreen ? 0 : -titlebarPadding } @@ -732,7 +733,9 @@ struct ContentView: View { } .padding(.top, effectiveTitlebarPadding) .overlay(alignment: .top) { - if !isMinimalMode { + // Card layout with the sidebar visible has no titlebar band (see + // effectiveTitlebarPadding) — the overlay would float over pills. + if !isMinimalMode && !(cardInsetAmount > 0 && sidebarState.isVisible) { // Titlebar overlay is only over terminal content, not the sidebar. customTitlebar } diff --git a/Sources/WindowPaneChromePortal.swift b/Sources/WindowPaneChromePortal.swift index af4e7460..362d5586 100644 --- a/Sources/WindowPaneChromePortal.swift +++ b/Sources/WindowPaneChromePortal.swift @@ -489,10 +489,19 @@ private final class NativePaneTabBarView: NSView { private let documentView = FlippedDocumentView(frame: .zero) private var pillViews: [TabID: NativeGlassTabPillView] = [:] private var descriptor: BonsplitPaneChromeDescriptor? - /// Safari-style "+" after the last pill; scrolls with the tabs. - private let newTabButton = GlassIconClusterView(symbols: [ - (name: "plus", tooltip: String(localized: "tabBar.newTab", defaultValue: "New Tab")), - ]) + /// Safari-style "+" after the last pill; scrolls with the tabs. Bare glyph, + /// no capsule — it's an affordance, not a peer of the tab pills. + private let newTabButton: NSButton = { + let title = String(localized: "tabBar.newTab", defaultValue: "New Tab") + let button = NSButton(frame: .zero) + button.image = NSImage(systemSymbolName: "plus", accessibilityDescription: title) + button.isBordered = false + button.contentTintColor = .secondaryLabelColor + button.toolTip = title + button.setAccessibilityLabel(title) + return button + }() + private var newTabAction: (() -> Void)? /// Space kept clear at the trailing edge for the workspace control capsules /// that share this strip. @@ -515,9 +524,13 @@ private final class NativePaneTabBarView: NSView { scrollView.verticalScrollElasticity = .none scrollView.documentView = documentView addSubview(scrollView) + newTabButton.target = self + newTabButton.action = #selector(newTabPressed) documentView.addSubview(newTabButton) } + @objc private func newTabPressed() { newTabAction?() } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layout() { @@ -558,7 +571,7 @@ private final class NativePaneTabBarView: NSView { dragState: { [weak descriptor] active in descriptor?.onDragStateChanged(tab.id, active) } ) } - newTabButton.setActions([descriptor.onNewTab]) + newTabAction = descriptor.onNewTab // Title changes arrive outside AppKit's layout cadence; needsLayout // reruns layoutPills() in the next pass (widths track intrinsic size). needsLayout = true @@ -571,7 +584,7 @@ private final class NativePaneTabBarView: NSView { let leadingInset = max(0, descriptor.leadingInset) let height = max(28, scrollView.contentSize.height) let pills = descriptor.tabs.compactMap { pillViews[$0.id] } - let plusWidth = newTabButton.preferredWidth + let plusWidth: CGFloat = 28 // Natural width per pill; only compress (which is what introduces // truncation) once the row genuinely runs out of space. The "+" always From 41557b2fe27400514e203e71fb53e8890c7111fa Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 14 Aug 2026 14:02:05 -0300 Subject: [PATCH 07/13] chore: settings pass for the inverted layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the Match Terminal Background toggle and its scheme-forcing modifier — the sidebar follows the system appearance by design, and the modifier was the last path that could force terminal-derived contrast onto it. Drop the dead sidebarBlendMode parameter from cmuxShouldApplyWindowGlass and update its test to the new contract (glass is stock when available; the legacy opt-in only gates the pre-26 fallback). Legacy key readers in fallback and debug paths stay untouched. --- Sources/AppDelegate.swift | 1 - Sources/GhosttyTerminalSupport.swift | 3 --- Sources/SettingsView.swift | 15 +++-------- Sources/SidebarVisuals.swift | 28 ------------------- Sources/VerticalTabsSidebar.swift | 1 - programaTests/GhosttyConfigTests.swift | 37 ++++++++------------------ 6 files changed, 15 insertions(+), 70 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 42e058c1..30ac82bf 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -9261,7 +9261,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser // inside WindowGlassEffect.apply. let currentThemeBackground = GhosttyBackgroundTheme.currentColor() let shouldApplyWindowGlass = cmuxShouldApplyWindowGlass( - sidebarBlendMode: sidebarBlendMode, bgGlassEnabled: bgGlassEnabled, glassEffectAvailable: WindowGlassEffect.isAvailable ) diff --git a/Sources/GhosttyTerminalSupport.swift b/Sources/GhosttyTerminalSupport.swift index 8ecb8ab6..3327e830 100644 --- a/Sources/GhosttyTerminalSupport.swift +++ b/Sources/GhosttyTerminalSupport.swift @@ -21,7 +21,6 @@ func ghostty_surface_select_cursor_cell_compat(_ surface: ghostty_surface_t) -> #if os(macOS) func cmuxShouldApplyWindowGlass( - sidebarBlendMode _: String, bgGlassEnabled: Bool, glassEffectAvailable: Bool, performanceOverride: Bool? = nil @@ -36,10 +35,8 @@ func cmuxShouldApplyWindowGlass( func cmuxShouldUseTransparentBackgroundWindow() -> Bool { let defaults = UserDefaults.standard - let sidebarBlendMode = defaults.string(forKey: "sidebarBlendMode") ?? "withinWindow" let bgGlassEnabled = defaults.object(forKey: "bgGlassEnabled") as? Bool ?? false return cmuxShouldApplyWindowGlass( - sidebarBlendMode: sidebarBlendMode, bgGlassEnabled: bgGlassEnabled, glassEffectAvailable: WindowGlassEffect.isAvailable, performanceOverride: ProgramaGlassSettings.startupOverride(for: .window) diff --git a/Sources/SettingsView.swift b/Sources/SettingsView.swift index bbb41b81..5da87548 100644 --- a/Sources/SettingsView.swift +++ b/Sources/SettingsView.swift @@ -966,17 +966,10 @@ struct SettingsView: View { // calls WorkspaceTabColorSettings.reset() and nils both hex keys, so // anything set while these rows existed is still recoverable. - SettingsCardDivider() - - SettingsCardRow( - String(localized: "settings.sidebarAppearance.matchTerminalBackground", defaultValue: "Match Terminal Background"), - subtitle: String(localized: "settings.sidebarAppearance.matchTerminalBackground.subtitle", defaultValue: "Use the same background color and transparency as the terminal.") - ) { - Toggle("", isOn: $sidebarMatchTerminalBackground) - .labelsHidden() - .toggleStyle(.switch) - .controlSize(.small) - } + // "Match Terminal Background" removed with the inverted glass layout: + // the sidebar sits on the system-appearance glass backdrop by design, + // and the toggle's only remaining effect was forcing terminal-derived + // text contrast that fought the system scheme. SettingsCardDivider() diff --git a/Sources/SidebarVisuals.swift b/Sources/SidebarVisuals.swift index bd071891..3186ffeb 100644 --- a/Sources/SidebarVisuals.swift +++ b/Sources/SidebarVisuals.swift @@ -517,34 +517,6 @@ enum SidebarTerminalAppearance { } } -/// Applies the terminal-derived scheme to a subtree while the sidebar is matching the -/// terminal background, so every label in it picks contrast from the colour it sits on. -struct SidebarTerminalColorScheme: ViewModifier { - @AppStorage("sidebarMatchTerminalBackground") private var matchTerminalBackground = false - @Environment(\.accessibilityReduceTransparency) private var accessibilityReduceTransparency - @State private var scheme: ColorScheme = SidebarTerminalAppearance.colorScheme() - - /// Inverted layout: the sidebar sits on the system-appearance window glass, so - /// it follows the system scheme. Only the explicit terminal-background option - /// still borrows the terminal's luminance. - private var followsTerminal: Bool { - matchTerminalBackground - } - - func body(content: Content) -> some View { - Group { - if followsTerminal { - content.environment(\.colorScheme, scheme) - } else { - content - } - } - .onReceive(NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)) { _ in - scheme = SidebarTerminalAppearance.colorScheme() - } - } -} - struct SidebarTopScrim: View { let height: CGFloat @AppStorage("sidebarMatchTerminalBackground") private var matchTerminalBackground = false diff --git a/Sources/VerticalTabsSidebar.swift b/Sources/VerticalTabsSidebar.swift index 92051ec3..c9b2b476 100644 --- a/Sources/VerticalTabsSidebar.swift +++ b/Sources/VerticalTabsSidebar.swift @@ -404,7 +404,6 @@ struct VerticalTabsSidebar: View { draggedTabId = nil } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .modifier(SidebarTerminalColorScheme()) } private func debugShortSidebarTabId(_ id: UUID?) -> String { diff --git a/programaTests/GhosttyConfigTests.swift b/programaTests/GhosttyConfigTests.swift index 54eed2f9..7e825363 100644 --- a/programaTests/GhosttyConfigTests.swift +++ b/programaTests/GhosttyConfigTests.swift @@ -677,46 +677,31 @@ final class WindowTransparencyDecisionTests: XCTestCase { } } - func testGlassEnabledDecisionIsIndependentOfSidebarBlendAndImplementationAvailability() { + func testGlassIsStockWhenAvailableAndOptInOtherwise() { + // Legacy opt-in still decides when the native glass is unavailable. XCTAssertTrue( - cmuxShouldApplyWindowGlass( - sidebarBlendMode: "behindWindow", - bgGlassEnabled: true, - glassEffectAvailable: false - ) + cmuxShouldApplyWindowGlass(bgGlassEnabled: true, glassEffectAvailable: false) ) - XCTAssertTrue( - cmuxShouldApplyWindowGlass( - sidebarBlendMode: "behindWindow", - bgGlassEnabled: true, - glassEffectAvailable: true - ) + XCTAssertFalse( + cmuxShouldApplyWindowGlass(bgGlassEnabled: false, glassEffectAvailable: false) ) + // Inverted layout: glass is the stock treatment whenever available. XCTAssertTrue( - cmuxShouldApplyWindowGlass( - sidebarBlendMode: "withinWindow", - bgGlassEnabled: true, - glassEffectAvailable: true - ) + cmuxShouldApplyWindowGlass(bgGlassEnabled: false, glassEffectAvailable: true) ) - XCTAssertFalse( - cmuxShouldApplyWindowGlass( - sidebarBlendMode: "withinWindow", - bgGlassEnabled: false, - glassEffectAvailable: true - ) + XCTAssertTrue( + cmuxShouldApplyWindowGlass(bgGlassEnabled: true, glassEffectAvailable: true) ) + // The startup performance override wins in both directions. XCTAssertTrue( cmuxShouldApplyWindowGlass( - sidebarBlendMode: "withinWindow", bgGlassEnabled: false, - glassEffectAvailable: true, + glassEffectAvailable: false, performanceOverride: true ) ) XCTAssertFalse( cmuxShouldApplyWindowGlass( - sidebarBlendMode: "behindWindow", bgGlassEnabled: true, glassEffectAvailable: true, performanceOverride: false From 7e5eaf4ab3823d53309db57554f95f51898183b0 Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 14 Aug 2026 14:35:40 -0300 Subject: [PATCH 08/13] fix: zero the titlebar safe area inside pane hosting views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each bonsplit pane is hosted by its own NSHostingController, which was still honoring the window's titlebar safe area — ~24pt of dead space above the tab strip in full-size-content windows. The pane owns its own top, so the hosting controller's safeAreaRegions are cleared. --- .../Sources/Bonsplit/Internal/Views/SplitNodeView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vendor/bonsplit/Sources/Bonsplit/Internal/Views/SplitNodeView.swift b/vendor/bonsplit/Sources/Bonsplit/Internal/Views/SplitNodeView.swift index ea098c97..1cce1aaa 100644 --- a/vendor/bonsplit/Sources/Bonsplit/Internal/Views/SplitNodeView.swift +++ b/vendor/bonsplit/Sources/Bonsplit/Internal/Views/SplitNodeView.swift @@ -70,6 +70,10 @@ struct SinglePaneWrapper: NSViewRepresentable ) let hostingController = NSHostingController(rootView: paneView) hostingController.view.translatesAutoresizingMaskIntoConstraints = false + // This hosting view lives inside the pane, not under the titlebar; the + // window's titlebar safe area must not inset the tab strip (it read as + // ~24pt of dead space above the tabs in full-size-content windows). + hostingController.safeAreaRegions = [] let containerView = PaneDragContainerView() containerView.wantsLayer = true From 6c89e0f166b157b60722be542477f2bd806d41a6 Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 14 Aug 2026 14:36:52 -0300 Subject: [PATCH 09/13] fix: hold traffic lights in place through live resize Titlebar relayout snaps the buttons back to the stock corner on every resize tick; re-seat them synchronously per tick instead of only at didEndLiveResize. --- Sources/WindowDecorationsController.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/WindowDecorationsController.swift b/Sources/WindowDecorationsController.swift index 216c4b34..15e4ca04 100644 --- a/Sources/WindowDecorationsController.swift +++ b/Sources/WindowDecorationsController.swift @@ -33,6 +33,16 @@ final class WindowDecorationsController { // Titlebar layout resets button positions on resize and fullscreen churn. observers.append(center.addObserver(forName: NSWindow.didEndLiveResizeNotification, object: nil, queue: .main, using: handler)) observers.append(center.addObserver(forName: NSWindow.didExitFullScreenNotification, object: nil, queue: .main, using: handler)) + // Live resize relayouts the titlebar per frame and snaps the buttons back + // to the stock corner until the drag ends; re-seat them synchronously on + // every resize tick so they hold position through the whole drag. + observers.append(center.addObserver(forName: NSWindow.didResizeNotification, object: nil, queue: .main) { [weak self] notification in + guard let self, let window = notification.object as? NSWindow else { return } + guard window.inLiveResize else { return } + let hidden = self.shouldHideTrafficLights(for: window) + let offset = hidden ? NSPoint.zero : self.trafficLightOffset(for: window) + self.applyTrafficLightOffsetNow(on: window, offset: offset) + }) } private func attachToExistingWindows() { From 5b829c9f3327f808f7921711d97e403e58dc5e7f Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 14 Aug 2026 14:41:44 -0300 Subject: [PATCH 10/13] polish: align quota rows to the sidebar content line Bare rows (no card surface) align to card-edge + row-inset (16pt), not the 8pt card-edge line; 8pt to the window bottom. --- Sources/SidebarQuotaFooter.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/SidebarQuotaFooter.swift b/Sources/SidebarQuotaFooter.swift index 773cf730..869a50f3 100644 --- a/Sources/SidebarQuotaFooter.swift +++ b/Sources/SidebarQuotaFooter.swift @@ -20,9 +20,11 @@ struct SidebarQuotaFooter: View { window: snapshot.sevenDay ) } - // Sidebar spacing grid: 4pt base, edges on 8. - .padding(.horizontal, 8) - .padding(.vertical, 4) + // Sidebar spacing grid: bare rows align to the content line (card + // edge 8 + row inset 8), not the card-edge line. + .padding(.horizontal, 16) + .padding(.top, 4) + .padding(.bottom, 8) } else { EmptyView() } From 163252b42d4083777d1885984f5e82ea0ca61fe5 Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 14 Aug 2026 14:46:14 -0300 Subject: [PATCH 11/13] =?UTF-8?q?fix:=20adjudicated=20review=20findings=20?= =?UTF-8?q?=E2=80=94=20tests=20to=20new=20contracts,=20predicate=20coheren?= =?UTF-8?q?ce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the two tests still asserting pre-inversion contracts (0.82 opacity clamp; contentView-replacement glass hierarchy) to the new ones: opaque cards stay opaque, and the backdrop installs as a theme-frame sibling below an untouched contentView and removes cleanly. Card corner rounding now shares the card-layout predicate (skipped under Reduce Transparency, which also disables the insets). The fallback sidebar restores terminal-derived label contrast when Match Terminal Background is on. --- Sources/GhosttySurfaceScrollView.swift | 7 +++++-- Sources/SidebarVisuals.swift | 10 +++++++++- Sources/WindowBrowserSlotView.swift | 6 ++++-- programaTests/GhosttyConfigTests.swift | 6 ++++-- programaTests/WindowAndDragTests.swift | 21 +++++++++++++-------- 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/Sources/GhosttySurfaceScrollView.swift b/Sources/GhosttySurfaceScrollView.swift index ce2aceb1..1e97ac0a 100644 --- a/Sources/GhosttySurfaceScrollView.swift +++ b/Sources/GhosttySurfaceScrollView.swift @@ -418,8 +418,11 @@ final class GhosttySurfaceScrollView: NSView { layer?.masksToBounds = true // Inverted glass layout: each pane is an elevated card over the window // backdrop. The layer already masks to bounds, so rounding it clips the - // Metal surface to the card shape for free. - if WindowGlassEffect.isAvailable { + // Metal surface to the card shape for free. Same predicate as the card + // insets (ContentView): Reduce Transparency disables the card layout, + // so it must not leave rounded corners on flush content. + if WindowGlassEffect.isAvailable, + !NSWorkspace.shared.accessibilityDisplayShouldReduceTransparency { layer?.cornerRadius = WindowGlassEffect.contentCardCornerRadius layer?.cornerCurve = .continuous } diff --git a/Sources/SidebarVisuals.swift b/Sources/SidebarVisuals.swift index 3186ffeb..be667e2b 100644 --- a/Sources/SidebarVisuals.swift +++ b/Sources/SidebarVisuals.swift @@ -1270,9 +1270,17 @@ struct SidebarSurface: View { SidebarBackdropContentHost(content: content) .frame(maxWidth: .infinity, maxHeight: .infinity) } else { + // Fallback path (pre-26 / Reduce Transparency): the backdrop can + // render terminal-colored when Match Terminal Background is on, + // so labels must resolve contrast against the terminal, not the + // system scheme. ZStack { SidebarBackdrop() - content + if matchTerminalBackground { + content.environment(\.colorScheme, terminalScheme) + } else { + content + } } .clipShape(RoundedRectangle(cornerRadius: standaloneCornerRadius, style: .continuous)) } diff --git a/Sources/WindowBrowserSlotView.swift b/Sources/WindowBrowserSlotView.swift index 97459769..5358dc21 100644 --- a/Sources/WindowBrowserSlotView.swift +++ b/Sources/WindowBrowserSlotView.swift @@ -54,8 +54,10 @@ final class WindowBrowserSlotView: NSView { wantsLayer = true layer?.masksToBounds = true // Inverted glass layout: browser panes are elevated cards like terminal - // panes (GhosttySurfaceScrollView carries the same radius). - if WindowGlassEffect.isAvailable { + // panes (GhosttySurfaceScrollView carries the same radius). Same + // predicate as the card insets — see GhosttySurfaceScrollView. + if WindowGlassEffect.isAvailable, + !NSWorkspace.shared.accessibilityDisplayShouldReduceTransparency { layer?.cornerRadius = WindowGlassEffect.contentCardCornerRadius layer?.cornerCurve = .continuous } diff --git a/programaTests/GhosttyConfigTests.swift b/programaTests/GhosttyConfigTests.swift index 7e825363..c145a42c 100644 --- a/programaTests/GhosttyConfigTests.swift +++ b/programaTests/GhosttyConfigTests.swift @@ -748,13 +748,15 @@ final class WindowTransparencyDecisionTests: XCTestCase { ) } - func testTerminalBackgroundOpacityCapsOpaqueFillWhenWindowGlassIsEnabled() { + func testTerminalBackgroundOpacityStaysOpaqueWithWindowGlass() { + // Inverted layout: panes are opaque elevated cards — window glass no + // longer clamps an opaque terminal fill. XCTAssertEqual( ProgramaGlassSettings.effectiveTerminalBackgroundOpacity( configuredOpacity: 1.0, windowGlassEnabled: true ), - 0.82, + 1.0, accuracy: 0.0001 ) } diff --git a/programaTests/WindowAndDragTests.swift b/programaTests/WindowAndDragTests.swift index f4452cb8..0a1a1772 100644 --- a/programaTests/WindowAndDragTests.swift +++ b/programaTests/WindowAndDragTests.swift @@ -29,7 +29,7 @@ private struct ShortcutHintSizingTestLabel: View { @MainActor final class WindowGlassEffectTests: XCTestCase { - func testRemoveRestoresOriginalContentHierarchy() { + func testBackdropInstallsBelowContentViewAndRemovesCleanly() throws { _ = NSApplication.shared let originalContentView = NSView(frame: NSRect(x: 0, y: 0, width: 320, height: 200)) @@ -43,18 +43,23 @@ final class WindowGlassEffectTests: XCTestCase { WindowGlassEffect.apply(to: window, tintColor: .systemBlue) - if WindowGlassEffect.isAvailable { - XCTAssertFalse(window.contentView === originalContentView) - XCTAssertTrue(WindowGlassEffect.hostedContentView(in: window.contentView!) === originalContentView) - } else { - XCTAssertTrue(window.contentView === originalContentView) - XCTAssertTrue(originalContentView.subviews.contains(where: { $0 is NSVisualEffectView })) + // Inverted layout: the contentView is never replaced; the backdrop is a + // theme-frame sibling BELOW it. (The titlebar's own material lives in a + // nested container, so direct theme-frame subviews identify ours.) + XCTAssertTrue(window.contentView === originalContentView) + let themeFrame = try XCTUnwrap(originalContentView.superview) + func isBackdrop(_ view: NSView) -> Bool { + WindowGlassEffect.isGlassEffectView(view) || view is NSVisualEffectView } + let backdrop = try XCTUnwrap(themeFrame.subviews.first(where: isBackdrop)) + let backdropIndex = try XCTUnwrap(themeFrame.subviews.firstIndex(of: backdrop)) + let contentIndex = try XCTUnwrap(themeFrame.subviews.firstIndex(of: originalContentView)) + XCTAssertLessThan(backdropIndex, contentIndex) WindowGlassEffect.remove(from: window) XCTAssertTrue(window.contentView === originalContentView) - XCTAssertFalse(originalContentView.subviews.contains(where: { $0 is NSVisualEffectView })) + XCTAssertFalse(themeFrame.subviews.contains(where: isBackdrop)) } func testNativePaneChromePillsOwnAppKitControlsAboveTerminalPortal() throws { From 67f5d32ee0a3a72def5fc520fee0dc82206bd22a Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 14 Aug 2026 15:38:57 -0300 Subject: [PATCH 12/13] ci: retrigger checks From 021fe5d37c46a304d3ebcccfd3601a1e818daf35 Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 14 Aug 2026 15:49:58 -0300 Subject: [PATCH 13/13] test: align window-transparency and backdrop tests with the stock-glass contract On macOS 26 the transparent window + glass backdrop are stock, so the transparency predicate returns true regardless of the legacy opt-in; the test now branches on availability. The backdrop test tracks the exact view apply() adds instead of type-matching, which also caught the theme frame's own titlebar material. --- programaTests/GhosttyConfigTests.swift | 14 +++++++++++--- programaTests/WindowAndDragTests.swift | 21 +++++++++++++-------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/programaTests/GhosttyConfigTests.swift b/programaTests/GhosttyConfigTests.swift index 4a2b8175..a2349748 100644 --- a/programaTests/GhosttyConfigTests.swift +++ b/programaTests/GhosttyConfigTests.swift @@ -696,15 +696,23 @@ final class WindowTransparencyDecisionTests: XCTestCase { private let sidebarBlendModeKey = "sidebarBlendMode" private let bgGlassEnabledKey = "bgGlassEnabled" - func testTranslucentOpacityForcesClearWindowBackgroundOutsideSidebarBlendModePath() { + func testWindowTransparencyFollowsGlassAvailabilityAndTerminalOpacity() { withTemporaryWindowBackgroundDefaults { let defaults = UserDefaults.standard defaults.set("withinWindow", forKey: sidebarBlendModeKey) defaults.set(false, forKey: bgGlassEnabledKey) - XCTAssertFalse(cmuxShouldUseTransparentBackgroundWindow()) + if WindowGlassEffect.isAvailable { + // Inverted layout: the glass backdrop is stock, and it samples + // behind the window — transparency is on regardless of opacity. + XCTAssertTrue(cmuxShouldUseTransparentBackgroundWindow()) + XCTAssertTrue(cmuxShouldUseClearWindowBackground(for: 1.0)) + } else { + // Pre-26: opt-in off means only a translucent terminal clears it. + XCTAssertFalse(cmuxShouldUseTransparentBackgroundWindow()) + XCTAssertFalse(cmuxShouldUseClearWindowBackground(for: 1.0)) + } XCTAssertTrue(cmuxShouldUseClearWindowBackground(for: 0.80)) - XCTAssertFalse(cmuxShouldUseClearWindowBackground(for: 1.0)) } } diff --git a/programaTests/WindowAndDragTests.swift b/programaTests/WindowAndDragTests.swift index 0a1a1772..09e669f6 100644 --- a/programaTests/WindowAndDragTests.swift +++ b/programaTests/WindowAndDragTests.swift @@ -41,17 +41,22 @@ final class WindowGlassEffectTests: XCTestCase { ) window.contentView = originalContentView + // Track exactly what apply() adds — the theme frame carries its own + // system furniture (titlebar material) that a type check would match. + let themeFrame = try XCTUnwrap(originalContentView.superview) + let preexisting = Set(themeFrame.subviews.map(ObjectIdentifier.init)) + WindowGlassEffect.apply(to: window, tintColor: .systemBlue) // Inverted layout: the contentView is never replaced; the backdrop is a - // theme-frame sibling BELOW it. (The titlebar's own material lives in a - // nested container, so direct theme-frame subviews identify ours.) + // theme-frame sibling BELOW it. XCTAssertTrue(window.contentView === originalContentView) - let themeFrame = try XCTUnwrap(originalContentView.superview) - func isBackdrop(_ view: NSView) -> Bool { - WindowGlassEffect.isGlassEffectView(view) || view is NSVisualEffectView - } - let backdrop = try XCTUnwrap(themeFrame.subviews.first(where: isBackdrop)) + let added = themeFrame.subviews.filter { !preexisting.contains(ObjectIdentifier($0)) } + XCTAssertEqual(added.count, 1) + let backdrop = try XCTUnwrap(added.first) + XCTAssertTrue( + WindowGlassEffect.isGlassEffectView(backdrop) || backdrop is NSVisualEffectView + ) let backdropIndex = try XCTUnwrap(themeFrame.subviews.firstIndex(of: backdrop)) let contentIndex = try XCTUnwrap(themeFrame.subviews.firstIndex(of: originalContentView)) XCTAssertLessThan(backdropIndex, contentIndex) @@ -59,7 +64,7 @@ final class WindowGlassEffectTests: XCTestCase { WindowGlassEffect.remove(from: window) XCTAssertTrue(window.contentView === originalContentView) - XCTAssertFalse(themeFrame.subviews.contains(where: isBackdrop)) + XCTAssertFalse(themeFrame.subviews.contains(where: { $0 === backdrop })) } func testNativePaneChromePillsOwnAppKitControlsAboveTerminalPortal() throws {