| 1 | #if os(macOS) |
| 2 | import AppKit |
| 3 | import SwiftUI |
| 4 | |
| 5 | /// The mini window pressed as a person presses it, for CallsInterop's |
| 6 | /// --mini-clicks (2026-10-05, Paul: "Sometimes I can't click the mini |
| 7 | /// window"). A real panel, on screen, in an app that is not active (the |
| 8 | /// harness never activates), with a call behind it; presses made as |
| 9 | /// NSEvents and sent through the window, so AppKit's hit-testing, first |
| 10 | /// mouse, the surface's tracking loop and SwiftUI's buttons all take |
| 11 | /// part. What each press did is read from the actions it called and |
| 12 | /// from the window's frame. This Mac refuses screen capture and event |
| 13 | /// posting to this process, so these presses do not pass through the |
| 14 | /// window server; everything after it they do. |
| 15 | @_spi(Fixtures) @MainActor public enum MiniWindowProbe { |
| 16 | public struct Result: Sendable { |
| 17 | public let name: String |
| 18 | public let passed: Bool |
| 19 | public let detail: String |
| 20 | } |
| 21 | |
| 22 | public static func run(call: Call) async -> [Result] { |
| 23 | var results: [Result] = [] |
| 24 | for width in [MiniWindowRules.minimumWidth, MiniWindowRules.size.width, 480] { |
| 25 | results += await pressWindow(call: call, width: width) |
| 26 | } |
| 27 | results += await pressTab(call: call) |
| 28 | return results |
| 29 | } |
| 30 | |
| 31 | static func trace(_ line: String) { |
| 32 | FileHandle.standardError.write(Data(("mini-clicks: " + line + "\n").utf8)) |
| 33 | } |
| 34 | |
| 35 | private final class Recorder { |
| 36 | var calls: [String] = [] |
| 37 | } |
| 38 | |
| 39 | private static func panel(call: Call, recorder: Recorder) -> MiniWindowPanel { |
| 40 | MiniWindowPanel( |
| 41 | call: call, |
| 42 | actions: MiniWindowActions( |
| 43 | leave: { recorder.calls.append("leave") }, |
| 44 | open: { recorder.calls.append("open") }, |
| 45 | moved: { option in recorder.calls.append(option ? "moved-option" : "moved") }, |
| 46 | resized: { recorder.calls.append("resized") }, |
| 47 | collapse: { recorder.calls.append("collapse") }, |
| 48 | expand: { recorder.calls.append("expand") }, |
| 49 | tabReleased: { recorder.calls.append("tabReleased") })) |
| 50 | } |
| 51 | |
| 52 | private static func pressWindow(call: Call, width: CGFloat) async -> [Result] { |
| 53 | let recorder = Recorder() |
| 54 | let size = CGSize(width: width, height: width / MiniWindowRules.aspect) |
| 55 | let panel = panel(call: call, recorder: recorder) |
| 56 | let visible = NSScreen.main?.visibleFrame ?? CGRect(x: 0, y: 0, width: 1440, height: 900) |
| 57 | let home = CGRect(x: visible.midX - size.width / 2, y: visible.midY - size.height / 2, width: size.width, height: size.height) |
| 58 | panel.expand(to: home, animated: false) |
| 59 | panel.reassert(because: "probe") |
| 60 | try? await Task.sleep(for: .milliseconds(300)) |
| 61 | var results: [Result] = [] |
| 62 | let tag = "\(Int(width))" |
| 63 | trace("window \(tag) up at \(panel.frame)") |
| 64 | func check(_ name: String, _ expected: String, _ act: () async -> Void, frame: ((CGRect) -> Bool)? = nil) async { |
| 65 | panel.expand(to: home, animated: false) |
| 66 | recorder.calls = [] |
| 67 | trace("\(tag) \(name)") |
| 68 | await act() |
| 69 | try? await Task.sleep(for: .milliseconds(80)) |
| 70 | let got = recorder.calls.joined(separator: ",") |
| 71 | let frameOK = frame?(panel.frame) ?? true |
| 72 | results.append( |
| 73 | Result( |
| 74 | name: "\(tag) \(name)", passed: got == expected && frameOK, |
| 75 | detail: "expected \(expected), got [\(got)], frame \(panel.frame)")) |
| 76 | } |
| 77 | let w = size.width, h = size.height |
| 78 | // Hover off: the picture alone. |
| 79 | panel.content.hovering = false |
| 80 | try? await Task.sleep(for: .milliseconds(100)) |
| 81 | await check("click in the middle brings the call forward", "open") { await click(panel, at: CGPoint(x: w / 2, y: h / 2)) } |
| 82 | await check("click on the name brings the call forward", "open") { await click(panel, at: CGPoint(x: 24, y: 16)) } |
| 83 | await check("click near the top left brings the call forward", "open") { await click(panel, at: CGPoint(x: 20, y: h - 20)) } |
| 84 | await check("a shaky click is still a click", "open") { |
| 85 | await press(panel, from: CGPoint(x: w / 2, y: h / 2), by: CGSize(width: 3, height: -2)) |
| 86 | } |
| 87 | await check("a drag from the middle moves it", "moved", { |
| 88 | await press(panel, from: CGPoint(x: w / 2, y: h / 2), by: CGSize(width: 60, height: -40)) |
| 89 | }, frame: { abs($0.minX - (home.minX + 60)) < 1 && abs($0.minY - (home.minY - 40)) < 1 }) |
| 90 | await check("a drag from the right edge resizes it", "resized", { |
| 91 | await press(panel, from: CGPoint(x: w - 3, y: h / 2), by: CGSize(width: 40, height: 0)) |
| 92 | }, frame: { $0.width > w + 20 || w >= MiniWindowRules.size(width: 10_000, in: visible).width }) |
| 93 | await check("a drag from the bottom left corner resizes it", "resized", { |
| 94 | await press(panel, from: CGPoint(x: 3, y: 3), by: CGSize(width: -30, height: -20)) |
| 95 | }, frame: { $0.width > w + 10 || w >= MiniWindowRules.size(width: 10_000, in: visible).width }) |
| 96 | // Marks over the picture are not controls: muted, and someone |
| 97 | // still connecting (the ear), a press on them is the window's. |
| 98 | call.showFixtureSelf(muted: true, cameraOff: false, sharingScreen: false) |
| 99 | call.showFixtureConnecting(["li"]) |
| 100 | try? await Task.sleep(for: .milliseconds(200)) |
| 101 | await check("click on the Muted mark brings the call forward", "open") { await click(panel, at: CGPoint(x: 10 + 16, y: h - 10 - 16)) } |
| 102 | await check("click on the connecting ear brings the call forward", "open") { await click(panel, at: CGPoint(x: w - 10 - 16, y: h - 10 - 16)) } |
| 103 | await check("a drag from the Muted mark moves it", "moved") { |
| 104 | await press(panel, from: CGPoint(x: 10 + 16, y: h - 10 - 16), by: CGSize(width: -50, height: -30)) |
| 105 | } |
| 106 | call.showFixtureSelf(muted: false, cameraOff: false, sharingScreen: false) |
| 107 | call.showFixtureConnecting([]) |
| 108 | // Hover on: the controls (44 pt, 8 apart, 10 in from the top right: |
| 109 | // Collapse, Mute, Leave) answer the first click. |
| 110 | panel.content.hovering = true |
| 111 | try? await Task.sleep(for: .milliseconds(200)) |
| 112 | let row = h - 10 - 22 |
| 113 | await check("first click on Leave", "leave") { await click(panel, at: CGPoint(x: w - 10 - 22, y: row), control: true) } |
| 114 | await check("first click on Collapse", "collapse") { await click(panel, at: CGPoint(x: w - 10 - 44 - 8 - 44 - 8 - 22, y: row), control: true) } |
| 115 | let before = call.muted |
| 116 | await click(panel, at: CGPoint(x: w - 10 - 44 - 8 - 22, y: row), control: true) |
| 117 | try? await Task.sleep(for: .milliseconds(80)) |
| 118 | results.append(Result(name: "\(tag) first click on Mute", passed: call.muted != before, detail: "muted \(before) -> \(call.muted)")) |
| 119 | if call.muted != before { call.showFixtureSelf(muted: before, cameraOff: false, sharingScreen: false) } |
| 120 | await check("click in the middle with the controls up", "open") { await click(panel, at: CGPoint(x: w / 2, y: h / 2)) } |
| 121 | panel.dismiss() |
| 122 | return results |
| 123 | } |
| 124 | |
| 125 | private static func pressTab(call: Call) async -> [Result] { |
| 126 | let recorder = Recorder() |
| 127 | let panel = panel(call: call, recorder: recorder) |
| 128 | let visible = NSScreen.main?.visibleFrame ?? CGRect(x: 0, y: 0, width: 1440, height: 900) |
| 129 | let expanded = CGRect(origin: CGPoint(x: visible.maxX - 340, y: visible.minY + 20), size: MiniWindowRules.size) |
| 130 | let tab = MiniWindowTab.dock(expanded, in: visible).1 |
| 131 | var results: [Result] = [] |
| 132 | func reset() { |
| 133 | panel.collapse(to: tab, expanded: expanded) |
| 134 | recorder.calls = [] |
| 135 | } |
| 136 | reset() |
| 137 | panel.reassert(because: "probe") |
| 138 | try? await Task.sleep(for: .milliseconds(300)) |
| 139 | let middle = CGPoint(x: tab.width / 2, y: tab.height / 2) |
| 140 | await click(panel, at: middle) |
| 141 | try? await Task.sleep(for: .milliseconds(80)) |
| 142 | results.append(Result(name: "tab click opens the window", passed: recorder.calls == ["expand"], detail: "\(recorder.calls)")) |
| 143 | reset() |
| 144 | await click(panel, at: middle, count: 2) |
| 145 | try? await Task.sleep(for: .milliseconds(80)) |
| 146 | results.append(Result(name: "tab double click opens the call", passed: recorder.calls.last == "open", detail: "\(recorder.calls)")) |
| 147 | reset() |
| 148 | await press(panel, from: CGPoint(x: 3, y: middle.y), by: CGSize(width: -120, height: 30)) |
| 149 | try? await Task.sleep(for: .milliseconds(80)) |
| 150 | results.append(Result(name: "tab drag, even from its edge, is a drag", passed: recorder.calls == ["tabReleased"], detail: "\(recorder.calls)")) |
| 151 | panel.dismiss() |
| 152 | return results |
| 153 | } |
| 154 | |
| 155 | private static func click(_ panel: NSWindow, at point: CGPoint, count: Int = 1, control: Bool = false) async { |
| 156 | for n in 1...count { |
| 157 | await press(panel, from: point, by: .zero, clickCount: n, control: control) |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | /// A press at `point` in the window, moved by `by` in steps, released: |
| 162 | /// the down sent through the window as AppKit would, the drags and |
| 163 | /// the up queued for the surface's tracking loop, in screen points. |
| 164 | /// A control's up goes to the window, where SwiftUI's button reads it; |
| 165 | /// the surface's tracking loop reads screen points from window-less |
| 166 | /// events (SurfaceView.screenLocation). |
| 167 | private static func press( |
| 168 | _ panel: NSWindow, from point: CGPoint, by: CGSize, clickCount: Int = 1, control: Bool = false |
| 169 | ) async { |
| 170 | let screenStart = panel.convertPoint(toScreen: point) |
| 171 | let now = ProcessInfo.processInfo.systemUptime |
| 172 | func event(_ type: NSEvent.EventType, at location: CGPoint, window: Bool) -> NSEvent? { |
| 173 | NSEvent.mouseEvent( |
| 174 | with: type, location: location, modifierFlags: [], timestamp: now, |
| 175 | windowNumber: window ? panel.windowNumber : 0, context: nil, eventNumber: 0, |
| 176 | clickCount: clickCount, pressure: type == .leftMouseUp ? 0 : 1) |
| 177 | } |
| 178 | let steps = by == .zero ? 0 : 5 |
| 179 | for i in stride(from: 1, through: steps, by: 1) { |
| 180 | let f = CGFloat(i) / CGFloat(steps) |
| 181 | if let drag = event(.leftMouseDragged, at: CGPoint(x: screenStart.x + by.width * f, y: screenStart.y + by.height * f), window: false) { |
| 182 | NSApp.postEvent(drag, atStart: false) |
| 183 | } |
| 184 | } |
| 185 | if control, let up = event(.leftMouseUp, at: point, window: true) { |
| 186 | NSApp.postEvent(up, atStart: false) |
| 187 | } else if let up = event(.leftMouseUp, at: CGPoint(x: screenStart.x + by.width, y: screenStart.y + by.height), window: false) { |
| 188 | NSApp.postEvent(up, atStart: false) |
| 189 | } |
| 190 | if let down = event(.leftMouseDown, at: point, window: true) { panel.sendEvent(down) } |
| 191 | // A SwiftUI button acts on the up, which it reads from the queue |
| 192 | // itself; let the run loop turn. |
| 193 | try? await Task.sleep(for: .milliseconds(60)) |
| 194 | } |
| 195 | } |
| 196 | #endif |
| 197 | |