Initial commit
All checks were successful
Deploy Docs / publish (push) Successful in 2m38s

This commit is contained in:
david-swift 2024-12-02 22:00:57 +01:00
commit e989bea14e
47 changed files with 2668 additions and 0 deletions

View File

@ -0,0 +1,40 @@
name: Bug report
description: Something is not working as expected.
title: Description of the bug
labels: bug
body:
- type: textarea
attributes:
label: Describe the bug
description: >-
A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: To Reproduce
description: >-
Steps to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: >-
A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: >-
Add any other context about the problem here.

View File

@ -0,0 +1,29 @@
name: Components request
description: Suggest an idea for a new component
title: Description of the component request
labels: enhancement
body:
- type: textarea
attributes:
label: Why would you like to add a new component?
placeholder: >-
A clear and concise description of why the component should be added.
validations:
required: false
- type: textarea
attributes:
label: Describe your idea for the implementation.
placeholder: >-
What could the implementation be like in the MacBackend?
validations:
required: false
- type: textarea
attributes:
label: Additional context
placeholder: >-
Add any other context about the component request here.
validations:
required: false

View File

@ -0,0 +1,36 @@
name: Feature request
description: Suggest an idea for this project
title: Description of the feature request
labels: enhancement
body:
- type: input
attributes:
label: Is your feature request related to a problem? Please describe.
placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: false
- type: textarea
attributes:
label: Describe the solution you'd like
placeholder: >-
A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
placeholder: >-
A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Additional context
placeholder: >-
Add any other context or screenshots about the feature request here.
validations:
required: true

View File

@ -0,0 +1,14 @@
## Steps
- [ ] Add your name or username and a link to your GitHub profile into the [Contributors.md][1] file.
- [ ] Build the project on your machine. If it does not compile, fix the errors.
- [ ] Describe the purpose and approach of your pull request below.
- [ ] Submit the pull request. Thank you very much for your contribution!
## Purpose
_Describe the problem or feature._
_If there is a related issue, add the link._
## Approach
_Describe how this pull request solves the problem or adds the feature._
[1]: /Contributors.md

34
.gitea/workflows/docs.yml Normal file
View File

@ -0,0 +1,34 @@
name: Deploy Docs
on:
push:
branches: ["main"]
jobs:
publish:
runs-on: david-macbook
steps:
- uses: actions/checkout@v4
- name: Build Docs
run: |
xcrun xcodebuild docbuild \
-scheme MacBackend \
-destination 'generic/platform=macOS' \
-derivedDataPath "$PWD/.derivedData" \
-skipPackagePluginValidation
xcrun docc process-archive transform-for-static-hosting \
"$PWD/.derivedData/Build/Products/Debug/MacBackend.doccarchive" \
--output-path "docs" \
--hosting-base-path "/"
- name: Modify Docs
run: |
echo "<script>window.location.href += \"/documentation/macbackend\"</script><p>Please enable JavaScript to view the documentation <a href='/documentation/macbackend'>here</a>.</p>" > docs/index.html;
sed -i '' 's/,2px/,10px/g' docs/css/index.*.css
- name: Upload
uses: wangyucode/sftp-upload-action@v2.0.2
with:
host: 'volans.uberspace.de'
username: 'akforum'
password: ${{ secrets.password }}
localDir: 'docs'
remoteDir: '/var/www/virtual/akforum/macbackend.aparoksha.dev/'

View File

@ -0,0 +1,30 @@
name: SwiftLint
on:
push:
paths:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
pull_request:
paths:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
workflow_dispatch:
paths:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
jobs:
SwiftLint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: SwiftLint
uses: norio-nomura/action-swiftlint@3.2.1
with:
args: --strict
env:
WORKING_DIRECTORY: Source

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm
.netrc
/Package.resolved
.Ulysses-Group.plist
/.docc-build
/io.github.AparokshaUI.Generation.json
/io.github.AparokshaUI.swiftlint.json
/.vscode

155
.swiftlint.yml Normal file
View File

@ -0,0 +1,155 @@
# Opt-In Rules
opt_in_rules:
- anonymous_argument_in_multiline_closure
- array_init
- attributes
- closure_body_length
- closure_end_indentation
- closure_spacing
- collection_alignment
- comma_inheritance
- conditional_returns_on_newline
- contains_over_filter_count
- contains_over_filter_is_empty
- contains_over_first_not_nil
- contains_over_range_nil_comparison
- convenience_type
- discouraged_none_name
- discouraged_object_literal
- empty_collection_literal
- empty_count
- empty_string
- enum_case_associated_values_count
- explicit_init
- fallthrough
- file_header
- file_name
- file_name_no_space
- first_where
- flatmap_over_map_reduce
- force_unwrapping
- function_default_parameter_at_end
- identical_operands
- implicit_return
- implicitly_unwrapped_optional
- joined_default_parameter
- last_where
- legacy_multiple
- let_var_whitespace
- literal_expression_end_indentation
- local_doc_comment
- lower_acl_than_parent
- missing_docs
- modifier_order
- multiline_arguments
- multiline_arguments_brackets
- multiline_function_chains
- multiline_literal_brackets
- multiline_parameters
- multiline_parameters_brackets
- no_extension_access_modifier
- no_grouping_extension
- number_separator
- operator_usage_whitespace
- optional_enum_case_matching
- prefer_self_in_static_references
- prefer_self_type_over_type_of_self
- prefer_zero_over_explicit_init
- prohibited_interface_builder
- redundant_nil_coalescing
- redundant_type_annotation
- return_value_from_void_function
- shorthand_optional_binding
- sorted_first_last
- sorted_imports
- static_operator
- strict_fileprivate
- switch_case_on_newline
- toggle_bool
- trailing_closure
- type_contents_order
- unneeded_parentheses_in_closure_argument
- yoda_condition
# Disabled Rules
disabled_rules:
- block_based_kvo
- class_delegate_protocol
- dynamic_inline
- is_disjoint
- no_fallthrough_only
- notification_center_detachment
- ns_number_init_as_function_reference
- nsobject_prefer_isequal
- private_over_fileprivate
- redundant_objc_attribute
- self_in_property_initialization
- todo
- unavailable_condition
- valid_ibinspectable
- xctfail_message
# Custom Rules
custom_rules:
fatal_error:
name: 'Fatal Error'
regex: 'fatalError.*\(.*\)'
message: 'Fatal error should not be used.'
severity: error
enum_case_parameter:
name: 'Enum Case Parameter'
regex: 'case [a-zA-Z0-9]*\([a-zA-Z0-9\.<>?,\n\t =]+\)'
message: 'The associated values of an enum case should have parameters.'
severity: warning
tab:
name: 'Whitespaces Instead of Tab'
regex: '\t'
message: 'Spaces should be used instead of tabs.'
severity: warning
# Thanks to the creator of the SwiftLint rule
# "empty_first_line"
# https://github.com/coteditor/CotEditor/blob/main/.swiftlint.yml
# in the GitHub repository
# "CotEditor"
# https://github.com/coteditor/CotEditor
empty_first_line:
name: 'Empty First Line'
regex: '(^[ a-zA-Z ]*(?:protocol|extension|class|struct) (?!(?:var|let))[ a-zA-Z:]*\{\n *\S+)'
message: 'There should be an empty line after a declaration'
severity: error
# Analyzer Rules
analyzer_rules:
- unused_declaration
- unused_import
# Options
file_header:
required_pattern: '(// swift-tools-version: .+)?//\n// .*.swift\n// MacBackend\n//\n// Created by .* on .*\.(\n// Edited by (.*,)+\.)*\n(\n// Thanks to .* for the .*:\n// ".*"\n// https://.* \(\d\d.\d\d.\d\d\))*//\n'
missing_docs:
warning: [internal, private]
error: [open, public]
excludes_inherited_types: false
type_contents_order:
order:
- case
- type_alias
- associated_type
- type_property
- instance_property
- ib_inspectable
- ib_outlet
- subscript
- initializer
- deinitializer
- subtype
- type_method
- view_life_cycle_method
- ib_action
- other_method
excluded:
- .build/

