diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index f6474644e0..8a2824db43 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -32,13 +32,13 @@ 28CE5EA027E6493D0065D29C /* StatusBar in Frameworks */ = {isa = PBXBuildFile; productRef = 28CE5E9F27E6493D0065D29C /* StatusBar */; }; 28FFE1BF27E3A441001939DB /* SideBarToolbarBottom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FFE1BE27E3A441001939DB /* SideBarToolbarBottom.swift */; }; 2B7A583527E4BA0100D25D4E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0468438427DC76E200F8E88E /* AppDelegate.swift */; }; - 345F667527DF6C180069BD69 /* FileTabRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345F667427DF6C180069BD69 /* FileTabRow.swift */; }; 34EE19BE27E0469C00F152CE /* BlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34EE19BD27E0469C00F152CE /* BlurView.swift */; }; 5C403B8F27E20F8000788241 /* WorkspaceClient in Frameworks */ = {isa = PBXBuildFile; productRef = 5C403B8E27E20F8000788241 /* WorkspaceClient */; }; 5CF38A5E27E48E6C0096A0F7 /* CodeFile in Frameworks */ = {isa = PBXBuildFile; productRef = 5CF38A5D27E48E6C0096A0F7 /* CodeFile */; }; B658FB3427DA9E1000EA4DBD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B658FB3327DA9E1000EA4DBD /* Assets.xcassets */; }; B658FB3727DA9E1000EA4DBD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B658FB3627DA9E1000EA4DBD /* Preview Assets.xcassets */; }; B65E614627E6765D00255275 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = B65E614527E6765D00255275 /* Introspect */; }; + B673FDAD27E8296A00795864 /* PressActionsModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B673FDAC27E8296A00795864 /* PressActionsModifier.swift */; }; D70F5E2C27E4E8CF004EE4B9 /* WelcomeModule in Frameworks */ = {isa = PBXBuildFile; productRef = D70F5E2B27E4E8CF004EE4B9 /* WelcomeModule */; }; D7211D4327E066CE008F2ED7 /* Localized+Ex.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7211D4227E066CE008F2ED7 /* Localized+Ex.swift */; }; D7211D4727E06BFE008F2ED7 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D7211D4927E06BFE008F2ED7 /* Localizable.strings */; }; @@ -92,7 +92,6 @@ 289978EC27E4E97E00BB0357 /* FileIconStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileIconStyle.swift; sourceTree = ""; }; 28B0A19727E385C300B73177 /* SideBarToolbarTop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideBarToolbarTop.swift; sourceTree = ""; }; 28FFE1BE27E3A441001939DB /* SideBarToolbarBottom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideBarToolbarBottom.swift; sourceTree = ""; }; - 345F667427DF6C180069BD69 /* FileTabRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTabRow.swift; sourceTree = ""; }; 34EE19BD27E0469C00F152CE /* BlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurView.swift; sourceTree = ""; }; 5E3C6A3427E72AE000A7CA0D /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CodeEdit.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -102,6 +101,7 @@ B658FB3827DA9E1000EA4DBD /* CodeEdit.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CodeEdit.entitlements; sourceTree = ""; }; B658FB3D27DA9E1000EA4DBD /* CodeEditTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CodeEditTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; B658FB4727DA9E1000EA4DBD /* CodeEditUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CodeEditUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + B673FDAC27E8296A00795864 /* PressActionsModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PressActionsModifier.swift; sourceTree = ""; }; D7211D4227E066CE008F2ED7 /* Localized+Ex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Localized+Ex.swift"; sourceTree = ""; }; D7211D4827E06BFE008F2ED7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; D7211D4A27E06C01008F2ED7 /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sr; path = sr.lproj/Localizable.strings; sourceTree = ""; }; @@ -211,14 +211,6 @@ path = TabBar; sourceTree = ""; }; - 345F667327DF6BCC0069BD69 /* Rows */ = { - isa = PBXGroup; - children = ( - 345F667427DF6C180069BD69 /* FileTabRow.swift */, - ); - path = Rows; - sourceTree = ""; - }; 34EE19BC27E0467F00F152CE /* CustomViews */ = { isa = PBXGroup; children = ( @@ -261,11 +253,11 @@ D72E1A8127E3B0A300EB11B9 /* Welcome */, 043C321227E31FE8006AE443 /* Documents */, 0468438427DC76E200F8E88E /* AppDelegate.swift */, + B673FDAC27E8296A00795864 /* PressActionsModifier.swift */, B658FB3127DA9E0F00EA4DBD /* WorkspaceView.swift */, 2875A46E27E3BE63007805F8 /* Breadcrumbs */, 287776EA27E350A100D46668 /* SideBar */, 287776EB27E350BA00D46668 /* TabBar */, - 345F667327DF6BCC0069BD69 /* Rows */, 04F2BF1027DBB3AF0024EAB1 /* Settings */, 34EE19BC27E0467F00F152CE /* CustomViews */, D7211D4427E066D4008F2ED7 /* Localization */, @@ -500,6 +492,7 @@ 04540D5B27DD08C300E91B77 /* SettingsView.swift in Sources */, D72E1A8927E44D7C00EB11B9 /* WelcomeWindowView.swift in Sources */, 04540D5C27DD08C300E91B77 /* GeneralSettingsView.swift in Sources */, + B673FDAD27E8296A00795864 /* PressActionsModifier.swift in Sources */, 043C321427E31FF6006AE443 /* CodeEditDocumentController.swift in Sources */, 04660F6427E3ACAF00477777 /* Appearances.swift in Sources */, 04540D5E27DD08C300E91B77 /* WorkspaceView.swift in Sources */, @@ -519,7 +512,6 @@ D72E1A8327E3B0D400EB11B9 /* WelcomeView.swift in Sources */, 043C321627E3201F006AE443 /* WorkspaceDocument.swift in Sources */, 28B0A19827E385C300B73177 /* SideBarToolbarTop.swift in Sources */, - 345F667527DF6C180069BD69 /* FileTabRow.swift in Sources */, 28FFE1BF27E3A441001939DB /* SideBarToolbarBottom.swift in Sources */, 0485EB2327E7791400138301 /* QuickOpenPreviewView.swift in Sources */, ); diff --git a/CodeEdit/CustomViews/BlurView.swift b/CodeEdit/CustomViews/BlurView.swift index b4acf57f4f..9ad6502fc4 100644 --- a/CodeEdit/CustomViews/BlurView.swift +++ b/CodeEdit/CustomViews/BlurView.swift @@ -16,7 +16,7 @@ struct BlurView: NSViewRepresentable { let visualEffectView = NSVisualEffectView() visualEffectView.material = material visualEffectView.blendingMode = blendingMode - visualEffectView.state = NSVisualEffectView.State.active + visualEffectView.state = NSVisualEffectView.State.followsWindowActiveState return visualEffectView } diff --git a/CodeEdit/Documents/WorkspaceCodeFileView.swift b/CodeEdit/Documents/WorkspaceCodeFileView.swift index 2a6db0fdc0..a5deea8514 100644 --- a/CodeEdit/Documents/WorkspaceCodeFileView.swift +++ b/CodeEdit/Documents/WorkspaceCodeFileView.swift @@ -23,7 +23,7 @@ struct WorkspaceCodeFileView: View { .safeAreaInset(edge: .top, spacing: 0) { VStack(spacing: 0) { TabBar(windowController: windowController, workspace: workspace) - CustomDivider() + TabBarDivider() BreadcrumbsView(item, workspace: workspace) } } diff --git a/CodeEdit/Documents/WorkspaceDocument.swift b/CodeEdit/Documents/WorkspaceDocument.swift index ed7418b195..40eddfc6a4 100644 --- a/CodeEdit/Documents/WorkspaceDocument.swift +++ b/CodeEdit/Documents/WorkspaceDocument.swift @@ -21,8 +21,11 @@ class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { @Published var sortFoldersOnTop: Bool = true @Published var fileItems: [WorkspaceClient.FileItem] = [] + var selected: WorkspaceClient.FileItem? { + guard let selectedId = selectedId else { return nil } + return fileItems.first(where: { $0.id == selectedId }) + } var quickOpenState: QuickOpenState? - var openedCodeFiles: [WorkspaceClient.FileItem: CodeFileDocument] = [:] private var cancellables = Set() @@ -48,6 +51,26 @@ class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { selectedId = openFileItems[idx - 1].id } } + func closeFileTabs(items: Items) where Items: Collection, Items.Element == WorkspaceClient.FileItem { + // TODO: Could potentially be optimized + for item in items { + closeFileTab(item: item) + } + } + + func closeFileTab(where predicate: (WorkspaceClient.FileItem) -> Bool) { + closeFileTabs(items: openFileItems.filter(predicate)) + } + + func closeFileTabs(after item: WorkspaceClient.FileItem) { + guard let startIdx = openFileItems.firstIndex(where: { $0.id == item.id }) else { + assert(false, "Expected file item to be present in openFileItems") + return + } + + let range = openFileItems[(startIdx+1)...] + closeFileTabs(items: range) + } func openFile(item: WorkspaceClient.FileItem) { do { diff --git a/CodeEdit/PressActionsModifier.swift b/CodeEdit/PressActionsModifier.swift new file mode 100644 index 0000000000..22a57ac210 --- /dev/null +++ b/CodeEdit/PressActionsModifier.swift @@ -0,0 +1,32 @@ +// +// Created by Gabriel Theodoropoulos on 1/11/20. +// + +import SwiftUI + +struct PressActions: ViewModifier { + var onPress: () -> Void + var onRelease: () -> Void + func body(content: Content) -> some View { + content + .simultaneousGesture( + DragGesture(minimumDistance: 0) + .onChanged({ _ in + onPress() + }) + .onEnded({ _ in + onRelease() + }) + ) + } +} + +extension View { + func pressAction(onPress: @escaping (() -> Void), onRelease: @escaping (() -> Void)) -> some View { + modifier(PressActions(onPress: { + onPress() + }, onRelease: { + onRelease() + })) + } +} diff --git a/CodeEdit/Rows/FileTabRow.swift b/CodeEdit/Rows/FileTabRow.swift deleted file mode 100644 index b94dd09e4a..0000000000 --- a/CodeEdit/Rows/FileTabRow.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// FileTabRow.swift -// CodeEdit -// -// Created by Rehatbir Singh on 14/03/2022. -// - -import SwiftUI -import WorkspaceClient - -struct FileTabRow: View { - @State var isHoveringClose: Bool = false - var fileItem: WorkspaceClient.FileItem - var isSelected: Bool - var isHovering: Bool - var closeAction: () -> Void - var body: some View { - let showingCloseButton = isHovering - HStack(alignment: .center, spacing: 5) { - ZStack { - if isSelected { - // Create a hidden button, if the tab is selected - // and hide the button in the ZStack. - Button(action: closeAction) { - Text("").hidden() - } - .frame(width: 0, height: 0) - .padding(0) - .opacity(0) - .keyboardShortcut("w", modifiers: [.command]) - } - Button(action: closeAction, label: { - Rectangle() - .fill(isHoveringClose - ? Color(nsColor: .secondaryLabelColor).opacity(0.28) - : Color(.clear)) - .frame(width: 16, height: 16) - .cornerRadius(2) - .overlay( - Image(systemName: "xmark") - .font(.system(size: 9.5, weight: .medium, design: .rounded)) - .foregroundColor(.secondary) - ) - }) - .buttonStyle(PlainButtonStyle()) - .accessibilityLabel(Text("Close")) - .opacity(showingCloseButton ? 1 : 0) - .onHover { hover in - isHoveringClose = hover - } - } - Image(systemName: fileItem.systemImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 12, height: 12) - Text(fileItem.url.lastPathComponent) - .font(.system(size: 11.0)) - .lineLimit(1) - } - .padding(.leading, 4) - .padding(.trailing, 28) - } -} - -struct FileTabRow_Previews: PreviewProvider { - static var previews: some View { - FileTabRow( - fileItem: WorkspaceClient.FileItem( - url: URL(string: "Code.swift")! - ), - isSelected: false, - isHovering: false, - closeAction: {} - ) - .frame(width: 160.0, height: 28.0) - } -} diff --git a/CodeEdit/SideBar/SideBarItem.swift b/CodeEdit/SideBar/SideBarItem.swift index f6efdd0404..389e526bfa 100644 --- a/CodeEdit/SideBar/SideBarItem.swift +++ b/CodeEdit/SideBar/SideBarItem.swift @@ -10,7 +10,6 @@ import WorkspaceClient import CodeFile struct SideBarItem: View { - @AppStorage(FileIconStyle.storageKey) var iconStyle: FileIconStyle = .default var item: WorkspaceClient.FileItem diff --git a/CodeEdit/TabBar/TabBar.swift b/CodeEdit/TabBar/TabBar.swift index 9a7aad6a96..9d8d983ddd 100644 --- a/CodeEdit/TabBar/TabBar.swift +++ b/CodeEdit/TabBar/TabBar.swift @@ -8,7 +8,7 @@ import SwiftUI import WorkspaceClient -struct CustomDivider: View { +struct TabBarDivider: View { @Environment(\.colorScheme) var colorScheme let height: CGFloat = 1 @@ -22,68 +22,36 @@ struct CustomDivider: View { } struct TabBar: View { + @Environment(\.colorScheme) var colorScheme var windowController: NSWindowController @ObservedObject var workspace: WorkspaceDocument var tabBarHeight = 28.0 var body: some View { VStack(spacing: 0.0) { - ScrollView(.horizontal, showsIndicators: false) { - ScrollViewReader { value in - HStack(alignment: .center, spacing: 0.0) { - ForEach(workspace.openFileItems, id: \.id) { item in - let isActive = workspace.selectedId == item.id - - Button( - action: { workspace.selectedId = item.id }, - label: { - if isActive { - TabBarItem( - item: item, - windowController: windowController, - workspace: workspace - ) - .background( - BlurView( - material: NSVisualEffectView.Material.titlebar, - blendingMode: NSVisualEffectView.BlendingMode.withinWindow - ) - ) - } else { - TabBarItem( - item: item, - windowController: windowController, - workspace: workspace - ) - } - } - ) - .animation(.easeOut(duration: 0.2), value: workspace.openFileItems) - .buttonStyle(.plain) - .id(item.id) - .keyboardShortcut( - self.getTabId(fileName: item.fileName), - modifiers: [.command] - ) + ZStack(alignment: .top) { + Rectangle() + .fill(Color(nsColor: .black).opacity(colorScheme == .dark ? 0.45 : 0.05)) + .frame(height: 28) + ScrollView(.horizontal, showsIndicators: false) { + ScrollViewReader { value in + HStack(alignment: .center, spacing: -1) { + ForEach(workspace.openFileItems, id: \.id) { item in + TabBarItem( + item: item, + windowController: windowController, + workspace: workspace + ) + } + } + .onAppear { + value.scrollTo(self.workspace.selectedId) } - } - .onAppear { - value.scrollTo(self.workspace.selectedId) } } + .padding(.leading, -1) } } - .background(BlurView(material: NSVisualEffectView.Material.windowBackground, + .background(BlurView(material: NSVisualEffectView.Material.titlebar, blendingMode: NSVisualEffectView.BlendingMode.withinWindow)) } - - func getTabId(fileName: String) -> KeyEquivalent { - for counter in 0..<9 where workspace.openFileItems.count > counter && - workspace.openFileItems[counter].fileName == fileName { - return KeyEquivalent.init( - Character.init("\(counter + 1)") - ) - } - - return "0" - } } diff --git a/CodeEdit/TabBar/TabBarItem.swift b/CodeEdit/TabBar/TabBarItem.swift index 647f3949fa..30b04f7b38 100644 --- a/CodeEdit/TabBar/TabBarItem.swift +++ b/CodeEdit/TabBar/TabBarItem.swift @@ -8,27 +8,103 @@ import SwiftUI import WorkspaceClient +struct TabDivider: View { + @Environment(\.colorScheme) var colorScheme + let width: CGFloat = 1 + + var body: some View { + Group { + Rectangle() + } + .frame(width: width) + .foregroundColor( + Color(nsColor: colorScheme == .dark ? .white : .black) + .opacity(colorScheme == .dark ? 0.08 : 0.12) + ) + } +} + struct TabBarItem: View { + @Environment(\.colorScheme) var colorScheme @State var isHovering: Bool = false - var item: WorkspaceClient.FileItem + @State var isHoveringClose: Bool = false + @State var isPressingClose: Bool = false + var item: WorkspaceClient.FileItem var windowController: NSWindowController + func closeAction () { + withAnimation { + workspace.closeFileTab(item: item) + } + } @ObservedObject var workspace: WorkspaceDocument var tabBarHeight: Double = 28.0 - var body: some View { - let isActive = item.id == workspace.selectedId + var isActive: Bool { + item.id == workspace.selectedId + } + @ViewBuilder + var content: some View { HStack(spacing: 0.0) { - FileTabRow(fileItem: item, isSelected: isActive, isHovering: isHovering) { - withAnimation { - workspace.closeFileTab(item: item) + TabDivider() + HStack(alignment: .center, spacing: 5) { + ZStack { + if isActive { + // Create a hidden button, if the tab is selected + // and hide the button in the ZStack. + Button(action: closeAction) { + Text("").hidden() + } + .frame(width: 0, height: 0) + .padding(0) + .opacity(0) + .keyboardShortcut("w", modifiers: [.command]) + } + Button(action: closeAction) { + Image(systemName: "xmark") + .font(.system(size: 9.5, weight: .medium, design: .rounded)) + .frame(width: 16, height: 16) + .contentShape(Rectangle()) + } + .buttonStyle(PlainButtonStyle()) + .foregroundColor(isPressingClose ? .primary : .secondary) + .background(colorScheme == .dark + ? Color(nsColor: .white).opacity(isPressingClose ? 0.32 : isHoveringClose ? 0.18 : 0) + : Color(nsColor: .black).opacity(isPressingClose ? 0.29 : isHoveringClose ? 0.11 : 0) + ) + .cornerRadius(2) + .accessibilityLabel(Text("Close")) + .onHover { hover in + isHoveringClose = hover + } + .pressAction { + isPressingClose = true + } onRelease: { + isPressingClose = false + } + .opacity(isHovering ? 1 : 0) + .animation(.easeInOut(duration: 0.15), value: isHovering) } + Image(systemName: item.systemImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 12, height: 12) + Text(item.url.lastPathComponent) + .font(.system(size: 11.0)) + .lineLimit(1) } - Divider() + .frame(height: 28) + .padding(.leading, 4) + .padding(.trailing, 28) + .background( + Color(nsColor: isActive ? .clear : .black) + .opacity( + colorScheme == .dark + ? isHovering ? 0.15 : 0.45 + : isHovering ? 0.15 : 0.05 + ) + .animation(.easeInOut(duration: 0.15), value: isHovering) + ) + TabDivider() } - .background( - Color(nsColor: .secondaryLabelColor) - .opacity(!isActive && isHovering ? 0.11 : 0) - .animation(.easeInOut(duration: 0.15)) - ) .frame(height: tabBarHeight) .foregroundColor(isActive ? .primary : .secondary) .onHover { hover in @@ -42,4 +118,50 @@ struct TabBarItem: View { } } } + var body: some View { + Button( + action: { workspace.selectedId = item.id }, + label: { content } + ) + .background(BlurView( + material: NSVisualEffectView.Material.titlebar, + blendingMode: NSVisualEffectView.BlendingMode.withinWindow + )) + .buttonStyle(.plain) + .id(item.id) + .keyboardShortcut( + workspace.getTabKeyEquivalent(item: item), + modifiers: [.command] + ) + .contextMenu { + Button("Close Tab") { + withAnimation { + workspace.closeFileTab(item: item) + } + } + Button("Close Other Tabs") { + withAnimation { + workspace.closeFileTab(where: { $0.id != item.id }) + } + } + Button("Close Tabs to the Right") { + withAnimation { + workspace.closeFileTabs(after: item) + } + } + } + } +} + +fileprivate extension WorkspaceDocument { + func getTabKeyEquivalent(item: WorkspaceClient.FileItem) -> KeyEquivalent { + for counter in 0..<9 where self.openFileItems.count > counter && + self.openFileItems[counter].fileName == item.fileName { + return KeyEquivalent.init( + Character.init("\(counter + 1)") + ) + } + + return "0" + } } diff --git a/CodeEdit/Welcome/WelcomeView.swift b/CodeEdit/Welcome/WelcomeView.swift index 6b961f3ca8..354b4c3a47 100644 --- a/CodeEdit/Welcome/WelcomeView.swift +++ b/CodeEdit/Welcome/WelcomeView.swift @@ -11,11 +11,10 @@ import Foundation import WelcomeModule struct WelcomeView: View { + @Environment(\.colorScheme) var colorScheme @State var isHovering: Bool = false @State var isHoveringClose: Bool = false - @AppStorage(ReopenBehavior.storageKey) var behavior: ReopenBehavior = .welcome - var dismissWindow: () -> Void private var dismissButton: some View { @@ -94,7 +93,7 @@ struct WelcomeView: View { .padding(.top, 20) .padding(.horizontal, 56) .padding(.bottom, 16) - .background(Color(nsColor: .windowBackgroundColor)) + .background(Color(nsColor: colorScheme == .dark ? .windowBackgroundColor : .white)) .onHover { isHovering in self.isHovering = isHovering }