@swiftbrowser/native 0.17.7 → 0.18.0

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.
Files changed (100) hide show
  1. package/README.md +99 -11
  2. package/dist/blob.d.ts +60 -0
  3. package/dist/blob.d.ts.map +1 -0
  4. package/dist/blob.js +153 -0
  5. package/dist/blob.js.map +1 -0
  6. package/dist/bridge.d.ts +65 -1
  7. package/dist/bridge.d.ts.map +1 -1
  8. package/dist/bridge.js.map +1 -1
  9. package/dist/fetch.d.ts +11 -3
  10. package/dist/fetch.d.ts.map +1 -1
  11. package/dist/fetch.js +26 -2
  12. package/dist/fetch.js.map +1 -1
  13. package/dist/hostRequests.d.ts +55 -0
  14. package/dist/hostRequests.d.ts.map +1 -0
  15. package/dist/hostRequests.js +196 -0
  16. package/dist/hostRequests.js.map +1 -0
  17. package/dist/index.d.ts +8 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +8 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/matchMedia.d.ts +43 -0
  22. package/dist/matchMedia.d.ts.map +1 -0
  23. package/dist/matchMedia.js +63 -0
  24. package/dist/matchMedia.js.map +1 -0
  25. package/dist/notifications.d.ts +108 -0
  26. package/dist/notifications.d.ts.map +1 -0
  27. package/dist/notifications.js +251 -0
  28. package/dist/notifications.js.map +1 -0
  29. package/dist/run.d.ts.map +1 -1
  30. package/dist/run.js +23 -2
  31. package/dist/run.js.map +1 -1
  32. package/dist/runtime.d.ts +2 -0
  33. package/dist/runtime.d.ts.map +1 -1
  34. package/dist/runtime.js +69 -0
  35. package/dist/runtime.js.map +1 -1
  36. package/dist/secureStorage.d.ts +20 -0
  37. package/dist/secureStorage.d.ts.map +1 -0
  38. package/dist/secureStorage.js +28 -0
  39. package/dist/secureStorage.js.map +1 -0
  40. package/dist/site.d.ts +3 -3
  41. package/dist/site.d.ts.map +1 -1
  42. package/dist/site.js +1 -1
  43. package/dist/site.js.map +1 -1
  44. package/dist/subtle.d.ts +38 -0
  45. package/dist/subtle.d.ts.map +1 -0
  46. package/dist/subtle.js +72 -0
  47. package/dist/subtle.js.map +1 -0
  48. package/dist/undo.d.ts +23 -0
  49. package/dist/undo.d.ts.map +1 -0
  50. package/dist/undo.js +44 -0
  51. package/dist/undo.js.map +1 -0
  52. package/dist/webSocket.d.ts +73 -0
  53. package/dist/webSocket.d.ts.map +1 -0
  54. package/dist/webSocket.js +145 -0
  55. package/dist/webSocket.js.map +1 -0
  56. package/package.json +3 -3
  57. package/src/blob.ts +186 -0
  58. package/src/bridge.ts +74 -1
  59. package/src/fetch.ts +29 -3
  60. package/src/hostRequests.ts +216 -0
  61. package/src/index.ts +8 -0
  62. package/src/matchMedia.ts +79 -0
  63. package/src/notifications.ts +317 -0
  64. package/src/run.ts +21 -2
  65. package/src/runtime.ts +71 -0
  66. package/src/secureStorage.ts +39 -0
  67. package/src/site.ts +3 -3
  68. package/src/subtle.ts +87 -0
  69. package/src/undo.ts +47 -0
  70. package/src/webSocket.ts +175 -0
  71. package/swift/Sources/SwiftBrowserNative/Host/AudioRecorderBridge.swift +199 -0
  72. package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +154 -0
  73. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +211 -0
  74. package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +451 -0
  75. package/swift/Sources/SwiftBrowserNative/Host/NotificationBridge.swift +304 -0
  76. package/swift/Sources/SwiftBrowserNative/Host/SecureStorage.swift +60 -0
  77. package/swift/Sources/SwiftBrowserNative/Host/ShimAliases.swift +1 -0
  78. package/swift/Sources/SwiftBrowserNative/Host/TextMeasureBridge.swift +139 -0
  79. package/swift/Sources/SwiftBrowserNative/Host/UndoBridge.swift +170 -0
  80. package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +118 -0
  81. package/swift/Sources/SwiftBrowserNative/Runtime/NativeApp.swift +56 -0
  82. package/swift/Sources/SwiftBrowserNative/Runtime/NativeAppDelegate.swift +36 -0
  83. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +362 -16
  84. package/swift/Sources/SwiftBrowserNative/Views/Kinds/DeleteBackward.swift +147 -0
  85. package/swift/Sources/SwiftBrowserNative/Views/Kinds/KeyboardBar.swift +84 -0
  86. package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +116 -11
  87. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +12 -1
  88. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Shapes.swift +30 -9
  89. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +163 -10
  90. package/swift/Sources/SwiftBrowserNative/Views/Kinds/TextAndImages.swift +17 -4
  91. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Video.swift +8 -3
  92. package/swift/Sources/SwiftBrowserNative/Views/NativeRootView.swift +8 -0
  93. package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +41 -0
  94. package/swift/Sources/SwiftBrowserProtocol/Op.swift +27 -4
  95. package/swift/Sources/SwiftBrowserProtocol/Props.swift +110 -3
  96. package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +47 -0
  97. package/swift/Sources/SwiftBrowserProtocol/SVGPath.swift +260 -0
  98. package/swift/Sources/SwiftBrowserProtocol/Style.swift +58 -0
  99. package/swift/Sources/SwiftBrowserTree/OpTree.swift +22 -2
  100. package/wasm/SwiftBrowserNative.wasm +2 -2
