oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1#if os(macOS)
2 import AppKit
3 import SwiftUI
4
5 /// Moving the full-window call by its picture, as FaceTime does. With no
6 /// title bar, the 32 pt strip across the top (`WindowDragStrip`) was the
7 /// only place a window could be picked up by; now a press anywhere on the
8 /// stage that is not something of its own — the bar, a panel, a notice,
9 /// the self view, which drags itself, a tile's ⓘ — moves the window once
10 /// the pointer has gone `threshold` points with the button down. Less
11 /// than that is a click, which shows the controls as any click does.
12 ///
13 /// Resizing needs nothing from here: AppKit keeps the few points inside
14 /// every edge (and more at the corners) for itself, above any content,
15 /// as long as the window is `.resizable` — which `CallWindowChrome`
16 /// leaves alone. `WindowMoveTests` holds both.
17 enum WindowMove {
18 /// How far the pointer goes with the button down before a press on
19 /// the picture is a drag rather than a click: a shaky click stays a
20 /// click. The mini window uses the same slack.
21 static let threshold: CGFloat = 4
22
23 /// Whether a press that went down at `start` and is now at `now` is a
24 /// drag.
25 static func isDrag(from start: CGPoint, to now: CGPoint) -> Bool {
26 hypot(now.x - start.x, now.y - start.y) >= threshold
27 }
28
29 /// Whether the window can be moved at all: not in full screen, where it
30 /// is the screen, and not when AppKit says it is not movable.
31 static func canMove(fullScreen: Bool, movable: Bool) -> Bool {
32 !fullScreen && movable
33 }
34
35 /// The window's frame origin for the pointer at `pointer`, when it was
36 /// at `origin` with the pointer at `grabbed` (all in screen points).
37 static func origin(from origin: CGPoint, grabbed: CGPoint, pointer: CGPoint) -> CGPoint {
38 CGPoint(x: origin.x + pointer.x - grabbed.x, y: origin.y + pointer.y - grabbed.y)
39 }
40
41 /// One press on the picture: a click until it passes the threshold,
42 /// then where the window should be for each position of the pointer.
43 struct Press: Equatable {
44 let origin: CGPoint
45 let grabbed: CGPoint
46 private(set) var moving = false
47
48 init(origin: CGPoint, grabbed: CGPoint) {
49 self.origin = origin
50 self.grabbed = grabbed
51 }
52
53 /// Nil while it is still a click.
54 mutating func moved(to pointer: CGPoint) -> CGPoint? {
55 if !moving, !WindowMove.isDrag(from: grabbed, to: pointer) { return nil }
56 moving = true
57 return WindowMove.origin(from: origin, grabbed: grabbed, pointer: pointer)
58 }
59 }
60 }
61
62 extension View {
63 /// A press and drag on this view moves its window (see `WindowMove`).
64 /// Views above it with gestures or controls of their own keep them.
65 func movesWindow() -> some View { modifier(WindowMoveModifier()) }
66
67 /// Lets the click that brings the window forward reach this view's
68 /// gestures, as it reaches a title bar (macOS 15 and later). Given to
69 /// the stage by `movesWindow`; a scroll view's content needs it again,
70 /// because it does not reach through one.
71 func takesActivatingClicks() -> some View { modifier(ActivatingClicks()) }
72 }
73
74 private struct WindowMoveModifier: ViewModifier {
75 @State private var mover = WindowMover()
76
77 func body(content: Content) -> some View {
78 content
79 // Every point of it, the gaps between tiles included, not only
80 // where something is drawn.
81 .contentShape(Rectangle())
82 .gesture(
83 // Only a drag: a click on the picture is left alone, and shows
84 // the controls as it always has (CallWindowActivity).
85 DragGesture(minimumDistance: WindowMove.threshold, coordinateSpace: .global)
86 .onChanged { _ in mover.begin() }
87 .onEnded { _ in mover.end() }
88 )
89 // What a double click on the title bar does, as on the strip that
90 // stands in for it: zoom, by default.
91 .onTapGesture(count: 2) { mover.doubleClicked() }
92 .takesActivatingClicks()
93 .background(WindowFinder(mover: mover))
94 }
95 }
96
97 private struct ActivatingClicks: ViewModifier {
98 func body(content: Content) -> some View {
99 if #available(macOS 15, *) {
100 content.allowsWindowActivationEvents(true)
101 } else {
102 content
103 }
104 }
105 }
106
107 /// Moves the window for a drag on the picture. The gesture is used only
108 /// to say that one has begun, on the picture and not on anything over
109 /// it: its values are in the view's coordinates, which shift as the
110 /// window does, and once the window followed the pointer SwiftUI stopped
111 /// reporting the drag after its first step. So the window's own mouse
112 /// events are watched instead, where the pointer is on screen, and
113 /// passed on untouched.
114 @MainActor final class WindowMover {
115 weak var window: NSWindow? {
116 didSet { if window !== oldValue { watch() } }
117 }
118 /// Where on screen the button last went down in the window.
119 private var down: CGPoint?
120 private var press: WindowMove.Press?
121 private var monitor: Any?
122
123 /// A drag on the picture has passed the threshold.
124 func begin() {
125 guard press == nil, let window, let down,
126 WindowMove.canMove(
127 fullScreen: window.styleMask.contains(.fullScreen), movable: window.isMovable)
128 else { return }
129 // The window has not moved since the button went down. It follows
130 // from the next drag event on.
131 press = WindowMove.Press(origin: window.frame.origin, grabbed: down)
132 }
133
134 func doubleClicked() {
135 guard let window, !window.styleMask.contains(.fullScreen) else { return }
136 TitleBarDoubleClick.current().perform(on: window)
137 }
138
139 func end() { press = nil }
140
141 func stop() {
142 if let monitor { NSEvent.removeMonitor(monitor) }
143 monitor = nil
144 press = nil
145 }
146
147 private func watch() {
148 stop()
149 guard window != nil else { return }
150 monitor = NSEvent.addLocalMonitorForEvents(
151 matching: [.leftMouseDown, .leftMouseDragged, .leftMouseUp]
152 ) { [weak self] event in
153 nonisolated(unsafe) let event = event
154 MainActor.assumeIsolated {
155 guard let self, event.window === self.window else { return }
156 switch event.type {
157 case .leftMouseDown: self.down = Self.screenLocation(of: event)
158 case .leftMouseDragged: self.move(to: Self.screenLocation(of: event))
159 default: self.end()
160 }
161 }
162 return event
163 }
164 }
165
166 private func move(to pointer: CGPoint) {
167 guard let window, let origin = press?.moved(to: pointer) else { return }
168 let screen = NSScreen.screens.first { $0.frame.contains(pointer) } ?? window.screen
169 let frame = window.constrainFrameRect(
170 NSRect(origin: origin, size: window.frame.size), to: screen)
171 window.setFrameOrigin(frame.origin)
172 }
173
174 /// Where an event happened on screen, in AppKit's screen points (y up
175 /// from the bottom of the main display). From the event itself, not
176 /// its place in the window: the window is moving under the pointer,
177 /// and an event queued before a move is placed against where the
178 /// window was.
179 private static func screenLocation(of event: NSEvent) -> CGPoint {
180 guard let global = event.cgEvent?.location, let main = NSScreen.screens.first else {
181 return NSEvent.mouseLocation
182 }
183 return CGPoint(x: global.x, y: main.frame.maxY - global.y)
184 }
185
186 deinit {
187 if let monitor { NSEvent.removeMonitor(monitor) }
188 }
189 }
190
191 extension TitleBarDoubleClick {
192 @MainActor func perform(on window: NSWindow) {
193 switch self {
194 case .zoom: window.zoom(nil)
195 case .minimize: window.miniaturize(nil)
196 case .none: break
197 }
198 }
199 }
200
201 /// Finds the window a SwiftUI view is in, for the mover. Takes no
202 /// clicks.
203 private struct WindowFinder: NSViewRepresentable {
204 let mover: WindowMover
205
206 func makeNSView(context: Context) -> FinderView {
207 let view = FinderView()
208 view.mover = mover
209 return view
210 }
211 func updateNSView(_ view: FinderView, context: Context) {
212 view.mover = mover
213 mover.window = view.window
214 }
215 static func dismantleNSView(_ view: FinderView, coordinator: ()) { view.mover?.window = nil }
216
217 final class FinderView: NSView {
218 var mover: WindowMover?
219 override func viewDidMoveToWindow() {
220 super.viewDidMoveToWindow()
221 mover?.window = window
222 }
223 override func hitTest(_ point: NSPoint) -> NSView? { nil }
224 }
225 }
226#endif
227