@swiftbrowser/swift 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/Package.swift +23 -0
  2. package/README.md +12 -2
  3. package/Sources/Charts/Charts.swift +65 -0
  4. package/Sources/FoundationBridge/FoundationBridge.swift +77 -0
  5. package/Sources/FoundationBridge/Locale.swift +86 -0
  6. package/Sources/FoundationBridge/Timer.swift +48 -0
  7. package/Sources/FoundationBridge/UITextChecker.swift +30 -0
  8. package/Sources/FoundationLite/Common.swift +3 -0
  9. package/Sources/FoundationLite/FoundationLite.swift +171 -0
  10. package/Sources/FoundationLite/Interaction.swift +146 -0
  11. package/Sources/FoundationLite/Timer.swift +48 -0
  12. package/Sources/SwiftUI/API/Accessibility.swift +132 -0
  13. package/Sources/SwiftUI/API/Alert.swift +221 -0
  14. package/Sources/SwiftUI/API/Animation.swift +253 -0
  15. package/Sources/SwiftUI/API/App.swift +57 -0
  16. package/Sources/SwiftUI/API/Color.swift +161 -0
  17. package/Sources/SwiftUI/API/Controls.swift +268 -0
  18. package/Sources/SwiftUI/API/CustomLayout.swift +403 -0
  19. package/Sources/SwiftUI/API/Decorations.swift +92 -0
  20. package/Sources/SwiftUI/API/EditMode.swift +269 -0
  21. package/Sources/SwiftUI/API/Effects.swift +290 -0
  22. package/Sources/SwiftUI/API/Environment.swift +355 -0
  23. package/Sources/SwiftUI/API/Focus.swift +129 -0
  24. package/Sources/SwiftUI/API/Font.swift +131 -0
  25. package/Sources/SwiftUI/API/Gauge.swift +289 -0
  26. package/Sources/SwiftUI/API/Geometry.swift +89 -0
  27. package/Sources/SwiftUI/API/GeometryReader.swift +124 -0
  28. package/Sources/SwiftUI/API/Gestures.swift +342 -0
  29. package/Sources/SwiftUI/API/Grid.swift +188 -0
  30. package/Sources/SwiftUI/API/LabelStyle.swift +117 -0
  31. package/Sources/SwiftUI/API/Layout.swift +165 -0
  32. package/Sources/SwiftUI/API/Lists.swift +276 -0
  33. package/Sources/SwiftUI/API/Menu.swift +51 -0
  34. package/Sources/SwiftUI/API/Modifiers.swift +243 -0
  35. package/Sources/SwiftUI/API/Navigation.swift +371 -0
  36. package/Sources/SwiftUI/API/ObservableObject.swift +279 -0
  37. package/Sources/SwiftUI/API/OnChange.swift +48 -0
  38. package/Sources/SwiftUI/API/Phase8Controls.swift +394 -0
  39. package/Sources/SwiftUI/API/Picker.swift +176 -0
  40. package/Sources/SwiftUI/API/Presentation.swift +109 -0
  41. package/Sources/SwiftUI/API/Primitives.swift +390 -0
  42. package/Sources/SwiftUI/API/Publishers.swift +107 -0
  43. package/Sources/SwiftUI/API/RowActions.swift +83 -0
  44. package/Sources/SwiftUI/API/ScrollTargets.swift +104 -0
  45. package/Sources/SwiftUI/API/ShapeStyle.swift +311 -0
  46. package/Sources/SwiftUI/API/State.swift +127 -0
  47. package/Sources/SwiftUI/API/StrokeStyle.swift +30 -0
  48. package/Sources/SwiftUI/API/Tabs.swift +119 -0
  49. package/Sources/SwiftUI/API/Tasks.swift +35 -0
  50. package/Sources/SwiftUI/API/Timeline.swift +212 -0
  51. package/Sources/SwiftUI/API/Toolbar.swift +215 -0
  52. package/Sources/SwiftUI/API/View.swift +207 -0
  53. package/Sources/SwiftUI/API/ViewModifier.swift +40 -0
  54. package/Sources/SwiftUI/API/VisualEffect.swift +82 -0
  55. package/Sources/SwiftUI/Charts/Axis.swift +319 -0
  56. package/Sources/SwiftUI/Charts/Chart.swift +646 -0
  57. package/Sources/SwiftUI/Charts/ChartContent.swift +376 -0
  58. package/Sources/SwiftUI/Charts/Marks.swift +204 -0
  59. package/Sources/SwiftUI/Charts/Plottable.swift +99 -0
  60. package/Sources/SwiftUI/Charts/Scales.swift +326 -0
  61. package/Sources/SwiftUI/Runtime/Executor.swift +83 -0
  62. package/Sources/SwiftUI/Runtime/Exports.swift +65 -0
  63. package/Sources/SwiftUI/Runtime/JSON.swift +71 -0
  64. package/Sources/SwiftUI/Runtime/JSONParser.swift +443 -0
  65. package/Sources/SwiftUI/Runtime/Ops.swift +106 -0
  66. package/Sources/SwiftUI/Runtime/Reconciler.swift +210 -0
  67. package/Sources/SwiftUI/Runtime/Runtime.swift +1367 -0
  68. package/Sources/_FoundationCommon/Bundle.swift +82 -0
  69. package/Sources/_FoundationCommon/UserDefaults.swift +170 -0
  70. package/Sources/_FoundationCommon/_FoundationCommon.swift +10 -0
  71. package/index.d.ts +2 -0
  72. package/index.mjs +16 -0
  73. package/package.json +28 -4
  74. package/sync-sources.mjs +19 -0
