Files
cmux/Sources/TmuxWorkspacePaneOverlayView.swift

174 lines
6.0 KiB
Swift

import AppKit
import CmuxFoundation
import SwiftUI
struct TmuxWorkspacePaneOverlayView: View {
let unreadRects: [CGRect]
let flashRect: CGRect?
let activePaneBorderRect: CGRect?
let activePaneBorderColorHex: String?
let flashStartedAt: Date?
let flashReason: WorkspaceAttentionFlashReason?
let workspaceAttentionColor: WorkspaceAttentionColor
@State private var completedFlashStartedAt: Date?
var body: some View {
let attentionColor = Color(nsColor: workspaceAttentionColor.nsColor)
overlayContent(attentionColor: attentionColor)
.allowsHitTesting(false)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
@ViewBuilder
private func overlayContent(attentionColor: Color) -> some View {
if shouldAnimateFlash, let flashStartedAt {
TimelineView(TmuxWorkspacePaneFlashTimelineSchedule(startDate: flashStartedAt)) { timeline in
overlayCanvas(timelineDate: timeline.date, attentionColor: attentionColor)
.onChange(of: timeline.date) { _, date in
if date.timeIntervalSince(flashStartedAt) >= FocusFlashPattern.duration {
completedFlashStartedAt = flashStartedAt
}
}
}
} else if !unreadRects.isEmpty || activePaneBorderRect != nil {
overlayCanvas(timelineDate: nil, attentionColor: attentionColor)
} else {
Color.clear
}
}
private var shouldAnimateFlash: Bool {
guard let flashRect,
let flashStartedAt else { return false }
guard completedFlashStartedAt != flashStartedAt,
ringPath(for: flashRect) != nil else { return false }
return Date() <= flashStartedAt.addingTimeInterval(FocusFlashPattern.duration)
}
private func overlayCanvas(timelineDate: Date?, attentionColor: Color) -> some View {
Canvas { context, _ in
if let activePaneBorderRect,
let activePaneBorderColorHex {
drawActivePaneBorder(
in: &context,
rect: activePaneBorderRect,
colorHex: activePaneBorderColorHex
)
}
for rect in unreadRects {
drawUnreadRing(in: &context, rect: rect, color: attentionColor)
}
guard let flashRect,
let flashStartedAt,
let timelineDate else { return }
let elapsed = timelineDate.timeIntervalSince(flashStartedAt)
let opacity = FocusFlashPattern.opacity(at: elapsed)
guard opacity > 0.001 else { return }
drawFlashRing(
in: &context,
rect: flashRect,
opacity: opacity,
reason: flashReason ?? .notificationArrival,
color: attentionColor
)
}
}
private func drawActivePaneBorder(
in context: inout GraphicsContext,
rect: CGRect,
colorHex: String
) {
guard let path = ringPath(for: rect),
let color = NSColor(hex: colorHex) else { return }
context.stroke(
path,
with: .color(Color(nsColor: color)),
style: StrokeStyle(
lineWidth: CGFloat(PaneChromeSettings.activeBorderLineWidth),
lineJoin: .round
)
)
}
private func drawUnreadRing(in context: inout GraphicsContext, rect: CGRect, color: Color) {
guard let path = ringPath(for: rect) else { return }
let presentation = WorkspaceAttentionCoordinator.notificationRingStyle
var glowContext = context
glowContext.addFilter(
.shadow(
color: color.opacity(presentation.glowOpacity),
radius: presentation.glowRadius
)
)
glowContext.stroke(
path,
with: .color(color),
style: StrokeStyle(lineWidth: PanelOverlayRingMetrics.lineWidth, lineJoin: .round)
)
}
private func drawFlashRing(
in context: inout GraphicsContext,
rect: CGRect,
opacity: Double,
reason: WorkspaceAttentionFlashReason,
color: Color
) {
guard let path = ringPath(for: rect) else { return }
let presentation = WorkspaceAttentionCoordinator.flashStyle(for: reason)
var glowContext = context
glowContext.addFilter(
.shadow(
color: color.opacity(opacity * presentation.glowOpacity),
radius: presentation.glowRadius
)
)
glowContext.stroke(
path,
with: .color(color.opacity(opacity)),
style: StrokeStyle(lineWidth: PanelOverlayRingMetrics.lineWidth, lineJoin: .round)
)
}
private func ringPath(for rect: CGRect) -> Path? {
guard rect.width > PanelOverlayRingMetrics.inset * 2,
rect.height > PanelOverlayRingMetrics.inset * 2 else { return nil }
return Path(
roundedRect: PanelOverlayRingMetrics.pathRect(in: rect),
cornerRadius: PanelOverlayRingMetrics.cornerRadius
)
}
}
struct TmuxWorkspacePaneFlashTimelineSchedule: TimelineSchedule {
let startDate: Date
func entries(from requestedStartDate: Date, mode: Mode) -> Entries {
let firstDate = requestedStartDate > startDate ? requestedStartDate : startDate
let interval = mode == .lowFrequency ? 1.0 / 10.0 : 1.0 / 60.0
return Entries(
nextDate: firstDate,
endDate: startDate.addingTimeInterval(FocusFlashPattern.duration),
interval: interval
)
}
struct Entries: Sequence, IteratorProtocol {
var nextDate: Date
let endDate: Date
let interval: TimeInterval
mutating func next() -> Date? {
guard nextDate <= endDate else { return nil }
let date = nextDate
nextDate = nextDate.addingTimeInterval(interval)
return date
}
}
}