Compare commits

...

11 Commits

Author SHA1 Message Date
05dd06af0a Migrate to Aparoksha Gitea instance
All checks were successful
Deploy Docs / publish (push) Successful in 2m20s
SwiftLint / SwiftLint (push) Successful in 4s
2024-10-14 11:25:51 +02:00
3ad655428b Update to Swift 6 2024-09-30 22:37:22 +02:00
a64873733a Use Property property wrappers for widgets 2024-09-18 07:14:40 +02:00
376d8c724f Update to latest Meta version 2024-08-26 07:20:48 +02:00
ec86bf1324 Merge remote-tracking branch 'refs/remotes/origin/main' 2024-08-25 11:41:21 +02:00
david-swift
a6bace169a
Fix building docs failing 2024-07-31 21:43:36 +02:00
8c68676994 Compare widgets to previous versions 2024-07-31 14:33:40 +02:00
c96e65e622 Fix links to docs not working 2024-07-20 09:27:02 +02:00
c48abc1c9a Fix documentation not building 2024-07-20 09:20:30 +02:00
eadc08d61d Update documentation 2024-07-20 09:15:47 +02:00
ef337544bc Fix links to docs not working 2024-07-20 08:19:42 +02:00
37 changed files with 364 additions and 386 deletions

View File

@ -4,21 +4,9 @@ on:
push:
branches: ["main"]
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
Deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: macos-14
publish:
runs-on: david-macbook
steps:
- uses: actions/checkout@v4
- name: Build Docs
@ -31,15 +19,16 @@ jobs:
xcrun docc process-archive transform-for-static-hosting \
"$PWD/.derivedData/Build/Products/Debug/TermKitBackend.doccarchive" \
--output-path "docs" \
--hosting-base-path "TermKitBackend"
--hosting-base-path "/"
- name: Modify Docs
run: |
echo "<script>window.location.href += \"/documentation/termkitbackend\"</script>" > docs/index.html;
sed -i '' 's/,2px/,10px/g' docs/css/index.038e887c.css
- name: Upload Artifact
uses: actions/upload-pages-artifact@v3
echo "<script>window.location.href += \"/documentation/termkitbackend\"</script><p>Please enable JavaScript to view the documentation <a href='/documentation/termkitbackend'>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:
path: 'docs'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
host: 'volans.uberspace.de'
username: 'akforum'
password: ${{ secrets.password }}
localDir: 'docs'
remoteDir: '/var/www/virtual/akforum/term-kit-backend.aparoksha.dev/'

View File

@ -3,17 +3,17 @@ name: SwiftLint
on:
push:
paths:
- '.github/workflows/swiftlint.yml'
- '.gitea/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
pull_request:
paths:
- '.github/workflows/swiftlint.yml'
- '.gitea/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
workflow_dispatch:
paths:
- '.github/workflows/swiftlint.yml'
- '.gitea/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
@ -21,10 +21,10 @@ jobs:
SwiftLint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- name: SwiftLint
uses: norio-nomura/action-swiftlint@3.2.1
with:
args: --strict
env:
WORKING_DIRECTORY: Source
WORKING_DIRECTORY: Source

View File

@ -94,12 +94,6 @@ disabled_rules:
# Custom Rules
custom_rules:
github_issue:
name: 'GitHub Issue'
regex: '//.(TODO|FIXME):.(?!.*(https://github\.com/david-swift/TermKitBackend/issues/\d))'
message: 'The related GitHub issue must be included in a TODO or FIXME.'
severity: warning
fatal_error:
name: 'Fatal Error'
regex: 'fatalError.*\(.*\)'

View File

@ -1,34 +0,0 @@
# Contributing
Thank you very much for taking the time for contributing to this project.
## Report a Bug
Just open a new issue on GitHub and describe the bug. It helps if your description is detailed. Thank you very much for your contribution!
## Suggest a New Feature
Just open a new issue on GitHub and describe the idea. Thank you very much for your contribution!
## Pull Requests
I am happy for every pull request, you do not have to follow these guidelines. However, it might help you to understand the project structure and make it easier for me to merge your pull request. Thank you very much for your contribution!
### 1. Fork & Clone this Project
Start by clicking on the `Fork` button at the top of the page. Then, clone this repository to your computer.
### 2. Open the Project
Open the project folder in GNOME Builder, Xcode or another IDE.
### 3. Understand the Project Structure
- The `README.md` file contains a description of the app or package.
- The `LICENSE.md` contains an MIT license.
- `CONTRIBUTING.md` is this file.
- Directory `Icons` that contains SVG files for the images used in the app and guides.
- `Sources` contains the source code of the project as well as a test app.
### 4. Edit the Code
Edit the code. If you add a new type, add documentation in the code.
### 5. Commit to the Fork
Commit and push the fork.
### 6. Pull Request
Open GitHub to submit a pull request. Thank you very much for your contribution!

