import SwiftUI import SaplingCore public enum WorkspaceTreeSelection: Hashable, Sendable { case folder(URL) case file(URL) case project(URL) case subproject(URL) } public struct WorkspaceTreeView: View { private let workspace: Workspace? private let isLoadingWorkspace: Bool @Binding private var selection: WorkspaceTreeSelection? private let childrenFor: (WorkspaceItem) -> [WorkspaceItem] private let isLoadingChildren: (WorkspaceItem) -> Bool private let onExpandItem: (WorkspaceItem) -> Void private let onSelectFile: (WorkspaceFile) -> Void private let onSelectProject: (Project) -> Void private let onOpenWorkspace: () -> Void public init( workspace: Workspace?, isLoadingWorkspace: Bool = false, selection: Binding, childrenFor: @escaping (WorkspaceItem) -> [WorkspaceItem] = { $0.children ?? [] }, isLoadingChildren: @escaping (WorkspaceItem) -> Bool = { _ in false }, onExpandItem: @escaping (WorkspaceItem) -> Void = { _ in }, onSelectFile: @escaping (WorkspaceFile) -> Void, onSelectProject: @escaping (Project) -> Void, onOpenWorkspace: @escaping () -> Void ) { self.workspace = workspace self.isLoadingWorkspace = isLoadingWorkspace self._selection = selection self.childrenFor = childrenFor self.isLoadingChildren = isLoadingChildren self.onExpandItem = onExpandItem self.onSelectFile = onSelectFile self.onSelectProject = onSelectProject self.onOpenWorkspace = onOpenWorkspace } public var body: some View { Group { if let workspace { List(selection: $selection) { if isLoadingWorkspace { Label("Scanning Workspace...", systemImage: "progress.indicator") .foregroundStyle(.secondary) } Section(workspace.name) { ForEach(workspace.items, id: \.stableTreeID) { item in WorkspaceItemRow( item: item, selection: $selection, childrenFor: childrenFor, isLoadingChildren: isLoadingChildren, onExpandItem: onExpandItem, onSelectFile: onSelectFile, onSelectProject: onSelectProject ) } } } .listStyle(.sidebar) } else { ContentUnavailableView { Label(isLoadingWorkspace ? "Loading Workspace" : "No Workspace", systemImage: "folder") } description: { Text(isLoadingWorkspace ? "Scanning Workspace..." : "Choose a folder to browse Markdown files.") } actions: { if isLoadingWorkspace { ProgressView() } else { Button("Open Workspace...", action: onOpenWorkspace) } } } } .navigationTitle("Workspace") .transaction { transaction in transaction.animation = nil } } } private struct WorkspaceItemRow: View { let item: WorkspaceItem @Binding var selection: WorkspaceTreeSelection? let childrenFor: (WorkspaceItem) -> [WorkspaceItem] let isLoadingChildren: (WorkspaceItem) -> Bool let onExpandItem: (WorkspaceItem) -> Void let onSelectFile: (WorkspaceFile) -> Void let onSelectProject: (Project) -> Void @State private var isExpanded = false var body: some View { switch item { case .folder(let folder): DisclosureGroup(isExpanded: expansionBinding) { children } label: { Label(folder.name, systemImage: "folder") .contentShape(Rectangle()) .onTapGesture { selection = .folder(folder.url) } } .tag(WorkspaceTreeSelection.folder(folder.url)) case .file(let file): Button { selection = .file(file.url) if file.kind == .markdown { onSelectFile(file) } } label: { Label(file.name, systemImage: iconName(for: file.kind)) .foregroundStyle(file.kind == .markdown ? .primary : .secondary) } .buttonStyle(.plain) .tag(WorkspaceTreeSelection.file(file.url)) case .project(let project): DisclosureGroup(isExpanded: expansionBinding) { children } label: { Label(project.name, systemImage: "leaf") .fontWeight(.medium) .foregroundStyle(.primary) .contentShape(Rectangle()) .onTapGesture { selection = .project(project.repositoryURL) onSelectProject(project) } } .tag(WorkspaceTreeSelection.project(project.repositoryURL)) case .subproject(let subproject): Label(subproject.name, systemImage: "rectangle.connected.to.line.below") .foregroundStyle(.secondary) .tag(WorkspaceTreeSelection.subproject(subproject.repositoryURL)) } } private var expansionBinding: Binding { Binding( get: { isExpanded }, set: { newValue in var transaction = Transaction() transaction.animation = nil withTransaction(transaction) { isExpanded = newValue } if newValue { onExpandItem(item) } } ) } @ViewBuilder private var children: some View { if isLoadingChildren(item) { Label("Loading...", systemImage: "progress.indicator") .foregroundStyle(.secondary) } else { ForEach(childrenFor(item), id: \.stableTreeID) { child in WorkspaceItemRow( item: child, selection: $selection, childrenFor: childrenFor, isLoadingChildren: isLoadingChildren, onExpandItem: onExpandItem, onSelectFile: onSelectFile, onSelectProject: onSelectProject ) } } } private func iconName(for kind: WorkspaceFileKind) -> String { switch kind { case .markdown: "doc.plaintext" case .attachment: "paperclip" case .other: "doc" } } } private extension WorkspaceItem { var stableTreeID: URL { switch self { case .folder(let folder): return folder.url.standardizedFileURL case .file(let file): return file.url.standardizedFileURL case .project(let project): return project.repositoryURL.standardizedFileURL case .subproject(let subproject): return subproject.repositoryURL.standardizedFileURL } } }