mirror of
https://github.com/phranck/TUIkit.git
synced 2026-05-21 09:50:35 +00:00
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:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (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<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
|
||||
}
|
||||
}
|
||||
@@ -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<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
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
// License: MIT Always rendered at the bottom of the terminal, never dimmed by overlays.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - StatusBar View
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
// while Theme defines the colors. Together they create a complete look.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Appearance
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// Created by LAYERED.work
|
||||
// License: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - ContentMode
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// Created by LAYERED.work
|
||||
// License: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Button Role
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// Created by LAYERED.work
|
||||
// License: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Radio Button Orientation
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// Created by LAYERED.work
|
||||
// License: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - SecureField
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// Created by LAYERED.work
|
||||
// License: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Slider
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// Created by LAYERED.work
|
||||
// License: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Stepper
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// Created by LAYERED.work
|
||||
// License: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Table
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// Created by LAYERED.work
|
||||
// License: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Column Width
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// Created by LAYERED.work
|
||||
// License: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - TextField
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// Created by LAYERED.work
|
||||
// License: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - ToggleStyle Protocol
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// Created by LAYERED.work
|
||||
// License: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - State Indices
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// Created by LAYERED.work
|
||||
// License: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - List Core (Internal Rendering)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user