View File

@ -1,4 +1,4 @@
// swift-tools-version: 5.9
// swift-tools-version: 6.0
//
// Package.swift
// TermKitBackend
@ -19,17 +19,21 @@ let package = Package(
)
],
dependencies: [
.package(url: "https://github.com/AparokshaUI/Meta", branch: "main"),
.package(url: "https://git.aparoksha.dev/aparoksha/meta", branch: "main"),
.package(url: "https://github.com/david-swift/TermKit", branch: "main")
],
targets: [
.target(
name: "TermKitBackend",
dependencies: ["TermKit", "Meta"]
dependencies: [
"TermKit",
.product(name: "Meta", package: "meta")
]
),
.executableTarget(
name: "TestApp",
dependencies: ["TermKitBackend"]
)
]
],
swiftLanguageModes: [.v5]
)

View File

@ -3,16 +3,16 @@
</p>
<p align="center">
<a href="https://david-swift.github.io/termkitbackend/">
<a href="https://term-kit-backend.aparoksha.dev/">
Documentation
</a>
·
<a href="https://github.com/david-swift/TermKitBackend">
GitHub
<a href="https://git.aparoksha.dev/david-swift/term-kit-backend">
Code
</a>
</p>
_TermKitBackend_ is a declarative framework allowing the creation of user interface for the terminal. It works on Linux, macOS and Windows thanks to the [TermKit project](https://github.com/migueldeicaza/TermKit).
_TermKitBackend_ is a declarative framework allowing the creation of user interfaces for the terminal. It works on Linux and macOS thanks to the [TermKit project](https://github.com/migueldeicaza/TermKit).
## Table of Contents
@ -22,21 +22,21 @@ _TermKitBackend_ is a declarative framework allowing the creation of user interf
## Overview
The declarative approach is based on the [Meta package](https://aparokshaui.github.io/meta/) which can be found on [GitHub](https://github.com/AparokshaUI/Meta).
The declarative approach is based on the [Meta package](https://meta.aparoksha.dev/), browse the code [here](https://git.aparoksha.dev/aparoksha/meta).
It is powered by [a fork of TermKit for Swift](https://github.com/david-swift/TermKit).
Detailed information about the declarative approach can be found in the [Meta docs](https://aparokshaui.github.io/meta/). Find the available widgets [here](https://david-swift.github.io/termkitbackend).
Detailed information about the declarative approach can be found in the [Meta docs](https://meta.aparoksha.dev/). Find the available widgets [here](https://term-kit-backend.aparoksha.dev/).
## Usage
Follow the tutorial in the [docs](https://david-swift.github.io/termkitbackend).
Follow the tutorial in the [docs](https://term-kit-backend.aparoksha.dev/).
## Thanks
### Dependencies
- [TermKit](https://github.com/david-swift/TermKit) licensed under the [MIT License](https://github.com/david-swift/TermKit/blob/main/LICENSE)
- [Meta](https://github.com/AparokshaUI/Meta) licensed under the [MIT License](https://github.com/AparokshaUI/Meta/blob/main/LICENSE.md)
- [Meta](https://git.aparoksha.dev/aparoksha/meta) licensed under the [MIT License](https://git.aparoksha.dev/aparoksha/meta/src/branch/main/LICENSE.md)
### Other Thanks

View File

@ -7,16 +7,14 @@
import TermKit
/// A menu is an item of a ``MenuBar``.
public struct ButtonCollection: ButtonContext.Widget, Wrapper {
/// A collection of buttons.
public struct ButtonCollection: ButtonWidget, Wrapper {
/// The content of the menu.
/// The content of the collection.
var content: Body
/// Initialize a menu.
/// - Parameters:
/// - label: The menu's label, displayed in the menu bar.
/// - content: The content of the menu.
/// Initialize a collection.
/// - Parameter content: The content of the collection.
public init(@ViewBuilder content: @escaping () -> Body) {
self.content = content()
}
@ -27,18 +25,18 @@ public struct ButtonCollection: ButtonContext.Widget, Wrapper {
/// - type: The type of the views.
/// - Returns: The view storage.
public func container<Data>(
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
var buttons: [Button] = []
for element in content.storages(modifiers: modifiers, type: type) {
for element in content.storages(data: data, type: type) {
if let button = element.pointer as? Button {
buttons.append(button)
} else if let collection = element.pointer as? [Button] {
buttons += collection
}
}
return .init(buttons, content: [.mainContent: content.storages(modifiers: modifiers, type: type)])
return .init(buttons, content: [.mainContent: content.storages(data: data, type: type)])
}
/// Update the stored content.
@ -49,7 +47,7 @@ public struct ButtonCollection: ButtonContext.Widget, Wrapper {
/// - type: The type of the views.
public func update<Data>(
_ storage: ViewStorage,
modifiers: [(AnyView) -> AnyView],
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {

View File

@ -11,11 +11,13 @@ import TermKit
public enum ButtonContext: ViewRenderData {
/// The type of the widgets.
public typealias WidgetType = Widget
public typealias WidgetType = ButtonWidget
/// The wrapper type.
public typealias WrapperType = ButtonCollection
/// The type of the widgets.
public protocol Widget: Meta.Widget { }
/// The either view type.
public typealias EitherViewType = NotUpdatableEitherView
}
/// The type of the widgets.
public protocol ButtonWidget: Meta.Widget { }

View File

@ -0,0 +1,23 @@
//
// NotUpdatableEitherView.swift
// TermKitBackend
//
// Created by david-swift on 25.08.2024.
//
/// An either view for views which do not support updating.
public struct NotUpdatableEitherView: SimpleView, Meta.EitherView {
/// The content.
public var view: Body
/// Initialize the either view.
/// - Parameters:
/// - condition: The condition.
/// - view1: The first view.
/// - view2: The other view.
public init(_ condition: Bool, view1: () -> Body, else view2: () -> Body) {
self.view = condition ? view1() : view2()
}
}

View File

@ -7,8 +7,8 @@
import TermKit
/// A menu is an item of a ``MenuBar``.
public struct Menu: MenuContext.Widget {
/// A menu is an item of a `MenuBar`.
public struct Menu: MenuWidget {
/// The menu's label, displayed in the menu bar.
var label: String
@ -30,10 +30,10 @@ public struct Menu: MenuContext.Widget {
/// - type: The type of the views.
/// - Returns: The view storage.
public func container<Data>(
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let children = content.storages(modifiers: modifiers, type: type)
let children = content.storages(data: data, type: type)
let menu = MenuBarItem(title: label, children: children.compactMap { $0.pointer as? MenuItem })
return .init(menu, content: [.mainContent: children])
}
@ -46,14 +46,14 @@ public struct Menu: MenuContext.Widget {
/// - type: The type of the views.
public func update<Data>(
_ storage: ViewStorage,
modifiers: [(AnyView) -> AnyView],
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
guard let storages = storage.content[.mainContent] else {
return
}
content.update(storages, modifiers: modifiers, updateProperties: updateProperties, type: type)
content.update(storages, data: data, updateProperties: updateProperties, type: type)
}
}

View File

@ -7,16 +7,14 @@
import TermKit
/// A menu is an item of a ``MenuBar``.
public struct MenuCollection: MenuContext.Widget, Wrapper {
/// A collection of menus.
public struct MenuCollection: MenuWidget, Wrapper {
/// The content of the menu.
/// The content of the collection.
var content: Body
/// Initialize a menu.
/// - Parameters:
/// - label: The menu's label, displayed in the menu bar.
/// - content: The content of the menu.
/// - Parameter content: The content of the collection.
public init(@ViewBuilder content: @escaping () -> Body) {
self.content = content()
}
@ -27,12 +25,12 @@ public struct MenuCollection: MenuContext.Widget, Wrapper {
/// - type: The type of the views.
/// - Returns: The view storage.
public func container<Data>(
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
var storages: [ViewStorage] = []
forEachMenu { menu in
storages.append(menu.container(modifiers: modifiers, type: type))
storages.append(menu.container(data: data, type: type))
}
return .init(storages.compactMap { $0.pointer }, content: [.mainContent: storages])
}
@ -45,7 +43,7 @@ public struct MenuCollection: MenuContext.Widget, Wrapper {
/// - type: The type of the views.
public func update<Data>(
_ storage: ViewStorage,
modifiers: [(AnyView) -> AnyView],
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {

View File

@ -11,11 +11,13 @@ import TermKit
public enum MenuContext: ViewRenderData {
/// The type of the widgets.
public typealias WidgetType = Widget
public typealias WidgetType = MenuWidget
/// The wrapper type.
public typealias WrapperType = MenuCollection
/// The type of the widgets.
public protocol Widget: Meta.Widget { }
/// The either view type.
public typealias EitherViewType = NotUpdatableEitherView
}
/// The type of the widgets.
public protocol MenuWidget: Meta.Widget { }

View File

@ -7,6 +7,7 @@
import TermKit
/// Extend `AnyView`.
extension AnyView {
/// Set a view's width and height.

View File

@ -1,5 +1,5 @@
//
// TermKitSceneElement.swift
// TermKitApp.swift
// TermKitBackend
//
// Created by david-swift on 01.07.2024.
@ -13,19 +13,13 @@ public class TermKitApp: AppStorage {
/// The scene element type of the TermKit backend.
public typealias SceneElementType = TermKitSceneElement
/// The widget type of the TermKit backend.
public typealias WidgetType = TermKitWidget
/// The wrapper type of the TermKit backend.
public typealias WrapperType = VStack
/// The app storage.
public var storage: StandardAppStorage = .init()
/// Initialize the app storage.
/// - Parameters:
/// - id: The identifier.
/// - app: The application.
public required init(id: String) { }
/// - Parameter id: The identifier.
public init() { }
/// Execute the app.
/// - Parameter setup: Set the scene elements up.

View File

@ -12,5 +12,7 @@ public enum TermKitMainView: ViewRenderData {
public typealias WidgetType = TermKitWidget
/// The wrapper type.
public typealias WrapperType = VStack
/// The either view type.
public typealias EitherViewType = EitherView
}

View File

@ -8,10 +8,10 @@
import TermKit
/// The menu bar scene element adds a menu bar to the top of the app.
public struct MenuBar: TermKitSceneElement {
struct MenuBar: TermKitSceneElement {
/// The identifier of the menu bar.
public var id: String
var id: String
/// The menu bar's content.
var content: Body
@ -19,34 +19,37 @@ public struct MenuBar: TermKitSceneElement {
/// - Parameters:
/// - id: The identifier of the menu bar.
/// - content: The menu bar's content.
public init(id: String = "main-menu", @ViewBuilder content: () -> Body) {
init(id: String = "main-menu", @ViewBuilder content: () -> Body) {
self.id = id
self.content = content()
}
/// Set up the initial scene storages.
/// - Parameter app: The app storage.
public func setupInitialContainers<Storage>(app: Storage) where Storage: AppStorage {
func setupInitialContainers<Storage>(app: Storage) where Storage: AppStorage {
app.storage.sceneStorage.append(container(app: app))
}
/// The scene storage.
/// - Parameter app: The app storage.
public func container<Storage>(app: Storage) -> SceneStorage where Storage: AppStorage {
let items = MenuCollection { content }.container(modifiers: [], type: MenuContext.self)
func container<Storage>(app: Storage) -> SceneStorage where Storage: AppStorage {
let storage = SceneStorage(id: id, pointer: nil) { }
let items = MenuCollection { content }.container(
data: .init(sceneStorage: storage, appStorage: app),
type: MenuContext.self
)
let menubar = TermKit.MenuBar(
menus: items.pointer as? [TermKit.MenuBarItem] ?? []
)
Task {
try await Task.sleep(nanoseconds: 1)
for element in Application.top.subviews {
element.y = .bottom(of: menubar)
}
Application.top.addSubview(menubar)
for element in Application.top.subviews {
element.y = .bottom(of: menubar)
}
return .init(id: id, pointer: menubar) {
Application.top.addSubview(menubar)
storage.pointer = menubar
storage.show = {
menubar.ensureFocus()
}
return storage
}
/// Update the stored content.
@ -54,7 +57,7 @@ public struct MenuBar: TermKitSceneElement {
/// - storage: The storage to update.
/// - app: The app storage.
/// - updateProperties: Whether to update the view's properties.
public func update<Storage>(
func update<Storage>(
_ storage: SceneStorage,
app: Storage,
updateProperties: Bool

View File

@ -40,13 +40,18 @@ public struct Window: TermKitSceneElement {
let win = TermKit.Window(title)
win.fill()
Application.top.addSubview(win)
let viewStorage = content.storage(modifiers: [], type: TermKitMainView.self)
let storage = SceneStorage(id: id, pointer: win) {
win.ensureFocus()
}
let viewStorage = content.storage(
data: .init(sceneStorage: storage, appStorage: app),
type: TermKitMainView.self
)
if let pointer = viewStorage.pointer as? TermKit.View {
win.addSubview(pointer)
}
return .init(id: id, pointer: win, content: [.mainContent: [viewStorage]]) {
win.ensureFocus()
}
storage.content = [.mainContent: [viewStorage]]
return storage
}
/// Update the stored content.
@ -63,8 +68,21 @@ public struct Window: TermKitSceneElement {
return
}
content
.updateStorage(viewStorage, modifiers: [], updateProperties: updateProperties, type: TermKitMainView.self)
.updateStorage(
viewStorage,
data: .init(sceneStorage: storage, appStorage: app),
updateProperties: updateProperties,
type: TermKitMainView.self
)
Application.refresh()
}
/// Add a menubar to the app.
/// - Parameter content: The mnu bar's content.
@SceneBuilder
public func menuBar(@ViewBuilder content: @escaping () -> Body) -> Scene {
self
MenuBar(content: content)
}
}

View File

@ -0,0 +1,39 @@
# Getting Started
Learn how to use the TermKit backend.
Knowledge about the Meta project is required.
Find more information [here](https://meta.aparoksha.dev).
## The App
Define your app in the following way:
```swift
import TermKitBackend
@main
struct TestApp: App {
let app = TermKitApp()
var scene: Scene {
Window {
// Views (see list in documentation)
}
.menuBar {
Menu("_File") { // Menus
Button("_New") { // Buttons
print("Hi")
}
}
}
}
}
```
## Widgets
All the available widgets can be found in the documentation.
Take a look at the [sample app](https://git.aparoksha.dev/david-swift/term-kit-backend/src/branch/main/Sources/TestApp/TestApp.swift) for more help.

View File

@ -1,8 +1,8 @@
# ``TermKitBackend``
_TermKitBackend_ is a declarative framework allowing the creation of user interface for the terminal. It works on Linux, macOS and Windows thanks to the [TermKit project](https://github.com/migueldeicaza/TermKit).
_TermKitBackend_ is a declarative framework allowing the creation of user interface for the terminal. It works on Linux and macOS thanks to the [TermKit project](https://github.com/migueldeicaza/TermKit).
## Overview
The declarative approach is based on the [Meta package](https://aparokshaui.github.io/meta/) which can be found on [GitHub](https://github.com/AparokshaUI/Meta).
The declarative approach is based on the [Meta package](https://meta.aparoksha.dev/), browse the code [here](https://git.aparoksha.dev/aparoksha/meta).
It is powered by [a fork of TermKit for Swift](https://github.com/david-swift/TermKit).

View File

@ -11,7 +11,6 @@
"button-text": "#ffffff",
"header": "#7f313b",
"documentation-intro-accent": "var(--color-header)",
"documentation-intro-fill": "radial-gradient(circle at top, var(--color-header) 30%, #000 100%)",
"link": "#ea3358",
"nav-link-color": "#ea3358",
"nav-dark-link-color": "#ea3358",

View File

@ -8,7 +8,7 @@
import TermKit
/// A simple button widget.
public struct Button: TermKitWidget, ButtonContext.Widget, MenuContext.Widget {
public struct Button: TermKitWidget, ButtonWidget, MenuWidget {
/// The button's label.
var label: String
@ -33,7 +33,7 @@ public struct Button: TermKitWidget, ButtonContext.Widget, MenuContext.Widget {
/// - type: The type of the app storage.
/// - Returns: The view storage.
public func container<Data>(
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
if type == MenuContext.self {
@ -48,7 +48,7 @@ public struct Button: TermKitWidget, ButtonContext.Widget, MenuContext.Widget {
return ViewStorage(self)
}
let button = TermKit.Button(label, clicked: action)
return .init(button)
return .init(button, state: self)
}
/// Update the stored content.
@ -59,19 +59,20 @@ public struct Button: TermKitWidget, ButtonContext.Widget, MenuContext.Widget {
/// - type: The type of the app storage.
public func update<Data>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
if type == MenuContext.self {
storage.fields[actionID] = action
}
guard let storage = storage.pointer as? TermKit.Button else {
guard let pointer = storage.pointer as? TermKit.Button else {
return
}
storage.clicked = { _ in action() }
if updateProperties {
storage.text = label
pointer.clicked = { _ in action() }
if updateProperties, (storage.previousState as? Self)?.label != label {
pointer.text = label
storage.previousState = self
}
}

View File

@ -11,9 +11,15 @@ import TermKit
public struct Checkbox: TermKitWidget {
/// The label of the checkbox.
var label: String
@Property(set: { $0.text = $1 }, pointer: TermKit.Checkbox.self)
var label = ""
/// Whether the checkbox is on.
var isOn: Binding<Bool>
@BindingProperty(
observe: { box, value in box.toggled = { value.wrappedValue = $0.checked } },
set: { $0.checked = $1 },
pointer: TermKit.Checkbox.self
)
var isOn: Binding<Bool> = .constant(false)
/// Initialize the checkbox.
/// - Parameters:
@ -24,42 +30,10 @@ public struct Checkbox: TermKitWidget {
self.isOn = isOn
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
/// - Returns: The view storage.
public func container<Data>(
modifiers: [(any AnyView) -> any AnyView],
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let button = TermKit.Checkbox(label, checked: isOn.wrappedValue)
button.toggled = { _ in
isOn.wrappedValue = button.checked
}
return .init(button)
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Data>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
guard let storage = storage.pointer as? TermKit.Checkbox, updateProperties else {
return
}
storage.text = label
storage.checked = isOn.wrappedValue
storage.toggled = { _ in
isOn.wrappedValue = storage.checked
}
/// Get the widget.
/// - Returns: The widget.
public func initializeWidget() -> Any {
TermKit.Checkbox(label)
}
}

View File

@ -32,11 +32,11 @@ struct Box: TermKitWidget {
/// - type: The type of the app storage.
/// - Returns: The view storage.
func container<Data>(
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let storage = ViewStorage(nil)
let contentStorage = content.storage(modifiers: modifiers, type: type)
let contentStorage = content.storage(data: data, type: type)
storage.pointer = contentStorage.pointer
return .init(contentStorage.pointer, content: [.mainContent: [contentStorage]])
}
@ -49,16 +49,16 @@ struct Box: TermKitWidget {
/// - type: The type of the app storage.
func update<Data>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
guard let storage = storage.content[.mainContent]?.first else {
return
}
content.updateStorage(storage, modifiers: modifiers, updateProperties: updateProperties, type: type)
content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
let buttons = ButtonCollection { self.buttons }
.storage(modifiers: modifiers, type: ButtonContext.self).pointer as? [Button] ?? []
.storage(data: data, type: ButtonContext.self).pointer as? [Button] ?? []
storage.fields[boxButtonsID] = buttons
if signal.update {
if buttons.isEmpty {
@ -77,6 +77,7 @@ struct Box: TermKitWidget {
}
/// Extend `AnyView`,
extension AnyView {
/// Add a query box.

View File

@ -0,0 +1,78 @@
//
// EitherView.swift
// TermKitBackend
//
// Created by david-swift on 25.08.2024.
//
import TermKit
/// A container which draws a frame around its contents.
public struct EitherView: TermKitWidget, Meta.EitherView {
/// The condition.
var condition: Bool
/// The first view.
var view1: Body
/// The second view.
var view2: Body
/// Initialize an either view.
/// - Parameters:
/// - condition: The condition.
/// - view1: The first view.
/// - view2: The second view.
public init(_ condition: Bool, @ViewBuilder view1: () -> Body, @ViewBuilder else view2: () -> Body) {
self.condition = condition
self.view1 = view1()
self.view2 = view2()
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
/// - Returns: The view storage.
public func container<Data>(
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let view = TermKit.View()
let storage = ViewStorage(view)
update(storage, data: data, updateProperties: true, type: type)
return storage
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Data>(
_ storage: ViewStorage,
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
guard let parent = storage.pointer as? TermKit.View else {
return
}
let view: TermKit.View?
let body = condition ? view1 : view2
if let content = storage.content[condition.description]?.first {
body.updateStorage(content, data: data, updateProperties: updateProperties, type: type)
view = content.pointer as? TermKit.View
} else {
let content = body.storage(data: data, type: type)
storage.content[condition.description] = [content]
view = content.pointer as? TermKit.View
}
if let view, (storage.previousState as? Self)?.condition != condition {
parent.removeAllSubviews()
parent.addSubview(view)
}
storage.previousState = self
}
}

View File

@ -11,8 +11,10 @@ import TermKit
public struct Frame: TermKitWidget {
/// The frame's label.
@Property(set: { $0.title = $1 }, pointer: TermKit.Frame.self)
var label: String?
/// The content.
@ViewProperty(set: { $0.addSubview($1) }, pointer: TermKit.Frame.self, subview: TermKit.View.self)
var view: Body
/// Initialize a frame.
@ -20,46 +22,14 @@ public struct Frame: TermKitWidget {
/// - label: The frame's label.
/// - content: The content.
public init(_ label: String? = nil, @ViewBuilder content: @escaping () -> Body) {
self.label = label
self.view = content()
self.label = label
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
/// - Returns: The view storage.
public func container<Data>(
modifiers: [(any AnyView) -> any AnyView],
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let frame = TermKit.Frame(label)
let subview = view.storage(modifiers: modifiers, type: type)
if let pointer = subview.pointer as? TermKit.View {
frame.addSubview(pointer)
}
return .init(frame, content: [.mainContent: [subview]])
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Data>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
if let storage = storage.content[.mainContent]?.first {
view.updateStorage(storage, modifiers: modifiers, updateProperties: updateProperties, type: type)
}
guard let storage = storage.pointer as? TermKit.Frame, updateProperties else {
return
}
storage.title = label
/// Get the widget.
/// - Returns: The widget.
public func initializeWidget() -> Any {
TermKit.Frame()
}
}

View File

@ -24,10 +24,10 @@ public struct HStack: Wrapper, TermKitWidget {
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
public func container<Data>(
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let storages = content.storages(modifiers: modifiers, type: type)
let storages = content.storages(data: data, type: type)
if storages.count == 1 {
return .init(storages[0].pointer, content: [.mainContent: storages])
}
@ -51,14 +51,14 @@ public struct HStack: Wrapper, TermKitWidget {
/// - type: The type of the app storage.
public func update<Data>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
guard let storages = storage.content[.mainContent] else {
return
}
content.update(storages, modifiers: modifiers, updateProperties: updateProperties, type: type)
content.update(storages, data: data, updateProperties: updateProperties, type: type)
}
}

View File

@ -11,7 +11,8 @@ import TermKit
public struct Label: TermKitWidget {
/// The label.
var label: String
@Property(set: { $0.text = $1 }, pointer: TermKit.Label.self)
var label = ""
/// Initialize a label.
/// - Parameter label: The label.
@ -19,35 +20,10 @@ public struct Label: TermKitWidget {
self.label = label
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
/// - Returns: The view storage.
public func container<Data>(
modifiers: [(any AnyView) -> any AnyView],
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let button = TermKit.Label(label)
return .init(button)
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Data>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
guard let storage = storage.pointer as? TermKit.Label, updateProperties else {
return
}
storage.text = label
/// Get the widget.
/// - Returns: The widget.
public func initializeWidget() -> Any {
TermKit.Label(label)
}
}

View File

@ -30,7 +30,7 @@ public struct ListView<Element>: TermKitWidget where Element: CustomStringConver
/// - type: The type of the app storage.
/// - Returns: The view storage.
public func container<Data>(
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let list = TermKit.ListView(items: items.map { $0.description })
@ -46,7 +46,7 @@ public struct ListView<Element>: TermKitWidget where Element: CustomStringConver
/// - type: The type of the app storage.
public func update<Data>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {

View File

@ -11,49 +11,21 @@ import TermKit
public struct ProgressBar: TermKitWidget {
/// The current value.
var value: Double
/// The maximum value.
var max: Double
@Property(set: { $0.fraction = $1 }, pointer: TermKit.ProgressBar.self)
var fraction: Float = 0
/// Initialize a progress bar.
/// - Parameters:
/// - value: The current value.
/// - max: The maximum value.
public init(value: Double, max: Double = 1) {
self.value = value
self.max = max
self.fraction = .init(value / max)
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
/// - Returns: The view storage.
public func container<Data>(
modifiers: [(any AnyView) -> any AnyView],
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let bar = TermKit.ProgressBar()
bar.fraction = .init(value / max)
return .init(bar)
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Data>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
guard let storage = storage.pointer as? TermKit.ProgressBar, updateProperties else {
return
}
storage.fraction = .init(value / max)
/// Get the widget.
/// - Returns: The widget.
public func initializeWidget() -> Any {
TermKit.ProgressBar()
}
}

View File

@ -25,10 +25,10 @@ public struct ScrollView: TermKitWidget {
/// - type: The type of the app storage.
/// - Returns: The view storage.
public func container<Data>(
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let storages = content.storages(modifiers: modifiers, type: type)
let storages = content.storages(data: data, type: type)
if storages.count == 1 {
return .init(storages[0].pointer, content: [.mainContent: storages])
}
@ -52,14 +52,14 @@ public struct ScrollView: TermKitWidget {
/// - type: The type of the app storage.
public func update<Data>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
guard let storages = storage.content[.mainContent] else {
return
}
content.update(storages, modifiers: modifiers, updateProperties: updateProperties, type: type)
content.update(storages, data: data, updateProperties: updateProperties, type: type)
}
}

View File

@ -11,8 +11,18 @@ import TermKit
public struct TextField: TermKitWidget {
/// The text.
var text: Binding<String>
@BindingProperty(
observe: { pointer, value in
pointer.textChanged = { _, _ in
value.wrappedValue = pointer.text
}
},
set: { $0.text = $1 },
pointer: TermKit.TextField.self
)
var text: Binding<String> = .constant("")
/// Whether the text field is secret.
@Property(set: { $0.secret = $1 }, pointer: TermKit.TextField.self)
var secret = false
/// The identifier for the closure.
@ -24,55 +34,10 @@ public struct TextField: TermKitWidget {
self.text = text
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
/// - Returns: The view storage.
public func container<Data>(
modifiers: [(any AnyView) -> any AnyView],
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let field = TermKit.TextField(text.wrappedValue)
let storage = ViewStorage(field)
field.secret = secret
field.textChanged = { _, _ in
(storage.fields[closureID] as? () -> Void)?()
}
storage.fields[closureID] = {
if field.text != text.wrappedValue {
text.wrappedValue = field.text
}
}
return storage
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Data>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
guard let field = storage.pointer as? TermKit.TextField else {
return
}
storage.fields[closureID] = {
if field.text != text.wrappedValue {
text.wrappedValue = field.text
}
}
if updateProperties {
field.secret = secret
if field.text != text.wrappedValue {
field.text = text.wrappedValue
}
}
/// Get the widget.
/// - Returns: The widget.
public func initializeWidget() -> Any {
TermKit.TextField(text.wrappedValue)
}
/// Set whether the text field is secret.

View File

@ -24,10 +24,10 @@ public struct VStack: Wrapper, TermKitWidget {
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
public func container<Data>(
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let storages = content.storages(modifiers: modifiers, type: type)
let storages = content.storages(data: data, type: type)
if storages.count == 1 {
return .init(storages[0].pointer, content: [.mainContent: storages])
}
@ -51,14 +51,14 @@ public struct VStack: Wrapper, TermKitWidget {
/// - type: The type of the app storage.
public func update<Data>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
guard let storages = storage.content[.mainContent] else {
return
}
content.update(storages, modifiers: modifiers, updateProperties: updateProperties, type: type)
content.update(storages, data: data, updateProperties: updateProperties, type: type)
}
}

View File

@ -24,10 +24,10 @@ public struct ZStack: Wrapper, TermKitWidget {
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
public func container<Data>(
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let storages = content.reversed().storages(modifiers: modifiers, type: type)
let storages = content.reversed().storages(data: data, type: type)
if storages.count == 1 {
return .init(storages[0].pointer, content: [.mainContent: storages])
}
@ -48,14 +48,14 @@ public struct ZStack: Wrapper, TermKitWidget {
/// - type: The type of the app storage.
public func update<Data>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
guard let storages = storage.content[.mainContent] else {
return
}
content.reversed().update(storages, modifiers: modifiers, updateProperties: updateProperties, type: type)
content.reversed().update(storages, data: data, updateProperties: updateProperties, type: type)
}
}

View File

@ -5,19 +5,17 @@
// Created by david-swift on 01.07.2024.
//
import Meta
import TermKitBackend
@main
struct TestApp: App {
@State private var about: Signal = .init()
@State private var state = false
let id = "io.github.AparokshaUI.TestApp"
var app: TermKitApp!
let app = TermKitApp()
var scene: Scene {
menuBar
Window {
VStack {
Demos()
@ -29,10 +27,7 @@ struct TestApp: App {
.vcenter()
.infoBox("About TermKitBackend", message: aboutInfo, signal: about)
}
}
var menuBar: MenuBar {
.init {
.menuBar {
fileMenu
Menu("_Actions") {
Button("_Hello, world!") { }
@ -86,31 +81,45 @@ struct Demos: View {
}
struct ControlsModel: Model {
var isOn = false
var fraction = 0
var text = "Controls"
var model: ModelData?
func increaseFraction() {
Task { @MainActor in
setModel { $0.fraction += 1 }
}
}
}
struct Controls: View {
@State private var isOn = false
@State private var fraction = 0
@State private var text = "Controls"
@State private var model = ControlsModel()
var view: Body {
Frame(text) {
Frame(model.text) {
HStack {
Button("Button (progress)") {
if fraction == 10 {
fraction = 0
if model.fraction == 10 {
model.fraction = 0
} else {
fraction += 1
model.increaseFraction()
}
}
Button("Button (text)") {
text = "Hello"
model.text = "Hello"
}
}
.frame(height: 1)
Checkbox(isOn ? "On" : "Off", isOn: $isOn)
TextField(text: $text)
.secret(isOn)
ProgressBar(value: .init(fraction), max: 10)
Checkbox(model.isOn ? "On" : "Off", isOn: $model.isOn)
TextField(text: $model.text)
.secret(model.isOn)
ProgressBar(value: .init(model.fraction), max: 10)
}
.frame(width: 40, height: 7)
}