@swiftbrowser/swift 0.0.0-stage → 0.1.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.
Files changed (74) hide show
  1. package/Package.swift +23 -0
  2. package/README.md +12 -2
  3. package/Sources/Charts/Charts.swift +65 -0
  4. package/Sources/FoundationBridge/FoundationBridge.swift +77 -0
  5. package/Sources/FoundationBridge/Locale.swift +86 -0
  6. package/Sources/FoundationBridge/Timer.swift +48 -0
  7. package/Sources/FoundationBridge/UITextChecker.swift +30 -0
  8. package/Sources/FoundationLite/Common.swift +3 -0
  9. package/Sources/FoundationLite/FoundationLite.swift +171 -0
  10. package/Sources/FoundationLite/Interaction.swift +146 -0
  11. package/Sources/FoundationLite/Timer.swift +48 -0
  12. package/Sources/SwiftUI/API/Accessibility.swift +132 -0
  13. package/Sources/SwiftUI/API/Alert.swift +221 -0
  14. package/Sources/SwiftUI/API/Animation.swift +253 -0
  15. package/Sources/SwiftUI/API/App.swift +57 -0
  16. package/Sources/SwiftUI/API/Color.swift +161 -0
  17. package/Sources/SwiftUI/API/Controls.swift +268 -0
  18. package/Sources/SwiftUI/API/CustomLayout.swift +403 -0
  19. package/Sources/SwiftUI/API/Decorations.swift +92 -0
  20. package/Sources/SwiftUI/API/EditMode.swift +269 -0
  21. package/Sources/SwiftUI/API/Effects.swift +290 -0
  22. package/Sources/SwiftUI/API/Environment.swift +355 -0
  23. package/Sources/SwiftUI/API/Focus.swift +129 -0
  24. package/Sources/SwiftUI/API/Font.swift +131 -0
  25. package/Sources/SwiftUI/API/Gauge.swift +289 -0
  26. package/Sources/SwiftUI/API/Geometry.swift +89 -0
  27. package/Sources/SwiftUI/API/GeometryReader.swift +124 -0
  28. package/Sources/SwiftUI/API/Gestures.swift +342 -0
  29. package/Sources/SwiftUI/API/Grid.swift +188 -0
  30. package/Sources/SwiftUI/API/LabelStyle.swift +117 -0
  31. package/Sources/SwiftUI/API/Layout.swift +165 -0
  32. package/Sources/SwiftUI/API/Lists.swift +276 -0
  33. package/Sources/SwiftUI/API/Menu.swift +51 -0
  34. package/Sources/SwiftUI/API/Modifiers.swift +243 -0
  35. package/Sources/SwiftUI/API/Navigation.swift +371 -0
  36. package/Sources/SwiftUI/API/ObservableObject.swift +279 -0
  37. package/Sources/SwiftUI/API/OnChange.swift +48 -0
  38. package/Sources/SwiftUI/API/Phase8Controls.swift +394 -0
  39. package/Sources/SwiftUI/API/Picker.swift +176 -0
  40. package/Sources/SwiftUI/API/Presentation.swift +109 -0
  41. package/Sources/SwiftUI/API/Primitives.swift +390 -0
  42. package/Sources/SwiftUI/API/Publishers.swift +107 -0
  43. package/Sources/SwiftUI/API/RowActions.swift +83 -0
  44. package/Sources/SwiftUI/API/ScrollTargets.swift +104 -0
  45. package/Sources/SwiftUI/API/ShapeStyle.swift +311 -0
  46. package/Sources/SwiftUI/API/State.swift +127 -0
  47. package/Sources/SwiftUI/API/StrokeStyle.swift +30 -0
  48. package/Sources/SwiftUI/API/Tabs.swift +119 -0
  49. package/Sources/SwiftUI/API/Tasks.swift +35 -0
  50. package/Sources/SwiftUI/API/Timeline.swift +212 -0
  51. package/Sources/SwiftUI/API/Toolbar.swift +215 -0
  52. package/Sources/SwiftUI/API/View.swift +207 -0
  53. package/Sources/SwiftUI/API/ViewModifier.swift +40 -0
  54. package/Sources/SwiftUI/API/VisualEffect.swift +82 -0
  55. package/Sources/SwiftUI/Charts/Axis.swift +319 -0
  56. package/Sources/SwiftUI/Charts/Chart.swift +646 -0
  57. package/Sources/SwiftUI/Charts/ChartContent.swift +376 -0
  58. package/Sources/SwiftUI/Charts/Marks.swift +204 -0
  59. package/Sources/SwiftUI/Charts/Plottable.swift +99 -0
  60. package/Sources/SwiftUI/Charts/Scales.swift +326 -0
  61. package/Sources/SwiftUI/Runtime/Executor.swift +83 -0
  62. package/Sources/SwiftUI/Runtime/Exports.swift +65 -0
  63. package/Sources/SwiftUI/Runtime/JSON.swift +71 -0
  64. package/Sources/SwiftUI/Runtime/JSONParser.swift +443 -0
  65. package/Sources/SwiftUI/Runtime/Ops.swift +106 -0
  66. package/Sources/SwiftUI/Runtime/Reconciler.swift +210 -0
  67. package/Sources/SwiftUI/Runtime/Runtime.swift +1367 -0
  68. package/Sources/_FoundationCommon/Bundle.swift +82 -0
  69. package/Sources/_FoundationCommon/UserDefaults.swift +170 -0
  70. package/Sources/_FoundationCommon/_FoundationCommon.swift +10 -0
  71. package/index.d.ts +2 -0
  72. package/index.mjs +16 -0
  73. package/package.json +28 -4
  74. package/sync-sources.mjs +19 -0
