mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
- Update all 4 phosphor themes (Green, Amber, White, Red) to use neutral dark background (#1E1E1E) matching Spotnik reference - Simplify theme names (remove 'Phosphor' suffix) - Menu: Use theme.foreground for items, theme.accent for selection - Button: Use theme colors for default focused style and focus indicator - Panel/Card: Use theme.border as default border color - BorderModifier: Use theme.border as default All 189 tests passing.
152 lines
4.9 KiB
Swift
152 lines
4.9 KiB
Swift
//
|
|
// Panel.swift
|
|
// TUIKit
|
|
//
|
|
// A titled container view with a header.
|
|
//
|
|
|
|
/// A bordered container with a title in the top border.
|
|
///
|
|
/// `Panel` is useful for grouping content with a visible label,
|
|
/// similar to a fieldset in HTML or a group box in desktop UIs.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```swift
|
|
/// Panel("Settings") {
|
|
/// Text("Option 1")
|
|
/// Text("Option 2")
|
|
/// }
|
|
///
|
|
/// Panel("User Info", borderStyle: .doubleLine, titleColor: .cyan) {
|
|
/// Text("Name: John")
|
|
/// Text("Age: 30")
|
|
/// }
|
|
/// ```
|
|
public struct Panel<Content: View>: View {
|
|
/// The title displayed in the top border.
|
|
public let title: String
|
|
|
|
/// The content of the panel.
|
|
public let content: Content
|
|
|
|
/// The border style.
|
|
public let borderStyle: BorderStyle
|
|
|
|
/// The border color.
|
|
public let borderColor: Color?
|
|
|
|
/// The title color.
|
|
public let titleColor: Color?
|
|
|
|
/// The padding inside the panel.
|
|
public let padding: EdgeInsets
|
|
|
|
/// Creates a panel with the specified options.
|
|
///
|
|
/// - Parameters:
|
|
/// - title: The title to display in the top border.
|
|
/// - borderStyle: The border style (default: .line).
|
|
/// - borderColor: The border color (default: nil).
|
|
/// - titleColor: The title color (default: nil, same as border).
|
|
/// - padding: The inner padding (default: horizontal 1, vertical 0).
|
|
/// - content: The content of the panel.
|
|
public init(
|
|
_ title: String,
|
|
borderStyle: BorderStyle = .line,
|
|
borderColor: Color? = nil,
|
|
titleColor: Color? = nil,
|
|
padding: EdgeInsets = EdgeInsets(horizontal: 1, vertical: 0),
|
|
@ViewBuilder content: () -> Content
|
|
) {
|
|
self.title = title
|
|
self.content = content()
|
|
self.borderStyle = borderStyle
|
|
self.borderColor = borderColor
|
|
self.titleColor = titleColor
|
|
self.padding = padding
|
|
}
|
|
|
|
public var body: Never {
|
|
fatalError("Panel renders via Renderable")
|
|
}
|
|
}
|
|
|
|
// MARK: - Panel Rendering
|
|
|
|
extension Panel: Renderable {
|
|
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
|
// Render the content first
|
|
let paddedContent = content.padding(padding)
|
|
let contentBuffer = TUIKit.renderToBuffer(paddedContent, context: context)
|
|
|
|
guard !contentBuffer.isEmpty else {
|
|
return FrameBuffer()
|
|
}
|
|
|
|
// Title with spaces: " Title "
|
|
let titleText = " \(title) "
|
|
let titleLength = titleText.count
|
|
|
|
// Inner width must fit both content and title (plus corner + one horizontal on each side)
|
|
// Top line structure: ┌─ Title ─────┐
|
|
// So minimum inner width = titleLength + 2 (for the ─ on each side of title)
|
|
let innerWidth = max(contentBuffer.width, titleLength + 2)
|
|
|
|
// Build top border with title
|
|
// Format: ┌─ Title ─────┐
|
|
let titleStyled = colorize(titleText, with: titleColor ?? borderColor)
|
|
|
|
// Left part: corner + one horizontal
|
|
let leftPart = colorize(
|
|
String(borderStyle.topLeft) + String(borderStyle.horizontal),
|
|
with: borderColor
|
|
)
|
|
|
|
// Right part: remaining horizontals + corner
|
|
// Total top line width (excluding corners) = innerWidth
|
|
// Used by: 1 (left horizontal) + titleLength + rightPartLength = innerWidth
|
|
let rightPartLength = max(0, innerWidth - 1 - titleLength)
|
|
let rightPart = colorize(
|
|
String(repeating: borderStyle.horizontal, count: rightPartLength) + String(borderStyle.topRight),
|
|
with: borderColor
|
|
)
|
|
|
|
let topLine = leftPart + titleStyled + rightPart
|
|
|
|
// Build bottom border (innerWidth horizontals between corners)
|
|
let bottomLine = colorize(
|
|
String(borderStyle.bottomLeft)
|
|
+ String(repeating: borderStyle.horizontal, count: innerWidth)
|
|
+ String(borderStyle.bottomRight),
|
|
with: borderColor
|
|
)
|
|
|
|
// Build result
|
|
var lines: [String] = []
|
|
lines.append(topLine)
|
|
|
|
// Content lines with side borders
|
|
// Important: Add reset before right border to prevent color bleeding
|
|
let reset = "\u{1B}[0m"
|
|
let leftBorder = colorize(String(borderStyle.vertical), with: borderColor)
|
|
let rightBorder = colorize(String(borderStyle.vertical), with: borderColor)
|
|
|
|
for line in contentBuffer.lines {
|
|
let paddedLine = line.padToVisibleWidth(innerWidth)
|
|
lines.append(leftBorder + paddedLine + reset + rightBorder)
|
|
}
|
|
|
|
lines.append(bottomLine)
|
|
|
|
return FrameBuffer(lines: lines)
|
|
}
|
|
|
|
/// Applies color to a string, using theme border color as default.
|
|
private func colorize(_ string: String, with color: Color?) -> String {
|
|
var style = TextStyle()
|
|
style.foregroundColor = color ?? Color.theme.border
|
|
return ANSIRenderer.render(string, with: style)
|
|
}
|
|
}
|