Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -13592,6 +13592,23 @@
}
}
}
},
"tabBar.newTab": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "New Tab"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "新規タブ"
}
}
}
}
},
"version": "1.0"
Expand Down
1 change: 0 additions & 1 deletion Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
20 changes: 19 additions & 1 deletion Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,12 @@ 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 {
if cardInsetAmount > 0 { return 0 }
return isFullScreen ? 0 : -titlebarPadding
}
return titlebarPadding
Expand Down Expand Up @@ -728,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
}
Expand Down Expand Up @@ -918,6 +925,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
Expand All @@ -930,6 +946,7 @@ struct ContentView: View {
layout = AnyView(
ZStack(alignment: .leading) {
terminalContentWithSidebarDropOverlay
.padding(cardInsetAmount)
.padding(.leading, sidebarState.isVisible ? sidebarWidth : 0)
if sidebarState.isVisible {
sidebarView
Expand All @@ -944,6 +961,7 @@ struct ContentView: View {
sidebarView
}
terminalContentWithSidebarDropOverlay
.padding(cardInsetAmount)
}
)
}
Expand Down
10 changes: 10 additions & 0 deletions Sources/GhosttySurfaceScrollView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,16 @@ 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. 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
}

backgroundView.wantsLayer = true
let initialTerminalBackground = GhosttyApp.shared.defaultBackgroundColor
Expand Down
14 changes: 7 additions & 7 deletions Sources/GhosttyTerminalSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,32 @@ func ghostty_surface_select_cursor_cell_compat(_ surface: ghostty_surface_t) ->

#if os(macOS)
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 {
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)
)
}

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
}

Expand Down
9 changes: 6 additions & 3 deletions Sources/ProgramaGlassSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
15 changes: 4 additions & 11 deletions Sources/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1035,17 +1035,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()

Expand Down
7 changes: 5 additions & 2 deletions Sources/SidebarQuotaFooter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ struct SidebarQuotaFooter: View {
window: snapshot.sevenDay
)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
// 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()
}
Expand Down
125 changes: 30 additions & 95 deletions Sources/SidebarVisuals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -487,17 +487,17 @@ 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"))
.font(.system(size: 11, weight: .semibold))
.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
Expand All @@ -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()

/// 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.
private var followsTerminal: Bool {
matchTerminalBackground ||
(WindowGlassEffect.isAvailable && !accessibilityReduceTransparency)
}

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
Expand Down Expand Up @@ -1237,13 +1209,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<Content: View>: 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<Content: View>: NSViewRepresentable {
let content: Content
let tintColor: NSColor?
let cornerRadius: CGFloat
let appearance: NSAppearance?

final class Coordinator {
let hostingView: NSHostingView<Content>
Expand All @@ -1253,7 +1225,7 @@ private struct SidebarNativeGlassContentHost<Content: View>: 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 = []
}
Expand All @@ -1264,53 +1236,12 @@ private struct SidebarNativeGlassContentHost<Content: View>: 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.
Expand All @@ -1332,20 +1263,24 @@ struct SidebarSurface<Content: View>: 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 {
// 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))
}
Expand Down
Loading
Loading