commit e989bea14e2719d56670932f6454237e15866a5f
Author: david-swift
Date: Mon Dec 2 22:00:57 2024 +0100
Initial commit
diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.yml b/.gitea/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..ee1a591
--- /dev/null
+++ b/.gitea/ISSUE_TEMPLATE/bug_report.yml
@@ -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.
diff --git a/.gitea/ISSUE_TEMPLATE/component_request.yml b/.gitea/ISSUE_TEMPLATE/component_request.yml
new file mode 100644
index 0000000..6003cba
--- /dev/null
+++ b/.gitea/ISSUE_TEMPLATE/component_request.yml
@@ -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
diff --git a/.gitea/ISSUE_TEMPLATE/feature_request.yml b/.gitea/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..c244dbb
--- /dev/null
+++ b/.gitea/ISSUE_TEMPLATE/feature_request.yml
@@ -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
diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..f0ed3f3
--- /dev/null
+++ b/.gitea/PULL_REQUEST_TEMPLATE.md
@@ -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
diff --git a/.gitea/workflows/docs.yml b/.gitea/workflows/docs.yml
new file mode 100644
index 0000000..b1aa4be
--- /dev/null
+++ b/.gitea/workflows/docs.yml
@@ -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 "Please enable JavaScript to view the documentation here.
" > 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/'
diff --git a/.gitea/workflows/swiftlint.yml b/.gitea/workflows/swiftlint.yml
new file mode 100644
index 0000000..5348bdb
--- /dev/null
+++ b/.gitea/workflows/swiftlint.yml
@@ -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
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..477926e
--- /dev/null
+++ b/.gitignore
@@ -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
diff --git a/.swiftlint.yml b/.swiftlint.yml
new file mode 100644
index 0000000..ca8b7fd
--- /dev/null
+++ b/.swiftlint.yml
@@ -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/
diff --git a/Bundler.toml b/Bundler.toml
new file mode 100644
index 0000000..9b833fe
--- /dev/null
+++ b/Bundler.toml
@@ -0,0 +1,4 @@
+[apps."Demo"]
+product = 'Demo'
+version = '0.1.0'
+minimum_macos_version = '14'
\ No newline at end of file
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..67c6220
--- /dev/null
+++ b/LICENSE.md
@@ -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.
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..a31e2cc
--- /dev/null
+++ b/Package.swift
@@ -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]
+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1074d83
--- /dev/null
+++ b/README.md
@@ -0,0 +1,35 @@
+
+
Backend for macOS
+
+
+
+
+ Documentation
+
+ ·
+
+ Code
+
+
+
+_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
diff --git a/Sources/Demo/Demo.swift b/Sources/Demo/Demo.swift
new file mode 100644
index 0000000..6be2a36
--- /dev/null
+++ b/Sources/Demo/Demo.swift
@@ -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
diff --git a/Sources/MacBackend/MacBackend.docc/Documentation.md b/Sources/MacBackend/MacBackend.docc/Documentation.md
new file mode 100644
index 0000000..b100966
--- /dev/null
+++ b/Sources/MacBackend/MacBackend.docc/Documentation.md
@@ -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/).
diff --git a/Sources/MacBackend/MacBackend.docc/SwiftUI.md b/Sources/MacBackend/MacBackend.docc/SwiftUI.md
new file mode 100644
index 0000000..850b69a
--- /dev/null
+++ b/Sources/MacBackend/MacBackend.docc/SwiftUI.md
@@ -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.
diff --git a/Sources/MacBackend/Menu/Divider.swift b/Sources/MacBackend/Menu/Divider.swift
new file mode 100644
index 0000000..02f2f20
--- /dev/null
+++ b/Sources/MacBackend/Menu/Divider.swift
@@ -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: 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(
+ _ storage: ViewStorage,
+ data: WidgetData,
+ updateProperties: Bool,
+ type: Data.Type
+ ) where Data: ViewRenderData {
+ }
+
+}
diff --git a/Sources/MacBackend/Menu/Menu.swift b/Sources/MacBackend/Menu/Menu.swift
new file mode 100644
index 0000000..94507c1
--- /dev/null
+++ b/Sources/MacBackend/Menu/Menu.swift
@@ -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: 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(
+ _ 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
+ }
+
+}
diff --git a/Sources/MacBackend/Menu/MenuButton.swift b/Sources/MacBackend/Menu/MenuButton.swift
new file mode 100644
index 0000000..8ffa39e
--- /dev/null
+++ b/Sources/MacBackend/Menu/MenuButton.swift
@@ -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: 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(
+ _ 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?()
+ }
+
+}
diff --git a/Sources/MacBackend/Menu/MenuCollection.swift b/Sources/MacBackend/Menu/MenuCollection.swift
new file mode 100644
index 0000000..296526b
--- /dev/null
+++ b/Sources/MacBackend/Menu/MenuCollection.swift
@@ -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: 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(
+ _ 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)
+ }
+ }
+ }
+
+}
diff --git a/Sources/MacBackend/Menu/MenuContext.swift b/Sources/MacBackend/Menu/MenuContext.swift
new file mode 100644
index 0000000..22e9142
--- /dev/null
+++ b/Sources/MacBackend/Menu/MenuContext.swift
@@ -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 { }
diff --git a/Sources/MacBackend/Menu/MenuEitherView.swift b/Sources/MacBackend/Menu/MenuEitherView.swift
new file mode 100644
index 0000000..b9021b9
--- /dev/null
+++ b/Sources/MacBackend/Menu/MenuEitherView.swift
@@ -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()
+ }
+
+}
diff --git a/Sources/MacBackend/Menu/ServicesMenu.swift b/Sources/MacBackend/Menu/ServicesMenu.swift
new file mode 100644
index 0000000..978ae0e
--- /dev/null
+++ b/Sources/MacBackend/Menu/ServicesMenu.swift
@@ -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: 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(
+ _ 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
+ }
+
+}
diff --git a/Sources/MacBackend/Model/Enumerations/Edge.swift b/Sources/MacBackend/Model/Enumerations/Edge.swift
new file mode 100644
index 0000000..14af93f
--- /dev/null
+++ b/Sources/MacBackend/Model/Enumerations/Edge.swift
@@ -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)
+ ])
+ }
+ }
+
+}
diff --git a/Sources/MacBackend/Model/Enumerations/Font.swift b/Sources/MacBackend/Model/Enumerations/Font.swift
new file mode 100644
index 0000000..ed98fa7
--- /dev/null
+++ b/Sources/MacBackend/Model/Enumerations/Font.swift
@@ -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
+ }
+ }
+
+}
diff --git a/Sources/MacBackend/Model/Enumerations/Icon.swift b/Sources/MacBackend/Model/Enumerations/Icon.swift
new file mode 100644
index 0000000..6b72514
--- /dev/null
+++ b/Sources/MacBackend/Model/Enumerations/Icon.swift
@@ -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)
+ }
+ }
+
+}
diff --git a/Sources/MacBackend/Model/Enumerations/KeyboardShortcut.swift b/Sources/MacBackend/Model/Enumerations/KeyboardShortcut.swift
new file mode 100644
index 0000000..1aa1629
--- /dev/null
+++ b/Sources/MacBackend/Model/Enumerations/KeyboardShortcut.swift
@@ -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)"
+ }
+ }
+
+ }
+
+}
diff --git a/Sources/MacBackend/Model/Extensions/Meta.Binding.swift b/Sources/MacBackend/Model/Extensions/Meta.Binding.swift
new file mode 100644
index 0000000..df88538
--- /dev/null
+++ b/Sources/MacBackend/Model/Extensions/Meta.Binding.swift
@@ -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 {
+ .init {
+ wrappedValue
+ } set: { newValue in
+ wrappedValue = newValue
+ }
+
+ }
+
+}
diff --git a/Sources/MacBackend/Model/Extensions/Set.swift b/Sources/MacBackend/Model/Extensions/Set.swift
new file mode 100644
index 0000000..5d55aaa
--- /dev/null
+++ b/Sources/MacBackend/Model/Extensions/Set.swift
@@ -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
+ }
+
+}
diff --git a/Sources/MacBackend/Model/MacApp.swift b/Sources/MacBackend/Model/MacApp.swift
new file mode 100644
index 0000000..cd2dedb
--- /dev/null
+++ b/Sources/MacBackend/Model/MacApp.swift
@@ -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 "" 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)
+ }
+
+}
diff --git a/Sources/MacBackend/Model/MacMainView.swift b/Sources/MacBackend/Model/MacMainView.swift
new file mode 100644
index 0000000..eed0eed
--- /dev/null
+++ b/Sources/MacBackend/Model/MacMainView.swift
@@ -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
+
+}
diff --git a/Sources/MacBackend/Model/MacSceneElement.swift b/Sources/MacBackend/Model/MacSceneElement.swift
new file mode 100644
index 0000000..752d6fd
--- /dev/null
+++ b/Sources/MacBackend/Model/MacSceneElement.swift
@@ -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 { }
diff --git a/Sources/MacBackend/Model/MacWidget.swift b/Sources/MacBackend/Model/MacWidget.swift
new file mode 100644
index 0000000..0219983
--- /dev/null
+++ b/Sources/MacBackend/Model/MacWidget.swift
@@ -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 { }
diff --git a/Sources/MacBackend/Model/SwiftUI/MacBackendView.swift b/Sources/MacBackend/Model/SwiftUI/MacBackendView.swift
new file mode 100644
index 0000000..10eacb6
--- /dev/null
+++ b/Sources/MacBackend/Model/SwiftUI/MacBackendView.swift
@@ -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) { }
+
+}
diff --git a/Sources/MacBackend/Model/SwiftUI/SwiftUIWidget.swift b/Sources/MacBackend/Model/SwiftUI/SwiftUIWidget.swift
new file mode 100644
index 0000000..b8905f1
--- /dev/null
+++ b/Sources/MacBackend/Model/SwiftUI/SwiftUIWidget.swift
@@ -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: 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: 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(
+ _ 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(
+ _ 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: 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() { }
+
+}
diff --git a/Sources/MacBackend/View/Alert.swift b/Sources/MacBackend/View/Alert.swift
new file mode 100644
index 0000000..8621da1
--- /dev/null
+++ b/Sources/MacBackend/View/Alert.swift
@@ -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
+ /// 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) -> Alert {
+ .init(title: title, description: description, isPresented: isPresented, child: self)
+ }
+ // swiftlint:enable function_default_parameter_at_end
+
+}
diff --git a/Sources/MacBackend/View/Button.swift b/Sources/MacBackend/View/Button.swift
new file mode 100644
index 0000000..112c21d
--- /dev/null
+++ b/Sources/MacBackend/View/Button.swift
@@ -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()
+ }
+ }
+ }
+
+}
diff --git a/Sources/MacBackend/View/EitherView.swift b/Sources/MacBackend/View/EitherView.swift
new file mode 100644
index 0000000..aa690e1
--- /dev/null
+++ b/Sources/MacBackend/View/EitherView.swift
@@ -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")
+ }
+ }
+
+}
diff --git a/Sources/MacBackend/View/Label.swift b/Sources/MacBackend/View/Label.swift
new file mode 100644
index 0000000..47331e6
--- /dev/null
+++ b/Sources/MacBackend/View/Label.swift
@@ -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
+ }
+
+ }
+
+}
diff --git a/Sources/MacBackend/View/List.swift b/Sources/MacBackend/View/List.swift
new file mode 100644
index 0000000..abc51cc
--- /dev/null
+++ b/Sources/MacBackend/View/List.swift
@@ -0,0 +1,51 @@
+//
+// List.swift
+// MacBackend
+//
+// Created by david-swift on 23.11.2024.
+//
+
+import SwiftUI
+
+/// A list widget.
+public struct List: SwiftUIWidget where Element: Identifiable {
+
+ /// The elements.
+ var elements: [Element]
+ /// The selected element.
+ var selection: Meta.Binding
+ /// 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,
+ @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)")
+ }
+ }
+
+}
diff --git a/Sources/MacBackend/View/NavigationSplitView.swift b/Sources/MacBackend/View/NavigationSplitView.swift
new file mode 100644
index 0000000..4dab89d
--- /dev/null
+++ b/Sources/MacBackend/View/NavigationSplitView.swift
@@ -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")
+ }
+ }
+
+}
diff --git a/Sources/MacBackend/View/PaddingView.swift b/Sources/MacBackend/View/PaddingView.swift
new file mode 100644
index 0000000..ecbdbc9
--- /dev/null
+++ b/Sources/MacBackend/View/PaddingView.swift
@@ -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
+ /// 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 = .all) -> Meta.AnyView {
+ PaddingView(padding: padding, edges: edges, child: self)
+ }
+
+}
diff --git a/Sources/MacBackend/View/ScrollView.swift b/Sources/MacBackend/View/ScrollView.swift
new file mode 100644
index 0000000..bc2d483
--- /dev/null
+++ b/Sources/MacBackend/View/ScrollView.swift
@@ -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)
+ }
+ }
+
+}
diff --git a/Sources/MacBackend/View/Spacer.swift b/Sources/MacBackend/View/Spacer.swift
new file mode 100644
index 0000000..5e3c49b
--- /dev/null
+++ b/Sources/MacBackend/View/Spacer.swift
@@ -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()
+ }
+
+}
diff --git a/Sources/MacBackend/View/Text.swift b/Sources/MacBackend/View/Text.swift
new file mode 100644
index 0000000..e3a37ce
--- /dev/null
+++ b/Sources/MacBackend/View/Text.swift
@@ -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 }
+ }
+
+}
diff --git a/Sources/MacBackend/View/VStack.swift b/Sources/MacBackend/View/VStack.swift
new file mode 100644
index 0000000..7852969
--- /dev/null
+++ b/Sources/MacBackend/View/VStack.swift
@@ -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: 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(
+ _ 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)
+ }
+ }
+
+}
diff --git a/Sources/MacBackend/Window/MenuBar.swift b/Sources/MacBackend/Window/MenuBar.swift
new file mode 100644
index 0000000..2bb8fab
--- /dev/null
+++ b/Sources/MacBackend/Window/MenuBar.swift
@@ -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(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(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: 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)
+ }
+
+}
diff --git a/Sources/MacBackend/Window/Window.swift b/Sources/MacBackend/Window/Window.swift
new file mode 100644
index 0000000..887a866
--- /dev/null
+++ b/Sources/MacBackend/Window/Window.swift
@@ -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?
+ /// The window's height.
+ var height: Binding?
+
+ // 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(app: Storage) where Storage: AppStorage {
+ for _ in 0..(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: 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? = nil, height: Binding? = 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
+ }
+
+}