@swiftbrowser/native 0.18.1 → 0.18.3
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 +73 -8
- package/dist/bridge.d.ts +64 -2
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js.map +1 -1
- package/dist/geolocation.d.ts +73 -0
- package/dist/geolocation.d.ts.map +1 -0
- package/dist/geolocation.js +109 -0
- package/dist/geolocation.js.map +1 -0
- package/dist/hostRequests.d.ts +3 -1
- package/dist/hostRequests.d.ts.map +1 -1
- package/dist/hostRequests.js +46 -5
- package/dist/hostRequests.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/nativeFunctions.d.ts +35 -0
- package/dist/nativeFunctions.d.ts.map +1 -0
- package/dist/nativeFunctions.js +90 -0
- package/dist/nativeFunctions.js.map +1 -0
- package/dist/reachability.d.ts +24 -0
- package/dist/reachability.d.ts.map +1 -0
- package/dist/reachability.js +41 -0
- package/dist/reachability.js.map +1 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +3 -1
- package/dist/run.js.map +1 -1
- package/dist/runtime.d.ts +9 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +62 -5
- package/dist/runtime.js.map +1 -1
- package/dist/speech.d.ts +148 -0
- package/dist/speech.d.ts.map +1 -0
- package/dist/speech.js +243 -0
- package/dist/speech.js.map +1 -0
- package/dist/wakeLock.d.ts +32 -0
- package/dist/wakeLock.d.ts.map +1 -0
- package/dist/wakeLock.js +60 -0
- package/dist/wakeLock.js.map +1 -0
- package/package.json +3 -3
- package/src/bridge.ts +70 -2
- package/src/geolocation.ts +160 -0
- package/src/hostRequests.ts +47 -7
- package/src/index.ts +1 -0
- package/src/nativeFunctions.ts +94 -0
- package/src/reachability.ts +49 -0
- package/src/run.ts +5 -3
- package/src/runtime.ts +58 -5
- package/src/speech.ts +312 -0
- package/src/wakeLock.ts +63 -0
- package/swift/Sources/SwiftBrowserNative/Host/AppleSignInBridge.swift +139 -0
- package/swift/Sources/SwiftBrowserNative/Host/AudioRecorderBridge.swift +5 -0
- package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +83 -12
- package/swift/Sources/SwiftBrowserNative/Host/GeolocationBridge.swift +150 -0
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +131 -2
- package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +81 -11
- package/swift/Sources/SwiftBrowserNative/Host/ICloudBridge.swift +112 -0
- package/swift/Sources/SwiftBrowserNative/Host/ReachabilityBridge.swift +47 -0
- package/swift/Sources/SwiftBrowserNative/Host/SpeechBridge.swift +106 -0
- package/swift/Sources/SwiftBrowserNative/Host/SpeechRecognitionBridge.swift +198 -0
- package/swift/Sources/SwiftBrowserNative/Host/WorkerHost.swift +14 -5
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeFunctions.swift +134 -0
- package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +1 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +2 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/TextInputHints.swift +105 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Touches.swift +102 -0
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +13 -0
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +9 -0
- package/wasm/SwiftBrowserNative.wasm +2 -2
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#if canImport(SwiftUI)
|
|
2
|
+
import SwiftBrowserProtocol
|
|
3
|
+
import SwiftUI
|
|
4
|
+
#if canImport(UIKit)
|
|
5
|
+
import UIKit
|
|
6
|
+
#elseif canImport(AppKit)
|
|
7
|
+
import AppKit
|
|
8
|
+
#endif
|
|
9
|
+
|
|
10
|
+
/// A `textfield`'s text input hints (docs/ops-protocol.md, "Text input
|
|
11
|
+
/// hints") as SwiftUI's own modifiers: `.keyboardType` and
|
|
12
|
+
/// `.textInputAutocapitalization` where there is an on-screen keyboard,
|
|
13
|
+
/// `.textContentType`, which is what AutoFill and the QuickType bar offer (an
|
|
14
|
+
/// email field with `emailAddress` suggests the viewer's addresses; a
|
|
15
|
+
/// `oneTimeCode` field the code from Messages), and `.autocorrectionDisabled`.
|
|
16
|
+
/// macOS takes the content type and the correction. A name the platform has
|
|
17
|
+
/// no case for is left to the default, as the web renderer leaves it to the
|
|
18
|
+
/// browser. Against the shim (the round trip) the modifiers are the shim's,
|
|
19
|
+
/// which carry the names on as props.
|
|
20
|
+
extension View {
|
|
21
|
+
func textInputHints(_ props: TextFieldProps) -> some View {
|
|
22
|
+
var view = AnyView(self)
|
|
23
|
+
#if os(iOS) || canImport(FoundationBridge)
|
|
24
|
+
if let keyboard = props.keyboard.flatMap(keyboardTypeNamed) { view = AnyView(view.keyboardType(keyboard)) }
|
|
25
|
+
if let capitalization = props.autocapitalization.flatMap(autocapitalizationNamed) {
|
|
26
|
+
view = AnyView(view.textInputAutocapitalization(capitalization))
|
|
27
|
+
}
|
|
28
|
+
#endif
|
|
29
|
+
if let contentType = props.contentType.flatMap(contentTypeNamed) { view = AnyView(view.textContentType(contentType)) }
|
|
30
|
+
if props.autocorrectionDisabled { view = AnyView(view.autocorrectionDisabled()) }
|
|
31
|
+
return view
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#if os(iOS) || canImport(FoundationBridge)
|
|
36
|
+
/// The protocol's `keyboard` names: `UIKeyboardType`'s cases.
|
|
37
|
+
private func keyboardTypeNamed(_ name: String) -> UIKeyboardType? {
|
|
38
|
+
switch name {
|
|
39
|
+
case "default": .default
|
|
40
|
+
case "asciiCapable": .asciiCapable
|
|
41
|
+
case "numbersAndPunctuation": .numbersAndPunctuation
|
|
42
|
+
case "URL": .URL
|
|
43
|
+
case "numberPad": .numberPad
|
|
44
|
+
case "phonePad": .phonePad
|
|
45
|
+
case "namePhonePad": .namePhonePad
|
|
46
|
+
case "emailAddress": .emailAddress
|
|
47
|
+
case "decimalPad": .decimalPad
|
|
48
|
+
case "twitter": .twitter
|
|
49
|
+
case "webSearch": .webSearch
|
|
50
|
+
case "asciiCapableNumberPad": .asciiCapableNumberPad
|
|
51
|
+
default: nil
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private func autocapitalizationNamed(_ name: String) -> TextInputAutocapitalization? {
|
|
56
|
+
switch name {
|
|
57
|
+
case "never": .never
|
|
58
|
+
case "words": .words
|
|
59
|
+
case "sentences": .sentences
|
|
60
|
+
case "characters": .characters
|
|
61
|
+
default: nil
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// The protocol's `contentType` names: `UITextContentType`'s.
|
|
66
|
+
private func contentTypeNamed(_ name: String) -> UITextContentType? {
|
|
67
|
+
switch name {
|
|
68
|
+
case "name": .name
|
|
69
|
+
case "givenName": .givenName
|
|
70
|
+
case "familyName": .familyName
|
|
71
|
+
case "emailAddress": .emailAddress
|
|
72
|
+
case "telephoneNumber": .telephoneNumber
|
|
73
|
+
case "username": .username
|
|
74
|
+
case "password": .password
|
|
75
|
+
case "newPassword": .newPassword
|
|
76
|
+
case "oneTimeCode": .oneTimeCode
|
|
77
|
+
case "URL": .URL
|
|
78
|
+
case "fullStreetAddress": .fullStreetAddress
|
|
79
|
+
case "postalCode": .postalCode
|
|
80
|
+
case "creditCardNumber": .creditCardNumber
|
|
81
|
+
default: nil
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
#elseif os(macOS)
|
|
85
|
+
/// The protocol's `contentType` names as `NSTextContentType`, which has them all from macOS 14.
|
|
86
|
+
private func contentTypeNamed(_ name: String) -> NSTextContentType? {
|
|
87
|
+
switch name {
|
|
88
|
+
case "name": .name
|
|
89
|
+
case "givenName": .givenName
|
|
90
|
+
case "familyName": .familyName
|
|
91
|
+
case "emailAddress": .emailAddress
|
|
92
|
+
case "telephoneNumber": .telephoneNumber
|
|
93
|
+
case "username": .username
|
|
94
|
+
case "password": .password
|
|
95
|
+
case "newPassword": .newPassword
|
|
96
|
+
case "oneTimeCode": .oneTimeCode
|
|
97
|
+
case "URL": .URL
|
|
98
|
+
case "fullStreetAddress": .fullStreetAddress
|
|
99
|
+
case "postalCode": .postalCode
|
|
100
|
+
case "creditCardNumber": .creditCardNumber
|
|
101
|
+
default: nil
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
#endif
|
|
105
|
+
#endif
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#if canImport(SwiftUI)
|
|
2
|
+
import SwiftBrowserProtocol
|
|
3
|
+
import SwiftBrowserTree
|
|
4
|
+
import SwiftUI
|
|
5
|
+
|
|
6
|
+
/// `touches`: every finger on the element (docs/ops-protocol.md,
|
|
7
|
+
/// "Multi-touch"), for chords and glissandi. On iOS 18 and macOS 15
|
|
8
|
+
/// `SpatialEventGesture` reports them all; before that, and against the shim
|
|
9
|
+
/// (the round trip), a `DragGesture` with no minimum distance reports the one
|
|
10
|
+
/// finger it can. Each change sends `touches` with every finger still down,
|
|
11
|
+
/// in the element's coordinates.
|
|
12
|
+
struct TouchesElement: View {
|
|
13
|
+
let node: ElementNode
|
|
14
|
+
@Environment(\.nativeApp) private var app
|
|
15
|
+
@State private var tracker = TouchTracker()
|
|
16
|
+
|
|
17
|
+
var body: some View {
|
|
18
|
+
let content = ChildrenView(node: node).contentShape(Rectangle())
|
|
19
|
+
#if os(iOS) || os(macOS)
|
|
20
|
+
if #available(iOS 18.0, macOS 15.0, *) {
|
|
21
|
+
content.gesture(
|
|
22
|
+
SpatialEventGesture()
|
|
23
|
+
.onChanged { events in report(events) }
|
|
24
|
+
.onEnded { events in report(events) }
|
|
25
|
+
)
|
|
26
|
+
} else {
|
|
27
|
+
single(content)
|
|
28
|
+
}
|
|
29
|
+
#else
|
|
30
|
+
single(content)
|
|
31
|
+
#endif
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#if os(iOS) || os(macOS)
|
|
35
|
+
@available(iOS 18.0, macOS 15.0, *)
|
|
36
|
+
private func report(_ events: SpatialEventCollection) {
|
|
37
|
+
let active = events.filter { $0.phase == .active }.map { (tracker.number(for: AnyHashable($0.id)), $0.location) }
|
|
38
|
+
let cancelled = events.contains { $0.phase == .cancelled }
|
|
39
|
+
send(tracker.update(active: active, cancelled: cancelled))
|
|
40
|
+
}
|
|
41
|
+
#endif
|
|
42
|
+
|
|
43
|
+
/// One finger, where the system reports no more.
|
|
44
|
+
private func single(_ content: some View) -> some View {
|
|
45
|
+
content.gesture(
|
|
46
|
+
DragGesture(minimumDistance: 0)
|
|
47
|
+
.onChanged { value in send(tracker.update(active: [(1, value.location)], cancelled: false)) }
|
|
48
|
+
.onEnded { _ in send(tracker.update(active: [], cancelled: false)) }
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private func send(_ change: TouchTracker.Change?) {
|
|
53
|
+
guard let change else { return }
|
|
54
|
+
app?.dispatch(.touches(node.id, phase: change.phase, touches: change.touches.map { (id: $0.id, x: $0.x, y: $0.y) }))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// The fingers last reported on a `touches` element, so each report tells
|
|
59
|
+
/// its phase. A reference, so a move redraws nothing.
|
|
60
|
+
final class TouchTracker {
|
|
61
|
+
struct Change: Equatable {
|
|
62
|
+
var phase: String
|
|
63
|
+
var touches: [Touch]
|
|
64
|
+
}
|
|
65
|
+
struct Touch: Equatable {
|
|
66
|
+
var id: Int
|
|
67
|
+
var x: Double
|
|
68
|
+
var y: Double
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// The system's event identifiers as small integers, stable while a finger is down.
|
|
72
|
+
private var numbers: [AnyHashable: Int] = [:]
|
|
73
|
+
private var next = 1
|
|
74
|
+
/// The fingers down, in order of arrival.
|
|
75
|
+
private var down: [(id: Int, point: CGPoint)] = []
|
|
76
|
+
|
|
77
|
+
func number(for id: AnyHashable) -> Int {
|
|
78
|
+
if let known = numbers[id] { return known }
|
|
79
|
+
numbers[id] = next
|
|
80
|
+
next += 1
|
|
81
|
+
return next - 1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Takes the fingers now down; what to report, or nil when nothing changed.
|
|
85
|
+
func update(active: [(Int, CGPoint)], cancelled: Bool) -> Change? {
|
|
86
|
+
let before = down
|
|
87
|
+
let beforeIds = Set(before.map(\.id))
|
|
88
|
+
let nowIds = Set(active.map(\.0))
|
|
89
|
+
let began = !nowIds.subtracting(beforeIds).isEmpty
|
|
90
|
+
let ended = !beforeIds.subtracting(nowIds).isEmpty
|
|
91
|
+
let points = Dictionary(active.map { ($0.0, $0.1) }, uniquingKeysWith: { _, last in last })
|
|
92
|
+
let moved = before.contains { finger in points[finger.id].map { $0 != finger.point } ?? false }
|
|
93
|
+
down = before.compactMap { finger in points[finger.id].map { (id: finger.id, point: $0) } }
|
|
94
|
+
+ active.filter { !beforeIds.contains($0.0) }.map { (id: $0.0, point: $0.1) }
|
|
95
|
+
if nowIds.isEmpty { numbers.removeAll() } else { numbers = numbers.filter { nowIds.contains($0.value) } }
|
|
96
|
+
let phase: String? = began ? "began" : ended ? (cancelled && nowIds.isEmpty ? "cancelled" : "ended") : moved ? "changed" : nil
|
|
97
|
+
guard let phase else { return nil }
|
|
98
|
+
let round = { (value: CGFloat) in (Double(value) * 100).rounded() / 100 }
|
|
99
|
+
return Change(phase: phase, touches: down.map { Touch(id: $0.id, x: round($0.point.x), y: round($0.point.y)) })
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
#endif
|
|
@@ -240,6 +240,15 @@ public struct TextFieldProps: Equatable, Sendable {
|
|
|
240
240
|
public var deleteBackward: Bool
|
|
241
241
|
public var submitLabel: String?
|
|
242
242
|
public var vertical: Bool
|
|
243
|
+
/// The text input hints (docs/ops-protocol.md, "Text input hints").
|
|
244
|
+
/// `.keyboardType(_:)`: a `UIKeyboardType` case name (`emailAddress`, `numberPad`…); nil for the default.
|
|
245
|
+
public var keyboard: String?
|
|
246
|
+
/// `.textContentType(_:)`: a `UITextContentType` name (`emailAddress`, `oneTimeCode`…), what autofill offers.
|
|
247
|
+
public var contentType: String?
|
|
248
|
+
/// `.textInputAutocapitalization(_:)`: `never`, `words`, `sentences` or `characters`.
|
|
249
|
+
public var autocapitalization: String?
|
|
250
|
+
/// `.autocorrectionDisabled()`.
|
|
251
|
+
public var autocorrectionDisabled: Bool
|
|
243
252
|
public init(props: JSONObject) {
|
|
244
253
|
text = props.string("text") ?? ""
|
|
245
254
|
placeholder = props.string("placeholder") ?? ""
|
|
@@ -250,6 +259,10 @@ public struct TextFieldProps: Equatable, Sendable {
|
|
|
250
259
|
deleteBackward = props.bool("deleteBackward") ?? false
|
|
251
260
|
submitLabel = props.string("submitLabel")
|
|
252
261
|
vertical = props.string("axis") == "vertical"
|
|
262
|
+
keyboard = props.string("keyboard")
|
|
263
|
+
contentType = props.string("contentType")
|
|
264
|
+
autocapitalization = props.string("autocapitalization")
|
|
265
|
+
autocorrectionDisabled = props.bool("autocorrectionDisabled") ?? false
|
|
253
266
|
}
|
|
254
267
|
}
|
|
255
268
|
|
|
@@ -126,6 +126,15 @@ public struct RendererEvent: Equatable, Sendable {
|
|
|
126
126
|
public static func lazy(_ id: Int) -> RendererEvent { with("lazy", id: id) }
|
|
127
127
|
/// A `gesture` long press.
|
|
128
128
|
public static func longPress(_ id: Int) -> RendererEvent { with("longpress", id: id) }
|
|
129
|
+
/// A `touches` element's pointers changed (docs/ops-protocol.md, "Multi-touch"): `phase` as a drag's
|
|
130
|
+
/// (`began` a finger down, `changed` moved, `ended` lifted, `cancelled` all lost); `touches` every finger
|
|
131
|
+
/// still down, in the element's coordinates, in order of arrival.
|
|
132
|
+
public static func touches(_ id: Int, phase: String, touches: [(id: Int, x: Double, y: Double)]) -> RendererEvent {
|
|
133
|
+
with("touches", id: id, [
|
|
134
|
+
"phase": .string(phase),
|
|
135
|
+
"touches": .array(touches.map { .object(["id": .number(Double($0.id)), "x": .number($0.x), "y": .number($0.y)]) }),
|
|
136
|
+
])
|
|
137
|
+
}
|
|
129
138
|
/// A `list` or `scrollview` with `refreshable` was pulled.
|
|
130
139
|
public static func refresh(_ id: Int) -> RendererEvent { with("refresh", id: id) }
|
|
131
140
|
/// `.scrollPosition(id:)`: the marked element at the scroll view's anchor once it settled.
|
|
@@ -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: 50071156 bytes
|
|
4
|
+
sha256: f976d6cb271c4fe729a1e960f0ddd5105bc868d35c85a4c1eb8bf79d19ac5e93
|