Skip to content

Commit e5f3cbe

Browse files
committed
Use new SizeProposal type instead of SIMD2<Int> in View.computeLayout
This makes it possible to request ideal sizing without calling computeLayout twice.
1 parent e7a66ff commit e5f3cbe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+514
-331
lines changed

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ public final class AppKitBackend: AppBackend {
3535
scrollerStyle: NSScroller.preferredScrollerStyle
3636
).rounded(.awayFromZero)
3737
)
38-
print(result)
3938
return result
4039
}
4140

Sources/AppKitBackend/NSViewRepresentable.swift

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -142,22 +142,27 @@ extension View where Self: NSViewRepresentable {
142142
public func computeLayout<Backend: AppBackend>(
143143
_ widget: Backend.Widget,
144144
children: any ViewGraphNodeChildren,
145-
proposedSize: SIMD2<Int>,
145+
proposedSize: SizeProposal,
146146
environment: EnvironmentValues,
147-
backend: Backend
147+
backend _: Backend
148148
) -> ViewLayoutResult {
149-
guard backend is AppKitBackend else {
150-
fatalError("NSViewRepresentable updated by \(Backend.self)")
151-
}
152-
149+
// We update the underlying view in computeLayout because we don't expect
150+
// UIViews to have their layout computations decoupled from their content.
153151
let representingWidget = widget as! RepresentingWidget<Self>
154152
representingWidget.update(with: environment)
155153

156-
let size = representingWidget.representable.determineViewSize(
157-
for: proposedSize,
154+
// TODO: Pass size proposal through to XRepresentable views
155+
var size = representingWidget.representable.determineViewSize(
156+
for: proposedSize.evaluated(withIdealSize: SIMD2(10, 10)),
158157
nsView: representingWidget.subview,
159158
context: representingWidget.context!
160159
)
160+
if proposedSize.width == nil {
161+
size.size.x = size.idealWidthForProposedHeight
162+
}
163+
if proposedSize.height == nil {
164+
size.size.y = size.idealHeightForProposedWidth
165+
}
161166

162167
return ViewLayoutResult.leafView(size: size)
163168
}

Sources/SwiftCrossUI/Environment/EnvironmentValues.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,12 @@ public struct EnvironmentValues {
7979
backend.activate(window: window as! Backend.Window)
8080
}
8181
activate(with: backend)
82-
print("Activated")
8382
}
8483

8584
/// The backend's representation of the window that the current view is
8685
/// in, if any. This is a very internal detail that should never get
8786
/// exposed to users.
88-
package var window: Any?
87+
public var window: Any?
8988
/// The backend in use. Mustn't change throughout the app's lifecycle.
9089
let backend: any AppBackend
9190

@@ -147,7 +146,7 @@ public struct EnvironmentValues {
147146
}
148147

149148
/// Creates the default environment.
150-
init<Backend: AppBackend>(backend: Backend) {
149+
public init<Backend: AppBackend>(backend: Backend) {
151150
self.backend = backend
152151

153152
onResize = { _ in }

Sources/SwiftCrossUI/Layout/LayoutSystem.swift

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import Foundation
2+
13
public enum LayoutSystem {
24
static func width(forHeight height: Int, aspectRatio: Double) -> Int {
35
roundSize(Double(height) * aspectRatio)
@@ -11,6 +13,18 @@ public enum LayoutSystem {
1113
Int(size.rounded(.towardZero))
1214
}
1315

16+
/// Clamps a value to a range given optional lower and upper bounds.
17+
static func clamp<T: Comparable>(_ value: T, minimum: T? = nil, maximum: T? = nil) -> T {
18+
var value = value
19+
if let minimum {
20+
value = max(value, minimum)
21+
}
22+
if let maximum {
23+
value = min(value, maximum)
24+
}
25+
return value
26+
}
27+
1428
static func aspectRatio(of frame: SIMD2<Double>) -> Double {
1529
if frame.x == 0 || frame.y == 0 {
1630
// Even though we could technically compute an aspect ratio when the
@@ -47,14 +61,14 @@ public enum LayoutSystem {
4761
public struct LayoutableChild {
4862
private var computeLayout:
4963
(
50-
_ proposedSize: SIMD2<Int>,
64+
_ proposedSize: SizeProposal,
5165
_ environment: EnvironmentValues
5266
) -> ViewLayoutResult
5367
private var _commit: () -> ViewLayoutResult
5468
var tag: String?
5569

5670
public init(
57-
computeLayout: @escaping (SIMD2<Int>, EnvironmentValues) -> ViewLayoutResult,
71+
computeLayout: @escaping (SizeProposal, EnvironmentValues) -> ViewLayoutResult,
5872
commit: @escaping () -> ViewLayoutResult,
5973
tag: String? = nil
6074
) {
@@ -64,7 +78,7 @@ public enum LayoutSystem {
6478
}
6579

6680
public func computeLayout(
67-
proposedSize: SIMD2<Int>,
81+
proposedSize: SizeProposal,
6882
environment: EnvironmentValues,
6983
dryRun: Bool = false
7084
) -> ViewLayoutResult {
@@ -84,7 +98,7 @@ public enum LayoutSystem {
8498
public static func computeStackLayout<Backend: AppBackend>(
8599
container: Backend.Widget,
86100
children: [LayoutableChild],
87-
proposedSize: SIMD2<Int>,
101+
proposedSize: SizeProposal,
88102
environment: EnvironmentValues,
89103
backend: Backend,
90104
inheritStackLayoutParticipation: Bool = false
@@ -103,7 +117,7 @@ public enum LayoutSystem {
103117
var isHidden = [Bool](repeating: false, count: children.count)
104118
let flexibilities = children.enumerated().map { i, child in
105119
let result = child.computeLayout(
106-
proposedSize: proposedSize,
120+
proposedSize: .ideal,
107121
environment: environment
108122
)
109123
isHidden[i] = !result.participatesInStackLayouts
@@ -118,11 +132,19 @@ public enum LayoutSystem {
118132
!hidden
119133
}.count
120134
let totalSpacing = max(visibleChildrenCount - 1, 0) * spacing
121-
let sortedChildren = zip(children.enumerated(), flexibilities)
122-
.sorted { first, second in
123-
first.1 <= second.1
124-
}
125-
.map(\.0)
135+
136+
let sortedChildren: [(offset: Int, element: LayoutSystem.LayoutableChild)]
137+
if orientation == .vertical && proposedSize.height == nil
138+
|| orientation == .horizontal && proposedSize.width == nil
139+
{
140+
sortedChildren = Array(children.enumerated())
141+
} else {
142+
sortedChildren = zip(children.enumerated(), flexibilities)
143+
.sorted { first, second in
144+
first.1 <= second.1
145+
}
146+
.map(\.0)
147+
}
126148

127149
var spaceUsedAlongStackAxis = 0
128150
var childrenRemaining = visibleChildrenCount
@@ -149,25 +171,35 @@ public enum LayoutSystem {
149171
continue
150172
}
151173

152-
let proposedWidth: Double
153-
let proposedHeight: Double
174+
let proposedWidth: Double?
175+
let proposedHeight: Double?
154176
switch orientation {
155177
case .horizontal:
156-
proposedWidth =
157-
Double(max(proposedSize.x - spaceUsedAlongStackAxis - totalSpacing, 0))
158-
/ Double(childrenRemaining)
159-
proposedHeight = Double(proposedSize.y)
178+
if let parentProposedWidth = proposedSize.width {
179+
proposedWidth = Double(max(
180+
parentProposedWidth - spaceUsedAlongStackAxis - totalSpacing,
181+
0
182+
)) / Double(childrenRemaining)
183+
} else {
184+
proposedWidth = nil
185+
}
186+
proposedHeight = proposedSize.height.map(Double.init)
160187
case .vertical:
161-
proposedHeight =
162-
Double(max(proposedSize.y - spaceUsedAlongStackAxis - totalSpacing, 0))
163-
/ Double(childrenRemaining)
164-
proposedWidth = Double(proposedSize.x)
188+
if let parentProposedHeight = proposedSize.height {
189+
proposedHeight = Double(max(
190+
parentProposedHeight - spaceUsedAlongStackAxis - totalSpacing,
191+
0
192+
)) / Double(childrenRemaining)
193+
} else {
194+
proposedHeight = nil
195+
}
196+
proposedWidth = proposedSize.width.map(Double.init)
165197
}
166198

167199
let childResult = child.computeLayout(
168-
proposedSize: SIMD2<Int>(
169-
Int(proposedWidth.rounded(.towardZero)),
170-
Int(proposedHeight.rounded(.towardZero))
200+
proposedSize: SizeProposal(
201+
proposedWidth.map { Int($0.rounded(.towardZero)) },
202+
proposedHeight.map { Int($0.rounded(.towardZero)) }
171203
),
172204
environment: environment
173205
)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/// A proposed view size. Uses `nil` to indicate when the ideal size should
2+
/// be used along a given axis.
3+
public struct SizeProposal: Hashable {
4+
/// An empty proposal.
5+
public static let zero = Self(0, 0)
6+
7+
/// A proposal for the view to take on its ideal size.
8+
static let ideal = Self(nil, nil)
9+
10+
/// The proposed width. If `nil`, the view should take on its ideal
11+
/// width for the proposed height.
12+
public var width: Int?
13+
/// The proposed height. If `nil`, the view should take on its ideal
14+
/// height for the proposed width.
15+
public var height: Int?
16+
17+
/// The proposal as a concrete size if both dimensions are non-nil,
18+
/// otherwise nil.
19+
var concrete: SIMD2<Int>? {
20+
guard let width, let height else {
21+
return nil
22+
}
23+
return SIMD2(width, height)
24+
}
25+
26+
/// Creates a new size proposal. Use `nil` for dimensions that the view
27+
/// should take on its ideal size along.
28+
public init(_ width: Int?, _ height: Int?) {
29+
self.width = width
30+
self.height = height
31+
}
32+
33+
/// Creates a new size proposal from a concrete size.
34+
public init(_ size: SIMD2<Int>) {
35+
self.width = size.x
36+
self.height = size.y
37+
}
38+
39+
/// Evaluates the size proposal with a view's ideal size.
40+
package func evaluated(withIdealSize idealSize: SIMD2<Int>) -> SIMD2<Int> {
41+
SIMD2(
42+
width ?? idealSize.x,
43+
height ?? idealSize.y
44+
)
45+
}
46+
}

Sources/SwiftCrossUI/State/State.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ public struct State<Value>: DynamicProperty, StateProperty {
9191
// to protocol Optional doesn't conform to can still succeed when the value
9292
// is `.some` and the wrapped type conforms to the protocol.
9393
if Value.self as? ObservableObject.Type != nil,
94-
let initialValue = initialValue as? ObservableObject {
94+
let initialValue = initialValue as? ObservableObject
95+
{
9596
storage.box.downstreamObservation = didChange.link(toUpstream: initialValue.didChange)
9697
} else if let initialValue = initialValue as? OptionalObservableObject,
9798
let innerDidChange = initialValue.didChange

Sources/SwiftCrossUI/Values/Color.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,15 @@ extension Color: ElementaryView {
7171

7272
func computeLayout<Backend: AppBackend>(
7373
_ widget: Backend.Widget,
74-
proposedSize: SIMD2<Int>,
74+
proposedSize: SizeProposal,
7575
environment: EnvironmentValues,
7676
backend: Backend
7777
) -> ViewLayoutResult {
78-
ViewLayoutResult.leafView(
78+
let idealSize = SIMD2(10, 10)
79+
return ViewLayoutResult.leafView(
7980
size: ViewSize(
80-
size: proposedSize,
81-
idealSize: SIMD2(10, 10),
81+
size: proposedSize.evaluated(withIdealSize: idealSize),
82+
idealSize: idealSize,
8283
minimumWidth: 0,
8384
minimumHeight: 0,
8485
maximumWidth: nil,

Sources/SwiftCrossUI/ViewGraph/AnyViewGraphNode.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class AnyViewGraphNode<NodeView: View> {
1515
private var _computeLayoutWithNewView:
1616
(
1717
_ newView: NodeView?,
18-
_ proposedSize: SIMD2<Int>,
18+
_ proposedSize: SizeProposal,
1919
_ environment: EnvironmentValues
2020
) -> ViewLayoutResult
2121
/// The node's type-erased commit method.
@@ -69,7 +69,7 @@ public class AnyViewGraphNode<NodeView: View> {
6969
/// the given size proposal already has a cached result.
7070
public func computeLayout(
7171
with newView: NodeView?,
72-
proposedSize: SIMD2<Int>,
72+
proposedSize: SizeProposal,
7373
environment: EnvironmentValues
7474
) -> ViewLayoutResult {
7575
_computeLayoutWithNewView(newView, proposedSize, environment)

Sources/SwiftCrossUI/ViewGraph/ErasedViewGraphNode.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public struct ErasedViewGraphNode {
1010
public var computeLayoutWithNewView:
1111
(
1212
_ newView: Any?,
13-
_ proposedSize: SIMD2<Int>,
13+
_ proposedSize: SizeProposal,
1414
_ environment: EnvironmentValues
1515
) -> (viewTypeMatched: Bool, size: ViewLayoutResult)
1616
/// The underlying view graph node's commit method.

Sources/SwiftCrossUI/ViewGraph/ViewGraph.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public class ViewGraph<Root: View> {
6262
if dryRun {
6363
result = rootNode.computeLayout(
6464
with: newView ?? view,
65-
proposedSize: proposedSize,
65+
proposedSize: SizeProposal(proposedSize),
6666
environment: parentEnvironment
6767
)
6868
} else {

0 commit comments

Comments
 (0)