@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.
- package/Package.swift +23 -0
- package/README.md +12 -2
- package/Sources/Charts/Charts.swift +65 -0
- package/Sources/FoundationBridge/FoundationBridge.swift +77 -0
- package/Sources/FoundationBridge/Locale.swift +86 -0
- package/Sources/FoundationBridge/Timer.swift +48 -0
- package/Sources/FoundationBridge/UITextChecker.swift +30 -0
- package/Sources/FoundationLite/Common.swift +3 -0
- package/Sources/FoundationLite/FoundationLite.swift +171 -0
- package/Sources/FoundationLite/Interaction.swift +146 -0
- package/Sources/FoundationLite/Timer.swift +48 -0
- package/Sources/SwiftUI/API/Accessibility.swift +132 -0
- package/Sources/SwiftUI/API/Alert.swift +221 -0
- package/Sources/SwiftUI/API/Animation.swift +253 -0
- package/Sources/SwiftUI/API/App.swift +57 -0
- package/Sources/SwiftUI/API/Color.swift +161 -0
- package/Sources/SwiftUI/API/Controls.swift +268 -0
- package/Sources/SwiftUI/API/CustomLayout.swift +403 -0
- package/Sources/SwiftUI/API/Decorations.swift +92 -0
- package/Sources/SwiftUI/API/EditMode.swift +269 -0
- package/Sources/SwiftUI/API/Effects.swift +290 -0
- package/Sources/SwiftUI/API/Environment.swift +355 -0
- package/Sources/SwiftUI/API/Focus.swift +129 -0
- package/Sources/SwiftUI/API/Font.swift +131 -0
- package/Sources/SwiftUI/API/Gauge.swift +289 -0
- package/Sources/SwiftUI/API/Geometry.swift +89 -0
- package/Sources/SwiftUI/API/GeometryReader.swift +124 -0
- package/Sources/SwiftUI/API/Gestures.swift +342 -0
- package/Sources/SwiftUI/API/Grid.swift +188 -0
- package/Sources/SwiftUI/API/LabelStyle.swift +117 -0
- package/Sources/SwiftUI/API/Layout.swift +165 -0
- package/Sources/SwiftUI/API/Lists.swift +276 -0
- package/Sources/SwiftUI/API/Menu.swift +51 -0
- package/Sources/SwiftUI/API/Modifiers.swift +243 -0
- package/Sources/SwiftUI/API/Navigation.swift +371 -0
- package/Sources/SwiftUI/API/ObservableObject.swift +279 -0
- package/Sources/SwiftUI/API/OnChange.swift +48 -0
- package/Sources/SwiftUI/API/Phase8Controls.swift +394 -0
- package/Sources/SwiftUI/API/Picker.swift +176 -0
- package/Sources/SwiftUI/API/Presentation.swift +109 -0
- package/Sources/SwiftUI/API/Primitives.swift +390 -0
- package/Sources/SwiftUI/API/Publishers.swift +107 -0
- package/Sources/SwiftUI/API/RowActions.swift +83 -0
- package/Sources/SwiftUI/API/ScrollTargets.swift +104 -0
- package/Sources/SwiftUI/API/ShapeStyle.swift +311 -0
- package/Sources/SwiftUI/API/State.swift +127 -0
- package/Sources/SwiftUI/API/StrokeStyle.swift +30 -0
- package/Sources/SwiftUI/API/Tabs.swift +119 -0
- package/Sources/SwiftUI/API/Tasks.swift +35 -0
- package/Sources/SwiftUI/API/Timeline.swift +212 -0
- package/Sources/SwiftUI/API/Toolbar.swift +215 -0
- package/Sources/SwiftUI/API/View.swift +207 -0
- package/Sources/SwiftUI/API/ViewModifier.swift +40 -0
- package/Sources/SwiftUI/API/VisualEffect.swift +82 -0
- package/Sources/SwiftUI/Charts/Axis.swift +319 -0
- package/Sources/SwiftUI/Charts/Chart.swift +646 -0
- package/Sources/SwiftUI/Charts/ChartContent.swift +376 -0
- package/Sources/SwiftUI/Charts/Marks.swift +204 -0
- package/Sources/SwiftUI/Charts/Plottable.swift +99 -0
- package/Sources/SwiftUI/Charts/Scales.swift +326 -0
- package/Sources/SwiftUI/Runtime/Executor.swift +83 -0
- package/Sources/SwiftUI/Runtime/Exports.swift +65 -0
- package/Sources/SwiftUI/Runtime/JSON.swift +71 -0
- package/Sources/SwiftUI/Runtime/JSONParser.swift +443 -0
- package/Sources/SwiftUI/Runtime/Ops.swift +106 -0
- package/Sources/SwiftUI/Runtime/Reconciler.swift +210 -0
- package/Sources/SwiftUI/Runtime/Runtime.swift +1367 -0
- package/Sources/_FoundationCommon/Bundle.swift +82 -0
- package/Sources/_FoundationCommon/UserDefaults.swift +170 -0
- package/Sources/_FoundationCommon/_FoundationCommon.swift +10 -0
- package/index.d.ts +2 -0
- package/index.mjs +16 -0
- package/package.json +28 -4
- 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
|
+
}
|