Merge pull request 'macOS & Simple Views' (#5) from devel into main
Some checks failed
Deploy Docs / publish (push) Has been cancelled
SwiftLint / SwiftLint (push) Has been cancelled

Reviewed-on: #5
This commit is contained in:
david-swift 2025-02-11 19:05:08 +01:00
commit 357f1890b3
27 changed files with 957 additions and 118 deletions

3
.gitignore vendored
View File

@ -4,8 +4,7 @@
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
/.swiftpm
.netrc
/Package.resolved
.Ulysses-Group.plist

10
Bundler.toml Normal file
View File

@ -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"]

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 5 2 c -1.089844 0 -2 0.910156 -2 2 v 5 c 0 1.089844 0.910156 2 2 2 h 6 c 1.089844 0 2 -0.910156 2 -2 v -5 c 0 -1.089844 -0.910156 -2 -2 -2 z m -3 1 c -1.089844 0 -2 0.910156 -2 2 v 3 c 0 1.089844 0.910156 2 2 2 z m 12 0 v 7 c 1.089844 0 2 -0.910156 2 -2 v -3 c 0 -1.089844 -0.910156 -2 -2 -2 z m -9 1 h 6 v 5 h -6 z m 0 8 c -0.550781 0 -1 0.449219 -1 1 s 0.449219 1 1 1 s 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 z m 3 0 c -0.550781 0 -1 0.449219 -1 1 s 0.449219 1 1 1 s 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 z m 3 0 c -0.550781 0 -1 0.449219 -1 1 s 0.449219 1 1 1 s 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 z m 0 0" fill="#222222"/></svg>

After

Width:  |  Height:  |  Size: 771 B

View File

@ -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: [

View File

@ -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

View File

@ -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 }
}
}

View File

@ -0,0 +1,51 @@
//
// 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 AppKit backend.
case appkit
/// The libadwaita backend.
case adwaita
/// The currently selected framework.
public static var current: Self {
#if WINUI
.winui
#elseif APPKIT
.appkit
#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.
/// - modifyOtherwise: The modification for other frameworks.
/// - Returns: The view.
public func modify(
frameworks: Framework...,
modify: (Self) -> AnyView,
else modifyOtherwise: (Self) -> AnyView = { $0 }
) -> AnyView {
if frameworks.contains(Framework.current) {
modify(self)
} else {
modifyOtherwise(self)
}
}
}

View File

