89 lines
3.4 KiB
Swift
89 lines
3.4 KiB
Swift
import Adw
|
|
|
|
/// Owns the application's live scene and re-attaches it when the selected
|
|
/// ``SceneBuilder`` branch changes.
|
|
///
|
|
/// The scene value is produced inside a ``DependencyTracker``, so the state the
|
|
/// branch condition reads becomes a dependency. Building the scene value is cheap -
|
|
/// an ``ApplicationWindow``'s content closure is not invoked until `attach` - so
|
|
/// only the condition, not the view tree, is tracked.
|
|
///
|
|
/// A re-evaluation whose ``SceneIdentity`` matches the live one is discarded: the
|
|
/// window is rebuilt exactly when the selected branch changes, never merely because
|
|
/// some state the body read changed.
|
|
@_spi(Portico) @MainActor public final class SceneHost {
|
|
private let app: Adw.Application
|
|
private let makeScene: () -> any Scene
|
|
private var tracker: DependencyTracker?
|
|
private var isStarted = false
|
|
private var pendingScene: (any MountableScene)?
|
|
private var targetIdentity: SceneIdentity?
|
|
private var flushScheduled = false
|
|
|
|
/// The currently attached window, or `nil` when the selected branch attaches none.
|
|
@_spi(Portico) public private(set) var live: SceneHandle?
|
|
|
|
/// The identity of ``live``.
|
|
@_spi(Portico) public private(set) var liveIdentity: SceneIdentity?
|
|
|
|
/// - Parameters:
|
|
/// - app: The application windows are attached to.
|
|
/// - makeScene: Produces the root scene; called on every re-evaluation.
|
|
@_spi(Portico) public init(app: Adw.Application, makeScene: @escaping () -> any Scene) {
|
|
self.app = app
|
|
self.makeScene = makeScene
|
|
}
|
|
|
|
/// Evaluates the scene tree and presents the selected window synchronously.
|
|
///
|
|
/// A second call - a second `activate` on a single-instance app - re-presents
|
|
/// the live window instead of building a duplicate.
|
|
@_spi(Portico) public func start() {
|
|
if isStarted {
|
|
live?.present()
|
|
return
|
|
}
|
|
let t = DependencyTracker { [weak self] in self?.evaluate() }
|
|
tracker = t
|
|
t.run()
|
|
isStarted = true
|
|
flush()
|
|
}
|
|
|
|
/// The tracked body: builds the scene value and records a swap when the
|
|
/// selected branch differs from the one already targeted.
|
|
private func evaluate() {
|
|
let scene = makeScene()
|
|
guard let m = scene as? MountableScene else {
|
|
fatalError(
|
|
"Portico: top-level Scene must conform to MountableScene "
|
|
+ "(use ApplicationWindow, optionally behind an if/else)"
|
|
)
|
|
}
|
|
let id = m.identity
|
|
guard targetIdentity != id else { return }
|
|
targetIdentity = id
|
|
pendingScene = m
|
|
scheduleFlush()
|
|
}
|
|
|
|
private func scheduleFlush() {
|
|
guard isStarted, !flushScheduled else { return }
|
|
flushScheduled = true
|
|
Idle { [weak self] in self?.flush() }
|
|
}
|
|
|
|
/// Presents the pending window, then destroys the previous one. The order
|
|
/// matters: GTK quits the application the moment it owns no windows.
|
|
private func flush() {
|
|
flushScheduled = false
|
|
guard let scene = pendingScene, let id = targetIdentity else { return }
|
|
pendingScene = nil
|
|
// The branch flipped away and back before the idle ran - keep the live window.
|
|
guard id != liveIdentity else { return }
|
|
let next = scene.attach(to: app)
|
|
live?.dismantle()
|
|
live = next
|
|
liveIdentity = id
|
|
}
|
|
}
|