oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1import AppKit
2@_spi(Fixtures) import CallsKit
3import SwiftUI
4
5/// The full-window call's control bar at every window size (2026-10-03:
6/// Paul's 0.4.6 window showed it off the left edge, mute and camera cut
7/// off). A real, titled, resizable window as the app's — content to the top,
8/// the app's 760 × 540 minimum on its content — taken through a run of
9/// sizes by resizing the one window live, as a person dragging its edge
10/// does, and set below the minimum the way only the system can (tiling,
11/// a display change). At each size the bar must be whole inside the
12/// window, centred on it, and its buttons inside the bar. A snapshot of
13/// each goes to the folder.
14@MainActor enum BarLayoutHarness {
15 struct Step {
16 var name: String
17 var size: CGSize
18 /// Captured as soon as the resize has laid out, not after it settles.
19 var quick = false
20 /// Resized as the system's own animation does (zoom, tiling).
21 var animate = false
22 }
23
24 static let steps: [Step] = [
25 Step(name: "1000x760", size: CGSize(width: 1000, height: 760)),
26 // Paul's screenshot was 1718 px wide: 859 pt on a Retina display.
27 Step(name: "859x700", size: CGSize(width: 859, height: 700)),
28 Step(name: "760x540", size: CGSize(width: 760, height: 540)),
29 Step(name: "1718x1000", size: CGSize(width: 1718, height: 1000)),
30 Step(name: "1718x1000-to-800-quick", size: CGSize(width: 800, height: 600), quick: true),
31 Step(name: "800x600", size: CGSize(width: 800, height: 600)),
32 // Below the minimum: a window the system made narrower than the app
33 // asked for.
34 Step(name: "640x480-forced", size: CGSize(width: 640, height: 480)),
35 Step(name: "480x400-forced", size: CGSize(width: 480, height: 400)),
36 Step(name: "360x400-forced", size: CGSize(width: 360, height: 400)),
37 Step(name: "1200x800-back", size: CGSize(width: 1200, height: 800)),
38 // Half of a 1728-pt display, as window tiling makes it, animated.
39 Step(name: "859x1080-tiled", size: CGSize(width: 859, height: 1080), animate: true),
40 Step(name: "1718x1080-zoomed", size: CGSize(width: 1718, height: 1080), animate: true),
41 Step(name: "859x1080-tiled-quick", size: CGSize(width: 859, height: 1080), quick: true, animate: true),
42 ]
43
44 static func run(to folder: URL) async -> Bool {
45 NSApplication.shared.setActivationPolicy(.prohibited)
46 try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
47 var failures: [String] = []
48 // The last two: no 760 × 540 minimum, as a window the system has made
49 // narrower than the app asked for — the widths only ⋯ can serve.
50 let variants: [(CallFixture, Bool, Bool, Bool)] = [
51 (.alone, false, false, true), (.alone, true, false, true), (.alone, false, true, true),
52 (.grid, false, false, true), (.grid, true, false, true), (.grid, false, true, true),
53 (.alone, false, false, false), (.grid, false, true, false),
54 ]
55 for (fixture, chat, devices, minimum) in variants {
56 do {
57 let call = Call(playAudio: false)
58 fixture.apply(to: call)
59 let content = MacCallView(
60 call: call, grid: false, showingChat: chat, showingDevices: devices, style: .fullWindow,
61 controlsShown: true, leave: {}, rejoin: {}
62 )
63 .tint(CallsAppearance.accent)
64 // As the app's window has it (CallsMac.swift).
65 .frame(minWidth: minimum ? 760 : 0, minHeight: minimum ? 540 : 0)
66 let view = NSHostingView(rootView: content.environment(\.colorScheme, .dark))
67 let window = NSWindow(
68 contentRect: NSRect(origin: CGPoint(x: 40, y: 40), size: steps[0].size),
69 styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
70 backing: .buffered, defer: false)
71 window.isReleasedWhenClosed = false
72 window.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.desktopWindow)) - 1)
73 window.appearance = NSAppearance(named: .darkAqua)
74 window.contentView = view
75 if !minimum { window.contentMinSize = CGSize(width: 200, height: 200) }
76 window.orderBack(nil)
77 try? await Task.sleep(for: .milliseconds(700))
78 let variant =
79 "\(fixture.rawValue)\(chat ? "-chat" : "")\(devices ? "-devices" : "")\(minimum ? "" : "-nominimum")"
80 for step in steps {
81 window.setFrame(
82 NSRect(origin: window.frame.origin, size: step.size), display: true, animate: step.animate)
83 view.layoutSubtreeIfNeeded()
84 try? await Task.sleep(for: .milliseconds(step.quick ? 30 : 500))
85 let width = view.bounds.width
86 let pane = CallBarProbe.pane
87 let row = CallBarProbe.row
88 let name = "\(variant)-\(step.name)"
89 var problems: [String] = []
90 if pane.minX < -0.5 || pane.maxX > width + 0.5 {
91 problems.append("bar \(fmt(pane)) leaves the window (width \(fmt(width)))")
92 }
93 if row.minX < pane.minX - 0.5 || row.maxX > pane.maxX + 0.5 {
94 problems.append("buttons \(fmt(row)) spill out of the bar \(fmt(pane))")
95 }
96 let off = abs(pane.midX - width / 2)
97 if off > 1 { problems.append("bar off centre by \(fmt(off)) (bar \(fmt(pane)), window \(fmt(width)))") }
98 if pane.width < 1 { problems.append("bar not laid out") }
99 let line = "\(name): window \(fmt(width)) bar \(fmt(pane)) buttons \(fmt(row))"
100 print(problems.isEmpty ? "ok \(line)" : "FAIL \(line): \(problems.joined(separator: "; "))")
101 failures += problems.map { "\(name): \($0)" }
102 let file = folder.appendingPathComponent("bar-\(name).png")
103 capture(window, to: file)
104 // What was drawn, not only what was laid out: the red of Leave
105 // in the picture must be where the layout put Leave, and whole.
106 if let red = leaveRed(in: file, windowWidth: window.frame.width) {
107 let expected = CallBarProbe.last
108 if abs(red.midX - expected.midX) > 3 || red.width < expected.width - 4 {
109 let problem =
110 "Leave drawn at \(fmt(red)), laid out at \(fmt(expected))"
111 print("FAIL \(name): \(problem)")
112 failures.append("\(name): \(problem)")
113 }
114 } else {
115 print("FAIL \(name): no Leave in the picture")
116 failures.append("\(name): no Leave in the picture")
117 }
118 }
119 window.close()
120 }
121 }
122 print(failures.isEmpty ? "bar layout: all sizes ok" : "bar layout: \(failures.count) failures")
123 return failures.isEmpty
124 }
125
126 private static func fmt(_ value: CGFloat) -> String { String(format: "%.1f", value) }
127 private static func fmt(_ rect: CGRect) -> String {
128 "\(fmt(rect.minX))…\(fmt(rect.maxX)) (\(fmt(rect.width)))"
129 }
130
131 /// The horizontal extent, in points, of Leave's red in a capture of a
132 /// window `windowWidth` points wide.
133 private static func leaveRed(in file: URL, windowWidth: CGFloat) -> CGRect? {
134 guard let image = NSImage(contentsOf: file),
135 let cg = image.cgImage(forProposedRect: nil, context: nil, hints: nil),
136 let data = cg.dataProvider?.data, let bytes = CFDataGetBytePtr(data)
137 else { return nil }
138 let width = cg.width
139 let height = cg.height
140 let row = cg.bytesPerRow
141 let step = cg.bitsPerPixel / 8
142 let scale = CGFloat(width) / windowWidth
143 let bgr = cg.bitmapInfo.contains(.byteOrder32Little)
144 var minX = Int.max
145 var maxX = Int.min
146 // The bottom 140 points: the bar and its margin.
147 let top = max(0, height - Int(140 * scale))
148 for y in top..<height {
149 for x in 0..<width {
150 let p = bytes + y * row + x * step
151 let (r, g, b) = bgr ? (p[2], p[1], p[0]) : (p[0], p[1], p[2])
152 // leaveFill is (0.84, 0.16, 0.14): about (214, 41, 36).
153 if r > 170, g < 90, b < 90 {
154 minX = min(minX, x)
155 maxX = max(maxX, x)
156 }
157 }
158 }
159 guard minX <= maxX else { return nil }
160 return CGRect(x: CGFloat(minX) / scale, y: 0, width: CGFloat(maxX - minX + 1) / scale, height: 1)
161 }
162
163 private static func capture(_ window: NSWindow, to file: URL) {
164 let capture = Process()
165 capture.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture")
166 capture.arguments = ["-x", "-o", "-l", String(window.windowNumber), file.path]
167 try? capture.run()
168 capture.waitUntilExit()
169 }
170}
171