@swiftbrowser/native 0.17.0 → 0.17.2
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swiftbrowser/native",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Run a @swiftbrowser/react app natively: the runtime a JavaScriptCore-hosted app needs (timers, console, fetch and the other web globals over the SwiftBrowser Native bridge), the `run` entry that renders into the native SwiftUI renderer, and the bundler that builds app.js for the Xcode target.",
|
|
6
6
|
"repository": {
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@babel/core": "^7.29.7",
|
|
62
|
-
"@swiftbrowser/react": "0.17.
|
|
63
|
-
"@swiftbrowser/web": "0.17.
|
|
62
|
+
"@swiftbrowser/react": "0.17.2",
|
|
63
|
+
"@swiftbrowser/web": "0.17.2",
|
|
64
64
|
"esbuild": "^0.28.2",
|
|
65
65
|
"react-refresh": "^0.17.0"
|
|
66
66
|
},
|
|
@@ -13,23 +13,7 @@ struct ListElement: View {
|
|
|
13
13
|
|
|
14
14
|
var body: some View {
|
|
15
15
|
let props = ListProps(props: node.props)
|
|
16
|
-
|
|
17
|
-
// makes them: the whole cell takes the tap and shows the system's
|
|
18
|
-
// highlight, and choosing a row sends its `tap`. In edit mode a row
|
|
19
|
-
// keeps its own tap (several may be chosen).
|
|
20
|
-
let rows = Self.selectableRows(in: node)
|
|
21
|
-
let selects = !rows.isEmpty && props.editing != true
|
|
22
|
-
let list: AnyView = selects
|
|
23
|
-
? AnyView(List(selection: Binding<Int?>(
|
|
24
|
-
get: { rows.first { ListRowProps(props: $0.props).selected }?.id },
|
|
25
|
-
set: { id in if let id { app?.dispatch(.tap(id)) } }
|
|
26
|
-
)) {
|
|
27
|
-
ListRowsView(children: node.children)
|
|
28
|
-
}
|
|
29
|
-
.environment(\.listSelectsRows, true))
|
|
30
|
-
: AnyView(List {
|
|
31
|
-
ListRowsView(children: node.children)
|
|
32
|
-
})
|
|
16
|
+
let list = selectionList(props)
|
|
33
17
|
let styled: AnyView = {
|
|
34
18
|
switch props.style {
|
|
35
19
|
case "plain": return AnyView(list.listStyle(.plain))
|
|
@@ -56,6 +40,51 @@ struct ListElement: View {
|
|
|
56
40
|
#endif
|
|
57
41
|
}
|
|
58
42
|
|
|
43
|
+
/// The `List`, with the selection its selectable rows make: as
|
|
44
|
+
/// `List(selection:)` makes them, the whole cell takes the tap and shows
|
|
45
|
+
/// the system's highlight, and a row the selection gains or loses sends
|
|
46
|
+
/// its `tap` (the app toggles or sets it). In edit mode a `Set`
|
|
47
|
+
/// selection is the system's multiple selection, with the checkmark
|
|
48
|
+
/// circles. Under the shim (the round trip) the shim's own list makes
|
|
49
|
+
/// rows of tags, so the rows keep their own taps there.
|
|
50
|
+
private func selectionList(_ props: ListProps) -> AnyView {
|
|
51
|
+
#if canImport(FoundationBridge)
|
|
52
|
+
return AnyView(List { ListRowsView(children: node.children) })
|
|
53
|
+
#else
|
|
54
|
+
let rows = Self.selectableRows(in: node)
|
|
55
|
+
let selected = rows.filter { ListRowProps(props: $0.props).selected }.map(\.id)
|
|
56
|
+
let editing = props.editing == true
|
|
57
|
+
if rows.isEmpty {
|
|
58
|
+
return AnyView(List { ListRowsView(children: node.children) })
|
|
59
|
+
}
|
|
60
|
+
if editing, props.multiSelect {
|
|
61
|
+
return AnyView(List(selection: Binding<Set<Int>>(
|
|
62
|
+
get: { Set(selected) },
|
|
63
|
+
set: { next in
|
|
64
|
+
for id in Set(selected).symmetricDifference(next) { app?.dispatch(.tap(id)) }
|
|
65
|
+
}
|
|
66
|
+
)) {
|
|
67
|
+
ListRowsView(children: node.children)
|
|
68
|
+
}
|
|
69
|
+
.environment(\.listSelectsRows, true))
|
|
70
|
+
}
|
|
71
|
+
return AnyView(List(selection: Binding<Int?>(
|
|
72
|
+
get: { selected.first },
|
|
73
|
+
set: { id in
|
|
74
|
+
if let id {
|
|
75
|
+
app?.dispatch(.tap(id))
|
|
76
|
+
} else if editing, let current = selected.first {
|
|
77
|
+
// Edit mode deselects a chosen row: the app clears it.
|
|
78
|
+
app?.dispatch(.tap(current))
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
)) {
|
|
82
|
+
ListRowsView(children: node.children)
|
|
83
|
+
}
|
|
84
|
+
.environment(\.listSelectsRows, true))
|
|
85
|
+
#endif
|
|
86
|
+
}
|
|
87
|
+
|
|
59
88
|
/// The list's selectable rows (not those of a list inside it).
|
|
60
89
|
static func selectableRows(in node: ElementNode) -> [ElementNode] {
|
|
61
90
|
var rows: [ElementNode] = []
|
|
@@ -166,8 +195,8 @@ struct SectionElement: View {
|
|
|
166
195
|
}
|
|
167
196
|
|
|
168
197
|
/// `listrow`: the row's content with its edit-mode hints; a selectable
|
|
169
|
-
/// row is the list's selection's tag (`ListElement`), or
|
|
170
|
-
/// `tap` itself.
|
|
198
|
+
/// row is the list's selection's tag (`ListElement`), or under the shim
|
|
199
|
+
/// sends `tap` itself.
|
|
171
200
|
struct ListRowElement: View {
|
|
172
201
|
let node: ElementNode
|
|
173
202
|
@Environment(\.nativeApp) private var app
|
|
@@ -147,12 +147,21 @@ struct ScrollViewElement: View {
|
|
|
147
147
|
@State private var visible: [Int] = []
|
|
148
148
|
@State private var idle = true
|
|
149
149
|
@State private var reported: Int?
|
|
150
|
+
@State private var appeared = false
|
|
150
151
|
|
|
151
152
|
var body: some View {
|
|
152
153
|
let props = ScrollViewProps(props: node.props)
|
|
153
154
|
let axes: Axis.Set = props.axes == "horizontal" ? .horizontal : props.axes == "both" ? [.horizontal, .vertical] : .vertical
|
|
154
155
|
let request = Self.scrollRequest(in: node)
|
|
155
156
|
let anchor = props.positionAnchor.map(UnitPoint.init(spec:))
|
|
157
|
+
// A request not yet served is the position already in this pass, the
|
|
158
|
+
// one that draws the content it came with (the app may have changed
|
|
159
|
+
// the content around its target): served in `onChange`, a pass later,
|
|
160
|
+
// the pass between would draw the new content at the old offset. Not
|
|
161
|
+
// before the view appears: the first pass has no size to scroll in,
|
|
162
|
+
// and the position the request then sets in `onAppear` would be no
|
|
163
|
+
// change, so nothing would scroll.
|
|
164
|
+
let ahead = request.flatMap { appeared && $0.serial > served && Self.positionServes($0, props: props) ? $0.id : nil }
|
|
156
165
|
ScrollViewReader { proxy in
|
|
157
166
|
let scroll = ScrollView(axes, showsIndicators: props.showsIndicators) {
|
|
158
167
|
ChildrenView(node: node)
|
|
@@ -161,7 +170,7 @@ struct ScrollViewElement: View {
|
|
|
161
170
|
let snapped: AnyView = props.snap == "paging" ? AnyView(refreshed.scrollTargetBehavior(.paging))
|
|
162
171
|
: props.snap == "viewAligned" ? AnyView(refreshed.scrollTargetBehavior(.viewAligned)) : refreshed
|
|
163
172
|
let positioned: AnyView = props.reportsPosition
|
|
164
|
-
? reportingSettled(AnyView(snapped.scrollPosition(id: Binding(get: { position }, set: { next in
|
|
173
|
+
? reportingSettled(AnyView(snapped.scrollPosition(id: Binding(get: { ahead ?? position }, set: { next in
|
|
165
174
|
guard next != position else { return }
|
|
166
175
|
position = next
|
|
167
176
|
// Where the settled position is reported, the binding only follows the view.
|
|
@@ -169,7 +178,10 @@ struct ScrollViewElement: View {
|
|
|
169
178
|
}), anchor: anchor)), anchor: anchor)
|
|
170
179
|
: snapped
|
|
171
180
|
positioned
|
|
172
|
-
.onAppear {
|
|
181
|
+
.onAppear {
|
|
182
|
+
perform(request, props: props, with: proxy)
|
|
183
|
+
appeared = true
|
|
184
|
+
}
|
|
173
185
|
.onChange(of: request?.serial) { _, _ in perform(request, props: props, with: proxy) }
|
|
174
186
|
}
|
|
175
187
|
}
|
|
@@ -177,18 +189,58 @@ struct ScrollViewElement: View {
|
|
|
177
189
|
private func perform(_ request: ScrollRequest?, props: ScrollViewProps, with proxy: ScrollViewProxy) {
|
|
178
190
|
guard let request, request.serial > served else { return }
|
|
179
191
|
served = request.serial
|
|
180
|
-
position
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
192
|
+
if position == request.id {
|
|
193
|
+
// Already its position (the app, having put content in front,
|
|
194
|
+
// asks for the target again): the binding only scrolls on a
|
|
195
|
+
// change, so it lets go and takes the target back next turn.
|
|
196
|
+
position = nil
|
|
197
|
+
let id = request.id
|
|
198
|
+
Task { @MainActor in position = id }
|
|
199
|
+
} else {
|
|
200
|
+
position = request.id
|
|
201
|
+
}
|
|
202
|
+
if props.reportsPosition, Self.reportsSettled, request.id != reported {
|
|
203
|
+
// Where the request lands is the app's to hear once the scroll is
|
|
204
|
+
// over: an animated one ends in an idle phase (which reports);
|
|
205
|
+
// one without animation has no phase, so it is reported here.
|
|
206
|
+
let id = request.id
|
|
207
|
+
Task { @MainActor in
|
|
208
|
+
try? await Task.sleep(for: .milliseconds(450))
|
|
209
|
+
if idle { reportSettled(id) }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if Self.positionServes(request, props: props) { return }
|
|
184
213
|
proxy.scrollTo(request.id, anchor: request.anchor)
|
|
185
214
|
}
|
|
186
215
|
|
|
187
|
-
///
|
|
216
|
+
/// On its own axes, at its own anchor, the position binding scrolls this view alone; else the proxy, which scrolls every scroll view around the target.
|
|
217
|
+
static func positionServes(_ request: ScrollRequest, props: ScrollViewProps) -> Bool {
|
|
218
|
+
let ownAxes = request.axes == props.axes && props.axes != "both"
|
|
219
|
+
return props.reportsPosition && ownAxes && (request.anchor == nil || request.anchor == props.positionAnchor.map(UnitPoint.init(spec:)))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// Tells the app the target at the anchor, once per target, after the
|
|
223
|
+
/// pass that found it: the app answers synchronously, with a pass of its
|
|
224
|
+
/// own, which must not land in the middle of SwiftUI's update (as
|
|
225
|
+
/// GeometryElement's reports wait).
|
|
188
226
|
private func report(_ id: Int?) {
|
|
189
227
|
guard let id, id != reported else { return }
|
|
190
228
|
reported = id
|
|
191
|
-
app
|
|
229
|
+
let app = app
|
|
230
|
+
Task { @MainActor in app?.dispatch(.scrollPosition(id)) }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// Reports where a scroll settled, and lets go of the position: the app
|
|
234
|
+
/// may answer by changing the content around the target (Amindia's
|
|
235
|
+
/// calendar loads the weeks either side of the day it hears of) and
|
|
236
|
+
/// asking for the target again. Held, the position would have to let go
|
|
237
|
+
/// then and take the target back a pass later, and the pass between would
|
|
238
|
+
/// draw the new content at the old offset (a frame of the wrong week).
|
|
239
|
+
/// Let go now, nothing moves, and the request lands with the content.
|
|
240
|
+
private func reportSettled(_ id: Int?) {
|
|
241
|
+
guard let id else { return }
|
|
242
|
+
report(id)
|
|
243
|
+
if position == id { position = nil }
|
|
192
244
|
}
|
|
193
245
|
|
|
194
246
|
/// Whether this OS reports the settled position (`reportingSettled`) rather than the binding's every change.
|
|
@@ -199,7 +251,12 @@ struct ScrollViewElement: View {
|
|
|
199
251
|
return false
|
|
200
252
|
}
|
|
201
253
|
|
|
202
|
-
/// The visible target at the anchor, reported when
|
|
254
|
+
/// The visible target at the anchor, reported when a scroll ends (the
|
|
255
|
+
/// phase goes from a drag, its deceleration or an animation to idle).
|
|
256
|
+
/// A change of what is visible while idle is not a scroll of the
|
|
257
|
+
/// viewer's: the first layout, before a requested position applies, or
|
|
258
|
+
/// content the app put in front, which it scrolls back itself. Reporting
|
|
259
|
+
/// those would send the app after them, in a loop.
|
|
203
260
|
private func reportingSettled(_ view: AnyView, anchor: UnitPoint?) -> AnyView {
|
|
204
261
|
#if !canImport(FoundationBridge)
|
|
205
262
|
if #available(iOS 18, macOS 15, *) {
|
|
@@ -207,11 +264,10 @@ struct ScrollViewElement: View {
|
|
|
207
264
|
return AnyView(view
|
|
208
265
|
.onScrollTargetVisibilityChange(idType: Int.self, threshold: 0.5) { ids in
|
|
209
266
|
visible = ids.sorted { (order[$0] ?? .max) < (order[$1] ?? .max) }
|
|
210
|
-
if idle { report(Self.target(at: anchor, among: visible)) }
|
|
211
267
|
}
|
|
212
|
-
.onScrollPhaseChange {
|
|
268
|
+
.onScrollPhaseChange { old, phase in
|
|
213
269
|
idle = phase == .idle
|
|
214
|
-
if idle {
|
|
270
|
+
if idle, old != .idle { reportSettled(Self.target(at: anchor, among: visible)) }
|
|
215
271
|
})
|
|
216
272
|
}
|
|
217
273
|
#endif
|
|
@@ -141,10 +141,13 @@ public struct ListProps: Equatable, Sendable {
|
|
|
141
141
|
public var style: String
|
|
142
142
|
public var refreshable: Bool
|
|
143
143
|
public var editing: Bool?
|
|
144
|
+
/// `List(selection:)` over a `Set`: several rows may be selected.
|
|
145
|
+
public var multiSelect: Bool
|
|
144
146
|
public init(props: JSONObject) {
|
|
145
147
|
style = props.string("style") ?? "insetGrouped"
|
|
146
148
|
refreshable = props.bool("refreshable") ?? false
|
|
147
149
|
editing = props.bool("editing")
|
|
150
|
+
multiSelect = props.bool("multiSelect") ?? false
|
|
148
151
|
}
|
|
149
152
|
}
|
|
150
153
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/wasm/SwiftBrowserNative.wasm
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 49330000 bytes
|
|
4
|
+
sha256: e1e3caa6b59f0db479d22e2969623ed0609ecda85225d254b534cf952a6ca929
|