@swiftbrowser/native 0.18.1 → 0.18.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.
Files changed (69) hide show
  1. package/README.md +73 -8
  2. package/dist/bridge.d.ts +64 -2
  3. package/dist/bridge.d.ts.map +1 -1
  4. package/dist/bridge.js.map +1 -1
  5. package/dist/geolocation.d.ts +73 -0
  6. package/dist/geolocation.d.ts.map +1 -0
  7. package/dist/geolocation.js +109 -0
  8. package/dist/geolocation.js.map +1 -0
  9. package/dist/hostRequests.d.ts +3 -1
  10. package/dist/hostRequests.d.ts.map +1 -1
  11. package/dist/hostRequests.js +46 -5
  12. package/dist/hostRequests.js.map +1 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/nativeFunctions.d.ts +35 -0
  18. package/dist/nativeFunctions.d.ts.map +1 -0
  19. package/dist/nativeFunctions.js +90 -0
  20. package/dist/nativeFunctions.js.map +1 -0
  21. package/dist/reachability.d.ts +24 -0
  22. package/dist/reachability.d.ts.map +1 -0
  23. package/dist/reachability.js +41 -0
  24. package/dist/reachability.js.map +1 -0
  25. package/dist/run.d.ts.map +1 -1
  26. package/dist/run.js +3 -1
  27. package/dist/run.js.map +1 -1
  28. package/dist/runtime.d.ts +9 -2
  29. package/dist/runtime.d.ts.map +1 -1
  30. package/dist/runtime.js +62 -5
  31. package/dist/runtime.js.map +1 -1
  32. package/dist/speech.d.ts +148 -0
  33. package/dist/speech.d.ts.map +1 -0
  34. package/dist/speech.js +243 -0
  35. package/dist/speech.js.map +1 -0
  36. package/dist/wakeLock.d.ts +32 -0
  37. package/dist/wakeLock.d.ts.map +1 -0
  38. package/dist/wakeLock.js +60 -0
  39. package/dist/wakeLock.js.map +1 -0
  40. package/package.json +3 -3
  41. package/src/bridge.ts +70 -2
  42. package/src/geolocation.ts +160 -0
  43. package/src/hostRequests.ts +47 -7
  44. package/src/index.ts +1 -0
  45. package/src/nativeFunctions.ts +94 -0
  46. package/src/reachability.ts +49 -0
  47. package/src/run.ts +5 -3
  48. package/src/runtime.ts +58 -5
  49. package/src/speech.ts +312 -0
  50. package/src/wakeLock.ts +63 -0
  51. package/swift/Sources/SwiftBrowserNative/Host/AppleSignInBridge.swift +139 -0
  52. package/swift/Sources/SwiftBrowserNative/Host/AudioRecorderBridge.swift +5 -0
  53. package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +83 -12
  54. package/swift/Sources/SwiftBrowserNative/Host/GeolocationBridge.swift +150 -0
  55. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +131 -2
  56. package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +81 -11
  57. package/swift/Sources/SwiftBrowserNative/Host/ICloudBridge.swift +112 -0
  58. package/swift/Sources/SwiftBrowserNative/Host/ReachabilityBridge.swift +47 -0
  59. package/swift/Sources/SwiftBrowserNative/Host/SpeechBridge.swift +106 -0
  60. package/swift/Sources/SwiftBrowserNative/Host/SpeechRecognitionBridge.swift +198 -0
  61. package/swift/Sources/SwiftBrowserNative/Host/WorkerHost.swift +14 -5
  62. package/swift/Sources/SwiftBrowserNative/Runtime/NativeFunctions.swift +134 -0
  63. package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +1 -0
  64. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +2 -0
  65. package/swift/Sources/SwiftBrowserNative/Views/Kinds/TextInputHints.swift +105 -0
  66. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Touches.swift +102 -0
  67. package/swift/Sources/SwiftBrowserProtocol/Props.swift +13 -0
  68. package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +9 -0
  69. 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: 49982707 bytes
4
- sha256: 1f02220b4e3d7e74ce3afb42d5336ba9254bb1cd1cb591409c878548c4a6f6c4
3
+ size: 50070927 bytes
4
+ sha256: a7eac05991af00f6c0975482a6ea350ecb020819984e3874d597af5929f7e233