Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
0150be2
sidebar toggle: titlebar title and minimal tab bar inset glide with t…
lucasr1b Oct 9, 2026
a7e958c
sidebar toggle: trailing pane buttons hold still through the slide
lucasr1b Oct 9, 2026
8a8aeed
sidebar toggle: every pane glides to its own docked edges
lucasr1b Oct 9, 2026
7e2501d
sidebar toggle: every pane's whole subtree glides, trailing chrome st…
lucasr1b Oct 9, 2026
f46e03c
sidebar toggle: tab bars lay out live at each pane's moving width
lucasr1b Oct 9, 2026
b95e73d
sidebar toggle: the action lane rides each pane's edge, tabs lead it
lucasr1b Oct 9, 2026
70b8600
sidebar toggle: tabs hold still and fade at the pane's moving edge
lucasr1b Oct 9, 2026
103a6ad
sidebar toggle: drop the slide when its window closes mid-slide
lucasr1b Oct 9, 2026
83509ef
Merge sidebar-animations (main cd85f00c9e47) into sidebar-toggle-glide
lucasr1b Oct 9, 2026
e17412b
sidebar toggle: keep the live right sidebar still instead of a snapsh…
lucasr1b Oct 9, 2026
5e0f274
sidebar toggle: skip the unused strip picture for tab bars and cache …
lucasr1b Oct 10, 2026
0091a7a
sidebar toggle: the browser toolbar keeps its tools through the slide
lucasr1b Oct 10, 2026
40fcd1c
sidebar toggle: each glide on a layer gets its own animation key
lucasr1b Oct 10, 2026
943e3e9
sidebar toggle: hide the real dividers with a mask during the slide
lucasr1b Oct 10, 2026
3b227b1
browser: panes run flush to the window bottom, import chip and omniba…
lucasr1b Oct 10, 2026
c51366e
workspace card: panes run flush to the trailing window edge too
lucasr1b Oct 10, 2026
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
6 changes: 3 additions & 3 deletions Sources/BrowserWindowPortal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1989,7 +1989,7 @@ final class WindowBrowserPortal: NSObject {
private var nextHostedWebViewRefreshGeneration: UInt64 = 0
private var pendingHostedWebViewRefreshes: [ObjectIdentifier: PendingHostedWebViewRefresh] = [:]

private struct Entry {
struct Entry {
weak var webView: WKWebView?
weak var containerView: WindowBrowserSlotView?
weak var anchorView: NSView?
Expand Down Expand Up @@ -2018,7 +2018,7 @@ final class WindowBrowserPortal: NSObject {
let delayedScheduler = MainActorDeferredActionScheduler()
}

private var entriesByWebViewId: [ObjectIdentifier: Entry] = [:]
private(set) var entriesByWebViewId: [ObjectIdentifier: Entry] = [:]
private var webViewByAnchorId: [ObjectIdentifier: ObjectIdentifier] = [:]

#if DEBUG
Expand Down Expand Up @@ -4147,7 +4147,7 @@ enum BrowserWindowPortalRegistry {
let frameInWindow: CGRect
}

private static var portalsByWindowId: [ObjectIdentifier: WindowBrowserPortal] = [:]
private(set) static var portalsByWindowId: [ObjectIdentifier: WindowBrowserPortal] = [:]
private static var webViewToWindowId: [ObjectIdentifier: ObjectIdentifier] = [:]
private static func postRegistryDidChange(for webView: WKWebView) {
NotificationCenter.default.post(name: .browserPortalRegistryDidChange, object: webView)
Expand Down
7 changes: 6 additions & 1 deletion Sources/ContentView+SidebarPeek.swift
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,12 @@ extension ContentView {
&& !(rightSidebarVisible && fileExplorerState.mode == .dock)
},
trailingStillWidth: { rightSidebarWidth },
isPeekPresenting: { sidebarPeek.presentsPanel }
isPeekPresenting: { sidebarPeek.presentsPanel },
dockedLayoutWillCommit: { docked in syncTrafficLightInset(docked: docked) },
tabBarInsetDelta: { tabBarLeadingInset(docked: false) - tabBarLeadingInset(docked: true) },
splitButtonCount: {
tabManager.selectedWorkspace?.bonsplitController.configuration.appearance.splitButtons.count ?? 0
}
)
sidebarPeek.setPolicy(SidebarCustomizationSettings.peekPolicy())
}
Expand Down
90 changes: 45 additions & 45 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1866,11 +1866,10 @@ struct ContentView: View {
.allowsHitTesting(sidebarSelectionState.selection == .tabs)
.accessibilityHidden(sidebarSelectionState.selection != .tabs)
}
// Panes meet the band and both sidebars, so the band line, the split
// dividers and the tab bar hairline join the sidebars' own lines.
// They keep an inset only where the card meets the window edge.
.padding(.trailing, rightSidebarVisible ? 0 : WorkspaceCardMetrics.paneInset)
.padding(.bottom, WorkspaceCardMetrics.paneInset)
// Panes meet the band, both sidebars and the window's edges, so the
// band line, the split dividers and the tab bar hairline join the
// sidebars' own lines. No inset at the window's edges: a browser page
// has its own background, so one reads as a dead strip.
// Reserves the titlebar band's height inside the card, so the band
// (drawn by the window-level overlay at the same fixed position)
// reads as the card's header.
Expand Down Expand Up @@ -2198,42 +2197,39 @@ struct ContentView: View {
.allowsHitTesting(false)

SidebarWidthReader(layout: sidebarLayout) { width in
HStack(spacing: 8) {
if isFullScreen && !sidebarState.isVisible {
// Reserve the controls' width so the title flows to their right.
// The visible controls are rendered once in the band overlay (see
// `workspaceTitlebarBand`) so their position never depends on
// sidebar visibility.
Color.clear
.frame(width: fullscreenControlsWidth, height: titlebarContentHeight)
.allowsHitTesting(false)
}

// Draggable folder icon + focused command name
if let directory = focusedDirectory {
DetachedFolderDragIcon(directory: directory)
.frame(width: 16, height: 16)
.padding(.leading, -6)
}

Text(titlebarText)
.cmuxFont(size: 13, weight: .bold)
.foregroundColor(fakeTitlebarTextColor(appearance: appearance))
.lineLimit(1)
.allowsHitTesting(false)

Spacer()

}
.frame(height: titlebarContentHeight)
.padding(.top, 2)
.padding(.leading, Self.customTitlebarLeadingPadding(
// Docked and hidden layouts rest the title at different x (the
// hidden one reserves the fullscreen controls' width, which are
// drawn once in the band overlay); its own layer lets the
// toggle's slide glide it between them.
let leading = Self.titlebarTitleLeadings(
isFullScreen: isFullScreen,
isSidebarVisible: sidebarState.isVisible,
sidebarWidth: width,
minimumSidebarWidth: minimumSidebarWidth,
titlebarLeadingInset: titlebarLeadingInset
))
titlebarLeadingInset: titlebarLeadingInset,
reservedControlsWidth: fullscreenControlsWidth
)
SidebarSlideGlideHost(
hiddenLeading: leading.hidden,
dockedLeading: leading.docked,
layout: sidebarLayout,
content: AnyView(HStack(spacing: 8) {
// Draggable folder icon + focused command name
if let directory = focusedDirectory {
DetachedFolderDragIcon(directory: directory)
.frame(width: 16, height: 16)
}
Text(titlebarText)
.cmuxFont(size: 13, weight: .bold)
.foregroundColor(fakeTitlebarTextColor(appearance: appearance))
.lineLimit(1)
.allowsHitTesting(false)
Spacer(minLength: 0)
}.cmuxFontMagnificationEnvironment())
)
.padding(.leading, focusedDirectory == nil ? 0 : -6)
.frame(height: titlebarContentHeight)
.padding(.top, 2)
.padding(.leading, sidebarState.isVisible && sidebarLayout.docksSidebar ? leading.docked : leading.hidden)
.padding(.trailing, 8)
}
}
Expand All @@ -2247,7 +2243,7 @@ struct ContentView: View {
orientation: .horizontal,
backgroundColor: appearance.resolvedChromeBackgroundColor
)
.padding(.leading, sidebarState.isVisible ? width : 0)
.padding(.leading, sidebarState.isVisible && sidebarLayout.docksSidebar ? width : 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Titlebar hairline snaps on hide

Medium Severity

The titlebar hairline inset now follows docksSidebar, which flips at hide start, but this border lives in the window overlay and has no glide. The line jumps to the window's leading edge while the sidebar is still on screen.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c51366e. Configure here.

}
}
}
Expand Down Expand Up @@ -2299,12 +2295,16 @@ struct ContentView: View {
}
}

