diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 723038b1c..379f795b7 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 287776EF27E3515300D46668 /* TabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287776EE27E3515300D46668 /* TabBarItem.swift */; }; 289978ED27E4E97E00BB0357 /* FileIconStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 289978EC27E4E97E00BB0357 /* FileIconStyle.swift */; }; 28B0A19827E385C300B73177 /* SideBarToolbarTop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B0A19727E385C300B73177 /* SideBarToolbarTop.swift */; }; + 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 */; }; @@ -107,6 +108,7 @@ 5C403B8F27E20F8000788241 /* WorkspaceClient in Frameworks */, B65E614627E6765D00255275 /* Introspect in Frameworks */, D70F5E2C27E4E8CF004EE4B9 /* WelcomeModule in Frameworks */, + 28CE5EA027E6493D0065D29C /* StatusBar in Frameworks */, 5CF38A5E27E48E6C0096A0F7 /* CodeFile in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -302,6 +304,7 @@ 5CF38A5D27E48E6C0096A0F7 /* CodeFile */, D70F5E2B27E4E8CF004EE4B9 /* WelcomeModule */, B65E614527E6765D00255275 /* Introspect */, + 28CE5E9F27E6493D0065D29C /* StatusBar */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -867,6 +870,10 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 28CE5E9F27E6493D0065D29C /* StatusBar */ = { + isa = XCSwiftPackageProductDependency; + productName = StatusBar; + }; 5C403B8E27E20F8000788241 /* WorkspaceClient */ = { isa = XCSwiftPackageProductDependency; productName = WorkspaceClient; diff --git a/CodeEdit/SideBar/SideBarItem.swift b/CodeEdit/SideBar/SideBarItem.swift index 670852921..53accca17 100644 --- a/CodeEdit/SideBar/SideBarItem.swift +++ b/CodeEdit/SideBar/SideBarItem.swift @@ -8,6 +8,7 @@ import SwiftUI import WorkspaceClient import CodeFile +import StatusBar struct SideBarItem: View { @@ -40,6 +41,9 @@ struct SideBarItem: View { BreadcrumbsView(item, workspace: workspace) } } + .safeAreaInset(edge: .bottom) { + StatusBarView() + } } else { Text("CodeEdit cannot open this file because its file type is not supported.") } diff --git a/CodeEditModules/Modules/StatusBar/src/StatusBar.swift b/CodeEditModules/Modules/StatusBar/src/StatusBar.swift new file mode 100644 index 000000000..e2f5a970a --- /dev/null +++ b/CodeEditModules/Modules/StatusBar/src/StatusBar.swift @@ -0,0 +1,254 @@ +// +// StatusBar.swift +// +// +// Created by Lukas Pistrol on 19.03.22. +// + +import SwiftUI + +@available(macOS 12, *) +public struct StatusBarView: View { + + @ObservedObject private var model: StatusBarModel + + public init() { + self.model = .init() + } + + public var body: some View { + VStack(spacing: 0) { + bar + if model.isExpanded { + terminal + } + } + // removes weird light gray bar above when in light mode + .padding(.top, -8) // (comment out to make it look normal in preview) + } + + private var dragGesture: some Gesture { + DragGesture() + .onChanged { value in + let newHeight = max(0, min(height - value.translation.height, 500)) + if newHeight-1 > height || newHeight+1 < height { + height = newHeight + } + model.isExpanded = height < 1 ? false : true + } + } + + private var bar: some View { + ZStack { + Rectangle() + .foregroundStyle(.bar) + HStack(spacing: 14) { + HStack(spacing: 8) { + labelButton(model.errorCount.formatted(), image: "xmark.octagon") + labelButton(model.warningCount.formatted(), image: "exclamationmark.triangle") + } + branchPicker + reloadButton + Spacer() + cursorLocationLabel + indentSelector + encodingSelector + lineEndSelector + expandButton + } + .padding(.horizontal, 10) + } + .overlay(alignment: .top) { + Divider() + } + .frame(height: 32) + .gesture(dragGesture) + .onHover { hovering in + if hovering { + NSCursor.resizeUpDown.push() + } else { + NSCursor.pop() + } + } + } + + @State private var height: Double = 300 + + private var terminal: some View { + Rectangle() + .foregroundColor(Color(red: 0.163, green: 0.163, blue: 0.188, opacity: 1.000)) + .frame(minHeight: 0, idealHeight: height, maxHeight: height) + } + + private func labelButton(_ text: String, image: String) -> some View { + Button { + // show errors/warnings + } label: { + HStack(spacing: 4) { + Image(systemName: image) + .font(.headline) + Text(text) + } + } + .buttonStyle(.borderless) + .foregroundStyle(.primary) + .onHover { hovering in + if hovering { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + } + + private var branchPicker: some View { + Menu(model.branches[model.selectedBranch]) { + ForEach(model.branches.indices, id: \.self) { branch in + Button { model.selectedBranch = branch } label: { + Text(model.branches[branch]) + // checkout branch + } + } + } + .menuStyle(.borderlessButton) + .fixedSize() + .onHover { hovering in + if hovering { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + } + + private var reloadButton: some View { + // Temporary + Button { + model.isReloading = true + // Just for looks for now. In future we'll call a function like + // `reloadFileStatus()` here which will set/unset `reloading` + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.model.isReloading = false + } + } label: { + Image(systemName: "arrow.triangle.2.circlepath") + .imageScale(.large) + .rotationEffect(.degrees(model.isReloading ? 360 : 0)) + .animation(animation, value: model.isReloading) + .opacity(model.isReloading ? 1 : 0) + // A bit of a hacky solution to prevent spinning counterclockwise once `reloading` changes to `false` + .overlay { + Image(systemName: "arrow.triangle.2.circlepath") + .imageScale(.large) + .opacity(model.isReloading ? 0 : 1) + } + + } + .buttonStyle(.borderless) + .foregroundStyle(.primary) + .onHover { hovering in + if hovering { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + } + + // Temporary + private var animation: Animation { + // 10x speed when not reloading to make invisible ccw spin go fast in case button is pressed multiple times. + .linear.speed(model.isReloading ? 0.5 : 10) + } + + private var cursorLocationLabel: some View { + Text("Ln \(model.currentLine), Col \(model.currentCol)") + .foregroundStyle(.primary) + .onHover { hovering in + if hovering { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + } + + private var indentSelector: some View { + Menu("2 Spaces") { + // 2 spaces, 4 spaces, ... + } + .menuStyle(.borderlessButton) + .fixedSize() + .onHover { hovering in + if hovering { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + } + + private var encodingSelector: some View { + Menu("UTF 8") { + // UTF 8, ASCII, ... + } + .menuStyle(.borderlessButton) + .fixedSize() + .onHover { hovering in + if hovering { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + } + + private var lineEndSelector: some View { + Menu("LF") { + // LF, CRLF + } + .menuStyle(.borderlessButton) + .fixedSize() + .onHover { hovering in + if hovering { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + } + + private var expandButton: some View { + Button { + model.isExpanded.toggle() + if model.isExpanded && height < 1 { + height = 300 + } + // Show/hide terminal window + } label: { + Image(systemName: "rectangle.bottomthird.inset.filled") + .imageScale(.large) + } + .tint(model.isExpanded ? .accentColor : .primary) + .buttonStyle(.borderless) + .onHover { hovering in + if hovering { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + } +} + +@available(macOS 12, *) +struct SwiftUIView_Previews: PreviewProvider { + static var previews: some View { + ZStack(alignment: .bottom) { + Color.white + StatusBarView() + .previewLayout(.fixed(width: 1.336, height: 500.0)) + .preferredColorScheme(.light) + } + } +} diff --git a/CodeEditModules/Modules/StatusBar/src/StatusBarModel.swift b/CodeEditModules/Modules/StatusBar/src/StatusBarModel.swift new file mode 100644 index 000000000..457fe3c99 --- /dev/null +++ b/CodeEditModules/Modules/StatusBar/src/StatusBarModel.swift @@ -0,0 +1,29 @@ +// +// StatusBarModel.swift +// +// +// Created by Lukas Pistrol on 20.03.22. +// + +import Foundation + +public class StatusBarModel: ObservableObject { + + // TODO: Implement logic for updating values + @Published public var errorCount: Int = 0 // Implementation missing + @Published public var warningCount: Int = 0 // Implementation missing + + @Published public var branches: [String] = ["main"] // Implementation missing + @Published public var selectedBranch: Int = 0 // Implementation missing + + @Published public var isReloading: Bool = false // Implementation missing + + @Published public var currentLine: Int = 1 // Implementation missing + @Published public var currentCol: Int = 1 // Implementation missing + + @Published public var isExpanded: Bool = false // Implementation missing + + // TODO: Add @Published vars for indentation, encoding, linebreak + + public init() {} +} diff --git a/CodeEditModules/Package.swift b/CodeEditModules/Package.swift index f193d3954..fc39fed2f 100644 --- a/CodeEditModules/Package.swift +++ b/CodeEditModules/Package.swift @@ -17,7 +17,14 @@ let package = Package( name: "CodeFile", targets: ["CodeFile"] ), - .library(name: "WelcomeModule", targets: ["WelcomeModule"]) + .library( + name: "WelcomeModule", + targets: ["WelcomeModule"] + ), + .library( + name: "StatusBar", + targets: ["StatusBar"] + ) ], dependencies: [ .package( @@ -71,6 +78,10 @@ let package = Package( "SnapshotTesting" ], path: "Modules/WelcomeModule/Tests" - ) + ), + .target( + name: "StatusBar", + path: "Modules/StatusBar/src" + ) ] )