@swiftbrowser/native 0.19.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -11
- package/dist/blob.d.ts +85 -11
- package/dist/blob.d.ts.map +1 -1
- package/dist/blob.js +184 -34
- package/dist/blob.js.map +1 -1
- package/dist/bridge.d.ts +33 -2
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js.map +1 -1
- package/dist/fetch.d.ts +19 -11
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +105 -41
- package/dist/fetch.js.map +1 -1
- package/dist/hostRequests.d.ts +3 -1
- package/dist/hostRequests.d.ts.map +1 -1
- package/dist/hostRequests.js +47 -6
- package/dist/hostRequests.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/webSocket.d.ts.map +1 -1
- package/dist/webSocket.js +13 -1
- package/dist/webSocket.js.map +1 -1
- package/package.json +3 -3
- package/src/blob.ts +215 -34
- package/src/bridge.ts +27 -2
- package/src/fetch.ts +99 -41
- package/src/hostRequests.ts +62 -8
- package/src/index.ts +1 -2
- package/src/runtime.ts +1 -1
- package/src/webSocket.ts +10 -1
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ProgramNode.swift +185 -24
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ScopeView.swift +79 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +2075 -213
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Program.swift +407 -16
- package/swift/Packages/SwiftBrowserShader/Package.swift +31 -0
- package/swift/Packages/SwiftBrowserShader/README.md +45 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderRenderer.swift +103 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderView.swift +168 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShaderCore/ShaderProps.swift +120 -0
- package/swift/Sources/SwiftBrowserNative/Host/BodyStream.swift +166 -0
- package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +141 -7
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +47 -1
- package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +98 -29
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeFunctions.swift +1 -1
- package/swift/Sources/SwiftBrowserNative/Runtime/ViewRegistry.swift +57 -0
- package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +11 -7
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +28 -6
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Drawing.swift +378 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +15 -10
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/PopTap.swift +31 -28
- package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +142 -0
- package/swift/Sources/SwiftBrowserProtocol/Drawing.swift +306 -0
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +5 -2
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +24 -3
- package/swift/Sources/SwiftBrowserProtocol/Style.swift +87 -0
- package/swift/Sources/SwiftBrowserProtocol/TextChange.swift +17 -0
- package/wasm/SwiftBrowserNative.wasm +2 -2
- package/dist/native.d.ts +0 -19
- package/dist/native.d.ts.map +0 -1
- package/dist/native.js +0 -17
- package/dist/native.js.map +0 -1
- package/src/native.ts +0 -35
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeRegistry.swift +0 -53
|
@@ -59,6 +59,8 @@ extension View {
|
|
|
59
59
|
}
|
|
60
60
|
// `labelIcon` marks a `Label`'s icon; HStackElement draws the label as one.
|
|
61
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))) }
|
|
62
64
|
|
|
63
65
|
// Layout.
|
|
64
66
|
if style.fixedSizeHorizontal != nil || style.fixedSizeVertical != nil {
|
|
@@ -107,6 +109,7 @@ extension View {
|
|
|
107
109
|
view = AnyView(view.aspectRatio(ratio.map { CGFloat($0) }, contentMode: mode))
|
|
108
110
|
}
|
|
109
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) }
|
|
110
113
|
|
|
111
114
|
// Paint.
|
|
112
115
|
if let background = style.background {
|
|
@@ -119,6 +122,7 @@ extension View {
|
|
|
119
122
|
if let radius = style.cornerRadius { view = AnyView(view.clipShape(RoundedRectangle(cornerRadius: radius, style: .continuous))) }
|
|
120
123
|
if style.clipped { view = AnyView(view.clipped()) }
|
|
121
124
|
if let mask = style.mask { view = AnyView(view.mask { Rectangle().fill(AnyShapeStyle(spec: mask)) }) }
|
|
125
|
+
if style.compositingGroup { view = AnyView(view.compositingGroup()) }
|
|
122
126
|
if let shadow = style.shadow {
|
|
123
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))
|
|
124
128
|
}
|
|
@@ -137,6 +141,12 @@ extension View {
|
|
|
137
141
|
if let rotation = style.rotation {
|
|
138
142
|
view = AnyView(view.rotationEffect(.degrees(rotation), anchor: style.rotationAnchor.map(UnitPoint.init(spec:)) ?? .center))
|
|
139
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
|
+
}
|
|
140
150
|
if let scale = style.scale {
|
|
141
151
|
view = AnyView(view.scaleEffect(x: scale.x, y: scale.y, anchor: style.scaleAnchor.map(UnitPoint.init(spec:)) ?? .center))
|
|
142
152
|
}
|
|
@@ -150,6 +160,11 @@ extension View {
|
|
|
150
160
|
if style.contentShape == "rectangle" { view = AnyView(view.contentShape(Rectangle())) }
|
|
151
161
|
if style.hidden { view = AnyView(view.hidden()) }
|
|
152
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))) }
|
|
153
168
|
// A `ScrollViewReader`'s `.id(x)`: the element's own id names it to a `scrollTo` and a scroll position.
|
|
154
169
|
if style.scrollID, let id { view = AnyView(view.id(id)) }
|
|
155
170
|
if let width = style.raw.double("inspectorColumnWidth") { view = AnyView(view.inspectorColumnWidth(width)) }
|
|
@@ -180,6 +195,133 @@ extension View {
|
|
|
180
195
|
}
|
|
181
196
|
}
|
|
182
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
|
+
|
|
183
325
|
extension View {
|
|
184
326
|
/// `.scrollTransition`: SwiftUI's own, its closure answering each phase
|
|
185
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
|
+
}
|
|
@@ -325,9 +325,12 @@ public struct NavSplitProps: Equatable, Sendable {
|
|
|
325
325
|
public struct NavLinkProps: Equatable, Sendable {
|
|
326
326
|
public var selected: Bool
|
|
327
327
|
public var plain: Bool
|
|
328
|
+
/// A split view sidebar's link: its row's selection takes the tap.
|
|
329
|
+
public var selects: Bool
|
|
328
330
|
public init(props: JSONObject) {
|
|
329
331
|
selected = props.bool("selected") ?? false
|
|
330
332
|
plain = props.string("style") == "plain"
|
|
333
|
+
selects = props.bool("selects") ?? false
|
|
331
334
|
}
|
|
332
335
|
}
|
|
333
336
|
|
|
@@ -576,8 +579,8 @@ public struct TapGestureProps: Equatable, Sendable {
|
|
|
576
579
|
}
|
|
577
580
|
}
|
|
578
581
|
|
|
579
|
-
/// The
|
|
580
|
-
public struct
|
|
582
|
+
/// The `registered` kind: a view registered under `name` (docs/native.md, "Registered views").
|
|
583
|
+
public struct RegisteredProps: Equatable, Sendable {
|
|
581
584
|
public var name: String
|
|
582
585
|
public var props: JSONValue
|
|
583
586
|
public init(props: JSONObject) {
|
|
@@ -158,9 +158,9 @@ public struct RendererEvent: Equatable, Sendable {
|
|
|
158
158
|
}
|
|
159
159
|
/// A `menu` opened or closed (lazy menus build their items on `open`).
|
|
160
160
|
public static func menu(_ id: Int, open: Bool) -> RendererEvent { with("menu", id: id, ["open": .bool(open)]) }
|
|
161
|
-
///
|
|
162
|
-
public static func
|
|
163
|
-
with("
|
|
161
|
+
/// What a `registered` element's view sent (docs/native.md, `ViewRegistry`).
|
|
162
|
+
public static func registered(_ id: Int, name: String, value: JSONValue) -> RendererEvent {
|
|
163
|
+
with("registered", id: id, ["name": .string(name), "value": value])
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
/// The device appearance, text size or size class (sent once at launch and on change).
|
|
@@ -178,6 +178,27 @@ public struct RendererEvent: Equatable, Sendable {
|
|
|
178
178
|
return RendererEvent(type: "environment", fields: fields)
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/// A styled box with `hover` was entered (`true`) or left by the pointer (`.onHover`).
|
|
182
|
+
public static func hover(_ id: Int, _ hovering: Bool) -> RendererEvent { with("hover", id: id, ["value": .bool(hovering)]) }
|
|
183
|
+
/// A styled box with `scrollVisibility` crossed its threshold of the scroll view's visible area.
|
|
184
|
+
public static func scrollVisibility(_ id: Int, visible: Bool) -> RendererEvent { with("scrollvisibility", id: id, ["visible": .bool(visible)]) }
|
|
185
|
+
/// A `draggable` was lifted: the app makes its payload now and keeps it until `dragend`.
|
|
186
|
+
public static func dragStart(_ id: Int) -> RendererEvent { with("dragstart", id: id) }
|
|
187
|
+
/// The lifted `draggable` was dropped.
|
|
188
|
+
public static func dragEnd(_ id: Int) -> RendererEvent { with("dragend", id: id) }
|
|
189
|
+
/// A drag over a `dropdestination`: `entered`, `exited` or `performed`, with the lifted `draggable`
|
|
190
|
+
/// (`source`) or what came from outside the app (`items`); the point in the destination's own box.
|
|
191
|
+
public static func drop(_ id: Int, phase: String, source: Int? = nil, items: [JSONValue]? = nil, x: Double = 0, y: Double = 0) -> RendererEvent {
|
|
192
|
+
var fields: JSONObject = ["phase": .string(phase), "x": .number(x), "y": .number(y)]
|
|
193
|
+
if let source { fields["source"] = .number(Double(source)) }
|
|
194
|
+
if let items { fields["items"] = .array(items) }
|
|
195
|
+
return with("drop", id: id, fields)
|
|
196
|
+
}
|
|
197
|
+
/// A `layout` element's record: the proposal it was placed with and each child's min, ideal and max sizes.
|
|
198
|
+
public static func layout(_ id: Int, proposal: LayoutProposalSpec, children: [LayoutMeasurementSpec]) -> RendererEvent {
|
|
199
|
+
with("layout", id: id, ["proposal": proposal.json, "children": .array(children.map(\.json))])
|
|
200
|
+
}
|
|
201
|
+
|
|
181
202
|
/// Several events delivered at once; the app renders once for all of them.
|
|
182
203
|
public static func batch(_ events: [RendererEvent]) -> RendererEvent {
|
|
183
204
|
RendererEvent(type: "batch", fields: ["events": .array(events.map(\.json))])
|
|
@@ -38,6 +38,7 @@ public struct Style: Equatable, Sendable {
|
|
|
38
38
|
public var scale: PointSpec?
|
|
39
39
|
public var scaleAnchor: UnitPointSpec?
|
|
40
40
|
public var shadow: ShadowSpec?
|
|
41
|
+
public var compositingGroup: Bool
|
|
41
42
|
public var clipped: Bool
|
|
42
43
|
public var tint: ColorSpec?
|
|
43
44
|
public var position: PointSpec?
|
|
@@ -77,6 +78,22 @@ public struct Style: Equatable, Sendable {
|
|
|
77
78
|
public var scrollTo: ScrollToSpec?
|
|
78
79
|
/// `.scrollTransition`: the effects in its three phases.
|
|
79
80
|
public var scrollTransition: ScrollTransitionSpec?
|
|
81
|
+
/// `.gridCellColumns(_:)`: the `grid` columns the cell spans.
|
|
82
|
+
public var gridCellColumns: Int?
|
|
83
|
+
/// `.gridCellAnchor(_:)`: the unit point the cell aligns by in its cell.
|
|
84
|
+
public var gridCellAnchor: UnitPointSpec?
|
|
85
|
+
/// `.onScrollVisibilityChange(threshold:)`: the share of the box that counts as visible; the box sends `scrollvisibility`.
|
|
86
|
+
public var scrollVisibilityThreshold: Double?
|
|
87
|
+
/// `.alignmentGuide(_:computeValue:)`, as `a + b·width + c·height`.
|
|
88
|
+
public var alignmentGuide: AlignmentGuideSpec?
|
|
89
|
+
/// `.rotation3DEffect(_:axis:anchor:perspective:)`.
|
|
90
|
+
public var rotation3D: Rotation3DSpec?
|
|
91
|
+
/// `.onHover`: the box sends `hover` as a pointer enters and leaves it.
|
|
92
|
+
public var hover: Bool
|
|
93
|
+
/// `.pointerStyle(_:)`: the pointer over the box, by its CSS name (`pointer`, `text`, `grab`…).
|
|
94
|
+
public var pointerStyle: String?
|
|
95
|
+
/// `.symbolEffect(_:options:isActive:)` / `.symbolEffect(_:options:value:)`.
|
|
96
|
+
public var symbolEffect: SymbolEffectSpec?
|
|
80
97
|
|
|
81
98
|
public init(json: JSONValue) {
|
|
82
99
|
let fields = json.object ?? [:]
|
|
@@ -115,6 +132,7 @@ public struct Style: Equatable, Sendable {
|
|
|
115
132
|
scale = PointSpec(json: fields.value("scale"))
|
|
116
133
|
scaleAnchor = UnitPointSpec(json: fields.value("scaleAnchor"))
|
|
117
134
|
shadow = ShadowSpec(json: fields.value("shadow"))
|
|
135
|
+
compositingGroup = fields.bool("compositingGroup") ?? false
|
|
118
136
|
clipped = fields.bool("clipped") ?? false
|
|
119
137
|
tint = ColorSpec(json: fields.value("tint"))
|
|
120
138
|
position = PointSpec(json: fields.value("position"))
|
|
@@ -152,6 +170,14 @@ public struct Style: Equatable, Sendable {
|
|
|
152
170
|
scrollID = fields.bool("scrollID") ?? false
|
|
153
171
|
scrollTo = ScrollToSpec(json: fields.value("scrollTo"))
|
|
154
172
|
scrollTransition = ScrollTransitionSpec(json: fields.value("scrollTransition"))
|
|
173
|
+
gridCellColumns = fields.int("gridCellColumns")
|
|
174
|
+
gridCellAnchor = UnitPointSpec(json: fields.value("gridCellAnchor"))
|
|
175
|
+
scrollVisibilityThreshold = fields.object("scrollVisibility")?.double("threshold")
|
|
176
|
+
alignmentGuide = AlignmentGuideSpec(json: fields.value("alignmentGuide"))
|
|
177
|
+
rotation3D = Rotation3DSpec(json: fields.value("rotation3D"))
|
|
178
|
+
hover = fields.bool("hover") ?? false
|
|
179
|
+
pointerStyle = fields.string("pointerStyle")
|
|
180
|
+
symbolEffect = SymbolEffectSpec(json: fields.value("symbolEffect"))
|
|
155
181
|
}
|
|
156
182
|
|
|
157
183
|
public init() {
|
|
@@ -162,6 +188,67 @@ public struct Style: Equatable, Sendable {
|
|
|
162
188
|
public var fieldNames: [String] { raw.keys.sorted() }
|
|
163
189
|
}
|
|
164
190
|
|
|
191
|
+
/// `.alignmentGuide(_:computeValue:)`: the box's guide `name` on `axis` is `a + b·width + c·height`.
|
|
192
|
+
public struct AlignmentGuideSpec: Equatable, Sendable {
|
|
193
|
+
/// `horizontal` (a `HorizontalAlignment`: leading, center, trailing…) or `vertical`.
|
|
194
|
+
public var axis: String
|
|
195
|
+
public var name: String
|
|
196
|
+
public var a: Double
|
|
197
|
+
public var b: Double
|
|
198
|
+
public var c: Double
|
|
199
|
+
public init?(json: JSONValue) {
|
|
200
|
+
guard let fields = json.object, let axis = fields.string("axis"), let name = fields.string("name") else { return nil }
|
|
201
|
+
let value = (fields.array("value") ?? []).compactMap(\.double)
|
|
202
|
+
self.axis = axis
|
|
203
|
+
self.name = name
|
|
204
|
+
a = value.count > 0 ? value[0] : 0
|
|
205
|
+
b = value.count > 1 ? value[1] : 0
|
|
206
|
+
c = value.count > 2 ? value[2] : 0
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// `.rotation3DEffect`: degrees about `axis`, around `anchor`, with SwiftUI's perspective factor.
|
|
211
|
+
public struct Rotation3DSpec: Equatable, Sendable {
|
|
212
|
+
public var angle: Double
|
|
213
|
+
public var x: Double
|
|
214
|
+
public var y: Double
|
|
215
|
+
public var z: Double
|
|
216
|
+
public var anchor: UnitPointSpec?
|
|
217
|
+
public var perspective: Double
|
|
218
|
+
public init?(json: JSONValue) {
|
|
219
|
+
guard let fields = json.object, let angle = fields.double("angle") else { return nil }
|
|
220
|
+
self.angle = angle
|
|
221
|
+
let axis = fields.object("axis") ?? [:]
|
|
222
|
+
x = axis.double("x") ?? 0
|
|
223
|
+
y = axis.double("y") ?? 0
|
|
224
|
+
z = axis.double("z") ?? 0
|
|
225
|
+
anchor = UnitPointSpec(json: fields.value("anchor"))
|
|
226
|
+
perspective = fields.double("perspective") ?? 1
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// An SF Symbols effect: `effect` by name (`bounce`, `pulse`, `variableColor`, `scale`, `rotate`, `wiggle`,
|
|
231
|
+
/// `breathe`, `appear`, `disappear`), run while the style has it, or once each time `trigger` changes.
|
|
232
|
+
public struct SymbolEffectSpec: Equatable, Sendable {
|
|
233
|
+
public var effect: String
|
|
234
|
+
/// `up` or `down`.
|
|
235
|
+
public var direction: String?
|
|
236
|
+
public var speed: Double?
|
|
237
|
+
/// How many times a run repeats; nil with `forever` false for the effect's own.
|
|
238
|
+
public var repeatCount: Int?
|
|
239
|
+
public var forever: Bool
|
|
240
|
+
public var trigger: Double?
|
|
241
|
+
public init?(json: JSONValue) {
|
|
242
|
+
guard let fields = json.object, let effect = fields.string("effect") else { return nil }
|
|
243
|
+
self.effect = effect
|
|
244
|
+
direction = fields.string("direction")
|
|
245
|
+
speed = fields.double("speed")
|
|
246
|
+
repeatCount = fields.int("repeat")
|
|
247
|
+
forever = fields.string("repeat") == "forever"
|
|
248
|
+
trigger = fields.double("trigger")
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
165
252
|
/// `.containerRelativeFrame(axes)` as the style hint carries it: `length = a × container + b`.
|
|
166
253
|
public struct ContainerRelativeFrameSpec: Equatable, Sendable {
|
|
167
254
|
public var horizontal: Bool
|