@swiftbrowser/native 0.0.0-stage → 0.10.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 (107) hide show
  1. package/README.md +143 -2
  2. package/bin/swiftbrowser-native.mjs +49 -0
  3. package/dist/bridge.d.ts +50 -0
  4. package/dist/bridge.d.ts.map +1 -0
  5. package/dist/bridge.js +10 -0
  6. package/dist/bridge.js.map +1 -0
  7. package/dist/build.d.ts +47 -0
  8. package/dist/build.d.ts.map +1 -0
  9. package/dist/build.js +156 -0
  10. package/dist/build.js.map +1 -0
  11. package/dist/devBuild.d.ts +34 -0
  12. package/dist/devBuild.d.ts.map +1 -0
  13. package/dist/devBuild.js +133 -0
  14. package/dist/devBuild.js.map +1 -0
  15. package/dist/encoding.d.ts +32 -0
  16. package/dist/encoding.d.ts.map +1 -0
  17. package/dist/encoding.js +136 -0
  18. package/dist/encoding.js.map +1 -0
  19. package/dist/fetch.d.ts +117 -0
  20. package/dist/fetch.d.ts.map +1 -0
  21. package/dist/fetch.js +272 -0
  22. package/dist/fetch.js.map +1 -0
  23. package/dist/index.d.ts +20 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +20 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/native.d.ts +19 -0
  28. package/dist/native.d.ts.map +1 -0
  29. package/dist/native.js +17 -0
  30. package/dist/native.js.map +1 -0
  31. package/dist/refresh.d.ts +18 -0
  32. package/dist/refresh.d.ts.map +1 -0
  33. package/dist/refresh.js +18 -0
  34. package/dist/refresh.js.map +1 -0
  35. package/dist/run.d.ts +27 -0
  36. package/dist/run.d.ts.map +1 -0
  37. package/dist/run.js +76 -0
  38. package/dist/run.js.map +1 -0
  39. package/dist/runtime.d.ts +30 -0
  40. package/dist/runtime.d.ts.map +1 -0
  41. package/dist/runtime.js +193 -0
  42. package/dist/runtime.js.map +1 -0
  43. package/dist/stubs/react-dom-client.d.ts +8 -0
  44. package/dist/stubs/react-dom-client.d.ts.map +1 -0
  45. package/dist/stubs/react-dom-client.js +11 -0
  46. package/dist/stubs/react-dom-client.js.map +1 -0
  47. package/dist/stubs/react-dom.d.ts +9 -0
  48. package/dist/stubs/react-dom.d.ts.map +1 -0
  49. package/dist/stubs/react-dom.js +13 -0
  50. package/dist/stubs/react-dom.js.map +1 -0
  51. package/dist/url.d.ts +48 -0
  52. package/dist/url.d.ts.map +1 -0
  53. package/dist/url.js +213 -0
  54. package/dist/url.js.map +1 -0
  55. package/dist/vendor.d.ts +11 -0
  56. package/dist/vendor.d.ts.map +1 -0
  57. package/dist/vendor.js +27 -0
  58. package/dist/vendor.js.map +1 -0
  59. package/dist/wasmHost.d.ts +35 -0
  60. package/dist/wasmHost.d.ts.map +1 -0
  61. package/dist/wasmHost.js +120 -0
  62. package/dist/wasmHost.js.map +1 -0
  63. package/package.json +73 -4
  64. package/src/bridge.ts +58 -0
  65. package/src/build.ts +185 -0
  66. package/src/devBuild.ts +152 -0
  67. package/src/encoding.ts +130 -0
  68. package/src/fetch.ts +305 -0
  69. package/src/index.ts +20 -0
  70. package/src/native.ts +35 -0
  71. package/src/refresh.ts +29 -0
  72. package/src/run.ts +98 -0
  73. package/src/runtime.ts +208 -0
  74. package/src/stubs/react-dom-client.ts +17 -0
  75. package/src/stubs/react-dom.ts +15 -0
  76. package/src/types/react-refresh.d.ts +38 -0
  77. package/src/url.ts +202 -0
  78. package/src/vendor.ts +30 -0
  79. package/src/wasmHost.ts +147 -0
  80. package/swift/Package.swift +31 -0
  81. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +228 -0
  82. package/swift/Sources/SwiftBrowserNative/Host/JSHost.swift +42 -0
  83. package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +102 -0
  84. package/swift/Sources/SwiftBrowserNative/Host/ShimAliases.swift +25 -0
  85. package/swift/Sources/SwiftBrowserNative/Host/WebHost.swift +138 -0
  86. package/swift/Sources/SwiftBrowserNative/Runtime/NativeApp.swift +251 -0
  87. package/swift/Sources/SwiftBrowserNative/Runtime/NativeRegistry.swift +53 -0
  88. package/swift/Sources/SwiftBrowserNative/SwiftBrowserNative.swift +6 -0
  89. package/swift/Sources/SwiftBrowserNative/Views/Conversions.swift +361 -0
  90. package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +169 -0
  91. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +268 -0
  92. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Lists.swift +175 -0
  93. package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +196 -0
  94. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +246 -0
  95. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Shapes.swift +108 -0
  96. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +189 -0
  97. package/swift/Sources/SwiftBrowserNative/Views/Kinds/TextAndImages.swift +102 -0
  98. package/swift/Sources/SwiftBrowserNative/Views/NativeRootView.swift +190 -0
  99. package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +182 -0
  100. package/swift/Sources/SwiftBrowserProtocol/JSONValue.swift +268 -0
  101. package/swift/Sources/SwiftBrowserProtocol/Op.swift +100 -0
  102. package/swift/Sources/SwiftBrowserProtocol/Props.swift +469 -0
  103. package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +117 -0
  104. package/swift/Sources/SwiftBrowserProtocol/Style.swift +148 -0
  105. package/swift/Sources/SwiftBrowserProtocol/Values.swift +279 -0
  106. package/swift/Sources/SwiftBrowserTree/ElementNode.swift +65 -0
  107. package/swift/Sources/SwiftBrowserTree/OpTree.swift +168 -0
