Implement scene and app
This commit is contained in:
parent
20b2fee279
commit
0749f01e86
@ -31,7 +31,7 @@ _Meta_ follows the following principles:
|
||||
It knows the following layers of UI:
|
||||
|
||||
- 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 menu is a list of buttons, other menus, and views. Certain views (such as menu buttons) allow menus to be used.
|
||||
|
||||
|
||||
@ -13,6 +13,6 @@ _Meta_ follows the following principles:
|
||||
It knows the following layers of UI:
|
||||
|
||||
- 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 menu is a list of buttons, other menus, and views. Certain views (such as menu buttons) allow menus to be used.
|
||||
|
||||
@ -14,6 +14,8 @@ public enum StateManager {
|
||||
public static var blockUpdates = false
|
||||
/// Whether to save state.
|
||||
public static var saveState = true
|
||||
/// The application identifier.
|
||||
static var appID: String?
|
||||
/// The functions handling view updates.
|
||||
static var updateHandlers: [(Bool) -> Void] = []
|
||||
/// The state.
|
||||
@ -35,7 +37,7 @@ public enum StateManager {
|
||||
func contains(id: UUID) -> Bool {
|
||||
ids.first == id || ids.second == id
|
||||
}
|
||||
|
||||
|
||||
/// Change the identifier to a new one.
|
||||
/// - Parameter newID: The new identifier.
|
||||
mutating func changeID(new newID: UUID) {
|
||||
|
||||
79
Sources/Model/User Interface/App/App.swift
Normal file
79
Sources/Model/User Interface/App/App.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
67
Sources/Model/User Interface/App/AppStorage.swift
Normal file
67
Sources/Model/User Interface/App/AppStorage.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
68
Sources/Model/User Interface/Scene/SceneBuilder.swift
Normal file
68
Sources/Model/User Interface/Scene/SceneBuilder.swift
Normal 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) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
28
Sources/Model/User Interface/Scene/SceneElement.swift
Normal file
28
Sources/Model/User Interface/Scene/SceneElement.swift
Normal 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]
|
||||
55
Sources/Model/User Interface/Scene/SceneStorage.swift
Normal file
55
Sources/Model/User Interface/Scene/SceneStorage.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@ -56,7 +56,7 @@ public enum ViewBuilder {
|
||||
/// 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) -> [AnyView] {
|
||||
public static func buildFinalResult(_ component: Component) -> Body {
|
||||
switch component {
|
||||
case let .element(element):
|
||||
return [element]
|
||||
|
||||
@ -3,16 +3,44 @@ import Meta
|
||||
import Observation
|
||||
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(iOS 17, *)
|
||||
struct DemoView: View {
|
||||
|
||||
@State private var model = TestModel()
|
||||
var app: Backend1.Backend1App
|
||||
|
||||
var view: Body {
|
||||
Backend1.TestWidget1()
|
||||
Backend1.Button(model.test) {
|
||||
model.test = "\(Int.random(in: 1...10))"
|
||||
app.addSceneElement("main")
|
||||
}
|
||||
TestView()
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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 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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -40,4 +40,28 @@ public enum Backend2 {
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user