Initial commit
This commit is contained in:
commit
bb14f33c6f
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.
|
||||
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
|
||||
11
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
11
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@ -0,0 +1,11 @@
|
||||
## Steps
|
||||
- [ ] 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._
|
||||
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 Aparoksha \
|
||||
-destination 'generic/platform=macOS' \
|
||||
-derivedDataPath "$PWD/.derivedData" \
|
||||
-skipPackagePluginValidation
|
||||
xcrun docc process-archive transform-for-static-hosting \
|
||||
"$PWD/.derivedData/Build/Products/Debug/Aparoksha.doccarchive" \
|
||||
--output-path "docs" \
|
||||
--hosting-base-path "/"
|
||||
- name: Modify Docs
|
||||
run: |
|
||||
echo "<script>window.location.href += \"/documentation/aparoksha\"</script><p>Please enable JavaScript to view the documentation <a href='/documentation/aparoksha'>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/aparoksha.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:
|
||||
- '.gitea/workflows/swiftlint.yml'
|
||||
- '.swiftlint.yml'
|
||||
- '**/*.swift'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.gitea/workflows/swiftlint.yml'
|
||||
- '.swiftlint.yml'
|
||||
- '**/*.swift'
|
||||
workflow_dispatch:
|
||||
paths:
|
||||
- '.gitea/workflows/swiftlint.yml'
|
||||
- '.swiftlint.yml'
|
||||
- '**/*.swift'
|
||||
|
||||
jobs:
|
||||
SwiftLint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: SwiftLint
|
||||
uses: norio-nomura/action-swiftlint@3.2.1
|
||||
with:
|
||||
args: --strict
|
||||
env:
|
||||
WORKING_DIRECTORY: Source
|
||||
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
/Package.resolved
|
||||
.Ulysses-Group.plist
|
||||
/.docc-build
|
||||
/.vscode
|
||||
159
.swiftlint.yml
Normal file
159
.swiftlint.yml
Normal file
@ -0,0 +1,159 @@
|
||||
# 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
|
||||
- discouraged_optional_boolean
|
||||
- discouraged_optional_collection
|
||||
- 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
|
||||
- no_magic_numbers
|
||||
- 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// Aparoksha\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:
|
||||
- Tests/
|
||||
- .build/
|
||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
||||
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 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
81
Package.swift
Normal file
81
Package.swift
Normal file
@ -0,0 +1,81 @@
|
||||
// swift-tools-version: 6.0
|
||||
//
|
||||
// Package.swift
|
||||
// Aparoksha
|
||||
//
|
||||
// Created by david-swift on 15.10.24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PackageDescription
|
||||
|
||||
/// The backend framework.
|
||||
enum Framework: String {
|
||||
|
||||
/// The WinUI backend.
|
||||
case winui = "WINUI"
|
||||
/// The libadwaita backend.
|
||||
case adwaita = "ADWAITA"
|
||||
|
||||
}
|
||||
|
||||
/// Get the backend preference from the environment variable.
|
||||
let setting = ProcessInfo.processInfo.environment["APAROKSHA_FRAMEWORK"]
|
||||
/// The framework based on the environment variable.
|
||||
var environmentFramework: Framework? = .init(rawValue: setting ?? "")
|
||||
|
||||
/// The dependencies.
|
||||
var dependencies: [Package.Dependency] = []
|
||||
/// The Aparoksha target dependencies.
|
||||
var targetDependencies: [Target.Dependency] = []
|
||||
|
||||
#if os(Windows)
|
||||
/// The framework used for rendering.
|
||||
let framework = environmentFramework ?? .winui
|
||||
#else
|
||||
/// The framework used for rendering.
|
||||
let framework = environmentFramework ?? .adwaita
|
||||
#endif
|
||||
|
||||
switch framework {
|
||||
case .winui:
|
||||
dependencies.append(.package(url: "https://git.aparoksha.dev/aparoksha/winui-swift", branch: "main"))
|
||||
targetDependencies.append(.product(name: "Core", package: "winui-swift"))
|
||||
case .adwaita:
|
||||
dependencies.append(.package(url: "https://git.aparoksha.dev/aparoksha/adwaita-swift", branch: "main"))
|
||||
targetDependencies.append(.product(name: "Core", package: "adwaita-swift"))
|
||||
}
|
||||
|
||||
/// The Meta package is the foundation of the Aparoksha project.
|
||||
let package = Package(
|
||||
name: "Aparoksha",
|
||||
platforms: [
|
||||
.macOS(.v10_15),
|
||||
.iOS(.v13)
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "Aparoksha",
|
||||
targets: ["Aparoksha"]
|
||||
)
|
||||
],
|
||||
dependencies: dependencies,
|
||||
targets: [
|
||||
.target(
|
||||
name: "Aparoksha",
|
||||
dependencies: targetDependencies,
|
||||
path: "Sources",
|
||||
swiftSettings: [.define(framework.rawValue)]
|
||||
),
|
||||
.executableTarget(
|
||||
name: "Demo",
|
||||
dependencies: ["Aparoksha"],
|
||||
path: "Tests",
|
||||
swiftSettings: [.define("APAROKSHA")]
|
||||
)
|
||||
],
|
||||
swiftLanguageModes: [.v5]
|
||||
)
|
||||
|
||||
/// Make SwiftLint happy.
|
||||
typealias Package = PackageDescription.Package
|
||||
56
README.md
Normal file
56
README.md
Normal file
@ -0,0 +1,56 @@
|
||||
<p align="center">
|
||||
<h1 align="center">Meta</h1>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://meta.aparoksha.dev/">
|
||||
Documentation
|
||||
</a>
|
||||
·
|
||||
<a href="https://git.aparoksha.dev/aparoksha/meta">
|
||||
Code
|
||||
</a>
|
||||
</p>
|
||||
|
||||
_Meta_ is a framework allowing the creation of user interface (UI) frameworks in Swift.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Usage](#usage)
|
||||
- [Thanks](#thanks)
|
||||
|
||||
## Overview
|
||||
|
||||
_Meta_ follows the following principles:
|
||||
|
||||
- It is a **declarative** framework, meaning that instead of writing _how_ to construct a user interface, you write _what_ it looks like.
|
||||
- The user interface is treated as a function of its **state**. Instead of directly modifying the UI, modify its state to update views.
|
||||
- Multiple UI frameworks can be used in the same code, but the **selection of the framework** happens when executing the app. This enables the creation of cross-platform UI frameworks combining several UI frameworks which render always with the same backend.
|
||||
|
||||
It knows the following layers of UI:
|
||||
|
||||
- An app is the entry point of the executable, containing the windows.
|
||||
- A scene element is a template for a container holding one or multiple views (e.g., a window).
|
||||
- A view is a part of the actual UI inside a window, or another view.
|
||||
|
||||
Detailed information can be found in the [docs](https://meta.aparoksha.dev/).
|
||||
|
||||
## Usage
|
||||
|
||||
_Meta_ can be used for creating UI frameworks in Swift which can then be used to create apps.
|
||||
|
||||
Follow those steps if you want to create a UI framework.
|
||||
|
||||
1. Open your Swift package in GNOME Builder, Xcode, or any other IDE.
|
||||
2. Open the `Package.swift` file.
|
||||
3. Into the `Package` initializer, under `dependencies`, paste:
|
||||
```swift
|
||||
.package(url: "https://git.aparoksha.dev/aparoksha/meta", from: "0.1.0")
|
||||
```
|
||||
|
||||
## Thanks
|
||||
|
||||
- [DocC](https://github.com/apple/swift-docc) used for the documentation
|
||||
- [SwiftLint](https://github.com/realm/SwiftLint) for checking whether code style conventions are violated
|
||||
- The programming language [Swift](https://github.com/swiftlang/swift)
|
||||
3
Sources/Aparoksha.docc/Aparoksha.md
Normal file
3
Sources/Aparoksha.docc/Aparoksha.md
Normal file
@ -0,0 +1,3 @@
|
||||
# ``Aparoksha``
|
||||
|
||||
Find more information on the [Aparoksha website](https://www.aparoksha.dev/).
|
||||
45
Sources/Aparoksha.docc/theme-settings.json
Normal file
45
Sources/Aparoksha.docc/theme-settings.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"theme": {
|
||||
"border-radius": "10px",
|
||||
"button": {
|
||||
"border-radius": "20px"
|
||||
},
|
||||
"color": {
|
||||
"button-background": "#ea3358",
|
||||
"button-background-active": "#ea3358",
|
||||
"button-background-hover": "#fc557a",
|
||||
"button-text": "#ffffff",
|
||||
"header": "#7f313b",
|
||||
"documentation-intro-accent": "var(--color-header)",
|
||||
"link": "#ea3358",
|
||||
"nav-link-color": "#ea3358",
|
||||
"nav-dark-link-color": "#ea3358",
|
||||
"tutorials-overview-link": "#fb4469",
|
||||
"step-background": {
|
||||
"light": "#fffaff",
|
||||
"dark": "#302c2d"
|
||||
},
|
||||
"step-focused": "#ea3358",
|
||||
"tabnav-item-border-color": "#ea3358",
|
||||
"tutorial-background": {
|
||||
"light": "",
|
||||
"dark": "#1d1d1f"
|
||||
},
|
||||
"tutorials-overview-background": "linear-gradient(180deg, rgba(43,20,23,1) 0%, rgba(41, 3, 8, 0.808) 60%, rgba(0,0,0,1) 100%)",
|
||||
"fill-light-blue-secondary": "#ea3358",
|
||||
"fill-blue": "#ea3358",
|
||||
"figure-blue": "#ea3358",
|
||||
"standard-blue-documentation-intro-fill": "#ea3358",
|
||||
"figure-blue": "#ea3358",
|
||||
"tutorial-hero-background": "#100a0b",
|
||||
"navigator-item-hover": {
|
||||
"light": "#ea335815",
|
||||
"dark": "#7f313b"
|
||||
}
|
||||
},
|
||||
"additionalProperties": "#ea3358",
|
||||
"tutorial-step": {
|
||||
"border-radius": "15px"
|
||||
}
|
||||
}
|
||||
}
|
||||
90
Sources/Model/Enumerations/Edge.swift
Normal file
90
Sources/Model/Enumerations/Edge.swift
Normal file
@ -0,0 +1,90 @@
|
||||
//
|
||||
// Edge.swift
|
||||
// Aparoksha
|
||||
//
|
||||
// Created by david-swift on 16.10.24.
|
||||
//
|
||||
|
||||
import Core
|
||||
|
||||
/// A view's edge.
|
||||
public enum Edge {
|
||||
|
||||
/// The leading edge.
|
||||
case leading
|
||||
/// The trailing edge.
|
||||
case trailing
|
||||
/// The top edge.
|
||||
case top
|
||||
/// The bottom edge.
|
||||
case bottom
|
||||
|
||||
}
|
||||
|
||||
extension Set<Edge> {
|
||||
|
||||
/// Horizontal and vertical edges.
|
||||
public static var all: Self { vertical.union(horizontal) }
|
||||
|
||||
/// Top and bottom edges.
|
||||
public static var vertical: Self { top.union(bottom) }
|
||||
|
||||
/// Leading and trailing edges.
|
||||
public static var horizontal: Self { leading.union(trailing) }
|
||||
|
||||
/// Top edge.
|
||||
public static var top: Self { [.top] }
|
||||
|
||||
/// Bottom edge.
|
||||
public static var bottom: Self { [.bottom] }
|
||||
|
||||
/// Leading edge.
|
||||
public static var leading: Self { [.leading] }
|
||||
|
||||
/// Trailing edge.
|
||||
public static var trailing: Self { [.trailing] }
|
||||
|
||||
#if WINUI
|
||||
/// The native edges.
|
||||
var nativeEdges: [Core.Edge] {
|
||||
.init(
|
||||
map { edge in
|
||||
switch edge {
|
||||
case .leading:
|
||||
.left
|
||||
case .trailing:
|
||||
.right
|
||||
case .top:
|
||||
.top
|
||||
case .bottom:
|
||||
.bottom
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
#else
|
||||
/// The native edges.
|
||||
var nativeEdges: Set<Core.Edge> {
|
||||
.init(
|
||||
map { edge in
|
||||
switch edge {
|
||||
case .leading:
|
||||
.leading
|
||||
case .trailing:
|
||||
.trailing
|
||||
case .top:
|
||||
.top
|
||||
case .bottom:
|
||||
.bottom
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Add a collection of edges to a collection of edges.
|
||||
/// - Parameter edges: The collection of edges.
|
||||
/// - Returns: Both collections combined.
|
||||
public func add(_ edges: Self) -> Self { union(edges) }
|
||||
|
||||
}
|
||||
52
Sources/Model/Enumerations/HorizontalAlignment.swift
Normal file
52
Sources/Model/Enumerations/HorizontalAlignment.swift
Normal file
@ -0,0 +1,52 @@
|
||||
//
|
||||
// HorizontalAlignment.swift
|
||||
// Aparoksha
|
||||
//
|
||||
// Created by david-swift on 16.10.24.
|
||||
//
|
||||
|
||||
import Core
|
||||
|
||||
/// The horizontal alignment.
|
||||
public enum HorizontalAlignment {
|
||||
|
||||
/// The leading alignment.
|
||||
case leading
|
||||
/// The center alignment.
|
||||
case center
|
||||
/// The trailing alignment.
|
||||
case trailing
|
||||
/// The fill alignment.
|
||||
case fill
|
||||
|
||||
#if WINUI
|
||||
/// The native alignment.
|
||||
var nativeAlignment: Core.HorizontalAlignment {
|
||||
switch self {
|
||||
case .leading:
|
||||
.left
|
||||
case .trailing:
|
||||
.right
|
||||
case .center:
|
||||
.center
|
||||
case .fill:
|
||||
.stretch
|
||||
}
|
||||
}
|
||||
#else
|
||||
/// The native alignment.
|
||||
var nativeAlignment: Core.Alignment {
|
||||
switch self {
|
||||
case .leading:
|
||||
.start
|
||||
case .trailing:
|
||||
.end
|
||||
case .center:
|
||||
.center
|
||||
case .fill:
|
||||
.fill
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
57
Sources/Model/Enumerations/Icon.swift
Normal file
57
Sources/Model/Enumerations/Icon.swift
Normal file
@ -0,0 +1,57 @@
|
||||
//
|
||||
// Icon.swift
|
||||
// Aparoksha
|
||||
//
|
||||
// Created by david-swift on 15.10.24.
|
||||
//
|
||||
|
||||
import Core
|
||||
|
||||
/// A cross-platform icon.
|
||||
public enum Icon {
|
||||
|
||||
/// A custom icon.
|
||||
case custom(winui: Character, adwaita: String)
|
||||
/// The airplane icon.
|
||||
case airplane
|
||||
/// The edit icon.
|
||||
case edit
|
||||
/// The headphones icon.
|
||||
case headphones
|
||||
|
||||
#if WINUI
|
||||
/// The native icon.
|
||||
var nativeIcon: Core.Icon {
|
||||
let unicode: Character = switch self {
|
||||
case .airplane:
|
||||
"\u{E709}"
|
||||
case .edit:
|
||||
"\u{E70F}"
|
||||
case .headphones:
|
||||
"\u{E7F6}"
|
||||
case let .custom(winui, _):
|
||||
winui
|
||||
}
|
||||
return .systemIcon(unicode: unicode)
|
||||
}
|
||||
#else
|
||||
/// The native icon.
|
||||
var nativeIcon: Core.Icon {
|
||||
if case let .custom(_, adwaita) = self {
|
||||
return .custom(name: adwaita)
|
||||
}
|
||||
let icon: Core.Icon.DefaultIcon = switch self {
|
||||
case .airplane:
|
||||
.airplaneMode
|
||||
case .edit:
|
||||
.documentEdit
|
||||
case .headphones:
|
||||
.audioHeadphones
|
||||
case .custom:
|
||||
.emojiSymbols
|
||||
}
|
||||
return .default(icon: icon)
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
52
Sources/Model/Enumerations/VerticalAlignment.swift
Normal file
52
Sources/Model/Enumerations/VerticalAlignment.swift
Normal file
@ -0,0 +1,52 @@
|
||||
//
|
||||
// VerticalAlignment.swift
|
||||
// Aparoksha
|
||||
//
|
||||
// Created by david-swift on 16.10.24.
|
||||
//
|
||||
|
||||
import Core
|
||||
|
||||
/// The vertical alignment.
|
||||
public enum VerticalAlignment {
|
||||
|
||||
/// The top alignment.
|
||||
case top
|
||||
/// The center alignment.
|
||||
case center
|
||||
/// The bottom alignment.
|
||||
case bottom
|
||||
/// The fill alignment.
|
||||
case fill
|
||||
|
||||
#if WINUI
|
||||
// The native alignment.
|
||||
var nativeAlignment: Core.VerticalAlignment {
|
||||
switch self {
|
||||
case .top:
|
||||
.top
|
||||
case .bottom:
|
||||
.bottom
|
||||
case .center:
|
||||
.center
|
||||
case .fill:
|
||||
.stretch
|
||||
}
|
||||
}
|
||||
#else
|
||||
/// The native alignment.
|
||||
var nativeAlignment: Core.Alignment {
|
||||
switch self {
|
||||
case .top:
|
||||
.start
|
||||
case .bottom:
|
||||
.end
|
||||
case .center:
|
||||
.center
|
||||
case .fill:
|
||||
.fill
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
31
Sources/Model/Extensions/Int.swift
Normal file
31
Sources/Model/Extensions/Int.swift
Normal file
@ -0,0 +1,31 @@
|
||||
//
|
||||
// Int.swift
|
||||
// Aparoksha
|
||||
//
|
||||
// Created by david-swift on 16.10.24.
|
||||
//
|
||||
|
||||
extension Int {
|
||||
|
||||
/// The factor Adwaita values are multiplied with before applying to WinUI.
|
||||
static var windowsFactor: Double { 1.7 }
|
||||
|
||||
/// Convert from the Adwaita to the WinUI scale.
|
||||
var windowsValue: Self {
|
||||
.init(Self.windowsFactor * .init(self))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Binding<Int> {
|
||||
|
||||
/// Convert from the Adwaita to the WinUI scale.
|
||||
var windowsValue: Self {
|
||||
.init {
|
||||
.init(Int.windowsFactor * .init(wrappedValue))
|
||||
} set: { newValue in
|
||||
wrappedValue = .init(.init(newValue) / Int.windowsFactor)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
58
Sources/Model/User Interface/App/Aparoksha.swift
Normal file
58
Sources/Model/User Interface/App/Aparoksha.swift
Normal file
@ -0,0 +1,58 @@
|
||||
//
|
||||
// Aparoksha.swift
|
||||
// Aparoksha
|
||||
//
|
||||
// Created by david-swift on 15.10.24.
|
||||
//
|
||||
|
||||
import Core
|
||||
@_exported import Meta
|
||||
|
||||
/// The Aparoksha app.
|
||||
public class Aparoksha: AppStorage {
|
||||
|
||||
/// The scene element type.
|
||||
public typealias SceneElementType = AparokshaSceneElement
|
||||
#if WINUI
|
||||
/// The native app type.
|
||||
typealias App = WinUIApp
|
||||
#else
|
||||
/// The native app type.
|
||||
typealias App = AdwaitaApp
|
||||
#endif
|
||||
|
||||
/// The native app
|
||||
let app: App
|
||||
|
||||
/// The standard app storage.
|
||||
public var storage: StandardAppStorage {
|
||||
get {
|
||||
app.storage
|
||||
}
|
||||
set {
|
||||
app.storage = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the app.
|
||||
/// - Parameter id: The reverse DNS style identifier.
|
||||
public init(id: String) {
|
||||
#if WINUI
|
||||
app = .init()
|
||||
#else
|
||||
app = .init(id: id)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Run the app.
|
||||
/// - Parameter setup: The setup function.
|
||||
public func run(setup: @escaping () -> Void) {
|
||||
app.run(setup: setup)
|
||||
}
|
||||
|
||||
/// Quit the app.
|
||||
public func quit() {
|
||||
app.quit()
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
//
|
||||
// AparokshaSceneElement.swift
|
||||
// Aparoksha
|
||||
//
|
||||
// Created by david-swift on 15.10.24.
|
||||
//
|
||||
|
||||
/// The Aparoksha scene element type.
|
||||
public protocol AparokshaSceneElement: SceneElement { }
|
||||
137
Sources/Scene/Window.swift
Normal file
137
Sources/Scene/Window.swift
Normal file
@ -0,0 +1,137 @@
|
||||
//
|
||||
// Window.swift
|
||||
// Aparoksha
|
||||
//
|
||||
// Created by david-swift on 15.10.24.
|
||||
//
|
||||
|
||||
import Core
|
||||
|
||||
/// A window.
|
||||
public struct Window: AparokshaSceneElement {
|
||||
|
||||
/// The window's identifier.
|
||||
public var id: String
|
||||
/// The window's title.
|
||||
var title: String
|
||||
/// The window's content.
|
||||
var content: Body
|
||||
/// The number of instances to open on startup.
|
||||
var `open`: Int
|
||||
/// Whether to show the default title bar.
|
||||
var defaultTitlebar = true
|
||||
/// The window's width.
|
||||
var width: Binding<Int>?
|
||||
/// The window's height.
|
||||
var height: Binding<Int>?
|
||||
|
||||
/// The content view.
|
||||
var contentView: AnyView {
|
||||
#if WINUI
|
||||
content
|
||||
#else
|
||||
if !defaultTitlebar {
|
||||
content
|
||||
} else {
|
||||
ToolbarView()
|
||||
.content {
|
||||
content
|
||||
}
|
||||
.top {
|
||||
HeaderBar.empty()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if WINUI
|
||||
/// The native window.
|
||||
var nativeWindow: Core.Window {
|
||||
.init(title, id: id, open: open) { _ in
|
||||
contentView
|
||||
}
|
||||
.extendContentIntoTitleBar(!defaultTitlebar)
|
||||
.frame(width: width?.windowsValue, height: height?.windowsValue)
|
||||
}
|
||||
#else
|
||||
/// The native window.
|
||||
var nativeWindow: Core.Window {
|
||||
.init(id: id, open: open) { _ in
|
||||
contentView
|
||||
}
|
||||
.title(title)
|
||||
.size(width: width, height: height)
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Initialize a window.
|
||||
/// - Parameters:
|
||||
/// - title: The window's title.
|
||||
/// - id: A unique identifier for the window type.
|
||||
/// - open: The number of instances to open on startup.
|
||||
/// - content: The view.
|
||||
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
|
||||
}
|
||||
|
||||
/// Set up the initial containers.
|
||||
/// - Parameter app: The app storage.
|
||||
public func setupInitialContainers<Storage>(app: Storage) where Storage: AppStorage {
|
||||
nativeWindow.setupInitialContainers(app: nativeApp(app: app))
|
||||
}
|
||||
|
||||
/// Initialize a container.
|
||||
/// - Parameter app: The app storage.
|
||||
/// - Returns: The scene storage.
|
||||
public func container<Storage>(app: Storage) -> SceneStorage where Storage: AppStorage {
|
||||
nativeWindow.container(app: nativeApp(app: app))
|
||||
}
|
||||
|
||||
/// Update the container.
|
||||
/// - Parameters:
|
||||
/// - storage: The scene storage.
|
||||
/// - app: The app storage.
|
||||
/// - updateProperties: Whether to update properties.
|
||||
public func update<Storage>(
|
||||
_ storage: SceneStorage,
|
||||
app: Storage,
|
||||
updateProperties: Bool
|
||||
) where Storage: AppStorage {
|
||||
nativeWindow.update(storage, app: nativeApp(app: app), updateProperties: updateProperties)
|
||||
}
|
||||
|
||||
/// Get the native app.
|
||||
/// - Parameter app: The app storage.
|
||||
/// - Returns: The native app.
|
||||
func nativeApp<Storage>(app: Storage) -> any AppStorage where Storage: AppStorage {
|
||||
var app: any AppStorage = app
|
||||
if let aparoksha = app as? Aparoksha {
|
||||
app = aparoksha.app
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
/// Hide the default title bar.
|
||||
/// - Returns: The window.
|
||||
public func hideDefaultTitlebar() -> Self {
|
||||
var newSelf = self
|
||||
newSelf.defaultTitlebar = false
|
||||
return newSelf
|
||||
}
|
||||
|
||||
/// Set and observe the window's width and height.
|
||||
/// - Parameters:
|
||||
/// - width: The window's width.
|
||||
/// - height: The window's height.
|
||||
/// - Returns: The view.
|
||||
public func frame(width: Binding<Int>? = nil, height: Binding<Int>? = nil) -> Self {
|
||||
var newSelf = self
|
||||
newSelf.width = width
|
||||
newSelf.height = height
|
||||
return newSelf
|
||||
}
|
||||
|
||||
}
|
||||
69
Sources/View/AnyView+.swift
Normal file
69
Sources/View/AnyView+.swift
Normal file
@ -0,0 +1,69 @@
|
||||
//
|
||||
// AnyView+.swift
|
||||
// Aparoksha
|
||||
//
|
||||
// Created by david-swift on 16.10.24.
|
||||
//
|
||||
|
||||
import Core
|
||||
|
||||
extension AnyView {
|
||||
|
||||
/// Set the padding around the view.
|
||||
/// - Parameters:
|
||||
/// - padding: The padding's value.
|
||||
/// - edges: The affected edges.
|
||||
/// - Returns: The view.
|
||||
public func padding(_ padding: Int, edges: Set<Edge> = .all) -> AnyView {
|
||||
#if WINUI
|
||||
ModifierWrapper(view: self, margin: edges.nativeEdges.set(.init(padding.windowsValue)))
|
||||
#else
|
||||
ModifierWrapper(content: self, padding: padding, edges: edges.nativeEdges)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Set the view's vertical alignment.
|
||||
/// - Parameter alignment: The vertical alignment.
|
||||
/// - Returns: The view.
|
||||
public func valign(_ alignment: VerticalAlignment) -> AnyView {
|
||||
#if WINUI
|
||||
ModifierWrapper(view: self, verticalAlignment: alignment.nativeAlignment)
|
||||
#else
|
||||
ModifierWrapper(content: self, valign: alignment.nativeAlignment)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Set the view's horizontal alignment.
|
||||
/// - Parameter alignment: The horizontal alignment.
|
||||
/// - Returns: The view.
|
||||
public func halign(_ alignment: HorizontalAlignment) -> AnyView {
|
||||
#if WINUI
|
||||
ModifierWrapper(view: self, horizontalAlignment: alignment.nativeAlignment)
|
||||
#else
|
||||
ModifierWrapper(content: self, halign: alignment.nativeAlignment)
|
||||
#endif
|
||||
}
|
||||
|
||||
#if ADWAITA
|
||||
/// Set the view's navigation title.
|
||||
/// - Parameter label: The navigation title.
|
||||
/// - Returns: A view.
|
||||
internal func navigationTitle(_ label: String) -> AnyView {
|
||||
inspect { storage, updateProperties in
|
||||
if updateProperties {
|
||||
storage.fields[.navigationLabel] = label
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a style class to the view.
|
||||
/// - Parameters:
|
||||
/// - style: The style class.
|
||||
/// - active: Whether the style is currently applied.
|
||||
/// - Returns: A view.
|
||||
internal func style(_ style: String, active: Bool = true) -> AnyView {
|
||||
ModifierWrapper(content: self, style: style, styleActive: active)
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
36
Sources/View/Button.swift
Normal file
36
Sources/View/Button.swift
Normal file
@ -0,0 +1,36 @@
|
||||
//
|
||||
// Button.swift
|
||||
// Aparoksha
|
||||
//
|
||||
// Created by david-swift on 15.10.24.
|
||||
//
|
||||
|
||||
import Core
|
||||
|
||||
/// The button view.
|
||||
public struct Button: SimpleView {
|
||||
|
||||
/// The button's label.
|
||||
var label: String
|
||||
/// The handler.
|
||||
var handler: () -> Void
|
||||
|
||||
/// The content view.
|
||||
public var view: Body {
|
||||
#if WINUI
|
||||
Core.Button(label, action: handler)
|
||||
#else
|
||||
Core.Button(label, handler: handler)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Initialize a button.
|
||||
/// - Parameters:
|
||||
/// - label: The button's label.
|
||||
/// - handler: The handler.
|
||||
public init(_ label: String, handler: @escaping () -> Void) {
|
||||
self.label = label
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
}
|
||||
128
Sources/View/FlatNavigation.swift
Normal file
128
Sources/View/FlatNavigation.swift
Normal file
@ -0,0 +1,128 @@
|
||||
//
|
||||
// FlatNavigation.swift
|
||||
// Aparoksha
|
||||
//
|
||||
// Created by david-swift on 15.10.24.
|
||||
//
|
||||
|
||||
#if ADWAITA
|
||||
import CAdw
|
||||
#endif
|
||||
import Core
|
||||
|
||||
/// A flat navigation has one layer of navigation.
|
||||
public struct FlatNavigation<Item>: SimpleView where Item: FlatNavigationItem {
|
||||
|
||||
/// The selected item.
|
||||
@Binding var selectedItem: Item.ID
|
||||
/// The items.
|
||||
var items: [Item]
|
||||
/// The content view.
|
||||
var content: Body
|
||||
|
||||
/// The view.
|
||||
public var view: Body {
|
||||
#if WINUI
|
||||
NavigationView(
|
||||
items: items.map { WinUINavigationItem(item: $0) },
|
||||
selection: .init {
|
||||
if let selected = items.first(where: { $0.id == selectedItem }) {
|
||||
.init(item: selected)
|
||||
} else if let first = items.first {
|
||||
.init(item: first)
|
||||
} else {
|
||||
// swiftlint:disable fatal_error
|
||||
fatalError("FlatNavigation needs at least one item")
|
||||
// swiftlint:enable fatal_error
|
||||
}
|
||||
} set: { newValue in
|
||||
selectedItem = newValue.item.id
|
||||
}
|
||||
) { content }
|
||||
#else
|
||||
NavigationSplitView {
|
||||
ScrollView {
|
||||
ToolbarView()
|
||||
.content {
|
||||
List(items, selection: $selectedItem) { item in
|
||||
let standardPadding = 6
|
||||
let doublePadding = 12
|
||||
HStack {
|
||||
ButtonContent()
|
||||
.iconName(item.icon.nativeIcon.string)
|
||||
Text(item.description)
|
||||
.halign(.leading)
|
||||
.padding(doublePadding, edges: .leading)
|
||||
}
|
||||
.padding(standardPadding, edges: .horizontal)
|
||||
.padding(doublePadding, edges: .vertical)
|
||||
}
|
||||
.style("navigation-sidebar")
|
||||
}
|
||||
.top {
|
||||
HeaderBar.end {
|
||||
Menu(icon: .default(icon: .openMenu)) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(.init(cString: g_get_application_name()))
|
||||
} content: {
|
||||
ToolbarView()
|
||||
.content {
|
||||
content
|
||||
}
|
||||
.top {
|
||||
HeaderBar
|
||||
.empty()
|
||||
}
|
||||
.navigationTitle(items.first { $0.id == selectedItem }?.description ?? "")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Initialize a flat navigation view.
|
||||
/// - Parameters:
|
||||
/// - items: The items.
|
||||
/// - selection: The selected item.
|
||||
/// - content: The main content.
|
||||
public init(_ items: [Item], selection: Binding<Item.ID>, @ViewBuilder content: () -> Body) {
|
||||
self.items = items
|
||||
_selectedItem = selection
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if WINUI
|
||||
/// A navigation item for WinUI.
|
||||
struct WinUINavigationItem<Item>: NavigationViewItem where Item: FlatNavigationItem {
|
||||
|
||||
/// The item.
|
||||
var item: Item
|
||||
|
||||
/// The icon.
|
||||
var icon: Core.Icon {
|
||||
item.icon.nativeIcon
|
||||
}
|
||||
|
||||
/// The label.
|
||||
var description: String {
|
||||
item.description
|
||||
}
|
||||
|
||||
/// Compare to items.
|
||||
static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.item.id == rhs.item.id
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
/// An item for the flat navigation view.
|
||||
public protocol FlatNavigationItem: CustomStringConvertible, Identifiable {
|
||||
|
||||
/// The view's icon.
|
||||
var icon: Icon { get }
|
||||
|
||||
}
|
||||
70
Tests/Demo.swift
Normal file
70
Tests/Demo.swift
Normal file
@ -0,0 +1,70 @@
|
||||
//
|
||||
// Demo.swift
|
||||
// Aparoksha
|
||||
//
|
||||
// Created by david-swift on 15.10.24.
|
||||
//
|
||||
|
||||
// swiftlint:disable
|
||||
|
||||
import Aparoksha
|
||||
|
||||
@main
|
||||
struct Demo: App {
|
||||
|
||||
@State private var width = 500
|
||||
@State private var height = 400
|
||||
let app = Aparoksha(id: "dev.aparoksha.Demo")
|
||||
|
||||
var scene: Scene {
|
||||
Window("Demo", id: "main") {
|
||||
ContentView()
|
||||
}
|
||||
.hideDefaultTitlebar()
|
||||
.frame(width: $width, height: $height)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@State private var selectedItem: Item = .travel
|
||||
|
||||
var view: Body {
|
||||
FlatNavigation(Item.allCases, selection: $selectedItem) {
|
||||
Button(selectedItem.description) {
|
||||
selectedItem = .allCases.filter { $0 != selectedItem }.randomElement() ?? .travel
|
||||
}
|
||||
.valign(.center)
|
||||
.halign(.center)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum Item: String, FlatNavigationItem, CaseIterable, Equatable {
|
||||
|
||||
var id: Self { self }
|
||||
|
||||
case travel
|
||||
case modify
|
||||
case audio
|
||||
|
||||
var description: String {
|
||||
rawValue.capitalized
|
||||
}
|
||||
|
||||
var icon: Icon {
|
||||
switch self {
|
||||
case .travel:
|
||||
.airplane
|
||||
case .modify:
|
||||
.edit
|
||||
case .audio:
|
||||
.headphones
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// swiftlint:enable
|
||||
Loading…
x
Reference in New Issue
Block a user