Blender4iOS/Sources/Workspace/WorkspaceLayoutView.swift

164 lines
5 KiB
Swift

// 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)
TimelinePlaceholder()
.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)
}
}