Add an iPhone client sharing the macOS app's SwiftUI code

This commit is contained in:
Ian Gulliver
2026-10-21 22:00:54 -07:00
parent 57d660c57f
commit 9e81e9107d
46 changed files with 1625 additions and 287 deletions
+48 -15
View File
@@ -1,4 +1,3 @@
import AppKit
import SwiftUI
struct MenuEntry: Identifiable {
@@ -55,6 +54,7 @@ final class Overlays {
@ObservationIgnored var pickerFrame: CGRect = .zero
@ObservationIgnored var menuHover = false
var paneFrame: CGRect = .zero
#if os(macOS)
@ObservationIgnored weak var window: NSWindow?
@ObservationIgnored private var monitor: Any?
@ObservationIgnored private var mouseMonitor: Any?
@@ -62,6 +62,7 @@ final class Overlays {
@ObservationIgnored private var holders = 0
@ObservationIgnored private var ctrl: @MainActor (String) -> Bool = { _ in false }
@ObservationIgnored private var escape: @MainActor () -> Bool = { false }
#endif
var isOpen: Bool {
!alerts.isEmpty || menu != nil || modal != nil || switcher != nil
@@ -84,11 +85,13 @@ final class Overlays {
func showModal(_ content: some View, _ buttons: @autoclosure @escaping @MainActor () -> [ModalButton], anchor: CGRect? = nil, inPane: Bool = false, canSubmit: @escaping @MainActor () -> Bool = { true }) async -> String {
modal?.done("cancel")
menu = nil
#if os(macOS)
if modal == nil {
let responder = window?.firstResponder
behindModal = (responder as? NSTextView).flatMap { $0.isFieldEditor ? $0.delegate as? NSResponder : nil } ?? responder
window?.makeFirstResponder(nil)
}
#endif
return await withCheckedContinuation { cont in
modal = OverlayModal(content: AnyView(content), buttons: buttons, anchor: anchor, inPane: inPane, canSubmit: canSubmit) { value in
cont.resume(returning: value)
@@ -108,10 +111,12 @@ final class Overlays {
func answerModal(_ value: String) {
let m = modal
modal = nil
#if os(macOS)
if let behindModal, window?.firstResponder !== behindModal {
window?.makeFirstResponder(behindModal)
}
behindModal = nil
#endif
m?.done(value)
}
@@ -119,7 +124,9 @@ final class Overlays {
if switcher != nil {
return
}
#if os(macOS)
logFocus("open switcher")
#endif
switcher = SwitcherModel(store: store, overlays: self, anchor: pickerFrame)
}
@@ -135,6 +142,7 @@ final class Overlays {
}
}
#if os(macOS)
func install(_ ctrl: @escaping @MainActor (String) -> Bool, escape: @escaping @MainActor () -> Bool) {
holders += 1
self.ctrl = ctrl
@@ -241,8 +249,10 @@ final class Overlays {
monitor = nil
mouseMonitor = nil
}
#endif
}
#if os(macOS)
@MainActor
func logFocus(_ what: String) {
let key = NSApp.keyWindow
@@ -250,6 +260,7 @@ func logFocus(_ what: String) {
let responder = key?.firstResponder.map { String(describing: type(of: $0)) } ?? "nil"
log.notice("focus debug: \(what, privacy: .public) active=\(NSApp.isActive) key=\(key?.title ?? "nil", privacy: .public) main=\(main?.title ?? "nil", privacy: .public) mainIsKey=\(main?.isKeyWindow ?? false) responder=\(responder, privacy: .public)")
}
#endif
struct ModalText: View {
let text: String
@@ -276,24 +287,32 @@ struct OverlayHost: View {
SwitcherPlacement(model: switcher)
}
if let menu = overlays.menu {
VStack(alignment: .leading, spacing: 0) {
ForEach(menu.entries) { entry in
MenuRow(entry: entry) {
if touchOnly {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
overlays.menu = nil
entry.action()
}
}
MenuPlacement(anchor: menu.anchor) {
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: Palette.overlayShadow, radius: 14, y: 8)
.environment(\.paneAccent, menu.accent)
.onHover { inside in
overlays.menuHover = inside
}
.fixedSize()
}
.padding(4)
.background(Palette.raised, in: RoundedRectangle(cornerRadius: 6))
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Palette.border))
.shadow(color: Palette.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 {
Palette.overlayBackdrop
@@ -338,6 +357,20 @@ private struct MenuRow: View {
let overlayMargin: CGFloat = 8
private struct MenuPlacement: Layout {
let anchor: CGRect
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let size = subviews[0].sizeThatFits(.unspecified)
let x = max(overlayMargin, min(anchor.minX, bounds.width - overlayMargin - size.width))
subviews[0].place(at: CGPoint(x: bounds.minX + x, y: bounds.minY + anchor.maxY + 4), proposal: ProposedViewSize(size))
}
}
private struct AnchoredBox: Layout {
let anchor: CGRect