Skip to content

Commit d0adbb2

Browse files
macOS Tahoe Tab Bar (#2130)
### Description Updates the tab bar for a new glass/capsule based design for macOS 26. This change is not retroactive and only affects CodeEdit running on macOS 26. The new design does two things primarily: - Tabs are now embedded in a glass container capsule. - Tabs are themselves glass, and are also capsule shaped. This changes makes the tab bar look at home on the new bubbly, liquid design of Tahoe. Tabs are still draggable, selectable, etc, they're just a little more prominently in their own container. We are not going to change our tab behavior to match Xcode, and this PR reflects that. Tabs still act the same way as they have in Sequoia, this is purely a design change. Tab content also still appears to slide underneath the toolbar. However due to the greater opacity of the toolbar, the split view was updated to be aware of the safe area and only draw where necessary. Alternatives were considered, such as trying to make the editor itself bleed over into the safe area, but due to SwiftUI limitations for escaping safe areas this was the best option I could find. ### Related Issues * closes #2123 * #2117 * #2129 - related, and commented on in code ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots Split tab panes with new tab bar design. <img width="1449" height="928" alt="Screenshot 2025-09-10 at 11 14 14 AM" src="https://github.com/user-attachments/assets/6d14cf30-259f-461b-85c9-9717702e730c" /> Some tab styles (selected, hovered, scrolled under container). <img width="414" height="132" alt="image" src="https://github.com/user-attachments/assets/9d171173-f689-4ec9-9ed7-bf15ffed9993" /> <img width="366" height="114" alt="image" src="https://github.com/user-attachments/assets/d1d27adf-2e74-47f2-8c84-23dd76bbf30a" /> <img width="476" height="78" alt="image" src="https://github.com/user-attachments/assets/338c1276-248e-44f7-b610-c9203e2b3482" />
1 parent e298776 commit d0adbb2

File tree

8 files changed

+147
-14
lines changed

8 files changed

+147
-14
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// GlassEffectView.swift
3+
// CodeEdit
4+
//
5+
// Created by Khan Winter on 9/2/25.
6+
//
7+
8+
import SwiftUI
9+
import AppKit
10+
11+
struct GlassEffectView: NSViewRepresentable {
12+
var tintColor: NSColor?
13+
14+
init(tintColor: NSColor? = nil) {
15+
self.tintColor = tintColor
16+
}
17+
18+
func makeNSView(context: Context) -> NSView {
19+
#if compiler(>=6.2)
20+
if #available(macOS 26, *) {
21+
let view = NSGlassEffectView()
22+
view.cornerRadius = 0
23+
view.tintColor = tintColor
24+
return view
25+
}
26+
#endif
27+
return NSView()
28+
}
29+
30+
func updateNSView(_ nsView: NSView, context: Context) {
31+
#if compiler(>=6.2)
32+
if #available(macOS 26, *), let view = nsView as? NSGlassEffectView {
33+
view.tintColor = tintColor
34+
}
35+
#endif
36+
}
37+
}

CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabBackground.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@ struct EditorTabBackground: View {
2929
ZStack {
3030
if isActive {
3131
// Content background (visible if active)
32-
EffectView(.contentBackground)
33-
.opacity(isActive ? 1 : 0)
32+
if #available(macOS 26, *) {
33+
GlassEffectView()
34+
} else {
35+
EffectView(.contentBackground)
36+
}
3437

3538
// Accent color (visible if active)
3639
Color(.controlAccentColor)

CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabCloseButton.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ struct EditorTabCloseButton: View {
4242
.frame(width: buttonSize, height: buttonSize)
4343
.background(backgroundColor)
4444
.foregroundColor(isPressingClose ? .primary : .secondary)
45-
.clipShape(RoundedRectangle(cornerRadius: 2))
45+
.if(.tahoe) {
46+
$0.clipShape(Circle())
47+
} else: {
48+
$0.clipShape(RoundedRectangle(cornerRadius: 2))
49+
}
4650
.contentShape(Rectangle())
4751
.gesture(
4852
DragGesture(minimumDistance: 0)

CodeEdit/Features/Editor/TabBar/Tabs/Tab/EditorTabView.swift

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,11 @@ struct EditorTabView: View {
122122

123123
@ViewBuilder var content: some View {
124124
HStack(spacing: 0.0) {
125-
EditorTabDivider()
126-
.opacity(
127-
(isActive || inHoldingState) ? 0.0 : 1.0
128-
)
125+
126+
if #unavailable(macOS 26) {
127+
EditorTabDivider()
128+
.opacity((isActive || inHoldingState) ? 0.0 : 1.0)
129+
}
129130
// Tab content (icon and text).
130131
HStack(alignment: .center, spacing: 3) {
131132
Image(nsImage: tabFile.nsIcon)
@@ -165,14 +166,19 @@ struct EditorTabView: View {
165166
}
166167
.frame(maxWidth: .infinity, alignment: .leading)
167168
}
169+
.if(.tahoe) {
170+
$0.padding(.horizontal, 1.5)
171+
}
168172
.opacity(
169173
// Inactive states for tab bar item content.
170174
activeState != .inactive
171175
? 1.0
172176
: isActive ? 0.6 : 0.4
173177
)
174-
EditorTabDivider()
175-
.opacity((isActive || inHoldingState) ? 0.0 : 1.0)
178+
if #unavailable(macOS 26) {
179+
EditorTabDivider()
180+
.opacity((isActive || inHoldingState) ? 0.0 : 1.0)
181+
}
176182
}
177183
.foregroundColor(
178184
isActive && isActiveEditor
@@ -220,6 +226,11 @@ struct EditorTabView: View {
220226
EditorTabBackground(isActive: isActive, isPressing: isPressing, isDragging: isDragging)
221227
.animation(.easeInOut(duration: 0.08), value: isHovering)
222228
}
229+
.if(.tahoe) {
230+
if #available(macOS 26, *) {
231+
$0.clipShape(Capsule()).clipped().containerShape(Capsule())
232+
}
233+
}
223234
// TODO: Enable the following code snippet when dragging-out behavior should be allowed.
224235
// Since we didn't handle the drop-outside event, dragging-out is disabled for now.
225236
// .onDrag({

CodeEdit/Features/Editor/TabBar/Tabs/Views/EditorTabs.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,12 @@ struct EditorTabs: View {
260260
) {
261261
ForEach(Array(openedTabs.enumerated()), id: \.element) { index, id in
262262
if let item = editor.tabs.first(where: { $0.file.id == id }) {
263+
if index != 0
264+
&& editor.selectedTab?.file.id != id
265+
&& editor.selectedTab?.file.id != openedTabs[index - 1] {
266+
EditorTabDivider()
267+
}
268+
263269
EditorTabView(
264270
file: item.file,
265271
index: index,
@@ -293,6 +299,12 @@ struct EditorTabs: View {
293299
tabWidth: $tabWidth
294300
)
295301
)
302+
303+
if index < openedTabs.count - 1
304+
&& editor.selectedTab?.file.id != id
305+
&& editor.selectedTab?.file.id != openedTabs[index + 1] {
306+
EditorTabDivider()
307+
}
296308
}
297309
}
298310
}
@@ -357,6 +369,19 @@ struct EditorTabs: View {
357369
)
358370
.opacity((scrollTrailingOffset ?? 0) <= 0 ? 0 : 1)
359371
}
372+
.if(.tahoe) {
373+
if #available(macOS 26.0, *) {
374+
// Unfortunate triple if here due to needing to compile on
375+
// earlier Xcodes.
376+
#if compiler(>=6.2)
377+
$0.background(GlassEffectView(tintColor: .tertiarySystemFill))
378+
.clipShape(Capsule())
379+
.clipped()
380+
#else
381+
$0
382+
#endif
383+
}
384+
}
360385
}
361386
}
362387

