Files
byre/mac/Sources/Byre/Sheet.swift
T

185 lines
6.0 KiB
Swift

import AppKit
import SwiftUI
enum SheetView {
case tab(TaskTab)
case image(NSImage)
case diagram(BoxLayout)
case page(Page)
}
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 case .page(let page) = sheet.view, 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"
case .page(let page): page.title(store)
}
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)
case .page(let page):
PageView(page: page)
}
}
}
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
}
}