Files
phranck b404a62731 Chore: README update, Foundation cleanup, file splitting
- Update README.md with multi-module structure, new components, macOS 14+
- Remove unnecessary import Foundation from 9 source files
- Split Focus.swift into Focus, Focusable, FocusState
- Split StatusBarItem.swift into StatusBarItem, SystemStatusBarItem, StatusBarItemBuilder
- Split ASCIIConverter.swift into ASCIIConverter, +Braille, +Dithering
- Update WHATS-NEXT.md with completed cleanup tasks
2026-02-14 16:57:16 +01:00

195 lines
5.4 KiB
Swift

// 🖥️ TUIKit — Terminal UI Kit for Swift
// PreferenceKey.swift
//
// Created by LAYERED.work
// License: MIT
// MARK: - Preference Key Protocol
/// A key for defining preference values that propagate up the view hierarchy.
///
/// Unlike Environment (which flows top-down), Preferences flow bottom-up
/// from child views to parent views.
///
/// # Example
///
/// ```swift
/// struct NavigationTitleKey: PreferenceKey {
/// static var defaultValue: String = ""
///
/// static func reduce(value: inout String, nextValue: () -> String) {
/// value = nextValue()
/// }
/// }
///
/// extension PreferenceValues {
/// var navigationTitle: String {
/// get { self[NavigationTitleKey.self] }
/// set { self[NavigationTitleKey.self] = newValue }
/// }
/// }
/// ```
public protocol PreferenceKey {
/// The type of value for this preference.
associatedtype Value
/// The default value when no preference is set.
static var defaultValue: Value { get }
/// Combines a sequence of values into a single value.
///
/// This is called when multiple children set the same preference.
/// The default implementation uses the last value.
///
/// - Parameters:
/// - value: The current accumulated value.
/// - nextValue: A closure that returns the next value to combine.
static func reduce(value: inout Value, nextValue: () -> Value)
}
// Default implementation: use the last value
extension PreferenceKey {
public static func reduce(value: inout Value, nextValue: () -> Value) {
value = nextValue()
}
}
// MARK: - Preference Values
/// A collection of preference values propagated up the view hierarchy.
public struct PreferenceValues: @unchecked Sendable {
/// Storage for preference values.
private var storage: [ObjectIdentifier: Any] = [:]
/// Creates empty preference values.
public init() {}
/// Accesses the preference value for the given key.
public subscript<K: PreferenceKey>(key: K.Type) -> K.Value {
get {
if let value = storage[ObjectIdentifier(key)] as? K.Value {
return value
}
return K.defaultValue
}
set {
storage[ObjectIdentifier(key)] = newValue
}
}
}
// MARK: - Public API
public extension PreferenceValues {
/// Merges another set of preference values into this one.
///
/// - Parameter other: The other preference values to merge.
mutating func merge(_ other: Self) {
for (key, value) in other.storage {
storage[key] = value
}
}
}
// MARK: - Preference Storage
/// Thread-local storage for collecting preferences during rendering.
public final class PreferenceStorage: @unchecked Sendable {
/// Stack of preference values for nested rendering.
private var stack: [PreferenceValues] = [PreferenceValues()]
/// Callbacks registered to receive preference changes.
private var callbacks: [ObjectIdentifier: [(Any) -> Void]] = [:]
/// Creates a new preference storage.
public init() {}
/// The current preference values.
public var current: PreferenceValues {
get { stack.last ?? PreferenceValues() }
set {
if stack.isEmpty {
stack.append(newValue)
} else {
stack[stack.count - 1] = newValue
}
}
}
}
// MARK: - Public API
public extension PreferenceStorage {
/// Pushes a new preference context.
func push() {
stack.append(PreferenceValues())
}
/// Pops the current preference context and merges into parent.
func pop() -> PreferenceValues {
guard stack.count > 1 else {
return stack.last ?? PreferenceValues()
}
let popped = stack.removeLast()
// Merge into parent
if !stack.isEmpty {
stack[stack.count - 1].merge(popped)
}
return popped
}
/// Sets a preference value.
func setValue<K: PreferenceKey>(_ value: K.Value, forKey key: K.Type) {
var currentValues = current
K.reduce(value: &currentValues[key]) { value }
current = currentValues
// Notify callbacks
let keyId = ObjectIdentifier(key)
if let keyCallbacks = callbacks[keyId] {
for callback in keyCallbacks {
callback(value)
}
}
}
/// Registers a callback for preference changes.
func onPreferenceChange<K: PreferenceKey>(
_ key: K.Type,
callback: @escaping (K.Value) -> Void
) {
let keyId = ObjectIdentifier(key)
let wrappedCallback: (Any) -> Void = { value in
if let typedValue = value as? K.Value {
callback(typedValue)
}
}
if callbacks[keyId] == nil {
callbacks[keyId] = []
}
callbacks[keyId]?.append(wrappedCallback)
}
/// Prepares preference storage for a new render pass.
///
/// Clears all accumulated callbacks and resets the value stack
/// to a single empty context. Called at the start of each frame
/// by `RenderLoop.render()` to prevent callback accumulation.
func beginRenderPass() {
callbacks.removeAll()
stack = [PreferenceValues()]
}
/// Resets all preference state.
///
/// Called once during app shutdown by `TUIContext.reset()`.
func reset() {
stack = [PreferenceValues()]
callbacks.removeAll()
}
}