mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
BREAKING CHANGE: Package name changed due to name collision with existing rensbreur/SwiftTUI package. Changes: - Rename package from SwiftTUI to TUIKit in Package.swift - Rename Sources/SwiftTUI to Sources/TUIKit - Rename Sources/SwiftTUIExample to Sources/TUIKitExample - Rename Tests/SwiftTUITests to Tests/TUIKitTests - Rename SwiftTUI.swift to TUIKit.swift - Update all imports: import SwiftTUI -> import TUIKit - Update all code references: SwiftTUI.renderToBuffer -> TUIKit.renderToBuffer - Update documentation comments - Rename swiftTUIVersion to tuiKitVersion All 181 tests passing.
326 lines
10 KiB
Swift
326 lines
10 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 maxW = maxWidth {
|
|
switch maxW {
|
|
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 maxH = maxHeight {
|
|
switch maxH {
|
|
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 minW = minWidth {
|
|
finalWidth = max(finalWidth, minW)
|
|
}
|
|
if let minH = minHeight {
|
|
finalHeight = max(finalHeight, minH)
|
|
}
|
|
|
|
// Apply maximum constraints (expand to fill if infinity)
|
|
if let maxW = maxWidth, case .infinity = maxW {
|
|
finalWidth = context.availableWidth
|
|
}
|
|
if let maxH = maxHeight, case .infinity = maxH {
|
|
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: - Fixed Frame Modifier (Legacy)
|
|
|
|
/// A modifier that constrains a view to a specific width and/or height.
|
|
///
|
|
/// Content is aligned within the frame according to the specified alignment.
|
|
public struct FrameModifier: ViewModifier {
|
|
/// The desired width (nil means intrinsic width).
|
|
public let width: Int?
|
|
|
|
/// The desired height (nil means intrinsic height).
|
|
public let height: Int?
|
|
|
|
/// The alignment of the content within the frame.
|
|
public let alignment: Alignment
|
|
|
|
public func modify(buffer: FrameBuffer, context: RenderContext) -> FrameBuffer {
|
|
let targetWidth = width ?? buffer.width
|
|
let targetHeight = height ?? buffer.height
|
|
|
|
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,
|
|
alignment: alignment.horizontal
|
|
)
|
|
result.append(aligned)
|
|
}
|
|
|
|
return FrameBuffer(lines: result)
|
|
}
|
|
|
|
/// Aligns a single line within the given width.
|
|
private func alignHorizontally(
|
|
_ line: String,
|
|
toWidth targetWidth: Int,
|
|
alignment: HorizontalAlignment
|
|
) -> String {
|
|
let visibleWidth = line.strippedLength
|
|
|
|
if visibleWidth >= targetWidth {
|
|
return line
|
|
}
|
|
|
|
let padding = targetWidth - visibleWidth
|
|
|
|
switch alignment {
|
|
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
|
|
) -> ModifiedView<Self, FrameModifier> {
|
|
modifier(FrameModifier(width: width, height: height, 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
|
|
)
|
|
}
|
|
}
|