CodeEdit/Features/Editor/Views/EditorAreaView.swift

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ struct EditorAreaView: View {
3030
@Environment(\.window.value)
3131
private var window: NSWindow?
3232

33+
@Environment(\.isEditorLayoutAtEdge)
34+
private var isAtEdge
35+
3336
init(editor: Editor, focus: FocusState<Editor?>.Binding) {
3437
self.editor = editor
3538
self._focus = focus
@@ -101,6 +104,10 @@ struct EditorAreaView: View {
101104
}
102105

103106
VStack(spacing: 0) {
107+
if isAtEdge != .top, #available(macOS 26, *) {
108+
Spacer().frame(height: 4)
109+
}
110+
104111
if topSafeArea > 0 {
105112
Rectangle()
106113
.fill(.clear)
@@ -111,7 +118,9 @@ struct EditorAreaView: View {
111118
EditorTabBarView(hasTopInsets: topSafeArea > 0, codeFile: fileBinding)
112119
.id("TabBarView" + editor.id.uuidString)
113120
.environmentObject(editor)
114-
Divider()
121+
if #unavailable(macOS 26) {
122+
Divider()
123+
}
115124
}
116125
if showEditorJumpBar {
117126
EditorJumpBarView(
@@ -125,13 +134,47 @@ struct EditorAreaView: View {
125134
}
126135
.environmentObject(editor)
127136
.padding(.top, shouldShowTabBar ? -1 : 0)
137+
if #unavailable(macOS 26) {
138+
Divider()
139+
}
140+
}
141+
// On Tahoe we only show one divider
142+
if #available(macOS 26, *), shouldShowTabBar || showEditorJumpBar {
128143
Divider()
129144
}
130145
}
131146
.environment(\.isActiveEditor, editor == editorManager.activeEditor)
132147
.if(.tahoe) {
133148
// FB20047271: Glass toolbar effect ignores floating scroll view views.
134149
// https://openradar.appspot.com/radar?id=EhAKBVJhZGFyEICAgKbGmesJ
150+
151+
// FB20191516: Can't disable backgrounded liquid glass tint
152+
// https://openradar.appspot.com/radar?id=EhAKBVJhZGFyEICAgLqTk-4J
153+
// Tracking Issue: #2191
154+
// Add this to the top:
155+
// ```
156+
// @AppSettings(\.theme.useThemeBackground)
157+
// var useThemeBackground
158+
//
159+
// private var backgroundColor: NSColor {
160+
// let fallback = NSColor.textBackgroundColor
161+
// return if useThemeBackground {
162+
// ThemeModel.shared.selectedTheme?.editor.background.nsColor ?? fallback
163+
// } else {
164+
// fallback
165+
// }
166+
// }
167+
// ```
168+
// And use this:
169+
// ```
170+
// $0.background(
171+
// Rectangle().fill(.clear)
172+
// .glassEffect(.regular.tint(Color(backgroundColor))
173+
// .ignoresSafeArea(.all)
174+
// )
175+
// ```
176+
// When we can figure out how to disable the 'not focused' glass effect.
177+
135178
$0.background(EffectView(.headerView).ignoresSafeArea(.all))
136179
} else: {
137180
$0.background(EffectView(.headerView))

CodeEdit/Features/Editor/Views/EditorLayoutView.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ struct EditorLayoutView: View {
1515
@Environment(\.window.value)
1616
private var window
1717

18-
@Environment(\.isAtEdge)
18+
@Environment(\.isEditorLayoutAtEdge)
1919
private var isAtEdge
2020

2121
var toolbarHeight: CGFloat {
@@ -63,7 +63,7 @@ struct EditorLayoutView: View {
6363
var splitView: some View {
6464
ForEach(Array(data.editorLayouts.enumerated()), id: \.offset) { index, item in
6565
EditorLayoutView(layout: item, focus: $focus)
66-
.transformEnvironment(\.isAtEdge) { belowToolbar in
66+
.transformEnvironment(\.isEditorLayoutAtEdge) { belowToolbar in
6767
calcIsAtEdge(current: &belowToolbar, index: index)
6868
}
6969
.environment(\.splitEditor) { [weak data] edge, newEditor in
@@ -87,12 +87,12 @@ struct EditorLayoutView: View {
8787
}
8888
}
8989

90-
private struct BelowToolbarEnvironmentKey: EnvironmentKey {
90+
struct BelowToolbarEnvironmentKey: EnvironmentKey {
9191
static var defaultValue: VerticalEdge.Set = .all
9292
}
9393

9494
extension EnvironmentValues {
95-
fileprivate var isAtEdge: BelowToolbarEnvironmentKey.Value {
95+
var isEditorLayoutAtEdge: BelowToolbarEnvironmentKey.Value {
9696
get { self[BelowToolbarEnvironmentKey.self] }
9797
set { self[BelowToolbarEnvironmentKey.self] = newValue }
9898
}

CodeEdit/Features/SplitView/Views/SplitViewControllerView.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ final class SplitViewController: NSSplitViewController {
9696
override var dividerThickness: CGFloat {
9797
customDividerStyle.customThickness ?? super.dividerThickness
9898
}
99+
100+
override func drawDivider(in rect: NSRect) {
101+
let safeRect = NSRect(
102+
x: rect.origin.x,
103+
y: max(rect.origin.y, safeAreaRect.origin.y),
104+
width: isVertical ? dividerThickness : rect.width,
105+
height: isVertical ? safeAreaRect.height : dividerThickness
106+
)
107+
super.drawDivider(in: safeRect)
108+
}
99109
}
100110

101111
var items: [SplitViewItem] = []

0 commit comments

Comments
 (0)