Skip to content

Commit 57a53ed

Browse files
committed
Debug runaway window update recursion, impl menu ForEach support
ForEach support in MenuItemsBuilder currently requires an explicit type annotation. The only way I see to get around that is to make menus just use ViewBuilder, but I really don't want to do that because then we need to support state in menus, and things start getting a bit dodgy and approximate (e.g. users of scui would have no way of knowing which view types work in menus and which don't).
1 parent 60c2c3c commit 57a53ed

File tree

7 files changed

+196
-93
lines changed

7 files changed

+196
-93
lines changed

Sources/GtkBackend/GtkBackend.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,7 @@ public final class GtkBackend: AppBackend {
807807
configure: { chooser in
808808
chooser.selectMultiple = openDialogOptions.allowMultipleSelections
809809
},
810-
window: window,
810+
window: window ?? windows[0],
811811
resultHandler: handleResult
812812
)
813813
}
@@ -826,7 +826,7 @@ public final class GtkBackend: AppBackend {
826826
chooser.setCurrentName(defaultFileName)
827827
}
828828
},
829-
window: window
829+
window: window ?? windows[0]
830830
) { result in
831831
switch result {
832832
case .success(let urls):

Sources/SwiftCrossUI/Builders/MenuItemsBuilder.swift

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,45 @@ public struct MenuItemsBuilder {
1717
first.items
1818
}
1919

20+
public static func buildPartialBlock<Items: Collection>(
21+
first: ForEach<Items, [MenuItem]>
22+
) -> [MenuItem] {
23+
first.elements.map(first.child).flatMap { $0 }
24+
}
25+
2026
public static func buildPartialBlock(
2127
accumulated: [MenuItem],
2228
next: Button
2329
) -> [MenuItem] {
24-
accumulated + [.button(next)]
30+
accumulated + buildPartialBlock(first: next)
2531
}
2632

2733
public static func buildPartialBlock(
2834
accumulated: [MenuItem],
2935
next: Text
3036
) -> [MenuItem] {
31-
accumulated + [.text(next)]
37+
accumulated + buildPartialBlock(first: next)
3238
}
3339

3440
public static func buildPartialBlock(
3541
accumulated: [MenuItem],
3642
next: Menu
3743
) -> [MenuItem] {
38-
accumulated + [.submenu(next)]
44+
accumulated + buildPartialBlock(first: next)
3945
}
4046

4147
public static func buildPartialBlock(
4248
accumulated: [MenuItem],
4349
next: Block
4450
) -> [MenuItem] {
45-
accumulated + next.items
51+
accumulated + buildPartialBlock(first: next)
52+
}
53+
54+
public static func buildPartialBlock<Items: Collection>(
55+
accumulated: [MenuItem],
56+
next: ForEach<Items, [MenuItem]>
57+
) -> [MenuItem] {
58+
accumulated + buildPartialBlock(first: next)
4659
}
4760

4861
public static func buildOptional(_ component: [MenuItem]?) -> Block {

Sources/SwiftCrossUI/LayoutSystem.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@ public enum LayoutSystem {
66
_ environment: EnvironmentValues,
77
_ dryRun: Bool
88
) -> ViewSize
9+
var tag: String?
910

10-
public init(update: @escaping (SIMD2<Int>, EnvironmentValues, Bool) -> ViewSize) {
11+
public init(
12+
update: @escaping (SIMD2<Int>, EnvironmentValues, Bool) -> ViewSize,
13+
tag: String? = nil
14+
) {
1115
self.update = update
16+
self.tag = tag
1217
}
1318

1419
public func update(
@@ -107,7 +112,12 @@ public enum LayoutSystem {
107112
dryRun: dryRun
108113
)
109114
if size.size != .zero || size.participateInStackLayoutsWhenEmpty {
110-
print("warning: Hidden view became visible on second update. Layout may break.")
115+
print(
116+
"""
117+
warning: Hidden view became visible on second update. \
118+
Layout may break. View: \(child.tag ?? "<unknown type>")
119+
"""
120+
)
111121
}
112122
renderedChildren[index] = .hidden
113123
continue

Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public final class WindowGroupNode<Content: View>: SceneGraphNode {
4848
guard let self else {
4949
return
5050
}
51+
print("Resized. newSize: \(newSize)")
5152
_ = self.update(
5253
scene,
5354
proposedWindowSize: newSize,
@@ -80,7 +81,8 @@ public final class WindowGroupNode<Content: View>: SceneGraphNode {
8081
_ newScene: WindowGroup<Content>?,
8182
proposedWindowSize: SIMD2<Int>,
8283
backend: Backend,
83-
environment: EnvironmentValues
84+
environment: EnvironmentValues,
85+
windowSizeIsFinal: Bool = false
8486
) -> ViewSize {
8587
guard let window = window as? Backend.Window else {
8688
fatalError("Scene updated with a backend incompatible with the window it was given")
@@ -121,33 +123,42 @@ public final class WindowGroupNode<Content: View>: SceneGraphNode {
121123
with: newScene?.body,
122124
proposedSize: proposedWindowSize,
123125
environment: environment,
124-
dryRun: true
125-
)
126-
127-
let newWindowSize = computeNewWindowSize(
128-
currentProposedSize: proposedWindowSize,
129-
backend: backend,
130-
contentSize: contentSize,
131-
environment: environment
126+
dryRun: windowSizeIsFinal
132127
)
133128

134-
// Restart the window update if the content has caused the window to
135-
// change size.
136-
if let newWindowSize {
137-
return update(
138-
scene,
139-
proposedWindowSize: newWindowSize,
129+
if !windowSizeIsFinal {
130+
let newWindowSize = computeNewWindowSize(
131+
currentProposedSize: proposedWindowSize,
140132
backend: backend,
133+
contentSize: contentSize,
141134
environment: environment
142135
)
136+
137+
// Restart the window update if the content has caused the window to
138+
// change size. To avoid infinite recursion, we take the view's word
139+
// and assume that it will take on the minimum/maximum size it claimed.
140+
if let newWindowSize {
141+
return update(
142+
scene,
143+
proposedWindowSize: newWindowSize,
144+
backend: backend,
145+
environment: environment,
146+
windowSizeIsFinal: true
147+
)
148+
}
143149
}
144150

145-
let finalContentSize = viewGraph.update(
146-
with: newScene?.body,
147-
proposedSize: proposedWindowSize,
148-
environment: environment,
149-
dryRun: false
150-
)
151+
let finalContentSize: ViewSize
152+
if windowSizeIsFinal {
153+
finalContentSize = contentSize
154+
} else {
155+
finalContentSize = viewGraph.update(
156+
with: newScene?.body,
157+
proposedSize: proposedWindowSize,
158+
environment: environment,
159+
dryRun: false
160+
)
161+
}
151162

152163
// The Gtk 3 backend has some broken sizing code that can't really be
153164
// fixed due to the design of Gtk 3. Our layout system underestimates

Sources/SwiftCrossUI/Views/ForEach.swift

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
11
/// A view that displays a variable amount of children.
2-
public struct ForEach<
3-
Items: Collection,
4-
Child: View
5-
>: TypeSafeView, View where Items.Index == Int {
6-
typealias Children = ForEachViewChildren<Items, Child>
7-
8-
public var body = EmptyView()
9-
2+
public struct ForEach<Items: Collection, Child> where Items.Index == Int {
103
/// A variable-length collection of elements to display.
114
var elements: Items
125
/// A method to display the elements as views.
136
var child: (Items.Element) -> Child
7+
}
8+
9+
extension ForEach where Child == [MenuItem] {
10+
/// Creates a view that creates child views on demand based on a collection of data.
11+
@_disfavoredOverload
12+
public init(
13+
_ elements: Items,
14+
@MenuItemsBuilder _ child: @escaping (Items.Element) -> [MenuItem]
15+
) {
16+
self.elements = elements
17+
self.child = child
18+
}
19+
}
20+
21+
extension ForEach: TypeSafeView, View where Child: View {
22+
typealias Children = ForEachViewChildren<Items, Child>
23+
24+
public var body: EmptyView {
25+
return EmptyView()
26+
}
1427

1528
/// Creates a view that creates child views on demand based on a collection of data.
1629
public init(

0 commit comments

Comments
 (0)