From 024c1eaf155c1fa5eae3d550c978de7450ef993a Mon Sep 17 00:00:00 2001 From: phranck Date: Mon, 9 Feb 2026 18:43:31 +0100 Subject: [PATCH] Fix: Replace CFAbsoluteTimeGetCurrent with Date for Linux compatibility --- Tests/TUIkitTests/RenderBottleneckTests.swift | 4 +- .../TUIkitTests/RenderPerformanceTests.swift | 21 +- .../TUIkitTests/StringPerformanceTests.swift | 4 +- plans/open/2026-02-09-textfield-component.md | 274 ++++++++++++++++++ 4 files changed, 288 insertions(+), 15 deletions(-) create mode 100644 plans/open/2026-02-09-textfield-component.md diff --git a/Tests/TUIkitTests/RenderBottleneckTests.swift b/Tests/TUIkitTests/RenderBottleneckTests.swift index 45f8753..bbe4de1 100644 --- a/Tests/TUIkitTests/RenderBottleneckTests.swift +++ b/Tests/TUIkitTests/RenderBottleneckTests.swift @@ -21,11 +21,11 @@ struct RenderBottleneckTests { } private func measure(_ name: String, iterations: Int = 1000, block: () -> Void) -> TimeInterval { - let start = CFAbsoluteTimeGetCurrent() + let start = Date() for _ in 0.. TimeInterval { - let start = CFAbsoluteTimeGetCurrent() + let start = Date() for _ in 0.. Void) -> TimeInterval { - let start = CFAbsoluteTimeGetCurrent() + let start = Date() for _ in 0..) +init(_ title: S, text: Binding) where S: StringProtocol + +// With prompt (placeholder) +init(_ titleKey: LocalizedStringKey, text: Binding, prompt: Text?) +init(_ title: S, text: Binding, prompt: Text?) where S: StringProtocol + +// With ViewBuilder label +init(text: Binding, prompt: Text?, @ViewBuilder label: () -> Label) +``` + +Key behaviors: +- Updates the bound value **continuously** as the user types +- `.onSubmit(of:_:)` modifier invokes action on Enter +- `.focused($focusState)` for focus management +- `.textFieldStyle(_:)` for styling (`.plain`, `.roundedBorder`, etc.) + +## Specification / Goal + +### Success Criteria + +1. **SwiftUI API parity**: Match TextField signatures exactly +2. **Text editing**: Insert, delete (backspace/delete), cursor movement +3. **Cursor navigation**: Left/Right arrows, Home/End +4. **Focus integration**: Register with FocusManager, visual focus indicator +5. **Prompt/placeholder**: Show when text is empty and not focused +6. **Submit action**: `.onSubmit()` modifier triggers on Enter +7. **Disabled state**: Prevent editing when disabled +8. **All tests pass** + +### Terminal-Specific Considerations + +- No mouse support (keyboard only) +- Single-line only (no multi-line in v1) +- No clipboard/paste support in v1 (terminal limitation) +- Cursor rendered as block or underscore character + +## Design + +### Visual Rendering + +``` +Unfocused, empty: [Enter username...] (prompt in dim) +Unfocused, with text: [john.doe ] (text in normal) +Focused, empty: [ā–ˆ ] (cursor, brackets pulse) +Focused, with text: [john.dā–ˆe ] (cursor in text, brackets pulse) +Disabled: [john.doe ] (dim text and brackets) +``` + +### Architecture + +Following the established TUIKit pattern (like Toggle, Button): + +```swift +// Public View +public struct TextField: View { + let title: String + let text: Binding + let prompt: Text? + let focusID: String + let isDisabled: Bool + + public var body: some View { + _TextFieldCore(...) + } +} + +// Internal Renderable +private struct _TextFieldCore: View, Renderable { + var body: Never { fatalError() } + + func renderToBuffer(context: RenderContext) -> FrameBuffer { + // Register TextFieldHandler with FocusManager + // Render text with cursor position + } +} +``` + +### TextFieldHandler (Focusable) + +New handler class for text field focus and key handling: + +```swift +final class TextFieldHandler: Focusable { + let focusID: String + var canBeFocused: Bool + + // Text state + var text: Binding + var cursorPosition: Int // Character index + + // Callbacks + var onSubmit: (() -> Void)? + + func handleKeyEvent(_ event: KeyEvent) -> Bool { + switch event.key { + case .character(let char): + insertCharacter(char) + return true + case .backspace: + deleteBackward() + return true + case .delete: + deleteForward() + return true + case .left: + moveCursorLeft() + return true + case .right: + moveCursorRight() + return true + case .home: + cursorPosition = 0 + return true + case .end: + cursorPosition = text.wrappedValue.count + return true + case .enter: + onSubmit?() + return true + default: + return false + } + } +} +``` + +### State Persistence + +Use `StateStorage` to persist cursor position between renders: + +```swift +struct TextFieldState { + var cursorPosition: Int +} +``` + +### Modifier Support + +```swift +// onSubmit modifier (already exists in TUIKit?) +TextField("Username", text: $username) + .onSubmit { + login() + } + +// textFieldStyle modifier +TextField("Search", text: $query) + .textFieldStyle(.roundedBorder) +``` + +## Implementation Plan + +### Phase 1: Core TextField + +1. Create `TextFieldHandler` in `Sources/TUIkit/Focus/` +2. Create `TextField.swift` in `Sources/TUIkit/Views/` +3. Implement basic rendering (text + cursor) +4. Implement character input +5. Implement backspace/delete +6. Implement cursor movement (left/right/home/end) +7. Add focus indicator (pulsing brackets) + +### Phase 2: SwiftUI API Parity + +1. Add `init(_:text:)` - basic +2. Add `init(_:text:prompt:)` - with placeholder +3. Add `init(text:prompt:label:)` - ViewBuilder label +4. Implement prompt/placeholder rendering + +### Phase 3: Modifiers & Polish + +1. Implement `.onSubmit()` support +2. Implement `.textFieldStyle(_:)` modifier +3. Add `.disabled()` support +4. Add width constraint support + +### Phase 4: Testing + +1. Unit tests for TextFieldHandler +2. Rendering tests for TextField +3. Focus integration tests +4. Example app integration + +## Checklist + +### Phase 1: Core TextField +- [ ] Create TextFieldHandler class +- [ ] Create TextField struct with body: some View +- [ ] Implement _TextFieldCore with Renderable +- [ ] Render text content with cursor +- [ ] Handle character input +- [ ] Handle backspace/delete +- [ ] Handle cursor movement (left/right) +- [ ] Handle home/end keys +- [ ] Add focus indicator (pulsing brackets) +- [ ] State persistence for cursor position + +### Phase 2: SwiftUI API Parity +- [ ] init(_:text:) - basic initializer +- [ ] init(_:text:prompt:) - with placeholder +- [ ] init(text:prompt:label:) - ViewBuilder label +- [ ] Render prompt when empty and unfocused + +### Phase 3: Modifiers & Polish +- [ ] .onSubmit() modifier support +- [ ] .textFieldStyle(_:) modifier +- [ ] TextFieldStyle protocol +- [ ] PlainTextFieldStyle +- [ ] RoundedBorderTextFieldStyle (default for TUI) +- [ ] .disabled() support +- [ ] Width/frame support + +### Phase 4: Testing +- [ ] TextFieldHandler key event tests +- [ ] TextField rendering tests +- [ ] Focus integration tests +- [ ] Prompt/placeholder tests +- [ ] Style tests +- [ ] Example app demo page + +## Open Questions + +1. **Cursor representation**: Block (`ā–ˆ`), underscore (`_`), or pipe (`|`)? + - Recommendation: Block when focused, hidden when unfocused + +2. **Max length**: Should TextField support a character limit? + - Can add later as `.limit(_:)` modifier + +3. **Secure field**: Should we implement SecureField (password) in this plan or separately? + - Recommendation: Separate plan, but design TextField to support masking + +4. **Selection**: Should we support text selection (shift+arrows)? + - v1: No, keep simple. Add in future version. + +## Files + +### New Files +- `Sources/TUIkit/Focus/TextFieldHandler.swift` +- `Sources/TUIkit/Views/TextField.swift` +- `Sources/TUIkit/Modifiers/TextFieldStyleModifier.swift` +- `Tests/TUIkitTests/TextFieldTests.swift` +- `Tests/TUIkitTests/TextFieldHandlerTests.swift` + +### Modified Files +- `Sources/TUIkitExample/Pages/` - Add TextField demo page + +## Dependencies + +- FocusManager (for focus registration) +- StateStorage (for cursor position persistence) +- ActionHandler pattern (reference implementation) +- Existing View/Renderable architecture