165 lines
5 KiB
Swift
165 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)
|
||
|
|
}
|
||
|
|
}
|