craft-native 0.0.183 → 0.0.185

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
@@ -3303,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3303
3303
  `);
3304
3304
  }
3305
3305
  // package.json
3306
- var version = "0.0.183";
3306
+ var version = "0.0.185";
3307
3307
 
3308
3308
  // bin/cli.ts
3309
3309
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -310,7 +310,7 @@ function craftBinaryNotFoundMessage(triedPath) {
310
310
  `);
311
311
  }
312
312
  // package.json
313
- var version = "0.0.183";
313
+ var version = "0.0.185";
314
314
 
315
315
  // src/sdk-version.ts
316
316
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ function craftBinaryNotFoundMessage(triedPath) {
58
58
  `);
59
59
  }
60
60
  // package.json
61
- var version = "0.0.183";
61
+ var version = "0.0.185";
62
62
 
63
63
  // src/sdk-version.ts
64
64
  var SDK_VERSION = version;
@@ -6332,9 +6332,17 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6332
6332
  view.accessibilityValue = nil
6333
6333
  }
6334
6334
  let role = props["accessibilityRole"] as? String
6335
- view.isAccessibilityElement = role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Link", "Checkbox", "Image", "ImageBackground", "Video", "TextInput", "Picker", "DatePicker", "Switch", "Slider"].contains(type))
6335
+ // A scroll view, a list or a group of controls holds what VoiceOver
6336
+ // reads: its label names the group, as a tab strip or a pager's
6337
+ // name, and never stands in for its content. As an element of its
6338
+ // own, a labelled pager hid every page from VoiceOver.
6339
+ let container = ["ScrollView", "FlatList", "SectionList"].contains(type)
6340
+ || ["tablist", "radiogroup", "list", "menu", "menubar", "toolbar"].contains(role ?? "")
6341
+ view.isAccessibilityElement = !container && role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Link", "Checkbox", "Image", "ImageBackground", "Video", "TextInput", "Picker", "DatePicker", "Switch", "Slider"].contains(type))
6342
+ view.accessibilityContainerType = container && view.accessibilityLabel != nil ? .semanticGroup : .none
6336
6343
  var traits: UIAccessibilityTraits = []
6337
6344
  switch role ?? type.lowercased() {
6345
+ case "tablist": traits.insert(.tabBar)
6338
6346
  case "button", "checkbox": traits.insert(.button)
6339
6347
  case "image": traits.insert(.image)
6340
6348
  case "header": traits.insert(.header)
@@ -6917,10 +6925,15 @@ final class CraftNativeAnimationRecord {
6917
6925
  let fontSize: CGFloat?
6918
6926
  let textColor: UIColor?
6919
6927
  let transitionID: String?
6928
+ /// Where it sat in its parent, to be put back after it faded elsewhere.
6929
+ let index: Int?
6930
+ let arrangedIndex: Int?
6920
6931
 
6921
6932
  init(view: UIView, screen: UIView, transitionID: String?) {
6922
6933
  self.view = view
6923
6934
  superview = view.superview
6935
+ index = view.superview?.subviews.firstIndex(of: view)
6936
+ arrangedIndex = (view.superview as? UIStackView)?.arrangedSubviews.firstIndex(of: view)
6924
6937
  frame = view.frame
6925
6938
  frameOnScreen = view.superview?.convert(view.frame, to: screen) ?? view.frame
6926
6939
  alpha = view.alpha
@@ -6939,6 +6952,10 @@ final class CraftNativeAnimationTransaction {
6939
6952
  var screenBackground: UIColor?
6940
6953
  /// Reduce Motion: the screen as it was, crossfaded into the new one.
6941
6954
  var picture: UIView?
6955
+ /// Commits the change as the run loop finishes the pass it came in.
6956
+ var observer: CFRunLoopObserver?
6957
+ /// The screen as it was, drawn over it until the animation is set up.
6958
+ var cover: UIView?
6942
6959
 
6943
6960
  init(spec: CraftNativeAnimationSpec, reduceMotion: Bool) {
6944
6961
  self.spec = spec
@@ -6948,7 +6965,11 @@ final class CraftNativeAnimationTransaction {
6948
6965
 
6949
6966
  /// An animation under way: finished at once if another change comes first.
6950
6967
  final class CraftNativeRunningAnimation {
6968
+ /// The Core Animation key every animation of a change goes under.
6969
+ static let key = "craft.change"
6951
6970
  var animators: [UIViewPropertyAnimator] = []
6971
+ /// Live layers carrying an animation of the change, under `key` + a suffix.
6972
+ var layers: [(CALayer, String)] = []
6952
6973
  var cleanup: [() -> Void] = []
6953
6974
  private var done = false
6954
6975
 
@@ -6959,6 +6980,10 @@ final class CraftNativeRunningAnimation {
6959
6980
  animator.stopAnimation(false)
6960
6981
  animator.finishAnimation(at: .end)
6961
6982
  }
6983
+ // A live view's own values were never touched: taking the animation
6984
+ // off leaves it exactly as the screen last drew it.
6985
+ for (layer, key) in layers { layer.removeAnimation(forKey: key) }
6986
+ layers.removeAll()
6962
6987
  cleanup.forEach { $0() }
6963
6988
  cleanup.removeAll()
6964
6989
  }
@@ -6983,16 +7008,40 @@ extension CraftNativeScreenController {
6983
7008
  let transaction = CraftNativeAnimationTransaction(spec: CraftNativeAnimationSpec(payload), reduceMotion: UIAccessibility.isReduceMotionEnabled)
6984
7009
  transaction.screenBackground = view.backgroundColor
6985
7010
  if transaction.reduceMotion {
6986
- transaction.picture = view.window == nil ? nil : view.snapshotView(afterScreenUpdates: false)
7011
+ // Held over the screen until the change is in, then faded away.
7012
+ if view.window != nil, let picture = view.snapshotView(afterScreenUpdates: false) {
7013
+ picture.frame = view.bounds
7014
+ picture.isUserInteractionEnabled = false
7015
+ view.addSubview(picture)
7016
+ transaction.picture = picture
7017
+ }
6987
7018
  } else {
6988
7019
  view.layoutIfNeeded()
6989
7020
  recordAnimationState(renderedRoot, into: transaction)
7021
+ // The screen as it is, held over it until the change's animation
7022
+ // is set up: UIKit can draw the new screen before the run loop's
7023
+ // pass ends, which showed it for a frame before it animated.
7024
+ if view.window != nil, let cover = view.snapshotView(afterScreenUpdates: false) {
7025
+ cover.frame = view.bounds
7026
+ cover.isUserInteractionEnabled = false
7027
+ view.addSubview(cover)
7028
+ transaction.cover = cover
7029
+ }
6990
7030
  }
6991
7031
  animationTransaction = transaction
6992
7032
  CraftNativeAnimationClock.duration = transaction.spec.duration
6993
7033
  // The change comes from the same turn of the script (the state it
6994
- // changed renders in a microtask); a slow one still animates.
6995
- DispatchQueue.main.async { [weak self] in self?.commitAnimatedChange() }
7034
+ // changed renders in a microtask), so it is set up as the run loop
7035
+ // finishes this pass, before Core Animation draws it: drawn first, the
7036
+ // new screen showed for a frame and then jumped back to animate. A
7037
+ // change that comes later still animates.
7038
+ let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.beforeWaiting.rawValue, false, 0) { [weak self] _, _ in
7039
+ self?.commitAnimatedChange()
7040
+ }
7041
+ if let observer {
7042
+ CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)
7043
+ transaction.observer = observer
7044
+ }
6996
7045
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in self?.commitAnimatedChange() }
6997
7046
  }
6998
7047
 
@@ -7012,6 +7061,13 @@ extension CraftNativeScreenController {
7012
7061
  private func commitAnimatedChange() {
7013
7062
  guard let transaction = animationTransaction else { return }
7014
7063
  animationTransaction = nil
7064
+ if let observer = transaction.observer {
7065
+ CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, .commonModes)
7066
+ transaction.observer = nil
7067
+ }
7068
+ // Taken off in the same transaction the animations start in.
7069
+ transaction.cover?.removeFromSuperview()
7070
+ transaction.cover = nil
7015
7071
  let spec = transaction.spec
7016
7072
  let running = CraftNativeRunningAnimation()
7017
7073
  runningAnimation = running
@@ -7019,9 +7075,6 @@ extension CraftNativeScreenController {
7019
7075
 
7020
7076
  if transaction.reduceMotion {
7021
7077
  guard let picture = transaction.picture else { running.finish(); return }
7022
- picture.frame = view.bounds
7023
- picture.isUserInteractionEnabled = false
7024
- view.addSubview(picture)
7025
7078
  let fade = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) { picture.alpha = 0 }
7026
7079
  fade.addCompletion { _ in running.finish() }
7027
7080
  running.animators.append(fade)
@@ -7065,26 +7118,69 @@ extension CraftNativeScreenController {
7065
7118
  view.addSubview(overlay)
7066
7119
  running.cleanup.append { overlay.removeFromSuperview() }
7067
7120
 
7068
- let main = UIViewPropertyAnimator(duration: spec.duration, timingParameters: spec.timing)
7069
- // Fades take the first part, so nothing is half there for long.
7121
+ // Views that are going are the change's own now: they move into the
7122
+ // overlay where they were and fade there. Put back as they were
7123
+ // afterwards, in case the screen shows them again.
7070
7124
  let fadeOut = UIViewPropertyAnimator(duration: spec.duration * 0.5, curve: .easeOut)
7071
- let fadeIn = UIViewPropertyAnimator(duration: spec.duration * 0.6, curve: .easeInOut)
7072
- running.animators = [main, fadeOut, fadeIn]
7073
-
7074
- // Removed views fade where they were, over what replaced them.
7075
- for record in removedRoots {
7125
+ let quickFade = UIViewPropertyAnimator(duration: spec.duration * 0.35, curve: .easeOut)
7126
+ let travel = UIViewPropertyAnimator(duration: spec.duration, timingParameters: spec.timing)
7127
+ running.animators = [fadeOut, quickFade, travel]
7128
+ func adopt(_ record: CraftNativeAnimationRecord, fading fade: UIViewPropertyAnimator) {
7076
7129
  let going = record.view
7130
+ let resizes = going.translatesAutoresizingMaskIntoConstraints
7131
+ let base = going.transform
7077
7132
  going.translatesAutoresizingMaskIntoConstraints = true
7078
7133
  going.transform = .identity
7079
7134
  going.frame = record.frameOnScreen
7135
+ going.alpha = record.alpha
7080
7136
  overlay.addSubview(going)
7081
- fadeOut.addAnimations { going.alpha = 0 }
7137
+ fade.addAnimations { going.alpha = 0 }
7138
+ running.cleanup.append {
7139
+ going.removeFromSuperview()
7140
+ going.transform = base
7141
+ going.alpha = record.alpha
7142
+ going.translatesAutoresizingMaskIntoConstraints = resizes
7143
+ going.frame = record.frame
7144
+ // Back in a parent that went with it, as it was: a screen
7145
+ // that shows that subtree again finds it whole.
7146
+ if let parent = record.superview, parent.window == nil {
7147
+ if let stack = parent as? UIStackView, let at = record.arrangedIndex {
7148
+ stack.insertArrangedSubview(going, at: min(at, stack.arrangedSubviews.count))
7149
+ } else if let at = record.index {
7150
+ parent.insertSubview(going, at: min(at, parent.subviews.count))
7151
+ }
7152
+ }
7153
+ }
7154
+ }
7155
+ for record in removedRoots { adopt(record, fading: fadeOut) }
7156
+
7157
+ // Live views are animated on their layers only, from how they were to
7158
+ // how the screen draws them now: nothing the screen sets on them
7159
+ // meanwhile (the next second on a timer, a style patch) is undone.
7160
+ let fades = spec.duration * 0.6
7161
+ func animate(_ layer: CALayer, _ keyPath: String, from: Any, duration: TimeInterval? = nil, spring: Bool = true) {
7162
+ let animation: CABasicAnimation
7163
+ if spring, duration == nil, spec.curve == "spring" {
7164
+ let bouncy = CASpringAnimation(keyPath: keyPath)
7165
+ bouncy.mass = 1
7166
+ bouncy.stiffness = pow(2 * .pi / spec.duration, 2)
7167
+ bouncy.damping = 2 * sqrt(bouncy.stiffness) * (1 - spec.bounce)
7168
+ bouncy.duration = bouncy.settlingDuration
7169
+ animation = bouncy
7170
+ } else {
7171
+ animation = CABasicAnimation(keyPath: keyPath)
7172
+ animation.duration = duration ?? spec.duration
7173
+ animation.timingFunction = CAMediaTimingFunction(name: spec.curve == "linear" ? .linear : .easeInEaseOut)
7174
+ }
7175
+ animation.fromValue = from
7176
+ let key = "\(CraftNativeRunningAnimation.key).\(keyPath)"
7177
+ layer.add(animation, forKey: key)
7178
+ running.layers.append((layer, key))
7082
7179
  }
7083
7180
 
7084
- // The background, the screen's own included, goes from the old colour.
7181
+ // The background, the screen's own included, comes from the old colour.
7085
7182
  if let before = transaction.screenBackground, let now = view.backgroundColor, !before.isEqual(now) {
7086
- view.backgroundColor = before
7087
- main.addAnimations { self.view.backgroundColor = now }
7183
+ animate(view.layer, "backgroundColor", from: before.cgColor, spring: false)
7088
7184
  }
7089
7185
 
7090
7186
  // Shared elements: the new view starts where the old one was, at its
@@ -7092,58 +7188,56 @@ extension CraftNativeScreenController {
7092
7188
  for (old, new) in pairs {
7093
7189
  let target = new.superview?.convert(new.frame, to: view) ?? new.frame
7094
7190
  let move = morph(from: old.frameOnScreen, to: target, uniform: true)
7095
- let finalAlpha = new.alpha
7096
- let base = new.transform
7097
- new.transform = base.concatenating(move)
7098
- new.alpha = 0
7099
- main.addAnimations { new.transform = base }
7100
- fadeIn.addAnimations { new.alpha = finalAlpha }
7191
+ animate(new.layer, "transform", from: CATransform3DMakeAffineTransform(new.transform.concatenating(move)))
7192
+ // Crossed quickly, so the two are never both half there for long.
7193
+ animate(new.layer, "opacity", from: 0, duration: spec.duration * 0.35)
7194
+ adopt(old, fading: quickFade)
7101
7195
  let going = old.view
7102
- going.translatesAutoresizingMaskIntoConstraints = true
7103
- going.transform = .identity
7104
- going.frame = old.frameOnScreen
7105
- going.alpha = old.alpha
7106
- overlay.addSubview(going)
7107
- main.addAnimations { going.transform = move.inverted() }
7108
- fadeOut.addAnimations { going.alpha = 0 }
7109
- running.cleanup.append { new.transform = base; new.alpha = finalAlpha }
7196
+ travel.addAnimations { going.transform = move.inverted() }
7110
7197
  }
7111
7198
 
7112
7199
  // New views fade in. A new container holding a shared element keeps it
7113
7200
  // in sight: only the rest of it fades, and its colour comes up.
7201
+ func fadeIn(_ added: UIView) {
7202
+ if pairedNew.contains(ObjectIdentifier(added)) { return }
7203
+ guard containsShared(added, pairedNew: pairedNew) else {
7204
+ animate(added.layer, "opacity", from: 0, duration: fades)
7205
+ return
7206
+ }
7207
+ if let colour = added.backgroundColor, colour != .clear {
7208
+ animate(added.layer, "backgroundColor", from: colour.withAlphaComponent(0).cgColor, duration: fades)
7209
+ }
7210
+ added.subviews.forEach(fadeIn)
7211
+ }
7114
7212
  for added in inserted where !(added.superview.map { insertedIds.contains(ObjectIdentifier($0)) } ?? false) {
7115
- fadeInInserted(added, pairedNew: pairedNew, animator: fadeIn, running: running)
7213
+ fadeIn(added)
7116
7214
  }
7117
7215
 
7118
7216
  // Views that stay go to their new places and sizes.
7119
7217
  for record in records.values where present.contains(ObjectIdentifier(record.view)) {
7120
7218
  let stays = record.view
7121
7219
  if let label = stays as? UILabel, let before = record.fontSize, abs(before - label.font.pointSize) > 0.5 {
7122
- let base = label.transform
7123
7220
  let target = label.superview?.convert(label.frame, to: view) ?? label.frame
7124
- label.transform = base.concatenating(morph(from: record.frameOnScreen, to: target, uniform: true))
7125
- main.addAnimations { label.transform = base }
7126
- running.cleanup.append { label.transform = base }
7127
- } else if record.superview === stays.superview, record.frame != stays.frame {
7128
- let final = stays.frame
7129
- stays.frame = record.frame
7130
- main.addAnimations { stays.frame = final }
7221
+ let move = morph(from: record.frameOnScreen, to: target, uniform: true)
7222
+ animate(label.layer, "transform", from: CATransform3DMakeAffineTransform(label.transform.concatenating(move)))
7223
+ } else if record.superview === stays.superview, record.frame != stays.frame, stays.transform.isIdentity {
7224
+ animate(stays.layer, "position", from: CGPoint(x: record.frame.midX, y: record.frame.midY))
7225
+ if record.frame.size != stays.frame.size {
7226
+ animate(stays.layer, "bounds.size", from: record.frame.size)
7227
+ }
7131
7228
  }
7132
7229
  if let before = record.backgroundColor, let now = stays.backgroundColor, !before.isEqual(now) {
7133
- stays.backgroundColor = before
7134
- main.addAnimations { stays.backgroundColor = now }
7135
- }
7136
- if let label = stays as? UILabel, let before = record.textColor, !before.isEqual(label.textColor) {
7137
- let now = label.textColor
7138
- label.textColor = before
7139
- UIView.transition(with: label, duration: spec.duration * 0.6, options: [.transitionCrossDissolve, .allowAnimatedContent]) { label.textColor = now }
7230
+ animate(stays.layer, "backgroundColor", from: before.cgColor, spring: false)
7140
7231
  }
7141
7232
  }
7142
7233
 
7143
- main.addCompletion { _ in running.finish() }
7234
+ travel.addCompletion { _ in running.finish() }
7144
7235
  fadeOut.startAnimation()
7145
- fadeIn.startAnimation()
7146
- main.startAnimation()
7236
+ quickFade.startAnimation()
7237
+ travel.startAnimation()
7238
+ // The springs settle a little after `duration`; the views are already
7239
+ // where the screen draws them, so finishing then changes nothing.
7240
+ DispatchQueue.main.asyncAfter(deadline: .now() + spec.duration * 2) { running.finish() }
7147
7241
  }
7148
7242
 
7149
7243
  /// The transform that puts a view at `final` where `start` was: moved
@@ -7158,23 +7252,6 @@ extension CraftNativeScreenController {
7158
7252
  return CGAffineTransform(translationX: dx, y: dy).scaledBy(x: sx, y: sy)
7159
7253
  }
7160
7254
 
7161
- private func fadeInInserted(_ view: UIView, pairedNew: Set<ObjectIdentifier>, animator: UIViewPropertyAnimator, running: CraftNativeRunningAnimation) {
7162
- if pairedNew.contains(ObjectIdentifier(view)) { return }
7163
- guard containsShared(view, pairedNew: pairedNew) else {
7164
- let finalAlpha = view.alpha
7165
- view.alpha = 0
7166
- animator.addAnimations { view.alpha = finalAlpha }
7167
- running.cleanup.append { view.alpha = finalAlpha }
7168
- return
7169
- }
7170
- if let colour = view.backgroundColor, colour != .clear {
7171
- view.backgroundColor = colour.withAlphaComponent(0)
7172
- animator.addAnimations { view.backgroundColor = colour }
7173
- running.cleanup.append { view.backgroundColor = colour }
7174
- }
7175
- for child in view.subviews { fadeInInserted(child, pairedNew: pairedNew, animator: animator, running: running) }
7176
- }
7177
-
7178
7255
  private func containsShared(_ view: UIView, pairedNew: Set<ObjectIdentifier>) -> Bool {
7179
7256
  if pairedNew.contains(ObjectIdentifier(view)) { return true }
7180
7257
  return view.subviews.contains { containsShared($0, pairedNew: pairedNew) }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.183",
3
+ "version": "0.0.185",
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>",