Skip to content

Jump Bar Overflow Accordion Effect #2045

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,19 @@ struct EditorJumpBarComponent: View {
@State var selection: CEWorkspaceFile
@State var isHovering: Bool = false
@State var button = NSPopUpButton()
@Binding var truncatedCrumbWidth: CGFloat?

init(
fileItem: CEWorkspaceFile,
tappedOpenFile: @escaping (CEWorkspaceFile) -> Void,
isLastItem: Bool
isLastItem: Bool,
isTruncated: Binding<CGFloat?>
) {
self.fileItem = fileItem
self._selection = .init(wrappedValue: fileItem)
self.tappedOpenFile = tappedOpenFile
self.isLastItem = isLastItem
self._truncatedCrumbWidth = isTruncated
}

var siblings: [CEWorkspaceFile] {
Expand Down Expand Up @@ -65,6 +68,28 @@ struct EditorJumpBarComponent: View {

return button
}
.frame(
maxWidth: isHovering || isLastItem ? nil : truncatedCrumbWidth,
alignment: .leading
)
.mask(
LinearGradient(
gradient: Gradient(
stops: truncatedCrumbWidth == nil ?
[
.init(color: .black, location: 0),
.init(color: .black, location: 1)
] : [
.init(color: .black, location: 0),
.init(color: .black, location: 0.8),
.init(color: .clear, location: 1)
]
),
startPoint: .leading,
endPoint: .trailing
)
)
.clipped()
.padding(.trailing, 11)
.background {
Color(nsColor: colorScheme == .dark ? .white : .black)
Expand All @@ -83,7 +108,9 @@ struct EditorJumpBarComponent: View {
}
.padding(.vertical, 3)
.onHover { hover in
isHovering = hover
withAnimation(.easeInOut(duration: 0.2)) {
isHovering = hover
}
}
.onLongPressGesture(minimumDuration: 0) {
button.performClick(nil)
Expand Down
111 changes: 94 additions & 17 deletions CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ struct EditorJumpBarView: View {

static let height = 28.0

@State private var textWidth: CGFloat = 0
@State private var containerWidth: CGFloat = 0
@State private var isTruncated: Bool = false
@State private var crumbWidth: CGFloat?
@State private var firstCrumbWidth: CGFloat?

init(
file: CEWorkspaceFile?,
shouldShowTabBar: Bool,
Expand All @@ -50,25 +56,60 @@ struct EditorJumpBarView: View {
}

var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
if file == nil {
Text("No Selection")
.font(.system(size: 11, weight: .regular))
.foregroundColor(
activeState != .inactive
? isActiveEditor ? .primary : .secondary
: Color(nsColor: .tertiaryLabelColor)
)
} else {
ForEach(fileItems, id: \.self) { fileItem in
EditorJumpBarComponent(
fileItem: fileItem,
tappedOpenFile: tappedOpenFile,
isLastItem: fileItems.last == fileItem
)
GeometryReader { containerProxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
if file == nil {
Text("No Selection")
.font(.system(size: 11, weight: .regular))
.foregroundColor(
activeState != .inactive
? isActiveEditor ? .primary : .secondary
: Color(nsColor: .tertiaryLabelColor)
)
} else {
ForEach(fileItems, id: \.self) { fileItem in
EditorJumpBarComponent(
fileItem: fileItem,
tappedOpenFile: tappedOpenFile,
isLastItem: fileItems.last == fileItem,
isTruncated: fileItems.first == fileItem ? $firstCrumbWidth : $crumbWidth
)
}

}
}
.background(
GeometryReader { proxy in
Color.clear
.onAppear {
if crumbWidth == nil {
textWidth = proxy.size.width
}
}
.onChange(of: proxy.size.width) { newValue in
if crumbWidth == nil {
textWidth = newValue
}
}
}
)
}
.onAppear {
containerWidth = containerProxy.size.width
}
.onChange(of: containerProxy.size.width) { newValue in
containerWidth = newValue
}
.onChange(of: textWidth) { _ in
withAnimation(.easeInOut(duration: 0.2)) {
resize()
}
}
.onChange(of: containerWidth) { _ in
withAnimation(.easeInOut(duration: 0.2)) {
resize()
}
}
}
.padding(.horizontal, shouldShowTabBar ? (file == nil ? 10 : 4) : 0)
Expand All @@ -86,4 +127,40 @@ struct EditorJumpBarView: View {
.opacity(activeState == .inactive ? 0.8 : 1.0)
.grayscale(isActiveEditor ? 0.0 : 1.0)
}

private func resize() {
let minWidth: CGFloat = 20
let snapThreshold: CGFloat = 30
let maxWidth: CGFloat = textWidth / CGFloat(fileItems.count)
let exponent: CGFloat = 5.0
var betweenWidth: CGFloat = 0.0

if textWidth >= containerWidth {
let scale = max(0, min(1, containerWidth / textWidth))
betweenWidth = floor((minWidth + (maxWidth - minWidth) * pow(scale, exponent)))
if betweenWidth < snapThreshold {
betweenWidth = minWidth
}
crumbWidth = betweenWidth
} else {
crumbWidth = nil
}

if betweenWidth > snapThreshold {
firstCrumbWidth = nil
} else {
let otherCrumbs = CGFloat(max(fileItems.count - 1, 1))
let usedWidth = otherCrumbs * snapThreshold

// Multiplier to reserve extra space for other crumbs in the jump bar.
// Increasing this value causes the first crumb to truncate sooner.
let crumbSpacingMultiplier: CGFloat = 1.5
let availableForFirst = containerWidth - usedWidth * crumbSpacingMultiplier
if availableForFirst < snapThreshold {
firstCrumbWidth = minWidth
} else {
firstCrumbWidth = availableForFirst
}
}
}
}
Loading