-
-
Notifications
You must be signed in to change notification settings - Fork 987
Add cleaner titlebar visibility options #1327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
b77d7c2
ca6f83f
cdcfa19
c8d561c
7385a01
d5cf95a
6b281dc
3e75be2
cf3a132
6244e49
d7d6c60
945881b
cc51646
dc394ad
9ecefc5
239ec1d
a82fa63
18d2d5f
5cd4b30
67f2bda
afd2d21
278a2ff
5c42da4
f1e0abe
8d96c97
ae0a4e3
b61698c
87ee59f
39d8cc2
3a41872
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1931,66 +1931,75 @@ struct ContentView: View { | |
| .frame(width: sidebarWidth) | ||
| } | ||
|
|
||
| /// Space at top of content area for the titlebar. This must be at least the actual titlebar | ||
| /// height; otherwise controls like Bonsplit tab dragging can be interpreted as window drags. | ||
| /// Space at top of content area reserved for the custom workspace titlebar. | ||
| @State private var titlebarPadding: CGFloat = 32 | ||
| private let collapsedTitlebarDragHandleHeight: CGFloat = 30 | ||
|
|
||
| private var terminalContent: some View { | ||
| let mountedWorkspaceIdSet = Set(mountedWorkspaceIds) | ||
| let mountedWorkspaces = tabManager.tabs.filter { mountedWorkspaceIdSet.contains($0.id) } | ||
| let selectedWorkspaceId = tabManager.selectedTabId | ||
| let retiringWorkspaceId = self.retiringWorkspaceId | ||
|
|
||
| return ZStack { | ||
| return ZStack(alignment: .top) { | ||
| ZStack { | ||
| ForEach(mountedWorkspaces) { tab in | ||
| let isSelectedWorkspace = selectedWorkspaceId == tab.id | ||
| let isRetiringWorkspace = retiringWorkspaceId == tab.id | ||
| let shouldPrimeInBackground = tabManager.pendingBackgroundWorkspaceLoadIds.contains(tab.id) | ||
| // Keep the retiring workspace visible during handoff, but never input-active. | ||
| // Allowing both selected+retiring workspaces to be input-active lets the | ||
| // old workspace steal first responder (notably with WKWebView), which can | ||
| // delay handoff completion and make browser returns feel laggy. | ||
| let isInputActive = isSelectedWorkspace | ||
| let isVisible = isSelectedWorkspace || isRetiringWorkspace | ||
| let portalPriority = isSelectedWorkspace ? 2 : (isRetiringWorkspace ? 1 : 0) | ||
| WorkspaceContentView( | ||
| workspace: tab, | ||
| isWorkspaceVisible: isVisible, | ||
| isWorkspaceInputActive: isInputActive, | ||
| workspacePortalPriority: portalPriority, | ||
| onThemeRefreshRequest: { reason, eventId, source, payloadHex in | ||
| scheduleTitlebarThemeRefreshFromWorkspace( | ||
| workspaceId: tab.id, | ||
| reason: reason, | ||
| backgroundEventId: eventId, | ||
| backgroundSource: source, | ||
| notificationPayloadHex: payloadHex | ||
| ) | ||
| ZStack { | ||
| ForEach(mountedWorkspaces) { tab in | ||
| let isSelectedWorkspace = selectedWorkspaceId == tab.id | ||
| let isRetiringWorkspace = retiringWorkspaceId == tab.id | ||
| let shouldPrimeInBackground = tabManager.pendingBackgroundWorkspaceLoadIds.contains(tab.id) | ||
| // Keep the retiring workspace visible during handoff, but never input-active. | ||
| // Allowing both selected+retiring workspaces to be input-active lets the | ||
| // old workspace steal first responder (notably with WKWebView), which can | ||
| // delay handoff completion and make browser returns feel laggy. | ||
| let isInputActive = isSelectedWorkspace | ||
| let isVisible = isSelectedWorkspace || isRetiringWorkspace | ||
| let portalPriority = isSelectedWorkspace ? 2 : (isRetiringWorkspace ? 1 : 0) | ||
| WorkspaceContentView( | ||
| workspace: tab, | ||
| isWorkspaceVisible: isVisible, | ||
| isWorkspaceInputActive: isInputActive, | ||
| workspacePortalPriority: portalPriority, | ||
| onThemeRefreshRequest: { reason, eventId, source, payloadHex in | ||
| scheduleTitlebarThemeRefreshFromWorkspace( | ||
| workspaceId: tab.id, | ||
| reason: reason, | ||
| backgroundEventId: eventId, | ||
| backgroundSource: source, | ||
| notificationPayloadHex: payloadHex | ||
| ) | ||
| } | ||
| ) | ||
| .opacity(isVisible ? 1 : 0) | ||
| .allowsHitTesting(isSelectedWorkspace) | ||
| .accessibilityHidden(!isVisible) | ||
| .zIndex(isSelectedWorkspace ? 2 : (isRetiringWorkspace ? 1 : 0)) | ||
| .task(id: shouldPrimeInBackground ? tab.id : nil) { | ||
| await primeBackgroundWorkspaceIfNeeded(workspaceId: tab.id) | ||
| } | ||
| ) | ||
| .opacity(isVisible ? 1 : 0) | ||
| .allowsHitTesting(isSelectedWorkspace) | ||
| .accessibilityHidden(!isVisible) | ||
| .zIndex(isSelectedWorkspace ? 2 : (isRetiringWorkspace ? 1 : 0)) | ||
| .task(id: shouldPrimeInBackground ? tab.id : nil) { | ||
| await primeBackgroundWorkspaceIfNeeded(workspaceId: tab.id) | ||
| } | ||
| } | ||
| .opacity(sidebarSelectionState.selection == .tabs ? 1 : 0) | ||
| .allowsHitTesting(sidebarSelectionState.selection == .tabs) | ||
| .accessibilityHidden(sidebarSelectionState.selection != .tabs) | ||
|
|
||
| NotificationsPage(selection: $sidebarSelectionState.selection) | ||
| .opacity(sidebarSelectionState.selection == .notifications ? 1 : 0) | ||
| .allowsHitTesting(sidebarSelectionState.selection == .notifications) | ||
| .accessibilityHidden(sidebarSelectionState.selection != .notifications) | ||
| } | ||
| .opacity(sidebarSelectionState.selection == .tabs ? 1 : 0) | ||
| .allowsHitTesting(sidebarSelectionState.selection == .tabs) | ||
| .accessibilityHidden(sidebarSelectionState.selection != .tabs) | ||
| .padding(.top, titlebarPadding) | ||
|
|
||
| NotificationsPage(selection: $sidebarSelectionState.selection) | ||
| .opacity(sidebarSelectionState.selection == .notifications ? 1 : 0) | ||
| .allowsHitTesting(sidebarSelectionState.selection == .notifications) | ||
| .accessibilityHidden(sidebarSelectionState.selection != .notifications) | ||
| } | ||
| .padding(.top, titlebarPadding) | ||
| .overlay(alignment: .top) { | ||
| // Titlebar overlay is only over terminal content, not the sidebar. | ||
| customTitlebar | ||
| if showWorkspaceTitlebar { | ||
cubic-dev-ai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Titlebar overlay is only over terminal content, not the sidebar. | ||
| customTitlebar | ||
|
Comment on lines
+2005
to
+2007
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When this branch omits Useful? React with 👍 / 👎. |
||
| } else { | ||
|
||
| // When the custom titlebar is hidden, keep empty space in the top pane tab bar | ||
| // draggable without turning the full content area into a window drag target. | ||
| WindowDragHandleView() | ||
| .frame(height: collapsedTitlebarDragHandleHeight) | ||
| .frame(maxWidth: .infinity) | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
|
|
@@ -2007,6 +2016,8 @@ struct ContentView: View { | |
| @AppStorage("bgGlassTintHex") private var bgGlassTintHex = "#000000" | ||
| @AppStorage("bgGlassTintOpacity") private var bgGlassTintOpacity = 0.03 | ||
| @AppStorage("bgGlassEnabled") private var bgGlassEnabled = false | ||
| @AppStorage(WorkspaceTitlebarSettings.showTitlebarKey) | ||
| private var showWorkspaceTitlebar = WorkspaceTitlebarSettings.defaultShowTitlebar | ||
| @AppStorage("debugTitlebarLeadingExtra") private var debugTitlebarLeadingExtra: Double = 0 | ||
|
|
||
| @State private var titlebarLeadingInset: CGFloat = 12 | ||
|
|
@@ -2623,7 +2634,7 @@ struct ContentView: View { | |
| removeSidebarResizerPointerMonitor() | ||
| }) | ||
|
|
||
| view = AnyView(view.background(WindowAccessor { [sidebarBlendMode, bgGlassEnabled, bgGlassTintHex, bgGlassTintOpacity] window in | ||
| view = AnyView(view.background(WindowAccessor { [sidebarBlendMode, bgGlassEnabled, bgGlassTintHex, bgGlassTintOpacity, showWorkspaceTitlebar] window in | ||
| window.identifier = NSUserInterfaceItemIdentifier(windowIdentifier) | ||
| window.titlebarAppearsTransparent = true | ||
| // Do not make the entire background draggable; it interferes with drag gestures | ||
|
|
@@ -2646,10 +2657,11 @@ struct ContentView: View { | |
| } | ||
| } | ||
|
|
||
| // Keep content below the titlebar so drags on Bonsplit's tab bar don't | ||
| // get interpreted as window drags. | ||
| // Reserve space for the custom titlebar only when it is enabled. When hidden, the | ||
| // top Bonsplit tab bar moves into this strip and its empty space gets an explicit | ||
| // drag handle overlay instead. | ||
| let computedTitlebarHeight = window.frame.height - window.contentLayoutRect.height | ||
| let nextPadding = max(28, min(72, computedTitlebarHeight)) | ||
| let nextPadding = showWorkspaceTitlebar ? max(28, min(72, computedTitlebarHeight)) : 0 | ||
| if abs(titlebarPadding - nextPadding) > 0.5 { | ||
|
Comment on lines
2669
to
2671
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| DispatchQueue.main.async { | ||
| titlebarPadding = nextPadding | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import Foundation | ||
|
|
||
| enum TitlebarControlsVisibilityMode: String, CaseIterable, Identifiable { | ||
| case always | ||
| case onHover | ||
|
|
||
| var id: String { rawValue } | ||
| } | ||
|
|
||
| enum TitlebarControlsVisibilitySettings { | ||
| static let modeKey = "titlebarControlsVisibilityMode" | ||
| static let defaultMode: TitlebarControlsVisibilityMode = .always | ||
|
|
||
| static func mode(for rawValue: String?) -> TitlebarControlsVisibilityMode { | ||
| guard let rawValue, let mode = TitlebarControlsVisibilityMode(rawValue: rawValue) else { | ||
| return defaultMode | ||
| } | ||
| return mode | ||
| } | ||
| } | ||
|
|
||
| enum WorkspaceTitlebarSettings { | ||
| static let showTitlebarKey = "workspaceTitlebarVisible" | ||
| static let defaultShowTitlebar = true | ||
|
|
||
| static func isVisible(defaults: UserDefaults = .standard) -> Bool { | ||
| if defaults.object(forKey: showTitlebarKey) == nil { | ||
| return defaultShowTitlebar | ||
| } | ||
| return defaults.bool(forKey: showTitlebarKey) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gate
WorkspaceContentViewvisibility on the active page.isWorkspaceVisible/isWorkspaceInputActivestaytruefor the selected workspace even whensidebarSelectionState.selection == .notifications. The outer.opacity(0)/.allowsHitTesting(false)only hide the SwiftUI wrapper; portal-backed workspace surfaces can still sit above SwiftUI and keep focus, so the notifications page can leak the previously selected workspace underneath.Suggested fix
Based on learnings,
Portal-hosted terminal views can sit above SwiftUI during split/workspace churn.🤖 Prompt for AI Agents