@@ -0,0 +1,260 @@
1
+ import Foundation
2
+
3
+ /// SVG path data, as the protocol carries a custom shape's path (`M`, `L`,
4
+ /// `H`, `V`, `C`, `S`, `Q`, `T`, `A`, `Z` and their relative forms): parsed
5
+ /// into moves, lines, curves and closes in absolute coordinates, arcs as
6
+ /// cubic curves, for a renderer to build its own path from. SwiftUI's
7
+ /// `Path(_:)` reads only `Path.description`'s form, not SVG.
8
+ public enum SVGPath {
9
+ public struct Point: Equatable, Sendable {
10
+ public var x: Double
11
+ public var y: Double
12
+ public init(x: Double, y: Double) {
13
+ self.x = x
14
+ self.y = y
15
+ }
16
+ }
17
+
18
+ public enum Element: Equatable, Sendable {
19
+ case move(Point)
20
+ case line(Point)
21
+ case quadCurve(to: Point, control: Point)
22
+ case curve(to: Point, control1: Point, control2: Point)
23
+ case closeSubpath
24
+ }
25
+
26
+ /// The elements of `data`; nil when it is not path data (an unknown
27
+ /// command, a number missing). Empty data is no elements.
28
+ public static func parse(_ data: String) -> [Element]? {
29
+ var scanner = PathScanner(Array(data.utf8))
30
+ var elements: [Element] = []
31
+ var current = Point(x: 0, y: 0)
32
+ var start = current
33
+ // The last curve's second control point, for `S` and `T`.
34
+ var lastCubic: Point?
35
+ var lastQuad: Point?
36
+ var command: UInt8?
37
+
38
+ while true {
39
+ scanner.skipSeparators()
40
+ guard let byte = scanner.peek else { break }
41
+ if PathScanner.isCommand(byte) {
42
+ command = byte
43
+ scanner.index += 1
44
+ } else if command == nil {
45
+ return nil
46
+ }
47
+ guard let op = command else { return nil }
48
+ let relative = op >= UInt8(ascii: "a")
49
+ let base = relative ? current : Point(x: 0, y: 0)
50
+ func point() -> Point? {
51
+ guard let x = scanner.number(), let y = scanner.number() else { return nil }
52
+ return Point(x: base.x + x, y: base.y + y)
53
+ }
54
+ var cubic: Point?
55
+ var quad: Point?
56
+ switch op | 0x20 {
57
+ case UInt8(ascii: "m"):
58
+ guard let to = point() else { return nil }
59
+ elements.append(.move(to))
60
+ current = to
61
+ start = to
62
+ // Pairs after a move are lines.
63
+ command = relative ? UInt8(ascii: "l") : UInt8(ascii: "L")
64
+ case UInt8(ascii: "l"):
65
+ guard let to = point() else { return nil }
66
+ elements.append(.line(to))
67
+ current = to
68
+ case UInt8(ascii: "h"):
69
+ guard let x = scanner.number() else { return nil }
70
+ current = Point(x: relative ? current.x + x : x, y: current.y)
71
+ elements.append(.line(current))
72
+ case UInt8(ascii: "v"):
73
+ guard let y = scanner.number() else { return nil }
74
+ current = Point(x: current.x, y: relative ? current.y + y : y)
75
+ elements.append(.line(current))
76
+ case UInt8(ascii: "c"):
77
+ guard let c1 = point(), let c2 = point(), let to = point() else { return nil }
78
+ elements.append(.curve(to: to, control1: c1, control2: c2))
79
+ current = to
80
+ cubic = c2
81
+ case UInt8(ascii: "s"):
82
+ guard let c2 = point(), let to = point() else { return nil }
83
+ let c1 = lastCubic.map { Point(x: 2 * current.x - $0.x, y: 2 * current.y - $0.y) } ?? current
84
+ elements.append(.curve(to: to, control1: c1, control2: c2))
85
+ current = to
86
+ cubic = c2
87
+ case UInt8(ascii: "q"):
88
+ guard let c = point(), let to = point() else { return nil }
89
+ elements.append(.quadCurve(to: to, control: c))
90
+ current = to
91
+ quad = c
92
+ case UInt8(ascii: "t"):
93
+ guard let to = point() else { return nil }
94
+ let c = lastQuad.map { Point(x: 2 * current.x - $0.x, y: 2 * current.y - $0.y) } ?? current
95
+ elements.append(.quadCurve(to: to, control: c))
96
+ current = to
97
+ quad = c
98
+ case UInt8(ascii: "a"):
99
+ guard let rx = scanner.number(), let ry = scanner.number(), let rotation = scanner.number(),
100
+ let large = scanner.flag(), let sweep = scanner.flag(), let to = point() else { return nil }
101
+ elements += arc(from: current, to: to, rx: rx, ry: ry, rotation: rotation, large: large, sweep: sweep)
102
+ current = to
103
+ case UInt8(ascii: "z"):
104
+ elements.append(.closeSubpath)
105
+ current = start
106
+ // A close takes no numbers: the next byte must be a command.
107
+ command = nil
108
+ default:
109
+ return nil
110
+ }
111
+ lastCubic = cubic
112
+ lastQuad = quad
113
+ }
114
+ return elements
115
+ }
116
+
117
+ /// An SVG arc (endpoint form) as cubic curves of a quarter turn or less
118
+ /// (SVG 1.1, F.6.5 and F.6.6). Equal endpoints draw nothing; a zero
119
+ /// radius is a line.
120
+ static func arc(from p0: Point, to p1: Point, rx rxIn: Double, ry ryIn: Double, rotation: Double, large: Bool, sweep: Bool) -> [Element] {
121
+ if p0 == p1 { return [] }
122
+ var rx = abs(rxIn)
123
+ var ry = abs(ryIn)
124
+ if rx == 0 || ry == 0 { return [.line(p1)] }
125
+ let phi = rotation * .pi / 180
126
+ let cosPhi = cos(phi)
127
+ let sinPhi = sin(phi)
128
+ let dx = (p0.x - p1.x) / 2
129
+ let dy = (p0.y - p1.y) / 2
130
+ let x1 = cosPhi * dx + sinPhi * dy
131
+ let y1 = -sinPhi * dx + cosPhi * dy
132
+ // Radii too small for the endpoints are scaled up until they fit.
133
+ let lambda = (x1 * x1) / (rx * rx) + (y1 * y1) / (ry * ry)
134
+ if lambda > 1 {
135
+ rx *= lambda.squareRoot()
136
+ ry *= lambda.squareRoot()
137
+ }
138
+ let numerator = rx * rx * ry * ry - rx * rx * y1 * y1 - ry * ry * x1 * x1
139
+ let denominator = rx * rx * y1 * y1 + ry * ry * x1 * x1
140
+ var factor = denominator == 0 ? 0 : (max(0, numerator) / denominator).squareRoot()
141
+ if large == sweep { factor = -factor }
142
+ let cx1 = factor * rx * y1 / ry
143
+ let cy1 = -factor * ry * x1 / rx
144
+ let cx = cosPhi * cx1 - sinPhi * cy1 + (p0.x + p1.x) / 2
145
+ let cy = sinPhi * cx1 + cosPhi * cy1 + (p0.y + p1.y) / 2
146
+ func angle(_ ux: Double, _ uy: Double, _ vx: Double, _ vy: Double) -> Double {
147
+ let sign: Double = ux * vy - uy * vx < 0 ? -1 : 1
148
+ let dot = ux * vx + uy * vy
149
+ let length = (ux * ux + uy * uy).squareRoot() * (vx * vx + vy * vy).squareRoot()
150
+ return sign * acos(min(1, max(-1, dot / length)))
151
+ }
152
+ let theta1 = angle(1, 0, (x1 - cx1) / rx, (y1 - cy1) / ry)
153
+ var delta = angle((x1 - cx1) / rx, (y1 - cy1) / ry, (-x1 - cx1) / rx, (-y1 - cy1) / ry)
154
+ if !sweep && delta > 0 { delta -= 2 * .pi }
155
+ if sweep && delta < 0 { delta += 2 * .pi }
156
+
157
+ let segments = max(1, Int((abs(delta) / (.pi / 2)).rounded(.up)))
158
+ let step = delta / Double(segments)
159
+ let k = 4.0 / 3.0 * tan(step / 4)
160
+ func onEllipse(_ t: Double) -> Point {
161
+ let x = rx * cos(t)
162
+ let y = ry * sin(t)
163
+ return Point(x: cx + cosPhi * x - sinPhi * y, y: cy + sinPhi * x + cosPhi * y)
164
+ }
165
+ func tangent(_ t: Double) -> Point {
166
+ let x = -rx * sin(t)
167
+ let y = ry * cos(t)
168
+ return Point(x: cosPhi * x - sinPhi * y, y: sinPhi * x + cosPhi * y)
169
+ }
170
+ var elements: [Element] = []
171
+ var t = theta1
172
+ var from = p0
173
+ for index in 0..<segments {
174
+ let next = t + step
175
+ // The last end is the endpoint itself, not its recomputation.
176
+ let to = index == segments - 1 ? p1 : onEllipse(next)
177
+ let d0 = tangent(t)
178
+ let d1 = tangent(next)
179
+ elements.append(.curve(
180
+ to: to,
181
+ control1: Point(x: from.x + k * d0.x, y: from.y + k * d0.y),
182
+ control2: Point(x: to.x - k * d1.x, y: to.y - k * d1.y)
183
+ ))
184
+ from = to
185
+ t = next
186
+ }
187
+ return elements
188
+ }
189
+ }
190
+
191
+ /// Numbers and flags out of path data's bytes.
192
+ private struct PathScanner {
193
+ let bytes: [UInt8]
194
+ var index = 0
195
+
196
+ init(_ bytes: [UInt8]) {
197
+ self.bytes = bytes
198
+ }
199
+
200
+ var peek: UInt8? { index < bytes.count ? bytes[index] : nil }
201
+
202
+ static func isCommand(_ byte: UInt8) -> Bool {
203
+ switch byte | 0x20 {
204
+ case UInt8(ascii: "m"), UInt8(ascii: "l"), UInt8(ascii: "h"), UInt8(ascii: "v"), UInt8(ascii: "c"), UInt8(ascii: "s"),
205
+ UInt8(ascii: "q"), UInt8(ascii: "t"), UInt8(ascii: "a"), UInt8(ascii: "z"):
206
+ return true
207
+ default:
208
+ return false
209
+ }
210
+ }
211
+
212
+ mutating func skipSeparators() {
213
+ while let byte = peek, byte == UInt8(ascii: " ") || byte == UInt8(ascii: ",") || byte == 0x0A || byte == 0x0D || byte == 0x09 {
214
+ index += 1
215
+ }
216
+ }
217
+
218
+ /// A number: sign, digits, a point, an exponent (`-1.5e-3`, `.5`; `1.5.5` is two).
219
+ mutating func number() -> Double? {
220
+ skipSeparators()
221
+ let begin = index
222
+ if let byte = peek, byte == UInt8(ascii: "-") || byte == UInt8(ascii: "+") { index += 1 }
223
+ var digits = false
224
+ var point = false
225
+ while let byte = peek {
226
+ if byte >= UInt8(ascii: "0") && byte <= UInt8(ascii: "9") {
227
+ digits = true
228
+ } else if byte == UInt8(ascii: "."), !point {
229
+ point = true
230
+ } else {
231
+ break
232
+ }
233
+ index += 1
234
+ }
235
+ guard digits else {
236
+ index = begin
237
+ return nil
238
+ }
239
+ if let byte = peek, byte == UInt8(ascii: "e") || byte == UInt8(ascii: "E") {
240
+ let mark = index
241
+ index += 1
242
+ if let sign = peek, sign == UInt8(ascii: "-") || sign == UInt8(ascii: "+") { index += 1 }
243
+ var exponent = false
244
+ while let byte = peek, byte >= UInt8(ascii: "0") && byte <= UInt8(ascii: "9") {
245
+ exponent = true
246
+ index += 1
247
+ }
248
+ if !exponent { index = mark }
249
+ }
250
+ return Double(String(decoding: bytes[begin..<index], as: UTF8.self))
251
+ }
252
+
253
+ /// An arc flag: one `0` or `1`, which may run into the next number (`a5 5 0 011 1`).
254
+ mutating func flag() -> Bool? {
255
+ skipSeparators()
256
+ guard let byte = peek, byte == UInt8(ascii: "0") || byte == UInt8(ascii: "1") else { return nil }
257
+ index += 1
258
+ return byte == UInt8(ascii: "1")
259
+ }
260
+ }
@@ -48,6 +48,8 @@ public struct Style: Equatable, Sendable {
48
48
  public var hidden: Bool
49
49
  public var mask: ShapeStyleSpec?
50
50
  public var hitTesting: Bool?
51
+ /// `.contentShape(Rectangle())`: `rectangle`.
52
+ public var contentShape: String?
51
53
  public var blendMode: String?
52
54
  public var zIndex: Double?
53
55
  public var blur: Double?
@@ -73,6 +75,8 @@ public struct Style: Equatable, Sendable {
73
75
  public var scrollID: Bool
74
76
  /// `proxy.scrollTo(x)`: the request (its serial is new each time) on the element it names.
75
77
  public var scrollTo: ScrollToSpec?
78
+ /// `.scrollTransition`: the effects in its three phases.
79
+ public var scrollTransition: ScrollTransitionSpec?
76
80
 
77
81
  public init(json: JSONValue) {
78
82
  let fields = json.object ?? [:]
@@ -121,6 +125,7 @@ public struct Style: Equatable, Sendable {
121
125
  hidden = fields.bool("hidden") ?? false
122
126
  mask = ShapeStyleSpec(json: fields.value("mask"))
123
127
  hitTesting = fields.bool("hitTesting")
128
+ contentShape = fields.string("contentShape")
124
129
  blendMode = fields.string("blendMode")
125
130
  zIndex = fields.double("zIndex")
126
131
  blur = fields.double("blur")
@@ -146,6 +151,7 @@ public struct Style: Equatable, Sendable {
146
151
  containerRelativeFrame = ContainerRelativeFrameSpec(json: fields.value("containerRelativeFrame"))
147
152
  scrollID = fields.bool("scrollID") ?? false
148
153
  scrollTo = ScrollToSpec(json: fields.value("scrollTo"))
154
+ scrollTransition = ScrollTransitionSpec(json: fields.value("scrollTransition"))
149
155
  }
150
156
 
151
157
  public init() {
@@ -187,3 +193,55 @@ public struct ScrollToSpec: Equatable, Sendable {
187
193
  axes = fields.string("axes") ?? "both"
188
194
  }
189
195
  }
196
+
197
+ /// `.scrollTransition(_:axis:transition:)` as the app sends it: its closure's
198
+ /// effects in each phase, drawn by SwiftUI's own as the view scrolls.
199
+ public struct ScrollTransitionSpec: Equatable, Sendable {
200
+ /// `interactive`, `animated` or `identity`.
201
+ public var configuration: String
202
+ /// `horizontal` or `vertical`; the scroll view's own axis when nil.
203
+ public var axis: String?
204
+ public var identity: EffectSpec
205
+ public var topLeading: EffectSpec
206
+ public var bottomTrailing: EffectSpec
207
+ public init?(json: JSONValue) {
208
+ guard let fields = json.object else { return nil }
209
+ configuration = fields.string("configuration") ?? "interactive"
210
+ axis = fields.string("axis")
211
+ identity = EffectSpec(json: fields.value("identity"))
212
+ topLeading = EffectSpec(json: fields.value("topLeading"))
213
+ bottomTrailing = EffectSpec(json: fields.value("bottomTrailing"))
214
+ }
215
+ }
216
+
217
+ /// The visual effects of one scroll-transition phase (the `Style` fields of
218
+ /// the same names); absent fields are the identity's.
219
+ public struct EffectSpec: Equatable, Sendable {
220
+ public var offset: PointSpec?
221
+ public var scale: PointSpec?
222
+ public var scaleAnchor: UnitPointSpec?
223
+ public var rotation: Double?
224
+ public var rotationAnchor: UnitPointSpec?
225
+ public var opacity: Double?
226
+ public var blur: Double?
227
+ public var brightness: Double?
228
+ public var contrast: Double?
229
+ public var saturation: Double?
230
+ public var grayscale: Double?
231
+ public var hueRotation: Double?
232
+ public init(json: JSONValue) {
233
+ let fields = json.object ?? [:]
234
+ offset = PointSpec(json: fields.value("offset"))
235
+ scale = PointSpec(json: fields.value("scale"))
236
+ scaleAnchor = UnitPointSpec(json: fields.value("scaleAnchor"))
237
+ rotation = fields.double("rotation")
238
+ rotationAnchor = UnitPointSpec(json: fields.value("rotationAnchor"))
239
+ opacity = fields.double("opacity")
240
+ blur = fields.double("blur")
241
+ brightness = fields.double("brightness")
242
+ contrast = fields.double("contrast")
243
+ saturation = fields.double("saturation")
244
+ grayscale = fields.double("grayscale")
245
+ hueRotation = fields.double("hueRotation")
246
+ }
247
+ }
@@ -35,6 +35,13 @@ public final class OpTree {
35
35
 
36
36
  /// Runs the body of a pass, animated as the commit asks (`withAnimation` on a SwiftUI host).
37
37
  public var animate: (AnimationSpec?, () -> Void) -> Void = { _, body in body() }
38
+ /// Runs the body of a pass whose commit wants a completion: the host calls
39
+ /// the last closure once the pass has finished animating (SwiftUI's
40
+ /// `withAnimation(_:completionCriteria:_:completion:)`), from any task;
41
+ /// the tree then sends `animationcomplete` on the main actor, a turn
42
+ /// later. Without a host hook the tree runs `animate` and sends it right
43
+ /// after the pass.
44
+ public var animateWithCompletion: ((AnimationSpec?, CommitCompletionSpec, () -> Void, @escaping @Sendable () -> Void) -> Void)?
38
45
  /// Called after every committed pass.
39
46
  public var onCommit: ((AnimationSpec?) -> Void)?
40
47
  /// Where the renderer's events go (the app's `dispatch`).
@@ -65,17 +72,30 @@ public final class OpTree {
65
72
  /// id, an insert into a missing parent); the ops before it have applied.
66
73
  public func apply(_ ops: [Op]) throws {
67
74
  for op in ops {
68
- if case .commit(let animation) = op {
75
+ if case .commit(let animation, let completion) = op {
69
76
  let pass = pending
70
77
  pending.removeAll()
71
78
  var failure: Error?
72
- animate(animation) {
79
+ let body = {
73
80
  do {
74
81
  for each in pass { try self.applyOne(each) }
75
82
  } catch {
76
83
  failure = error
77
84
  }
78
85
  }
86
+ if let completion {
87
+ if let animateWithCompletion {
88
+ let serial = completion.serial
89
+ animateWithCompletion(animation, completion, body) { [weak self] in
90
+ Task { @MainActor in self?.send(.animationComplete(serial: serial)) }
91
+ }
92
+ } else {
93
+ animate(animation, body)
94
+ send(.animationComplete(serial: completion.serial))
95
+ }
96
+ } else {
97
+ animate(animation, body)
98
+ }
79
99
  if let failure { throw failure }
80
100
  commits += 1
81
101
  onCommit?(animation)
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/wasm/SwiftBrowserNative.wasm
3
- size: 49658341 bytes
4
- sha256: 9aa09e4a1125c88786c07b9293dd9eec6264583717cd1f8227808596da2d911a
3
+ size: 49982707 bytes
4
+ sha256: 1f02220b4e3d7e74ce3afb42d5336ba9254bb1cd1cb591409c878548c4a6f6c4