diff --git a/Sources/CodeCanvas/CodeCanvas.swift b/Sources/CodeCanvas/CodeCanvas.swift index bd9df08..fb1eb3e 100644 --- a/Sources/CodeCanvas/CodeCanvas.swift +++ b/Sources/CodeCanvas/CodeCanvas.swift @@ -1,31 +1,26 @@ -import Foundation - #if canImport(SwiftUI) import SwiftUI -#endif -/// Public entry-points for the CodeCanvas module. -public enum CodeCanvasAPI { - /// Format the document text (placeholder using SwiftFormat later). - public static func format(_ doc: CodeDocument) -> CodeDocument { - // TODO: integrate real formatting via swift-format when needed. - return doc +public struct CodeCanvas: View { + private let extensions: [CodeCanvasExtension] + + public init(extensions: [CodeCanvasExtension]) { + self.extensions = extensions + } + + public var body: some View { + CodeCanvasShell(extensions: extensions) } } -#if canImport(SwiftUI) -/// A minimal SwiftUI-based editor view for Apple platforms. -public struct CodeCanvasView: View { - @State private var text: String +#else - public init(initialText: String = "// Welcome to CodeCanvas\n") { - self._text = State(initialValue: initialText) - } +public struct CodeCanvas { + public init() {} - public var body: some View { - TextEditor(text: $text) - .font(.system(.body, design: .monospaced)) - .padding() + public func start() { + CodeCanvasTUI().start() } } + #endif diff --git a/Sources/CodeCanvas/Console/CodeCanvasTUI.swift b/Sources/CodeCanvas/Console/CodeCanvasTUI.swift new file mode 100644 index 0000000..5658576 --- /dev/null +++ b/Sources/CodeCanvas/Console/CodeCanvasTUI.swift @@ -0,0 +1,7 @@ +import Foundation + +public struct CodeCanvasTUI { + public func start() { + + } +} diff --git a/Sources/CodeCanvas/Document.swift b/Sources/CodeCanvas/Document.swift deleted file mode 100644 index 8d617b5..0000000 --- a/Sources/CodeCanvas/Document.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -/// A minimal in-memory text document used by CodeCanvas. -public struct CodeDocument: Sendable, Equatable { - public var text: String - - public init(text: String = "") { - self.text = text - } -} diff --git a/Sources/CodeCanvas/Models/CodeCanvasBench.swift b/Sources/CodeCanvas/Models/CodeCanvasBench.swift new file mode 100644 index 0000000..9e11b0b --- /dev/null +++ b/Sources/CodeCanvas/Models/CodeCanvasBench.swift @@ -0,0 +1,27 @@ +#if canImport(SwiftUI) +import Foundation +import SwiftUI + +public protocol CodeCanvasBenchAction { + var id: String { get } + var title: String { get } + var icon: String { get } + func perform(on bench: CodeCanvasBench) +} + +public protocol CodeCanvasBenchComponent{ + var id: String { get } + var title: String { get } + var icon: String { get } + var actions: [CodeCanvasBenchAction] { get } + func content() -> AnyView +} + +public protocol CodeCanvasBench { + var id: String { get } + var name: String { get } + var icon: String { get } + var components: [CodeCanvasBenchComponent] { get } +} + +#endif diff --git a/Sources/CodeCanvas/Models/CodeCanvasExtension.swift b/Sources/CodeCanvas/Models/CodeCanvasExtension.swift new file mode 100644 index 0000000..275b1ab --- /dev/null +++ b/Sources/CodeCanvas/Models/CodeCanvasExtension.swift @@ -0,0 +1,13 @@ +#if canImport(SwiftUI) +public struct CodeCanvasExtension { + public let name: String + public let icon: String + public let benches: [CodeCanvasBench] + + public init(name: String, icon: String, benches: [CodeCanvasBench]) { + self.name = name + self.icon = icon + self.benches = benches + } +} +#endif diff --git a/Sources/CodeCanvas/Models/CodeCanvasFile.swift b/Sources/CodeCanvas/Models/CodeCanvasFile.swift new file mode 100644 index 0000000..5e2d5d0 --- /dev/null +++ b/Sources/CodeCanvas/Models/CodeCanvasFile.swift @@ -0,0 +1,11 @@ +public struct CodeCanvasFile: Identifiable { + public let id: String + public let name: String + public let content: String + + public init(id: String, name: String, content: String) { + self.id = id + self.name = name + self.content = content + } +} diff --git a/Sources/CodeCanvas/Models/CodeCanvasWorkspace.swift b/Sources/CodeCanvas/Models/CodeCanvasWorkspace.swift new file mode 100644 index 0000000..352dae3 --- /dev/null +++ b/Sources/CodeCanvas/Models/CodeCanvasWorkspace.swift @@ -0,0 +1,7 @@ +#if canImport(SwiftUI) +import SwiftUI + +public protocol CodeCanvasWorkspace: Identifiable { + func content() -> AnyView +} +#endif diff --git a/Sources/CodeCanvas/State/CodeCanvasStore.swift b/Sources/CodeCanvas/State/CodeCanvasStore.swift new file mode 100644 index 0000000..59289c1 --- /dev/null +++ b/Sources/CodeCanvas/State/CodeCanvasStore.swift @@ -0,0 +1,17 @@ +#if canImport(SwiftUI) +import SwiftUI + +public final class CodeCanvasStore: ObservableObject { + @Published public var workspace: (any CodeCanvasWorkspace)? + + public init() {} + + public func open(_ workspace: any CodeCanvasWorkspace) { + self.workspace = workspace + } + + public func clear() { + workspace = nil + } +} +#endif diff --git a/Sources/CodeCanvas/TUI.swift b/Sources/CodeCanvas/TUI.swift deleted file mode 100644 index ec4eab0..0000000 --- a/Sources/CodeCanvas/TUI.swift +++ /dev/null @@ -1,13 +0,0 @@ -#if !canImport(SwiftUI) -import Foundation - -/// Minimal TUI stub for non-Apple platforms (Linux/Windows). -public struct CodeCanvasTUI { - public init() {} - - public func start(with document: CodeDocument) { - // Placeholder TUI: just print the text for now. - print(document.text) - } -} -#endif diff --git a/Sources/CodeCanvas/Views/CodeCanvasShell.swift b/Sources/CodeCanvas/Views/CodeCanvasShell.swift new file mode 100644 index 0000000..e16ff31 --- /dev/null +++ b/Sources/CodeCanvas/Views/CodeCanvasShell.swift @@ -0,0 +1,41 @@ +#if canImport(SwiftUI) + import SwiftUI + + public struct CodeCanvasShell: View { + private let extensions: [CodeCanvasExtension] + + @State private var showInspector = false + @StateObject private var store = CodeCanvasStore() + + public init(extensions: [CodeCanvasExtension]) { + self.extensions = extensions + } + + public var body: some View { + NavigationSplitView { + CodeBenchContainer(benches: extensions.flatMap { $0.benches }) + } detail: { + CodeSpaceContainer() + } + .environmentObject(store) + .inspector( + isPresented: $showInspector, + content: { + CodeInspectorContainer() + #if os(macOS) + .inspectorColumnWidth(min: 340, ideal: 340, max: 680) + #endif + .toolbar { + Spacer() + Button(action: { + showInspector.toggle() + }) { + Label("Toggle Inspector", systemImage: "sidebar.right") + } + } + } + ) + } + } + +#endif diff --git a/Sources/CodeCanvas/Views/Contents/CodeBenchContainer.swift b/Sources/CodeCanvas/Views/Contents/CodeBenchContainer.swift new file mode 100644 index 0000000..bc60aaa --- /dev/null +++ b/Sources/CodeCanvas/Views/Contents/CodeBenchContainer.swift @@ -0,0 +1,53 @@ +#if canImport(SwiftUI) + import SwiftUI + + public struct CodeBenchContainer: View { + private let benches: [CodeCanvasBench] + @State private var selectedBench: (any CodeCanvasBench)? + @EnvironmentObject private var store: CodeCanvasStore + + public init(benches: [CodeCanvasBench]) { + self.benches = benches + _selectedBench = State(initialValue: benches.first) + } + + public var body: some View { + HStack(spacing: 0) { + CodeBenchSelector(benches: benches) { bench in + selectedBench = bench + store.clear() + } + .frame(maxHeight: .infinity) + + if let selectedBench { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + ForEach(selectedBench.components, id: \.id) { component in + DisclosureGroup { + component.content() + } label: { + HStack { + Text(component.title).font(.headline) + Spacer() + ForEach(component.actions, id: \.id) { action in + Button { + action.perform(on: selectedBench) + } label: { + Image(systemName: action.icon) + } + .buttonStyle(.plain) + .help(action.title) + } + } + } + } + } + } + } else { + Spacer() + } + } + } +} + +#endif diff --git a/Sources/CodeCanvas/Views/Contents/CodeInspectorContainer.swift b/Sources/CodeCanvas/Views/Contents/CodeInspectorContainer.swift new file mode 100644 index 0000000..9b583ed --- /dev/null +++ b/Sources/CodeCanvas/Views/Contents/CodeInspectorContainer.swift @@ -0,0 +1,15 @@ +#if canImport(SwiftUI) + import SwiftUI + + public struct CodeInspectorContainer: View { + public init() {} + + public var body: some View { + VStack { + Spacer() + Text("Inspector") + Spacer() + } + } + } +#endif diff --git a/Sources/CodeCanvas/Views/Contents/CodeSpaceContainer.swift b/Sources/CodeCanvas/Views/Contents/CodeSpaceContainer.swift new file mode 100644 index 0000000..3746d01 --- /dev/null +++ b/Sources/CodeCanvas/Views/Contents/CodeSpaceContainer.swift @@ -0,0 +1,17 @@ +#if canImport(SwiftUI) + import SwiftUI + + public struct CodeSpaceContainer: View { + @EnvironmentObject private var store: CodeCanvasStore + + public init() {} + + public var body: some View { + if let workspace = store.workspace { + workspace.content() + } else { + Text("Canvas") + } + } + } +#endif diff --git a/Sources/CodeCanvas/Views/Controls/CodeBenchSelector.swift b/Sources/CodeCanvas/Views/Controls/CodeBenchSelector.swift new file mode 100644 index 0000000..1798a49 --- /dev/null +++ b/Sources/CodeCanvas/Views/Controls/CodeBenchSelector.swift @@ -0,0 +1,43 @@ +#if canImport(SwiftUI) +import SwiftUI + +public struct CodeBenchSelector: View { + private let benches: [CodeCanvasBench] + private let onSelect: (CodeCanvasBench) -> Void + + @State private var selectedID: String? + + public init(benches: [CodeCanvasBench], onSelect: @escaping (CodeCanvasBench) -> Void) { + self.benches = benches + self.onSelect = onSelect + self._selectedID = State(initialValue: benches.first?.id) + } + + public var body: some View { + VStack(spacing: 15) { + ForEach(benches, id: \.id) { bench in + Button(action: { + selectedID = bench.id + onSelect(bench) + }) { + Image(systemName: bench.icon) + .font(.title) + .symbolVariant(selectedID == bench.id ? .fill : .none) + .frame(width: 50, height: 50) + .foregroundColor(selectedID == bench.id ? .accentColor : .secondary) + .background( + Color.secondary.opacity(selectedID == bench.id ? 0.25 : 0) + ) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + .buttonStyle(.plain) + .help(bench.name) + } + Spacer() + } + .padding(.vertical) + .frame(width: 60) + } +} + +#endif diff --git a/Sources/CodeCanvasShowCase/CodeCanvasShowCase.swift b/Sources/CodeCanvasShowCase/CodeCanvasShowCase.swift index 81aec8e..db34fe8 100644 --- a/Sources/CodeCanvasShowCase/CodeCanvasShowCase.swift +++ b/Sources/CodeCanvasShowCase/CodeCanvasShowCase.swift @@ -1,24 +1,42 @@ -import Foundation import CodeCanvas +import Foundation #if canImport(SwiftUI) -import SwiftUI + import SwiftUI -@main -struct CodeCanvasShowCaseApp: App { + @main + struct CodeCanvasShowCaseApp: App { + #if os(macOS) + // Ensure the app shows in Dock, has menu bar, and supports full screen when launched as a SwiftPM executable + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + #endif var body: some Scene { - WindowGroup { - CodeCanvasView(initialText: "// CodeCanvas Showcase\nprint(\"Hello, world!\")\n") - } + WindowGroup { + CodeCanvas( + extensions: [ + CodeCanvasExtension(name: "Editor", icon: "code", benches: [CodeEditorBench()]), + CodeCanvasExtension(name: "Restful", icon: "network", benches: [ClientRestfulBench()]), + ] + ) + } + } + } + #if os(macOS) + import AppKit + final class AppDelegate: NSObject, NSApplicationDelegate { + func applicationDidFinishLaunching(_ notification: Notification) { + // Switch activation policy to regular so we have Dock icon and menu bar + NSApp.setActivationPolicy(.regular) + NSApp.activate(ignoringOtherApps: true) + } } -} + #endif #else -@main -struct CodeCanvasShowCaseCLI { + @main + struct CodeCanvasShowCaseCLI { static func main() { - let doc = CodeDocument(text: "// CodeCanvas Showcase CLI\nprint(\"Hello, world!\")\n") - let tui = CodeCanvasTUI() - tui.start(with: doc) + let tui = CodeCanvas() + tui.start() } -} + } #endif diff --git a/Sources/CodeCanvasShowCase/Extensions/ClientRestfulExtension.swift b/Sources/CodeCanvasShowCase/Extensions/ClientRestfulExtension.swift new file mode 100644 index 0000000..693cae1 --- /dev/null +++ b/Sources/CodeCanvasShowCase/Extensions/ClientRestfulExtension.swift @@ -0,0 +1,57 @@ +#if canImport(SwiftUI) +import SwiftUI +import CodeCanvas + +public struct ClientRestfulWorkspace: CodeCanvasWorkspace { + public let id = UUID().uuidString + let trace: String + + public func content() -> AnyView { + AnyView( + VStack { + Text("Restful Client") + .font(.headline) + Text(trace) + } + .padding() + ) + } +} + +public struct ClientRestfulTraceCollection: CodeCanvasBenchComponent { + public let id: String = UUID().uuidString + public let title: String = "Traces" + public let icon: String = "network" + public var actions: [any CodeCanvasBenchAction] = [] + + private let traces: [String] + + public init(traces: [String] = ["GET /users", "POST /login"]) { + self.traces = traces + } + + public func content() -> AnyView { + AnyView(TraceListView(traces: traces)) + } + + struct TraceListView: View { + @EnvironmentObject var store: CodeCanvasStore + let traces: [String] + + var body: some View { + List(traces, id: \.self) { trace in + Button(trace) { + store.open(ClientRestfulWorkspace(trace: trace)) + } + } + } + } +} + +public struct ClientRestfulBench: CodeCanvasBench { + public let id: String = UUID().uuidString + public let name: String = "Restful Client" + public let icon: String = "network" + public let components: [CodeCanvasBenchComponent] = [ClientRestfulTraceCollection()] +} +#endif diff --git a/Sources/CodeCanvasShowCase/Extensions/CodeEditorExtension.swift b/Sources/CodeCanvasShowCase/Extensions/CodeEditorExtension.swift new file mode 100644 index 0000000..caf90c3 --- /dev/null +++ b/Sources/CodeCanvasShowCase/Extensions/CodeEditorExtension.swift @@ -0,0 +1,63 @@ +#if canImport(SwiftUI) +import SwiftUI +import CodeCanvas + +public struct CodeEditorWorkspace: CodeCanvasWorkspace { + public let id = UUID().uuidString + let file: CodeCanvasFile + + public func content() -> AnyView { + AnyView( + VStack(alignment: .leading) { + Text("Editing: \(file.name)") + .font(.headline) + ScrollView { + Text(file.content) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding() + ) + } +} + +public struct CodeCanvasBenchFileCollection: CodeCanvasBenchComponent { + public let id: String = UUID().uuidString + public let title: String = "File Collection" + public let icon: String = "folder" + public var actions: [any CodeCanvasBenchAction] = [] + + private let files: [CodeCanvasFile] + + public init(files: [CodeCanvasFile] = [ + CodeCanvasFile(id: "1", name: "Example.swift", content: "print(\"Hello\")"), + CodeCanvasFile(id: "2", name: "Data.json", content: "{\n \"value\": 42\n}") + ]) { + self.files = files + } + + public func content() -> AnyView { + AnyView(FileCollectionView(files: files)) + } + + struct FileCollectionView: View { + @EnvironmentObject var store: CodeCanvasStore + let files: [CodeCanvasFile] + + var body: some View { + List(files) { file in + Button(file.name) { + store.open(CodeEditorWorkspace(file: file)) + } + } + } + } +} + +public struct CodeEditorBench: CodeCanvasBench { + public let id: String = UUID().uuidString + public let name: String = "Editor" + public let icon: String = "document" + public let components: [CodeCanvasBenchComponent] = [CodeCanvasBenchFileCollection()] +} +#endif diff --git a/Tests/CodeCanvasTests/CodeCanvasTests.swift b/Tests/CodeCanvasTests/CodeCanvasTests.swift index 7fdc579..a448a10 100644 --- a/Tests/CodeCanvasTests/CodeCanvasTests.swift +++ b/Tests/CodeCanvasTests/CodeCanvasTests.swift @@ -1,15 +1,13 @@ -import XCTest +import Testing @testable import CodeCanvas +#if canImport(SwiftUI) +import SwiftUI +#endif -final class CodeCanvasTests: XCTestCase { - func testDocumentInit() { - let d = CodeDocument(text: "hello") - XCTAssertEqual(d.text, "hello") - } +struct CodeCanvasTests { +#if canImport(SwiftUI) - func testFormatIsIdentityForNow() { - let d = CodeDocument(text: "print(\"hi\")") - let formatted = CodeCanvasAPI.format(d) - XCTAssertEqual(formatted, d) - } +#else + +#endif }