4
Bundler.toml Normal file
View File

@ -0,0 +1,4 @@
[apps."Demo"]
product = 'Demo'
version = '0.1.0'
minimum_macos_version = '14'

23
LICENSE.md Normal file
View File

@ -0,0 +1,23 @@
MIT License
Copyright (c) 2024 david-swift
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CO
ECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

41
Package.swift Normal file
View File

@ -0,0 +1,41 @@
// swift-tools-version: 6.0
//
// Package.swift
// MacBackend
//
// Created by david-swift on 08.06.23.
//
import PackageDescription
/// The MacBackend package.
let package = Package(
name: "MacBackend",
platforms: [.macOS(.v13)],
products: [
.library(
name: "MacBackend",
targets: ["MacBackend"]
)
],
dependencies: [
.package(url: "https://git.aparoksha.dev/aparoksha/meta", branch: "main"),
.package(url: "https://git.aparoksha.dev/aparoksha/meta-sqlite", branch: "main"),
.package(url: "https://git.aparoksha.dev/aparoksha/levenshtein-transformations", branch: "main")
],
targets: [
.target(
name: "MacBackend",
dependencies: [
.product(name: "Meta", package: "Meta"),
.product(name: "MetaSQLite", package: "meta-sqlite"),
.product(name: "LevenshteinTransformations", package: "levenshtein-transformations")
]
),
.executableTarget(
name: "Demo",
dependencies: ["MacBackend"]
)
],
swiftLanguageModes: [.v5]
)

35
README.md Normal file
View File