@@ -0,0 +1,268 @@
1
+ // MARK: - TextField
2
+
3
+ public protocol TextFieldStyle {
4
+ var _name: String { get }
5
+ }
6
+ public struct DefaultTextFieldStyle: TextFieldStyle {
7
+ public init() {}
8
+ public var _name: String { "plain" }
9
+ }
10
+ public struct PlainTextFieldStyle: TextFieldStyle {
11
+ public init() {}
12
+ public var _name: String { "plain" }
13
+ }
14
+ public struct RoundedBorderTextFieldStyle: TextFieldStyle {
15
+ public init() {}
16
+ public var _name: String { "roundedBorder" }
17
+ }
18
+ extension TextFieldStyle where Self == DefaultTextFieldStyle {
19
+ public static var automatic: DefaultTextFieldStyle { DefaultTextFieldStyle() }
20
+ }
21
+ extension TextFieldStyle where Self == PlainTextFieldStyle {
22
+ public static var plain: PlainTextFieldStyle { PlainTextFieldStyle() }
23
+ }
24
+ extension TextFieldStyle where Self == RoundedBorderTextFieldStyle {
25
+ public static var roundedBorder: RoundedBorderTextFieldStyle { RoundedBorderTextFieldStyle() }
26
+ }
27
+
28
+ extension View {
29
+ public func textFieldStyle<S: TextFieldStyle>(_ style: S) -> some View {
30
+ environment(\._textFieldStyle, style._name)
31
+ }
32
+ }
33
+
34
+ /// Formats a value as text and parses text back (`Text(_:format:)`,
35
+ /// `TextField(_:value:format:)`). `FoundationLite` conforms its format styles
36
+ /// (`.number`, `.currency(code:)`); the shim itself has none.
37
+ public protocol _TextFormat {
38
+ associatedtype Input
39
+ func _formatText(_ value: Input) -> String
40
+ func _parseText(_ text: String) -> Input?
41
+ }
42
+
43
+ /// What the user typed into a formatted field, and the formatted form of the
44
+ /// value it parsed to. The field shows the typed text while the bound value
45
+ /// still formats to that form; an outside change to the value replaces it.
46
+ struct _TextDraft {
47
+ var typed: String
48
+ var formatted: String
49
+ }
50
+
51
+ public struct TextField<Label: View> {
52
+ let placeholder: String
53
+ let text: Binding<String>
54
+ let secure: Bool
55
+ /// Persistent storage for `TextField(_:value:format:)` (unused otherwise);
56
+ /// a `State` so the runtime keeps it across re-renders.
57
+ let draft: State<_TextDraft?>
58
+
59
+ init(placeholder: String, text: Binding<String>, secure: Bool) {
60
+ self.placeholder = placeholder
61
+ self.text = text
62
+ self.secure = secure
63
+ draft = State(wrappedValue: nil)
64
+ }
65
+ }
66
+
67
+ extension TextField where Label == Text {
68
+ public init<S: StringProtocol>(_ title: S, text: Binding<String>) {
69
+ self.init(placeholder: String(title), text: text, secure: false)
70
+ }
71
+
72
+ /// The prompt, when given, is the placeholder; the title names the field.
73
+ public init<S: StringProtocol>(_ title: S, text: Binding<String>, prompt: Text?) {
74
+ self.init(placeholder: prompt?.content ?? String(title), text: text, secure: false)
75
+ }
76
+
77
+ public init(_ titleKey: LocalizedStringKey, text: Binding<String>, prompt: Text? = nil) {
78
+ self.init(placeholder: prompt?.content ?? titleKey.key, text: text, secure: false)
79
+ }
80
+
81
+ /// A field bound to a value through a format: it shows the formatted value
82
+ /// and, as the user types, parses the text and sets the value when parsing
83
+ /// succeeds. The typed text stays visible while the value matches it.
84
+ public init<S: StringProtocol, F: _TextFormat>(_ title: S, value: Binding<F.Input>, format: F,
85
+ prompt: Text? = nil) {
86
+ self.init(_placeholder: prompt?.content ?? String(title), value: value, format: format)
87
+ }
88
+
89
+ public init<F: _TextFormat>(_ titleKey: LocalizedStringKey, value: Binding<F.Input>, format: F,
90
+ prompt: Text? = nil) {
91
+ self.init(_placeholder: prompt?.content ?? titleKey.key, value: value, format: format)
92
+ }
93
+
94
+ /// The formatted field with its placeholder resolved; public so a bridge
95
+ /// module can wrap other format protocols (FoundationBridge wraps
96
+ /// Foundation's `ParseableFormatStyle`) without re-entering the public
97
+ /// initializers above.
98
+ public init<F: _TextFormat>(_placeholder placeholder: String, value: Binding<F.Input>, format: F) {
99
+ let draft = State<_TextDraft?>(wrappedValue: nil)
100
+ let text = Binding<String>(
101
+ get: {
102
+ let formatted = format._formatText(value.wrappedValue)
103
+ if let current = draft.wrappedValue, current.formatted == formatted { return current.typed }
104
+ return formatted
105
+ },
106
+ set: { typed in
107
+ if let parsed = format._parseText(typed) {
108
+ draft.wrappedValue = _TextDraft(typed: typed, formatted: format._formatText(parsed))
109
+ value.wrappedValue = parsed
110
+ } else {
111
+ let formatted = draft.wrappedValue?.formatted ?? format._formatText(value.wrappedValue)
112
+ draft.wrappedValue = _TextDraft(typed: typed, formatted: formatted)
113
+ }
114
+ }
115
+ )
116
+ self.placeholder = placeholder
117
+ self.text = text
118
+ secure = false
119
+ self.draft = draft
120
+ }
121
+ }
122
+ extension TextField: _PrimitiveView {}
123
+ extension TextField: _HostView {
124
+ var _kind: String { "textfield" }
125
+ func _props(in environment: EnvironmentValues) -> _JSON {
126
+ var props: [String: _JSON] = [
127
+ "text": .string(text.wrappedValue),
128
+ "placeholder": .string(placeholder),
129
+ "style": .string(environment._textFieldStyle),
130
+ "secure": .bool(secure),
131
+ ]
132
+ // Phase 12: `.focused(...)` binds the field's focus; `.onSubmit` wants
133
+ // the Enter key; `.submitLabel` names the return key.
134
+ if let focus = environment._focus { props["focused"] = .bool(focus.isFocused()) }
135
+ if environment._onSubmit != nil { props["submit"] = .bool(true) }
136
+ if let label = environment._submitLabel { props["submitLabel"] = .string(label) }
137
+ return .object(props)
138
+ }
139
+ var _handler: ((_JSON) -> Void)? { _handler(in: EnvironmentValues()) }
140
+ }
141
+ extension TextField: _EnvironmentHandling {
142
+ /// Text (`"value"` as a string), `focus` (`{type, value: Bool}`) and `submit` (`{type}`) events.
143
+ func _handler(in environment: EnvironmentValues) -> ((_JSON) -> Void)? {
144
+ let text = self.text
145
+ let focus = environment._focus
146
+ let onSubmit = environment._onSubmit
147
+ return { payload in
148
+ switch payload {
149
+ case .string(let value):
150
+ text.wrappedValue = value
151
+ case .object(let fields):
152
+ switch fields["type"] {
153
+ case .string("focus")?:
154
+ if case .bool(let focused)? = fields["value"] { focus?.setFocused(focused) }
155
+ case .string("submit")?:
156
+ onSubmit?()
157
+ default:
158
+ break
159
+ }
160
+ default:
161
+ break
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ public struct SecureField<Label: View> {
168
+ let field: TextField<Label>
169
+ }
170
+ extension SecureField where Label == Text {
171
+ public init<S: StringProtocol>(_ title: S, text: Binding<String>) {
172
+ field = TextField(placeholder: String(title), text: text, secure: true)
173
+ }
174
+ }
175
+ extension SecureField: _PrimitiveView {}
176
+ extension SecureField: _HostView {
177
+ var _kind: String { "textfield" }
178
+ func _props(in environment: EnvironmentValues) -> _JSON { field._props(in: environment) }
179
+ var _handler: ((_JSON) -> Void)? { field._handler }
180
+ }
181
+ extension SecureField: _EnvironmentHandling {
182
+ func _handler(in environment: EnvironmentValues) -> ((_JSON) -> Void)? { field._handler(in: environment) }
183
+ }
184
+
185
+ // MARK: - Toggle
186
+
187
+ public struct Toggle<Label: View> {
188
+ let isOn: Binding<Bool>
189
+ let label: Label
190
+
191
+ public init(isOn: Binding<Bool>, @ViewBuilder label: () -> Label) {
192
+ self.isOn = isOn
193
+ self.label = label()
194
+ }
195
+ }
196
+ extension Toggle where Label == Text {
197
+ public init<S: StringProtocol>(_ title: S, isOn: Binding<Bool>) {
198
+ self.init(isOn: isOn) { Text(title) }
199
+ }
200
+ }
201
+ extension Toggle: _PrimitiveView {}
202
+ extension Toggle: _HostView {
203
+ var _kind: String { "toggle" }
204
+ func _props(in environment: EnvironmentValues) -> _JSON { .object(["isOn": .bool(isOn.wrappedValue)]) }
205
+ var _content: [any View] { [label] }
206
+ var _handler: ((_JSON) -> Void)? {
207
+ let isOn = self.isOn
208
+ return { payload in
209
+ if case .bool(let value) = payload { isOn.wrappedValue = value }
210
+ }
211
+ }
212
+ }
213
+
214
+ // MARK: - Label
215
+
216
+ /// A title and an icon, laid out like SwiftUI's default label style.
217
+ public struct Label<Title: View, Icon: View>: View {
218
+ let title: Title
219
+ let icon: Icon
220
+
221
+ public init(@ViewBuilder title: () -> Title, @ViewBuilder icon: () -> Icon) {
222
+ self.title = title()
223
+ self.icon = icon()
224
+ }
225
+
226
+ public var body: some View {
227
+ _StyledLabel(title: title, icon: icon)
228
+ }
229
+ }
230
+
231
+ extension Label where Title == Text, Icon == Image {
232
+ public init<S: StringProtocol>(_ title: S, systemImage: String) {
233
+ self.title = Text(title)
234
+ self.icon = Image(systemName: systemImage)
235
+ }
236
+
237
+ public init(_ titleKey: LocalizedStringKey, systemImage: String) {
238
+ self.title = Text(titleKey)
239
+ self.icon = Image(systemName: systemImage)
240
+ }
241
+
242
+ public init<S: StringProtocol>(_ title: S, image name: String) {
243
+ self.title = Text(title)
244
+ self.icon = Image(name)
245
+ }
246
+ }
247
+
248
+ // MARK: - Enabling
249
+
250
+ extension View {
251
+ public func disabled(_ disabled: Bool) -> some View {
252
+ _styled { $0.disabled = disabled ? true : nil }
253
+ }
254
+
255
+ /// Runs `action` once, right after the view is first rendered.
256
+ public func onAppear(perform action: (@MainActor () -> Void)? = nil) -> some View {
257
+ _OnAppearView(content: self, action: action)
258
+ }
259
+ }
260
+
261
+ struct _OnAppearView<Content: View> {
262
+ let content: Content
263
+ let action: (@MainActor () -> Void)?
264
+ }
265
+ extension _OnAppearView: _PrimitiveView {}
266
+ extension _OnAppearView: _GroupView {
267
+ var _children: [any View] { [content] }
268
+ }
@@ -0,0 +1,403 @@
1
+ // The `Layout` protocol, measured by the host. The Swift side has no text
2
+ // metrics, so a custom layout cannot measure its children itself: the
3
+ // renderer measures them (minimum, ideal and maximum size for the proposal
4
+ // it places the `layout` element with) and reports the results in a `layout`
5
+ // event. The runtime then runs `sizeThatFits` / `placeSubviews` and sends the
6
+ // resulting size and frames back as props. See docs/ops-protocol.md (Phase 6).
7
+
8
+ // MARK: - Public API
9
+
10
+ /// A size proposed to a view; a `nil` dimension means "whatever you want".
11
+ public struct ProposedViewSize: Equatable, Sendable {
12
+ public var width: CGFloat?
13
+ public var height: CGFloat?
14
+
15
+ public init(width: CGFloat?, height: CGFloat?) {
16
+ self.width = width
17
+ self.height = height
18
+ }
19
+
20
+ public init(_ size: CGSize) {
21
+ self.init(width: size.width, height: size.height)
22
+ }
23
+
24
+ public static let zero = ProposedViewSize(width: 0, height: 0)
25
+ public static let unspecified = ProposedViewSize(width: nil, height: nil)
26
+ public static let infinity = ProposedViewSize(width: .infinity, height: .infinity)
27
+
28
+ public func replacingUnspecifiedDimensions(by size: CGSize = CGSize(width: 10, height: 10)) -> CGSize {
29
+ CGSize(width: width ?? size.width, height: height ?? size.height)
30
+ }
31
+
32
+ var _json: _JSON {
33
+ .object(["width": _dimensionJSON(width), "height": _dimensionJSON(height)])
34
+ }
35
+ }
36
+
37
+ /// The size of a view, as `LayoutSubview.dimensions(in:)` reports it.
38
+ public struct ViewDimensions: Equatable, Sendable {
39
+ public var width: CGFloat
40
+ public var height: CGFloat
41
+
42
+ init(width: CGFloat, height: CGFloat) {
43
+ self.width = width
44
+ self.height = height
45
+ }
46
+
47
+ public subscript(guide: HorizontalAlignment) -> CGFloat {
48
+ switch guide.name {
49
+ case "leading": 0
50
+ case "trailing": width
51
+ default: width / 2
52
+ }
53
+ }
54
+
55
+ public subscript(guide: VerticalAlignment) -> CGFloat {
56
+ switch guide.name {
57
+ case "top": 0
58
+ case "bottom": height
59
+ default: height / 2
60
+ }
61
+ }
62
+ }
63
+
64
+ /// Preferred spacing around a view. The shim has no per-view spacing
65
+ /// preferences: every distance is the system default (8 points).
66
+ public struct ViewSpacing: Equatable, Sendable {
67
+ public init() {}
68
+ public static let zero = ViewSpacing()
69
+
70
+ public func distance(to next: ViewSpacing, along axis: Axis) -> CGFloat { 8 }
71
+ public mutating func formUnion(_ other: ViewSpacing, edges: Edge.Set = .all) {}
72
+ public func union(_ other: ViewSpacing, edges: Edge.Set = .all) -> ViewSpacing { self }
73
+ }
74
+
75
+ /// Characteristics of a layout the system may take into account.
76
+ public struct LayoutProperties: Sendable {
77
+ public var stackOrientation: Axis?
78
+ public init() {}
79
+ }
80
+
81
+ /// A key for values a subview exposes to its layout via `layoutValue(key:value:)`.
82
+ /// The shim does not propagate such values; the layout always sees the default.
83
+ public protocol LayoutValueKey {
84
+ associatedtype Value
85
+ static var defaultValue: Value { get }
86
+ }
87
+
88
+ extension View {
89
+ /// Accepted for source compatibility: custom layouts read `K.defaultValue`.
90
+ public func layoutValue<K: LayoutValueKey>(key: K.Type, value: K.Value) -> some View { self }
91
+ }
92
+
93
+ /// A proxy for one child of a custom layout.
94
+ ///
95
+ /// The sizes come from the renderer's measurement of the child: its minimum
96
+ /// (proposal 0), ideal (proposal `nil`) and maximum (proposal infinity).
97
+ /// `sizeThatFits` answers from those: the ideal for an unspecified axis,
98
+ /// otherwise the proposal clamped to the minimum and maximum.
99
+ public struct LayoutSubview {
100
+ let index: Int
101
+ let min: CGSize
102
+ let ideal: CGSize
103
+ let max: CGSize
104
+ let placements: _LayoutPlacements
105
+
106
+ public func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
107
+ CGSize(
108
+ width: Self.fit(proposal.width, min: min.width, ideal: ideal.width, max: max.width),
109
+ height: Self.fit(proposal.height, min: min.height, ideal: ideal.height, max: max.height)
110
+ )
111
+ }
112
+
113
+ static func fit(_ proposed: CGFloat?, min: CGFloat, ideal: CGFloat, max: CGFloat) -> CGFloat {
114
+ guard let proposed else { return ideal }
115
+ let lower = Swift.min(min, max)
116
+ return Swift.max(lower, Swift.min(proposed, max))
117
+ }
118
+
119
+ public func dimensions(in proposal: ProposedViewSize) -> ViewDimensions {
120
+ let size = sizeThatFits(proposal)
121
+ return ViewDimensions(width: size.width, height: size.height)
122
+ }
123
+
124
+ public var spacing: ViewSpacing { ViewSpacing() }
125
+
126
+ public var priority: Double { 0 }
127
+
128
+ public subscript<K: LayoutValueKey>(key: K.Type) -> K.Value { K.defaultValue }
129
+
130
+ /// Records where the child goes. Its size is `sizeThatFits(proposal)` and
131
+ /// `anchor` says which point of that size sits at `position`.
132
+ public func place(at position: CGPoint, anchor: UnitPoint = .topLeading, proposal: ProposedViewSize) {
133
+ let size = sizeThatFits(proposal)
134
+ let origin = CGPoint(x: position.x - anchor.x * size.width, y: position.y - anchor.y * size.height)
135
+ placements.frames[index] = CGRect(origin: origin, size: size)
136
+ }
137
+ }
138
+
139
+ /// Frames recorded by `LayoutSubview.place`, shared by every proxy of one pass.
140
+ public final class _LayoutPlacements {
141
+ var frames: [Int: CGRect] = [:]
142
+ init() {}
143
+ }
144
+
145
+ /// The children of a custom layout.
146
+ public struct LayoutSubviews: RandomAccessCollection, Equatable {
147
+ public typealias Element = LayoutSubview
148
+ public typealias Index = Int
149
+ public typealias SubSequence = LayoutSubviews
150
+
151
+ let subviews: [LayoutSubview]
152
+ public let startIndex: Int
153
+ public let endIndex: Int
154
+
155
+ init(_ subviews: [LayoutSubview]) {
156
+ self.subviews = subviews
157
+ startIndex = 0
158
+ endIndex = subviews.count
159
+ }
160
+
161
+ private init(_ subviews: [LayoutSubview], range: Range<Int>) {
162
+ self.subviews = subviews
163
+ startIndex = range.lowerBound
164
+ endIndex = range.upperBound
165
+ }
166
+
167
+ public subscript(position: Int) -> LayoutSubview { subviews[position] }
168
+
169
+ public subscript(bounds: Range<Int>) -> LayoutSubviews {
170
+ precondition(bounds.lowerBound >= startIndex && bounds.upperBound <= endIndex, "LayoutSubviews index out of range")
171
+ return LayoutSubviews(subviews, range: bounds)
172
+ }
173
+
174
+ public static func == (lhs: LayoutSubviews, rhs: LayoutSubviews) -> Bool {
175
+ lhs.startIndex == rhs.startIndex && lhs.endIndex == rhs.endIndex
176
+ && lhs.subviews.count == rhs.subviews.count
177
+ && lhs.subviews.indices.allSatisfy {
178
+ lhs.subviews[$0].min == rhs.subviews[$0].min
179
+ && lhs.subviews[$0].ideal == rhs.subviews[$0].ideal
180
+ && lhs.subviews[$0].max == rhs.subviews[$0].max
181
+ }
182
+ }
183
+ }
184
+
185
+ /// A custom container layout. Conforming types compile unchanged against
186
+ /// Apple's SwiftUI; see the file comment for how measurement works here.
187
+ public protocol Layout {
188
+ associatedtype Cache = Void
189
+ typealias Subviews = LayoutSubviews
190
+
191
+ static var layoutProperties: LayoutProperties { get }
192
+
193
+ func makeCache(subviews: Subviews) -> Cache
194
+ func updateCache(_ cache: inout Cache, subviews: Subviews)
195
+ func spacing(subviews: Subviews, cache: inout Cache) -> ViewSpacing
196
+ func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) -> CGSize
197
+ func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache)
198
+ func explicitAlignment(of guide: HorizontalAlignment, in bounds: CGRect, proposal: ProposedViewSize,
199
+ subviews: Subviews, cache: inout Cache) -> CGFloat?
200
+ func explicitAlignment(of guide: VerticalAlignment, in bounds: CGRect, proposal: ProposedViewSize,
201
+ subviews: Subviews, cache: inout Cache) -> CGFloat?
202
+ }
203
+
204
+ extension Layout {
205
+ public static var layoutProperties: LayoutProperties { LayoutProperties() }
206
+
207
+ public func updateCache(_ cache: inout Cache, subviews: Subviews) {
208
+ cache = makeCache(subviews: subviews)
209
+ }
210
+
211
+ public func spacing(subviews: Subviews, cache: inout Cache) -> ViewSpacing { ViewSpacing() }
212
+
213
+ public func explicitAlignment(of guide: HorizontalAlignment, in bounds: CGRect, proposal: ProposedViewSize,
214
+ subviews: Subviews, cache: inout Cache) -> CGFloat? { nil }
215
+
216
+ public func explicitAlignment(of guide: VerticalAlignment, in bounds: CGRect, proposal: ProposedViewSize,
217
+ subviews: Subviews, cache: inout Cache) -> CGFloat? { nil }
218
+ }
219
+
220
+ extension Layout where Cache == Void {
221
+ public func makeCache(subviews: Subviews) -> Cache { () }
222
+ public func updateCache(_ cache: inout Cache, subviews: Subviews) {}
223
+ }
224
+
225
+ extension Layout {
226
+ /// Uses the layout as a container: `MyLayout { child; child }`.
227
+ public func callAsFunction<V: View>(@ViewBuilder _ content: () -> V) -> some View {
228
+ _LayoutView(layout: self, content: content())
229
+ }
230
+ }
231
+
232
+ // MARK: - Runtime
233
+
234
+ /// The result of running a layout for one host measurement; what the `layout`
235
+ /// element's props carry. Values are rounded to 2 decimals when stored so an
236
+ /// unchanged measurement compares equal and emits no `update`.
237
+ struct _LayoutResult: Equatable {
238
+ var proposal: ProposedViewSize
239
+ var size: CGSize
240
+ var frames: [CGRect]
241
+ /// The layout's own flexibility: `sizeThatFits` for a zero, unspecified and
242
+ /// infinite proposal. The host reads these when a parent measures the
243
+ /// layout with a proposal other than the one it answered, instead of
244
+ /// guessing from the answered size.
245
+ var sizes: _LayoutSizes
246
+
247
+ var _json: [String: _JSON] {
248
+ [
249
+ "proposal": proposal._json,
250
+ "size": .object(["width": .number(size.width), "height": .number(size.height)]),
251
+ "frames": .array(frames.map {
252
+ .object(["x": .number($0.minX), "y": .number($0.minY),
253
+ "width": .number($0.width), "height": .number($0.height)])
254
+ }),
255
+ "sizes": sizes._json,
256
+ ]
257
+ }
258
+ }
259
+
260
+ /// A layout's min / ideal / max sizes. Infinite dimensions serialize as
261
+ /// `"infinity"`, like proposals do.
262
+ struct _LayoutSizes: Equatable {
263
+ var min: CGSize
264
+ var ideal: CGSize
265
+ var max: CGSize
266
+
267
+ var _json: _JSON {
268
+ func size(_ s: CGSize) -> _JSON {
269
+ .object(["width": _dimensionJSON(s.width), "height": _dimensionJSON(s.height)])
270
+ }
271
+ return .object(["min": size(min), "ideal": size(ideal), "max": size(max)])
272
+ }
273
+ }
274
+
275
+ /// Holds a layout's cache across measurements without triggering renders.
276
+ final class _LayoutCacheBox {
277
+ var cache: Any?
278
+ }
279
+
280
+ /// The element a custom layout renders as: kind `layout`, the children
281
+ /// flattened inside it like a `zstack`'s. The host measures the children and
282
+ /// sends a `layout` event; the handler runs the layout and stores the result
283
+ /// in `State`, so it persists across re-renders and invalidates on change.
284
+ struct _LayoutView<L: Layout, Content: View> {
285
+ let layout: L
286
+ let content: Content
287
+ @State var result: _LayoutResult? = nil
288
+ @State var cacheBox = _LayoutCacheBox()
289
+
290
+ init(layout: L, content: Content) {
291
+ self.layout = layout
292
+ self.content = content
293
+ }
294
+ }
295
+ extension _LayoutView: _PrimitiveView {}
296
+ extension _LayoutView: _HostView {
297
+ var _kind: String { "layout" }
298
+
299
+ func _props(in environment: EnvironmentValues) -> _JSON {
300
+ .object(result?._json ?? ["proposal": .null, "size": .null, "frames": .null])
301
+ }
302
+
303
+ var _content: [any View] { [content] }
304
+
305
+ var _handler: ((_JSON) -> Void)? {
306
+ let layout = self.layout
307
+ let resultBox = _result.box
308
+ let cacheBox = self.cacheBox
309
+ return { event in
310
+ guard let (proposal, subviews) = _parseLayoutEvent(event) else { return }
311
+ let computed = _runLayout(layout, proposal: proposal, subviews: subviews, cacheBox: cacheBox)
312
+ if computed != resultBox.value { resultBox.value = computed }
313
+ }
314
+ }
315
+ }
316
+
317
+ /// Parses `{"proposal": {...}, "children": [{"min", "ideal", "max"}]}`.
318
+ func _parseLayoutEvent(_ event: _JSON) -> (ProposedViewSize, LayoutSubviews)? {
319
+ guard case .object(let fields) = event else { return nil }
320
+ let proposal: ProposedViewSize
321
+ if case .object(let p)? = fields["proposal"] {
322
+ proposal = ProposedViewSize(width: _dimension(p["width"]), height: _dimension(p["height"]))
323
+ } else {
324
+ proposal = .unspecified
325
+ }
326
+ guard case .array(let children)? = fields["children"] else { return nil }
327
+ let placements = _LayoutPlacements()
328
+ let subviews = children.enumerated().map { index, child in
329
+ func size(_ key: String) -> CGSize {
330
+ guard case .object(let c) = child, case .object(let s)? = c[key] else { return .zero }
331
+ return CGSize(width: _dimension(s["width"]) ?? 0, height: _dimension(s["height"]) ?? 0)
332
+ }
333
+ return LayoutSubview(index: index, min: size("min"), ideal: size("ideal"), max: size("max"),
334
+ placements: placements)
335
+ }
336
+ return (proposal, LayoutSubviews(subviews))
337
+ }
338
+
339
+ /// Runs `sizeThatFits` and `placeSubviews`; unplaced subviews are centered at
340
+ /// their ideal size, like SwiftUI does.
341
+ func _runLayout<L: Layout>(_ layout: L, proposal: ProposedViewSize, subviews: LayoutSubviews,
342
+ cacheBox: _LayoutCacheBox) -> _LayoutResult {
343
+ var cache: L.Cache
344
+ if let existing = cacheBox.cache as? L.Cache {
345
+ cache = existing
346
+ layout.updateCache(&cache, subviews: subviews)
347
+ } else {
348
+ cache = layout.makeCache(subviews: subviews)
349
+ }
350
+ // The flexibility probes run before the answer so `placeSubviews` sees the
351
+ // cache state `sizeThatFits` left for the real proposal.
352
+ let sizes = _LayoutSizes(
353
+ min: _rounded(layout.sizeThatFits(proposal: .zero, subviews: subviews, cache: &cache)),
354
+ ideal: _rounded(layout.sizeThatFits(proposal: .unspecified, subviews: subviews, cache: &cache)),
355
+ max: _rounded(layout.sizeThatFits(proposal: .infinity, subviews: subviews, cache: &cache))
356
+ )
357
+ let size = layout.sizeThatFits(proposal: proposal, subviews: subviews, cache: &cache)
358
+ let bounds = CGRect(origin: .zero, size: size)
359
+ layout.placeSubviews(in: bounds, proposal: proposal, subviews: subviews, cache: &cache)
360
+ cacheBox.cache = cache
361
+
362
+ let placements = subviews.first?.placements
363
+ let frames = subviews.map { subview -> CGRect in
364
+ if let frame = placements?.frames[subview.index] { return _rounded(frame) }
365
+ let ideal = subview.ideal
366
+ return _rounded(CGRect(x: bounds.midX - ideal.width / 2, y: bounds.midY - ideal.height / 2,
367
+ width: ideal.width, height: ideal.height))
368
+ }
369
+ return _LayoutResult(
370
+ proposal: proposal,
371
+ size: CGSize(width: _round2(size.width), height: _round2(size.height)),
372
+ frames: frames,
373
+ sizes: sizes
374
+ )
375
+ }
376
+
377
+ /// A dimension from an event: a number, the string `"infinity"`, or nothing.
378
+ func _dimension(_ json: _JSON?) -> CGFloat? {
379
+ switch json {
380
+ case .number(let n)?: n
381
+ case .string("infinity")?: .infinity
382
+ default: nil
383
+ }
384
+ }
385
+
386
+ func _dimensionJSON(_ value: CGFloat?) -> _JSON {
387
+ guard let value else { return .null }
388
+ return value == .infinity ? .string("infinity") : .number(_round2(value))
389
+ }
390
+
391
+ /// Rounds to 2 decimals so float noise never flips a snapshot.
392
+ func _round2(_ value: CGFloat) -> CGFloat {
393
+ guard value.isFinite else { return value }
394
+ return (value * 100).rounded() / 100
395
+ }
396
+
397
+ private func _rounded(_ rect: CGRect) -> CGRect {
398
+ CGRect(x: _round2(rect.minX), y: _round2(rect.minY), width: _round2(rect.width), height: _round2(rect.height))
399
+ }
400
+
401
+ private func _rounded(_ size: CGSize) -> CGSize {
402
+ CGSize(width: _round2(size.width), height: _round2(size.height))
403
+ }