This commit is contained in:
parent
640cf90b63
commit
4eff4d4587
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"]
|
||||
@ -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: [
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
}
|
||||
@ -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.
|
||||
@ -69,15 +69,50 @@ public enum Icon: Equatable {
|
||||
"\u{F8AC}"
|
||||
case .forward:
|
||||
"\u{F8AD}"
|
||||
case let .custom(winui, _):
|
||||
case let .custom(winui, _, _):
|
||||
winui
|
||||
}
|
||||
return .systemIcon(unicode: unicode)
|
||||
}
|
||||
#elseif APPKIT
|
||||
/// The native icon.
|
||||
var nativeIcon: Core.Icon {
|
||||
let name = switch self {
|
||||
case .airplane:
|
||||
"airplane"
|
||||
case .edit:
|
||||
"pencil"
|
||||
case .headphones:
|
||||
"headphones"
|
||||
case .plus:
|
||||
"plus"
|
||||
case .copy:
|
||||
"doc.on.doc"
|
||||
case .navigation:
|
||||
"sidebar.left"
|
||||
case .warning:
|
||||
"exclamationmark.triangle"
|
||||
case .trash:
|
||||
"trash"
|
||||
case .play:
|
||||
"play"
|
||||
case .document:
|
||||
"doc"
|
||||
case .repeat:
|
||||
"repeat"
|
||||
case .back:
|
||||
"backward"
|
||||
case .forward:
|
||||
"forward"
|
||||
case let .custom(_, _, appkit):
|
||||
appkit
|
||||
}
|
||||
return .system(name: name)
|
||||
}
|
||||
#else
|
||||
/// The native icon.
|
||||
var nativeIcon: Core.Icon {
|
||||
if case let .custom(_, adwaita) = self {
|
||||
if case let .custom(_, adwaita, _) = self {
|
||||
return .custom(name: adwaita)
|
||||
}
|
||||
let icon: Core.Icon.DefaultIcon = switch self {
|
||||
|
||||
@ -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,4 +102,21 @@ public class Aparoksha: AppStorage {
|
||||
app.quit()
|
||||
}
|
||||
|
||||
/// 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,14 +19,28 @@ struct AppInformation {
|
||||
var version: String
|
||||
/// The app's website.
|
||||
var website: URL?
|
||||
/// The label for the website.
|
||||
var websiteLabel: String?
|
||||
/// The app's URL for issues.
|
||||
var issues: URL?
|
||||
/// The label for the issues.
|
||||
var issuesLabel: String?
|
||||
/// The app icon.
|
||||
var icon: Icon?
|
||||
/// The about action's label.
|
||||
var about: String
|
||||
/// The settings label.
|
||||
var settings: String
|
||||
/// The services label.
|
||||
var services: String?
|
||||
/// The hide app label.
|
||||
var hide: String?
|
||||
/// The hide others label.
|
||||
var hideOthers: String?
|
||||
/// The show all label.
|
||||
var showAll: String?
|
||||
/// The quit label.
|
||||
var quit: String
|
||||
|
||||
#if ADWAITA
|
||||
/// The main menu for GNOME.
|
||||
|
||||
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,13 +10,16 @@ import Core
|
||||
/// A window.
|
||||
public struct Window: AparokshaSceneElement {
|
||||
|
||||
#if WINUI
|
||||
/// The native window type.
|
||||
typealias NativeWindow = WinUIWindow
|
||||
#else
|
||||
/// The native window type.
|
||||
typealias NativeWindow = AdwaitaWindow
|
||||
#endif
|
||||
#if WINUI
|
||||
/// The native window type.
|
||||
typealias NativeWindow = WinUIWindow
|
||||
#elseif APPKIT
|
||||
/// The native window type.
|
||||
typealias NativeWindow = MacWindow
|
||||
#else
|
||||
/// The native window type.
|
||||
typealias NativeWindow = AdwaitaWindow
|
||||
#endif
|
||||
|
||||
/// The window's identifier.
|
||||
public var id: String
|
||||
@ -62,6 +65,7 @@ public struct Window: AparokshaSceneElement {
|
||||
/// The native window.
|
||||
var native: NativeWindow
|
||||
|
||||
#if !APPKIT
|
||||
/// Whether the default title bar is visible.
|
||||
var defaultTitlebar: Bool {
|
||||
get {
|
||||
@ -71,6 +75,7 @@ public struct Window: AparokshaSceneElement {
|
||||
native.fields["default-titlebar"] = newValue
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Initialize a window.
|
||||
/// - Parameter native: The native window.
|
||||
@ -213,6 +218,21 @@ public struct Window: AparokshaSceneElement {
|
||||
.extendContentIntoTitleBar(true, gap: app.appInformation.gap)
|
||||
.frame(width: width?.windowsValue, height: height?.windowsValue)
|
||||
}
|
||||
#elseif APPKIT
|
||||
/// The native window.
|
||||
func nativeWindow(app: Aparoksha) -> Core.Window {
|
||||
.init(title, id: id, open: open) { window in
|
||||
WindowMainView(
|
||||
content: content,
|
||||
window: .init(native: window),
|
||||
app: app,
|
||||
toolbarItems: toolbarItems,
|
||||
contentLabel: title,
|
||||
contentIcon: icon
|
||||
)
|
||||
}
|
||||
.frame(width: width, height: height)
|
||||
}
|
||||
#else
|
||||
/// The native window.
|
||||
func nativeWindow(app: Aparoksha) -> Core.Window {
|
||||
@ -283,6 +303,9 @@ struct WindowMainView: View {
|
||||
} else {
|
||||
fullContent(app: app)
|
||||
}
|
||||
#elseif APPKIT
|
||||
// TODO: Toolbar for apps without navigation view
|
||||
fullContent(app: app)
|
||||
#else
|
||||
if !window.defaultTitlebar {
|
||||
fullContent(app: app)
|
||||
@ -332,6 +355,13 @@ struct ToolbarItem {
|
||||
visible: active && !settings
|
||||
)
|
||||
}
|
||||
#elseif APPKIT
|
||||
/// The native button.
|
||||
func button() -> AnyView {
|
||||
DisabledWrapper(disabled: !active) {
|
||||
Core.Button(title, icon: icon.nativeIcon, action: action)
|
||||
}
|
||||
}
|
||||
#else
|
||||
/// The native button.
|
||||
func button() -> AnyView {
|
||||
|
||||
@ -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,6 +71,8 @@ extension AnyView {
|
||||
public func hexpand(_ expand: Bool = true) -> AnyView {
|
||||
#if WINUI
|
||||
self
|
||||
#elseif APPKIT
|
||||
self
|
||||
#else
|
||||
ModifierWrapper(content: self, hexpand: expand)
|
||||
#endif
|
||||
@ -80,6 +92,10 @@ extension AnyView {
|
||||
minWidth: .init(optional: minWidth?.windowsValue),
|
||||
minHeight: .init(optional: minHeight?.windowsValue)
|
||||
)
|
||||
#elseif APPKIT
|
||||
FrameWrapper(minWidth: .init(optional: minWidth), minHeight: .init(optional: minHeight)) {
|
||||
self
|
||||
}
|
||||
#else
|
||||
ModifierWrapper(
|
||||
content: self,
|
||||
@ -99,6 +115,10 @@ extension AnyView {
|
||||
view: self,
|
||||
maxWidth: .init(optional: maxWidth?.windowsValue)
|
||||
)
|
||||
#elseif APPKIT
|
||||
FrameWrapper(maxWidth: .init(optional: maxWidth)) {
|
||||
self
|
||||
}
|
||||
#else
|
||||
Clamp(vertical: false)
|
||||
.child {
|
||||
@ -118,6 +138,10 @@ extension AnyView {
|
||||
view: self,
|
||||
maxHeight: .init(optional: maxHeight?.windowsValue)
|
||||
)
|
||||
#elseif APPKIT
|
||||
FrameWrapper(maxHeight: .init(optional: maxHeight)) {
|
||||
self
|
||||
}
|
||||
#else
|
||||
Clamp(vertical: true)
|
||||
.child {
|
||||
@ -134,6 +158,10 @@ extension AnyView {
|
||||
Card {
|
||||
self
|
||||
}
|
||||
#elseif APPKIT
|
||||
Card {
|
||||
self
|
||||
}
|
||||
#else
|
||||
style("card")
|
||||
#endif
|
||||
|
||||
@ -19,7 +19,7 @@ public struct Alert: SimpleView {
|
||||
var title: String
|
||||
/// The content.
|
||||
var content: String
|
||||
// TODO: Implement on Windows
|
||||
// TODO: Restrict to certain controls (e.g. text fields) and implement on macOS & Windows
|
||||
/// The view content.
|
||||
var extraChild: Body?
|
||||
/// The identifier.
|
||||
@ -61,6 +61,33 @@ public struct Alert: SimpleView {
|
||||
close.1()
|
||||
visible = false
|
||||
}
|
||||
#elseif APPKIT
|
||||
var dialog = Core.Alert(
|
||||
title: title,
|
||||
description: content,
|
||||
isPresented: $visible,
|
||||
child: child
|
||||
)
|
||||
if let primary {
|
||||
if primary.1 == .destructive {
|
||||
dialog = dialog.destructiveButton(primary.0.0, default: primary.1 == .suggested, action: primary.0.1)
|
||||
} else {
|
||||
dialog = dialog.button(primary.0.0, default: primary.1 == .suggested, action: primary.0.1)
|
||||
}
|
||||
}
|
||||
if let secondary {
|
||||
if secondary.1 == .destructive {
|
||||
dialog = dialog.destructiveButton(
|
||||
secondary.0.0,
|
||||
default: secondary.1 == .suggested,
|
||||
action: secondary.0.1
|
||||
)
|
||||
} else {
|
||||
dialog = dialog.button(secondary.0.0, default: secondary.1 == .suggested, action: secondary.0.1)
|
||||
}
|
||||
}
|
||||
dialog = dialog.cancelButton(close.0, action: close.1)
|
||||
return dialog
|
||||
#else
|
||||
var dialog = AlertDialog(
|
||||
visible: $visible,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -50,6 +50,34 @@ public struct FlatNavigation<Item>: View where Item: FlatNavigationItem {
|
||||
.onAppear {
|
||||
app?.removeDefaultTitlebar()
|
||||
}
|
||||
#elseif APPKIT
|
||||
Core.NavigationSplitView {
|
||||
Core.List(
|
||||
items.map { ListItem(item: $0) },
|
||||
selection: .init {
|
||||
selectedItem.description
|
||||
} set: { newValue in
|
||||
if let item = items.first(where: { $0.description == newValue }) {
|
||||
selectedItem = item
|
||||
}
|
||||
}
|
||||
) { item in
|
||||
Core.Label(item.item.description, icon: item.item.icon.nativeIcon)
|
||||
}
|
||||
} detail: {
|
||||
ToolbarView(
|
||||
child: ToolbarView(child: content, type: .end) {
|
||||
(toolbarItems ?? []).filter { !$0.leftAlign }.map { item in
|
||||
item.button()
|
||||
}
|
||||
},
|
||||
type: .start
|
||||
) {
|
||||
(toolbarItems ?? []).filter { $0.leftAlign }.map { item in
|
||||
item.button()
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
if useViewSwitcher {
|
||||
AdwViewSwitcher(
|
||||
@ -198,12 +226,11 @@ public struct FlatNavigation<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)
|
||||
@ -214,12 +241,14 @@ 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
|
||||
@ -368,7 +397,7 @@ struct WinUINavigationItem: NavigationViewItem {
|
||||
}
|
||||
|
||||
}
|
||||
#else
|
||||
#elseif ADWAITA
|
||||
/// The view switcher for GNOME.
|
||||
struct AdwViewSwitcher<Item>: View where Item: FlatNavigationItem {
|
||||
|
||||
@ -503,13 +532,13 @@ struct AdwNavigationSplitView<Item>: View where Item: FlatNavigationItem {
|
||||
var sidebar: AnyView {
|
||||
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 {
|
||||
@ -568,17 +597,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.
|
||||
@ -37,17 +53,6 @@ public struct HierarchicalNavigation<Item>: View where Item: Equatable, Item: Cu
|
||||
/// The window object.
|
||||
@Environment("window")
|
||||
private var window: Window.Window?
|
||||
#else
|
||||
/// The navigation path.
|
||||
@Environment("path")
|
||||
private var path: Binding<[NavigationSelection<WinUINavigationItem>]>?
|
||||
/// The currently selected item.
|
||||
var selectedItem: Item? {
|
||||
if case let .custom(item: item) = path?.wrappedValue.last {
|
||||
return item.item as? Item
|
||||
}
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
|
||||
/// The view.
|
||||
@ -74,6 +79,33 @@ public struct HierarchicalNavigation<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 {
|
||||
@ -145,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 {
|
||||
@ -166,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 {
|
||||
@ -182,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.
|
||||
|
||||
@ -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,11 +19,23 @@ struct Demo: App {
|
||||
about: "About Demo",
|
||||
developer: "Aparoksha",
|
||||
version: "main",
|
||||
quit: "Quit Demo",
|
||||
website: .init(string: "https://aparoksha.dev/")!,
|
||||
issues: .init(string: "https://forums.aparoksha.dev/")!
|
||||
websiteLabel: "Website",
|
||||
issues: .init(string: "https://forums.aparoksha.dev/")!,
|
||||
issuesLabel: "Issues",
|
||||
services: "Services"
|
||||
)
|
||||
.hideMenu(hideApp: "Hide Demo", hideOthers: "Hide Others", showAll: "Show All")
|
||||
|
||||
var scene: Scene {
|
||||
MenuBar {
|
||||
Submenu("Test") {
|
||||
MenuButton("Test") {
|
||||
print("Test")
|
||||
}
|
||||
}
|
||||
}
|
||||
Window("Demo", icon: .headphones, id: "main") {
|
||||
ContentView()
|
||||
}
|
||||
@ -31,6 +43,10 @@ struct Demo: App {
|
||||
Text("Hi")
|
||||
.padding(10)
|
||||
}
|
||||
.frame(width: .constant(300), height: .constant(200))
|
||||
.leadingToolbarItem("Test", icon: .airplane) {
|
||||
print("Plane")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -141,7 +157,7 @@ enum Item: String, FlatNavigationItem, CaseIterable, Equatable, Codable {
|
||||
case .navigation:
|
||||
.navigation
|
||||
case .carousel:
|
||||
.custom(winui: "🤣", adwaita: "dev.aparoksha.Demo.carousel-symbolic")
|
||||
.custom(winui: "🤣", adwaita: "dev.aparoksha.Demo.carousel-symbolic", appkit: "slider.horizontal.below.rectangle")
|
||||
case .alert:
|
||||
.warning
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user