@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,148 @@
1
+ import Foundation
2
+
3
+ /// A `styled` element's style: every field optional, each one modifier of
4
+ /// the app's (docs/ops-protocol.md, "Style"; `interface Style` in
5
+ /// Web/src/protocol.ts). The fields a renderer draws are typed here; `raw`
6
+ /// keeps the whole object, so a field this type does not name is still there
7
+ /// to read.
8
+ public struct Style: Equatable, Sendable {
9
+ public var raw: JSONObject
10
+
11
+ public var padding: EdgeInsetsSpec?
12
+ public var font: FontSpec?
13
+ public var foreground: ShapeStyleSpec?
14
+ public var foregroundLevels: [ShapeStyleSpec]
15
+ public var background: ShapeStyleSpec?
16
+ public var frame: FrameSpec?
17
+ public var cornerRadius: Double?
18
+ public var backgroundCornerRadius: Double?
19
+ public var opacity: Double?
20
+ public var disabled: Bool?
21
+ public var layoutPriority: Double?
22
+ /// `.lineLimit(n)`; `lineLimitLifted` when the app passed `nil` to lift an inherited limit.
23
+ public var lineLimit: Int?
24
+ public var lineLimitLifted: Bool
25
+ public var lineLimitMin: Int?
26
+ public var multilineTextAlignment: String?
27
+ public var fixedSizeHorizontal: Bool?
28
+ public var fixedSizeVertical: Bool?
29
+ public var animation: AnimationSpec?
30
+ public var animationToken: Double?
31
+ public var labelIcon: Bool
32
+ public var transition: [TransitionSpec]?
33
+ public var aspectRatio: Double??
34
+ public var aspectRatioContentMode: String?
35
+ public var offset: PointSpec?
36
+ public var rotation: Double?
37
+ public var rotationAnchor: UnitPointSpec?
38
+ public var scale: PointSpec?
39
+ public var scaleAnchor: UnitPointSpec?
40
+ public var shadow: ShadowSpec?
41
+ public var clipped: Bool
42
+ public var tint: ColorSpec?
43
+ public var position: PointSpec?
44
+ public var badge: String?
45
+ public var listRowInsets: EdgeInsetsSpec?
46
+ public var listRowBackground: ShapeStyleSpec?
47
+ public var imageScale: String?
48
+ public var hidden: Bool
49
+ public var mask: ShapeStyleSpec?
50
+ public var hitTesting: Bool?
51
+ public var blendMode: String?
52
+ public var zIndex: Double?
53
+ public var blur: Double?
54
+ public var brightness: Double?
55
+ public var saturation: Double?
56
+ public var contrast: Double?
57
+ public var grayscale: Double?
58
+ public var hueRotation: Double?
59
+ public var colorInvert: Bool
60
+ public var colorMultiply: ColorSpec?
61
+ public var underline: Bool?
62
+ public var strikethrough: Bool?
63
+ public var italic: Bool?
64
+ /// `.ignoresSafeArea(edges:)`: the edges, or all four for `"all"`. Nil when not set.
65
+ public var ignoresSafeArea: [String]?
66
+ public var ignoresKeyboard: Bool
67
+ public var help: String?
68
+ public var matchedGeometry: String?
69
+ public var contentTransition: String?
70
+
71
+ public init(json: JSONValue) {
72
+ let fields = json.object ?? [:]
73
+ raw = fields
74
+ padding = EdgeInsetsSpec(json: fields.value("padding"))
75
+ font = FontSpec(json: fields.value("font"))
76
+ foreground = ShapeStyleSpec(json: fields.value("foreground"))
77
+ foregroundLevels = (fields.array("foregroundLevels") ?? []).compactMap(ShapeStyleSpec.init(json:))
78
+ background = ShapeStyleSpec(json: fields.value("background"))
79
+ frame = FrameSpec(json: fields.value("frame"))
80
+ cornerRadius = fields.double("cornerRadius")
81
+ backgroundCornerRadius = fields.double("backgroundCornerRadius")
82
+ opacity = fields.double("opacity")
83
+ disabled = fields.bool("disabled")
84
+ layoutPriority = fields.double("layoutPriority")
85
+ lineLimit = fields.int("lineLimit")
86
+ lineLimitLifted = fields["lineLimit"]?.isNull ?? false
87
+ lineLimitMin = fields.int("lineLimitMin")
88
+ multilineTextAlignment = fields.string("multilineTextAlignment")
89
+ fixedSizeHorizontal = fields.object("fixedSize")?.bool("horizontal")
90
+ fixedSizeVertical = fields.object("fixedSize")?.bool("vertical")
91
+ animation = AnimationSpec(json: fields.value("animation"))
92
+ animationToken = fields.double("animationToken")
93
+ labelIcon = fields.bool("labelIcon") ?? false
94
+ transition = fields["transition"].map(TransitionSpec.list(json:))
95
+ if let ratio = fields.object("aspectRatio") {
96
+ aspectRatio = .some(ratio.double("ratio"))
97
+ aspectRatioContentMode = ratio.string("contentMode")
98
+ } else {
99
+ aspectRatio = nil
100
+ aspectRatioContentMode = nil
101
+ }
102
+ offset = PointSpec(json: fields.value("offset"))
103
+ rotation = fields.double("rotation")
104
+ rotationAnchor = UnitPointSpec(json: fields.value("rotationAnchor"))
105
+ scale = PointSpec(json: fields.value("scale"))
106
+ scaleAnchor = UnitPointSpec(json: fields.value("scaleAnchor"))
107
+ shadow = ShadowSpec(json: fields.value("shadow"))
108
+ clipped = fields.bool("clipped") ?? false
109
+ tint = ColorSpec(json: fields.value("tint"))
110
+ position = PointSpec(json: fields.value("position"))
111
+ badge = fields.string("badge")
112
+ listRowInsets = EdgeInsetsSpec(json: fields.value("listRowInsets"))
113
+ listRowBackground = ShapeStyleSpec(json: fields.value("listRowBackground"))
114
+ imageScale = fields.string("imageScale")
115
+ hidden = fields.bool("hidden") ?? false
116
+ mask = ShapeStyleSpec(json: fields.value("mask"))
117
+ hitTesting = fields.bool("hitTesting")
118
+ blendMode = fields.string("blendMode")
119
+ zIndex = fields.double("zIndex")
120
+ blur = fields.double("blur")
121
+ brightness = fields.double("brightness")
122
+ saturation = fields.double("saturation")
123
+ contrast = fields.double("contrast")
124
+ grayscale = fields.double("grayscale")
125
+ hueRotation = fields.double("hueRotation")
126
+ colorInvert = fields.bool("colorInvert") ?? false
127
+ colorMultiply = ColorSpec(json: fields.value("colorMultiply"))
128
+ underline = fields.bool("underline")
129
+ strikethrough = fields.bool("strikethrough")
130
+ italic = fields.bool("italic")
131
+ if fields.string("ignoresSafeArea") == "all" {
132
+ ignoresSafeArea = ["top", "leading", "bottom", "trailing"]
133
+ } else {
134
+ ignoresSafeArea = fields.array("ignoresSafeArea")?.compactMap(\.string)
135
+ }
136
+ ignoresKeyboard = fields.bool("ignoresKeyboard") ?? false
137
+ help = fields.string("help")
138
+ matchedGeometry = fields.string("matchedGeometry")
139
+ contentTransition = fields.object("contentTransition")?.string("kind")
140
+ }
141
+
142
+ public init() {
143
+ self.init(json: .object([:]))
144
+ }
145
+
146
+ /// The style's field names, for the unsupported-field diagnostics and tests.
147
+ public var fieldNames: [String] { raw.keys.sorted() }
148
+ }
@@ -0,0 +1,279 @@
1
+ import Foundation
2
+
3
+ // The value types of docs/ops-protocol.md (Web/src/protocol.ts), read from
4
+ // their JSON. Each `init?(json:)` accepts the forms the protocol defines and
5
+ // answers nil for anything else, so a renderer falls back to a default rather
6
+ // than failing on a field it has not met.
7
+
8
+ /// A color: explicit sRGB, a semantic name (`primary`, `blue`, `systemBackground`…)
9
+ /// with an opacity multiplier, or an asset catalog color set.
10
+ public enum ColorSpec: Equatable, Sendable {
11
+ case rgba(r: Double, g: Double, b: Double, a: Double)
12
+ case named(String, opacity: Double)
13
+ case asset(String, opacity: Double)
14
+
15
+ public init?(json: JSONValue) {
16
+ guard let fields = json.object else { return nil }
17
+ if let r = fields.double("r"), let g = fields.double("g"), let b = fields.double("b") {
18
+ self = .rgba(r: r, g: g, b: b, a: fields.double("a") ?? 1)
19
+ } else if let name = fields.string("name") {
20
+ self = .named(name, opacity: fields.double("a") ?? 1)
21
+ } else if let name = fields.string("named") {
22
+ self = .asset(name, opacity: fields.double("opacity") ?? 1)
23
+ } else {
24
+ return nil
25
+ }
26
+ }
27
+ }
28
+
29
+ /// A unit point in a view's bounds (0...1 on each axis).
30
+ public struct UnitPointSpec: Equatable, Sendable {
31
+ public var x: Double
32
+ public var y: Double
33
+ public init(x: Double, y: Double) {
34
+ self.x = x
35
+ self.y = y
36
+ }
37
+ public init?(json: JSONValue) {
38
+ guard let x = json["x"].double, let y = json["y"].double else { return nil }
39
+ self.init(x: x, y: y)
40
+ }
41
+ }
42
+
43
+ public struct GradientStopSpec: Equatable, Sendable {
44
+ public var color: ColorSpec
45
+ public var location: Double
46
+ public init?(json: JSONValue) {
47
+ guard let color = ColorSpec(json: json["color"]), let location = json["location"].double else { return nil }
48
+ self.color = color
49
+ self.location = location
50
+ }
51
+ }
52
+
53
+ /// Everything a view can be painted with (`ShapeStyle` in protocol.ts).
54
+ public enum ShapeStyleSpec: Equatable, Sendable {
55
+ case color(ColorSpec)
56
+ /// `.primary` ... `.quaternary` (1...4).
57
+ case hierarchical(level: Int, opacity: Double)
58
+ case linearGradient(stops: [GradientStopSpec], start: UnitPointSpec, end: UnitPointSpec, opacity: Double)
59
+ case radialGradient(stops: [GradientStopSpec], center: UnitPointSpec, startRadius: Double, endRadius: Double, opacity: Double)
60
+ case angularGradient(stops: [GradientStopSpec], center: UnitPointSpec, startAngle: Double, endAngle: Double, opacity: Double)
61
+ /// `Color.gradient`: a subtle vertical gradient from one color.
62
+ case colorGradient(ColorSpec, opacity: Double)
63
+ /// `ultraThin` ... `ultraThick`, `glass`, `glassClear`.
64
+ case material(String, opacity: Double)
65
+
66
+ public init?(json: JSONValue) {
67
+ guard let fields = json.object else { return nil }
68
+ let opacity = fields.double("opacity") ?? 1
69
+ if let color = ColorSpec(json: json) {
70
+ self = .color(color)
71
+ } else if let level = fields.int("hierarchical") {
72
+ self = .hierarchical(level: level, opacity: opacity)
73
+ } else if let gradient = fields.object("linearGradient"),
74
+ let start = UnitPointSpec(json: gradient.value("start")), let end = UnitPointSpec(json: gradient.value("end"))
75
+ {
76
+ self = .linearGradient(stops: ShapeStyleSpec.stops(gradient), start: start, end: end, opacity: opacity)
77
+ } else if let gradient = fields.object("radialGradient"), let center = UnitPointSpec(json: gradient.value("center")) {
78
+ self = .radialGradient(
79
+ stops: ShapeStyleSpec.stops(gradient), center: center,
80
+ startRadius: gradient.double("startRadius") ?? 0, endRadius: gradient.double("endRadius") ?? 0, opacity: opacity
81
+ )
82
+ } else if let gradient = fields.object("angularGradient"), let center = UnitPointSpec(json: gradient.value("center")) {
83
+ self = .angularGradient(
84
+ stops: ShapeStyleSpec.stops(gradient), center: center,
85
+ startAngle: gradient.double("startAngle") ?? 0, endAngle: gradient.double("endAngle") ?? 360, opacity: opacity
86
+ )
87
+ } else if let color = ColorSpec(json: fields.value("colorGradient")) {
88
+ self = .colorGradient(color, opacity: opacity)
89
+ } else if let material = fields.string("material") {
90
+ self = .material(material, opacity: opacity)
91
+ } else {
92
+ return nil
93
+ }
94
+ }
95
+
96
+ private static func stops(_ gradient: JSONObject) -> [GradientStopSpec] {
97
+ (gradient.array("stops") ?? []).compactMap(GradientStopSpec.init(json:))
98
+ }
99
+ }
100
+
101
+ /// `padding`, `listRowInsets`: each edge optional.
102
+ public struct EdgeInsetsSpec: Equatable, Sendable {
103
+ public var top: Double?
104
+ public var leading: Double?
105
+ public var bottom: Double?
106
+ public var trailing: Double?
107
+
108
+ public init(top: Double? = nil, leading: Double? = nil, bottom: Double? = nil, trailing: Double? = nil) {
109
+ self.top = top
110
+ self.leading = leading
111
+ self.bottom = bottom
112
+ self.trailing = trailing
113
+ }
114
+
115
+ public init?(json: JSONValue) {
116
+ guard let fields = json.object else { return nil }
117
+ self.init(top: fields.double("top"), leading: fields.double("leading"), bottom: fields.double("bottom"), trailing: fields.double("trailing"))
118
+ }
119
+ }
120
+
121
+ /// `font`: a text style that scales with Dynamic Type, or a fixed size
122
+ /// (`relativeTo` names the style it scales like), with weight and design.
123
+ public struct FontSpec: Equatable, Sendable {
124
+ public var textStyle: String?
125
+ public var size: Double?
126
+ public var relativeTo: String?
127
+ public var weight: String?
128
+ public var design: String?
129
+ public var monospacedDigit: Bool
130
+
131
+ public init?(json: JSONValue) {
132
+ guard let fields = json.object else { return nil }
133
+ textStyle = fields.string("textStyle")
134
+ size = fields.double("size")
135
+ relativeTo = fields.string("relativeTo")
136
+ weight = fields.string("weight")
137
+ design = fields.string("design")
138
+ monospacedDigit = fields.bool("monospacedDigit") ?? false
139
+ }
140
+ }
141
+
142
+ /// A frame dimension: points, or `"infinity"` to fill the available space.
143
+ public enum DimensionSpec: Equatable, Sendable {
144
+ case points(Double)
145
+ case infinity
146
+
147
+ public init?(json: JSONValue) {
148
+ if let value = json.double { self = .points(value) } else if json.string == "infinity" { self = .infinity } else { return nil }
149
+ }
150
+ }
151
+
152
+ /// `.frame(width:height:)` or `.frame(minWidth:…)` with the content's alignment.
153
+ public struct FrameSpec: Equatable, Sendable {
154
+ public var width: Double?
155
+ public var height: Double?
156
+ public var minWidth: DimensionSpec?
157
+ public var maxWidth: DimensionSpec?
158
+ public var minHeight: DimensionSpec?
159
+ public var maxHeight: DimensionSpec?
160
+ public var alignment: String
161
+
162
+ /// Whether this is the flexible form (any min/max), which `.frame(minWidth:…)` draws.
163
+ public var isFlexible: Bool { minWidth != nil || maxWidth != nil || minHeight != nil || maxHeight != nil }
164
+
165
+ public init?(json: JSONValue) {
166
+ guard let fields = json.object else { return nil }
167
+ width = fields.double("width")
168
+ height = fields.double("height")
169
+ minWidth = DimensionSpec(json: fields.value("minWidth"))
170
+ maxWidth = DimensionSpec(json: fields.value("maxWidth"))
171
+ minHeight = DimensionSpec(json: fields.value("minHeight"))
172
+ maxHeight = DimensionSpec(json: fields.value("maxHeight"))
173
+ alignment = fields.string("alignment") ?? "center"
174
+ }
175
+ }
176
+
177
+ /// A `commit`'s animation, or a `styled` element's `.animation(_:value:)`.
178
+ public struct AnimationSpec: Equatable, Sendable {
179
+ /// `default`, `linear`, `easeIn`, `easeOut`, `easeInOut`, `spring`.
180
+ public var type: String
181
+ /// Seconds; 0.35 when absent. A spring's perceptual duration.
182
+ public var duration: Double?
183
+ /// A spring's bounce (0 critically damped).
184
+ public var bounce: Double?
185
+ public var delay: Double?
186
+ public var repeatCount: Int?
187
+ public var repeatsForever: Bool
188
+ public var autoreverses: Bool
189
+
190
+ public init?(json: JSONValue) {
191
+ guard let fields = json.object, let type = fields.string("type") else { return nil }
192
+ self.type = type
193
+ duration = fields.double("duration")
194
+ bounce = fields.double("bounce")
195
+ delay = fields.double("delay")
196
+ let repeatSpec = fields.object("repeat")
197
+ repeatCount = repeatSpec?.int("count")
198
+ repeatsForever = repeatSpec?.string("count") == "forever"
199
+ autoreverses = repeatSpec?.bool("autoreverses") ?? false
200
+ }
201
+
202
+ public var json: JSONValue {
203
+ var fields: JSONObject = ["type": .string(type)]
204
+ if let duration { fields["duration"] = .number(duration) }
205
+ if let bounce { fields["bounce"] = .number(bounce) }
206
+ if let delay { fields["delay"] = .number(delay) }
207
+ if repeatCount != nil || repeatsForever {
208
+ fields["repeat"] = .object([
209
+ "count": repeatsForever ? .string("forever") : .number(Double(repeatCount ?? 1)),
210
+ "autoreverses": .bool(autoreverses),
211
+ ])
212
+ }
213
+ return .object(fields)
214
+ }
215
+ }
216
+
217
+ /// One part of a `transition`: `opacity`, `scale`, `slide`, `move` (with an
218
+ /// edge), `offset` (x, y), `identity`, or `asymmetric` (insertion and removal).
219
+ public struct TransitionSpec: Equatable, Sendable {
220
+ public var kind: String
221
+ public var edge: String?
222
+ public var scale: Double?
223
+ public var x: Double?
224
+ public var y: Double?
225
+ public var insertion: [TransitionSpec]?
226
+ public var removal: [TransitionSpec]?
227
+
228
+ public init?(json: JSONValue) {
229
+ guard let fields = json.object, let kind = fields.string("kind") else { return nil }
230
+ self.kind = kind
231
+ edge = fields.string("edge")
232
+ scale = fields.double("scale")
233
+ x = fields.double("x")
234
+ y = fields.double("y")
235
+ insertion = fields["insertion"].map(TransitionSpec.list(json:))
236
+ removal = fields["removal"].map(TransitionSpec.list(json:))
237
+ }
238
+
239
+ /// A transition field: one part or a combined list of parts.
240
+ public static func list(json: JSONValue) -> [TransitionSpec] {
241
+ if let parts = json.array { return parts.compactMap(TransitionSpec.init(json:)) }
242
+ return TransitionSpec(json: json).map { [$0] } ?? []
243
+ }
244
+ }
245
+
246
+ public struct ShadowSpec: Equatable, Sendable {
247
+ public var color: ColorSpec?
248
+ public var radius: Double
249
+ public var x: Double
250
+ public var y: Double
251
+
252
+ public init?(json: JSONValue) {
253
+ guard let fields = json.object, let radius = fields.double("radius") else { return nil }
254
+ color = ColorSpec(json: fields.value("color"))
255
+ self.radius = radius
256
+ x = fields.double("x") ?? 0
257
+ y = fields.double("y") ?? 0
258
+ }
259
+ }
260
+
261
+ public struct PointSpec: Equatable, Sendable {
262
+ public var x: Double
263
+ public var y: Double
264
+ public init?(json: JSONValue) {
265
+ guard let fields = json.object else { return nil }
266
+ x = fields.double("x") ?? 0
267
+ y = fields.double("y") ?? 0
268
+ }
269
+ }
270
+
271
+ public struct SizeSpec: Equatable, Sendable {
272
+ public var width: Double
273
+ public var height: Double
274
+ public init?(json: JSONValue) {
275
+ guard let width = json["width"].double, let height = json["height"].double else { return nil }
276
+ self.width = width
277
+ self.height = height
278
+ }
279
+ }
@@ -0,0 +1,65 @@
1
+ import Foundation
2
+ import Observation
3
+ import SwiftBrowserProtocol
4
+
5
+ /// One element of the live tree: its id (stable for its life, assigned by the
6
+ /// producer), its kind, its props as the last `create` or `update` left them,
7
+ /// and its children in order.
8
+ ///
9
+ /// Observable, so a SwiftUI view that reads one node's props re-evaluates
10
+ /// when that node changes and not when another does: the granularity the
11
+ /// protocol's `update` op already works at.
12
+ @Observable
13
+ public final class ElementNode: Identifiable {
14
+ public let id: Int
15
+ public let kind: String
16
+ public internal(set) var props: JSONObject
17
+ public internal(set) var children: [ElementNode] = []
18
+ public internal(set) weak var parent: ElementNode?
19
+
20
+ init(id: Int, kind: String, props: JSONObject) {
21
+ self.id = id
22
+ self.kind = kind
23
+ self.props = props
24
+ }
25
+
26
+ /// The root container, element id 0: never created or removed.
27
+ static func root() -> ElementNode { ElementNode(id: 0, kind: "root", props: [:]) }
28
+
29
+ /// The `styled` element's style (an empty style on any other kind).
30
+ public var style: Style { Style(json: props.value("style")) }
31
+
32
+ /// The first child, the one a wrapper kind (`styled`, `button`, `geometry`…) has.
33
+ public var child: ElementNode? { children.first }
34
+
35
+ /// The children of a kind.
36
+ public func children(ofKind kind: String) -> [ElementNode] { children.filter { $0.kind == kind } }
37
+
38
+ /// The children that are not of the given kinds (a screen's content, without its toolbars).
39
+ public func children(except kinds: Set<String>) -> [ElementNode] { children.filter { !kinds.contains($0.kind) } }
40
+
41
+ /// The index of this element among its parent's children, or nil when detached.
42
+ public var indexInParent: Int? { parent?.children.firstIndex { $0 === self } }
43
+
44
+ /// The nearest ancestor of a kind.
45
+ public func ancestor(ofKind kind: String) -> ElementNode? {
46
+ var node = parent
47
+ while let current = node {
48
+ if current.kind == kind { return current }
49
+ node = current.parent
50
+ }
51
+ return nil
52
+ }
53
+
54
+ /// The tree below this node as indented lines, `kind#id {props}`, for tests and logs.
55
+ public func dump(indent: Int = 0) -> String {
56
+ var lines: [String] = []
57
+ func visit(_ node: ElementNode, depth: Int) {
58
+ let props = node.props.isEmpty ? "" : " " + JSONValue.object(node.props).json
59
+ lines.append(String(repeating: " ", count: depth) + "\(node.kind)#\(node.id)\(props)")
60
+ for child in node.children { visit(child, depth: depth + 1) }
61
+ }
62
+ visit(self, depth: indent)
63
+ return lines.joined(separator: "\n")
64
+ }
65
+ }
@@ -0,0 +1,168 @@
1
+ import Foundation
2
+ import SwiftBrowserProtocol
3
+
4
+ public struct OpTreeError: Error, CustomStringConvertible, Equatable {
5
+ public let message: String
6
+ public init(_ message: String) { self.message = message }
7
+ public var description: String { message }
8
+ }
9
+
10
+ /// The live element tree a renderer draws, kept current by applying ops
11
+ /// (docs/ops-protocol.md, "Ops"). The counterpart of the web renderer's
12
+ /// `elements` and `parents` maps (Web/src/renderer.ts), with Observation
13
+ /// doing the invalidation: each `ElementNode` is observable, so whatever
14
+ /// reads a node re-evaluates when an op changes it.
15
+ ///
16
+ /// A pass is the ops up to and including a `commit`; the whole pass is
17
+ /// applied inside one call of `animate`, so a host that sets
18
+ /// `animate` to `withAnimation` gets the protocol's animated commits: every
19
+ /// frame and style change of the pass animates, and children inserted or
20
+ /// removed in it transition. `apply` may be called with several passes at
21
+ /// once, or with part of one (the rest follows before the next commit).
22
+ ///
23
+ /// Main-thread only, like the views reading it.
24
+ @MainActor
25
+ public final class OpTree {
26
+ /// The root container, element id 0.
27
+ public let root: ElementNode
28
+ private var nodes: [Int: ElementNode]
29
+ /// The ops of the current pass, until its `commit`.
30
+ private var pending: [Op] = []
31
+ /// How many passes were committed.
32
+ public private(set) var commits = 0
33
+ /// Ops applied that this tree does not render (`defaults`, `request`…), by name, for diagnostics.
34
+ public private(set) var unhandledOps: [String: Int] = [:]
35
+
36
+ /// Runs the body of a pass, animated as the commit asks (`withAnimation` on a SwiftUI host).
37
+ public var animate: (AnimationSpec?, () -> Void) -> Void = { _, body in body() }
38
+ /// Called after every committed pass.
39
+ public var onCommit: ((AnimationSpec?) -> Void)?
40
+ /// Where the renderer's events go (the app's `dispatch`).
41
+ public var onEvent: ((RendererEvent) -> Void)?
42
+ /// Ops this tree does not apply (a `request`, say), for the host to handle.
43
+ public var onOtherOp: ((String, JSONObject) -> Void)?
44
+
45
+ public init() {
46
+ root = ElementNode.root()
47
+ nodes = [0: root]
48
+ }
49
+
50
+ /// The element with this id, when it exists (created and not removed).
51
+ public func node(_ id: Int) -> ElementNode? { nodes[id] }
52
+
53
+ /// How many elements exist, the root included.
54
+ public var count: Int { nodes.count }
55
+
56
+ /// The root's props (`.preferredColorScheme`).
57
+ public var rootProps: RootProps { RootProps(props: root.props) }
58
+
59
+ /// Sends a renderer event to the app.
60
+ public func send(_ event: RendererEvent) {
61
+ onEvent?(event)
62
+ }
63
+
64
+ /// Applies ops in order. Throws on an op that cannot apply (an unknown
65
+ /// id, an insert into a missing parent); the ops before it have applied.
66
+ public func apply(_ ops: [Op]) throws {
67
+ for op in ops {
68
+ if case .commit(let animation) = op {
69
+ let pass = pending
70
+ pending.removeAll()
71
+ var failure: Error?
72
+ animate(animation) {
73
+ do {
74
+ for each in pass { try self.applyOne(each) }
75
+ } catch {
76
+ failure = error
77
+ }
78
+ }
79
+ if let failure { throw failure }
80
+ commits += 1
81
+ onCommit?(animation)
82
+ } else {
83
+ pending.append(op)
84
+ }
85
+ }
86
+ }
87
+
88
+ /// Applies a JSON array of ops (what crosses the bridge).
89
+ public func apply(json: String) throws {
90
+ try apply(Op.decodeArray(json))
91
+ }
92
+
93
+ /// Applies the ops of a pass that had no `commit` yet, now (headless
94
+ /// streams end without one; tests use it).
95
+ public func flush() throws {
96
+ if pending.isEmpty { return }
97
+ try apply([.commit(animation: nil)])
98
+ }
99
+
100
+ private func applyOne(_ op: Op) throws {
101
+ switch op {
102
+ case .create(let id, let kind, let props):
103
+ guard id != 0 else { throw OpTreeError("create: id 0 is the root") }
104
+ if let existing = nodes[id] {
105
+ // The producer reuses an id only after removing it; a create
106
+ // for a live id replaces it in place (the web renderer does the same).
107
+ detach(existing)
108
+ }
109
+ nodes[id] = ElementNode(id: id, kind: kind, props: props)
110
+ case .update(let id, let props):
111
+ guard let node = nodes[id] else { throw OpTreeError("update: no element \(id)") }
112
+ if node.props != props { node.props = props }
113
+ case .insert(let parentId, let id, let index):
114
+ guard let node = nodes[id] else { throw OpTreeError("insert: no element \(id)") }
115
+ guard let parent = nodes[parentId] else { throw OpTreeError("insert: no parent \(parentId) for element \(id)") }
116
+ guard id != 0, !isAncestor(node, of: parent) else { throw OpTreeError("insert: element \(id) into its own subtree") }
117
+ if let current = node.parent {
118
+ if current === parent, let at = parent.children.firstIndex(where: { $0 === node }) {
119
+ // A move among siblings.
120
+ if at == index { return }
121
+ var children = parent.children
122
+ children.remove(at: at)
123
+ children.insert(node, at: min(index, children.count))
124
+ parent.children = children
125
+ return
126
+ }
127
+ detach(node)
128
+ }
129
+ var children = parent.children
130
+ children.insert(node, at: min(max(index, 0), children.count))
131
+ parent.children = children
132
+ node.parent = parent
133
+ case .remove(let id):
134
+ guard id != 0 else { throw OpTreeError("remove: id 0 is the root") }
135
+ guard let node = nodes[id] else { throw OpTreeError("remove: no element \(id)") }
136
+ detach(node)
137
+ forget(node)
138
+ case .commit:
139
+ break
140
+ case .other(let name, let fields):
141
+ unhandledOps[name, default: 0] += 1
142
+ onOtherOp?(name, fields)
143
+ }
144
+ }
145
+
146
+ private func isAncestor(_ node: ElementNode, of other: ElementNode) -> Bool {
147
+ var current: ElementNode? = other
148
+ while let each = current {
149
+ if each === node { return true }
150
+ current = each.parent
151
+ }
152
+ return false
153
+ }
154
+
155
+ private func detach(_ node: ElementNode) {
156
+ guard let parent = node.parent else { return }
157
+ parent.children.removeAll { $0 === node }
158
+ node.parent = nil
159
+ }
160
+
161
+ private func forget(_ node: ElementNode) {
162
+ nodes[node.id] = nil
163
+ for child in node.children { forget(child) }
164
+ }
165
+
166
+ /// The whole tree as indented lines (tests, logs).
167
+ public func dump() -> String { root.dump() }
168
+ }