Add support for custom renderable types

This commit is contained in:
david-swift 2024-07-03 20:56:31 +02:00
parent fedc845b3f
commit 700b953953
10 changed files with 224 additions and 7 deletions

View File

@ -32,8 +32,8 @@ 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, another view or a menu.
- A menu is a list of buttons, other menus, and views. Certain views (such as menu buttons) allow menus to be used.
- A view is a part of the actual UI inside a window, another view, or another renderable component.
- Custom renderable components can be used for more restrictive UI elements, such as menus.
Detailed information can be found in the [docs](https://aparokshaui.github.io/meta/).

View File

@ -14,5 +14,5 @@ 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, another view or a menu.
- A menu is a list of buttons, other menus, and views. Certain views (such as menu buttons) allow menus to be used.
- A view is a part of the actual UI inside a window, another view, or another renderable component.
- Custom renderable components can be used for more restrictive UI elements, such as menus.

View File

@ -36,18 +36,18 @@ extension Array: AnyView where Element == AnyView {
/// Update a collection of views with a collection of view storages.
/// - Parameters:
/// - storage: The collection of view storages.
/// - storages: The collection of view storages.
/// - modifiers: Modify views before being updated.
/// - updateProperties: Whether to update properties.
/// - type: The type of the app storage.
public func update<Storage>(
_ storage: [ViewStorage],
_ storages: [ViewStorage],
modifiers: [(AnyView) -> AnyView],
updateProperties: Bool,
type: Storage.Type
) where Storage: AppStorage {
for (index, element) in filter({ $0.renderable(type: type, modifiers: modifiers) }).enumerated() {
if let storage = storage[safe: index] {
if let storage = storages[safe: index] {
element
.widget(modifiers: modifiers, type: type)
.updateStorage(storage, modifiers: modifiers, updateProperties: updateProperties, type: type)
@ -114,3 +114,44 @@ extension Array where Element: Identifiable {
}
}
extension Array where Element == Renderable {
/// Update a collection of renderable elements.
/// - Parameters:
/// - storage: The collection of renderable storages.
/// - updateProperties: Whether to update properties.
/// - type: The type of the renderable element.
/// - fields: Additional information.
public func update<RenderableType>(
_ storages: [RenderableStorage],
updateProperties: Bool,
type: RenderableType.Type,
fields: [String: Any]
) {
for (index, element) in filter({ $0 as? RenderableType != nil }).enumerated() {
if let storage = storages[safe: index] {
element
.update(storage, updateProperties: updateProperties, type: type, fields: fields)
}
}
}
/// Get the renderable storages of a collection of renderable elements.
/// - Parameters:
/// - type: The type of the renderable element.
/// - fields: Additional information.
/// - Returns: The storages.
public func storages<RenderableType>(
type: RenderableType.Type,
fields: [String: Any]
) -> [RenderableStorage] {
compactMap { element in
if element as? RenderableType != nil {
return element.container(type: type, fields: fields)
}
return nil
}
}
}

View File

@ -0,0 +1,68 @@
//
// Builder.swift
// Meta
//
// Created by david-swift on 03.07.24.
//
import Foundation
/// The ``Builder`` is a result builder for scenes.
@resultBuilder
public enum Builder<RenderableType> {
/// A component used in the ``MenuBuilder``.
public enum Component {
/// A renderable element as a component.
case element(_: RenderableType)
/// An array of components as a component.
case components(_: [Self])
}
/// Build combined results from statement blocks.
/// - Parameter components: The components.
/// - Returns: The components in a component.
public static func buildBlock(_ elements: Component...) -> Component {
.components(elements)
}
/// Translate an element into a ``MenuBuilder.Component``.
/// - Parameter element: The element to translate.
/// - Returns: A component created from the element.
public static func buildExpression(_ element: RenderableType) -> Component {
.element(element)
}
/// Translate an array of elements into a ``MenuBuilder.Component``.
/// - Parameter elements: The elements to translate.
/// - Returns: A component created from the element.
public static func buildExpression(_ elements: [RenderableType]) -> Component {
var components: [Component] = []
for element in elements {
components.append(.element(element))
}
return .components(components)
}
/// Fetch a component.
/// - Parameter component: A component.
/// - Returns: The component.
public static func buildExpression(_ component: Component) -> Component {
component
}
/// Convert a component to an array of elements.
/// - Parameter component: The component to convert.
/// - Returns: The generated array of elements.
public static func buildFinalResult(_ component: Component) -> [RenderableType] {
switch component {
case let .element(element):
return [element]
case let .components(components):
return components.flatMap { buildFinalResult($0) }
}
}
}

View File

@ -0,0 +1,30 @@
//
// Renderable.swift
// Meta
//
// Created by david-swift on 03.07.24.
//
/// A structure conforming to `Renderable` is a more limited UI type similar to a view, e.g. a menu.
public protocol Renderable {
/// The view storage.
/// - Parameters:
/// - type: The type of the renderable elements.
/// - fields: More information.
func container<RenderableType>(type: RenderableType.Type, fields: [String: Any]) -> RenderableStorage
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - updateProperties: Whether to update the properties.
/// - type: The type of the renderable elements.
/// - fields:
func update<RenderableType>(
_ storage: RenderableStorage,
updateProperties: Bool,
type: RenderableType.Type,
fields: [String: Any]
)
}

View File

@ -0,0 +1,34 @@
//
// ViewStorage.swift
// Meta
//
// Created by david-swift on 26.05.24.
//
/// Store a reference to a rendered view in a view storage.
public class RenderableStorage {
/// The pointer.
///
/// It can be a C pointer, a Swift class, or other information depending on the backend and renderable type.
public var pointer: Any?
/// Various properties of a widget.
public var fields: [String: Any] = [:]
/// The pointer as an opaque pointer, as this is may be needed with backends interoperating with C or C++.
public var opaquePointer: OpaquePointer? {
get {
pointer as? OpaquePointer
}
set {
pointer = newValue
}
}
/// Initialize a renderable storage.
/// - Parameter pointer: The pointer to the renderable element, its type depends on the backend.
public init(_ pointer: Any?, content: [String: [ViewStorage]] = [:]) {
self.pointer = pointer
}
}

View File

@ -14,6 +14,8 @@ public class ViewStorage {
public var pointer: Any?
/// The view's content for container widgets.
public var content: [String: [ViewStorage]]
/// The view's renderable content.
public var renderableContent: [String: [RenderableStorage]] = [:]
/// The view's state (used in `StateWrapper`).
var state: [String: StateProtocol] = [:]
/// Various properties of a widget.

View File

@ -19,6 +19,7 @@ public protocol Widget: AnyView {
modifiers: [(AnyView) -> AnyView],
type: Storage.Type
) -> ViewStorage where Storage: AppStorage
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.

View File

@ -71,6 +71,10 @@ struct TestView: View {
test = "\(Int.random(in: 1...10))"
}
}
Backend1.Menu("Hi") {
Backend1.Menu("World") {
}
}
}
}

View File

@ -70,6 +70,39 @@ public enum Backend1 {
}
public struct Menu: BackendWidget, MenuElement {
var label: String
var content: MenuContent
public init(_ label: String, @Builder<MenuElement> content: @escaping () -> MenuContent) {
self.label = label
self.content = content()
}
public func container<RenderableType>(type: RenderableType.Type, fields: [String : Any]) -> RenderableStorage {
.init(nil)
}
public func update<RenderableType>(_ storage: RenderableStorage, updateProperties: Bool, type: RenderableType.Type, fields: [String : Any]) {
print("Update renderable")
}
public func container<Storage>(modifiers: [(any AnyView) -> any AnyView], type: Storage.Type) -> ViewStorage where Storage: AppStorage {
let storage = ViewStorage(nil)
storage.renderableContent[.mainContent] = (content as [Renderable]).storages(type: MenuElement.self, fields: [:]) ?? []
return storage
}
public func update<Storage>(_ storage: ViewStorage, modifiers: [(any AnyView) -> any AnyView], updateProperties: Bool, type: Storage.Type) {
guard updateProperties, let content = storage.renderableContent[.mainContent] else {
return
}
(self.content as [Renderable]).update(content, updateProperties: updateProperties, type: MenuElement.self, fields: [:])
}
}
public struct Window: BackendSceneElement {
public var id: String
@ -133,6 +166,10 @@ public enum Backend1 {
public protocol BackendSceneElement: SceneElement { }
public protocol MenuElement: Renderable { }
public typealias MenuContent = [MenuElement]
public class Backend1App: AppStorage {
public typealias SceneElementType = BackendSceneElement