Implement scene and app

This commit is contained in:
david-swift 2024-07-01 17:07:08 +02:00
parent 20b2fee279
commit 0749f01e86
12 changed files with 415 additions and 27 deletions

View File

@ -31,7 +31,7 @@ _Meta_ follows the following principles:
It knows the following layers of UI: It knows the following layers of UI:
- An app is the entry point of the executable, containing the windows. - An app is the entry point of the executable, containing the windows.
- A window is a container holding one or multiple views. - 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 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 menu is a list of buttons, other menus, and views. Certain views (such as menu buttons) allow menus to be used.

View File

@ -13,6 +13,6 @@ _Meta_ follows the following principles:
It knows the following layers of UI: It knows the following layers of UI:
- An app is the entry point of the executable, containing the windows. - An app is the entry point of the executable, containing the windows.
- A window is a container holding one or multiple views. - 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 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 menu is a list of buttons, other menus, and views. Certain views (such as menu buttons) allow menus to be used.

View File

@ -14,6 +14,8 @@ public enum StateManager {
public static var blockUpdates = false public static var blockUpdates = false
/// Whether to save state. /// Whether to save state.
public static var saveState = true public static var saveState = true
/// The application identifier.
static var appID: String?
/// The functions handling view updates. /// The functions handling view updates.
static var updateHandlers: [(Bool) -> Void] = [] static var updateHandlers: [(Bool) -> Void] = []
/// The state. /// The state.
@ -35,7 +37,7 @@ public enum StateManager {
func contains(id: UUID) -> Bool { func contains(id: UUID) -> Bool {
ids.first == id || ids.second == id ids.first == id || ids.second == id
} }
/// Change the identifier to a new one. /// Change the identifier to a new one.
/// - Parameter newID: The new identifier. /// - Parameter newID: The new identifier.
mutating func changeID(new newID: UUID) { mutating func changeID(new newID: UUID) {

View File

@ -0,0 +1,79 @@
//
// App.swift
// Meta
//
// Created by david-swift on 01.07.24.
//
/// A structure conforming to `App` is the entry point of your app.
///
/// ```swift
/// @main
/// struct Test: App {
///
/// let id = "io.github.AparokshaUI.TestApp"
///
/// var scene: Scene {
/// WindowScene()
/// }
///
/// }
/// ```
///
public protocol App {
/// The app storage typ.
associatedtype Storage: AppStorage
/// The app's application ID.
var id: String { get }
/// The app's scene.
@SceneBuilder var scene: Scene { get }
// swiftlint:disable implicitly_unwrapped_optional
/// The app storage.
var app: Storage! { get set }
// swiftlint:enable implicitly_unwrapped_optional
/// An app has to have an `init()` initializer.
init()
}
extension App {
/// The application's entry point.
public static func main() {
let app = setupApp()
app.app.run {
for element in app.scene {
element.setupInitialContainers(app: app.app)
}
}
}
/// Initialize and get the app with the app storage.
///
/// To run the app, call the ``AppStorage/run(automaticSetup:manualSetup:)`` function.
public static func setupApp() -> Self {
var appInstance = self.init()
appInstance.app = Storage(id: appInstance.id) { appInstance }
StateManager.addUpdateHandler { force in
var removeIndices: [Int] = []
for (index, element) in appInstance.app.sceneStorage.enumerated() {
if element.destroy {
removeIndices.insert(index, at: 0)
} else if let scene = appInstance.scene.first(
where: { $0.id == element.id }
) as? Storage.SceneElementType as? SceneElement {
scene.update(element, app: appInstance.app, updateProperties: force)
}
}
for index in removeIndices {
appInstance.app.sceneStorage.remove(at: index)
}
}
StateManager.appID = appInstance.id
return appInstance
}
}

View File

@ -0,0 +1,67 @@
//
// AppStorage.swift
// Meta
//
// Created by david-swift on 01.07.24.
//
/// The app storage protocol.
public protocol AppStorage: AnyObject {
/// The type of scene elements (which should be backend-specific).
associatedtype SceneElementType
/// The type of widget elements (which should be backend-specific).
associatedtype WidgetType
/// The scene.
var app: () -> any App { get }
/// The scene storage.
var sceneStorage: [SceneStorage] { get set }
/// Initialize the app storage.
/// - Parameters:
/// - id: The app's identifier.
/// - app: Get the application.
init(id: String, app: @escaping () -> any App)
/// Run the application.
/// - Parameter setup: A closure that is expected to be executed right at the beginning.
func run(setup: @escaping () -> Void)
/// Terminate the application.
func quit()
}
extension AppStorage {
/// Focus the scene element with a certain id (if supported). Create the element if it doesn't already exist.
/// - Parameter id: The element's id.
public func showSceneElement(_ id: String) {
sceneStorage.last { $0.id == id && !$0.destroy }?.show() ?? addSceneElement(id)
}
/// Add a new scene element with the content of the scene element with a certain id.
/// - Parameter id: The element's id.
public func addSceneElement(_ id: String) {
if let element = app().scene.last(where: { $0.id == id }) {
let container = element.container(app: self)
sceneStorage.append(container)
showSceneElement(id)
}
}
/// Focus the window with a certain id (if supported). Create the window if it doesn't already exist.
/// - Parameter id: The window's id.
public func showWindow(_ id: String) {
showSceneElement(id)
}
/// Add a new window with the content of the window template with a certain id.
/// - Parameter id: The window template's id.
public func addWindow(_ id: String) {
addSceneElement(id)
}
}

View File

@ -0,0 +1,68 @@
//
// SceneBuilder.swift
// Meta
//
// Created by david-swift on 30.06.24.
//
import Foundation
/// The ``SceneBuilder`` is a result builder for scenes.
@resultBuilder
public enum SceneBuilder {
/// A component used in the ``SceneBuilder``.
public enum Component {
/// A scene as a component.
case element(_: any SceneElement)
/// 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 ``SceneBuilder.Component``.
/// - Parameter element: The element to translate.
/// - Returns: A component created from the element.
public static func buildExpression(_ element: any SceneElement) -> Component {
.element(element)
}
/// Translate an array of elements into a ``SceneBuilder.Component``.
/// - Parameter elements: The elements to translate.
/// - Returns: A component created from the element.
public static func buildExpression(_ elements: [any SceneElement]) -> 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) -> Scene {
switch component {
case let .element(element):
return [element]
case let .components(components):
return components.flatMap { buildFinalResult($0) }
}
}
}

View File

@ -0,0 +1,28 @@
//
// SceneElement.swift
// Meta
//
// Created by david-swift on 30.06.24.
//
/// A structure conforming to `SceneElement` can be added to an app's `scene` property.
public protocol SceneElement {
/// The window type's identifier.
var id: String { get }
/// Set up the initial scene storages.
/// - Parameter app: The app storage.
func setupInitialContainers<Storage>(app: Storage) where Storage: AppStorage
/// The scene storage.
/// - Parameter app: The app storage.
func container<Storage>(app: Storage) -> SceneStorage where Storage: AppStorage
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - updateProperties: Whether to update the view's properties.
func update<Storage>(_ storage: SceneStorage, app: Storage, updateProperties: Bool) where Storage: AppStorage
}
/// `Scene` is an array of scene elements.
public typealias Scene = [any SceneElement]

View File

@ -0,0 +1,55 @@
//
// SceneStorage.swift
// Meta
//
// Created by david-swift on 30.06.24.
//
/// Store a reference to a rendered scene element in a view storage.
public class SceneStorage {
/// The scene element's identifier.
public var id: String
/// The pointer.
///
/// It can be a C pointer, a Swift class, or other information depending on the backend,
/// or it can be left out.
public var pointer: Any?
/// The scene element's view content.
public var content: [String: [ViewStorage]]
/// Various properties of a scene element.
public var fields: [String: Any] = [:]
/// Whether the reference to the window should disappear in the next update.
public var destroy = false
/// Show the scene element (including moving into the foreground, if possible).
public var show: () -> Void
/// The pointer as an opaque pointer, as this may be needed with backends interoperating with C or C++.
public var opaquePointer: OpaquePointer? {
get {
pointer as? OpaquePointer
}
set {
pointer = newValue
}
}
/// Initialize a scene storage.
/// - Parameters:
/// - id: The scene element's identifier.
/// - pointer: The pointer to the widget, its type depends on the backend.
/// - content: The view's content for container widgets.
/// - show: Function called when the scene element should be displayed.
public init(
id: String,
pointer: Any?,
content: [String: [ViewStorage]] = [:],
show: @escaping () -> Void
) {
self.id = id
self.pointer = pointer
self.content = content
self.show = show
}
}

View File

@ -56,7 +56,7 @@ public enum ViewBuilder {
/// Convert a component to an array of elements. /// Convert a component to an array of elements.
/// - Parameter component: The component to convert. /// - Parameter component: The component to convert.
/// - Returns: The generated array of elements. /// - Returns: The generated array of elements.
public static func buildFinalResult(_ component: Component) -> [AnyView] { public static func buildFinalResult(_ component: Component) -> Body {
switch component { switch component {
case let .element(element): case let .element(element):
return [element] return [element]

View File

@ -3,16 +3,44 @@ import Meta
import Observation import Observation
import SampleBackends import SampleBackends
@main
struct TestExecutable {
public static func main() {
DemoApp.main()
sleep(2)
}
}
@available(macOS 14, *)
@available(iOS 17, *)
struct DemoApp: App {
typealias Storage = Backend1.Backend1App
let id = "io.github.AparokshaUI.DemoApp"
var app: Backend1.Backend1App!
var scene: Scene {
Backend1.Window("main", spawn: 1) {
DemoView(app: app)
}
}
}
@available(macOS 14, *) @available(macOS 14, *)
@available(iOS 17, *) @available(iOS 17, *)
struct DemoView: View { struct DemoView: View {
@State private var model = TestModel() @State private var model = TestModel()
var app: Backend1.Backend1App
var view: Body { var view: Body {
Backend1.TestWidget1() Backend1.TestWidget1()
Backend1.Button(model.test) { Backend1.Button(model.test) {
model.test = "\(Int.random(in: 1...10))" model.test = "\(Int.random(in: 1...10))"
app.addSceneElement("main")
} }
TestView() TestView()
testContent testContent
@ -51,26 +79,3 @@ class TestModel {
} }
@main
@available(macOS 14, *)
@available(iOS 17, *)
struct DemoApp {
static func main() {
let backendType = Backend1.BackendWidget.self
let storage = DemoView().storage(modifiers: [], type: backendType)
for round in 0...2 {
print("#\(round)")
DemoView().updateStorage(storage, modifiers: [], updateProperties: true, type: backendType)
}
StateManager.addUpdateHandler { force in
print("#*")
DemoView().updateStorage(storage, modifiers: [], updateProperties: force, type: backendType)
}
sleep(2)
}
}

View File

@ -70,7 +70,67 @@ public enum Backend1 {
} }
public struct Window: BackendSceneElement {
public var id: String
var spawn: Int
var content: Body
public init(_ id: String, spawn: Int, @ViewBuilder content: () -> Body) {
self.id = id
self.spawn = spawn
self.content = content()
}
public func setupInitialContainers<Storage>(app: Storage) where Storage: AppStorage {
for _ in 0..<spawn {
app.sceneStorage.append(container(app: app))
}
}
public func container<Storage>(app: Storage) -> SceneStorage where Storage: AppStorage {
print("Show \(id)")
let viewStorage = content.storage(modifiers: [], type: Storage.WidgetType.self)
return .init(id: id, pointer: nil, content: [.mainContent : [viewStorage]]) {
print("Make visible")
}
}
public func update<Storage>(_ storage: SceneStorage, app: Storage, updateProperties: Bool) where Storage: AppStorage {
print("Update \(id)")
guard let viewStorage = storage.content[.mainContent]?.first else {
return
}
content.updateStorage(viewStorage, modifiers: [], updateProperties: updateProperties, type: Storage.WidgetType.self)
}
}
public protocol BackendWidget: Widget { } public protocol BackendWidget: Widget { }
public protocol BackendSceneElement: SceneElement { }
public class Backend1App: AppStorage {
public typealias SceneElementType = BackendSceneElement
public typealias WidgetType = BackendWidget
public var app: () -> any App
public var sceneStorage: [SceneStorage] = []
public required init(id: String, app: @escaping () -> any App) {
self.app = app
}
public func run(setup: @escaping () -> Void) {
setup()
}
public func quit() {
fatalError("Quit")
}
}
} }

View File

@ -40,4 +40,28 @@ public enum Backend2 {
public protocol BackendWidget: Widget { } public protocol BackendWidget: Widget { }
public protocol BackendSceneElement: SceneElement { }
public class Backend2App: AppStorage {
public typealias SceneElementType = BackendSceneElement
public typealias WidgetType = BackendWidget
public var app: () -> any App
public var sceneStorage: [SceneStorage] = []
public required init(id: String, app: @escaping () -> any App) {
self.app = app
}
public func run(setup: @escaping () -> Void) {
setup()
}
public func quit() {
fatalError("Quit")
}
}
} }