mirror of
https://github.com/phranck/TUIkit.git
synced 2026-05-21 09:50:35 +00:00
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:
@@ -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
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user