macOS client parity review

This commit is contained in:
Ian Gulliver
2026-10-17 10:54:06 -07:00
parent b85de9a2d0
commit 31037e1c9f
24 changed files with 651 additions and 225 deletions
+56 -34
View File
@@ -10,6 +10,7 @@ struct MenuEntry: Identifiable {
struct OverlayMenu {
let anchor: CGRect
let accent: Color
let entries: [MenuEntry]
}
@@ -44,29 +45,42 @@ enum LeftTab {
final class Overlays {
var alerts: [OverlayAlert] = []
var image: NSImage?
var menu: OverlayMenu?
var menu: OverlayMenu? {
didSet {
menuHover = false
}
}
var modal: OverlayModal?
var switcher: SwitcherModel?
var leftTab = LeftTab.lead
var leadFocus = 0
@ObservationIgnored var pickerFrame: CGRect = .zero
@ObservationIgnored var menuHover = false
var paneFrame: CGRect = .zero
@ObservationIgnored private var monitor: Any?
@ObservationIgnored private var mouseMonitor: Any?
var isOpen: Bool {
!alerts.isEmpty || image != nil || menu != nil || modal != nil || switcher != nil
}
var dialogOpen: Bool {
!alerts.isEmpty || modal != nil
}
func showAlert(_ text: String) {
menu = nil
alerts.append(OverlayAlert(content: AnyView(ModalText(text: text))))
}
func showAlert(_ content: some View) {
menu = nil
alerts.append(OverlayAlert(content: AnyView(content)))
}
func showModal(_ content: some View, _ buttons: [ModalButton], anchor: CGRect? = nil, inPane: Bool = false, canSubmit: @escaping @MainActor () -> Bool = { true }) async -> String {
modal?.done("cancel")
menu = nil
return await withCheckedContinuation { cont in
modal = OverlayModal(content: AnyView(content), buttons: buttons, anchor: anchor, inPane: inPane, canSubmit: canSubmit) { value in
cont.resume(returning: value)
@@ -103,7 +117,7 @@ final class Overlays {
func closeTop() {
if !alerts.isEmpty {
alerts.removeFirst()
alerts.removeLast()
} else if modal != nil {
answerModal("cancel")
} else if image != nil {
@@ -128,13 +142,21 @@ final class Overlays {
}
return taken ? nil : event
}
mouseMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown, .otherMouseDown]) { [weak self] event in
MainActor.assumeIsolated {
if let self, self.menu != nil, !self.menuHover {
self.menu = nil
}
}
return event
}
}
private func key(_ code: UInt16, _ flags: NSEvent.ModifierFlags, _ chars: String, _ ctrl: @MainActor (String) -> Bool) -> Bool {
if !dialogOpen && flags.contains(.control) && !flags.contains(.command) && goKeys.contains(chars) {
return ctrl(chars)
}
if !isOpen {
if flags.contains(.control) && !flags.contains(.command) && goKeys.contains(chars) {
return ctrl(chars)
}
return false
}
if code == 53 {
@@ -145,7 +167,7 @@ final class Overlays {
let marked = (NSApp.keyWindow?.firstResponder as? NSTextView)?.hasMarkedText() ?? false
if !alerts.isEmpty {
if enter {
alerts.removeFirst()
alerts.removeLast()
return true
}
return false
@@ -180,7 +202,11 @@ final class Overlays {
if let monitor {
NSEvent.removeMonitor(monitor)
}
if let mouseMonitor {
NSEvent.removeMonitor(mouseMonitor)
}
monitor = nil
mouseMonitor = nil
}
}
@@ -208,27 +234,6 @@ struct OverlayHost: View {
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 switcher = overlays.switcher {
Color.black.opacity(0.35)
.contentShape(Rectangle())
@@ -252,22 +257,39 @@ struct OverlayHost: View {
overlays.image = nil
}
}
if let menu = overlays.menu {
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: overlayShadow, radius: 14, y: 8)
.environment(\.paneAccent, menu.accent)
.onHover { inside in
overlays.menuHover = inside
}
.fixedSize()
.offset(x: menu.anchor.minX, y: menu.anchor.maxY + 4)
}
if let m = overlays.modal {
Color.black.opacity(0.35)
.contentShape(Rectangle())
.onTapGesture {
overlays.answerModal("cancel")
}
ModalPlacement(modal: m, pane: overlays.paneFrame) { value in
overlays.answerModal(value)
}
}
if let alert = overlays.alerts.first {
if let alert = overlays.alerts.last {
Color.black.opacity(0.35)
.contentShape(Rectangle())
GeometryReader { g in
AlertBox(alert: alert, maxText: max(60, g.size.height - 48 - 90)) {
overlays.alerts.removeFirst()
overlays.alerts.removeLast()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
@@ -399,7 +421,7 @@ private struct ModalBox: View {
.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)
.shadow(color: overlayShadow, radius: 14, y: 8)
}
}
@@ -427,7 +449,7 @@ private struct AlertBox: View {
.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)
.shadow(color: overlayShadow, radius: 14, y: 8)
.padding(24)
}
}