Open task views in a sheet over the left pane
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
enum SheetView {
|
||||
case tab(TaskTab)
|
||||
case image(NSImage)
|
||||
case diagram(BoxLayout)
|
||||
}
|
||||
|
||||
struct Sheet {
|
||||
enum Owner: Equatable {
|
||||
case lead
|
||||
case task(String?)
|
||||
}
|
||||
|
||||
let owner: Owner
|
||||
let view: SheetView
|
||||
|
||||
var tab: TaskTab? {
|
||||
if case .tab(let t) = view {
|
||||
return t
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private let stripWidth: CGFloat = 56
|
||||
private let corner: CGFloat = 8
|
||||
|
||||
struct SheetHost: View {
|
||||
let sheet: Sheet
|
||||
@Environment(Store.self) private var store
|
||||
|
||||
var body: some View {
|
||||
let overLeft = sheet.owner != .lead
|
||||
HStack(spacing: 0) {
|
||||
if overLeft {
|
||||
strip
|
||||
}
|
||||
SheetPanel(sheet: sheet, overLeft: overLeft)
|
||||
.onGeometryChange(for: CGRect.self) { g in
|
||||
g.frame(in: .global)
|
||||
} action: { frame in
|
||||
store.paneFocus.setSheetFrame(frame, lead: !overLeft)
|
||||
}
|
||||
.onDisappear {
|
||||
store.paneFocus.setSheetFrame(.zero, lead: !overLeft)
|
||||
}
|
||||
if !overLeft {
|
||||
strip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var strip: some View {
|
||||
Palette.bg.opacity(0.55)
|
||||
.frame(width: stripWidth)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
store.sheet = nil
|
||||
}
|
||||
.help("Close")
|
||||
}
|
||||
}
|
||||
|
||||
private struct SheetPanel: View {
|
||||
let sheet: Sheet
|
||||
let overLeft: Bool
|
||||
@Environment(Store.self) private var store
|
||||
|
||||
var body: some View {
|
||||
let task = store.selectedTaskId.flatMap { store.findTask($0) }
|
||||
let accent = overLeft ? task.map { taskPalette[$0.color] } ?? Palette.accent : Palette.accent
|
||||
let clip = UnevenRoundedRectangle(topLeadingRadius: overLeft ? corner : 0, bottomLeadingRadius: overLeft ? corner : 0,
|
||||
bottomTrailingRadius: overLeft ? 0 : corner, topTrailingRadius: overLeft ? 0 : corner)
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 8) {
|
||||
Text(title(task))
|
||||
.font(Fonts.font(13, weight: 700))
|
||||
.foregroundStyle(accent)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
Spacer(minLength: 0)
|
||||
Button {
|
||||
store.sheet = nil
|
||||
} label: {
|
||||
Text("✕")
|
||||
.font(Fonts.font(13, weight: 700))
|
||||
.foregroundStyle(accent)
|
||||
.padding(.horizontal, 6)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help("Close (Esc)")
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 7)
|
||||
.background(accent.opacity(0.12))
|
||||
.overlay(alignment: .bottom) {
|
||||
Rectangle().fill(accent).frame(height: 1)
|
||||
}
|
||||
content
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
.background(Palette.bg)
|
||||
.clipShape(clip)
|
||||
.overlay(SheetEdge(openTrailing: overLeft).stroke(accent, lineWidth: 1.5))
|
||||
.shadow(color: Palette.overlayShadow, radius: 14, x: overLeft ? -6 : 6)
|
||||
.environment(\.paneAccent, accent)
|
||||
.environment(\.inTask, overLeft)
|
||||
}
|
||||
|
||||
private func title(_ task: ByreTask?) -> String {
|
||||
let label = switch sheet.view {
|
||||
case .tab(let t): tabLabels[t]!
|
||||
case .image: "Image"
|
||||
case .diagram: "Diagram"
|
||||
}
|
||||
if !overLeft {
|
||||
return "Lead · \(label)"
|
||||
}
|
||||
let project = store.focusedProject()?.name ?? ""
|
||||
guard let task else {
|
||||
return "\(project) · \(label)"
|
||||
}
|
||||
return "\(project) ▸ \(task.name) · \(label)"
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
switch sheet.view {
|
||||
case .tab(let t):
|
||||
TaskTabBody(tab: t)
|
||||
case .image(let image):
|
||||
ScrollView([.horizontal, .vertical]) {
|
||||
Image(nsImage: image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: image.size.width, maxHeight: image.size.height)
|
||||
.padding(16)
|
||||
}
|
||||
.defaultScrollAnchor(.center)
|
||||
case .diagram(let layout):
|
||||
DiagramViewer(layout: layout)
|
||||
.id(layout.id)
|
||||
.padding(12)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct SheetEdge: Shape {
|
||||
let openTrailing: Bool
|
||||
|
||||
func path(in r: CGRect) -> Path {
|
||||
let closed = openTrailing ? r.minX : r.maxX
|
||||
let open = openTrailing ? r.maxX : r.minX
|
||||
let inward: CGFloat = openTrailing ? corner : -corner
|
||||
var p = Path()
|
||||
p.move(to: CGPoint(x: open, y: r.minY))
|
||||
p.addLine(to: CGPoint(x: closed + inward, y: r.minY))
|
||||
p.addArc(tangent1End: CGPoint(x: closed, y: r.minY), tangent2End: CGPoint(x: closed, y: r.minY + corner), radius: corner)
|
||||
p.addLine(to: CGPoint(x: closed, y: r.maxY - corner))
|
||||
p.addArc(tangent1End: CGPoint(x: closed, y: r.maxY), tangent2End: CGPoint(x: closed + inward, y: r.maxY), radius: corner)
|
||||
p.addLine(to: CGPoint(x: open, y: r.maxY))
|
||||
return p
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user