@swiftbrowser/native 0.17.7 → 0.18.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 +99 -11
- package/dist/blob.d.ts +60 -0
- package/dist/blob.d.ts.map +1 -0
- package/dist/blob.js +153 -0
- package/dist/blob.js.map +1 -0
- package/dist/bridge.d.ts +65 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js.map +1 -1
- package/dist/fetch.d.ts +11 -3
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +26 -2
- package/dist/fetch.js.map +1 -1
- package/dist/hostRequests.d.ts +55 -0
- package/dist/hostRequests.d.ts.map +1 -0
- package/dist/hostRequests.js +196 -0
- package/dist/hostRequests.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/matchMedia.d.ts +43 -0
- package/dist/matchMedia.d.ts.map +1 -0
- package/dist/matchMedia.js +63 -0
- package/dist/matchMedia.js.map +1 -0
- package/dist/notifications.d.ts +108 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +251 -0
- package/dist/notifications.js.map +1 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +23 -2
- package/dist/run.js.map +1 -1
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +69 -0
- package/dist/runtime.js.map +1 -1
- package/dist/secureStorage.d.ts +20 -0
- package/dist/secureStorage.d.ts.map +1 -0
- package/dist/secureStorage.js +28 -0
- package/dist/secureStorage.js.map +1 -0
- package/dist/site.d.ts +3 -3
- package/dist/site.d.ts.map +1 -1
- package/dist/site.js +1 -1
- package/dist/site.js.map +1 -1
- package/dist/subtle.d.ts +38 -0
- package/dist/subtle.d.ts.map +1 -0
- package/dist/subtle.js +72 -0
- package/dist/subtle.js.map +1 -0
- package/dist/undo.d.ts +23 -0
- package/dist/undo.d.ts.map +1 -0
- package/dist/undo.js +44 -0
- package/dist/undo.js.map +1 -0
- package/dist/webSocket.d.ts +73 -0
- package/dist/webSocket.d.ts.map +1 -0
- package/dist/webSocket.js +145 -0
- package/dist/webSocket.js.map +1 -0
- package/package.json +3 -3
- package/src/blob.ts +186 -0
- package/src/bridge.ts +74 -1
- package/src/fetch.ts +29 -3
- package/src/hostRequests.ts +216 -0
- package/src/index.ts +8 -0
- package/src/matchMedia.ts +79 -0
- package/src/notifications.ts +317 -0
- package/src/run.ts +21 -2
- package/src/runtime.ts +71 -0
- package/src/secureStorage.ts +39 -0
- package/src/site.ts +3 -3
- package/src/subtle.ts +87 -0
- package/src/undo.ts +47 -0
- package/src/webSocket.ts +175 -0
- package/swift/Sources/SwiftBrowserNative/Host/AudioRecorderBridge.swift +199 -0
- package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +154 -0
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +211 -0
- package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +451 -0
- package/swift/Sources/SwiftBrowserNative/Host/NotificationBridge.swift +304 -0
- package/swift/Sources/SwiftBrowserNative/Host/SecureStorage.swift +60 -0
- package/swift/Sources/SwiftBrowserNative/Host/ShimAliases.swift +1 -0
- package/swift/Sources/SwiftBrowserNative/Host/TextMeasureBridge.swift +139 -0
- package/swift/Sources/SwiftBrowserNative/Host/UndoBridge.swift +170 -0
- package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +118 -0
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeApp.swift +56 -0
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeAppDelegate.swift +36 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +362 -16
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/DeleteBackward.swift +147 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/KeyboardBar.swift +84 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +116 -11
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +12 -1
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Shapes.swift +30 -9
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +163 -10
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/TextAndImages.swift +17 -4
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Video.swift +8 -3
- package/swift/Sources/SwiftBrowserNative/Views/NativeRootView.swift +8 -0
- package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +41 -0
- package/swift/Sources/SwiftBrowserProtocol/Op.swift +27 -4
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +110 -3
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +47 -0
- package/swift/Sources/SwiftBrowserProtocol/SVGPath.swift +260 -0
- package/swift/Sources/SwiftBrowserProtocol/Style.swift +58 -0
- package/swift/Sources/SwiftBrowserTree/OpTree.swift +22 -2
- package/wasm/SwiftBrowserNative.wasm +2 -2
|
@@ -125,7 +125,10 @@ final class VideoController {
|
|
|
125
125
|
if player.isMuted != props.muted { player.isMuted = props.muted }
|
|
126
126
|
let volume = Float(props.volume)
|
|
127
127
|
if player.volume != volume { player.volume = volume }
|
|
128
|
-
|
|
128
|
+
// The first apply installs the observer whether or not `report` is set
|
|
129
|
+
// (`applied?.report` is nil both before any apply and for an app that
|
|
130
|
+
// sends none, as @swiftbrowser/react does); a new interval replaces it.
|
|
131
|
+
if timeObserver == nil || props.report != applied?.report { observeTime(every: props.report ?? 0.25) }
|
|
129
132
|
if props.seekSerial != applied?.seekSerial {
|
|
130
133
|
seek(to: props.seekTime, serial: props.seekSerial)
|
|
131
134
|
}
|
|
@@ -173,7 +176,8 @@ final class VideoController {
|
|
|
173
176
|
if let url { report("error", ["url": .string(url), "code": .number(4), "message": .string("not a URL: \(url)")]) }
|
|
174
177
|
return
|
|
175
178
|
}
|
|
176
|
-
|
|
179
|
+
// A file the app keeps (a recording) may sit under a moved container.
|
|
180
|
+
let item = AVPlayerItem(url: AppFiles.current(parsed))
|
|
177
181
|
player.replaceCurrentItem(with: item)
|
|
178
182
|
let center = NotificationCenter.default
|
|
179
183
|
notifications.append(center.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: item, queue: .main) { [weak self] _ in
|
|
@@ -279,7 +283,8 @@ final class VideoController {
|
|
|
279
283
|
|
|
280
284
|
private func observeTime(every interval: Double) {
|
|
281
285
|
if let timeObserver { player.removeTimeObserver(timeObserver) }
|
|
282
|
-
|
|
286
|
+
// A frame at most (`report: 1 / 60`, Voice Memos' scrubber moving with the sound).
|
|
287
|
+
let step = max(1.0 / 60.0, interval)
|
|
283
288
|
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: step, preferredTimescale: 600), queue: .main) { [weak self] time in
|
|
284
289
|
Task { @MainActor in
|
|
285
290
|
guard let self, self.applied?.playing == true else { return }
|
|
@@ -13,6 +13,9 @@ public struct NativeRootView: View {
|
|
|
13
13
|
@Environment(\.colorScheme) private var colorScheme
|
|
14
14
|
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
|
15
15
|
@Environment(\.scenePhase) private var scenePhase
|
|
16
|
+
#if !canImport(FoundationBridge)
|
|
17
|
+
@Environment(\.undoManager) private var undoManager
|
|
18
|
+
#endif
|
|
16
19
|
#if os(iOS) || canImport(FoundationBridge)
|
|
17
20
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
18
21
|
private var sizeClassName: String { horizontalSizeClass == .regular ? "regular" : "compact" }
|
|
@@ -52,6 +55,11 @@ public struct NativeRootView: View {
|
|
|
52
55
|
observed = AnyView(observed.onChange(of: app.isLoaded) { _, loaded in
|
|
53
56
|
if loaded { sendEnvironment() }
|
|
54
57
|
})
|
|
58
|
+
#if !canImport(FoundationBridge)
|
|
59
|
+
// The window's undo manager, which mirrors the app's (UndoBridge.swift).
|
|
60
|
+
observed = AnyView(observed.onAppear { app.setUndoManager(undoManager) })
|
|
61
|
+
observed = AnyView(observed.onChange(of: undoManager) { _, manager in app.setUndoManager(manager) })
|
|
62
|
+
#endif
|
|
55
63
|
return observed
|
|
56
64
|
}
|
|
57
65
|
|
|
@@ -51,6 +51,8 @@ extension View {
|
|
|
51
51
|
view = AnyView(view.imageScale(scale == "small" ? .small : scale == "large" ? .large : .medium))
|
|
52
52
|
}
|
|
53
53
|
if let tint = style.tint { view = AnyView(view.tint(Color(spec: tint))) }
|
|
54
|
+
// `.scrollDisabled`: the scroll views and text editors below (a text editor then as tall as its lines).
|
|
55
|
+
if let disabled = style.raw.bool("scrollDisabled") { view = AnyView(view.scrollDisabled(disabled)) }
|
|
54
56
|
// `labelIcon` marks a `Label`'s icon; HStackElement draws the label as one.
|
|
55
57
|
if let alignment = style.raw.string("gridColumnAlignment") { view = AnyView(view.gridColumnAlignment(HorizontalAlignment(name: alignment))) }
|
|
56
58
|
|
|
@@ -135,11 +137,13 @@ extension View {
|
|
|
135
137
|
view = AnyView(view.scaleEffect(x: scale.x, y: scale.y, anchor: style.scaleAnchor.map(UnitPoint.init(spec:)) ?? .center))
|
|
136
138
|
}
|
|
137
139
|
if let offset = style.offset { view = AnyView(view.offset(x: offset.x, y: offset.y)) }
|
|
140
|
+
if let transition = style.scrollTransition { view = AnyView(view.scrollTransition(spec: transition)) }
|
|
138
141
|
if let zIndex = style.zIndex { view = AnyView(view.zIndex(zIndex)) }
|
|
139
142
|
|
|
140
143
|
// Interaction and visibility.
|
|
141
144
|
if style.disabled == true { view = AnyView(view.disabled(true)) }
|
|
142
145
|
if style.hitTesting == false { view = AnyView(view.allowsHitTesting(false)) }
|
|
146
|
+
if style.contentShape == "rectangle" { view = AnyView(view.contentShape(Rectangle())) }
|
|
143
147
|
if style.hidden { view = AnyView(view.hidden()) }
|
|
144
148
|
if let help = style.help { view = AnyView(view.help(help)) }
|
|
145
149
|
// A `ScrollViewReader`'s `.id(x)`: the element's own id names it to a `scrollTo` and a scroll position.
|
|
@@ -172,6 +176,43 @@ extension View {
|
|
|
172
176
|
}
|
|
173
177
|
}
|
|
174
178
|
|
|
179
|
+
extension View {
|
|
180
|
+
/// `.scrollTransition`: SwiftUI's own, its closure answering each phase
|
|
181
|
+
/// with the app's effects for it (SwiftUI draws between them as the view
|
|
182
|
+
/// scrolls, for `interactive`). Every effect is applied in every phase, so
|
|
183
|
+
/// the closure's type is one and the phases interpolate field by field.
|
|
184
|
+
func scrollTransition(spec: ScrollTransitionSpec) -> some View {
|
|
185
|
+
let configuration: ScrollTransitionConfiguration = switch spec.configuration {
|
|
186
|
+
case "animated": .animated
|
|
187
|
+
case "identity": .identity
|
|
188
|
+
// Linear, as the protocol draws it (between phases by how far out the
|
|
189
|
+
// view is): SwiftUI's default curve, ease in and out, outruns the
|
|
190
|
+
// scroll mid-way, so an offset of most of the view's length (a book's
|
|
191
|
+
// page coming out from under the one turning) ran backwards.
|
|
192
|
+
default: .interactive(timingCurve: .linear)
|
|
193
|
+
}
|
|
194
|
+
let axis: Axis? = switch spec.axis {
|
|
195
|
+
case "horizontal": .horizontal
|
|
196
|
+
case "vertical": .vertical
|
|
197
|
+
default: nil
|
|
198
|
+
}
|
|
199
|
+
return scrollTransition(configuration, axis: axis) { content, phase in
|
|
200
|
+
let effect = phase.value < 0 ? spec.topLeading : phase.value > 0 ? spec.bottomTrailing : spec.identity
|
|
201
|
+
return content
|
|
202
|
+
.offset(x: effect.offset?.x ?? 0, y: effect.offset?.y ?? 0)
|
|
203
|
+
.scaleEffect(x: effect.scale?.x ?? 1, y: effect.scale?.y ?? 1, anchor: effect.scaleAnchor.map(UnitPoint.init(spec:)) ?? .center)
|
|
204
|
+
.rotationEffect(.degrees(effect.rotation ?? 0), anchor: effect.rotationAnchor.map(UnitPoint.init(spec:)) ?? .center)
|
|
205
|
+
.opacity(effect.opacity ?? 1)
|
|
206
|
+
.blur(radius: effect.blur ?? 0)
|
|
207
|
+
.brightness(effect.brightness ?? 0)
|
|
208
|
+
.contrast(effect.contrast ?? 1)
|
|
209
|
+
.saturation(effect.saturation ?? 1)
|
|
210
|
+
.grayscale(effect.grayscale ?? 0)
|
|
211
|
+
.hueRotation(.degrees(effect.hueRotation ?? 0))
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
175
216
|
extension BlendMode {
|
|
176
217
|
init?(name: String) {
|
|
177
218
|
switch name {
|
|
@@ -12,8 +12,8 @@ public enum Op: Equatable, Sendable {
|
|
|
12
12
|
case insert(parent: Int, id: Int, index: Int)
|
|
13
13
|
/// Detach and destroy the element and its subtree.
|
|
14
14
|
case remove(id: Int)
|
|
15
|
-
/// End of a render pass; with `animation`, every change of the pass animates.
|
|
16
|
-
case commit(animation: AnimationSpec?)
|
|
15
|
+
/// End of a render pass; with `animation`, every change of the pass animates; with `completion`, the app hears `animationcomplete` once it has.
|
|
16
|
+
case commit(animation: AnimationSpec?, completion: CommitCompletionSpec? = nil)
|
|
17
17
|
/// Any other op, by name, with the whole object.
|
|
18
18
|
case other(name: String, fields: JSONObject)
|
|
19
19
|
|
|
@@ -56,7 +56,7 @@ extension Op {
|
|
|
56
56
|
case "remove":
|
|
57
57
|
self = .remove(id: try id("id"))
|
|
58
58
|
case "commit":
|
|
59
|
-
self = .commit(animation: fields["animation"].flatMap(AnimationSpec.init(json:)))
|
|
59
|
+
self = .commit(animation: fields["animation"].flatMap(AnimationSpec.init(json:)), completion: CommitCompletionSpec(fields: fields))
|
|
60
60
|
default:
|
|
61
61
|
self = .other(name: name, fields: fields)
|
|
62
62
|
}
|
|
@@ -89,12 +89,35 @@ extension Op {
|
|
|
89
89
|
return .object(["op": "insert", "parent": .number(Double(parent)), "id": .number(Double(id)), "index": .number(Double(index))])
|
|
90
90
|
case .remove(let id):
|
|
91
91
|
return .object(["op": "remove", "id": .number(Double(id))])
|
|
92
|
-
case .commit(let animation):
|
|
92
|
+
case .commit(let animation, let completion):
|
|
93
93
|
var fields: JSONObject = ["op": "commit"]
|
|
94
94
|
if let animation { fields["animation"] = animation.json }
|
|
95
|
+
if let completion {
|
|
96
|
+
fields["completion"] = .number(Double(completion.serial))
|
|
97
|
+
if completion.criteria != "logicallyComplete" { fields["completionCriteria"] = .string(completion.criteria) }
|
|
98
|
+
}
|
|
95
99
|
return .object(fields)
|
|
96
100
|
case .other(_, let fields):
|
|
97
101
|
return .object(fields)
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
104
|
}
|
|
105
|
+
|
|
106
|
+
/// `withAnimation(_:completionCriteria:_:completion:)` on a `commit`: the
|
|
107
|
+
/// serial the app's completion waits under, and when it should run
|
|
108
|
+
/// (`logicallyComplete`, the default, or `removed`).
|
|
109
|
+
public struct CommitCompletionSpec: Equatable, Sendable {
|
|
110
|
+
public var serial: Int
|
|
111
|
+
public var criteria: String
|
|
112
|
+
|
|
113
|
+
public init(serial: Int, criteria: String = "logicallyComplete") {
|
|
114
|
+
self.serial = serial
|
|
115
|
+
self.criteria = criteria
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// From a `commit`'s fields; nil without a `completion`.
|
|
119
|
+
public init?(fields: JSONObject) {
|
|
120
|
+
guard let serial = fields.int("completion") else { return nil }
|
|
121
|
+
self.init(serial: serial, criteria: fields.string("completionCriteria") ?? "logicallyComplete")
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -22,6 +22,8 @@ public struct TextRunSpec: Equatable, Sendable {
|
|
|
22
22
|
public var image: ImageProps?
|
|
23
23
|
public var font: FontSpec?
|
|
24
24
|
public var foreground: ShapeStyleSpec?
|
|
25
|
+
/// `AttributedString`'s `backgroundColor`: a color behind the run's glyphs.
|
|
26
|
+
public var background: ColorSpec?
|
|
25
27
|
public var italic: Bool
|
|
26
28
|
public var underline: Bool
|
|
27
29
|
public var underlineColor: ColorSpec?
|
|
@@ -37,6 +39,7 @@ public struct TextRunSpec: Equatable, Sendable {
|
|
|
37
39
|
image = fields.object("image").map(ImageProps.init(props:))
|
|
38
40
|
font = FontSpec(json: fields.value("font"))
|
|
39
41
|
foreground = ShapeStyleSpec(json: fields.value("foreground"))
|
|
42
|
+
background = ColorSpec(json: fields.value("background"))
|
|
40
43
|
italic = fields.bool("italic") ?? false
|
|
41
44
|
underline = fields["underline"] != nil
|
|
42
45
|
underlineColor = ColorSpec(json: fields.value("underline")["color"])
|
|
@@ -56,11 +59,14 @@ public struct StackProps: Equatable, Sendable {
|
|
|
56
59
|
|
|
57
60
|
/// `.scrollTargetLayout()`: the stack's children are the snap points of the scroll view around it.
|
|
58
61
|
public var scrollTarget: Bool
|
|
62
|
+
/// `LazyVStack(pinnedViews:)`: `sectionHeaders`, `sectionFooters`; empty for a plain stack.
|
|
63
|
+
public var pinned: [String]
|
|
59
64
|
|
|
60
65
|
public init(props: JSONObject) {
|
|
61
66
|
spacing = props.double("spacing")
|
|
62
67
|
alignment = props.string("alignment") ?? "center"
|
|
63
68
|
scrollTarget = props.bool("scrollTarget") ?? false
|
|
69
|
+
pinned = props.array("pinned")?.compactMap(\.string) ?? []
|
|
64
70
|
}
|
|
65
71
|
}
|
|
66
72
|
|
|
@@ -120,6 +126,12 @@ public struct ScrollViewProps: Equatable, Sendable {
|
|
|
120
126
|
/// `.scrollPosition(id:)`: the view reports the marked element at its anchor as it settles.
|
|
121
127
|
public var reportsPosition: Bool
|
|
122
128
|
public var positionAnchor: UnitPointSpec?
|
|
129
|
+
/// `.onScrollGeometryChange`: the view sends `scrollgeometry` as it scrolls and lays out.
|
|
130
|
+
public var reportsGeometry: Bool
|
|
131
|
+
/// `ScrollPosition.scrollTo(x:y:)`: the content offset to scroll to, once per serial.
|
|
132
|
+
public var scrollOffset: ScrollOffsetSpec?
|
|
133
|
+
/// `.defaultScrollAnchor(_:)`: where the content starts, and where it stays as it grows.
|
|
134
|
+
public var defaultScrollAnchor: UnitPointSpec?
|
|
123
135
|
public init(props: JSONObject) {
|
|
124
136
|
axes = props.string("axes") ?? "vertical"
|
|
125
137
|
showsIndicators = props.bool("showsIndicators") ?? true
|
|
@@ -127,6 +139,23 @@ public struct ScrollViewProps: Equatable, Sendable {
|
|
|
127
139
|
snap = props.string("snap")
|
|
128
140
|
reportsPosition = props.bool("reportsPosition") ?? false
|
|
129
141
|
positionAnchor = UnitPointSpec(json: props.value("positionAnchor"))
|
|
142
|
+
reportsGeometry = props.bool("reportsGeometry") ?? false
|
|
143
|
+
scrollOffset = ScrollOffsetSpec(json: props.value("scrollOffset"))
|
|
144
|
+
defaultScrollAnchor = UnitPointSpec(json: props.value("defaultScrollAnchor"))
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// A `scrollview`'s `scrollOffset` command: where to scroll (an axis left out keeps its offset).
|
|
149
|
+
public struct ScrollOffsetSpec: Equatable, Sendable {
|
|
150
|
+
public var x: Double?
|
|
151
|
+
public var y: Double?
|
|
152
|
+
/// Increases with every request; carried out once each.
|
|
153
|
+
public var serial: Double
|
|
154
|
+
public init?(json: JSONValue) {
|
|
155
|
+
guard let fields = json.object, let serial = fields.double("serial") else { return nil }
|
|
156
|
+
self.serial = serial
|
|
157
|
+
x = fields.double("x")
|
|
158
|
+
y = fields.double("y")
|
|
130
159
|
}
|
|
131
160
|
}
|
|
132
161
|
|
|
@@ -160,14 +189,55 @@ public struct SectionProps: Equatable, Sendable {
|
|
|
160
189
|
}
|
|
161
190
|
}
|
|
162
191
|
|
|
192
|
+
/// `texteditor`: its text, a bound focus, and for the rich editor its runs and a bound selection.
|
|
193
|
+
public struct TextEditorProps: Equatable, Sendable {
|
|
194
|
+
public var text: String
|
|
195
|
+
public var focused: Bool?
|
|
196
|
+
/// `deleteBackward`: Backspace at the start of the text sends `deletebackward`.
|
|
197
|
+
public var deleteBackward: Bool
|
|
198
|
+
/// Rich text (`runs`, an empty array included): the runs `text` is made of; nil for a plain editor.
|
|
199
|
+
public var runs: [TextRunSpec]?
|
|
200
|
+
/// `selection` in the props: the editor reports the viewer's selection (UTF-16 offsets).
|
|
201
|
+
public var selection: TextSelectionSpec?
|
|
202
|
+
|
|
203
|
+
public var rich: Bool { runs != nil }
|
|
204
|
+
|
|
205
|
+
public init(props: JSONObject) {
|
|
206
|
+
text = props.string("text") ?? ""
|
|
207
|
+
focused = props.bool("focused")
|
|
208
|
+
deleteBackward = props.bool("deleteBackward") ?? false
|
|
209
|
+
runs = props.array("runs")?.compactMap(TextRunSpec.init(json:))
|
|
210
|
+
selection = TextSelectionSpec(json: props.value("selection"))
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// A range of an editor's text in UTF-16 offsets; `start == end` is the caret.
|
|
215
|
+
public struct TextSelectionSpec: Equatable, Sendable {
|
|
216
|
+
public var start: Int
|
|
217
|
+
public var end: Int
|
|
218
|
+
|
|
219
|
+
public init(start: Int, end: Int) {
|
|
220
|
+
self.start = start
|
|
221
|
+
self.end = end
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
public init?(json: JSONValue) {
|
|
225
|
+
guard let fields = json.object, let start = fields.int("start"), let end = fields.int("end") else { return nil }
|
|
226
|
+
self.init(start: start, end: end)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
163
230
|
public struct TextFieldProps: Equatable, Sendable {
|
|
164
231
|
public var text: String
|
|
165
232
|
public var placeholder: String
|
|
166
233
|
/// `plain` or `roundedBorder`.
|
|
167
234
|
public var style: String
|
|
168
235
|
public var secure: Bool
|
|
236
|
+
/// A bound focus; after a `submit` the value the app's pass leaves wins over Return's resign (`TextFieldElement`).
|
|
169
237
|
public var focused: Bool?
|
|
170
238
|
public var submit: Bool
|
|
239
|
+
/// `deleteBackward`: Backspace at the start of the text sends `deletebackward`.
|
|
240
|
+
public var deleteBackward: Bool
|
|
171
241
|
public var submitLabel: String?
|
|
172
242
|
public var vertical: Bool
|
|
173
243
|
public init(props: JSONObject) {
|
|
@@ -177,6 +247,7 @@ public struct TextFieldProps: Equatable, Sendable {
|
|
|
177
247
|
secure = props.bool("secure") ?? false
|
|
178
248
|
focused = props.bool("focused")
|
|
179
249
|
submit = props.bool("submit") ?? false
|
|
250
|
+
deleteBackward = props.bool("deleteBackward") ?? false
|
|
180
251
|
submitLabel = props.string("submitLabel")
|
|
181
252
|
vertical = props.string("axis") == "vertical"
|
|
182
253
|
}
|
|
@@ -184,7 +255,12 @@ public struct TextFieldProps: Equatable, Sendable {
|
|
|
184
255
|
|
|
185
256
|
public struct ToggleProps: Equatable, Sendable {
|
|
186
257
|
public var isOn: Bool
|
|
187
|
-
|
|
258
|
+
/// `automatic`, `switch` or `button` (`.toggleStyle`).
|
|
259
|
+
public var style: String
|
|
260
|
+
public init(props: JSONObject) {
|
|
261
|
+
isOn = props.bool("isOn") ?? false
|
|
262
|
+
style = props.string("style") ?? "automatic"
|
|
263
|
+
}
|
|
188
264
|
}
|
|
189
265
|
|
|
190
266
|
public struct NavScreenProps: Equatable, Sendable {
|
|
@@ -229,12 +305,22 @@ public struct StrokeSpec: Equatable, Sendable {
|
|
|
229
305
|
public var lineWidth: Double
|
|
230
306
|
public var inset: Bool
|
|
231
307
|
public var dash: [Double]
|
|
308
|
+
public var dashPhase: Double
|
|
309
|
+
/// `butt`, `round` or `square`.
|
|
310
|
+
public var lineCap: String
|
|
311
|
+
/// `miter`, `round` or `bevel`.
|
|
312
|
+
public var lineJoin: String
|
|
313
|
+
public var miterLimit: Double
|
|
232
314
|
public init?(json: JSONValue) {
|
|
233
315
|
guard let fields = json.object else { return nil }
|
|
234
316
|
style = ShapeStyleSpec(json: fields.value("color"))
|
|
235
317
|
lineWidth = fields.double("lineWidth") ?? 1
|
|
236
318
|
inset = fields.bool("inset") ?? false
|
|
237
319
|
dash = (fields.array("dash") ?? []).compactMap(\.double)
|
|
320
|
+
dashPhase = fields.double("dashPhase") ?? 0
|
|
321
|
+
lineCap = fields.string("lineCap") ?? "butt"
|
|
322
|
+
lineJoin = fields.string("lineJoin") ?? "miter"
|
|
323
|
+
miterLimit = fields.double("miterLimit") ?? 10
|
|
238
324
|
}
|
|
239
325
|
}
|
|
240
326
|
|
|
@@ -332,7 +418,7 @@ public struct ListRowProps: Equatable, Sendable {
|
|
|
332
418
|
}
|
|
333
419
|
|
|
334
420
|
public struct ToolbarProps: Equatable, Sendable {
|
|
335
|
-
/// `trailing`, `leading`, `principal`, `bottom`, `secondary`, `largeTitle`.
|
|
421
|
+
/// `trailing`, `leading`, `principal`, `bottom`, `secondary`, `largeTitle`, `keyboard`.
|
|
336
422
|
public var placement: String
|
|
337
423
|
/// A `ToolbarSpacer`: `fixed` or `flexible`; the element has no items.
|
|
338
424
|
public var spacer: String?
|
|
@@ -443,7 +529,12 @@ public struct SliderProps: Equatable, Sendable {
|
|
|
443
529
|
|
|
444
530
|
public struct TapGestureProps: Equatable, Sendable {
|
|
445
531
|
public var location: Bool
|
|
446
|
-
|
|
532
|
+
/// `.onTapGesture(count:)`: the taps it takes, 1 by default.
|
|
533
|
+
public var count: Int
|
|
534
|
+
public init(props: JSONObject) {
|
|
535
|
+
location = props.bool("location") ?? false
|
|
536
|
+
count = max(1, Int(props.double("count") ?? 1))
|
|
537
|
+
}
|
|
447
538
|
}
|
|
448
539
|
|
|
449
540
|
/// The producer-only `native` kind (docs/native.md, "The native escape hatch").
|
|
@@ -500,13 +591,29 @@ public struct DatePickerProps: Equatable, Sendable {
|
|
|
500
591
|
public struct GestureProps: Equatable, Sendable {
|
|
501
592
|
public var drag: Bool
|
|
502
593
|
public var longPress: Bool
|
|
594
|
+
/// Two fingers spreading or pinching (`MagnifyGesture`), and turning (`RotateGesture`): `pinch` events.
|
|
595
|
+
public var magnify: Bool
|
|
596
|
+
public var rotate: Bool
|
|
503
597
|
public var minimumDistance: Double
|
|
504
598
|
public var minimumDuration: Double
|
|
599
|
+
public var minimumScaleDelta: Double
|
|
600
|
+
/// Degrees.
|
|
601
|
+
public var minimumAngleDelta: Double
|
|
602
|
+
/// The space of a drag's `location` and `startLocation`: `local` (the element's own) or `global`.
|
|
603
|
+
public var coordinateSpace: String
|
|
604
|
+
/// `simultaneous` (`.simultaneousGesture`), `highPriority` (`.highPriorityGesture`), or nil for `.gesture`.
|
|
605
|
+
public var mode: String?
|
|
505
606
|
public init(props: JSONObject) {
|
|
506
607
|
drag = props.bool("drag") ?? false
|
|
507
608
|
longPress = props.bool("longPress") ?? false
|
|
609
|
+
magnify = props.bool("magnify") ?? false
|
|
610
|
+
rotate = props.bool("rotate") ?? false
|
|
508
611
|
minimumDistance = props.double("minimumDistance") ?? 10
|
|
509
612
|
minimumDuration = props.double("minimumDuration") ?? 0.5
|
|
613
|
+
minimumScaleDelta = props.double("minimumScaleDelta") ?? 0.01
|
|
614
|
+
minimumAngleDelta = props.double("minimumAngleDelta") ?? 1
|
|
615
|
+
coordinateSpace = props.string("coordinateSpace") ?? "local"
|
|
616
|
+
mode = props.string("mode")
|
|
510
617
|
}
|
|
511
618
|
}
|
|
512
619
|
|
|
@@ -75,6 +75,14 @@ public struct RendererEvent: Equatable, Sendable {
|
|
|
75
75
|
public static func focus(_ id: Int, _ focused: Bool) -> RendererEvent { with("focus", id: id, ["value": .bool(focused)]) }
|
|
76
76
|
/// The Return key in a `textfield` with `submit`.
|
|
77
77
|
public static func submit(_ id: Int) -> RendererEvent { with("submit", id: id) }
|
|
78
|
+
/// Backspace at the start of a `textfield` or `texteditor` with `deleteBackward`, nothing selected.
|
|
79
|
+
public static func deleteBackward(_ id: Int) -> RendererEvent { with("deletebackward", id: id) }
|
|
80
|
+
/// The viewer's selection in a `texteditor` with `selection`, in UTF-16 offsets.
|
|
81
|
+
public static func selection(_ id: Int, start: Int, end: Int) -> RendererEvent {
|
|
82
|
+
with("selection", id: id, ["value": .object(["start": .number(Double(start)), "end": .number(Double(end))])])
|
|
83
|
+
}
|
|
84
|
+
/// A rich `texteditor`'s runs after the editor's own formatting changed their attributes alone (docs/ops-protocol.md, "Rich text editors"): the text unchanged, `runs` as the editor shows it.
|
|
85
|
+
public static func runs(_ id: Int, _ runs: [JSONValue]) -> RendererEvent { with("runs", id: id, ["value": .array(runs)]) }
|
|
78
86
|
/// A `listrow` was deleted by a swipe or in edit mode.
|
|
79
87
|
public static func delete(_ id: Int) -> RendererEvent { with("delete", id: id) }
|
|
80
88
|
/// A `listrow` was moved to index `to` among its siblings.
|
|
@@ -92,6 +100,28 @@ public struct RendererEvent: Equatable, Sendable {
|
|
|
92
100
|
"velocity": .object(["width": .number(velocity.width), "height": .number(velocity.height)]),
|
|
93
101
|
])
|
|
94
102
|
}
|
|
103
|
+
/// A `gesture` with `magnify` or `rotate`: two fingers since `began`. `scale`
|
|
104
|
+
/// is the magnification (1 at the start), `rotation` the turn in degrees,
|
|
105
|
+
/// clockwise positive, the velocities per second; `startLocation` where the
|
|
106
|
+
/// fingers began, in the element's coordinates, `startAnchor` the same as a
|
|
107
|
+
/// fraction of its size, `location` where their midpoint is now (left out
|
|
108
|
+
/// when the renderer cannot tell).
|
|
109
|
+
public static func pinch(
|
|
110
|
+
_ id: Int, phase: String, scale: Double, rotation: Double, scaleVelocity: Double, rotationVelocity: Double,
|
|
111
|
+
startLocation: (x: Double, y: Double), startAnchor: (x: Double, y: Double), location: (x: Double, y: Double)? = nil
|
|
112
|
+
) -> RendererEvent {
|
|
113
|
+
var fields: JSONObject = [
|
|
114
|
+
"phase": .string(phase),
|
|
115
|
+
"scale": .number(scale),
|
|
116
|
+
"rotation": .number(rotation),
|
|
117
|
+
"scaleVelocity": .number(scaleVelocity),
|
|
118
|
+
"rotationVelocity": .number(rotationVelocity),
|
|
119
|
+
"startLocation": .object(["x": .number(startLocation.x), "y": .number(startLocation.y)]),
|
|
120
|
+
"startAnchor": .object(["x": .number(startAnchor.x), "y": .number(startAnchor.y)]),
|
|
121
|
+
]
|
|
122
|
+
if let location { fields["location"] = .object(["x": .number(location.x), "y": .number(location.y)]) }
|
|
123
|
+
return with("pinch", id: id, fields)
|
|
124
|
+
}
|
|
95
125
|
/// A `lazyspacer` came into view: the app mounts the next batch of rows.
|
|
96
126
|
public static func lazy(_ id: Int) -> RendererEvent { with("lazy", id: id) }
|
|
97
127
|
/// A `gesture` long press.
|
|
@@ -100,6 +130,23 @@ public struct RendererEvent: Equatable, Sendable {
|
|
|
100
130
|
public static func refresh(_ id: Int) -> RendererEvent { with("refresh", id: id) }
|
|
101
131
|
/// `.scrollPosition(id:)`: the marked element at the scroll view's anchor once it settled.
|
|
102
132
|
public static func scrollPosition(_ id: Int) -> RendererEvent { with("scrollposition", id: id) }
|
|
133
|
+
/// `.onScrollGeometryChange`: a `scrollview` with `reportsGeometry` scrolled or laid out (points, 2 decimals).
|
|
134
|
+
public static func scrollGeometry(
|
|
135
|
+
_ id: Int, offset: (x: Double, y: Double), contentSize: (width: Double, height: Double),
|
|
136
|
+
containerSize: (width: Double, height: Double), contentInsets: (top: Double, leading: Double, bottom: Double, trailing: Double)
|
|
137
|
+
) -> RendererEvent {
|
|
138
|
+
let r = { (value: Double) in JSONValue.number((value * 100).rounded() / 100) }
|
|
139
|
+
return with("scrollgeometry", id: id, [
|
|
140
|
+
"offset": .object(["x": r(offset.x), "y": r(offset.y)]),
|
|
141
|
+
"contentSize": .object(["width": r(contentSize.width), "height": r(contentSize.height)]),
|
|
142
|
+
"containerSize": .object(["width": r(containerSize.width), "height": r(containerSize.height)]),
|
|
143
|
+
"contentInsets": .object(["top": r(contentInsets.top), "leading": r(contentInsets.leading), "bottom": r(contentInsets.bottom), "trailing": r(contentInsets.trailing)]),
|
|
144
|
+
])
|
|
145
|
+
}
|
|
146
|
+
/// The pass whose `commit` carried this `completion` serial has finished animating (`withAnimation`'s completion).
|
|
147
|
+
public static func animationComplete(serial: Int) -> RendererEvent {
|
|
148
|
+
RendererEvent(type: "animationcomplete", fields: ["serial": .number(Double(serial))])
|
|
149
|
+
}
|
|
103
150
|
/// A `menu` opened or closed (lazy menus build their items on `open`).
|
|
104
151
|
public static func menu(_ id: Int, open: Bool) -> RendererEvent { with("menu", id: id, ["open": .bool(open)]) }
|
|
105
152
|
/// The host's answer to a `native` element's own event (docs/native.md, the escape hatch).
|