Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 44 additions & 36 deletions Sources/TUIkit/Extensions/View+Layout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// Created by LAYERED.work
// License: MIT

import Foundation

// MARK: - Border

extension View {
Expand Down Expand Up @@ -114,23 +116,26 @@ extension View {
/// ```
///
/// - 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).
/// - width: The desired width (nil preserves intrinsic width).
/// - height: The desired height (nil preserves intrinsic height).
/// - alignment: The alignment within the frame (default: .center,
/// matching SwiftUI).
/// - Returns: A view constrained to the specified frame.
public func frame(
width: Int? = nil,
height: Int? = nil,
alignment: Alignment = .topLeading
width: CGFloat? = nil,
height: CGFloat? = nil,
alignment: Alignment = .center
) -> some View {
FlexibleFrameView(
let cellWidth = width.map { max(0, TerminalGeometry.cells($0)) }
let cellHeight = height.map { max(0, TerminalGeometry.cells($0)) }
return FlexibleFrameView(
content: self,
minWidth: width,
idealWidth: width,
maxWidth: width.map { .fixed($0) },
minHeight: height,
idealHeight: height,
maxHeight: height.map { .fixed($0) },
minWidth: cellWidth,
idealWidth: cellWidth,
maxWidth: cellWidth,
minHeight: cellHeight,
idealHeight: cellHeight,
maxHeight: cellHeight,
alignment: alignment
)
}
Expand All @@ -156,31 +161,31 @@ extension View {
/// ```
///
/// - Parameters:
/// - minWidth: Minimum width in characters.
/// - minWidth: Minimum width.
/// - idealWidth: Preferred width (used when no max is set).
/// - maxWidth: Maximum width, or `.infinity` to fill available space.
/// - minHeight: Minimum height in lines.
/// - minHeight: Minimum height.
/// - 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,
minWidth: CGFloat? = nil,
idealWidth: CGFloat? = nil,
maxWidth: CGFloat? = nil,
minHeight: CGFloat? = nil,
idealHeight: CGFloat? = nil,
maxHeight: CGFloat? = nil,
alignment: Alignment = .center
) -> some View {
FlexibleFrameView(
content: self,
minWidth: minWidth,
idealWidth: idealWidth,
maxWidth: maxWidth,
minHeight: minHeight,
idealHeight: idealHeight,
maxHeight: maxHeight,
minWidth: minWidth.map { max(0, TerminalGeometry.cells($0)) },
idealWidth: idealWidth.map { max(0, TerminalGeometry.cells($0)) },
maxWidth: maxWidth.map { max(0, TerminalGeometry.cells($0)) },
minHeight: minHeight.map { max(0, TerminalGeometry.cells($0)) },
idealHeight: idealHeight.map { max(0, TerminalGeometry.cells($0)) },
maxHeight: maxHeight.map { max(0, TerminalGeometry.cells($0)) },
alignment: alignment
)
}
Expand Down Expand Up @@ -232,29 +237,32 @@ extension View {
/// .padding(2) // 2 lines top/bottom, 2 chars left/right
/// ```
///
/// - Parameter amount: The padding amount on all sides (default: 1).
/// - Parameter length: The padding amount on all sides.
/// - Returns: A padded view.
public func padding(_ amount: Int = 1) -> some View {
BufferModifiedView(content: self, modifier: PaddingModifier(insets: EdgeInsets(all: amount)))
public func padding(_ length: CGFloat) -> some View {
padding(.all, length)
}

/// Adds padding on specific edges.
///
/// In a terminal context, 1 unit of padding means:
/// - **Vertical (top/bottom):** 1 line
/// - **Horizontal (leading/trailing):** 1 character
/// Matches SwiftUI's signature. In a terminal context, 1 unit of
/// padding means one line vertically and one character horizontally;
/// `nil` uses the terminal default of one cell. Fractional lengths
/// quantize through `TerminalGeometry`.
///
/// ```swift
/// Text("Hello")
/// .padding() // 1 cell on all edges
/// .padding(.horizontal, 4) // 4 chars left and right
/// .padding(.vertical, 2) // 2 lines top and bottom
/// ```
///
/// - Parameters:
/// - edges: The edges to pad.
/// - amount: The padding amount (default: 1).
/// - edges: The set of edges to pad (default: all).
/// - length: The padding amount, or `nil` for the default cell.
/// - Returns: A padded view.
public func padding(_ edges: Edge, _ amount: Int = 1) -> some View {
public func padding(_ edges: Edge.Set = .all, _ length: CGFloat? = nil) -> some View {
let amount = CGFloat(TerminalGeometry.spacing(length, default: 1))
let insets = EdgeInsets(
top: edges.contains(.top) ? amount : 0,
leading: edges.contains(.leading) ? amount : 0,
Expand Down
2 changes: 2 additions & 0 deletions Sources/TUIkit/Extensions/View+ListRowSeparator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// Created by LAYERED.work
// License: MIT

import Foundation

// MARK: - List Row Separator Modifier

extension View {
Expand Down
73 changes: 22 additions & 51 deletions Sources/TUIkit/Modifiers/FrameModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,6 @@
// Created by LAYERED.work
// License: MIT

// 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.
Expand All @@ -34,17 +20,17 @@ public struct FlexibleFrameView<Content: View>: View {
/// The ideal width in characters, or nil to use intrinsic size.
let idealWidth: Int?

/// The maximum width constraint, or nil for no maximum.
let maxWidth: FrameDimension?
/// The maximum width in cells (`Int.max` fills), or nil for no maximum.
let maxWidth: Int?

/// The minimum height in lines, or nil for no minimum.
let minHeight: Int?

/// The ideal height in lines, or nil to use intrinsic size.
let idealHeight: Int?

/// The maximum height constraint, or nil for no maximum.
let maxHeight: FrameDimension?
/// The maximum height in cells (`Int.max` fills), or nil for no maximum.
let maxHeight: Int?

/// The alignment of the content within the frame.
let alignment: Alignment
Expand Down Expand Up @@ -76,12 +62,9 @@ extension FlexibleFrameView: Renderable {
// 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)
}
targetWidth = maximumWidth == .max
? context.availableWidth
: min(maximumWidth, context.availableWidth)
} else if let ideal = idealWidth {
targetWidth = min(ideal, context.availableWidth)
} else {
Expand All @@ -92,12 +75,9 @@ extension FlexibleFrameView: Renderable {
// 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)
}
targetHeight = maximumHeight == .max
? context.availableHeight
: min(maximumHeight, context.availableHeight)
} else if let ideal = idealHeight {
targetHeight = min(ideal, context.availableHeight)
} else {
Expand Down Expand Up @@ -131,10 +111,10 @@ extension FlexibleFrameView: Renderable {
}

// Apply maximum constraints (expand to fill if infinity)
if let maximumWidth = maxWidth, case .infinity = maximumWidth {
if maxWidth == .max {
finalWidth = context.availableWidth
}
if let maximumHeight = maxHeight, case .infinity = maximumHeight {
if maxHeight == .max {
finalHeight = context.availableHeight
}

Expand All @@ -152,15 +132,10 @@ extension FlexibleFrameView: Renderable {
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)
}
let verticalOffset = alignment.vertical.cellOffset(
childHeight: buffer.height,
containerHeight: targetHeight
)

for row in 0..<targetHeight {
let contentRow = row - verticalOffset
Expand Down Expand Up @@ -189,15 +164,11 @@ extension FlexibleFrameView: Renderable {

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
}
let left = alignment.horizontal.cellOffset(
childWidth: visibleWidth,
containerWidth: targetWidth
)
let right = padding - left
return String(repeating: " ", count: left) + line + String(repeating: " ", count: right)
}
}
2 changes: 2 additions & 0 deletions Sources/TUIkit/Modifiers/ListRowSeparatorModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// Created by LAYERED.work
// License: MIT

import Foundation

/// A stub modifier for list row separators.
///
/// This modifier exists for SwiftUI API compatibility but has no visual effect
Expand Down
26 changes: 8 additions & 18 deletions Sources/TUIkit/Modifiers/OverlayModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,26 +57,16 @@ extension OverlayModifier: Renderable {
let overlayHeight = overlayBuffer.height

// Calculate horizontal position
let horizontalOffset: Int
switch alignment.horizontal {
case .leading:
horizontalOffset = 0
case .center:
horizontalOffset = max(0, (baseWidth - overlayWidth) / 2)
case .trailing:
horizontalOffset = max(0, baseWidth - overlayWidth)
}
let horizontalOffset = alignment.horizontal.cellOffset(
childWidth: overlayWidth,
containerWidth: baseWidth
)

// Calculate vertical position
let verticalOffset: Int
switch alignment.vertical {
case .top:
verticalOffset = 0
case .center:
verticalOffset = max(0, (baseHeight - overlayHeight) / 2)
case .bottom:
verticalOffset = max(0, baseHeight - overlayHeight)
}
let verticalOffset = alignment.vertical.cellOffset(
childHeight: overlayHeight,
containerHeight: baseHeight
)

// Composite the overlay onto the base
return baseBuffer.composited(with: overlayBuffer, at: (x: horizontalOffset, y: verticalOffset))
Expand Down
Loading
Loading