@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,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
|
+
}
|