portico/Sources/Example/ExampleApp.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

107 lines
3.2 KiB
Swift

import Portico
@main
struct ExampleApp: App {
var applicationId: String? { "dev.bscubed.PorticoExample" }
@State private var count = 0
@State private var name = "Portico"
@State private var darkMode = false
@State private var volume = 75.0
@State private var currentPage: UInt32 = 0
@State private var uptime = 0
var body: some Scene {
ApplicationWindow { _ in
VStack {
HeaderBar()
Carousel {
introPage
counterPage
settingsPage
AsyncDemoPage()
EnvironmentDemoPage()
}
.onPageChanged { page in currentPage = page }
Label("Uptime: \(uptime)s, Page \(currentPage + 1) of 5")
.dimmed()
.halign(.center)
.margin(8)
.task(every: .seconds(1)) { uptime += 1 }
}
}
.title("Portico Demo")
.defaultSize(width: 1080, height: 720)
}
var introPage: some View {
Clamp {
StatusPage {
VStack(spacing: 6) {
Label("Declarative GTK apps in Swift")
.title3()
.marginTop(16)
Label("A familiar SwiftUI-like API, zero GObject boilerplate")
.marginBottom(16)
}
.card()
}
.title("Welcome to Portico")
.description("Swipe to explore")
.iconName("face-smile-symbolic")
}
.hexpand(true)
.vexpand(true)
}
var counterPage: some View {
Clamp {
StatusPage {
HStack(spacing: 12) {
Button("-") { count -= 1 }
.circular()
Label("Count: \(count)")
.title1()
Button("+") { count += 1 }
.circular()
}
.halign(.center)
}
.title("Reactive Counter")
.description("@State + Binding drive targeted widget updates")
.iconName("applications-engineering-symbolic")
}
.hexpand(true)
.vexpand(true)
}
var settingsPage: some View {
Clamp {
StatusPage {
VStack(spacing: 12) {
Label("Name: \(name), Dark mode: \(darkMode), Volume: \(volume)")
.title2()
ListBox {
EntryRow()
.title("Your Name")
.text($name)
SwitchRow()
.title("Dark Mode")
.active($darkMode)
SpinRow(min: 0, max: 100, step: 5)
.title("Volume")
.value($volume)
}
.boxedList()
}
}
.title("Quick Settings")
.description("Live GObject property bindings")
.iconName("application-x-firmware-symbolic")
}
.hexpand(true)
.vexpand(true)
}
}