craft-native 0.0.410 → 0.0.411

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.
package/dist/cli.js CHANGED
@@ -3047,7 +3047,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3047
3047
  `);
3048
3048
  }
3049
3049
  // package.json
3050
- var version = "0.0.410";
3050
+ var version = "0.0.411";
3051
3051
 
3052
3052
  // bin/cli.ts
3053
3053
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -375,7 +375,7 @@ function craftBinaryNotFoundMessage(triedPath) {
375
375
  `);
376
376
  }
377
377
  // package.json
378
- var version = "0.0.410";
378
+ var version = "0.0.411";
379
379
 
380
380
  // src/sdk-version.ts
381
381
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -117,7 +117,7 @@ function craftBinaryNotFoundMessage(triedPath) {
117
117
  `);
118
118
  }
119
119
  // package.json
120
- var version = "0.0.410";
120
+ var version = "0.0.411";
121
121
 
122
122
  // src/sdk-version.ts
123
123
  var SDK_VERSION = version;
@@ -11133,6 +11133,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
11133
11133
  if self.keyboardScreenFrame != nil { self.insetScrollViewsForKeyboard() }
11134
11134
  guard let scroll = self.enclosingVerticalScroll(of: textView),
11135
11135
  let rect = self.caretRect(of: textView, in: scroll) else { return }
11136
+ // UIKit's text view scrolls this scroll view itself, animated, when
11137
+ // the keyboard finishes appearing. Arriving after an edit and
11138
+ // before this pass, it aims at the caret of the stale layout (the
11139
+ // input's top), and while it runs it carries any offset set here
11140
+ // back toward that target. This pass measured the current layout:
11141
+ // stop that animation so the caret stays where it can be seen. A
11142
+ // finger's drag or fling is the person's, and is left alone.
11143
+ if !scroll.isTracking && !scroll.isDecelerating {
11144
+ scroll.setContentOffset(scroll.contentOffset, animated: false)
11145
+ }
11136
11146
  let visible = scroll.bounds.inset(by: scroll.adjustedContentInset)
11137
11147
  guard !visible.contains(rect) else { return }
11138
11148
  scroll.scrollRectToVisible(rect, animated: false)
@@ -19,6 +19,8 @@ final class CraftMacScene: NSObject, NSWindowDelegate {
19
19
  /// Where the close button's top-left corner sits, from the window's top-left (`--traffic-lights 18,18`):
20
20
  /// a browser lines the buttons up with its own toolbar row. Nil keeps AppKit's place.
21
21
  let trafficLights: CGPoint?
22
+ /// The screen has work to finish before the app quits (`craft.desktop.onQuit`).
23
+ var holdsQuit = false
22
24
  init(name: String, route: String, options: [String: Any], application: CraftMacApplication) {
23
25
  self.name = name
24
26
  runtime = CraftMacRuntime(route: route, params: options["params"] as? [String: Any] ?? [:], network: application.network)
@@ -69,6 +71,7 @@ final class CraftMacScene: NSObject, NSWindowDelegate {
69
71
  func windowWillClose(_ notification: Notification) {
70
72
  runtime.dispose()
71
73
  CraftMacApplication.shared.scenes.removeValue(forKey: name)
74
+ CraftMacApplication.shared.quitReady(scene: name)
72
75
  }
73
76
  func windowDidBecomeKey(_ notification: Notification) { runtime.send("SCREEN_FOCUS", ["focused": true]) }
74
77
  func windowDidResignKey(_ notification: Notification) { runtime.send("SCREEN_FOCUS", ["focused": false]) }
@@ -99,6 +102,9 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
99
102
  var testTimer: Timer?
100
103
  var processedActions = Set<String>()
101
104
  var appearanceObservation: NSKeyValueObservation?
105
+ /// A quit waiting for screens to finish (`craft.desktop.onQuit`), for at most `quitGrace` seconds.
106
+ private var pendingQuit: (token: String, waiting: Set<String>, timeout: Timer)?
107
+ static let quitGrace: TimeInterval = 3
102
108
 
103
109
  func applicationDidFinishLaunching(_ notification: Notification) {
104
110
  installMenus()
@@ -128,6 +134,34 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
128
134
  func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { scenes["main"]?.window.makeKeyAndOrderFront(nil); return true }
129
135
  func applicationWillTerminate(_ notification: Notification) { scenes.values.forEach { $0.runtime.dispose() }; testTimer?.invalidate() }
130
136
  func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { false }
137
+ /// Screens holding the quit hear `craftDesktopQuit` and answer `quitReady` once their work is saved.
138
+ /// The main queue keeps running while AppKit waits, so their timers and requests still complete,
139
+ /// unless `terminate` was called from inside a main-queue block: GCD runs no other main-queue
140
+ /// block until that one returns. So the host never calls it from one, and the timeout is a run
141
+ /// loop timer, which fires either way. A second quit while waiting quits at once.
142
+ func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
143
+ let holding = scenes.values.filter(\.holdsQuit)
144
+ guard !holding.isEmpty, pendingQuit == nil else { return .terminateNow }
145
+ let token = UUID().uuidString
146
+ let timeout = Timer(timeInterval: Self.quitGrace, repeats: false) { [weak self] _ in self?.finishQuit(token) }
147
+ RunLoop.main.add(timeout, forMode: .common)
148
+ pendingQuit = (token, Set(holding.map(\.name)), timeout)
149
+ for scene in holding { scene.runtime.send("CRAFT_EVENT", ["event": "craftDesktopQuit", "detail": ["token": token]]) }
150
+ return .terminateLater
151
+ }
152
+ /// A screen finished its quit work, or went away. `token` nil matches the quit in progress.
153
+ func quitReady(scene: String, token: String? = nil) {
154
+ guard var pending = pendingQuit, token == nil || token == pending.token else { return }
155
+ pending.waiting.remove(scene)
156
+ pendingQuit = pending
157
+ if pending.waiting.isEmpty { finishQuit(pending.token) }
158
+ }
159
+ private func finishQuit(_ token: String) {
160
+ guard let pending = pendingQuit, pending.token == token else { return }
161
+ pending.timeout.invalidate()
162
+ pendingQuit = nil
163
+ NSApp.reply(toApplicationShouldTerminate: true)
164
+ }
131
165
  func open(name: String, route: String, options: [String: Any]) {
132
166
  if let scene = scenes[name] { scene.window.makeKeyAndOrderFront(nil); return }
133
167
  scenes[name] = CraftMacScene(name: name, route: route, options: options, application: self)
@@ -253,6 +287,8 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
253
287
  guard let image = filter.outputImage?.transformed(by: CGAffineTransform(scaleX: 8, y: 8)), let cg = CIContext().createCGImage(image, from: image.extent), let data = NSBitmapImageRep(cgImage: cg).representation(using: .png, properties: [:]) else { reply(NSNull()); return }
254
288
  reply("data:image/png;base64," + data.base64EncodedString())
255
289
  case "setBadge": let count = (args.first as? NSNumber)?.intValue ?? 0; NSApp.dockTile.badgeLabel = count > 0 ? String(count) : nil; reply(true)
290
+ case "holdQuit": scene?.holdsQuit = args.first as? Bool != false; reply(true)
291
+ case "quitReady": if let scene { quitReady(scene: scene.name, token: args.first as? String) }; reply(true)
256
292
  default: reply(["error": "Unsupported desktop operation: \(method)"])
257
293
  }
258
294
  }
@@ -322,7 +358,8 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
322
358
  private func testAction(_ action: [String: Any]) -> [String: Any] {
323
359
  let kind = action["action"] as? String ?? ""
324
360
  if kind == "command", let id = action["id"] as? String { broadcast(id); return ["ok": true] }
325
- if kind == "quit" { DispatchQueue.main.async { NSApp.terminate(nil) }; return ["ok": true] }
361
+ // After this result is written, from the run loop rather than a main-queue block (see applicationShouldTerminate).
362
+ if kind == "quit" { NSApp.perform(#selector(NSApplication.terminate(_:)), with: nil, afterDelay: 0); return ["ok": true] }
326
363
  if kind == "appearance" {
327
364
  NSApp.appearance = NSAppearance(named: action["value"] as? String == "dark" ? .darkAqua : .aqua)
328
365
  appearanceChanged(); return ["ok": true]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.410",
3
+ "version": "0.0.411",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",