@ -0,0 +1,35 @@
<p align="center">
<h1 align="center">Backend for macOS</h1>
</p>
<p align="center">
<a href="https://macbackend.aparoksha.dev/">
Documentation
</a>
·
<a href="https://git.aparoksha.dev/aparoksha/macbackend">
Code
</a>
</p>
_MacBackend_ enables the creation of macOS apps via the [Aparoksha package](https://aparoksha.dev/). It does so by wrapping SwiftUI views.
Therefore, if you want to create apps for macOS only, it is recommended to use SwiftUI directly.
Find more information in the [documentation](https://macbackend.aparoksha.dev/).
## Thanks
### Dependencies
- [Meta](https://git.aparoksha.dev/aparoksha/meta) licensed under the [MIT License](https://git.aparoksha.dev/aparoksha/meta/src/branch/main/LICENSE.md)
- [Levenshtein Transformations](https://git.aparoksha.dev/aparoksha/levenshtein-transformations) licensed under the [MIT License](https://git.aparoksha.dev/aparoksha/levenshtein-transformations/src/branch/main/LICENSE.md)
- [SQLite for Meta](https://git.aparoksha.dev/aparoksha/meta-sqlite) licensed under the [MIT License](https://git.aparoksha.dev/aparoksha/meta-sqlite/src/branch/main/LICENSE.md)
### Other Thanks
- [DocC](https://github.com/swiftlang/swift-docc) used for generating the documentation
- [SwiftLint][21] for checking whether code style conventions are violated
- The programming language [Swift][22]
- [SwiftUI](https://developer.apple.com/xcode/swiftui/)
[21]: https://github.com/realm/SwiftLint
[22]: https://github.com/apple/swift

121
Sources/Demo/Demo.swift Normal file
View File

@ -0,0 +1,121 @@
//
// Demo.swift
// MacBackend
//
// Created by david-swift on 25.09.23.
//
// swiftlint:disable missing_docs no_magic_numbers closure_body_length
import Foundation
import MacBackend
@main
struct Demo: App {
var app = MacApp(id: "dev.aparoksha.Demo")
@State("bool")
private var bool = false
@State(blockUpdates: true)
private var width = 500
@State(blockUpdates: true)
private var height = 400
@State private var elements: [Element] = [.init()]
@State private var selectedElement: String?
@State private var alert = false
var scene: Scene {
Window("Main", id: "main") {
NavigationSplitView {
List(elements, selection: $selectedElement) { element in
Label(element.id, icon: .system(name: "play.fill"))
}
} detail: {
VStack {
Button(selectedElement ?? "World") {
let element = Element()
elements.append(element)
selectedElement = element.id
}
Button(alert.description) {
alert = true
}
}
.alert("Hello", isPresented: $alert)
.cancelButton("Cancel") {
print("Cancel")
}
.destructiveButton("Destructive", default: true) {
print("Destructive")
}
}
}
.frame(width: $width, height: $height)
MenuBar {
Menu("Test") {
MenuButton(bool.description) {
bool.toggle()
}
.keyboardShortcut("h")
.selected(bool)
Divider()
Menu("Test") {
MenuButton("Hi") {}
Menu("Hi?") { }
}
MenuButton("Hello") {
print("Hello")
}
.enabled(bool)
.keyboardShortcut("s")
MenuButton("World") {
print("World")
}
Menu("Actions") {
MenuButton("Quit") {
app.quit()
}
}
}
Menu("Quit") {
MenuButton("Quit") {
app.quit()
}
.keyboardShortcut("q")
}
} app: {
MenuButton("About Demo") {
app.showAboutWindow()
}
Divider()
ServicesMenu("Services")
Divider()
MenuButton("Hide Demo") {
app.hide()
}
.keyboardShortcut("h")
MenuButton("Hide Others") {
app.hideOthers()
}
.keyboardShortcut(.init("h", alt: true))
MenuButton("Show All") {
app.showAll()
}
Divider()
MenuButton("Quit Demo") {
app.quit()
}
.keyboardShortcut("q")
}
}
}
struct Element: Identifiable {
var id: String = UUID().uuidString
}
// swiftlint:enable missing_docs no_magic_numbers closure_body_length

View File

@ -0,0 +1,7 @@
# ``MacBackend``
_MacBackend_ enables the creation of macOS apps via the [Aparoksha package](https://aparoksha.dev/). It does so by wrapping SwiftUI views.
Therefore, if you want to create apps for macOS only, it is recommended to use SwiftUI directly.
Find more information in the [documentation](https://macbackend.aparoksha.dev/).

View File

@ -0,0 +1,59 @@
# SwiftUI Integration
Learn how to render SwiftUI views in a macOS app.
## Simple Views
Wrap a SwiftUI view by creating a widget conforming to ``SwiftUIWidget``.
Define your SwiftUI view in the ``SwiftUIWidget/view(properties:)`` function.
```swift
import MacBackend
import SwiftUI
struct Label: SwiftUIWidget {
var label: String
var icon: MacBackend.Icon
static func view(properties: Self) -> some SwiftUI.View {
SwiftUI.Label {
SwiftUI.Text(properties.label)
} icon: {
properties.icon.image
}
}
}
```
## Container Views
It is possible to pass `MacBackend` views as child views to SwiftUI views.
Add them to ``SwiftUIWidget/wrappedViews`` and reference via the identifier.
```swift
import MacBackend
import SwiftUI
struct ContainerView: SwiftUIWidget {
var child: Body
init(@MacBackend.ViewBuilder child: () -> Body) {
self.child = child()
}
var wrappedViews: [String: MacBackend.AnyView] {
[.mainContent: child]
}
func view(properties: Self) -> some SwiftUI.View {
MacBackendView(.mainContent)
.background(.red)
}
}
```
You can add new wrapped views or delete old ones dynamically.

View File

@ -0,0 +1,42 @@
//
// Divider.swift
// MacBackend
//
// Created by david-swift on 22.10.23.
//
import AppKit
/// A button widget for menus.
public struct Divider: MenuWidget {
/// Initialize a section for menus.
public init() { }
/// The view storage.
/// - Parameters:
/// - data: The widget data.
/// - type: The type of the views.
/// - Returns: The view storage.
public func container<Data>(
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
.init(NSMenuItem.separator())
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - data: The widget data.
/// - updateProperties: Whether to update the properties.
/// - type: The type of the views.
public func update<Data>(
_ storage: ViewStorage,
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
}
}

View File

@ -0,0 +1,71 @@
//
// Submenu.swift
// MacBackend
//
// Created by david-swift on 22.10.23.
//
import AppKit
/// A submenu widget.
public struct Menu: MenuWidget {
/// The label of the submenu.
var label: String
/// The content of the submenu.
var content: Body
/// Initialize a submenu.
/// - Parameters:
/// - label: The submenu's label.
/// - content: The content of the section.
public init(_ label: String, @ViewBuilder content: () -> Body) {
self.label = label
self.content = content()
}
/// The view storage.
/// - Parameters:
/// - data: The widget data.
/// - type: The type of the views.
/// - Returns: The view storage.
public func container<Data>(
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let item = NSMenuItem()
let menu = NSMenu()
item.submenu = menu
let storage = ViewStorage(item)
let content = MenuCollection { self.content }.getMenu(data: data, menu: menu)
storage.content[.mainContent] = [content]
update(storage, data: data, updateProperties: true, type: type)
return storage
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - data: The widget data.
/// - updateProperties: Whether to update the properties.
/// - type: The type of the views.
public func update<Data>(
_ storage: ViewStorage,
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
if let content = storage.content[.mainContent]?.first {
MenuCollection { self.content }.update(content, data: data, updateProperties: updateProperties, type: type)
}
guard updateProperties, let item = storage.pointer as? NSMenuItem else {
return
}
let previousState = storage.previousState as? Self
if previousState?.label != label {
item.title = label
}
storage.previousState = self
}
}

View File

@ -0,0 +1,145 @@
//
// MenuButton.swift
// MacBackend
//
// Created by david-swift on 22.10.23.
//
import AppKit
/// A button widget for menus.
public struct MenuButton: MenuWidget {
/// The button's label.
var label: String
/// The button's action handler.
var handler: () -> Void
/// The keyboard shortcut.
var shortcut: KeyboardShortcut?
/// Whether the button is selected.
var selected: Bool?
/// Whether the button is enabled.
var enabled = true
/// The action label.
var filteredLabel: String { label.filter { $0.isLetter || $0.isNumber || $0 == "-" || $0 == "." } }
/// Initialize a menu button.
/// - Parameters:
/// - label: The buttons label.
/// - handler: The button's action handler.
public init(_ label: String, handler: @escaping () -> Void) {
self.label = label
self.handler = handler
}
/// The view storage.
/// - Parameters:
/// - data: The widget data.
/// - type: The type of the views.
/// - Returns: The view storage.
public func container<Data>(
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let button = NSMenuItem()
let storage = ViewStorage(button)
update(storage, data: data, updateProperties: true, type: type)
return storage
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - data: The widget data.
/// - updateProperties: Whether to update the properties.
/// - type: The type of the views.
public func update<Data>(
_ storage: ViewStorage,
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
guard let button = storage.pointer as? NSMenuItem else {
return
}
if enabled {
button.actionClosure = handler
} else {
button.action = nil
}
guard updateProperties else {
return
}
let previousState = storage.previousState as? Self
if previousState?.label != label {
button.title = label
}
if let shortcut, previousState?.shortcut != shortcut {
button.keyEquivalent = shortcut.character.macOSRepresentation
button.keyEquivalentModifierMask = shortcut.modifiers
}
if let selected, previousState?.selected != selected {
button.state = selected ? .on : .off
}
storage.previousState = self
}
/// Create a keyboard shortcut for an application from a button.
///
/// Note that the keyboard shortcut is available after the view has been visible for the first time.
/// - Parameter shortcut: The keyboard shortcut.
/// - Returns: The button.
public func keyboardShortcut(_ shortcut: KeyboardShortcut) -> Self {
modify { $0.shortcut = shortcut }
}
/// Create a keyboard shortcut for an application from a button.
///
/// Note that the keyboard shortcut is available after the view has been visible for the first time.
/// - Parameter shortcut: The keyboard shortcut.
/// - Returns: The button.
public func keyboardShortcut(_ shortcut: Character) -> Self {
modify { $0.shortcut = .init(shortcut) }
}
/// Whether the button is selected.
/// - Parameter selected: Whether it is selected.
/// - Returns: The button.
public func selected(_ selected: Bool = true) -> Self {
modify { $0.selected = selected }
}
/// Whether the button is enabled.
/// - Parameter enabled: Whether it is enabled.
/// - Returns: The button.
public func enabled(_ enabled: Bool = true) -> Self {
modify { $0.enabled = enabled }
}
}
extension NSMenuItem {
/// The closure key.
private static var closureKey: UInt8 = 0
/// The action closure.
var actionClosure: (() -> Void)? {
get {
objc_getAssociatedObject(self, &Self.closureKey) as? () -> Void
}
set {
objc_setAssociatedObject(self, &Self.closureKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
self.target = self
self.action = #selector(menuItemAction)
}
}
/// The action closure.
@objc
private func menuItemAction() {
actionClosure?()
}
}

View File

@ -0,0 +1,85 @@
//
// MenuCollection.swift
// MacBackend
//
// Created by david-swift on 02.08.2024.
//
import AppKit
import Foundation
/// A collection of menus.
public struct MenuCollection: MenuWidget, Wrapper {
/// The content of the collection.
var content: Body
/// Initialize a menu.
/// - Parameter content: The content of the collection.
public init(@ViewBuilder content: @escaping () -> Body) {
self.content = content()
}
/// The view storage.
/// - Parameters:
/// - data: The widget data.
/// - type: The type of the views.
/// - Returns: The view storage.
public func container<Data>(
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let storages = content.storages(data: data, type: type)
return .init(nil, content: [.mainContent: storages])
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - data: The widget data.
/// - updateProperties: Whether to update the properties.
/// - type: The type of the views.
public func update<Data>(
_ storage: ViewStorage,
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
guard let storages = storage.content[.mainContent] else {
return
}
content.update(storages, data: data, updateProperties: updateProperties, type: type)
}
/// Render the collection as a menu.
/// - Parameters:
/// - data: The widget data.
/// - menu: The menu.
/// - Returns: The view storage with the GMenu as the pointer.
public func getMenu(data: WidgetData, menu: NSMenu?) -> ViewStorage {
let item = NSMenuItem()
let menu = menu ?? .init()
let storage = container(data: data.noModifiers, type: MenuContext.self)
initializeMenu(menu: menu, storage: storage)
storage.pointer = item
item.menu = menu
return storage
}
/// Initialize a menu.
/// - Parameters:
/// - menu: The pointer to the GMenu.
/// - storage: The storage for the menu's content.
/// - app: The app object.
/// - window: The window object.
func initializeMenu(menu: NSMenu, storage: ViewStorage) {
if let item = storage.pointer as? NSMenuItem {
menu.addItem(item)
} else {
for element in storage.content[.mainContent] ?? [] {
initializeMenu(menu: menu, storage: element)
}
}
}
}

View File

@ -0,0 +1,21 @@
//
// MenuContext.swift
// MacBackend
//
// Created by david-swift on 01.08.24.
//
/// The menu items view context.
public enum MenuContext: ViewRenderData {
/// The type of the widgets.
public typealias WidgetType = MenuWidget
/// The wrapper type.
public typealias WrapperType = MenuCollection
/// The either view type.
public typealias EitherViewType = MenuEitherView
}
/// The type of the widgets.
public protocol MenuWidget: Meta.Widget { }

View File

@ -0,0 +1,23 @@
//
// MenuEitherView.swift
// MacBackend
//
// Created by david-swift on 06.08.2024.
//
/// Show one of two views depending on a condition.
public struct MenuEitherView: Meta.EitherView, SimpleView {
/// The view.
public var view: Body
/// Initialize an either view.
/// - Parameters:
/// - condition: The condition.
/// - view1: The first view.
/// - view2: The second view.
public init(_ condition: Bool, view1: () -> Body, else view2: () -> Body) {
self.view = condition ? view1() : view2()
}
}

View File

@ -0,0 +1,61 @@
//
// DefaultMenu.swift
// MacBackend
//
// Created by david-swift on 10.09.2024.
//
import AppKit
/// A services submenu.
public struct ServicesMenu: MenuWidget {
/// The label of the submenu.
var label: String
/// Initialize a submenu.
/// - Parameter label: The submenu's label.
public init(_ label: String) {
self.label = label
}
/// The view storage.
/// - Parameters:
/// - data: The widget data.
/// - type: The type of the views.
/// - Returns: The view storage.
public func container<Data>(
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let item = NSMenuItem()
item.submenu = .init()
NSApp.servicesMenu = item.submenu
let storage = ViewStorage(item)
update(storage, data: data, updateProperties: true, type: type)
return storage
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - data: The widget data.
/// - updateProperties: Whether to update the properties.
/// - type: The type of the views.
public func update<Data>(
_ storage: ViewStorage,
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
guard updateProperties, let item = storage.pointer as? NSMenuItem else {
return
}
let previousState = storage.previousState as? Self
if previousState?.label != label {
item.title = label
}
storage.previousState = self
}
}

View File

@ -0,0 +1,48 @@
//
// Edge.swift
// MacBackend
//
// Created by david-swift on 11.10.2024.
//
import AppKit
/// A view's edges.
public enum Edge {
/// The leading edge.
case leading
/// The trailing edge.
case trailing
/// The top edge.
case top
/// The bottom edge.
case bottom
/// Activate layout constraints affecting this edge.
/// - Parameters:
/// - view: The view.
/// - parent: The parent view.
/// - padding: The padding value.
func activate(in view: NSView, to parent: NSView, padding: CGFloat) {
switch self {
case .top:
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: view.topAnchor, constant: padding)
])
case .bottom:
NSLayoutConstraint.activate([
view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -padding)
])
case .leading:
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding)
])
case .trailing:
NSLayoutConstraint.activate([
view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding)
])
}
}
}

View File

@ -0,0 +1,64 @@
//
// Font.swift
// MacBackend
//
// Created by david-swift on 01.12.2024.
//
import SwiftUI
/// The font.
public enum Font {
/// The body font.
case body
/// The callout font.
case callout
/// The caption font.
case caption
/// The caption font 2.
case caption2
/// The footnote font.
case footnote
/// The headline font.
case headline
/// The large title font.
case largeTitle
/// The subheadline font.
case subheadline
/// The title font.
case title
/// The title font 2.
case title2
/// The title font 3.
case title3
/// The SwiftUI font.
var swiftUI: SwiftUI.Font {
switch self {
case .body:
.body
case .callout:
.callout
case .caption:
.caption
case .caption2:
.caption2
case .footnote:
.footnote
case .headline:
.headline
case .largeTitle:
.largeTitle
case .subheadline:
.subheadline
case .title:
.title
case .title2:
.title2
case .title3:
.title3
}
}
}

View File

@ -0,0 +1,24 @@
//
// Icon.swift
// MacBackend
//
// Created by david-swift on 30.11.2024.
//
import SwiftUI
/// The icon type.
public enum Icon {
/// A system icon.
case system(name: String)
/// The SwiftUI image.
var image: Image {
switch self {
case let .system(name):
.init(systemName: name)
}
}
}

View File

@ -0,0 +1,115 @@
//
// KeyboardShortcut.swift
// MacBackend
//
// Created by david-swift on 10.09.2024.
//
import AppKit
/// A keyboard shortcut used e.g. in menus.
public struct KeyboardShortcut: Equatable {
/// The character or a sequence representing a letter or symbol.
public var character: ShortcutCharacter
/// Whether the Ctrl key is part of the shortcut.
public var command: Bool
/// Whether the Shift key is part of the shortcut.
public var shift: Bool
/// Whether the Alt key is part of the shortcut.
public var option: Bool
/// The modifiers for the macOS shortcut.
var modifiers: NSEvent.ModifierFlags {
var flags: NSEvent.ModifierFlags = []
if command {
flags.insert(.command)
}
if shift {
flags.insert(.shift)
}
if option {
flags.insert(.option)
}
return flags
}
/// Initialize a keyboard shortcut.
/// - Parameters:
/// - character: A letter.
/// - ctrl: Whether Ctrl is part of the shortcut.
/// - shift: Whether Shift is part of the shortcut.
/// - alt: Whether Alt is part of the shortcut.
public init(_ character: Character, ctrl: Bool = true, shift: Bool = false, alt: Bool = false) {
self.character = .character(character)
self.command = ctrl
self.shift = shift
self.option = alt
}
/// Initialize a keyboard shortcut.
/// - Parameters:
/// - symbol: A character.
/// - ctrl: Whether Ctrl is part of the shortcut.
/// - shift: Whether Shift is part of the shortcut.
/// - alt: Whether Alt is part of the shortcut.
public init(symbol: ShortcutCharacter, ctrl: Bool = true, shift: Bool = false, alt: Bool = false) {
self.character = symbol
self.command = ctrl
self.shift = shift
self.option = alt
}
/// The special characters available for shortcuts.
public enum ShortcutCharacter: Equatable {
/// The character.
case backspace
/// The character.
case delete
/// The character.
case tab
/// The character.
case enter
/// The character.
case escape
/// The character.
case space
// swiftlint:disable identifier_name
/// An arrow key.
case up, down, left, right
// swiftlint:enable identifier_name
/// A custom character.
case character(_ character: Character)
/// A representation of the keys for macOS.
var macOSRepresentation: String {
switch self {
case .backspace:
return "\u{8}"
case .delete:
return "\u{7F}"
case .tab:
return "\u{9}"
case .enter:
return "\u{A}"
case .escape:
return "\u{1B}"
case .space:
return " "
case .up:
return ""
case .down:
return ""
case .left:
return ""
case .right:
return ""
case let .character(character):
return "\(character)"
}
}
}
}

View File

@ -0,0 +1,22 @@
//
// Meta.Binding.swift
// MacBackend
//
// Created by david-swift on 29.11.2024.
//
import SwiftUI
extension Meta.Binding {
/// The SwiftUI binding.
public var swiftUI: SwiftUI.Binding<Value> {
.init {
wrappedValue
} set: { newValue in
wrappedValue = newValue
}
}
}

View File

@ -0,0 +1,65 @@
//
// Set.swift
// MacBackend
//
// Created by david-swift on 11.10.2024.
//
import SwiftUI
extension Set where Element == Edge {
/// All edges.
public static var all: Self {
vertical.union(horizontal)
}
/// The vertical edges.
public static var vertical: Self {
top.union(bottom)
}
/// The horizontal edges.
public static var horizontal: Self {
leading.union(trailing)
}
/// The top edge.
public static var top: Self {
[.top]
}
/// The bottom edge.
public static var bottom: Self {
[.bottom]
}
/// The leading edge.
public static var leading: Self {
[.leading]
}
/// The trailing edge.
public static var trailing: Self {
[.trailing]
}
/// The SwiftUI edge.
var swiftUI: SwiftUI.Edge.Set {
var edges: SwiftUI.Edge.Set = []
for edge in self {
switch edge {
case .top:
edges.insert(.top)
case .bottom:
edges.insert(.bottom)
case .leading:
edges.insert(.leading)
case .trailing:
edges.insert(.trailing)
}
}
return edges
}
}

View File

@ -0,0 +1,102 @@
//
// MacApp.swift
// MacBackend
//
// Created by david-swift on 31.07.2024.
//
import AppKit
@_exported import Meta
@_exported import MetaSQLite
/// The Meta app storage for the macOS backend.
public class MacApp: AppStorage {
/// The scene element type of the macOS backend.
public typealias SceneElementType = MacSceneElement
/// The app storage.
public var storage: StandardAppStorage = .init()
/// The application.
let app = NSApplication.shared
/// The menu bar.
let mainMenu = NSMenu()
/// The "<App>" menu.
let appItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
/// The "Window" menu.
let windowsItem = NSMenuItem(title: "Window", action: nil, keyEquivalent: "")
/// The "Help" menu.
let helpItem = NSMenuItem(title: "Help", action: nil, keyEquivalent: "")
/// Initialize the app storage.
/// - Parameter id: The identifier.
public init(id: String) {
app.mainMenu = mainMenu
DatabaseInformation.setPath(
URL.applicationSupportDirectory
.appendingPathComponent(id)
.appendingPathComponent("database.sqlite")
.path
)
}
/// The app menu.
func appMenu() {
let appMenu = NSMenu()
mainMenu.addItem(appItem)
appItem.submenu = appMenu
}
/// The windows menu.
func windowsMenu() {
let windowsMenu = NSMenu()
mainMenu.addItem(windowsItem)
windowsItem.submenu = windowsMenu
app.windowsMenu = windowsMenu
}
/// The help menu.
func helpMenu() {
let helpMenu = NSMenu()
mainMenu.addItem(helpItem)
helpItem.submenu = helpMenu
app.helpMenu = helpMenu
}
/// Execute the app.
/// - Parameter setup: Set the scene elements up.
public func run(setup: @escaping () -> Void) {
appMenu()
setup()
windowsMenu()
helpMenu()
app.run()
}
/// Present the about window.
public func showAboutWindow() {
app.orderFrontStandardAboutPanel()
}
/// Quit the app.
@objc
public func quit() {
app.terminate(nil)
}
/// Hide the app.
public func hide() {
app.hide(nil)
}
/// Hide other apps.
public func hideOthers() {
app.hideOtherApplications(nil)
}
/// Show all the apps.
public func showAll() {
app.unhideAllApplications(nil)
}
}

View File

@ -0,0 +1,18 @@
//
// MacMainView.swift
// MacBackend
//
// Created by david-swift on 31.07.2024.
//
/// The type of widgets of the macOS backend.
public enum MacMainView: ViewRenderData {
/// The type of the widgets.
public typealias WidgetType = MacWidget
/// The wrapper type.
public typealias WrapperType = VStack
/// The either view type.
public typealias EitherViewType = EitherView
}

View File

@ -0,0 +1,9 @@
//
// MacSceneElement.swift
// MacBackend
//
// Created by david-swift on 31.07.2024.
//
/// The type of scene elements of the macOS backend.
public protocol MacSceneElement: SceneElement { }

View File

@ -0,0 +1,9 @@
//
// MacWidget.swift
// MacBackend
//
// Created by david-swift on 31.07.2024.
//
/// The type of widgets of the macOS backend.
public protocol MacWidget: Widget { }

View File

@ -0,0 +1,38 @@
//
// MacBackendView.swift
// MacBackend
//
// Created by david-swift on 27.11.2024.
//
import SwiftUI
/// Display a view inside a SwiftUI view which is defined in the parent `MacBackend` widget.
struct MacBackendView: NSViewRepresentable {
/// The views defined in the parent `MacBackend` widget.
@SwiftUI.Environment(\.views)
var views
/// The view's identifier.
var id: String
/// Initialize a SwiftUI view displaying a view which is defined in the parent `MacBackend` widget.
/// - Parameter id: The identifier.
init(_ id: String) {
self.id = id
}
/// Initialize the `NSView`.
/// - Parameter context: The view context.
/// - Returns: The view.
func makeNSView(context: Context) -> NSView {
views?[id]?.pointer as? NSView ?? .init()
}
/// Update the `NSView`.
/// - Parameters:
/// - nsView: The view.
/// - context: The view context.
func updateNSView(_ nsView: NSViewType, context: Context) { }
}

View File

@ -0,0 +1,175 @@
//
// SwiftUIWidget.swift
// MacBackend
//
// Created by david-swift on 25.11.2024.
//
import LevenshteinTransformations
import SwiftUI
/// Wrap a SwiftUI widget to be used inside a `MacBackend` view.
public protocol SwiftUIWidget: MacWidget {
/// The content SwiftUI view.
associatedtype Content: SwiftUI.View
/// The wrapped views.
var wrappedViews: [String: Meta.AnyView] { get }
/// Get the SwiftUI view.
/// - Parameter properties: The widget data.
/// - Returns: The SwiftUI view.
@SwiftUI.ViewBuilder
static func view(properties: Self) -> Content
}
extension SwiftUIWidget {
/// The wrapped views.
public var wrappedViews: [String: Meta.AnyView] {
[:]
}
/// The view storage.
/// - Parameters:
/// - data: Modify views before being updated.
/// - type: The view render data type.
/// - Returns: The view storage.
public func container<Data>(
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
internalContainer(data: data, type: type)
}
/// The view storage.
/// - Parameters:
/// - data: Modify views before being updated.
/// - type: The view render data type.
/// - Returns: The view storage.
func internalContainer<Data>(
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let id = UUID().uuidString
let updater = SwiftUIUpdater.updater
updater.state[id] = self
let wrappedStorages = wrappedViews.reduce(into: [String: ViewStorage]()) { partialResult, element in
partialResult[element.key] = element.value.storage(data: data, type: type)
}
let storage: ViewStorage = .init(nil)
storage.fields["child-storages"] = wrappedStorages
let view = NSHostingView(
rootView: SwiftUIWrapperView(updater: updater, id: id, data: data) { value in
if let value = value as? Self {
Self.view(properties: value)
.environment(\.views, storage.fields["child-storages"] as? [String: ViewStorage])
}
}
)
storage.pointer = view
storage.fields["updater-id"] = id
return storage
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - data: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The view render data type.
public func update<Data>(
_ storage: ViewStorage,
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
internalUpdate(storage, data: data, updateProperties: updateProperties, type: type)
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - data: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The view render data type.
func internalUpdate<Data>(
_ storage: ViewStorage,
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
if updateProperties, let id = storage.fields["updater-id"] as? String {
SwiftUIUpdater.updater.state[id] = self
}
var children = storage.fields["child-storages"] as? [String: ViewStorage] ?? [:]
for view in wrappedViews where !children.contains(where: { $0.key == view.key }) {
children[view.key] = view.value.storage(data: data, type: type)
}
for view in children where !wrappedViews.contains(where: { $0.key == view.key }) {
children[view.key] = nil
}
storage.fields["child-storages"] = children
for (key, storage) in children {
wrappedViews[key]?.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
}
}
}
/// A SwiftUI view which can be displayed and updated inside a `MacBackend` widget.
struct SwiftUIWrapperView<Content>: SwiftUI.View where Content: SwiftUI.View {
/// The updater observable object.
@ObservedObject var updater: SwiftUIUpdater
/// The identifier.
var id: String
/// The widget data.
var data: WidgetData
/// The wrapped view.
var view: (Any) -> Content
/// The SwiftUI view content.
var body: some SwiftUI.View {
if let state = updater.state[id] {
view(state)
}
}
/// Initialize the SwiftUI wrapper view.
/// - Parameters:
/// - updater: The updater observable object.
/// - id: The identifier.
/// - data: The widget data.
/// - view: The wrapped view.
init(updater: SwiftUIUpdater, id: String, data: WidgetData, @SwiftUI.ViewBuilder view: @escaping (Any) -> Content) {
self.updater = updater
self.id = id
self.view = view
self.data = data
}
}
extension EnvironmentValues {
/// The views environment value.
@Entry var views: [String: ViewStorage]?
}
/// The SwiftUI updater object.
class SwiftUIUpdater: ObservableObject {
/// The updater.
static var updater: SwiftUIUpdater = .init()
/// The state for SwiftUI views.
@Published var state: [String: Any] = [:]
/// Initialize an updater.
init() { }
}

View File

@ -0,0 +1,164 @@
//
// Alert.swift
// MacBackend
//
// Created by david-swift on 01.12.2024.
//
import SwiftUI
/// The alert view.
public struct Alert: SwiftUIWidget {
/// The alert's title.
var title: String
/// The alert's description.
var description: String
/// Whether the alert is presented.
var isPresented: Meta.Binding<Bool>
/// The alert's actions.
var actions: [Action] = []
/// The wrapped view.
var child: Meta.AnyView
/// The wrapped views.
public var wrappedViews: [String: Meta.AnyView] {
[.mainContent: child]
}
/// An alert action.
enum Action {
/// A regular button.
case button(button: Button)
/// A cancel button.
case cancel(button: Button)
/// A destructive button.
case destructive(button: Button)
/// Get the SwiftUI button.
@SwiftUI.ViewBuilder var button: some SwiftUI.View {
switch self {
case let .button(button):
button.button()
case let .cancel(button):
button.button(cancel: true)
case let .destructive(button):
button.button(destructive: true)
}
}
}
/// The button.
struct Button {
/// The button's label.
var label: String
/// The button's action.
var action: () -> Void
/// Whether it is the default action.
var defaultAction: Bool
/// Get the SwiftUI button.
/// - Parameters:
/// - cancel: Whether it is the cancel action.
/// - destructive: Whether it is a destructive action.
/// - Returns: The SwiftUI view.
@SwiftUI.ViewBuilder
func button(cancel: Bool = false, destructive: Bool = false) -> some SwiftUI.View {
let button = SwiftUI.Button(
label,
role: cancel ? .cancel : (destructive ? .destructive : nil),
action: action
)
if defaultAction {
button
.keyboardShortcut(.defaultAction)
} else {
button
}
}
}
/// Get the SwiftUI view.
/// - Parameter properties: The widget data.
/// - Returns: The SwiftUI view.
public static func view(properties: Self) -> some SwiftUI.View {
MacBackendView(.mainContent)
.alert(properties.title, isPresented: properties.isPresented.swiftUI) {
ForEach(Array(properties.actions.enumerated()), id: \.offset) { action in
action.element.button
}
} message: {
SwiftUI.Text(properties.description)
}
}
/// Add a button to the alert.
/// - Parameters:
/// - label: The button's label.
/// - defaultAction: Whether it is a default action.
/// - action: The handler.
/// - Returns: The alert.
public func button(
_ label: String,
default defaultAction: Bool = false,
action: @escaping () -> Void
) -> Self {
modify { alert in
alert.actions.append(.button(button: .init(label: label, action: action, defaultAction: defaultAction)))
}
}
/// Add a cancel button to the alert.
/// - Parameters:
/// - label: The button's label.
/// - defaultAction: Whether it is a default action.
/// - action: The handler.
/// - Returns: The alert.
public func cancelButton(
_ label: String,
default defaultAction: Bool = false,
action: @escaping () -> Void
) -> Self {
modify { alert in
alert.actions.append(.cancel(button: .init(label: label, action: action, defaultAction: defaultAction)))
}
}
/// Add a destructive button to the alert.
/// - Parameters:
/// - label: The button's label.
/// - defaultAction: Whether it is a default action.
/// - action: The handler.
/// - Returns: The alert.
public func destructiveButton(
_ label: String,
default defaultAction: Bool = false,
action: @escaping () -> Void
) -> Self {
modify { alert in
alert.actions
.append(.destructive(button: .init(label: label, action: action, defaultAction: defaultAction)))
}
}
}
extension Meta.AnyView {
// swiftlint:disable function_default_parameter_at_end
/// Add an alert to a view.
/// - Parameters:
/// - title: The title.
/// - description: The description.
/// - isPresented: Whether the alert is visible.
/// - Returns: The alert.
public func alert(_ title: String, description: String = "", isPresented: Meta.Binding<Bool>) -> Alert {
.init(title: title, description: description, isPresented: isPresented, child: self)
}
// swiftlint:enable function_default_parameter_at_end
}

View File

@ -0,0 +1,64 @@
//
// Button.swift
// MacBackend
//
// Created by david-swift on 18.09.2024.
//
import SwiftUI
/// A button widget.
public struct Button: SwiftUIWidget {
/// The button's label.
var label: String?
/// The button's icon.
var icon: Icon?
/// The button's action.
var action: () -> Void
/// Initialize a button.
/// - Parameters:
/// - label: The button's label.
/// - icon: The button's icon.
/// - action: The handler.
public init(_ label: String, icon: Icon? = nil, action: @escaping () -> Void) {
self.label = label
self.icon = icon
self.action = action
}
/// Initialize a button.
/// - Parameters:
/// - icon: The button's icon.
/// - action: The handler.
public init(icon: Icon, action: @escaping () -> Void) {
self.icon = icon
self.action = action
}
/// Get the SwiftUI view.
/// - Parameter properties: The widget data.
/// - Returns: The SwiftUI view.
public static func view(properties: Self) -> some SwiftUI.View {
if let icon = properties.icon {
SwiftUI.Button {
properties.action()
} label: {
SwiftUI.Label {
if let label = properties.label {
SwiftUI.Text(label)
}
} icon: {
icon.image
}
}
} else if let label = properties.label {
SwiftUI.Button(label) {
properties.action()
}
}
}
}

View File

@ -0,0 +1,48 @@
//
// EitherView.swift
// MacBackend
//
// Created by david-swift on 02.12.24.
//
import SwiftUI
/// A widget showing one of two widgets based on a condition.
public struct EitherView: SwiftUIWidget, Meta.EitherView {
/// The condition.
var condition: Bool
/// The first view.
var view1: Body
/// The second view.
var view2: Body
/// The wrapped views.
public var wrappedViews: [String: Meta.AnyView] {
condition ? ["1": view1] : ["2": view2]
}
/// Initialize an either view.
/// - Parameters:
/// - condition: The condition.
/// - view1: The first view.
/// - view2: The second view.
public init(_ condition: Bool, view1: () -> Body, else view2: () -> Body) {
self.condition = condition
self.view1 = view1()
self.view2 = view2()
}
/// Get the SwiftUI view.
/// - Parameter properties: The widget data.
/// - Returns: The SwiftUI view.
@SwiftUI.ViewBuilder
public static func view(properties: Self) -> some SwiftUI.View {
if properties.condition {
MacBackendView("1")
} else {
MacBackendView("2")
}
}
}

View File

@ -0,0 +1,39 @@
//
// Label.swift
// MacBackend
//
// Created by david-swift on 30.11.2024.
//
import SwiftUI
/// A label widget.
public struct Label: SwiftUIWidget {
/// The label.
var label: String
/// The icon.
var icon: Icon
/// Initialize the label.
/// - Parameters:
/// - label: The text.
/// - icon: The icon.
public init(_ label: String, icon: Icon) {
self.label = label
self.icon = icon
}
/// Get the SwiftUI view.
/// - Parameter properties: The widget data.
/// - Returns: The SwiftUI view.
public static func view(properties: Self) -> some SwiftUI.View {
SwiftUI.Label {
SwiftUI.Text(properties.label)
} icon: {
properties.icon.image
}
}
}

View File

@ -0,0 +1,51 @@
//
// List.swift
// MacBackend
//
// Created by david-swift on 23.11.2024.
//
import SwiftUI
/// A list widget.
public struct List<Element>: SwiftUIWidget where Element: Identifiable {
/// The elements.
var elements: [Element]
/// The selected element.
var selection: Meta.Binding<Element.ID?>
/// The content for an element.
var content: (Element) -> Body
/// The wrapped views.
public var wrappedViews: [String: any Meta.AnyView] {
elements.reduce(into: [:]) { partialResult, element in
partialResult["\(element.id)"] = content(element)
}
}
/// Initialize a list widget.
/// - Parameters:
/// - elements: The elements.
/// - selection: The selected element.
/// - content: The content for an element.
public init(
_ elements: [Element],
selection: Meta.Binding<Element.ID?>,
@Meta.ViewBuilder content: @escaping (Element) -> Body
) {
self.elements = elements
self.content = content
self.selection = selection
}
/// Get the SwiftUI view.
/// - Parameter properties: The widget data.
/// - Returns: The SwiftUI view.
public static func view(properties: Self) -> some SwiftUI.View {
SwiftUI.List(properties.elements, selection: properties.selection.swiftUI) { element in
MacBackendView("\(element.id)")
}
}
}

View File

@ -0,0 +1,46 @@
//
// NavigationSplitView.swift
// MacBackend
//
// Created by david-swift on 18.09.2024.
//
import SwiftUI
/// A navigation split view widget.
public struct NavigationSplitView: SwiftUIWidget {
/// The sidebar view.
var sidebar: Body
/// The detail view.
var detail: Body
/// The wrapped views.
public var wrappedViews: [String: Meta.AnyView] {
["sidebar": sidebar, "detail": detail]
}
/// Initialize the navigation split view.
/// - Parameters:
/// - sidebar: The sidebar view.
/// - detail: The detail view.
public init(
@Meta.ViewBuilder sidebar: () -> Body,
@Meta.ViewBuilder detail: () -> Body
) {
self.detail = detail()
self.sidebar = sidebar()
}
/// Get the SwiftUI view.
/// - Parameter properties: The widget data.
/// - Returns: The SwiftUI view.
public static func view(properties: Self) -> some SwiftUI.View {
SwiftUI.NavigationSplitView {
MacBackendView("sidebar")
} detail: {
MacBackendView("detail")
}
}
}

View File

@ -0,0 +1,46 @@
//
// PaddingView.swift
// MacBackend
//
// Created by david-swift on 11.10.2024.
//
import SwiftUI
/// The padding view.
struct PaddingView: SwiftUIWidget {
/// The padding.
var padding: Double
/// The edges.
var edges: Set<Edge>
/// The wrapped view.
var child: Meta.AnyView
/// The wrapped views.
var wrappedViews: [String: Meta.AnyView] {
[.mainContent: child]
}
/// Get the SwiftUI view.
/// - Parameter properties: The widget data.
/// - Returns: The SwiftUI view.
static func view(properties: Self) -> some SwiftUI.View {
MacBackendView(.mainContent)
.padding(properties.edges.swiftUI, properties.padding)
}
}
extension Meta.AnyView {
/// Set the padding.
/// - Parameters:
/// - padding: The padding.
/// - edges: The edges.
/// - Returns: The view.
public func padding(_ padding: Double, edges: Set<Edge> = .all) -> Meta.AnyView {
PaddingView(padding: padding, edges: edges, child: self)
}
}

View File

@ -0,0 +1,36 @@
//
// ScrollView.swift
// MacBackend
//
// Created by david-swift on 02.12.2024.
//
import SwiftUI
/// The scroll view widget.
public struct ScrollView: SwiftUIWidget {
/// The view's content.
var content: Body
/// The wrapped views.
public var wrappedViews: [String: any Meta.AnyView] {
[.mainContent: content]
}
/// Initialize the scroll view.
/// - Parameter content: The content view.
public init(@Meta.ViewBuilder content: () -> Body) {
self.content = content()
}
/// Get the SwiftUI view.
/// - Parameter properties: The widget data.
/// - Returns: The SwiftUI view.
public static func view(properties: Self) -> some SwiftUI.View {
SwiftUI.ScrollView {
MacBackendView(.mainContent)
}
}
}

View File

@ -0,0 +1,23 @@
//
// Spacer.swift
// MacBackend
//
// Created by david-swift on 01.12.2024.
//
import SwiftUI
/// The spacer widget.
public struct Spacer: SwiftUIWidget {
/// Initialize the spacer.
public init() { }
/// Get the SwiftUI view.
/// - Parameter properties: The widget data.
/// - Returns: The SwiftUI view.
public static func view(properties: Self) -> some SwiftUI.View {
SwiftUI.Spacer()
}
}

View File

@ -0,0 +1,56 @@
//
// Text.swift
// MacBackend
//
// Created by david-swift on 29.11.2024.
//
import SwiftUI
/// The text widget.
public struct Text: SwiftUIWidget {
/// The label.
var label: String
/// The font.
var font: Font?
/// Whether the text is selectable.
var selectionDisabled = true
/// Initialize the text widget.
/// - Parameter label: The text.
public init(_ label: String) {
self.label = label
}
/// Get the SwiftUI view.
/// - Parameter properties: The widget data.
/// - Returns: The SwiftUI view.
@SwiftUI.ViewBuilder
public static func view(properties: Self) -> some SwiftUI.View {
let text = SwiftUI.Text(properties.label)
.font(properties.font?.swiftUI)
if properties.selectionDisabled {
text
.textSelection(.disabled)
} else {
text
.textSelection(.enabled)
}
}
/// Set the font.
/// - Parameter font: The font.
/// - Returns: The text view.
public func font(_ font: Font?) -> Self {
modify { $0.font = font }
}
/// Whether the selection is disabled.
/// - Parameter isDisabled: The selection is disabled.
/// - Returns: The text view.
public func selectionDisabled(_ isDisabled: Bool = true) -> Self {
modify { $0.selectionDisabled = isDisabled }
}
}

View File

@ -0,0 +1,71 @@
//
// VStack.swift
// MacBackend
//
// Created by david-swift on 23.08.23.
//
import SwiftUI
/// A `VStack` view.
public struct VStack: SwiftUIWidget, Wrapper {
/// The content view.
var content: Body
/// The wrapped views.
public var wrappedViews: [String: Meta.AnyView] {
content.enumerated().reduce(into: [:]) { partialResult, element in
partialResult["\(element.offset)"] = element.element
}
}
/// Initialize the ``VStack``.
/// - Parameter content: The content.
public init(@Meta.ViewBuilder content: @escaping () -> Body) {
self.content = content()
}
/// Get the SwiftUI view.
/// - Parameter properties: The widget data.
/// - Returns: The SwiftUI view.
public static func view(properties: Self) -> some SwiftUI.View {
SwiftUI.VStack {
ForEach(properties.content.indices, id: \.self) { index in
MacBackendView("\(index)")
}
}
}
/// The view storage.
/// - Parameters:
/// - data: Modify views before being updated.
/// - type: The view render data type.
/// - Returns: The view storage.
public func container<Data>(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
if content.count == 1, let storage = content.first?.storage(data: data, type: type) {
return storage
}
return internalContainer(data: data, type: type)
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - data: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The view render data type.
public func update<Data>(
_ storage: ViewStorage,
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
if content.count == 1, let first = content.first {
first.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
} else {
internalUpdate(storage, data: data, updateProperties: updateProperties, type: type)
}
}
}

View File

@ -0,0 +1,88 @@
//
// MenuBar.swift
// MacBackend
//
// Created by david-swift on 08.09.2024.
//
import AppKit
/// A structure representing the menu bar.
public struct MenuBar: MacSceneElement {
/// The window's identifier.
public var id: String
/// The window's content.
var content: Body
/// The app menu.
var app: Body
/// The window menu.
var window: Body
/// The help menu.
var help: Body
/// Create a menu bar.
/// - Parameters:
/// - id: The identifier.
/// - content: The content.
/// - app: The app menu.
/// - window: The window menu.
/// - help: The help menu.
public init(
id: String = "main-menu-bar",
@ViewBuilder content: @escaping () -> Body,
@ViewBuilder app: @escaping () -> Body = { [] },
@ViewBuilder window: @escaping () -> Body = { [] },
@ViewBuilder help: @escaping () -> Body = { [] }
) {
self.content = content()
self.id = id
self.app = app()
self.window = window()
self.help = help()
}
/// Set up the initial scene storages.
/// - Parameter app: The app storage.
public func setupInitialContainers<Storage>(app: Storage) where Storage: AppStorage {
let container = container(app: app)
container.show()
app.storage.sceneStorage.append(container)
}
/// The scene storage.
/// - Parameter app: The app storage.
public func container<Storage>(app: Storage) -> SceneStorage where Storage: AppStorage {
guard let app = app as? MacApp else {
return .init(id: id, pointer: nil) { }
}
let scene = SceneStorage(id: id, pointer: app.mainMenu) { }
let data = WidgetData(sceneStorage: scene, appStorage: app)
let storage = MenuCollection { self.content }.getMenu(data: data, menu: app.mainMenu)
let appStorage = MenuCollection { self.app }.getMenu(data: data, menu: app.appItem.submenu)
scene.content[.mainContent] = [storage]
scene.content["app"] = [appStorage]
return scene
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - app: The app storage.
/// - updateProperties: Whether to update the view's properties.
public func update<Storage>(
_ storage: SceneStorage,
app: Storage,
updateProperties: Bool
) where Storage: AppStorage {
let data = WidgetData(sceneStorage: storage, appStorage: app)
if let content = storage.content["app"]?.first {
self.app.updateStorage(content, data: data, updateProperties: updateProperties, type: MenuContext.self)
}
guard let content = storage.content[.mainContent]?.first else {
return
}
self.content.updateStorage(content, data: data, updateProperties: updateProperties, type: MenuContext.self)
}
}

View File

@ -0,0 +1,157 @@
//
// Window.swift
// MacBackend
//
// Created by david-swift on 14.09.23.
//
import AppKit
/// A structure representing an application window type.
///
/// Note that it may be possible to open multiple instances of a window at the same time.
public struct Window: MacSceneElement {
/// The window's identifier.
public var id: String
/// The window's content.
var content: Body
/// Whether an instance of the window type should be opened when the app is starting up.
var `open`: Int
/// The window's title.
var title: String
/// Whether the window is miniaturizable.
var miniaturizable = true
/// Whether the window is resizable.
var resizable = true
/// The window's width.
var width: Binding<Int>?
/// The window's height.
var height: Binding<Int>?
// swiftlint:disable function_default_parameter_at_end
/// Create a window type with a certain identifier and user interface.
/// - Parameters:
/// - id: The identifier.
/// - open: The number of instances of the window type when the app is starting.
/// - content: The window's content.
/// - title: The window's title.
public init(_ title: String = "", id: String, `open`: Int = 1, @ViewBuilder content: @escaping () -> Body) {
self.title = title
self.content = content()
self.id = id
self.open = open
}
// swiftlint:enable function_default_parameter_at_end
/// Set up the initial scene storages.
/// - Parameter app: The app storage.
public func setupInitialContainers<Storage>(app: Storage) where Storage: AppStorage {
for _ in 0..<open {
let container = container(app: app)
container.show()
app.storage.sceneStorage.append(container)
}
}
/// The scene storage.
/// - Parameter app: The app storage.
public func container<Storage>(app: Storage) -> SceneStorage where Storage: AppStorage {
let window = NSWindow()
let storage = SceneStorage(id: id, pointer: window) {
window.makeKeyAndOrderFront(nil)
}
NotificationCenter.default
.addObserver(forName: NSWindow.willCloseNotification, object: window, queue: nil) { _ in
storage.destroy = true
}
let content = content.storage(data: .init(sceneStorage: storage, appStorage: app), type: MacMainView.self)
if let pointer = content.pointer as? NSView {
window.contentView = pointer
}
storage.content[.mainContent] = [content]
window.styleMask = [.titled, .closable, .fullSizeContentView, .resizable, .miniaturizable]
update(storage, app: app, updateProperties: true)
window.setFrame(
.init(origin: .zero, size: .init(width: width?.wrappedValue ?? -1, height: height?.wrappedValue ?? -1)),
display: true
)
return storage
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - app: The app storage.
/// - updateProperties: Whether to update the view's properties.
public func update<Storage>(
_ storage: SceneStorage,
app: Storage,
updateProperties: Bool
) where Storage: AppStorage {
if let content = storage.content[.mainContent]?.first {
self.content.updateStorage(
content,
data: .init(sceneStorage: storage, appStorage: app),
updateProperties: updateProperties,
type: MacMainView.self
)
}
guard let window = storage.pointer as? NSWindow else {
return
}
guard updateProperties else {
return
}
let previousState = storage.previousState as? Self
if previousState?.title != title {
window.title = title
}
if previousState?.miniaturizable != miniaturizable {
if miniaturizable {
window.styleMask.insert(.miniaturizable)
} else {
window.styleMask.remove(.miniaturizable)
}
}
if previousState?.resizable != resizable {
if resizable {
window.styleMask.insert(.resizable)
} else {
window.styleMask.remove(.resizable)
}
}
storage.previousState = self
}
/// The window's width and height.
/// - Parameters:
/// - width: The width.
/// - height: The height.
/// - Returns: The window.
public func frame(width: Binding<Int>? = nil, height: Binding<Int>? = nil) -> Self {
var newSelf = self
newSelf.width = width
newSelf.height = height
return newSelf
}
/// Whether the window is miniaturizable.
/// - Parameter miniaturizable: Whether the window is miniaturizable.
/// - Returns: The window.
public func miniaturizable(_ miniaturizable: Bool = true) -> Self {
var newSelf = self
newSelf.miniaturizable = miniaturizable
return newSelf
}
/// Whether the window is resizable.
/// - Parameter resizable: Whether the window is resizable.
/// - Returns: The window.
public func resizable(_ resizable: Bool = true) -> Self {
var newSelf = self
newSelf.resizable = resizable
return newSelf
}
}