Resolve App dynamic properties before startup

This commit is contained in:
Brendan Szymanski 2026-08-10 23:44:42 -04:00
parent 44f95fe0ad
commit af7fa6656d
3 changed files with 133 additions and 0 deletions

View 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")
}
}