Skip to content

Commit ac18bff

Browse files
Implement scene preferences system (#325)
* add scene preferences * use implicit memberwise initializer for `[Scene]PreferenceValues` * make `Commands.overlayed(with:)` `consuming` * fix `AlertScene` * documentation and style fixups --------- Co-authored-by: stackotter <[email protected]>
1 parent 5b3326c commit ac18bff

15 files changed

+433
-624
lines changed

Sources/SwiftCrossUI/App.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ extension App {
122122
let _app = _App(app)
123123
_forceRefresh = {
124124
app.backend.runInMainThread {
125-
_app.forceRefresh()
125+
_app.refreshSceneGraph()
126126
}
127127
}
128128
_app.run()

Sources/SwiftCrossUI/Scenes/AlertScene.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public final class AlertSceneNode: SceneGraphNode {
5353
_ newScene: AlertScene?,
5454
backend: Backend,
5555
environment: EnvironmentValues
56-
) {
56+
) -> SceneUpdateResult {
5757
if let newScene {
5858
self.scene = newScene
5959
}
@@ -77,5 +77,7 @@ public final class AlertSceneNode: SceneGraphNode {
7777
backend.dismissAlert(alert as! Backend.Alert, window: nil)
7878
self.alert = nil
7979
}
80+
81+
return .leafScene()
8082
}
8183
}

Sources/SwiftCrossUI/Scenes/Commands.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,31 @@ public struct Commands: Sendable {
77
self.menus = menus
88
}
99

10-
public func overlayed(with newCommands: Commands) -> Commands {
10+
public consuming func overlayed(with newCommands: Commands) -> Commands {
1111
var newMenusByName: [String: Int] = [:]
1212
for (i, menu) in newCommands.menus.enumerated() {
1313
newMenusByName[menu.name] = i
1414
}
1515

16-
var commands = self
1716
for (i, menu) in menus.enumerated() {
1817
guard let newMenuIndex = newMenusByName[menu.name] else {
1918
continue
2019
}
21-
commands.menus[i] = CommandMenu(
20+
menus[i] = CommandMenu(
2221
name: menu.name,
2322
content: menu.content + newCommands.menus[newMenuIndex].content
2423
)
2524
}
2625

27-
let existingMenuNames = Set(commands.menus.map(\.name))
26+
let existingMenuNames = Set(menus.map(\.name))
2827
for newMenu in newCommands.menus {
2928
guard !existingMenuNames.contains(newMenu.name) else {
3029
continue
3130
}
32-
commands.menus.append(newMenu)
31+
menus.append(newMenu)
3332
}
3433

35-
return commands
34+
return self
3635
}
3736

3837
/// Resolves the menus to a representation used by backends.

Sources/SwiftCrossUI/Scenes/SceneGraphNode.swift renamed to Sources/SwiftCrossUI/Scenes/Graph/SceneGraphNode.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ public protocol SceneGraphNode: AnyObject {
2323
/// - newScene: The new recomputed scene if the update is due to being recomputed.
2424
/// - backend: The backend to use.
2525
/// - environment: The current root-level environment.
26+
/// - Returns: The result of updating the scene.
2627
func update<Backend: AppBackend>(
2728
_ newScene: NodeScene?,
2829
backend: Backend,
2930
environment: EnvironmentValues
30-
)
31+
) -> SceneUpdateResult
3132
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// A scene's preferences. Propagated up the scene hierarchy automatically.
2+
public struct ScenePreferenceValues: Sendable {
3+
/// The default preferences.
4+
public static let `default` = ScenePreferenceValues(commands: .empty)
5+
6+
/// The commands to be shown by the app.
7+
public var commands: Commands
8+
}
9+
10+
extension ScenePreferenceValues {
11+
init(merging children: [ScenePreferenceValues]) {
12+
commands = children.map(\.commands).reduce(.empty) { $0.overlayed(with: $1) }
13+
}
14+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// The result of updating a scene.
2+
public struct SceneUpdateResult: Sendable {
3+
/// The preference values produced by the scene and its children.
4+
public var preferences: ScenePreferenceValues
5+
6+
public init(preferences: ScenePreferenceValues) {
7+
self.preferences = preferences
8+
}
9+
10+
/// Creates an update result by combining the preference values of a scene's
11+
/// children.
12+
public init(childResults: [SceneUpdateResult]) {
13+
preferences = ScenePreferenceValues(merging: childResults.map(\.preferences))
14+
}
15+
16+
/// Creates the layout result of a leaf scene (one with no children and no
17+
/// special preference behaviour). Uses ``ScenePreferenceValues/default``.
18+
public static func leafScene() -> Self {
19+
SceneUpdateResult(preferences: .default)
20+
}
21+
}
Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
extension Scene {
22
public func commands(@CommandsBuilder _ commands: () -> Commands) -> some Scene {
3-
CommandsModifier(content: self, newCommands: commands())
3+
CommandsModifier(content: self, commands: commands())
44
}
55
}
66

@@ -10,23 +10,25 @@ struct CommandsModifier<Content: Scene>: Scene {
1010
var content: Content
1111
var commands: Commands
1212

13-
init(content: Content, newCommands: Commands) {
13+
init(content: Content, commands: Commands) {
1414
self.content = content
15-
self.commands = content.commands.overlayed(with: newCommands)
15+
self.commands = commands
1616
}
1717
}
1818

1919
final class CommandsModifierNode<Content: Scene>: SceneGraphNode {
2020
typealias NodeScene = CommandsModifier<Content>
2121

22+
var commands: Commands
2223
var contentNode: Content.Node
2324

2425
init<Backend: AppBackend>(
2526
from scene: NodeScene,
2627
backend: Backend,
2728
environment: EnvironmentValues
2829
) {
29-
contentNode = Content.Node(
30+
self.commands = scene.commands
31+
self.contentNode = Content.Node(
3032
from: scene.content,
3133
backend: backend,
3234
environment: environment
@@ -37,11 +39,17 @@ final class CommandsModifierNode<Content: Scene>: SceneGraphNode {
3739
_ newScene: NodeScene?,
3840
backend: Backend,
3941
environment: EnvironmentValues
40-
) {
41-
contentNode.update(
42+
) -> SceneUpdateResult {
43+
if let newScene {
44+
self.commands = newScene.commands
45+
}
46+
47+
var result = contentNode.update(
4248
newScene?.content,
4349
backend: backend,
4450
environment: environment
4551
)
52+
result.preferences.commands = result.preferences.commands.overlayed(with: commands)
53+
return result
4654
}
4755
}

Sources/SwiftCrossUI/Scenes/Modifiers/SceneEnvironmentModifier.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ struct SceneEnvironmentModifier<Content: Scene>: Scene {
3636
var content: Content
3737
var modification: (EnvironmentValues) -> EnvironmentValues
3838

39-
var commands: Commands { content.commands }
40-
4139
init(
4240
_ content: Content,
4341
modification: @escaping (EnvironmentValues) -> EnvironmentValues
@@ -59,7 +57,7 @@ final class SceneEnvironmentModifierNode<Content: Scene>: SceneGraphNode {
5957
environment: EnvironmentValues
6058
) {
6159
self.modification = scene.modification
62-
contentNode = Content.Node(
60+
self.contentNode = Content.Node(
6361
from: scene.content,
6462
backend: backend,
6563
environment: modification(environment)
@@ -70,12 +68,12 @@ final class SceneEnvironmentModifierNode<Content: Scene>: SceneGraphNode {
7068
_ newScene: NodeScene?,
7169
backend: Backend,
7270
environment: EnvironmentValues
73-
) {
71+
) -> SceneUpdateResult {
7472
if let newScene {
7573
self.modification = newScene.modification
7674
}
7775

78-
contentNode.update(
76+
return contentNode.update(
7977
newScene?.content,
8078
backend: backend,
8179
environment: modification(environment)

Sources/SwiftCrossUI/Scenes/Scene.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,4 @@
66
public protocol Scene {
77
/// The node type used to manage this scene in the scene graph.
88
associatedtype Node: SceneGraphNode where Node.NodeScene == Self
9-
10-
/// The commands to be propagated up from the scene.
11-
var commands: Commands { get }
129
}

0 commit comments

Comments
 (0)