@ -33,7 +33,7 @@ public enum HorizontalAlignment {
.stretch
}
}
#else
#elseif ADWAITA
/// The native alignment.
var nativeAlignment: Core.Alignment {
switch self {

View File

@ -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.
@ -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:
@ -73,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 {
@ -99,8 +130,6 @@ public enum Icon: Equatable {
.editCopy
case .navigation:
.sidebarShow
case .panel:
.panelCenter
case .warning:
.dialogWarning
case .trash:

View File

@ -33,7 +33,7 @@ public enum VerticalAlignment {
.stretch
}
}
#else
#elseif ADWAITA
/// The native alignment.
var nativeAlignment: Core.Alignment {
switch self {

View File

@ -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 <App>".
/// - developer: The developer.
/// - version: The app's version.
/// - quit: The quit label, usually "Quit <App>".
/// - 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,12 +102,21 @@ public class Aparoksha: AppStorage {
app.quit()
}
/// Remove the default title bar.
func removeDefaultTitlebar() {
appInformation.defaultTitlebar = false
Task {
StateManager.updateViews(force: true)
}
/// The hide menu's labels on macOS.
/// - Parameters:
/// - hideApp: Hide the app, usually "Hide <App>".
/// - 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
}
}

View File

@ -19,23 +19,35 @@ 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 WINUI
/// Whether the gap is active.
var gap = false
/// 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
/// 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.

114
Sources/Scene/MenuBar.swift Normal file
View File

@ -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<Storage>(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<Storage>(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>(
_ 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
}
}

View File

@ -10,6 +10,17 @@ import Core
/// A window.
public struct Window: AparokshaSceneElement {
#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
/// The window's title.
@ -48,6 +59,73 @@ public struct Window: AparokshaSceneElement {
self.open = open
}
/// 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.
/// - 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
try? native.close()
#else
native.close()
#endif
}
}
/// Set up the initial containers.
/// - Parameter app: The app storage.
public func setupInitialContainers<Storage>(app: Storage) where Storage: AppStorage {
@ -158,26 +236,44 @@ 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(
id: id,
content: content,
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
/// The native window.
func nativeWindow(app: Aparoksha) -> Core.Window {
.init(title, id: id, open: open) { window in
WindowMainView(
id: id,
content: content,
window: window,
app: app,
toolbarItems: toolbarItems,
contentLabel: title,
contentIcon: icon
)
}
.frame(width: width, height: height)
}
#else
/// The native window.
func nativeWindow(app: Aparoksha) -> Core.Window {
.init(id: id, open: open) { _ in
.init(id: id, open: open) { window in
WindowMainView(
id: id,
content: content,
defaultTitlebar: app.appInformation.defaultTitlebar,
window: window,
app: app,
toolbarItems: toolbarItems,
contentLabel: title,
@ -198,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 defaultTitlebar: Bool
var window: Window.NativeWindow
/// The app.
var app: Aparoksha
/// The toolbar items.
@ -211,14 +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: 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<WinUINavigationItem>.self) { view in
view
.customContent {
@ -235,8 +345,11 @@ struct WindowMainView: View {
} else {
fullContent(app: app)
}
#elseif APPKIT
// TODO: Toolbar for apps without navigation view
fullContent(app: app)
#else
if !defaultTitlebar {
if !aparokshaWindow.defaultTitlebar {
fullContent(app: app)
} else {
ToolbarView()
@ -284,6 +397,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 {

View File

@ -17,6 +17,8 @@ extension AnyView {
public func padding(_ padding: Int, edges: Set<Edge> = .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,11 +71,86 @@ extension AnyView {
public func hexpand(_ expand: Bool = true) -> AnyView {
#if WINUI
self
#elseif APPKIT
self
#else
ModifierWrapper(content: self, hexpand: expand)
#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
ModifierWrapper(
view: Grid { self },
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,
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
ModifierWrapper(
view: self,
maxWidth: .init(optional: maxWidth?.windowsValue)
)
#elseif APPKIT
FrameWrapper(maxWidth: .init(optional: maxWidth)) {
self
}
#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
ModifierWrapper(
view: self,
maxHeight: .init(optional: maxHeight?.windowsValue)
)
#elseif APPKIT
FrameWrapper(maxHeight: .init(optional: maxHeight)) {
self
}
#else
Clamp(vertical: true)
.child {
self
}
.maximumSize(maxHeight)
#endif
}
/// Wrap the view with a card.
/// - Returns: The card.
public func card() -> AnyView {
@ -73,6 +158,10 @@ extension AnyView {
Card {
self
}
#elseif APPKIT
Card {
self
}
#else
style("card")
#endif
@ -87,6 +176,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<Bool>,
@ -96,7 +186,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
@ -123,3 +220,17 @@ extension AnyView {
#endif
}
extension Optional<Double> {
/// Initialize an optional double with an optional integer.
/// - Parameter integer: The optional integer.
public init(optional: Int?) {
if let optional {
self = .init(optional)
} else {
self = nil
}
}
}

View File

@ -27,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<String>)?
/// The close option.
var close: Button
@ -40,6 +42,9 @@ public struct Alert: SimpleView {
#if WINUI
var dialog = ContentDialog(visible: $visible, child: child, id: id, title: title) {
Text(content)
if let textField {
Core.TextBox(textField.0, text: textField.1)
}
}
if let primary {
dialog = dialog.primaryResponse(primary.0.0, suggested: primary.1 == .suggested) {
@ -57,23 +62,64 @@ public struct Alert: SimpleView {
close.1()
visible = false
}
#else
var dialog = AlertDialog(visible: $visible, child: child, id: id, heading: title, body: content)
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)
#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)
}
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 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)
}
}
if let textField {
dialog = dialog.textField(textField.0, text: textField.1)
}
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,
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)
}
if let primary {
dialog = dialog
.response(primary.0.0, appearance: primary.1.appearance, role: .default, action: primary.0.1)
}
return dialog
#endif
}
@ -129,4 +175,15 @@ public struct Alert: SimpleView {
modify { $0.secondary = ((title, action), style) }
}
/// Set the content view in the alert.
/// - Parameters:
/// - label: The text field's label.
/// - text: The text content.
public func textField(
_ label: String,
text: Binding<String>
) -> Self {
modify { $0.textField = (label, text) }
}
}

View File

@ -19,6 +19,18 @@ public struct Carousel<Element>: 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

View File

@ -24,6 +24,12 @@ public struct FlatNavigation<Item>: View where Item: FlatNavigationItem {
/// The toolbar items.
@Environment("toolbar")
private var toolbarItems: [ToolbarItem]?
/// 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.
@ -45,8 +51,46 @@ public struct FlatNavigation<Item>: View where Item: FlatNavigationItem {
#if WINUI
winView
.onAppear {
app?.removeDefaultTitlebar()
if !(defaultTitlebar ?? true) {
window?.defaultTitlebar = false
StateManager.updateViews(force: true)
}
}
#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)
}
.bottomAction(
primaryActionProperties?.title ?? "",
icon: primaryActionProperties?.icon.nativeIcon ?? .system(name: ""),
visible: primaryActionProperties?.active ?? false
) {
primaryActionProperties?.action()
}
} 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(
@ -57,7 +101,7 @@ public struct FlatNavigation<Item>: View where Item: FlatNavigationItem {
primaryAction: primaryActionProperties?.button() ?? []
)
.onAppear {
app?.removeDefaultTitlebar()
window?.defaultTitlebar = false
}
} else {
AdwNavigationSplitView(
@ -69,7 +113,7 @@ public struct FlatNavigation<Item>: View where Item: FlatNavigationItem {
navigationTitle: navigationTitle
)
.onAppear {
app?.removeDefaultTitlebar()
window?.defaultTitlebar = false
}
}
#endif
@ -88,9 +132,9 @@ public struct FlatNavigation<Item>: 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)
}
}
@ -136,11 +180,13 @@ public struct FlatNavigation<Item>: View where Item: FlatNavigationItem {
/// - selection: The selected element's id.
/// - content: The main content.
public init<Element>(
_ elements: [Element], selection: Binding<Element.ID?>, @ViewBuilder content: () -> Body
_ elements: @escaping @autoclosure () -> [Element],
selection: Binding<Element.ID?>,
@ViewBuilder content: () -> Body
) where Element: DynamicFlatNavigationItem, Element: Identifiable, Item == DynamicWrapper<Element> {
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
@ -193,12 +239,11 @@ public struct FlatNavigation<Item>: View where Item: FlatNavigationItem {
/// - stack: The hierarchical navigation stack.
/// - content: The hierarchical content.
/// - Returns: The complex view.
@ViewBuilder
public func complexNavigation<StackItem>(
stack: Binding<NavigationStack<StackItem>>,
@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)
@ -209,20 +254,24 @@ public struct FlatNavigation<Item>: 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
content(item)
} initialView: {
self
}
.hideDefaultToolbar()
[
HierarchicalNavigation(stack: stack, initialPageLabel: navigationTitle ?? "") { item in
content(item)
} initialView: {
self
}
.hideDefaultToolbar()
]
#endif
}
@ -314,8 +363,8 @@ struct WinNavigationView<Item>: 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)
}
}
}
@ -363,7 +412,7 @@ struct WinUINavigationItem: NavigationViewItem {
}
}
#else
#elseif ADWAITA
/// The view switcher for GNOME.
struct AdwViewSwitcher<Item>: View where Item: FlatNavigationItem {
@ -496,15 +545,15 @@ struct AdwNavigationSplitView<Item>: View where Item: FlatNavigationItem {
/// The sidebar.
var sidebar: AnyView {
ScrollView {
Core.ScrollView {
let selectedItem: Binding<String> = 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 {
@ -563,17 +612,19 @@ struct AdwViewSwitcherItem<Item>: ViewSwitcherOption where Item: FlatNavigationI
}
}
#endif
/// An item for the Adwaita list in the sidebar.
struct AdwListItem<Item>: Identifiable where Item: FlatNavigationItem {
/// An item for the list in the sidebar.
struct ListItem<Item>: 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 {

View File

@ -21,7 +21,23 @@ public struct HierarchicalNavigation<Item>: 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<WinUINavigationItem>]>?
/// 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<Item> = .init()
/// The item presented in a dialog.
@State private var dialog: Item?
#else
/// The Adwaita stack.
@State private var adwStack: NavigationView.NavigationStack<Item> = .init()
/// The item presented in a dialog.
@ -34,17 +50,9 @@ public struct HierarchicalNavigation<Item>: View where Item: Equatable, Item: Cu
/// The app.
@Environment("app")
var app: Aparoksha?
#else
/// The navigation path.
@Environment("path")
private var path: Binding<[NavigationSelection<WinUINavigationItem>]>?
/// The currently selected item.
var selectedItem: Item? {
if case let .custom(item: item) = path?.wrappedValue.last {
return item.item as? Item
}
return nil
}
/// The window object.
@Environment("window")
private var window: Window.Window?
#endif
/// The view.
@ -71,6 +79,33 @@ public struct HierarchicalNavigation<Item>: 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 {
@ -101,7 +136,7 @@ public struct HierarchicalNavigation<Item>: View where Item: Equatable, Item: Cu
title: dialog?.description
)
.onAppear {
app?.removeDefaultTitlebar()
window?.defaultTitlebar = false
}
}
#endif
@ -142,6 +177,8 @@ public struct HierarchicalNavigation<Item>: View where Item: Equatable, Item: Cu
case .pop:
_ = path?.wrappedValue.popLast()
}
#elseif APPKIT
executeMacAction(action: action)
#else
Task {
switch action {
@ -163,13 +200,35 @@ public struct HierarchicalNavigation<Item>: View where Item: Equatable, Item: Cu
#endif
}
#if APPKIT
/// Execute the action on macOS.
/// - Parameter action: The action.
func executeMacAction(action: NavigationStack<Item>.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 {
@ -179,8 +238,9 @@ public struct HierarchicalNavigation<Item>: View where Item: Equatable, Item: Cu
.top {
HeaderBar.empty()
}
#endif
.freeze(item == nil)
}
#endif
/// Hide the toolbar on Adwaita.
/// - Returns: The view.

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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()
}
}

View File

@ -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 {

View File

@ -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.

View File

@ -19,14 +19,34 @@ 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()
}
Window("Demo 2", icon: .headphones, id: "second") {
Text("Hi")
.padding(10)
}
.frame(width: .constant(300), height: .constant(200))
.leadingToolbarItem("Test", icon: .airplane) {
print("Plane")
}
}
}
@ -39,6 +59,7 @@ struct ContentView: WindowView {
@State private var forceSplitView = true
@State private var stack: NavigationStack<GenericItem> = .init()
@State private var showAlert = false
@State private var text = ""
var view: Body {
FlatNavigation(Item.allCases, selection: $selectedItem) {
@ -89,7 +110,7 @@ struct ContentView: WindowView {
Text(element.description)
.valign(.center)
.halign(.center)
.padding(50)
.frame(minWidth: 300, minHeight: 250)
}
.valign(.center)
.card()
@ -108,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 {
@ -137,7 +159,7 @@ enum Item: String, FlatNavigationItem, CaseIterable, Equatable, Codable {
case .navigation:
.navigation
case .carousel:
.panel
.custom(winui: "🤣", adwaita: "dev.aparoksha.Demo.carousel-symbolic", appkit: "slider.horizontal.below.rectangle")
case .alert:
.warning
}
@ -163,4 +185,3 @@ enum GenericItem: String, Equatable, CustomStringConvertible, CaseIterable, Iden
}
// swiftlint:enable

49
dev.aparoksha.Demo.json Normal file
View File

@ -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"
]
}
]
}