craft-native 0.0.184 → 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.184";
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.184";
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.184";
61
+ var version = "0.0.185";
62
62
 
63
63
  // src/sdk-version.ts
64
64
  var SDK_VERSION = version;
@@ -6925,10 +6925,15 @@ final class CraftNativeAnimationRecord {
6925
6925
  let fontSize: CGFloat?
6926
6926
  let textColor: UIColor?
6927
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?
6928
6931
 
6929
6932
  init(view: UIView, screen: UIView, transitionID: String?) {
6930
6933
  self.view = view
6931
6934
  superview = view.superview
6935
+ index = view.superview?.subviews.firstIndex(of: view)
6936
+ arrangedIndex = (view.superview as? UIStackView)?.arrangedSubviews.firstIndex(of: view)
6932
6937
  frame = view.frame
6933
6938
  frameOnScreen = view.superview?.convert(view.frame, to: screen) ?? view.frame
6934
6939
  alpha = view.alpha
@@ -6947,6 +6952,10 @@ final class CraftNativeAnimationTransaction {
6947
6952
  var screenBackground: UIColor?
6948
6953
  /// Reduce Motion: the screen as it was, crossfaded into the new one.
6949
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?
6950
6959
 
6951
6960
  init(spec: CraftNativeAnimationSpec, reduceMotion: Bool) {
6952
6961
  self.spec = spec
@@ -6956,7 +6965,11 @@ final class CraftNativeAnimationTransaction {
6956
6965
 
6957
6966
  /// An animation under way: finished at once if another change comes first.
6958
6967
  final class CraftNativeRunningAnimation {
6968
+ /// The Core Animation key every animation of a change goes under.
6969
+ static let key = "craft.change"
6959
6970
  var animators: [UIViewPropertyAnimator] = []
6971
+ /// Live layers carrying an animation of the change, under `key` + a suffix.
6972
+ var layers: [(CALayer, String)] = []
6960
6973
  var cleanup: [() -> Void] = []
6961
6974
  private var done = false
6962
6975
 
@@ -6967,6 +6980,10 @@ final class CraftNativeRunningAnimation {
6967
6980
  animator.stopAnimation(false)
6968
6981
  animator.finishAnimation(at: .end)
6969
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()
6970
6987
  cleanup.forEach { $0() }
6971
6988
  cleanup.removeAll()
6972
6989
  }
@@ -6991,16 +7008,40 @@ extension CraftNativeScreenController {
6991
7008
  let transaction = CraftNativeAnimationTransaction(spec: CraftNativeAnimationSpec(payload), reduceMotion: UIAccessibility.isReduceMotionEnabled)
6992
7009
  transaction.screenBackground = view.backgroundColor
6993
7010
  if transaction.reduceMotion {
6994
- 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
+ }
6995
7018
  } else {
6996
7019
  view.layoutIfNeeded()
6997
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
+ }
6998
7030
  }
6999
7031
  animationTransaction = transaction
7000
7032
  CraftNativeAnimationClock.duration = transaction.spec.duration
7001
7033
  // The change comes from the same turn of the script (the state it
7002
- // changed renders in a microtask); a slow one still animates.
7003
- 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
+ }
7004
7045
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in self?.commitAnimatedChange() }
7005
7046
  }
7006
7047
 
@@ -7020,6 +7061,13 @@ extension CraftNativeScreenController {
7020
7061
  private func commitAnimatedChange() {
7021
7062
  guard let transaction = animationTransaction else { return }
7022
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
7023
7071
  let spec = transaction.spec
7024
7072
  let running = CraftNativeRunningAnimation()
7025
7073
  runningAnimation = running
@@ -7027,9 +7075,6 @@ extension CraftNativeScreenController {
7027
7075
 
7028
7076
  if transaction.reduceMotion {
7029
7077
  guard let picture = transaction.picture else { running.finish(); return }
7030
- picture.frame = view.bounds
7031
- picture.isUserInteractionEnabled = false
7032
- view.addSubview(picture)
7033
7078
  let fade = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) { picture.alpha = 0 }
7034
7079
  fade.addCompletion { _ in running.finish() }
7035
7080
  running.animators.append(fade)
@@ -7073,26 +7118,69 @@ extension CraftNativeScreenController {
7073
7118
  view.addSubview(overlay)
7074
7119
  running.cleanup.append { overlay.removeFromSuperview() }
7075
7120
 
7076
- let main = UIViewPropertyAnimator(duration: spec.duration, timingParameters: spec.timing)
7077
- // 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.
7078
7124
  let fadeOut = UIViewPropertyAnimator(duration: spec.duration * 0.5, curve: .easeOut)
7079
- let fadeIn = UIViewPropertyAnimator(duration: spec.duration * 0.6, curve: .easeInOut)
7080
- running.animators = [main, fadeOut, fadeIn]
7081
-
7082
- // Removed views fade where they were, over what replaced them.
7083
- 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) {
7084
7129
  let going = record.view
7130
+ let resizes = going.translatesAutoresizingMaskIntoConstraints
7131
+ let base = going.transform
7085
7132
  going.translatesAutoresizingMaskIntoConstraints = true
7086
7133
  going.transform = .identity
7087
7134
  going.frame = record.frameOnScreen
7135
+ going.alpha = record.alpha
7088
7136
  overlay.addSubview(going)
7089
- 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))
7090
7179
  }
7091
7180
 
7092
- // 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.
7093
7182
  if let before = transaction.screenBackground, let now = view.backgroundColor, !before.isEqual(now) {
7094
- view.backgroundColor = before
7095
- main.addAnimations { self.view.backgroundColor = now }
7183
+ animate(view.layer, "backgroundColor", from: before.cgColor, spring: false)
7096
7184
  }
7097
7185
 
7098
7186
  // Shared elements: the new view starts where the old one was, at its
@@ -7100,58 +7188,56 @@ extension CraftNativeScreenController {
7100
7188
  for (old, new) in pairs {
7101
7189
  let target = new.superview?.convert(new.frame, to: view) ?? new.frame
7102
7190
  let move = morph(from: old.frameOnScreen, to: target, uniform: true)
7103
- let finalAlpha = new.alpha
7104
- let base = new.transform
7105
- new.transform = base.concatenating(move)
7106
- new.alpha = 0
7107
- main.addAnimations { new.transform = base }
7108
- 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)
7109
7195
  let going = old.view
7110
- going.translatesAutoresizingMaskIntoConstraints = true
7111
- going.transform = .identity
7112
- going.frame = old.frameOnScreen
7113
- going.alpha = old.alpha
7114
- overlay.addSubview(going)
7115
- main.addAnimations { going.transform = move.inverted() }
7116
- fadeOut.addAnimations { going.alpha = 0 }
7117
- running.cleanup.append { new.transform = base; new.alpha = finalAlpha }
7196
+ travel.addAnimations { going.transform = move.inverted() }
7118
7197
  }
7119
7198
 
7120
7199
  // New views fade in. A new container holding a shared element keeps it
7121
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
+ }
7122
7212
  for added in inserted where !(added.superview.map { insertedIds.contains(ObjectIdentifier($0)) } ?? false) {
7123
- fadeInInserted(added, pairedNew: pairedNew, animator: fadeIn, running: running)
7213
+ fadeIn(added)
7124
7214
  }
7125
7215
 
7126
7216
  // Views that stay go to their new places and sizes.
7127
7217
  for record in records.values where present.contains(ObjectIdentifier(record.view)) {
7128
7218
  let stays = record.view
7129
7219
  if let label = stays as? UILabel, let before = record.fontSize, abs(before - label.font.pointSize) > 0.5 {
7130
- let base = label.transform
7131
7220
  let target = label.superview?.convert(label.frame, to: view) ?? label.frame
7132
- label.transform = base.concatenating(morph(from: record.frameOnScreen, to: target, uniform: true))
7133
- main.addAnimations { label.transform = base }
7134
- running.cleanup.append { label.transform = base }
7135
- } else if record.superview === stays.superview, record.frame != stays.frame {
7136
- let final = stays.frame
7137
- stays.frame = record.frame
7138
- 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
+ }
7139
7228
  }
7140
7229
  if let before = record.backgroundColor, let now = stays.backgroundColor, !before.isEqual(now) {
7141
- stays.backgroundColor = before
7142
- main.addAnimations { stays.backgroundColor = now }
7143
- }
7144
- if let label = stays as? UILabel, let before = record.textColor, !before.isEqual(label.textColor) {
7145
- let now = label.textColor
7146
- label.textColor = before
7147
- 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)
7148
7231
  }
7149
7232
  }
