Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
27 changes: 18 additions & 9 deletions Examples/Sources/WindowingExample/WindowingApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,12 @@ struct SheetDemo: View {
@HotReloadable
struct WindowingApp: App {
@State var title = "My window"
@State var resizable = false
@State var resizable = true
@State var isAlertSceneShown = false
@State var toggle = false
@State var enforceMaxSize = true
@State var closable = true
@State var minimizable = true

var body: some Scene {
WindowGroup(title) {
Expand All @@ -174,9 +177,12 @@ struct WindowingApp: App {
TextField("My window", text: $title)
}

Button(resizable ? "Disable resizing" : "Enable resizing") {
resizable = !resizable
}
Toggle("Enable resizing", isOn: $resizable)
.windowResizeBehavior(resizable ? .enabled : .disabled)
Toggle("Enable closing", isOn: $closable)
.windowDismissBehavior(closable ? .enabled : .disabled)
Toggle("Enable minimizing", isOn: $minimizable)
.preferredWindowMinimizeBehavior(minimizable ? .enabled : .disabled)

Image(Bundle.module.bundleURL.appendingPathComponent("Banner.png"))
.resizable()
Expand Down Expand Up @@ -204,7 +210,6 @@ struct WindowingApp: App {
}
}
.defaultSize(width: 500, height: 500)
.windowResizability(resizable ? .contentMinSize : .contentSize)
.commands {
CommandMenu("Demo menu") {
Button("Menu item") {}
Expand All @@ -223,12 +228,17 @@ struct WindowingApp: App {
#if !(os(iOS) || os(tvOS) || os(Windows))
WindowGroup("Secondary window") {
#hotReloadable {
Text("This a secondary window!")
.padding(10)
VStack {
Text("This a secondary window!")

Toggle("Enforce max size", isOn: $enforceMaxSize)
.toggleStyle(.checkbox)
}
.padding(10)
}
}
.defaultSize(width: 200, height: 200)
.windowResizability(.contentMinSize)
.windowResizability(enforceMaxSize ? .contentSize : .contentMinSize)

WindowGroup("Tertiary window") {
#hotReloadable {
Expand All @@ -237,7 +247,6 @@ struct WindowingApp: App {
}
}
.defaultSize(width: 200, height: 200)
.windowResizability(.contentMinSize)
#endif
}
}
34 changes: 30 additions & 4 deletions Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,18 @@ public final class AppKitBackend: AppBackend {
window.setContentSize(NSSize(width: newSize.x, height: newSize.y))
}

public func setMinimumSize(ofWindow window: Window, to minimumSize: SIMD2<Int>) {
window.contentMinSize.width = CGFloat(minimumSize.x)
window.contentMinSize.height = CGFloat(minimumSize.y)
public func setSizeLimits(
ofWindow window: Window,
minimum minimumSize: SIMD2<Int>,
maximum maximumSize: SIMD2<Int>?
) {
window.contentMinSize = CGSize(width: minimumSize.x, height: minimumSize.y)
window.contentMaxSize =
if let maximumSize {
CGSize(width: maximumSize.x, height: maximumSize.y)
} else {
CGSize(width: Double.infinity, height: .infinity)
}
}

public func setResizeHandler(
Expand All @@ -103,7 +112,24 @@ public final class AppKitBackend: AppBackend {
window.title = title
}

public func setResizability(ofWindow window: Window, to resizable: Bool) {
public func setBehaviors(
ofWindow window: Window,
closable: Bool,
minimizable: Bool,
resizable: Bool
) {
if closable {
window.styleMask.insert(.closable)
} else {
window.styleMask.remove(.closable)
}

if minimizable {
window.styleMask.insert(.miniaturizable)
} else {
window.styleMask.remove(.miniaturizable)
}

if resizable {
window.styleMask.insert(.resizable)
} else {
Expand Down
19 changes: 17 additions & 2 deletions Sources/DummyBackend/DummyBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ public final class DummyBackend: AppBackend {

public var size: SIMD2<Int>
public var minimumSize: SIMD2<Int> = .zero
public var maximumSize: SIMD2<Int>?
public var title = "Window"
public var resizable = true
public var closable = true
public var minimizable = true
public var content: Widget?
public var resizeHandler: ((SIMD2<Int>) -> Void)?

Expand Down Expand Up @@ -264,7 +267,14 @@ public final class DummyBackend: AppBackend {
window.title = title
}

public func setResizability(ofWindow window: Window, to resizable: Bool) {
public func setBehaviors(
ofWindow window: Window,
closable: Bool,
minimizable: Bool,
resizable: Bool
) {
window.closable = closable
window.minimizable = minimizable
window.resizable = resizable
}

Expand All @@ -284,8 +294,13 @@ public final class DummyBackend: AppBackend {
window.size = newSize
}

public func setMinimumSize(ofWindow window: Window, to minimumSize: SIMD2<Int>) {
public func setSizeLimits(
ofWindow window: Window,
minimum minimumSize: SIMD2<Int>,
maximum maximumSize: SIMD2<Int>?
) {
window.minimumSize = minimumSize
window.maximumSize = maximumSize
}

public func setResizeHandler(ofWindow window: Window, to action: @escaping (SIMD2<Int>) -> Void)
Expand Down
1 change: 1 addition & 0 deletions Sources/Gtk/Widgets/Window.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ open class Window: Widget {

@GObjectProperty(named: "title") public var title: String?
@GObjectProperty(named: "resizable") public var resizable: Bool
@GObjectProperty(named: "deletable") public var deletable: Bool
@GObjectProperty(named: "modal") public var isModal: Bool
@GObjectProperty(named: "decorated") public var isDecorated: Bool
@GObjectProperty(named: "destroy-with-parent") public var destroyWithParent: Bool
Expand Down
1 change: 1 addition & 0 deletions Sources/Gtk3/Widgets/Window.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ open class Window: Bin {

@GObjectProperty(named: "title") public var title: String?
@GObjectProperty(named: "resizable") public var resizable: Bool
@GObjectProperty(named: "deletable") public var deletable: Bool
@GObjectProperty(named: "modal") public var isModal: Bool
@GObjectProperty(named: "decorated") public var isDecorated: Bool

Expand Down
47 changes: 45 additions & 2 deletions Sources/Gtk3Backend/Gtk3Backend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,28 @@ public final class Gtk3Backend: AppBackend {
/// precreated window until it gets 'created' via `createWindow`.
var windows: [Window] = []

private struct LogLocation: Hashable, Equatable {
let file: String
let line: Int
let column: Int
}

private var logsPerformed: Set<LogLocation> = []

func debugLogOnce(
_ message: String,
file: String = #file,
line: Int = #line,
column: Int = #column
) {
#if DEBUG
let location = LogLocation(file: file, line: line, column: column)
if logsPerformed.insert(location).inserted {
logger.notice("\(message)")
}
#endif
}

// A separate initializer to satisfy ``AppBackend``'s requirements.
public convenience init() {
self.init(appIdentifier: nil)
Expand Down Expand Up @@ -160,7 +182,18 @@ public final class Gtk3Backend: AppBackend {
window.title = title
}

public func setResizability(ofWindow window: Window, to resizable: Bool) {
public func setBehaviors(
ofWindow window: Window,
closable: Bool,
minimizable: Bool,
resizable: Bool
) {
// FIXME: This doesn't seem to work on macOS at least
window.deletable = closable

// TODO: Figure out if there's some magic way to disable minimization
// in a framework where the minimize button usually doesn't even exist

window.resizable = resizable
}

Expand Down Expand Up @@ -207,9 +240,19 @@ public final class Gtk3Backend: AppBackend {
)
}

public func setMinimumSize(ofWindow window: Window, to minimumSize: SIMD2<Int>) {
public func setSizeLimits(
ofWindow window: Window,
minimum minimumSize: SIMD2<Int>,
maximum maximumSize: SIMD2<Int>?
) {
let child = window.child! as! CustomRootWidget
child.setMinimumSize(minimumWidth: minimumSize.x, minimumHeight: minimumSize.y)

// NB: GTK does not support setting maximum sizes for widgets. It just doesn't.
// https://discourse.gnome.org/t/how-to-build-fixed-size-windows-in-gtk-4/22807/10
if maximumSize != nil {
debugLogOnce("GTK does not support setting maximum window sizes")
}
}

public func setResizeHandler(
Expand Down
47 changes: 45 additions & 2 deletions Sources/GtkBackend/GtkBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,28 @@
/// precreated window until it gets 'created' via `createWindow`.
var windows: [Window] = []

private struct LogLocation: Hashable, Equatable {
let file: String
let line: Int
let column: Int
}

private var logsPerformed: Set<LogLocation> = []

func debugLogOnce(
_ message: String,
file: String = #file,
line: Int = #line,
column: Int = #column
) {
#if DEBUG
let location = LogLocation(file: file, line: line, column: column)
if logsPerformed.insert(location).inserted {
logger.notice("\(message)")
}
#endif
}

// A separate initializer to satisfy ``AppBackend``'s requirements.
public convenience init() {
self.init(appIdentifier: nil)
Expand Down Expand Up @@ -149,7 +171,18 @@
window.title = title
}

public func setResizability(ofWindow window: Window, to resizable: Bool) {
public func setBehaviors(
ofWindow window: Window,
closable: Bool,
minimizable: Bool,
resizable: Bool
) {
// FIXME: This doesn't seem to work on macOS at least
window.deletable = closable

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

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: FIXMEs should be resolved (This doesn't seem to work on m...) (todo)

// TODO: Figure out if there's some magic way to disable minimization
// in a framework where the minimize button usually doesn't even exist

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

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Figure out if there's some mag...) (todo)

window.resizable = resizable
}

Expand All @@ -164,7 +197,7 @@
#else
if window.showMenuBar {
// TODO: Don't hardcode this (if possible), because some Gtk
// themes may affect the height of the menu bar.

Check warning on line 200 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)
25
} else {
0
Expand All @@ -180,7 +213,7 @@

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

Check warning on line 216 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)
}

public func setSize(ofWindow window: Window, to newSize: SIMD2<Int>) {
Expand All @@ -192,8 +225,18 @@
child.preemptAllocatedSize(allocatedWidth: newSize.x, allocatedHeight: newSize.y)
}

public func setMinimumSize(ofWindow window: Window, to minimumSize: SIMD2<Int>) {
public func setSizeLimits(
ofWindow window: Window,
minimum minimumSize: SIMD2<Int>,
maximum maximumSize: SIMD2<Int>?
) {
window.setMinimumSize(to: Size(width: minimumSize.x, height: minimumSize.y))

// NB: GTK does not support setting maximum sizes for widgets. It just doesn't.
// https://discourse.gnome.org/t/how-to-build-fixed-size-windows-in-gtk-4/22807/10
if maximumSize != nil {
debugLogOnce("GTK does not support setting maximum window sizes")
}
}

public func setResizeHandler(
Expand Down Expand Up @@ -414,14 +457,14 @@
}
}
}

Check warning on line 460 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 show(widget: Widget) {
widget.show()
}

public func tag(widget: Widget, as tag: String) {
widget.tag(as: tag)
}

Check warning on line 467 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)

// MARK: Containers

Expand All @@ -429,7 +472,7 @@
return Fixed()
}

public func removeAllChildren(of container: Widget) {

Check warning on line 475 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)
let container = container as! Fixed
container.removeAllChildren()
}
Expand Down Expand Up @@ -725,7 +768,7 @@
// }

// }

Check warning on line 771 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 setColumnCount(ofTable table: Widget, to columns: Int) {
// let table = table as! Grid

Expand Down Expand Up @@ -1218,7 +1261,7 @@
chooser.response = { (_: NativeDialog, response: Int) -> Void in
// 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

Check warning on line 1264 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)
// immediately if we don't do this in the response signal handler).
chooser.response = nil

Expand Down Expand Up @@ -1251,7 +1294,7 @@
gtk_gesture_single_set_button(gtkGesture.opaquePointer, guint(GDK_BUTTON_SECONDARY))
case .longPress:
gtkGesture = GestureLongPress()
}

Check warning on line 1297 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)
child.addEventController(gtkGesture)
return child
}
Expand Down
34 changes: 28 additions & 6 deletions Sources/SwiftCrossUI/Backend/AppBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,20 @@ public protocol AppBackend: Sendable {
func createWindow(withDefaultSize defaultSize: SIMD2<Int>?) -> Window
/// Sets the title of a window.
func setTitle(ofWindow window: Window, to title: String)
/// Sets the resizability of a window. Even if resizable, the window
/// shouldn't be allowed to become smaller than its content.
func setResizability(ofWindow window: Window, to resizable: Bool)
/// Sets the behaviors of a window.
/// - Parameters:
/// - window: The window to set the behaviors on.
/// - closable: Whether the window can be closed by the user.
/// - minimizable: Whether the window can be minimized by the user.
/// - resizable: Whether the window can be resized by the user. Even if
/// resizable, the window shouldn't be allowed to become smaller than its
/// minimum size, or larger than its maximum size.
func setBehaviors(
ofWindow window: Window,
closable: Bool,
minimizable: Bool,
resizable: Bool
)
/// Sets the root child of a window (replaces the previous child if any).
func setChild(ofWindow window: Window, to child: Widget)
/// Gets the size of the given window in pixels.
Expand All @@ -148,9 +159,20 @@ public protocol AppBackend: Sendable {
func isWindowProgrammaticallyResizable(_ window: Window) -> Bool
/// Sets the size of the given window in pixels.
func setSize(ofWindow window: Window, to newSize: SIMD2<Int>)
/// Sets the minimum width and height of the window. Prevents the user from making the
/// window any smaller than the given size.
func setMinimumSize(ofWindow window: Window, to minimumSize: SIMD2<Int>)
/// Sets the minimum and maximum width and height of a window.
///
/// Prevents the user from making the window any smaller or larger than the given minimum and
/// maximum sizes, respectively.
/// - Parameters:
/// - window: The window to set the size limits of.
/// - minimumSize: The minimum window size.
/// - maximumSize: The maximum window size. If `nil`, any existing maximum size
/// constraints should be removed.
func setSizeLimits(
ofWindow window: Window,
minimum minimumSize: SIMD2<Int>,
maximum maximumSize: SIMD2<Int>?
)
/// Sets the handler for the window's resizing events. `action` takes the proposed size
/// of the window and returns the final size for the window (which allows SwiftCrossUI
/// to implement features such as dynamic minimum window sizes based off the content's
Expand Down
4 changes: 4 additions & 0 deletions Sources/SwiftCrossUI/Environment/EnvironmentValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ public struct EnvironmentValues {
/// Whether the text should be selectable. Set by ``View/textSelectionEnabled(_:)``.
public var isTextSelectionEnabled: Bool

/// The resizing behaviour of the current window.
var windowResizability: WindowResizability

/// The menu ordering to use.
public var menuOrder: MenuOrder

Expand Down Expand Up @@ -229,6 +232,7 @@ public struct EnvironmentValues {
isEnabled = true
scrollDismissesKeyboardMode = .automatic
isTextSelectionEnabled = false
windowResizability = .automatic
menuOrder = .automatic
allowLayoutCaching = false
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
extension Scene {
/// Sets the resizability of windows controlled by this scene.
///
/// This modifier controls how SwiftCrossUI determines the bounds within which
/// windows can be resized, whereas ``View/windowResizeBehavior(_:)`` controls
/// whether the user can resize the enclosing window. The only time this
/// modifier can disable interactive resizing is when a window's content has
/// a fixed size and `resizability` is ``WindowResizability/contentSize``.
public func windowResizability(_ resizability: WindowResizability) -> some Scene {
environment(\.windowResizability, resizability)
}
}
Loading
Loading