From db12959b996bab0e0bd012491c28eb61fc8b468b Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 12:03:58 +0200 Subject: [PATCH 1/6] Fix: Align ViewBuilder result surface with SwiftUI - Add buildBlock() -> EmptyView for empty builder blocks - Reshape TupleView to SwiftUI's tuple-generic TupleView with a public value property; the builder captures children on a fast path and the public initializer resolves them by reflection - Give TupleView an unconditional best-effort Equatable so .equatable() keeps working on multi-child containers (Swift 6.0 cannot constrain tuple elements); non-comparable children safely degrade to cache misses - Rename ConditionalView to SwiftUI's _ConditionalContent spelling and rename buildOptional to buildIf - Erase limited-availability content to AnyView and add AnyView's labeled init(erasing:) - Drop the optional-expression overload SwiftUI does not expose --- .swiftlint.yml | 2 + Sources/TUIkit/Views/Alert.swift | 9 +- .../TUIkitCore/Rendering/ViewIdentity.swift | 8 +- Sources/TUIkitView/Core/PrimitiveViews.swift | 22 ++- Sources/TUIkitView/Core/TupleViews.swift | 149 +++++++++++++---- Sources/TUIkitView/Core/ViewBuilder.swift | 49 ++++-- .../TUIkitView/Rendering/RenderContext.swift | 2 +- Sources/TUIkitView/Rendering/Renderable.swift | 2 +- Sources/TUIkitView/State/StateStorage.swift | 2 +- .../ViewBuilderAlignmentTests.swift | 158 ++++++++++++++++++ 10 files changed, 327 insertions(+), 76 deletions(-) create mode 100644 Tests/TUIkitTests/ViewBuilderAlignmentTests.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 255e6c9b1..699e7a9a2 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -113,6 +113,8 @@ type_name: excluded: - ID # Internal _Core views follow SwiftUI pattern (e.g. _AlertCore, _DialogCore) + # _ConditionalContent matches SwiftUI's public builder result type name + - _ConditionalContent - _AlertCore - _DialogCore - _PanelCore diff --git a/Sources/TUIkit/Views/Alert.swift b/Sources/TUIkit/Views/Alert.swift index 1c9264dac..c83aa592a 100644 --- a/Sources/TUIkit/Views/Alert.swift +++ b/Sources/TUIkit/Views/Alert.swift @@ -379,13 +379,8 @@ extension EmptyView: ButtonProvider { extension TupleView: ButtonProvider { func extractButtons() -> [Button] { - var buttons: [Button] = [] - func collect(_ view: T) { - if let provider = view as? ButtonProvider { - buttons.append(contentsOf: provider.extractButtons()) - } + resolvedChildren().flatMap { child in + (child as? ButtonProvider)?.extractButtons() ?? [] } - repeat collect(each children) - return buttons } } diff --git a/Sources/TUIkitCore/Rendering/ViewIdentity.swift b/Sources/TUIkitCore/Rendering/ViewIdentity.swift index e91c887ea..c0fd8829f 100644 --- a/Sources/TUIkitCore/Rendering/ViewIdentity.swift +++ b/Sources/TUIkitCore/Rendering/ViewIdentity.swift @@ -25,13 +25,13 @@ /// ``` /// /// Container views (`VStack`, `HStack`, `TupleView`, `ViewArray`) append -/// their child index. Leaf views append their type name. `ConditionalView` +/// their child index. Leaf views append their type name. `_ConditionalContent` /// appends a branch label (`"true"` or `"false"`). /// /// ## Stability /// /// The identity is **stable across render passes** as long as the view tree -/// structure does not change. If a `ConditionalView` switches branches, the +/// structure does not change. If a `_ConditionalContent` switches branches, the /// old branch's state is invalidated. public struct ViewIdentity: Hashable, CustomStringConvertible, Sendable { /// The structural path from root to this view. @@ -85,7 +85,7 @@ public extension ViewIdentity { /// Returns a child identity by appending a branch label. /// - /// Used by ``ConditionalView`` to distinguish between the + /// Used by ``_ConditionalContent`` to distinguish between the /// `true` and `false` branches of an `if-else`. /// /// - Parameter label: The branch label (`"true"` or `"false"`). @@ -97,7 +97,7 @@ public extension ViewIdentity { /// Whether the given path is a descendant of this identity. /// /// Used by `StateStorage` to invalidate all state under a branch - /// when a `ConditionalView` switches. + /// when a `_ConditionalContent` switches. /// /// - Parameter descendant: The path to check. /// - Returns: `true` if `descendant` starts with this identity's path. diff --git a/Sources/TUIkitView/Core/PrimitiveViews.swift b/Sources/TUIkitView/Core/PrimitiveViews.swift index 24ded4c69..260cf205a 100644 --- a/Sources/TUIkitView/Core/PrimitiveViews.swift +++ b/Sources/TUIkitView/Core/PrimitiveViews.swift @@ -28,7 +28,7 @@ public struct EmptyView: View, Equatable { } } -// MARK: - ConditionalView +// MARK: - _ConditionalContent /// A view that represents either the true or false branch of a conditional. /// @@ -36,7 +36,7 @@ public struct EmptyView: View, Equatable { /// /// - Important: This is framework infrastructure. Created automatically by /// `@ViewBuilder` for `if`/`else` branches. Do not instantiate directly. -public enum ConditionalView: View { +public enum _ConditionalContent: View { /// The true branch was executed. case trueContent(TrueContent) @@ -44,7 +44,7 @@ public enum ConditionalView: View { case falseContent(FalseContent) public var body: Never { - fatalError("ConditionalView renders its children directly") + fatalError("_ConditionalContent renders its children directly") } } @@ -106,6 +106,14 @@ public struct AnyView: View { } } + /// Creates an AnyView wrapping the given view, matching SwiftUI's + /// labeled erasure initializer. + /// + /// - Parameter view: The view to type-erase. + public init(erasing view: V) { + self.init(view) + } + public var body: Never { fatalError("AnyView renders via Renderable") } @@ -127,9 +135,9 @@ extension EmptyView: Renderable { } } -// MARK: - ConditionalView Rendering +// MARK: - _ConditionalContent Rendering -extension ConditionalView: Renderable { +extension _ConditionalContent: Renderable { public func renderToBuffer(context: RenderContext) -> FrameBuffer { let stateStorage = context.environment.stateStorage! switch self { @@ -143,9 +151,9 @@ extension ConditionalView: Renderable { } } -// MARK: - ConditionalView Child Traversal +// MARK: - _ConditionalContent Child Traversal -extension ConditionalView: ChildInfoProvider, ChildViewProvider { +extension _ConditionalContent: ChildInfoProvider, ChildViewProvider { public func childInfos(context: RenderContext) -> [ChildInfo] { switch self { case .trueContent(let content): diff --git a/Sources/TUIkitView/Core/TupleViews.swift b/Sources/TUIkitView/Core/TupleViews.swift index f42a942b6..28e1bf7a8 100644 --- a/Sources/TUIkitView/Core/TupleViews.swift +++ b/Sources/TUIkitView/Core/TupleViews.swift @@ -5,26 +5,50 @@ // License: MIT import TUIkitCore -/// A view that contains multiple child views packed via a parameter pack. + +/// A view that contains multiple child views packed into a tuple. +/// +/// `TupleView` matches SwiftUI's public shape: it is generic over one tuple +/// type `T` and exposes the packed children through ``value``. It is created +/// automatically by `ViewBuilder` when multiple views appear in a +/// `@ViewBuilder` closure. /// -/// `TupleView` replaces the previous `TupleView2` through `TupleView10` -/// types with a single generic struct using Swift Parameter Packs (SE-0393). -/// This removes the 10-child limit and eliminates ~400 lines of boilerplate. +/// ## Child resolution /// -/// `TupleView` is created automatically by `ViewBuilder` when multiple -/// views appear in a `@ViewBuilder` closure. +/// Rendering needs the individual children, not the tuple. Builder-created +/// instances capture the children directly from the parameter pack (no +/// reflection cost). Instances created through the public initializer resolve +/// children by reflecting over the tuple at render time. /// /// - Important: This is framework infrastructure. Created automatically by /// `@ViewBuilder`. Do not instantiate directly. -public struct TupleView: View { +public struct TupleView: View { /// The packed child views. - public let children: (repeat each V) + public var value: T + + /// Children captured from the builder's parameter pack. + /// + /// `nil` for publicly constructed instances, which resolve children by + /// reflection instead. Builder instances are recreated every frame, so + /// the captured array cannot go stale. + private let packedChildren: [any View]? + + /// Creates a tuple view from a tuple of child views. + /// + /// - Parameter value: The tuple containing the child views. + public init(_ value: T) { + self.value = value + self.packedChildren = nil + } - /// Creates a tuple view from a parameter pack of child views. + /// Builder fast path carrying the statically known children. /// - /// - Parameter children: The child views. - init(_ children: repeat each V) { - self.children = (repeat each children) + /// - Parameters: + /// - value: The tuple containing the child views. + /// - children: The children in declaration order. + init(value: T, children: [any View]) { + self.value = value + self.packedChildren = children } public var body: Never { @@ -32,14 +56,56 @@ public struct TupleView: View { } } +// MARK: - Child Resolution + +extension TupleView { + /// Returns the child views in declaration order. + /// + /// Uses the builder-captured children when available and falls back to + /// reflecting over the tuple for publicly constructed instances. Non-view + /// tuple elements are ignored. + package func resolvedChildren() -> [any View] { + if let packedChildren { + return packedChildren + } + if let single = value as? any View { + return [single] + } + return Mirror(reflecting: value).children.compactMap { $0.value as? any View } + } +} + // MARK: - Equatable Conformance -extension TupleView: @preconcurrency Equatable where repeat each V: Equatable { +/// Compares two type-erased views for equality. +/// +/// Views that do not conform to `Equatable` never compare as equal. This is +/// the safe direction for subtree memoization: an unequal comparison causes a +/// cache miss and a fresh render instead of stale output. +private func anyViewsEqual(_ lhs: any View, _ rhs: any View) -> Bool { + guard let comparable = lhs as? any Equatable else { return false } + return openedEqual(comparable, rhs) +} + +/// Opens the `Equatable` existential and compares both values as `E`. +private func openedEqual(_ lhs: E, _ rhs: Any) -> Bool { + guard let typed = rhs as? E else { return false } + return lhs == typed +} + +extension TupleView: @preconcurrency Equatable { + /// Element-wise best-effort equality over the packed children. + /// + /// SwiftUI's `TupleView` declares no `Equatable` conformance; TUIkit adds + /// this one so `.equatable()` keeps working on multi-child containers + /// (Swift 6.0 cannot express conditional conformance over tuple + /// elements). Children without `Equatable` conformance make the whole + /// comparison unequal, which safely degrades to a cache miss. public static func == (lhs: TupleView, rhs: TupleView) -> Bool { - func isEqual(_ left: T, _ right: T) -> Bool { left == right } - var result = true - repeat result = result && isEqual(each lhs.children, each rhs.children) - return result + let leftChildren = lhs.resolvedChildren() + let rightChildren = rhs.resolvedChildren() + guard leftChildren.count == rightChildren.count else { return false } + return zip(leftChildren, rightChildren).allSatisfy(anyViewsEqual) } } @@ -52,15 +118,21 @@ extension TupleView: Renderable, ChildInfoProvider { public func childInfos(context: RenderContext) -> [ChildInfo] { var infos: [ChildInfo] = [] - var childIndex = 0 - func append(_ child: Child) { - let childContext = context.withChildIdentity(type: Child.self, index: childIndex) - childIndex += 1 - infos.append(contentsOf: resolveChildInfos(from: child, context: childContext)) + for (index, child) in resolvedChildren().enumerated() { + infos.append(contentsOf: childInfos(for: child, index: index, context: context)) } - repeat append(each children) return infos } + + /// Resolves one child's infos with its opened concrete type. + private func childInfos( + for child: Child, + index: Int, + context: RenderContext + ) -> [ChildInfo] { + let childContext = context.withChildIdentity(type: Child.self, index: index) + return resolveChildInfos(from: child, context: childContext) + } } // MARK: - TupleView Two-Pass Layout Support @@ -68,21 +140,24 @@ extension TupleView: Renderable, ChildInfoProvider { extension TupleView: ChildViewProvider { public func childViews(context: RenderContext) -> [ChildView] { var views: [ChildView] = [] - var childIndex = 0 - func append(_ child: Child) { - let index = childIndex - childIndex += 1 - - if let provider = child as? ChildViewProvider { - let childContext = context.withChildIdentity(type: Child.self, index: index) - views.append(contentsOf: provider.childViews(context: childContext).map { - $0.scoped(to: childContext.identity) - }) - } else { - views.append(ChildView(child, childIndex: index)) - } + for (index, child) in resolvedChildren().enumerated() { + views.append(contentsOf: childViews(for: child, index: index, context: context)) } - repeat append(each children) return views } + + /// Resolves one child's layout views with its opened concrete type. + private func childViews( + for child: Child, + index: Int, + context: RenderContext + ) -> [ChildView] { + if let provider = child as? ChildViewProvider { + let childContext = context.withChildIdentity(type: Child.self, index: index) + return provider.childViews(context: childContext).map { + $0.scoped(to: childContext.identity) + } + } + return [ChildView(child, childIndex: index)] + } } diff --git a/Sources/TUIkitView/Core/ViewBuilder.swift b/Sources/TUIkitView/Core/ViewBuilder.swift index ea5be2929..109ecbec9 100644 --- a/Sources/TUIkitView/Core/ViewBuilder.swift +++ b/Sources/TUIkitView/Core/ViewBuilder.swift @@ -29,6 +29,13 @@ import TUIkitCore @resultBuilder public struct ViewBuilder { + // MARK: - Empty Block + + /// Builds an empty block into an EmptyView, matching SwiftUI. + public static func buildBlock() -> EmptyView { + EmptyView() + } + // MARK: - Single View /// Builds a single view. @@ -38,14 +45,17 @@ public struct ViewBuilder { // MARK: - Multiple Views (Parameter Pack) - /// Builds multiple views into a TupleView using Swift Parameter Packs. + /// Builds multiple views into a tuple-typed TupleView, matching SwiftUI's + /// `TupleView<(repeat each Content)>` result shape. /// - /// This single overload replaces the previous 9 arity-specific `buildBlock` - /// overloads (`TupleView2` through `TupleView10`), removing the 10-child limit. - public static func buildBlock( - _ content: repeat each C - ) -> TupleView { - TupleView(repeat each content) + /// The children are captured alongside the tuple so rendering does not + /// need to reflect over the tuple value. + public static func buildBlock( + _ content: repeat each Content + ) -> TupleView<(repeat each Content)> { + var children: [any View] = [] + repeat children.append(each content) + return TupleView(value: (repeat each content), children: children) } // MARK: - Conditionals @@ -53,30 +63,38 @@ public struct ViewBuilder { /// Supports the true branch of an if-else. public static func buildEither( first content: TrueContent - ) -> ConditionalView { + ) -> _ConditionalContent { .trueContent(content) } /// Supports the false branch of an if-else. public static func buildEither( second content: FalseContent - ) -> ConditionalView { + ) -> _ConditionalContent { .falseContent(content) } - /// Supports optional views (if let, if without else). - public static func buildOptional(_ content: Content?) -> Content? { + /// Supports optional views (if let, if without else), matching SwiftUI's + /// `buildIf` spelling. + public static func buildIf(_ content: Content?) -> Content? { content } /// Supports availability limiting. - public static func buildLimitedAvailability(_ content: Content) -> Content { - content + /// + /// The branch content is erased to ``AnyView`` because the enclosing + /// builder block cannot name types that are only available inside the + /// `#available` branch. This matches SwiftUI's result shape. + public static func buildLimitedAvailability(_ content: Content) -> AnyView { + AnyView(content) } // MARK: - Arrays /// Supports for-in loops. + /// + /// SwiftUI's `ViewBuilder` has no array support (`ForEach` covers + /// iteration); TUIkit keeps this as a documented additive convenience. public static func buildArray(_ components: [Content]) -> ViewArray { ViewArray(components) } @@ -87,9 +105,4 @@ public struct ViewBuilder { public static func buildExpression(_ expression: Content) -> Content { expression } - - /// Supports optional expressions. - public static func buildExpression(_ expression: Content?) -> Content? { - expression - } } diff --git a/Sources/TUIkitView/Rendering/RenderContext.swift b/Sources/TUIkitView/Rendering/RenderContext.swift index 158d92e2d..3792450f9 100644 --- a/Sources/TUIkitView/Rendering/RenderContext.swift +++ b/Sources/TUIkitView/Rendering/RenderContext.swift @@ -118,7 +118,7 @@ public struct RenderContext { /// Creates a new context with a branch identity. /// - /// Used by `ConditionalView` to distinguish between if/else branches. + /// Used by `_ConditionalContent` to distinguish between if/else branches. /// /// - Parameter label: The branch label (`"true"` or `"false"`). /// - Returns: A new RenderContext with the branch identity. diff --git a/Sources/TUIkitView/Rendering/Renderable.swift b/Sources/TUIkitView/Rendering/Renderable.swift index 960bad4fc..f47f69991 100644 --- a/Sources/TUIkitView/Rendering/Renderable.swift +++ b/Sources/TUIkitView/Rendering/Renderable.swift @@ -28,7 +28,7 @@ import TUIkitCore /// /// - **Leaf views**: `Text`, `EmptyView`, `Spacer`, `Divider` /// - **Layout containers**: `VStack`, `HStack`, `ZStack` -/// - **ViewBuilder glue**: `TupleView`, `ConditionalView`, `ViewArray` +/// - **ViewBuilder glue**: `TupleView`, `_ConditionalContent`, `ViewArray` /// - **Interactive views**: `Button`, `ButtonRow`, `Menu`, `StatusBar` /// - **Containers**: `Panel`, `ContainerView`, `Alert`, `Dialog`, `Card` /// - **Modifiers**: `ModifiedView`, `DimmedModifier`, etc. diff --git a/Sources/TUIkitView/State/StateStorage.swift b/Sources/TUIkitView/State/StateStorage.swift index ddc4bf2da..c27e76dc6 100644 --- a/Sources/TUIkitView/State/StateStorage.swift +++ b/Sources/TUIkitView/State/StateStorage.swift @@ -202,7 +202,7 @@ extension StateStorage { /// Removes all state for descendants of the given identity. /// - /// Called by ``ConditionalView`` when switching branches to clean up + /// Called by ``_ConditionalContent`` when switching branches to clean up /// state from the now-inactive branch. /// /// - Parameter ancestor: The branch identity whose descendants should be removed. diff --git a/Tests/TUIkitTests/ViewBuilderAlignmentTests.swift b/Tests/TUIkitTests/ViewBuilderAlignmentTests.swift new file mode 100644 index 000000000..799cda10e --- /dev/null +++ b/Tests/TUIkitTests/ViewBuilderAlignmentTests.swift @@ -0,0 +1,158 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// ViewBuilderAlignmentTests.swift +// +// Created by LAYERED.work +// License: MIT + +import Testing + +@testable import TUIkit + +@MainActor +@Suite("ViewBuilder SwiftUI Alignment") +struct ViewBuilderAlignmentTests { + + /// Creates an isolated render context for builder output assertions. + private func testContext(width: Int = 40, height: Int = 10) -> RenderContext { + RenderContext( + availableWidth: width, + availableHeight: height, + tuiContext: TUIContext() + ) + } + + // MARK: - Empty Block + + @Test("An empty builder block produces EmptyView") + func emptyBuilderBlockProducesEmptyView() { + // Swift 6.0 does not apply the builder transform to fully empty + // bodies, so the zero-argument surface is asserted directly. + let empty = ViewBuilder.buildBlock() + + #expect(type(of: empty) == EmptyView.self) + } + + // MARK: - Tuple Blocks + + @Test("A multi-view block produces a tuple-typed TupleView") + func multiViewBlockProducesTupleTypedTupleView() { + struct Two: View { + var body: some View { + Text("a") + Text("b") + } + } + + #expect(Two.Body.self == TupleView<(Text, Text)>.self) + } + + @Test("TupleView exposes its children through value") + func tupleViewExposesValue() { + let tuple = TupleView((Text("a"), Text("b"))) + + #expect(tuple.value.0 == Text("a")) + #expect(tuple.value.1 == Text("b")) + } + + @Test("A publicly constructed TupleView renders its children") + func publicTupleInitRenders() { + let tuple = TupleView((Text("first"), Text("second"))) + + let buffer = renderToBuffer(tuple, context: testContext()) + + #expect(buffer.lines.count == 2) + #expect(buffer.lines[0].stripped == "first") + #expect(buffer.lines[1].stripped == "second") + } + + // MARK: - Runtime Equality + + @Test("TupleViews with equal comparable children compare as equal") + func tupleEqualityForComparableChildren() { + let first = TupleView((Text("same"), Text("same"))) + let second = TupleView((Text("same"), Text("same"))) + let different = TupleView((Text("same"), Text("changed"))) + + #expect(first == second) + #expect(first != different) + } + + @Test("TupleViews with non-comparable children never compare as equal") + func tupleEqualityFallsBackToNotEqual() { + struct Uncomparable: View { + let action: () -> Void + var body: some View { Text("x") } + } + + let view = TupleView((Text("a"), Uncomparable(action: {}))) + + // swiftlint:disable:next identical_operands + #expect(view != view) + } + + // MARK: - Conditionals + + @Test("An if-else block produces _ConditionalContent") + func ifElseProducesConditionalContent() { + struct Cond: View { + let flag: Bool + var body: some View { + if flag { + Text("yes") + } else { + Spacer() + } + } + } + + #expect(Cond.Body.self == _ConditionalContent.self) + + let buffer = renderToBuffer(Cond(flag: true), context: testContext()) + #expect(buffer.lines.first?.stripped == "yes") + } + + @Test("An if without else keeps optional content") + func ifWithoutElseKeepsOptionalContent() { + struct MaybeView: View { + let flag: Bool + var body: some View { + if flag { + Text("shown") + } + } + } + + #expect(MaybeView.Body.self == Text?.self) + + let shown = renderToBuffer(MaybeView(flag: true), context: testContext()) + #expect(shown.lines.first?.stripped == "shown") + } + + // MARK: - Limited Availability + + @Test("Limited availability content is erased to AnyView") + func limitedAvailabilityErasesToAnyView() { + struct Available: View { + var body: some View { + if #available(macOS 10.15, *) { + Text("modern") + } + } + } + + #expect(Available.Body.self == AnyView?.self) + + let buffer = renderToBuffer(Available(), context: testContext()) + #expect(buffer.lines.first?.stripped == "modern") + } + + // MARK: - AnyView Erasure + + @Test("AnyView supports the erasing initializer") + func anyViewErasingInitializer() { + let erased = AnyView(erasing: Text("erased")) + + let buffer = renderToBuffer(erased, context: testContext()) + #expect(buffer.lines.first?.stripped == "erased") + } +} From fcc7204d66ebf86d49075806aa64d72c89ce48d0 Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 12:09:25 +0200 Subject: [PATCH 2/6] Fix: Adopt SwiftUI's ViewModifier contract with ModifiedContent - Replace the buffer-level public ViewModifier protocol with SwiftUI's body(content:) contract, including the _ViewModifier_Content placeholder that resolves to the wrapped view at its body position - Add the public ModifiedContent value type (unconstrained pair, conditional View and Equatable conformances) and return it from View.modifier(_:) - Move procedural buffer transformations behind the package-internal BufferViewModifier layer with BufferModifiedView; PaddingModifier and BackgroundModifier become internal and .padding()/.background() route through the internal wrapper - Cover the new contract with rendering, environment-propagation, nesting, and value-semantics tests plus a padding regression test --- Sources/TUIkit/Extensions/View+Layout.swift | 8 +- Sources/TUIkit/Extensions/View+Modifier.swift | 10 +- .../TUIkit/Modifiers/BackgroundModifier.swift | 8 +- .../TUIkit/Modifiers/PaddingModifier.swift | 10 +- Sources/TUIkitView/Core/ModifiedContent.swift | 64 ++++++++ Sources/TUIkitView/Core/ViewModifier.swift | 143 ++++++++++++------ Sources/TUIkitView/Rendering/Renderable.swift | 2 +- .../ViewModifierContentTests.swift | 136 +++++++++++++++++ 8 files changed, 319 insertions(+), 62 deletions(-) create mode 100644 Sources/TUIkitView/Core/ModifiedContent.swift create mode 100644 Tests/TUIkitTests/ViewModifierContentTests.swift diff --git a/Sources/TUIkit/Extensions/View+Layout.swift b/Sources/TUIkit/Extensions/View+Layout.swift index 7c40fbf2f..7eaef7a90 100644 --- a/Sources/TUIkit/Extensions/View+Layout.swift +++ b/Sources/TUIkit/Extensions/View+Layout.swift @@ -95,7 +95,7 @@ extension View { /// - Parameter color: The background color. /// - Returns: A view with the background color applied. public func background(_ color: Color) -> some View { - modifier(BackgroundModifier(color: color)) + BufferModifiedView(content: self, modifier: BackgroundModifier(color: color)) } } @@ -235,7 +235,7 @@ extension View { /// - Parameter amount: The padding amount on all sides (default: 1). /// - Returns: A padded view. public func padding(_ amount: Int = 1) -> some View { - modifier(PaddingModifier(insets: EdgeInsets(all: amount))) + BufferModifiedView(content: self, modifier: PaddingModifier(insets: EdgeInsets(all: amount))) } /// Adds padding on specific edges. @@ -261,7 +261,7 @@ extension View { bottom: edges.contains(.bottom) ? amount : 0, trailing: edges.contains(.trailing) ? amount : 0 ) - return modifier(PaddingModifier(insets: insets)) + return BufferModifiedView(content: self, modifier: PaddingModifier(insets: insets)) } /// Adds padding with explicit edge insets. @@ -274,6 +274,6 @@ extension View { /// - Parameter insets: The edge insets. /// - Returns: A padded view. public func padding(_ insets: EdgeInsets) -> some View { - modifier(PaddingModifier(insets: insets)) + BufferModifiedView(content: self, modifier: PaddingModifier(insets: insets)) } } diff --git a/Sources/TUIkit/Extensions/View+Modifier.swift b/Sources/TUIkit/Extensions/View+Modifier.swift index 3d0de2233..f15bd05ea 100644 --- a/Sources/TUIkit/Extensions/View+Modifier.swift +++ b/Sources/TUIkit/Extensions/View+Modifier.swift @@ -7,11 +7,11 @@ // MARK: - View Modifier Extension extension View { - /// Applies a modifier to this view. + /// Applies a modifier to this view and returns a new view. /// - /// - Parameter modifier: The modifier to apply. - /// - Returns: A modified view. - public func modifier(_ modifier: M) -> ModifiedView { - ModifiedView(content: self, modifier: modifier) + /// - Parameter modifier: The modifier to apply to this view. + /// - Returns: A new view with the modifier applied. + public func modifier(_ modifier: M) -> ModifiedContent { + ModifiedContent(content: self, modifier: modifier) } } diff --git a/Sources/TUIkit/Modifiers/BackgroundModifier.swift b/Sources/TUIkit/Modifiers/BackgroundModifier.swift index 9531c1a2c..bfa41cf5d 100644 --- a/Sources/TUIkit/Modifiers/BackgroundModifier.swift +++ b/Sources/TUIkit/Modifiers/BackgroundModifier.swift @@ -6,13 +6,13 @@ /// A modifier that fills the background of a view with a color. /// -/// - Important: This is framework infrastructure. Use `.background()` on any -/// ``View`` instead of instantiating this type directly. -public struct BackgroundModifier: ViewModifier { +/// Framework infrastructure behind `.background()`; operates on rendered +/// buffers through the internal buffer-modifier layer. +struct BackgroundModifier: BufferViewModifier { /// The background color. let color: Color - public func modify(buffer: FrameBuffer, context: RenderContext) -> FrameBuffer { + func modify(buffer: FrameBuffer, context: RenderContext) -> FrameBuffer { guard !buffer.isEmpty else { return buffer } let resolvedColor = color.resolve(with: context.environment.palette) diff --git a/Sources/TUIkit/Modifiers/PaddingModifier.swift b/Sources/TUIkit/Modifiers/PaddingModifier.swift index 14929387b..b4ff35a79 100644 --- a/Sources/TUIkit/Modifiers/PaddingModifier.swift +++ b/Sources/TUIkit/Modifiers/PaddingModifier.swift @@ -85,20 +85,20 @@ public struct Edge: OptionSet, Sendable { /// A modifier that adds padding around a view. /// -/// - Important: This is framework infrastructure. Use `.padding()` on any -/// ``View`` instead of instantiating this type directly. -public struct PaddingModifier: ViewModifier { +/// Framework infrastructure behind `.padding()`; operates on rendered +/// buffers through the internal buffer-modifier layer. +struct PaddingModifier: BufferViewModifier { /// The padding insets. let insets: EdgeInsets - public func adjustContext(_ context: RenderContext) -> RenderContext { + func adjustContext(_ context: RenderContext) -> RenderContext { var adjusted = context adjusted.availableWidth = max(0, context.availableWidth - insets.leading - insets.trailing) adjusted.availableHeight = max(0, context.availableHeight - insets.top - insets.bottom) return adjusted } - public func modify(buffer: FrameBuffer, context: RenderContext) -> FrameBuffer { + func modify(buffer: FrameBuffer, context: RenderContext) -> FrameBuffer { var result: [String] = [] let leadingPad = String(repeating: " ", count: insets.leading) diff --git a/Sources/TUIkitView/Core/ModifiedContent.swift b/Sources/TUIkitView/Core/ModifiedContent.swift new file mode 100644 index 000000000..8bccdaf61 --- /dev/null +++ b/Sources/TUIkitView/Core/ModifiedContent.swift @@ -0,0 +1,64 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// ModifiedContent.swift +// +// Created by LAYERED.work +// License: MIT + +import TUIkitCore + +// MARK: - ModifiedContent + +/// A value with a modifier applied to it. +/// +/// `ModifiedContent` matches SwiftUI's public shape: an unconstrained pair of +/// content and modifier that conforms to ``View`` when its content is a view +/// and its modifier is a ``ViewModifier``. It is the return type of +/// ``View/modifier(_:)``; users normally never construct it directly. +public struct ModifiedContent { + /// The content that the modifier transforms. + public var content: Content + + /// The view modifier applied to the content. + public var modifier: Modifier + + /// Creates a modified content value from a content value and a modifier. + /// + /// - Parameters: + /// - content: The content that the modifier changes. + /// - modifier: The modifier to apply. + public init(content: Content, modifier: Modifier) { + self.content = content + self.modifier = modifier + } +} + +// MARK: - Equatable Conformance + +extension ModifiedContent: Equatable where Content: Equatable, Modifier: Equatable {} + +// MARK: - View Conformance + +extension ModifiedContent: View where Content: View, Modifier: ViewModifier { + /// Never called — rendering is handled by `Renderable` conformance. + public var body: Never { + fatalError("ModifiedContent renders via Renderable") + } +} + +// MARK: - Rendering + +extension ModifiedContent: Renderable where Content: View, Modifier: ViewModifier { + public func renderToBuffer(context: RenderContext) -> FrameBuffer { + // The placeholder resolves to the stored content at whatever tree + // position (and with whatever environment) the modifier body placed + // it. The modifier body itself renders under a child identity so + // state and lifecycle inside the body stay structurally stable. + let content = self.content + let placeholder = _ViewModifier_Content(renderContent: { placeholderContext in + TUIkitView.renderToBuffer(content, context: placeholderContext) + }) + let bodyView = modifier.body(content: placeholder) + let bodyContext = context.withChildIdentity(type: Modifier.Body.self) + return TUIkitView.renderToBuffer(bodyView, context: bodyContext) + } +} diff --git a/Sources/TUIkitView/Core/ViewModifier.swift b/Sources/TUIkitView/Core/ViewModifier.swift index e5c2053b3..7c3bdbbc5 100644 --- a/Sources/TUIkitView/Core/ViewModifier.swift +++ b/Sources/TUIkitView/Core/ViewModifier.swift @@ -5,93 +5,150 @@ // License: MIT import TUIkitCore -/// A modifier that transforms a view's rendered output. -/// -/// `ViewModifier` works on the `FrameBuffer` level: it takes a rendered -/// buffer and returns a transformed buffer. This allows modifiers like -/// `.padding()` and `.frame()` to manipulate layout after rendering. + +// MARK: - ViewModifier + +/// A modifier that you apply to a view, producing a different version of it. /// -/// # Example +/// `ViewModifier` follows SwiftUI's contract: implement ``body(content:)`` +/// and compose the received `content` placeholder with other views: /// /// ```swift -/// struct MyModifier: ViewModifier { -/// func modify(buffer: FrameBuffer, context: RenderContext) -> FrameBuffer { -/// // transform the buffer -/// return buffer +/// struct Framed: ViewModifier { +/// func body(content: Content) -> some View { +/// VStack { +/// Divider() +/// content +/// Divider() +/// } /// } /// } +/// +/// Text("Hello").modifier(Framed()) /// ``` +/// +/// Apply a modifier with ``View/modifier(_:)``, which wraps the view and the +/// modifier in a ``ModifiedContent`` value. +/// +/// Terminal-specific buffer transformations (padding, background fills) are +/// framework infrastructure and live behind the internal buffer-modifier +/// layer, not behind this public protocol. @MainActor public protocol ViewModifier { + /// The type of view produced by ``body(content:)``. + associatedtype Body: View + + /// The view content placeholder passed to ``body(content:)``. + typealias Content = _ViewModifier_Content + + /// Returns the current body of `self`, wrapping the given content. + /// + /// - Parameter content: A placeholder view standing in for the view this + /// modifier was applied to. Place it wherever the modified content + /// should appear. + /// - Returns: The composed replacement view. + @ViewBuilder + func body(content: Self.Content) -> Self.Body +} + +// MARK: - Modifier Content Placeholder + +/// The placeholder view a ``ViewModifier`` receives in `body(content:)`. +/// +/// Matches SwiftUI's `_ViewModifier_Content`: an opaque stand-in for the view +/// the modifier was applied to. Rendering resolves the placeholder to the +/// wrapped view captured by ``ModifiedContent``, with the environment and +/// layout constraints active at the placeholder's position in the modifier +/// body. +public struct _ViewModifier_Content: View { + /// Renders the wrapped content at the placeholder's tree position. + let renderContent: (RenderContext) -> FrameBuffer + + /// Creates a placeholder resolving to the given render step. + init(renderContent: @escaping (RenderContext) -> FrameBuffer) { + self.renderContent = renderContent + } + + public var body: Never { + fatalError("_ViewModifier_Content renders via Renderable") + } +} + +extension _ViewModifier_Content: Renderable { + public func renderToBuffer(context: RenderContext) -> FrameBuffer { + renderContent(context) + } +} + +// MARK: - Buffer Modifier Layer + +/// A procedural modifier operating directly on rendered terminal buffers. +/// +/// This is the internal layer for terminal-specific transformations that +/// cannot be expressed as view composition (padding rows, background fills). +/// Public API never exposes this protocol; user-facing modifiers go through +/// ``ViewModifier`` and ``View/modifier(_:)``. +package protocol BufferViewModifier { /// Transforms a rendered buffer. /// /// - Parameters: /// - buffer: The rendered content of the wrapped view. /// - context: The rendering context. /// - Returns: The modified buffer. + @MainActor func modify(buffer: FrameBuffer, context: RenderContext) -> FrameBuffer /// Adjusts the rendering context before the wrapped content is rendered. /// - /// Override this method in modifiers that consume space (like padding) - /// to reduce `availableWidth` or `availableHeight` so that flexible - /// child views size themselves correctly. - /// - /// The default implementation returns the context unchanged. + /// Modifiers that consume space (like padding) reduce `availableWidth` + /// or `availableHeight` here so flexible child views size themselves + /// correctly. The default implementation returns the context unchanged. /// /// - Parameter context: The current rendering context. /// - Returns: The adjusted context for content rendering. + @MainActor func adjustContext(_ context: RenderContext) -> RenderContext } -extension ViewModifier { - public func adjustContext(_ context: RenderContext) -> RenderContext { +extension BufferViewModifier { + package func adjustContext(_ context: RenderContext) -> RenderContext { context } } -// MARK: - ModifiedView +// MARK: - BufferModifiedView -/// A view that wraps another view with a modifier. -/// -/// This is the return type of modifier methods like `.frame()` and `.padding()`. -/// It is created automatically — users don't instantiate this directly. +/// A view that applies a buffer modifier to its wrapped content. /// -/// `ModifiedView` is a **primitive view**: it declares `body: Never` -/// and conforms to `Renderable`. The rendering system calls -/// `renderToBuffer(context:)` which first renders the -/// wrapped `content`, then applies the modifier's transformation. -/// The `body` property is never called. -/// -/// - Important: This is framework infrastructure. Created automatically by -/// `.modifier()`. Do not instantiate directly. -public struct ModifiedView: View { +/// `BufferModifiedView` is internal framework infrastructure: it renders its +/// content with the modifier-adjusted context, then applies the buffer +/// transformation. Public modifier chains produce ``ModifiedContent`` values +/// instead. +package struct BufferModifiedView: View { /// The original view. - public let content: Content + package let content: Content - /// The modifier to apply. - public let modifier: Modifier + /// The buffer modifier to apply. + package let modifier: Modifier - /// Creates a modified view. + /// Creates a buffer-modified view. /// /// - Parameters: /// - content: The original view. - /// - modifier: The modifier to apply. - public init(content: Content, modifier: Modifier) { + /// - modifier: The buffer modifier to apply. + package init(content: Content, modifier: Modifier) { self.content = content self.modifier = modifier } /// Never called — rendering is handled by `Renderable` conformance. - public var body: Never { - fatalError("ModifiedView renders via Renderable") + package var body: Never { + fatalError("BufferModifiedView renders via Renderable") } } -// MARK: - ModifiedView Rendering - -extension ModifiedView: Renderable { - public func renderToBuffer(context: RenderContext) -> FrameBuffer { +extension BufferModifiedView: Renderable { + package func renderToBuffer(context: RenderContext) -> FrameBuffer { let adjustedContext = modifier.adjustContext(context) let childBuffer = TUIkitView.renderToBuffer(content, context: adjustedContext) return modifier.modify(buffer: childBuffer, context: context) diff --git a/Sources/TUIkitView/Rendering/Renderable.swift b/Sources/TUIkitView/Rendering/Renderable.swift index f47f69991..186ca38be 100644 --- a/Sources/TUIkitView/Rendering/Renderable.swift +++ b/Sources/TUIkitView/Rendering/Renderable.swift @@ -31,7 +31,7 @@ import TUIkitCore /// - **ViewBuilder glue**: `TupleView`, `_ConditionalContent`, `ViewArray` /// - **Interactive views**: `Button`, `ButtonRow`, `Menu`, `StatusBar` /// - **Containers**: `Panel`, `ContainerView`, `Alert`, `Dialog`, `Card` -/// - **Modifiers**: `ModifiedView`, `DimmedModifier`, etc. +/// - **Modifiers**: `BufferModifiedView`, `DimmedModifier`, etc. /// /// All of these declare `body: Never` (which `fatalError`s) because /// their rendering is fully handled by `Renderable`. diff --git a/Tests/TUIkitTests/ViewModifierContentTests.swift b/Tests/TUIkitTests/ViewModifierContentTests.swift new file mode 100644 index 000000000..1b89786de --- /dev/null +++ b/Tests/TUIkitTests/ViewModifierContentTests.swift @@ -0,0 +1,136 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// ViewModifierContentTests.swift +// +// Created by LAYERED.work +// License: MIT + +import Foundation +import Testing + +@testable import TUIkit + +@MainActor +@Suite("ViewModifier body(content:) Contract") +struct ViewModifierContentTests { + + /// Creates an isolated render context for modifier assertions. + private func testContext(width: Int = 40, height: Int = 10) -> RenderContext { + RenderContext( + availableWidth: width, + availableHeight: height, + tuiContext: TUIContext() + ) + } + + // MARK: - Test Modifiers + + /// Wraps its content between two marker lines. + struct Framed: ViewModifier { + func body(content: Content) -> some View { + VStack { + Text("top") + content + Text("bottom") + } + } + } + + /// Passes its content through unchanged. + struct Passthrough: ViewModifier { + func body(content: Content) -> some View { + content + } + } + + /// Tints its content through the environment. + struct Tinted: ViewModifier, Equatable { + let color: Color + + func body(content: Content) -> some View { + content.foregroundStyle(color) + } + } + + // MARK: - Contract + + @Test("modifier(_:) returns ModifiedContent") + func modifierReturnsModifiedContent() { + let modified = Text("base").modifier(Passthrough()) + + #expect(type(of: modified) == ModifiedContent.self) + } + + @Test("A body-based modifier renders content inside its body") + func bodyModifierWrapsContent() { + let modified = Text("mid").modifier(Framed()) + + let buffer = renderToBuffer(modified, context: testContext()) + + #expect(buffer.lines.count == 3) + #expect(buffer.lines[0].stripped.trimmingCharacters(in: .whitespaces) == "top") + #expect(buffer.lines[1].stripped.trimmingCharacters(in: .whitespaces) == "mid") + #expect(buffer.lines[2].stripped.trimmingCharacters(in: .whitespaces) == "bottom") + } + + @Test("A passthrough modifier renders content unchanged") + func passthroughRendersContentUnchanged() { + let plain = renderToBuffer(Text("same"), context: testContext()) + let modified = renderToBuffer( + Text("same").modifier(Passthrough()), + context: testContext() + ) + + #expect(modified.lines.map(\.stripped) == plain.lines.map(\.stripped)) + } + + @Test("Environment values set in the modifier body reach the content") + func environmentFlowsThroughModifierBody() { + let tinted = renderToBuffer( + Text("tinted").modifier(Tinted(color: .red)), + context: testContext() + ) + let plain = renderToBuffer(Text("tinted"), context: testContext()) + + #expect(tinted.lines != plain.lines) + #expect(tinted.lines[0].contains("31") || tinted.lines[0].contains("38;")) + } + + @Test("Nested modifiers apply outside-in") + func nestedModifiersApply() { + let nested = Text("core") + .modifier(Passthrough()) + .modifier(Framed()) + + let buffer = renderToBuffer(nested, context: testContext()) + + #expect(buffer.lines.count == 3) + #expect(buffer.lines[1].stripped.trimmingCharacters(in: .whitespaces) == "core") + } + + // MARK: - Value Semantics + + @Test("ModifiedContent exposes content and modifier and equates by parts") + func modifiedContentValueSemantics() { + let first = ModifiedContent(content: Text("a"), modifier: Tinted(color: .red)) + let second = ModifiedContent(content: Text("a"), modifier: Tinted(color: .red)) + let third = ModifiedContent(content: Text("a"), modifier: Tinted(color: .blue)) + + #expect(first.content == Text("a")) + #expect(first.modifier == Tinted(color: .red)) + #expect(first == second) + #expect(first != third) + } + + // MARK: - Buffer Modifier Regression + + @Test("Padding still wraps content after the contract change") + func paddingStillWorks() { + let padded = renderToBuffer( + Text("pad").padding(1), + context: testContext() + ) + + #expect(padded.lines.count == 3) + #expect(padded.lines[1].stripped == " pad ") + } +} From 8a73ec32bcc9aef097d4756751fe4b09d488d9d1 Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 12:09:42 +0200 Subject: [PATCH 3/6] Chore: Allow SwiftUI-parity underscore type names in SwiftLint - Add _ViewModifier_Content to the type_name exclusion list --- .swiftlint.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 699e7a9a2..ef9170cf6 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -113,8 +113,9 @@ type_name: excluded: - ID # Internal _Core views follow SwiftUI pattern (e.g. _AlertCore, _DialogCore) - # _ConditionalContent matches SwiftUI's public builder result type name + # _ConditionalContent and _ViewModifier_Content match SwiftUI's public type names - _ConditionalContent + - _ViewModifier_Content - _AlertCore - _DialogCore - _PanelCore From 82de831d7cbfc916599c1ba62be58682d4ca0e3f Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 12:15:35 +0200 Subject: [PATCH 4/6] Fix: Match SwiftUI's Swift 6.0 isolation contract on core protocols - Add @preconcurrency to the View and ViewModifier protocols per the pinned SwiftUI reference surface - Document why ViewBuilder stays @MainActor as a compiler-floor exception: Swift 6.0 infers main-actor isolation onto conforming types' initializers, which the builder must call - Extend the issue-17 compile fixture with custom views, conditional and availability builder forms, generic content, a custom modifier, and nested modified content --- Sources/TUIkitView/Core/View.swift | 4 ++ Sources/TUIkitView/Core/ViewBuilder.swift | 5 ++ Sources/TUIkitView/Core/ViewModifier.swift | 1 + .../CompileContracts/CorePositive.swift | 71 +++++++++++++++++++ 4 files changed, 81 insertions(+) diff --git a/Sources/TUIkitView/Core/View.swift b/Sources/TUIkitView/Core/View.swift index ce7757ab8..18d757038 100644 --- a/Sources/TUIkitView/Core/View.swift +++ b/Sources/TUIkitView/Core/View.swift @@ -54,6 +54,10 @@ import TUIkitCore /// /// Like SwiftUI, all view operations are confined to the main actor. /// This ensures thread-safe access to state, environment, and rendering. +/// The `@preconcurrency` isolation matches SwiftUI's Swift 6.0 contract: +/// conformances written for pre-concurrency code keep compiling while new +/// code gets full main-actor checking. +@preconcurrency @MainActor public protocol View { /// The type of the body view. diff --git a/Sources/TUIkitView/Core/ViewBuilder.swift b/Sources/TUIkitView/Core/ViewBuilder.swift index 109ecbec9..26cfa060a 100644 --- a/Sources/TUIkitView/Core/ViewBuilder.swift +++ b/Sources/TUIkitView/Core/ViewBuilder.swift @@ -25,6 +25,11 @@ import TUIkitCore /// - Conditionals (`if`, `if-else`) /// - Optional views (`if let`) /// - Arrays of views (`for-in`) +/// +/// SwiftUI's builder is nonisolated; TUIkit keeps `@MainActor` because Swift +/// 6.0 infers main-actor isolation from the `View` protocol onto every +/// conforming type's initializers, which the builder must call. Documented as +/// a compiler-floor exception in the compatibility manifest. @MainActor @resultBuilder public struct ViewBuilder { diff --git a/Sources/TUIkitView/Core/ViewModifier.swift b/Sources/TUIkitView/Core/ViewModifier.swift index 7c3bdbbc5..720e419e2 100644 --- a/Sources/TUIkitView/Core/ViewModifier.swift +++ b/Sources/TUIkitView/Core/ViewModifier.swift @@ -33,6 +33,7 @@ import TUIkitCore /// Terminal-specific buffer transformations (padding, background fills) are /// framework infrastructure and live behind the internal buffer-modifier /// layer, not behind this public protocol. +@preconcurrency @MainActor public protocol ViewModifier { /// The type of view produced by ``body(content:)``. diff --git a/Tools/APICompatibility/Configuration/CompileContracts/CorePositive.swift b/Tools/APICompatibility/Configuration/CompileContracts/CorePositive.swift index 3dde724d4..f6109543d 100644 --- a/Tools/APICompatibility/Configuration/CompileContracts/CorePositive.swift +++ b/Tools/APICompatibility/Configuration/CompileContracts/CorePositive.swift @@ -1,6 +1,77 @@ import TUIkit +// Representative SwiftUI-style declarations that must compile unchanged +// under Swift 6.0: custom views, builder forms, custom modifiers, generic +// content, type erasure, and nested modified content. + @MainActor func erasedView() -> AnyView { AnyView(Text("Core")) } + +@MainActor +private struct CustomView: View { + var body: some View { + Text("custom") + } +} + +@MainActor +private struct ConditionalBuilders: View { + let flag: Bool + let optionalText: String? + + var body: some View { + VStack { + if flag { + Text("true branch") + } else { + Text("false branch") + } + if let optionalText { + Text(optionalText) + } + if #available(macOS 10.15, *) { + Text("available") + } + } + } +} + +@MainActor +private struct GenericContent: View { + let content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + var body: some View { + content + } +} + +private struct FramedModifier: ViewModifier { + func body(content: Content) -> some View { + VStack { + content + } + } +} + +@MainActor +private func nestedModifiedContent() -> ModifiedContent< + ModifiedContent, FramedModifier +> { + Text("nested") + .modifier(FramedModifier()) + .modifier(FramedModifier()) +} + +@MainActor +private func genericComposition() -> some View { + GenericContent { + CustomView() + ConditionalBuilders(flag: true, optionalText: nil) + } +} From 88b263a85ecbeee335f0ac2448a96c4037ddfc23 Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 12:28:08 +0200 Subject: [PATCH 5/6] Docs: Describe the SwiftUI modifier contract and fix symbol links - Rewrite the modifier sections in CustomViews, Architecture, and RenderCycle for body(content:), ModifiedContent, and the internal buffer layer - Replace links to removed or underscored symbols (ModifiedView, modify(buffer:context:), _ConditionalContent) that DocC can no longer resolve --- .../TUIkit.docc/Articles/Architecture.md | 8 ++- .../TUIkit.docc/Articles/CustomViews.md | 49 +++++++++---------- .../TUIkit.docc/Articles/RenderCycle.md | 27 +++++++--- .../TUIkit.docc/Articles/StateManagement.md | 2 +- Sources/TUIkit/TUIkit.docc/TUIkit.md | 2 +- .../TUIkitCore/Rendering/FrameBuffer.swift | 2 +- .../TUIkitCore/Rendering/ViewIdentity.swift | 2 +- .../TUIkitView/Rendering/RenderContext.swift | 2 +- Sources/TUIkitView/State/StateStorage.swift | 2 +- 9 files changed, 57 insertions(+), 39 deletions(-) diff --git a/Sources/TUIkit/TUIkit.docc/Articles/Architecture.md b/Sources/TUIkit/TUIkit.docc/Articles/Architecture.md index 478a17018..b17a75f00 100644 --- a/Sources/TUIkit/TUIkit.docc/Articles/Architecture.md +++ b/Sources/TUIkit/TUIkit.docc/Articles/Architecture.md @@ -43,7 +43,13 @@ This enables spacers, flexible text fields, and proportional sizing. See FrameBuffer { - var result = FrameBuffer( - emptyWithWidth: max(0, columns), - height: buffer.height - ) - if !buffer.isEmpty { - result.appendHorizontally(buffer) + func body(content: Content) -> some View { + VStack { + Text(title).bold() + content } - return result + .padding() + .border() } } ``` -Apply it using `.modifier(_:)`: +Apply it using `.modifier(_:)`, which produces a ``ModifiedContent`` value: ```swift -Text("Indented") - .modifier(IndentModifier(columns: 2)) +Text("Details") + .modifier(SectionCard(title: "Info")) ``` ### Convenience Extensions @@ -151,24 +151,23 @@ For a cleaner API, add a `View` extension: ```swift extension View { - func indented(_ columns: Int = 2) -> some View { - modifier(IndentModifier(columns: columns)) + func sectionCard(_ title: String) -> some View { + modifier(SectionCard(title: title)) } } // Usage -Text("Indented").indented(4) +Text("Details").sectionCard("Info") ``` ### When to Use ViewModifier -Use ``ViewModifier`` when your transformation is a pure buffer-to-buffer operation: adding visual effects, changing backgrounds, or adjusting layout after rendering. The ``RenderContext`` gives you access to: - -| Property | Description | -|----------|-------------| -| `availableWidth` | Maximum width in columns for this view | -| `availableHeight` | Maximum height in rows for this view | -| `environment` | Current ``EnvironmentValues`` (palette, focus manager, etc.) | +Use ``ViewModifier`` whenever a reusable decoration or wrapping applies to +arbitrary content: cards, frames, badges, spacing conventions, environment +tweaks. Everything the modifier body sets (environment values, padding, +borders) flows into the wrapped content exactly as if it were written +inline. Procedural buffer transformations remain framework-internal; custom +modifiers compose existing views and modifiers instead. ## Type Erasure with AnyView @@ -209,7 +208,7 @@ let view = Text("Hello").asAnyView() ### Supporting Types - ``ViewBuilder`` -- ``ModifiedView`` +- ``ModifiedContent`` - ``AnyView`` - ``RenderContext`` - ``FrameBuffer`` diff --git a/Sources/TUIkit/TUIkit.docc/Articles/RenderCycle.md b/Sources/TUIkit/TUIkit.docc/Articles/RenderCycle.md index 6b6745bc8..29fe18ad8 100644 --- a/Sources/TUIkit/TUIkit.docc/Articles/RenderCycle.md +++ b/Sources/TUIkit/TUIkit.docc/Articles/RenderCycle.md @@ -225,7 +225,7 @@ This path is used by: - **Layout containers**: `VStack`, `HStack`, `ZStack` - **Interactive views**: ``Button``, ``ButtonRow``, ``Menu`` - **Container views**: ``Panel``, ``Card``, ``Alert``, ``Dialog`` -- **Modifier wrappers**: `ModifiedView`, `DimmedModifier`, `OverlayModifier`, `EnvironmentModifier`, ``EquatableView``, and all lifecycle modifiers +- **Modifier wrappers**: ``ModifiedContent``, `BufferModifiedView`, `DimmedModifier`, `OverlayModifier`, `EnvironmentModifier`, ``EquatableView``, and all lifecycle modifiers ### Path 2: Composition (body) @@ -338,20 +338,33 @@ traversal, and never for unchanged values. ## ViewModifier Pipeline -TUIkit has two modifier architectures: +TUIkit has three modifier architectures: -### Buffer Modifiers (ViewModifier protocol) +### Public ViewModifier (body composition) -These transform a ``FrameBuffer`` after the content has rendered: +The public ``ViewModifier`` protocol follows SwiftUI's contract: the modifier +composes a replacement view around a placeholder for the modified content. ```swift public protocol ViewModifier { - func modify(buffer: FrameBuffer, context: RenderContext) -> FrameBuffer - func adjustContext(_ context: RenderContext) -> RenderContext // default: returns context unchanged + associatedtype Body: View + typealias Content = _ViewModifier_Content + @ViewBuilder func body(content: Self.Content) -> Self.Body } ``` -`ModifiedView` wraps a view and a modifier. It first calls `adjustContext(_:)` to let the modifier reduce available space (e.g. padding), then renders the content, then calls `modify(buffer:context:)`. Examples: +`.modifier(_:)` wraps the view and the modifier in a ``ModifiedContent`` +value. Rendering evaluates `body(content:)` with a placeholder that resolves +to the wrapped view at its position in the modifier body — with the +environment and layout constraints active there, exactly as if the content +had been written inline. + +### Buffer Modifiers (internal) + +Terminal-specific transformations that cannot be expressed as composition +run behind the internal `BufferViewModifier` layer. `BufferModifiedView` +first lets the modifier adjust the context (e.g. padding reduces available +space), then renders the content, then transforms the rendered buffer: - **`PaddingModifier`**: Adds empty lines (top/bottom) and spaces (leading/trailing) around the buffer - **`BackgroundModifier`**: Paints the background style directly onto cells and fills each row to the surface width diff --git a/Sources/TUIkit/TUIkit.docc/Articles/StateManagement.md b/Sources/TUIkit/TUIkit.docc/Articles/StateManagement.md index 79611b663..1dc57fc6d 100644 --- a/Sources/TUIkit/TUIkit.docc/Articles/StateManagement.md +++ b/Sources/TUIkit/TUIkit.docc/Articles/StateManagement.md @@ -162,7 +162,7 @@ from earlier view values become inert. Registrations are removed when their iden ### Garbage Collection Views that disappear from the tree (e.g., a conditional branch switches) have their state -automatically cleaned up at the end of each render pass. `ConditionalView` also immediately +automatically cleaned up at the end of each render pass. `_ConditionalContent` also immediately invalidates the inactive branch's state to prevent stale values. This is simple and predictable: the view tree is fully re-evaluated each frame (no virtual DOM), with persistent state. Terminal output is then diffed at the line level: only changed lines are written. See for details on the output optimization pipeline. diff --git a/Sources/TUIkit/TUIkit.docc/TUIkit.md b/Sources/TUIkit/TUIkit.docc/TUIkit.md index 13213a4c1..fb9e5cb46 100644 --- a/Sources/TUIkit/TUIkit.docc/TUIkit.md +++ b/Sources/TUIkit/TUIkit.docc/TUIkit.md @@ -188,7 +188,7 @@ struct MyApp: App { - ``ViewBuilder`` - ``ViewModifier`` -- ``ModifiedView`` +- ``ModifiedContent`` - ``EquatableView`` ### Focus System diff --git a/Sources/TUIkitCore/Rendering/FrameBuffer.swift b/Sources/TUIkitCore/Rendering/FrameBuffer.swift index 2c34f555b..789e7f2dd 100644 --- a/Sources/TUIkitCore/Rendering/FrameBuffer.swift +++ b/Sources/TUIkitCore/Rendering/FrameBuffer.swift @@ -16,7 +16,7 @@ /// normalized style state, and transparency explicitly. /// /// - Important: This is framework infrastructure used as the rendering primitive in -/// ``ViewModifier/modify(buffer:context:)``. Most developers don't need to interact +/// the internal buffer-modifier layer. Most developers don't need to interact /// with this type directly. public struct FrameBuffer: Sendable, Equatable { private var surface: TerminalSurface diff --git a/Sources/TUIkitCore/Rendering/ViewIdentity.swift b/Sources/TUIkitCore/Rendering/ViewIdentity.swift index c0fd8829f..e1ed00245 100644 --- a/Sources/TUIkitCore/Rendering/ViewIdentity.swift +++ b/Sources/TUIkitCore/Rendering/ViewIdentity.swift @@ -85,7 +85,7 @@ public extension ViewIdentity { /// Returns a child identity by appending a branch label. /// - /// Used by ``_ConditionalContent`` to distinguish between the + /// Used by `_ConditionalContent` to distinguish between the /// `true` and `false` branches of an `if-else`. /// /// - Parameter label: The branch label (`"true"` or `"false"`). diff --git a/Sources/TUIkitView/Rendering/RenderContext.swift b/Sources/TUIkitView/Rendering/RenderContext.swift index 3792450f9..182a0f396 100644 --- a/Sources/TUIkitView/Rendering/RenderContext.swift +++ b/Sources/TUIkitView/Rendering/RenderContext.swift @@ -18,7 +18,7 @@ import TUIkitCore /// view tree has been rendered into a ``FrameBuffer``. /// /// - Important: This is framework infrastructure passed to -/// ``ViewModifier/modify(buffer:context:)``. Most developers only need +/// the internal buffer-modifier layer. Most developers only need /// ``availableWidth``, ``availableHeight``, and ``environment``. public struct RenderContext { /// The available width in characters. diff --git a/Sources/TUIkitView/State/StateStorage.swift b/Sources/TUIkitView/State/StateStorage.swift index c27e76dc6..646682ae1 100644 --- a/Sources/TUIkitView/State/StateStorage.swift +++ b/Sources/TUIkitView/State/StateStorage.swift @@ -202,7 +202,7 @@ extension StateStorage { /// Removes all state for descendants of the given identity. /// - /// Called by ``_ConditionalContent`` when switching branches to clean up + /// Called by `_ConditionalContent` when switching branches to clean up /// state from the now-inactive branch. /// /// - Parameter ancestor: The branch identity whose descendants should be removed. From 8e2d1414c6bcec353b22f497ab9c36a8bc878823 Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 22 Jul 2026 12:45:21 +0200 Subject: [PATCH 6/6] Chore: Regenerate the API compatibility manifest for the view contracts - Drop overrides for removed symbols: the buffer-level ViewModifier requirements, ModifiedView, ConditionalView, the pack-generic TupleView members, buildOptional, and the internalized padding and background modifiers - Add overrides for the new surface: ModifiedContent, the body(content:) contract, tuple-typed TupleView members, buildIf, buildBlock(), AnyView(erasing:), and the AnyView-erasing buildLimitedAvailability - Regenerate the manifest with TUIkitAPICheck against fresh 6.0.3 macOS and Linux snapshots --- .../Configuration/compatibility-manifest.json | 152 ++++------ .../Configuration/review-policy.json | 260 +++++++----------- 2 files changed, 156 insertions(+), 256 deletions(-) diff --git a/Tools/APICompatibility/Configuration/compatibility-manifest.json b/Tools/APICompatibility/Configuration/compatibility-manifest.json index a902b7c51..6286949e3 100644 --- a/Tools/APICompatibility/Configuration/compatibility-manifest.json +++ b/Tools/APICompatibility/Configuration/compatibility-manifest.json @@ -337884,52 +337884,22 @@ { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView011ConditionalB0O" - }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView011ConditionalB0O10childInfos7contextSayAA9ChildInfoVGAA13RenderContextV_tF" - }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView011ConditionalB0O10childViews7contextSayAA05ChildB0VGAA13RenderContextV_tF" - }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView011ConditionalB0O11trueContentyACyxq_GxcAEmAA0B0RzAaFR_r0_lF" - }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView011ConditionalB0O12falseContentyACyxq_Gq_cAEmAA0B0RzAaFR_r0_lF" - }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView011ConditionalB0O14renderToBuffer7context0A4Core05FrameF0VAA13RenderContextV_tF" - }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView011ConditionalB0O4bodys5NeverOvp" + "symbolID" : "s:10TUIkitView03AnyB0V" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView03AnyB0V" + "symbolID" : "s:10TUIkitView03AnyB0V14renderToBuffer7context0A4Core05FrameF0VAA13RenderContextV_tF" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView03AnyB0V14renderToBuffer7context0A4Core05FrameF0VAA13RenderContextV_tF" + "symbolID" : "s:10TUIkitView03AnyB0V4bodys5NeverOvp" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView03AnyB0V4bodys5NeverOvp" + "symbolID" : "s:10TUIkitView03AnyB0V7erasingACx_tcAA0B0Rzlufc" }, { "classification" : "implementationLeak", @@ -338024,47 +337994,22 @@ { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView05TupleB0V4bodys5NeverOvp" - }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView05TupleB0V8childrenxxQp_tvp" - }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView05TupleB0VAASQRzrlE2eeoiySbACyxxQp_QPG_AEtFZ" - }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView08ModifiedB0V" - }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView08ModifiedB0V14renderToBuffer7context0A4Core05FrameF0VAA13RenderContextV_tF" - }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView08ModifiedB0V4bodys5NeverOvp" + "symbolID" : "s:10TUIkitView05TupleB0V2eeoiySbACyxG_AEtFZ" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView08ModifiedB0V7content8modifierACyxq_Gx_q_tcfc" + "symbolID" : "s:10TUIkitView05TupleB0V4bodys5NeverOvp" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView08ModifiedB0V7contentxvp" + "symbolID" : "s:10TUIkitView05TupleB0V5valuexvp" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView08ModifiedB0V8modifierq_vp" + "symbolID" : "s:10TUIkitView05TupleB0VyACyxGxcfc" }, { "classification" : "implementationLeak", @@ -338294,7 +338239,7 @@ { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView0B0P0A0E4task8priority_QrScP_yyYaYbctF" + "symbolID" : "s:10TUIkitView0B0P0A0E4task8priority_QrScP_yyYactF" }, { "classification" : "implementationLeak", @@ -338374,7 +338319,7 @@ { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView0B0P0A0E8modifieryAA08ModifiedB0Vyxqd__Gqd__AA0B8ModifierRd__lF" + "symbolID" : "s:10TUIkitView0B0P0A0E8modifieryAA15ModifiedContentVyxqd__Gqd__lF" }, { "classification" : "implementationLeak", @@ -338459,42 +338404,42 @@ { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView0B7BuilderV10buildBlockyAA05TupleB0VyxxQp_QPGxxQpRvzAA0B0RzlFZ" + "symbolID" : "s:10TUIkitView0B7BuilderV10buildBlockAA05EmptyB0VyFZ" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView0B7BuilderV10buildBlockyxxAA0B0RzlFZ" + "symbolID" : "s:10TUIkitView0B7BuilderV10buildBlockyAA05TupleB0VyxxQp_tGxxQpRvzAA0B0RzlFZ" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView0B7BuilderV11buildEither5firstAA011ConditionalB0Oyxq_Gx_tAA0B0RzAaIR_r0_lFZ" + "symbolID" : "s:10TUIkitView0B7BuilderV10buildBlockyxxAA0B0RzlFZ" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView0B7BuilderV11buildEither6secondAA011ConditionalB0Oyxq_Gq__tAA0B0RzAaIR_r0_lFZ" + "symbolID" : "s:10TUIkitView0B7BuilderV11buildEither5firstAA19_ConditionalContentOyxq_Gx_tAA0B0RzAaIR_r0_lFZ" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView0B7BuilderV13buildOptionalyxSgAeA0B0RzlFZ" + "symbolID" : "s:10TUIkitView0B7BuilderV11buildEither6secondAA19_ConditionalContentOyxq_Gq__tAA0B0RzAaIR_r0_lFZ" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView0B7BuilderV15buildExpressionyxSgAeA0B0RzlFZ" + "symbolID" : "s:10TUIkitView0B7BuilderV15buildExpressionyxxAA0B0RzlFZ" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView0B7BuilderV15buildExpressionyxxAA0B0RzlFZ" + "symbolID" : "s:10TUIkitView0B7BuilderV24buildLimitedAvailabilityyAA03AnyB0VxAA0B0RzlFZ" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView0B7BuilderV24buildLimitedAvailabilityyxxAA0B0RzlFZ" + "symbolID" : "s:10TUIkitView0B7BuilderV7buildIfyxSgAeA0B0RzlFZ" }, { "classification" : "implementationLeak", @@ -338504,17 +338449,17 @@ { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView0B8ModifierP13adjustContextyAA06RenderE0VAFF" + "symbolID" : "s:10TUIkitView0B8ModifierP4BodyQa" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView0B8ModifierP6modify6buffer7context0A4Core11FrameBufferVAI_AA13RenderContextVtF" + "symbolID" : "s:10TUIkitView0B8ModifierP4body7content4BodyQzAA01_bC8_ContentVyxG_tF" }, { "classification" : "implementationLeak", "ownerIssue" : "#35", - "symbolID" : "s:10TUIkitView0B8ModifierPAAE13adjustContextyAA06RenderE0VAFF" + "symbolID" : "s:10TUIkitView0B8ModifierP7Contenta" }, { "classification" : "implementationLeak", @@ -338946,6 +338891,36 @@ "ownerIssue" : "#35", "symbolID" : "s:10TUIkitView14renderToBuffer_7context0A4Core05FrameE0Vx_AA13RenderContextVtAA0B0RzlF" }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView15ModifiedContentV" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView15ModifiedContentV7content8modifierACyxq_Gx_q_tcfc" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView15ModifiedContentV7contentxvp" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView15ModifiedContentV8modifierq_vp" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView15ModifiedContentVA2A0B0RzAA0B8ModifierR_rlE14renderToBuffer7context0A4Core05FrameH0VAA13RenderContextV_tF" + }, + { + "classification" : "implementationLeak", + "ownerIssue" : "#35", + "symbolID" : "s:10TUIkitView15ModifiedContentVA2A0B0RzAA0B8ModifierR_rlE4bodys5NeverOvp" + }, { "classification" : "implementationLeak", "ownerIssue" : "#35", @@ -342621,21 +342596,6 @@ "ownerIssue" : "#35", "symbolID" : "s:6TUIkit15OverlayModifierVAASQRzSQR_rlE2eeoiySbACyxq_G_AEtFZ" }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:6TUIkit15PaddingModifierV" - }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:6TUIkit15PaddingModifierV13adjustContexty0A4View06RenderE0VAGF" - }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:6TUIkit15PaddingModifierV6modify6buffer7context0A4Core11FrameBufferVAI_0A4View13RenderContextVtF" - }, { "classification" : "implementationLeak", "ownerIssue" : "#35", @@ -342901,16 +342861,6 @@ "ownerIssue" : "#35", "symbolID" : "s:6TUIkit17extractBadgeValue4fromAA0cD0OSgx_t0A4View0F0RzlF" }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:6TUIkit18BackgroundModifierV" - }, - { - "classification" : "implementationLeak", - "ownerIssue" : "#35", - "symbolID" : "s:6TUIkit18BackgroundModifierV6modify6buffer7context0A4Core11FrameBufferVAI_0A4View13RenderContextVtF" - }, { "classification" : "implementationLeak", "ownerIssue" : "#35", diff --git a/Tools/APICompatibility/Configuration/review-policy.json b/Tools/APICompatibility/Configuration/review-policy.json index 9cc6920b4..c8beedbed 100644 --- a/Tools/APICompatibility/Configuration/review-policy.json +++ b/Tools/APICompatibility/Configuration/review-policy.json @@ -18403,31 +18403,6 @@ "ownerIssue": "#35", "symbolID": "s:10TUIkitCore8ViewSizeV8flexible8minWidth0F6HeightACSi_SitFZ" }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView011ConditionalB0O" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView011ConditionalB0O11trueContentyACyxq_GxcAEmAA0B0RzAaFR_r0_lF" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView011ConditionalB0O12falseContentyACyxq_Gq_cAEmAA0B0RzAaFR_r0_lF" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView011ConditionalB0O14renderToBuffer7context0A4Core05FrameF0VAA13RenderContextV_tF" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView011ConditionalB0O4bodys5NeverOvp" - }, { "action": "implementationLeak", "ownerIssue": "#35", @@ -18538,46 +18513,6 @@ "ownerIssue": "#35", "symbolID": "s:10TUIkitView05TupleB0V4bodys5NeverOvp" }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView05TupleB0V8childrenxxQp_tvp" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView05TupleB0VAASQRzrlE2eeoiySbACyxxQp_QPG_AEtFZ" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView08ModifiedB0V" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView08ModifiedB0V14renderToBuffer7context0A4Core05FrameF0VAA13RenderContextV_tF" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView08ModifiedB0V4bodys5NeverOvp" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView08ModifiedB0V7content8modifierACyxq_Gx_q_tcfc" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView08ModifiedB0V7contentxvp" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView08ModifiedB0V8modifierq_vp" - }, { "action": "implementationLeak", "ownerIssue": "#35", @@ -18803,11 +18738,6 @@ "ownerIssue": "#35", "symbolID": "s:10TUIkitView0B0P0A0E23imagePlaceholderSpinneryQrSbF" }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView0B0P0A0E4task8priority_QrScP_yyYaYbctF" - }, { "action": "implementationLeak", "ownerIssue": "#35", @@ -18883,11 +18813,6 @@ "ownerIssue": "#35", "symbolID": "s:10TUIkitView0B0P0A0E7paletteyQr0A7Styling7Palette_pF" }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView0B0P0A0E8modifieryAA08ModifiedB0Vyxqd__Gqd__AA0B8ModifierRd__lF" - }, { "action": "implementationLeak", "ownerIssue": "#35", @@ -18963,66 +18888,21 @@ "ownerIssue": "#35", "symbolID": "s:10TUIkitView0B7BuilderV10buildArrayyAA0bE0VyxGSayxGAA0B0RzlFZ" }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView0B7BuilderV10buildBlockyAA05TupleB0VyxxQp_QPGxxQpRvzAA0B0RzlFZ" - }, { "action": "implementationLeak", "ownerIssue": "#35", "symbolID": "s:10TUIkitView0B7BuilderV10buildBlockyxxAA0B0RzlFZ" }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView0B7BuilderV11buildEither5firstAA011ConditionalB0Oyxq_Gx_tAA0B0RzAaIR_r0_lFZ" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView0B7BuilderV11buildEither6secondAA011ConditionalB0Oyxq_Gq__tAA0B0RzAaIR_r0_lFZ" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView0B7BuilderV13buildOptionalyxSgAeA0B0RzlFZ" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView0B7BuilderV15buildExpressionyxSgAeA0B0RzlFZ" - }, { "action": "implementationLeak", "ownerIssue": "#35", "symbolID": "s:10TUIkitView0B7BuilderV15buildExpressionyxxAA0B0RzlFZ" }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView0B7BuilderV24buildLimitedAvailabilityyxxAA0B0RzlFZ" - }, { "action": "implementationLeak", "ownerIssue": "#35", "symbolID": "s:10TUIkitView0B8ModifierP" }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView0B8ModifierP13adjustContextyAA06RenderE0VAFF" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView0B8ModifierP6modify6buffer7context0A4Core11FrameBufferVAI_AA13RenderContextVtF" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView0B8ModifierPAAE13adjustContextyAA06RenderE0VAFF" - }, { "action": "implementationLeak", "ownerIssue": "#35", @@ -23093,21 +22973,6 @@ "ownerIssue": "#35", "symbolID": "s:6TUIkit15OverlayModifierVAASQRzSQR_rlE2eeoiySbACyxq_G_AEtFZ" }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:6TUIkit15PaddingModifierV" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:6TUIkit15PaddingModifierV13adjustContexty0A4View06RenderE0VAGF" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:6TUIkit15PaddingModifierV6modify6buffer7context0A4Core11FrameBufferVAI_0A4View13RenderContextVtF" - }, { "action": "implementationLeak", "ownerIssue": "#35", @@ -23373,16 +23238,6 @@ "ownerIssue": "#35", "symbolID": "s:6TUIkit17extractBadgeValue4fromAA0cD0OSgx_t0A4View0F0RzlF" }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:6TUIkit18BackgroundModifierV" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:6TUIkit18BackgroundModifierV6modify6buffer7context0A4Core11FrameBufferVAI_0A4View13RenderContextVtF" - }, { "action": "implementationLeak", "ownerIssue": "#35", @@ -25008,16 +24863,6 @@ "ownerIssue": "#35", "symbolID": "s:6TUIkit7ForEachV_2id7contentACyxq_q0_Gx_s7KeyPathCy7ElementQzq_Gq0_AJctcfc" }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView011ConditionalB0O10childInfos7contextSayAA9ChildInfoVGAA13RenderContextV_tF" - }, - { - "action": "implementationLeak", - "ownerIssue": "#35", - "symbolID": "s:10TUIkitView011ConditionalB0O10childViews7contextSayAA05ChildB0VGAA13RenderContextV_tF" - }, { "action": "implementationLeak", "ownerIssue": "#35", @@ -25927,6 +25772,111 @@ "action": "tuiSpecific", "ownerIssue": "#35", "symbolID": "s:6TUIkit23StoragePersistenceErrorV9operationAC9OperationOvp" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView03AnyB0V7erasingACx_tcAA0B0Rzlufc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView05TupleB0V2eeoiySbACyxG_AEtFZ" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView05TupleB0V5valuexvp" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView05TupleB0VyACyxGxcfc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView0B0P0A0E4task8priority_QrScP_yyYactF" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView0B0P0A0E8modifieryAA15ModifiedContentVyxqd__Gqd__lF" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView0B7BuilderV10buildBlockAA05EmptyB0VyFZ" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView0B7BuilderV10buildBlockyAA05TupleB0VyxxQp_tGxxQpRvzAA0B0RzlFZ" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView0B7BuilderV11buildEither5firstAA19_ConditionalContentOyxq_Gx_tAA0B0RzAaIR_r0_lFZ" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView0B7BuilderV11buildEither6secondAA19_ConditionalContentOyxq_Gq__tAA0B0RzAaIR_r0_lFZ" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView0B7BuilderV24buildLimitedAvailabilityyAA03AnyB0VxAA0B0RzlFZ" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView0B7BuilderV7buildIfyxSgAeA0B0RzlFZ" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView0B8ModifierP4BodyQa" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView0B8ModifierP4body7content4BodyQzAA01_bC8_ContentVyxG_tF" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView0B8ModifierP7Contenta" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView15ModifiedContentV" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView15ModifiedContentV7content8modifierACyxq_Gx_q_tcfc" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView15ModifiedContentV7contentxvp" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView15ModifiedContentV8modifierq_vp" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView15ModifiedContentVA2A0B0RzAA0B8ModifierR_rlE14renderToBuffer7context0A4Core05FrameH0VAA13RenderContextV_tF" + }, + { + "action": "implementationLeak", + "ownerIssue": "#35", + "symbolID": "s:10TUIkitView15ModifiedContentVA2A0B0RzAA0B8ModifierR_rlE4bodys5NeverOvp" } ] }