Feat: Add TextCursor modifier for cursor styling in TextField/SecureField

- Add cursorColor to Palette protocol with HSL-computed values for all SystemPalette presets
- Create TextCursorStyle with shapes (block, bar, underscore) and animations (none, blink, pulse)
- Add .textCursor(_:) View modifier propagating through environment
- Update TextField and SecureField to use cursorColor and respect cursor style
- Add 17 tests for TextCursorStyle
This commit is contained in:
phranck
2026-02-10 16:51:45 +01:00
parent 0e9b9a76f7
commit ee25db7202
13 changed files with 848 additions and 102 deletions
+85 -42
View File
@@ -1,4 +1,4 @@
// 🖥 TUIKit Terminal UI Kit for Swift
// TUIKit - Terminal UI Kit for Swift
// TextFieldPage.swift
//
// Created by LAYERED.work
@@ -10,42 +10,76 @@ import TUIkit
///
/// Shows interactive text field features including:
/// - Basic text input with cursor
/// - Cursor styles (block, bar, underscore)
/// - Cursor animations (none, blink, pulse)
/// - Cursor speeds (slow, regular, fast)
/// - Cursor navigation (left/right/home/end)
/// - Text editing (insert, backspace, delete)
/// - onSubmit action
/// - Disabled state
/// - Live state display
struct TextFieldPage: View {
@State var username: String = ""
@State var email: String = ""
@State var demoText: String = ""
@State var searchQuery: String = ""
@State var disabledText: String = "Cannot edit"
@State var submittedValue: String = ""
@State var cursorShapeIndex: Int = 0
@State var cursorAnimationIndex: Int = 0
@State var cursorSpeedIndex: Int = 1 // Start at regular
private let shapes: [TextCursorStyle.Shape] = [.block, .bar, .underscore]
private let animations: [TextCursorStyle.Animation] = [.none, .blink, .pulse]
private let speeds: [TextCursorStyle.Speed] = [.slow, .regular, .fast]
private var currentShape: TextCursorStyle.Shape {
shapes[cursorShapeIndex]
}
private var currentAnimation: TextCursorStyle.Animation {
animations[cursorAnimationIndex]
}
private var currentSpeed: TextCursorStyle.Speed {
speeds[cursorSpeedIndex]
}
private var shapeLabel: String {
switch currentShape {
case .block: "█ Block"
case .bar: "│ Bar"
case .underscore: "▁ Underscore"
}
}
private var animationLabel: String {
switch currentAnimation {
case .none: "Static"
case .blink: "Blink"
case .pulse: "Pulse"
}
}
private var speedLabel: String {
switch currentSpeed {
case .slow: "Slow"
case .regular: "Regular"
case .fast: "Fast"
}
}
var body: some View {
VStack(alignment: .leading, spacing: 1) {
DemoSection("Basic Text Fields") {
DemoSection("Cursor Demo") {
VStack(alignment: .leading, spacing: 1) {
HStack(spacing: 1) {
Text("Username:").foregroundStyle(.palette.foregroundSecondary)
TextField("Username", text: $username)
Text("Input:").foregroundStyle(.palette.foregroundSecondary)
TextField("Type here...", text: $demoText)
}
HStack(spacing: 1) {
Text("Email:").foregroundStyle(.palette.foregroundSecondary)
TextField("Email", text: $email, prompt: Text("you@example.com"))
}
}
}
DemoSection("With onSubmit") {
VStack(alignment: .leading, spacing: 1) {
HStack(spacing: 1) {
Text("Search:").foregroundStyle(.palette.foregroundSecondary)
TextField("Search", text: $searchQuery)
.onSubmit {
submittedValue = searchQuery
}
.onSubmit { submittedValue = searchQuery }
}
if !submittedValue.isEmpty {
HStack(spacing: 1) {
@@ -53,6 +87,7 @@ struct TextFieldPage: View {
Text(submittedValue).foregroundStyle(.palette.success)
}
}
Text("Cursor style set on container, inherited by all fields").dim()
}
}
@@ -65,37 +100,31 @@ struct TextFieldPage: View {
}
}
DemoSection("Current Values") {
VStack(alignment: .leading, spacing: 1) {
HStack(spacing: 1) {
Text("Username:").foregroundStyle(.palette.foregroundSecondary)
Text(username.isEmpty ? "(empty)" : "\"\(username)\"").foregroundStyle(.palette.accent)
}
HStack(spacing: 1) {
Text("Email:").foregroundStyle(.palette.foregroundSecondary)
Text(email.isEmpty ? "(empty)" : "\"\(email)\"").foregroundStyle(.palette.accent)
}
HStack(spacing: 1) {
Text("Search:").foregroundStyle(.palette.foregroundSecondary)
Text(searchQuery.isEmpty ? "(empty)" : "\"\(searchQuery)\"").foregroundStyle(.palette.accent)
HStack(alignment: .top, spacing: 3) {
DemoSection("Keyboard Controls") {
VStack(alignment: .leading) {
Text("[←] [→] Move cursor left/right").dim()
Text("[Home] [End] Jump to start/end").dim()
Text("[Backspace] Delete before cursor").dim()
Text("[Delete] Delete at cursor").dim()
Text("[Enter] Submit (triggers onSubmit)").dim()
Text("[Tab] Move to next field").dim()
}
}
}
DemoSection("Keyboard Controls") {
VStack(alignment: .leading) {
Text("Type any character to insert at cursor").dim()
Text("[←] [→] Move cursor left/right").dim()
Text("[Home] [End] Jump to start/end").dim()
Text("[Backspace] Delete before cursor").dim()
Text("[Delete] Delete at cursor").dim()
Text("[Enter] Submit (triggers onSubmit)").dim()
Text("[Tab] Move to next field").dim()
DemoSection("Cursor Settings") {
VStack(alignment: .leading) {
Text("[F1] Shape: Block, Bar, Underscore").dim()
Text("[F2] Animation: Static, Blink, Pulse").dim()
Text("[F3] Speed: Slow, Regular, Fast").dim()
}
}
}
Spacer()
}
.textCursor(currentShape, animation: currentAnimation, speed: currentSpeed)
.statusBarItems(cursorStatusBarItems)
.appHeader {
HStack {
Text("TextField Demo").bold().foregroundStyle(.palette.accent)
@@ -104,4 +133,18 @@ struct TextFieldPage: View {
}
}
}
private var cursorStatusBarItems: [any StatusBarItemProtocol] {
[
StatusBarItem(shortcut: Shortcut.f1, label: shapeLabel) {
cursorShapeIndex = (cursorShapeIndex + 1) % shapes.count
},
StatusBarItem(shortcut: Shortcut.f2, label: animationLabel) {
cursorAnimationIndex = (cursorAnimationIndex + 1) % animations.count
},
StatusBarItem(shortcut: Shortcut.f3, label: speedLabel) {
cursorSpeedIndex = (cursorSpeedIndex + 1) % speeds.count
},
]
}
}