diff --git a/README.md b/README.md index 5c06f54..9db9440 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/Sources/Meta.docc/Meta.md b/Sources/Meta.docc/Meta.md index e2d537c..fb395cc 100644 --- a/Sources/Meta.docc/Meta.md +++ b/Sources/Meta.docc/Meta.md @@ -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. diff --git a/Sources/Model/Data Flow/StateManager.swift b/Sources/Model/Data Flow/StateManager.swift index 9a1a0aa..faf4ce0 100644 --- a/Sources/Model/Data Flow/StateManager.swift +++ b/Sources/Model/Data Flow/StateManager.swift @@ -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) { diff --git a/Sources/Model/User Interface/App/App.swift b/Sources/Model/User Interface/App/App.swift new file mode 100644 index 0000000..d138d98 --- /dev/null +++ b/Sources/Model/User Interface/App/App.swift @@ -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 + } + +} diff --git a/Sources/Model/User Interface/App/AppStorage.swift b/Sources/Model/User Interface/App/AppStorage.swift new file mode 100644 index 0000000..3473fe6 --- /dev/null +++ b/Sources/Model/User Interface/App/AppStorage.swift @@ -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) + } + +} diff --git a/Sources/Model/User Interface/Scene/SceneBuilder.swift b/Sources/Model/User Interface/Scene/SceneBuilder.swift new file mode 100644 index 0000000..a7288ae --- /dev/null +++ b/Sources/Model/User Interface/Scene/SceneBuilder.swift @@ -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) } + } + } + +} diff --git a/Sources/Model/User Interface/Scene/SceneElement.swift b/Sources/Model/User Interface/Scene/SceneElement.swift new file mode 100644 index 0000000..6af5189 --- /dev/null +++ b/Sources/Model/User Interface/Scene/SceneElement.swift @@ -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(app: Storage) where Storage: AppStorage + /// The scene storage. + /// - Parameter app: The app storage. + func container(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: SceneStorage, app: Storage, updateProperties: Bool) where Storage: AppStorage + +} + +/// `Scene` is an array of scene elements. +public typealias Scene = [any SceneElement] diff --git a/Sources/Model/User Interface/Scene/SceneStorage.swift b/Sources/Model/User Interface/Scene/SceneStorage.swift new file mode 100644 index 0000000..117670d --- /dev/null +++ b/Sources/Model/User Interface/Scene/SceneStorage.swift @@ -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 + } + +} diff --git a/Sources/Model/User Interface/View/ViewBuilder.swift b/Sources/Model/User Interface/View/ViewBuilder.swift index 54fdae4..09df426 100644 --- a/Sources/Model/User Interface/View/ViewBuilder.swift +++ b/Sources/Model/User Interface/View/ViewBuilder.swift @@ -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] diff --git a/Tests/DemoApp/DemoApp.swift b/Tests/DemoApp/DemoApp.swift index e3434fa..34601e4 100644 --- a/Tests/DemoApp/DemoApp.swift +++ b/Tests/DemoApp/DemoApp.swift @@ -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) - } - -} diff --git a/Tests/SampleBackends/Backend1.swift b/Tests/SampleBackends/Backend1.swift index 09f96cd..805859e 100644 --- a/Tests/SampleBackends/Backend1.swift +++ b/Tests/SampleBackends/Backend1.swift @@ -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(app: Storage) where Storage: AppStorage { + for _ in 0..(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: 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") + } + + } + } diff --git a/Tests/SampleBackends/Backend2.swift b/Tests/SampleBackends/Backend2.swift index aff1a86..a5340b0 100644 --- a/Tests/SampleBackends/Backend2.swift +++ b/Tests/SampleBackends/Backend2.swift @@ -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") + } + + } + }