mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
refactor: Quick wins — deduplicate Alert presets, remove renderView duplicate, refactor Color/Focus, replace magic values (A.9, A.11, A.13, A.14, C.5, F.3, F.4)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<V: View>(_ 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()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Content: View>(@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<Content: View>(@ViewBuilder content: () -> Content) {
|
||||
let view = content()
|
||||
let renderer = ViewRenderer()
|
||||
renderer.render(view)
|
||||
return 0 // Line count not tracked by ViewRenderer
|
||||
}
|
||||
|
||||
@@ -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<EmptyView> {
|
||||
Alert<EmptyView>(
|
||||
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<EmptyView> {
|
||||
Alert<EmptyView>(
|
||||
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<EmptyView> {
|
||||
Alert<EmptyView>(
|
||||
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<EmptyView> {
|
||||
Alert<EmptyView>(
|
||||
title: title,
|
||||
message: message,
|
||||
borderColor: .green,
|
||||
titleColor: .green
|
||||
)
|
||||
Alert.success(title: title, message: message) { EmptyView() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user