@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.
Files changed (102) hide show
  1. package/README.md +53 -11
  2. package/bin/swiftbrowser-native.mjs +47 -0
  3. package/dist/blob.d.ts +85 -11
  4. package/dist/blob.d.ts.map +1 -1
  5. package/dist/blob.js +184 -34
  6. package/dist/blob.js.map +1 -1
  7. package/dist/bridge.d.ts +66 -8
  8. package/dist/bridge.d.ts.map +1 -1
  9. package/dist/bridge.js.map +1 -1
  10. package/dist/encoding.d.ts +13 -0
  11. package/dist/encoding.d.ts.map +1 -1
  12. package/dist/encoding.js +24 -0
  13. package/dist/encoding.js.map +1 -1
  14. package/dist/fetch.d.ts +27 -13
  15. package/dist/fetch.d.ts.map +1 -1
  16. package/dist/fetch.js +117 -45
  17. package/dist/fetch.js.map +1 -1
  18. package/dist/hostRequests.d.ts +24 -6
  19. package/dist/hostRequests.d.ts.map +1 -1
  20. package/dist/hostRequests.js +108 -15
  21. package/dist/hostRequests.js.map +1 -1
  22. package/dist/index.d.ts +1 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/ios.d.ts +79 -0
  27. package/dist/ios.d.ts.map +1 -0
  28. package/dist/ios.js +181 -0
  29. package/dist/ios.js.map +1 -0
  30. package/dist/runtime.d.ts.map +1 -1
  31. package/dist/runtime.js +16 -7
  32. package/dist/runtime.js.map +1 -1
  33. package/dist/subtle.d.ts +1 -1
  34. package/dist/subtle.d.ts.map +1 -1
  35. package/dist/subtle.js +9 -5
  36. package/dist/subtle.js.map +1 -1
  37. package/dist/webSocket.d.ts +12 -5
  38. package/dist/webSocket.d.ts.map +1 -1
  39. package/dist/webSocket.js +37 -11
  40. package/dist/webSocket.js.map +1 -1
  41. package/package.json +7 -3
  42. package/src/blob.ts +215 -34
  43. package/src/bridge.ts +56 -8
  44. package/src/encoding.ts +22 -0
  45. package/src/fetch.ts +111 -48
  46. package/src/hostRequests.ts +129 -22
  47. package/src/index.ts +1 -2
  48. package/src/ios.ts +253 -0
  49. package/src/runtime.ts +23 -8
  50. package/src/subtle.ts +10 -6
  51. package/src/webSocket.ts +37 -13
  52. package/swift/Packages/SwiftBrowserDSP/Package.swift +31 -0
  53. package/swift/Packages/SwiftBrowserDSP/README.md +42 -0
  54. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ProgramNode.swift +306 -0
  55. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ScopeView.swift +79 -0
  56. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +1561 -0
  57. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Program.swift +483 -0
  58. package/swift/Packages/SwiftBrowserLocalModel/Package.swift +35 -0
  59. package/swift/Packages/SwiftBrowserLocalModel/README.md +66 -0
  60. package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LlamaEngine.swift +223 -0
  61. package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LocalLanguageModel.swift +409 -0
  62. package/swift/Packages/SwiftBrowserShader/Package.swift +31 -0
  63. package/swift/Packages/SwiftBrowserShader/README.md +45 -0
  64. package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderRenderer.swift +103 -0
  65. package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderView.swift +168 -0
  66. package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShaderCore/ShaderProps.swift +120 -0
  67. package/swift/Sources/SwiftBrowserNative/Host/AudioEngineBridge.swift +921 -0
  68. package/swift/Sources/SwiftBrowserNative/Host/AudioEngineNodes.swift +70 -0
  69. package/swift/Sources/SwiftBrowserNative/Host/BodyStream.swift +166 -0
  70. package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +144 -10
  71. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +151 -27
  72. package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +139 -40
  73. package/swift/Sources/SwiftBrowserNative/Host/JSHost.swift +116 -0
  74. package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +141 -2
  75. package/swift/Sources/SwiftBrowserNative/Host/LanguageModelBridge.swift +511 -0
  76. package/swift/Sources/SwiftBrowserNative/Host/LanguageModelProvider.swift +195 -0
  77. package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +24 -11
  78. package/swift/Sources/SwiftBrowserNative/Host/WorkerHost.swift +3 -2
  79. package/swift/Sources/SwiftBrowserNative/Runtime/NativeFunctions.swift +1 -1
  80. package/swift/Sources/SwiftBrowserNative/Runtime/ViewRegistry.swift +57 -0
  81. package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +12 -7
  82. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +18 -1
  83. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Drawing.swift +378 -0
  84. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Lists.swift +41 -1
  85. package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +88 -17
  86. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +87 -21
  87. package/swift/Sources/SwiftBrowserNative/Views/Kinds/PinchTracker.swift +200 -0
  88. package/swift/Sources/SwiftBrowserNative/Views/Kinds/PopTap.swift +103 -0
  89. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +3 -2
  90. package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +146 -0
  91. package/swift/Sources/SwiftBrowserProtocol/Drawing.swift +306 -0
  92. package/swift/Sources/SwiftBrowserProtocol/Props.swift +31 -2
  93. package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +24 -3
  94. package/swift/Sources/SwiftBrowserProtocol/Style.swift +87 -0
  95. package/swift/Sources/SwiftBrowserTree/OpTree.swift +25 -0
  96. package/wasm/SwiftBrowserNative.wasm +2 -2
  97. package/dist/native.d.ts +0 -19
  98. package/dist/native.d.ts.map +0 -1
  99. package/dist/native.js +0 -17
  100. package/dist/native.js.map +0 -1
  101. package/src/native.ts +0 -35
  102. 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 producer-only `native` kind (docs/native.md, "The native escape hatch").
554
- public struct NativeProps: Equatable, Sendable {
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
- /// The host's answer to a `native` element's own event (docs/native.md, the escape hatch).
162
- public static func native(_ id: Int, name: String, value: JSONValue) -> RendererEvent {
163
- with("native", id: id, ["name": .string(name), "value": value])
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))])