This commit is contained in:
commit
e989bea14e
40
.gitea/ISSUE_TEMPLATE/bug_report.yml
Normal file
40
.gitea/ISSUE_TEMPLATE/bug_report.yml
Normal 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.
|
||||||
29
.gitea/ISSUE_TEMPLATE/component_request.yml
Normal file
29
.gitea/ISSUE_TEMPLATE/component_request.yml
Normal 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
|
||||||
36
.gitea/ISSUE_TEMPLATE/feature_request.yml
Normal file
36
.gitea/ISSUE_TEMPLATE/feature_request.yml
Normal 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
|
||||||
14
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
14
.gitea/PULL_REQUEST_TEMPLATE.md
Normal 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
34
.gitea/workflows/docs.yml
Normal 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/'
|
||||||
30
.gitea/workflows/swiftlint.yml
Normal file
30
.gitea/workflows/swiftlint.yml
Normal 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
14
.gitignore
vendored
Normal 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
155
.swiftlint.yml
Normal 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
4
Bundler.toml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[apps."Demo"]
|
||||||
|
product = 'Demo'
|
||||||
|
version = '0.1.0'
|
||||||
|
minimum_macos_version = '14'
|
||||||
23
LICENSE.md
Normal file
23
LICENSE.md
Normal 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
41
Package.swift
Normal 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
35
README.md
Normal 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
121
Sources/Demo/Demo.swift
Normal 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
|
||||||
7
Sources/MacBackend/MacBackend.docc/Documentation.md
Normal file
7
Sources/MacBackend/MacBackend.docc/Documentation.md
Normal 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/).
|
||||||
59
Sources/MacBackend/MacBackend.docc/SwiftUI.md
Normal file
59
Sources/MacBackend/MacBackend.docc/SwiftUI.md
Normal 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.
|
||||||
42
Sources/MacBackend/Menu/Divider.swift
Normal file
42
Sources/MacBackend/Menu/Divider.swift
Normal 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 {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
71
Sources/MacBackend/Menu/Menu.swift
Normal file
71
Sources/MacBackend/Menu/Menu.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
145
Sources/MacBackend/Menu/MenuButton.swift
Normal file
145
Sources/MacBackend/Menu/MenuButton.swift
Normal 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?()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
85
Sources/MacBackend/Menu/MenuCollection.swift
Normal file
85
Sources/MacBackend/Menu/MenuCollection.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
Sources/MacBackend/Menu/MenuContext.swift
Normal file
21
Sources/MacBackend/Menu/MenuContext.swift
Normal 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 { }
|
||||||
23
Sources/MacBackend/Menu/MenuEitherView.swift
Normal file
23
Sources/MacBackend/Menu/MenuEitherView.swift
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
61
Sources/MacBackend/Menu/ServicesMenu.swift
Normal file
61
Sources/MacBackend/Menu/ServicesMenu.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
48
Sources/MacBackend/Model/Enumerations/Edge.swift
Normal file
48
Sources/MacBackend/Model/Enumerations/Edge.swift
Normal 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)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
64
Sources/MacBackend/Model/Enumerations/Font.swift
Normal file
64
Sources/MacBackend/Model/Enumerations/Font.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
24
Sources/MacBackend/Model/Enumerations/Icon.swift
Normal file
24
Sources/MacBackend/Model/Enumerations/Icon.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
115
Sources/MacBackend/Model/Enumerations/KeyboardShortcut.swift
Normal file
115
Sources/MacBackend/Model/Enumerations/KeyboardShortcut.swift
Normal 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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
22
Sources/MacBackend/Model/Extensions/Meta.Binding.swift
Normal file
22
Sources/MacBackend/Model/Extensions/Meta.Binding.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
65
Sources/MacBackend/Model/Extensions/Set.swift
Normal file
65
Sources/MacBackend/Model/Extensions/Set.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
102
Sources/MacBackend/Model/MacApp.swift
Normal file
102
Sources/MacBackend/Model/MacApp.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
18
Sources/MacBackend/Model/MacMainView.swift
Normal file
18
Sources/MacBackend/Model/MacMainView.swift
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
9
Sources/MacBackend/Model/MacSceneElement.swift
Normal file
9
Sources/MacBackend/Model/MacSceneElement.swift
Normal 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 { }
|
||||||
9
Sources/MacBackend/Model/MacWidget.swift
Normal file
9
Sources/MacBackend/Model/MacWidget.swift
Normal 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 { }
|
||||||
38
Sources/MacBackend/Model/SwiftUI/MacBackendView.swift
Normal file
38
Sources/MacBackend/Model/SwiftUI/MacBackendView.swift
Normal 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) { }
|
||||||
|
|
||||||
|
}
|
||||||
175
Sources/MacBackend/Model/SwiftUI/SwiftUIWidget.swift
Normal file
175
Sources/MacBackend/Model/SwiftUI/SwiftUIWidget.swift
Normal 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() { }
|
||||||
|
|
||||||
|
}
|
||||||
164
Sources/MacBackend/View/Alert.swift
Normal file
164
Sources/MacBackend/View/Alert.swift
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
64
Sources/MacBackend/View/Button.swift
Normal file
64
Sources/MacBackend/View/Button.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
48
Sources/MacBackend/View/EitherView.swift
Normal file
48
Sources/MacBackend/View/EitherView.swift
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
39
Sources/MacBackend/View/Label.swift
Normal file
39
Sources/MacBackend/View/Label.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
51
Sources/MacBackend/View/List.swift
Normal file
51
Sources/MacBackend/View/List.swift
Normal 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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
46
Sources/MacBackend/View/NavigationSplitView.swift
Normal file
46
Sources/MacBackend/View/NavigationSplitView.swift
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
46
Sources/MacBackend/View/PaddingView.swift
Normal file
46
Sources/MacBackend/View/PaddingView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
36
Sources/MacBackend/View/ScrollView.swift
Normal file
36
Sources/MacBackend/View/ScrollView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
Sources/MacBackend/View/Spacer.swift
Normal file
23
Sources/MacBackend/View/Spacer.swift
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
56
Sources/MacBackend/View/Text.swift
Normal file
56
Sources/MacBackend/View/Text.swift
Normal 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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
71
Sources/MacBackend/View/VStack.swift
Normal file
71
Sources/MacBackend/View/VStack.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
88
Sources/MacBackend/Window/MenuBar.swift
Normal file
88
Sources/MacBackend/Window/MenuBar.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
157
Sources/MacBackend/Window/Window.swift
Normal file
157
Sources/MacBackend/Window/Window.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user