mirror of
https://github.com/phranck/TUIkit.git
synced 2026-05-21 09:50:35 +00:00
Refactor: Move runtime services from RenderContext to EnvironmentValues
- Add ServiceEnvironment.swift with 9 EnvironmentKeys for runtime services (stateStorage, lifecycle, keyEventDispatcher, renderCache, preferenceStorage, pulsePhase, cursorTimer, focusIndicatorColor, activeFocusSectionID) - Remove tuiContext, pulsePhase, cursorTimer, focusIndicatorColor, and activeFocusSectionID as direct RenderContext properties - Inject all services through EnvironmentValues in RenderLoop.buildEnvironment() - Add convenience RenderContext init that accepts TUIContext and auto-injects services into the environment - Simplify isolatedForBackground() to only swap environment values - Migrate ~49 access sites in ~25 source files from context.tuiContext.X and context.pulsePhase/cursorTimer to context.environment.X - Update 38 test files to use the new convenience init
This commit is contained in:
@@ -188,7 +188,9 @@ extension RenderLoop {
|
||||
let terminalHeight = terminalSize.height
|
||||
|
||||
// Create render context with environment
|
||||
let environment = buildEnvironment()
|
||||
var environment = buildEnvironment()
|
||||
environment.pulsePhase = pulsePhase
|
||||
environment.cursorTimer = cursorTimer
|
||||
invalidateCacheIfEnvironmentChanged(environment: environment)
|
||||
invalidateCacheIfPulsePhaseChanged(pulsePhase: pulsePhase)
|
||||
|
||||
@@ -216,8 +218,7 @@ extension RenderLoop {
|
||||
let measureContext = RenderContext(
|
||||
availableWidth: terminalWidth,
|
||||
availableHeight: terminalHeight - statusBarHeight,
|
||||
environment: environment,
|
||||
tuiContext: tuiContext
|
||||
environment: environment
|
||||
)
|
||||
_ = renderScene(scene, context: measureContext.withChildIdentity(type: type(of: scene)))
|
||||
appHeaderHeight = appHeader.height
|
||||
@@ -233,13 +234,10 @@ extension RenderLoop {
|
||||
var context = RenderContext(
|
||||
availableWidth: terminalWidth,
|
||||
availableHeight: contentHeight,
|
||||
environment: environment,
|
||||
tuiContext: tuiContext
|
||||
environment: environment
|
||||
)
|
||||
context.hasExplicitWidth = true // Terminal has a fixed width
|
||||
context.hasExplicitHeight = true // Terminal has a fixed height
|
||||
context.pulsePhase = pulsePhase
|
||||
context.cursorTimer = cursorTimer
|
||||
|
||||
// Render main content into a FrameBuffer.
|
||||
// app.body is evaluated fresh each frame. @State values survive
|
||||
@@ -261,13 +259,10 @@ extension RenderLoop {
|
||||
var correctedContext = RenderContext(
|
||||
availableWidth: terminalWidth,
|
||||
availableHeight: actualContentHeight,
|
||||
environment: environment,
|
||||
tuiContext: tuiContext
|
||||
environment: environment
|
||||
)
|
||||
correctedContext.hasExplicitWidth = true
|
||||
correctedContext.hasExplicitHeight = true
|
||||
correctedContext.pulsePhase = pulsePhase
|
||||
correctedContext.cursorTimer = cursorTimer
|
||||
buffer = renderScene(scene, context: correctedContext.withChildIdentity(type: type(of: scene)))
|
||||
}
|
||||
|
||||
@@ -356,6 +351,14 @@ extension RenderLoop {
|
||||
environment.appearance = appearance
|
||||
}
|
||||
environment.notificationService = NotificationService.current
|
||||
|
||||
// Runtime services (previously accessed via context.tuiContext)
|
||||
environment.stateStorage = tuiContext.stateStorage
|
||||
environment.lifecycle = tuiContext.lifecycle
|
||||
environment.keyEventDispatcher = tuiContext.keyEventDispatcher
|
||||
environment.renderCache = tuiContext.renderCache
|
||||
environment.preferenceStorage = tuiContext.preferences
|
||||
|
||||
return environment
|
||||
}
|
||||
}
|
||||
@@ -410,8 +413,7 @@ private extension RenderLoop {
|
||||
let context = RenderContext(
|
||||
availableWidth: terminalWidth,
|
||||
availableHeight: appHeader.height,
|
||||
environment: environment,
|
||||
tuiContext: tuiContext
|
||||
environment: environment
|
||||
)
|
||||
|
||||
let buffer = renderToBuffer(headerView, context: context)
|
||||
@@ -449,8 +451,7 @@ private extension RenderLoop {
|
||||
let context = RenderContext(
|
||||
availableWidth: terminalWidth,
|
||||
availableHeight: statusBarView.height,
|
||||
environment: environment,
|
||||
tuiContext: tuiContext
|
||||
environment: environment
|
||||
)
|
||||
|
||||
let buffer = renderToBuffer(statusBarView, context: context)
|
||||
|
||||
@@ -76,7 +76,7 @@ public struct EquatableView<Content: View & Equatable>: View {
|
||||
|
||||
extension EquatableView: Renderable {
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
let cache = context.tuiContext.renderCache
|
||||
let cache = context.environment.renderCache!
|
||||
let identity = context.identity
|
||||
|
||||
cache.markActive(identity)
|
||||
@@ -119,7 +119,7 @@ private extension EquatableView {
|
||||
/// Their state identities must still be marked active to prevent
|
||||
/// StateStorage from garbage-collecting them.
|
||||
func markSubtreeActive(context: RenderContext) {
|
||||
context.tuiContext.stateStorage.markActive(context.identity)
|
||||
context.environment.stateStorage!.markActive(context.identity)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ extension EmptyView: Renderable {
|
||||
|
||||
extension ConditionalView: Renderable {
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
let stateStorage = context.tuiContext.stateStorage
|
||||
let stateStorage = context.environment.stateStorage!
|
||||
switch self {
|
||||
case .trueContent(let content):
|
||||
stateStorage.invalidateDescendants(of: context.identity.branch("false"))
|
||||
|
||||
@@ -25,7 +25,7 @@ struct PreferenceModifier<Content: View, K: PreferenceKey>: View {
|
||||
extension PreferenceModifier: Renderable {
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
// Set the preference value
|
||||
context.tuiContext.preferences.setValue(value, forKey: K.self)
|
||||
context.environment.preferenceStorage!.setValue(value, forKey: K.self)
|
||||
|
||||
// Render content
|
||||
return TUIkit.renderToBuffer(content, context: context)
|
||||
@@ -50,7 +50,7 @@ where K.Value: Equatable {
|
||||
|
||||
extension OnPreferenceChangeModifier: Renderable {
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
let prefs = context.tuiContext.preferences
|
||||
let prefs = context.environment.preferenceStorage!
|
||||
|
||||
// Register callback for preference changes
|
||||
prefs.onPreferenceChange(K.self, callback: action)
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// 🖥️ TUIKit — Terminal UI Kit for Swift
|
||||
// ServiceEnvironment.swift
|
||||
//
|
||||
// Created by LAYERED.work
|
||||
// License: MIT
|
||||
|
||||
// MARK: - State Storage
|
||||
|
||||
/// EnvironmentKey for the persistent `@State` value storage.
|
||||
private struct StateStorageKey: EnvironmentKey {
|
||||
static let defaultValue: StateStorage? = nil
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle Manager
|
||||
|
||||
/// EnvironmentKey for view lifecycle tracking (appear/disappear/task).
|
||||
private struct LifecycleKey: EnvironmentKey {
|
||||
static let defaultValue: LifecycleManager? = nil
|
||||
}
|
||||
|
||||
// MARK: - Key Event Dispatcher
|
||||
|
||||
/// EnvironmentKey for key event handler registration and dispatch.
|
||||
private struct KeyEventDispatcherKey: EnvironmentKey {
|
||||
static let defaultValue: KeyEventDispatcher? = nil
|
||||
}
|
||||
|
||||
// MARK: - Render Cache
|
||||
|
||||
/// EnvironmentKey for memoized subtree rendering results.
|
||||
private struct RenderCacheKey: EnvironmentKey {
|
||||
static let defaultValue: RenderCache? = nil
|
||||
}
|
||||
|
||||
// MARK: - Preference Storage
|
||||
|
||||
/// EnvironmentKey for preference value collection during rendering.
|
||||
private struct PreferenceStorageKey: EnvironmentKey {
|
||||
static let defaultValue: PreferenceStorage? = nil
|
||||
}
|
||||
|
||||
// MARK: - Pulse Phase
|
||||
|
||||
/// EnvironmentKey for the focus indicator breathing animation phase.
|
||||
private struct PulsePhaseKey: EnvironmentKey {
|
||||
static let defaultValue: Double = 0
|
||||
}
|
||||
|
||||
// MARK: - Cursor Timer
|
||||
|
||||
/// EnvironmentKey for TextField/SecureField cursor blink animation.
|
||||
private struct CursorTimerKey: EnvironmentKey {
|
||||
nonisolated(unsafe) static let defaultValue: CursorTimer? = nil
|
||||
}
|
||||
|
||||
// MARK: - Focus Indicator Color
|
||||
|
||||
/// EnvironmentKey for the focus indicator color in the current subtree.
|
||||
private struct FocusIndicatorColorKey: EnvironmentKey {
|
||||
static let defaultValue: Color? = nil
|
||||
}
|
||||
|
||||
// MARK: - Active Focus Section
|
||||
|
||||
/// EnvironmentKey for the focus section that child views should register in.
|
||||
private struct ActiveFocusSectionKey: EnvironmentKey {
|
||||
static let defaultValue: String? = nil
|
||||
}
|
||||
|
||||
// MARK: - EnvironmentValues Extensions
|
||||
|
||||
extension EnvironmentValues {
|
||||
|
||||
/// The persistent `@State` value storage indexed by `ViewIdentity`.
|
||||
var stateStorage: StateStorage? {
|
||||
get { self[StateStorageKey.self] }
|
||||
set { self[StateStorageKey.self] = newValue }
|
||||
}
|
||||
|
||||
/// View lifecycle tracking (appear, disappear, task management).
|
||||
var lifecycle: LifecycleManager? {
|
||||
get { self[LifecycleKey.self] }
|
||||
set { self[LifecycleKey.self] = newValue }
|
||||
}
|
||||
|
||||
/// Key event handler registration and dispatch.
|
||||
var keyEventDispatcher: KeyEventDispatcher? {
|
||||
get { self[KeyEventDispatcherKey.self] }
|
||||
set { self[KeyEventDispatcherKey.self] = newValue }
|
||||
}
|
||||
|
||||
/// Cache for memoized subtree rendering results.
|
||||
var renderCache: RenderCache? {
|
||||
get { self[RenderCacheKey.self] }
|
||||
set { self[RenderCacheKey.self] = newValue }
|
||||
}
|
||||
|
||||
/// Preference value collection during rendering.
|
||||
var preferenceStorage: PreferenceStorage? {
|
||||
get { self[PreferenceStorageKey.self] }
|
||||
set { self[PreferenceStorageKey.self] = newValue }
|
||||
}
|
||||
|
||||
/// The current breathing animation phase (0-1) for the focus indicator.
|
||||
var pulsePhase: Double {
|
||||
get { self[PulsePhaseKey.self] }
|
||||
set { self[PulsePhaseKey.self] = newValue }
|
||||
}
|
||||
|
||||
/// The cursor timer for TextField/SecureField animations.
|
||||
var cursorTimer: CursorTimer? {
|
||||
get { self[CursorTimerKey.self] }
|
||||
set { self[CursorTimerKey.self] = newValue }
|
||||
}
|
||||
|
||||
/// The focus indicator color for the first border encountered in this subtree.
|
||||
var focusIndicatorColor: Color? {
|
||||
get { self[FocusIndicatorColorKey.self] }
|
||||
set { self[FocusIndicatorColorKey.self] = newValue }
|
||||
}
|
||||
|
||||
/// The ID of the focus section that child views should register in.
|
||||
var activeFocusSectionID: String? {
|
||||
get { self[ActiveFocusSectionKey.self] }
|
||||
set { self[ActiveFocusSectionKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
@@ -190,12 +190,12 @@ extension LifecycleManager {
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// View modifiers access the context through `RenderContext`:
|
||||
/// View modifiers access services through `RenderContext.environment`:
|
||||
///
|
||||
/// ```swift
|
||||
/// extension MyModifier: Renderable {
|
||||
/// func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
/// context.tuiContext.keyEventDispatcher.addHandler { event in
|
||||
/// context.environment.keyEventDispatcher!.addHandler { event in
|
||||
/// // handle key
|
||||
/// }
|
||||
/// return TUIkit.renderToBuffer(content, context: context)
|
||||
|
||||
@@ -89,7 +89,7 @@ struct FocusRegistration {
|
||||
defaultPrefix: String,
|
||||
propertyIndex: Int
|
||||
) -> String {
|
||||
let stateStorage = context.tuiContext.stateStorage
|
||||
let stateStorage = context.environment.stateStorage!
|
||||
let defaultID = explicitFocusID ?? "\(defaultPrefix)-\(context.identity.path)"
|
||||
let key = StateStorage.StateKey(identity: context.identity, propertyIndex: propertyIndex)
|
||||
let box: StateBox<String> = stateStorage.storage(for: key, default: defaultID)
|
||||
@@ -105,8 +105,8 @@ struct FocusRegistration {
|
||||
/// - handler: The focusable handler to register.
|
||||
static func register(context: RenderContext, handler: Focusable) {
|
||||
guard !context.isMeasuring else { return }
|
||||
context.environment.focusManager.register(handler, inSection: context.activeFocusSectionID)
|
||||
context.tuiContext.stateStorage.markActive(context.identity)
|
||||
context.environment.focusManager.register(handler, inSection: context.environment.activeFocusSectionID)
|
||||
context.environment.stateStorage!.markActive(context.identity)
|
||||
}
|
||||
|
||||
/// Determines whether the given focusID currently has focus.
|
||||
|
||||
@@ -105,7 +105,7 @@ extension AlertPresentationModifier: Renderable {
|
||||
|
||||
// Register ESC handler to dismiss the alert
|
||||
let isPresentedBinding = isPresented
|
||||
context.tuiContext.keyEventDispatcher.addHandler { event in
|
||||
context.environment.keyEventDispatcher!.addHandler { event in
|
||||
if event.key == .escape {
|
||||
isPresentedBinding.wrappedValue = false
|
||||
return true
|
||||
@@ -116,7 +116,7 @@ extension AlertPresentationModifier: Renderable {
|
||||
// Set the alert section in the context so child focusables
|
||||
// (buttons in the alert) register in the alert section.
|
||||
var alertContext = context
|
||||
alertContext.activeFocusSectionID = sectionID
|
||||
alertContext.environment.activeFocusSectionID = sectionID
|
||||
|
||||
let alertBuffer = TUIkit.renderToBuffer(alert, context: alertContext)
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ extension FocusSectionModifier: Renderable {
|
||||
// Create a child context with the active section ID set,
|
||||
// so that focusable children (buttons, menus) register in this section.
|
||||
var sectionContext = context
|
||||
sectionContext.activeFocusSectionID = sectionID
|
||||
sectionContext.environment.activeFocusSectionID = sectionID
|
||||
|
||||
// If this section is active, compute the breathing indicator color.
|
||||
// The first border view in the subtree will consume this and render ●.
|
||||
@@ -59,9 +59,9 @@ extension FocusSectionModifier: Renderable {
|
||||
if !context.isMeasuring && focusManager.isActiveSection(sectionID) {
|
||||
let accentColor = context.environment.palette.accent
|
||||
let dimColor = accentColor.opacity(ViewConstants.focusBorderDim)
|
||||
sectionContext.focusIndicatorColor = Color.lerp(dimColor, accentColor, phase: context.pulsePhase)
|
||||
sectionContext.environment.focusIndicatorColor = Color.lerp(dimColor, accentColor, phase: context.environment.pulsePhase)
|
||||
} else {
|
||||
sectionContext.focusIndicatorColor = nil
|
||||
sectionContext.environment.focusIndicatorColor = nil
|
||||
}
|
||||
|
||||
return TUIkit.renderToBuffer(content, context: sectionContext)
|
||||
|
||||
@@ -29,7 +29,7 @@ public struct KeyPressModifier<Content: View>: View {
|
||||
extension KeyPressModifier: Renderable {
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
// Register the key handler
|
||||
context.tuiContext.keyEventDispatcher.addHandler { [keys, handler] event in
|
||||
context.environment.keyEventDispatcher!.addHandler { [keys, handler] event in
|
||||
// Check if we should handle this key
|
||||
if let allowedKeys = keys {
|
||||
guard allowedKeys.contains(event.key) else {
|
||||
|
||||
@@ -26,7 +26,7 @@ struct OnAppearModifier<Content: View>: View {
|
||||
extension OnAppearModifier: Renderable {
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
// Record appearance and execute action if first time
|
||||
_ = context.tuiContext.lifecycle.recordAppear(token: token, action: action)
|
||||
_ = context.environment.lifecycle!.recordAppear(token: token, action: action)
|
||||
|
||||
// Render content
|
||||
return TUIkit.renderToBuffer(content, context: context)
|
||||
@@ -54,10 +54,10 @@ struct OnDisappearModifier<Content: View>: View {
|
||||
extension OnDisappearModifier: Renderable {
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
// Register the disappear callback
|
||||
context.tuiContext.lifecycle.registerDisappear(token: token, action: action)
|
||||
context.environment.lifecycle!.registerDisappear(token: token, action: action)
|
||||
|
||||
// Mark as visible in current render
|
||||
_ = context.tuiContext.lifecycle.recordAppear(token: token, action: {})
|
||||
_ = context.environment.lifecycle!.recordAppear(token: token, action: {})
|
||||
|
||||
// Render content
|
||||
return TUIkit.renderToBuffer(content, context: context)
|
||||
@@ -89,7 +89,7 @@ struct TaskModifier<Content: View>: View {
|
||||
|
||||
extension TaskModifier: Renderable {
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
let lifecycle = context.tuiContext.lifecycle
|
||||
let lifecycle = context.environment.lifecycle!
|
||||
|
||||
// Start task on first appearance
|
||||
let isFirstAppear = !lifecycle.hasAppeared(token: token)
|
||||
|
||||
@@ -73,7 +73,7 @@ extension ModalPresentationModifier: Renderable {
|
||||
// Set the modal section in the context so child focusables
|
||||
// (buttons in the modal) register in the modal section.
|
||||
var modalContext = context
|
||||
modalContext.activeFocusSectionID = sectionID
|
||||
modalContext.environment.activeFocusSectionID = sectionID
|
||||
|
||||
let modalBuffer = TUIkit.renderToBuffer(modal, context: modalContext)
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ struct NavigationSplitViewColumnWidthView<Content: View>: View {
|
||||
extension NavigationSplitViewColumnWidthView: Renderable {
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
// Set the preference for NavigationSplitView to read
|
||||
context.tuiContext.preferences.setValue(columnWidth, forKey: NavigationSplitViewColumnWidthKey.self)
|
||||
context.environment.preferenceStorage!.setValue(columnWidth, forKey: NavigationSplitViewColumnWidthKey.self)
|
||||
|
||||
// Render content
|
||||
return TUIkit.renderToBuffer(content, context: context)
|
||||
|
||||
@@ -65,7 +65,7 @@ extension StatusBarItemsModifier: Renderable {
|
||||
// Register items with the focus section's composition strategy.
|
||||
// If inside a focus section, items are associated with that section.
|
||||
// Otherwise, they become global items.
|
||||
if let sectionID = renderContext.activeFocusSectionID {
|
||||
if let sectionID = renderContext.environment.activeFocusSectionID {
|
||||
statusBar.registerSectionItems(
|
||||
sectionID: sectionID,
|
||||
items: items,
|
||||
|
||||
@@ -54,7 +54,7 @@ extension NotificationHostModifier: Renderable {
|
||||
// Start the animation timer if not already running.
|
||||
startAnimationTask(
|
||||
entries: activeEntries,
|
||||
lifecycle: context.tuiContext.lifecycle
|
||||
lifecycle: context.environment.lifecycle!
|
||||
)
|
||||
|
||||
let now = Date().timeIntervalSinceReferenceDate
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
|
||||
/// The context for rendering a view.
|
||||
///
|
||||
/// Contains layout constraints, environment values, and the central
|
||||
/// `TUIContext` that views need to determine their size, content, and
|
||||
/// access framework services.
|
||||
/// Contains layout constraints, environment values, and the view's
|
||||
/// structural identity. Runtime services (state storage, lifecycle,
|
||||
/// key dispatch, etc.) are accessed through ``environment`` using
|
||||
/// `EnvironmentKey`-based properties.
|
||||
///
|
||||
/// `RenderContext` is a pure data container — it does not hold a reference
|
||||
/// to `Terminal`. All terminal I/O happens in `RenderLoop` after the
|
||||
@@ -27,14 +28,6 @@ public struct RenderContext {
|
||||
/// The environment values for this render pass.
|
||||
public var environment: EnvironmentValues
|
||||
|
||||
/// The central dependency container for framework services.
|
||||
///
|
||||
/// Provides access to lifecycle tracking, key event dispatch,
|
||||
/// and preference storage via constructor injection.
|
||||
/// Mutable to allow modal presentation to substitute an isolated
|
||||
/// context for background content rendering.
|
||||
var tuiContext: TUIContext
|
||||
|
||||
/// The current view's structural identity in the render tree.
|
||||
///
|
||||
/// Built incrementally as `renderToBuffer` traverses the view hierarchy.
|
||||
@@ -42,34 +35,6 @@ public struct RenderContext {
|
||||
/// Used by `StateStorage` to persist `@State` values across render passes.
|
||||
var identity: ViewIdentity
|
||||
|
||||
/// The ID of the focus section that child views should register in.
|
||||
///
|
||||
/// Set by `FocusSectionModifier` during rendering. Focusable children
|
||||
/// (buttons, menus) read this to register in the correct section.
|
||||
/// When nil, elements register in the active or default section.
|
||||
var activeFocusSectionID: String?
|
||||
|
||||
/// The current breathing animation phase (0–1) for the focus indicator.
|
||||
///
|
||||
/// Set by `RenderLoop` from the `PulseTimer` at the start of each frame.
|
||||
/// Read by `BorderRenderer` to interpolate the ● indicator color.
|
||||
/// A value of 0 means dimmest, 1 means brightest.
|
||||
var pulsePhase: Double = 0
|
||||
|
||||
/// The cursor timer for TextField/SecureField animations.
|
||||
///
|
||||
/// Set by `RenderLoop` at the start of each frame.
|
||||
/// Read by text fields to compute blink and pulse phases.
|
||||
var cursorTimer: CursorTimer?
|
||||
|
||||
/// The focus indicator color for the first border encountered in this subtree.
|
||||
///
|
||||
/// Set by `FocusSectionModifier` when the section is active.
|
||||
/// The first view that renders a border (Panel, Box, `.border()`) reads
|
||||
/// this color, renders the ● indicator, and sets it to nil so that
|
||||
/// nested borders don't also show the indicator.
|
||||
var focusIndicatorColor: Color?
|
||||
|
||||
/// Whether an explicit frame width constraint has been set.
|
||||
///
|
||||
/// Set by `FlexibleFrameView` when a fixed width is specified.
|
||||
@@ -96,19 +61,46 @@ public struct RenderContext {
|
||||
/// - availableWidth: The available width in characters.
|
||||
/// - availableHeight: The available height in lines.
|
||||
/// - environment: The environment values (defaults to empty).
|
||||
/// - tuiContext: The TUI context (defaults to a fresh instance).
|
||||
/// - identity: The view identity path (defaults to root).
|
||||
init(
|
||||
availableWidth: Int,
|
||||
availableHeight: Int,
|
||||
environment: EnvironmentValues = EnvironmentValues(),
|
||||
tuiContext: TUIContext = TUIContext(),
|
||||
identity: ViewIdentity = ViewIdentity(path: "")
|
||||
) {
|
||||
self.availableWidth = availableWidth
|
||||
self.availableHeight = availableHeight
|
||||
self.environment = environment
|
||||
self.tuiContext = tuiContext
|
||||
self.identity = identity
|
||||
}
|
||||
|
||||
/// Creates a new RenderContext with runtime services from a `TUIContext`.
|
||||
///
|
||||
/// Injects all services from the `TUIContext` into `EnvironmentValues`,
|
||||
/// making them accessible via `context.environment.stateStorage`, etc.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - availableWidth: The available width in characters.
|
||||
/// - availableHeight: The available height in lines.
|
||||
/// - environment: The environment values (defaults to empty).
|
||||
/// - tuiContext: The TUI context whose services are injected into the environment.
|
||||
/// - identity: The view identity path (defaults to root).
|
||||
init(
|
||||
availableWidth: Int,
|
||||
availableHeight: Int,
|
||||
environment: EnvironmentValues = EnvironmentValues(),
|
||||
tuiContext: TUIContext,
|
||||
identity: ViewIdentity = ViewIdentity(path: "")
|
||||
) {
|
||||
var env = environment
|
||||
env.stateStorage = tuiContext.stateStorage
|
||||
env.lifecycle = tuiContext.lifecycle
|
||||
env.keyEventDispatcher = tuiContext.keyEventDispatcher
|
||||
env.renderCache = tuiContext.renderCache
|
||||
env.preferenceStorage = tuiContext.preferences
|
||||
self.availableWidth = availableWidth
|
||||
self.availableHeight = availableHeight
|
||||
self.environment = env
|
||||
self.identity = identity
|
||||
}
|
||||
|
||||
@@ -172,12 +164,7 @@ public struct RenderContext {
|
||||
func isolatedForBackground() -> Self {
|
||||
var copy = self
|
||||
copy.environment.focusManager = FocusManager()
|
||||
copy.tuiContext = TUIContext(
|
||||
lifecycle: tuiContext.lifecycle,
|
||||
keyEventDispatcher: KeyEventDispatcher(),
|
||||
preferences: tuiContext.preferences,
|
||||
stateStorage: tuiContext.stateStorage
|
||||
)
|
||||
copy.environment.keyEventDispatcher = KeyEventDispatcher()
|
||||
return copy
|
||||
}
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ func renderToBuffer<V: View>(_ view: V, context: RenderContext) -> FrameBuffer {
|
||||
// Activate hydration: @State.init will use this to look up persistent storage.
|
||||
StateRegistration.activeContext = HydrationContext(
|
||||
identity: context.identity,
|
||||
storage: context.tuiContext.stateStorage
|
||||
storage: context.environment.stateStorage!
|
||||
)
|
||||
StateRegistration.counter = 0
|
||||
|
||||
@@ -192,7 +192,7 @@ func renderToBuffer<V: View>(_ view: V, context: RenderContext) -> FrameBuffer {
|
||||
// Restore previous hydration state and mark this identity as active for GC.
|
||||
StateRegistration.activeContext = previousContext
|
||||
StateRegistration.counter = previousCounter
|
||||
context.tuiContext.stateStorage.markActive(context.identity)
|
||||
context.environment.stateStorage!.markActive(context.identity)
|
||||
|
||||
return renderToBuffer(body, context: childContext)
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ private struct _ButtonCore: View, Renderable {
|
||||
// Plain: pulsing dot prefix + label, no brackets
|
||||
let focusPrefix = BorderRenderer.focusIndicatorPrefix(
|
||||
isFocused: isFocused && !isDisabled,
|
||||
pulsePhase: context.pulsePhase,
|
||||
pulsePhase: context.environment.pulsePhase,
|
||||
palette: palette
|
||||
)
|
||||
let styledLabel = ANSIRenderer.render(paddedLabel, with: textStyle)
|
||||
@@ -340,7 +340,7 @@ private struct _ButtonCore: View, Renderable {
|
||||
if isDisabled {
|
||||
resolvedCapColor = buttonBg
|
||||
} else if isFocused {
|
||||
resolvedCapColor = Color.lerp(buttonBg, palette.accent.opacity(ViewConstants.buttonCapPulseBright), phase: context.pulsePhase)
|
||||
resolvedCapColor = Color.lerp(buttonBg, palette.accent.opacity(ViewConstants.buttonCapPulseBright), phase: context.environment.pulsePhase)
|
||||
} else {
|
||||
resolvedCapColor = buttonBg
|
||||
}
|
||||
|
||||
@@ -325,8 +325,8 @@ private struct _ContainerViewCore<Content: View, Footer: View>: View, Renderable
|
||||
var innerContext = context.forBorderedContent()
|
||||
|
||||
// Consume focus indicator so nested containers don't also show it.
|
||||
let indicatorColor = context.focusIndicatorColor
|
||||
innerContext.focusIndicatorColor = nil
|
||||
let indicatorColor = context.environment.focusIndicatorColor
|
||||
innerContext.environment.focusIndicatorColor = nil
|
||||
|
||||
// Render body content first to determine its natural width.
|
||||
let paddedContent = content.padding(padding)
|
||||
|
||||
@@ -294,7 +294,7 @@ private struct _MenuCore: View, Renderable {
|
||||
let menuItems = items
|
||||
let selectCallback = onSelect
|
||||
|
||||
context.tuiContext.keyEventDispatcher.addHandler { event in
|
||||
context.environment.keyEventDispatcher!.addHandler { event in
|
||||
switch event.key {
|
||||
case .up:
|
||||
// Move selection up
|
||||
|
||||
@@ -251,15 +251,15 @@ private struct _NavigationSplitViewCore<Sidebar: View, Content: View, Detail: Vi
|
||||
|
||||
// Create a context with the active focus section
|
||||
var sectionContext = columnContext
|
||||
sectionContext.activeFocusSectionID = sectionID
|
||||
sectionContext.environment.activeFocusSectionID = sectionID
|
||||
|
||||
// If this section is active, set the focus indicator color for borders (never active during measurement)
|
||||
if !columnContext.isMeasuring && focusManager.isActiveSection(sectionID) {
|
||||
let accentColor = context.environment.palette.accent
|
||||
let dimColor = accentColor.opacity(ViewConstants.focusBorderDim)
|
||||
sectionContext.focusIndicatorColor = Color.lerp(dimColor, accentColor, phase: context.pulsePhase)
|
||||
sectionContext.environment.focusIndicatorColor = Color.lerp(dimColor, accentColor, phase: context.environment.pulsePhase)
|
||||
} else {
|
||||
sectionContext.focusIndicatorColor = nil
|
||||
sectionContext.environment.focusIndicatorColor = nil
|
||||
}
|
||||
|
||||
let buffer = renderColumn(column, context: sectionContext)
|
||||
|
||||
@@ -207,7 +207,7 @@ private struct _RadioButtonGroupCore<Value: Hashable>: View, Renderable {
|
||||
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
let palette = context.environment.palette
|
||||
let stateStorage = context.tuiContext.stateStorage
|
||||
let stateStorage = context.environment.stateStorage!
|
||||
|
||||
// Create type-erased selection binding and item values
|
||||
let erasedSelection = Binding<AnyHashable>(
|
||||
@@ -323,7 +323,7 @@ private struct _RadioButtonGroupCore<Value: Hashable>: View, Renderable {
|
||||
} else if isFocused {
|
||||
// Focused: pulsing accent (whether selected or not)
|
||||
let dimAccent = palette.accent.opacity(ViewConstants.focusPulseMin)
|
||||
indicatorColor = Color.lerp(dimAccent, palette.accent, phase: context.pulsePhase)
|
||||
indicatorColor = Color.lerp(dimAccent, palette.accent, phase: context.environment.pulsePhase)
|
||||
} else if isSelected {
|
||||
// Selected but not focused: solid accent
|
||||
indicatorColor = palette.accent
|
||||
|
||||
@@ -244,7 +244,7 @@ private struct _SecureFieldCore: View, Renderable, Layoutable {
|
||||
}
|
||||
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
let stateStorage = context.tuiContext.stateStorage
|
||||
let stateStorage = context.environment.stateStorage!
|
||||
let palette = context.environment.palette
|
||||
let cursorStyle = context.environment.textCursorStyle
|
||||
|
||||
@@ -295,7 +295,7 @@ private struct _SecureFieldCore: View, Renderable, Layoutable {
|
||||
isFocused: isFocused,
|
||||
palette: palette,
|
||||
cursorStyle: cursorStyle,
|
||||
cursorTimer: context.cursorTimer,
|
||||
cursorTimer: context.environment.cursorTimer,
|
||||
contentWidth: contentWidth
|
||||
)
|
||||
|
||||
|
||||
@@ -285,7 +285,7 @@ private struct _SliderCore<Label: View, ValueLabel: View>: View, Renderable, Lay
|
||||
}
|
||||
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
let stateStorage = context.tuiContext.stateStorage
|
||||
let stateStorage = context.environment.stateStorage!
|
||||
let palette = context.environment.palette
|
||||
|
||||
// Slider expands to fill available width (with minimum)
|
||||
@@ -330,7 +330,7 @@ private struct _SliderCore<Label: View, ValueLabel: View>: View, Renderable, Lay
|
||||
fraction: fraction,
|
||||
isFocused: isFocused,
|
||||
palette: palette,
|
||||
pulsePhase: context.pulsePhase,
|
||||
pulsePhase: context.environment.pulsePhase,
|
||||
trackWidth: trackWidth
|
||||
)
|
||||
|
||||
|
||||
@@ -270,8 +270,8 @@ private struct _SpinnerCore: View, Renderable {
|
||||
}
|
||||
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
let lifecycle = context.tuiContext.lifecycle
|
||||
let stateStorage = context.tuiContext.stateStorage
|
||||
let lifecycle = context.environment.lifecycle!
|
||||
let stateStorage = context.environment.stateStorage!
|
||||
|
||||
// Retrieve or create persistent start time for this spinner.
|
||||
let timeKey = StateStorage.StateKey(identity: context.identity, propertyIndex: 0)
|
||||
|
||||
@@ -304,7 +304,7 @@ private struct _StepperCore<Label: View>: View, Renderable {
|
||||
}
|
||||
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
let stateStorage = context.tuiContext.stateStorage
|
||||
let stateStorage = context.environment.stateStorage!
|
||||
let palette = context.environment.palette
|
||||
|
||||
let persistedFocusID = FocusRegistration.persistFocusID(
|
||||
@@ -343,7 +343,7 @@ private struct _StepperCore<Label: View>: View, Renderable {
|
||||
let content = buildContent(
|
||||
isFocused: isFocused,
|
||||
palette: palette,
|
||||
pulsePhase: context.pulsePhase
|
||||
pulsePhase: context.environment.pulsePhase
|
||||
)
|
||||
|
||||
return FrameBuffer(text: content)
|
||||
|
||||
@@ -191,7 +191,7 @@ private struct _TableCore<Value: Identifiable & Sendable>: View, Renderable wher
|
||||
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
let palette = context.environment.palette
|
||||
let stateStorage = context.tuiContext.stateStorage
|
||||
let stateStorage = context.environment.stateStorage!
|
||||
|
||||
// Calculate available width inside container (subtract border + padding)
|
||||
let innerWidth = max(0, context.availableWidth - 4)
|
||||
@@ -410,7 +410,7 @@ private struct _TableCore<Value: Identifiable & Sendable>: View, Renderable wher
|
||||
) -> (indicator: String, indicatorColor: Color, backgroundColor: Color?) {
|
||||
if isFocused && isSelected {
|
||||
let dimAccent = palette.accent.opacity(ViewConstants.focusPulseMin)
|
||||
let bg = Color.lerp(dimAccent, palette.accent.opacity(ViewConstants.focusPulseMax), phase: context.pulsePhase)
|
||||
let bg = Color.lerp(dimAccent, palette.accent.opacity(ViewConstants.focusPulseMax), phase: context.environment.pulsePhase)
|
||||
return ("●", palette.accent, bg)
|
||||
} else if isFocused {
|
||||
return (" ", palette.foregroundTertiary, palette.focusBackground)
|
||||
|
||||
@@ -253,7 +253,7 @@ private struct _TextFieldCore<Label: View>: View, Renderable, Layoutable {
|
||||
}
|
||||
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
let stateStorage = context.tuiContext.stateStorage
|
||||
let stateStorage = context.environment.stateStorage!
|
||||
let palette = context.environment.palette
|
||||
let cursorStyle = context.environment.textCursorStyle
|
||||
|
||||
@@ -306,7 +306,7 @@ private struct _TextFieldCore<Label: View>: View, Renderable, Layoutable {
|
||||
isFocused: isFocused,
|
||||
palette: palette,
|
||||
cursorStyle: cursorStyle,
|
||||
cursorTimer: context.cursorTimer,
|
||||
cursorTimer: context.environment.cursorTimer,
|
||||
contentWidth: contentWidth
|
||||
)
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ private struct _ToggleCore<Label: View>: View, Renderable {
|
||||
bracketColor = palette.foregroundTertiary.opacity(ViewConstants.disabledForeground)
|
||||
} else if isFocused {
|
||||
let dimAccent = palette.accent.opacity(ViewConstants.focusPulseMin)
|
||||
bracketColor = Color.lerp(dimAccent, palette.accent, phase: context.pulsePhase)
|
||||
bracketColor = Color.lerp(dimAccent, palette.accent, phase: context.environment.pulsePhase)
|
||||
} else {
|
||||
bracketColor = palette.foregroundTertiary.opacity(ViewConstants.disabledForeground)
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ struct _ImageCore: View, Renderable, Layoutable {
|
||||
// MARK: - Renderable
|
||||
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
let stateStorage = context.tuiContext.stateStorage
|
||||
let lifecycle = context.tuiContext.lifecycle
|
||||
let stateStorage = context.environment.stateStorage!
|
||||
let lifecycle = context.environment.lifecycle!
|
||||
let identity = context.identity
|
||||
|
||||
let width = context.availableWidth
|
||||
|
||||
@@ -28,7 +28,7 @@ struct _ListCore<SelectionValue: Hashable & Sendable, Content: View, Footer: Vie
|
||||
func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||||
let palette = context.environment.palette
|
||||
let style = context.environment.listStyle
|
||||
let stateStorage = context.tuiContext.stateStorage
|
||||
let stateStorage = context.environment.stateStorage!
|
||||
|
||||
// Extract rows from content
|
||||
let rows = extractRows(from: content, context: context)
|
||||
@@ -367,7 +367,7 @@ struct _ListCore<SelectionValue: Hashable & Sendable, Content: View, Footer: Vie
|
||||
case .content:
|
||||
if isFocused && isSelected {
|
||||
let dimAccent = palette.accent.opacity(ViewConstants.focusPulseMin)
|
||||
return Color.lerp(dimAccent, palette.accent.opacity(ViewConstants.focusPulseMax), phase: context.pulsePhase)
|
||||
return Color.lerp(dimAccent, palette.accent.opacity(ViewConstants.focusPulseMax), phase: context.environment.pulsePhase)
|
||||
} else if isFocused {
|
||||
return palette.focusBackground
|
||||
} else if isSelected {
|
||||
|
||||
@@ -12,7 +12,7 @@ import Testing
|
||||
|
||||
/// Creates a default render context for testing.
|
||||
private func testContext(width: Int = 40, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
// MARK: - BackgroundModifier Tests
|
||||
|
||||
@@ -12,7 +12,7 @@ import Testing
|
||||
|
||||
/// Creates a default render context for testing.
|
||||
private func testContext(width: Int = 40, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
// MARK: - BorderModifier Tests
|
||||
|
||||
@@ -19,7 +19,8 @@ private func createTestContext(width: Int = 80, height: Int = 24) -> RenderConte
|
||||
return RenderContext(
|
||||
availableWidth: width,
|
||||
availableHeight: height,
|
||||
environment: environment
|
||||
environment: environment,
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import Testing
|
||||
|
||||
/// Creates a default render context for testing.
|
||||
private func testContext(width: Int = 40, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
// MARK: - Border via ContainerView Tests
|
||||
|
||||
@@ -15,7 +15,7 @@ struct AlertTests {
|
||||
@Test("Alert renders with border")
|
||||
func alertRendering() {
|
||||
let alert = Alert(title: "Warning", message: "Something happened")
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(alert, context: context)
|
||||
#expect(buffer.height > 2)
|
||||
// Should have border characters
|
||||
@@ -34,7 +34,7 @@ struct DialogTests {
|
||||
let dialog = Dialog(title: "Test Dialog") {
|
||||
Text("Content here")
|
||||
}
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(dialog, context: context)
|
||||
#expect(buffer.height > 1)
|
||||
// Should contain title and content
|
||||
@@ -87,7 +87,7 @@ struct MenuTests {
|
||||
MenuItem(label: "Second"),
|
||||
]
|
||||
)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(menu, context: context)
|
||||
#expect(buffer.height >= 3) // border + items + border
|
||||
let allContent = buffer.lines.joined()
|
||||
|
||||
@@ -92,7 +92,8 @@ struct EnvironmentModifierTests {
|
||||
let context = RenderContext(
|
||||
availableWidth: 80,
|
||||
availableHeight: 24,
|
||||
environment: EnvironmentValues()
|
||||
environment: EnvironmentValues(),
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
@@ -114,7 +115,8 @@ struct EnvironmentModifierTests {
|
||||
let context = RenderContext(
|
||||
availableWidth: 80,
|
||||
availableHeight: 24,
|
||||
environment: EnvironmentValues()
|
||||
environment: EnvironmentValues(),
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
@@ -134,7 +136,8 @@ struct EnvironmentModifierTests {
|
||||
let context = RenderContext(
|
||||
availableWidth: 80,
|
||||
availableHeight: 24,
|
||||
environment: EnvironmentValues()
|
||||
environment: EnvironmentValues(),
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
@@ -200,7 +203,8 @@ struct ForegroundStylePropagationTests {
|
||||
let context = RenderContext(
|
||||
availableWidth: 80,
|
||||
availableHeight: 24,
|
||||
environment: EnvironmentValues()
|
||||
environment: EnvironmentValues(),
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
@@ -223,7 +227,8 @@ struct ForegroundStylePropagationTests {
|
||||
let context = RenderContext(
|
||||
availableWidth: 80,
|
||||
availableHeight: 24,
|
||||
environment: EnvironmentValues()
|
||||
environment: EnvironmentValues(),
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
@@ -243,7 +248,8 @@ struct ForegroundStylePropagationTests {
|
||||
let context = RenderContext(
|
||||
availableWidth: 80,
|
||||
availableHeight: 24,
|
||||
environment: EnvironmentValues()
|
||||
environment: EnvironmentValues(),
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
@@ -261,7 +267,8 @@ struct ForegroundStylePropagationTests {
|
||||
let context = RenderContext(
|
||||
availableWidth: 80,
|
||||
availableHeight: 24,
|
||||
environment: EnvironmentValues()
|
||||
environment: EnvironmentValues(),
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
|
||||
@@ -21,16 +21,23 @@ private struct LabelView: View, Equatable {
|
||||
@Suite("EquatableView Tests", .serialized)
|
||||
struct EquatableViewTests {
|
||||
|
||||
/// Creates a test context with a fresh TUIContext.
|
||||
/// Creates a test context with a fresh environment including render cache.
|
||||
private func testContext(
|
||||
width: Int = 80,
|
||||
height: Int = 24,
|
||||
identity: ViewIdentity = ViewIdentity(path: "Root")
|
||||
) -> RenderContext {
|
||||
RenderContext(
|
||||
let tuiContext = TUIContext()
|
||||
var env = EnvironmentValues()
|
||||
env.stateStorage = tuiContext.stateStorage
|
||||
env.lifecycle = tuiContext.lifecycle
|
||||
env.keyEventDispatcher = tuiContext.keyEventDispatcher
|
||||
env.renderCache = tuiContext.renderCache
|
||||
env.preferenceStorage = tuiContext.preferences
|
||||
return RenderContext(
|
||||
availableWidth: width,
|
||||
availableHeight: height,
|
||||
tuiContext: TUIContext(),
|
||||
environment: env,
|
||||
identity: identity
|
||||
)
|
||||
}
|
||||
@@ -45,7 +52,7 @@ struct EquatableViewTests {
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
|
||||
#expect(buffer.lines[0].stripped == "Hello")
|
||||
#expect(context.tuiContext.renderCache.count == 1)
|
||||
#expect(context.environment.renderCache!.count == 1)
|
||||
}
|
||||
|
||||
// MARK: - Cache Hit
|
||||
@@ -63,7 +70,7 @@ struct EquatableViewTests {
|
||||
let buffer2 = renderToBuffer(view2, context: context)
|
||||
|
||||
#expect(buffer1.lines == buffer2.lines)
|
||||
#expect(context.tuiContext.renderCache.count == 1)
|
||||
#expect(context.environment.renderCache!.count == 1)
|
||||
}
|
||||
|
||||
// MARK: - Cache Miss on Changed Content
|
||||
@@ -91,21 +98,28 @@ struct EquatableViewTests {
|
||||
let tuiContext = TUIContext()
|
||||
let identity = ViewIdentity(path: "Root")
|
||||
|
||||
// First render at 80×24
|
||||
var env = EnvironmentValues()
|
||||
env.stateStorage = tuiContext.stateStorage
|
||||
env.lifecycle = tuiContext.lifecycle
|
||||
env.keyEventDispatcher = tuiContext.keyEventDispatcher
|
||||
env.renderCache = tuiContext.renderCache
|
||||
env.preferenceStorage = tuiContext.preferences
|
||||
|
||||
// First render at 80x24
|
||||
let context1 = RenderContext(
|
||||
availableWidth: 80,
|
||||
availableHeight: 24,
|
||||
tuiContext: tuiContext,
|
||||
environment: env,
|
||||
identity: identity
|
||||
)
|
||||
let view = EquatableView(content: LabelView(text: "Size"))
|
||||
_ = renderToBuffer(view, context: context1)
|
||||
|
||||
// Second render at 120×40 — should miss
|
||||
// Second render at 120x40 -- should miss
|
||||
let context2 = RenderContext(
|
||||
availableWidth: 120,
|
||||
availableHeight: 40,
|
||||
tuiContext: tuiContext,
|
||||
environment: env,
|
||||
identity: identity
|
||||
)
|
||||
let buffer2 = renderToBuffer(view, context: context2)
|
||||
|
||||
@@ -91,7 +91,7 @@ struct OverlayTests {
|
||||
.overlay(alignment: .center) {
|
||||
Text("Top")
|
||||
}
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
// The overlay "Top" should be centered on "Base Content"
|
||||
#expect(buffer.height >= 1)
|
||||
@@ -102,7 +102,7 @@ struct OverlayTests {
|
||||
@Test("Dimmed modifier strips styling and applies uniform palette colors")
|
||||
func dimmedRendering() {
|
||||
let view = Text("Dimmed text").dimmed()
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
#expect(buffer.height == 1)
|
||||
// Should not use ANSI dim — uses palette-based flat coloring now
|
||||
@@ -117,7 +117,7 @@ struct OverlayTests {
|
||||
.modal {
|
||||
Text("Modal")
|
||||
}
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
// The result should contain both the dimmed background and the modal overlay
|
||||
#expect(buffer.height == 1)
|
||||
|
||||
@@ -12,7 +12,7 @@ import Testing
|
||||
|
||||
/// Creates a default render context for testing.
|
||||
private func testContext(width: Int = 40, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
// MARK: - FrameModifier Tests
|
||||
|
||||
@@ -110,7 +110,7 @@ struct LayoutableTests {
|
||||
@Test("Text sizeThatFits returns content size")
|
||||
func textSizeThatFits() {
|
||||
let text = Text("Hello")
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
|
||||
let size = text.sizeThatFits(proposal: .unspecified, context: context)
|
||||
|
||||
@@ -123,7 +123,7 @@ struct LayoutableTests {
|
||||
@Test("Text sizeThatFits wraps with proposed width")
|
||||
func textSizeThatFitsWraps() {
|
||||
let text = Text("Hello World")
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
|
||||
// With narrow proposed width, text should wrap
|
||||
let size = text.sizeThatFits(proposal: ProposedSize(width: 6, height: nil), context: context)
|
||||
@@ -135,7 +135,7 @@ struct LayoutableTests {
|
||||
@Test("Spacer sizeThatFits is flexible")
|
||||
func spacerSizeThatFits() {
|
||||
let spacer = Spacer()
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
|
||||
let size = spacer.sizeThatFits(proposal: .unspecified, context: context)
|
||||
|
||||
@@ -148,7 +148,7 @@ struct LayoutableTests {
|
||||
@Test("Spacer with minLength has minimum size")
|
||||
func spacerWithMinLength() {
|
||||
let spacer = Spacer(minLength: 5)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
|
||||
let size = spacer.sizeThatFits(proposal: .unspecified, context: context)
|
||||
|
||||
@@ -161,7 +161,7 @@ struct LayoutableTests {
|
||||
@Test("Divider sizeThatFits is width-flexible")
|
||||
func dividerSizeThatFits() {
|
||||
let divider = Divider()
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
|
||||
let size = divider.sizeThatFits(proposal: .unspecified, context: context)
|
||||
|
||||
@@ -178,7 +178,7 @@ struct LayoutableTests {
|
||||
Text("Search:")
|
||||
TextField("Search", text: binding, prompt: Text("Enter search term..."))
|
||||
}
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(hstack, context: context)
|
||||
|
||||
#expect(buffer.width == 80, "HStack should fill exactly available width, got \(buffer.width)")
|
||||
@@ -190,7 +190,7 @@ struct LayoutableTests {
|
||||
var text = ""
|
||||
let binding = Binding(get: { text }, set: { text = $0 })
|
||||
let textField = TextField("Test", text: binding)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
|
||||
let size = measureChild(textField, proposal: .unspecified, context: context)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import Testing
|
||||
|
||||
@MainActor
|
||||
private func testContext(width: Int = 40, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
// MARK: - LazyVStack Tests
|
||||
|
||||
@@ -89,6 +89,7 @@ private func createTestContext(width: Int = 80, height: Int = 24) -> RenderConte
|
||||
return RenderContext(
|
||||
availableWidth: width,
|
||||
availableHeight: height,
|
||||
environment: environment
|
||||
environment: environment,
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ private func createTestContext(width: Int = 80, height: Int = 24) -> RenderConte
|
||||
return RenderContext(
|
||||
availableWidth: width,
|
||||
availableHeight: height,
|
||||
environment: environment
|
||||
environment: environment,
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import Testing
|
||||
/// Creates a default render context for testing.
|
||||
@MainActor
|
||||
private func testContext(width: Int = 40, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
// MARK: - Modifier Propagation Tests
|
||||
|
||||
@@ -16,7 +16,6 @@ private func testContext(width: Int = 80, height: Int = 24) -> RenderContext {
|
||||
RenderContext(
|
||||
availableWidth: width,
|
||||
availableHeight: height,
|
||||
environment: EnvironmentValues(),
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import Testing
|
||||
|
||||
/// Creates a default render context for testing.
|
||||
private func testContext(width: Int = 40, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
// MARK: - PaddingModifier Tests
|
||||
|
||||
@@ -12,7 +12,7 @@ import Testing
|
||||
|
||||
/// Creates a default render context for testing.
|
||||
private func testContext(width: Int = 30, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
// MARK: - ProgressView Rendering Tests
|
||||
|
||||
@@ -18,7 +18,8 @@ private func createTestContext(width: Int = 80, height: Int = 24) -> RenderConte
|
||||
return RenderContext(
|
||||
availableWidth: width,
|
||||
availableHeight: height,
|
||||
environment: environment
|
||||
environment: environment,
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import Testing
|
||||
struct RenderBottleneckTests {
|
||||
|
||||
private func testContext(width: Int = 80, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
/// Measures execution time of a block over multiple iterations.
|
||||
|
||||
@@ -24,7 +24,7 @@ struct RenderPerformanceTests {
|
||||
// MARK: - Test Helpers
|
||||
|
||||
private func testContext(width: Int = 80, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
/// Measures the time to render a view multiple times.
|
||||
@@ -282,7 +282,7 @@ struct RenderPerformanceTests {
|
||||
struct RenderPerformanceStatistics {
|
||||
|
||||
private func testContext(width: Int = 80, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
@Test("Print render performance statistics")
|
||||
|
||||
@@ -15,7 +15,7 @@ struct RenderingTests {
|
||||
@Test("Text renders to single line buffer")
|
||||
func textBuffer() {
|
||||
let text = Text("Hello")
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(text, context: context)
|
||||
#expect(buffer.height == 1)
|
||||
#expect(buffer.lines[0].stripped == "Hello")
|
||||
@@ -24,7 +24,7 @@ struct RenderingTests {
|
||||
@Test("EmptyView renders to empty buffer")
|
||||
func emptyViewBuffer() {
|
||||
let empty = EmptyView()
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(empty, context: context)
|
||||
#expect(buffer.isEmpty)
|
||||
}
|
||||
@@ -35,7 +35,7 @@ struct RenderingTests {
|
||||
Text("Line 1")
|
||||
Text("Line 2")
|
||||
}
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(stack, context: context)
|
||||
#expect(buffer.height == 2)
|
||||
#expect(buffer.lines[0].stripped.contains("Line 1"))
|
||||
@@ -48,7 +48,7 @@ struct RenderingTests {
|
||||
Text("A")
|
||||
Text("B")
|
||||
}
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(stack, context: context)
|
||||
#expect(buffer.height == 3)
|
||||
#expect(buffer.lines[0].stripped.contains("A"))
|
||||
@@ -62,7 +62,7 @@ struct RenderingTests {
|
||||
Text("Left")
|
||||
Text("Right")
|
||||
}
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(stack, context: context)
|
||||
#expect(buffer.height == 1)
|
||||
#expect(buffer.lines[0].stripped == "Left Right")
|
||||
@@ -74,7 +74,7 @@ struct RenderingTests {
|
||||
Text("A")
|
||||
Text("B")
|
||||
}
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(stack, context: context)
|
||||
#expect(buffer.height == 1)
|
||||
#expect(buffer.lines[0].stripped == "A B")
|
||||
@@ -86,7 +86,7 @@ struct RenderingTests {
|
||||
Text("Label:")
|
||||
Text("Value")
|
||||
}
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(layout, context: context)
|
||||
#expect(buffer.height == 1)
|
||||
#expect(buffer.lines[0].stripped == "Label: Value")
|
||||
@@ -104,7 +104,7 @@ struct RenderingTests {
|
||||
}
|
||||
|
||||
let view = MyView()
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
#expect(buffer.height == 2)
|
||||
#expect(buffer.lines[0].stripped.contains("Hello"))
|
||||
@@ -114,7 +114,7 @@ struct RenderingTests {
|
||||
@Test("Divider renders to full width")
|
||||
func dividerBuffer() {
|
||||
let divider = Divider()
|
||||
let context = RenderContext(availableWidth: 20, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 20, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(divider, context: context)
|
||||
#expect(buffer.height == 1)
|
||||
#expect(buffer.lines[0] == String(repeating: "─", count: 20))
|
||||
@@ -123,7 +123,7 @@ struct RenderingTests {
|
||||
@Test("Spacer renders empty lines")
|
||||
func spacerBuffer() {
|
||||
let spacer = Spacer(minLength: 3)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(spacer, context: context)
|
||||
#expect(buffer.height == 3)
|
||||
}
|
||||
@@ -135,7 +135,7 @@ struct RenderingTests {
|
||||
Text("Visible")
|
||||
EmptyView()
|
||||
}
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(stack, context: context)
|
||||
#expect(buffer.height == 1)
|
||||
#expect(buffer.lines[0].stripped == "Visible")
|
||||
@@ -147,7 +147,7 @@ struct RenderingTests {
|
||||
Text("AB")
|
||||
Text("CD")
|
||||
}
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(stack, context: context)
|
||||
#expect(buffer.height == 1)
|
||||
#expect(buffer.lines[0].stripped == "ABCD")
|
||||
@@ -165,7 +165,7 @@ struct RenderingTests {
|
||||
}
|
||||
Text("D")
|
||||
}
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(layout, context: context)
|
||||
#expect(buffer.height == 3)
|
||||
// Last line contains "D", possibly with trailing spaces from VStack alignment
|
||||
@@ -179,7 +179,7 @@ struct RenderingTests {
|
||||
Spacer()
|
||||
Text("Bottom")
|
||||
}
|
||||
let context = RenderContext(availableWidth: 40, availableHeight: 10)
|
||||
let context = RenderContext(availableWidth: 40, availableHeight: 10, tuiContext: TUIContext())
|
||||
|
||||
// Debug: Check child infos
|
||||
let infos = resolveChildInfos(from: stack.content, context: context)
|
||||
@@ -216,7 +216,7 @@ struct RenderingTests {
|
||||
Spacer()
|
||||
Text("R")
|
||||
}
|
||||
var context = RenderContext(availableWidth: 20, availableHeight: 10)
|
||||
var context = RenderContext(availableWidth: 20, availableHeight: 10, tuiContext: TUIContext())
|
||||
context.hasExplicitWidth = true // Simulate terminal/frame constraint
|
||||
let buffer = renderToBuffer(stack, context: context)
|
||||
|
||||
@@ -229,7 +229,7 @@ struct RenderingTests {
|
||||
@Test("Text wraps at availableWidth")
|
||||
func textWrapsAtWidth() {
|
||||
let text = Text("This is a long text that should wrap")
|
||||
let context = RenderContext(availableWidth: 15, availableHeight: 10)
|
||||
let context = RenderContext(availableWidth: 15, availableHeight: 10, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(text, context: context)
|
||||
|
||||
// Text should wrap into multiple lines
|
||||
@@ -244,7 +244,7 @@ struct RenderingTests {
|
||||
func frameConstrainsWidth() {
|
||||
let view = Text("This is a long text that should be wrapped because of the frame modifier")
|
||||
.frame(width: 20)
|
||||
let context = RenderContext(availableWidth: 100, availableHeight: 10)
|
||||
let context = RenderContext(availableWidth: 100, availableHeight: 10, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
|
||||
// Text should wrap at frame width (20), not available width (100)
|
||||
@@ -260,7 +260,7 @@ struct RenderingTests {
|
||||
Text("X")
|
||||
Spacer()
|
||||
}
|
||||
var context = RenderContext(availableWidth: 11, availableHeight: 1)
|
||||
var context = RenderContext(availableWidth: 11, availableHeight: 1, tuiContext: TUIContext())
|
||||
context.hasExplicitWidth = true // Simulate terminal/frame constraint
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
|
||||
@@ -288,7 +288,7 @@ struct RenderingTests {
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
var context = RenderContext(availableWidth: 20, availableHeight: 5)
|
||||
var context = RenderContext(availableWidth: 20, availableHeight: 5, tuiContext: TUIContext())
|
||||
context.hasExplicitWidth = true // Simulate terminal/frame constraint
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
|
||||
@@ -317,7 +317,7 @@ struct RenderingTests {
|
||||
Text("Hi")
|
||||
Spacer()
|
||||
}
|
||||
let context = RenderContext(availableWidth: 20, availableHeight: 10)
|
||||
let context = RenderContext(availableWidth: 20, availableHeight: 10, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
|
||||
// VStack height should fill available height due to spacers
|
||||
@@ -338,7 +338,7 @@ struct RenderingTests {
|
||||
Text("Short")
|
||||
Text("Longer text here")
|
||||
}
|
||||
let context = RenderContext(availableWidth: 40, availableHeight: 10)
|
||||
let context = RenderContext(availableWidth: 40, availableHeight: 10, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
|
||||
// With default .center alignment (like SwiftUI), shorter text should be centered
|
||||
@@ -356,7 +356,7 @@ struct RenderingTests {
|
||||
Text("Hi")
|
||||
Text("Hello World")
|
||||
}
|
||||
let context = RenderContext(availableWidth: 40, availableHeight: 10)
|
||||
let context = RenderContext(availableWidth: 40, availableHeight: 10, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
|
||||
// "Hello World" is 11 chars, "Hi" is 2 chars
|
||||
@@ -392,7 +392,7 @@ struct RenderingTests {
|
||||
|
||||
// Test through WindowGroup like the real app
|
||||
let windowGroup = WindowGroup { contentView }
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 30)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 30, tuiContext: TUIContext())
|
||||
let buffer = windowGroup.renderScene(context: context)
|
||||
|
||||
print("Buffer height: \(buffer.height)")
|
||||
@@ -429,7 +429,7 @@ struct RenderingTests {
|
||||
Text("End")
|
||||
}.border()
|
||||
|
||||
var context = RenderContext(availableWidth: 80, availableHeight: 10)
|
||||
var context = RenderContext(availableWidth: 80, availableHeight: 10, tuiContext: TUIContext())
|
||||
context.hasExplicitWidth = true
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
|
||||
@@ -461,7 +461,7 @@ struct RenderingTests {
|
||||
}
|
||||
}
|
||||
|
||||
var context = RenderContext(availableWidth: 80, availableHeight: 10)
|
||||
var context = RenderContext(availableWidth: 80, availableHeight: 10, tuiContext: TUIContext())
|
||||
context.hasExplicitWidth = true
|
||||
context.hasExplicitHeight = true
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
@@ -496,7 +496,7 @@ struct RenderingTests {
|
||||
Text("End")
|
||||
}.border()
|
||||
|
||||
var context = RenderContext(availableWidth: 80, availableHeight: 10)
|
||||
var context = RenderContext(availableWidth: 80, availableHeight: 10, tuiContext: TUIContext())
|
||||
context.hasExplicitWidth = true
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ private func createTestContext(width: Int = 80, height: Int = 24) -> RenderConte
|
||||
return RenderContext(
|
||||
availableWidth: width,
|
||||
availableHeight: height,
|
||||
environment: environment
|
||||
environment: environment,
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ private func createTestContext(width: Int = 80, height: Int = 24) -> RenderConte
|
||||
return RenderContext(
|
||||
availableWidth: width,
|
||||
availableHeight: height,
|
||||
environment: environment
|
||||
environment: environment,
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import Testing
|
||||
struct SecureFieldTests {
|
||||
|
||||
private func testContext(width: Int = 80, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
@@ -73,6 +73,7 @@ private func createTestContext(width: Int = 80, height: Int = 24) -> RenderConte
|
||||
return RenderContext(
|
||||
availableWidth: width,
|
||||
availableHeight: height,
|
||||
environment: environment
|
||||
environment: environment,
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import Testing
|
||||
|
||||
/// Creates a default render context for testing.
|
||||
private func testContext(width: Int = 40, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
@@ -12,7 +12,7 @@ import Testing
|
||||
|
||||
/// Creates a render context for spinner testing.
|
||||
private func testContext(width: Int = 40, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
// MARK: - SpinnerStyle Tests
|
||||
|
||||
@@ -207,10 +207,16 @@ struct StateStorageIdentityTests {
|
||||
@Test("State survives reconstruction through renderToBuffer")
|
||||
func stateSurvivesRenderToBuffer() {
|
||||
let tuiContext = TUIContext()
|
||||
var env = EnvironmentValues()
|
||||
env.stateStorage = tuiContext.stateStorage
|
||||
env.lifecycle = tuiContext.lifecycle
|
||||
env.keyEventDispatcher = tuiContext.keyEventDispatcher
|
||||
env.renderCache = tuiContext.renderCache
|
||||
env.preferenceStorage = tuiContext.preferences
|
||||
let context = RenderContext(
|
||||
availableWidth: 80,
|
||||
availableHeight: 24,
|
||||
tuiContext: tuiContext,
|
||||
environment: env,
|
||||
identity: ViewIdentity(path: "")
|
||||
)
|
||||
|
||||
@@ -226,10 +232,16 @@ struct StateStorageIdentityTests {
|
||||
@Test("Nested views get independent state identities")
|
||||
func nestedViewsIndependentState() {
|
||||
let tuiContext = TUIContext()
|
||||
var env = EnvironmentValues()
|
||||
env.stateStorage = tuiContext.stateStorage
|
||||
env.lifecycle = tuiContext.lifecycle
|
||||
env.keyEventDispatcher = tuiContext.keyEventDispatcher
|
||||
env.renderCache = tuiContext.renderCache
|
||||
env.preferenceStorage = tuiContext.preferences
|
||||
let context = RenderContext(
|
||||
availableWidth: 80,
|
||||
availableHeight: 24,
|
||||
tuiContext: tuiContext,
|
||||
environment: env,
|
||||
identity: ViewIdentity(path: "")
|
||||
)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ struct StatusBarViewTests {
|
||||
style: .compact
|
||||
)
|
||||
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(statusBar, context: context)
|
||||
|
||||
#expect(buffer.height == 1)
|
||||
@@ -42,7 +42,7 @@ struct StatusBarViewTests {
|
||||
)
|
||||
|
||||
// Use default appearance (rounded)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(statusBar, context: context)
|
||||
|
||||
#expect(buffer.height == 3)
|
||||
@@ -58,7 +58,7 @@ struct StatusBarViewTests {
|
||||
func emptyStatusBar() {
|
||||
let statusBar = StatusBar(items: [])
|
||||
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(statusBar, context: context)
|
||||
|
||||
#expect(buffer.isEmpty)
|
||||
@@ -71,7 +71,7 @@ struct StatusBarViewTests {
|
||||
StatusBarItem(shortcut: "b", label: "beta"),
|
||||
])
|
||||
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(statusBar, context: context)
|
||||
|
||||
let content = buffer.lines.joined()
|
||||
@@ -91,7 +91,7 @@ struct StatusBarViewTests {
|
||||
|
||||
#expect(statusBar.alignment == .leading)
|
||||
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(statusBar, context: context)
|
||||
|
||||
// Content should start near the beginning (after padding)
|
||||
@@ -112,7 +112,7 @@ struct StatusBarViewTests {
|
||||
|
||||
#expect(statusBar.alignment == .trailing)
|
||||
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(statusBar, context: context)
|
||||
|
||||
// Content should be at the end
|
||||
@@ -133,7 +133,7 @@ struct StatusBarViewTests {
|
||||
|
||||
#expect(statusBar.alignment == .center)
|
||||
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(statusBar, context: context)
|
||||
|
||||
// Content should be centered
|
||||
@@ -155,7 +155,7 @@ struct StatusBarViewTests {
|
||||
|
||||
#expect(statusBar.alignment == .justified)
|
||||
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(statusBar, context: context)
|
||||
|
||||
// All items should be present
|
||||
@@ -179,7 +179,7 @@ struct StatusBarViewTests {
|
||||
#expect(statusBar.style == .bordered)
|
||||
#expect(statusBar.alignment == .center)
|
||||
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(statusBar, context: context)
|
||||
|
||||
#expect(buffer.height == 3)
|
||||
@@ -199,7 +199,7 @@ struct StatusBarAlignmentTests {
|
||||
alignment: .justified
|
||||
)
|
||||
|
||||
let context = RenderContext(availableWidth: 40, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 40, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(statusBar, context: context)
|
||||
|
||||
// Single item should be centered in justified mode
|
||||
@@ -233,7 +233,8 @@ struct StatusBarItemsModifierTests {
|
||||
let context = RenderContext(
|
||||
availableWidth: 80,
|
||||
availableHeight: 24,
|
||||
environment: environment
|
||||
environment: environment,
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
|
||||
_ = renderToBuffer(view, context: context)
|
||||
@@ -266,7 +267,8 @@ struct StatusBarItemsModifierTests {
|
||||
let context = RenderContext(
|
||||
availableWidth: 80,
|
||||
availableHeight: 24,
|
||||
environment: environment
|
||||
environment: environment,
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
|
||||
_ = renderToBuffer(view, context: context)
|
||||
@@ -297,7 +299,8 @@ struct StatusBarItemsModifierTests {
|
||||
let context = RenderContext(
|
||||
availableWidth: 80,
|
||||
availableHeight: 24,
|
||||
environment: environment
|
||||
environment: environment,
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
|
||||
let buffer = renderToBuffer(view, context: context)
|
||||
@@ -330,7 +333,8 @@ struct StatusBarItemsModifierTests {
|
||||
let context = RenderContext(
|
||||
availableWidth: 80,
|
||||
availableHeight: 24,
|
||||
environment: environment
|
||||
environment: environment,
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
|
||||
_ = renderToBuffer(outerView, context: context)
|
||||
|
||||
@@ -10,7 +10,7 @@ import Testing
|
||||
|
||||
/// Creates a default render context for testing.
|
||||
private func testContext(width: Int = 40, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
@@ -34,7 +34,8 @@ private func createTestContext(width: Int = 80, height: Int = 24) -> RenderConte
|
||||
return RenderContext(
|
||||
availableWidth: width,
|
||||
availableHeight: height,
|
||||
environment: environment
|
||||
environment: environment,
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import Testing
|
||||
struct TextFieldTests {
|
||||
|
||||
private func testContext(width: Int = 80, height: Int = 24) -> RenderContext {
|
||||
RenderContext(availableWidth: width, availableHeight: height)
|
||||
RenderContext(availableWidth: width, availableHeight: height, tuiContext: TUIContext())
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
@@ -19,7 +19,8 @@ private func createTestContext(width: Int = 80, height: Int = 24) -> RenderConte
|
||||
return RenderContext(
|
||||
availableWidth: width,
|
||||
availableHeight: height,
|
||||
environment: environment
|
||||
environment: environment,
|
||||
tuiContext: TUIContext()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,16 +12,23 @@ import Testing
|
||||
@Suite("TupleView Equatable Tests", .serialized)
|
||||
struct TupleViewEquatableTests {
|
||||
|
||||
/// Creates a test context with a fresh TUIContext.
|
||||
/// Creates a test context with a fresh environment including render cache.
|
||||
private func testContext(
|
||||
width: Int = 80,
|
||||
height: Int = 24,
|
||||
identity: ViewIdentity = ViewIdentity(path: "Root")
|
||||
) -> RenderContext {
|
||||
RenderContext(
|
||||
let tuiContext = TUIContext()
|
||||
var env = EnvironmentValues()
|
||||
env.stateStorage = tuiContext.stateStorage
|
||||
env.lifecycle = tuiContext.lifecycle
|
||||
env.keyEventDispatcher = tuiContext.keyEventDispatcher
|
||||
env.renderCache = tuiContext.renderCache
|
||||
env.preferenceStorage = tuiContext.preferences
|
||||
return RenderContext(
|
||||
availableWidth: width,
|
||||
availableHeight: height,
|
||||
tuiContext: TUIContext(),
|
||||
environment: env,
|
||||
identity: identity
|
||||
)
|
||||
}
|
||||
@@ -93,7 +100,7 @@ struct TupleViewEquatableTests {
|
||||
@Test("VStack with equatable content gets cache hit on second render")
|
||||
func cacheHitForEqualVStack() {
|
||||
let context = testContext()
|
||||
let cache = context.tuiContext.renderCache
|
||||
let cache = context.environment.renderCache!
|
||||
|
||||
let stack1 = VStack {
|
||||
Text("Static A")
|
||||
@@ -119,7 +126,7 @@ struct TupleViewEquatableTests {
|
||||
@Test("VStack with changed content causes cache miss")
|
||||
func cacheMissForChangedVStack() {
|
||||
let context = testContext()
|
||||
let cache = context.tuiContext.renderCache
|
||||
let cache = context.environment.renderCache!
|
||||
|
||||
let stack1 = VStack {
|
||||
Text("Before")
|
||||
@@ -146,13 +153,20 @@ struct TupleViewEquatableTests {
|
||||
let tuiContext = TUIContext()
|
||||
let cache = tuiContext.renderCache
|
||||
|
||||
var env = EnvironmentValues()
|
||||
env.stateStorage = tuiContext.stateStorage
|
||||
env.lifecycle = tuiContext.lifecycle
|
||||
env.keyEventDispatcher = tuiContext.keyEventDispatcher
|
||||
env.renderCache = tuiContext.renderCache
|
||||
env.preferenceStorage = tuiContext.preferences
|
||||
|
||||
let innerIdentity = ViewIdentity(path: "Root/Inner")
|
||||
let outerIdentity = ViewIdentity(path: "Root/Outer")
|
||||
|
||||
// Render inner
|
||||
let innerContext = RenderContext(
|
||||
availableWidth: 80, availableHeight: 24,
|
||||
tuiContext: tuiContext, identity: innerIdentity
|
||||
environment: env, identity: innerIdentity
|
||||
)
|
||||
let inner = EquatableView(content: HStack {
|
||||
Text("Left")
|
||||
@@ -163,7 +177,7 @@ struct TupleViewEquatableTests {
|
||||
// Render outer
|
||||
let outerContext = RenderContext(
|
||||
availableWidth: 80, availableHeight: 24,
|
||||
tuiContext: tuiContext, identity: outerIdentity
|
||||
environment: env, identity: outerIdentity
|
||||
)
|
||||
let outer = EquatableView(content: VStack {
|
||||
Text("Top")
|
||||
|
||||
@@ -16,7 +16,7 @@ struct AnyViewTests {
|
||||
func anyViewWrapping() {
|
||||
let text = Text("Hello")
|
||||
let anyView = AnyView(text)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(anyView, context: context)
|
||||
#expect(buffer.lines[0].stripped == "Hello")
|
||||
}
|
||||
@@ -24,7 +24,7 @@ struct AnyViewTests {
|
||||
@Test("asAnyView extension works")
|
||||
func asAnyViewExtension() {
|
||||
let anyView = Text("Test").bold().asAnyView()
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24)
|
||||
let context = RenderContext(availableWidth: 80, availableHeight: 24, tuiContext: TUIContext())
|
||||
let buffer = renderToBuffer(anyView, context: context)
|
||||
#expect(buffer.height == 1)
|
||||
#expect(buffer.lines[0].stripped == "Test")
|
||||
|
||||
Reference in New Issue
Block a user