@swiftbrowser/native 0.10.4 → 0.12.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 +26 -0
- package/bin/swiftbrowser-native.mjs +25 -5
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +15 -4
- package/dist/build.js.map +1 -1
- package/dist/site.d.ts +52 -0
- package/dist/site.d.ts.map +1 -0
- package/dist/site.js +74 -0
- package/dist/site.js.map +1 -0
- package/dist/wasmHost.d.ts +6 -32
- package/dist/wasmHost.d.ts.map +1 -1
- package/dist/wasmHost.js +6 -117
- package/dist/wasmHost.js.map +1 -1
- package/package.json +8 -3
- package/src/build.ts +16 -4
- package/src/site.ts +111 -0
- package/src/wasmHost.ts +6 -145
- package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +9 -1
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Charts.swift +340 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Inspector.swift +29 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +78 -14
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +58 -6
- package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +25 -7
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +176 -0
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +2 -0
- package/swift/Sources/SwiftBrowserProtocol/Style.swift +41 -0
- package/wasm/SwiftBrowserNative.wasm +4 -0
|
@@ -15,10 +15,19 @@ struct NavigationStackElement: View {
|
|
|
15
15
|
@Environment(\.nativeApp) private var app
|
|
16
16
|
|
|
17
17
|
var body: some View {
|
|
18
|
-
|
|
18
|
+
Self.stack(for: node, app: app)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// The stack itself, with its root screen and the pushed ones as its
|
|
22
|
+
/// path. A split view's detail column is one of these directly (not a
|
|
23
|
+
/// view around one): the shim wraps a detail that is not a
|
|
24
|
+
/// `NavigationStack` in a stack of its own, and a stack nested in a
|
|
25
|
+
/// stack is flattened, its path with it.
|
|
26
|
+
static func stack(for node: ElementNode?, app: NativeApp?) -> NavigationStack<[Int], AnyView> {
|
|
27
|
+
let screens = node?.children.filter { $0.kind == "navscreen" } ?? []
|
|
19
28
|
let pushed = screens.dropFirst().map(\.id)
|
|
20
29
|
let path = Binding<[Int]>(
|
|
21
|
-
get: { node
|
|
30
|
+
get: { node?.children.filter { $0.kind == "navscreen" }.dropFirst().map(\.id) ?? [] },
|
|
22
31
|
set: { next in
|
|
23
32
|
// Only pops come from here (pushes come from the app).
|
|
24
33
|
for id in pushed where !next.contains(id) {
|
|
@@ -26,21 +35,69 @@ struct NavigationStackElement: View {
|
|
|
26
35
|
}
|
|
27
36
|
}
|
|
28
37
|
)
|
|
29
|
-
NavigationStack(path: path) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
return NavigationStack(path: path) {
|
|
39
|
+
AnyView(
|
|
40
|
+
Group {
|
|
41
|
+
if let root = screens.first {
|
|
42
|
+
NavigationScreenElement(node: root)
|
|
43
|
+
}
|
|
33
44
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
.navigationDestination(for: Int.self) { id in
|
|
46
|
+
if let screen = node?.children.first(where: { $0.id == id }) {
|
|
47
|
+
NavigationScreenElement(node: screen)
|
|
48
|
+
} else {
|
|
49
|
+
// Popped by the app already; SwiftUI is on its way out.
|
|
50
|
+
Color.clear
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// `navsplit`: the sidebar's and the detail's stacks (its two `navstack`
|
|
59
|
+
/// children, in that order) as the columns of a split view. The app emits
|
|
60
|
+
/// it in a regular width only; in a compact width it emits one stack rooted
|
|
61
|
+
/// at the sidebar, as iOS shows. `sidebarVisible` is the column visibility
|
|
62
|
+
/// (`detailOnly` hides the sidebar); the sidebar toggle sends `tap` with the
|
|
63
|
+
/// split's id, and the app flips the visibility, as the protocol says.
|
|
64
|
+
struct NavigationSplitElement: View {
|
|
65
|
+
let node: ElementNode
|
|
66
|
+
@Environment(\.nativeApp) private var app
|
|
67
|
+
|
|
68
|
+
var body: some View {
|
|
69
|
+
let columns = node.children.filter { $0.kind == "navstack" }
|
|
70
|
+
let sidebar = columns.first
|
|
71
|
+
let detail = columns.count > 1 ? columns[1] : nil
|
|
72
|
+
let visibility = Binding<NavigationSplitViewVisibility>(
|
|
73
|
+
get: { NavSplitProps(props: node.props).sidebarVisible ? .all : .detailOnly },
|
|
74
|
+
set: { next in
|
|
75
|
+
if (next == .detailOnly) == NavSplitProps(props: node.props).sidebarVisible {
|
|
76
|
+
app?.dispatch(.tap(node.id))
|
|
41
77
|
}
|
|
42
78
|
}
|
|
79
|
+
)
|
|
80
|
+
NavigationSplitView(columnVisibility: visibility) {
|
|
81
|
+
sidebarColumn(sidebar)
|
|
82
|
+
} detail: {
|
|
83
|
+
NavigationStackElement.stack(for: detail, app: app)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// The sidebar column: natively its stack (the column pushes within
|
|
88
|
+
/// itself); against the shim its root screen alone, since the shim gives
|
|
89
|
+
/// the sidebar a stack of its own and would flatten this one into it.
|
|
90
|
+
@ViewBuilder
|
|
91
|
+
private func sidebarColumn(_ sidebar: ElementNode?) -> some View {
|
|
92
|
+
#if canImport(FoundationBridge)
|
|
93
|
+
if let root = sidebar?.children.first(where: { $0.kind == "navscreen" }) {
|
|
94
|
+
NavigationScreenElement(node: root)
|
|
43
95
|
}
|
|
96
|
+
#else
|
|
97
|
+
if let sidebar {
|
|
98
|
+
NavigationStackElement(node: sidebar)
|
|
99
|
+
}
|
|
100
|
+
#endif
|
|
44
101
|
}
|
|
45
102
|
}
|
|
46
103
|
|
|
@@ -98,7 +155,14 @@ struct NavigationScreenElement: View {
|
|
|
98
155
|
case "principal": return .principal
|
|
99
156
|
case "secondary": return .secondaryAction
|
|
100
157
|
case "bottom": return bottomPlacement
|
|
101
|
-
|
|
158
|
+
case "largeTitle":
|
|
159
|
+
// Beside the large title: the shim's own placement; SwiftUI has none, so the bar's trailing edge.
|
|
160
|
+
#if canImport(FoundationBridge)
|
|
161
|
+
return .largeTitle
|
|
162
|
+
#else
|
|
163
|
+
return trailingPlacement
|
|
164
|
+
#endif
|
|
165
|
+
default: return trailingPlacement
|
|
102
166
|
}
|
|
103
167
|
}
|
|
104
168
|
}
|
|
@@ -11,6 +11,7 @@ struct VStackElement: View {
|
|
|
11
11
|
VStack(alignment: HorizontalAlignment(name: props.alignment), spacing: props.spacing.map { CGFloat($0) }) {
|
|
12
12
|
ChildrenView(node: node)
|
|
13
13
|
}
|
|
14
|
+
.scrollTargetLayout(isEnabled: props.scrollTarget)
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -32,6 +33,7 @@ struct HStackElement: View {
|
|
|
32
33
|
HStack(alignment: VerticalAlignment(name: props.alignment), spacing: props.spacing.map { CGFloat($0) }) {
|
|
33
34
|
ChildrenView(node: node)
|
|
34
35
|
}
|
|
36
|
+
.scrollTargetLayout(isEnabled: props.scrollTarget)
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
}
|
|
@@ -106,21 +108,71 @@ struct GeometryElement: View {
|
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
/// `scrollview`: its axes and indicators; `refreshable` sends `refresh`;
|
|
112
|
+
/// `snap` is its scroll target behavior (the stack inside marked
|
|
113
|
+
/// `scrollTarget` is the layout whose children snap); `reportsPosition`
|
|
114
|
+
/// is `.scrollPosition(id:)`, which sends `scrollposition` with the marked
|
|
115
|
+
/// element at the anchor once the view settles. A `ScrollViewReader`'s
|
|
116
|
+
/// `proxy.scrollTo(x)` arrives as a `scrollTo` hint, with a new serial each
|
|
117
|
+
/// time, on the marked element it names (`scrollID`, `.id(x)`): the view
|
|
118
|
+
/// finds the latest in its content and scrolls to it.
|
|
109
119
|
struct ScrollViewElement: View {
|
|
110
120
|
let node: ElementNode
|
|
111
121
|
@Environment(\.nativeApp) private var app
|
|
122
|
+
@State private var position: Int?
|
|
123
|
+
@State private var served: Double = -1
|
|
112
124
|
|
|
113
125
|
var body: some View {
|
|
114
126
|
let props = ScrollViewProps(props: node.props)
|
|
115
127
|
let axes: Axis.Set = props.axes == "horizontal" ? .horizontal : props.axes == "both" ? [.horizontal, .vertical] : .vertical
|
|
116
|
-
let
|
|
117
|
-
|
|
128
|
+
let request = Self.scrollRequest(in: node)
|
|
129
|
+
ScrollViewReader { proxy in
|
|
130
|
+
let scroll = ScrollView(axes, showsIndicators: props.showsIndicators) {
|
|
131
|
+
ChildrenView(node: node)
|
|
132
|
+
}
|
|
133
|
+
let refreshed: AnyView = props.refreshable ? AnyView(scroll.refreshable { app?.dispatch(.refresh(node.id)) }) : AnyView(scroll)
|
|
134
|
+
let snapped: AnyView = props.snap == "paging" ? AnyView(refreshed.scrollTargetBehavior(.paging))
|
|
135
|
+
: props.snap == "viewAligned" ? AnyView(refreshed.scrollTargetBehavior(.viewAligned)) : refreshed
|
|
136
|
+
let positioned: AnyView = props.reportsPosition
|
|
137
|
+
? AnyView(snapped.scrollPosition(id: Binding(get: { position }, set: { next in
|
|
138
|
+
guard next != position else { return }
|
|
139
|
+
position = next
|
|
140
|
+
if let next { app?.dispatch(.scrollPosition(next)) }
|
|
141
|
+
}), anchor: props.positionAnchor.map(UnitPoint.init(spec:))))
|
|
142
|
+
: snapped
|
|
143
|
+
positioned
|
|
144
|
+
.onAppear { perform(request, with: proxy) }
|
|
145
|
+
.onChange(of: request?.serial) { _, _ in perform(request, with: proxy) }
|
|
118
146
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private func perform(_ request: ScrollRequest?, with proxy: ScrollViewProxy) {
|
|
150
|
+
guard let request, request.serial > served else { return }
|
|
151
|
+
served = request.serial
|
|
152
|
+
position = request.id
|
|
153
|
+
proxy.scrollTo(request.id, anchor: request.anchor)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
struct ScrollRequest: Equatable {
|
|
157
|
+
var serial: Double
|
|
158
|
+
var id: Int
|
|
159
|
+
var anchor: UnitPoint?
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// The newest `scrollTo` among the content's marked elements (not those of a scroll view nested inside).
|
|
163
|
+
static func scrollRequest(in node: ElementNode) -> ScrollRequest? {
|
|
164
|
+
var best: ScrollRequest?
|
|
165
|
+
func visit(_ element: ElementNode) {
|
|
166
|
+
for child in element.children {
|
|
167
|
+
if child.kind == "scrollview" || child.kind == "list" { continue }
|
|
168
|
+
if child.kind == "styled", let request = child.style.scrollTo, best == nil || request.serial > best!.serial {
|
|
169
|
+
best = ScrollRequest(serial: request.serial, id: child.id, anchor: request.anchor.map(UnitPoint.init(spec:)))
|
|
170
|
+
}
|
|
171
|
+
visit(child)
|
|
172
|
+
}
|
|
123
173
|
}
|
|
174
|
+
visit(node)
|
|
175
|
+
return best
|
|
124
176
|
}
|
|
125
177
|
}
|
|
126
178
|
|
|
@@ -11,7 +11,7 @@ import SwiftUI
|
|
|
11
11
|
/// Type-erased step by step: a chain of sixty optional modifiers would
|
|
12
12
|
/// otherwise be one type the compiler has to name.
|
|
13
13
|
extension View {
|
|
14
|
-
func styled(_ style: Style) -> AnyView {
|
|
14
|
+
func styled(_ style: Style, id: Int? = nil) -> AnyView {
|
|
15
15
|
var view = AnyView(self)
|
|
16
16
|
|
|
17
17
|
// Text attributes, which flow down the environment. A font with only a
|
|
@@ -71,16 +71,31 @@ extension View {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
if let frame = style.frame {
|
|
74
|
-
|
|
74
|
+
// Per axis, a fixed length wins over a min/max, as the web renderer
|
|
75
|
+
// lays it out: `{ maxWidth: "infinity", height: 1 }` is
|
|
76
|
+
// `.frame(height: 1).frame(maxWidth: .infinity)`, a rule across
|
|
77
|
+
// the width.
|
|
78
|
+
let alignment = Alignment(name: frame.alignment)
|
|
79
|
+
if frame.width != nil || frame.height != nil {
|
|
80
|
+
view = AnyView(view.frame(width: frame.width.map { CGFloat($0) }, height: frame.height.map { CGFloat($0) }, alignment: alignment))
|
|
81
|
+
}
|
|
82
|
+
let flexibleWidth = frame.width == nil && (frame.minWidth != nil || frame.maxWidth != nil)
|
|
83
|
+
let flexibleHeight = frame.height == nil && (frame.minHeight != nil || frame.maxHeight != nil)
|
|
84
|
+
if flexibleWidth || flexibleHeight {
|
|
75
85
|
view = AnyView(view.frame(
|
|
76
|
-
minWidth: CGFloat(dimension: frame.minWidth), maxWidth: CGFloat(dimension: frame.maxWidth),
|
|
77
|
-
minHeight: CGFloat(dimension: frame.minHeight), maxHeight: CGFloat(dimension: frame.maxHeight),
|
|
78
|
-
alignment:
|
|
86
|
+
minWidth: flexibleWidth ? CGFloat(dimension: frame.minWidth) : nil, maxWidth: flexibleWidth ? CGFloat(dimension: frame.maxWidth) : nil,
|
|
87
|
+
minHeight: flexibleHeight ? CGFloat(dimension: frame.minHeight) : nil, maxHeight: flexibleHeight ? CGFloat(dimension: frame.maxHeight) : nil,
|
|
88
|
+
alignment: alignment
|
|
79
89
|
))
|
|
80
|
-
} else {
|
|
81
|
-
view = AnyView(view.frame(width: frame.width.map { CGFloat($0) }, height: frame.height.map { CGFloat($0) }, alignment: Alignment(name: frame.alignment)))
|
|
82
90
|
}
|
|
83
91
|
}
|
|
92
|
+
if let relative = style.containerRelativeFrame {
|
|
93
|
+
var axes: Axis.Set = []
|
|
94
|
+
if relative.horizontal { axes.insert(.horizontal) }
|
|
95
|
+
if relative.vertical { axes.insert(.vertical) }
|
|
96
|
+
let a = relative.a, b = relative.b
|
|
97
|
+
view = AnyView(view.containerRelativeFrame(axes, alignment: Alignment(name: relative.alignment)) { length, _ in a * length + b })
|
|
98
|
+
}
|
|
84
99
|
if let ratio = style.aspectRatio {
|
|
85
100
|
let mode: ContentMode = style.aspectRatioContentMode == "fill" ? .fill : .fit
|
|
86
101
|
view = AnyView(view.aspectRatio(ratio.map { CGFloat($0) }, contentMode: mode))
|
|
@@ -127,6 +142,9 @@ extension View {
|
|
|
127
142
|
if style.hitTesting == false { view = AnyView(view.allowsHitTesting(false)) }
|
|
128
143
|
if style.hidden { view = AnyView(view.hidden()) }
|
|
129
144
|
if let help = style.help { view = AnyView(view.help(help)) }
|
|
145
|
+
// A `ScrollViewReader`'s `.id(x)`: the element's own id names it to a `scrollTo` and a scroll position.
|
|
146
|
+
if style.scrollID, let id { view = AnyView(view.id(id)) }
|
|
147
|
+
if let width = style.raw.double("inspectorColumnWidth") { view = AnyView(view.inspectorColumnWidth(width)) }
|
|
130
148
|
|
|
131
149
|
// Lists.
|
|
132
150
|
if let badge = style.badge { view = AnyView(view.badge(Text(badge))) }
|
|
@@ -54,9 +54,13 @@ public struct StackProps: Equatable, Sendable {
|
|
|
54
54
|
/// A `VStack`'s horizontal alignment, an `HStack`'s vertical one, a `ZStack`'s frame alignment.
|
|
55
55
|
public var alignment: String
|
|
56
56
|
|
|
57
|
+
/// `.scrollTargetLayout()`: the stack's children are the snap points of the scroll view around it.
|
|
58
|
+
public var scrollTarget: Bool
|
|
59
|
+
|
|
57
60
|
public init(props: JSONObject) {
|
|
58
61
|
spacing = props.double("spacing")
|
|
59
62
|
alignment = props.string("alignment") ?? "center"
|
|
63
|
+
scrollTarget = props.bool("scrollTarget") ?? false
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
|
|
@@ -111,13 +115,27 @@ public struct ScrollViewProps: Equatable, Sendable {
|
|
|
111
115
|
public var axes: String
|
|
112
116
|
public var showsIndicators: Bool
|
|
113
117
|
public var refreshable: Bool
|
|
118
|
+
/// `.scrollTargetBehavior`: `viewAligned` or `paging`.
|
|
119
|
+
public var snap: String?
|
|
120
|
+
/// `.scrollPosition(id:)`: the view reports the marked element at its anchor as it settles.
|
|
121
|
+
public var reportsPosition: Bool
|
|
122
|
+
public var positionAnchor: UnitPointSpec?
|
|
114
123
|
public init(props: JSONObject) {
|
|
115
124
|
axes = props.string("axes") ?? "vertical"
|
|
116
125
|
showsIndicators = props.bool("showsIndicators") ?? true
|
|
117
126
|
refreshable = props.bool("refreshable") ?? false
|
|
127
|
+
snap = props.string("snap")
|
|
128
|
+
reportsPosition = props.bool("reportsPosition") ?? false
|
|
129
|
+
positionAnchor = UnitPointSpec(json: props.value("positionAnchor"))
|
|
118
130
|
}
|
|
119
131
|
}
|
|
120
132
|
|
|
133
|
+
/// `inspector`: the modified view's elements, then an `inspectorpanel`.
|
|
134
|
+
public struct InspectorProps: Equatable, Sendable {
|
|
135
|
+
public var presented: Bool
|
|
136
|
+
public init(props: JSONObject) { presented = props.bool("presented") ?? false }
|
|
137
|
+
}
|
|
138
|
+
|
|
121
139
|
public struct ListProps: Equatable, Sendable {
|
|
122
140
|
/// `insetGrouped` (the default), `grouped`, `plain`, `sidebar`.
|
|
123
141
|
public var style: String
|
|
@@ -183,6 +201,12 @@ public struct NavScreenProps: Equatable, Sendable {
|
|
|
183
201
|
}
|
|
184
202
|
}
|
|
185
203
|
|
|
204
|
+
public struct NavSplitProps: Equatable, Sendable {
|
|
205
|
+
/// Whether the sidebar column shows (`false` hides it).
|
|
206
|
+
public var sidebarVisible: Bool
|
|
207
|
+
public init(props: JSONObject) { sidebarVisible = props.bool("sidebarVisible") ?? true }
|
|
208
|
+
}
|
|
209
|
+
|
|
186
210
|
public struct NavLinkProps: Equatable, Sendable {
|
|
187
211
|
public var selected: Bool
|
|
188
212
|
public var plain: Bool
|
|
@@ -467,3 +491,155 @@ public struct SizeReportProps: Equatable, Sendable {
|
|
|
467
491
|
reader = props.int("reader")
|
|
468
492
|
}
|
|
469
493
|
}
|
|
494
|
+
|
|
495
|
+
// MARK: - Charts
|
|
496
|
+
|
|
497
|
+
/// `chart`: the marks in unit coordinates of the plot (x 0…1 left to right,
|
|
498
|
+
/// y 0…1 top to bottom), the axes with their grid lines, ticks and labels
|
|
499
|
+
/// already placed and formatted, and the legend (docs/ops-protocol.md,
|
|
500
|
+
/// "Phase 7"). A renderer measures the labels, lays the plot out and draws.
|
|
501
|
+
public struct ChartProps: Equatable, Sendable {
|
|
502
|
+
public var marks: [ChartMarkSpec]
|
|
503
|
+
public var xAxis: ChartAxisSpec?
|
|
504
|
+
public var yAxis: ChartAxisSpec?
|
|
505
|
+
public var legend: ChartLegendSpec?
|
|
506
|
+
public init(props: JSONObject) {
|
|
507
|
+
marks = (props.array("marks") ?? []).compactMap(ChartMarkSpec.init(json:))
|
|
508
|
+
xAxis = ChartAxisSpec(json: props.value("xAxis"))
|
|
509
|
+
yAxis = ChartAxisSpec(json: props.value("yAxis"))
|
|
510
|
+
legend = ChartLegendSpec(json: props.value("legend"))
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/// A point of a line (`y`) or an area (`y0 ≤ y1`) mark.
|
|
515
|
+
public struct ChartPointSpec: Equatable, Sendable {
|
|
516
|
+
public var x: Double
|
|
517
|
+
public var y: Double
|
|
518
|
+
public var y0: Double
|
|
519
|
+
public var y1: Double
|
|
520
|
+
init?(json: JSONValue) {
|
|
521
|
+
guard let x = json["x"].double else { return nil }
|
|
522
|
+
self.x = x
|
|
523
|
+
y = json["y"].double ?? json["y0"].double ?? 0
|
|
524
|
+
y0 = json["y0"].double ?? y
|
|
525
|
+
y1 = json["y1"].double ?? y
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/// One mark: `rect` (bars), `line`, `area`, `point` or `rule`.
|
|
530
|
+
public struct ChartMarkSpec: Equatable, Sendable {
|
|
531
|
+
public var kind: String
|
|
532
|
+
public var style: ShapeStyleSpec?
|
|
533
|
+
public var opacity: Double
|
|
534
|
+
/// `rect` and `rule`: the unit rect (a rule's two ends); `point`: `x0`/`y0`.
|
|
535
|
+
public var x0: Double
|
|
536
|
+
public var x1: Double
|
|
537
|
+
public var y0: Double
|
|
538
|
+
public var y1: Double
|
|
539
|
+
public var cornerRadius: Double
|
|
540
|
+
/// A rect this wide in points, centered on the unit rect, instead of the unit rect's width.
|
|
541
|
+
public var fixedWidth: Double?
|
|
542
|
+
/// `line` and `area`.
|
|
543
|
+
public var points: [ChartPointSpec]
|
|
544
|
+
public var lineWidth: Double
|
|
545
|
+
public var interpolation: String
|
|
546
|
+
/// `circle`, `square`, `triangle`, `diamond`, `cross`, `plus`, `asterisk`; nil for none.
|
|
547
|
+
public var symbol: String?
|
|
548
|
+
public var symbolSize: Double
|
|
549
|
+
public init?(json: JSONValue) {
|
|
550
|
+
guard let kind = json["kind"].string else { return nil }
|
|
551
|
+
self.kind = kind
|
|
552
|
+
style = ShapeStyleSpec(json: json["style"])
|
|
553
|
+
opacity = json["opacity"].double ?? 1
|
|
554
|
+
x0 = json["x0"].double ?? json["x"].double ?? 0
|
|
555
|
+
x1 = json["x1"].double ?? x0
|
|
556
|
+
y0 = json["y0"].double ?? json["y"].double ?? 0
|
|
557
|
+
y1 = json["y1"].double ?? y0
|
|
558
|
+
cornerRadius = json["cornerRadius"].double ?? 0
|
|
559
|
+
fixedWidth = json["fixedWidth"].double
|
|
560
|
+
points = (json["points"].array ?? []).compactMap(ChartPointSpec.init(json:))
|
|
561
|
+
lineWidth = json["lineWidth"].double ?? (kind == "rule" ? 1 : 2)
|
|
562
|
+
interpolation = json["interpolation"].string ?? "linear"
|
|
563
|
+
symbol = json["symbol"].string ?? (kind == "point" ? "circle" : nil)
|
|
564
|
+
symbolSize = json["symbolSize"].double ?? 8
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
public struct ChartAxisLabelSpec: Equatable, Sendable {
|
|
569
|
+
/// Unit position along the axis (y: 0 is the top).
|
|
570
|
+
public var at: Double
|
|
571
|
+
public var text: String
|
|
572
|
+
/// `leading`: an x label that starts at its tick instead of centering on it.
|
|
573
|
+
public var anchor: String?
|
|
574
|
+
init?(json: JSONValue) {
|
|
575
|
+
guard let at = json["at"].double else { return nil }
|
|
576
|
+
self.at = at
|
|
577
|
+
text = json["text"].string ?? ""
|
|
578
|
+
anchor = json["anchor"].string
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
public struct ChartAxisSpec: Equatable, Sendable {
|
|
583
|
+
/// x: `bottom` or `top`; y: `trailing` or `leading`.
|
|
584
|
+
public var position: String
|
|
585
|
+
public var gridLines: [Double]
|
|
586
|
+
public var dashed: Bool
|
|
587
|
+
public var ticks: [Double]
|
|
588
|
+
public var labels: [ChartAxisLabelSpec]
|
|
589
|
+
public init?(json: JSONValue) {
|
|
590
|
+
guard let fields = json.object else { return nil }
|
|
591
|
+
position = fields.string("position") ?? "bottom"
|
|
592
|
+
gridLines = (fields.array("gridLines") ?? []).compactMap(\.double)
|
|
593
|
+
dashed = fields.bool("dashed") ?? false
|
|
594
|
+
ticks = (fields.array("ticks") ?? []).compactMap(\.double)
|
|
595
|
+
labels = (fields.array("labels") ?? []).compactMap(ChartAxisLabelSpec.init(json:))
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
public struct ChartLegendItemSpec: Equatable, Sendable {
|
|
600
|
+
public var label: String
|
|
601
|
+
public var style: ShapeStyleSpec?
|
|
602
|
+
/// Nil: a short line instead of a symbol.
|
|
603
|
+
public var symbol: String?
|
|
604
|
+
init(json: JSONValue) {
|
|
605
|
+
label = json["label"].string ?? ""
|
|
606
|
+
style = ShapeStyleSpec(json: json["style"])
|
|
607
|
+
symbol = json["symbol"].string
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
public struct ChartLegendSpec: Equatable, Sendable {
|
|
612
|
+
/// `top`, `bottom`, `leading` or `trailing`.
|
|
613
|
+
public var position: String
|
|
614
|
+
public var items: [ChartLegendItemSpec]
|
|
615
|
+
public init?(json: JSONValue) {
|
|
616
|
+
guard let fields = json.object else { return nil }
|
|
617
|
+
position = fields.string("position") ?? "bottom"
|
|
618
|
+
items = (fields.array("items") ?? []).map(ChartLegendItemSpec.init(json:))
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/// `chartitem`: an annotation against its mark's unit rect, or an axis label
|
|
623
|
+
/// view at its tick (`x0 == x1` for an x label, `y0 == y1` for a y label).
|
|
624
|
+
public struct ChartItemProps: Equatable, Sendable {
|
|
625
|
+
/// `annotation`, `xLabel` or `yLabel`.
|
|
626
|
+
public var role: String
|
|
627
|
+
public var x0: Double
|
|
628
|
+
public var x1: Double
|
|
629
|
+
public var y0: Double
|
|
630
|
+
public var y1: Double
|
|
631
|
+
/// `top`, `bottom`, `leading`, `trailing` or `overlay`.
|
|
632
|
+
public var position: String
|
|
633
|
+
public var alignment: String
|
|
634
|
+
public var spacing: Double
|
|
635
|
+
public init(props: JSONObject) {
|
|
636
|
+
role = props.string("role") ?? "annotation"
|
|
637
|
+
x0 = props.double("x0") ?? 0
|
|
638
|
+
x1 = props.double("x1") ?? x0
|
|
639
|
+
y0 = props.double("y0") ?? 0
|
|
640
|
+
y1 = props.double("y1") ?? y0
|
|
641
|
+
position = props.string("position") ?? "overlay"
|
|
642
|
+
alignment = props.string("alignment") ?? "center"
|
|
643
|
+
spacing = props.double("spacing") ?? 0
|
|
644
|
+
}
|
|
645
|
+
}
|
|
@@ -88,6 +88,8 @@ public struct RendererEvent: Equatable, Sendable {
|
|
|
88
88
|
public static func longPress(_ id: Int) -> RendererEvent { with("longpress", id: id) }
|
|
89
89
|
/// A `list` or `scrollview` with `refreshable` was pulled.
|
|
90
90
|
public static func refresh(_ id: Int) -> RendererEvent { with("refresh", id: id) }
|
|
91
|
+
/// `.scrollPosition(id:)`: the marked element at the scroll view's anchor once it settled.
|
|
92
|
+
public static func scrollPosition(_ id: Int) -> RendererEvent { with("scrollposition", id: id) }
|
|
91
93
|
/// A `menu` opened or closed (lazy menus build their items on `open`).
|
|
92
94
|
public static func menu(_ id: Int, open: Bool) -> RendererEvent { with("menu", id: id, ["open": .bool(open)]) }
|
|
93
95
|
/// The host's answer to a `native` element's own event (docs/native.md, the escape hatch).
|
|
@@ -67,6 +67,12 @@ public struct Style: Equatable, Sendable {
|
|
|
67
67
|
public var help: String?
|
|
68
68
|
public var matchedGeometry: String?
|
|
69
69
|
public var contentTransition: String?
|
|
70
|
+
/// `.containerRelativeFrame`: the length along each named axis is `a × container + b`.
|
|
71
|
+
public var containerRelativeFrame: ContainerRelativeFrameSpec?
|
|
72
|
+
/// `.id(x)` inside a `ScrollViewReader`: the element a `scrollTo` or a scroll position names.
|
|
73
|
+
public var scrollID: Bool
|
|
74
|
+
/// `proxy.scrollTo(x)`: the request (its serial is new each time) on the element it names.
|
|
75
|
+
public var scrollTo: ScrollToSpec?
|
|
70
76
|
|
|
71
77
|
public init(json: JSONValue) {
|
|
72
78
|
let fields = json.object ?? [:]
|
|
@@ -137,6 +143,9 @@ public struct Style: Equatable, Sendable {
|
|
|
137
143
|
help = fields.string("help")
|
|
138
144
|
matchedGeometry = fields.string("matchedGeometry")
|
|
139
145
|
contentTransition = fields.object("contentTransition")?.string("kind")
|
|
146
|
+
containerRelativeFrame = ContainerRelativeFrameSpec(json: fields.value("containerRelativeFrame"))
|
|
147
|
+
scrollID = fields.bool("scrollID") ?? false
|
|
148
|
+
scrollTo = ScrollToSpec(json: fields.value("scrollTo"))
|
|
140
149
|
}
|
|
141
150
|
|
|
142
151
|
public init() {
|
|
@@ -146,3 +155,35 @@ public struct Style: Equatable, Sendable {
|
|
|
146
155
|
/// The style's field names, for the unsupported-field diagnostics and tests.
|
|
147
156
|
public var fieldNames: [String] { raw.keys.sorted() }
|
|
148
157
|
}
|
|
158
|
+
|
|
159
|
+
/// `.containerRelativeFrame(axes)` as the style hint carries it: `length = a × container + b`.
|
|
160
|
+
public struct ContainerRelativeFrameSpec: Equatable, Sendable {
|
|
161
|
+
public var horizontal: Bool
|
|
162
|
+
public var vertical: Bool
|
|
163
|
+
public var a: Double
|
|
164
|
+
public var b: Double
|
|
165
|
+
public var alignment: String
|
|
166
|
+
public init?(json: JSONValue) {
|
|
167
|
+
guard let fields = json.object else { return nil }
|
|
168
|
+
horizontal = fields.bool("horizontal") ?? false
|
|
169
|
+
vertical = fields.bool("vertical") ?? false
|
|
170
|
+
a = fields.double("a") ?? 1
|
|
171
|
+
b = fields.double("b") ?? 0
|
|
172
|
+
alignment = fields.string("alignment") ?? "center"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// A `ScrollViewReader` proxy's `scrollTo` on the element it names.
|
|
177
|
+
public struct ScrollToSpec: Equatable, Sendable {
|
|
178
|
+
/// Increases with every request, across all readers.
|
|
179
|
+
public var serial: Double
|
|
180
|
+
public var anchor: UnitPointSpec?
|
|
181
|
+
/// `horizontal`, `vertical` or `both` (the default).
|
|
182
|
+
public var axes: String
|
|
183
|
+
public init?(json: JSONValue) {
|
|
184
|
+
guard let fields = json.object, let serial = fields.double("serial") else { return nil }
|
|
185
|
+
self.serial = serial
|
|
186
|
+
anchor = UnitPointSpec(json: fields.value("anchor"))
|
|
187
|
+
axes = fields.string("axes") ?? "both"
|
|
188
|
+
}
|
|
189
|
+
}
|