mirror of
https://github.com/phranck/TUIkit.git
synced 2026-05-21 09:50:35 +00:00
f118437092
- Remove dead code: legacy BorderModifier (70 lines), legacy FrameModifier (74 lines) - Fix state-mismatch bug in Theme/Appearance setTheme()/setAppearance() - Eliminate force unwraps in Panel.body and Menu.dividerLineIndex - Remove anti-patterns: _ = self, pointless ternary, identical branches - Rename all 1-2 char variables to self-descriptive names across 9 files - Centralize ANSI constants: add dim, ansiPattern to ANSIRenderer - Replace all hardcoded ANSI reset strings with ANSIRenderer.reset - Replace raw ANSI regex with ANSIRenderer.ansiPattern in FrameBuffer - Inline all local ANSIRenderer alias variables (13 removals) - Replace manual CSI-building in BackgroundModifier with ANSIRenderer.backgroundCode() - Migrate View.frame() to use FlexibleFrameView - Update macOS deployment target from 10.15 to 13 - Remove redundant explicit raw values from DemoPage enum 22 files changed, 113 insertions(+), 310 deletions(-)
255 lines
8.2 KiB
Swift
255 lines
8.2 KiB
Swift
//
|
|
// FrameModifier.swift
|
|
// TUIKit
|
|
//
|
|
// The .frame() modifier for setting explicit size constraints.
|
|
//
|
|
|
|
// MARK: - Frame Dimension
|
|
|
|
/// Represents a frame dimension that can be a fixed value or infinity.
|
|
public enum FrameDimension: Equatable, Sendable {
|
|
/// A fixed size in characters/lines.
|
|
case fixed(Int)
|
|
|
|
/// Expand to fill all available space.
|
|
case infinity
|
|
|
|
/// The special infinity value for frame constraints.
|
|
public static let max: FrameDimension = .infinity
|
|
}
|
|
|
|
// MARK: - Flexible Frame View
|
|
|
|
/// A view that applies flexible frame constraints to its content.
|
|
///
|
|
/// This view handles min/max constraints and renders content with
|
|
/// the appropriate available space.
|
|
public struct FlexibleFrameView<Content: View>: View {
|
|
let content: Content
|
|
let minWidth: Int?
|
|
let idealWidth: Int?
|
|
let maxWidth: FrameDimension?
|
|
let minHeight: Int?
|
|
let idealHeight: Int?
|
|
let maxHeight: FrameDimension?
|
|
let alignment: Alignment
|
|
|
|
public var body: Never {
|
|
fatalError("FlexibleFrameView renders via Renderable")
|
|
}
|
|
}
|
|
|
|
extension FlexibleFrameView: Renderable {
|
|
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
|
|
// Calculate the target width based on constraints
|
|
let targetWidth: Int
|
|
if let maximumWidth = maxWidth {
|
|
switch maximumWidth {
|
|
case .infinity:
|
|
targetWidth = context.availableWidth
|
|
case .fixed(let value):
|
|
targetWidth = min(value, context.availableWidth)
|
|
}
|
|
} else if let ideal = idealWidth {
|
|
targetWidth = min(ideal, context.availableWidth)
|
|
} else {
|
|
// No max constraint - render with available width, then size to content
|
|
targetWidth = context.availableWidth
|
|
}
|
|
|
|
// Calculate the target height based on constraints
|
|
let targetHeight: Int?
|
|
if let maximumHeight = maxHeight {
|
|
switch maximumHeight {
|
|
case .infinity:
|
|
targetHeight = context.availableHeight
|
|
case .fixed(let value):
|
|
targetHeight = min(value, context.availableHeight)
|
|
}
|
|
} else if let ideal = idealHeight {
|
|
targetHeight = min(ideal, context.availableHeight)
|
|
} else {
|
|
targetHeight = nil // Use intrinsic height
|
|
}
|
|
|
|
// Create context for content with constrained width
|
|
var contentContext = context
|
|
contentContext.availableWidth = targetWidth
|
|
if let height = targetHeight {
|
|
contentContext.availableHeight = height
|
|
}
|
|
|
|
// Render content
|
|
let buffer = TUIKit.renderToBuffer(content, context: contentContext)
|
|
|
|
// Apply minimum constraints
|
|
var finalWidth = buffer.width
|
|
var finalHeight = buffer.height
|
|
|
|
if let minimumWidth = minWidth {
|
|
finalWidth = max(finalWidth, minimumWidth)
|
|
}
|
|
if let minimumHeight = minHeight {
|
|
finalHeight = max(finalHeight, minimumHeight)
|
|
}
|
|
|
|
// Apply maximum constraints (expand to fill if infinity)
|
|
if let maximumWidth = maxWidth, case .infinity = maximumWidth {
|
|
finalWidth = context.availableWidth
|
|
}
|
|
if let maximumHeight = maxHeight, case .infinity = maximumHeight {
|
|
finalHeight = context.availableHeight
|
|
}
|
|
|
|
// If size matches buffer, return as-is
|
|
if finalWidth == buffer.width && finalHeight == buffer.height {
|
|
return buffer
|
|
}
|
|
|
|
// Otherwise, align content within the frame
|
|
return alignBuffer(buffer, toWidth: finalWidth, height: finalHeight)
|
|
}
|
|
|
|
/// Aligns buffer content within the target frame size.
|
|
private func alignBuffer(_ buffer: FrameBuffer, toWidth targetWidth: Int, height targetHeight: Int) -> FrameBuffer {
|
|
var result: [String] = []
|
|
|
|
// Calculate vertical offset for alignment
|
|
let verticalOffset: Int
|
|
switch alignment.vertical {
|
|
case .top:
|
|
verticalOffset = 0
|
|
case .center:
|
|
verticalOffset = max(0, (targetHeight - buffer.height) / 2)
|
|
case .bottom:
|
|
verticalOffset = max(0, targetHeight - buffer.height)
|
|
}
|
|
|
|
for row in 0..<targetHeight {
|
|
let contentRow = row - verticalOffset
|
|
let line: String
|
|
if contentRow >= 0 && contentRow < buffer.lines.count {
|
|
line = buffer.lines[contentRow]
|
|
} else {
|
|
line = ""
|
|
}
|
|
|
|
// Align horizontally within the frame
|
|
let aligned = alignHorizontally(line, toWidth: targetWidth)
|
|
result.append(aligned)
|
|
}
|
|
|
|
return FrameBuffer(lines: result)
|
|
}
|
|
|
|
/// Aligns a single line within the given width.
|
|
private func alignHorizontally(_ line: String, toWidth targetWidth: Int) -> String {
|
|
let visibleWidth = line.strippedLength
|
|
|
|
if visibleWidth >= targetWidth {
|
|
return line
|
|
}
|
|
|
|
let padding = targetWidth - visibleWidth
|
|
|
|
switch alignment.horizontal {
|
|
case .leading:
|
|
return line + String(repeating: " ", count: padding)
|
|
case .center:
|
|
let left = padding / 2
|
|
let right = padding - left
|
|
return String(repeating: " ", count: left) + line + String(repeating: " ", count: right)
|
|
case .trailing:
|
|
return String(repeating: " ", count: padding) + line
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - View Extension
|
|
|
|
extension View {
|
|
/// Sets an explicit frame size for this view.
|
|
///
|
|
/// The content is aligned within the frame according to the specified alignment.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```swift
|
|
/// Text("Hello")
|
|
/// .frame(width: 20, alignment: .center)
|
|
/// ```
|
|
///
|
|
/// - Parameters:
|
|
/// - width: The desired width in characters (nil preserves intrinsic width).
|
|
/// - height: The desired height in lines (nil preserves intrinsic height).
|
|
/// - alignment: The alignment within the frame (default: .topLeading).
|
|
/// - Returns: A view constrained to the specified frame.
|
|
public func frame(
|
|
width: Int? = nil,
|
|
height: Int? = nil,
|
|
alignment: Alignment = .topLeading
|
|
) -> some View {
|
|
FlexibleFrameView(
|
|
content: self,
|
|
minWidth: width,
|
|
idealWidth: width,
|
|
maxWidth: width.map { .fixed($0) },
|
|
minHeight: height,
|
|
idealHeight: height,
|
|
maxHeight: height.map { .fixed($0) },
|
|
alignment: alignment
|
|
)
|
|
}
|
|
|
|
/// Sets flexible frame constraints for this view.
|
|
///
|
|
/// Use `.infinity` for maxWidth/maxHeight to expand to fill available space.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```swift
|
|
/// // Expand to full width
|
|
/// Text("Hello")
|
|
/// .frame(maxWidth: .infinity)
|
|
///
|
|
/// // Expand to full size
|
|
/// Color.blue
|
|
/// .frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
///
|
|
/// // Minimum size with expansion
|
|
/// Text("Button")
|
|
/// .frame(minWidth: 10, maxWidth: .infinity)
|
|
/// ```
|
|
///
|
|
/// - Parameters:
|
|
/// - minWidth: Minimum width in characters.
|
|
/// - idealWidth: Preferred width (used when no max is set).
|
|
/// - maxWidth: Maximum width, or `.infinity` to fill available space.
|
|
/// - minHeight: Minimum height in lines.
|
|
/// - idealHeight: Preferred height (used when no max is set).
|
|
/// - maxHeight: Maximum height, or `.infinity` to fill available space.
|
|
/// - alignment: The alignment within the frame (default: .center).
|
|
/// - Returns: A view with flexible frame constraints.
|
|
public func frame(
|
|
minWidth: Int? = nil,
|
|
idealWidth: Int? = nil,
|
|
maxWidth: FrameDimension? = nil,
|
|
minHeight: Int? = nil,
|
|
idealHeight: Int? = nil,
|
|
maxHeight: FrameDimension? = nil,
|
|
alignment: Alignment = .center
|
|
) -> some View {
|
|
FlexibleFrameView(
|
|
content: self,
|
|
minWidth: minWidth,
|
|
idealWidth: idealWidth,
|
|
maxWidth: maxWidth,
|
|
minHeight: minHeight,
|
|
idealHeight: idealHeight,
|
|
maxHeight: maxHeight,
|
|
alignment: alignment
|
|
)
|
|
}
|
|
}
|