// WorkspaceLayoutView.swift // Reads the WorkspaceManager's panel configuration and builds the // layout dynamically using GeometryReader. Panels are resolved to // concrete SwiftUI views via a factory. import SwiftUI struct WorkspaceLayoutView: View { let workspace: WorkspaceManager let context: MetalContext var body: some View { GeometryReader { geo in let totalWidth = geo.size.width let mainHeight = workspace.showTimeline ? geo.size.height * (1 - workspace.timelineHeight) : geo.size.height VStack(spacing: 0) { // ── Main row ── HStack(spacing: 0) { // Leading panels ForEach(workspace.leadingPanels) { slot in panelView(for: slot) .frame(width: totalWidth * slot.size) } // Center panels (split evenly among themselves) let centerSlots = workspace.centerPanels let centerTotalProportion = centerSlots.reduce(0) { $0 + $1.size } ForEach(Array(centerSlots.enumerated()), id: \.element.id) { idx, slot in let proportion = slot.size / max(centerTotalProportion, 0.01) let usedByEdges = workspace.leadingPanels.reduce(0) { $0 + $1.size } + workspace.trailingPanels.reduce(0) { $0 + $1.size } let centerWidth = totalWidth * (1 - usedByEdges) panelView(for: slot) .frame(width: centerWidth * proportion) // Divider between center panels if idx < centerSlots.count - 1 { PanelDivider(axis: .vertical) } } // Trailing panels ForEach(workspace.trailingPanels) { slot in PanelDivider(axis: .vertical) panelView(for: slot) .frame(width: totalWidth * slot.size) } } .frame(height: mainHeight) // ── Timeline (optional bottom panel) ── if workspace.showTimeline { PanelDivider(axis: .horizontal) AnimationPanelView(animSystem: context.animationSystem) .frame(height: geo.size.height * workspace.timelineHeight) } } } .clipped() } // MARK: - Panel factory @ViewBuilder private func panelView(for slot: PanelSlot) -> some View { switch slot.id { case .toolSidebar: ToolSidebar(activeTool: .constant(.cursor)) case .viewport3D: if context.renderers.indices.contains(0) { ViewportPane(renderer: context.renderers[0], label: "3D Viewport") } case .uvEditor: UVEditorView() case .textureBake: TextureBakePanel() case .brushPanel: GPBrushPanel() case .gpCanvas: if context.renderers.indices.contains(0) { GPCanvasView(renderer: context.renderers[0]) } case .gpLayers: GPLayerPanel() case .onionSkin: OnionSkinPanel() case .outliner: OutlinerPanel(sceneGraph: context.sceneGraph) case .properties: PropertiesPanel( isExpanded: .constant(true), sceneGraph: context.sceneGraph ) case .renderSettings: RenderSettingsPlaceholder() case .timeline: TimelinePlaceholder() } } } // MARK: - Panel divider struct PanelDivider: View { let axis: Axis private let thickness: CGFloat = 1.5 var body: some View { Rectangle() .fill(Color.white.opacity(0.1)) .frame( width: axis == .vertical ? thickness : nil, height: axis == .horizontal ? thickness : nil ) } } // MARK: - Placeholders for panels not yet built struct RenderSettingsPlaceholder: View { var body: some View { VStack { Image(systemName: "sun.max.fill") .font(.title2) .foregroundStyle(.orange.opacity(0.5)) Text("Render Settings") .font(.caption) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(.ultraThinMaterial) } } struct TimelinePlaceholder: View { var body: some View { HStack { Image(systemName: "play.rectangle") .foregroundStyle(.secondary) Text("Timeline") .font(.caption) .foregroundStyle(.secondary) Spacer() } .padding(.horizontal, 16) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(.ultraThinMaterial) } }