Refactor: Replace MainActor.assumeIsolated with @preconcurrency Equatable

- Migrate 20 Equatable conformances across 17 files from
  nonisolated + MainActor.assumeIsolated to @preconcurrency Equatable (SE-0423)
- Remove unnecessary import Foundation from 29 source files
- Extract TextFieldHandler clipboard ops into TextFieldHandler+Clipboard.swift
- Extract RenderContext into RenderContext.swift (Renderable.swift 553 -> 279 lines)
- Extract ANSIColor enum into ANSIColor.swift (Color.swift 600 -> 533 lines)
- Add deprecation timeline note for progressBarStyle(_:)
- Migrate test usages from progressBarStyle to trackStyle
This commit is contained in:
phranck
2026-02-14 02:10:26 +01:00
parent 02d1921bf4
commit e214215610
53 changed files with 661 additions and 706 deletions
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - List Row
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - List Row Type
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - Environment Key Protocol
@@ -5,7 +5,6 @@
// License: MIT Similar to SwiftUI's PreferenceKey system.
//
import Foundation
// MARK: - Preference Key Protocol
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - Alert Presentation
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - Selection Mode
@@ -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..<endIndex])
copyToClipboard(selectedText)
}
/// Cuts the selected text to the system clipboard.
///
/// Uses `pbcopy` on macOS. Does nothing if no text is selected.
func cutSelection() {
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..<endIndex])
copyToClipboard(selectedText)
pushUndoState()
deleteRangeWithoutUndo(range)
clearSelection()
}
/// Pastes text from the system clipboard at the cursor position.
///
/// Uses `pbpaste` on macOS. Replaces selection if any.
func paste() {
guard let pastedText = pasteFromClipboard() else { return }
insertText(pastedText)
}
/// Inserts a string at the cursor position in a single operation.
///
/// Used by both clipboard paste (`Ctrl+V`) and bracketed paste
/// (terminal paste via `Cmd+V`). Replaces selection if any.
///
/// - Parameter string: The text to insert.
func insertText(_ string: String) {
guard !string.isEmpty else { return }
// For single-line text fields, strip newlines from pasted text.
var sanitized = string.replacingOccurrences(of: "\n", with: "")
.replacingOccurrences(of: "\r", with: "")
// Filter by content type if set.
if let contentType = textContentType {
sanitized = contentType.filterString(sanitized)
}
guard !sanitized.isEmpty else { return }
pushUndoState()
// Replace selection if present
if let range = selectionRange {
deleteRangeWithoutUndo(range)
clearSelection()
}
// Insert text
var current = text.wrappedValue
let index = current.index(current.startIndex, offsetBy: min(cursorPosition, current.count))
current.insert(contentsOf: sanitized, at: index)
text.wrappedValue = current
cursorPosition += sanitized.count
}
}
// MARK: - Clipboard Helpers
private extension TextFieldHandler {
/// Copies text to the system clipboard using platform-specific command.
func copyToClipboard(_ text: String) {
#if os(macOS)
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/pbcopy")
let pipe = Pipe()
process.standardInput = pipe
do {
try process.run()
pipe.fileHandleForWriting.write(Data(text.utf8))
pipe.fileHandleForWriting.closeFile()
process.waitUntilExit()
} catch {
// Silently fail if clipboard is unavailable
}
#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"] : ["--clipboard", "--input"]
let pipe = Pipe()
process.standardInput = pipe
do {
try process.run()
pipe.fileHandleForWriting.write(Data(text.utf8))
pipe.fileHandleForWriting.closeFile()
process.waitUntilExit()
return
} catch {
continue
}
}
}
#endif
}
/// Pastes text from the system clipboard using platform-specific command.
func pasteFromClipboard() -> 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
}
}
+2 -188
View File
@@ -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<Int>) {
func deleteRangeWithoutUndo(_ range: Range<Int>) {
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..<endIndex])
copyToClipboard(selectedText)
}
/// Cuts the selected text to the system clipboard.
///
/// Uses `pbcopy` on macOS. Does nothing if no text is selected.
func cutSelection() {
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..<endIndex])
copyToClipboard(selectedText)
pushUndoState()
deleteRangeWithoutUndo(range)
clearSelection()
}
/// Pastes text from the system clipboard at the cursor position.
///
/// Uses `pbpaste` on macOS. Replaces selection if any.
func paste() {
guard let pastedText = pasteFromClipboard() else { return }
insertText(pastedText)
}
/// Inserts a string at the cursor position in a single operation.
///
/// Used by both clipboard paste (`Ctrl+V`) and bracketed paste
/// (terminal paste via `Cmd+V`). Replaces selection if any.
///
/// - Parameter string: The text to insert.
func insertText(_ string: String) {
guard !string.isEmpty else { return }
// For single-line text fields, strip newlines from pasted text.
var sanitized = string.replacingOccurrences(of: "\n", with: "")
.replacingOccurrences(of: "\r", with: "")
// Filter by content type if set.
if let contentType = textContentType {
sanitized = contentType.filterString(sanitized)
}
guard !sanitized.isEmpty else { return }
pushUndoState()
// Replace selection if present
if let range = selectionRange {
deleteRangeWithoutUndo(range)
clearSelection()
}
// Insert text
var current = text.wrappedValue
let index = current.index(current.startIndex, offsetBy: min(cursorPosition, current.count))
current.insert(contentsOf: sanitized, at: index)
text.wrappedValue = current
cursorPosition += sanitized.count
}
}
// MARK: - Clipboard Helpers
private extension TextFieldHandler {
/// Copies text to the system clipboard using platform-specific command.
func copyToClipboard(_ text: String) {
#if os(macOS)
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/pbcopy")
let pipe = Pipe()
process.standardInput = pipe
do {
try process.run()
pipe.fileHandleForWriting.write(Data(text.utf8))
pipe.fileHandleForWriting.closeFile()
process.waitUntilExit()
} catch {
// Silently fail if clipboard is unavailable
}
#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"] : ["--clipboard", "--input"]
let pipe = Pipe()
process.standardInput = pipe
do {
try process.run()
pipe.fileHandleForWriting.write(Data(text.utf8))
pipe.fileHandleForWriting.closeFile()
process.waitUntilExit()
return
} catch {
continue
}
}
}
#endif
}
/// Pastes text from the system clipboard using platform-specific command.
func pasteFromClipboard() -> 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 {
+3 -3
View File
@@ -59,9 +59,9 @@ public enum BadgeValue: Sendable {
// MARK: - Equatable
extension BadgeModifier: Equatable where Content: Equatable {
nonisolated public static func == (lhs: BadgeModifier<Content>, rhs: BadgeModifier<Content>) -> Bool {
MainActor.assumeIsolated { lhs.content == rhs.content && lhs.value == rhs.value }
extension BadgeModifier: @preconcurrency Equatable where Content: Equatable {
public static func == (lhs: BadgeModifier<Content>, rhs: BadgeModifier<Content>) -> Bool {
lhs.content == rhs.content && lhs.value == rhs.value
}
}
@@ -24,9 +24,9 @@ public struct DimmedModifier<Content: View>: View {
// MARK: - Equatable Conformance
extension DimmedModifier: Equatable where Content: Equatable {
nonisolated public static func == (lhs: DimmedModifier<Content>, rhs: DimmedModifier<Content>) -> Bool {
MainActor.assumeIsolated { lhs.content == rhs.content }
extension DimmedModifier: @preconcurrency Equatable where Content: Equatable {
public static func == (lhs: DimmedModifier<Content>, rhs: DimmedModifier<Content>) -> Bool {
lhs.content == rhs.content
}
}
+10 -12
View File
@@ -56,18 +56,16 @@ public struct FlexibleFrameView<Content: View>: View {
// MARK: - Equatable Conformance
extension FlexibleFrameView: Equatable where Content: Equatable {
nonisolated public static func == (lhs: FlexibleFrameView<Content>, rhs: FlexibleFrameView<Content>) -> 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<Content>, rhs: FlexibleFrameView<Content>) -> 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
}
}
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - OnAppear Modifier
@@ -75,13 +75,11 @@ public enum VerticalEdge: Sendable {
// MARK: - Equatable
extension ListRowSeparatorModifier: Equatable where Content: Equatable {
nonisolated public static func == (lhs: ListRowSeparatorModifier<Content>, rhs: ListRowSeparatorModifier<Content>) -> 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<Content>, rhs: ListRowSeparatorModifier<Content>) -> Bool {
lhs.content == rhs.content &&
lhs.visibility == rhs.visibility &&
lhs.edges == rhs.edges
}
}
@@ -26,13 +26,11 @@ public struct OverlayModifier<Base: View, Overlay: View>: View {
// MARK: - Equatable Conformance
extension OverlayModifier: Equatable where Base: Equatable, Overlay: Equatable {
nonisolated public static func == (lhs: OverlayModifier<Base, Overlay>, rhs: OverlayModifier<Base, Overlay>) -> 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<Base, Overlay>, rhs: OverlayModifier<Base, Overlay>) -> Bool {
lhs.base == rhs.base &&
lhs.overlay == rhs.overlay &&
lhs.alignment == rhs.alignment
}
}
@@ -38,9 +38,9 @@ public struct SelectionDisabledModifier<Content: View>: View {
// MARK: - Equatable
extension SelectionDisabledModifier: Equatable where Content: Equatable {
nonisolated public static func == (lhs: SelectionDisabledModifier<Content>, rhs: SelectionDisabledModifier<Content>) -> Bool {
MainActor.assumeIsolated { lhs.content == rhs.content && lhs.isDisabled == rhs.isDisabled }
extension SelectionDisabledModifier: @preconcurrency Equatable where Content: Equatable {
public static func == (lhs: SelectionDisabledModifier<Content>, rhs: SelectionDisabledModifier<Content>) -> Bool {
lhs.content == rhs.content && lhs.isDisabled == rhs.isDisabled
}
}
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - StatusBarItemsModifier
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - StatusBarSystemItemsModifier
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
/// Reusable building blocks for border rendering.
///
@@ -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 (01) 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<V>(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<V>(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
}
}
-274
View File
@@ -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 (01) 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<V>(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<V>(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.
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - Scroll Direction
-1
View File
@@ -5,7 +5,6 @@
// License: MIT Always rendered at the bottom of the terminal, never dimmed by overlays.
//
import Foundation
// MARK: - StatusBar View
+70
View File
@@ -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)
}
}
}
-1
View File
@@ -7,7 +7,6 @@
// while Theme defines the colors. Together they create a complete look.
//
import Foundation
// MARK: - Appearance
-67
View File
@@ -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.
-1
View File
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - ContentMode
-1
View File
@@ -5,7 +5,6 @@
// License: MIT and palette registry.
//
import Foundation
// MARK: - Palette Protocol
@@ -6,7 +6,6 @@
// with a single, reusable implementation.
//
import Foundation
// MARK: - Cyclable Protocol
+5 -7
View File
@@ -166,12 +166,10 @@ struct BufferView: View, Renderable {
// MARK: - Equatable Conformance
extension Box: Equatable where Content: Equatable {
nonisolated static func == (lhs: Box<Content>, rhs: Box<Content>) -> 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<Content>, rhs: Box<Content>) -> Bool {
lhs.content == rhs.content &&
lhs.borderStyle == rhs.borderStyle &&
lhs.borderColor == rhs.borderColor
}
}
-1
View File
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - Button Role
+7 -9
View File
@@ -133,15 +133,13 @@ public struct Card<Content: View, Footer: View>: View {
// MARK: - Equatable Conformance
extension Card: Equatable where Content: Equatable, Footer: Equatable {
nonisolated public static func == (lhs: Card<Content, Footer>, rhs: Card<Content, Footer>) -> 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<Content, Footer>, rhs: Card<Content, Footer>) -> Bool {
lhs.title == rhs.title &&
lhs.content == rhs.content &&
lhs.footer == rhs.footer &&
lhs.config == rhs.config &&
lhs.backgroundColor == rhs.backgroundColor
}
}
+16 -20
View File
@@ -246,16 +246,14 @@ struct ContainerView<Content: View, Footer: View>: View {
// MARK: - Equatable Conformance
extension ContainerView: Equatable where Content: Equatable, Footer: Equatable {
nonisolated static func == (lhs: ContainerView<Content, Footer>, rhs: ContainerView<Content, Footer>) -> 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<Content, Footer>, rhs: ContainerView<Content, Footer>) -> 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<Content: View, Footer: View>: View, Renderable
// MARK: - Equatable Conformance
extension _ContainerViewCore: Equatable where Content: Equatable, Footer: Equatable {
nonisolated static func == (lhs: _ContainerViewCore<Content, Footer>, rhs: _ContainerViewCore<Content, Footer>) -> 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<Content, Footer>, rhs: _ContainerViewCore<Content, Footer>) -> 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
}
}
+6 -8
View File
@@ -107,14 +107,12 @@ public struct Dialog<Content: View, Footer: View>: View {
// MARK: - Equatable Conformance
extension Dialog: Equatable where Content: Equatable, Footer: Equatable {
nonisolated public static func == (lhs: Dialog<Content, Footer>, rhs: Dialog<Content, Footer>) -> 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<Content, Footer>, rhs: Dialog<Content, Footer>) -> Bool {
lhs.title == rhs.title &&
lhs.content == rhs.content &&
lhs.footer == rhs.footer &&
lhs.config == rhs.config
}
}
+5 -7
View File
@@ -164,12 +164,10 @@ private struct _HStackCore<Content: View>: View, Renderable, Layoutable {
// MARK: - Equatable
extension HStack: Equatable where Content: Equatable {
nonisolated public static func == (lhs: HStack<Content>, rhs: HStack<Content>) -> 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<Content>, rhs: HStack<Content>) -> Bool {
lhs.alignment == rhs.alignment &&
lhs.spacing == rhs.spacing &&
lhs.content == rhs.content
}
}
+2 -3
View File
@@ -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
}
}
+10 -14
View File
@@ -278,22 +278,18 @@ private struct _LazyHStackCore<Content: View>: View, Renderable {
// MARK: - Equatable Conformances
extension LazyVStack: Equatable where Content: Equatable {
nonisolated public static func == (lhs: LazyVStack<Content>, rhs: LazyVStack<Content>) -> 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<Content>, rhs: LazyVStack<Content>) -> 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<Content>, rhs: LazyHStack<Content>) -> 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<Content>, rhs: LazyHStack<Content>) -> Bool {
lhs.alignment == rhs.alignment &&
lhs.spacing == rhs.spacing &&
lhs.content == rhs.content
}
}
-1
View File
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - List (Single Selection)
@@ -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
}
}
+6 -8
View File
@@ -127,14 +127,12 @@ public struct Panel<Content: View, Footer: View>: View {
// MARK: - Equatable Conformance
extension Panel: Equatable where Content: Equatable, Footer: Equatable {
nonisolated public static func == (lhs: Panel<Content, Footer>, rhs: Panel<Content, Footer>) -> 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<Content, Footer>, rhs: Panel<Content, Footer>) -> Bool {
lhs.title == rhs.title &&
lhs.content == rhs.content &&
lhs.footer == rhs.footer &&
lhs.config == rhs.config
}
}
+7 -8
View File
@@ -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<Label, CurrentValueLabel>, rhs: ProgressView<Label, CurrentValueLabel>) -> 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<Label, CurrentValueLabel>, rhs: ProgressView<Label, CurrentValueLabel>) -> Bool {
lhs.fractionCompleted == rhs.fractionCompleted &&
lhs.style == rhs.style &&
lhs.label == rhs.label &&
lhs.currentValueLabel == rhs.currentValueLabel
}
}
-1
View File
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - Radio Button Orientation
-1
View File
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - SecureField
-1
View File
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - Slider
-1
View File
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - Stepper
-1
View File
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - Table
-1
View File
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - Column Width
-1
View File
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - TextField
-1
View File
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - ToggleStyle Protocol
+5 -7
View File
@@ -197,12 +197,10 @@ private struct _VStackCore<Content: View>: View, Renderable, Layoutable {
// MARK: - Equatable
extension VStack: Equatable where Content: Equatable {
nonisolated public static func == (lhs: VStack<Content>, rhs: VStack<Content>) -> 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<Content>, rhs: VStack<Content>) -> Bool {
lhs.alignment == rhs.alignment &&
lhs.spacing == rhs.spacing &&
lhs.content == rhs.content
}
}
+4 -6
View File
@@ -69,11 +69,9 @@ private struct _ZStackCore<Content: View>: View, Renderable {
// MARK: - Equatable
extension ZStack: Equatable where Content: Equatable {
nonisolated public static func == (lhs: ZStack<Content>, rhs: ZStack<Content>) -> Bool {
MainActor.assumeIsolated {
lhs.alignment == rhs.alignment &&
lhs.content == rhs.content
}
extension ZStack: @preconcurrency Equatable where Content: Equatable {
public static func == (lhs: ZStack<Content>, rhs: ZStack<Content>) -> Bool {
lhs.alignment == rhs.alignment &&
lhs.content == rhs.content
}
}
-1
View File
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - State Indices
-1
View File
@@ -4,7 +4,6 @@
// Created by LAYERED.work
// License: MIT
import Foundation
// MARK: - List Core (Internal Rendering)
+9 -9
View File
@@ -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)