@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,342 @@
|
|
|
1
|
+
/// Gestures: `DragGesture`, `LongPressGesture` and `TapGesture`, attached with
|
|
2
|
+
/// `.gesture` (and its `highPriority` / `simultaneous` twins, which the shim
|
|
3
|
+
/// treats identically). A drag or long press becomes a `gesture` element
|
|
4
|
+
/// whose child is the content; the renderer tracks pointers on it and sends
|
|
5
|
+
/// `drag` / `longpress` events (see docs/ops-protocol.md, Phase 8). A plain
|
|
6
|
+
/// tap gesture is the Phase 6 `tapgesture` element.
|
|
7
|
+
|
|
8
|
+
/// Which gestures of a subtree a `.gesture` modifier competes with. Accepted
|
|
9
|
+
/// for source compatibility; the renderer resolves the innermost control.
|
|
10
|
+
public struct GestureMask: OptionSet, Sendable {
|
|
11
|
+
public let rawValue: UInt
|
|
12
|
+
public init(rawValue: UInt) { self.rawValue = rawValue }
|
|
13
|
+
public static let none = GestureMask([])
|
|
14
|
+
public static let gesture = GestureMask(rawValue: 1)
|
|
15
|
+
public static let subviews = GestureMask(rawValue: 2)
|
|
16
|
+
public static let all: GestureMask = [.gesture, .subviews]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// What the renderer must track for a gesture and what to do with its events.
|
|
20
|
+
/// Combined gestures (`sequenced`, `simultaneously`) merge their configurations.
|
|
21
|
+
public struct _GestureConfig: Sendable {
|
|
22
|
+
var drag = false
|
|
23
|
+
var longPress = false
|
|
24
|
+
var minimumDistance: CGFloat = 10
|
|
25
|
+
var minimumDuration: Double = 0.5
|
|
26
|
+
var onDragChanged: [@MainActor (DragGesture.Value) -> Void] = []
|
|
27
|
+
var onDragEnded: [@MainActor (DragGesture.Value) -> Void] = []
|
|
28
|
+
var onLongPressChanged: [@MainActor (Bool) -> Void] = []
|
|
29
|
+
var onLongPressEnded: [@MainActor (Bool) -> Void] = []
|
|
30
|
+
var onTap: [@MainActor () -> Void] = []
|
|
31
|
+
|
|
32
|
+
func merged(with other: _GestureConfig) -> _GestureConfig {
|
|
33
|
+
var result = self
|
|
34
|
+
result.drag = drag || other.drag
|
|
35
|
+
result.longPress = longPress || other.longPress
|
|
36
|
+
result.minimumDistance = drag && other.drag ? min(minimumDistance, other.minimumDistance)
|
|
37
|
+
: (other.drag ? other.minimumDistance : minimumDistance)
|
|
38
|
+
result.minimumDuration = longPress && other.longPress ? min(minimumDuration, other.minimumDuration)
|
|
39
|
+
: (other.longPress ? other.minimumDuration : minimumDuration)
|
|
40
|
+
result.onDragChanged += other.onDragChanged
|
|
41
|
+
result.onDragEnded += other.onDragEnded
|
|
42
|
+
result.onLongPressChanged += other.onLongPressChanged
|
|
43
|
+
result.onLongPressEnded += other.onLongPressEnded
|
|
44
|
+
result.onTap += other.onTap
|
|
45
|
+
return result
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// A gesture the renderer recognizes. `Value` is what the callbacks receive.
|
|
50
|
+
public protocol Gesture {
|
|
51
|
+
associatedtype Value
|
|
52
|
+
var _config: _GestureConfig { get }
|
|
53
|
+
/// Adds callbacks for the gesture's changed / ended phases to a config
|
|
54
|
+
/// (each gesture type knows which lists its `Value` belongs in).
|
|
55
|
+
static func _adding(to config: _GestureConfig,
|
|
56
|
+
changed: (@MainActor (Value) -> Void)?,
|
|
57
|
+
ended: (@MainActor (Value) -> Void)?) -> _GestureConfig
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
extension Gesture {
|
|
61
|
+
public func onChanged(_ action: @escaping @MainActor (Value) -> Void) -> _ModifiedGesture<Self> {
|
|
62
|
+
_ModifiedGesture(_config: Self._adding(to: _config, changed: action, ended: nil))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public func onEnded(_ action: @escaping @MainActor (Value) -> Void) -> _ModifiedGesture<Self> {
|
|
66
|
+
_ModifiedGesture(_config: Self._adding(to: _config, changed: nil, ended: action))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Both gestures are recognized; the shim does not order them.
|
|
70
|
+
public func sequenced<Other: Gesture>(before other: Other) -> _CombinedGesture<Self, Other> {
|
|
71
|
+
_CombinedGesture(_config: _config.merged(with: other._config))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public func simultaneously<Other: Gesture>(with other: Other) -> _CombinedGesture<Self, Other> {
|
|
75
|
+
_CombinedGesture(_config: _config.merged(with: other._config))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public func exclusively<Other: Gesture>(before other: Other) -> _CombinedGesture<Self, Other> {
|
|
79
|
+
_CombinedGesture(_config: _config.merged(with: other._config))
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// A gesture with `onChanged` / `onEnded` callbacks attached; keeps the base
|
|
84
|
+
/// gesture's `Value` so further callbacks can be chained.
|
|
85
|
+
public struct _ModifiedGesture<Base: Gesture>: Gesture {
|
|
86
|
+
public typealias Value = Base.Value
|
|
87
|
+
public let _config: _GestureConfig
|
|
88
|
+
|
|
89
|
+
public static func _adding(to config: _GestureConfig,
|
|
90
|
+
changed: (@MainActor (Value) -> Void)?,
|
|
91
|
+
ended: (@MainActor (Value) -> Void)?) -> _GestureConfig {
|
|
92
|
+
Base._adding(to: config, changed: changed, ended: ended)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Two gestures combined. Its `Value` is never produced: attach callbacks to
|
|
97
|
+
/// the parts before combining them.
|
|
98
|
+
public struct _CombinedGesture<First: Gesture, Second: Gesture>: Gesture {
|
|
99
|
+
public typealias Value = Void
|
|
100
|
+
public let _config: _GestureConfig
|
|
101
|
+
|
|
102
|
+
public static func _adding(to config: _GestureConfig,
|
|
103
|
+
changed: (@MainActor (Value) -> Void)?,
|
|
104
|
+
ended: (@MainActor (Value) -> Void)?) -> _GestureConfig {
|
|
105
|
+
config
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// MARK: - DragGesture
|
|
110
|
+
|
|
111
|
+
public struct DragGesture: Gesture {
|
|
112
|
+
/// The drag so far. Coordinates are in the gesture element's own box
|
|
113
|
+
/// (`coordinateSpace` is accepted and ignored). `predictedEnd…` project
|
|
114
|
+
/// the velocity a quarter second ahead.
|
|
115
|
+
public struct Value: Equatable, Sendable {
|
|
116
|
+
public var location: CGPoint
|
|
117
|
+
public var startLocation: CGPoint
|
|
118
|
+
public var translation: CGSize
|
|
119
|
+
public var velocity: CGSize
|
|
120
|
+
|
|
121
|
+
public init(location: CGPoint = .zero, startLocation: CGPoint = .zero,
|
|
122
|
+
translation: CGSize = .zero, velocity: CGSize = .zero) {
|
|
123
|
+
self.location = location
|
|
124
|
+
self.startLocation = startLocation
|
|
125
|
+
self.translation = translation
|
|
126
|
+
self.velocity = velocity
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public var predictedEndLocation: CGPoint {
|
|
130
|
+
CGPoint(x: location.x + velocity.width / 4, y: location.y + velocity.height / 4)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public var predictedEndTranslation: CGSize {
|
|
134
|
+
CGSize(width: translation.width + velocity.width / 4, height: translation.height + velocity.height / 4)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public var minimumDistance: CGFloat
|
|
139
|
+
public var coordinateSpace: CoordinateSpace
|
|
140
|
+
|
|
141
|
+
public init(minimumDistance: CGFloat = 10, coordinateSpace: CoordinateSpace = .local) {
|
|
142
|
+
self.minimumDistance = minimumDistance
|
|
143
|
+
self.coordinateSpace = coordinateSpace
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public var _config: _GestureConfig {
|
|
147
|
+
var config = _GestureConfig()
|
|
148
|
+
config.drag = true
|
|
149
|
+
config.minimumDistance = minimumDistance
|
|
150
|
+
return config
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public static func _adding(to config: _GestureConfig,
|
|
154
|
+
changed: (@MainActor (Value) -> Void)?,
|
|
155
|
+
ended: (@MainActor (Value) -> Void)?) -> _GestureConfig {
|
|
156
|
+
var config = config
|
|
157
|
+
if let changed { config.onDragChanged.append(changed) }
|
|
158
|
+
if let ended { config.onDragEnded.append(ended) }
|
|
159
|
+
return config
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// MARK: - LongPressGesture
|
|
164
|
+
|
|
165
|
+
/// `Value` is `true` once the press is recognized. The renderer reports only
|
|
166
|
+
/// the recognition, so `onChanged(true)` and `onEnded(true)` fire together.
|
|
167
|
+
public struct LongPressGesture: Gesture {
|
|
168
|
+
public typealias Value = Bool
|
|
169
|
+
public var minimumDuration: Double
|
|
170
|
+
public var maximumDistance: CGFloat
|
|
171
|
+
|
|
172
|
+
public init(minimumDuration: Double = 0.5, maximumDistance: CGFloat = 10) {
|
|
173
|
+
self.minimumDuration = minimumDuration
|
|
174
|
+
self.maximumDistance = maximumDistance
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public var _config: _GestureConfig {
|
|
178
|
+
var config = _GestureConfig()
|
|
179
|
+
config.longPress = true
|
|
180
|
+
config.minimumDuration = minimumDuration
|
|
181
|
+
return config
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public static func _adding(to config: _GestureConfig,
|
|
185
|
+
changed: (@MainActor (Value) -> Void)?,
|
|
186
|
+
ended: (@MainActor (Value) -> Void)?) -> _GestureConfig {
|
|
187
|
+
var config = config
|
|
188
|
+
if let changed { config.onLongPressChanged.append(changed) }
|
|
189
|
+
if let ended { config.onLongPressEnded.append(ended) }
|
|
190
|
+
return config
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// MARK: - TapGesture
|
|
195
|
+
|
|
196
|
+
/// A tap; `count` is accepted and ignored (every tap is reported).
|
|
197
|
+
public struct TapGesture: Gesture {
|
|
198
|
+
public typealias Value = Void
|
|
199
|
+
public var count: Int
|
|
200
|
+
|
|
201
|
+
public init(count: Int = 1) { self.count = count }
|
|
202
|
+
|
|
203
|
+
public var _config: _GestureConfig { _GestureConfig() }
|
|
204
|
+
|
|
205
|
+
public static func _adding(to config: _GestureConfig,
|
|
206
|
+
changed: (@MainActor (Value) -> Void)?,
|
|
207
|
+
ended: (@MainActor (Value) -> Void)?) -> _GestureConfig {
|
|
208
|
+
var config = config
|
|
209
|
+
if let ended { config.onTap.append { ended(()) } }
|
|
210
|
+
return config
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// MARK: - The element
|
|
215
|
+
|
|
216
|
+
/// The `gesture` element: props say what to track, the handler parses the
|
|
217
|
+
/// renderer's `drag` / `longpress` events. A `cancelled` drag ends the
|
|
218
|
+
/// gesture with its last value (`onEnded` runs), so state the app resets in
|
|
219
|
+
/// `onEnded` is reset.
|
|
220
|
+
struct _GestureHost {
|
|
221
|
+
let content: any View
|
|
222
|
+
let config: _GestureConfig
|
|
223
|
+
}
|
|
224
|
+
extension _GestureHost: _PrimitiveView {}
|
|
225
|
+
extension _GestureHost: _HostView {
|
|
226
|
+
var _kind: String { "gesture" }
|
|
227
|
+
func _props(in environment: EnvironmentValues) -> _JSON {
|
|
228
|
+
.object([
|
|
229
|
+
"drag": .bool(config.drag),
|
|
230
|
+
"longPress": .bool(config.longPress),
|
|
231
|
+
"minimumDistance": .number(config.minimumDistance),
|
|
232
|
+
"minimumDuration": .number(config.minimumDuration),
|
|
233
|
+
])
|
|
234
|
+
}
|
|
235
|
+
var _content: [any View] { [content] }
|
|
236
|
+
var _handler: ((_JSON) -> Void)? {
|
|
237
|
+
let config = self.config
|
|
238
|
+
return { payload in
|
|
239
|
+
guard case .object(let fields) = payload, case .string(let type)? = fields["type"] else { return }
|
|
240
|
+
MainActor.assumeIsolated {
|
|
241
|
+
switch type {
|
|
242
|
+
case "drag":
|
|
243
|
+
let value = DragGesture.Value(fields)
|
|
244
|
+
let phase: String = if case .string(let p)? = fields["phase"] { p } else { "changed" }
|
|
245
|
+
switch phase {
|
|
246
|
+
case "began", "changed":
|
|
247
|
+
for action in config.onDragChanged { action(value) }
|
|
248
|
+
case "ended", "cancelled":
|
|
249
|
+
for action in config.onDragEnded { action(value) }
|
|
250
|
+
default:
|
|
251
|
+
break
|
|
252
|
+
}
|
|
253
|
+
case "longpress":
|
|
254
|
+
for action in config.onLongPressChanged { action(true) }
|
|
255
|
+
for action in config.onLongPressEnded { action(true) }
|
|
256
|
+
default:
|
|
257
|
+
break
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
extension DragGesture.Value {
|
|
265
|
+
/// Parses a `drag` event's fields; absent fields are zero.
|
|
266
|
+
init(_ fields: [String: _JSON]) {
|
|
267
|
+
func point(_ key: String) -> CGPoint {
|
|
268
|
+
guard case .object(let o)? = fields[key] else { return .zero }
|
|
269
|
+
return CGPoint(x: _number(o["x"]), y: _number(o["y"]))
|
|
270
|
+
}
|
|
271
|
+
func size(_ key: String) -> CGSize {
|
|
272
|
+
guard case .object(let o)? = fields[key] else { return .zero }
|
|
273
|
+
return CGSize(width: _number(o["width"]), height: _number(o["height"]))
|
|
274
|
+
}
|
|
275
|
+
self.init(location: point("location"), startLocation: point("startLocation"),
|
|
276
|
+
translation: size("translation"), velocity: size("velocity"))
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private func _number(_ json: _JSON?) -> Double {
|
|
281
|
+
if case .number(let n)? = json { return n }
|
|
282
|
+
return 0
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// `.gesture(_:)`: the content under a `gesture` element when the gesture
|
|
286
|
+
/// drags or long-presses, under a `tapgesture` when it taps, both when it does
|
|
287
|
+
/// both, and plain when the gesture has no callbacks.
|
|
288
|
+
struct _GestureView<Content: View> {
|
|
289
|
+
let content: Content
|
|
290
|
+
let config: _GestureConfig
|
|
291
|
+
}
|
|
292
|
+
extension _GestureView: _PrimitiveView {}
|
|
293
|
+
extension _GestureView: _GroupView {
|
|
294
|
+
var _children: [any View] {
|
|
295
|
+
var inner: any View = content
|
|
296
|
+
if !config.onTap.isEmpty {
|
|
297
|
+
let taps = config.onTap
|
|
298
|
+
inner = _TapGestureView(content: AnyView(inner)) { for action in taps { action() } }
|
|
299
|
+
}
|
|
300
|
+
if config.drag || config.longPress {
|
|
301
|
+
return [_GestureHost(content: inner, config: config)]
|
|
302
|
+
}
|
|
303
|
+
return [inner]
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
extension View {
|
|
308
|
+
public func gesture<G: Gesture>(_ gesture: G, including mask: GestureMask = .all) -> some View {
|
|
309
|
+
_GestureView(content: self, config: gesture._config)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
public func highPriorityGesture<G: Gesture>(_ gesture: G, including mask: GestureMask = .all) -> some View {
|
|
313
|
+
_GestureView(content: self, config: gesture._config)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
public func simultaneousGesture<G: Gesture>(_ gesture: G, including mask: GestureMask = .all) -> some View {
|
|
317
|
+
_GestureView(content: self, config: gesture._config)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/// `onPressingChanged` is told `true` then `false` around `action`, when
|
|
321
|
+
/// the press is recognized (the renderer reports no earlier touch-down).
|
|
322
|
+
public func onLongPressGesture(minimumDuration: Double = 0.5, maximumDistance: CGFloat = 10,
|
|
323
|
+
perform action: @escaping @MainActor () -> Void,
|
|
324
|
+
onPressingChanged: (@MainActor (Bool) -> Void)? = nil) -> some View {
|
|
325
|
+
var config = _GestureConfig()
|
|
326
|
+
config.longPress = true
|
|
327
|
+
config.minimumDuration = minimumDuration
|
|
328
|
+
config.onLongPressEnded = [{ _ in
|
|
329
|
+
onPressingChanged?(true)
|
|
330
|
+
action()
|
|
331
|
+
onPressingChanged?(false)
|
|
332
|
+
}]
|
|
333
|
+
return _GestureView(content: self, config: config)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
public func onLongPressGesture(minimumDuration: Double = 0.5, maximumDistance: CGFloat = 10,
|
|
337
|
+
pressing: (@MainActor (Bool) -> Void)?,
|
|
338
|
+
perform action: @escaping @MainActor () -> Void) -> some View {
|
|
339
|
+
onLongPressGesture(minimumDuration: minimumDuration, maximumDistance: maximumDistance,
|
|
340
|
+
perform: action, onPressingChanged: pressing)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Grid containers. The renderer lays them out; the Swift side sends the
|
|
2
|
+
// configuration and the children. See docs/ops-protocol.md (Phase 6).
|
|
3
|
+
|
|
4
|
+
// MARK: - Grid / GridRow
|
|
5
|
+
|
|
6
|
+
/// A two-dimensional layout of rows (`GridRow`) and columns.
|
|
7
|
+
public struct Grid<Content: View> {
|
|
8
|
+
let alignment: Alignment
|
|
9
|
+
let horizontalSpacing: CGFloat?
|
|
10
|
+
let verticalSpacing: CGFloat?
|
|
11
|
+
let content: Content
|
|
12
|
+
|
|
13
|
+
public init(alignment: Alignment = .center, horizontalSpacing: CGFloat? = nil,
|
|
14
|
+
verticalSpacing: CGFloat? = nil, @ViewBuilder content: () -> Content) {
|
|
15
|
+
self.alignment = alignment
|
|
16
|
+
self.horizontalSpacing = horizontalSpacing
|
|
17
|
+
self.verticalSpacing = verticalSpacing
|
|
18
|
+
self.content = content()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
extension Grid: _PrimitiveView {}
|
|
22
|
+
extension Grid: _HostView {
|
|
23
|
+
var _kind: String { "grid" }
|
|
24
|
+
func _props(in environment: EnvironmentValues) -> _JSON {
|
|
25
|
+
.object([
|
|
26
|
+
"horizontalSpacing": horizontalSpacing.map(_JSON.number) ?? .null,
|
|
27
|
+
"verticalSpacing": verticalSpacing.map(_JSON.number) ?? .null,
|
|
28
|
+
"alignment": .string(alignment._name),
|
|
29
|
+
])
|
|
30
|
+
}
|
|
31
|
+
var _content: [any View] { [content] }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// One row of a `Grid`; each child is a cell.
|
|
35
|
+
public struct GridRow<Content: View> {
|
|
36
|
+
let alignment: VerticalAlignment?
|
|
37
|
+
let content: Content
|
|
38
|
+
|
|
39
|
+
public init(alignment: VerticalAlignment? = nil, @ViewBuilder content: () -> Content) {
|
|
40
|
+
self.alignment = alignment
|
|
41
|
+
self.content = content()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
extension GridRow: _PrimitiveView {}
|
|
45
|
+
extension GridRow: _HostView {
|
|
46
|
+
var _kind: String { "gridrow" }
|
|
47
|
+
func _props(in environment: EnvironmentValues) -> _JSON {
|
|
48
|
+
// Baseline alignments are approximated as `center` (see VerticalAlignment).
|
|
49
|
+
.object(["alignment": alignment.map { .string($0.name) } ?? .null])
|
|
50
|
+
}
|
|
51
|
+
var _content: [any View] { [content] }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// A `styled` element carrying grid cell hints. `_Style` lives in
|
|
55
|
+
/// Modifiers.swift; this view is protocol-identical for the host.
|
|
56
|
+
struct _GridCellHintView<Content: View> {
|
|
57
|
+
let content: Content
|
|
58
|
+
let style: [String: _JSON]
|
|
59
|
+
}
|
|
60
|
+
extension _GridCellHintView: _PrimitiveView {}
|
|
61
|
+
extension _GridCellHintView: _HostView {
|
|
62
|
+
var _kind: String { "styled" }
|
|
63
|
+
func _props(in environment: EnvironmentValues) -> _JSON { .object(["style": .object(style)]) }
|
|
64
|
+
var _content: [any View] { [content] }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
extension View {
|
|
68
|
+
/// Makes the cell span `count` columns of its `GridRow`.
|
|
69
|
+
public func gridCellColumns(_ count: Int) -> some View {
|
|
70
|
+
_GridCellHintView(content: self, style: ["gridCellColumns": .number(Double(count))])
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Where the cell's content sits when the cell is larger than the content.
|
|
74
|
+
public func gridCellAnchor(_ anchor: UnitPoint) -> some View {
|
|
75
|
+
_GridCellHintView(content: self, style: ["gridCellAnchor": .object(["x": .number(anchor.x), "y": .number(anchor.y)])])
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Accepted for source compatibility; the renderer aligns columns by the grid's alignment.
|
|
79
|
+
public func gridColumnAlignment(_ guide: HorizontalAlignment) -> some View { self }
|
|
80
|
+
|
|
81
|
+
/// Accepted for source compatibility; cells are sized by their content.
|
|
82
|
+
public func gridCellUnsizedAxes(_ axes: Axis.Set) -> some View { self }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// MARK: - GridItem
|
|
86
|
+
|
|
87
|
+
/// A column of a `LazyVGrid` (or a row of a `LazyHGrid`).
|
|
88
|
+
public struct GridItem: Sendable {
|
|
89
|
+
public enum Size: Sendable {
|
|
90
|
+
case fixed(CGFloat)
|
|
91
|
+
case flexible(minimum: CGFloat = 10, maximum: CGFloat = .infinity)
|
|
92
|
+
case adaptive(minimum: CGFloat, maximum: CGFloat = .infinity)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public var size: Size
|
|
96
|
+
public var spacing: CGFloat?
|
|
97
|
+
public var alignment: Alignment?
|
|
98
|
+
|
|
99
|
+
public init(_ size: Size = .flexible(), spacing: CGFloat? = nil, alignment: Alignment? = nil) {
|
|
100
|
+
self.size = size
|
|
101
|
+
self.spacing = spacing
|
|
102
|
+
self.alignment = alignment
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
var _json: _JSON {
|
|
106
|
+
func bound(_ value: CGFloat) -> _JSON { value == .infinity ? .string("infinity") : .number(value) }
|
|
107
|
+
let size: _JSON = switch self.size {
|
|
108
|
+
case .fixed(let value):
|
|
109
|
+
.object(["kind": .string("fixed"), "value": .number(value)])
|
|
110
|
+
case .flexible(let minimum, let maximum):
|
|
111
|
+
.object(["kind": .string("flexible"), "minimum": .number(minimum), "maximum": bound(maximum)])
|
|
112
|
+
case .adaptive(let minimum, let maximum):
|
|
113
|
+
.object(["kind": .string("adaptive"), "minimum": .number(minimum), "maximum": bound(maximum)])
|
|
114
|
+
}
|
|
115
|
+
return .object([
|
|
116
|
+
"size": size,
|
|
117
|
+
"spacing": spacing.map(_JSON.number) ?? .null,
|
|
118
|
+
"alignment": alignment.map { .string($0._name) } ?? .null,
|
|
119
|
+
])
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Views a lazy grid keeps pinned while scrolling. Accepted for source
|
|
124
|
+
/// compatibility; the renderer does not pin.
|
|
125
|
+
public struct PinnedScrollableViews: OptionSet, Sendable {
|
|
126
|
+
public let rawValue: UInt32
|
|
127
|
+
public init(rawValue: UInt32) { self.rawValue = rawValue }
|
|
128
|
+
public static let sectionHeaders = PinnedScrollableViews(rawValue: 1 << 0)
|
|
129
|
+
public static let sectionFooters = PinnedScrollableViews(rawValue: 1 << 1)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// MARK: - LazyVGrid / LazyHGrid
|
|
133
|
+
|
|
134
|
+
/// A grid that grows vertically, filling the given columns row by row.
|
|
135
|
+
public struct LazyVGrid<Content: View> {
|
|
136
|
+
let columns: [GridItem]
|
|
137
|
+
let alignment: HorizontalAlignment
|
|
138
|
+
let spacing: CGFloat?
|
|
139
|
+
let content: Content
|
|
140
|
+
|
|
141
|
+
public init(columns: [GridItem], alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil,
|
|
142
|
+
pinnedViews: PinnedScrollableViews = .init(), @ViewBuilder content: () -> Content) {
|
|
143
|
+
self.columns = columns
|
|
144
|
+
self.alignment = alignment
|
|
145
|
+
self.spacing = spacing
|
|
146
|
+
self.content = content()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
extension LazyVGrid: _PrimitiveView {}
|
|
150
|
+
extension LazyVGrid: _HostView {
|
|
151
|
+
var _kind: String { "lazyvgrid" }
|
|
152
|
+
func _props(in environment: EnvironmentValues) -> _JSON {
|
|
153
|
+
.object([
|
|
154
|
+
"columns": .array(columns.map(\._json)),
|
|
155
|
+
"spacing": spacing.map(_JSON.number) ?? .null,
|
|
156
|
+
"alignment": .string(alignment.name),
|
|
157
|
+
])
|
|
158
|
+
}
|
|
159
|
+
var _content: [any View] { [content] }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// A grid that grows horizontally, filling the given rows column by column.
|
|
163
|
+
public struct LazyHGrid<Content: View> {
|
|
164
|
+
let rows: [GridItem]
|
|
165
|
+
let alignment: VerticalAlignment
|
|
166
|
+
let spacing: CGFloat?
|
|
167
|
+
let content: Content
|
|
168
|
+
|
|
169
|
+
public init(rows: [GridItem], alignment: VerticalAlignment = .center, spacing: CGFloat? = nil,
|
|
170
|
+
pinnedViews: PinnedScrollableViews = .init(), @ViewBuilder content: () -> Content) {
|
|
171
|
+
self.rows = rows
|
|
172
|
+
self.alignment = alignment
|
|
173
|
+
self.spacing = spacing
|
|
174
|
+
self.content = content()
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
extension LazyHGrid: _PrimitiveView {}
|
|
178
|
+
extension LazyHGrid: _HostView {
|
|
179
|
+
var _kind: String { "lazyhgrid" }
|
|
180
|
+
func _props(in environment: EnvironmentValues) -> _JSON {
|
|
181
|
+
.object([
|
|
182
|
+
"rows": .array(rows.map(\._json)),
|
|
183
|
+
"spacing": spacing.map(_JSON.number) ?? .null,
|
|
184
|
+
"alignment": .string(alignment.name),
|
|
185
|
+
])
|
|
186
|
+
}
|
|
187
|
+
var _content: [any View] { [content] }
|
|
188
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/// `LabelStyle`: how a `Label` arranges its title and icon. `Label.body`
|
|
2
|
+
/// consults the environment's style (set by `.labelStyle`).
|
|
3
|
+
public protocol LabelStyle {
|
|
4
|
+
associatedtype Body: View
|
|
5
|
+
typealias Configuration = LabelStyleConfiguration
|
|
6
|
+
@ViewBuilder @MainActor func makeBody(configuration: Configuration) -> Body
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
public struct LabelStyleConfiguration {
|
|
10
|
+
public struct Title: View {
|
|
11
|
+
let view: any View
|
|
12
|
+
public var body: some View { AnyView(view) }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public struct Icon: View {
|
|
16
|
+
let view: any View
|
|
17
|
+
public var body: some View { AnyView(view) }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public let title: Title
|
|
21
|
+
public let icon: Icon
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public struct DefaultLabelStyle: LabelStyle {
|
|
25
|
+
public init() {}
|
|
26
|
+
public func makeBody(configuration: Configuration) -> some View {
|
|
27
|
+
HStack(spacing: 8) {
|
|
28
|
+
configuration.icon
|
|
29
|
+
configuration.title
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public struct TitleAndIconLabelStyle: LabelStyle {
|
|
35
|
+
public init() {}
|
|
36
|
+
public func makeBody(configuration: Configuration) -> some View {
|
|
37
|
+
HStack(spacing: 8) {
|
|
38
|
+
configuration.icon
|
|
39
|
+
configuration.title
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public struct IconOnlyLabelStyle: LabelStyle {
|
|
45
|
+
public init() {}
|
|
46
|
+
public func makeBody(configuration: Configuration) -> some View { configuration.icon }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public struct TitleOnlyLabelStyle: LabelStyle {
|
|
50
|
+
public init() {}
|
|
51
|
+
public func makeBody(configuration: Configuration) -> some View { configuration.title }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
extension LabelStyle where Self == DefaultLabelStyle {
|
|
55
|
+
public static var automatic: DefaultLabelStyle { DefaultLabelStyle() }
|
|
56
|
+
}
|
|
57
|
+
extension LabelStyle where Self == TitleAndIconLabelStyle {
|
|
58
|
+
public static var titleAndIcon: TitleAndIconLabelStyle { TitleAndIconLabelStyle() }
|
|
59
|
+
}
|
|
60
|
+
extension LabelStyle where Self == IconOnlyLabelStyle {
|
|
61
|
+
public static var iconOnly: IconOnlyLabelStyle { IconOnlyLabelStyle() }
|
|
62
|
+
}
|
|
63
|
+
extension LabelStyle where Self == TitleOnlyLabelStyle {
|
|
64
|
+
public static var titleOnly: TitleOnlyLabelStyle { TitleOnlyLabelStyle() }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Type-erased style stored in the environment.
|
|
68
|
+
struct _AnyLabelStyle {
|
|
69
|
+
let make: @MainActor (LabelStyleConfiguration) -> any View
|
|
70
|
+
|
|
71
|
+
@MainActor
|
|
72
|
+
init<S: LabelStyle>(_ style: S) {
|
|
73
|
+
make = { configuration in style.makeBody(configuration: configuration) }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
nonisolated init(make: @escaping @MainActor (LabelStyleConfiguration) -> any View) {
|
|
77
|
+
self.make = make
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Usable from nonisolated code (toolbars build their children there).
|
|
81
|
+
nonisolated static var iconOnly: _AnyLabelStyle {
|
|
82
|
+
_AnyLabelStyle(make: { configuration in IconOnlyLabelStyle().makeBody(configuration: configuration) })
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private struct _LabelStyleKey: EnvironmentKey {
|
|
87
|
+
static var defaultValue: _AnyLabelStyle? { nil }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
extension EnvironmentValues {
|
|
91
|
+
var _labelStyle: _AnyLabelStyle? {
|
|
92
|
+
get { self[_LabelStyleKey.self] }
|
|
93
|
+
set { self[_LabelStyleKey.self] = newValue }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
extension View {
|
|
98
|
+
public func labelStyle<S: LabelStyle>(_ style: S) -> some View {
|
|
99
|
+
environment(\._labelStyle, _AnyLabelStyle(style))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Resolves a label through the environment's style. `Label.body` returns
|
|
104
|
+
/// this so the style is read where the label is mounted.
|
|
105
|
+
struct _StyledLabel: View {
|
|
106
|
+
let title: any View
|
|
107
|
+
let icon: any View
|
|
108
|
+
@Environment(\._labelStyle) private var style
|
|
109
|
+
|
|
110
|
+
var body: some View {
|
|
111
|
+
let configuration = LabelStyleConfiguration(title: .init(view: title), icon: .init(view: icon))
|
|
112
|
+
if let style {
|
|
113
|
+
return AnyView(style.make(configuration))
|
|
114
|
+
}
|
|
115
|
+
return AnyView(DefaultLabelStyle().makeBody(configuration: configuration))
|
|
116
|
+
}
|
|
117
|
+
}
|