@@ -0,0 +1,326 @@
1
+ // Scale domains, "nice" tick generation for numbers and dates, and the
2
+ // default label formats. No Foundation: dates are UTC epoch seconds and the
3
+ // calendar arithmetic is done here (Howard Hinnant's civil date algorithms).
4
+
5
+ // MARK: - ScaleDomain
6
+
7
+ /// The domain of a chart scale: `.automatic(includesZero:)`, a closed range or
8
+ /// an array of categories / values.
9
+ public protocol ScaleDomain {
10
+ var _domainSpec: _ScaleDomainSpec { get }
11
+ }
12
+
13
+ public enum _ScaleDomainSpec: Equatable, Sendable {
14
+ case automatic(includesZero: Bool?, reversed: Bool)
15
+ case values([_PlottableValue])
16
+ }
17
+
18
+ public struct AutomaticScaleDomain: ScaleDomain, Sendable {
19
+ let includesZero: Bool?
20
+ let reversed: Bool
21
+
22
+ public var _domainSpec: _ScaleDomainSpec { .automatic(includesZero: includesZero, reversed: reversed) }
23
+ }
24
+
25
+ extension ScaleDomain where Self == AutomaticScaleDomain {
26
+ public static var automatic: AutomaticScaleDomain { AutomaticScaleDomain(includesZero: nil, reversed: false) }
27
+
28
+ public static func automatic(includesZero: Bool? = nil, reversed: Bool? = nil) -> AutomaticScaleDomain {
29
+ AutomaticScaleDomain(includesZero: includesZero, reversed: reversed ?? false)
30
+ }
31
+ }
32
+
33
+ extension ClosedRange: ScaleDomain where Bound: Plottable {
34
+ public var _domainSpec: _ScaleDomainSpec {
35
+ .values([lowerBound._plottableValue, upperBound._plottableValue])
36
+ }
37
+ }
38
+
39
+ extension Array: ScaleDomain where Element: Plottable {
40
+ public var _domainSpec: _ScaleDomainSpec { .values(map(\._plottableValue)) }
41
+ }
42
+
43
+ // MARK: - Nice numbers
44
+
45
+ /// Tick spacing options from `AxisMarks(values: .automatic(...))`.
46
+ struct _TickOptions: Equatable {
47
+ var desiredCount: Int?
48
+ var minimumStride: Double?
49
+ var roundLowerBound = true
50
+ var roundUpperBound = true
51
+ }
52
+
53
+ /// A "nice" step for a range: 1, 2, 2.5, 5 or 10 × 10^k, whichever is
54
+ /// nearest (geometrically) to `span / (desiredCount − 1)`; about 5 ticks by
55
+ /// default.
56
+ func _niceStep(span: Double, desiredCount: Int?, minimumStride: Double?) -> Double {
57
+ let count = max(1, (desiredCount ?? 5) - 1)
58
+ guard span > 0, span.isFinite else { return max(minimumStride ?? 1, 1) }
59
+ let raw = span / Double(count)
60
+ var magnitude = 1.0
61
+ while magnitude * 10 <= raw { magnitude *= 10 }
62
+ while magnitude > raw { magnitude /= 10 }
63
+ let residual = raw / magnitude
64
+ // Thresholds are the geometric means of neighbouring candidates.
65
+ let factor: Double = if residual >= 7.0710678 { 10 }
66
+ else if residual >= 3.5355339 { 5 }
67
+ else if residual >= 2.2360680 { 2.5 }
68
+ else if residual >= 1.4142136 { 2 }
69
+ else { 1 }
70
+ var step = factor * magnitude
71
+ if let minimumStride, step < minimumStride {
72
+ // Honor the minimum: the smallest nice multiple of the magnitude at or above it.
73
+ var s = minimumStride
74
+ var m = 1.0
75
+ while m * 10 <= s { m *= 10 }
76
+ while m > s { m /= 10 }
77
+ for candidate in [1.0, 2.0, 2.5, 5.0, 10.0] where candidate * m >= s - 1e-9 {
78
+ s = candidate * m
79
+ break
80
+ }
81
+ step = s
82
+ }
83
+ return step
84
+ }
85
+
86
+ /// Rounds away floating noise so `0.30000000000000004` prints as `0.3`.
87
+ func _cleanTick(_ value: Double, step: Double) -> Double {
88
+ var scale = 1.0
89
+ var s = step
90
+ while s < 1, scale < 1e9 { s *= 10; scale *= 10 }
91
+ return (value * scale).rounded() / scale
92
+ }
93
+
94
+ /// Nice ticks over `lower...upper`. The ticks always cover the domain
95
+ /// (first ≤ lower, last ≥ upper); the caller decides whether to extend the
96
+ /// domain to them or drop the ones outside.
97
+ func _niceTicks(lower: Double, upper: Double, options: _TickOptions) -> [Double] {
98
+ let step = _niceStep(span: upper - lower, desiredCount: options.desiredCount, minimumStride: options.minimumStride)
99
+ let first = (lower / step).rounded(.down) * step
100
+ let last = (upper / step).rounded(.up) * step
101
+ var ticks: [Double] = []
102
+ var tick = first
103
+ var guardCount = 0
104
+ while tick <= last + step * 1e-9, guardCount < 1000 {
105
+ ticks.append(_cleanTick(tick, step: step))
106
+ tick += step
107
+ guardCount += 1
108
+ }
109
+ return ticks
110
+ }
111
+
112
+ // MARK: - Civil dates (UTC)
113
+
114
+ struct _CivilDate: Equatable {
115
+ var year: Int
116
+ var month: Int
117
+ var day: Int
118
+ var hour: Int
119
+ var minute: Int
120
+ var second: Int
121
+ }
122
+
123
+ let _secondsPerDay = 86400.0
124
+
125
+ /// Days since 1970-01-01 for a civil date (Hinnant's `days_from_civil`).
126
+ func _daysFromCivil(year: Int, month: Int, day: Int) -> Int {
127
+ let y = month <= 2 ? year - 1 : year
128
+ let era = (y >= 0 ? y : y - 399) / 400
129
+ let yoe = y - era * 400
130
+ let mp = (month + 9) % 12
131
+ let doy = (153 * mp + 2) / 5 + day - 1
132
+ let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
133
+ return era * 146097 + doe - 719468
134
+ }
135
+
136
+ /// The civil date of a day count since 1970-01-01 (Hinnant's `civil_from_days`).
137
+ func _civilFromDays(_ days: Int) -> (year: Int, month: Int, day: Int) {
138
+ let z = days + 719468
139
+ let era = (z >= 0 ? z : z - 146096) / 146097
140
+ let doe = z - era * 146097
141
+ let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
142
+ let y = yoe + era * 400
143
+ let doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
144
+ let mp = (5 * doy + 2) / 153
145
+ let d = doy - (153 * mp + 2) / 5 + 1
146
+ let m = mp < 10 ? mp + 3 : mp - 9
147
+ return (m <= 2 ? y + 1 : y, m, d)
148
+ }
149
+
150
+ func _civil(fromSeconds seconds: Double) -> _CivilDate {
151
+ let days = Int((seconds / _secondsPerDay).rounded(.down))
152
+ var rest = Int((seconds - Double(days) * _secondsPerDay).rounded(.down))
153
+ rest = max(0, min(86399, rest))
154
+ let (y, m, d) = _civilFromDays(days)
155
+ return _CivilDate(year: y, month: m, day: d, hour: rest / 3600, minute: (rest % 3600) / 60, second: rest % 60)
156
+ }
157
+
158
+ func _seconds(year: Int, month: Int, day: Int, hour: Int = 0) -> Double {
159
+ Double(_daysFromCivil(year: year, month: month, day: day)) * _secondsPerDay + Double(hour) * 3600
160
+ }
161
+
162
+ // MARK: - Date ticks
163
+
164
+ /// The calendar unit a date axis labels in.
165
+ enum _DateUnit: Equatable {
166
+ case hour, day, month, year
167
+ }
168
+
169
+ enum _DateFormat: Equatable {
170
+ /// "9 AM", "12 PM"
171
+ case hour
172
+ /// "Sep 30"
173
+ case dayMonth
174
+ /// "Jan"
175
+ case month
176
+ /// "Jan 2026"
177
+ case monthYear
178
+ /// "2026"
179
+ case year
180
+ }
181
+
182
+ let _monthAbbreviations = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
183
+
184
+ func _formatChartDate(_ seconds: Double, as format: _DateFormat) -> String {
185
+ let c = _civil(fromSeconds: seconds)
186
+ switch format {
187
+ case .hour:
188
+ let h12 = c.hour % 12 == 0 ? 12 : c.hour % 12
189
+ let suffix = c.hour < 12 ? "AM" : "PM"
190
+ if c.minute != 0 {
191
+ let minute = c.minute < 10 ? "0\(c.minute)" : "\(c.minute)"
192
+ return "\(h12):\(minute) \(suffix)"
193
+ }
194
+ return "\(h12) \(suffix)"
195
+ case .dayMonth:
196
+ return "\(_monthAbbreviations[c.month - 1]) \(c.day)"
197
+ case .month:
198
+ return _monthAbbreviations[c.month - 1]
199
+ case .monthYear:
200
+ return "\(_monthAbbreviations[c.month - 1]) \(c.year)"
201
+ case .year:
202
+ return "\(c.year)"
203
+ }
204
+ }
205
+
206
+ /// The ticks of a date axis: a unit and stride chosen for the span (hours
207
+ /// for up to three days, days for up to about 45, then months and years),
208
+ /// aligned to calendar boundaries and covering `lower...upper`.
209
+ func _niceDateTicks(lower: Double, upper: Double, options: _TickOptions) -> (ticks: [Double], format: _DateFormat) {
210
+ let span = max(upper - lower, 0)
211
+ let maxIntervals = Double(max(2, (options.desiredCount ?? 7) + 1))
212
+ func fits(_ strideSeconds: Double) -> Bool { span / strideSeconds <= maxIntervals }
213
+
214
+ if span <= 3 * _secondsPerDay {
215
+ let hours = [1.0, 3, 6, 12].first { fits($0 * 3600) } ?? 12
216
+ let step = hours * 3600
217
+ var ticks: [Double] = []
218
+ var tick = (lower / step).rounded(.down) * step
219
+ let last = (upper / step).rounded(.up) * step
220
+ while tick <= last + 1e-6, ticks.count < 1000 {
221
+ ticks.append(tick)
222
+ tick += step
223
+ }
224
+ return (ticks, .hour)
225
+ }
226
+
227
+ if span <= 45 * _secondsPerDay {
228
+ let days = [1.0, 2, 7, 14].first { fits($0 * _secondsPerDay) } ?? 14
229
+ let step = days * _secondsPerDay
230
+ var ticks: [Double] = []
231
+ var tick = (lower / _secondsPerDay).rounded(.down) * _secondsPerDay
232
+ let last = (upper / _secondsPerDay).rounded(.up) * _secondsPerDay
233
+ while tick <= last + 1e-6, ticks.count < 1000 {
234
+ ticks.append(tick)
235
+ tick += step
236
+ }
237
+ // Make sure the last tick covers the upper bound.
238
+ if let final = ticks.last, final < last { ticks.append(final + step) }
239
+ return (ticks, .dayMonth)
240
+ }
241
+
242
+ // Months and years: strides in months.
243
+ let lowerCivil = _civil(fromSeconds: lower)
244
+ let upperCivil = _civil(fromSeconds: upper)
245
+ let spanMonths = Double((upperCivil.year - lowerCivil.year) * 12 + (upperCivil.month - lowerCivil.month) + 1)
246
+ let monthStrides = [1, 2, 3, 6, 12, 24, 60, 120]
247
+ let stride = monthStrides.first { spanMonths / Double($0) <= maxIntervals } ?? 120
248
+ var ticks: [Double] = []
249
+ var year = lowerCivil.year
250
+ var month = lowerCivil.month
251
+ if stride >= 12 {
252
+ month = 1
253
+ let yearsStride = stride / 12
254
+ year = (year / yearsStride) * yearsStride
255
+ } else {
256
+ month = ((month - 1) / stride) * stride + 1
257
+ }
258
+ var crossesYear = false
259
+ while ticks.count < 1000 {
260
+ let tick = _seconds(year: year, month: month, day: 1)
261
+ ticks.append(tick)
262
+ if year != lowerCivil.year { crossesYear = true }
263
+ if tick >= upper { break }
264
+ month += stride
265
+ while month > 12 { month -= 12; year += 1 }
266
+ }
267
+ if stride >= 12 { return (ticks, .year) }
268
+ return (ticks, crossesYear || lowerCivil.year != upperCivil.year ? .monthYear : .month)
269
+ }
270
+
271
+ /// The format for explicit date values: hours when any value is not at
272
+ /// midnight, days when any is not a month start, else months.
273
+ func _dateFormat(forValues values: [Double]) -> _DateFormat {
274
+ var hasHours = false
275
+ var hasDays = false
276
+ var years = Set<Int>()
277
+ for value in values {
278
+ let c = _civil(fromSeconds: value)
279
+ if c.hour != 0 || c.minute != 0 { hasHours = true }
280
+ if c.day != 1 { hasDays = true }
281
+ years.insert(c.year)
282
+ }
283
+ if hasHours { return .hour }
284
+ if hasDays { return .dayMonth }
285
+ return years.count > 1 ? .monthYear : .month
286
+ }
287
+
288
+ // MARK: - Number formatting
289
+
290
+ /// Thousands grouping for a run of digits.
291
+ func _groupThousands(_ digits: String) -> String {
292
+ var out = ""
293
+ for (index, ch) in digits.enumerated() {
294
+ if index > 0, (digits.count - index) % 3 == 0 { out += "," }
295
+ out.append(ch)
296
+ }
297
+ return out
298
+ }
299
+
300
+ /// `formatted()`-like: grouped thousands, up to two fraction digits, no
301
+ /// trailing zeros.
302
+ func _formatChartNumber(_ value: Double) -> String {
303
+ guard value.isFinite else { return "\(value)" }
304
+ let negative = value < 0
305
+ let magnitude = abs(value)
306
+ let scaled = (magnitude * 100).rounded()
307
+ var integer = Int(scaled / 100)
308
+ var fraction = Int(scaled) - integer * 100
309
+ if fraction < 0 { fraction = 0 }
310
+ if fraction >= 100 { integer += 1; fraction = 0 }
311
+ var text = _groupThousands(String(integer))
312
+ if fraction != 0 {
313
+ if fraction % 10 == 0 {
314
+ text += ".\(fraction / 10)"
315
+ } else {
316
+ text += fraction < 10 ? ".0\(fraction)" : ".\(fraction)"
317
+ }
318
+ }
319
+ return (negative && (integer != 0 || fraction != 0) ? "-" : "") + text
320
+ }
321
+
322
+ /// Four decimals, so unit coordinates are byte-stable in snapshots.
323
+ func _unit(_ value: Double) -> Double {
324
+ guard value.isFinite else { return 0 }
325
+ return (value * 10000).rounded() / 10000
326
+ }
@@ -0,0 +1,83 @@
1
+ @_spi(ExperimentalCustomExecutors) @_spi(ExperimentalScheduling) import _Concurrency
2
+
3
+ /// A host-driven cooperative executor. Jobs created by `Task`, `.task` and
4
+ /// sleeps queue here until the host calls `sb_run_jobs`; delays become
5
+ /// timers against a clock the host sets (real time in the browser, a
6
+ /// virtual clock in headless mode). See docs/ops-protocol.md (Phase 4).
7
+ final class _HostExecutor: MainExecutor, SchedulingExecutor, TaskExecutor, @unchecked Sendable {
8
+ static let shared = _HostExecutor()
9
+
10
+ private var queue: [UnownedJob] = []
11
+ private var timers: [(deadlineNs: UInt64, job: UnownedJob)] = []
12
+ /// The executor's clock in nanoseconds; origin is arbitrary.
13
+ var nowNs: UInt64 = 0
14
+
15
+ func enqueue(_ job: consuming ExecutorJob) {
16
+ queue.append(UnownedJob(job))
17
+ }
18
+
19
+ func run() throws {}
20
+ func stop() {}
21
+ func checkIsolated() {}
22
+ func isIsolatingCurrentContext() -> Bool? { true }
23
+
24
+ func enqueue<C: Clock>(_ job: consuming ExecutorJob, after delay: C.Duration, tolerance: C.Duration?, clock: C) {
25
+ timers.append((nowNs + nanoseconds(of: delay), UnownedJob(job)))
26
+ }
27
+
28
+ func enqueue<C: Clock>(_ job: consuming ExecutorJob, at instant: C.Instant, tolerance: C.Duration?, clock: C) {
29
+ let delay = clock.now.duration(to: instant)
30
+ timers.append((nowNs + nanoseconds(of: delay), UnownedJob(job)))
31
+ }
32
+
33
+ /// Delays are rounded so timers stay aligned and headless runs are
34
+ /// deterministic.
35
+ private func nanoseconds<D: DurationProtocol>(of duration: D) -> UInt64 {
36
+ guard let duration = duration as? Duration else { return 0 }
37
+ let (seconds, attoseconds) = duration.components
38
+ let ns = Double(max(0, seconds)) * 1e9 + Double(max(0, attoseconds)) / 1e9
39
+ // 10 ms granularity: the WASI clock is coarse, and `Task.sleep(for:)`
40
+ // computes an absolute instant from it before we see the delay.
41
+ return UInt64((ns / 1e7).rounded() * 1e7)
42
+ }
43
+
44
+ /// Runs every queued job and every due timer, repeatedly, until the
45
+ /// queue is empty. Returns nanoseconds until the next timer, or -1.
46
+ func runJobs() -> Int64 {
47
+ var ran = 0
48
+ repeat {
49
+ ran = 0
50
+ while !queue.isEmpty {
51
+ let job = queue.removeFirst()
52
+ job.runSynchronously(on: asUnownedSerialExecutor())
53
+ ran += 1
54
+ }
55
+ timers.sort { $0.deadlineNs < $1.deadlineNs }
56
+ while let first = timers.first, first.deadlineNs <= nowNs {
57
+ timers.removeFirst()
58
+ queue.append(first.job)
59
+ ran += 1
60
+ }
61
+ } while ran > 0
62
+ guard let next = timers.first else { return -1 }
63
+ return Int64(next.deadlineNs - nowNs)
64
+ }
65
+
66
+ var hasTimers: Bool { !timers.isEmpty }
67
+ }
68
+
69
+ enum _HostExecutorFactory: ExecutorFactory {
70
+ static var mainExecutor: any MainExecutor { _HostExecutor.shared }
71
+ static var defaultExecutor: any TaskExecutor { _HostExecutor.shared }
72
+ }
73
+
74
+ nonisolated(unsafe) private var executorInstalled = false
75
+
76
+ /// Installs the host-driven executor. Must run before any task is created;
77
+ /// `App.main()` calls it. Native test processes must not install it, since
78
+ /// the test runner drives the main actor itself.
79
+ public func _installHostExecutor() {
80
+ guard !executorInstalled else { return }
81
+ executorInstalled = true
82
+ _createExecutors(factory: _HostExecutorFactory.self)
83
+ }
@@ -0,0 +1,65 @@
1
+ // C-ABI entry points for the browser renderer. See docs/ops-protocol.md.
2
+ // Wasm is single-threaded, so the main actor is the only executor.
3
+
4
+ @_cdecl("sb_event")
5
+ public func _sb_event(_ id: Int32) {
6
+ MainActor.assumeIsolated {
7
+ _Runtime.shared.dispatch(tap: Int(id))
8
+ }
9
+ }
10
+
11
+ @_cdecl("sb_ops_ptr")
12
+ public func _sb_ops_ptr() -> Int32 {
13
+ MainActor.assumeIsolated { _BufferedHost.shared.pointer }
14
+ }
15
+
16
+ @_cdecl("sb_ops_len")
17
+ public func _sb_ops_len() -> Int32 {
18
+ MainActor.assumeIsolated { _BufferedHost.shared.length }
19
+ }
20
+
21
+ @_cdecl("sb_ops_clear")
22
+ public func _sb_ops_clear() {
23
+ MainActor.assumeIsolated { _BufferedHost.shared.clear() }
24
+ }
25
+
26
+ /// Allocates a buffer the renderer fills before calling `sb_event_json`.
27
+ @_cdecl("sb_alloc")
28
+ public func _sb_alloc(_ length: Int32) -> Int32 {
29
+ let pointer = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(max(length, 1)))
30
+ return Int32(Int(bitPattern: pointer))
31
+ }
32
+
33
+ /// Delivers one UTF-8 JSON event written into a buffer from `sb_alloc`.
34
+ @_cdecl("sb_event_json")
35
+ public func _sb_event_json(_ pointer: Int32, _ length: Int32) {
36
+ guard let base = UnsafeMutablePointer<UInt8>(bitPattern: Int(pointer)) else { return }
37
+ let text = String(decoding: UnsafeBufferPointer(start: base, count: Int(length)), as: UTF8.self)
38
+ base.deallocate()
39
+ MainActor.assumeIsolated {
40
+ guard let event = try? _JSON.parse(text) else {
41
+ print("[SwiftUI] ignoring malformed event: \(text)")
42
+ return
43
+ }
44
+ _Runtime.shared.dispatch(event: event)
45
+ }
46
+ }
47
+
48
+ @_cdecl("sb_state_ptr")
49
+ public func _sb_state_ptr() -> Int32 {
50
+ MainActor.assumeIsolated {
51
+ _StateBuffer.shared.refresh(with: _Runtime.shared.snapshot())
52
+ return _StateBuffer.shared.pointer
53
+ }
54
+ }
55
+
56
+ @_cdecl("sb_state_len")
57
+ public func _sb_state_len() -> Int32 {
58
+ MainActor.assumeIsolated { _StateBuffer.shared.length }
59
+ }
60
+
61
+ /// Drives Swift concurrency: see `_Runtime.runJobs(nowMilliseconds:)`.
62
+ @_cdecl("sb_run_jobs")
63
+ public func _sb_run_jobs(_ nowMilliseconds: Double) -> Double {
64
+ MainActor.assumeIsolated { _Runtime.shared.runJobs(nowMilliseconds: nowMilliseconds) }
65
+ }
@@ -0,0 +1,71 @@
1
+ /// A minimal JSON value. The shim avoids Foundation (see Geometry.swift), so it
2
+ /// carries its own serializer. Object keys serialize in sorted order so the
3
+ /// output is byte-stable for snapshot tests.
4
+ public enum _JSON: Equatable, Sendable {
5
+ case null
6
+ case bool(Bool)
7
+ case number(Double)
8
+ case string(String)
9
+ case array([_JSON])
10
+ case object([String: _JSON])
11
+
12
+ public var serialized: String {
13
+ var out = ""
14
+ write(into: &out)
15
+ return out
16
+ }
17
+
18
+ private func write(into out: inout String) {
19
+ switch self {
20
+ case .null:
21
+ out += "null"
22
+ case .bool(let value):
23
+ out += value ? "true" : "false"
24
+ case .number(let value):
25
+ if value.isFinite, value == value.rounded(), abs(value) < 1e15 {
26
+ out += String(Int64(value))
27
+ } else if value.isFinite {
28
+ out += "\(value)"
29
+ } else {
30
+ out += "null"
31
+ }
32
+ case .string(let value):
33
+ _JSON.writeString(value, into: &out)
34
+ case .array(let items):
35
+ out += "["
36
+ for (index, item) in items.enumerated() {
37
+ if index > 0 { out += "," }
38
+ item.write(into: &out)
39
+ }
40
+ out += "]"
41
+ case .object(let members):
42
+ out += "{"
43
+ for (index, key) in members.keys.sorted().enumerated() {
44
+ if index > 0 { out += "," }
45
+ _JSON.writeString(key, into: &out)
46
+ out += ":"
47
+ members[key]!.write(into: &out)
48
+ }
49
+ out += "}"
50
+ }
51
+ }
52
+
53
+ private static func writeString(_ value: String, into out: inout String) {
54
+ out += "\""
55
+ for scalar in value.unicodeScalars {
56
+ switch scalar {
57
+ case "\"": out += "\\\""
58
+ case "\\": out += "\\\\"
59
+ case "\n": out += "\\n"
60
+ case "\r": out += "\\r"
61
+ case "\t": out += "\\t"
62
+ case _ where scalar.value < 0x20:
63
+ let hex = String(scalar.value, radix: 16)
64
+ out += "\\u" + String(repeating: "0", count: 4 - hex.count) + hex
65
+ default:
66
+ out.unicodeScalars.append(scalar)
67
+ }
68
+ }
69
+ out += "\""
70
+ }
71
+ }