diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index b1cf239e4639..73b61f28e6d3 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -88,6 +88,7 @@ A50019B0 /* SettingsSearchAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50019B1 /* SettingsSearchAliases.swift */; }; A50019B2 /* SettingsSearchIndexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50019B3 /* SettingsSearchIndexTests.swift */; }; A5001002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001012 /* ContentView.swift */; }; + C0DE35010000000000000001 /* SidebarScrim.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE35010000000000000002 /* SidebarScrim.swift */; }; 62270F3DCECB4787D789CCE3 /* SidebarWorkspaceSnapshotRefreshPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F016B5C09357B3226FA2E014 /* SidebarWorkspaceSnapshotRefreshPolicyTests.swift */; }; C3408A000000000000000001 /* ContentView+RightSidebarCommandPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3408A000000000000000002 /* ContentView+RightSidebarCommandPalette.swift */; }; D7AB00000000000000000003 /* ContentView+MoveTabToNewWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000000004 /* ContentView+MoveTabToNewWorkspace.swift */; }; @@ -443,6 +444,7 @@ A50019B1 /* SettingsSearchAliases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSearchAliases.swift; sourceTree = ""; }; A50019B3 /* SettingsSearchIndexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSearchIndexTests.swift; sourceTree = ""; }; A5001012 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + C0DE35010000000000000002 /* SidebarScrim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarScrim.swift; sourceTree = ""; }; C3408A000000000000000002 /* ContentView+RightSidebarCommandPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+RightSidebarCommandPalette.swift"; sourceTree = ""; }; D7AB00000000000000000004 /* ContentView+MoveTabToNewWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+MoveTabToNewWorkspace.swift"; sourceTree = ""; }; D7AB34300000000000000002 /* SidebarDropPlanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/SidebarDropPlanner.swift; sourceTree = ""; }; @@ -838,6 +840,7 @@ A50019A1 /* SettingsNavigation.swift */, A50019B1 /* SettingsSearchAliases.swift */, A5001012 /* ContentView.swift */, + C0DE35010000000000000002 /* SidebarScrim.swift */, C3408A000000000000000002 /* ContentView+RightSidebarCommandPalette.swift */, D7AB00000000000000000004 /* ContentView+MoveTabToNewWorkspace.swift */, D7AB34300000000000000002 /* SidebarDropPlanner.swift */, @@ -1344,6 +1347,7 @@ A50019A0 /* SettingsNavigation.swift in Sources */, A50019B0 /* SettingsSearchAliases.swift in Sources */, A5001002 /* ContentView.swift in Sources */, + C0DE35010000000000000001 /* SidebarScrim.swift in Sources */, C3408A000000000000000001 /* ContentView+RightSidebarCommandPalette.swift in Sources */, D7AB00000000000000000003 /* ContentView+MoveTabToNewWorkspace.swift in Sources */, D7AB34300000000000000001 /* SidebarDropPlanner.swift in Sources */, diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 86aa83b75d8d..ed4009f4130f 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -9809,6 +9809,10 @@ struct VerticalTabsSidebar: View { SidebarWorkspaceListMetrics.topScrimHeight } + private var sidebarBottomScrimHeight: CGFloat { + SidebarWorkspaceListMetrics.bottomScrimHeight + } + private var isMinimalMode: Bool { WorkspacePresentationModeSettings.mode(for: workspacePresentationMode) == .minimal } @@ -9909,7 +9913,7 @@ struct VerticalTabsSidebar: View { workspaceTerminalScrollBarHiddenById: workspaceTerminalScrollBarHiddenById ) - VStack(spacing: 0) { + ZStack(alignment: .bottomLeading) { workspaceScrollArea(renderContext: renderContext) SidebarFooter(updateViewModel: updateViewModel, fileExplorerState: fileExplorerState, onSendFeedback: onSendFeedback) .frame(maxWidth: .infinity, alignment: .leading) @@ -10008,14 +10012,21 @@ struct VerticalTabsSidebar: View { .frame(width: 0, height: 0) ) .safeAreaInset(edge: .top, spacing: 0) { - Color.clear - .frame(height: workspaceScrollTopVisibilityInset) + Color.clear.frame(height: workspaceScrollTopVisibilityInset) + .allowsHitTesting(false) + } + .safeAreaInset(edge: .bottom, spacing: 0) { + Color.clear.frame(height: sidebarBottomScrimHeight) .allowsHitTesting(false) } .overlay(alignment: .top) { SidebarTopScrim(height: sidebarTopScrimHeight) .allowsHitTesting(false) } + .overlay(alignment: .bottom) { + SidebarBottomScrim(height: sidebarBottomScrimHeight) + .allowsHitTesting(false) + } .overlay(alignment: .top) { // The sidebar top strip remains draggable and handles // double-clicks with the standard titlebar action. @@ -12427,15 +12438,6 @@ private struct SidebarDevFooter: View { } #endif -private struct SidebarTopScrim: View { - let height: CGFloat - - var body: some View { - Color.clear - .frame(height: height) - } -} - private struct SidebarScrollViewResolver: NSViewRepresentable { let onResolve: (NSScrollView?) -> Void diff --git a/Sources/SidebarScrim.swift b/Sources/SidebarScrim.swift new file mode 100644 index 000000000000..87f182cd60de --- /dev/null +++ b/Sources/SidebarScrim.swift @@ -0,0 +1,68 @@ +import AppKit +import SwiftUI + +struct SidebarTopScrim: View { + let height: CGFloat + + var body: some View { + SidebarEdgeScrim(height: height, edge: .top) + } +} + +struct SidebarBottomScrim: View { + let height: CGFloat + + var body: some View { + SidebarEdgeScrim(height: height, edge: .bottom) + } +} + +struct SidebarEdgeScrim: View { + enum Edge { + case top + case bottom + } + + let height: CGFloat + let edge: Edge + + var body: some View { + SidebarEdgeBlurEffect() + .frame(height: height) + .mask( + LinearGradient( + colors: gradientColors, + startPoint: .top, + endPoint: .bottom + ) + ) + } + + private var gradientColors: [Color] { + let colors = [ + Color.black.opacity(0.95), + Color.black.opacity(0.75), + Color.black.opacity(0.35), + Color.clear, + ] + switch edge { + case .top: + return colors + case .bottom: + return Array(colors.reversed()) + } + } +} + +struct SidebarEdgeBlurEffect: NSViewRepresentable { + func makeNSView(context: Context) -> NSVisualEffectView { + let view = NSVisualEffectView() + view.blendingMode = .withinWindow + view.material = .underWindowBackground + view.state = .active + view.isEmphasized = false + return view + } + + func updateNSView(_ nsView: NSVisualEffectView, context: Context) {} +} diff --git a/Sources/WindowChromeMetrics.swift b/Sources/WindowChromeMetrics.swift index 300cedeb7bc7..ec6d1d016cad 100644 --- a/Sources/WindowChromeMetrics.swift +++ b/Sources/WindowChromeMetrics.swift @@ -32,6 +32,7 @@ enum SidebarWorkspaceListMetrics { static let firstRowTopOffset: CGFloat = MinimalModeChromeMetrics.titlebarHeight + 2 static let rowVerticalPadding: CGFloat = 8 static let topScrimHeight: CGFloat = firstRowTopOffset + 20 + static let bottomScrimHeight: CGFloat = topScrimHeight static var scrollTopInset: CGFloat { max(0, firstRowTopOffset - rowVerticalPadding)