Resolve App dynamic properties before startup
This commit is contained in:
parent
44f95fe0ad
commit
af7fa6656d
3 changed files with 133 additions and 0 deletions
102
Tests/PorticoTests/AppEnvironmentTests.swift
Normal file
102
Tests/PorticoTests/AppEnvironmentTests.swift
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import Testing
|
||||
@_spi(Portico) import Portico
|
||||
@_spi(SGTKInternal) import Gtk
|
||||
import Adw
|
||||
|
||||
@_silgen_name("g_main_context_iteration")
|
||||
private nonisolated func appEnv_g_main_context_iteration(
|
||||
_ context: UnsafeMutableRawPointer?, _ mayBlock: Int32
|
||||
) -> Int32
|
||||
|
||||
/// Drives the GLib main context until `condition` holds or `turns` is exhausted.
|
||||
/// Returns `true` if the loop exhausted turns without the condition becoming true.
|
||||
@MainActor private func pump(until condition: () -> Bool, turns: Int = 200) -> Bool {
|
||||
for _ in 0..<turns {
|
||||
if condition() { return false }
|
||||
_ = appEnv_g_main_context_iteration(nil, 0)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private enum AppFlagKey: EnvironmentKey {
|
||||
static let defaultValue = false
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
fileprivate var appFlag: Bool {
|
||||
get { self[AppFlagKey.self] }
|
||||
set { self[AppFlagKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared reference storage, so every struct copy of the wrapper observes one resolution.
|
||||
@MainActor private final class FlagStorage {
|
||||
var box: StateBox<Bool>?
|
||||
}
|
||||
|
||||
/// A wrapper with no lazy fallback, standing in for a third-party `DynamicProperty`
|
||||
/// that has nothing to read before `_resolve` runs.
|
||||
@propertyWrapper @MainActor private struct AppFlag: DynamicProperty {
|
||||
private let storage = FlagStorage()
|
||||
|
||||
init() {}
|
||||
|
||||
func _resolve(in environment: EnvironmentValues) {
|
||||
storage.box = environment._box(for: \.appFlag)
|
||||
}
|
||||
|
||||
/// `nil` until resolution; a tracking read afterwards.
|
||||
var wrappedValue: Bool? { storage.box?.get() }
|
||||
|
||||
var isResolved: Bool { storage.box != nil }
|
||||
}
|
||||
|
||||
private struct ProbeApp: App {
|
||||
@AppFlag var flag: Bool?
|
||||
|
||||
/// Surfaces the wrapper's resolution state; `_flag` is not reachable from the suite type.
|
||||
var isResolved: Bool { _flag.isResolved }
|
||||
|
||||
var body: some Scene {
|
||||
if flag == true {
|
||||
ApplicationWindow { _ in Gtk.Label(str: "on") }.title("On")
|
||||
} else {
|
||||
ApplicationWindow { _ in Gtk.Label(str: "off") }.title("Off")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor @Suite(.serialized) struct AppEnvironmentTests {
|
||||
@Test func rootResolutionBindsAppDynamicProperties() {
|
||||
let box = EnvironmentValues()._box(for: \.appFlag)!
|
||||
box.set(false)
|
||||
|
||||
let app = ProbeApp()
|
||||
#expect(!app.isResolved)
|
||||
|
||||
PorticoRuntime._resolveRootProperties(app)
|
||||
|
||||
#expect(app.isResolved)
|
||||
#expect(app.flag == false)
|
||||
}
|
||||
|
||||
@Test func sceneConditionalTracksAppDynamicProperty() {
|
||||
guard Gtk.initCheck() else { return }
|
||||
|
||||
let box = EnvironmentValues()._box(for: \.appFlag)!
|
||||
box.set(false)
|
||||
let app = ProbeApp()
|
||||
PorticoRuntime._resolveRootProperties(app)
|
||||
|
||||
let adwApp = Adw.Application(applicationId: nil, flags: .defaultFlags)
|
||||
let host = SceneHost(app: adwApp, makeScene: { app.body })
|
||||
host.start()
|
||||
#expect(host.liveIdentity == .second(.leaf))
|
||||
#expect(host.live?.window.getTitle() == "Off")
|
||||
|
||||
box.set(true)
|
||||
let timedOut = pump(until: { host.liveIdentity == .first(.leaf) })
|
||||
#expect(!timedOut, "pump timed out waiting for branch swap")
|
||||
#expect(host.live?.window.getTitle() == "On")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue