From b049f2c96908ea97029acf0b067eca83dff514bc Mon Sep 17 00:00:00 2001 From: david-swift Date: Wed, 20 Nov 2024 12:58:36 +0100 Subject: [PATCH 01/17] Add support for max/min width/height --- Sources/View/AnyView+.swift | 48 +++++++++++++++++++++++++++++++++++++ Tests/Demo.swift | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/Sources/View/AnyView+.swift b/Sources/View/AnyView+.swift index 1fe06c1..d1ffeea 100644 --- a/Sources/View/AnyView+.swift +++ b/Sources/View/AnyView+.swift @@ -66,6 +66,54 @@ extension AnyView { #endif } + /// Set the view's minimum width and/or height. + /// - Parameters: + /// - minWidth: The minimum width. + /// - minHeight: The minimum height. + public func frame( + minWidth: Int? = nil, + minHeight: Int? = nil + ) -> AnyView { + #if WINUI + #else + ModifierWrapper( + content: self, + minWidth: minWidth, + minHeight: minHeight + ) + #endif + } + + /// Set the view's maximum width. + /// - Parameter maxWidth: The maximum width. + public func frame( + maxWidth: Int? = nil + ) -> AnyView { + #if WINUI + #else + Clamp(vertical: false) + .child { + self + } + .maximumSize(maxWidth) + #endif + } + + /// Set the view's maximum height. + /// - Parameter maxHeight: The maximum height. + public func frame( + maxHeight: Int? = nil + ) -> AnyView { + #if WINUI + #else + Clamp(vertical: true) + .child { + self + } + .maximumSize(maxHeight) + #endif + } + /// Wrap the view with a card. /// - Returns: The card. public func card() -> AnyView { diff --git a/Tests/Demo.swift b/Tests/Demo.swift index e3d0578..fa4df65 100644 --- a/Tests/Demo.swift +++ b/Tests/Demo.swift @@ -89,7 +89,7 @@ struct ContentView: WindowView { Text(element.description) .valign(.center) .halign(.center) - .padding(50) + .frame(minWidth: 300, minHeight: 250) } .valign(.center) .card() From a21b59eff5e806b7780fd8d3b435db6d28bc522e Mon Sep 17 00:00:00 2001 From: david-swift Date: Wed, 20 Nov 2024 13:16:53 +0100 Subject: [PATCH 02/17] Add support for detecting the framework --- Sources/Model/Enumerations/Framework.swift | 42 ++++++++++++++++++++++ Tests/Demo.swift | 1 + 2 files changed, 43 insertions(+) create mode 100644 Sources/Model/Enumerations/Framework.swift diff --git a/Sources/Model/Enumerations/Framework.swift b/Sources/Model/Enumerations/Framework.swift new file mode 100644 index 0000000..f53d424 --- /dev/null +++ b/Sources/Model/Enumerations/Framework.swift @@ -0,0 +1,42 @@ +// +// Framework.swift +// Aparoksha +// +// Created by david-swift on 20.11.24. +// + +/// The current backend framework. +public enum Framework: Equatable { + + /// The WinUI backend. + case winui + /// The libadwaita backend. + case adwaita + + /// The currently selected framework. + public static var currentFramework: Self { + #if WINUI + .winui + #else + .adwaita + #endif + } + +} + +extension AnyView { + + /// Modify the view only if it is being rendered with a particular framework. + /// - Parameters: + /// - frameworks: The frameworks. + /// - modify: The modification. + /// - Returns: The view. + public func modify(frameworks: Framework..., modify: (Self) -> AnyView) -> AnyView { + if frameworks.contains(Framework.currentFramework) { + modify(self) + } else { + self + } + } + +} diff --git a/Tests/Demo.swift b/Tests/Demo.swift index fa4df65..22b6437 100644 --- a/Tests/Demo.swift +++ b/Tests/Demo.swift @@ -164,3 +164,4 @@ enum GenericItem: String, Equatable, CustomStringConvertible, CaseIterable, Iden // swiftlint:enable + From 1388fc487752dbdefcfaee7cc7691665a30793ab Mon Sep 17 00:00:00 2001 From: david-swift Date: Wed, 20 Nov 2024 17:13:50 +0100 Subject: [PATCH 03/17] Extend logic for framework-specific code --- Sources/Model/Enumerations/Framework.swift | 13 +++++++++---- Sources/View/{ => Navigation}/FlatNavigation.swift | 0 .../{ => Navigation}/HierarchicalNavigation.swift | 0 3 files changed, 9 insertions(+), 4 deletions(-) rename Sources/View/{ => Navigation}/FlatNavigation.swift (100%) rename Sources/View/{ => Navigation}/HierarchicalNavigation.swift (100%) diff --git a/Sources/Model/Enumerations/Framework.swift b/Sources/Model/Enumerations/Framework.swift index f53d424..95bb8aa 100644 --- a/Sources/Model/Enumerations/Framework.swift +++ b/Sources/Model/Enumerations/Framework.swift @@ -14,7 +14,7 @@ public enum Framework: Equatable { case adwaita /// The currently selected framework. - public static var currentFramework: Self { + public static var current: Self { #if WINUI .winui #else @@ -30,12 +30,17 @@ extension AnyView { /// - Parameters: /// - frameworks: The frameworks. /// - modify: The modification. + /// - modifyOtherwise: The modification for other frameworks. /// - Returns: The view. - public func modify(frameworks: Framework..., modify: (Self) -> AnyView) -> AnyView { - if frameworks.contains(Framework.currentFramework) { + public func modify( + frameworks: Framework..., + modify: (Self) -> AnyView, + else modifyOtherwise: (Self) -> AnyView = { $0 } + ) -> AnyView { + if frameworks.contains(Framework.current) { modify(self) } else { - self + modifyOtherwise(self) } } diff --git a/Sources/View/FlatNavigation.swift b/Sources/View/Navigation/FlatNavigation.swift similarity index 100% rename from Sources/View/FlatNavigation.swift rename to Sources/View/Navigation/FlatNavigation.swift diff --git a/Sources/View/HierarchicalNavigation.swift b/Sources/View/Navigation/HierarchicalNavigation.swift similarity index 100% rename from Sources/View/HierarchicalNavigation.swift rename to Sources/View/Navigation/HierarchicalNavigation.swift From e8c7773394aee58d5c8e87deac50a78166973c40 Mon Sep 17 00:00:00 2001 From: david-swift Date: Thu, 21 Nov 2024 07:20:47 +0100 Subject: [PATCH 04/17] Fix placement in alerts with Adwaita --- Sources/View/Complex/Alert.swift | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Sources/View/Complex/Alert.swift b/Sources/View/Complex/Alert.swift index e4e0305..21f2d69 100644 --- a/Sources/View/Complex/Alert.swift +++ b/Sources/View/Complex/Alert.swift @@ -59,21 +59,15 @@ public struct Alert: SimpleView { } #else var dialog = AlertDialog(visible: $visible, child: child, id: id, heading: title, body: content) + dialog = dialog.response(close.0, role: .close, action: close.1) if let secondary { - dialog = dialog.response(close.0, role: .close, action: close.1) dialog = dialog.response(secondary.0.0, appearance: secondary.1.appearance, action: secondary.0.1) - if let primary { - dialog = dialog - .response(primary.0.0, appearance: primary.1.appearance, role: .default, action: primary.0.1) - } - return dialog - } else { - if let primary { - dialog = dialog - .response(primary.0.0, appearance: primary.1.appearance, role: .default, action: primary.0.1) - } - return dialog.response(close.0, role: .close, action: close.1) } + if let primary { + dialog = dialog + .response(primary.0.0, appearance: primary.1.appearance, role: .default, action: primary.0.1) + } + return dialog #endif } From 0010471a2979721c58fd6759db7aeee332206d9b Mon Sep 17 00:00:00 2001 From: david-swift Date: Sat, 23 Nov 2024 16:23:56 -0800 Subject: [PATCH 05/17] Update WinUI interface to latest changes --- Sources/View/AnyView+.swift | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Sources/View/AnyView+.swift b/Sources/View/AnyView+.swift index d1ffeea..2c5a181 100644 --- a/Sources/View/AnyView+.swift +++ b/Sources/View/AnyView+.swift @@ -75,6 +75,11 @@ extension AnyView { minHeight: Int? = nil ) -> AnyView { #if WINUI + ModifierWrapper( + view: self, + minWidth: .init(minWidth), + minHeight: .init(minHeight) + ) #else ModifierWrapper( content: self, @@ -90,6 +95,10 @@ extension AnyView { maxWidth: Int? = nil ) -> AnyView { #if WINUI + ModifierWrapper( + view: self, + maxWidth: .init(maxWidth) + ) #else Clamp(vertical: false) .child { @@ -105,6 +114,10 @@ extension AnyView { maxHeight: Int? = nil ) -> AnyView { #if WINUI + ModifierWrapper( + view: self, + maxHeight: .init(maxHeight) + ) #else Clamp(vertical: true) .child { @@ -171,3 +184,17 @@ extension AnyView { #endif } + +extension Optional { + + /// Initialize an optional double with an optional integer. + /// - Parameter integer: The optional integer. + public init(_ integer: Int?) { + if let integer { + self = .init(integer) + } else { + self = nil + } + } + +} \ No newline at end of file From d1f30b9de5a8a2099f3c76b952169f8cd5d64273 Mon Sep 17 00:00:00 2001 From: david-swift Date: Sat, 23 Nov 2024 18:41:33 +0100 Subject: [PATCH 06/17] Fix flat navigation with dynamic identifiable elements on Windows --- Sources/View/Navigation/FlatNavigation.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/View/Navigation/FlatNavigation.swift b/Sources/View/Navigation/FlatNavigation.swift index 54507bc..625e52c 100644 --- a/Sources/View/Navigation/FlatNavigation.swift +++ b/Sources/View/Navigation/FlatNavigation.swift @@ -136,11 +136,11 @@ public struct FlatNavigation: View where Item: FlatNavigationItem { /// - selection: The selected element's id. /// - content: The main content. public init( - _ elements: [Element], selection: Binding, @ViewBuilder content: () -> Body + _ elements: @escaping @autoclosure () -> [Element], selection: Binding, @ViewBuilder content: () -> Body ) where Element: DynamicFlatNavigationItem, Element: Identifiable, Item == DynamicWrapper { - self.items = elements.map { .init(item: $0) } + self.items = elements().map { .init(item: $0) } _selectedItem = Binding { - .init(item: elements.first { $0.id == selection.wrappedValue }) + .init(item: elements().first { $0.id == selection.wrappedValue }) } set: { newValue in if let item = newValue.item { selection.wrappedValue = item.id From 4a60dd9e5ffc6a197e7917db263ae1cf150560b5 Mon Sep 17 00:00:00 2001 From: david-swift Date: Mon, 25 Nov 2024 07:18:22 +0100 Subject: [PATCH 07/17] Add Flatpak manifest and custom carousel icon --- Data/GNOME/carousel-symbolic.svg | 2 ++ Sources/Model/Enumerations/Icon.swift | 6 ---- Tests/Demo.swift | 2 +- dev.aparoksha.Demo.json | 49 +++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 Data/GNOME/carousel-symbolic.svg create mode 100644 dev.aparoksha.Demo.json diff --git a/Data/GNOME/carousel-symbolic.svg b/Data/GNOME/carousel-symbolic.svg new file mode 100644 index 0000000..aac8f37 --- /dev/null +++ b/Data/GNOME/carousel-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/Sources/Model/Enumerations/Icon.swift b/Sources/Model/Enumerations/Icon.swift index 2ac1c25..63389c4 100644 --- a/Sources/Model/Enumerations/Icon.swift +++ b/Sources/Model/Enumerations/Icon.swift @@ -24,8 +24,6 @@ public enum Icon: Equatable { case copy /// The navigation icon. case navigation - /// The panel icon. - case panel /// The warning icon. case warning /// The trash icon. @@ -57,8 +55,6 @@ public enum Icon: Equatable { "\u{E8C8}" case .navigation: "\u{E700}" - case .panel: - "\u{E7FB}" case .warning: "\u{E7BA}" case .trash: @@ -99,8 +95,6 @@ public enum Icon: Equatable { .editCopy case .navigation: .sidebarShow - case .panel: - .panelCenter case .warning: .dialogWarning case .trash: diff --git a/Tests/Demo.swift b/Tests/Demo.swift index 22b6437..c839e68 100644 --- a/Tests/Demo.swift +++ b/Tests/Demo.swift @@ -137,7 +137,7 @@ enum Item: String, FlatNavigationItem, CaseIterable, Equatable, Codable { case .navigation: .navigation case .carousel: - .panel + .custom(winui: "🤣", adwaita: "dev.aparoksha.Demo.carousel-symbolic") case .alert: .warning } diff --git a/dev.aparoksha.Demo.json b/dev.aparoksha.Demo.json new file mode 100644 index 0000000..1d3c9a5 --- /dev/null +++ b/dev.aparoksha.Demo.json @@ -0,0 +1,49 @@ +{ + "app-id": "dev.aparoksha.Demo", + "runtime": "org.gnome.Platform", + "runtime-version": "master", + "sdk": "org.gnome.Sdk", + "sdk-extensions": [ + "org.freedesktop.Sdk.Extension.swift6" + ], + "command": "Demo", + "finish-args": [ + "--share=ipc", + "--socket=fallback-x11", + "--device=dri", + "--socket=wayland" + ], + "build-options": { + "append-path": "/usr/lib/sdk/swift6/bin", + "prepend-ld-library-path": "/usr/lib/sdk/swift6/lib" + }, + "cleanup": [ + "/include", + "/lib/pkgconfig", + "/man", + "/share/doc", + "/share/gtk-doc", + "/share/man", + "/share/pkgconfig", + "*.la", + "*.a" + ], + "modules": [ + { + "name": "Demo", + "builddir": true, + "buildsystem": "simple", + "sources": [ + { + "type": "dir", + "path": "." + } + ], + "build-commands": [ + "swift build --static-swift-stdlib", + "install -Dm755 .build/debug/Demo /app/bin/Demo", + "install -Dm644 Data/GNOME/carousel-symbolic.svg $DESTDIR/app/share/icons/hicolor/symbolic/apps/dev.aparoksha.Demo.carousel-symbolic.svg" + ] + } + ] +} From 5b6284c488e70871771113b0c6bb4ad2a587c1d7 Mon Sep 17 00:00:00 2001 From: david-swift Date: Tue, 26 Nov 2024 11:32:29 +0100 Subject: [PATCH 08/17] Fix min width and height not working on Windows --- Sources/View/AnyView+.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/View/AnyView+.swift b/Sources/View/AnyView+.swift index 2c5a181..d5ea3bf 100644 --- a/Sources/View/AnyView+.swift +++ b/Sources/View/AnyView+.swift @@ -76,9 +76,9 @@ extension AnyView { ) -> AnyView { #if WINUI ModifierWrapper( - view: self, - minWidth: .init(minWidth), - minHeight: .init(minHeight) + view: Grid { self }, + minWidth: .init(optional: minWidth?.windowsValue), + minHeight: .init(optional: minHeight?.windowsValue) ) #else ModifierWrapper( @@ -97,7 +97,7 @@ extension AnyView { #if WINUI ModifierWrapper( view: self, - maxWidth: .init(maxWidth) + maxWidth: .init(optional: maxWidth?.windowsValue) ) #else Clamp(vertical: false) @@ -116,7 +116,7 @@ extension AnyView { #if WINUI ModifierWrapper( view: self, - maxHeight: .init(maxHeight) + maxHeight: .init(optional: maxHeight?.windowsValue) ) #else Clamp(vertical: true) @@ -189,9 +189,9 @@ extension Optional { /// Initialize an optional double with an optional integer. /// - Parameter integer: The optional integer. - public init(_ integer: Int?) { - if let integer { - self = .init(integer) + public init(optional: Int?) { + if let optional { + self = .init(optional) } else { self = nil } From 3d70c673be163c7919da90141336f92bf4684eb1 Mon Sep 17 00:00:00 2001 From: david-swift Date: Thu, 28 Nov 2024 12:58:55 +0100 Subject: [PATCH 09/17] Add support for extra child in alert --- Sources/View/AnyView+.swift | 12 +++++++++-- Sources/View/Complex/Alert.swift | 22 +++++++++++++++++++- Sources/View/Navigation/FlatNavigation.swift | 4 +++- Tests/Demo.swift | 3 +++ 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/Sources/View/AnyView+.swift b/Sources/View/AnyView+.swift index d5ea3bf..35bffee 100644 --- a/Sources/View/AnyView+.swift +++ b/Sources/View/AnyView+.swift @@ -148,6 +148,7 @@ extension AnyView { /// - id: The identifier. /// - closeLabel: The label for the close button. /// - closeAction: The actino for the close button. + /// - extraChild: The content view in the alert. /// - Returns: The alert. public func alert( visible: Binding, @@ -157,7 +158,14 @@ extension AnyView { closeLabel: String, closeAction: @escaping () -> Void ) -> Alert { - .init(visible: visible, title: title, content: content, id: id, child: self, close: (closeLabel, closeAction)) + .init( + visible: visible, + title: title, + content: content, + id: id, + child: self, + close: (closeLabel, closeAction) + ) } // swiftlint:enable function_default_parameter_at_end @@ -197,4 +205,4 @@ extension Optional { } } -} \ No newline at end of file +} diff --git a/Sources/View/Complex/Alert.swift b/Sources/View/Complex/Alert.swift index 21f2d69..6511375 100644 --- a/Sources/View/Complex/Alert.swift +++ b/Sources/View/Complex/Alert.swift @@ -19,6 +19,9 @@ public struct Alert: SimpleView { var title: String /// The content. var content: String + // TODO: Implement on Windows + /// The view content. + var extraChild: Body? /// The identifier. var id: String /// The child view. @@ -40,6 +43,7 @@ public struct Alert: SimpleView { #if WINUI var dialog = ContentDialog(visible: $visible, child: child, id: id, title: title) { Text(content) + extraChild } if let primary { dialog = dialog.primaryResponse(primary.0.0, suggested: primary.1 == .suggested) { @@ -58,7 +62,14 @@ public struct Alert: SimpleView { visible = false } #else - var dialog = AlertDialog(visible: $visible, child: child, id: id, heading: title, body: content) + var dialog = AlertDialog( + visible: $visible, + child: child, + id: id, + heading: title, + body: content, + extraChild: extraChild + ) dialog = dialog.response(close.0, role: .close, action: close.1) if let secondary { dialog = dialog.response(secondary.0.0, appearance: secondary.1.appearance, action: secondary.0.1) @@ -123,4 +134,13 @@ public struct Alert: SimpleView { modify { $0.secondary = ((title, action), style) } } + /// Set the content view in the alert. + /// - Parameters: + /// - child: The view. + public func extraChild( + @ViewBuilder child: () -> Body + ) -> Self { + modify { $0.extraChild = child() } + } + } diff --git a/Sources/View/Navigation/FlatNavigation.swift b/Sources/View/Navigation/FlatNavigation.swift index 625e52c..4126f4f 100644 --- a/Sources/View/Navigation/FlatNavigation.swift +++ b/Sources/View/Navigation/FlatNavigation.swift @@ -136,7 +136,9 @@ public struct FlatNavigation: View where Item: FlatNavigationItem { /// - selection: The selected element's id. /// - content: The main content. public init( - _ elements: @escaping @autoclosure () -> [Element], selection: Binding, @ViewBuilder content: () -> Body + _ elements: @escaping @autoclosure () -> [Element], + selection: Binding, + @ViewBuilder content: () -> Body ) where Element: DynamicFlatNavigationItem, Element: Identifiable, Item == DynamicWrapper { self.items = elements().map { .init(item: $0) } _selectedItem = Binding { diff --git a/Tests/Demo.swift b/Tests/Demo.swift index c839e68..1a62477 100644 --- a/Tests/Demo.swift +++ b/Tests/Demo.swift @@ -108,6 +108,9 @@ struct ContentView: WindowView { .alert(visible: $showAlert, title: "Sample Alert", content: "This is the main content", closeLabel: "Close") { print("Close") } + .extraChild { + Text("Test") + } .primaryResponse("Primary", style: .suggested) { print("Primary") } From 628034293aeea7be1bd562631eceb2f24a94a507 Mon Sep 17 00:00:00 2001 From: david-swift Date: Sun, 1 Dec 2024 15:08:51 +0100 Subject: [PATCH 10/17] Set default titlebar per window --- .../Model/User Interface/App/Aparoksha.swift | 8 --- .../User Interface/App/AppInformation.swift | 8 --- Sources/Scene/Window.swift | 58 +++++++++++++++++-- Sources/View/Navigation/FlatNavigation.swift | 7 ++- .../Navigation/HierarchicalNavigation.swift | 5 +- Tests/Demo.swift | 7 ++- 6 files changed, 66 insertions(+), 27 deletions(-) diff --git a/Sources/Model/User Interface/App/Aparoksha.swift b/Sources/Model/User Interface/App/Aparoksha.swift index bbd05b1..f5e4f4f 100644 --- a/Sources/Model/User Interface/App/Aparoksha.swift +++ b/Sources/Model/User Interface/App/Aparoksha.swift @@ -87,12 +87,4 @@ public class Aparoksha: AppStorage { app.quit() } - /// Remove the default title bar. - func removeDefaultTitlebar() { - appInformation.defaultTitlebar = false - Task { - StateManager.updateViews(force: true) - } - } - } diff --git a/Sources/Model/User Interface/App/AppInformation.swift b/Sources/Model/User Interface/App/AppInformation.swift index d20dab2..96933f8 100644 --- a/Sources/Model/User Interface/App/AppInformation.swift +++ b/Sources/Model/User Interface/App/AppInformation.swift @@ -28,14 +28,6 @@ struct AppInformation { /// The settings label. var settings: String - #if WINUI - /// Whether the gap is active. - var gap = false - #endif - - /// Whether the default title bar is visible. - var defaultTitlebar = true - #if ADWAITA /// The main menu for GNOME. /// - Parameter showAbout: Whether to show the about dialog. diff --git a/Sources/Scene/Window.swift b/Sources/Scene/Window.swift index 737d219..d368736 100644 --- a/Sources/Scene/Window.swift +++ b/Sources/Scene/Window.swift @@ -10,6 +10,14 @@ import Core /// A window. public struct Window: AparokshaSceneElement { + #if WINUI + /// The native window type. + typealias NativeWindow = WinUIWindow + #else + /// The native window type. + typealias NativeWindow = AdwaitaWindow + #endif + /// The window's identifier. public var id: String /// The window's title. @@ -48,6 +56,39 @@ public struct Window: AparokshaSceneElement { self.open = open } + /// The Aparoksha window object. + public struct Window { + + /// The native window. + var native: NativeWindow + + /// Whether the default title bar is visible. + var defaultTitlebar: Bool { + get { + native.fields["default-titlebar"] as? Bool ?? true + } + nonmutating set { + native.fields["default-titlebar"] = newValue + } + } + + /// Initialize a window. + /// - Parameter native: The native window. + init(native: NativeWindow) { + self.native = native + } + + /// Close the window. + public func close() { + #if WINUI + // TODO: Implement + #else + native.close() + #endif + } + + } + /// Set up the initial containers. /// - Parameter app: The app storage. public func setupInitialContainers(app: Storage) where Storage: AppStorage { @@ -158,9 +199,10 @@ public struct Window: AparokshaSceneElement { #if WINUI /// The native window. func nativeWindow(app: Aparoksha) -> Core.Window { - .init(title, id: id, open: open) { _ in + .init(title, id: id, open: open) { window in WindowMainView( content: content, + window: .init(native: window), defaultTitlebar: app.appInformation.defaultTitlebar, app: app, toolbarItems: toolbarItems, @@ -174,10 +216,10 @@ public struct Window: AparokshaSceneElement { #else /// The native window. func nativeWindow(app: Aparoksha) -> Core.Window { - .init(id: id, open: open) { _ in + .init(id: id, open: open) { window in WindowMainView( content: content, - defaultTitlebar: app.appInformation.defaultTitlebar, + window: .init(native: window), app: app, toolbarItems: toolbarItems, contentLabel: title, @@ -201,7 +243,7 @@ struct WindowMainView: View { /// The window's content. var content: Body /// Whether to show the default title bar. - var defaultTitlebar: Bool + var window: Window.Window /// The app. var app: Aparoksha /// The toolbar items. @@ -213,6 +255,12 @@ struct WindowMainView: View { /// The main view. var view: Body { + contentView + .environment("window", data: window) + } + + /// The content. + @ViewBuilder var contentView: Body { #if WINUI if defaultTitlebar { let item = ContentItem(description: contentLabel, icon: contentIcon) @@ -236,7 +284,7 @@ struct WindowMainView: View { fullContent(app: app) } #else - if !defaultTitlebar { + if !window.defaultTitlebar { fullContent(app: app) } else { ToolbarView() diff --git a/Sources/View/Navigation/FlatNavigation.swift b/Sources/View/Navigation/FlatNavigation.swift index 4126f4f..b7ec48f 100644 --- a/Sources/View/Navigation/FlatNavigation.swift +++ b/Sources/View/Navigation/FlatNavigation.swift @@ -24,6 +24,9 @@ public struct FlatNavigation: View where Item: FlatNavigationItem { /// The toolbar items. @Environment("toolbar") private var toolbarItems: [ToolbarItem]? + /// The window object. + @Environment("window") + private var window: Window.Window? /// The items. var items: [Item] /// The content view. @@ -57,7 +60,7 @@ public struct FlatNavigation: View where Item: FlatNavigationItem { primaryAction: primaryActionProperties?.button() ?? [] ) .onAppear { - app?.removeDefaultTitlebar() + window?.defaultTitlebar = false } } else { AdwNavigationSplitView( @@ -69,7 +72,7 @@ public struct FlatNavigation: View where Item: FlatNavigationItem { navigationTitle: navigationTitle ) .onAppear { - app?.removeDefaultTitlebar() + window?.defaultTitlebar = false } } #endif diff --git a/Sources/View/Navigation/HierarchicalNavigation.swift b/Sources/View/Navigation/HierarchicalNavigation.swift index b9919c0..c071a69 100644 --- a/Sources/View/Navigation/HierarchicalNavigation.swift +++ b/Sources/View/Navigation/HierarchicalNavigation.swift @@ -34,6 +34,9 @@ public struct HierarchicalNavigation: View where Item: Equatable, Item: Cu /// The app. @Environment("app") var app: Aparoksha? + /// The window object. + @Environment("window") + private var window: Window.Window? #else /// The navigation path. @Environment("path") @@ -101,7 +104,7 @@ public struct HierarchicalNavigation: View where Item: Equatable, Item: Cu title: dialog?.description ) .onAppear { - app?.removeDefaultTitlebar() + window?.defaultTitlebar = false } } #endif diff --git a/Tests/Demo.swift b/Tests/Demo.swift index 1a62477..ea79ef2 100644 --- a/Tests/Demo.swift +++ b/Tests/Demo.swift @@ -27,6 +27,10 @@ struct Demo: App { Window("Demo", icon: .headphones, id: "main") { ContentView() } + Window("Demo 2", icon: .headphones, id: "second") { + Text("Hi") + .padding(10) + } } } @@ -108,9 +112,6 @@ struct ContentView: WindowView { .alert(visible: $showAlert, title: "Sample Alert", content: "This is the main content", closeLabel: "Close") { print("Close") } - .extraChild { - Text("Test") - } .primaryResponse("Primary", style: .suggested) { print("Primary") } From 640cf90b63733e8c93064e3575a177b4dd907388 Mon Sep 17 00:00:00 2001 From: david-swift Date: Thu, 19 Dec 2024 13:07:53 +0100 Subject: [PATCH 11/17] Add support for scroll views --- Sources/View/Navigation/FlatNavigation.swift | 2 +- Sources/View/Simple/ScrollView.swift | 29 ++++++++++++++++++++ Tests/Demo.swift | 2 -- 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 Sources/View/Simple/ScrollView.swift diff --git a/Sources/View/Navigation/FlatNavigation.swift b/Sources/View/Navigation/FlatNavigation.swift index b7ec48f..e096363 100644 --- a/Sources/View/Navigation/FlatNavigation.swift +++ b/Sources/View/Navigation/FlatNavigation.swift @@ -501,7 +501,7 @@ struct AdwNavigationSplitView: View where Item: FlatNavigationItem { /// The sidebar. var sidebar: AnyView { - ScrollView { + Core.ScrollView { let selectedItem: Binding = Binding { AdwListItem(item: self.selectedItem).id } set: { newValue in diff --git a/Sources/View/Simple/ScrollView.swift b/Sources/View/Simple/ScrollView.swift new file mode 100644 index 0000000..c358487 --- /dev/null +++ b/Sources/View/Simple/ScrollView.swift @@ -0,0 +1,29 @@ +// +// ScrollView.swift +// Aparoksha +// +// Created by david-swift on 19.12.24. +// + +import Core + +/// The scroll view. +public struct ScrollView: SimpleView { + + /// The scroll view's content. + var content: Body + + /// The view's content. + public var view: Body { + Core.ScrollView { + content + } + } + + /// Initialize a scroll view. + /// - Parameter content: The scroll view's content. + public init(@ViewBuilder content: () -> Body) { + self.content = content() + } + +} diff --git a/Tests/Demo.swift b/Tests/Demo.swift index ea79ef2..ccdfcb0 100644 --- a/Tests/Demo.swift +++ b/Tests/Demo.swift @@ -167,5 +167,3 @@ enum GenericItem: String, Equatable, CustomStringConvertible, CaseIterable, Iden } // swiftlint:enable - - From 4eff4d4587e4ed23a1f59f3fb8632f4d59f94864 Mon Sep 17 00:00:00 2001 From: david-swift Date: Fri, 31 Jan 2025 17:48:44 +0100 Subject: [PATCH 12/17] Add support for macOS --- .gitignore | 3 +- Bundler.toml | 10 ++ Package.swift | 10 +- Sources/Menu/Submenu.swift | 46 +++++++ .../Enumerations/HorizontalAlignment.swift | 2 +- Sources/Model/Enumerations/Icon.swift | 41 ++++++- .../Enumerations/VerticalAlignment.swift | 2 +- .../Model/User Interface/App/Aparoksha.swift | 38 +++++- .../User Interface/App/AppInformation.swift | 14 +++ Sources/Scene/MenuBar.swift | 114 ++++++++++++++++++ Sources/Scene/Window.swift | 44 +++++-- Sources/View/AnyView+.swift | 28 +++++ Sources/View/Complex/Alert.swift | 29 ++++- Sources/View/Complex/Carousel.swift | 12 ++ Sources/View/Navigation/FlatNavigation.swift | 57 +++++++-- .../Navigation/HierarchicalNavigation.swift | 89 +++++++++++--- Sources/View/Simple/Button.swift | 6 + Sources/View/Simple/HStack.swift | 6 - Sources/View/Simple/Menu.swift | 20 ++- Sources/View/Simple/Text.swift | 19 +++ Sources/View/Simple/VStack.swift | 6 - Tests/Demo.swift | 20 ++- 22 files changed, 553 insertions(+), 63 deletions(-) create mode 100644 Bundler.toml create mode 100644 Sources/Menu/Submenu.swift create mode 100644 Sources/Scene/MenuBar.swift diff --git a/.gitignore b/.gitignore index 3c890f6..d3e2362 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,7 @@ /*.xcodeproj xcuserdata/ DerivedData/ -.swiftpm/config/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +/.swiftpm .netrc /Package.resolved .Ulysses-Group.plist diff --git a/Bundler.toml b/Bundler.toml new file mode 100644 index 0000000..265e800 --- /dev/null +++ b/Bundler.toml @@ -0,0 +1,10 @@ +format_version = 2 + +[apps.Demo] +product = 'Demo' +version = '0.1.0' +identifier = "dev.aparoksha.Demo" +minimum_macos_version = '13' + +[apps.Demo.plist] +CFBundleLocalizations = ["en", "de"] diff --git a/Package.swift b/Package.swift index f54e1b8..ca7d819 100644 --- a/Package.swift +++ b/Package.swift @@ -14,6 +14,8 @@ enum Framework: String { /// The WinUI backend. case winui = "WINUI" + /// The AppKit backend. + case appkit = "APPKIT" /// The libadwaita backend. case adwaita = "ADWAITA" @@ -32,6 +34,9 @@ var targetDependencies: [Target.Dependency] = [] #if os(Windows) /// The framework used for rendering. let framework = environmentFramework ?? .winui +#elseif os(macOS) +/// The framework used for rendering. +let framework = environmentFramework ?? .appkit #else /// The framework used for rendering. let framework = environmentFramework ?? .adwaita @@ -41,6 +46,9 @@ switch framework { case .winui: dependencies.append(.package(url: "https://git.aparoksha.dev/aparoksha/winui-swift", branch: "main")) targetDependencies.append(.product(name: "Core", package: "winui-swift")) +case .appkit: + dependencies.append(.package(url: "https://git.aparoksha.dev/aparoksha/macbackend", branch: "main")) + targetDependencies.append(.product(name: "Core", package: "macbackend")) case .adwaita: dependencies.append(.package(url: "https://git.aparoksha.dev/aparoksha/adwaita-swift", branch: "main")) targetDependencies.append(.product(name: "Core", package: "adwaita-swift")) @@ -50,7 +58,7 @@ case .adwaita: let package = Package( name: "Aparoksha", platforms: [ - .macOS(.v10_15), + .macOS(.v13), .iOS(.v13) ], products: [ diff --git a/Sources/Menu/Submenu.swift b/Sources/Menu/Submenu.swift new file mode 100644 index 0000000..66d96ea --- /dev/null +++ b/Sources/Menu/Submenu.swift @@ -0,0 +1,46 @@ +// +// Submenu.swift +// Aparoksha +// +// Created by david-swift on 07.12.2024. +// + +import Core + +/// A submenu widget. +public struct Submenu: SimpleView { + + /// The button's label. + var label = "" + /// An optional icon, displayed on WinUI. + var icon: Icon? + /// The button's action handler. + var content: Body + + // TODO: Add support for other platforms + /// The view. + public var view: Body { + #if APPKIT + Menu(label) { + content + } + #endif + } + + /// Initialize a submenu. + /// - Parameters: + /// - label: The buttons label. + /// - handler: The button's action handler. + public init(_ label: String, @ViewBuilder content: () -> Body) { + self.label = label + self.content = content() + } + + /// Set the menu's icon. + /// - Parameter icon: The icon. + /// - Returns: The menu button. + public func icon(_ icon: Icon?) -> Self { + modify { $0.icon = icon } + } + +} diff --git a/Sources/Model/Enumerations/HorizontalAlignment.swift b/Sources/Model/Enumerations/HorizontalAlignment.swift index b98208f..532f748 100644 --- a/Sources/Model/Enumerations/HorizontalAlignment.swift +++ b/Sources/Model/Enumerations/HorizontalAlignment.swift @@ -33,7 +33,7 @@ public enum HorizontalAlignment { .stretch } } - #else + #elseif ADWAITA /// The native alignment. var nativeAlignment: Core.Alignment { switch self { diff --git a/Sources/Model/Enumerations/Icon.swift b/Sources/Model/Enumerations/Icon.swift index 63389c4..b5d9c81 100644 --- a/Sources/Model/Enumerations/Icon.swift +++ b/Sources/Model/Enumerations/Icon.swift @@ -11,7 +11,7 @@ import Core public enum Icon: Equatable { /// A custom icon. - case custom(winui: Character, adwaita: String) + case custom(winui: Character, adwaita: String, appkit: String) /// The airplane icon. case airplane /// The edit icon. @@ -69,15 +69,50 @@ public enum Icon: Equatable { "\u{F8AC}" case .forward: "\u{F8AD}" - case let .custom(winui, _): + case let .custom(winui, _, _): winui } return .systemIcon(unicode: unicode) } + #elseif APPKIT + /// The native icon. + var nativeIcon: Core.Icon { + let name = switch self { + case .airplane: + "airplane" + case .edit: + "pencil" + case .headphones: + "headphones" + case .plus: + "plus" + case .copy: + "doc.on.doc" + case .navigation: + "sidebar.left" + case .warning: + "exclamationmark.triangle" + case .trash: + "trash" + case .play: + "play" + case .document: + "doc" + case .repeat: + "repeat" + case .back: + "backward" + case .forward: + "forward" + case let .custom(_, _, appkit): + appkit + } + return .system(name: name) + } #else /// The native icon. var nativeIcon: Core.Icon { - if case let .custom(_, adwaita) = self { + if case let .custom(_, adwaita, _) = self { return .custom(name: adwaita) } let icon: Core.Icon.DefaultIcon = switch self { diff --git a/Sources/Model/Enumerations/VerticalAlignment.swift b/Sources/Model/Enumerations/VerticalAlignment.swift index b104472..f2f770b 100644 --- a/Sources/Model/Enumerations/VerticalAlignment.swift +++ b/Sources/Model/Enumerations/VerticalAlignment.swift @@ -33,7 +33,7 @@ public enum VerticalAlignment { .stretch } } - #else + #elseif ADWAITA /// The native alignment. var nativeAlignment: Core.Alignment { switch self { diff --git a/Sources/Model/User Interface/App/Aparoksha.swift b/Sources/Model/User Interface/App/Aparoksha.swift index f5e4f4f..306b77d 100644 --- a/Sources/Model/User Interface/App/Aparoksha.swift +++ b/Sources/Model/User Interface/App/Aparoksha.swift @@ -17,6 +17,9 @@ public class Aparoksha: AppStorage { #if WINUI /// The native app type. typealias App = WinUIApp + #elseif APPKIT + /// The native app type. + typealias App = MacApp #else /// The native app type. typealias App = AdwaitaApp @@ -41,13 +44,17 @@ public class Aparoksha: AppStorage { /// - Parameters: /// - id: The reverse DNS style identifier. /// - name: The app name. - /// - settings: The settings label. + /// - settings: The settings label, usually "Settings". /// - about: The text for the app information, usually "About ". /// - developer: The developer. /// - version: The app's version. + /// - quit: The quit label, usually "Quit ". /// - icon: The app icon. /// - website: The app's website. + /// - websiteLabel: The label for the website button on macOS and Windows, for example "Website". /// - issues: The app's URL for issues. + /// - issuesLabel: The label for the issues button on macOS, for example "Issues". + /// - services: The text for the services menu, usually "Services". public init( id: String, name: String, @@ -55,9 +62,13 @@ public class Aparoksha: AppStorage { about: String, developer: String, version: String, + quit: String, icon: Icon? = nil, website: URL? = nil, - issues: URL? = nil + websiteLabel: String? = nil, + issues: URL? = nil, + issuesLabel: String? = nil, + services: String? = nil ) { #if WINUI app = .init() @@ -69,10 +80,14 @@ public class Aparoksha: AppStorage { developer: developer, version: version, website: website, + websiteLabel: websiteLabel, issues: issues, + issuesLabel: issuesLabel, icon: icon, about: about, - settings: settings + settings: settings, + services: services, + quit: quit ) } @@ -87,4 +102,21 @@ public class Aparoksha: AppStorage { app.quit() } + /// The hide menu's labels on macOS. + /// - Parameters: + /// - hideApp: Hide the app, usually "Hide ". + /// - hideOthers: Hide other apps, usually "Hide Others". + /// - showAll: Show all apps, usually "Show All". + /// - Returns: The app object. + public func hideMenu( + hideApp: String, + hideOthers: String, + showAll: String + ) -> Self { + appInformation.hide = hideApp + appInformation.hideOthers = hideOthers + appInformation.showAll = showAll + return self + } + } diff --git a/Sources/Model/User Interface/App/AppInformation.swift b/Sources/Model/User Interface/App/AppInformation.swift index 96933f8..b67a2fe 100644 --- a/Sources/Model/User Interface/App/AppInformation.swift +++ b/Sources/Model/User Interface/App/AppInformation.swift @@ -19,14 +19,28 @@ struct AppInformation { var version: String /// The app's website. var website: URL? + /// The label for the website. + var websiteLabel: String? /// The app's URL for issues. var issues: URL? + /// The label for the issues. + var issuesLabel: String? /// The app icon. var icon: Icon? /// The about action's label. var about: String /// The settings label. var settings: String + /// The services label. + var services: String? + /// The hide app label. + var hide: String? + /// The hide others label. + var hideOthers: String? + /// The show all label. + var showAll: String? + /// The quit label. + var quit: String #if ADWAITA /// The main menu for GNOME. diff --git a/Sources/Scene/MenuBar.swift b/Sources/Scene/MenuBar.swift new file mode 100644 index 0000000..69df682 --- /dev/null +++ b/Sources/Scene/MenuBar.swift @@ -0,0 +1,114 @@ +// +// MenuBar.swift +// Aparoksha +// +// Created by david-swift on 07.12.2024. +// + +#if APPKIT +import AppKit +#endif +import Core + +/// The menu bar control which will be displayed on macOS. +public struct MenuBar: AparokshaSceneElement { + + /// The menu bar's identifier. + public var id = "menu" + /// The menu bar's content. + var content: Body + + /// Initialize a menu bar. + /// - Parameter content: The menu bar's content. + public init(@ViewBuilder content: () -> Body = { [] }) { + self.content = content() + } + + #if APPKIT + /// Get the menu bar on macOS. + /// - Parameter app: The app. + /// - Returns: The menu bar. + func menuBar(app: Aparoksha) -> Core.MenuBar { + .init(id: id) { + content + } app: { + MenuButton(app.appInformation.about) { + app.app.showAboutWindow() + } + if let services = app.appInformation.services { + Divider() + ServicesMenu(services) + } + if let hide = app.appInformation.hide, + let others = app.appInformation.hideOthers, + let all = app.appInformation.showAll { + Divider() + Core.MenuButton(hide) { + app.app.hide() + } + .keyboardShortcut("h") + Core.MenuButton(others) { + app.app.hideOthers() + } + .keyboardShortcut(.init("h", alt: true)) + MenuButton(all) { + app.app.showAll() + } + } + Divider() + Core.MenuButton(app.appInformation.quit) { + app.quit() + } + .keyboardShortcut("q") + } help: { + if let website = app.appInformation.website, let label = app.appInformation.websiteLabel { + Core.MenuButton(label) { + _ = NSWorkspace.shared.open(website) + } + .keyboardShortcut(.init("?")) + } + if let issues = app.appInformation.issues, let label = app.appInformation.issuesLabel { + MenuButton(label) { + _ = NSWorkspace.shared.open(issues) + } + } + } + + } + #endif + + /// Set up the initial scene storages + /// - Parameter app: The app storage. + public func setupInitialContainers(app: Storage) where Storage: AppStorage { + #if APPKIT + if let aparoksha = app as? Aparoksha { + menuBar(app: aparoksha).setupInitialContainers(app: aparoksha.app) + } + #endif + } + + /// The scene storage. + /// - Parameter app: The app storage. + /// - Returns: The scene storage. + public func container(app: Storage) -> SceneStorage where Storage: AppStorage { + .init(id: "", pointer: nil) { } + } + + /// Update stored content. + /// - Parameters: + /// - storage: The storage to update. + /// - app: The app storage. + /// - updateProperties: Whether to update the properties. + public func update( + _ storage: SceneStorage, + app: Storage, + updateProperties: Bool + ) where Storage: AppStorage { + #if APPKIT + if let aparoksha = app as? Aparoksha { + menuBar(app: aparoksha).update(storage, app: app, updateProperties: updateProperties) + } + #endif + } + +} diff --git a/Sources/Scene/Window.swift b/Sources/Scene/Window.swift index d368736..f716d8d 100644 --- a/Sources/Scene/Window.swift +++ b/Sources/Scene/Window.swift @@ -10,13 +10,16 @@ import Core /// A window. public struct Window: AparokshaSceneElement { - #if WINUI - /// The native window type. - typealias NativeWindow = WinUIWindow - #else - /// The native window type. - typealias NativeWindow = AdwaitaWindow - #endif + #if WINUI + /// The native window type. + typealias NativeWindow = WinUIWindow + #elseif APPKIT + /// The native window type. + typealias NativeWindow = MacWindow + #else + /// The native window type. + typealias NativeWindow = AdwaitaWindow + #endif /// The window's identifier. public var id: String @@ -62,6 +65,7 @@ public struct Window: AparokshaSceneElement { /// The native window. var native: NativeWindow + #if !APPKIT /// Whether the default title bar is visible. var defaultTitlebar: Bool { get { @@ -71,6 +75,7 @@ public struct Window: AparokshaSceneElement { native.fields["default-titlebar"] = newValue } } + #endif /// Initialize a window. /// - Parameter native: The native window. @@ -213,6 +218,21 @@ public struct Window: AparokshaSceneElement { .extendContentIntoTitleBar(true, gap: app.appInformation.gap) .frame(width: width?.windowsValue, height: height?.windowsValue) } + #elseif APPKIT + /// The native window. + func nativeWindow(app: Aparoksha) -> Core.Window { + .init(title, id: id, open: open) { window in + WindowMainView( + content: content, + window: .init(native: window), + app: app, + toolbarItems: toolbarItems, + contentLabel: title, + contentIcon: icon + ) + } + .frame(width: width, height: height) + } #else /// The native window. func nativeWindow(app: Aparoksha) -> Core.Window { @@ -283,6 +303,9 @@ struct WindowMainView: View { } else { fullContent(app: app) } + #elseif APPKIT + // TODO: Toolbar for apps without navigation view + fullContent(app: app) #else if !window.defaultTitlebar { fullContent(app: app) @@ -332,6 +355,13 @@ struct ToolbarItem { visible: active && !settings ) } + #elseif APPKIT + /// The native button. + func button() -> AnyView { + DisabledWrapper(disabled: !active) { + Core.Button(title, icon: icon.nativeIcon, action: action) + } + } #else /// The native button. func button() -> AnyView { diff --git a/Sources/View/AnyView+.swift b/Sources/View/AnyView+.swift index 35bffee..edd1c67 100644 --- a/Sources/View/AnyView+.swift +++ b/Sources/View/AnyView+.swift @@ -17,6 +17,8 @@ extension AnyView { public func padding(_ padding: Int, edges: Set = .all) -> AnyView { #if WINUI ModifierWrapper(view: Grid { self }, margin: edges.nativeEdges.set(.init(padding.windowsValue))) + #elseif APPKIT + PaddingView(padding: .init(padding), edges: edges.nativeEdges, child: self) #else ModifierWrapper(content: self, padding: padding, edges: edges.nativeEdges) #endif @@ -28,6 +30,9 @@ extension AnyView { public func valign(_ alignment: VerticalAlignment) -> AnyView { #if WINUI ModifierWrapper(view: self, verticalAlignment: alignment.nativeAlignment) + #elseif APPKIT + // TODO: Set alignment macOS + self #else ModifierWrapper(content: self, valign: alignment.nativeAlignment) #endif @@ -39,6 +44,9 @@ extension AnyView { public func halign(_ alignment: HorizontalAlignment) -> AnyView { #if WINUI ModifierWrapper(view: self, horizontalAlignment: alignment.nativeAlignment) + #elseif APPKIT + // TODO: Set alignment macOS + self #else ModifierWrapper(content: self, halign: alignment.nativeAlignment) #endif @@ -50,6 +58,8 @@ extension AnyView { public func vexpand(_ expand: Bool = true) -> AnyView { #if WINUI self + #elseif APPKIT + self #else ModifierWrapper(content: self, vexpand: expand) #endif @@ -61,6 +71,8 @@ extension AnyView { public func hexpand(_ expand: Bool = true) -> AnyView { #if WINUI self + #elseif APPKIT + self #else ModifierWrapper(content: self, hexpand: expand) #endif @@ -80,6 +92,10 @@ extension AnyView { minWidth: .init(optional: minWidth?.windowsValue), minHeight: .init(optional: minHeight?.windowsValue) ) + #elseif APPKIT + FrameWrapper(minWidth: .init(optional: minWidth), minHeight: .init(optional: minHeight)) { + self + } #else ModifierWrapper( content: self, @@ -99,6 +115,10 @@ extension AnyView { view: self, maxWidth: .init(optional: maxWidth?.windowsValue) ) + #elseif APPKIT + FrameWrapper(maxWidth: .init(optional: maxWidth)) { + self + } #else Clamp(vertical: false) .child { @@ -118,6 +138,10 @@ extension AnyView { view: self, maxHeight: .init(optional: maxHeight?.windowsValue) ) + #elseif APPKIT + FrameWrapper(maxHeight: .init(optional: maxHeight)) { + self + } #else Clamp(vertical: true) .child { @@ -134,6 +158,10 @@ extension AnyView { Card { self } + #elseif APPKIT + Card { + self + } #else style("card") #endif diff --git a/Sources/View/Complex/Alert.swift b/Sources/View/Complex/Alert.swift index 6511375..6e2979c 100644 --- a/Sources/View/Complex/Alert.swift +++ b/Sources/View/Complex/Alert.swift @@ -19,7 +19,7 @@ public struct Alert: SimpleView { var title: String /// The content. var content: String - // TODO: Implement on Windows + // TODO: Restrict to certain controls (e.g. text fields) and implement on macOS & Windows /// The view content. var extraChild: Body? /// The identifier. @@ -61,6 +61,33 @@ public struct Alert: SimpleView { close.1() visible = false } + #elseif APPKIT + var dialog = Core.Alert( + title: title, + description: content, + isPresented: $visible, + child: child + ) + if let primary { + if primary.1 == .destructive { + dialog = dialog.destructiveButton(primary.0.0, default: primary.1 == .suggested, action: primary.0.1) + } else { + dialog = dialog.button(primary.0.0, default: primary.1 == .suggested, action: primary.0.1) + } + } + if let secondary { + if secondary.1 == .destructive { + dialog = dialog.destructiveButton( + secondary.0.0, + default: secondary.1 == .suggested, + action: secondary.0.1 + ) + } else { + dialog = dialog.button(secondary.0.0, default: secondary.1 == .suggested, action: secondary.0.1) + } + } + dialog = dialog.cancelButton(close.0, action: close.1) + return dialog #else var dialog = AlertDialog( visible: $visible, diff --git a/Sources/View/Complex/Carousel.swift b/Sources/View/Complex/Carousel.swift index a27984f..4b8b763 100644 --- a/Sources/View/Complex/Carousel.swift +++ b/Sources/View/Complex/Carousel.swift @@ -19,6 +19,18 @@ public struct Carousel: SimpleView where Element: Identifiable { public var view: Body { #if WINUI Core.FlipView(elements, content: content) + #elseif APPKIT + Core.ScrollView(.horizontal) { + Core.HStack { + Core.Spacer() + Core.ForEach(elements, horizontal: true) { element in + content(element) + } + Core.Spacer() + } + .padding(20, edges: .horizontal) + } + .hideScrollIndicators() #else Core.Carousel(elements, content: content) #endif diff --git a/Sources/View/Navigation/FlatNavigation.swift b/Sources/View/Navigation/FlatNavigation.swift index e096363..1199ff6 100644 --- a/Sources/View/Navigation/FlatNavigation.swift +++ b/Sources/View/Navigation/FlatNavigation.swift @@ -50,6 +50,34 @@ public struct FlatNavigation: View where Item: FlatNavigationItem { .onAppear { app?.removeDefaultTitlebar() } + #elseif APPKIT + Core.NavigationSplitView { + Core.List( + items.map { ListItem(item: $0) }, + selection: .init { + selectedItem.description + } set: { newValue in + if let item = items.first(where: { $0.description == newValue }) { + selectedItem = item + } + } + ) { item in + Core.Label(item.item.description, icon: item.item.icon.nativeIcon) + } + } detail: { + ToolbarView( + child: ToolbarView(child: content, type: .end) { + (toolbarItems ?? []).filter { !$0.leftAlign }.map { item in + item.button() + } + }, + type: .start + ) { + (toolbarItems ?? []).filter { $0.leftAlign }.map { item in + item.button() + } + } + } #else if useViewSwitcher { AdwViewSwitcher( @@ -198,12 +226,11 @@ public struct FlatNavigation: View where Item: FlatNavigationItem { /// - stack: The hierarchical navigation stack. /// - content: The hierarchical content. /// - Returns: The complex view. - @ViewBuilder public func complexNavigation( stack: Binding>, @ViewBuilder content: @escaping (StackItem) -> Body ) -> Body where StackItem: Equatable, StackItem: CustomStringConvertible { - #if WINUI + #if WINUI || APPKIT let flat = Self(items, selection: $selectedItem) { HierarchicalNavigation(stack: stack, initialPageLabel: navigationTitle ?? "") { item in content(item) @@ -214,12 +241,14 @@ public struct FlatNavigation: View where Item: FlatNavigationItem { .forceSplitView(forceSidebar) .navigationTitle(navigationTitle) if let primary = primaryActionProperties { - flat - .primaryAction(primary.title, icon: primary.icon, disabled: !primary.active) { - primary.action() - } + return [ + flat + .primaryAction(primary.title, icon: primary.icon, disabled: !primary.active) { + primary.action() + } + ] } else { - flat + return [flat] } #else HierarchicalNavigation(stack: stack, initialPageLabel: navigationTitle ?? "") { item in @@ -368,7 +397,7 @@ struct WinUINavigationItem: NavigationViewItem { } } -#else +#elseif ADWAITA /// The view switcher for GNOME. struct AdwViewSwitcher: View where Item: FlatNavigationItem { @@ -503,13 +532,13 @@ struct AdwNavigationSplitView: View where Item: FlatNavigationItem { var sidebar: AnyView { Core.ScrollView { let selectedItem: Binding = Binding { - AdwListItem(item: self.selectedItem).id + ListItem(item: self.selectedItem).id } set: { newValue in if let item = items.first(where: { $0.description == newValue }) { self.selectedItem = item } } - List(items.map { AdwListItem(item: $0) }, selection: selectedItem) { item in + List(items.map { ListItem(item: $0) }, selection: selectedItem) { item in let standardPadding = 6 let doublePadding = 12 HStack { @@ -568,17 +597,19 @@ struct AdwViewSwitcherItem: ViewSwitcherOption where Item: FlatNavigationI } } +#endif -/// An item for the Adwaita list in the sidebar. -struct AdwListItem: Identifiable where Item: FlatNavigationItem { +/// An item for the list in the sidebar. +struct ListItem: Identifiable, CustomStringConvertible where Item: FlatNavigationItem { /// The item. var item: Item /// The item's identifier. var id: String { item.description } + /// The item's description. + var description: String { item.description } } -#endif /// A dynamic item for the flat navigation view. public protocol DynamicFlatNavigationItem: CustomStringConvertible { diff --git a/Sources/View/Navigation/HierarchicalNavigation.swift b/Sources/View/Navigation/HierarchicalNavigation.swift index c071a69..9218a1e 100644 --- a/Sources/View/Navigation/HierarchicalNavigation.swift +++ b/Sources/View/Navigation/HierarchicalNavigation.swift @@ -21,7 +21,23 @@ public struct HierarchicalNavigation: View where Item: Equatable, Item: Cu /// Hide the toolbar. var hideToolbar = false - #if ADWAITA + #if WinUI + /// The navigation path. + @Environment("path") + private var path: Binding<[NavigationSelection]>? + /// The currently selected item. + var selectedItem: Item? { + if case let .custom(item: item) = path?.wrappedValue.last { + return item.item as? Item + } + return nil + } +#elseif APPKIT + /// The navigation stack. + @State private var macStack: Core.NavigationPath = .init() + /// The item presented in a dialog. + @State private var dialog: Item? + #else /// The Adwaita stack. @State private var adwStack: NavigationView.NavigationStack = .init() /// The item presented in a dialog. @@ -37,17 +53,6 @@ public struct HierarchicalNavigation: View where Item: Equatable, Item: Cu /// The window object. @Environment("window") private var window: Window.Window? - #else - /// The navigation path. - @Environment("path") - private var path: Binding<[NavigationSelection]>? - /// The currently selected item. - var selectedItem: Item? { - if case let .custom(item: item) = path?.wrappedValue.last { - return item.item as? Item - } - return nil - } #endif /// The view. @@ -74,6 +79,33 @@ public struct HierarchicalNavigation: View where Item: Equatable, Item: Cu initialView } } + #elseif APPKIT + /// The native AppKit view. + @ViewBuilder var nativeView: Body { + SheetWrapper( + isPresented: .init { + dialog != nil + } set: { newValue in + if !newValue { dialog = nil } + } + ) { + Core.NavigationStack(path: $macStack) { component in + childView(component) + } initialView: { + initialView + } + .onUpdate { + update() + } + } dialog: { + VStack { + if let dialog { + childView(dialog) + } + } + .freeze(dialog == nil) + } + } #else /// The native Adwaita view. @ViewBuilder var nativeView: Body { @@ -145,6 +177,8 @@ public struct HierarchicalNavigation: View where Item: Equatable, Item: Cu case .pop: _ = path?.wrappedValue.popLast() } + #elseif APPKIT + executeMacAction(action: action) #else Task { switch action { @@ -166,13 +200,35 @@ public struct HierarchicalNavigation: View where Item: Equatable, Item: Cu #endif } +#if APPKIT + /// Execute the action on macOS. + /// - Parameter action: The action. + func executeMacAction(action: NavigationStack.Action) { + Task { @MainActor in + switch action { + case let .push(item: item, target: target): + switch target { + case .page: + macStack.push(item) + case .dialog: + dialog = item + } + case .pop: + if dialog != nil { + dialog = nil + } else { + macStack.pop() + } + } + } + } + #endif + + #if ADWAITA /// The content for an item. /// - Parameter item: The item. /// - Returns: The content. func content(item: Item?) -> AnyView { - #if WINUI - [] - #else ToolbarView() .content { if let item { @@ -182,8 +238,9 @@ public struct HierarchicalNavigation: View where Item: Equatable, Item: Cu .top { HeaderBar.empty() } - #endif + .freeze(item == nil) } + #endif /// Hide the toolbar on Adwaita. /// - Returns: The view. diff --git a/Sources/View/Simple/Button.swift b/Sources/View/Simple/Button.swift index 26f0e7f..0a53b1f 100644 --- a/Sources/View/Simple/Button.swift +++ b/Sources/View/Simple/Button.swift @@ -24,18 +24,24 @@ public struct Button: SimpleView { if let label, let icon { #if WINUI return [Core.Button(icon: icon.nativeIcon, label: label, action: handler)] + #elseif APPKIT + return [Core.Button(label, icon: icon.nativeIcon, action: handler)] #else return [Core.Button(label, icon: icon.nativeIcon, handler: handler).style("pill", active: pill)] #endif } else if let label { #if WINUI return [Core.Button(label, action: handler)] + #elseif APPKIT + return [Core.Button(label, action: handler)] #else return [Core.Button(label, handler: handler).style("pill", active: pill)] #endif } else if let icon { #if WINUI return [Core.Button(icon: icon.nativeIcon, action: handler)] + #elseif APPKIT + return [Core.Button(icon: icon.nativeIcon, action: handler)] #else return [Core.Button(icon: icon.nativeIcon, handler: handler).style("pill", active: pill)] #endif diff --git a/Sources/View/Simple/HStack.swift b/Sources/View/Simple/HStack.swift index 1980446..011355e 100644 --- a/Sources/View/Simple/HStack.swift +++ b/Sources/View/Simple/HStack.swift @@ -15,15 +15,9 @@ public struct HStack: SimpleView { /// The content view. public var view: Body { - #if WINUI Core.HStack { content } - #else - Core.HStack { - content - } - #endif } /// Initialize the horizontal stack. diff --git a/Sources/View/Simple/Menu.swift b/Sources/View/Simple/Menu.swift index 001b8d3..1597673 100644 --- a/Sources/View/Simple/Menu.swift +++ b/Sources/View/Simple/Menu.swift @@ -8,8 +8,12 @@ import Core /// The button view. -public struct Menu: SimpleView { +public struct Menu: View { + #if APPKIT + /// Whether the menu is visible. + @State private var showMenu: Signal = .init() + #endif /// The button's label. var label: String? /// The button's icon. @@ -21,6 +25,19 @@ public struct Menu: SimpleView { /// The content view. public var view: Body { + #if APPKIT + // swiftlint:disable trailing_closure + MenuWrapper( + content: { + Core.Button(label ?? "", icon: icon?.nativeIcon) { + showMenu.signal() + } + }, + present: $showMenu, + menu: { menu } + ) + // swiftlint:enable trailing_closure + #else if let label, let icon { #if WINUI // TODO: Menu with icon @@ -43,6 +60,7 @@ public struct Menu: SimpleView { #endif } return [] + #endif } /// Initialize a button. diff --git a/Sources/View/Simple/Text.swift b/Sources/View/Simple/Text.swift index f08d9ea..6a5af41 100644 --- a/Sources/View/Simple/Text.swift +++ b/Sources/View/Simple/Text.swift @@ -21,6 +21,9 @@ public struct Text: SimpleView { #if WINUI Core.Text(label) .style(style.style) + #elseif APPKIT + Core.Text(label) + .font(style.font) #else Core.Text(label) .style(style.style) @@ -59,6 +62,22 @@ public struct Text: SimpleView { .caption } } + #elseif APPKIT + /// The AppKit font. + var font: Core.Font { + switch self { + case .title1: + .title + case .title2: + .title2 + case .title3: + .title3 + case .body: + .body + case .caption: + .caption + } + } #else /// The Adwaita style. var style: String { diff --git a/Sources/View/Simple/VStack.swift b/Sources/View/Simple/VStack.swift index 72c5225..dcada99 100644 --- a/Sources/View/Simple/VStack.swift +++ b/Sources/View/Simple/VStack.swift @@ -15,15 +15,9 @@ public struct VStack: SimpleView { /// The content view. public var view: Body { - #if WINUI Core.VStack { content } - #else - Core.VStack { - content - } - #endif } /// Initialize the vertical stack. diff --git a/Tests/Demo.swift b/Tests/Demo.swift index ccdfcb0..f6d72f4 100644 --- a/Tests/Demo.swift +++ b/Tests/Demo.swift @@ -19,11 +19,23 @@ struct Demo: App { about: "About Demo", developer: "Aparoksha", version: "main", + quit: "Quit Demo", website: .init(string: "https://aparoksha.dev/")!, - issues: .init(string: "https://forums.aparoksha.dev/")! + websiteLabel: "Website", + issues: .init(string: "https://forums.aparoksha.dev/")!, + issuesLabel: "Issues", + services: "Services" ) + .hideMenu(hideApp: "Hide Demo", hideOthers: "Hide Others", showAll: "Show All") var scene: Scene { + MenuBar { + Submenu("Test") { + MenuButton("Test") { + print("Test") + } + } + } Window("Demo", icon: .headphones, id: "main") { ContentView() } @@ -31,6 +43,10 @@ struct Demo: App { Text("Hi") .padding(10) } + .frame(width: .constant(300), height: .constant(200)) + .leadingToolbarItem("Test", icon: .airplane) { + print("Plane") + } } } @@ -141,7 +157,7 @@ enum Item: String, FlatNavigationItem, CaseIterable, Equatable, Codable { case .navigation: .navigation case .carousel: - .custom(winui: "🤣", adwaita: "dev.aparoksha.Demo.carousel-symbolic") + .custom(winui: "🤣", adwaita: "dev.aparoksha.Demo.carousel-symbolic", appkit: "slider.horizontal.below.rectangle") case .alert: .warning } From b0f18984b241153590462c828bfb0e7c4d98d9ab Mon Sep 17 00:00:00 2001 From: david-swift Date: Fri, 31 Jan 2025 19:21:11 +0100 Subject: [PATCH 13/17] Add support for primary action on macOS --- Sources/View/Navigation/FlatNavigation.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/View/Navigation/FlatNavigation.swift b/Sources/View/Navigation/FlatNavigation.swift index 1199ff6..515e33c 100644 --- a/Sources/View/Navigation/FlatNavigation.swift +++ b/Sources/View/Navigation/FlatNavigation.swift @@ -64,6 +64,13 @@ public struct FlatNavigation: View where Item: FlatNavigationItem { ) { item in Core.Label(item.item.description, icon: item.item.icon.nativeIcon) } + .bottomAction( + primaryActionProperties?.title ?? "", + icon: primaryActionProperties?.icon.nativeIcon ?? .system(name: ""), + visible: primaryActionProperties?.active ?? false + ) { + primaryActionProperties?.action() + } } detail: { ToolbarView( child: ToolbarView(child: content, type: .end) { From 7a01923017367e8f5994435918c6839f2c3fe2ff Mon Sep 17 00:00:00 2001 From: david-swift Date: Fri, 31 Jan 2025 19:25:22 +0100 Subject: [PATCH 14/17] Move from custom extra child to text fields in alerts --- Sources/View/Complex/Alert.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Sources/View/Complex/Alert.swift b/Sources/View/Complex/Alert.swift index 6e2979c..73937f4 100644 --- a/Sources/View/Complex/Alert.swift +++ b/Sources/View/Complex/Alert.swift @@ -19,9 +19,6 @@ public struct Alert: SimpleView { var title: String /// The content. var content: String - // TODO: Restrict to certain controls (e.g. text fields) and implement on macOS & Windows - /// The view content. - var extraChild: Body? /// The identifier. var id: String /// The child view. @@ -30,6 +27,8 @@ public struct Alert: SimpleView { var primary: (Button, Style)? /// The secondary option. var secondary: (Button, Style)? + /// The text field. + var textField: (String, Binding)? /// The close option. var close: Button @@ -86,6 +85,9 @@ public struct Alert: SimpleView { dialog = dialog.button(secondary.0.0, default: secondary.1 == .suggested, action: secondary.0.1) } } + if let textField { + dialog = dialog.textField(textField.0, text: textField.1) + } dialog = dialog.cancelButton(close.0, action: close.1) return dialog #else @@ -163,11 +165,13 @@ public struct Alert: SimpleView { /// Set the content view in the alert. /// - Parameters: - /// - child: The view. - public func extraChild( - @ViewBuilder child: () -> Body + /// - label: The text field's label. + /// - text: The text content. + public func textField( + _ label: String, + text: Binding ) -> Self { - modify { $0.extraChild = child() } + modify { $0.textField = (label, text) } } } From 082c83aec513fb728c9aec6e3eba0f7b80f64512 Mon Sep 17 00:00:00 2001 From: david-swift Date: Fri, 31 Jan 2025 19:33:29 +0100 Subject: [PATCH 15/17] Add missing information about macOS --- README.md | 3 +++ Sources/Model/Enumerations/Framework.swift | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/README.md b/README.md index 36b8b75..72e0e47 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ which render native widgets on each supported platform. The following platforms are currently supported: - GNOME (libadwaita/GTK) +- macOS (AppKit/SwiftUI) - Windows (WinUI) Discuss adding your backends via an issue or [a new discussion](https://forums.aparoksha.dev/t/projects). @@ -46,6 +47,7 @@ The framework for rendering can be selected explicitly by setting the `APAROKSHA Otherwise, the platform's default will be used. - `WINUI` for the WinUI framework (default on Windows) +- `APPKIT` for the AppKit framework (default on macOS) - `ADWAITA` for the libadwaita framework (default on any other platform) ## Thanks @@ -53,6 +55,7 @@ Otherwise, the platform's default will be used. ### Backends - [adwaita-swift](https://git.aparoksha.dev/aparoksha/adwaita-swift) is a dependency when selecting the ADWAITA framework +- [macbackend](https://git.aparoksha.dev/aparoksha/macbackend) is a dependency when selecting the APPKIT framework - [winui-swift](https://git.aparoksha.dev/aparoksha/winui-swift) is a dependency when selecting the WINUI framework ### Other Thanks diff --git a/Sources/Model/Enumerations/Framework.swift b/Sources/Model/Enumerations/Framework.swift index 95bb8aa..7e55b38 100644 --- a/Sources/Model/Enumerations/Framework.swift +++ b/Sources/Model/Enumerations/Framework.swift @@ -10,6 +10,8 @@ public enum Framework: Equatable { /// The WinUI backend. case winui + /// The AppKit backend. + case appkit /// The libadwaita backend. case adwaita @@ -17,6 +19,8 @@ public enum Framework: Equatable { public static var current: Self { #if WINUI .winui + #elseif APPKIT + .appkit #else .adwaita #endif From 7b38fd83813f2e505e07817118d32ca01257c58f Mon Sep 17 00:00:00 2001 From: david-swift Date: Tue, 11 Feb 2025 07:07:42 +0100 Subject: [PATCH 16/17] Implement changes on Windows --- .../User Interface/App/AppInformation.swift | 6 ++ Sources/Scene/Window.swift | 66 +++++++++++++++---- Sources/View/Complex/Alert.swift | 4 +- Sources/View/Navigation/FlatNavigation.swift | 16 +++-- .../Navigation/HierarchicalNavigation.swift | 4 +- 5 files changed, 76 insertions(+), 20 deletions(-) diff --git a/Sources/Model/User Interface/App/AppInformation.swift b/Sources/Model/User Interface/App/AppInformation.swift index b67a2fe..f39785f 100644 --- a/Sources/Model/User Interface/App/AppInformation.swift +++ b/Sources/Model/User Interface/App/AppInformation.swift @@ -41,6 +41,12 @@ struct AppInformation { var showAll: String? /// The quit label. var quit: String + #if WINUI + /// Whether the default title bar is visible in the different window types. + var defaultTitlebars: [String: Bool] = [:] + /// Whether the gap is visible in the different window types. + var gaps: [String: Bool] = [:] + #endif #if ADWAITA /// The main menu for GNOME. diff --git a/Sources/Scene/Window.swift b/Sources/Scene/Window.swift index f716d8d..f0c9313 100644 --- a/Sources/Scene/Window.swift +++ b/Sources/Scene/Window.swift @@ -62,31 +62,63 @@ public struct Window: AparokshaSceneElement { /// The Aparoksha window object. public struct Window { + /// The window's identifier. + var id: String /// The native window. var native: NativeWindow + /// The app object. + var app: Aparoksha + + #if WINUI + /// Whether to display the gap left to the title bar. + var gap: Bool { + get { + app.appInformation.gaps[id] ?? false + } + nonmutating set { + app.appInformation.gaps[id] = newValue + } + } + #endif #if !APPKIT /// Whether the default title bar is visible. var defaultTitlebar: Bool { get { + #if WINUI + app.appInformation.defaultTitlebars[id] ?? true + #else native.fields["default-titlebar"] as? Bool ?? true + #endif } nonmutating set { + #if WINUI + if id == "second" { + print(newValue) + } + app.appInformation.defaultTitlebars[id] = newValue + #else native.fields["default-titlebar"] = newValue + #endif } } #endif /// Initialize a window. - /// - Parameter native: The native window. - init(native: NativeWindow) { + /// - Parameters: + /// - id: The window's identifier. + /// - native: The native window. + /// - app: The app object. + init(id: String, native: NativeWindow, app: Aparoksha) { + self.id = id self.native = native + self.app = app } /// Close the window. public func close() { #if WINUI - // TODO: Implement + try? native.close() #else native.close() #endif @@ -206,16 +238,16 @@ public struct Window: AparokshaSceneElement { func nativeWindow(app: Aparoksha) -> Core.Window { .init(title, id: id, open: open) { window in WindowMainView( + id: id, content: content, - window: .init(native: window), - defaultTitlebar: app.appInformation.defaultTitlebar, + window: window, app: app, toolbarItems: toolbarItems, contentLabel: title, contentIcon: icon ) } - .extendContentIntoTitleBar(true, gap: app.appInformation.gap) + .extendContentIntoTitleBar(true, gap: app.appInformation.gaps[id] ?? false) .frame(width: width?.windowsValue, height: height?.windowsValue) } #elseif APPKIT @@ -223,8 +255,9 @@ public struct Window: AparokshaSceneElement { func nativeWindow(app: Aparoksha) -> Core.Window { .init(title, id: id, open: open) { window in WindowMainView( + id: id, content: content, - window: .init(native: window), + window: window, app: app, toolbarItems: toolbarItems, contentLabel: title, @@ -238,8 +271,9 @@ public struct Window: AparokshaSceneElement { func nativeWindow(app: Aparoksha) -> Core.Window { .init(id: id, open: open) { window in WindowMainView( + id: id, content: content, - window: .init(native: window), + window: window, app: app, toolbarItems: toolbarItems, contentLabel: title, @@ -260,10 +294,12 @@ struct WindowMainView: View { /// Whether to show the about dialog on GNOME. @State private var showAbout = false + /// The window's id. + var id: String /// The window's content. var content: Body /// Whether to show the default title bar. - var window: Window.Window + var window: Window.NativeWindow /// The app. var app: Aparoksha /// The toolbar items. @@ -273,20 +309,26 @@ struct WindowMainView: View { /// The content's icon. var contentIcon: Icon + var aparokshaWindow: Window.Window { + .init(id: id, native: window, app: app) + } + /// The main view. var view: Body { contentView - .environment("window", data: window) + .environment("window", data: aparokshaWindow) } /// The content. @ViewBuilder var contentView: Body { #if WINUI - if defaultTitlebar { + if aparokshaWindow.defaultTitlebar { let item = ContentItem(description: contentLabel, icon: contentIcon) FlatNavigation([item], selection: .constant(item)) { fullContent(app: app) + .environment("default-titlebar", data: false) } + .environment("default-titlebar", data: true) .modifyContent(NavigationView.self) { view in view .customContent { @@ -307,7 +349,7 @@ struct WindowMainView: View { // TODO: Toolbar for apps without navigation view fullContent(app: app) #else - if !window.defaultTitlebar { + if !aparokshaWindow.defaultTitlebar { fullContent(app: app) } else { ToolbarView() diff --git a/Sources/View/Complex/Alert.swift b/Sources/View/Complex/Alert.swift index 73937f4..3c5ec72 100644 --- a/Sources/View/Complex/Alert.swift +++ b/Sources/View/Complex/Alert.swift @@ -42,7 +42,9 @@ public struct Alert: SimpleView { #if WINUI var dialog = ContentDialog(visible: $visible, child: child, id: id, title: title) { Text(content) - extraChild + if let textField { + Core.TextBox(textField.0, text: textField.1) + } } if let primary { dialog = dialog.primaryResponse(primary.0.0, suggested: primary.1 == .suggested) { diff --git a/Sources/View/Navigation/FlatNavigation.swift b/Sources/View/Navigation/FlatNavigation.swift index 515e33c..04e0845 100644 --- a/Sources/View/Navigation/FlatNavigation.swift +++ b/Sources/View/Navigation/FlatNavigation.swift @@ -27,6 +27,9 @@ public struct FlatNavigation: View where Item: FlatNavigationItem { /// The window object. @Environment("window") private var window: Window.Window? + /// Whether the navigation view is in the default title bar. + @Environment("default-titlebar") + private var defaultTitlebar: Bool? /// The items. var items: [Item] /// The content view. @@ -48,7 +51,10 @@ public struct FlatNavigation: View where Item: FlatNavigationItem { #if WINUI winView .onAppear { - app?.removeDefaultTitlebar() + if !(defaultTitlebar ?? true) { + window?.defaultTitlebar = false + StateManager.updateViews(force: true) + } } #elseif APPKIT Core.NavigationSplitView { @@ -126,9 +132,9 @@ public struct FlatNavigation: View where Item: FlatNavigationItem { content: content ) .onUpdate { - if app?.appInformation.gap == useViewSwitcher { + if window?.gap == useViewSwitcher { Task { @MainActor in - app?.appInformation.gap = !useViewSwitcher + window?.gap = !useViewSwitcher StateManager.updateViews(force: true) } } @@ -355,8 +361,8 @@ struct WinNavigationView: View where Item: FlatNavigationItem { icon: .systemIcon(unicode: "\u{E946}"), description: info.developer ) { - if let website = info.website { - HyperlinkButton(website.absoluteString, url: website.absoluteString) + if let website = info.website, let label = info.websiteLabel { + HyperlinkButton(label, url: website.absoluteString) } } } diff --git a/Sources/View/Navigation/HierarchicalNavigation.swift b/Sources/View/Navigation/HierarchicalNavigation.swift index 9218a1e..bf1df7e 100644 --- a/Sources/View/Navigation/HierarchicalNavigation.swift +++ b/Sources/View/Navigation/HierarchicalNavigation.swift @@ -21,7 +21,7 @@ public struct HierarchicalNavigation: View where Item: Equatable, Item: Cu /// Hide the toolbar. var hideToolbar = false - #if WinUI + #if WINUI /// The navigation path. @Environment("path") private var path: Binding<[NavigationSelection]>? @@ -32,7 +32,7 @@ public struct HierarchicalNavigation: View where Item: Equatable, Item: Cu } return nil } -#elseif APPKIT + #elseif APPKIT /// The navigation stack. @State private var macStack: Core.NavigationPath = .init() /// The item presented in a dialog. From 0c2ac1ca57951e25eb17f348750814c301e1e5e7 Mon Sep 17 00:00:00 2001 From: david-swift Date: Tue, 11 Feb 2025 18:49:35 +0100 Subject: [PATCH 17/17] Implement changes on GNOME --- Sources/View/Complex/Alert.swift | 10 ++++++++++ Sources/View/Navigation/FlatNavigation.swift | 14 ++++++++------ Tests/Demo.swift | 4 +++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Sources/View/Complex/Alert.swift b/Sources/View/Complex/Alert.swift index 3c5ec72..5585e3d 100644 --- a/Sources/View/Complex/Alert.swift +++ b/Sources/View/Complex/Alert.swift @@ -93,6 +93,16 @@ public struct Alert: SimpleView { dialog = dialog.cancelButton(close.0, action: close.1) return dialog #else + let extraChild: Body? + if let textField { + extraChild = [ + Form { + EntryRow(textField.0, text: textField.1) + } + ] + } else { + extraChild = nil + } var dialog = AlertDialog( visible: $visible, child: child, diff --git a/Sources/View/Navigation/FlatNavigation.swift b/Sources/View/Navigation/FlatNavigation.swift index 04e0845..1a1f488 100644 --- a/Sources/View/Navigation/FlatNavigation.swift +++ b/Sources/View/Navigation/FlatNavigation.swift @@ -264,12 +264,14 @@ public struct FlatNavigation: View where Item: FlatNavigationItem { return [flat] } #else - HierarchicalNavigation(stack: stack, initialPageLabel: navigationTitle ?? "") { item in - content(item) - } initialView: { - self - } - .hideDefaultToolbar() + [ + HierarchicalNavigation(stack: stack, initialPageLabel: navigationTitle ?? "") { item in + content(item) + } initialView: { + self + } + .hideDefaultToolbar() + ] #endif } diff --git a/Tests/Demo.swift b/Tests/Demo.swift index f6d72f4..b1ed82a 100644 --- a/Tests/Demo.swift +++ b/Tests/Demo.swift @@ -59,6 +59,7 @@ struct ContentView: WindowView { @State private var forceSplitView = true @State private var stack: NavigationStack = .init() @State private var showAlert = false + @State private var text = "" var view: Body { FlatNavigation(Item.allCases, selection: $selectedItem) { @@ -128,9 +129,10 @@ struct ContentView: WindowView { .alert(visible: $showAlert, title: "Sample Alert", content: "This is the main content", closeLabel: "Close") { print("Close") } - .primaryResponse("Primary", style: .suggested) { + .primaryResponse(text, style: .suggested) { print("Primary") } + .textField("Hello", text: $text) } func window(_ window: Window) -> Window {