Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
133 changes: 118 additions & 15 deletions Examples/Sources/WindowingExample/WindowingApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,103 @@ struct AlertDemo: View {
}
}

// kind of a stress test for the dismiss action
struct SheetDemo: View {
@State var isPresented = false
@State var isShortTermSheetPresented = false

var body: some View {
Button("Open Sheet") {
isPresented = true
}
Button("Show Sheet for 5s") {
isShortTermSheetPresented = true
Task {
try? await Task.sleep(nanoseconds: 1_000_000_000 * 5)
isShortTermSheetPresented = false
}
}
.sheet(isPresented: $isPresented) {
print("sheet dismissed")
} content: {
SheetBody()
.presentationDetents([.height(150), .medium, .large])
.presentationDragIndicatorVisibility(.visible)
.presentationBackground(.green)
}
.sheet(isPresented: $isShortTermSheetPresented) {
Text("I'm only here for 5s")
.padding(20)
.presentationDetents([.height(150), .medium, .large])
.presentationCornerRadius(10)
.presentationBackground(.red)
}
}

struct SheetBody: View {
@State var isPresented = false
@Environment(\.dismiss) var dismiss

var body: some View {
VStack {
Text("Nice sheet content")
.padding(20)
Button("I want more sheet") {
isPresented = true
print("should get presented")
}
Button("Dismiss") {
dismiss()
}
Spacer()
}
.sheet(isPresented: $isPresented) {
print("nested sheet dismissed")
} content: {
NestedSheetBody(dismissParent: { dismiss() })
.presentationCornerRadius(35)
}
}

struct NestedSheetBody: View {
@Environment(\.dismiss) var dismiss
var dismissParent: () -> Void
@State var showNextChild = false

var body: some View {
Text("I'm nested. Its claustrophobic in here.")
Button("New Child Sheet") {
showNextChild = true
}
.sheet(isPresented: $showNextChild) {
DoubleNestedSheetBody(dismissParent: { dismiss() })
.interactiveDismissDisabled()
}
Button("dismiss parent sheet") {
dismissParent()
}
Button("dismiss") {
dismiss()
}
}
}
struct DoubleNestedSheetBody: View {
@Environment(\.dismiss) var dismiss
var dismissParent: () -> Void

var body: some View {
Text("I'm nested. Its claustrophobic in here.")
Button("dismiss parent sheet") {
dismissParent()
}
Button("dismiss") {
dismiss()
}
}
}
}
}

@main
@HotReloadable
struct WindowingApp: App {
Expand Down Expand Up @@ -92,6 +189,11 @@ struct WindowingApp: App {
Divider()

AlertDemo()

Divider()

SheetDemo()
.padding(.bottom, 20)
}
.padding(20)
}
Expand All @@ -108,23 +210,24 @@ struct WindowingApp: App {
}
}
}

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

WindowGroup("Tertiary window") {
#hotReloadable {
Text("This a tertiary window!")
.padding(10)
WindowGroup("Tertiary window") {
#hotReloadable {
Text("This a tertiary window!")
.padding(10)
}
}
}
.defaultSize(width: 200, height: 200)
.windowResizability(.contentMinSize)
.defaultSize(width: 200, height: 200)
.windowResizability(.contentMinSize)
#endif
}
}
2 changes: 1 addition & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

108 changes: 108 additions & 0 deletions Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public final class AppKitBackend: AppBackend {
public typealias Menu = NSMenu
public typealias Alert = NSAlert
public typealias Path = NSBezierPath
public typealias Sheet = NSCustomSheet

public let defaultTableRowContentHeight = 20
public let defaultTableCellVerticalPadding = 4
Expand Down Expand Up @@ -1685,6 +1686,113 @@ public final class AppKitBackend: AppBackend {
let request = URLRequest(url: url)
webView.load(request)
}

public func createSheet() -> NSCustomSheet {
// Initialize with a default contentRect, similar to window creation (lines 58-68)
let sheet = NSCustomSheet(
contentRect: NSRect(
x: 0,
y: 0,
width: 400, // Default width
height: 300 // Default height
),
styleMask: [.titled, .closable],
backing: .buffered,
defer: true
)
return sheet
}

public func updateSheet(
_ sheet: NSCustomSheet, content: NSView, onDismiss: @escaping () -> Void
) {
let contentSize = naturalSize(of: content)

let width = max(contentSize.x, 10)
let height = max(contentSize.y, 10)
sheet.setContentSize(NSSize(width: width, height: height))

sheet.contentView = content
sheet.onDismiss = onDismiss
}

public func showSheet(_ sheet: NSCustomSheet, window: NSCustomWindow?) {
guard let window else {
print("warning: Cannot show sheet without a parent window")
return
}
// critical sheets stack
// beginSheet only shows a nested
// sheet after its parent gets dismissed
window.beginCriticalSheet(sheet)
}

public func dismissSheet(_ sheet: NSCustomSheet, window: NSCustomWindow?) {
if let window {
window.endSheet(sheet)
} else {
NSApplication.shared.stopModal()
}
}

public func setPresentationBackground(of sheet: NSCustomSheet, to color: Color) {
let backgroundView = NSView()
backgroundView.wantsLayer = true
backgroundView.layer?.backgroundColor = color.nsColor.cgColor

if let existingContentView = sheet.contentView {
let container = NSView()
container.translatesAutoresizingMaskIntoConstraints = false

container.addSubview(backgroundView)
backgroundView.translatesAutoresizingMaskIntoConstraints = false
backgroundView.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive =
true
backgroundView.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
backgroundView.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive =
true
backgroundView.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true

container.addSubview(existingContentView)
existingContentView.translatesAutoresizingMaskIntoConstraints = false
existingContentView.leadingAnchor.constraint(equalTo: container.leadingAnchor)
.isActive = true
existingContentView.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
existingContentView.trailingAnchor.constraint(equalTo: container.trailingAnchor)
.isActive = true
existingContentView.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive =
true

sheet.contentView = container
}
}

public func setInteractiveDismissDisabled(for sheet: NSCustomSheet, to disabled: Bool) {
sheet.interactiveDismissDisabled = disabled
}
}

public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate, SheetImplementation {
public var sheetSize: SIMD2<Int> {
guard let size = self.contentView?.frame.size else {
return SIMD2(x: 0, y: 0)
}
return SIMD2(x: Int(size.width), y: Int(size.height))
}
public var onDismiss: (() -> Void)?

public var interactiveDismissDisabled: Bool = false

public func dismiss() {
onDismiss?()
self.contentViewController?.dismiss(self)
}

@objc override public func cancelOperation(_ sender: Any?) {
if !interactiveDismissDisabled {
dismiss()
}
}
}

final class NSCustomTapGestureTarget: NSView {
Expand Down
7 changes: 7 additions & 0 deletions Sources/Gtk3Backend/Gtk3Backend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public final class Gtk3Backend: AppBackend {
public typealias Widget = Gtk3.Widget
public typealias Menu = Gtk3.Menu
public typealias Alert = Gtk3.MessageDialog
public typealias Sheet = Gtk3.Window

public final class Path {
var path: SwiftCrossUI.Path?
Expand Down Expand Up @@ -1516,3 +1517,9 @@ struct Gtk3Error: LocalizedError {
"gerror: code=\(code), domain=\(domain), message=\(message)"
}
}

extension Gtk3.Window: SheetImplementation {
public var sheetSize: SIMD2<Int> {
SIMD2(x: size.width, y: size.height)
}
}
Loading
Loading