@@ -0,0 +1,469 @@
1
+ import Foundation
2
+
3
+ // Typed readers over an element's props, one per kind a renderer draws
4
+ // (docs/ops-protocol.md, "Element kinds and props"). Each reads the fields
5
+ // the protocol defines with the protocol's defaults, from the JSON the tree
6
+ // keeps; `init(props:)` never fails, since a renderer draws what it can.
7
+
8
+ public struct TextProps: Equatable, Sendable {
9
+ public var text: String
10
+ /// Rich text (`runs`), when the text has styled runs; empty for plain text.
11
+ public var runs: [TextRunSpec]
12
+
13
+ public init(props: JSONObject) {
14
+ text = props.string("text") ?? ""
15
+ runs = (props.array("runs") ?? []).compactMap(TextRunSpec.init(json:))
16
+ }
17
+ }
18
+
19
+ /// One run of a rich `text`: its text or inline image, and the attributes over the text's own.
20
+ public struct TextRunSpec: Equatable, Sendable {
21
+ public var text: String?
22
+ public var image: ImageProps?
23
+ public var font: FontSpec?
24
+ public var foreground: ShapeStyleSpec?
25
+ public var italic: Bool
26
+ public var underline: Bool
27
+ public var underlineColor: ColorSpec?
28
+ public var strikethrough: Bool
29
+ public var strikethroughColor: ColorSpec?
30
+ public var kerning: Double?
31
+ public var baselineOffset: Double?
32
+ public var link: String?
33
+
34
+ public init?(json: JSONValue) {
35
+ guard let fields = json.object else { return nil }
36
+ text = fields.string("text")
37
+ image = fields.object("image").map(ImageProps.init(props:))
38
+ font = FontSpec(json: fields.value("font"))
39
+ foreground = ShapeStyleSpec(json: fields.value("foreground"))
40
+ italic = fields.bool("italic") ?? false
41
+ underline = fields["underline"] != nil
42
+ underlineColor = ColorSpec(json: fields.value("underline")["color"])
43
+ strikethrough = fields["strikethrough"] != nil
44
+ strikethroughColor = ColorSpec(json: fields.value("strikethrough")["color"])
45
+ kerning = fields.double("kerning")
46
+ baselineOffset = fields.double("baselineOffset")
47
+ link = fields.string("link")
48
+ }
49
+ }
50
+
51
+ public struct StackProps: Equatable, Sendable {
52
+ /// Nil is the system default spacing.
53
+ public var spacing: Double?
54
+ /// A `VStack`'s horizontal alignment, an `HStack`'s vertical one, a `ZStack`'s frame alignment.
55
+ public var alignment: String
56
+
57
+ public init(props: JSONObject) {
58
+ spacing = props.double("spacing")
59
+ alignment = props.string("alignment") ?? "center"
60
+ }
61
+ }
62
+
63
+ public struct SpacerProps: Equatable, Sendable {
64
+ public var minLength: Double?
65
+ public init(props: JSONObject) { minLength = props.double("minLength") }
66
+ }
67
+
68
+ public struct ButtonProps: Equatable, Sendable {
69
+ /// `automatic`, `plain`, `bordered`, `borderedProminent`, `glass`.
70
+ public var style: String
71
+ /// `destructive`, `cancel` or nil.
72
+ public var role: String?
73
+ public var controlSize: String?
74
+ /// An `EditButton`: `active` or `inactive`, the mode it shows and flips.
75
+ public var editMode: String?
76
+
77
+ public init(props: JSONObject) {
78
+ style = props.string("style") ?? "automatic"
79
+ role = props.string("role")
80
+ controlSize = props.string("controlSize")
81
+ editMode = props.string("editMode")
82
+ }
83
+ }
84
+
85
+ /// An `image`: an SF Symbol, an asset catalog image, a remote URL or a bitmap recipe.
86
+ public enum ImageProps: Equatable, Sendable {
87
+ case symbol(name: String, paletteRendering: Bool)
88
+ case asset(name: String, resizable: Bool)
89
+ case url(String, resizable: Bool, width: Double, height: Double)
90
+ case bitmap(recipe: JSONValue, resizable: Bool, width: Double, height: Double)
91
+ case unknown
92
+
93
+ public init(props: JSONObject) {
94
+ let resizable = props.bool("resizable") ?? false
95
+ if let name = props.string("systemName") {
96
+ self = .symbol(name: name, paletteRendering: props.string("renderingMode") == "palette")
97
+ } else if let name = props.string("name") {
98
+ self = .asset(name: name, resizable: resizable)
99
+ } else if let url = props.string("url") {
100
+ self = .url(url, resizable: resizable, width: props.double("width") ?? 0, height: props.double("height") ?? 0)
101
+ } else if let recipe = props["bitmap"] {
102
+ self = .bitmap(recipe: recipe, resizable: resizable, width: props.double("width") ?? 0, height: props.double("height") ?? 0)
103
+ } else {
104
+ self = .unknown
105
+ }
106
+ }
107
+ }
108
+
109
+ public struct ScrollViewProps: Equatable, Sendable {
110
+ /// `vertical`, `horizontal` or `both`.
111
+ public var axes: String
112
+ public var showsIndicators: Bool
113
+ public var refreshable: Bool
114
+ public init(props: JSONObject) {
115
+ axes = props.string("axes") ?? "vertical"
116
+ showsIndicators = props.bool("showsIndicators") ?? true
117
+ refreshable = props.bool("refreshable") ?? false
118
+ }
119
+ }
120
+
121
+ public struct ListProps: Equatable, Sendable {
122
+ /// `insetGrouped` (the default), `grouped`, `plain`, `sidebar`.
123
+ public var style: String
124
+ public var refreshable: Bool
125
+ public var editing: Bool?
126
+ public init(props: JSONObject) {
127
+ style = props.string("style") ?? "insetGrouped"
128
+ refreshable = props.bool("refreshable") ?? false
129
+ editing = props.bool("editing")
130
+ }
131
+ }
132
+
133
+ public struct SectionProps: Equatable, Sendable {
134
+ public var header: String?
135
+ public var footer: String?
136
+ public init(props: JSONObject) {
137
+ header = props.string("header")
138
+ footer = props.string("footer")
139
+ }
140
+ }
141
+
142
+ public struct TextFieldProps: Equatable, Sendable {
143
+ public var text: String
144
+ public var placeholder: String
145
+ /// `plain` or `roundedBorder`.
146
+ public var style: String
147
+ public var secure: Bool
148
+ public var focused: Bool?
149
+ public var submit: Bool
150
+ public var submitLabel: String?
151
+ public var vertical: Bool
152
+ public init(props: JSONObject) {
153
+ text = props.string("text") ?? ""
154
+ placeholder = props.string("placeholder") ?? ""
155
+ style = props.string("style") ?? "plain"
156
+ secure = props.bool("secure") ?? false
157
+ focused = props.bool("focused")
158
+ submit = props.bool("submit") ?? false
159
+ submitLabel = props.string("submitLabel")
160
+ vertical = props.string("axis") == "vertical"
161
+ }
162
+ }
163
+
164
+ public struct ToggleProps: Equatable, Sendable {
165
+ public var isOn: Bool
166
+ public init(props: JSONObject) { isOn = props.bool("isOn") ?? false }
167
+ }
168
+
169
+ public struct NavScreenProps: Equatable, Sendable {
170
+ public var title: String
171
+ /// `automatic`, `large` or `inline`.
172
+ public var displayMode: String
173
+ /// 0 for the stack's root screen.
174
+ public var depth: Int
175
+ public var toolbarRole: String?
176
+ public var backButtonHidden: Bool
177
+ public init(props: JSONObject) {
178
+ title = props.string("title") ?? ""
179
+ displayMode = props.string("displayMode") ?? "automatic"
180
+ depth = props.int("depth") ?? 0
181
+ toolbarRole = props.string("toolbarRole")
182
+ backButtonHidden = props.bool("backButtonHidden") ?? false
183
+ }
184
+ }
185
+
186
+ public struct NavLinkProps: Equatable, Sendable {
187
+ public var selected: Bool
188
+ public var plain: Bool
189
+ public init(props: JSONObject) {
190
+ selected = props.bool("selected") ?? false
191
+ plain = props.string("style") == "plain"
192
+ }
193
+ }
194
+
195
+ public struct ColorProps: Equatable, Sendable {
196
+ public var color: ColorSpec?
197
+ public init(props: JSONObject) { color = ColorSpec(json: props.value("color")) }
198
+ }
199
+
200
+ public struct StrokeSpec: Equatable, Sendable {
201
+ public var style: ShapeStyleSpec?
202
+ public var lineWidth: Double
203
+ public var inset: Bool
204
+ public var dash: [Double]
205
+ public init?(json: JSONValue) {
206
+ guard let fields = json.object else { return nil }
207
+ style = ShapeStyleSpec(json: fields.value("color"))
208
+ lineWidth = fields.double("lineWidth") ?? 1
209
+ inset = fields.bool("inset") ?? false
210
+ dash = (fields.array("dash") ?? []).compactMap(\.double)
211
+ }
212
+ }
213
+
214
+ public struct ShapeProps: Equatable, Sendable {
215
+ /// `rectangle`, `roundedRectangle`, `circle`, `capsule`, `ellipse`, `containerRelative`, `path`.
216
+ public var shape: String
217
+ public var cornerRadius: Double
218
+ /// Nil fills with the current foreground; `.some(nil)` is an explicit no fill.
219
+ public var fill: ShapeStyleSpec?
220
+ public var fillIsNull: Bool
221
+ public var stroke: StrokeSpec?
222
+ public var trimFrom: Double?
223
+ public var trimTo: Double?
224
+ public var path: String?
225
+ /// The size `path` was drawn for.
226
+ public var size: SizeSpec?
227
+ public init(props: JSONObject) {
228
+ shape = props.string("shape") ?? "rectangle"
229
+ cornerRadius = props.double("cornerRadius") ?? 0
230
+ fill = ShapeStyleSpec(json: props.value("fill"))
231
+ fillIsNull = props["fill"]?.isNull ?? true
232
+ stroke = StrokeSpec(json: props.value("stroke"))
233
+ trimFrom = props.object("trim")?.double("from")
234
+ trimTo = props.object("trim")?.double("to")
235
+ path = props.string("path")
236
+ size = SizeSpec(json: props.value("size"))
237
+ }
238
+ }
239
+
240
+ public struct SheetProps: Equatable, Sendable {
241
+ /// `medium`, `large`, `full`, `height:<points>`, `fraction:<f>`.
242
+ public var detents: [String]
243
+ public var interactiveDismissDisabled: Bool
244
+ public init(props: JSONObject) {
245
+ detents = (props.array("detents") ?? []).compactMap(\.string)
246
+ interactiveDismissDisabled = props.bool("interactiveDismissDisabled") ?? false
247
+ }
248
+ public var isFullScreen: Bool { detents == ["full"] }
249
+ }
250
+
251
+ public struct TabViewProps: Equatable, Sendable {
252
+ public var selected: Int
253
+ public init(props: JSONObject) { selected = props.int("selected") ?? 0 }
254
+ }
255
+
256
+ public struct TabProps: Equatable, Sendable {
257
+ public var title: String
258
+ public var systemImage: String?
259
+ public var image: String?
260
+ public var badge: String?
261
+ public var isSearch: Bool
262
+ public init(props: JSONObject) {
263
+ title = props.string("title") ?? ""
264
+ systemImage = props.string("systemImage")
265
+ image = props.string("image")
266
+ badge = props.string("badge")
267
+ isSearch = props.string("role") == "search"
268
+ }
269
+ }
270
+
271
+ public struct PickerProps: Equatable, Sendable {
272
+ /// `segmented`, `menu`, `inline`, `wheel`.
273
+ public var style: String
274
+ public var label: String
275
+ public var options: [String]
276
+ public var selected: Int
277
+ public init(props: JSONObject) {
278
+ style = props.string("style") ?? "menu"
279
+ label = props.string("label") ?? ""
280
+ options = (props.array("options") ?? []).compactMap(\.string)
281
+ selected = props.int("selected") ?? 0
282
+ }
283
+ }
284
+
285
+ public struct GeometryProps: Equatable, Sendable {
286
+ public var reportsFrame: Bool
287
+ public var reader: Int?
288
+ public init(props: JSONObject) {
289
+ reportsFrame = props.bool("reportsFrame") ?? false
290
+ reader = props.int("reader")
291
+ }
292
+ }
293
+
294
+ public struct ListRowProps: Equatable, Sendable {
295
+ public var selectable: Bool
296
+ public var selected: Bool
297
+ public var deletable: Bool
298
+ public var movable: Bool
299
+ public init(props: JSONObject) {
300
+ selectable = props.bool("selectable") ?? false
301
+ selected = props.bool("selected") ?? false
302
+ deletable = props.bool("deletable") ?? false
303
+ movable = props.bool("movable") ?? false
304
+ }
305
+ }
306
+
307
+ public struct ToolbarProps: Equatable, Sendable {
308
+ /// `trailing`, `leading`, `principal`, `bottom`, `secondary`, `largeTitle`.
309
+ public var placement: String
310
+ /// A `ToolbarSpacer`: `fixed` or `flexible`; the element has no items.
311
+ public var spacer: String?
312
+ /// `hidden` for `.sharedBackgroundVisibility(.hidden)`: no glass of the bar's around the items.
313
+ public var sharedBackground: String?
314
+ public init(props: JSONObject) {
315
+ placement = props.string("placement") ?? "trailing"
316
+ spacer = props.string("spacer")
317
+ sharedBackground = props.string("sharedBackground")
318
+ }
319
+ }
320
+
321
+ public struct SearchFieldProps: Equatable, Sendable {
322
+ public var text: String
323
+ public var placeholder: String
324
+ public init(props: JSONObject) {
325
+ text = props.string("text") ?? ""
326
+ placeholder = props.string("placeholder") ?? "Search"
327
+ }
328
+ }
329
+
330
+ public struct DecoratedProps: Equatable, Sendable {
331
+ /// `overlay` or `background`.
332
+ public var role: String
333
+ public var alignment: String
334
+ public init(props: JSONObject) {
335
+ role = props.string("role") ?? "overlay"
336
+ alignment = props.string("alignment") ?? "center"
337
+ }
338
+ }
339
+
340
+ public struct AlertProps: Equatable, Sendable {
341
+ public var title: String
342
+ public var message: String?
343
+ /// `alert` or `confirmationDialog`.
344
+ public var style: String
345
+ public init(props: JSONObject) {
346
+ title = props.string("title") ?? ""
347
+ message = props.string("message")
348
+ style = props.string("style") ?? "alert"
349
+ }
350
+ }
351
+
352
+ public struct StepperProps: Equatable, Sendable {
353
+ public var canIncrement: Bool
354
+ public var canDecrement: Bool
355
+ public init(props: JSONObject) {
356
+ canIncrement = props.bool("canIncrement") ?? true
357
+ canDecrement = props.bool("canDecrement") ?? true
358
+ }
359
+ }
360
+
361
+ public struct SliderProps: Equatable, Sendable {
362
+ public var value: Double
363
+ public var min: Double
364
+ public var max: Double
365
+ public var step: Double?
366
+ public init(props: JSONObject) {
367
+ value = props.double("value") ?? 0
368
+ min = props.double("min") ?? 0
369
+ max = props.double("max") ?? 1
370
+ step = props.double("step")
371
+ }
372
+ }
373
+
374
+ public struct TapGestureProps: Equatable, Sendable {
375
+ public var location: Bool
376
+ public init(props: JSONObject) { location = props.bool("location") ?? false }
377
+ }
378
+
379
+ /// The producer-only `native` kind (docs/native.md, "The native escape hatch").
380
+ public struct NativeProps: Equatable, Sendable {
381
+ public var name: String
382
+ public var props: JSONValue
383
+ public init(props: JSONObject) {
384
+ name = props.string("name") ?? ""
385
+ self.props = props.value("props")
386
+ }
387
+ }
388
+
389
+ /// The root container's props (`update` of id 0): `.preferredColorScheme`.
390
+ public struct RootProps: Equatable, Sendable {
391
+ /// `light`, `dark`, or nil for the viewer's setting.
392
+ public var colorScheme: String?
393
+ public init(props: JSONObject) { colorScheme = props.string("colorScheme") }
394
+ }
395
+
396
+ public struct GaugeProps: Equatable, Sendable {
397
+ /// Normalized to 0...1.
398
+ public var value: Double
399
+ /// `automatic`, `linearCapacity`, `accessoryLinear`, `accessoryCircular`, `accessoryCircularCapacity`, `progress`.
400
+ public var style: String
401
+ public var hasLabel: Bool
402
+ public var hasCurrentValueLabel: Bool
403
+ public init(props: JSONObject) {
404
+ value = min(1, max(0, props.double("value") ?? 0))
405
+ style = props.string("style") ?? "automatic"
406
+ hasLabel = props.bool("hasLabel") ?? false
407
+ hasCurrentValueLabel = props.bool("hasCurrentValueLabel") ?? false
408
+ }
409
+ }
410
+
411
+ public struct DatePickerProps: Equatable, Sendable {
412
+ /// Seconds since 1970.
413
+ public var seconds: Double
414
+ /// `date`, `hourAndMinute`, `dateAndTime`.
415
+ public var components: String
416
+ public var min: Double?
417
+ public var max: Double?
418
+ public var timeZone: String?
419
+ public var labelsHidden: Bool
420
+ public init(props: JSONObject) {
421
+ seconds = props.double("seconds") ?? 0
422
+ components = props.string("components") ?? "date"
423
+ min = props.double("min")
424
+ max = props.double("max")
425
+ timeZone = props.string("timeZone")
426
+ labelsHidden = props.bool("labelsHidden") ?? false
427
+ }
428
+ }
429
+
430
+ public struct GestureProps: Equatable, Sendable {
431
+ public var drag: Bool
432
+ public var longPress: Bool
433
+ public var minimumDistance: Double
434
+ public var minimumDuration: Double
435
+ public init(props: JSONObject) {
436
+ drag = props.bool("drag") ?? false
437
+ longPress = props.bool("longPress") ?? false
438
+ minimumDistance = props.double("minimumDistance") ?? 10
439
+ minimumDuration = props.double("minimumDuration") ?? 0.5
440
+ }
441
+ }
442
+
443
+ public struct ViewThatFitsProps: Equatable, Sendable {
444
+ /// `horizontal`, `vertical` or `both`.
445
+ public var axes: String
446
+ public init(props: JSONObject) { axes = props.string("axes") ?? "both" }
447
+ }
448
+
449
+ public struct LazySpacerProps: Equatable, Sendable {
450
+ public var axis: String
451
+ public var remaining: Int
452
+ public init(props: JSONObject) {
453
+ axis = props.string("axis") ?? "vertical"
454
+ remaining = props.int("remaining") ?? 0
455
+ }
456
+ }
457
+
458
+ /// `zstack` and `shape` props that ask for the element's laid-out size
459
+ /// (`geometry` events, like a `GeometryReader`'s): anchor preferences, custom shapes.
460
+ public struct SizeReportProps: Equatable, Sendable {
461
+ public var reportsSize: Bool
462
+ public var reportsFrame: Bool
463
+ public var reader: Int?
464
+ public init(props: JSONObject) {
465
+ reportsSize = props.bool("reportsSize") ?? false
466
+ reportsFrame = props.bool("reportsFrame") ?? false
467
+ reader = props.int("reader")
468
+ }
469
+ }
@@ -0,0 +1,117 @@
1
+ import Foundation
2
+
3
+ /// An event the renderer sends the app (docs/ops-protocol.md, "Events"):
4
+ /// a `type`, usually the `id` of the element it concerns, and the fields of
5
+ /// that type. Built with the constructors below and sent as JSON.
6
+ public struct RendererEvent: Equatable, Sendable {
7
+ public var type: String
8
+ public var fields: JSONObject
9
+
10
+ public init(type: String, fields: JSONObject = [:]) {
11
+ self.type = type
12
+ self.fields = fields
13
+ }
14
+
15
+ /// The element the event names, when it names one.
16
+ public var id: Int? { fields.int("id") }
17
+
18
+ /// The event as a JSON object (`type` and the fields), sorted keys.
19
+ public var json: JSONValue {
20
+ var members = fields
21
+ members["type"] = .string(type)
22
+ return .object(members)
23
+ }
24
+
25
+ public var jsonString: String { json.json }
26
+
27
+ public init?(json: JSONValue) {
28
+ guard var members = json.object, let type = members["type"]?.string else { return nil }
29
+ members["type"] = nil
30
+ self.init(type: type, fields: members)
31
+ }
32
+
33
+ private static func with(_ type: String, id: Int, _ extra: JSONObject = [:]) -> RendererEvent {
34
+ var fields = extra
35
+ fields["id"] = .number(Double(id))
36
+ return RendererEvent(type: type, fields: fields)
37
+ }
38
+
39
+ /// A `button`, `navlink`, `tapgesture`, `listrow` was tapped; a `navscreen`'s back button; a `sheet` dismissed.
40
+ public static func tap(_ id: Int) -> RendererEvent { with("tap", id: id) }
41
+ /// A `tapgesture` with `location`.
42
+ public static func tap(_ id: Int, x: Double, y: Double) -> RendererEvent { with("tap", id: id, ["x": .number(x), "y": .number(y)]) }
43
+ /// A `textfield`, `searchfield` or `texteditor` received input.
44
+ public static func text(_ id: Int, _ value: String) -> RendererEvent { with("text", id: id, ["value": .string(value)]) }
45
+ /// A `toggle` was switched.
46
+ public static func toggle(_ id: Int, _ value: Bool) -> RendererEvent { with("toggle", id: id, ["value": .bool(value)]) }
47
+ /// A `tabview` item or `picker` option was chosen, by index.
48
+ public static func select(_ id: Int, _ index: Int) -> RendererEvent { with("select", id: id, ["value": .number(Double(index))]) }
49
+ /// A link run of a `text` was tapped.
50
+ public static func link(_ id: Int, _ url: String) -> RendererEvent { with("link", id: id, ["value": .string(url)]) }
51
+ /// A `geometry` element's laid-out size (and, with `reportsFrame`, its origin).
52
+ public static func geometry(_ id: Int, width: Double, height: Double, x: Double? = nil, y: Double? = nil) -> RendererEvent {
53
+ var fields: JSONObject = ["width": .number(width), "height": .number(height)]
54
+ if let x { fields["x"] = .number(x) }
55
+ if let y { fields["y"] = .number(y) }
56
+ return with("geometry", id: id, fields)
57
+ }
58
+ /// A `stepper` half was tapped (`value` is ±1).
59
+ public static func step(_ id: Int, _ delta: Int) -> RendererEvent { with("step", id: id, ["value": .number(Double(delta))]) }
60
+ /// A `slider` moved.
61
+ public static func slide(_ id: Int, _ value: Double) -> RendererEvent { with("slide", id: id, ["value": .number(value)]) }
62
+ /// A `datepicker` changed; `seconds` since 1970.
63
+ public static func date(_ id: Int, seconds: Double) -> RendererEvent { with("date", id: id, ["value": .number(seconds)]) }
64
+ /// A `textfield` with `focused` gained or lost focus.
65
+ public static func focus(_ id: Int, _ focused: Bool) -> RendererEvent { with("focus", id: id, ["value": .bool(focused)]) }
66
+ /// The Return key in a `textfield` with `submit`.
67
+ public static func submit(_ id: Int) -> RendererEvent { with("submit", id: id) }
68
+ /// A `listrow` was deleted by a swipe or in edit mode.
69
+ public static func delete(_ id: Int) -> RendererEvent { with("delete", id: id) }
70
+ /// A `listrow` was moved to index `to` among its siblings.
71
+ public static func move(_ id: Int, to: Int) -> RendererEvent { with("move", id: id, ["to": .number(Double(to))]) }
72
+ /// A `gesture` drag: `phase` is `began`, `changed`, `ended` or `cancelled`; points in the element's coordinates.
73
+ public static func drag(
74
+ _ id: Int, phase: String, translation: (width: Double, height: Double), location: (x: Double, y: Double),
75
+ startLocation: (x: Double, y: Double), velocity: (width: Double, height: Double)
76
+ ) -> RendererEvent {
77
+ with("drag", id: id, [
78
+ "phase": .string(phase),
79
+ "translation": .object(["width": .number(translation.width), "height": .number(translation.height)]),
80
+ "location": .object(["x": .number(location.x), "y": .number(location.y)]),
81
+ "startLocation": .object(["x": .number(startLocation.x), "y": .number(startLocation.y)]),
82
+ "velocity": .object(["width": .number(velocity.width), "height": .number(velocity.height)]),
83
+ ])
84
+ }
85
+ /// A `lazyspacer` came into view: the app mounts the next batch of rows.
86
+ public static func lazy(_ id: Int) -> RendererEvent { with("lazy", id: id) }
87
+ /// A `gesture` long press.
88
+ public static func longPress(_ id: Int) -> RendererEvent { with("longpress", id: id) }
89
+ /// A `list` or `scrollview` with `refreshable` was pulled.
90
+ public static func refresh(_ id: Int) -> RendererEvent { with("refresh", id: id) }
91
+ /// A `menu` opened or closed (lazy menus build their items on `open`).
92
+ public static func menu(_ id: Int, open: Bool) -> RendererEvent { with("menu", id: id, ["open": .bool(open)]) }
93
+ /// The host's answer to a `native` element's own event (docs/native.md, the escape hatch).
94
+ public static func native(_ id: Int, name: String, value: JSONValue) -> RendererEvent {
95
+ with("native", id: id, ["name": .string(name), "value": value])
96
+ }
97
+
98
+ /// The device appearance, text size or size class (sent once at launch and on change).
99
+ public static func environment(
100
+ colorScheme: String? = nil,
101
+ dynamicTypeSize: String? = nil,
102
+ horizontalSizeClass: String? = nil,
103
+ scenePhase: String? = nil
104
+ ) -> RendererEvent {
105
+ var fields: JSONObject = [:]
106
+ if let colorScheme { fields["colorScheme"] = .string(colorScheme) }
107
+ if let dynamicTypeSize { fields["dynamicTypeSize"] = .string(dynamicTypeSize) }
108
+ if let horizontalSizeClass { fields["horizontalSizeClass"] = .string(horizontalSizeClass) }
109
+ if let scenePhase { fields["scenePhase"] = .string(scenePhase) }
110
+ return RendererEvent(type: "environment", fields: fields)
111
+ }
112
+
113
+ /// Several events delivered at once; the app renders once for all of them.
114
+ public static func batch(_ events: [RendererEvent]) -> RendererEvent {
115
+ RendererEvent(type: "batch", fields: ["events": .array(events.map(\.json))])
116
+ }
117
+ }