@swiftbrowser/native 0.18.3 → 0.21.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.
- package/README.md +53 -11
- package/bin/swiftbrowser-native.mjs +47 -0
- package/dist/blob.d.ts +85 -11
- package/dist/blob.d.ts.map +1 -1
- package/dist/blob.js +184 -34
- package/dist/blob.js.map +1 -1
- package/dist/bridge.d.ts +66 -8
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js.map +1 -1
- package/dist/encoding.d.ts +13 -0
- package/dist/encoding.d.ts.map +1 -1
- package/dist/encoding.js +24 -0
- package/dist/encoding.js.map +1 -1
- package/dist/fetch.d.ts +27 -13
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +117 -45
- package/dist/fetch.js.map +1 -1
- package/dist/hostRequests.d.ts +24 -6
- package/dist/hostRequests.d.ts.map +1 -1
- package/dist/hostRequests.js +108 -15
- package/dist/hostRequests.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/ios.d.ts +79 -0
- package/dist/ios.d.ts.map +1 -0
- package/dist/ios.js +181 -0
- package/dist/ios.js.map +1 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +16 -7
- package/dist/runtime.js.map +1 -1
- package/dist/subtle.d.ts +1 -1
- package/dist/subtle.d.ts.map +1 -1
- package/dist/subtle.js +9 -5
- package/dist/subtle.js.map +1 -1
- package/dist/webSocket.d.ts +12 -5
- package/dist/webSocket.d.ts.map +1 -1
- package/dist/webSocket.js +37 -11
- package/dist/webSocket.js.map +1 -1
- package/package.json +7 -3
- package/src/blob.ts +215 -34
- package/src/bridge.ts +56 -8
- package/src/encoding.ts +22 -0
- package/src/fetch.ts +111 -48
- package/src/hostRequests.ts +129 -22
- package/src/index.ts +1 -2
- package/src/ios.ts +253 -0
- package/src/runtime.ts +23 -8
- package/src/subtle.ts +10 -6
- package/src/webSocket.ts +37 -13
- package/swift/Packages/SwiftBrowserDSP/Package.swift +31 -0
- package/swift/Packages/SwiftBrowserDSP/README.md +42 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ProgramNode.swift +306 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ScopeView.swift +79 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +1561 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Program.swift +483 -0
- package/swift/Packages/SwiftBrowserLocalModel/Package.swift +35 -0
- package/swift/Packages/SwiftBrowserLocalModel/README.md +66 -0
- package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LlamaEngine.swift +223 -0
- package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LocalLanguageModel.swift +409 -0
- package/swift/Packages/SwiftBrowserShader/Package.swift +31 -0
- package/swift/Packages/SwiftBrowserShader/README.md +45 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderRenderer.swift +103 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderView.swift +168 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShaderCore/ShaderProps.swift +120 -0
- package/swift/Sources/SwiftBrowserNative/Host/AudioEngineBridge.swift +921 -0
- package/swift/Sources/SwiftBrowserNative/Host/AudioEngineNodes.swift +70 -0
- package/swift/Sources/SwiftBrowserNative/Host/BodyStream.swift +166 -0
- package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +144 -10
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +151 -27
- package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +139 -40
- package/swift/Sources/SwiftBrowserNative/Host/JSHost.swift +116 -0
- package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +141 -2
- package/swift/Sources/SwiftBrowserNative/Host/LanguageModelBridge.swift +511 -0
- package/swift/Sources/SwiftBrowserNative/Host/LanguageModelProvider.swift +195 -0
- package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +24 -11
- package/swift/Sources/SwiftBrowserNative/Host/WorkerHost.swift +3 -2
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeFunctions.swift +1 -1
- package/swift/Sources/SwiftBrowserNative/Runtime/ViewRegistry.swift +57 -0
- package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +12 -7
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +18 -1
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Drawing.swift +378 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Lists.swift +41 -1
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +88 -17
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +87 -21
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/PinchTracker.swift +200 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/PopTap.swift +103 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +3 -2
- package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +146 -0
- package/swift/Sources/SwiftBrowserProtocol/Drawing.swift +306 -0
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +31 -2
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +24 -3
- package/swift/Sources/SwiftBrowserProtocol/Style.swift +87 -0
- package/swift/Sources/SwiftBrowserTree/OpTree.swift +25 -0
- package/wasm/SwiftBrowserNative.wasm +2 -2
- package/dist/native.d.ts +0 -19
- package/dist/native.d.ts.map +0 -1
- package/dist/native.js +0 -17
- package/dist/native.js.map +0 -1
- package/src/native.ts +0 -35
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeRegistry.swift +0 -53
|
@@ -53,8 +53,14 @@ extension View {
|
|
|
53
53
|
if let tint = style.tint { view = AnyView(view.tint(Color(spec: tint))) }
|
|
54
54
|
// `.scrollDisabled`: the scroll views and text editors below (a text editor then as tall as its lines).
|
|
55
55
|
if let disabled = style.raw.bool("scrollDisabled") { view = AnyView(view.scrollDisabled(disabled)) }
|
|
56
|
+
// `.textSelection(.enabled)`: the text below can be selected.
|
|
57
|
+
if let selectable = style.raw.bool("textSelection") {
|
|
58
|
+
view = selectable ? AnyView(view.textSelection(.enabled)) : AnyView(view.textSelection(.disabled))
|
|
59
|
+
}
|
|
56
60
|
// `labelIcon` marks a `Label`'s icon; HStackElement draws the label as one.
|
|
57
61
|
if let alignment = style.raw.string("gridColumnAlignment") { view = AnyView(view.gridColumnAlignment(HorizontalAlignment(name: alignment))) }
|
|
62
|
+
if let columns = style.gridCellColumns { view = AnyView(view.gridCellColumns(columns)) }
|
|
63
|
+
if let anchor = style.gridCellAnchor { view = AnyView(view.gridCellAnchor(UnitPoint(spec: anchor))) }
|
|
58
64
|
|
|
59
65
|
// Layout.
|
|
60
66
|
if style.fixedSizeHorizontal != nil || style.fixedSizeVertical != nil {
|
|
@@ -103,6 +109,7 @@ extension View {
|
|
|
103
109
|
view = AnyView(view.aspectRatio(ratio.map { CGFloat($0) }, contentMode: mode))
|
|
104
110
|
}
|
|
105
111
|
if let position = style.position { view = AnyView(view.position(x: position.x, y: position.y)) }
|
|
112
|
+
if let guide = style.alignmentGuide { view = view.alignmentGuide(spec: guide) }
|
|
106
113
|
|
|
107
114
|
// Paint.
|
|
108
115
|
if let background = style.background {
|
|
@@ -115,6 +122,7 @@ extension View {
|
|
|
115
122
|
if let radius = style.cornerRadius { view = AnyView(view.clipShape(RoundedRectangle(cornerRadius: radius, style: .continuous))) }
|
|
116
123
|
if style.clipped { view = AnyView(view.clipped()) }
|
|
117
124
|
if let mask = style.mask { view = AnyView(view.mask { Rectangle().fill(AnyShapeStyle(spec: mask)) }) }
|
|
125
|
+
if style.compositingGroup { view = AnyView(view.compositingGroup()) }
|
|
118
126
|
if let shadow = style.shadow {
|
|
119
127
|
view = AnyView(view.shadow(color: shadow.color.map(Color.init(spec:)) ?? Color.black.opacity(0.33), radius: shadow.radius, x: shadow.x, y: shadow.y))
|
|
120
128
|
}
|
|
@@ -133,6 +141,12 @@ extension View {
|
|
|
133
141
|
if let rotation = style.rotation {
|
|
134
142
|
view = AnyView(view.rotationEffect(.degrees(rotation), anchor: style.rotationAnchor.map(UnitPoint.init(spec:)) ?? .center))
|
|
135
143
|
}
|
|
144
|
+
if let rotation = style.rotation3D {
|
|
145
|
+
view = AnyView(view.rotation3DEffect(
|
|
146
|
+
.degrees(rotation.angle), axis: (x: rotation.x, y: rotation.y, z: rotation.z),
|
|
147
|
+
anchor: rotation.anchor.map(UnitPoint.init(spec:)) ?? .center, perspective: rotation.perspective
|
|
148
|
+
))
|
|
149
|
+
}
|
|
136
150
|
if let scale = style.scale {
|
|
137
151
|
view = AnyView(view.scaleEffect(x: scale.x, y: scale.y, anchor: style.scaleAnchor.map(UnitPoint.init(spec:)) ?? .center))
|
|
138
152
|
}
|
|
@@ -146,6 +160,11 @@ extension View {
|
|
|
146
160
|
if style.contentShape == "rectangle" { view = AnyView(view.contentShape(Rectangle())) }
|
|
147
161
|
if style.hidden { view = AnyView(view.hidden()) }
|
|
148
162
|
if let help = style.help { view = AnyView(view.help(help)) }
|
|
163
|
+
if let pointer = style.pointerStyle { view = view.pointerStyle(name: pointer) }
|
|
164
|
+
if let effect = style.symbolEffect { view = AnyView(view.modifier(SymbolEffectModifier(spec: effect))) }
|
|
165
|
+
// The events a box sends of its own.
|
|
166
|
+
if let id, style.hover { view = AnyView(view.modifier(HoverReport(id: id))) }
|
|
167
|
+
if let id, let threshold = style.scrollVisibilityThreshold { view = AnyView(view.modifier(ScrollVisibilityReport(id: id, threshold: threshold))) }
|
|
149
168
|
// A `ScrollViewReader`'s `.id(x)`: the element's own id names it to a `scrollTo` and a scroll position.
|
|
150
169
|
if style.scrollID, let id { view = AnyView(view.id(id)) }
|
|
151
170
|
if let width = style.raw.double("inspectorColumnWidth") { view = AnyView(view.inspectorColumnWidth(width)) }
|
|
@@ -176,6 +195,133 @@ extension View {
|
|
|
176
195
|
}
|
|
177
196
|
}
|
|
178
197
|
|
|
198
|
+
extension View {
|
|
199
|
+
/// `.alignmentGuide`: the guide by name on its axis, at `a + b·width + c·height` of the view.
|
|
200
|
+
func alignmentGuide(spec: AlignmentGuideSpec) -> AnyView {
|
|
201
|
+
let (a, b, c) = (spec.a, spec.b, spec.c)
|
|
202
|
+
if spec.axis == "vertical" {
|
|
203
|
+
return AnyView(alignmentGuide(VerticalAlignment(name: spec.name)) { d in a + b * d.width + c * d.height })
|
|
204
|
+
}
|
|
205
|
+
return AnyView(alignmentGuide(HorizontalAlignment(name: spec.name)) { d in a + b * d.width + c * d.height })
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// `.pointerStyle`: the Mac's pointer by its CSS name (macOS 15). iOS has no pointer styles of
|
|
209
|
+
/// this kind (an iPad's pointer takes the shape of what it hovers), so the view is as it was.
|
|
210
|
+
func pointerStyle(name: String) -> AnyView {
|
|
211
|
+
#if os(macOS) && !canImport(FoundationBridge)
|
|
212
|
+
if #available(macOS 15, *) {
|
|
213
|
+
let style: PointerStyle = switch name {
|
|
214
|
+
case "pointer", "link": .link
|
|
215
|
+
case "text": .horizontalText
|
|
216
|
+
case "vertical-text", "verticalText": .verticalText
|
|
217
|
+
case "grab", "grabIdle": .grabIdle
|
|
218
|
+
case "grabbing", "grabActive": .grabActive
|
|
219
|
+
case "zoom-in", "zoomIn": .zoomIn
|
|
220
|
+
case "zoom-out", "zoomOut": .zoomOut
|
|
221
|
+
case "crosshair", "rectSelection": .rectSelection
|
|
222
|
+
default: .default
|
|
223
|
+
}
|
|
224
|
+
return AnyView(self.pointerStyle(style))
|
|
225
|
+
}
|
|
226
|
+
#endif
|
|
227
|
+
return AnyView(self)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/// `.onHover`: the box's `hover` events, as a pointer (an iPad's, a Mac's) enters and leaves it.
|
|
232
|
+
struct HoverReport: ViewModifier {
|
|
233
|
+
let id: Int
|
|
234
|
+
@Environment(\.nativeApp) private var app
|
|
235
|
+
|
|
236
|
+
func body(content: Content) -> some View {
|
|
237
|
+
content.onHover { hovering in app?.dispatch(.hover(id, hovering)) }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// `.onScrollVisibilityChange(threshold:)` (iOS 18, macOS 15): the box's `scrollvisibility` as it
|
|
242
|
+
/// crosses the threshold of its scroll view's visible area; earlier systems send none.
|
|
243
|
+
struct ScrollVisibilityReport: ViewModifier {
|
|
244
|
+
let id: Int
|
|
245
|
+
let threshold: Double
|
|
246
|
+
@Environment(\.nativeApp) private var app
|
|
247
|
+
|
|
248
|
+
func body(content: Content) -> some View {
|
|
249
|
+
if #available(iOS 18, macOS 15, *) {
|
|
250
|
+
content.onScrollVisibilityChange(threshold: threshold) { visible in app?.dispatch(.scrollVisibility(id, visible: visible)) }
|
|
251
|
+
} else {
|
|
252
|
+
content
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// `.symbolEffect`: SF Symbols' own effect, on the symbols inside. With a `trigger` it plays once each
|
|
258
|
+
/// time the trigger changes; without, it runs while the style has it. `rotate`, `wiggle` and `breathe`,
|
|
259
|
+
/// and a `bounce` that runs on, need iOS 18 / macOS 15; before, the symbol is still.
|
|
260
|
+
struct SymbolEffectModifier: ViewModifier {
|
|
261
|
+
let spec: SymbolEffectSpec
|
|
262
|
+
|
|
263
|
+
private var options: SymbolEffectOptions {
|
|
264
|
+
var options: SymbolEffectOptions = .default
|
|
265
|
+
if let speed = spec.speed { options = options.speed(speed) }
|
|
266
|
+
if let count = spec.repeatCount { options = options.repeat(count) } else if spec.forever { options = options.repeating }
|
|
267
|
+
return options
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private var up: Bool { spec.direction != "down" }
|
|
271
|
+
|
|
272
|
+
func body(content: Content) -> some View {
|
|
273
|
+
if let trigger = spec.trigger {
|
|
274
|
+
discrete(content, trigger: trigger)
|
|
275
|
+
} else {
|
|
276
|
+
running(content)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@ViewBuilder
|
|
281
|
+
private func discrete(_ content: Content, trigger: Double) -> some View {
|
|
282
|
+
switch spec.effect {
|
|
283
|
+
case "bounce": content.symbolEffect(up ? .bounce.up : .bounce.down, options: options, value: trigger)
|
|
284
|
+
case "pulse": content.symbolEffect(.pulse, options: options, value: trigger)
|
|
285
|
+
case "variableColor": content.symbolEffect(.variableColor, options: options, value: trigger)
|
|
286
|
+
case "rotate", "wiggle", "breathe":
|
|
287
|
+
if #available(iOS 18, macOS 15, *) {
|
|
288
|
+
switch spec.effect {
|
|
289
|
+
case "rotate": content.symbolEffect(.rotate, options: options, value: trigger)
|
|
290
|
+
case "wiggle": content.symbolEffect(.wiggle, options: options, value: trigger)
|
|
291
|
+
default: content.symbolEffect(.breathe, options: options, value: trigger)
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
content
|
|
295
|
+
}
|
|
296
|
+
// Scale, appear and disappear only run on: while the style has them.
|
|
297
|
+
default: running(content)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@ViewBuilder
|
|
302
|
+
private func running(_ content: Content) -> some View {
|
|
303
|
+
switch spec.effect {
|
|
304
|
+
case "pulse": content.symbolEffect(.pulse, options: options, isActive: true)
|
|
305
|
+
case "variableColor": content.symbolEffect(.variableColor, options: options, isActive: true)
|
|
306
|
+
case "scale": content.symbolEffect(up ? .scale.up : .scale.down, options: options, isActive: true)
|
|
307
|
+
case "appear": content.symbolEffect(.appear, options: options, isActive: true)
|
|
308
|
+
case "disappear": content.symbolEffect(.disappear, options: options, isActive: true)
|
|
309
|
+
case "bounce", "rotate", "wiggle", "breathe":
|
|
310
|
+
if #available(iOS 18, macOS 15, *) {
|
|
311
|
+
switch spec.effect {
|
|
312
|
+
case "bounce": content.symbolEffect(up ? .bounce.up : .bounce.down, options: options, isActive: true)
|
|
313
|
+
case "rotate": content.symbolEffect(.rotate, options: options, isActive: true)
|
|
314
|
+
case "wiggle": content.symbolEffect(.wiggle, options: options, isActive: true)
|
|
315
|
+
default: content.symbolEffect(.breathe, options: options, isActive: true)
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
content
|
|
319
|
+
}
|
|
320
|
+
default: content
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
179
325
|
extension View {
|
|
180
326
|
/// `.scrollTransition`: SwiftUI's own, its closure answering each phase
|
|
181
327
|
/// with the app's effects for it (SwiftUI draws between them as the view
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
// The props and event values of the kinds that draw or lay out on the app's
|
|
4
|
+
// say (docs/ops-protocol.md): a custom `layout`, a `canvas`'s commands, and
|
|
5
|
+
// the drag and drop pair.
|
|
6
|
+
|
|
7
|
+
// MARK: - layout (a custom `Layout`)
|
|
8
|
+
|
|
9
|
+
/// One axis of a proposal: points, unbounded (`"infinity"`), or unspecified (nil).
|
|
10
|
+
public enum LayoutDimensionSpec: Equatable, Sendable {
|
|
11
|
+
case points(Double)
|
|
12
|
+
case infinity
|
|
13
|
+
|
|
14
|
+
public init?(json: JSONValue) {
|
|
15
|
+
if let value = json.double { self = .points(value) } else if json.string == "infinity" { self = .infinity } else { return nil }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// From a length: unbounded when not finite, else to two decimals as the protocol carries it.
|
|
19
|
+
public init(_ value: Double) {
|
|
20
|
+
self = value.isFinite ? .points((value * 100).rounded() / 100) : .infinity
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public var value: Double {
|
|
24
|
+
switch self {
|
|
25
|
+
case .points(let points): points
|
|
26
|
+
case .infinity: .infinity
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public var json: JSONValue {
|
|
31
|
+
switch self {
|
|
32
|
+
case .points(let points): .number(points)
|
|
33
|
+
case .infinity: .string("infinity")
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// A proposal as Swift's `ProposedViewSize`: nil for an unspecified axis.
|
|
39
|
+
public struct LayoutProposalSpec: Equatable, Sendable {
|
|
40
|
+
public var width: LayoutDimensionSpec?
|
|
41
|
+
public var height: LayoutDimensionSpec?
|
|
42
|
+
|
|
43
|
+
public init(width: LayoutDimensionSpec?, height: LayoutDimensionSpec?) {
|
|
44
|
+
self.width = width
|
|
45
|
+
self.height = height
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public init?(json: JSONValue) {
|
|
49
|
+
guard let fields = json.object else { return nil }
|
|
50
|
+
width = LayoutDimensionSpec(json: fields.value("width"))
|
|
51
|
+
height = LayoutDimensionSpec(json: fields.value("height"))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public var json: JSONValue {
|
|
55
|
+
.object(["width": width?.json ?? .null, "height": height?.json ?? .null])
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// A size whose axes may be unbounded.
|
|
60
|
+
public struct LayoutSizeSpec: Equatable, Sendable {
|
|
61
|
+
public var width: LayoutDimensionSpec
|
|
62
|
+
public var height: LayoutDimensionSpec
|
|
63
|
+
|
|
64
|
+
public init(width: Double, height: Double) {
|
|
65
|
+
self.width = LayoutDimensionSpec(width)
|
|
66
|
+
self.height = LayoutDimensionSpec(height)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public init?(json: JSONValue) {
|
|
70
|
+
guard let width = LayoutDimensionSpec(json: json["width"]), let height = LayoutDimensionSpec(json: json["height"]) else { return nil }
|
|
71
|
+
self.width = width
|
|
72
|
+
self.height = height
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public var json: JSONValue { .object(["width": width.json, "height": height.json]) }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// A subview's sizes for a zero, unspecified and unbounded proposal: what the app's layout measures it by.
|
|
79
|
+
public struct LayoutMeasurementSpec: Equatable, Sendable {
|
|
80
|
+
public var min: LayoutSizeSpec
|
|
81
|
+
public var ideal: LayoutSizeSpec
|
|
82
|
+
public var max: LayoutSizeSpec
|
|
83
|
+
|
|
84
|
+
public init(min: LayoutSizeSpec, ideal: LayoutSizeSpec, max: LayoutSizeSpec) {
|
|
85
|
+
self.min = min
|
|
86
|
+
self.ideal = ideal
|
|
87
|
+
self.max = max
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public var json: JSONValue { .object(["min": min.json, "ideal": ideal.json, "max": max.json]) }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// `layout`: the app's answer for `proposal` (nil until its first): its size, and one frame per child
|
|
94
|
+
/// relative to its origin; `sizes`, its own size for a zero, unspecified and unbounded proposal.
|
|
95
|
+
public struct LayoutProps: Equatable, Sendable {
|
|
96
|
+
public struct Frame: Equatable, Sendable {
|
|
97
|
+
public var x: Double
|
|
98
|
+
public var y: Double
|
|
99
|
+
public var width: Double
|
|
100
|
+
public var height: Double
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public var proposal: LayoutProposalSpec?
|
|
104
|
+
public var size: SizeSpec?
|
|
105
|
+
public var frames: [Frame]?
|
|
106
|
+
public var min: LayoutSizeSpec?
|
|
107
|
+
public var ideal: LayoutSizeSpec?
|
|
108
|
+
public var max: LayoutSizeSpec?
|
|
109
|
+
|
|
110
|
+
public init(props: JSONObject) {
|
|
111
|
+
proposal = LayoutProposalSpec(json: props.value("proposal"))
|
|
112
|
+
size = SizeSpec(json: props.value("size"))
|
|
113
|
+
frames = props.array("frames")?.compactMap { json -> Frame? in
|
|
114
|
+
guard let fields = json.object else { return nil }
|
|
115
|
+
return Frame(x: fields.double("x") ?? 0, y: fields.double("y") ?? 0, width: fields.double("width") ?? 0, height: fields.double("height") ?? 0)
|
|
116
|
+
}
|
|
117
|
+
let sizes = props.object("sizes")
|
|
118
|
+
min = sizes.flatMap { LayoutSizeSpec(json: $0.value("min")) }
|
|
119
|
+
ideal = sizes.flatMap { LayoutSizeSpec(json: $0.value("ideal")) }
|
|
120
|
+
max = sizes.flatMap { LayoutSizeSpec(json: $0.value("max")) }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// MARK: - canvas
|
|
125
|
+
|
|
126
|
+
/// What a `canvas` command paints with: a view's shape style, or a gradient in the drawing's own points.
|
|
127
|
+
public enum CanvasShadingSpec: Equatable, Sendable {
|
|
128
|
+
case style(ShapeStyleSpec)
|
|
129
|
+
case linearGradient(stops: [GradientStopSpec], start: PointSpec, end: PointSpec)
|
|
130
|
+
case radialGradient(stops: [GradientStopSpec], center: PointSpec, startRadius: Double, endRadius: Double)
|
|
131
|
+
|
|
132
|
+
public init?(json: JSONValue) {
|
|
133
|
+
if let gradient = json["canvasLinearGradient"].object,
|
|
134
|
+
let start = PointSpec(json: gradient.value("start")), let end = PointSpec(json: gradient.value("end"))
|
|
135
|
+
{
|
|
136
|
+
self = .linearGradient(stops: (gradient.array("stops") ?? []).compactMap(GradientStopSpec.init(json:)), start: start, end: end)
|
|
137
|
+
} else if let gradient = json["canvasRadialGradient"].object, let center = PointSpec(json: gradient.value("center")) {
|
|
138
|
+
self = .radialGradient(
|
|
139
|
+
stops: (gradient.array("stops") ?? []).compactMap(GradientStopSpec.init(json:)), center: center,
|
|
140
|
+
startRadius: gradient.double("startRadius") ?? 0, endRadius: gradient.double("endRadius") ?? 0
|
|
141
|
+
)
|
|
142
|
+
} else if let style = ShapeStyleSpec(json: json) {
|
|
143
|
+
self = .style(style)
|
|
144
|
+
} else {
|
|
145
|
+
return nil
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// A `GraphicsContext.Filter`.
|
|
151
|
+
public enum CanvasFilterSpec: Equatable, Sendable {
|
|
152
|
+
case blur(Double)
|
|
153
|
+
case shadow(color: ColorSpec?, radius: Double, x: Double, y: Double)
|
|
154
|
+
case grayscale(Double)
|
|
155
|
+
case saturation(Double)
|
|
156
|
+
case brightness(Double)
|
|
157
|
+
case contrast(Double)
|
|
158
|
+
case hueRotation(Double)
|
|
159
|
+
case invert(Double)
|
|
160
|
+
|
|
161
|
+
public init?(json: JSONValue) {
|
|
162
|
+
guard let fields = json.object else { return nil }
|
|
163
|
+
if let radius = fields.double("blur") {
|
|
164
|
+
self = .blur(radius)
|
|
165
|
+
} else if let shadow = fields.object("shadow") {
|
|
166
|
+
self = .shadow(color: ColorSpec(json: shadow.value("color")), radius: shadow.double("radius") ?? 0, x: shadow.double("x") ?? 0, y: shadow.double("y") ?? 0)
|
|
167
|
+
} else if let amount = fields.double("grayscale") {
|
|
168
|
+
self = .grayscale(amount)
|
|
169
|
+
} else if let amount = fields.double("saturation") {
|
|
170
|
+
self = .saturation(amount)
|
|
171
|
+
} else if let amount = fields.double("brightness") {
|
|
172
|
+
self = .brightness(amount)
|
|
173
|
+
} else if let amount = fields.double("contrast") {
|
|
174
|
+
self = .contrast(amount)
|
|
175
|
+
} else if let degrees = fields.double("hueRotation") {
|
|
176
|
+
self = .hueRotation(degrees)
|
|
177
|
+
} else if let amount = fields.double("invert") {
|
|
178
|
+
self = .invert(amount)
|
|
179
|
+
} else {
|
|
180
|
+
return nil
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// One thing a `canvas` drew, with the context's state at the time: paths are SVG path data in the drawing's
|
|
186
|
+
/// coordinates (before `transform`), clips in the canvas's own.
|
|
187
|
+
public struct CanvasCommand: Equatable, Sendable {
|
|
188
|
+
public enum Operation: Equatable, Sendable {
|
|
189
|
+
case fill(path: String, shading: CanvasShadingSpec, evenOdd: Bool)
|
|
190
|
+
case stroke(path: String, shading: CanvasShadingSpec, style: StrokeStyleSpec)
|
|
191
|
+
case image(ImageProps, x: Double, y: Double, anchor: UnitPointSpec, width: Double?, height: Double?, shading: CanvasShadingSpec?)
|
|
192
|
+
case text(String, x: Double, y: Double, anchor: UnitPointSpec, shading: CanvasShadingSpec, font: FontSpec?)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// `lineWidth`, the caps and joins by name, the dash pattern.
|
|
196
|
+
public struct StrokeStyleSpec: Equatable, Sendable {
|
|
197
|
+
public var lineWidth: Double
|
|
198
|
+
public var lineCap: String?
|
|
199
|
+
public var lineJoin: String?
|
|
200
|
+
public var miterLimit: Double?
|
|
201
|
+
public var dash: [Double]
|
|
202
|
+
public var dashPhase: Double
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
public var operation: Operation
|
|
206
|
+
/// `[a, b, c, d, tx, ty]`; nil for the identity.
|
|
207
|
+
public var transform: [Double]?
|
|
208
|
+
public var opacity: Double
|
|
209
|
+
public var blendMode: String?
|
|
210
|
+
public var clips: [String]
|
|
211
|
+
public var filters: [CanvasFilterSpec]
|
|
212
|
+
|
|
213
|
+
public init?(json: JSONValue) {
|
|
214
|
+
guard let fields = json.object, let op = fields.string("op") else { return nil }
|
|
215
|
+
let anchor = UnitPointSpec(json: fields.value("anchor")) ?? UnitPointSpec(x: 0.5, y: 0.5)
|
|
216
|
+
switch op {
|
|
217
|
+
case "fill":
|
|
218
|
+
guard let path = fields.string("path"), let shading = CanvasShadingSpec(json: fields.value("shading")) else { return nil }
|
|
219
|
+
operation = .fill(path: path, shading: shading, evenOdd: fields.bool("evenOdd") ?? false)
|
|
220
|
+
case "stroke":
|
|
221
|
+
guard let path = fields.string("path"), let shading = CanvasShadingSpec(json: fields.value("shading")) else { return nil }
|
|
222
|
+
operation = .stroke(path: path, shading: shading, style: StrokeStyleSpec(
|
|
223
|
+
lineWidth: fields.double("lineWidth") ?? 1, lineCap: fields.string("lineCap"), lineJoin: fields.string("lineJoin"),
|
|
224
|
+
miterLimit: fields.double("miterLimit"), dash: (fields.array("dash") ?? []).compactMap(\.double), dashPhase: fields.double("dashPhase") ?? 0
|
|
225
|
+
))
|
|
226
|
+
case "image":
|
|
227
|
+
operation = .image(
|
|
228
|
+
ImageProps(props: fields.object("image") ?? [:]), x: fields.double("x") ?? 0, y: fields.double("y") ?? 0, anchor: anchor,
|
|
229
|
+
width: fields.double("width"), height: fields.double("height"), shading: CanvasShadingSpec(json: fields.value("shading"))
|
|
230
|
+
)
|
|
231
|
+
case "text":
|
|
232
|
+
guard let text = fields.string("text") else { return nil }
|
|
233
|
+
operation = .text(
|
|
234
|
+
text, x: fields.double("x") ?? 0, y: fields.double("y") ?? 0, anchor: anchor,
|
|
235
|
+
shading: CanvasShadingSpec(json: fields.value("shading")) ?? .style(.hierarchical(level: 1, opacity: 1)), font: FontSpec(json: fields.value("font"))
|
|
236
|
+
)
|
|
237
|
+
default:
|
|
238
|
+
return nil
|
|
239
|
+
}
|
|
240
|
+
let matrix = (fields.array("transform") ?? []).compactMap(\.double)
|
|
241
|
+
transform = matrix.count == 6 ? matrix : nil
|
|
242
|
+
opacity = fields.double("opacity") ?? 1
|
|
243
|
+
blendMode = fields.string("blendMode")
|
|
244
|
+
clips = (fields.array("clips") ?? []).compactMap(\.string)
|
|
245
|
+
filters = (fields.array("filters") ?? []).compactMap(CanvasFilterSpec.init(json:))
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// `canvas`: the commands the app's renderer drew for `width` × `height`, the size it was told.
|
|
250
|
+
public struct CanvasProps: Equatable, Sendable {
|
|
251
|
+
public var width: Double
|
|
252
|
+
public var height: Double
|
|
253
|
+
public var commands: [CanvasCommand]
|
|
254
|
+
|
|
255
|
+
public init(props: JSONObject) {
|
|
256
|
+
width = props.double("width") ?? 0
|
|
257
|
+
height = props.double("height") ?? 0
|
|
258
|
+
commands = (props.array("commands") ?? []).compactMap(CanvasCommand.init(json:))
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// MARK: - drag and drop
|
|
263
|
+
|
|
264
|
+
/// `dropdestination`: the MIME patterns (`text/plain`, `image/*`, `*/*`) it takes from outside the app.
|
|
265
|
+
public struct DropDestinationProps: Equatable, Sendable {
|
|
266
|
+
public var accepts: [String]
|
|
267
|
+
|
|
268
|
+
public init(props: JSONObject) {
|
|
269
|
+
accepts = (props.array("accepts") ?? []).compactMap(\.string)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// Whether text dragged in from outside the app is taken.
|
|
273
|
+
public var acceptsText: Bool {
|
|
274
|
+
accepts.contains { $0 == "*/*" || $0.hasPrefix("text/") }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// Whether files (or folders) dragged in from another app may be taken: any pattern beyond text.
|
|
278
|
+
public var acceptsFiles: Bool {
|
|
279
|
+
accepts.contains { $0 == "*/*" || !$0.hasPrefix("text/") }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// Whether a file of the MIME type is taken, as the page's `acceptsType` decides (src/dragDrop.ts).
|
|
283
|
+
public func accepts(mimeType: String) -> Bool {
|
|
284
|
+
let mime = mimeType.split(separator: ";").first.map { $0.trimmingCharacters(in: .whitespaces).lowercased() } ?? ""
|
|
285
|
+
let type = mime.isEmpty ? "application/octet-stream" : mime
|
|
286
|
+
return accepts.contains { pattern in
|
|
287
|
+
let p = pattern.lowercased()
|
|
288
|
+
if p == "*/*" { return true }
|
|
289
|
+
if p.hasSuffix("/*") { return type.hasPrefix(String(p.dropLast())) }
|
|
290
|
+
return type == p
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/// What a `draggable` carries natively: a marker and the element's id, as text, so a drop names its source.
|
|
296
|
+
public enum DragPayload {
|
|
297
|
+
private static let prefix = "\u{2063}swiftbrowser-drag:"
|
|
298
|
+
|
|
299
|
+
public static func text(for source: Int) -> String { prefix + String(source) }
|
|
300
|
+
|
|
301
|
+
/// The `draggable` a dropped text names, if it is one of these.
|
|
302
|
+
public static func source(of text: String) -> Int? {
|
|
303
|
+
guard text.hasPrefix(prefix) else { return nil }
|
|
304
|
+
return Int(text.dropFirst(prefix.count))
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -132,6 +132,8 @@ public struct ScrollViewProps: Equatable, Sendable {
|
|
|
132
132
|
public var scrollOffset: ScrollOffsetSpec?
|
|
133
133
|
/// `.defaultScrollAnchor(_:)`: where the content starts, and where it stays as it grows.
|
|
134
134
|
public var defaultScrollAnchor: UnitPointSpec?
|
|
135
|
+
/// `.scrollDismissesKeyboard(_:)`: `immediately` or `interactively`; nil leaves the keyboard be.
|
|
136
|
+
public var dismissesKeyboard: String?
|
|
135
137
|
public init(props: JSONObject) {
|
|
136
138
|
axes = props.string("axes") ?? "vertical"
|
|
137
139
|
showsIndicators = props.bool("showsIndicators") ?? true
|
|
@@ -142,6 +144,7 @@ public struct ScrollViewProps: Equatable, Sendable {
|
|
|
142
144
|
reportsGeometry = props.bool("reportsGeometry") ?? false
|
|
143
145
|
scrollOffset = ScrollOffsetSpec(json: props.value("scrollOffset"))
|
|
144
146
|
defaultScrollAnchor = UnitPointSpec(json: props.value("defaultScrollAnchor"))
|
|
147
|
+
dismissesKeyboard = props.bool("dismissesKeyboard") == true ? "immediately" : props.string("dismissesKeyboard")
|
|
145
148
|
}
|
|
146
149
|
}
|
|
147
150
|
|
|
@@ -183,9 +186,22 @@ public struct ListProps: Equatable, Sendable {
|
|
|
183
186
|
public struct SectionProps: Equatable, Sendable {
|
|
184
187
|
public var header: String?
|
|
185
188
|
public var footer: String?
|
|
189
|
+
/// `Section(isExpanded:)`: the section collapses (in a sidebar-style list); a `tap` toggles it.
|
|
190
|
+
public var collapsible: Bool
|
|
191
|
+
public var expanded: Bool
|
|
186
192
|
public init(props: JSONObject) {
|
|
187
193
|
header = props.string("header")
|
|
188
194
|
footer = props.string("footer")
|
|
195
|
+
collapsible = props.bool("collapsible") ?? false
|
|
196
|
+
expanded = props.bool("expanded") ?? true
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// `disclosuregroup`: open or not; its children are the label (first) and the content; a `tap` toggles it.
|
|
201
|
+
public struct DisclosureGroupProps: Equatable, Sendable {
|
|
202
|
+
public var expanded: Bool
|
|
203
|
+
public init(props: JSONObject) {
|
|
204
|
+
expanded = props.bool("expanded") ?? false
|
|
189
205
|
}
|
|
190
206
|
}
|
|
191
207
|
|
|
@@ -284,12 +300,19 @@ public struct NavScreenProps: Equatable, Sendable {
|
|
|
284
300
|
public var depth: Int
|
|
285
301
|
public var toolbarRole: String?
|
|
286
302
|
public var backButtonHidden: Bool
|
|
303
|
+
/// `.toolbar(.hidden, for: .navigationBar)` / `.bottomBar`, and `.statusBarHidden()`.
|
|
304
|
+
public var navigationBarHidden: Bool
|
|
305
|
+
public var bottomBarHidden: Bool
|
|
306
|
+
public var statusBarHidden: Bool
|
|
287
307
|
public init(props: JSONObject) {
|
|
288
308
|
title = props.string("title") ?? ""
|
|
289
309
|
displayMode = props.string("displayMode") ?? "automatic"
|
|
290
310
|
depth = props.int("depth") ?? 0
|
|
291
311
|
toolbarRole = props.string("toolbarRole")
|
|
292
312
|
backButtonHidden = props.bool("backButtonHidden") ?? false
|
|
313
|
+
navigationBarHidden = props.bool("navigationBarHidden") ?? false
|
|
314
|
+
bottomBarHidden = props.bool("bottomBarHidden") ?? false
|
|
315
|
+
statusBarHidden = props.bool("statusBarHidden") ?? false
|
|
293
316
|
}
|
|
294
317
|
}
|
|
295
318
|
|
|
@@ -302,9 +325,12 @@ public struct NavSplitProps: Equatable, Sendable {
|
|
|
302
325
|
public struct NavLinkProps: Equatable, Sendable {
|
|
303
326
|
public var selected: Bool
|
|
304
327
|
public var plain: Bool
|
|
328
|
+
/// A split view sidebar's link: its row's selection takes the tap.
|
|
329
|
+
public var selects: Bool
|
|
305
330
|
public init(props: JSONObject) {
|
|
306
331
|
selected = props.bool("selected") ?? false
|
|
307
332
|
plain = props.string("style") == "plain"
|
|
333
|
+
selects = props.bool("selects") ?? false
|
|
308
334
|
}
|
|
309
335
|
}
|
|
310
336
|
|
|
@@ -532,11 +558,14 @@ public struct SliderProps: Equatable, Sendable {
|
|
|
532
558
|
public var min: Double
|
|
533
559
|
public var max: Double
|
|
534
560
|
public var step: Double?
|
|
561
|
+
/// What each child is, in order: `label`, `min` (the minimum value's label), `max`.
|
|
562
|
+
public var labels: [String]
|
|
535
563
|
public init(props: JSONObject) {
|
|
536
564
|
value = props.double("value") ?? 0
|
|
537
565
|
min = props.double("min") ?? 0
|
|
538
566
|
max = props.double("max") ?? 1
|
|
539
567
|
step = props.double("step")
|
|
568
|
+
labels = props.array("labels")?.compactMap(\.string) ?? []
|
|
540
569
|
}
|
|
541
570
|
}
|
|
542
571
|
|
|
@@ -550,8 +579,8 @@ public struct TapGestureProps: Equatable, Sendable {
|
|
|
550
579
|
}
|
|
551
580
|
}
|
|
552
581
|
|
|
553
|
-
/// The
|
|
554
|
-
public struct
|
|
582
|
+
/// The `registered` kind: a view registered under `name` (docs/native.md, "Registered views").
|
|
583
|
+
public struct RegisteredProps: Equatable, Sendable {
|
|
555
584
|
public var name: String
|
|
556
585
|
public var props: JSONValue
|
|
557
586
|
public init(props: JSONObject) {
|
|
@@ -158,9 +158,9 @@ public struct RendererEvent: Equatable, Sendable {
|
|
|
158
158
|
}
|
|
159
159
|
/// A `menu` opened or closed (lazy menus build their items on `open`).
|
|
160
160
|
public static func menu(_ id: Int, open: Bool) -> RendererEvent { with("menu", id: id, ["open": .bool(open)]) }
|
|
161
|
-
///
|
|
162
|
-
public static func
|
|
163
|
-
with("
|
|
161
|
+
/// What a `registered` element's view sent (docs/native.md, `ViewRegistry`).
|
|
162
|
+
public static func registered(_ id: Int, name: String, value: JSONValue) -> RendererEvent {
|
|
163
|
+
with("registered", id: id, ["name": .string(name), "value": value])
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
/// The device appearance, text size or size class (sent once at launch and on change).
|
|
@@ -178,6 +178,27 @@ public struct RendererEvent: Equatable, Sendable {
|
|
|
178
178
|
return RendererEvent(type: "environment", fields: fields)
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/// A styled box with `hover` was entered (`true`) or left by the pointer (`.onHover`).
|
|
182
|
+
public static func hover(_ id: Int, _ hovering: Bool) -> RendererEvent { with("hover", id: id, ["value": .bool(hovering)]) }
|
|
183
|
+
/// A styled box with `scrollVisibility` crossed its threshold of the scroll view's visible area.
|
|
184
|
+
public static func scrollVisibility(_ id: Int, visible: Bool) -> RendererEvent { with("scrollvisibility", id: id, ["visible": .bool(visible)]) }
|
|
185
|
+
/// A `draggable` was lifted: the app makes its payload now and keeps it until `dragend`.
|
|
186
|
+
public static func dragStart(_ id: Int) -> RendererEvent { with("dragstart", id: id) }
|
|
187
|
+
/// The lifted `draggable` was dropped.
|
|
188
|
+
public static func dragEnd(_ id: Int) -> RendererEvent { with("dragend", id: id) }
|
|
189
|
+
/// A drag over a `dropdestination`: `entered`, `exited` or `performed`, with the lifted `draggable`
|
|
190
|
+
/// (`source`) or what came from outside the app (`items`); the point in the destination's own box.
|
|
191
|
+
public static func drop(_ id: Int, phase: String, source: Int? = nil, items: [JSONValue]? = nil, x: Double = 0, y: Double = 0) -> RendererEvent {
|
|
192
|
+
var fields: JSONObject = ["phase": .string(phase), "x": .number(x), "y": .number(y)]
|
|
193
|
+
if let source { fields["source"] = .number(Double(source)) }
|
|
194
|
+
if let items { fields["items"] = .array(items) }
|
|
195
|
+
return with("drop", id: id, fields)
|
|
196
|
+
}
|
|
197
|
+
/// A `layout` element's record: the proposal it was placed with and each child's min, ideal and max sizes.
|
|
198
|
+
public static func layout(_ id: Int, proposal: LayoutProposalSpec, children: [LayoutMeasurementSpec]) -> RendererEvent {
|
|
199
|
+
with("layout", id: id, ["proposal": proposal.json, "children": .array(children.map(\.json))])
|
|
200
|
+
}
|
|
201
|
+
|
|
181
202
|
/// Several events delivered at once; the app renders once for all of them.
|
|
182
203
|
public static func batch(_ events: [RendererEvent]) -> RendererEvent {
|
|
183
204
|
RendererEvent(type: "batch", fields: ["events": .array(events.map(\.json))])
|