mirror of
https://github.com/phranck/TUIkit.git
synced 2026-05-21 09:50:35 +00:00
db8ea40c0a
- Fix 80 SwiftLint warnings (159 -> 79): vertical_whitespace, prefer_self_in_static_references, modifier_order, trailing_newline, trailing_whitespace, prefer_for_where, unneeded_synthesized_initializer, redundant_type_annotation, implicit_optional_initialization, superfluous_disable_command, shorthand_optional_binding, syntactic_sugar, empty_string, vertical_whitespace_closing_braces, identifier_name in BadgeModifier - Refactor StatusBar from direct Renderable to _StatusBarCore pattern (public View with real body wrapping private Renderable core)
155 lines
5.0 KiB
Swift
155 lines
5.0 KiB
Swift
// 🖥️ TUIKit — Terminal UI Kit for Swift
|
||
// EquatableView.swift
|
||
//
|
||
// Created by LAYERED.work
|
||
// License: MIT
|
||
|
||
import TUIkitCore
|
||
// MARK: - EquatableView
|
||
|
||
/// A wrapper that enables subtree memoization for views conforming to `Equatable`.
|
||
///
|
||
/// When TUIKit renders an `EquatableView`, it compares the current content with
|
||
/// the previously cached value. If the content is unchanged **and** the available
|
||
/// size hasn't changed, the cached ``FrameBuffer`` is returned immediately —
|
||
/// skipping the entire subtree rendering.
|
||
///
|
||
/// ## Usage
|
||
///
|
||
/// Apply `.equatable()` to any `Equatable` view:
|
||
///
|
||
/// ```swift
|
||
/// struct ScoreDisplay: View, Equatable {
|
||
/// let name: String
|
||
/// let score: Int
|
||
///
|
||
/// var body: some View {
|
||
/// VStack {
|
||
/// Text(name)
|
||
/// Text("Score: \(score)")
|
||
/// }
|
||
/// }
|
||
/// }
|
||
///
|
||
/// // In a parent view:
|
||
/// ScoreDisplay(name: "Player 1", score: 42).equatable()
|
||
/// ```
|
||
///
|
||
/// When `name` and `score` are unchanged between frames, the `VStack` and both
|
||
/// `Text` views are never re-rendered — the cached buffer is returned directly.
|
||
///
|
||
/// ## When to Use
|
||
///
|
||
/// - **Large static subtrees** — views with many children that rarely change
|
||
/// - **Expensive rendering** — views whose `body` or `renderToBuffer` is costly
|
||
/// - **Animation siblings** — static views next to animated ones
|
||
///
|
||
/// ## When NOT to Use
|
||
///
|
||
/// - Views that read `@State` directly (state lives in a reference-type box,
|
||
/// so the view struct compares as equal even when state changed)
|
||
/// - Views that change every frame (the cache overhead adds no value)
|
||
/// - Views that depend on environment values that change frequently
|
||
/// - **Views containing focused interactive elements** (Button, Toggle, Slider,
|
||
/// etc.) whose focus indicator animates via pulse phase. The cached buffer
|
||
/// would show a frozen pulse animation.
|
||
///
|
||
/// ## Cache Invalidation
|
||
///
|
||
/// The render cache is selectively cleared when `@State` values change:
|
||
/// only cache entries in the ancestor/descendant path of the changed state
|
||
/// are invalidated. Sibling subtrees retain their cached buffers.
|
||
/// Pulse animation changes do **not** invalidate the cache, which is why
|
||
/// subtrees containing focused interactive views should not be wrapped.
|
||
///
|
||
/// - SeeAlso: ``View/equatable()``
|
||
public struct EquatableView<Content: View & Equatable>: View {
|
||
/// The wrapped view content.
|
||
let content: Content
|
||
|
||
/// Creates an equatable view wrapping the given content.
|
||
///
|
||
/// - Parameter content: The equatable view to memoize.
|
||
public init(content: Content) {
|
||
self.content = content
|
||
}
|
||
|
||
public var body: Never {
|
||
fatalError("EquatableView is a primitive view")
|
||
}
|
||
}
|
||
|
||
// MARK: - Rendering
|
||
|
||
extension EquatableView: Renderable {
|
||
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
||
let cache = context.environment.renderCache!
|
||
let identity = context.identity
|
||
|
||
cache.markActive(identity)
|
||
|
||
// Cache hit: view unchanged and context size matches
|
||
if let cached = cache.lookup(
|
||
identity: identity,
|
||
view: content,
|
||
contextWidth: context.availableWidth,
|
||
contextHeight: context.availableHeight
|
||
) {
|
||
// Still need to run hydration for @State properties inside
|
||
// the cached subtree, so they stay active for GC.
|
||
// But we skip the actual rendering work.
|
||
markSubtreeActive(context: context)
|
||
return cached
|
||
}
|
||
|
||
// Cache miss: render normally and store result
|
||
let buffer = TUIkitView.renderToBuffer(content, context: context)
|
||
|
||
cache.store(
|
||
identity: identity,
|
||
view: content,
|
||
buffer: buffer,
|
||
contextWidth: context.availableWidth,
|
||
contextHeight: context.availableHeight
|
||
)
|
||
|
||
return buffer
|
||
}
|
||
}
|
||
|
||
// MARK: - Private Helpers
|
||
|
||
private extension EquatableView {
|
||
/// Marks the content's identity as active in StateStorage for GC.
|
||
///
|
||
/// When returning a cached buffer, the subtree's views aren't visited.
|
||
/// Their state identities must still be marked active to prevent
|
||
/// StateStorage from garbage-collecting them.
|
||
func markSubtreeActive(context: RenderContext) {
|
||
context.environment.stateStorage!.markActive(context.identity)
|
||
}
|
||
}
|
||
|
||
// MARK: - View Extension
|
||
|
||
public extension View where Self: Equatable {
|
||
/// Wraps this view in an ``EquatableView`` for subtree memoization.
|
||
///
|
||
/// When the view's properties are unchanged between frames, the entire
|
||
/// subtree is skipped and the cached rendering result is reused.
|
||
///
|
||
/// ```swift
|
||
/// struct MyView: View, Equatable {
|
||
/// let title: String
|
||
/// var body: some View { Text(title) }
|
||
/// }
|
||
///
|
||
/// MyView(title: "Hello").equatable()
|
||
/// ```
|
||
///
|
||
/// - Returns: An ``EquatableView`` wrapping this view.
|
||
func equatable() -> EquatableView<Self> {
|
||
EquatableView(content: self)
|
||
}
|
||
}
|