diff --git a/Sources/TUIkit/Core/ListRowExtractor.swift b/Sources/TUIkit/Core/ListRowExtractor.swift index 31ed5167..044c61b0 100644 --- a/Sources/TUIkit/Core/ListRowExtractor.swift +++ b/Sources/TUIkit/Core/ListRowExtractor.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - List Row diff --git a/Sources/TUIkit/Core/SelectableListRow.swift b/Sources/TUIkit/Core/SelectableListRow.swift index 7f2b8bf2..d77ed08f 100644 --- a/Sources/TUIkit/Core/SelectableListRow.swift +++ b/Sources/TUIkit/Core/SelectableListRow.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - List Row Type diff --git a/Sources/TUIkit/Environment/Environment.swift b/Sources/TUIkit/Environment/Environment.swift index 0d106e8d..d2798d8e 100644 --- a/Sources/TUIkit/Environment/Environment.swift +++ b/Sources/TUIkit/Environment/Environment.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - Environment Key Protocol diff --git a/Sources/TUIkit/Environment/Preferences.swift b/Sources/TUIkit/Environment/Preferences.swift index e2c7b565..149313b6 100644 --- a/Sources/TUIkit/Environment/Preferences.swift +++ b/Sources/TUIkit/Environment/Preferences.swift @@ -5,7 +5,6 @@ // License: MIT Similar to SwiftUI's PreferenceKey system. // -import Foundation // MARK: - Preference Key Protocol diff --git a/Sources/TUIkit/Extensions/View+Presentation.swift b/Sources/TUIkit/Extensions/View+Presentation.swift index 19c58ae9..4b06483e 100644 --- a/Sources/TUIkit/Extensions/View+Presentation.swift +++ b/Sources/TUIkit/Extensions/View+Presentation.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - Alert Presentation diff --git a/Sources/TUIkit/Focus/ItemListHandler.swift b/Sources/TUIkit/Focus/ItemListHandler.swift index b8b60220..4e1d1f60 100644 --- a/Sources/TUIkit/Focus/ItemListHandler.swift +++ b/Sources/TUIkit/Focus/ItemListHandler.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - Selection Mode diff --git a/Sources/TUIkit/Focus/TextFieldHandler+Clipboard.swift b/Sources/TUIkit/Focus/TextFieldHandler+Clipboard.swift new file mode 100644 index 00000000..c9e3c4eb --- /dev/null +++ b/Sources/TUIkit/Focus/TextFieldHandler+Clipboard.swift @@ -0,0 +1,193 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// TextFieldHandler+Clipboard.swift +// +// Created by LAYERED.work +// License: MIT + +import Foundation + +// MARK: - Clipboard Operations + +extension TextFieldHandler { + /// Selects all text in the field. + func selectAll() { + guard !text.wrappedValue.isEmpty else { return } + selectionAnchor = 0 + cursorPosition = text.wrappedValue.count + } + + /// Copies the selected text to the system clipboard. + /// + /// Uses `pbcopy` on macOS. Does nothing if no text is selected. + func copySelection() { + guard let range = selectionRange else { return } + + let current = text.wrappedValue + let startIndex = current.index(current.startIndex, offsetBy: range.lowerBound) + let endIndex = current.index(current.startIndex, offsetBy: range.upperBound) + let selectedText = String(current[startIndex.. String? { + #if os(macOS) + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/pbpaste") + + let pipe = Pipe() + process.standardOutput = pipe + + do { + try process.run() + process.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + // Strip trailing newline that pbpaste adds + var result = String(data: data, encoding: .utf8) ?? "" + if result.hasSuffix("\n") { + result.removeLast() + } + return result + } catch { + return nil + } + #elseif os(Linux) + // Try xclip first, then xsel + for command in ["/usr/bin/xclip", "/usr/bin/xsel"] { + if FileManager.default.fileExists(atPath: command) { + let process = Process() + process.executableURL = URL(fileURLWithPath: command) + process.arguments = command.contains("xclip") ? ["-selection", "clipboard", "-o"] : ["--clipboard", "--output"] + + let pipe = Pipe() + process.standardOutput = pipe + + do { + try process.run() + process.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + var result = String(data: data, encoding: .utf8) ?? "" + if result.hasSuffix("\n") { + result.removeLast() + } + return result + } catch { + continue + } + } + } + return nil + #else + return nil + #endif + } +} diff --git a/Sources/TUIkit/Focus/TextFieldHandler.swift b/Sources/TUIkit/Focus/TextFieldHandler.swift index 2a6d5ee6..d2d396c3 100644 --- a/Sources/TUIkit/Focus/TextFieldHandler.swift +++ b/Sources/TUIkit/Focus/TextFieldHandler.swift @@ -158,7 +158,7 @@ extension TextFieldHandler { /// Used internally when undo state has already been pushed. /// /// - Parameter range: The range of characters to delete. - private func deleteRangeWithoutUndo(_ range: Range) { + func deleteRangeWithoutUndo(_ range: Range) { var current = text.wrappedValue let startIndex = current.index(current.startIndex, offsetBy: range.lowerBound) let endIndex = current.index(current.startIndex, offsetBy: range.upperBound) @@ -408,7 +408,7 @@ extension TextFieldHandler { extension TextFieldHandler { /// Pushes the current state onto the undo stack. - private func pushUndoState() { + func pushUndoState() { let state = (text: text.wrappedValue, cursor: cursorPosition) // Avoid duplicate states @@ -433,192 +433,6 @@ extension TextFieldHandler { } } -// MARK: - Clipboard Operations - -extension TextFieldHandler { - /// Selects all text in the field. - func selectAll() { - guard !text.wrappedValue.isEmpty else { return } - selectionAnchor = 0 - cursorPosition = text.wrappedValue.count - } - - /// Copies the selected text to the system clipboard. - /// - /// Uses `pbcopy` on macOS. Does nothing if no text is selected. - func copySelection() { - guard let range = selectionRange else { return } - - let current = text.wrappedValue - let startIndex = current.index(current.startIndex, offsetBy: range.lowerBound) - let endIndex = current.index(current.startIndex, offsetBy: range.upperBound) - let selectedText = String(current[startIndex.. String? { - #if os(macOS) - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/pbpaste") - - let pipe = Pipe() - process.standardOutput = pipe - - do { - try process.run() - process.waitUntilExit() - - let data = pipe.fileHandleForReading.readDataToEndOfFile() - // Strip trailing newline that pbpaste adds - var result = String(data: data, encoding: .utf8) ?? "" - if result.hasSuffix("\n") { - result.removeLast() - } - return result - } catch { - return nil - } - #elseif os(Linux) - // Try xclip first, then xsel - for command in ["/usr/bin/xclip", "/usr/bin/xsel"] { - if FileManager.default.fileExists(atPath: command) { - let process = Process() - process.executableURL = URL(fileURLWithPath: command) - process.arguments = command.contains("xclip") ? ["-selection", "clipboard", "-o"] : ["--clipboard", "--output"] - - let pipe = Pipe() - process.standardOutput = pipe - - do { - try process.run() - process.waitUntilExit() - - let data = pipe.fileHandleForReading.readDataToEndOfFile() - var result = String(data: data, encoding: .utf8) ?? "" - if result.hasSuffix("\n") { - result.removeLast() - } - return result - } catch { - continue - } - } - } - return nil - #else - return nil - #endif - } -} - // MARK: - Focus Lifecycle extension TextFieldHandler { diff --git a/Sources/TUIkit/Modifiers/BadgeModifier.swift b/Sources/TUIkit/Modifiers/BadgeModifier.swift index fd965dc4..63adaade 100644 --- a/Sources/TUIkit/Modifiers/BadgeModifier.swift +++ b/Sources/TUIkit/Modifiers/BadgeModifier.swift @@ -59,9 +59,9 @@ public enum BadgeValue: Sendable { // MARK: - Equatable -extension BadgeModifier: Equatable where Content: Equatable { - nonisolated public static func == (lhs: BadgeModifier, rhs: BadgeModifier) -> Bool { - MainActor.assumeIsolated { lhs.content == rhs.content && lhs.value == rhs.value } +extension BadgeModifier: @preconcurrency Equatable where Content: Equatable { + public static func == (lhs: BadgeModifier, rhs: BadgeModifier) -> Bool { + lhs.content == rhs.content && lhs.value == rhs.value } } diff --git a/Sources/TUIkit/Modifiers/DimmedModifier.swift b/Sources/TUIkit/Modifiers/DimmedModifier.swift index 79b387e4..46c18393 100644 --- a/Sources/TUIkit/Modifiers/DimmedModifier.swift +++ b/Sources/TUIkit/Modifiers/DimmedModifier.swift @@ -24,9 +24,9 @@ public struct DimmedModifier: View { // MARK: - Equatable Conformance -extension DimmedModifier: Equatable where Content: Equatable { - nonisolated public static func == (lhs: DimmedModifier, rhs: DimmedModifier) -> Bool { - MainActor.assumeIsolated { lhs.content == rhs.content } +extension DimmedModifier: @preconcurrency Equatable where Content: Equatable { + public static func == (lhs: DimmedModifier, rhs: DimmedModifier) -> Bool { + lhs.content == rhs.content } } diff --git a/Sources/TUIkit/Modifiers/FrameModifier.swift b/Sources/TUIkit/Modifiers/FrameModifier.swift index 17d5d852..906c6e07 100644 --- a/Sources/TUIkit/Modifiers/FrameModifier.swift +++ b/Sources/TUIkit/Modifiers/FrameModifier.swift @@ -56,18 +56,16 @@ public struct FlexibleFrameView: View { // MARK: - Equatable Conformance -extension FlexibleFrameView: Equatable where Content: Equatable { - nonisolated public static func == (lhs: FlexibleFrameView, rhs: FlexibleFrameView) -> Bool { - MainActor.assumeIsolated { - lhs.content == rhs.content && - lhs.minWidth == rhs.minWidth && - lhs.idealWidth == rhs.idealWidth && - lhs.maxWidth == rhs.maxWidth && - lhs.minHeight == rhs.minHeight && - lhs.idealHeight == rhs.idealHeight && - lhs.maxHeight == rhs.maxHeight && - lhs.alignment == rhs.alignment - } +extension FlexibleFrameView: @preconcurrency Equatable where Content: Equatable { + public static func == (lhs: FlexibleFrameView, rhs: FlexibleFrameView) -> Bool { + lhs.content == rhs.content && + lhs.minWidth == rhs.minWidth && + lhs.idealWidth == rhs.idealWidth && + lhs.maxWidth == rhs.maxWidth && + lhs.minHeight == rhs.minHeight && + lhs.idealHeight == rhs.idealHeight && + lhs.maxHeight == rhs.maxHeight && + lhs.alignment == rhs.alignment } } diff --git a/Sources/TUIkit/Modifiers/LifecycleModifier.swift b/Sources/TUIkit/Modifiers/LifecycleModifier.swift index 1a572b0a..824bc352 100644 --- a/Sources/TUIkit/Modifiers/LifecycleModifier.swift +++ b/Sources/TUIkit/Modifiers/LifecycleModifier.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - OnAppear Modifier diff --git a/Sources/TUIkit/Modifiers/ListRowSeparatorModifier.swift b/Sources/TUIkit/Modifiers/ListRowSeparatorModifier.swift index bb4b6943..0cc4a356 100644 --- a/Sources/TUIkit/Modifiers/ListRowSeparatorModifier.swift +++ b/Sources/TUIkit/Modifiers/ListRowSeparatorModifier.swift @@ -75,13 +75,11 @@ public enum VerticalEdge: Sendable { // MARK: - Equatable -extension ListRowSeparatorModifier: Equatable where Content: Equatable { - nonisolated public static func == (lhs: ListRowSeparatorModifier, rhs: ListRowSeparatorModifier) -> Bool { - MainActor.assumeIsolated { - lhs.content == rhs.content && - lhs.visibility == rhs.visibility && - lhs.edges == rhs.edges - } +extension ListRowSeparatorModifier: @preconcurrency Equatable where Content: Equatable { + public static func == (lhs: ListRowSeparatorModifier, rhs: ListRowSeparatorModifier) -> Bool { + lhs.content == rhs.content && + lhs.visibility == rhs.visibility && + lhs.edges == rhs.edges } } diff --git a/Sources/TUIkit/Modifiers/OverlayModifier.swift b/Sources/TUIkit/Modifiers/OverlayModifier.swift index 09c72894..ea2511f3 100644 --- a/Sources/TUIkit/Modifiers/OverlayModifier.swift +++ b/Sources/TUIkit/Modifiers/OverlayModifier.swift @@ -26,13 +26,11 @@ public struct OverlayModifier: View { // MARK: - Equatable Conformance -extension OverlayModifier: Equatable where Base: Equatable, Overlay: Equatable { - nonisolated public static func == (lhs: OverlayModifier, rhs: OverlayModifier) -> Bool { - MainActor.assumeIsolated { - lhs.base == rhs.base && - lhs.overlay == rhs.overlay && - lhs.alignment == rhs.alignment - } +extension OverlayModifier: @preconcurrency Equatable where Base: Equatable, Overlay: Equatable { + public static func == (lhs: OverlayModifier, rhs: OverlayModifier) -> Bool { + lhs.base == rhs.base && + lhs.overlay == rhs.overlay && + lhs.alignment == rhs.alignment } } diff --git a/Sources/TUIkit/Modifiers/SelectionDisabledModifier.swift b/Sources/TUIkit/Modifiers/SelectionDisabledModifier.swift index 4d65948e..54d2d592 100644 --- a/Sources/TUIkit/Modifiers/SelectionDisabledModifier.swift +++ b/Sources/TUIkit/Modifiers/SelectionDisabledModifier.swift @@ -38,9 +38,9 @@ public struct SelectionDisabledModifier: View { // MARK: - Equatable -extension SelectionDisabledModifier: Equatable where Content: Equatable { - nonisolated public static func == (lhs: SelectionDisabledModifier, rhs: SelectionDisabledModifier) -> Bool { - MainActor.assumeIsolated { lhs.content == rhs.content && lhs.isDisabled == rhs.isDisabled } +extension SelectionDisabledModifier: @preconcurrency Equatable where Content: Equatable { + public static func == (lhs: SelectionDisabledModifier, rhs: SelectionDisabledModifier) -> Bool { + lhs.content == rhs.content && lhs.isDisabled == rhs.isDisabled } } diff --git a/Sources/TUIkit/Modifiers/StatusBarItemsModifier.swift b/Sources/TUIkit/Modifiers/StatusBarItemsModifier.swift index 6ced8eed..f795bd15 100644 --- a/Sources/TUIkit/Modifiers/StatusBarItemsModifier.swift +++ b/Sources/TUIkit/Modifiers/StatusBarItemsModifier.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - StatusBarItemsModifier diff --git a/Sources/TUIkit/Modifiers/StatusBarSystemItemsModifier.swift b/Sources/TUIkit/Modifiers/StatusBarSystemItemsModifier.swift index 7309a957..0fcb1bb4 100644 --- a/Sources/TUIkit/Modifiers/StatusBarSystemItemsModifier.swift +++ b/Sources/TUIkit/Modifiers/StatusBarSystemItemsModifier.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - StatusBarSystemItemsModifier diff --git a/Sources/TUIkit/Rendering/BorderRenderer.swift b/Sources/TUIkit/Rendering/BorderRenderer.swift index 2a2e9ade..843945a2 100644 --- a/Sources/TUIkit/Rendering/BorderRenderer.swift +++ b/Sources/TUIkit/Rendering/BorderRenderer.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation /// Reusable building blocks for border rendering. /// diff --git a/Sources/TUIkit/Rendering/RenderContext.swift b/Sources/TUIkit/Rendering/RenderContext.swift new file mode 100644 index 00000000..b44af8b5 --- /dev/null +++ b/Sources/TUIkit/Rendering/RenderContext.swift @@ -0,0 +1,279 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// RenderContext.swift +// +// Created by LAYERED.work +// License: MIT + +/// The context for rendering a view. +/// +/// Contains layout constraints, environment values, and the central +/// `TUIContext` that views need to determine their size, content, and +/// access framework services. +/// +/// `RenderContext` is a pure data container — it does not hold a reference +/// to `Terminal`. All terminal I/O happens in `RenderLoop` after the +/// view tree has been rendered into a ``FrameBuffer``. +/// +/// - Important: This is framework infrastructure passed to +/// ``ViewModifier/modify(buffer:context:)``. Most developers only need +/// ``availableWidth``, ``availableHeight``, and ``environment``. +public struct RenderContext { + /// The available width in characters. + public var availableWidth: Int + + /// The available height in lines. + public var availableHeight: Int + + /// The environment values for this render pass. + public var environment: EnvironmentValues + + /// The central dependency container for framework services. + /// + /// Provides access to lifecycle tracking, key event dispatch, + /// and preference storage via constructor injection. + /// Mutable to allow modal presentation to substitute an isolated + /// context for background content rendering. + var tuiContext: TUIContext + + /// The current view's structural identity in the render tree. + /// + /// Built incrementally as `renderToBuffer` traverses the view hierarchy. + /// Container views append child indices, composite views append type names. + /// Used by `StateStorage` to persist `@State` values across render passes. + var identity: ViewIdentity + + /// The ID of the focus section that child views should register in. + /// + /// Set by `FocusSectionModifier` during rendering. Focusable children + /// (buttons, menus) read this to register in the correct section. + /// When nil, elements register in the active or default section. + var activeFocusSectionID: String? + + /// The current breathing animation phase (0–1) for the focus indicator. + /// + /// Set by `RenderLoop` from the `PulseTimer` at the start of each frame. + /// Read by `BorderRenderer` to interpolate the ● indicator color. + /// A value of 0 means dimmest, 1 means brightest. + var pulsePhase: Double = 0 + + /// The cursor timer for TextField/SecureField animations. + /// + /// Set by `RenderLoop` at the start of each frame. + /// Read by text fields to compute blink and pulse phases. + var cursorTimer: CursorTimer? + + /// The focus indicator color for the first border encountered in this subtree. + /// + /// Set by `FocusSectionModifier` when the section is active. + /// The first view that renders a border (Panel, Box, `.border()`) reads + /// this color, renders the ● indicator, and sets it to nil so that + /// nested borders don't also show the indicator. + var focusIndicatorColor: Color? + + /// Whether an explicit frame width constraint has been set. + /// + /// Set by `FlexibleFrameView` when a fixed width is specified. + /// Container views use this to decide whether to expand to fill + /// the available width or shrink to fit their content. + var hasExplicitWidth: Bool = false + + /// Whether an explicit frame height constraint has been set. + /// + /// Set by layout containers (e.g., NavigationSplitView) when a fixed height is specified. + /// Container views use this to decide whether to expand to fill + /// the available height or shrink to fit their content. + var hasExplicitHeight: Bool = false + + /// Whether this is a measurement pass (no side-effects should occur). + /// + /// Set to true during two-pass layout when measuring non-Layoutable views. + /// Views should skip side-effects like focus registration when this is true. + var isMeasuring: Bool = false + + /// Creates a new RenderContext. + /// + /// - Parameters: + /// - availableWidth: The available width in characters. + /// - availableHeight: The available height in lines. + /// - environment: The environment values (defaults to empty). + /// - tuiContext: The TUI context (defaults to a fresh instance). + /// - identity: The view identity path (defaults to root). + init( + availableWidth: Int, + availableHeight: Int, + environment: EnvironmentValues = EnvironmentValues(), + tuiContext: TUIContext = TUIContext(), + identity: ViewIdentity = ViewIdentity(path: "") + ) { + self.availableWidth = availableWidth + self.availableHeight = availableHeight + self.environment = environment + self.tuiContext = tuiContext + self.identity = identity + } + + /// Creates a new context with the same size but different environment. + /// + /// - Parameter environment: The new environment values. + /// - Returns: A new RenderContext with the updated environment. + func withEnvironment(_ environment: EnvironmentValues) -> Self { + var copy = self + copy.environment = environment + return copy + } + + /// Creates a new context with a child identity for the given type and index. + /// + /// Used by container views (`TupleView`, `ViewArray`) to assign + /// structural identities to their children. + /// + /// - Parameters: + /// - type: The child view's type. + /// - index: The child's position within the container. + /// - Returns: A new RenderContext with the extended identity path. + func withChildIdentity(type: V.Type, index: Int) -> Self { + var copy = self + copy.identity = identity.child(type: type, index: index) + return copy + } + + /// Creates a new context with a child identity for a composite view's body. + /// + /// Used when descending into a view's `body` where there is exactly + /// one child (no sibling disambiguation needed). + /// + /// - Parameter type: The child view's type. + /// - Returns: A new RenderContext with the extended identity path. + func withChildIdentity(type: V.Type) -> Self { + var copy = self + copy.identity = identity.child(type: type) + return copy + } + + /// Creates a new context with a branch identity. + /// + /// Used by `ConditionalView` to distinguish between if/else branches. + /// + /// - Parameter label: The branch label (`"true"` or `"false"`). + /// - Returns: A new RenderContext with the branch identity. + func withBranchIdentity(_ label: String) -> Self { + var copy = self + copy.identity = identity.branch(label) + return copy + } + + /// Creates a context isolated from the real focus and key event systems. + /// + /// Used by modal presentation modifiers to render background content + /// visually without letting its buttons and key handlers interfere + /// with the modal's interactive elements. The returned context has a + /// throwaway `FocusManager` and `KeyEventDispatcher` while sharing + /// lifecycle, preferences, and state storage with the real context. + func isolatedForBackground() -> Self { + var copy = self + copy.environment.focusManager = FocusManager() + copy.tuiContext = TUIContext( + lifecycle: tuiContext.lifecycle, + keyEventDispatcher: KeyEventDispatcher(), + preferences: tuiContext.preferences, + stateStorage: tuiContext.stateStorage + ) + return copy + } + + /// Creates a new context with a different available width. + /// + /// Used by layout containers (e.g., NavigationSplitView) to constrain + /// child views to a specific column width. + /// + /// This also sets `hasExplicitWidth` to true so that child views + /// (like List) know to expand to fill the available width. + /// + /// - Parameter width: The new available width in characters. + /// - Returns: A new RenderContext with the updated width. + func withAvailableWidth(_ width: Int) -> Self { + var copy = self + copy.availableWidth = width + copy.hasExplicitWidth = true + return copy + } + + /// Creates a copy with updated available height. + /// + /// Used by layout containers (e.g., NavigationSplitView) to constrain + /// child views to a specific height. + /// + /// This also sets `hasExplicitHeight` to true so that child views + /// (like List) know to expand to fill the available height. + /// + /// - Parameter height: The new available height in lines. + /// - Returns: A new RenderContext with the updated height. + func withAvailableHeight(_ height: Int) -> Self { + var copy = self + copy.availableHeight = height + copy.hasExplicitHeight = true + return copy + } + + /// Creates a copy with updated available width and height. + /// + /// Used by layout containers to constrain child views to specific dimensions. + /// + /// - Parameters: + /// - width: The new available width in characters. + /// - height: The new available height in lines. + /// - Returns: A new RenderContext with the updated dimensions. + func withAvailableSize(width: Int, height: Int) -> Self { + var copy = self + copy.availableWidth = width + copy.availableHeight = height + copy.hasExplicitWidth = true + copy.hasExplicitHeight = true + return copy + } + + // MARK: - Container Layout Helpers + + /// Creates a context for rendering content inside a bordered container. + /// + /// Subtracts the border width (2 characters for left + right) from available width. + /// Propagates `hasExplicitWidth` from parent so children know whether to expand. + /// + /// - Parameter hasBorder: Whether the container has a border (default: true). + /// - Returns: A new context with adjusted width for inner content. + func forBorderedContent(hasBorder: Bool = true) -> Self { + var copy = self + if hasBorder { + copy.availableWidth = max(0, availableWidth - 2) + } + // Propagate hasExplicitWidth from parent - if parent has explicit width, + // children should also expand to fill the (reduced) available space. + return copy + } + + /// Calculates the inner width for a container based on content. + /// + /// Containers (borders, panels, cards) size to fit their content. + /// They do not auto-expand beyond the content width. + /// + /// - Parameters: + /// - contentWidth: The natural width of the content. + /// - innerAvailableWidth: The available width inside the container (unused). + /// - Returns: The content width. + func resolveContainerWidth(contentWidth: Int, innerAvailableWidth: Int) -> Int { + return contentWidth + } + + /// Calculates the inner height for a container based on content. + /// + /// Containers size to fit their content height. + /// They do not auto-expand to fill available space. + /// + /// - Parameters: + /// - contentHeight: The natural height of the content. + /// - borderOverhead: Lines used by borders/title/footer (unused, kept for API compatibility). + /// - Returns: The content height. + func resolveContainerHeight(contentHeight: Int, borderOverhead: Int = 0) -> Int { + return contentHeight + } +} diff --git a/Sources/TUIkit/Rendering/Renderable.swift b/Sources/TUIkit/Rendering/Renderable.swift index d3488ea7..73175fce 100644 --- a/Sources/TUIkit/Rendering/Renderable.swift +++ b/Sources/TUIkit/Rendering/Renderable.swift @@ -199,280 +199,6 @@ extension Layoutable { } } -/// The context for rendering a view. -/// -/// Contains layout constraints, environment values, and the central -/// `TUIContext` that views need to determine their size, content, and -/// access framework services. -/// -/// `RenderContext` is a pure data container — it does not hold a reference -/// to `Terminal`. All terminal I/O happens in `RenderLoop` after the -/// view tree has been rendered into a ``FrameBuffer``. -/// -/// - Important: This is framework infrastructure passed to -/// ``ViewModifier/modify(buffer:context:)``. Most developers only need -/// ``availableWidth``, ``availableHeight``, and ``environment``. -public struct RenderContext { - /// The available width in characters. - public var availableWidth: Int - - /// The available height in lines. - public var availableHeight: Int - - /// The environment values for this render pass. - public var environment: EnvironmentValues - - /// The central dependency container for framework services. - /// - /// Provides access to lifecycle tracking, key event dispatch, - /// and preference storage via constructor injection. - /// Mutable to allow modal presentation to substitute an isolated - /// context for background content rendering. - var tuiContext: TUIContext - - /// The current view's structural identity in the render tree. - /// - /// Built incrementally as `renderToBuffer` traverses the view hierarchy. - /// Container views append child indices, composite views append type names. - /// Used by `StateStorage` to persist `@State` values across render passes. - var identity: ViewIdentity - - /// The ID of the focus section that child views should register in. - /// - /// Set by `FocusSectionModifier` during rendering. Focusable children - /// (buttons, menus) read this to register in the correct section. - /// When nil, elements register in the active or default section. - var activeFocusSectionID: String? - - /// The current breathing animation phase (0–1) for the focus indicator. - /// - /// Set by `RenderLoop` from the `PulseTimer` at the start of each frame. - /// Read by `BorderRenderer` to interpolate the ● indicator color. - /// A value of 0 means dimmest, 1 means brightest. - var pulsePhase: Double = 0 - - /// The cursor timer for TextField/SecureField animations. - /// - /// Set by `RenderLoop` at the start of each frame. - /// Read by text fields to compute blink and pulse phases. - var cursorTimer: CursorTimer? - - /// The focus indicator color for the first border encountered in this subtree. - /// - /// Set by `FocusSectionModifier` when the section is active. - /// The first view that renders a border (Panel, Box, `.border()`) reads - /// this color, renders the ● indicator, and sets it to nil so that - /// nested borders don't also show the indicator. - var focusIndicatorColor: Color? - - /// Whether an explicit frame width constraint has been set. - /// - /// Set by `FlexibleFrameView` when a fixed width is specified. - /// Container views use this to decide whether to expand to fill - /// the available width or shrink to fit their content. - var hasExplicitWidth: Bool = false - - /// Whether an explicit frame height constraint has been set. - /// - /// Set by layout containers (e.g., NavigationSplitView) when a fixed height is specified. - /// Container views use this to decide whether to expand to fill - /// the available height or shrink to fit their content. - var hasExplicitHeight: Bool = false - - /// Whether this is a measurement pass (no side-effects should occur). - /// - /// Set to true during two-pass layout when measuring non-Layoutable views. - /// Views should skip side-effects like focus registration when this is true. - var isMeasuring: Bool = false - - /// Creates a new RenderContext. - /// - /// - Parameters: - /// - availableWidth: The available width in characters. - /// - availableHeight: The available height in lines. - /// - environment: The environment values (defaults to empty). - /// - tuiContext: The TUI context (defaults to a fresh instance). - /// - identity: The view identity path (defaults to root). - init( - availableWidth: Int, - availableHeight: Int, - environment: EnvironmentValues = EnvironmentValues(), - tuiContext: TUIContext = TUIContext(), - identity: ViewIdentity = ViewIdentity(path: "") - ) { - self.availableWidth = availableWidth - self.availableHeight = availableHeight - self.environment = environment - self.tuiContext = tuiContext - self.identity = identity - } - - /// Creates a new context with the same size but different environment. - /// - /// - Parameter environment: The new environment values. - /// - Returns: A new RenderContext with the updated environment. - func withEnvironment(_ environment: EnvironmentValues) -> Self { - var copy = self - copy.environment = environment - return copy - } - - /// Creates a new context with a child identity for the given type and index. - /// - /// Used by container views (`TupleView`, `ViewArray`) to assign - /// structural identities to their children. - /// - /// - Parameters: - /// - type: The child view's type. - /// - index: The child's position within the container. - /// - Returns: A new RenderContext with the extended identity path. - func withChildIdentity(type: V.Type, index: Int) -> Self { - var copy = self - copy.identity = identity.child(type: type, index: index) - return copy - } - - /// Creates a new context with a child identity for a composite view's body. - /// - /// Used when descending into a view's `body` where there is exactly - /// one child (no sibling disambiguation needed). - /// - /// - Parameter type: The child view's type. - /// - Returns: A new RenderContext with the extended identity path. - func withChildIdentity(type: V.Type) -> Self { - var copy = self - copy.identity = identity.child(type: type) - return copy - } - - /// Creates a new context with a branch identity. - /// - /// Used by `ConditionalView` to distinguish between if/else branches. - /// - /// - Parameter label: The branch label (`"true"` or `"false"`). - /// - Returns: A new RenderContext with the branch identity. - func withBranchIdentity(_ label: String) -> Self { - var copy = self - copy.identity = identity.branch(label) - return copy - } - - /// Creates a context isolated from the real focus and key event systems. - /// - /// Used by modal presentation modifiers to render background content - /// visually without letting its buttons and key handlers interfere - /// with the modal's interactive elements. The returned context has a - /// throwaway `FocusManager` and `KeyEventDispatcher` while sharing - /// lifecycle, preferences, and state storage with the real context. - func isolatedForBackground() -> Self { - var copy = self - copy.environment.focusManager = FocusManager() - copy.tuiContext = TUIContext( - lifecycle: tuiContext.lifecycle, - keyEventDispatcher: KeyEventDispatcher(), - preferences: tuiContext.preferences, - stateStorage: tuiContext.stateStorage - ) - return copy - } - - /// Creates a new context with a different available width. - /// - /// Used by layout containers (e.g., NavigationSplitView) to constrain - /// child views to a specific column width. - /// - /// This also sets `hasExplicitWidth` to true so that child views - /// (like List) know to expand to fill the available width. - /// - /// - Parameter width: The new available width in characters. - /// - Returns: A new RenderContext with the updated width. - func withAvailableWidth(_ width: Int) -> Self { - var copy = self - copy.availableWidth = width - copy.hasExplicitWidth = true - return copy - } - - /// Creates a copy with updated available height. - /// - /// Used by layout containers (e.g., NavigationSplitView) to constrain - /// child views to a specific height. - /// - /// This also sets `hasExplicitHeight` to true so that child views - /// (like List) know to expand to fill the available height. - /// - /// - Parameter height: The new available height in lines. - /// - Returns: A new RenderContext with the updated height. - func withAvailableHeight(_ height: Int) -> Self { - var copy = self - copy.availableHeight = height - copy.hasExplicitHeight = true - return copy - } - - /// Creates a copy with updated available width and height. - /// - /// Used by layout containers to constrain child views to specific dimensions. - /// - /// - Parameters: - /// - width: The new available width in characters. - /// - height: The new available height in lines. - /// - Returns: A new RenderContext with the updated dimensions. - func withAvailableSize(width: Int, height: Int) -> Self { - var copy = self - copy.availableWidth = width - copy.availableHeight = height - copy.hasExplicitWidth = true - copy.hasExplicitHeight = true - return copy - } - - // MARK: - Container Layout Helpers - - /// Creates a context for rendering content inside a bordered container. - /// - /// Subtracts the border width (2 characters for left + right) from available width. - /// Propagates `hasExplicitWidth` from parent so children know whether to expand. - /// - /// - Parameter hasBorder: Whether the container has a border (default: true). - /// - Returns: A new context with adjusted width for inner content. - func forBorderedContent(hasBorder: Bool = true) -> Self { - var copy = self - if hasBorder { - copy.availableWidth = max(0, availableWidth - 2) - } - // Propagate hasExplicitWidth from parent - if parent has explicit width, - // children should also expand to fill the (reduced) available space. - return copy - } - - /// Calculates the inner width for a container based on content. - /// - /// Containers (borders, panels, cards) size to fit their content. - /// They do not auto-expand beyond the content width. - /// - /// - Parameters: - /// - contentWidth: The natural width of the content. - /// - innerAvailableWidth: The available width inside the container (unused). - /// - Returns: The content width. - func resolveContainerWidth(contentWidth: Int, innerAvailableWidth: Int) -> Int { - return contentWidth - } - - /// Calculates the inner height for a container based on content. - /// - /// Containers size to fit their content height. - /// They do not auto-expand to fill available space. - /// - /// - Parameters: - /// - contentHeight: The natural height of the content. - /// - borderOverhead: Lines used by borders/title/footer (unused, kept for API compatibility). - /// - Returns: The content height. - func resolveContainerHeight(contentHeight: Int, borderOverhead: Int = 0) -> Int { - return contentHeight - } -} - // MARK: - Rendering Dispatch /// Renders any `View` into a ``FrameBuffer`` using the dual rendering system. diff --git a/Sources/TUIkit/Rendering/ScrollIndicator.swift b/Sources/TUIkit/Rendering/ScrollIndicator.swift index 698735e2..4907f9dc 100644 --- a/Sources/TUIkit/Rendering/ScrollIndicator.swift +++ b/Sources/TUIkit/Rendering/ScrollIndicator.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - Scroll Direction diff --git a/Sources/TUIkit/StatusBar/StatusBar.swift b/Sources/TUIkit/StatusBar/StatusBar.swift index e45b1f81..dd9dabde 100644 --- a/Sources/TUIkit/StatusBar/StatusBar.swift +++ b/Sources/TUIkit/StatusBar/StatusBar.swift @@ -5,7 +5,6 @@ // License: MIT Always rendered at the bottom of the terminal, never dimmed by overlays. // -import Foundation // MARK: - StatusBar View diff --git a/Sources/TUIkit/Styling/ANSIColor.swift b/Sources/TUIkit/Styling/ANSIColor.swift new file mode 100644 index 00000000..34c9d41f --- /dev/null +++ b/Sources/TUIkit/Styling/ANSIColor.swift @@ -0,0 +1,70 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// ANSIColor.swift +// +// Created by LAYERED.work +// License: MIT + +/// The 8 standard ANSI colors. +enum ANSIColor: UInt8, Sendable { + case black = 0 + case red = 1 + case green = 2 + case yellow = 3 + case blue = 4 + case magenta = 5 + case cyan = 6 + case white = 7 + case `default` = 9 + + /// The ANSI code for foreground color (30-37, 39 for default). + var foregroundCode: UInt8 { + 30 + rawValue + } + + /// The ANSI code for background color (40-47, 49 for default). + var backgroundCode: UInt8 { + 40 + rawValue + } + + /// The ANSI code for bright foreground color (90-97). + var brightForegroundCode: UInt8 { + 90 + rawValue + } + + /// The ANSI code for bright background color (100-107). + var brightBackgroundCode: UInt8 { + 100 + rawValue + } + + // MARK: - xterm Standard RGB Values + + /// The standard RGB values for this ANSI color (xterm defaults). + var rgbValues: (red: UInt8, green: UInt8, blue: UInt8) { + switch self { + case .black: return (0, 0, 0) + case .red: return (205, 0, 0) + case .green: return (0, 205, 0) + case .yellow: return (205, 205, 0) + case .blue: return (0, 0, 238) + case .magenta: return (205, 0, 205) + case .cyan: return (0, 205, 205) + case .white: return (229, 229, 229) + case .default: return (229, 229, 229) + } + } + + /// The bright RGB values for this ANSI color (xterm defaults). + var brightRGBValues: (red: UInt8, green: UInt8, blue: UInt8) { + switch self { + case .black: return (127, 127, 127) + case .red: return (255, 0, 0) + case .green: return (0, 255, 0) + case .yellow: return (255, 255, 0) + case .blue: return (92, 92, 255) + case .magenta: return (255, 0, 255) + case .cyan: return (0, 255, 255) + case .white: return (255, 255, 255) + case .default: return (255, 255, 255) + } + } +} diff --git a/Sources/TUIkit/Styling/Appearance.swift b/Sources/TUIkit/Styling/Appearance.swift index 07f34c61..4b89e485 100644 --- a/Sources/TUIkit/Styling/Appearance.swift +++ b/Sources/TUIkit/Styling/Appearance.swift @@ -7,7 +7,6 @@ // while Theme defines the colors. Together they create a complete look. // -import Foundation // MARK: - Appearance diff --git a/Sources/TUIkit/Styling/Color.swift b/Sources/TUIkit/Styling/Color.swift index 172bd7cc..0a4accbe 100644 --- a/Sources/TUIkit/Styling/Color.swift +++ b/Sources/TUIkit/Styling/Color.swift @@ -484,73 +484,6 @@ private extension Color { } } -// MARK: - ANSIColor - -/// The 8 standard ANSI colors. -enum ANSIColor: UInt8, Sendable { - case black = 0 - case red = 1 - case green = 2 - case yellow = 3 - case blue = 4 - case magenta = 5 - case cyan = 6 - case white = 7 - case `default` = 9 - - /// The ANSI code for foreground color (30-37, 39 for default). - var foregroundCode: UInt8 { - 30 + rawValue - } - - /// The ANSI code for background color (40-47, 49 for default). - var backgroundCode: UInt8 { - 40 + rawValue - } - - /// The ANSI code for bright foreground color (90-97). - var brightForegroundCode: UInt8 { - 90 + rawValue - } - - /// The ANSI code for bright background color (100-107). - var brightBackgroundCode: UInt8 { - 100 + rawValue - } - - // MARK: - xterm Standard RGB Values - - /// The standard RGB values for this ANSI color (xterm defaults). - var rgbValues: (red: UInt8, green: UInt8, blue: UInt8) { - switch self { - case .black: return (0, 0, 0) - case .red: return (205, 0, 0) - case .green: return (0, 205, 0) - case .yellow: return (205, 205, 0) - case .blue: return (0, 0, 238) - case .magenta: return (205, 0, 205) - case .cyan: return (0, 205, 205) - case .white: return (229, 229, 229) - case .default: return (229, 229, 229) - } - } - - /// The bright RGB values for this ANSI color (xterm defaults). - var brightRGBValues: (red: UInt8, green: UInt8, blue: UInt8) { - switch self { - case .black: return (127, 127, 127) - case .red: return (255, 0, 0) - case .green: return (0, 255, 0) - case .yellow: return (255, 255, 0) - case .blue: return (92, 92, 255) - case .magenta: return (255, 0, 255) - case .cyan: return (0, 255, 255) - case .white: return (255, 255, 255) - case .default: return (255, 255, 255) - } - } -} - // MARK: - Foreground Style Environment /// Environment key for the foreground style. diff --git a/Sources/TUIkit/Styling/ContentMode.swift b/Sources/TUIkit/Styling/ContentMode.swift index e2a814f6..37923883 100644 --- a/Sources/TUIkit/Styling/ContentMode.swift +++ b/Sources/TUIkit/Styling/ContentMode.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - ContentMode diff --git a/Sources/TUIkit/Styling/Theme.swift b/Sources/TUIkit/Styling/Theme.swift index 8debb8e8..c68a7b9a 100644 --- a/Sources/TUIkit/Styling/Theme.swift +++ b/Sources/TUIkit/Styling/Theme.swift @@ -5,7 +5,6 @@ // License: MIT and palette registry. // -import Foundation // MARK: - Palette Protocol diff --git a/Sources/TUIkit/Styling/ThemeManager.swift b/Sources/TUIkit/Styling/ThemeManager.swift index 0d2c0db8..2f7b3aed 100644 --- a/Sources/TUIkit/Styling/ThemeManager.swift +++ b/Sources/TUIkit/Styling/ThemeManager.swift @@ -6,7 +6,6 @@ // with a single, reusable implementation. // -import Foundation // MARK: - Cyclable Protocol diff --git a/Sources/TUIkit/Views/Box.swift b/Sources/TUIkit/Views/Box.swift index 4cbf64c7..ba873f83 100644 --- a/Sources/TUIkit/Views/Box.swift +++ b/Sources/TUIkit/Views/Box.swift @@ -166,12 +166,10 @@ struct BufferView: View, Renderable { // MARK: - Equatable Conformance -extension Box: Equatable where Content: Equatable { - nonisolated static func == (lhs: Box, rhs: Box) -> Bool { - MainActor.assumeIsolated { - lhs.content == rhs.content && - lhs.borderStyle == rhs.borderStyle && - lhs.borderColor == rhs.borderColor - } +extension Box: @preconcurrency Equatable where Content: Equatable { + static func == (lhs: Box, rhs: Box) -> Bool { + lhs.content == rhs.content && + lhs.borderStyle == rhs.borderStyle && + lhs.borderColor == rhs.borderColor } } diff --git a/Sources/TUIkit/Views/Button.swift b/Sources/TUIkit/Views/Button.swift index 8c6d0240..2d7685f2 100644 --- a/Sources/TUIkit/Views/Button.swift +++ b/Sources/TUIkit/Views/Button.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - Button Role diff --git a/Sources/TUIkit/Views/Card.swift b/Sources/TUIkit/Views/Card.swift index 4ed49b27..220111d7 100644 --- a/Sources/TUIkit/Views/Card.swift +++ b/Sources/TUIkit/Views/Card.swift @@ -133,15 +133,13 @@ public struct Card: View { // MARK: - Equatable Conformance -extension Card: Equatable where Content: Equatable, Footer: Equatable { - nonisolated public static func == (lhs: Card, rhs: Card) -> Bool { - MainActor.assumeIsolated { - lhs.title == rhs.title && - lhs.content == rhs.content && - lhs.footer == rhs.footer && - lhs.config == rhs.config && - lhs.backgroundColor == rhs.backgroundColor - } +extension Card: @preconcurrency Equatable where Content: Equatable, Footer: Equatable { + public static func == (lhs: Card, rhs: Card) -> Bool { + lhs.title == rhs.title && + lhs.content == rhs.content && + lhs.footer == rhs.footer && + lhs.config == rhs.config && + lhs.backgroundColor == rhs.backgroundColor } } diff --git a/Sources/TUIkit/Views/ContainerView.swift b/Sources/TUIkit/Views/ContainerView.swift index 785af751..f4e25503 100644 --- a/Sources/TUIkit/Views/ContainerView.swift +++ b/Sources/TUIkit/Views/ContainerView.swift @@ -246,16 +246,14 @@ struct ContainerView: View { // MARK: - Equatable Conformance -extension ContainerView: Equatable where Content: Equatable, Footer: Equatable { - nonisolated static func == (lhs: ContainerView, rhs: ContainerView) -> Bool { - MainActor.assumeIsolated { - lhs.title == rhs.title && - lhs.titleColor == rhs.titleColor && - lhs.content == rhs.content && - lhs.footer == rhs.footer && - lhs.style == rhs.style && - lhs.padding == rhs.padding - } +extension ContainerView: @preconcurrency Equatable where Content: Equatable, Footer: Equatable { + static func == (lhs: ContainerView, rhs: ContainerView) -> Bool { + lhs.title == rhs.title && + lhs.titleColor == rhs.titleColor && + lhs.content == rhs.content && + lhs.footer == rhs.footer && + lhs.style == rhs.style && + lhs.padding == rhs.padding } } @@ -476,15 +474,13 @@ private struct _ContainerViewCore: View, Renderable // MARK: - Equatable Conformance -extension _ContainerViewCore: Equatable where Content: Equatable, Footer: Equatable { - nonisolated static func == (lhs: _ContainerViewCore, rhs: _ContainerViewCore) -> Bool { - MainActor.assumeIsolated { - lhs.title == rhs.title && - lhs.titleColor == rhs.titleColor && - lhs.content == rhs.content && - lhs.footer == rhs.footer && - lhs.style == rhs.style && - lhs.padding == rhs.padding - } +extension _ContainerViewCore: @preconcurrency Equatable where Content: Equatable, Footer: Equatable { + static func == (lhs: _ContainerViewCore, rhs: _ContainerViewCore) -> Bool { + lhs.title == rhs.title && + lhs.titleColor == rhs.titleColor && + lhs.content == rhs.content && + lhs.footer == rhs.footer && + lhs.style == rhs.style && + lhs.padding == rhs.padding } } diff --git a/Sources/TUIkit/Views/Dialog.swift b/Sources/TUIkit/Views/Dialog.swift index ced81a73..71c53057 100644 --- a/Sources/TUIkit/Views/Dialog.swift +++ b/Sources/TUIkit/Views/Dialog.swift @@ -107,14 +107,12 @@ public struct Dialog: View { // MARK: - Equatable Conformance -extension Dialog: Equatable where Content: Equatable, Footer: Equatable { - nonisolated public static func == (lhs: Dialog, rhs: Dialog) -> Bool { - MainActor.assumeIsolated { - lhs.title == rhs.title && - lhs.content == rhs.content && - lhs.footer == rhs.footer && - lhs.config == rhs.config - } +extension Dialog: @preconcurrency Equatable where Content: Equatable, Footer: Equatable { + public static func == (lhs: Dialog, rhs: Dialog) -> Bool { + lhs.title == rhs.title && + lhs.content == rhs.content && + lhs.footer == rhs.footer && + lhs.config == rhs.config } } diff --git a/Sources/TUIkit/Views/HStack.swift b/Sources/TUIkit/Views/HStack.swift index fe0d897b..bed324ed 100644 --- a/Sources/TUIkit/Views/HStack.swift +++ b/Sources/TUIkit/Views/HStack.swift @@ -164,12 +164,10 @@ private struct _HStackCore: View, Renderable, Layoutable { // MARK: - Equatable -extension HStack: Equatable where Content: Equatable { - nonisolated public static func == (lhs: HStack, rhs: HStack) -> Bool { - MainActor.assumeIsolated { - lhs.alignment == rhs.alignment && - lhs.spacing == rhs.spacing && - lhs.content == rhs.content - } +extension HStack: @preconcurrency Equatable where Content: Equatable { + public static func == (lhs: HStack, rhs: HStack) -> Bool { + lhs.alignment == rhs.alignment && + lhs.spacing == rhs.spacing && + lhs.content == rhs.content } } diff --git a/Sources/TUIkit/Views/Image.swift b/Sources/TUIkit/Views/Image.swift index 73755280..82de722b 100644 --- a/Sources/TUIkit/Views/Image.swift +++ b/Sources/TUIkit/Views/Image.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - Image Source @@ -80,8 +79,8 @@ public struct Image: View { // MARK: - Equatable -extension Image: Equatable { - nonisolated public static func == (lhs: Image, rhs: Image) -> Bool { +extension Image: @preconcurrency Equatable { + public static func == (lhs: Image, rhs: Image) -> Bool { lhs.source == rhs.source } } diff --git a/Sources/TUIkit/Views/LazyStacks.swift b/Sources/TUIkit/Views/LazyStacks.swift index da1e4136..85efc3a1 100644 --- a/Sources/TUIkit/Views/LazyStacks.swift +++ b/Sources/TUIkit/Views/LazyStacks.swift @@ -278,22 +278,18 @@ private struct _LazyHStackCore: View, Renderable { // MARK: - Equatable Conformances -extension LazyVStack: Equatable where Content: Equatable { - nonisolated public static func == (lhs: LazyVStack, rhs: LazyVStack) -> Bool { - MainActor.assumeIsolated { - lhs.alignment == rhs.alignment && - lhs.spacing == rhs.spacing && - lhs.content == rhs.content - } +extension LazyVStack: @preconcurrency Equatable where Content: Equatable { + public static func == (lhs: LazyVStack, rhs: LazyVStack) -> Bool { + lhs.alignment == rhs.alignment && + lhs.spacing == rhs.spacing && + lhs.content == rhs.content } } -extension LazyHStack: Equatable where Content: Equatable { - nonisolated public static func == (lhs: LazyHStack, rhs: LazyHStack) -> Bool { - MainActor.assumeIsolated { - lhs.alignment == rhs.alignment && - lhs.spacing == rhs.spacing && - lhs.content == rhs.content - } +extension LazyHStack: @preconcurrency Equatable where Content: Equatable { + public static func == (lhs: LazyHStack, rhs: LazyHStack) -> Bool { + lhs.alignment == rhs.alignment && + lhs.spacing == rhs.spacing && + lhs.content == rhs.content } } diff --git a/Sources/TUIkit/Views/List.swift b/Sources/TUIkit/Views/List.swift index af7e5744..03981e3e 100644 --- a/Sources/TUIkit/Views/List.swift +++ b/Sources/TUIkit/Views/List.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - List (Single Selection) diff --git a/Sources/TUIkit/Views/NavigationSplitView.swift b/Sources/TUIkit/Views/NavigationSplitView.swift index 4f833333..93afbd48 100644 --- a/Sources/TUIkit/Views/NavigationSplitView.swift +++ b/Sources/TUIkit/Views/NavigationSplitView.swift @@ -455,13 +455,11 @@ private extension _NavigationSplitViewCore { // MARK: - Equatable Conformance -extension NavigationSplitView: Equatable where Sidebar: Equatable, Content: Equatable, Detail: Equatable { - nonisolated public static func == (lhs: Self, rhs: Self) -> Bool { - MainActor.assumeIsolated { - lhs.sidebar == rhs.sidebar && - lhs.content == rhs.content && - lhs.detail == rhs.detail && - lhs.isThreeColumn == rhs.isThreeColumn - } +extension NavigationSplitView: @preconcurrency Equatable where Sidebar: Equatable, Content: Equatable, Detail: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.sidebar == rhs.sidebar && + lhs.content == rhs.content && + lhs.detail == rhs.detail && + lhs.isThreeColumn == rhs.isThreeColumn } } diff --git a/Sources/TUIkit/Views/Panel.swift b/Sources/TUIkit/Views/Panel.swift index bf0d16ba..2b15a17c 100644 --- a/Sources/TUIkit/Views/Panel.swift +++ b/Sources/TUIkit/Views/Panel.swift @@ -127,14 +127,12 @@ public struct Panel: View { // MARK: - Equatable Conformance -extension Panel: Equatable where Content: Equatable, Footer: Equatable { - nonisolated public static func == (lhs: Panel, rhs: Panel) -> Bool { - MainActor.assumeIsolated { - lhs.title == rhs.title && - lhs.content == rhs.content && - lhs.footer == rhs.footer && - lhs.config == rhs.config - } +extension Panel: @preconcurrency Equatable where Content: Equatable, Footer: Equatable { + public static func == (lhs: Panel, rhs: Panel) -> Bool { + lhs.title == rhs.title && + lhs.content == rhs.content && + lhs.footer == rhs.footer && + lhs.config == rhs.config } } diff --git a/Sources/TUIkit/Views/ProgressView.swift b/Sources/TUIkit/Views/ProgressView.swift index 7e5cd681..31534499 100644 --- a/Sources/TUIkit/Views/ProgressView.swift +++ b/Sources/TUIkit/Views/ProgressView.swift @@ -185,6 +185,7 @@ extension ProgressView { /// /// - Parameter style: The progress bar style. /// - Returns: A progress view with the specified style. + /// - Note: Scheduled for removal in the next major version. @available(*, deprecated, renamed: "trackStyle(_:)") public func progressBarStyle(_ style: TrackStyle) -> ProgressView { trackStyle(style) @@ -193,14 +194,12 @@ extension ProgressView { // MARK: - Equatable Conformance -extension ProgressView: Equatable where Label: Equatable, CurrentValueLabel: Equatable { - nonisolated public static func == (lhs: ProgressView, rhs: ProgressView) -> Bool { - MainActor.assumeIsolated { - lhs.fractionCompleted == rhs.fractionCompleted && - lhs.style == rhs.style && - lhs.label == rhs.label && - lhs.currentValueLabel == rhs.currentValueLabel - } +extension ProgressView: @preconcurrency Equatable where Label: Equatable, CurrentValueLabel: Equatable { + public static func == (lhs: ProgressView, rhs: ProgressView) -> Bool { + lhs.fractionCompleted == rhs.fractionCompleted && + lhs.style == rhs.style && + lhs.label == rhs.label && + lhs.currentValueLabel == rhs.currentValueLabel } } diff --git a/Sources/TUIkit/Views/RadioButton.swift b/Sources/TUIkit/Views/RadioButton.swift index 98095763..c5cdb383 100644 --- a/Sources/TUIkit/Views/RadioButton.swift +++ b/Sources/TUIkit/Views/RadioButton.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - Radio Button Orientation diff --git a/Sources/TUIkit/Views/SecureField.swift b/Sources/TUIkit/Views/SecureField.swift index 1824270e..28344718 100644 --- a/Sources/TUIkit/Views/SecureField.swift +++ b/Sources/TUIkit/Views/SecureField.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - SecureField diff --git a/Sources/TUIkit/Views/Slider.swift b/Sources/TUIkit/Views/Slider.swift index 7f25971c..c3a0491c 100644 --- a/Sources/TUIkit/Views/Slider.swift +++ b/Sources/TUIkit/Views/Slider.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - Slider diff --git a/Sources/TUIkit/Views/Stepper.swift b/Sources/TUIkit/Views/Stepper.swift index b0ef3808..c1688806 100644 --- a/Sources/TUIkit/Views/Stepper.swift +++ b/Sources/TUIkit/Views/Stepper.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - Stepper diff --git a/Sources/TUIkit/Views/Table.swift b/Sources/TUIkit/Views/Table.swift index 329671f3..6cd84c92 100644 --- a/Sources/TUIkit/Views/Table.swift +++ b/Sources/TUIkit/Views/Table.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - Table diff --git a/Sources/TUIkit/Views/TableColumn.swift b/Sources/TUIkit/Views/TableColumn.swift index 070e74f7..94bdd5d1 100644 --- a/Sources/TUIkit/Views/TableColumn.swift +++ b/Sources/TUIkit/Views/TableColumn.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - Column Width diff --git a/Sources/TUIkit/Views/TextField.swift b/Sources/TUIkit/Views/TextField.swift index 481fce80..05e7d024 100644 --- a/Sources/TUIkit/Views/TextField.swift +++ b/Sources/TUIkit/Views/TextField.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - TextField diff --git a/Sources/TUIkit/Views/Toggle.swift b/Sources/TUIkit/Views/Toggle.swift index 190db7f8..1e872da4 100644 --- a/Sources/TUIkit/Views/Toggle.swift +++ b/Sources/TUIkit/Views/Toggle.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - ToggleStyle Protocol diff --git a/Sources/TUIkit/Views/VStack.swift b/Sources/TUIkit/Views/VStack.swift index 4e91ad25..d134c9fb 100644 --- a/Sources/TUIkit/Views/VStack.swift +++ b/Sources/TUIkit/Views/VStack.swift @@ -197,12 +197,10 @@ private struct _VStackCore: View, Renderable, Layoutable { // MARK: - Equatable -extension VStack: Equatable where Content: Equatable { - nonisolated public static func == (lhs: VStack, rhs: VStack) -> Bool { - MainActor.assumeIsolated { - lhs.alignment == rhs.alignment && - lhs.spacing == rhs.spacing && - lhs.content == rhs.content - } +extension VStack: @preconcurrency Equatable where Content: Equatable { + public static func == (lhs: VStack, rhs: VStack) -> Bool { + lhs.alignment == rhs.alignment && + lhs.spacing == rhs.spacing && + lhs.content == rhs.content } } diff --git a/Sources/TUIkit/Views/ZStack.swift b/Sources/TUIkit/Views/ZStack.swift index 464ed762..57a991d2 100644 --- a/Sources/TUIkit/Views/ZStack.swift +++ b/Sources/TUIkit/Views/ZStack.swift @@ -69,11 +69,9 @@ private struct _ZStackCore: View, Renderable { // MARK: - Equatable -extension ZStack: Equatable where Content: Equatable { - nonisolated public static func == (lhs: ZStack, rhs: ZStack) -> Bool { - MainActor.assumeIsolated { - lhs.alignment == rhs.alignment && - lhs.content == rhs.content - } +extension ZStack: @preconcurrency Equatable where Content: Equatable { + public static func == (lhs: ZStack, rhs: ZStack) -> Bool { + lhs.alignment == rhs.alignment && + lhs.content == rhs.content } } diff --git a/Sources/TUIkit/Views/_ImageCore.swift b/Sources/TUIkit/Views/_ImageCore.swift index d6d94efa..65f73c4a 100644 --- a/Sources/TUIkit/Views/_ImageCore.swift +++ b/Sources/TUIkit/Views/_ImageCore.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - State Indices diff --git a/Sources/TUIkit/Views/_ListCore.swift b/Sources/TUIkit/Views/_ListCore.swift index 4fc6af1f..bf50ca45 100644 --- a/Sources/TUIkit/Views/_ListCore.swift +++ b/Sources/TUIkit/Views/_ListCore.swift @@ -4,7 +4,6 @@ // Created by LAYERED.work // License: MIT -import Foundation // MARK: - List Core (Internal Rendering) diff --git a/Tests/TUIkitTests/ProgressViewTests.swift b/Tests/TUIkitTests/ProgressViewTests.swift index f5263f34..da230569 100644 --- a/Tests/TUIkitTests/ProgressViewTests.swift +++ b/Tests/TUIkitTests/ProgressViewTests.swift @@ -134,7 +134,7 @@ struct ProgressViewStyleTests { @Test("Block style uses only █ and ░ characters") func blockStyleWholeBlocks() { - let view = ProgressView(value: 0.33).progressBarStyle(.block) + let view = ProgressView(value: 0.33).trackStyle(.block) let context = testContext(width: 10) let buffer = renderToBuffer(view, context: context) @@ -146,7 +146,7 @@ struct ProgressViewStyleTests { @Test("BlockFine style uses fractional blocks for sub-character precision") func blockFineStyleFractionalBlocks() { // 33% of 10 = 3.3 cells → 3 full + fractional - let view = ProgressView(value: 0.33).progressBarStyle(.blockFine) + let view = ProgressView(value: 0.33).trackStyle(.blockFine) let context = testContext(width: 10) let buffer = renderToBuffer(view, context: context) @@ -158,7 +158,7 @@ struct ProgressViewStyleTests { @Test("Shade style uses ▓ and ░ characters") func shadeStyleCharacters() { - let view = ProgressView(value: 0.5).progressBarStyle(.shade) + let view = ProgressView(value: 0.5).trackStyle(.shade) let context = testContext(width: 20) let buffer = renderToBuffer(view, context: context) @@ -169,7 +169,7 @@ struct ProgressViewStyleTests { @Test("Bar style uses ▌ and ─ characters") func barStyleCharacters() { - let view = ProgressView(value: 0.5).progressBarStyle(.bar) + let view = ProgressView(value: 0.5).trackStyle(.bar) let context = testContext(width: 20) let buffer = renderToBuffer(view, context: context) @@ -180,7 +180,7 @@ struct ProgressViewStyleTests { @Test("Dot style uses ▬, ● head, and ─ characters") func dotStyleCharacters() { - let view = ProgressView(value: 0.5).progressBarStyle(.dot) + let view = ProgressView(value: 0.5).trackStyle(.dot) let context = testContext(width: 20) let buffer = renderToBuffer(view, context: context) @@ -192,7 +192,7 @@ struct ProgressViewStyleTests { @Test("Style modifier returns correct style") func styleModifierWorks() { - let view = ProgressView(value: 0.5).progressBarStyle(.shade) + let view = ProgressView(value: 0.5).trackStyle(.shade) #expect(view.style == .shade) } @@ -202,7 +202,7 @@ struct ProgressViewStyleTests { let context = testContext(width: 20) for style in styles { - let view = ProgressView(value: 0.5).progressBarStyle(style) + let view = ProgressView(value: 0.5).trackStyle(style) let buffer = renderToBuffer(view, context: context) let barLine = buffer.lines[0].stripped #expect(barLine.count == 20, "Style \(style) should render width 20, got \(barLine.count)") @@ -211,7 +211,7 @@ struct ProgressViewStyleTests { @Test("Dot style at 0% shows no head and all empty") func dotStyleZeroPercent() { - let view = ProgressView(value: 0.0).progressBarStyle(.dot) + let view = ProgressView(value: 0.0).trackStyle(.dot) let context = testContext(width: 10) let buffer = renderToBuffer(view, context: context) @@ -223,7 +223,7 @@ struct ProgressViewStyleTests { @Test("Dot style at 100% shows head at end") func dotStyleFullPercent() { - let view = ProgressView(value: 1.0).progressBarStyle(.dot) + let view = ProgressView(value: 1.0).trackStyle(.dot) let context = testContext(width: 10) let buffer = renderToBuffer(view, context: context)