From ffd29a8dd61602b56fd202201958d0f9c1593f90 Mon Sep 17 00:00:00 2001 From: "Kaleb A. Ascevich" Date: Wed, 24 Dec 2025 17:39:10 -0500 Subject: [PATCH 1/5] initial attempt at closing windows using `DismissAction` (not successful) --- Examples/Sources/WindowingExample/WindowingApp.swift | 6 ++++++ Sources/AppKitBackend/AppKitBackend.swift | 4 ++++ Sources/SwiftCrossUI/Backend/AppBackend.swift | 6 ++++++ Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift | 11 ++++++++++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index 4049151923..db08cf5f6d 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -163,6 +163,8 @@ struct WindowingApp: App { @State var title = "My window" @State var resizable = false + @Environment(\.dismiss) private var dismiss + var body: some Scene { WindowGroup(title) { #hotReloadable { @@ -222,6 +224,10 @@ struct WindowingApp: App { #hotReloadable { Text("This a tertiary window!") .padding(10) + + Button("Close window") { + dismiss() + } } } .defaultSize(width: 200, height: 200) diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index 32adf784eb..5ad8a5eab3 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -123,6 +123,10 @@ public final class AppKitBackend: AppBackend { window.makeKeyAndOrderFront(nil) } + public func close(window: Window) { + window.close() + } + public func openExternalURL(_ url: URL) throws { NSWorkspace.shared.open(url) } diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 52dfbab1ad..afeaa926ec 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -167,6 +167,8 @@ public protocol AppBackend: Sendable { /// receives an external URL or file to handle from the desktop environment. /// May be used in other circumstances eventually. func activate(window: Window) + /// Closes a window. + func close(window: Window) /// Sets the application's global menu. Some backends may make use of the host /// platform's global menu bar (such as macOS's menu bar), and others may render their @@ -825,6 +827,10 @@ extension AppBackend { // MARK: Application + public func close(window: Window) { + todo() + } + public func setApplicationMenu(_ submenus: [ResolvedMenu.Submenu]) { todo() } diff --git a/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift b/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift index 717906b484..35caa30ff9 100644 --- a/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift +++ b/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift @@ -29,7 +29,12 @@ public final class WindowGroupNode: SceneGraphNode { viewGraph = ViewGraph( for: scene.body, backend: backend, - environment: environment.with(\.window, window) + environment: environment + .with(\.window, window) + .with(\.dismiss, DismissAction(action: { + print("Closing window \(window)") + backend.close(window: window) + })) ) let rootWidget = viewGraph.rootNode.concreteNode(for: Backend.self).widget @@ -135,6 +140,10 @@ public final class WindowGroupNode: SceneGraphNode { ) } .with(\.window, window) + .with(\.dismiss, DismissAction(action: { + print("Closing window \(window)") + backend.close(window: window) + })) let dryRunResult: ViewUpdateResult? if !windowSizeIsFinal { From d93856bb4bdee02f7cbbbc25538e953c1fc92979 Mon Sep 17 00:00:00 2001 From: "Kaleb A. Ascevich" Date: Sat, 27 Dec 2025 19:04:25 -0500 Subject: [PATCH 2/5] "fix" `DismissAction` for windows (it already worked, I just wasn't using it properly in the example) --- .../WindowingExample/WindowingApp.swift | 24 ++++++++++++------- .../Environment/Actions/DismissAction.swift | 5 ++-- .../SwiftCrossUI/Scenes/WindowGroupNode.swift | 10 +++----- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index db08cf5f6d..594f3b1f2a 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -157,14 +157,27 @@ struct SheetDemo: View { } } +struct TertiaryWindowView: View { + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack { + Text("This a tertiary window!") + .padding(10) + + Button("Close window") { + dismiss() + } + } + } +} + @main @HotReloadable struct WindowingApp: App { @State var title = "My window" @State var resizable = false - @Environment(\.dismiss) private var dismiss - var body: some Scene { WindowGroup(title) { #hotReloadable { @@ -222,12 +235,7 @@ struct WindowingApp: App { WindowGroup("Tertiary window") { #hotReloadable { - Text("This a tertiary window!") - .padding(10) - - Button("Close window") { - dismiss() - } + TertiaryWindowView() } } .defaultSize(width: 200, height: 200) diff --git a/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift b/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift index 1258d67193..3dce8979bc 100644 --- a/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift +++ b/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift @@ -1,7 +1,7 @@ /// An action that dismisses the current presentation context. /// /// Use the `dismiss` environment value to get an instance of this action, -/// then call it to dismiss the current sheet. +/// then call it to dismiss (close) the current window or sheet. /// /// Example usage: /// ```swift @@ -48,7 +48,8 @@ extension EnvironmentValues { /// An action that dismisses the current presentation context. /// /// Use this environment value to get a dismiss action that can be called - /// to dismiss the current sheet, popover, or other presentation. + /// to dismiss (close) the current window, sheet, popover, or other + /// presentation. /// /// Example: /// ```swift diff --git a/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift b/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift index 35caa30ff9..90cfa9b103 100644 --- a/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift +++ b/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift @@ -31,10 +31,9 @@ public final class WindowGroupNode: SceneGraphNode { backend: backend, environment: environment .with(\.window, window) - .with(\.dismiss, DismissAction(action: { - print("Closing window \(window)") + .with(\.dismiss, DismissAction { backend.close(window: window) - })) + }) ) let rootWidget = viewGraph.rootNode.concreteNode(for: Backend.self).widget @@ -140,10 +139,7 @@ public final class WindowGroupNode: SceneGraphNode { ) } .with(\.window, window) - .with(\.dismiss, DismissAction(action: { - print("Closing window \(window)") - backend.close(window: window) - })) + .with(\.dismiss, DismissAction { backend.close(window: window) }) let dryRunResult: ViewUpdateResult? if !windowSizeIsFinal { From 82f863dd9048c757e1d8dcb9e37d4e18000ad5e2 Mon Sep 17 00:00:00 2001 From: "Kaleb A. Ascevich" Date: Sat, 27 Dec 2025 21:06:34 -0500 Subject: [PATCH 3/5] add dedicated `dismissWindow` action --- .../WindowingExample/WindowingApp.swift | 4 +- .../Actions/DismissWindowAction.swift | 38 +++++++++++++++++++ .../Environment/EnvironmentValues.swift | 9 +++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 Sources/SwiftCrossUI/Environment/Actions/DismissWindowAction.swift diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index 594f3b1f2a..7ab8a3855c 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -158,7 +158,7 @@ struct SheetDemo: View { } struct TertiaryWindowView: View { - @Environment(\.dismiss) private var dismiss + @Environment(\.dismissWindow) private var dismissWindow var body: some View { VStack { @@ -166,7 +166,7 @@ struct TertiaryWindowView: View { .padding(10) Button("Close window") { - dismiss() + dismissWindow() } } } diff --git a/Sources/SwiftCrossUI/Environment/Actions/DismissWindowAction.swift b/Sources/SwiftCrossUI/Environment/Actions/DismissWindowAction.swift new file mode 100644 index 0000000000..f320f6b0d6 --- /dev/null +++ b/Sources/SwiftCrossUI/Environment/Actions/DismissWindowAction.swift @@ -0,0 +1,38 @@ +/// An action that closes the current window. +/// +/// Use the `dismissWindow` environment value to get an instance of this action, +/// then call it to close the current window . +/// +/// Example usage: +/// ```swift +/// struct ContentView: View { +/// @Environment(\.dismissWindow) var dismissWindow +/// +/// var body: some View { +/// VStack { +/// Text("Window Content") +/// Button("Close") { +/// dismissWindow() +/// } +/// } +/// } +/// } +/// ``` +@MainActor +public struct DismissWindowAction { + let backend: any AppBackend + let window: MainActorBox + + /// Dismisses the current presentation context. + public func callAsFunction() { + func closeWindow(backend: Backend) { + guard let window = window.value else { + print("warning: dismissWindow() called outside of a window") + return + } + backend.close(window: window as! Backend.Window) + } + + closeWindow(backend: backend) + } +} diff --git a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift index cafe0dea63..a993bc94f9 100644 --- a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift +++ b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift @@ -183,6 +183,15 @@ public struct EnvironmentValues { ) } + /// Closes the current window. + @MainActor + public var dismissWindow: DismissWindowAction { + return DismissWindowAction( + backend: backend, + window: .init(value: window) + ) + } + /// Reveals a file in the system's file manager. This opens /// the file's enclosing directory and highlighting the file. /// From 4d242cc9f2887de8722b713725dec464167e8b07 Mon Sep 17 00:00:00 2001 From: "Kaleb A. Ascevich" Date: Mon, 29 Dec 2025 12:30:22 -0500 Subject: [PATCH 4/5] use default padding in WindowingExample --- Examples/Sources/WindowingExample/WindowingApp.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index 7ab8a3855c..dc8fd02cd6 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -163,12 +163,12 @@ struct TertiaryWindowView: View { var body: some View { VStack { Text("This a tertiary window!") - .padding(10) Button("Close window") { dismissWindow() } } + .padding() } } @@ -227,7 +227,7 @@ struct WindowingApp: App { WindowGroup("Secondary window") { #hotReloadable { Text("This a secondary window!") - .padding(10) + .padding() } } .defaultSize(width: 200, height: 200) From f66eb0174ad7ed68b29a72447c4f447289fa08cc Mon Sep 17 00:00:00 2001 From: "Kaleb A. Ascevich" Date: Mon, 29 Dec 2025 12:32:47 -0500 Subject: [PATCH 5/5] documentation fixups --- .../SwiftCrossUI/Environment/Actions/DismissAction.swift | 4 ++-- .../Environment/Actions/DismissWindowAction.swift | 8 ++++---- Sources/SwiftCrossUI/Environment/EnvironmentValues.swift | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift b/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift index 3dce8979bc..6e0eb6a180 100644 --- a/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift +++ b/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift @@ -1,7 +1,7 @@ /// An action that dismisses the current presentation context. /// /// Use the `dismiss` environment value to get an instance of this action, -/// then call it to dismiss (close) the current window or sheet. +/// then call it to dismiss (close) the enclosing window or sheet. /// /// Example usage: /// ```swift @@ -48,7 +48,7 @@ extension EnvironmentValues { /// An action that dismisses the current presentation context. /// /// Use this environment value to get a dismiss action that can be called - /// to dismiss (close) the current window, sheet, popover, or other + /// to dismiss (close) the enclosing window, sheet, popover, or other /// presentation. /// /// Example: diff --git a/Sources/SwiftCrossUI/Environment/Actions/DismissWindowAction.swift b/Sources/SwiftCrossUI/Environment/Actions/DismissWindowAction.swift index f320f6b0d6..0129eab7e0 100644 --- a/Sources/SwiftCrossUI/Environment/Actions/DismissWindowAction.swift +++ b/Sources/SwiftCrossUI/Environment/Actions/DismissWindowAction.swift @@ -1,7 +1,7 @@ -/// An action that closes the current window. +/// An action that closes the enclosing window. /// /// Use the `dismissWindow` environment value to get an instance of this action, -/// then call it to close the current window . +/// then call it to close the enclosing window. /// /// Example usage: /// ```swift @@ -23,11 +23,11 @@ public struct DismissWindowAction { let backend: any AppBackend let window: MainActorBox - /// Dismisses the current presentation context. + /// Closes the enclosing window. public func callAsFunction() { func closeWindow(backend: Backend) { guard let window = window.value else { - print("warning: dismissWindow() called outside of a window") + print("warning: dismissWindow() accessed outside of a window's scope") return } backend.close(window: window as! Backend.Window) diff --git a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift index a993bc94f9..0e766b13f0 100644 --- a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift +++ b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift @@ -183,7 +183,7 @@ public struct EnvironmentValues { ) } - /// Closes the current window. + /// Closes the enclosing window. @MainActor public var dismissWindow: DismissWindowAction { return DismissWindowAction(