mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
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
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user