Skip to content

Commit 3d1ac2b

Browse files
authored
Jump Bar Overflow Accordion Effect (#2045)
### Description This PR introduces an initial implementation of an accordion-like effect for displaying file paths in the tab bar. The goal is to improve usability when file paths are too long to fit in the available space. **Current (Draft) Behavior:** - The accordion effect is always shown, regardless of whether the file path is truncated. **Expected Behavior:** - The accordion effect should only be triggered when a file path is truncated due to limited space in the scroll view. - If the file path fits within the visible area, the full path should be shown without any animation or effect. **Request for Feedback:** - I would appreciate guidance on the best way to detect when a file path is actually truncated, so the effect only triggers in those cases. - Feedback on the user experience and any edge cases to consider is also welcome. --- ### Related Issues * Related to #241 --- ### 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 https://github.com/user-attachments/assets/7ed81d9a-b4ea-4b4b-b97d-b68863e00e87
1 parent 41b590d commit 3d1ac2b

File tree

2 files changed

+124
-19
lines changed

2 files changed

+124
-19
lines changed

CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,19 @@ struct EditorJumpBarComponent: View {
2626
@State var selection: CEWorkspaceFile
2727
@State var isHovering: Bool = false
2828
@State var button = NSPopUpButton()
29+
@Binding var truncatedCrumbWidth: CGFloat?
2930

3031
init(
3132
fileItem: CEWorkspaceFile,
3233
tappedOpenFile: @escaping (CEWorkspaceFile) -> Void,
33-
isLastItem: Bool
34+
isLastItem: Bool,
35+
isTruncated: Binding<CGFloat?>
3436
) {
3537
self.fileItem = fileItem
3638
self._selection = .init(wrappedValue: fileItem)
3739
self.tappedOpenFile = tappedOpenFile
3840
self.isLastItem = isLastItem
41+
self._truncatedCrumbWidth = isTruncated
3942
}
4043

4144
var siblings: [CEWorkspaceFile] {
@@ -65,6 +68,28 @@ struct EditorJumpBarComponent: View {
6568

6669
return button
6770
}
71+
.frame(
72+
maxWidth: isHovering || isLastItem ? nil : truncatedCrumbWidth,
73+
alignment: .leading
74+
)
75+
.mask(
76+
LinearGradient(
77+
gradient: Gradient(
78+
stops: truncatedCrumbWidth == nil || isHovering ?
79+
[
80+
.init(color: .black, location: 0),
81+
.init(color: .black, location: 1)
82+
] : [
83+
.init(color: .black, location: 0),
84+
.init(color: .black, location: 0.8),
85+
.init(color: .clear, location: 1)
86+
]
87+
),
88+
startPoint: .leading,
89+
endPoint: .trailing
90+
)
91+
)
92+
.clipped()
6893
.padding(.trailing, 11)
6994
.background {
7095
Color(nsColor: colorScheme == .dark ? .white : .black)
@@ -83,7 +108,9 @@ struct EditorJumpBarComponent: View {
83108
}
84109
.padding(.vertical, 3)
85110
.onHover { hover in
86-
isHovering = hover
111+
withAnimation(.easeInOut(duration: 0.2)) {
112+
isHovering = hover
113+
}
87114
}
88115
.onLongPressGesture(minimumDuration: 0) {
89116
button.performClick(nil)

CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ struct EditorJumpBarView: View {
2525

2626
static let height = 28.0
2727

28+
@State private var textWidth: CGFloat = 0
29+
@State private var containerWidth: CGFloat = 0
30+
@State private var isTruncated: Bool = false
31+
@State private var crumbWidth: CGFloat?
32+
@State private var firstCrumbWidth: CGFloat?
33+
2834
init(
2935
file: CEWorkspaceFile?,
3036
shouldShowTabBar: Bool,
@@ -50,25 +56,61 @@ struct EditorJumpBarView: View {
5056
}
5157

5258
var body: some View {
53-
ScrollView(.horizontal, showsIndicators: false) {
54-
HStack(spacing: 0) {
55-
if file == nil {
56-
Text("No Selection")
57-
.font(.system(size: 11, weight: .regular))
58-
.foregroundColor(
59-
activeState != .inactive
60-
? isActiveEditor ? .primary : .secondary
61-
: Color(nsColor: .tertiaryLabelColor)
62-
)
63-
} else {
64-
ForEach(fileItems, id: \.self) { fileItem in
65-
EditorJumpBarComponent(
66-
fileItem: fileItem,
67-
tappedOpenFile: tappedOpenFile,
68-
isLastItem: fileItems.last == fileItem
69-
)
59+
GeometryReader { containerProxy in
60+
ScrollView(.horizontal, showsIndicators: false) {
61+
HStack(spacing: 0) {
62+
if file == nil {
63+
Text("No Selection")
64+
.font(.system(size: 11, weight: .regular))
65+
.foregroundColor(
66+
activeState != .inactive
67+
? isActiveEditor ? .primary : .secondary
68+
: Color(nsColor: .tertiaryLabelColor)
69+
)
70+
.frame(maxHeight: .infinity)
71+
} else {
72+
ForEach(fileItems, id: \.self) { fileItem in
73+
EditorJumpBarComponent(
74+
fileItem: fileItem,
75+
tappedOpenFile: tappedOpenFile,
76+
isLastItem: fileItems.last == fileItem,
77+
isTruncated: fileItems.first == fileItem ? $firstCrumbWidth : $crumbWidth
78+
)
79+
}
80+
7081
}
7182
}
83+
.background(
84+
GeometryReader { proxy in
85+
Color.clear
86+
.onAppear {
87+
if crumbWidth == nil {
88+
textWidth = proxy.size.width
89+
}
90+
}
91+
.onChange(of: proxy.size.width) { newValue in
92+
if crumbWidth == nil {
93+
textWidth = newValue
94+
}
95+
}
96+
}
97+
)
98+
}
99+
.onAppear {
100+
containerWidth = containerProxy.size.width
101+
}
102+
.onChange(of: containerProxy.size.width) { newValue in
103+
containerWidth = newValue
104+
}
105+
.onChange(of: textWidth) { _ in
106+
withAnimation(.easeInOut(duration: 0.2)) {
107+
resize()
108+
}
109+
}
110+
.onChange(of: containerWidth) { _ in
111+
withAnimation(.easeInOut(duration: 0.2)) {
112+
resize()
113+
}
72114
}
73115
}
74116
.padding(.horizontal, shouldShowTabBar ? (file == nil ? 10 : 4) : 0)
@@ -86,4 +128,40 @@ struct EditorJumpBarView: View {
86128
.opacity(activeState == .inactive ? 0.8 : 1.0)
87129
.grayscale(isActiveEditor ? 0.0 : 1.0)
88130
}
131+
132+
private func resize() {
133+
let minWidth: CGFloat = 20
134+
let snapThreshold: CGFloat = 30
135+
let maxWidth: CGFloat = textWidth / CGFloat(fileItems.count)
136+
let exponent: CGFloat = 5.0
137+
var betweenWidth: CGFloat = 0.0
138+
139+
if textWidth >= containerWidth {
140+
let scale = max(0, min(1, containerWidth / textWidth))
141+
betweenWidth = floor((minWidth + (maxWidth - minWidth) * pow(scale, exponent)))
142+
if betweenWidth < snapThreshold {
143+
betweenWidth = minWidth
144+
}
145+
crumbWidth = betweenWidth
146+
} else {
147+
crumbWidth = nil
148+
}
149+
150+
if betweenWidth > snapThreshold || crumbWidth == nil {
151+
firstCrumbWidth = nil
152+
} else {
153+
let otherCrumbs = CGFloat(max(fileItems.count - 1, 1))
154+
let usedWidth = otherCrumbs * snapThreshold
155+
156+
// Multiplier to reserve extra space for other crumbs in the jump bar.
157+
// Increasing this value causes the first crumb to truncate sooner.
158+
let crumbSpacingMultiplier: CGFloat = 1.5
159+
let availableForFirst = containerWidth - usedWidth * crumbSpacingMultiplier
160+
if availableForFirst < snapThreshold {
161+
firstCrumbWidth = minWidth
162+
} else {
163+
firstCrumbWidth = availableForFirst
164+
}
165+
}
166+
}
89167
}

0 commit comments

Comments
 (0)