macOS client panes and transcript
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
struct MenuEntry: Identifiable {
|
||||
let id: String
|
||||
let label: String
|
||||
let badge: TabBadge
|
||||
let action: () -> Void
|
||||
}
|
||||
|
||||
struct OverlayMenu {
|
||||
let anchor: CGRect
|
||||
let entries: [MenuEntry]
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class Overlays {
|
||||
var alerts: [String] = []
|
||||
var image: NSImage?
|
||||
var menu: OverlayMenu?
|
||||
@ObservationIgnored private var monitor: Any?
|
||||
|
||||
var isOpen: Bool {
|
||||
!alerts.isEmpty || image != nil || menu != nil
|
||||
}
|
||||
|
||||
func showAlert(_ text: String) {
|
||||
alerts.append(text)
|
||||
}
|
||||
|
||||
func closeTop() {
|
||||
if menu != nil {
|
||||
menu = nil
|
||||
} else if image != nil {
|
||||
image = nil
|
||||
} else if !alerts.isEmpty {
|
||||
alerts.removeFirst()
|
||||
}
|
||||
}
|
||||
|
||||
func install() {
|
||||
if monitor != nil {
|
||||
return
|
||||
}
|
||||
monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
|
||||
let code = event.keyCode
|
||||
let taken = MainActor.assumeIsolated {
|
||||
self?.key(code) ?? false
|
||||
}
|
||||
return taken ? nil : event
|
||||
}
|
||||
}
|
||||
|
||||
private func key(_ code: UInt16) -> Bool {
|
||||
if !isOpen {
|
||||
return false
|
||||
}
|
||||
if code == 53 {
|
||||
closeTop()
|
||||
return true
|
||||
}
|
||||
if menu == nil && image == nil && !alerts.isEmpty && (code == 36 || code == 76) {
|
||||
alerts.removeFirst()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func uninstall() {
|
||||
if let monitor {
|
||||
NSEvent.removeMonitor(monitor)
|
||||
}
|
||||
monitor = nil
|
||||
}
|
||||
}
|
||||
|
||||
struct OverlayHost: View {
|
||||
@Environment(Overlays.self) private var overlays
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
if let menu = overlays.menu {
|
||||
Color.clear
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
overlays.menu = nil
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(menu.entries) { entry in
|
||||
MenuRow(entry: entry) {
|
||||
overlays.menu = nil
|
||||
entry.action()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(4)
|
||||
.background(Palette.raised, in: RoundedRectangle(cornerRadius: 6))
|
||||
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Palette.border))
|
||||
.shadow(color: .black.opacity(0.18), radius: 14, y: 8)
|
||||
.fixedSize()
|
||||
.offset(x: menu.anchor.minX, y: menu.anchor.maxY + 4)
|
||||
}
|
||||
if let image = overlays.image {
|
||||
Color.black.opacity(0.6)
|
||||
.onTapGesture {
|
||||
overlays.image = nil
|
||||
}
|
||||
Image(nsImage: image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: image.size.width, maxHeight: image.size.height)
|
||||
.padding(24)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.onTapGesture {
|
||||
overlays.image = nil
|
||||
}
|
||||
}
|
||||
if let text = overlays.alerts.first {
|
||||
Color.black.opacity(0.35)
|
||||
.contentShape(Rectangle())
|
||||
AlertBox(text: text) {
|
||||
overlays.alerts.removeFirst()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
}
|
||||
}
|
||||
|
||||
private struct MenuRow: View {
|
||||
let entry: MenuEntry
|
||||
let action: () -> Void
|
||||
@ViewState private var hover = false
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 6) {
|
||||
Text(entry.label)
|
||||
TabBadgeView(badge: entry.badge)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 4)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(hover ? Palette.surface : .clear, in: RoundedRectangle(cornerRadius: 4))
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.onHover { hover = $0 }
|
||||
}
|
||||
}
|
||||
|
||||
private struct AlertBox: View {
|
||||
let text: String
|
||||
let ok: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text(text)
|
||||
.textSelection(.enabled)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("OK", action: ok)
|
||||
.buttonStyle(WebButtonStyle(primary: true))
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.frame(maxWidth: 520)
|
||||
.background(Palette.raised, in: RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Palette.border))
|
||||
.shadow(color: .black.opacity(0.18), radius: 14, y: 8)
|
||||
.padding(24)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user