Implement hidden-titlebar minimalism mode
This commit is contained in:
parent
0109731bca
commit
e4ef98aca1
7 changed files with 215 additions and 6 deletions
|
|
@ -43635,6 +43635,57 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings.app.showWorkspaceTitlebar": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Show Workspace Title Bar"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ワークスペースのタイトルバーを表示"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.app.showWorkspaceTitlebar.subtitleOff": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Hide the workspace title bar and show sidebar or pane actions only on hover."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ワークスペースのタイトルバーを隠し、サイドバーやペインタブの操作はホバー時のみ表示します。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.app.showWorkspaceTitlebar.subtitleOn": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Show the folder and active title above pane tabs."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ペインタブの上にフォルダ名と現在のタイトルを表示します。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.app.showPullRequests": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
|
|
|
|||
|
|
@ -7663,6 +7663,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
titlebarAccessoryController.dismissNotificationsPopoverIfShown()
|
||||
}
|
||||
|
||||
func isNotificationsPopoverShown() -> Bool {
|
||||
titlebarAccessoryController.isNotificationsPopoverShown()
|
||||
}
|
||||
|
||||
func jumpToLatestUnread() {
|
||||
guard let notificationStore else { return }
|
||||
#if DEBUG
|
||||
|
|
|
|||
|
|
@ -1951,6 +1951,12 @@ struct ContentView: View {
|
|||
/// 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.
|
||||
@State private var titlebarPadding: CGFloat = 32
|
||||
@AppStorage(WorkspaceTitlebarSettings.showTitlebarKey)
|
||||
private var showWorkspaceTitlebar = WorkspaceTitlebarSettings.defaultShowTitlebar
|
||||
|
||||
private var effectiveTitlebarPadding: CGFloat {
|
||||
showWorkspaceTitlebar ? titlebarPadding : 0
|
||||
}
|
||||
|
||||
private var terminalContent: some View {
|
||||
let mountedWorkspaceIdSet = Set(mountedWorkspaceIds)
|
||||
|
|
@ -2004,10 +2010,12 @@ struct ContentView: View {
|
|||
.allowsHitTesting(sidebarSelectionState.selection == .notifications)
|
||||
.accessibilityHidden(sidebarSelectionState.selection != .notifications)
|
||||
}
|
||||
.padding(.top, titlebarPadding)
|
||||
.padding(.top, effectiveTitlebarPadding)
|
||||
.overlay(alignment: .top) {
|
||||
// Titlebar overlay is only over terminal content, not the sidebar.
|
||||
customTitlebar
|
||||
if showWorkspaceTitlebar {
|
||||
// Titlebar overlay is only over terminal content, not the sidebar.
|
||||
customTitlebar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2224,7 +2232,7 @@ struct ContentView: View {
|
|||
contentAndSidebarLayout
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.overlay(alignment: .topLeading) {
|
||||
if isFullScreen && sidebarState.isVisible {
|
||||
if isFullScreen && sidebarState.isVisible && showWorkspaceTitlebar {
|
||||
fullscreenControls
|
||||
.padding(.leading, 10)
|
||||
.padding(.top, 4)
|
||||
|
|
@ -7765,10 +7773,13 @@ struct VerticalTabsSidebar: View {
|
|||
private var sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails
|
||||
@AppStorage(SidebarWorkspaceDetailSettings.showNotificationMessageKey)
|
||||
private var sidebarShowNotificationMessage = SidebarWorkspaceDetailSettings.defaultShowNotificationMessage
|
||||
@AppStorage(WorkspaceTitlebarSettings.showTitlebarKey)
|
||||
private var showWorkspaceTitlebar = WorkspaceTitlebarSettings.defaultShowTitlebar
|
||||
|
||||
/// Space at top of sidebar for traffic light buttons
|
||||
private let trafficLightPadding: CGFloat = 28
|
||||
private let tabRowSpacing: CGFloat = 2
|
||||
private let hiddenTitlebarControlsLeadingInset: CGFloat = 72
|
||||
|
||||
private var showsSidebarNotificationMessage: Bool {
|
||||
SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility(
|
||||
|
|
@ -7856,6 +7867,13 @@ struct VerticalTabsSidebar: View {
|
|||
WindowDragHandleView()
|
||||
.frame(height: trafficLightPadding)
|
||||
}
|
||||
.overlay(alignment: .topLeading) {
|
||||
if !showWorkspaceTitlebar {
|
||||
HiddenTitlebarSidebarControlsView(notificationStore: notificationStore)
|
||||
.padding(.leading, hiddenTitlebarControlsLeadingInset)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
}
|
||||
.background(Color.clear)
|
||||
.modifier(ClearScrollBackground())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,22 @@ final class TitlebarControlsViewModel: ObservableObject {
|
|||
weak var notificationsAnchorView: NSView?
|
||||
}
|
||||
|
||||
extension Notification.Name {
|
||||
static let cmuxNotificationsPopoverVisibilityDidChange = Notification.Name("cmux.notificationsPopoverVisibilityDidChange")
|
||||
}
|
||||
|
||||
private enum NotificationsPopoverVisibilityUserInfoKey {
|
||||
static let isShown = "isShown"
|
||||
}
|
||||
|
||||
private func postNotificationsPopoverVisibilityDidChange(isShown: Bool) {
|
||||
NotificationCenter.default.post(
|
||||
name: .cmuxNotificationsPopoverVisibilityDidChange,
|
||||
object: nil,
|
||||
userInfo: [NotificationsPopoverVisibilityUserInfoKey.isShown: isShown]
|
||||
)
|
||||
}
|
||||
|
||||
struct NotificationsAnchorView: NSViewRepresentable {
|
||||
let onResolve: (NSView) -> Void
|
||||
|
||||
|
|
@ -508,6 +524,54 @@ struct TitlebarControlsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct HiddenTitlebarSidebarControlsView: View {
|
||||
@ObservedObject var notificationStore: TerminalNotificationStore
|
||||
@StateObject private var viewModel = TitlebarControlsViewModel()
|
||||
@State private var isHoveringControls = false
|
||||
@State private var isNotificationsPopoverShown = false
|
||||
|
||||
private let hostWidth: CGFloat = 124
|
||||
private let hostHeight: CGFloat = 28
|
||||
|
||||
private var shouldShowControls: Bool {
|
||||
isHoveringControls || isNotificationsPopoverShown
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
Color.clear
|
||||
.frame(width: hostWidth, height: hostHeight)
|
||||
|
||||
TitlebarControlsView(
|
||||
notificationStore: notificationStore,
|
||||
viewModel: viewModel,
|
||||
onToggleSidebar: { _ = AppDelegate.shared?.sidebarState?.toggle() },
|
||||
onToggleNotifications: { [viewModel] in
|
||||
AppDelegate.shared?.toggleNotificationsPopover(
|
||||
animated: true,
|
||||
anchorView: viewModel.notificationsAnchorView
|
||||
)
|
||||
},
|
||||
onNewTab: { _ = AppDelegate.shared?.tabManager?.addTab() }
|
||||
)
|
||||
.opacity(shouldShowControls ? 1 : 0)
|
||||
.allowsHitTesting(shouldShowControls)
|
||||
.animation(.easeInOut(duration: 0.12), value: shouldShowControls)
|
||||
}
|
||||
.frame(width: hostWidth, height: hostHeight, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
.onHover { hovering in
|
||||
isHoveringControls = hovering
|
||||
}
|
||||
.onAppear {
|
||||
isNotificationsPopoverShown = AppDelegate.shared?.isNotificationsPopoverShown() ?? false
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .cmuxNotificationsPopoverVisibilityDidChange)) { notification in
|
||||
isNotificationsPopoverShown = (notification.userInfo?[NotificationsPopoverVisibilityUserInfoKey.isShown] as? Bool) ?? false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class TitlebarShortcutHintModifierMonitor: ObservableObject {
|
||||
@Published private(set) var isModifierPressed = false
|
||||
|
|
@ -714,6 +778,7 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
|
|||
private let viewModel = TitlebarControlsViewModel()
|
||||
private var userDefaultsObserver: NSObjectProtocol?
|
||||
var popoverIsShownForTesting: Bool { notificationsPopover.isShown }
|
||||
private var showWorkspaceTitlebar: Bool { WorkspaceTitlebarSettings.isVisible() }
|
||||
|
||||
init(notificationStore: TerminalNotificationStore) {
|
||||
self.notificationStore = notificationStore
|
||||
|
|
@ -749,9 +814,11 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
|
|||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.applyWorkspaceTitlebarVisibility()
|
||||
self?.scheduleSizeUpdate(invalidateFittingSize: true)
|
||||
}
|
||||
|
||||
applyWorkspaceTitlebarVisibility()
|
||||
scheduleSizeUpdate(invalidateFittingSize: true)
|
||||
}
|
||||
|
||||
|
|
@ -796,6 +863,8 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
|
|||
}
|
||||
|
||||
private func updateSize() {
|
||||
applyWorkspaceTitlebarVisibility()
|
||||
guard showWorkspaceTitlebar else { return }
|
||||
let contentSize: NSSize
|
||||
if fittingSizeNeedsRefresh || cachedFittingSize == nil {
|
||||
hostingView.invalidateIntrinsicContentSize()
|
||||
|
|
@ -828,6 +897,16 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
|
|||
hostingView.frame = NSRect(x: 0, y: yOffset, width: contentSize.width, height: contentSize.height)
|
||||
}
|
||||
|
||||
private func applyWorkspaceTitlebarVisibility() {
|
||||
let shouldShow = showWorkspaceTitlebar
|
||||
view.isHidden = !shouldShow
|
||||
if !shouldShow {
|
||||
preferredContentSize = .zero
|
||||
containerView.frame = .zero
|
||||
hostingView.frame = .zero
|
||||
}
|
||||
}
|
||||
|
||||
func toggleNotificationsPopover(animated: Bool = true, externalAnchor: NSView? = nil) {
|
||||
if notificationsPopover.isShown {
|
||||
notificationsPopover.performClose(nil)
|
||||
|
|
@ -861,6 +940,7 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
|
|||
if !anchorRect.isEmpty {
|
||||
notificationsPopover.animates = animated
|
||||
notificationsPopover.show(relativeTo: anchorRect, of: contentView, preferredEdge: .maxY)
|
||||
postNotificationsPopoverVisibilityDidChange(isShown: true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -871,6 +951,7 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
|
|||
if !anchorRect.isEmpty {
|
||||
notificationsPopover.animates = animated
|
||||
notificationsPopover.show(relativeTo: anchorRect, of: contentView, preferredEdge: .maxY)
|
||||
postNotificationsPopoverVisibilityDidChange(isShown: true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -880,6 +961,7 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
|
|||
let anchorRect = NSRect(x: 12, y: bounds.maxY - 8, width: 1, height: 1)
|
||||
notificationsPopover.animates = animated
|
||||
notificationsPopover.show(relativeTo: anchorRect, of: contentView, preferredEdge: .maxY)
|
||||
postNotificationsPopoverVisibilityDidChange(isShown: true)
|
||||
}
|
||||
|
||||
func dismissNotificationsPopover() {
|
||||
|
|
@ -902,6 +984,7 @@ final class TitlebarControlsAccessoryViewController: NSTitlebarAccessoryViewCont
|
|||
func popoverDidClose(_ notification: Notification) {
|
||||
// Clear the content view controller to stop SwiftUI observers when popover is hidden
|
||||
notificationsPopover.contentViewController = nil
|
||||
postNotificationsPopoverVisibilityDidChange(isShown: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ struct WorkspaceContentView: View {
|
|||
_ notificationPayloadHex: String?
|
||||
) -> Void)?
|
||||
@State private var config = WorkspaceContentView.resolveGhosttyAppearanceConfig(reason: "stateInit")
|
||||
@AppStorage(WorkspaceTitlebarSettings.showTitlebarKey)
|
||||
private var showWorkspaceTitlebar = WorkspaceTitlebarSettings.defaultShowTitlebar
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@EnvironmentObject var notificationStore: TerminalNotificationStore
|
||||
|
||||
|
|
@ -52,7 +54,7 @@ struct WorkspaceContentView: View {
|
|||
}
|
||||
}()
|
||||
|
||||
BonsplitView(controller: workspace.bonsplitController) { tab, paneId in
|
||||
let bonsplitView = BonsplitView(controller: workspace.bonsplitController) { tab, paneId in
|
||||
// Content for each tab in bonsplit
|
||||
let _ = Self.debugPanelLookup(tab: tab, workspace: workspace)
|
||||
if let panel = workspace.panel(for: tab.id) {
|
||||
|
|
@ -147,6 +149,15 @@ struct WorkspaceContentView: View {
|
|||
notificationPayloadHex: payloadHex
|
||||
)
|
||||
}
|
||||
|
||||
Group {
|
||||
if showWorkspaceTitlebar {
|
||||
bonsplitView
|
||||
} else {
|
||||
bonsplitView
|
||||
.ignoresSafeArea(.container, edges: .top)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func syncBonsplitNotificationBadges() {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,18 @@ import Darwin
|
|||
import Bonsplit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@main
|
||||
struct cmuxApp: App {
|
||||
@StateObject private var tabManager: TabManager
|
||||
|
|
@ -3085,6 +3097,8 @@ struct SettingsView: View {
|
|||
@AppStorage(LanguageSettings.languageKey) private var appLanguage = LanguageSettings.defaultLanguage.rawValue
|
||||
@AppStorage(AppearanceSettings.appearanceModeKey) private var appearanceMode = AppearanceSettings.defaultMode.rawValue
|
||||
@AppStorage(AppIconSettings.modeKey) private var appIconMode = AppIconSettings.defaultMode.rawValue
|
||||
@AppStorage(WorkspaceTitlebarSettings.showTitlebarKey)
|
||||
private var showWorkspaceTitlebar = WorkspaceTitlebarSettings.defaultShowTitlebar
|
||||
@AppStorage(SocketControlSettings.appStorageKey) private var socketControlMode = SocketControlSettings.defaultMode.rawValue
|
||||
@AppStorage(ClaudeCodeIntegrationSettings.hooksEnabledKey)
|
||||
private var claudeCodeHooksEnabled = ClaudeCodeIntegrationSettings.defaultHooksEnabled
|
||||
|
|
@ -3167,6 +3181,19 @@ struct SettingsView: View {
|
|||
NewWorkspacePlacement(rawValue: newWorkspacePlacement) ?? WorkspacePlacementSettings.defaultPlacement
|
||||
}
|
||||
|
||||
private var workspaceTitlebarSubtitle: String {
|
||||
if showWorkspaceTitlebar {
|
||||
return String(
|
||||
localized: "settings.app.showWorkspaceTitlebar.subtitleOn",
|
||||
defaultValue: "Show the folder and active title above pane tabs."
|
||||
)
|
||||
}
|
||||
return String(
|
||||
localized: "settings.app.showWorkspaceTitlebar.subtitleOff",
|
||||
defaultValue: "Hide the workspace title bar and show sidebar or pane actions only on hover."
|
||||
)
|
||||
}
|
||||
|
||||
private var selectedSidebarActiveTabIndicatorStyle: SidebarActiveTabIndicatorStyle {
|
||||
SidebarActiveTabIndicatorSettings.resolvedStyle(rawValue: sidebarActiveTabIndicatorStyle)
|
||||
}
|
||||
|
|
@ -3559,6 +3586,20 @@ struct SettingsView: View {
|
|||
|
||||
SettingsCardDivider()
|
||||
|
||||
SettingsCardRow(
|
||||
String(localized: "settings.app.showWorkspaceTitlebar", defaultValue: "Show Workspace Title Bar"),
|
||||
subtitle: workspaceTitlebarSubtitle
|
||||
) {
|
||||
Toggle("", isOn: $showWorkspaceTitlebar)
|
||||
.labelsHidden()
|
||||
.controlSize(.small)
|
||||
.accessibilityLabel(
|
||||
String(localized: "settings.app.showWorkspaceTitlebar", defaultValue: "Show Workspace Title Bar")
|
||||
)
|
||||
}
|
||||
|
||||
SettingsCardDivider()
|
||||
|
||||
SettingsCardRow(
|
||||
String(localized: "settings.app.reorderOnNotification", defaultValue: "Reorder on Notification"),
|
||||
subtitle: String(localized: "settings.app.reorderOnNotification.subtitle", defaultValue: "Move workspaces to the top when they receive a notification. Disable for stable shortcut positions.")
|
||||
|
|
@ -4623,6 +4664,7 @@ struct SettingsView: View {
|
|||
ShortcutHintDebugSettings.resetVisibilityDefaults()
|
||||
alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
|
||||
newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue
|
||||
showWorkspaceTitlebar = WorkspaceTitlebarSettings.defaultShowTitlebar
|
||||
workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
|
||||
sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails
|
||||
sidebarShowNotificationMessage = SidebarWorkspaceDetailSettings.defaultShowNotificationMessage
|
||||
|
|
|
|||
2
vendor/bonsplit
vendored
2
vendor/bonsplit
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 73c1ef2df9a6c8a2837212ecce900794d0f21826
|
||||
Subproject commit a55981319828bd832981c9be2275d199c266da41
|
||||
Loading…
Add table
Add a link
Reference in a new issue