eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1import SwiftUI
2#if canImport(AppKit) && !targetEnvironment(macCatalyst)
3import AppKit
4#else
5import UIKit
6#endif
7
8// πŸ€– A view's live window frame, read on demand β€” the lightbox zooms out from
9// it (Mac & iOS), and the Mac's swipe / window-drag monitors test hit points
10// against it. Every consumer reads at tap/event time; NOTHING needs a
11// continuously-updated value.
12//
13// The frame must NOT be tracked with a GeometryReader + `onChange(of:)`: that's
14// a continuous observation of a value that changes on EVERY layout pass. A chat
15// open runs several passes per displayed frame (scroll-settle pin, avatar
16// spring, composer inset, keyboard) and β€” on macOS β€” a window maximise/restore
17// animates the window frame across many passes, so SwiftUI flags
18// "onChange(of: CGRect) action tried to update multiple times per frame": real
19// main-thread layout churn (the #580 stall on iOS; the #899 resize churn on
20// Mac). Moving the write off @State was NOT enough β€” the loop wasn't
21// self-invalidation, it's that we were observing a high-frequency value at all.
22//
23// So nothing observes anything on either platform: a passive backing view sits
24// in the tracked view's background and `rect` asks IT for its window frame only
25// at the moment it's needed. The holder stays a plain reference so no write
26// ever invalidates the view. Only ever touched on the main actor (SwiftUI view
27// code / AppKit event monitors), hence @MainActor.
28@MainActor
29public final class LiveFrame {
30 #if canImport(AppKit) && !targetEnvironment(macCatalyst)
31 weak var anchor: NSView?
32 /// The anchor's frame in SwiftUI `.global` space. AppKit window coords are
33 /// bottom-left; SwiftUI `.global` is top-left β€” flip against the content
34 /// view's height, exactly as the swipe / drag monitors flip their event
35 /// points (`content.bounds.height - locationInWindow.y`).
36 public var rect: CGRect {
37 guard let anchor, let window = anchor.window else { return .zero }
38 let w = anchor.convert(anchor.bounds, to: nil) // window base coords (bottom-left)
39 let h = window.contentView?.bounds.height ?? window.frame.height
40 return CGRect(x: w.minX, y: h - w.maxY, width: w.width, height: w.height)
41 }
42 #else
43 weak var anchor: UIView?
44 public var rect: CGRect {
45 guard let anchor, anchor.window != nil else { return .zero }
46 return anchor.convert(anchor.bounds, to: nil) // window coords == SwiftUI .global (top-left)
47 }
48 #endif
49
50 public init() {}
51}
52
53public extension View {
54 /// Keep `holder.rect` answerable with this view's window frame, computed
55 /// lazily at read time via a passive backing view β€” no `onChange(of: CGRect)`
56 /// layout churn (see LiveFrame).
57 func trackingFrame(into holder: LiveFrame) -> some View {
58 background(FrameAnchor(holder: holder))
59 }
60}
61
62// The passive anchor: fills the tracked view (as its background), never draws,
63// never participates in hit-testing β€” exists only so LiveFrame can convert its
64// bounds to window coordinates on demand.
65#if canImport(AppKit) && !targetEnvironment(macCatalyst)
66private struct FrameAnchor: NSViewRepresentable {
67 let holder: LiveFrame
68
69 func makeNSView(context: Context) -> NSView {
70 let v = PassiveView()
71 holder.anchor = v
72 return v
73 }
74
75 func updateNSView(_ v: NSView, context: Context) {
76 holder.anchor = v
77 }
78
79 // Transparent to clicks so the tracked content (and anything beneath it)
80 // keeps its taps β€” the same idiom as AnimatedGIFView's backing view.
81 private final class PassiveView: NSView {
82 override func hitTest(_ point: NSPoint) -> NSView? { nil }
83 }
84}
85#else
86private struct FrameAnchor: UIViewRepresentable {
87 let holder: LiveFrame
88
89 func makeUIView(context: Context) -> UIView {
90 let v = UIView()
91 v.isUserInteractionEnabled = false
92 v.backgroundColor = .clear
93 holder.anchor = v
94 return v
95 }
96
97 func updateUIView(_ v: UIView, context: Context) {
98 holder.anchor = v
99 }
100}
101#endif
102