@hearthlog/native 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ #if STOR_DEV
2
+ import AppKit
3
+ import WebKit
4
+
5
+ /// Debugging aids for builds made with `--dev` (never in a release build). Set in the environment:
6
+ /// STOR_ORIGIN open this origin instead of the site's, for example http://127.0.0.1:5180
7
+ /// STOR_DEV_DELAY seconds to wait for the page before the steps below (default 6)
8
+ /// STOR_DEV_EVAL body of an async JavaScript function; its returned string is printed as `EVAL <value>`
9
+ /// STOR_DEV_EVAL_FILE the same, read from a file
10
+ /// STOR_DEV_NAVIGATE a route to open through the shell, as the Go menu does
11
+ /// STOR_ACCESS_HOST treat this host as the Cloudflare Access login
12
+ /// STOR_DEV_CLICK "x,y;x,y" window points (from the top left) to send a mouse-down to, after the eval
13
+ /// STOR_DEV_LEGACY_PAGE=1 do not define window.storShell, as a page that predates the shell sees it
14
+ /// STOR_DEV_DOWNLOAD_DIR save downloads here without a panel
15
+ /// STOR_DEV_ALLOW_POPUPS=1 let scripts call window.open without a user gesture
16
+ /// STOR_DEV_NO_NOTIFICATIONS=1 do not schedule reminders (that asks for permission); log them instead
17
+ /// STOR_DEV_SETTLE seconds to wait after the eval before the screenshot (default 0)
18
+ /// STOR_DEV_SCREENSHOT write a PNG of the window, with its window controls and glass
19
+ /// STOR_DEV_EXIT=1 quit afterwards
20
+ @MainActor
21
+ enum DevTools {
22
+ static let environment = ProcessInfo.processInfo.environment
23
+
24
+ static func log(_ message: String) {
25
+ print("LOG", message)
26
+ }
27
+
28
+ static func schedule(for controller: MainWindowController) {
29
+ let environment = Self.environment
30
+ guard environment["STOR_DEV_EVAL"] != nil || environment["STOR_DEV_EVAL_FILE"] != nil || environment["STOR_DEV_SCREENSHOT"] != nil || environment["STOR_DEV_NAVIGATE"] != nil else { return }
31
+ let delay = Double(environment["STOR_DEV_DELAY"] ?? "") ?? 6
32
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
33
+ Task { @MainActor in
34
+ if let path = environment["STOR_DEV_NAVIGATE"] {
35
+ controller.host.navigate(to: path)
36
+ try? await Task.sleep(for: .seconds(1))
37
+ }
38
+ let script = environment["STOR_DEV_EVAL"] ?? environment["STOR_DEV_EVAL_FILE"].flatMap { try? String(contentsOfFile: $0, encoding: .utf8) }
39
+ if let body = script {
40
+ do {
41
+ let value = try await controller.host.webView.callAsyncJavaScript(body, contentWorld: .page)
42
+ print("EVAL", value ?? "nil")
43
+ } catch {
44
+ print("EVAL error:", error.localizedDescription)
45
+ }
46
+ }
47
+ if let clicks = environment["STOR_DEV_CLICK"], let window = controller.window {
48
+ for (index, spec) in clicks.split(separator: ";").enumerated() {
49
+ let parts = spec.split(separator: ",").compactMap { Double($0) }
50
+ guard parts.count == 2,
51
+ let event = NSEvent.mouseEvent(with: .leftMouseDown, location: NSPoint(x: parts[0], y: window.frame.height - parts[1]), modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber, context: nil, eventNumber: index + 1, clickCount: 1, pressure: 1)
52
+ else { continue }
53
+ log("click \(spec)")
54
+ window.sendEvent(event)
55
+ }
56
+ }
57
+ try? await Task.sleep(for: .seconds(Double(environment["STOR_DEV_SETTLE"] ?? "") ?? 0))
58
+ if let window = controller.window, let close = window.standardWindowButton(.closeButton) {
59
+ var chain: [String] = []
60
+ var view: NSView? = close
61
+ while let current = view { chain.append("\(type(of: current)) \(current.frame)"); view = current.superview }
62
+ print("TITLEBAR", chain.joined(separator: " < "))
63
+ }
64
+ if let window = controller.window {
65
+ let buttons = [NSWindow.ButtonType.closeButton, .miniaturizeButton, .zoomButton].compactMap { window.standardWindowButton($0)?.frame }
66
+ print("STATE badge=\(NSApp.dockTile.badgeLabel ?? "none") menus=\((NSApp.delegate as? AppDelegate)?.menuSummary ?? "")")
67
+ print("WINDOW key=\(window.isKeyWindow) visible=\(window.isVisible) occluded=\(!window.occlusionState.contains(.visible)) appActive=\(NSApp.isActive) frame=\(window.frame) buttons=\(buttons.map { "\($0.origin.x),\($0.origin.y)" })")
68
+ }
69
+ if let path = environment["STOR_DEV_SCREENSHOT"], let window = controller.window {
70
+ print("SCREENSHOT", capture(window, to: path) ? path : "failed")
71
+ }
72
+ if environment["STOR_DEV_EXIT"] == "1" { exit(0) }
73
+ }
74
+ }
75
+ }
76
+
77
+ private static func capture(_ window: NSWindow, to path: String) -> Bool {
78
+ let options: CGWindowImageOption = [.boundsIgnoreFraming, .bestResolution]
79
+ guard let image = CGWindowListCreateImage(.null, .optionIncludingWindow, CGWindowID(window.windowNumber), options),
80
+ let png = NSBitmapImageRep(cgImage: image).representation(using: .png, properties: [:])
81
+ else { return false }
82
+ return (try? png.write(to: URL(fileURLWithPath: path))) != nil
83
+ }
84
+ }
85
+ #endif
@@ -0,0 +1,206 @@
1
+ import AppKit
2
+ import WebKit
3
+
4
+ /// One window onto the site. The title bar is hidden and the content runs under it: the page draws
5
+ /// the sidebar, the window's glass shows through it, and the standard window buttons sit on the
6
+ /// sidebar's brand row.
7
+ @MainActor
8
+ final class MainWindowController: NSWindowController, NSWindowDelegate, StorWebHostDelegate {
9
+ let host: StorWebHost
10
+ private(set) var pendingUploads = 0
11
+
12
+ private unowned let app: AppDelegate
13
+ private let lights: TrafficLights
14
+ private var signIn: SignInWindowController?
15
+ private var titleObservation: NSKeyValueObservation?
16
+
17
+ /// Smaller than this the page would switch to its phone layout, which a Mac window never shows.
18
+ static let minimumSize = NSSize(width: 860, height: 560)
19
+
20
+ init(site: SiteConfig, app: AppDelegate, inspectable: Bool, restoresFrame: Bool) {
21
+ self.app = app
22
+ let window = StorWindow(
23
+ contentRect: NSRect(x: 0, y: 0, width: 1280, height: 820),
24
+ styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
25
+ backing: .buffered,
26
+ defer: false
27
+ )
28
+ window.title = site.title
29
+ window.titleVisibility = .hidden
30
+ window.titlebarAppearsTransparent = true
31
+ window.tabbingMode = .disallowed
32
+ window.isReleasedWhenClosed = false
33
+ window.contentMinSize = Self.minimumSize
34
+
35
+ host = StorWebHost(site: site, kind: .mac, inspectable: inspectable, lightsWidth: TrafficLights.extent(of: window))
36
+ lights = TrafficLights(window: window)
37
+
38
+ // The glass behind the page, with one canvas tint across the window and a translucent sidebar.
39
+ let glass = NSVisualEffectView()
40
+ glass.material = .sidebar
41
+ glass.blendingMode = .behindWindow
42
+ glass.state = .followsWindowActiveState
43
+ host.webView.frame = glass.bounds
44
+ host.webView.autoresizingMask = [.width, .height]
45
+ glass.addSubview(host.webView)
46
+ window.contentView = glass
47
+ window.pageView = host.webView
48
+
49
+ window.center()
50
+ if restoresFrame { window.setFrameAutosaveName("StorWindow.\(site.id)") }
51
+
52
+ super.init(window: window)
53
+ window.delegate = self
54
+ host.delegate = self
55
+ titleObservation = host.webView.observe(\.title, options: [.new]) { [weak window] webView, _ in
56
+ if let title = webView.title, !title.isEmpty { window?.title = title }
57
+ }
58
+ }
59
+
60
+ required init?(coder: NSCoder) { fatalError("init(coder:) is not used") }
61
+
62
+ override func showWindow(_ sender: Any?) {
63
+ super.showWindow(sender)
64
+ lights.apply()
65
+ }
66
+
67
+ // MARK: Window
68
+
69
+ func windowDidBecomeKey(_ notification: Notification) { host.setAttribute("data-stor-inactive", false) }
70
+ func windowDidResignKey(_ notification: Notification) { host.setAttribute("data-stor-inactive", true) }
71
+ func windowWillEnterFullScreen(_ notification: Notification) { host.setAttribute("data-stor-fullscreen", true) }
72
+ func windowDidExitFullScreen(_ notification: Notification) {
73
+ host.setAttribute("data-stor-fullscreen", false)
74
+ lights.apply()
75
+ }
76
+ func windowWillClose(_ notification: Notification) { app.windowClosed(self) }
77
+
78
+ // MARK: StorWebHostDelegate
79
+
80
+ func host(_ host: StorWebHost, received message: ShellMessage) {
81
+ switch message {
82
+ case .lights(let x, let y): lights.move(to: CGPoint(x: x, y: y))
83
+ case .backgroundHover(let on): (window as? StorWindow)?.backgroundHover = on
84
+ case .outbox(let pending): pendingUploads = pending
85
+ case .haptic, .drag: break
86
+ case .appearance, .badge, .reminders, .menu: app.handle(message)
87
+ }
88
+ }
89
+
90
+ func host(_ host: StorWebHost, openWindowFor configuration: WKWebViewConfiguration, url: URL) -> WKWebView? {
91
+ app.openPopup(configuration: configuration, host: host)
92
+ }
93
+
94
+ func host(_ host: StorWebHost, showSignIn url: URL) {
95
+ #if STOR_DEV
96
+ DevTools.log("sign-in window for \(url)")
97
+ #endif
98
+ if let signIn {
99
+ signIn.showWindow(nil)
100
+ return
101
+ }
102
+ let controller = SignInWindowController(site: host.site, policy: host.policy, dataStore: host.dataStore, url: url) { [weak self] success in
103
+ guard let self else { return }
104
+ self.signIn = nil
105
+ #if STOR_DEV
106
+ DevTools.log("sign-in finished success=\(success)")
107
+ #endif
108
+ if success { self.host.reloadAfterSignIn() }
109
+ }
110
+ signIn = controller
111
+ controller.showWindow(nil)
112
+ }
113
+
114
+ func host(_ host: StorWebHost, openExternally url: URL) {
115
+ NSWorkspace.shared.open(url)
116
+ }
117
+
118
+ func host(_ host: StorWebHost, chooseDestinationFor suggestedFilename: String) async -> URL? {
119
+ #if STOR_DEV
120
+ // No panel for automated runs.
121
+ if let directory = DevTools.environment["STOR_DEV_DOWNLOAD_DIR"] {
122
+ DevTools.log("download \(suggestedFilename)")
123
+ return URL(fileURLWithPath: directory).appendingPathComponent(suggestedFilename)
124
+ }
125
+ #endif
126
+ let panel = NSSavePanel()
127
+ panel.nameFieldStringValue = suggestedFilename
128
+ panel.canCreateDirectories = true
129
+ return await run(panel) == .OK ? panel.url : nil
130
+ }
131
+
132
+ func host(_ host: StorWebHost, savedDownload url: URL) {
133
+ #if STOR_DEV
134
+ DevTools.log("download saved \(url.lastPathComponent)")
135
+ #endif
136
+ }
137
+
138
+ func host(_ host: StorWebHost, chooseFilesFor parameters: WKOpenPanelParameters) async -> [URL]? {
139
+ let panel = NSOpenPanel()
140
+ panel.allowsMultipleSelection = parameters.allowsMultipleSelection
141
+ panel.canChooseDirectories = parameters.allowsDirectories
142
+ panel.canChooseFiles = true
143
+ return await run(panel) == .OK ? panel.urls : nil
144
+ }
145
+
146
+ func host(_ host: StorWebHost, present dialog: JSDialog) async -> JSDialogResult {
147
+ let alert = NSAlert()
148
+ alert.messageText = host.site.title
149
+ var field: NSTextField?
150
+ switch dialog {
151
+ case .alert(let message):
152
+ alert.informativeText = message
153
+ alert.addButton(withTitle: "OK")
154
+ case .confirm(let message):
155
+ alert.informativeText = message
156
+ alert.addButton(withTitle: "OK")
157
+ alert.addButton(withTitle: "Cancel")
158
+ case .prompt(let message, let defaultText):
159
+ alert.informativeText = message
160
+ alert.addButton(withTitle: "OK")
161
+ alert.addButton(withTitle: "Cancel")
162
+ let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 260, height: 24))
163
+ input.stringValue = defaultText ?? ""
164
+ alert.accessoryView = input
165
+ field = input
166
+ }
167
+ let response: NSApplication.ModalResponse
168
+ if let window { response = await alert.beginSheetModal(for: window) } else { response = alert.runModal() }
169
+ return response == .alertFirstButtonReturn ? .ok(field?.stringValue) : .cancel
170
+ }
171
+
172
+ /// Shows a save or open panel as a sheet on this window.
173
+ private func run(_ panel: NSSavePanel) async -> NSApplication.ModalResponse {
174
+ if let window { return await panel.beginSheetModal(for: window) }
175
+ return await panel.begin()
176
+ }
177
+ }
178
+
179
+ /// A window the page opened itself, such as a PDF in its own viewer. It shares the site's data store
180
+ /// and follows the same navigation rules as the main window.
181
+ @MainActor
182
+ final class PopupWindowController: NSWindowController, NSWindowDelegate {
183
+ private let onClose: (PopupWindowController) -> Void
184
+
185
+ init(webView: WKWebView, title: String, onClose: @escaping (PopupWindowController) -> Void) {
186
+ self.onClose = onClose
187
+ let window = NSWindow(
188
+ contentRect: NSRect(x: 0, y: 0, width: 920, height: 1040),
189
+ styleMask: [.titled, .closable, .miniaturizable, .resizable],
190
+ backing: .buffered,
191
+ defer: false
192
+ )
193
+ window.title = title
194
+ window.isReleasedWhenClosed = false
195
+ webView.frame = NSRect(origin: .zero, size: window.contentLayoutRect.size)
196
+ webView.autoresizingMask = [.width, .height]
197
+ window.contentView = webView
198
+ window.center()
199
+ super.init(window: window)
200
+ window.delegate = self
201
+ }
202
+
203
+ required init?(coder: NSCoder) { fatalError("init(coder:) is not used") }
204
+
205
+ func windowWillClose(_ notification: Notification) { onClose(self) }
206
+ }
@@ -0,0 +1,60 @@
1
+ import AppKit
2
+ import WebKit
3
+
4
+ /// The Cloudflare Access login, in its own window. It shares the app's data store, so the session
5
+ /// cookie Access sets is the one the app's web view sends. The window closes itself as soon as
6
+ /// Access sends the person back to the app.
7
+ @MainActor
8
+ final class SignInWindowController: NSWindowController, NSWindowDelegate, WKNavigationDelegate {
9
+ private let policy: NavigationPolicy
10
+ private let webView: WKWebView
11
+ private let onFinished: (Bool) -> Void
12
+ private var finished = false
13
+
14
+ init(site: SiteConfig, policy: NavigationPolicy, dataStore: WKWebsiteDataStore, url: URL, onFinished: @escaping (Bool) -> Void) {
15
+ self.policy = policy
16
+ self.onFinished = onFinished
17
+
18
+ let configuration = WKWebViewConfiguration()
19
+ configuration.websiteDataStore = dataStore
20
+ webView = WKWebView(frame: .zero, configuration: configuration)
21
+
22
+ let window = NSWindow(
23
+ contentRect: NSRect(x: 0, y: 0, width: 480, height: 700),
24
+ styleMask: [.titled, .closable, .miniaturizable],
25
+ backing: .buffered,
26
+ defer: false
27
+ )
28
+ window.title = "Sign in to \(site.title)"
29
+ window.isReleasedWhenClosed = false
30
+ window.contentView = webView
31
+ window.center()
32
+ super.init(window: window)
33
+
34
+ window.delegate = self
35
+ webView.navigationDelegate = self
36
+ webView.load(URLRequest(url: url))
37
+ }
38
+
39
+ required init?(coder: NSCoder) { fatalError("init(coder:) is not used") }
40
+
41
+ func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
42
+ let url = navigationAction.request.url
43
+ if policy.finishesSignIn(url) {
44
+ finish(success: true)
45
+ return .cancel
46
+ }
47
+ return policy.allowsSignIn(url) ? .allow : .cancel
48
+ }
49
+
50
+ func windowWillClose(_ notification: Notification) {
51
+ finish(success: false)
52
+ }
53
+
54
+ private func finish(success: Bool) {
55
+ guard !finished else { return }
56
+ finished = true
57
+ onFinished(success)
58
+ if success { close() }
59
+ }
60
+ }
@@ -0,0 +1,48 @@
1
+ import AppKit
2
+
3
+ /// The Mac window. A click on a background of the page (the shell's script reports when the pointer is
4
+ /// over one) moves the window. The mouse-down has to be handled here, before any view sees it and with
5
+ /// the very event that started it: a drag started later, from a message, is not reliable.
6
+ @MainActor
7
+ final class StorWindow: NSWindow {
8
+ /// Set from the page: the pointer is over something that is not a control, text or a graphic.
9
+ var backgroundHover = false
10
+ /// The web view; only clicks that land inside it can be background clicks, so the window buttons
11
+ /// and the native title bar strip keep behaving as they do in any Mac window.
12
+ weak var pageView: NSView?
13
+
14
+ override func sendEvent(_ event: NSEvent) {
15
+ if event.type == .leftMouseDown, backgroundHover, startsBackgroundDrag(event) { return }
16
+ super.sendEvent(event)
17
+ }
18
+
19
+ private func startsBackgroundDrag(_ event: NSEvent) -> Bool {
20
+ #if STOR_DEV
21
+ if let frameView = contentView?.superview {
22
+ let hit = frameView.hitTest(event.locationInWindow)
23
+ var chain: [String] = []
24
+ var view = hit
25
+ while let current = view, chain.count < 8 { chain.append(String(describing: type(of: current))); view = current.superview }
26
+ DevTools.log("hit \(chain.joined(separator: " < ")) inPage=\(hit?.isDescendant(of: pageView ?? frameView) == true) frameView=\(type(of: frameView))")
27
+ }
28
+ #endif
29
+ // Modified clicks are the page's (selection, context menus).
30
+ guard event.modifierFlags.intersection([.control, .option, .command, .shift]).isEmpty,
31
+ let pageView, let frameView = contentView?.superview,
32
+ frameView.hitTest(event.locationInWindow)?.isDescendant(of: pageView) == true
33
+ else { return false }
34
+ #if STOR_DEV
35
+ DevTools.log("background mouse-down clickCount=\(event.clickCount)")
36
+ #endif
37
+ if event.clickCount >= 2 {
38
+ switch UserDefaults.standard.string(forKey: "AppleActionOnDoubleClick") {
39
+ case "Minimize": miniaturize(nil)
40
+ case "None": break
41
+ default: zoom(nil)
42
+ }
43
+ } else {
44
+ performDrag(with: event)
45
+ }
46
+ return true
47
+ }
48
+ }
@@ -0,0 +1,115 @@
1
+ import AppKit
2
+
3
+ /// Places the standard window buttons where the page's sidebar reserved room for them. The page
4
+ /// reports that spot (`[data-stor-lights]`), so the layout decides it and it follows the sidebar.
5
+ @MainActor
6
+ final class TrafficLights {
7
+ /// Before the page reports: the expanded sidebar's brand row.
8
+ static let fallback = CGPoint(x: 26, y: 38)
9
+
10
+ private weak var window: NSWindow?
11
+ private var anchor = TrafficLights.fallback
12
+ private let buttonSpacing: CGFloat
13
+ private var observers: [NSObjectProtocol] = []
14
+ private var applyScheduled = false
15
+ private var isApplying = false
16
+
17
+ init(window: NSWindow) {
18
+ self.window = window
19
+ // Keep AppKit's original spacing: a frame notification may arrive while only one button
20
+ // has been reset, so measuring the partly updated group would give the wrong spacing.
21
+ if let close = window.standardWindowButton(.closeButton),
22
+ let miniaturize = window.standardWindowButton(.miniaturizeButton) {
23
+ buttonSpacing = miniaturize.frame.minX - close.frame.minX
24
+ } else {
25
+ buttonSpacing = 20
26
+ }
27
+ let center = NotificationCenter.default
28
+ // AppKit lays the buttons out again on these, so put them back each time.
29
+ for name in [NSWindow.didResizeNotification, NSWindow.didEndLiveResizeNotification,
30
+ NSWindow.didExitFullScreenNotification, NSWindow.didBecomeKeyNotification,
31
+ NSWindow.didResignKeyNotification] {
32
+ observers.append(center.addObserver(forName: name, object: window, queue: .main) { [weak self] _ in
33
+ MainActor.assumeIsolated {
34
+ if name == NSWindow.didResizeNotification {
35
+ // AppKit has finished this resize's layout. Restore the group before it
36
+ // is displayed; queued work can wait until the mouse is released.
37
+ self?.apply()
38
+ } else {
39
+ self?.scheduleApply()
40
+ }
41
+ }
42
+ })
43
+ }
44
+ let close = window.standardWindowButton(.closeButton)
45
+ // Focus changes can reset just the buttons without changing their container's frame.
46
+ let views: [NSView?] = [close, window.standardWindowButton(.miniaturizeButton),
47
+ window.standardWindowButton(.zoomButton), close?.superview,
48
+ close?.superview?.superview]
49
+ for view in views.compactMap({ $0 }) {
50
+ view.postsFrameChangedNotifications = true
51
+ observers.append(center.addObserver(forName: NSView.frameDidChangeNotification, object: view, queue: .main) { [weak self] _ in
52
+ MainActor.assumeIsolated { self?.scheduleApply() }
53
+ })
54
+ }
55
+ }
56
+
57
+ deinit {
58
+ observers.forEach(NotificationCenter.default.removeObserver)
59
+ }
60
+
61
+ /// Width the three buttons take, so the page can leave exactly that much room. Spacing differs between macOS versions.
62
+ static func extent(of window: NSWindow) -> CGFloat {
63
+ guard let close = window.standardWindowButton(.closeButton),
64
+ let zoom = window.standardWindowButton(.zoomButton)
65
+ else { return 62 }
66
+ let width = zoom.frame.maxX - close.frame.minX
67
+ return width >= 40 ? ceil(width) : 62
68
+ }
69
+
70
+ func move(to anchor: CGPoint) {
71
+ self.anchor = anchor
72
+ apply()
73
+ }
74
+
75
+ private func scheduleApply() {
76
+ guard !isApplying, !applyScheduled else { return }
77
+ applyScheduled = true
78
+ // Finish the current AppKit layout before moving the group; never move one button back
79
+ // synchronously while AppKit is still resetting the others.
80
+ DispatchQueue.main.async { [weak self] in
81
+ guard let self else { return }
82
+ self.applyScheduled = false
83
+ self.apply()
84
+ }
85
+ }
86
+
87
+ func apply() {
88
+ guard !isApplying, let window, !window.styleMask.contains(.fullScreen),
89
+ let close = window.standardWindowButton(.closeButton),
90
+ let miniaturize = window.standardWindowButton(.miniaturizeButton),
91
+ let zoom = window.standardWindowButton(.zoomButton),
92
+ let container = close.superview
93
+ else { return }
94
+ isApplying = true
95
+ defer { isApplying = false }
96
+ let step = buttonSpacing
97
+ // macOS only hit-tests a view inside its container, so a button moved below the title bar would stop
98
+ // working. Grow the title bar container to hold them, and only as wide as they are, so the page's
99
+ // own controls along the top edge stay reachable.
100
+ if let titlebarView = close.superview, let titlebarContainer = titlebarView.superview, titlebarContainer !== window.contentView?.superview {
101
+ let height = ceil(anchor.y + close.frame.height)
102
+ let width = ceil(anchor.x + 2 * step + close.frame.width + 12)
103
+ let frame = NSRect(x: 0, y: window.frame.height - height, width: width, height: height)
104
+ if titlebarContainer.frame != frame { titlebarContainer.frame = frame }
105
+ if titlebarView.frame != titlebarContainer.bounds { titlebarView.frame = titlebarContainer.bounds }
106
+ }
107
+ // The page measures from the window's top; window coordinates start at the bottom.
108
+ let centerY = window.frame.height - anchor.y
109
+ for (index, button) in [close, miniaturize, zoom].enumerated() {
110
+ let point = container.convert(NSPoint(x: anchor.x + CGFloat(index) * step, y: centerY), from: nil)
111
+ let origin = NSPoint(x: point.x, y: point.y - button.frame.height / 2)
112
+ if button.frame.origin != origin { button.setFrameOrigin(origin) }
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,9 @@
1
+ import AppKit
2
+
3
+ MainActor.assumeIsolated {
4
+ let application = NSApplication.shared
5
+ let delegate = AppDelegate()
6
+ application.delegate = delegate
7
+ application.setActivationPolicy(.regular)
8
+ application.run()
9
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.