Merge pull request 'macOS & Simple Views' (#5) from devel into main
Reviewed-on: #5
This commit is contained in:
commit
357f1890b3
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
10
Bundler.toml
Normal 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"]
|
||||
2
Data/GNOME/carousel-symbolic.svg
Normal file
2
Data/GNOME/carousel-symbolic.svg
Normal 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 |
@ -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: [
|
||||
|
||||
@ -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
|
||||
|
||||
46
Sources/Menu/Submenu.swift
Normal file
46
Sources/Menu/Submenu.swift
Normal 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 }
|
||||
}
|
||||
|
||||
}
|
||||
51
Sources/Model/Enumerations/Framework.swift
Normal file
51
Sources/Model/Enumerations/Framework.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -33,7 +33,7 @@ public enum HorizontalAlignment {
|
||||
.stretch
|
||||
}
|
||||
}
|
||||
#else
|
||||
#elseif ADWAITA
|
||||
/// The native alignment.
|
||||
var nativeAlignment: Core.Alignment {
|
||||
switch self {
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -33,7 +33,7 @@ public enum VerticalAlignment {
|
||||
.stretch
|
||||
}
|
||||
}
|
||||
#else
|
||||
#elseif ADWAITA
|
||||
/// The native alignment.
|
||||
var nativeAlignment: Core.Alignment {
|
||||
switch self {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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
114
Sources/Scene/MenuBar.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
@ -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.
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
29
Sources/View/Simple/ScrollView.swift
Normal file
29
Sources/View/Simple/ScrollView.swift
Normal 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()
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
49
dev.aparoksha.Demo.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user