portico/Sources/Portico/Core/MountContext.swift
Brendan Szymanski da91b02a6a Port @Environment and @Observable support
Adds SwiftUI-style @Environment property wrapper with key-path and
object-injection flavors, an @Observable bridge that flushes on a GLib
idle source at highIdle priority (ahead of GTK redraw), targeted widget
updates driven by Observation tracking, and two-way observable bindings
that ride the existing bindProperty path.

New library files (4):
- State/ObservationBridge.swift -- coalesced flush bridge
- State/EnvironmentValues.swift -- scope-local key-to-box map
- State/Environment.swift -- @Environment wrapper, DynamicProperty
- Core/View+Environment.swift -- .environment(...) modifiers

Modified library files (8):
- DependencyTracker: import Observation, observing: param, didArmObservation
- EitherView/ForEach: opt container trackers out of Observation
- NodeRegistry: isEmpty for retention check
- PropertyBinding: hasDependencies || didArmObservation gate
- Binding: readOnly init, observable key-path init
- MountContext: environment field, withEnvironment, inheritance
- AnyView: _resolveDynamicProperties hook before user body eval

New tests (21 across 3 files):
- EnvironmentTests (7): scoping, nesting, projections, ForEach, lazy branches, reuse
- ObservableTests (11): acceptance case, write-back, coalescing, teardown,
  binding echo, re-arm de-dupe, retention, flush ordering
- StateTests (+3): Observation arming unit tests

Example: EnvironmentDemoPage demonstrates the feature end to end.
2026-07-30 16:53:10 -04:00

46 lines
2 KiB
Swift

/// Context threaded through the mount pipeline. Carries the ``NodeRegistry``
/// that collects reactive resources for teardown, and the ``EnvironmentValues``
/// visible to the subtree being mounted.
@_spi(Portico) @MainActor public final class MountContext {
/// The registry for the current subtree; reactive sites register here.
@_spi(Portico) public let registry: NodeRegistry
/// The environment visible to this subtree.
///
/// Containers pass their context to children unchanged, so a context
/// produced by ``withEnvironment(_:)`` is seen by exactly the subtree the
/// `.environment(...)` modifier wraps - that is what makes injection
/// subtree-scoped without any extra plumbing in containers.
@_spi(Portico) public let environment: EnvironmentValues
/// Creates a root context with a fresh registry and an empty environment.
@_spi(Portico) public init() {
self.registry = NodeRegistry()
self.environment = EnvironmentValues()
}
private init(registry: NodeRegistry, environment: EnvironmentValues) {
self.registry = registry
self.environment = environment
}
/// Returns a child context whose registry is an independently-torn-down
/// child of this context's registry. Used per `ForEach` row so removing a
/// row releases only that row's resources. The environment is inherited.
@_spi(Portico) public func makeChild() -> MountContext {
let child = NodeRegistry()
registry.addChild(child)
return MountContext(registry: child, environment: environment)
}
/// Returns a context sharing this registry but carrying a derived
/// environment. Teardown scope is deliberately unchanged: injecting a value
/// does not create an independently-unmountable subtree.
@_spi(Portico) public func withEnvironment(
_ transform: (inout EnvironmentValues) -> Void
) -> MountContext {
var derived = environment
transform(&derived)
return MountContext(registry: registry, environment: derived)
}
}