Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
ffd29a8
initial attempt at closing windows using `DismissAction` (not success…
Dec 24, 2025
d93856b
"fix" `DismissAction` for windows (it already worked, I just wasn't u…
kaascevich Dec 28, 2025
82f863d
add dedicated `dismissWindow` action
kaascevich Dec 28, 2025
0cb2f33
Merge branch 'stackotter:main' into window-dismissal
kaascevich Dec 28, 2025
4d242cc
use default padding in WindowingExample
kaascevich Dec 29, 2025
f66eb01
documentation fixups
kaascevich Dec 29, 2025
28a45b4
Merge branch 'stackotter:main' into window-dismissal
kaascevich Dec 31, 2025
3de204e
initial attempt at closing windows using `DismissAction` (not success…
Dec 24, 2025
947dd46
"fix" `DismissAction` for windows (it already worked, I just wasn't u…
kaascevich Dec 28, 2025
7dcc10a
add `Window`, `WindowNode`, and `windowOpenFunctionsByID` preference
kaascevich Dec 28, 2025
db88876
add `OpenWindowAction`, update WindowingExample to use it
kaascevich Dec 28, 2025
d6ca1f8
move `windowOpenFunctionsByID` to a global for now, finish implementi…
kaascevich Dec 28, 2025
94f51b4
factor out `Window`/`WindowNode` and `WindowGroup`/`WindowGroupNode` …
kaascevich Dec 28, 2025
4116bff
add `defaultLaunchBehavior(_:)` scene modifier
kaascevich Dec 28, 2025
dd75f60
don't silently fail if `openWindow(id:)` is called with an invalid ID
kaascevich Dec 30, 2025
ff98962
Merge branch 'window-dismissal'
kaascevich Jan 2, 2026
baa6d81
fix window closing and implement multi-window support for `WindowGroup`
kaascevich Jan 2, 2026
de1f51e
remove use of `isolated deinit` since that's a 6.2 feature
kaascevich Jan 2, 2026
159b83e
make `WindowInfo` a `struct` like all good types should be
kaascevich Jan 2, 2026
39e9674
Merge branch 'stackotter:main' into window-scene
kaascevich Jan 2, 2026
e756c39
tweak `setCloseHandler(ofWindow:to:)` docs
kaascevich Jan 2, 2026
e9eb32d
add `supportsMultipleWindows` environment value
kaascevich Jan 2, 2026
924b4b7
Merge branch 'stackotter:main' into window-scene
kaascevich Jan 2, 2026
47e2c38
replace `print` calls with `logger.warning`
kaascevich Jan 2, 2026
b8d6479
stub `close(window:)` and `setCloseHandler(ofWindow:to:)` in UIKitBac…
kaascevich Jan 2, 2026
357732f
use debug log level in debug mode
kaascevich Jan 2, 2026
c4742c4
implement `close(window:)` and `setCloseHandler(ofWindow:to:)` for Gt…
kaascevich Jan 2, 2026
03c1654
(maybe) fix weirdness caused by returning from `deinit` before `onDei…
kaascevich Jan 2, 2026
32935f4
add `AnyObject` constraints to `AppBackend` associated types
kaascevich Jan 3, 2026
4d5417e
fix window opening
kaascevich Jan 3, 2026
4e88f70
AppKitBackend: set `isReleasedWhenClosed` to `false` for all windows …
kaascevich Jan 3, 2026
6041c82
refactor window closing logic, fix it for GtkBackend
kaascevich Jan 3, 2026
8b95c21
"implement" `close(window:)` and `setResizeHandler(ofWindow:to:)` for…
kaascevich Jan 3, 2026
43294cc
add window closing support to Gtk3Backend
kaascevich Jan 3, 2026
e1d8524
remove `AnyObject` constraint from `Widget`, `Alert`, and `Sheet` ass…
kaascevich Jan 3, 2026
f1dd76d
add `SceneEnvironmentModifier`
kaascevich Jan 3, 2026
ed5e248
add `AlertScene`
kaascevich Jan 3, 2026
3d27222
Revert "add `AlertScene`"
kaascevich Jan 3, 2026
6429499
Revert "add `SceneEnvironmentModifier`"
kaascevich Jan 3, 2026
5095c05
un-`unowned` `WindowReference`'s reference to the window object
kaascevich Jan 4, 2026
1b2cf79
add window closing support to WinUIBackend
kaascevich Jan 4, 2026
4c1a400
fix name of DummyBackend's `setCloseHandler(ofWindow:to:)`
kaascevich Jan 4, 2026
9df1051
reformat WinUIBackend.swift somewhat for consistency
kaascevich Jan 4, 2026
c0288d0
Merge remote-tracking branch 'origin/main' into window-scene
kaascevich Jan 5, 2026
2ecaa36
small windowing refactors
kaascevich Jan 5, 2026
99754c1
Merge branch 'stackotter:main' into window-scene
kaascevich Jan 6, 2026
f8ba3c5
Merge branch 'main' into window-scene
kaascevich Jan 6, 2026
f23edac
make `WindowReference` properties `let`s when possble
kaascevich Jan 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 68 additions & 5 deletions Examples/Sources/WindowingExample/WindowingApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,56 @@ struct SheetDemo: View {
}
}

struct OpenWindowDemo: View {
@Environment(\.openWindow) private var openWindow
@Environment(\.supportsMultipleWindows) private var supportsMultipleWindows

var body: some View {
Text("Backend supports multi-window: \(supportsMultipleWindows)")

Button("Open singleton window") {
openWindow(id: "singleton-window")
}
Button("Open new tertiary window instance") {
openWindow(id: "tertiary-window")
}
}
}

struct TertiaryWindowView: View {
@Environment(\.dismissWindow) private var dismissWindow
@Environment(\.openWindow) private var openWindow

var body: some View {
VStack {
Text("This a tertiary window!")

Button("Close window") {
dismissWindow()
}
Button("Open new instance") {
openWindow(id: "tertiary-window")
}
}
.padding()
}
}

struct SingletonWindowView: View {
@Environment(\.dismissWindow) private var dismissWindow

var body: some View {
VStack {
Text("This a singleton window!")

Button("Close window") {
dismissWindow()
}
}
.padding()
}
}

@main
@HotReloadable
struct WindowingApp: App {
Expand Down Expand Up @@ -194,6 +244,10 @@ struct WindowingApp: App {
Divider()

SheetDemo()

Divider()

OpenWindowDemo()
.padding(.bottom, 20)
}
.padding(20)
Expand All @@ -213,20 +267,29 @@ struct WindowingApp: App {
}
}
}

#if !(os(iOS) || os(tvOS) || os(Windows))
WindowGroup("Secondary window") {
WindowGroup("Secondary window", id: "secondary-window") {
#hotReloadable {
Text("This a secondary window!")
.padding(10)
.padding()
}
}
.defaultSize(width: 200, height: 200)
.windowResizability(.contentMinSize)

WindowGroup("Tertiary window (hidden)", id: "tertiary-window") {
#hotReloadable {
TertiaryWindowView()
}
}
.defaultSize(width: 200, height: 200)
.windowResizability(.contentMinSize)
.defaultLaunchBehavior(.suppressed)

WindowGroup("Tertiary window") {
Window("Singleton window", id: "singleton-window") {
#hotReloadable {
Text("This a tertiary window!")
.padding(10)
SingletonWindowView()
}
}
.defaultSize(width: 200, height: 200)
Expand Down
30 changes: 28 additions & 2 deletions Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public final class AppKitBackend: AppBackend {
public let requiresImageUpdateOnScaleFactorChange = false
public let menuImplementationStyle = MenuImplementationStyle.dynamicPopover
public let canRevealFiles = true
public let supportsMultipleWindows = true
public let deviceClass = DeviceClass.desktop

public var scrollBarWidth: Int {
Expand Down Expand Up @@ -68,6 +69,11 @@ public final class AppKitBackend: AppBackend {
)
window.delegate = window.customDelegate

// NB: If this isn't set, AppKit will crash within -[NSApplication run]
// the *second* time `openWindow` is called. I have absolutely no idea
// why.
window.isReleasedWhenClosed = false

return window
}

Expand Down Expand Up @@ -96,7 +102,7 @@ public final class AppKitBackend: AppBackend {
ofWindow window: Window,
to action: @escaping (SIMD2<Int>) -> Void
) {
window.customDelegate.setHandler(action)
window.customDelegate.setResizeHandler(action)
}

public func setTitle(ofWindow window: Window, to title: String) {
Expand All @@ -123,6 +129,17 @@ public final class AppKitBackend: AppBackend {
window.makeKeyAndOrderFront(nil)
}

public func close(window: Window) {
window.close()
}

public func setCloseHandler(
ofWindow window: Window,
to action: @escaping () -> Void
) {
window.customDelegate.setCloseHandler(action)
}

public func openExternalURL(_ url: URL) throws {
NSWorkspace.shared.open(url)
}
Expand Down Expand Up @@ -2253,11 +2270,16 @@ public class NSCustomWindow: NSWindow {

class Delegate: NSObject, NSWindowDelegate {
var resizeHandler: ((SIMD2<Int>) -> Void)?
var closeHandler: (() -> Void)?

func setHandler(_ resizeHandler: @escaping (SIMD2<Int>) -> Void) {
func setResizeHandler(_ resizeHandler: @escaping (SIMD2<Int>) -> Void) {
self.resizeHandler = resizeHandler
}

func setCloseHandler(_ closeHandler: @escaping () -> Void) {
self.closeHandler = closeHandler
}

func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize {
guard let resizeHandler else {
return frameSize
Expand All @@ -2279,6 +2301,10 @@ public class NSCustomWindow: NSWindow {
return frameSize
}

func windowWillClose(_ notification: Notification) {
closeHandler?()
}

func windowWillReturnUndoManager(_ window: NSWindow) -> UndoManager? {
(window as! NSCustomWindow).persistentUndoManager
}
Expand Down
10 changes: 10 additions & 0 deletions Sources/DummyBackend/DummyBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public final class DummyBackend: AppBackend {
public var resizable = true
public var content: Widget?
public var resizeHandler: ((SIMD2<Int>) -> Void)?
public var closeHandler: (() -> Void)?

public init(defaultSize: SIMD2<Int>?) {
size = defaultSize ?? Self.defaultSize
Expand Down Expand Up @@ -247,6 +248,7 @@ public final class DummyBackend: AppBackend {
public var menuImplementationStyle = MenuImplementationStyle.dynamicPopover
public var deviceClass = DeviceClass.desktop
public var canRevealFiles = false
public let supportsMultipleWindows = true

public var incomingURLHandler: ((URL) -> Void)?

Expand Down Expand Up @@ -297,6 +299,14 @@ public final class DummyBackend: AppBackend {

public func activate(window: Window) {}

public func close(window: Window) {
window.closeHandler?()
}

public func setCloseHandler(ofWindow window: Window, to action: @escaping () -> Void) {
window.closeHandler = action
}

public func runInMainThread(action: @escaping @MainActor () -> Void) {
DispatchQueue.main.async {
action()
Expand Down
32 changes: 21 additions & 11 deletions Sources/Gtk/Widgets/Window.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,10 @@ open class Window: Widget {

public func present() {
gtk_window_present(castedPointer())
}

addSignal(name: "close-request") { [weak self] () in
guard let self else { return }
self.onCloseRequest?(self)
}

addSignal(name: "destroy") { [weak self] () in
guard let self else { return }
self.onDestroy?(self)
}
public func close() {
gtk_window_close(castedPointer())
}

public func setEscapeKeyPressedHandler(to handler: (() -> Void)?) {
Expand All @@ -111,8 +105,24 @@ open class Window: Widget {

private var escapeKeyEventController: EventControllerKey?

public var onCloseRequest: ((Window) -> Void)?
public var onDestroy: ((Window) -> Void)?
public var onCloseRequest: ((Window) -> Void)? {
didSet {
addSignal(name: "close-request") { [weak self] () in
guard let self else { return }
self.onCloseRequest?(self)
}
}
}

public var onDestroy: ((Window) -> Void)? {
didSet {
addSignal(name: "destroy") { [weak self] () in
guard let self else { return }
self.onDestroy?(self)
}
}
}

public var escapeKeyPressed: (() -> Void)?
}

Expand Down
20 changes: 20 additions & 0 deletions Sources/Gtk3/Widgets/Window.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ open class Window: Bin {
gtk_window_present(castedPointer())
}

public func close() {
gtk_window_close(castedPointer())
}

public func setMinimumSize(to minimumSize: Size) {
gtk_widget_set_size_request(
castedPointer(),
Expand All @@ -72,4 +76,20 @@ open class Window: Bin {
public func setPosition(to position: WindowPosition) {
gtk_window_set_position(castedPointer(), position.toGtk())
}

public var onCloseRequest: ((Window) -> Void)? {
didSet {
let handler:
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
{ _, value1, data in
SignalBox1<OpaquePointer>.run(data, value1)
}

addSignal(name: "delete-event", handler: gCallback(handler)) {
[weak self] (_: OpaquePointer) in
guard let self else { return }
self.onCloseRequest?(self)
}
}
}
}
20 changes: 20 additions & 0 deletions Sources/Gtk3Backend/Gtk3Backend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public final class Gtk3Backend: AppBackend {
public let requiresImageUpdateOnScaleFactorChange = true
public let menuImplementationStyle = MenuImplementationStyle.dynamicPopover
public let canRevealFiles = true
public let supportsMultipleWindows = true
public let deviceClass = DeviceClass.desktop

var gtkApp: Application
Expand Down Expand Up @@ -297,6 +298,25 @@ public final class Gtk3Backend: AppBackend {
window.present()
}

public func close(window: Window) {
window.close()
window.destroy()

// NB: It seems GTK3 won't automatically signal `::delete-event` if
// the window is closed programmatically.
window.onCloseRequest?(window)
}

public func setCloseHandler(
ofWindow window: Window,
to action: @escaping () -> Void
) {
window.onCloseRequest = { _ in
action()
window.destroy()
}
}

public func openExternalURL(_ url: URL) throws {
// Used instead of gtk_uri_launcher_launch to maintain <4.10 compatibility
var error: UnsafeMutablePointer<GError>? = nil
Expand Down
16 changes: 16 additions & 0 deletions Sources/GtkBackend/GtkBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
public let requiresImageUpdateOnScaleFactorChange = false
public let menuImplementationStyle = MenuImplementationStyle.dynamicPopover
public let canRevealFiles = true
public let supportsMultipleWindows = true
public let deviceClass = DeviceClass.desktop
public let defaultSheetCornerRadius = 10

Expand Down Expand Up @@ -163,7 +164,7 @@
return 0
#else
if window.showMenuBar {
// TODO: Don't hardcode this (if possible), because some Gtk

Check warning on line 167 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Don't hardcode this (if possib...) (todo)
// themes may affect the height of the menu bar.
25
} else {
Expand All @@ -179,7 +180,7 @@
}

public func isWindowProgrammaticallyResizable(_ window: Window) -> Bool {
// TODO: Detect whether window is fullscreen

Check warning on line 183 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Detect whether window is fulls...) (todo)
return true
}

Expand Down Expand Up @@ -216,6 +217,21 @@
window.present()
}

public func close(window: Window) {
window.close()
window.destroy()
}

public func setCloseHandler(
ofWindow window: Window,
to action: @escaping () -> Void
) {
window.onCloseRequest = { _ in
action()
window.destroy()
}
}

public func openExternalURL(_ url: URL) throws {
// Used instead of gtk_uri_launcher_launch to maintain <4.10 compatibility
gtk_show_uri(nil, url.absoluteString, guint(GDK_CURRENT_TIME))
Expand Down Expand Up @@ -280,7 +296,7 @@

model.appendItem(label: label, actionName: "\(actionNamespace).\(actionName)")
case .toggle(let label, let value, let onChange):
// FIXME: Implement

Check warning on line 299 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: FIXMEs should be resolved (Implement) (todo)
logger.warning("menu toggles not implemented")
case .submenu(let submenu):
model.appendSubmenu(
Expand Down Expand Up @@ -386,14 +402,14 @@
}

public func setRootEnvironmentChangeHandler(to action: @escaping () -> Void) {
// TODO: React to theme changes

Check warning on line 405 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (React to theme changes) (todo)
}

public func computeWindowEnvironment(
window: Window,
rootEnvironment: EnvironmentValues
) -> EnvironmentValues {
// TODO: Record window scale factor in here

Check warning on line 412 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Record window scale factor in ...) (todo)
rootEnvironment
}

Expand All @@ -401,7 +417,7 @@
of window: Window,
to action: @escaping () -> Void
) {
// TODO: Notify when window scale factor changes

Check warning on line 420 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Notify when window scale facto...) (todo)
}

public func setIncomingURLHandler(to action: @escaping (URL) -> Void) {
Expand Down Expand Up @@ -686,7 +702,7 @@

// private let tables = Tables()

// TODO: Implement tables

Check warning on line 705 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Implement tables) (todo)
// public func createTable(rows: Int, columns: Int) -> Widget {
// let widget = Grid()

Expand Down Expand Up @@ -763,7 +779,7 @@
environment: EnvironmentValues,
action: @escaping () -> Void
) {
// TODO: Update button label color using environment

Check warning on line 782 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Update button label color usin...) (todo)
let button = button as! Gtk.Button
button.sensitive = environment.isEnabled
button.label = label
Expand Down Expand Up @@ -1179,7 +1195,7 @@
}
},
window: window ?? windows[0]
) { result in

Check warning on line 1198 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Multiple Closures with Trailing Closure Violation: Trailing closure syntax should not be used when passing more than one closure argument (multiple_closures_with_trailing_closure)
switch result {
case .success(let urls):
handleResult(.success(urls[0]))
Expand Down Expand Up @@ -1212,7 +1228,7 @@
configure(chooser)

chooser.registerSignals()
chooser.response = { (_: NativeDialog, response: Int) -> Void in

Check warning on line 1231 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Redundant Void Return Violation: Returning Void in a function declaration is redundant (redundant_void_return)
// Release our intentional retain cycle which ironically only exists
// because of this line. The retain cycle keeps the file chooser
// around long enough for the user to respond (it gets released
Expand Down
8 changes: 7 additions & 1 deletion Sources/SwiftCrossUI/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,13 @@ extension App {
label: String,
metadataProvider: Logger.MetadataProvider?
) -> any LogHandler {
StreamLogHandler.standardError(label: label)
var logHandler = StreamLogHandler.standardError(label: label)
#if DEBUG
logHandler.logLevel = .debug
#else
logHandler.logLevel = .info
#endif
return logHandler
}

/// Runs the application.
Expand Down
Loading
Loading