Skip to content

Commit bcfa2dc

Browse files
authored
Add View.frame support (#312)
* Add FrameLayout * Add FrameLayoutCommon * Fix ViewProposal issue
1 parent 2a5a23f commit bcfa2dc

File tree

6 files changed

+195
-18
lines changed

6 files changed

+195
-18
lines changed

Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,9 @@ extension NSHostingView {
313313
extension NSHostingView: ViewRendererHost {
314314
package func updateEnvironment() {}
315315

316-
package func updateSize() {}
316+
package func updateSize() {
317+
viewGraph.setProposedSize(bounds.size)
318+
}
317319

318320
package func updateSafeArea() {}
319321

Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ extension _UIHostingView: ViewRendererHost {
505505
}
506506

507507
package func updateSize() {
508-
// preconditionFailure("TODO")
508+
viewGraph.setProposedSize(bounds.size)
509509
}
510510

511511
package func updateSafeArea() {

Sources/OpenSwiftUICore/Layout/Edge/Edge.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,19 @@ public enum Edge: Int8, CaseIterable {
2323
self.rawValue = rawValue
2424
}
2525

26-
public static let top: Set = .init(.top) // 1
26+
public static let top: Set = .init(.top)
2727

28-
public static let leading: Set = .init(.leading) // 2
28+
public static let leading: Set = .init(.leading)
2929

30-
public static let bottom: Set = .init(.bottom) // 4
30+
public static let bottom: Set = .init(.bottom)
3131

32-
public static let trailing: Set = .init(.trailing) // 8
32+
public static let trailing: Set = .init(.trailing)
3333

3434
public static let all: Set = [.top, .leading, .bottom, .trailing]
3535

36-
public static let horizontal: Set = [.leading, .trailing] // 0xa
36+
public static let horizontal: Set = [.leading, .trailing]
3737

38-
public static let vertical: Set = [.top, .bottom] // 0x5
38+
public static let vertical: Set = [.top, .bottom]
3939

4040
/// Creates an instance containing just e
4141
public init(_ e: Edge) {
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
//
2+
// FrameLayout.swift
3+
// OpenSwiftUICore
4+
//
5+
// Status: WIP
6+
// ID: 73C64038119BBD0A6D8557B14379A404 (SwiftUICore)
7+
8+
public import Foundation
9+
10+
// MARK: - FrameLayoutCommon [6.4.41]
11+
12+
private protocol FrameLayoutCommon {
13+
var alignment: Alignment { get }
14+
}
15+
16+
extension FrameLayoutCommon {
17+
func commonPlacement(of child: LayoutProxy, in context: PlacementContext, childProposal: _ProposedSize) -> _Placement {
18+
let defaultDimensions = ViewDimensions(guideComputer: LayoutComputer.defaultValue, size: .fixed(context.size))
19+
let dimensions = child.dimensions(in: childProposal)
20+
21+
let horizontalKey = alignment.horizontal.key
22+
let horizontalID = horizontalKey.id
23+
24+
let verticalKey = alignment.vertical.key
25+
let verticalID = verticalKey.id
26+
27+
let horizontalDefaultValue = horizontalID.defaultValue(in: defaultDimensions)
28+
let verticalDefaultValue = verticalID.defaultValue(in: defaultDimensions)
29+
30+
let horizontalAlignmentValue = dimensions[horizontalKey]
31+
let verticalAlignmentValue = dimensions[verticalKey]
32+
33+
let x = horizontalDefaultValue - horizontalAlignmentValue
34+
let y = verticalDefaultValue - verticalAlignmentValue
35+
36+
return _Placement(
37+
proposedSize: childProposal,
38+
anchoring: .topLeading,
39+
at: CGPoint(x: x, y: y)
40+
)
41+
}
42+
}
43+
44+
// MARK: - FrameLayout [6.4.41]
45+
46+
/// A modifier that centers its child in an invisible frame with one or two
47+
/// fixed dimensions.
48+
@frozen
49+
public struct _FrameLayout: UnaryLayout, FrameLayoutCommon {
50+
let width: CGFloat?
51+
52+
let height: CGFloat?
53+
54+
let alignment: Alignment
55+
56+
@usableFromInline
57+
package init(width: CGFloat?, height: CGFloat?, alignment: Alignment) {
58+
if isLinkedOnOrAfter(.v2) {
59+
let isWidthInvalid: Bool = if let width, width < 0 || !width.isFinite { true } else { false }
60+
let isHeightInvalid: Bool = if let width, width < 0 || !width.isFinite { true } else { false }
61+
if isWidthInvalid || isHeightInvalid {
62+
Log.runtimeIssues("Invalid frame dimension (negative or non-finite).")
63+
}
64+
self.width = isWidthInvalid ? nil : width
65+
self.height = isHeightInvalid ? nil : height
66+
self.alignment = alignment
67+
} else {
68+
self.width = width
69+
self.height = height
70+
self.alignment = alignment
71+
}
72+
}
73+
74+
package func sizeThatFits(in proposedSize: _ProposedSize, context: SizeAndSpacingContext, child: LayoutProxy) -> CGSize {
75+
if let width, let height {
76+
return CGSize(width: width, height: height)
77+
}
78+
let size = child.size(in: _ProposedSize(width: width ?? proposedSize.width, height: height ?? proposedSize.height))
79+
return CGSize(width: width ?? size.width, height: height ?? size.height)
80+
}
81+
82+
package func placement(of child: LayoutProxy, in context: PlacementContext) -> _Placement {
83+
var proposal = context.proposedSize
84+
if let width {
85+
proposal.width = width
86+
}
87+
if let height {
88+
proposal.height = height
89+
}
90+
return commonPlacement(of: child, in: context, childProposal: proposal)
91+
}
92+
93+
package func spacing(in context: SizeAndSpacingContext, child: LayoutProxy) -> Spacing {
94+
if _SemanticFeature_v3.isEnabled, !child.requiresSpacingProjection {
95+
var spacing = child.layoutComputer.spacing()
96+
var edges: Edge.Set = []
97+
if height != nil {
98+
edges.formUnion(.vertical)
99+
}
100+
if width != nil {
101+
edges.formUnion(.horizontal)
102+
}
103+
spacing.reset(.init(edges, layoutDirection: context.layoutDirection))
104+
return spacing
105+
} else {
106+
return child.layoutComputer.spacing()
107+
}
108+
}
109+
}
110+
111+
extension View {
112+
/// Positions this view within an invisible frame with the specified size.
113+
///
114+
/// Use this method to specify a fixed size for a view's width, height, or
115+
/// both. If you only specify one of the dimensions, the resulting view
116+
/// assumes this view's sizing behavior in the other dimension.
117+
///
118+
/// For example, the following code lays out an ellipse in a fixed 200 by
119+
/// 100 frame. Because a shape always occupies the space offered to it by
120+
/// the layout system, the first ellipse is 200x100 points. The second
121+
/// ellipse is laid out in a frame with only a fixed height, so it occupies
122+
/// that height, and whatever width the layout system offers to its parent.
123+
///
124+
/// VStack {
125+
/// Ellipse()
126+
/// .fill(Color.purple)
127+
/// .frame(width: 200, height: 100)
128+
/// Ellipse()
129+
/// .fill(Color.blue)
130+
/// .frame(height: 100)
131+
/// }
132+
///
133+
/// ![A screenshot showing the effect of frame size options: a purple
134+
/// ellipse shows the effect of a fixed frame size, while a blue ellipse
135+
/// shows the effect of constraining a view in one
136+
/// dimension.](OpenSwiftUI-View-frame-1.png)
137+
///
138+
/// `The alignment` parameter specifies this view's alignment within the
139+
/// frame.
140+
///
141+
/// Text("Hello world!")
142+
/// .frame(width: 200, height: 30, alignment: .topLeading)
143+
/// .border(Color.gray)
144+
///
145+
/// In the example above, the text is positioned at the top, leading corner
146+
/// of the frame. If the text is taller than the frame, its bounds may
147+
/// extend beyond the bottom of the frame's bounds.
148+
///
149+
/// ![A screenshot showing the effect of frame size options on a text view
150+
/// showing a fixed frame size with a specified
151+
/// alignment.](OpenSwiftUI-View-frame-2.png)
152+
///
153+
/// - Parameters:
154+
/// - width: A fixed width for the resulting view. If `width` is `nil`,
155+
/// the resulting view assumes this view's sizing behavior.
156+
/// - height: A fixed height for the resulting view. If `height` is `nil`,
157+
/// the resulting view assumes this view's sizing behavior.
158+
/// - alignment: The alignment of this view inside the resulting frame.
159+
/// Note that most alignment values have no apparent effect when the
160+
/// size of the frame happens to match that of this view.
161+
///
162+
/// - Returns: A view with fixed dimensions of `width` and `height`, for the
163+
/// parameters that are non-`nil`.
164+
@inlinable
165+
nonisolated
166+
public func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center) -> some View {
167+
return modifier(
168+
_FrameLayout(width: width, height: height, alignment: alignment)
169+
)
170+
}
171+
172+
173+
/// Positions this view within an invisible frame.
174+
///
175+
/// Use ``View/frame(width:height:alignment:)`` or
176+
/// ``View/frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)``
177+
/// instead.
178+
@available(*, deprecated, message: "Please pass one or more parameters.")
179+
@inlinable
180+
nonisolated public func frame() -> some View {
181+
return frame(width: nil, height: nil, alignment: .center)
182+
}
183+
}
184+
185+
// MARK: - FlexFrameLayout [6.4.41] [WIP]

Sources/OpenSwiftUICore/Render/RendererLeafView.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -138,16 +138,6 @@ private struct LeafDisplayList<V>: StatefulRule, CustomStringConvertible where V
138138
version: version
139139
)
140140
item.canonicalize(options: options)
141-
#if _OPENSWIFTUI_SWIFTUI_RENDER
142-
143-
// FIXME: Remove me after Layout system is implemented
144-
#if os(macOS)
145-
item.frame = CGRect(x: 0, y: 0, width: 500, height: 300)
146-
#elseif os(iOS)
147-
item.frame = CGRect(x: 0, y: 100.333, width: 402, height: 739)
148-
#endif
149-
150-
#endif
151141
value = DisplayList(item)
152142
}
153143

0 commit comments

Comments
 (0)