@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,92 @@
|
|
|
1
|
+
/// `.overlay` and `.background` with view content: a `decorated` element
|
|
2
|
+
/// whose first child decides the size and whose second child is drawn over
|
|
3
|
+
/// (`overlay`) or under (`background`) it, aligned inside its box.
|
|
4
|
+
struct _DecoratedView<Content: View, Decoration: View> {
|
|
5
|
+
let content: Content
|
|
6
|
+
let decoration: Decoration
|
|
7
|
+
let role: String
|
|
8
|
+
let alignment: Alignment
|
|
9
|
+
}
|
|
10
|
+
extension _DecoratedView: _PrimitiveView {}
|
|
11
|
+
extension _DecoratedView: _HostView {
|
|
12
|
+
var _kind: String { "decorated" }
|
|
13
|
+
func _props(in environment: EnvironmentValues) -> _JSON {
|
|
14
|
+
.object(["role": .string(role), "alignment": .string(alignment._name)])
|
|
15
|
+
}
|
|
16
|
+
var _content: [any View] { [content, decoration] }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
extension EnvironmentValues {
|
|
20
|
+
/// `.backgroundStyle(_:)`: the paint `.background(in:)` fills shapes with.
|
|
21
|
+
var _backgroundStyle: (any ShapeStyle)? {
|
|
22
|
+
get { self[_BackgroundStyleKey.self] }
|
|
23
|
+
set { self[_BackgroundStyleKey.self] = newValue }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private struct _BackgroundStyleKey: EnvironmentKey {
|
|
28
|
+
static var defaultValue: (any ShapeStyle)? { nil }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Fills `shape` with the environment's background style (or the system
|
|
32
|
+
/// background) — `.background(in:)` without an explicit style.
|
|
33
|
+
struct _EnvironmentBackgroundShape<S: Shape> {
|
|
34
|
+
let shape: S
|
|
35
|
+
}
|
|
36
|
+
extension _EnvironmentBackgroundShape: _PrimitiveView {}
|
|
37
|
+
extension _EnvironmentBackgroundShape: _HostView {
|
|
38
|
+
var _kind: String { "shape" }
|
|
39
|
+
func _props(in environment: EnvironmentValues) -> _JSON {
|
|
40
|
+
let style = environment._backgroundStyle ?? BackgroundStyle()
|
|
41
|
+
return .object([
|
|
42
|
+
"shape": .string(shape._name),
|
|
43
|
+
"cornerRadius": .number(shape._cornerRadius),
|
|
44
|
+
"fill": style._json,
|
|
45
|
+
"stroke": .null,
|
|
46
|
+
])
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
extension View {
|
|
51
|
+
public func overlay<V: View>(alignment: Alignment = .center, @ViewBuilder content: () -> V) -> some View {
|
|
52
|
+
_DecoratedView(content: self, decoration: content(), role: "overlay", alignment: alignment)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// The pre-iOS 15 spelling, `overlay(view, alignment:)`.
|
|
56
|
+
@_disfavoredOverload
|
|
57
|
+
public func overlay<V: View>(_ overlay: V, alignment: Alignment = .center) -> some View {
|
|
58
|
+
_DecoratedView(content: self, decoration: overlay, role: "overlay", alignment: alignment)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public func overlay<S: ShapeStyle>(_ style: S, ignoresSafeAreaEdges edges: Edge.Set = .all) -> some View {
|
|
62
|
+
_DecoratedView(content: self, decoration: Rectangle().fill(style), role: "overlay", alignment: .center)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public func overlay<S: ShapeStyle, T: Shape>(_ style: S, in shape: T, fillStyle: FillStyle = FillStyle()) -> some View {
|
|
66
|
+
_DecoratedView(content: self, decoration: shape.fill(style), role: "overlay", alignment: .center)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public func background<V: View>(alignment: Alignment = .center, @ViewBuilder content: () -> V) -> some View {
|
|
70
|
+
_DecoratedView(content: self, decoration: content(), role: "background", alignment: alignment)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// The pre-iOS 15 spelling, `background(view, alignment:)`.
|
|
74
|
+
@_disfavoredOverload
|
|
75
|
+
public func background<V: View>(_ background: V, alignment: Alignment = .center) -> some View {
|
|
76
|
+
_DecoratedView(content: self, decoration: background, role: "background", alignment: alignment)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// `shape` filled with `style`, behind the view.
|
|
80
|
+
public func background<S: ShapeStyle, T: Shape>(_ style: S, in shape: T, fillStyle: FillStyle = FillStyle()) -> some View {
|
|
81
|
+
_DecoratedView(content: self, decoration: shape.fill(style), role: "background", alignment: .center)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// `shape` filled with the background style (`.backgroundStyle`), behind the view.
|
|
85
|
+
public func background<T: Shape>(in shape: T, fillStyle: FillStyle = FillStyle()) -> some View {
|
|
86
|
+
_DecoratedView(content: self, decoration: _EnvironmentBackgroundShape(shape: shape), role: "background", alignment: .center)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public func backgroundStyle<S: ShapeStyle>(_ style: S) -> some View {
|
|
90
|
+
environment(\._backgroundStyle, style)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/// Edit mode, `EditButton`, `ForEach.onDelete` / `onMove`, offset sets and
|
|
2
|
+
/// `refreshable`. See docs/ops-protocol.md, "Phase 8: interaction".
|
|
3
|
+
|
|
4
|
+
// MARK: - Offset sets
|
|
5
|
+
|
|
6
|
+
/// A set of offsets into a collection, as `onDelete` / `onMove` report and
|
|
7
|
+
/// `remove(atOffsets:)` / `move(fromOffsets:toOffset:)` consume. Foundation's
|
|
8
|
+
/// `IndexSet` is not in FoundationEssentials, so the shim is generic over this
|
|
9
|
+
/// protocol: `FoundationLite.IndexSet` conforms, and `_IndexSetLite` serves
|
|
10
|
+
/// apps that import neither (it is what `onDelete` infers when nothing else
|
|
11
|
+
/// pins the type).
|
|
12
|
+
public protocol _OffsetSet {
|
|
13
|
+
init(_offsets: [Int])
|
|
14
|
+
/// The offsets in ascending order, without duplicates.
|
|
15
|
+
var _offsets: [Int] { get }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// A minimal `IndexSet`: sorted unique integers.
|
|
19
|
+
public struct _IndexSetLite: _OffsetSet, Hashable, Sendable, Sequence, ExpressibleByArrayLiteral {
|
|
20
|
+
public private(set) var _offsets: [Int]
|
|
21
|
+
|
|
22
|
+
public init(_offsets: [Int]) {
|
|
23
|
+
self._offsets = Array(Set(_offsets)).sorted()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public init() { _offsets = [] }
|
|
27
|
+
public init(integer: Int) { _offsets = [integer] }
|
|
28
|
+
public init<S: Sequence>(_ elements: S) where S.Element == Int { self.init(_offsets: Array(elements)) }
|
|
29
|
+
public init(arrayLiteral elements: Int...) { self.init(_offsets: elements) }
|
|
30
|
+
public init(integersIn range: Range<Int>) { _offsets = Array(range) }
|
|
31
|
+
|
|
32
|
+
public var count: Int { _offsets.count }
|
|
33
|
+
public var isEmpty: Bool { _offsets.isEmpty }
|
|
34
|
+
public var first: Int? { _offsets.first }
|
|
35
|
+
public var last: Int? { _offsets.last }
|
|
36
|
+
public func contains(_ integer: Int) -> Bool { _offsets.contains(integer) }
|
|
37
|
+
|
|
38
|
+
public mutating func insert(_ integer: Int) {
|
|
39
|
+
guard !contains(integer) else { return }
|
|
40
|
+
_offsets.append(integer)
|
|
41
|
+
_offsets.sort()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public mutating func remove(_ integer: Int) {
|
|
45
|
+
_offsets.removeAll { $0 == integer }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public func makeIterator() -> IndexingIterator<[Int]> { _offsets.makeIterator() }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
extension RangeReplaceableCollection {
|
|
52
|
+
/// Removes the elements at `offsets` (offsets from `startIndex`).
|
|
53
|
+
public mutating func remove<O: _OffsetSet>(atOffsets offsets: O) {
|
|
54
|
+
for offset in offsets._offsets.sorted(by: >) where offset >= 0 && offset < count {
|
|
55
|
+
remove(at: index(startIndex, offsetBy: offset))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
extension MutableCollection where Self: RangeReplaceableCollection {
|
|
61
|
+
/// Moves the elements at `source` so that they start at `destination`
|
|
62
|
+
/// (an offset into the collection before the move), like Foundation's.
|
|
63
|
+
public mutating func move<O: _OffsetSet>(fromOffsets source: O, toOffset destination: Int) {
|
|
64
|
+
let offsets = source._offsets.filter { $0 >= 0 && $0 < count }
|
|
65
|
+
guard !offsets.isEmpty else { return }
|
|
66
|
+
let moving = offsets.map { self[index(startIndex, offsetBy: $0)] }
|
|
67
|
+
let before = offsets.filter { $0 < destination }.count
|
|
68
|
+
for offset in offsets.sorted(by: >) {
|
|
69
|
+
remove(at: index(startIndex, offsetBy: offset))
|
|
70
|
+
}
|
|
71
|
+
let target = Swift.max(0, Swift.min(destination - before, count))
|
|
72
|
+
insert(contentsOf: moving, at: index(startIndex, offsetBy: target))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// MARK: - onDelete / onMove
|
|
77
|
+
|
|
78
|
+
/// A view built from a collection, like SwiftUI's: `ForEach` and what
|
|
79
|
+
/// `onDelete` / `onMove` return.
|
|
80
|
+
public protocol DynamicViewContent: View {
|
|
81
|
+
associatedtype Data: Collection
|
|
82
|
+
var data: Data { get }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
extension ForEach: DynamicViewContent where Content: View {}
|
|
86
|
+
|
|
87
|
+
/// A keyed group whose rows a `List` can delete (and, when the renderer learns
|
|
88
|
+
/// to, move). The runtime wraps each row in a `listrow` with `deletable`.
|
|
89
|
+
protocol _DeletableGroup {
|
|
90
|
+
/// Calls the group's `onDelete` with the row at `offset`; nil without one.
|
|
91
|
+
var _delete: ((Int) -> Void)? { get }
|
|
92
|
+
var _move: ((Int, Int) -> Void)? { get }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public struct _EditableForEach<Base: DynamicViewContent> {
|
|
96
|
+
let base: Base
|
|
97
|
+
let delete: ((Int) -> Void)?
|
|
98
|
+
let move: ((Int, Int) -> Void)?
|
|
99
|
+
}
|
|
100
|
+
extension _EditableForEach: _PrimitiveView {}
|
|
101
|
+
extension _EditableForEach: DynamicViewContent {
|
|
102
|
+
public var data: Base.Data { base.data }
|
|
103
|
+
}
|
|
104
|
+
extension _EditableForEach: _GroupView {
|
|
105
|
+
var _children: [any View] { (base as? any _GroupView)?._children ?? [base] }
|
|
106
|
+
var _keys: [AnyHashable]? { (base as? any _GroupView)?._keys }
|
|
107
|
+
}
|
|
108
|
+
extension _EditableForEach: _DeletableGroup {
|
|
109
|
+
var _delete: ((Int) -> Void)? { delete }
|
|
110
|
+
var _move: ((Int, Int) -> Void)? { move }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
extension DynamicViewContent {
|
|
114
|
+
/// Lets a `List` delete the rows: `action` receives the offsets of the
|
|
115
|
+
/// removed elements in `data`. Pass `nil` to opt out.
|
|
116
|
+
public func onDelete<O: _OffsetSet>(perform action: ((O) -> Void)?) -> _EditableForEach<Self> {
|
|
117
|
+
_EditableForEach(base: self, delete: action.map { action in { action(O(_offsets: [$0])) } }, move: nil)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// The offsets default to `_IndexSetLite` when nothing else pins the type.
|
|
121
|
+
public func onDelete(perform action: ((_IndexSetLite) -> Void)?) -> _EditableForEach<Self> {
|
|
122
|
+
_EditableForEach(base: self, delete: action.map { action in { action(_IndexSetLite(_offsets: [$0])) } }, move: nil)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Accepted for source compatibility; the renderer offers no reordering yet.
|
|
126
|
+
public func onMove<O: _OffsetSet>(perform action: ((O, Int) -> Void)?) -> _EditableForEach<Self> {
|
|
127
|
+
_EditableForEach(base: self, delete: nil, move: action.map { action in { action(O(_offsets: [$0]), $1) } })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public func onMove(perform action: ((_IndexSetLite, Int) -> Void)?) -> _EditableForEach<Self> {
|
|
131
|
+
_EditableForEach(base: self, delete: nil, move: action.map { action in { action(_IndexSetLite(_offsets: [$0]), $1) } })
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
extension _EditableForEach {
|
|
136
|
+
/// `ForEach(...).onDelete(...).onMove(...)`: the second modifier merges into the first.
|
|
137
|
+
public func onDelete<O: _OffsetSet>(perform action: ((O) -> Void)?) -> _EditableForEach<Base> {
|
|
138
|
+
_EditableForEach(base: base, delete: action.map { action in { action(O(_offsets: [$0])) } }, move: move)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public func onDelete(perform action: ((_IndexSetLite) -> Void)?) -> _EditableForEach<Base> {
|
|
142
|
+
_EditableForEach(base: base, delete: action.map { action in { action(_IndexSetLite(_offsets: [$0])) } }, move: move)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public func onMove<O: _OffsetSet>(perform action: ((O, Int) -> Void)?) -> _EditableForEach<Base> {
|
|
146
|
+
_EditableForEach(base: base, delete: delete, move: action.map { action in { action(O(_offsets: [$0]), $1) } })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public func onMove(perform action: ((_IndexSetLite, Int) -> Void)?) -> _EditableForEach<Base> {
|
|
150
|
+
_EditableForEach(base: base, delete: delete, move: action.map { action in { action(_IndexSetLite(_offsets: [$0]), $1) } })
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// `.deleteDisabled` / `.moveDisabled` on a row.
|
|
155
|
+
protocol _RowEditingDisabling {
|
|
156
|
+
var _deleteDisabled: Bool { get }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
struct _DeleteDisabledView<Content: View> {
|
|
160
|
+
let content: Content
|
|
161
|
+
let disabled: Bool
|
|
162
|
+
}
|
|
163
|
+
extension _DeleteDisabledView: _PrimitiveView {}
|
|
164
|
+
extension _DeleteDisabledView: _GroupView {
|
|
165
|
+
var _children: [any View] { [content] }
|
|
166
|
+
}
|
|
167
|
+
extension _DeleteDisabledView: _RowEditingDisabling {
|
|
168
|
+
var _deleteDisabled: Bool { disabled }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
extension View {
|
|
172
|
+
public func deleteDisabled(_ isDisabled: Bool) -> some View {
|
|
173
|
+
_DeleteDisabledView(content: self, disabled: isDisabled)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Accepted for source compatibility; rows cannot be moved yet.
|
|
177
|
+
public func moveDisabled(_ isDisabled: Bool) -> some View { self }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// MARK: - Edit mode
|
|
181
|
+
|
|
182
|
+
/// Whether a view is being edited. Each `navscreen` (and a `List` outside
|
|
183
|
+
/// a navigation stack) owns an edit mode the runtime puts in the environment
|
|
184
|
+
/// as a binding; `EditButton` toggles it and `List` reports it as `editing`.
|
|
185
|
+
public enum EditMode: Hashable, Sendable {
|
|
186
|
+
case inactive
|
|
187
|
+
case transient
|
|
188
|
+
case active
|
|
189
|
+
|
|
190
|
+
public var isEditing: Bool { self != .inactive }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
enum _EditModeKey: EnvironmentKey { static var defaultValue: Binding<EditMode>? { nil } }
|
|
194
|
+
|
|
195
|
+
extension EnvironmentValues {
|
|
196
|
+
public var editMode: Binding<EditMode>? {
|
|
197
|
+
get { self[_EditModeKey.self] }
|
|
198
|
+
set { self[_EditModeKey.self] = newValue }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// A button that toggles the enclosing edit mode between "Edit" and "Done".
|
|
203
|
+
public struct EditButton: View {
|
|
204
|
+
@Environment(\.editMode) private var editMode
|
|
205
|
+
|
|
206
|
+
public init() {}
|
|
207
|
+
|
|
208
|
+
public var body: some View {
|
|
209
|
+
let isEditing = editMode?.wrappedValue.isEditing == true
|
|
210
|
+
let mode = editMode
|
|
211
|
+
return Button(isEditing ? "Done" : "Edit") {
|
|
212
|
+
mode?.wrappedValue = isEditing ? .inactive : .active
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// MARK: - Refreshable
|
|
218
|
+
|
|
219
|
+
enum _RefreshActionKey: EnvironmentKey {
|
|
220
|
+
static var defaultValue: (@Sendable () async -> Void)? { nil }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
extension EnvironmentValues {
|
|
224
|
+
/// `.refreshable`'s action, read by the `List` / `ScrollView` it wraps.
|
|
225
|
+
var _refreshAction: (@Sendable () async -> Void)? {
|
|
226
|
+
get { self[_RefreshActionKey.self] }
|
|
227
|
+
set { self[_RefreshActionKey.self] = newValue }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
extension View {
|
|
232
|
+
/// Pull to refresh on the `List` or `ScrollView` this modifies: the view
|
|
233
|
+
/// shows a spinner (`refreshing`) while `action` runs.
|
|
234
|
+
public func refreshable(action: @escaping @Sendable () async -> Void) -> some View {
|
|
235
|
+
environment(\._refreshAction, action)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// Shared by `List` and `ScrollView`: the `refreshable` / `refreshing` props
|
|
240
|
+
/// and the `refresh` handler, driven by a `@State` flag on the view.
|
|
241
|
+
struct _Refresh {
|
|
242
|
+
let action: (@Sendable () async -> Void)?
|
|
243
|
+
let refreshing: Binding<Bool>
|
|
244
|
+
|
|
245
|
+
func addProps(to props: inout [String: _JSON]) {
|
|
246
|
+
guard action != nil else { return }
|
|
247
|
+
props["refreshable"] = .bool(true)
|
|
248
|
+
props["refreshing"] = .bool(refreshing.wrappedValue)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@MainActor var handler: ((_JSON) -> Void)? {
|
|
252
|
+
guard let action else { return nil }
|
|
253
|
+
// Main-actor closures are Sendable; the binding itself is not.
|
|
254
|
+
let refreshing = self.refreshing
|
|
255
|
+
let isRefreshing: @MainActor () -> Bool = { refreshing.wrappedValue }
|
|
256
|
+
let setRefreshing: @MainActor (Bool) -> Void = { refreshing.wrappedValue = $0 }
|
|
257
|
+
return { payload in
|
|
258
|
+
guard case .object(let fields) = payload, case .string("refresh")? = fields["type"] else { return }
|
|
259
|
+
MainActor.assumeIsolated {
|
|
260
|
+
guard !isRefreshing() else { return }
|
|
261
|
+
setRefreshing(true)
|
|
262
|
+
Task {
|
|
263
|
+
await action()
|
|
264
|
+
setRefreshing(false)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/// Visual effects and hints that need no layout logic on the Swift side: the
|
|
2
|
+
/// renderer applies them from the `styled` element's style keys (see
|
|
3
|
+
/// docs/ops-protocol.md, "Phase 6 additions").
|
|
4
|
+
|
|
5
|
+
public struct Angle: Hashable, Sendable, Comparable {
|
|
6
|
+
public var radians: Double
|
|
7
|
+
|
|
8
|
+
public var degrees: Double {
|
|
9
|
+
get { radians * 180 / .pi }
|
|
10
|
+
set { radians = newValue * .pi / 180 }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public init() { radians = 0 }
|
|
14
|
+
public init(radians: Double) { self.radians = radians }
|
|
15
|
+
public init(degrees: Double) { radians = degrees * .pi / 180 }
|
|
16
|
+
|
|
17
|
+
public static func radians(_ radians: Double) -> Angle { Angle(radians: radians) }
|
|
18
|
+
public static func degrees(_ degrees: Double) -> Angle { Angle(degrees: degrees) }
|
|
19
|
+
public static let zero = Angle()
|
|
20
|
+
|
|
21
|
+
public static func < (lhs: Angle, rhs: Angle) -> Bool { lhs.radians < rhs.radians }
|
|
22
|
+
|
|
23
|
+
public static prefix func - (angle: Angle) -> Angle { Angle(radians: -angle.radians) }
|
|
24
|
+
public static func + (lhs: Angle, rhs: Angle) -> Angle { Angle(radians: lhs.radians + rhs.radians) }
|
|
25
|
+
public static func - (lhs: Angle, rhs: Angle) -> Angle { Angle(radians: lhs.radians - rhs.radians) }
|
|
26
|
+
public static func * (lhs: Angle, rhs: Double) -> Angle { Angle(radians: lhs.radians * rhs) }
|
|
27
|
+
public static func * (lhs: Double, rhs: Angle) -> Angle { Angle(radians: lhs * rhs.radians) }
|
|
28
|
+
public static func / (lhs: Angle, rhs: Double) -> Angle { Angle(radians: lhs.radians / rhs) }
|
|
29
|
+
public static func += (lhs: inout Angle, rhs: Angle) { lhs = lhs + rhs }
|
|
30
|
+
public static func -= (lhs: inout Angle, rhs: Angle) { lhs = lhs - rhs }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// `.formStyle`: accepted for source compatibility; forms are always grouped.
|
|
34
|
+
public protocol FormStyle {}
|
|
35
|
+
public struct AutomaticFormStyle: FormStyle { public init() {} }
|
|
36
|
+
public struct GroupedFormStyle: FormStyle { public init() {} }
|
|
37
|
+
public struct ColumnsFormStyle: FormStyle { public init() {} }
|
|
38
|
+
extension FormStyle where Self == AutomaticFormStyle {
|
|
39
|
+
public static var automatic: AutomaticFormStyle { AutomaticFormStyle() }
|
|
40
|
+
}
|
|
41
|
+
extension FormStyle where Self == GroupedFormStyle {
|
|
42
|
+
public static var grouped: GroupedFormStyle { GroupedFormStyle() }
|
|
43
|
+
}
|
|
44
|
+
extension FormStyle where Self == ColumnsFormStyle {
|
|
45
|
+
public static var columns: ColumnsFormStyle { ColumnsFormStyle() }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
extension Image {
|
|
49
|
+
public enum Scale: Hashable, Sendable {
|
|
50
|
+
case small, medium, large
|
|
51
|
+
|
|
52
|
+
var name: String {
|
|
53
|
+
switch self {
|
|
54
|
+
case .small: "small"
|
|
55
|
+
case .medium: "medium"
|
|
56
|
+
case .large: "large"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
extension View {
|
|
63
|
+
/// Translates the rendered view without affecting layout.
|
|
64
|
+
public func offset(x: CGFloat = 0, y: CGFloat = 0) -> some View {
|
|
65
|
+
_styled { $0.extras["offset"] = .object(["x": .number(x), "y": .number(y)]) }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public func offset(_ offset: CGSize) -> some View {
|
|
69
|
+
self.offset(x: offset.width, y: offset.height)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public func rotationEffect(_ angle: Angle, anchor: UnitPoint = .center) -> some View {
|
|
73
|
+
_styled {
|
|
74
|
+
$0.extras["rotation"] = .number(angle.degrees)
|
|
75
|
+
$0.extras["rotationAnchor"] = anchor._json
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public func scaleEffect(_ scale: CGFloat, anchor: UnitPoint = .center) -> some View {
|
|
80
|
+
scaleEffect(x: scale, y: scale, anchor: anchor)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public func scaleEffect(_ scale: CGSize, anchor: UnitPoint = .center) -> some View {
|
|
84
|
+
scaleEffect(x: scale.width, y: scale.height, anchor: anchor)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public func scaleEffect(x: CGFloat = 1, y: CGFloat = 1, anchor: UnitPoint = .center) -> some View {
|
|
88
|
+
_styled {
|
|
89
|
+
$0.extras["scale"] = .object(["x": .number(x), "y": .number(y)])
|
|
90
|
+
$0.extras["scaleAnchor"] = anchor._json
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Rotates the view in 3D about `axis` through `anchor`: the `rotation3D`
|
|
95
|
+
/// style hint (angle in degrees; the renderer applies the perspective).
|
|
96
|
+
public func rotation3DEffect(_ angle: Angle, axis: (x: CGFloat, y: CGFloat, z: CGFloat),
|
|
97
|
+
anchor: UnitPoint = .center, anchorZ: CGFloat = 0,
|
|
98
|
+
perspective: CGFloat = 1) -> some View {
|
|
99
|
+
_styled {
|
|
100
|
+
$0.extras["rotation3D"] = .object([
|
|
101
|
+
"angle": .number(angle.degrees),
|
|
102
|
+
"axis": .object(["x": .number(axis.x), "y": .number(axis.y), "z": .number(axis.z)]),
|
|
103
|
+
"anchor": anchor._json,
|
|
104
|
+
"perspective": .number(perspective),
|
|
105
|
+
])
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// A Gaussian blur of `radius` points (`opaque` is accepted and ignored).
|
|
110
|
+
public func blur(radius: CGFloat, opaque: Bool = false) -> some View {
|
|
111
|
+
_styled { $0.extras["blur"] = .number(radius) }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// `false` makes the subtree ignore pointer events (`hitTesting: false`).
|
|
115
|
+
public func allowsHitTesting(_ enabled: Bool) -> some View {
|
|
116
|
+
_styled { if !enabled { $0.extras["hitTesting"] = .bool(false) } }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Lets the view extend under the safe area on `edges`: the
|
|
120
|
+
/// `ignoresSafeArea` hint, `"all"` or the list of edges. `regions` is accepted
|
|
121
|
+
/// and ignored (the browser has no keyboard safe area).
|
|
122
|
+
public func ignoresSafeArea(_ regions: SafeAreaRegions = .all, edges: Edge.Set = .all) -> some View {
|
|
123
|
+
_styled {
|
|
124
|
+
if edges == .all {
|
|
125
|
+
$0.extras["ignoresSafeArea"] = .string("all")
|
|
126
|
+
} else {
|
|
127
|
+
let names = Edge.allCases.filter { edges.contains(Edge.Set(rawValue: 1 << $0.rawValue)) }.map(\.name)
|
|
128
|
+
$0.extras["ignoresSafeArea"] = .array(names.map(_JSON.string))
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public func shadow(color: Color = Color(white: 0, opacity: 0.33), radius: CGFloat, x: CGFloat = 0, y: CGFloat = 0) -> some View {
|
|
134
|
+
_styled {
|
|
135
|
+
$0.extras["shadow"] = .object([
|
|
136
|
+
"color": color._json, "radius": .number(radius), "x": .number(x), "y": .number(y),
|
|
137
|
+
])
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public func clipped(antialiased: Bool = false) -> some View {
|
|
142
|
+
_styled { $0.extras["clipped"] = .bool(true) }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Keeps the view's layout but draws nothing.
|
|
146
|
+
public func hidden() -> some View {
|
|
147
|
+
_styled { $0.extras["hidden"] = .bool(true) }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Masks the view with a paint: a `Color` or gradient mask becomes the
|
|
151
|
+
/// `mask` style hint (its alpha clips the view). Any other mask view is
|
|
152
|
+
/// not supported and leaves the view unmasked.
|
|
153
|
+
public func mask<Mask: View>(alignment: Alignment = .center, @ViewBuilder _ mask: () -> Mask) -> some View {
|
|
154
|
+
self.mask(mask())
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public func mask<Mask: View>(_ mask: Mask) -> some View {
|
|
158
|
+
_styled {
|
|
159
|
+
if let style = mask as? any ShapeStyle { $0.extras["mask"] = style._json }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Fixes the view's center at a point in its parent's coordinate space; the
|
|
164
|
+
/// view then fills the proposed space.
|
|
165
|
+
public func position(x: CGFloat, y: CGFloat) -> some View {
|
|
166
|
+
_styled { $0.extras["position"] = .object(["x": .number(x), "y": .number(y)]) }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public func position(_ position: CGPoint) -> some View {
|
|
170
|
+
self.position(x: position.x, y: position.y)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// The accent color for controls in the subtree (gauges, toggles, buttons).
|
|
174
|
+
public func tint(_ tint: Color?) -> some View {
|
|
175
|
+
_styled { $0.extras["tint"] = tint?._json ?? .null }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public func imageScale(_ scale: Image.Scale) -> some View {
|
|
179
|
+
_styled { $0.extras["imageScale"] = .string(scale.name) }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// MARK: List rows
|
|
183
|
+
|
|
184
|
+
/// Trailing text in a list row.
|
|
185
|
+
public func badge(_ count: Int) -> some View {
|
|
186
|
+
_styled { $0.extras["badge"] = count == 0 ? .null : .string(String(count)) }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public func badge<S: StringProtocol>(_ label: S?) -> some View {
|
|
190
|
+
_styled { $0.extras["badge"] = label.map { .string(String($0)) } ?? .null }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public func badge(_ label: Text?) -> some View {
|
|
194
|
+
_styled { $0.extras["badge"] = label.map { .string($0.content) } ?? .null }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public func listRowInsets(_ insets: EdgeInsets?) -> some View {
|
|
198
|
+
_styled { $0.extras["listRowInsets"] = insets?._json ?? .null }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// MARK: Accepted without effect (yet)
|
|
202
|
+
|
|
203
|
+
public func listRowBackground<V: View>(_ view: V?) -> some View { self }
|
|
204
|
+
public func listItemTint(_ tint: Color?) -> some View { self }
|
|
205
|
+
public func formStyle<S: FormStyle>(_ style: S) -> some View { self }
|
|
206
|
+
public func headerProminence(_ prominence: Prominence) -> some View { self }
|
|
207
|
+
public func containerShape<T: Shape>(_ shape: T) -> some View { self }
|
|
208
|
+
public func symbolVariant(_ variant: SymbolVariants) -> some View { self }
|
|
209
|
+
public func symbolRenderingMode(_ mode: SymbolRenderingMode?) -> some View { self }
|
|
210
|
+
public func blendMode(_ mode: BlendMode) -> some View { self }
|
|
211
|
+
public func contentTransition(_ transition: ContentTransition) -> some View { self }
|
|
212
|
+
public func textSelection<S: TextSelectability>(_ selectability: S) -> some View { self }
|
|
213
|
+
public func toolbarRole(_ role: ToolbarRole) -> some View { self }
|
|
214
|
+
public func controlSize(_ size: ControlSize) -> some View { self }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public enum Prominence: Hashable, Sendable { case standard, increased }
|
|
218
|
+
|
|
219
|
+
/// `.controlSize`: accepted for source compatibility; controls keep one size.
|
|
220
|
+
public enum ControlSize: Hashable, CaseIterable, Sendable { case mini, small, regular, large, extraLarge }
|
|
221
|
+
|
|
222
|
+
public struct SymbolVariants: Hashable, Sendable {
|
|
223
|
+
let name: String
|
|
224
|
+
public static let none = SymbolVariants(name: "none")
|
|
225
|
+
public static let circle = SymbolVariants(name: "circle")
|
|
226
|
+
public static let square = SymbolVariants(name: "square")
|
|
227
|
+
public static let rectangle = SymbolVariants(name: "rectangle")
|
|
228
|
+
public static let fill = SymbolVariants(name: "fill")
|
|
229
|
+
public static let slash = SymbolVariants(name: "slash")
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public enum SymbolRenderingMode: Hashable, Sendable { case monochrome, multicolor, hierarchical, palette }
|
|
233
|
+
|
|
234
|
+
public enum BlendMode: Hashable, Sendable {
|
|
235
|
+
case normal, multiply, screen, overlay, darken, lighten, colorDodge, colorBurn, softLight, hardLight,
|
|
236
|
+
difference, exclusion, hue, saturation, color, luminosity, sourceAtop, destinationOver,
|
|
237
|
+
destinationOut, plusDarker, plusLighter
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
public struct SafeAreaRegions: OptionSet, Sendable {
|
|
241
|
+
public let rawValue: UInt
|
|
242
|
+
public init(rawValue: UInt) { self.rawValue = rawValue }
|
|
243
|
+
public static let container = SafeAreaRegions(rawValue: 1)
|
|
244
|
+
public static let keyboard = SafeAreaRegions(rawValue: 2)
|
|
245
|
+
public static let all: SafeAreaRegions = [.container, .keyboard]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public struct ContentTransition: Hashable, Sendable {
|
|
249
|
+
let name: String
|
|
250
|
+
public static let identity = ContentTransition(name: "identity")
|
|
251
|
+
public static let opacity = ContentTransition(name: "opacity")
|
|
252
|
+
public static let interpolate = ContentTransition(name: "interpolate")
|
|
253
|
+
public static func numericText(countsDown: Bool = false) -> ContentTransition { ContentTransition(name: "numericText") }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
public protocol TextSelectability {}
|
|
257
|
+
public struct EnabledTextSelectability: TextSelectability { public init() {} }
|
|
258
|
+
public struct DisabledTextSelectability: TextSelectability { public init() {} }
|
|
259
|
+
extension TextSelectability where Self == EnabledTextSelectability {
|
|
260
|
+
public static var enabled: EnabledTextSelectability { EnabledTextSelectability() }
|
|
261
|
+
}
|
|
262
|
+
extension TextSelectability where Self == DisabledTextSelectability {
|
|
263
|
+
public static var disabled: DisabledTextSelectability { DisabledTextSelectability() }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
public enum ToolbarRole: Hashable, Sendable { case automatic, navigationStack, browser, editor }
|
|
267
|
+
|
|
268
|
+
// MARK: - Tap gesture
|
|
269
|
+
|
|
270
|
+
/// `.onTapGesture`: a `tapgesture` element whose tap event runs the action.
|
|
271
|
+
struct _TapGestureView<Content: View> {
|
|
272
|
+
let content: Content
|
|
273
|
+
let action: @MainActor () -> Void
|
|
274
|
+
}
|
|
275
|
+
extension _TapGestureView: _PrimitiveView {}
|
|
276
|
+
extension _TapGestureView: _HostView {
|
|
277
|
+
var _kind: String { "tapgesture" }
|
|
278
|
+
func _props(in environment: EnvironmentValues) -> _JSON { .object([:]) }
|
|
279
|
+
var _content: [any View] { [content] }
|
|
280
|
+
var _handler: ((_JSON) -> Void)? {
|
|
281
|
+
let action = self.action
|
|
282
|
+
return { _ in MainActor.assumeIsolated { action() } }
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
extension View {
|
|
287
|
+
public func onTapGesture(count: Int = 1, perform action: @escaping @MainActor () -> Void) -> some View {
|
|
288
|
+
_TapGestureView(content: self, action: action)
|
|
289
|
+
}
|
|
290
|
+
}
|