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