7150
7233
 
7151
- main.addCompletion { _ in running.finish() }
7234
+ travel.addCompletion { _ in running.finish() }
7152
7235
  fadeOut.startAnimation()
7153
- fadeIn.startAnimation()
7154
- 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() }
7155
7241
  }
7156
7242
 
7157
7243
  /// The transform that puts a view at `final` where `start` was: moved
@@ -7166,23 +7252,6 @@ extension CraftNativeScreenController {
7166
7252
  return CGAffineTransform(translationX: dx, y: dy).scaledBy(x: sx, y: sy)
7167
7253
  }
7168
7254
 
7169
- private func fadeInInserted(_ view: UIView, pairedNew: Set<ObjectIdentifier>, animator: UIViewPropertyAnimator, running: CraftNativeRunningAnimation) {
7170
- if pairedNew.contains(ObjectIdentifier(view)) { return }
7171
- guard containsShared(view, pairedNew: pairedNew) else {
7172
- let finalAlpha = view.alpha
7173
- view.alpha = 0
7174
- animator.addAnimations { view.alpha = finalAlpha }
7175
- running.cleanup.append { view.alpha = finalAlpha }
7176
- return
7177
- }
7178
- if let colour = view.backgroundColor, colour != .clear {
7179
- view.backgroundColor = colour.withAlphaComponent(0)
7180
- animator.addAnimations { view.backgroundColor = colour }
7181
- running.cleanup.append { view.backgroundColor = colour }
7182
- }
7183
- for child in view.subviews { fadeInInserted(child, pairedNew: pairedNew, animator: animator, running: running) }
7184
- }
7185
-
7186
7255
  private func containsShared(_ view: UIView, pairedNew: Set<ObjectIdentifier>) -> Bool {
7187
7256
  if pairedNew.contains(ObjectIdentifier(view)) { return true }
7188
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.184",
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>",