Initial commit
Some checks failed
Deploy Docs / publish (push) Successful in 1m18s
SwiftLint / SwiftLint (push) Failing after 4s

This commit is contained in:
david-swift 2024-10-15 23:25:29 +02:00
commit bb14f33c6f
24 changed files with 1318 additions and 0 deletions

View File

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

View File

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

View File

@ -0,0 +1,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
View File

@ -0,0 +1,34 @@
name: Deploy Docs
on:
push:
branches: ["main"]
jobs:
publish:
runs-on: david-macbook
steps:
- uses: actions/checkout@v4
- name: Build Docs
run: |
xcrun xcodebuild docbuild \
-scheme 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/'

View 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
View 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
View 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
View 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
View 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
View 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)

View File

@ -0,0 +1,3 @@
# ``Aparoksha``
Find more information on the [Aparoksha website](https://www.aparoksha.dev/).

View 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"
}
}
}

View 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) }
}

View 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
}

View 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
}

View 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
}

View 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)
}
}
}

View 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()
}
}

View File

@ -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
View 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
}
}

View 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
View 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
}
}

View 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
View 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