mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
BREAKING CHANGE: Package name changed due to name collision with existing rensbreur/SwiftTUI package. Changes: - Rename package from SwiftTUI to TUIKit in Package.swift - Rename Sources/SwiftTUI to Sources/TUIKit - Rename Sources/SwiftTUIExample to Sources/TUIKitExample - Rename Tests/SwiftTUITests to Tests/TUIKitTests - Rename SwiftTUI.swift to TUIKit.swift - Update all imports: import SwiftTUI -> import TUIKit - Update all code references: SwiftTUI.renderToBuffer -> TUIKit.renderToBuffer - Update documentation comments - Rename swiftTUIVersion to tuiKitVersion All 181 tests passing.
178 lines
4.3 KiB
Swift
178 lines
4.3 KiB
Swift
//
|
|
// State.swift
|
|
// TUIKit
|
|
//
|
|
// State management for TUIKit views.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
// MARK: - App State
|
|
|
|
/// Global application state that triggers re-renders when modified.
|
|
///
|
|
/// Since TUIKit runs in a single-threaded event loop, we use a simple
|
|
/// observable pattern. The AppRunner subscribes to state changes and
|
|
/// re-renders when notified.
|
|
public final class AppState: @unchecked Sendable {
|
|
/// The shared app state instance.
|
|
public static let shared = AppState()
|
|
|
|
/// Callbacks to invoke when state changes.
|
|
private var observers: [() -> Void] = []
|
|
|
|
/// Whether state has changed since last render.
|
|
private(set) var needsRender = false
|
|
|
|
private init() {}
|
|
|
|
/// Registers an observer to be notified of state changes.
|
|
///
|
|
/// - Parameter callback: The callback to invoke on state change.
|
|
internal func observe(_ callback: @escaping () -> Void) {
|
|
observers.append(callback)
|
|
}
|
|
|
|
/// Clears all observers.
|
|
internal func clearObservers() {
|
|
observers.removeAll()
|
|
}
|
|
|
|
/// Marks state as changed and notifies observers.
|
|
public func setNeedsRender() {
|
|
needsRender = true
|
|
for observer in observers {
|
|
observer()
|
|
}
|
|
}
|
|
|
|
/// Resets the needs render flag.
|
|
internal func didRender() {
|
|
needsRender = false
|
|
}
|
|
}
|
|
|
|
// MARK: - Binding
|
|
|
|
/// A two-way connection to a mutable value.
|
|
///
|
|
/// `Binding` provides read and write access to a value owned elsewhere.
|
|
/// Use bindings to connect interactive views to state.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```swift
|
|
/// struct ContentView: View {
|
|
/// @State var selectedIndex = 0
|
|
///
|
|
/// var body: some View {
|
|
/// Menu(items: menuItems, selection: $selectedIndex)
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
@propertyWrapper
|
|
public struct Binding<Value> {
|
|
/// The getter for the value.
|
|
private let getValue: () -> Value
|
|
|
|
/// The setter for the value.
|
|
private let setValue: (Value) -> Void
|
|
|
|
/// The current value.
|
|
public var wrappedValue: Value {
|
|
get { getValue() }
|
|
nonmutating set { setValue(newValue) }
|
|
}
|
|
|
|
/// The binding itself (for projectedValue access).
|
|
public var projectedValue: Binding<Value> {
|
|
self
|
|
}
|
|
|
|
/// Creates a binding with custom getter and setter.
|
|
///
|
|
/// - Parameters:
|
|
/// - get: The getter closure.
|
|
/// - set: The setter closure.
|
|
public init(get: @escaping () -> Value, set: @escaping (Value) -> Void) {
|
|
self.getValue = get
|
|
self.setValue = set
|
|
}
|
|
|
|
/// Creates a constant binding that never changes.
|
|
///
|
|
/// - Parameter value: The constant value.
|
|
/// - Returns: A binding that always returns the given value.
|
|
public static func constant(_ value: Value) -> Binding<Value> {
|
|
Binding(get: { value }, set: { _ in })
|
|
}
|
|
}
|
|
|
|
// MARK: - State Property Wrapper
|
|
|
|
/// A property wrapper that stores mutable state for a view.
|
|
///
|
|
/// When the value changes, the view hierarchy is re-rendered.
|
|
/// Use `@State` for simple value types owned by a single view.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```swift
|
|
/// struct CounterView: View {
|
|
/// @State var count = 0
|
|
///
|
|
/// var body: some View {
|
|
/// VStack {
|
|
/// Text("Count: \(count)")
|
|
/// // When count changes, view re-renders
|
|
/// }
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// # Accessing the Binding
|
|
///
|
|
/// Use the `$` prefix to get a `Binding` to the state:
|
|
///
|
|
/// ```swift
|
|
/// Menu(selection: $selectedIndex)
|
|
/// ```
|
|
@propertyWrapper
|
|
public struct State<Value> {
|
|
/// The storage for the state value.
|
|
private final class Storage {
|
|
var value: Value {
|
|
didSet {
|
|
AppState.shared.setNeedsRender()
|
|
}
|
|
}
|
|
|
|
init(_ value: Value) {
|
|
self.value = value
|
|
}
|
|
}
|
|
|
|
private let storage: Storage
|
|
|
|
/// The current state value.
|
|
public var wrappedValue: Value {
|
|
get { storage.value }
|
|
nonmutating set { storage.value = newValue }
|
|
}
|
|
|
|
/// A binding to the state value.
|
|
public var projectedValue: Binding<Value> {
|
|
Binding(
|
|
get: { self.storage.value },
|
|
set: { self.storage.value = $0 }
|
|
)
|
|
}
|
|
|
|
/// Creates a state with an initial value.
|
|
///
|
|
/// - Parameter wrappedValue: The initial value.
|
|
public init(wrappedValue: Value) {
|
|
self.storage = Storage(wrappedValue)
|
|
}
|
|
}
|