diff --git a/Sources/TUIKit/Core/Color.swift b/Sources/TUIKit/Core/Color.swift index fba851b..d8d28bc 100644 --- a/Sources/TUIKit/Core/Color.swift +++ b/Sources/TUIKit/Core/Color.swift @@ -216,15 +216,7 @@ public struct Color: Sendable, Equatable { /// - Parameter amount: The amount to lighten (0-1, default 0.2). /// - Returns: A lighter color. public func lighter(by amount: Double = 0.2) -> Color { - guard case .rgb(let red, let green, let blue) = value else { - return self - } - - let newRed = UInt8(min(255, Double(red) + 255 * amount)) - let newGreen = UInt8(min(255, Double(green) + 255 * amount)) - let newBlue = UInt8(min(255, Double(blue) + 255 * amount)) - - return .rgb(newRed, newGreen, newBlue) + adjusted(by: amount) } /// Returns a darker version of this color. @@ -232,13 +224,24 @@ public struct Color: Sendable, Equatable { /// - Parameter amount: The amount to darken (0-1, default 0.2). /// - Returns: A darker color. public func darker(by amount: Double = 0.2) -> Color { + adjusted(by: -amount) + } + + /// Adjusts brightness by a signed amount. + /// + /// Positive values lighten, negative values darken. + /// + /// - Parameter amount: The adjustment amount (-1 to 1). + /// - Returns: The adjusted color, or self if not an RGB color. + private func adjusted(by amount: Double) -> Color { guard case .rgb(let red, let green, let blue) = value else { return self } - let newRed = UInt8(max(0, Double(red) - 255 * amount)) - let newGreen = UInt8(max(0, Double(green) - 255 * amount)) - let newBlue = UInt8(max(0, Double(blue) - 255 * amount)) + let shift = 255 * amount + let newRed = UInt8(min(255, max(0, Double(red) + shift))) + let newGreen = UInt8(min(255, max(0, Double(green) + shift))) + let newBlue = UInt8(min(255, max(0, Double(blue) + shift))) return .rgb(newRed, newGreen, newBlue) } diff --git a/Sources/TUIKit/Core/Environment.swift b/Sources/TUIKit/Core/Environment.swift index 2d4140c..b573186 100644 --- a/Sources/TUIKit/Core/Environment.swift +++ b/Sources/TUIKit/Core/Environment.swift @@ -202,24 +202,7 @@ extension EnvironmentModifier: Renderable { // Render content with modified environment return EnvironmentStorage.shared.withEnvironment(modifiedEnvironment) { - renderView(content, context: modifiedContext) + TUIKit.renderToBuffer(content, context: modifiedContext) } } } - -// MARK: - Internal Rendering Helper - -/// Internal helper to render a view (avoids name collision with Renderable.renderToBuffer). -private func renderView(_ view: V, context: RenderContext) -> FrameBuffer { - if let renderable = view as? Renderable { - return renderable.renderToBuffer(context: context) - } - - if V.Body.self != Never.self { - return renderView(view.body, context: context) - } - - return FrameBuffer() -} - - diff --git a/Sources/TUIKit/Core/Focus.swift b/Sources/TUIKit/Core/Focus.swift index cfcea59..3bfe4dc 100644 --- a/Sources/TUIKit/Core/Focus.swift +++ b/Sources/TUIKit/Core/Focus.swift @@ -148,24 +148,23 @@ public final class FocusManager: @unchecked Sendable { /// Moves focus to the next focusable element. public func focusNext() { - guard !focusables.isEmpty else { return } - - let availableFocusables = focusables.filter { $0.canBeFocused } - guard !availableFocusables.isEmpty else { return } - - if let currentID = focusedID, - let currentIndex = availableFocusables.firstIndex(where: { $0.focusID == currentID }) { - // Move to next (wrap around) - let nextIndex = (currentIndex + 1) % availableFocusables.count - focus(availableFocusables[nextIndex]) - } else { - // Focus first available - focus(availableFocusables[0]) - } + moveFocus(direction: .forward) } /// Moves focus to the previous focusable element. public func focusPrevious() { + moveFocus(direction: .backward) + } + + /// The direction in which focus moves. + private enum FocusDirection { + case forward, backward + } + + /// Moves focus in the specified direction (wrapping around). + /// + /// - Parameter direction: The direction to move focus. + private func moveFocus(direction: FocusDirection) { guard !focusables.isEmpty else { return } let availableFocusables = focusables.filter { $0.canBeFocused } @@ -173,12 +172,18 @@ public final class FocusManager: @unchecked Sendable { if let currentID = focusedID, let currentIndex = availableFocusables.firstIndex(where: { $0.focusID == currentID }) { - // Move to previous (wrap around) - let prevIndex = currentIndex == 0 ? availableFocusables.count - 1 : currentIndex - 1 - focus(availableFocusables[prevIndex]) + let targetIndex: Int + switch direction { + case .forward: + targetIndex = (currentIndex + 1) % availableFocusables.count + case .backward: + targetIndex = currentIndex == 0 ? availableFocusables.count - 1 : currentIndex - 1 + } + focus(availableFocusables[targetIndex]) } else { - // Focus last available - focus(availableFocusables[availableFocusables.count - 1]) + // No current focus: forward → first, backward → last + let fallbackIndex = direction == .forward ? 0 : availableFocusables.count - 1 + focus(availableFocusables[fallbackIndex]) } } diff --git a/Sources/TUIKit/Core/Theme.swift b/Sources/TUIKit/Core/Theme.swift index 7ebb2c7..c6479fc 100644 --- a/Sources/TUIKit/Core/Theme.swift +++ b/Sources/TUIKit/Core/Theme.swift @@ -640,12 +640,16 @@ public struct GeneratedTheme: Theme, Sendable { /// Predefined hue values for common generated themes. public enum Hue { + /// Green hue (120°). + public static let green: Double = 120 /// Violet hue (270°). public static let violet: Double = 270 } // MARK: - Presets + /// A green generated theme — for direct comparison with GreenPhosphorTheme. + public static let green = GeneratedTheme(name: "Gen. Green", hue: Hue.green) /// A violet generated theme. public static let violet = GeneratedTheme(name: "Violet", hue: Hue.violet) } @@ -656,9 +660,10 @@ public struct GeneratedTheme: Theme, Sendable { public struct ThemeRegistry { /// All available themes in cycling order. /// - /// Order: Green → Amber → White → Red → NCurses → Violet (generated) + /// Order: Green → Gen. Green → Amber → White → Red → NCurses → Violet (generated) public static let all: [Theme] = [ GreenPhosphorTheme(), + GeneratedTheme.green, AmberPhosphorTheme(), WhitePhosphorTheme(), RedPhosphorTheme(), diff --git a/Sources/TUIKit/Modifiers/BorderModifier.swift b/Sources/TUIKit/Modifiers/BorderModifier.swift index d885b12..7dbbf99 100644 --- a/Sources/TUIKit/Modifiers/BorderModifier.swift +++ b/Sources/TUIKit/Modifiers/BorderModifier.swift @@ -34,7 +34,7 @@ extension BorderedView: Renderable { // Reduce available width for content by 2 (left + right border) var contentContext = context - contentContext.availableWidth = max(1, context.availableWidth - 2) + contentContext.availableWidth = max(1, context.availableWidth - BorderRenderer.borderWidthOverhead) // Render content with reduced width let buffer = TUIKit.renderToBuffer(content, context: contentContext) diff --git a/Sources/TUIKit/Rendering/BorderRenderer.swift b/Sources/TUIKit/Rendering/BorderRenderer.swift index 254d967..4d6c7c4 100644 --- a/Sources/TUIKit/Rendering/BorderRenderer.swift +++ b/Sources/TUIKit/Rendering/BorderRenderer.swift @@ -16,6 +16,9 @@ /// - **Block**: half-block characters (▄ █ ▀) for smooth visual edges public enum BorderRenderer { + /// The total width consumed by left + right border characters (1 + 1 = 2). + public static let borderWidthOverhead = 2 + // MARK: - Standard Style (Box-Drawing Characters) /// Renders a plain top border line. @@ -155,7 +158,7 @@ public enum BorderRenderer { innerWidth: Int, color: Color ) -> String { - let line = String(repeating: "▄", count: innerWidth + 2) + let line = String(repeating: BorderStyle.block.horizontal, count: innerWidth + 2) return ANSIRenderer.colorize(line, foreground: color) } @@ -171,7 +174,7 @@ public enum BorderRenderer { innerWidth: Int, color: Color ) -> String { - let line = String(repeating: "▀", count: innerWidth + 2) + let line = String(repeating: BorderStyle.blockBottomHorizontal, count: innerWidth + 2) return ANSIRenderer.colorize(line, foreground: color) } @@ -191,7 +194,7 @@ public enum BorderRenderer { sectionColor: Color ) -> String { let paddedLine = content.padToVisibleWidth(innerWidth) - let sideBorder = ANSIRenderer.colorize("█", foreground: sectionColor) + let sideBorder = ANSIRenderer.colorize(String(BorderStyle.block.vertical), foreground: sectionColor) let styledContent = ANSIRenderer.applyPersistentBackground(paddedLine, color: sectionColor) return sideBorder + styledContent + ANSIRenderer.reset + sideBorder } @@ -203,13 +206,13 @@ public enum BorderRenderer { /// /// - Parameters: /// - innerWidth: The content width (separator width = innerWidth + 2). - /// - character: The separator character (`"▀"` for header→body, `"▄"` for body→footer). + /// - character: The separator character (`.blockBottomHorizontal` for header→body, `.blockFooterSeparator` 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 = "▀", + character: Character = BorderStyle.blockBottomHorizontal, foregroundColor: Color, backgroundColor: Color ) -> String { diff --git a/Sources/TUIKit/TUIKit.swift b/Sources/TUIKit/TUIKit.swift index cf8b886..a37bbf1 100644 --- a/Sources/TUIKit/TUIKit.swift +++ b/Sources/TUIKit/TUIKit.swift @@ -31,10 +31,13 @@ public let tuiKitVersion = "0.1.0" /// ``` /// /// - Parameter content: A ViewBuilder closure that defines the view to render. -@discardableResult -public func renderOnce(@ViewBuilder content: () -> Content) -> Int { +/// Renders a view hierarchy once and prints the result to standard output. +/// +/// This is useful for simple CLI tools that don't need a full App lifecycle. +/// +/// - Parameter content: A ViewBuilder closure that defines the view to render. +public func renderOnce(@ViewBuilder content: () -> Content) { let view = content() let renderer = ViewRenderer() renderer.render(view) - return 0 // Line count not tracked by ViewRenderer } diff --git a/Sources/TUIKit/Views/Alert.swift b/Sources/TUIKit/Views/Alert.swift index b40cd4a..c61de8c 100644 --- a/Sources/TUIKit/Views/Alert.swift +++ b/Sources/TUIKit/Views/Alert.swift @@ -256,41 +256,21 @@ extension Alert { extension Alert where Actions == EmptyView { /// Creates a warning-style alert without actions. public static func warning(title: String = "Warning", message: String) -> Alert { - Alert( - title: title, - message: message, - borderColor: .yellow, - titleColor: .yellow - ) + Alert.warning(title: title, message: message) { EmptyView() } } /// Creates an error-style alert without actions. public static func error(title: String = "Error", message: String) -> Alert { - Alert( - title: title, - message: message, - borderColor: .red, - titleColor: .red - ) + Alert.error(title: title, message: message) { EmptyView() } } /// Creates an info-style alert without actions. public static func info(title: String = "Info", message: String) -> Alert { - Alert( - title: title, - message: message, - borderColor: .cyan, - titleColor: .cyan - ) + Alert.info(title: title, message: message) { EmptyView() } } /// Creates a success-style alert without actions. public static func success(title: String = "Success", message: String) -> Alert { - Alert( - title: title, - message: message, - borderColor: .green, - titleColor: .green - ) + Alert.success(title: title, message: message) { EmptyView() } } } diff --git a/Sources/TUIKit/Views/ContainerView.swift b/Sources/TUIKit/Views/ContainerView.swift index 9bfa70e..782af00 100644 --- a/Sources/TUIKit/Views/ContainerView.swift +++ b/Sources/TUIKit/Views/ContainerView.swift @@ -330,7 +330,7 @@ extension ContainerView: Renderable { if let footerBuf = footerBuffer, !footerBuf.isEmpty { if style.showFooterSeparator { lines.append(BorderRenderer.blockSeparator( - innerWidth: innerWidth, character: "▄", + innerWidth: innerWidth, character: BorderStyle.blockFooterSeparator, foregroundColor: headerFooterBg, backgroundColor: bodyBg )) } diff --git a/Sources/TUIKit/Views/StatusBar.swift b/Sources/TUIKit/Views/StatusBar.swift index 8d6d5b5..9b6807d 100644 --- a/Sources/TUIKit/Views/StatusBar.swift +++ b/Sources/TUIKit/Views/StatusBar.swift @@ -896,7 +896,7 @@ extension StatusBar: Renderable { /// Renders the bordered style using the current appearance's border style. private func renderBordered(itemStrings: [String], width: Int, context: RenderContext) -> FrameBuffer { - let innerWidth = width - 2 // Account for left and right border + let innerWidth = width - BorderRenderer.borderWidthOverhead let content = alignContent(itemStrings: itemStrings, width: innerWidth) // Check if we're using block appearance for special rendering