181 lines
5.9 KiB
Swift
181 lines
5.9 KiB
Swift
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)
|
|
if sheet.tab == .page, let page = store.selectedTaskId.flatMap({ store.shownPages[$0] }), let url = page.url(store) {
|
|
Button {
|
|
NSWorkspace.shared.open(url)
|
|
} label: {
|
|
Text("↗ Browser")
|
|
.font(Fonts.font(13, weight: 700))
|
|
.foregroundStyle(accent)
|
|
.padding(.horizontal, 6)
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
.help("Open the page in the default browser")
|
|
}
|
|
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
|
|
}
|
|
}
|