@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,390 @@
1
+ // MARK: - Text
2
+
3
+ /// A localization key. Without localization tables the key is the text;
4
+ /// interpolations are rendered into it.
5
+ public struct LocalizedStringKey: Equatable, Sendable, ExpressibleByStringInterpolation {
6
+ public let key: String
7
+
8
+ public init(_ value: String) { key = value }
9
+ public init(stringLiteral value: String) { key = value }
10
+ public init(stringInterpolation: StringInterpolation) { key = stringInterpolation.text }
11
+
12
+ public struct StringInterpolation: StringInterpolationProtocol {
13
+ var text = ""
14
+ public init(literalCapacity: Int, interpolationCount: Int) {}
15
+ public mutating func appendLiteral(_ literal: String) { text += literal }
16
+ public mutating func appendInterpolation<T>(_ value: T) { text += String(describing: value) }
17
+ public mutating func appendInterpolation<T>(_ value: T, specifier: String) { text += String(describing: value) }
18
+ }
19
+ }
20
+
21
+ public struct Text: Equatable, Sendable {
22
+ let content: String
23
+
24
+ public init(verbatim content: String) { self.content = content }
25
+ public init(_ key: LocalizedStringKey) { self.content = key.key }
26
+ @_disfavoredOverload
27
+ public init<S: StringProtocol>(_ content: S) { self.content = String(content) }
28
+
29
+ /// A value formatted by `format` (`FoundationLite` supplies the formats;
30
+ /// FoundationBridge adds Foundation's `FormatStyle`).
31
+ public init<F: _TextFormat>(_ input: F.Input, format: F) { content = format._formatText(input) }
32
+
33
+ /// The text as a string, for bridge modules (a `prompt` becomes a placeholder).
34
+ public var _content: String { content }
35
+ }
36
+ extension Text: _PrimitiveView {}
37
+ extension Text: _HostView {
38
+ var _kind: String { "text" }
39
+ func _props(in environment: EnvironmentValues) -> _JSON { .object(["text": .string(content)]) }
40
+ }
41
+
42
+ // MARK: - Image
43
+
44
+ /// An SF Symbol (`systemName`) or an image from the app's asset catalog
45
+ /// (`name`, possibly namespaced like `"dough/plain-full"`). The renderer owns
46
+ /// the pixels: asset images come from the catalog manifest the web side
47
+ /// builds from `Examples/<App>/*.xcassets` (see docs/ops-protocol.md).
48
+ public struct Image: Equatable, Sendable {
49
+ enum Source: Equatable, Sendable {
50
+ case system(String)
51
+ case named(String)
52
+ }
53
+
54
+ let source: Source
55
+ /// `.resizable()`: fill the proposed size instead of the intrinsic one.
56
+ let isResizable: Bool
57
+
58
+ public init(systemName: String) {
59
+ source = .system(systemName)
60
+ isResizable = false
61
+ }
62
+
63
+ /// An image from the asset catalog. SwiftUI's `bundle:` parameter is not
64
+ /// offered: a Wasm app has no bundles, the catalog is the whole app's.
65
+ public init(_ name: String) {
66
+ source = .named(name)
67
+ isResizable = false
68
+ }
69
+
70
+ /// A catalog image hidden from assistive technology (no bridge in the
71
+ /// browser, so it draws like `Image(_:)`). As for `Image(_:)`, no `bundle:`.
72
+ public init(decorative name: String) {
73
+ source = .named(name)
74
+ isResizable = false
75
+ }
76
+
77
+ init(source: Source, isResizable: Bool) {
78
+ self.source = source
79
+ self.isResizable = isResizable
80
+ }
81
+
82
+ /// The SF Symbol name, for labels that render the icon as a glyph (tab
83
+ /// items, menu pickers); nil for catalog images.
84
+ var systemName: String? {
85
+ if case .system(let name) = source { return name }
86
+ return nil
87
+ }
88
+
89
+ public func resizable() -> Image { Image(source: source, isResizable: true) }
90
+
91
+ /// Accepted for source compatibility; the browser picks the interpolation.
92
+ public enum Interpolation: Hashable, Sendable {
93
+ case none, low, medium, high
94
+ }
95
+
96
+ public func interpolation(_ interpolation: Interpolation) -> Image { self }
97
+ public func antialiased(_ isAntialiased: Bool) -> Image { self }
98
+ }
99
+ extension Image: _PrimitiveView {}
100
+ extension Image: _HostView {
101
+ var _kind: String { "image" }
102
+ func _props(in environment: EnvironmentValues) -> _JSON {
103
+ switch source {
104
+ case .system(let name):
105
+ return .object(["systemName": .string(name)])
106
+ case .named(let name):
107
+ return .object(["name": .string(name), "resizable": .bool(isResizable)])
108
+ }
109
+ }
110
+ }
111
+
112
+ // MARK: - Stacks
113
+
114
+ public struct VStack<Content: View> {
115
+ let alignment: HorizontalAlignment
116
+ let spacing: CGFloat?
117
+ let content: Content
118
+
119
+ public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil,
120
+ @ViewBuilder content: () -> Content) {
121
+ self.alignment = alignment
122
+ self.spacing = spacing
123
+ self.content = content()
124
+ }
125
+ }
126
+ extension VStack: _PrimitiveView {}
127
+ extension VStack: _HostView {
128
+ var _kind: String { "vstack" }
129
+ func _props(in environment: EnvironmentValues) -> _JSON {
130
+ .object(["spacing": spacing.map(_JSON.number) ?? .null, "alignment": .string(alignment.name)])
131
+ }
132
+ var _content: [any View] { [content] }
133
+ }
134
+
135
+ public struct HStack<Content: View> {
136
+ let alignment: VerticalAlignment
137
+ let spacing: CGFloat?
138
+ let content: Content
139
+
140
+ public init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil,
141
+ @ViewBuilder content: () -> Content) {
142
+ self.alignment = alignment
143
+ self.spacing = spacing
144
+ self.content = content()
145
+ }
146
+ }
147
+ extension HStack: _PrimitiveView {}
148
+ extension HStack: _HostView {
149
+ var _kind: String { "hstack" }
150
+ func _props(in environment: EnvironmentValues) -> _JSON {
151
+ .object(["spacing": spacing.map(_JSON.number) ?? .null, "alignment": .string(alignment.name)])
152
+ }
153
+ var _content: [any View] { [content] }
154
+ }
155
+
156
+ public struct ZStack<Content: View> {
157
+ let alignment: Alignment
158
+ let content: Content
159
+
160
+ public init(alignment: Alignment = .center, @ViewBuilder content: () -> Content) {
161
+ self.alignment = alignment
162
+ self.content = content()
163
+ }
164
+ }
165
+ extension ZStack: _PrimitiveView {}
166
+ extension ZStack: _HostView {
167
+ var _kind: String { "zstack" }
168
+ func _props(in environment: EnvironmentValues) -> _JSON { .object(["alignment": .string(alignment._name)]) }
169
+ var _content: [any View] { [content] }
170
+ }
171
+
172
+ /// Shows the first of its children whose ideal size fits the proposed space
173
+ /// on `axes` (the last when none fits). The renderer measures and chooses
174
+ /// (see docs/ops-protocol.md, Phase 7); the children are all its candidates.
175
+ public struct ViewThatFits<Content: View> {
176
+ let axes: Axis.Set
177
+ let content: Content
178
+
179
+ public init(in axes: Axis.Set = [.horizontal, .vertical], @ViewBuilder content: () -> Content) {
180
+ self.axes = axes
181
+ self.content = content()
182
+ }
183
+ }
184
+ extension ViewThatFits: _PrimitiveView {}
185
+ extension ViewThatFits: _HostView {
186
+ var _kind: String { "viewthatfits" }
187
+ func _props(in environment: EnvironmentValues) -> _JSON { .object(["axes": .string(axes._name)]) }
188
+ var _content: [any View] { [content] }
189
+ }
190
+
191
+ public struct Spacer: Sendable {
192
+ let minLength: CGFloat?
193
+ public init(minLength: CGFloat? = nil) { self.minLength = minLength }
194
+ }
195
+ extension Spacer: _PrimitiveView {}
196
+ extension Spacer: _HostView {
197
+ var _kind: String { "spacer" }
198
+ func _props(in environment: EnvironmentValues) -> _JSON {
199
+ .object(["minLength": minLength.map(_JSON.number) ?? .null])
200
+ }
201
+ }
202
+
203
+ public struct Divider: Sendable {
204
+ public init() {}
205
+ }
206
+ extension Divider: _PrimitiveView {}
207
+ extension Divider: _HostView {
208
+ var _kind: String { "divider" }
209
+ func _props(in environment: EnvironmentValues) -> _JSON { .object([:]) }
210
+ }
211
+
212
+ // MARK: - ScrollView
213
+
214
+ public enum Axis: Int8, CaseIterable, Sendable {
215
+ case horizontal, vertical
216
+
217
+ public struct Set: OptionSet, Sendable {
218
+ public let rawValue: Int8
219
+ public init(rawValue: Int8) { self.rawValue = rawValue }
220
+ public static let horizontal = Set(rawValue: 1 << 0)
221
+ public static let vertical = Set(rawValue: 1 << 1)
222
+
223
+ /// `"horizontal"`, `"vertical"` or `"both"` (also for an empty set).
224
+ var _name: String {
225
+ if self == .horizontal { return "horizontal" }
226
+ if self == .vertical { return "vertical" }
227
+ return "both"
228
+ }
229
+ }
230
+ }
231
+
232
+ public struct ScrollView<Content: View> {
233
+ let axes: Axis.Set
234
+ let content: Content
235
+ /// `.refreshable`: true while the action runs.
236
+ @State var refreshing = false
237
+
238
+ public init(_ axes: Axis.Set = .vertical, showsIndicators: Bool = true,
239
+ @ViewBuilder content: () -> Content) {
240
+ self.axes = axes
241
+ self.content = content()
242
+ }
243
+ }
244
+ extension ScrollView: _PrimitiveView {}
245
+ extension ScrollView: _HostView {
246
+ var _kind: String { "scrollview" }
247
+ func _props(in environment: EnvironmentValues) -> _JSON {
248
+ var props: [String: _JSON] = ["axes": .string(axes._name)]
249
+ _refresh(in: environment).addProps(to: &props)
250
+ if let snap = environment._scrollTargetBehavior { props["snap"] = .string(snap) }
251
+ return .object(props)
252
+ }
253
+ var _content: [any View] { [content] }
254
+ }
255
+ extension ScrollView: _Refreshing {
256
+ func _refresh(in environment: EnvironmentValues) -> _Refresh {
257
+ _Refresh(action: environment._refreshAction, refreshing: $refreshing)
258
+ }
259
+ }
260
+
261
+ // MARK: - Button
262
+
263
+ public struct ButtonRole: Equatable, Sendable {
264
+ let name: String
265
+ public static let destructive = ButtonRole(name: "destructive")
266
+ public static let cancel = ButtonRole(name: "cancel")
267
+ }
268
+
269
+ public struct Button<Label: View> {
270
+ let action: @MainActor () -> Void
271
+ let role: ButtonRole?
272
+ let label: Label
273
+
274
+ public init(action: @escaping @MainActor () -> Void, @ViewBuilder label: () -> Label) {
275
+ self.action = action
276
+ self.role = nil
277
+ self.label = label()
278
+ }
279
+
280
+ public init(role: ButtonRole?, action: @escaping @MainActor () -> Void,
281
+ @ViewBuilder label: () -> Label) {
282
+ self.action = action
283
+ self.role = role
284
+ self.label = label()
285
+ }
286
+ }
287
+
288
+ extension Button where Label == Text {
289
+ public init<S: StringProtocol>(_ title: S, action: @escaping @MainActor () -> Void) {
290
+ self.init(action: action) { Text(title) }
291
+ }
292
+
293
+ public init<S: StringProtocol>(_ title: S, role: ButtonRole?, action: @escaping @MainActor () -> Void) {
294
+ self.init(role: role, action: action) { Text(title) }
295
+ }
296
+
297
+ public init(_ titleKey: LocalizedStringKey, action: @escaping @MainActor () -> Void) {
298
+ self.init(action: action) { Text(titleKey) }
299
+ }
300
+
301
+ public init(_ titleKey: LocalizedStringKey, role: ButtonRole?, action: @escaping @MainActor () -> Void) {
302
+ self.init(role: role, action: action) { Text(titleKey) }
303
+ }
304
+ }
305
+
306
+ extension Button where Label == SwiftUI.Label<Text, Image> {
307
+ public init<S: StringProtocol>(_ title: S, systemImage: String, action: @escaping @MainActor () -> Void) {
308
+ let title = String(title)
309
+ self.init(action: action) { SwiftUI.Label(title, systemImage: systemImage) }
310
+ }
311
+
312
+ public init<S: StringProtocol>(_ title: S, systemImage: String, role: ButtonRole?,
313
+ action: @escaping @MainActor () -> Void) {
314
+ let title = String(title)
315
+ self.init(role: role, action: action) { SwiftUI.Label(title, systemImage: systemImage) }
316
+ }
317
+
318
+ public init(_ titleKey: LocalizedStringKey, systemImage: String, action: @escaping @MainActor () -> Void) {
319
+ self.init(action: action) { SwiftUI.Label(titleKey, systemImage: systemImage) }
320
+ }
321
+
322
+ public init(_ titleKey: LocalizedStringKey, systemImage: String, role: ButtonRole?,
323
+ action: @escaping @MainActor () -> Void) {
324
+ self.init(role: role, action: action) { SwiftUI.Label(titleKey, systemImage: systemImage) }
325
+ }
326
+ }
327
+ extension Button: _PrimitiveView {}
328
+ extension Button: _HostView {
329
+ var _kind: String { "button" }
330
+ func _props(in environment: EnvironmentValues) -> _JSON {
331
+ .object([
332
+ "style": .string(environment._buttonStyle),
333
+ "role": role.map { .string($0.name) } ?? .null,
334
+ ])
335
+ }
336
+ var _content: [any View] { [label] }
337
+ var _handler: ((_JSON) -> Void)? {
338
+ let action = self.action
339
+ return { _ in MainActor.assumeIsolated { action() } }
340
+ }
341
+ }
342
+
343
+ // MARK: - Button styles
344
+
345
+ public protocol PrimitiveButtonStyle {
346
+ var _name: String { get }
347
+ }
348
+
349
+ public struct DefaultButtonStyle: PrimitiveButtonStyle {
350
+ public init() {}
351
+ public var _name: String { "automatic" }
352
+ }
353
+ public struct PlainButtonStyle: PrimitiveButtonStyle {
354
+ public init() {}
355
+ public var _name: String { "plain" }
356
+ }
357
+ public struct BorderedButtonStyle: PrimitiveButtonStyle {
358
+ public init() {}
359
+ public var _name: String { "bordered" }
360
+ }
361
+ public struct BorderedProminentButtonStyle: PrimitiveButtonStyle {
362
+ public init() {}
363
+ public var _name: String { "borderedProminent" }
364
+ }
365
+
366
+ public struct BorderlessButtonStyle: PrimitiveButtonStyle {
367
+ public init() {}
368
+ public var _name: String { "plain" }
369
+ }
370
+ extension PrimitiveButtonStyle where Self == BorderlessButtonStyle {
371
+ public static var borderless: BorderlessButtonStyle { BorderlessButtonStyle() }
372
+ }
373
+ extension PrimitiveButtonStyle where Self == DefaultButtonStyle {
374
+ public static var automatic: DefaultButtonStyle { DefaultButtonStyle() }
375
+ }
376
+ extension PrimitiveButtonStyle where Self == PlainButtonStyle {
377
+ public static var plain: PlainButtonStyle { PlainButtonStyle() }
378
+ }
379
+ extension PrimitiveButtonStyle where Self == BorderedButtonStyle {
380
+ public static var bordered: BorderedButtonStyle { BorderedButtonStyle() }
381
+ }
382
+ extension PrimitiveButtonStyle where Self == BorderedProminentButtonStyle {
383
+ public static var borderedProminent: BorderedProminentButtonStyle { BorderedProminentButtonStyle() }
384
+ }
385
+
386
+ extension View {
387
+ public func buttonStyle<S: PrimitiveButtonStyle>(_ style: S) -> some View {
388
+ environment(\._buttonStyle, style._name)
389
+ }
390
+ }
@@ -0,0 +1,107 @@
1
+ /// The sliver of Combine that SwiftUI code reaches for without importing
2
+ /// Combine (Apple's SwiftUI re-exports it): `onReceive(_:perform:)` on a
3
+ /// publisher such as `Timer.publish(…).autoconnect()`. A `_Publisher` delivers
4
+ /// values to one subscriber at a time, on the main actor, until cancelled.
5
+ public protocol _Publisher {
6
+ associatedtype Output
7
+ /// Starts delivering values; the returned cancellable stops it.
8
+ @MainActor func _subscribe(_ receive: @escaping @MainActor (Output) -> Void) -> any _Cancellable
9
+ }
10
+
11
+ public protocol _Cancellable: Sendable {
12
+ func cancel()
13
+ }
14
+
15
+ /// A cancellable backed by a task (publishers that tick from the executor).
16
+ public struct _TaskCancellable: _Cancellable {
17
+ let task: Task<Void, Never>
18
+ public init(_ task: Task<Void, Never>) { self.task = task }
19
+ public func cancel() { task.cancel() }
20
+ }
21
+
22
+ /// Emits `make()` every `interval` seconds, from the executor's clock (the same
23
+ /// clock as `.task` sleeps, so headless runs stay deterministic). The shim
24
+ /// cannot name `Date`, so `Timer.publish` in FoundationLite and FoundationBridge
25
+ /// wraps this with `{ Date() }`.
26
+ public struct _IntervalPublisher<Output: Sendable>: _Publisher, Sendable {
27
+ public let interval: Double
28
+ let make: @Sendable () -> Output
29
+
30
+ public init(every interval: Double, make: @escaping @Sendable () -> Output) {
31
+ self.interval = interval
32
+ self.make = make
33
+ }
34
+
35
+ /// Starts ticking as soon as `onReceive` subscribes; Combine's `autoconnect`
36
+ /// has the same effect, so this is the identity.
37
+ public func autoconnect() -> _IntervalPublisher<Output> { self }
38
+
39
+ @MainActor public func _subscribe(_ receive: @escaping @MainActor (Output) -> Void) -> any _Cancellable {
40
+ let interval = max(interval, 0.001)
41
+ let make = self.make
42
+ let task = Task { @MainActor in
43
+ while !Task.isCancelled {
44
+ try? await Task.sleep(for: .seconds(interval))
45
+ if Task.isCancelled { break }
46
+ receive(make())
47
+ }
48
+ }
49
+ return _TaskCancellable(task)
50
+ }
51
+ }
52
+
53
+ /// The executor-driven timer behind `Timer.scheduledTimer(withTimeInterval:repeats:block:)`:
54
+ /// fires `block` after `interval` seconds, again every `interval` when
55
+ /// repeating, until `invalidate()`. Like Foundation's, a scheduled timer stays
56
+ /// alive until it has fired (or forever while repeating) whether or not the
57
+ /// caller keeps a reference.
58
+ public final class _ExecutorTimer: @unchecked Sendable {
59
+ public let timeInterval: Double
60
+ public private(set) var isValid = true
61
+ private var task: Task<Void, Never>?
62
+
63
+ public init(interval: Double, repeats: Bool, block: @escaping @Sendable (_ExecutorTimer) -> Void) {
64
+ timeInterval = interval
65
+ task = Task { @MainActor in
66
+ repeat {
67
+ try? await Task.sleep(for: .seconds(max(interval, 0.001)))
68
+ guard self.isValid, !Task.isCancelled else { return }
69
+ block(self)
70
+ } while repeats && !Task.isCancelled
71
+ self.isValid = false
72
+ }
73
+ }
74
+
75
+ public func invalidate() {
76
+ isValid = false
77
+ task?.cancel()
78
+ task = nil
79
+ }
80
+ }
81
+
82
+ /// `onReceive` is a task that subscribes while the view is on screen (the
83
+ /// runtime starts it on appear and cancels it when the view leaves).
84
+ struct _OnReceiveView<Content: View, P: _Publisher>: View {
85
+ let content: Content
86
+ let publisher: P
87
+ let action: @MainActor (P.Output) -> Void
88
+ var body: some View {
89
+ let publisher = self.publisher
90
+ let action = self.action
91
+ return content.task { @MainActor in
92
+ let cancellable = publisher._subscribe(action)
93
+ // Stay alive until cancelled; the publisher drives the callbacks.
94
+ while !Task.isCancelled {
95
+ try? await Task.sleep(for: .seconds(3600))
96
+ }
97
+ cancellable.cancel()
98
+ }
99
+ }
100
+ }
101
+
102
+ extension View {
103
+ /// Calls `action` with every value `publisher` emits while the view is shown.
104
+ public func onReceive<P: _Publisher>(_ publisher: P, perform action: @escaping @MainActor (P.Output) -> Void) -> some View {
105
+ _OnReceiveView(content: self, publisher: publisher, action: action)
106
+ }
107
+ }
@@ -0,0 +1,83 @@
1
+ /// Swipe actions and context menus on list rows (docs/ops-protocol.md, Phase 8).
2
+
3
+ public enum HorizontalEdge: Hashable, CaseIterable, Sendable {
4
+ case leading, trailing
5
+ }
6
+
7
+ // MARK: - Swipe actions
8
+
9
+ /// The `swipeactions` element: `[content, ...trailing, ...leading]`, the
10
+ /// actions flattened to their `button`s (a `.tint` on a button stays as a
11
+ /// `styled` wrapper carrying `tint`). Several `.swipeActions` on one row
12
+ /// accumulate into one element.
13
+ public struct _SwipeActionsView {
14
+ let content: any View
15
+ var trailing: [any View] = []
16
+ var leading: [any View] = []
17
+ var fullSwipeTrailing = true
18
+ var fullSwipeLeading = true
19
+
20
+ @MainActor
21
+ mutating func add(edge: HorizontalEdge, allowsFullSwipe: Bool, actions: any View) {
22
+ let flat = _flattenChildren(actions)
23
+ switch edge {
24
+ case .trailing:
25
+ trailing += flat
26
+ fullSwipeTrailing = allowsFullSwipe
27
+ case .leading:
28
+ leading += flat
29
+ fullSwipeLeading = allowsFullSwipe
30
+ }
31
+ }
32
+ }
33
+ extension _SwipeActionsView: _PrimitiveView {}
34
+ extension _SwipeActionsView: _HostView {
35
+ var _kind: String { "swipeactions" }
36
+ func _props(in environment: EnvironmentValues) -> _JSON {
37
+ .object([
38
+ "trailing": .number(Double(trailing.count)),
39
+ "leading": .number(Double(leading.count)),
40
+ "fullSwipeTrailing": .bool(fullSwipeTrailing),
41
+ "fullSwipeLeading": .bool(fullSwipeLeading),
42
+ ])
43
+ }
44
+ var _content: [any View] { [content] + trailing + leading }
45
+ }
46
+
47
+ extension View {
48
+ /// Buttons revealed by swiping the row horizontally. The first declared
49
+ /// action is nearest the content and is the one a full swipe performs.
50
+ public func swipeActions<T: View>(edge: HorizontalEdge = .trailing, allowsFullSwipe: Bool = true,
51
+ @ViewBuilder content: () -> T) -> _SwipeActionsView {
52
+ var view = (self as? _SwipeActionsView) ?? _SwipeActionsView(content: self)
53
+ view.add(edge: edge, allowsFullSwipe: allowsFullSwipe, actions: content())
54
+ return view
55
+ }
56
+ }
57
+
58
+ // MARK: - Context menu
59
+
60
+ /// The `contextmenu` element: `[content, ...items]`, the items as in a `menu`.
61
+ struct _ContextMenuView<Content: View, Items: View> {
62
+ let content: Content
63
+ let items: Items
64
+ }
65
+ extension _ContextMenuView: _PrimitiveView {}
66
+ extension _ContextMenuView: _HostView {
67
+ var _kind: String { "contextmenu" }
68
+ func _props(in environment: EnvironmentValues) -> _JSON { .object([:]) }
69
+ var _content: [any View] { [content, items] }
70
+ }
71
+
72
+ extension View {
73
+ /// Items shown in a popover on a long press or right click.
74
+ public func contextMenu<M: View>(@ViewBuilder menuItems: () -> M) -> some View {
75
+ _ContextMenuView(content: self, items: menuItems())
76
+ }
77
+
78
+ /// The preview is accepted and ignored.
79
+ public func contextMenu<M: View, P: View>(@ViewBuilder menuItems: () -> M,
80
+ @ViewBuilder preview: () -> P) -> some View {
81
+ _ContextMenuView(content: self, items: menuItems())
82
+ }
83
+ }
@@ -0,0 +1,104 @@
1
+ /// Scroll snapping (docs/ops-protocol.md, "Phase 12"): `.scrollTargetBehavior`
2
+ /// on a `ScrollView` and `.scrollTargetLayout()` on the stack inside it. The
3
+ /// scroll view carries `snap` (`viewAligned` or `paging`) and the stack
4
+ /// `scrollTarget: true`; the renderer maps them to CSS scroll snapping, so
5
+ /// the children of the target layout become the snap points.
6
+
7
+ public struct ScrollTargetBehavior: Hashable, Sendable {
8
+ let name: String
9
+ public static let viewAligned = ScrollTargetBehavior(name: "viewAligned")
10
+ public static let paging = ScrollTargetBehavior(name: "paging")
11
+ public static func viewAligned(limitBehavior: ViewAlignedScrollTargetBehavior.LimitBehavior) -> ScrollTargetBehavior { .viewAligned }
12
+ }
13
+
14
+ public enum ViewAlignedScrollTargetBehavior {
15
+ public struct LimitBehavior: Hashable, Sendable {
16
+ let name: String
17
+ public static let automatic = LimitBehavior(name: "automatic")
18
+ public static let always = LimitBehavior(name: "always")
19
+ public static let never = LimitBehavior(name: "never")
20
+ }
21
+ }
22
+
23
+ enum _ScrollTargetBehaviorKey: EnvironmentKey { static let defaultValue: String? = nil }
24
+ enum _ScrollTargetLayoutKey: EnvironmentKey { static let defaultValue = false }
25
+
26
+ extension EnvironmentValues {
27
+ var _scrollTargetBehavior: String? {
28
+ get { self[_ScrollTargetBehaviorKey.self] }
29
+ set { self[_ScrollTargetBehaviorKey.self] = newValue }
30
+ }
31
+ /// Set by `.scrollTargetLayout()`; the first stack below consumes it.
32
+ var _scrollTargetLayout: Bool {
33
+ get { self[_ScrollTargetLayoutKey.self] }
34
+ set { self[_ScrollTargetLayoutKey.self] = newValue }
35
+ }
36
+ }
37
+
38
+ extension View {
39
+ public func scrollTargetBehavior(_ behavior: ScrollTargetBehavior) -> some View {
40
+ _EnvironmentView(content: self) { $0._scrollTargetBehavior = behavior.name }
41
+ }
42
+
43
+ public func scrollTargetLayout(isEnabled: Bool = true) -> some View {
44
+ _EnvironmentView(content: self) { $0._scrollTargetLayout = isEnabled }
45
+ }
46
+
47
+ /// Accepted for source compatibility; the browser's scroll views clip to their frame already.
48
+ public func scrollClipDisabled(_ disabled: Bool = true) -> some View { self }
49
+
50
+ /// Accepted for source compatibility; the browser decides bounce behavior.
51
+ public func scrollBounceBehavior(_ behavior: ScrollBounceBehavior, axes: Axis.Set = [.vertical]) -> some View { self }
52
+ }
53
+
54
+ public struct ScrollBounceBehavior: Hashable, Sendable {
55
+ let name: String
56
+ public static let automatic = ScrollBounceBehavior(name: "automatic")
57
+ public static let always = ScrollBounceBehavior(name: "always")
58
+ public static let basedOnSize = ScrollBounceBehavior(name: "basedOnSize")
59
+ }
60
+
61
+ /// The stacks `.scrollTargetLayout()` applies to; the runtime clears the flag
62
+ /// for their children so nested stacks do not add snap points.
63
+ protocol _ScrollTargetContainer {}
64
+ extension HStack: _ScrollTargetContainer {}
65
+ extension VStack: _ScrollTargetContainer {}
66
+
67
+ // MARK: - containerRelativeFrame
68
+
69
+ extension View {
70
+ /// Sizes the view relative to its nearest container (the scroll view's
71
+ /// viewport or the screen): the whole container along `axes`, or `span`
72
+ /// of `count` equal columns separated by `spacing`. The renderer's layout
73
+ /// engine resolves it against the container it knows (`containerRelativeFrame`
74
+ /// style hint: an affine `a × container + b` per axis).
75
+ public func containerRelativeFrame(_ axes: Axis.Set, alignment: Alignment = .center) -> some View {
76
+ _containerRelativeFrame(axes, a: 1, b: 0, alignment: alignment)
77
+ }
78
+
79
+ public func containerRelativeFrame(_ axes: Axis.Set, count: Int, span: Int = 1, spacing: CGFloat,
80
+ alignment: Alignment = .center) -> some View {
81
+ let n = CGFloat(max(count, 1)), s = CGFloat(max(span, 1))
82
+ // SwiftUI: (container - spacing × (count - 1)) × span / count + spacing × (span - 1).
83
+ return _containerRelativeFrame(axes, a: s / n, b: -spacing * (n - 1) * s / n + spacing * (s - 1), alignment: alignment)
84
+ }
85
+
86
+ /// The closure variant. The closure is sampled at two container lengths and
87
+ /// treated as affine (`width * 0.6`, `width - 40`), which the common uses are.
88
+ public func containerRelativeFrame(_ axes: Axis.Set, alignment: Alignment = .center,
89
+ _ length: @escaping (CGFloat, Axis) -> CGFloat) -> some View {
90
+ let axis: Axis = axes.contains(.horizontal) ? .horizontal : .vertical
91
+ let b = length(0, axis)
92
+ let a = (length(1000, axis) - b) / 1000
93
+ return _containerRelativeFrame(axes, a: a, b: b, alignment: alignment)
94
+ }
95
+
96
+ func _containerRelativeFrame(_ axes: Axis.Set, a: CGFloat, b: CGFloat, alignment: Alignment) -> some View {
97
+ _styled {
98
+ var hint: [String: _JSON] = ["a": .number(a), "b": .number(b), "alignment": .string(alignment._name)]
99
+ if axes.contains(.horizontal) { hint["horizontal"] = .bool(true) }
100
+ if axes.contains(.vertical) { hint["vertical"] = .bool(true) }
101
+ $0.extras["containerRelativeFrame"] = .object(hint)
102
+ }
103
+ }
104
+ }