private func syncTrafficLightInset(isMinimalMode: Bool? = nil) {
let resolvedIsMinimalMode = isMinimalMode ?? currentIsMinimalMode
let inset: CGFloat = (resolvedIsMinimalMode && !sidebarState.isVisible && !isFullScreen)
? CGFloat(titlebarDebugChromeSnapshot.trafficLightTabBarLeadingInset)
: 0
tabManager.syncWorkspaceTabBarLeadingInset(inset)
/// Minimal mode's tab bar makes room for the traffic lights while the
/// docked layout gives the sidebar no width.
func tabBarLeadingInset(docked: Bool, isMinimalMode: Bool? = nil) -> CGFloat {
(isMinimalMode ?? currentIsMinimalMode) && !docked && !isFullScreen
? CGFloat(titlebarDebugChromeSnapshot.trafficLightTabBarLeadingInset) : 0
}

func syncTrafficLightInset(isMinimalMode: Bool? = nil, docked: Bool? = nil) {
let docked = docked ?? (sidebarState.isVisible && sidebarLayout.docksSidebar)
tabManager.syncWorkspaceTabBarLeadingInset(tabBarLeadingInset(docked: docked, isMinimalMode: isMinimalMode))
}

private func handleWorkspacePresentationModeChange(isMinimalMode: Bool) {
Expand Down
64 changes: 52 additions & 12 deletions Sources/Panels/BrowserPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -322,13 +322,16 @@ struct BrowserPanelView: View {
@State private var omnibarPillFrame: CGRect = .zero
@State private var addressBarHeight: CGFloat = 0
@State private var addressBarWidth: CGFloat = 0
/// The compact state held through a sidebar slide (see
/// `SidebarToggleAnimator.isSliding`).
@State private var heldChromeCompact: Bool?

/// Below this chrome width the full accessory row would crowd out the
/// omnibar, so the toolbar tools collapse into More.
private static let compactChromeWidthThreshold: CGFloat = 420

private var isChromeCompact: Bool {
addressBarWidth > 0 && addressBarWidth < Self.compactChromeWidthThreshold
heldChromeCompact ?? (addressBarWidth > 0 && addressBarWidth < Self.compactChromeWidthThreshold)
}

@State private var isBrowserImportHintPopoverPresented = false
Expand Down Expand Up @@ -1125,6 +1128,10 @@ struct BrowserPanelView: View {
.onReceive(NotificationCenter.default.publisher(for: .webViewDidReceiveClick)) { notification in
handleBrowserWebViewClickIntent(notification)
}
.onReceive(NotificationCenter.default.publisher(for: SidebarToggleAnimator.slideDidLand)) { _ in
guard heldChromeCompact != nil else { return }
withAnimation(.easeInOut(duration: 0.2)) { heldChromeCompact = nil }
}
.onReceive(NotificationCenter.default.publisher(for: .ghosttySurfaceTabBarFontSizeDidChange)) { _ in
tabBarFontSize = GhosttyConfig.loadForCmux(globalFontMagnificationPercent: GlobalFontMagnification.storedPercent).surfaceTabBarFontSize
}
Expand All @@ -1146,6 +1153,11 @@ struct BrowserPanelView: View {
addressBarHeight = height
}
.onPreferenceChange(BrowserAddressBarWidthPreferenceKey.self) { width in
// Through a sidebar slide the toolbar keeps the tools it
// started with; they change, faded, once the slide lands.
if SidebarToggleAnimator.isSliding, heldChromeCompact == nil {
heldChromeCompact = isChromeCompact
}
addressBarWidth = width
}
}
Expand Down Expand Up @@ -1649,21 +1661,31 @@ struct BrowserPanelView: View {
Button(action: {
isBrowserImportHintPopoverPresented.toggle()
}) {
HStack(spacing: 4) {
CmuxSystemSymbolImage(systemName: "square.and.arrow.down.on.square", pointSize: 10, weight: .medium, tint: devToolsColorOption.color(accent: cmuxAccent))
Text(String(localized: "browser.import.hint.toolbar", defaultValue: "Import"))
.cmuxFont(size: 11, weight: .medium)
.lineLimit(1)
let icon = CmuxSystemSymbolImage(systemName: "square.and.arrow.down.on.square", pointSize: 10, weight: .medium, tint: devToolsColorOption.color(accent: cmuxAccent))
// The omnibar comes first: a compact toolbar keeps just the icon,
// as square as the other tools, so the label never truncates to
// "I...".
if isChromeCompact {
icon.frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center)
} else {
HStack(spacing: 4) {
icon
Text(String(localized: "browser.import.hint.toolbar", defaultValue: "Import"))
.cmuxFont(size: 11, weight: .medium)
.lineLimit(1)
}
.fixedSize()
.foregroundStyle(devToolsColorOption.color(accent: cmuxAccent))
.padding(.horizontal, 8)
.padding(.vertical, 4)
}
.foregroundStyle(devToolsColorOption.color(accent: cmuxAccent))
.padding(.horizontal, 8)
.padding(.vertical, 4)
}
.buttonStyle(OmnibarAddressButtonStyle())
.popover(isPresented: $isBrowserImportHintPopoverPresented, arrowEdge: .bottom) {
browserImportHintPopover.browserChromePopoverAppearance(resolvedColorScheme)
}
.safeHelp(String(localized: "browser.import.hint.toolbar.help", defaultValue: "Import browser data"))
.accessibilityLabel(String(localized: "browser.import.hint.toolbar", defaultValue: "Import"))
.accessibilityIdentifier("BrowserImportHintToolbarChip")
}

Expand Down Expand Up @@ -4176,7 +4198,9 @@ final class OmnibarNativeTextField: NSTextField {
isBezeled = false
drawsBackground = false
focusRingType = .none
lineBreakMode = .byTruncatingTail
// Clipped, not ellipsized: the text then reads the same while a
// sidebar slide narrows the pane as once it lands.
lineBreakMode = .byClipping
usesSingleLineMode = true
}

Expand Down Expand Up @@ -4955,7 +4979,7 @@ struct OmnibarTextFieldRepresentable: NSViewRepresentable {
BrowserOmnibarNativeFieldRegistry.shared.register(field, panelId: panelId)
field.identifier = browserOmnibarTextFieldIdentifier
field.font = .systemFont(ofSize: fontSize)
field.placeholderString = placeholder
applyPlaceholder(to: field)
field.delegate = context.coordinator
field.target = nil
field.action = nil
Expand All @@ -4974,6 +4998,22 @@ struct OmnibarTextFieldRepresentable: NSViewRepresentable {
return field
}

/// The placeholder, clipped like the field's text. While the omnibar is
/// focused its field editor draws the placeholder and would otherwise
/// wrap it at a word, dropping "URL" outright the moment the field
/// narrows (a sidebar slide's landing).
private func applyPlaceholder(to field: NSTextField) {
let style = NSMutableParagraphStyle()
style.lineBreakMode = .byClipping
let placeholder = NSAttributedString(string: placeholder, attributes: [
.font: NSFont.systemFont(ofSize: fontSize),
.foregroundColor: NSColor.placeholderTextColor,
.paragraphStyle: style,
])
guard field.placeholderAttributedString != placeholder else { return }
field.placeholderAttributedString = placeholder
}

func updateNSView(_ nsView: OmnibarNativeTextField, context: Context) {
context.coordinator.parent = self
context.coordinator.parentField = nsView
Expand All @@ -4982,10 +5022,10 @@ struct OmnibarTextFieldRepresentable: NSViewRepresentable {
}
nsView.panelId = panelId
BrowserOmnibarNativeFieldRegistry.shared.register(nsView, panelId: panelId)
nsView.placeholderString = placeholder
if nsView.font?.pointSize != fontSize {
nsView.font = .systemFont(ofSize: fontSize)
}
applyPlaceholder(to: nsView)
context.coordinator.queueSelectAllRequest(selectAllRequestId)

let activeInlineCompletion = omnibarInlineCompletionIfBufferMatchesTypedPrefix(
Expand Down
3 changes: 3 additions & 0 deletions Sources/Sidebar/SidebarLayoutModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ final class SidebarLayoutModel: ObservableObject {
/// The docked pane's host, so the toggle animator can start row
/// animations at a show's first frame without a SwiftUI pass.
weak var dockedPane: SidebarDockedPaneHost.ContainerView?
/// The titlebar title's host, which the toggle's slide glides between
/// its hidden and docked resting x.
weak var titlebarTitle: SidebarSlideGlideHost.ContainerView?

/// How far the window ground reaches past the leading edge, so a toggle
/// slide (which translates the content root by up to the sidebar width)
Expand Down
Loading
Loading