diff --git a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift index 130fc6098..c6663721b 100644 --- a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift +++ b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarComponent.swift @@ -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 ) { self.fileItem = fileItem self._selection = .init(wrappedValue: fileItem) self.tappedOpenFile = tappedOpenFile self.isLastItem = isLastItem + self._truncatedCrumbWidth = isTruncated } var siblings: [CEWorkspaceFile] { @@ -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) @@ -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) diff --git a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift index ce53d07b8..5c70aef98 100644 --- a/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift +++ b/CodeEdit/Features/Editor/JumpBar/Views/EditorJumpBarView.swift @@ -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, @@ -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) @@ -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 + } + } + } }