From d5c3d2c784a09f90302416733e82c024cfbcf6ac Mon Sep 17 00:00:00 2001 From: phranck Date: Fri, 30 Jan 2026 11:04:31 +0100 Subject: [PATCH] refactor: Extract BorderRenderer utility for shared border rendering (A.3, A.4, B.1) - Create BorderRenderer enum with reusable line-level border primitives - Standard style: standardTopBorder, standardBottomBorder, standardDivider, standardContentLine (with optional persistent background) - Block style: blockTopBorder, blockBottomBorder, blockContentLine, blockSeparator (with FG/BG transition support) - standardTopBorder has title overload for ContainerView inline titles - Refactor 5 files to use BorderRenderer: BorderModifier, Button, Menu, ContainerView, StatusBar - Eliminate ~170 lines of duplicated border-assembly code Net reduction: 167 lines across 5 files --- Sources/TUIKit/Modifiers/BorderModifier.swift | 83 ++----- Sources/TUIKit/Rendering/BorderRenderer.swift | 219 ++++++++++++++++++ Sources/TUIKit/Views/Button.swift | 24 +- Sources/TUIKit/Views/ContainerView.swift | 122 ++++------ Sources/TUIKit/Views/Menu.swift | 76 ++---- Sources/TUIKit/Views/StatusBar.swift | 48 +--- 6 files changed, 312 insertions(+), 260 deletions(-) create mode 100644 Sources/TUIKit/Rendering/BorderRenderer.swift diff --git a/Sources/TUIKit/Modifiers/BorderModifier.swift b/Sources/TUIKit/Modifiers/BorderModifier.swift index 98166c3..1526575 100644 --- a/Sources/TUIKit/Modifiers/BorderModifier.swift +++ b/Sources/TUIKit/Modifiers/BorderModifier.swift @@ -53,87 +53,32 @@ extension BorderedView: Renderable { /// Renders with standard box-drawing characters. private func renderStandardStyle(buffer: FrameBuffer, innerWidth: Int, style: BorderStyle) -> FrameBuffer { - let borderForeground = color ?? Color.theme.border - - // Build the top border line - let topLine = buildBorderLine( - left: style.topLeft, - fill: style.horizontal, - right: style.topRight, - width: innerWidth - ) - - // Build the bottom border line - let bottomLine = buildBorderLine( - left: style.bottomLeft, - fill: style.horizontal, - right: style.bottomRight, - width: innerWidth - ) - - // Build the result + let borderColor = color ?? Color.theme.border var lines: [String] = [] - // Top border - lines.append(ANSIRenderer.colorize(topLine, foreground: borderForeground)) - - // Content lines with side borders + lines.append(BorderRenderer.standardTopBorder(style: style, innerWidth: innerWidth, color: borderColor)) for line in buffer.lines { - let paddedLine = line.padToVisibleWidth(innerWidth) - let borderedLine = ANSIRenderer.colorize(String(style.vertical), foreground: borderForeground) - + paddedLine - + ANSIRenderer.reset - + ANSIRenderer.colorize(String(style.vertical), foreground: borderForeground) - lines.append(borderedLine) + lines.append(BorderRenderer.standardContentLine( + content: line, innerWidth: innerWidth, style: style, color: borderColor + )) } - - // Bottom border - lines.append(ANSIRenderer.colorize(bottomLine, foreground: borderForeground)) + lines.append(BorderRenderer.standardBottomBorder(style: style, innerWidth: innerWidth, color: borderColor)) return FrameBuffer(lines: lines) } /// Renders with half-block characters for block appearance. - /// - /// Block style design: - /// ``` - /// ▄▄▄▄▄▄▄▄▄▄ ← Top: ▄, FG = container BG, BG = transparent - /// █ Content █ ← Sides: █, FG = container BG, content has container BG - /// ▀▀▀▀▀▀▀▀▀▀ ← Bottom: ▀, FG = container BG, BG = transparent - /// ``` private func renderBlockStyle(buffer: FrameBuffer, innerWidth: Int) -> FrameBuffer { - var lines: [String] = [] - - // For block style, use container background color for borders let containerBg = Color.theme.containerBackground - let sideBorder = ANSIRenderer.colorize("█", foreground: containerBg) - - // Top border: ▄▄▄ with FG = container BG - let topLine = String(repeating: "▄", count: innerWidth + 2) - lines.append(ANSIRenderer.colorize(topLine, foreground: containerBg)) - - // Content lines with █ side borders and container background - for line in buffer.lines { - let paddedLine = line.padToVisibleWidth(innerWidth) - let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: containerBg) - lines.append(sideBorder + styledContent + ANSIRenderer.reset + sideBorder) - } - - // Bottom border: ▀▀▀ with FG = container BG - let bottomLine = String(repeating: "▀", count: innerWidth + 2) - lines.append(ANSIRenderer.colorize(bottomLine, foreground: containerBg)) - - return FrameBuffer(lines: lines) - } + var lines: [String] = [] - /// Builds a horizontal border line. - private func buildBorderLine( - left: Character, - fill: Character, - right: Character, - width: Int - ) -> String { - String(left) + String(repeating: fill, count: width) + String(right) + lines.append(BorderRenderer.blockTopBorder(innerWidth: innerWidth, color: containerBg)) + for line in buffer.lines { + lines.append(BorderRenderer.blockContentLine(content: line, innerWidth: innerWidth, sectionColor: containerBg)) + } + lines.append(BorderRenderer.blockBottomBorder(innerWidth: innerWidth, color: containerBg)) + + return FrameBuffer(lines: lines) } } diff --git a/Sources/TUIKit/Rendering/BorderRenderer.swift b/Sources/TUIKit/Rendering/BorderRenderer.swift new file mode 100644 index 0000000..254d967 --- /dev/null +++ b/Sources/TUIKit/Rendering/BorderRenderer.swift @@ -0,0 +1,219 @@ +// +// BorderRenderer.swift +// TUIKit +// +// Centralized border rendering utilities for standard and block styles. +// + +/// Reusable building blocks for border rendering. +/// +/// Each method produces a single rendered line (`String`) that callers +/// append to a `[String]` array for `FrameBuffer` construction. +/// This eliminates duplicated border-assembly code across Views and Modifiers. +/// +/// Two style families are supported: +/// - **Standard**: box-drawing characters (┌─┐│└─┘├─┤) +/// - **Block**: half-block characters (▄ █ ▀) for smooth visual edges +public enum BorderRenderer { + + // MARK: - Standard Style (Box-Drawing Characters) + + /// Renders a plain top border line. + /// + /// ┌──────────────┐ + /// + /// - Parameters: + /// - style: The border style providing corner and edge characters. + /// - innerWidth: The width of the content area (excluding borders). + /// - color: The foreground color for the border. + /// - Returns: A colorized top border string. + public static func standardTopBorder( + style: BorderStyle, + innerWidth: Int, + color: Color + ) -> String { + let line = String(style.topLeft) + + String(repeating: style.horizontal, count: innerWidth) + + String(style.topRight) + return ANSIRenderer.colorize(line, foreground: color) + } + + /// Renders a top border line with an inline title. + /// + /// ┌─ Title ──────┐ + /// + /// - Parameters: + /// - style: The border style. + /// - innerWidth: The content width. + /// - color: The border color. + /// - title: The title text. + /// - titleColor: The title foreground color. + /// - Returns: A colorized top border string with embedded title. + public static func standardTopBorder( + style: BorderStyle, + innerWidth: Int, + color: Color, + title: String, + titleColor: Color + ) -> String { + let titleStyled = ANSIRenderer.colorize(" \(title) ", foreground: titleColor, bold: true) + let leftPart = ANSIRenderer.colorize( + String(style.topLeft) + String(style.horizontal), + foreground: color + ) + let rightPartLength = max(0, innerWidth - 1 - title.count - 2) + let rightPart = ANSIRenderer.colorize( + String(repeating: style.horizontal, count: rightPartLength) + String(style.topRight), + foreground: color + ) + return leftPart + titleStyled + rightPart + } + + /// Renders a plain bottom border line. + /// + /// └──────────────┘ + /// + /// - Parameters: + /// - style: The border style. + /// - innerWidth: The content width. + /// - color: The border color. + /// - Returns: A colorized bottom border string. + public static func standardBottomBorder( + style: BorderStyle, + innerWidth: Int, + color: Color + ) -> String { + let line = String(style.bottomLeft) + + String(repeating: style.horizontal, count: innerWidth) + + String(style.bottomRight) + return ANSIRenderer.colorize(line, foreground: color) + } + + /// Renders a horizontal divider with T-junctions. + /// + /// ├──────────────┤ + /// + /// - Parameters: + /// - style: The border style (uses leftT, horizontal, rightT). + /// - innerWidth: The content width. + /// - color: The border color. + /// - Returns: A colorized divider string. + public static func standardDivider( + style: BorderStyle, + innerWidth: Int, + color: Color + ) -> String { + let line = String(style.leftT) + + String(repeating: style.horizontal, count: innerWidth) + + String(style.rightT) + return ANSIRenderer.colorize(line, foreground: color) + } + + /// Wraps a single content line with vertical side borders. + /// + /// │ padded content │ + /// + /// If `backgroundColor` is provided, `applyPersistentBackground` is used + /// so the background survives inner ANSI resets. + /// + /// - Parameters: + /// - content: The content string (will be padded to `innerWidth`). + /// - innerWidth: The target content width. + /// - style: The border style (for the vertical character). + /// - color: The border color. + /// - backgroundColor: Optional background applied to the content area. + /// - Returns: The bordered content line. + public static func standardContentLine( + content: String, + innerWidth: Int, + style: BorderStyle, + color: Color, + backgroundColor: Color? = nil + ) -> String { + let paddedLine = content.padToVisibleWidth(innerWidth) + let styledContent: String + if let bgColor = backgroundColor { + styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: bgColor) + } else { + styledContent = paddedLine + } + let vertical = ANSIRenderer.colorize(String(style.vertical), foreground: color) + return vertical + styledContent + ANSIRenderer.reset + vertical + } + + // MARK: - Block Style (Half-Block Characters) + + /// Renders a block-style top border. + /// + /// ▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + /// + /// - Parameters: + /// - innerWidth: The content width (border width = innerWidth + 2). + /// - color: The foreground color (typically the section's background color). + /// - Returns: The top border string. + public static func blockTopBorder( + innerWidth: Int, + color: Color + ) -> String { + let line = String(repeating: "▄", count: innerWidth + 2) + return ANSIRenderer.colorize(line, foreground: color) + } + + /// Renders a block-style bottom border. + /// + /// ▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + /// + /// - Parameters: + /// - innerWidth: The content width (border width = innerWidth + 2). + /// - color: The foreground color (typically the section's background color). + /// - Returns: The bottom border string. + public static func blockBottomBorder( + innerWidth: Int, + color: Color + ) -> String { + let line = String(repeating: "▀", count: innerWidth + 2) + return ANSIRenderer.colorize(line, foreground: color) + } + + /// Wraps a single content line with full-block side borders + /// and applies a persistent background. + /// + /// █ content █ + /// + /// - Parameters: + /// - content: The content string (will be padded to `innerWidth`). + /// - innerWidth: The target content width. + /// - sectionColor: The color for both `█` borders and content background. + /// - Returns: The bordered content line. + public static func blockContentLine( + content: String, + innerWidth: Int, + sectionColor: Color + ) -> String { + let paddedLine = content.padToVisibleWidth(innerWidth) + let sideBorder = ANSIRenderer.colorize("█", foreground: sectionColor) + let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: sectionColor) + return sideBorder + styledContent + ANSIRenderer.reset + sideBorder + } + + /// Renders a block-style section separator (transition between two background colors). + /// + /// ▀▀▀▀▀▀▀▀▀▀▀▀▀▀ (header→body: FG=headerBg, BG=bodyBg) + /// ▄▄▄▄▄▄▄▄▄▄▄▄▄▄ (body→footer: FG=footerBg, BG=bodyBg) + /// + /// - Parameters: + /// - innerWidth: The content width (separator width = innerWidth + 2). + /// - character: The separator character (`"▀"` for header→body, `"▄"` for body→footer). + /// - foregroundColor: The FG color (the section being transitioned from or to). + /// - backgroundColor: The BG color (the adjacent section). + /// - Returns: The separator line. + public static func blockSeparator( + innerWidth: Int, + character: Character = "▀", + foregroundColor: Color, + backgroundColor: Color + ) -> String { + let line = String(repeating: character, count: innerWidth + 2) + return ANSIRenderer.colorize(line, foreground: foregroundColor, background: backgroundColor) + } +} diff --git a/Sources/TUIKit/Views/Button.swift b/Sources/TUIKit/Views/Button.swift index ac35ea5..a5fc447 100644 --- a/Sources/TUIKit/Views/Button.swift +++ b/Sources/TUIKit/Views/Button.swift @@ -300,27 +300,13 @@ extension Button: Renderable { let innerWidth = buffer.width var result: [String] = [] - // Border characters (optionally colored) - let vertical = ANSIRenderer.colorize(String(style.vertical), foreground: color) - - // Top border - let topLine = String(style.topLeft) - + String(repeating: style.horizontal, count: innerWidth) - + String(style.topRight) - result.append(ANSIRenderer.colorize(topLine, foreground: color)) - - // Content lines with side borders + result.append(BorderRenderer.standardTopBorder(style: style, innerWidth: innerWidth, color: color)) for line in buffer.lines { - let paddedLine = line.padToVisibleWidth(innerWidth) - let borderedLine = vertical + paddedLine + ANSIRenderer.reset + vertical - result.append(borderedLine) + result.append(BorderRenderer.standardContentLine( + content: line, innerWidth: innerWidth, style: style, color: color + )) } - - // Bottom border - let bottomLine = String(style.bottomLeft) - + String(repeating: style.horizontal, count: innerWidth) - + String(style.bottomRight) - result.append(ANSIRenderer.colorize(bottomLine, foreground: color)) + result.append(BorderRenderer.standardBottomBorder(style: style, innerWidth: innerWidth, color: color)) return FrameBuffer(lines: result) } diff --git a/Sources/TUIKit/Views/ContainerView.swift b/Sources/TUIKit/Views/ContainerView.swift index faec7c6..9bfa70e 100644 --- a/Sources/TUIKit/Views/ContainerView.swift +++ b/Sources/TUIKit/Views/ContainerView.swift @@ -222,69 +222,46 @@ extension ContainerView: Renderable { var lines: [String] = [] // Top border (with title if present) - let topLine: String if let titleText = title { - let titleStyled = ANSIRenderer.colorize(" \(titleText) ", foreground: titleColor ?? Color.theme.accent, bold: true) - let leftPart = ANSIRenderer.colorize( - String(borderStyle.topLeft) + String(borderStyle.horizontal), - foreground: borderColor - ) - let rightPartLength = max(0, innerWidth - 1 - titleText.count - 2) - let rightPart = ANSIRenderer.colorize( - String(repeating: borderStyle.horizontal, count: rightPartLength) + String(borderStyle.topRight), - foreground: borderColor - ) - topLine = leftPart + titleStyled + rightPart + lines.append(BorderRenderer.standardTopBorder( + style: borderStyle, innerWidth: innerWidth, color: borderColor, + title: titleText, titleColor: titleColor ?? Color.theme.accent + )) } else { - topLine = ANSIRenderer.colorize( - String(borderStyle.topLeft) - + String(repeating: borderStyle.horizontal, count: innerWidth) - + String(borderStyle.topRight), - foreground: borderColor - ) + lines.append(BorderRenderer.standardTopBorder( + style: borderStyle, innerWidth: innerWidth, color: borderColor + )) } - lines.append(topLine) - - // Vertical border characters - let leftBorder = ANSIRenderer.colorize(String(borderStyle.vertical), foreground: borderColor) - let rightBorder = ANSIRenderer.colorize(String(borderStyle.vertical), foreground: borderColor) // Body lines with theme background let bodyBg = context.environment.theme.containerBackground for line in bodyBuffer.lines { - let paddedLine = line.padToVisibleWidth(innerWidth) - let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: bodyBg) - lines.append(leftBorder + styledContent + ANSIRenderer.reset + rightBorder) + lines.append(BorderRenderer.standardContentLine( + content: line, innerWidth: innerWidth, style: borderStyle, + color: borderColor, backgroundColor: bodyBg + )) } // Footer section (if present) if let footerBuf = footerBuffer, !footerBuf.isEmpty { - // Footer separator if style.showFooterSeparator { - let separatorLine = ANSIRenderer.colorize( - String(borderStyle.leftT) - + String(repeating: borderStyle.horizontal, count: innerWidth) - + String(borderStyle.rightT), - foreground: borderColor - ) - lines.append(separatorLine) + lines.append(BorderRenderer.standardDivider( + style: borderStyle, innerWidth: innerWidth, color: borderColor + )) } // Footer lines (no background - footer has its own styling) for line in footerBuf.lines { - let paddedLine = line.padToVisibleWidth(innerWidth) - lines.append(leftBorder + paddedLine + ANSIRenderer.reset + rightBorder) + lines.append(BorderRenderer.standardContentLine( + content: line, innerWidth: innerWidth, style: borderStyle, color: borderColor + )) } } // Bottom border - let bottomLine = ANSIRenderer.colorize( - String(borderStyle.bottomLeft) - + String(repeating: borderStyle.horizontal, count: innerWidth) - + String(borderStyle.bottomRight), - foreground: borderColor - ) - lines.append(bottomLine) + lines.append(BorderRenderer.standardBottomBorder( + style: borderStyle, innerWidth: innerWidth, color: borderColor + )) return FrameBuffer(lines: lines) } @@ -324,66 +301,51 @@ extension ContainerView: Renderable { let hasFooter = footerBuffer != nil && !(footerBuffer?.isEmpty ?? true) // === TOP BORDER === - // ▄▄▄: FG = header/body BG, BG = transparent (App BG shows through) - let topLine = String(repeating: "▄", count: innerWidth + 2) - if hasHeader { - lines.append(ANSIRenderer.colorize(topLine, foreground: headerFooterBg)) - } else { - lines.append(ANSIRenderer.colorize(topLine, foreground: bodyBg)) - } + lines.append(BorderRenderer.blockTopBorder( + innerWidth: innerWidth, color: hasHeader ? headerFooterBg : bodyBg + )) // === HEADER SECTION (if title present) === if let titleText = title { - // █ TITLE █: FG = header BG for █, content has header BG let titleStyled = ANSIRenderer.colorize(" \(titleText) ", foreground: titleColor ?? Color.theme.accent, bold: true) - let paddedTitle = titleStyled.padToVisibleWidth(innerWidth) - let sideBorder = ANSIRenderer.colorize("█", foreground: headerFooterBg) - let styledContent = ANSIRenderer.applyPersistentBackground(paddedTitle, color: headerFooterBg) - lines.append(sideBorder + styledContent + ANSIRenderer.reset + sideBorder) + lines.append(BorderRenderer.blockContentLine( + content: titleStyled, innerWidth: innerWidth, sectionColor: headerFooterBg + )) - // Header/Body separator: ▀▀▀ - // FG = header BG, BG = body BG (creates smooth transition) if style.showHeaderSeparator { - let sepLine = String(repeating: "▀", count: innerWidth + 2) - lines.append(ANSIRenderer.colorize(sepLine, foreground: headerFooterBg, background: bodyBg)) + lines.append(BorderRenderer.blockSeparator( + innerWidth: innerWidth, foregroundColor: headerFooterBg, backgroundColor: bodyBg + )) } } // === BODY LINES === - // █ Content █: FG = body BG for █, content has body BG for line in bodyBuffer.lines { - let paddedLine = line.padToVisibleWidth(innerWidth) - let sideBorder = ANSIRenderer.colorize("█", foreground: bodyBg) - let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: bodyBg) - lines.append(sideBorder + styledContent + ANSIRenderer.reset + sideBorder) + lines.append(BorderRenderer.blockContentLine( + content: line, innerWidth: innerWidth, sectionColor: bodyBg + )) } // === FOOTER SECTION (if present) === if let footerBuf = footerBuffer, !footerBuf.isEmpty { - // Body/Footer separator: ▄▄▄ - // FG = footer BG, BG = body BG (creates smooth transition) if style.showFooterSeparator { - let sepLine = String(repeating: "▄", count: innerWidth + 2) - lines.append(ANSIRenderer.colorize(sepLine, foreground: headerFooterBg, background: bodyBg)) + lines.append(BorderRenderer.blockSeparator( + innerWidth: innerWidth, character: "▄", + foregroundColor: headerFooterBg, backgroundColor: bodyBg + )) } - // █ Footer █: FG = footer BG for █, content has footer BG for line in footerBuf.lines { - let paddedLine = line.padToVisibleWidth(innerWidth) - let sideBorder = ANSIRenderer.colorize("█", foreground: headerFooterBg) - let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: headerFooterBg) - lines.append(sideBorder + styledContent + ANSIRenderer.reset + sideBorder) + lines.append(BorderRenderer.blockContentLine( + content: line, innerWidth: innerWidth, sectionColor: headerFooterBg + )) } } // === BOTTOM BORDER === - // ▀▀▀: FG = footer/body BG, BG = transparent (App BG shows through) - let bottomLine = String(repeating: "▀", count: innerWidth + 2) - if hasFooter { - lines.append(ANSIRenderer.colorize(bottomLine, foreground: headerFooterBg)) - } else { - lines.append(ANSIRenderer.colorize(bottomLine, foreground: bodyBg)) - } + lines.append(BorderRenderer.blockBottomBorder( + innerWidth: innerWidth, color: hasFooter ? headerFooterBg : bodyBg + )) return FrameBuffer(lines: lines) } diff --git a/Sources/TUIKit/Views/Menu.swift b/Sources/TUIKit/Views/Menu.swift index b680d12..5d709ba 100644 --- a/Sources/TUIKit/Views/Menu.swift +++ b/Sources/TUIKit/Views/Menu.swift @@ -340,85 +340,53 @@ extension Menu: Renderable { var result: [String] = [] if isBlockStyle { - // Block style: use half-blocks with special coloring - // Header/Footer BG = darker (containerHeaderBackground) - // Body BG = lighter (containerBackground) - // App BG = transparent (no background set) let headerFooterBg = Color.theme.containerHeaderBackground let bodyBg = Color.theme.containerBackground - - // Determine if we have a header (divider present) let hasHeader = dividerLineIndex != nil - // Top border: ▄▄▄ - // FG = header background, BG = App background (transparent/none) - let topLine = String(repeating: "▄", count: innerWidth + 2) - if hasHeader { - result.append(ANSIRenderer.colorize(topLine, foreground: headerFooterBg)) - } else { - // No header: use body background for top - result.append(ANSIRenderer.colorize(topLine, foreground: bodyBg)) - } + // Top border + result.append(BorderRenderer.blockTopBorder( + innerWidth: innerWidth, color: hasHeader ? headerFooterBg : bodyBg + )) - // Content lines with side borders + // Content lines with section-aware coloring for (index, line) in buffer.lines.enumerated() { let isHeaderLine = hasHeader && dividerLineIndex.map({ index < $0 }) ?? false let isDividerLine = hasHeader && dividerLineIndex.map({ index == $0 }) ?? false if isDividerLine { - // Header/Body separator: ▀▀▀ - // FG = header BG, BG = body BG (creates smooth transition) - let dividerLine = String(repeating: "▀", count: innerWidth + 2) - result.append(ANSIRenderer.colorize(dividerLine, foreground: headerFooterBg, background: bodyBg)) + result.append(BorderRenderer.blockSeparator( + innerWidth: innerWidth, foregroundColor: headerFooterBg, backgroundColor: bodyBg + )) } else if isHeaderLine { - // Header line: █ borders and content with header background - let paddedLine = line.padToVisibleWidth(innerWidth) - let sideBorder = ANSIRenderer.colorize("█", foreground: headerFooterBg) - let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: headerFooterBg) - result.append(sideBorder + styledContent + ANSIRenderer.reset + sideBorder) + result.append(BorderRenderer.blockContentLine( + content: line, innerWidth: innerWidth, sectionColor: headerFooterBg + )) } else { - // Body line: █ borders with body background - let paddedLine = line.padToVisibleWidth(innerWidth) - let sideBorder = ANSIRenderer.colorize("█", foreground: bodyBg) - let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: bodyBg) - result.append(sideBorder + styledContent + ANSIRenderer.reset + sideBorder) + result.append(BorderRenderer.blockContentLine( + content: line, innerWidth: innerWidth, sectionColor: bodyBg + )) } } - // Bottom border: ▀▀▀ - // FG = body background, BG = App background (transparent) - let bottomLine = String(repeating: "▀", count: innerWidth + 2) - result.append(ANSIRenderer.colorize(bottomLine, foreground: bodyBg)) + // Bottom border + result.append(BorderRenderer.blockBottomBorder(innerWidth: innerWidth, color: bodyBg)) } else { - // Standard style: regular box-drawing characters let borderForeground = color ?? Color.theme.border - let vertical = ANSIRenderer.colorize(String(style.vertical), foreground: borderForeground) - // Top border - let topLine = String(style.topLeft) - + String(repeating: style.horizontal, count: innerWidth) - + String(style.topRight) - result.append(ANSIRenderer.colorize(topLine, foreground: borderForeground)) + result.append(BorderRenderer.standardTopBorder(style: style, innerWidth: innerWidth, color: borderForeground)) - // Content lines with side borders for (index, line) in buffer.lines.enumerated() { if let dividerIndex = dividerLineIndex, index == dividerIndex { - // Render horizontal divider with T-junctions - let dividerLine = String(style.leftT) - + String(repeating: style.horizontal, count: innerWidth) - + String(style.rightT) - result.append(ANSIRenderer.colorize(dividerLine, foreground: borderForeground)) + result.append(BorderRenderer.standardDivider(style: style, innerWidth: innerWidth, color: borderForeground)) } else { - let paddedLine = line.padToVisibleWidth(innerWidth) - result.append(vertical + paddedLine + ANSIRenderer.reset + vertical) + result.append(BorderRenderer.standardContentLine( + content: line, innerWidth: innerWidth, style: style, color: borderForeground + )) } } - // Bottom border - let bottomLine = String(style.bottomLeft) - + String(repeating: style.horizontal, count: innerWidth) - + String(style.bottomRight) - result.append(ANSIRenderer.colorize(bottomLine, foreground: borderForeground)) + result.append(BorderRenderer.standardBottomBorder(style: style, innerWidth: innerWidth, color: borderForeground)) } return FrameBuffer(lines: result) diff --git a/Sources/TUIKit/Views/StatusBar.swift b/Sources/TUIKit/Views/StatusBar.swift index 1671bd7..8d6d5b5 100644 --- a/Sources/TUIKit/Views/StatusBar.swift +++ b/Sources/TUIKit/Views/StatusBar.swift @@ -909,50 +909,22 @@ extension StatusBar: Renderable { let border = context.environment.appearance.borderStyle let borderColor = context.environment.theme.border - // Build the three lines with theme border color - let topBorder = ANSIRenderer.colorize(String(border.topLeft) - + String(repeating: border.horizontal, count: innerWidth) - + String(border.topRight), foreground: borderColor) - - let contentLine = ANSIRenderer.colorize(String(border.vertical), foreground: borderColor) - + content - + ANSIRenderer.reset // Prevent color bleeding - + ANSIRenderer.colorize(String(border.vertical), foreground: borderColor) - - let bottomBorder = ANSIRenderer.colorize(String(border.bottomLeft) - + String(repeating: border.horizontal, count: innerWidth) - + String(border.bottomRight), foreground: borderColor) - - return FrameBuffer(lines: [topBorder, contentLine, bottomBorder]) + return FrameBuffer(lines: [ + BorderRenderer.standardTopBorder(style: border, innerWidth: innerWidth, color: borderColor), + BorderRenderer.standardContentLine(content: content, innerWidth: innerWidth, style: border, color: borderColor), + BorderRenderer.standardBottomBorder(style: border, innerWidth: innerWidth, color: borderColor), + ]) } /// Renders block-style status bar with half-block characters. - /// ``` - /// ▄▄▄▄▄▄▄▄▄▄ ← Top: ▄, statusBar BG color - /// █ Content █ ← Sides: █, statusBar BG color; content with statusBar BG - /// ▀▀▀▀▀▀▀▀▀▀ ← Bottom: ▀, statusBar BG color - /// ``` private func renderBlockBordered(content: String, innerWidth: Int, context: RenderContext) -> FrameBuffer { - var lines: [String] = [] - - // Get colors from theme let statusBarBg = context.environment.theme.statusBarBackground - // Top border: ▄▄▄ with statusBar background color - let topLine = String(repeating: "▄", count: innerWidth + 2) - lines.append(ANSIRenderer.colorize(topLine, foreground: statusBarBg)) - - // Content line with █ side borders and statusBar BG applied to content - let styledContent = ANSIRenderer.applyPersistentBackground(content, color: statusBarBg) - let sideBorder = ANSIRenderer.colorize("█", foreground: statusBarBg) - let contentLine = sideBorder + styledContent + ANSIRenderer.reset + sideBorder - lines.append(contentLine) - - // Bottom border: ▀▀▀ (upper half block) with statusBar background color - let bottomLine = String(repeating: "▀", count: innerWidth + 2) - lines.append(ANSIRenderer.colorize(bottomLine, foreground: statusBarBg)) - - return FrameBuffer(lines: lines) + return FrameBuffer(lines: [ + BorderRenderer.blockTopBorder(innerWidth: innerWidth, color: statusBarBg), + BorderRenderer.blockContentLine(content: content, innerWidth: innerWidth, sectionColor: statusBarBg), + BorderRenderer.blockBottomBorder(innerWidth: innerWidth, color: statusBarBg), + ]) } }