Add an iPhone client sharing the macOS app's SwiftUI code
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user