diff --git a/Examples/Sources/WindowingExample/WindowingApp.swift b/Examples/Sources/WindowingExample/WindowingApp.swift index 29056b0605..0958ed2a64 100644 --- a/Examples/Sources/WindowingExample/WindowingApp.swift +++ b/Examples/Sources/WindowingExample/WindowingApp.swift @@ -157,6 +157,21 @@ struct SheetDemo: View { } } +struct TertiaryWindowView: View { + @Environment(\.dismissWindow) private var dismissWindow + + var body: some View { + VStack { + Text("This a tertiary window!") + + Button("Close window") { + dismissWindow() + } + } + .padding() + } +} + @main @HotReloadable struct WindowingApp: App { @@ -214,7 +229,7 @@ struct WindowingApp: App { WindowGroup("Secondary window") { #hotReloadable { Text("This a secondary window!") - .padding(10) + .padding() } } .defaultSize(width: 200, height: 200) @@ -222,8 +237,7 @@ struct WindowingApp: App { WindowGroup("Tertiary window") { #hotReloadable { - Text("This a tertiary window!") - .padding(10) + TertiaryWindowView() } } .defaultSize(width: 200, height: 200) diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index 1d2d2e7487..75dcb84bb8 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 c0209a76e7..81101967a7 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 @@ -833,6 +835,10 @@ extension AppBackend { // MARK: Application + public func close(window: Window) { + todo() + } + public func setApplicationMenu(_ submenus: [ResolvedMenu.Submenu]) { todo() } diff --git a/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift b/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift index e3c3c8529d..02bcb9023e 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 enclosing window or sheet. /// /// Example usage: /// ```swift @@ -47,7 +47,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 enclosing window, sheet, popover, or other + /// presentation. /// /// Example: /// ```swift diff --git a/Sources/SwiftCrossUI/Environment/Actions/DismissWindowAction.swift b/Sources/SwiftCrossUI/Environment/Actions/DismissWindowAction.swift new file mode 100644 index 0000000000..0129eab7e0 --- /dev/null +++ b/Sources/SwiftCrossUI/Environment/Actions/DismissWindowAction.swift @@ -0,0 +1,38 @@ +/// 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 enclosing 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 + + /// Closes the enclosing window. + public func callAsFunction() { + func closeWindow(backend: Backend) { + guard let window = window.value else { + print("warning: dismissWindow() accessed outside of a window's scope") + 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 f9f5f1d2be..0debda5541 100644 --- a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift +++ b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift @@ -192,6 +192,15 @@ public struct EnvironmentValues { ) } + /// Closes the enclosing 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. /// diff --git a/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift b/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift index fad7cf42f5..f13e5cc053 100644 --- a/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift +++ b/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift @@ -29,7 +29,11 @@ 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 { + backend.close(window: window) + }) ) let rootWidget = viewGraph.rootNode.concreteNode(for: Backend.self).widget @@ -166,6 +170,7 @@ public final class WindowGroupNode: SceneGraphNode { ) } .with(\.window, window) + .with(\.dismiss, DismissAction { backend.close(window: window) }) let finalContentResult: ViewLayoutResult if scene.resizability.isResizable {