diff --git a/Package.swift b/Package.swift index fb425d1..3383905 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,7 @@ let package = Package( // Minimum deployment targets for Apple platforms // Linux is automatically supported (no platform specification needed) platforms: [ - .macOS(.v13) + .macOS(.v14) ], products: [ .library( diff --git a/Sources/TUIKit/Core/TupleViews.swift b/Sources/TUIKit/Core/TupleViews.swift index 018f977..26fbabe 100644 --- a/Sources/TUIKit/Core/TupleViews.swift +++ b/Sources/TUIKit/Core/TupleViews.swift @@ -1,143 +1,30 @@ -// swiftlint:disable large_tuple // // TupleViews.swift // TUIKit // -// Container types for multiple views in ViewBuilder. +// Container type for multiple views in ViewBuilder, using Swift Parameter Packs. // -// MARK: - TupleView2 +/// A view that contains multiple child views packed via a parameter pack. +/// +/// `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. +/// +/// `TupleView` is created automatically by `ViewBuilder` when multiple +/// views appear in a `@ViewBuilder` closure. +public struct TupleView: View { + /// The packed child views. + public let children: (repeat each V) -/// A view that contains two child views. -public struct TupleView2: View { - public let value: (V0, V1) - - public init(_ v0: V0, _ v1: V1) { - self.value = (v0, v1) + /// Creates a tuple view from a parameter pack of child views. + /// + /// - Parameter children: The child views. + public init(_ children: repeat each V) { + self.children = (repeat each children) } public var body: Never { - fatalError("TupleView2 renders its children directly") + fatalError("TupleView renders its children directly") } } - -// MARK: - TupleView3 - -/// A view that contains three child views. -public struct TupleView3: View { - public let value: (V0, V1, V2) - - public init(_ v0: V0, _ v1: V1, _ v2: V2) { - self.value = (v0, v1, v2) - } - - public var body: Never { - fatalError("TupleView3 renders its children directly") - } -} - -// MARK: - TupleView4 - -/// A view that contains four child views. -public struct TupleView4: View { - public let value: (V0, V1, V2, V3) - - public init(_ v0: V0, _ v1: V1, _ v2: V2, _ v3: V3) { - self.value = (v0, v1, v2, v3) - } - - public var body: Never { - fatalError("TupleView4 renders its children directly") - } -} - -// MARK: - TupleView5 - -/// A view that contains five child views. -public struct TupleView5: View { - public let value: (V0, V1, V2, V3, V4) - - public init(_ v0: V0, _ v1: V1, _ v2: V2, _ v3: V3, _ v4: V4) { - self.value = (v0, v1, v2, v3, v4) - } - - public var body: Never { - fatalError("TupleView5 renders its children directly") - } -} - -// MARK: - TupleView6 - -/// A view that contains six child views. -public struct TupleView6: View { - public let value: (V0, V1, V2, V3, V4, V5) - - public init(_ v0: V0, _ v1: V1, _ v2: V2, _ v3: V3, _ v4: V4, _ v5: V5) { - self.value = (v0, v1, v2, v3, v4, v5) - } - - public var body: Never { - fatalError("TupleView6 renders its children directly") - } -} - -// MARK: - TupleView7 - -/// A view that contains seven child views. -public struct TupleView7: View { - public let value: (V0, V1, V2, V3, V4, V5, V6) - - public init(_ v0: V0, _ v1: V1, _ v2: V2, _ v3: V3, _ v4: V4, _ v5: V5, _ v6: V6) { - self.value = (v0, v1, v2, v3, v4, v5, v6) - } - - public var body: Never { - fatalError("TupleView7 renders its children directly") - } -} - -// MARK: - TupleView8 - -/// A view that contains eight child views. -public struct TupleView8: View { - public let value: (V0, V1, V2, V3, V4, V5, V6, V7) - - public init(_ v0: V0, _ v1: V1, _ v2: V2, _ v3: V3, _ v4: V4, _ v5: V5, _ v6: V6, _ v7: V7) { - self.value = (v0, v1, v2, v3, v4, v5, v6, v7) - } - - public var body: Never { - fatalError("TupleView8 renders its children directly") - } -} - -// MARK: - TupleView9 - -/// A view that contains nine child views. -public struct TupleView9: View { - public let value: (V0, V1, V2, V3, V4, V5, V6, V7, V8) - - public init(_ v0: V0, _ v1: V1, _ v2: V2, _ v3: V3, _ v4: V4, _ v5: V5, _ v6: V6, _ v7: V7, _ v8: V8) { - self.value = (v0, v1, v2, v3, v4, v5, v6, v7, v8) - } - - public var body: Never { - fatalError("TupleView9 renders its children directly") - } -} - -// MARK: - TupleView10 - -/// A view that contains ten child views. -public struct TupleView10: View { - public let value: (V0, V1, V2, V3, V4, V5, V6, V7, V8, V9) - - public init(_ v0: V0, _ v1: V1, _ v2: V2, _ v3: V3, _ v4: V4, _ v5: V5, _ v6: V6, _ v7: V7, _ v8: V8, _ v9: V9) { - self.value = (v0, v1, v2, v3, v4, v5, v6, v7, v8, v9) - } - - public var body: Never { - fatalError("TupleView10 renders its children directly") - } -} -// swiftlint:enable large_tuple diff --git a/Sources/TUIKit/Core/ViewBuilder.swift b/Sources/TUIKit/Core/ViewBuilder.swift index c2a7904..9c97998 100644 --- a/Sources/TUIKit/Core/ViewBuilder.swift +++ b/Sources/TUIKit/Core/ViewBuilder.swift @@ -1,4 +1,3 @@ -// swiftlint:disable function_parameter_count // // ViewBuilder.swift // TUIKit @@ -22,7 +21,7 @@ /// /// The builder supports: /// - Single views -/// - Multiple views (up to 10) +/// - Multiple views (unlimited, via Parameter Packs) /// - Conditionals (`if`, `if-else`) /// - Optional views (`if let`) /// - Arrays of views (`for-in`) @@ -36,114 +35,16 @@ public struct ViewBuilder { content } - // MARK: - Multiple Views (Tuple Views) + // MARK: - Multiple Views (Parameter Pack) - /// Builds two views into a TupleView. - public static func buildBlock( - _ c0: C0, - _ c1: C1 - ) -> TupleView2 { - TupleView2(c0, c1) - } - - /// Builds three views into a TupleView. - public static func buildBlock( - _ c0: C0, - _ c1: C1, - _ c2: C2 - ) -> TupleView3 { - TupleView3(c0, c1, c2) - } - - /// Builds four views into a TupleView. - public static func buildBlock( - _ c0: C0, - _ c1: C1, - _ c2: C2, - _ c3: C3 - ) -> TupleView4 { - TupleView4(c0, c1, c2, c3) - } - - /// Builds five views into a TupleView. - public static func buildBlock( - _ c0: C0, - _ c1: C1, - _ c2: C2, - _ c3: C3, - _ c4: C4 - ) -> TupleView5 { - TupleView5(c0, c1, c2, c3, c4) - } - - /// Builds six views into a TupleView. - public static func buildBlock( - _ c0: C0, - _ c1: C1, - _ c2: C2, - _ c3: C3, - _ c4: C4, - _ c5: C5 - ) -> TupleView6 { - TupleView6(c0, c1, c2, c3, c4, c5) - } - - /// Builds seven views into a TupleView. - public static func buildBlock( - _ c0: C0, - _ c1: C1, - _ c2: C2, - _ c3: C3, - _ c4: C4, - _ c5: C5, - _ c6: C6 - ) -> TupleView7 { - TupleView7(c0, c1, c2, c3, c4, c5, c6) - } - - /// Builds eight views into a TupleView. - public static func buildBlock( - _ c0: C0, - _ c1: C1, - _ c2: C2, - _ c3: C3, - _ c4: C4, - _ c5: C5, - _ c6: C6, - _ c7: C7 - ) -> TupleView8 { - TupleView8(c0, c1, c2, c3, c4, c5, c6, c7) - } - - /// Builds nine views into a TupleView. - public static func buildBlock( - _ c0: C0, - _ c1: C1, - _ c2: C2, - _ c3: C3, - _ c4: C4, - _ c5: C5, - _ c6: C6, - _ c7: C7, - _ c8: C8 - ) -> TupleView9 { - TupleView9(c0, c1, c2, c3, c4, c5, c6, c7, c8) - } - - /// Builds ten views into a TupleView. - public static func buildBlock( - _ c0: C0, - _ c1: C1, - _ c2: C2, - _ c3: C3, - _ c4: C4, - _ c5: C5, - _ c6: C6, - _ c7: C7, - _ c8: C8, - _ c9: C9 - ) -> TupleView10 { - TupleView10(c0, c1, c2, c3, c4, c5, c6, c7, c8, c9) + /// Builds multiple views into a TupleView using Swift Parameter Packs. + /// + /// 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) } // MARK: - Conditionals @@ -191,4 +92,3 @@ public struct ViewBuilder { expression } } -// swiftlint:enable function_parameter_count diff --git a/Sources/TUIKit/Extensions/String+ANSI.swift b/Sources/TUIKit/Extensions/String+ANSI.swift index 320139f..dc8661f 100644 --- a/Sources/TUIKit/Extensions/String+ANSI.swift +++ b/Sources/TUIKit/Extensions/String+ANSI.swift @@ -15,11 +15,7 @@ extension String { /// The string with all ANSI escape codes removed. var stripped: String { - replacingOccurrences( - of: ANSIRenderer.ansiPattern, - with: "", - options: .regularExpression - ) + replacing(ANSIRenderer.ansiRegex, with: "") } /// Pads the string to the specified visible width using spaces. diff --git a/Sources/TUIKit/Rendering/ANSIRenderer.swift b/Sources/TUIKit/Rendering/ANSIRenderer.swift index 868db29..f3c88e6 100644 --- a/Sources/TUIKit/Rendering/ANSIRenderer.swift +++ b/Sources/TUIKit/Rendering/ANSIRenderer.swift @@ -22,8 +22,12 @@ public enum ANSIRenderer { /// Dim/faint text style code. public static let dim = "\(csi)2m" - /// Regex pattern that matches any ANSI escape sequence. - public static let ansiPattern = "\u{1B}\\[[0-9;]*[a-zA-Z]" + /// Precompiled regex that matches any ANSI escape sequence. + /// + /// Used by `String.stripped` and `String.strippedLength` to remove + /// formatting codes for visible-width calculations. Compiling once + /// avoids per-call overhead in the hot rendering path. + public static nonisolated(unsafe) let ansiRegex = /\u{1B}\[[0-9;]*[a-zA-Z]/ // MARK: - SGR Style Codes diff --git a/Sources/TUIKit/Rendering/ViewRenderer.swift b/Sources/TUIKit/Rendering/ViewRenderer.swift index b42e9c3..caffc57 100644 --- a/Sources/TUIKit/Rendering/ViewRenderer.swift +++ b/Sources/TUIKit/Rendering/ViewRenderer.swift @@ -227,156 +227,15 @@ extension ZStack: Renderable { // MARK: - TupleView Rendering + ChildInfoProvider -extension TupleView2: Renderable, ChildInfoProvider { +extension TupleView: Renderable, ChildInfoProvider { public func renderToBuffer(context: RenderContext) -> FrameBuffer { FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer)) } func childInfos(context: RenderContext) -> [ChildInfo] { - [ - makeChildInfo(for: value.0, context: context), - makeChildInfo(for: value.1, context: context), - ] - } -} - -extension TupleView3: Renderable, ChildInfoProvider { - public func renderToBuffer(context: RenderContext) -> FrameBuffer { - FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer)) - } - - func childInfos(context: RenderContext) -> [ChildInfo] { - [ - makeChildInfo(for: value.0, context: context), - makeChildInfo(for: value.1, context: context), - makeChildInfo(for: value.2, context: context), - ] - } -} - -extension TupleView4: Renderable, ChildInfoProvider { - public func renderToBuffer(context: RenderContext) -> FrameBuffer { - FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer)) - } - - func childInfos(context: RenderContext) -> [ChildInfo] { - [ - makeChildInfo(for: value.0, context: context), - makeChildInfo(for: value.1, context: context), - makeChildInfo(for: value.2, context: context), - makeChildInfo(for: value.3, context: context), - ] - } -} - -extension TupleView5: Renderable, ChildInfoProvider { - public func renderToBuffer(context: RenderContext) -> FrameBuffer { - FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer)) - } - - func childInfos(context: RenderContext) -> [ChildInfo] { - [ - makeChildInfo(for: value.0, context: context), - makeChildInfo(for: value.1, context: context), - makeChildInfo(for: value.2, context: context), - makeChildInfo(for: value.3, context: context), - makeChildInfo(for: value.4, context: context), - ] - } -} - -extension TupleView6: Renderable, ChildInfoProvider { - public func renderToBuffer(context: RenderContext) -> FrameBuffer { - FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer)) - } - - func childInfos(context: RenderContext) -> [ChildInfo] { - [ - makeChildInfo(for: value.0, context: context), - makeChildInfo(for: value.1, context: context), - makeChildInfo(for: value.2, context: context), - makeChildInfo(for: value.3, context: context), - makeChildInfo(for: value.4, context: context), - makeChildInfo(for: value.5, context: context), - ] - } -} - -extension TupleView7: Renderable, ChildInfoProvider { - public func renderToBuffer(context: RenderContext) -> FrameBuffer { - FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer)) - } - - func childInfos(context: RenderContext) -> [ChildInfo] { - [ - makeChildInfo(for: value.0, context: context), - makeChildInfo(for: value.1, context: context), - makeChildInfo(for: value.2, context: context), - makeChildInfo(for: value.3, context: context), - makeChildInfo(for: value.4, context: context), - makeChildInfo(for: value.5, context: context), - makeChildInfo(for: value.6, context: context), - ] - } -} - -extension TupleView8: Renderable, ChildInfoProvider { - public func renderToBuffer(context: RenderContext) -> FrameBuffer { - FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer)) - } - - func childInfos(context: RenderContext) -> [ChildInfo] { - [ - makeChildInfo(for: value.0, context: context), - makeChildInfo(for: value.1, context: context), - makeChildInfo(for: value.2, context: context), - makeChildInfo(for: value.3, context: context), - makeChildInfo(for: value.4, context: context), - makeChildInfo(for: value.5, context: context), - makeChildInfo(for: value.6, context: context), - makeChildInfo(for: value.7, context: context), - ] - } -} - -extension TupleView9: Renderable, ChildInfoProvider { - public func renderToBuffer(context: RenderContext) -> FrameBuffer { - FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer)) - } - - func childInfos(context: RenderContext) -> [ChildInfo] { - [ - makeChildInfo(for: value.0, context: context), - makeChildInfo(for: value.1, context: context), - makeChildInfo(for: value.2, context: context), - makeChildInfo(for: value.3, context: context), - makeChildInfo(for: value.4, context: context), - makeChildInfo(for: value.5, context: context), - makeChildInfo(for: value.6, context: context), - makeChildInfo(for: value.7, context: context), - makeChildInfo(for: value.8, context: context), - ] - } -} - -extension TupleView10: Renderable, ChildInfoProvider { - public func renderToBuffer(context: RenderContext) -> FrameBuffer { - FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer)) - } - - func childInfos(context: RenderContext) -> [ChildInfo] { - [ - makeChildInfo(for: value.0, context: context), - makeChildInfo(for: value.1, context: context), - makeChildInfo(for: value.2, context: context), - makeChildInfo(for: value.3, context: context), - makeChildInfo(for: value.4, context: context), - makeChildInfo(for: value.5, context: context), - makeChildInfo(for: value.6, context: context), - makeChildInfo(for: value.7, context: context), - makeChildInfo(for: value.8, context: context), - makeChildInfo(for: value.9, context: context), - ] + var infos: [ChildInfo] = [] + repeat infos.append(makeChildInfo(for: each children, context: context)) + return infos } } diff --git a/Sources/TUIKit/Views/Alert.swift b/Sources/TUIKit/Views/Alert.swift index fd66b5c..a606f58 100644 --- a/Sources/TUIKit/Views/Alert.swift +++ b/Sources/TUIKit/Views/Alert.swift @@ -89,20 +89,9 @@ public struct Alert: View { extension Alert: Renderable { public func renderToBuffer(context: RenderContext) -> FrameBuffer { let hasActions = !(actions is EmptyView) - let effectiveConfig = - hasActions - ? config - : ContainerConfig( - borderStyle: config.borderStyle, - borderColor: config.borderColor, - titleColor: config.titleColor, - padding: config.padding, - showFooterSeparator: false - ) - return renderContainer( title: title, - config: effectiveConfig, + config: config, content: Text(message), footer: hasActions ? actions : nil, context: context diff --git a/Sources/TUIKit/Views/ContainerView.swift b/Sources/TUIKit/Views/ContainerView.swift index 86834a0..f6b320b 100644 --- a/Sources/TUIKit/Views/ContainerView.swift +++ b/Sources/TUIKit/Views/ContainerView.swift @@ -139,36 +139,27 @@ internal func renderContainer( footer: Footer?, context: RenderContext ) -> FrameBuffer { - let containerStyle = ContainerStyle(from: config) + let hasFooter = footer != nil + let style = ContainerStyle( + showHeaderSeparator: true, + showFooterSeparator: hasFooter && config.showFooterSeparator, + borderStyle: config.borderStyle, + borderColor: config.borderColor + ) - if let footerView = footer { - let container = ContainerView( - title: title, - titleColor: config.titleColor, - style: containerStyle, - padding: config.padding - ) { - content - } footer: { + let container = ContainerView( + title: title, + titleColor: config.titleColor, + style: style, + padding: config.padding + ) { + content + } footer: { + if let footerView = footer { footerView } - return container.renderToBuffer(context: context) - } else { - let container = ContainerView( - title: title, - titleColor: config.titleColor, - style: ContainerStyle( - showHeaderSeparator: true, - showFooterSeparator: false, - borderStyle: config.borderStyle, - borderColor: config.borderColor - ), - padding: config.padding - ) { - content - } - return container.renderToBuffer(context: context) } + return container.renderToBuffer(context: context) } // MARK: - Container View diff --git a/Sources/TUIKit/Views/Panel.swift b/Sources/TUIKit/Views/Panel.swift index db53fa1..24e5fc4 100644 --- a/Sources/TUIKit/Views/Panel.swift +++ b/Sources/TUIKit/Views/Panel.swift @@ -122,7 +122,7 @@ extension Panel where Footer == EmptyView { borderColor: borderColor, titleColor: titleColor, padding: padding, - showFooterSeparator: true + showFooterSeparator: false ) } } diff --git a/Tests/TUIKitTests/TViewTests.swift b/Tests/TUIKitTests/TViewTests.swift index bb901ba..d8e7c34 100644 --- a/Tests/TUIKitTests/TViewTests.swift +++ b/Tests/TUIKitTests/TViewTests.swift @@ -76,7 +76,7 @@ struct ViewBuilderTests { } let views = buildViews() - #expect(views is TupleView2) + #expect(views is TupleView) } @Test("ViewBuilder with three views") @@ -89,7 +89,7 @@ struct ViewBuilderTests { } let views = buildViews() - #expect(views is TupleView3) + #expect(views is TupleView) } @Test("VStack can contain views")