v0.1.0 initial commit
This commit is contained in:
+228
@@ -0,0 +1,228 @@
|
||||
package tui
|
||||
|
||||
import "github.com/lixenwraith/terminal"
|
||||
|
||||
// FormField pairs a label with an editable text field
|
||||
type FormField struct {
|
||||
Label string
|
||||
State *TextFieldState
|
||||
}
|
||||
|
||||
// FormState holds state for a multi-field form with focus tracking
|
||||
type FormState struct {
|
||||
Fields []FormField
|
||||
Focus int
|
||||
}
|
||||
|
||||
// NewFormState creates a form with labeled fields initialized to empty values
|
||||
func NewFormState(labels ...string) *FormState {
|
||||
fields := make([]FormField, len(labels))
|
||||
for i, label := range labels {
|
||||
fields[i] = FormField{
|
||||
Label: label,
|
||||
State: NewTextFieldState(""),
|
||||
}
|
||||
}
|
||||
return &FormState{Fields: fields}
|
||||
}
|
||||
|
||||
// Value returns the text value of the field at idx
|
||||
func (f *FormState) Value(idx int) string {
|
||||
if idx >= 0 && idx < len(f.Fields) {
|
||||
return f.Fields[idx].State.Value()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetValue replaces the text of the field at idx
|
||||
func (f *FormState) SetValue(idx int, val string) {
|
||||
if idx >= 0 && idx < len(f.Fields) {
|
||||
f.Fields[idx].State.SetValue(val)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear empties all fields in the form
|
||||
func (f *FormState) Clear() {
|
||||
for i := range f.Fields {
|
||||
f.Fields[i].State.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
// FocusNext moves focus to the next field, wrapping around
|
||||
func (f *FormState) FocusNext() {
|
||||
if len(f.Fields) > 0 {
|
||||
f.Focus = (f.Focus + 1) % len(f.Fields)
|
||||
}
|
||||
}
|
||||
|
||||
// FocusPrev moves focus to the previous field, wrapping around
|
||||
func (f *FormState) FocusPrev() {
|
||||
if len(f.Fields) > 0 {
|
||||
f.Focus = (f.Focus - 1 + len(f.Fields)) % len(f.Fields)
|
||||
}
|
||||
}
|
||||
|
||||
// CurrentField returns the TextFieldState of the focused field, or nil
|
||||
func (f *FormState) CurrentField() *TextFieldState {
|
||||
if f.Focus >= 0 && f.Focus < len(f.Fields) {
|
||||
return f.Fields[f.Focus].State
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleKey processes keyboard input for form navigation and field editing, returns true if state changed
|
||||
func (f *FormState) HandleKey(key terminal.Key, r rune, mod terminal.Modifier) bool {
|
||||
switch key {
|
||||
case terminal.KeyTab:
|
||||
if mod&terminal.ModShift != 0 {
|
||||
f.FocusPrev()
|
||||
} else {
|
||||
f.FocusNext()
|
||||
}
|
||||
return true
|
||||
case terminal.KeyUp:
|
||||
f.FocusPrev()
|
||||
return true
|
||||
case terminal.KeyDown:
|
||||
f.FocusNext()
|
||||
return true
|
||||
default:
|
||||
if field := f.CurrentField(); field != nil {
|
||||
return field.HandleKey(key, r, mod)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FormOpts configures form rendering
|
||||
type FormOpts struct {
|
||||
LabelWidth int
|
||||
Spacing int
|
||||
Style FormStyle
|
||||
}
|
||||
|
||||
// FormStyle defines form colors
|
||||
type FormStyle struct {
|
||||
LabelFg terminal.RGB
|
||||
FieldFg terminal.RGB
|
||||
FieldBg terminal.RGB
|
||||
FocusBg terminal.RGB
|
||||
CursorFg terminal.RGB
|
||||
CursorBg terminal.RGB
|
||||
Bg terminal.RGB
|
||||
}
|
||||
|
||||
// DefaultFormStyle returns default form colors
|
||||
func DefaultFormStyle() FormStyle {
|
||||
return FormStyle{
|
||||
LabelFg: terminal.RGB{R: 150, G: 150, B: 180},
|
||||
FieldFg: terminal.RGB{R: 220, G: 220, B: 220},
|
||||
FieldBg: terminal.RGB{R: 35, G: 35, B: 45},
|
||||
FocusBg: terminal.RGB{R: 45, G: 45, B: 60},
|
||||
CursorFg: terminal.RGB{R: 0, G: 0, B: 0},
|
||||
CursorBg: terminal.RGB{R: 200, G: 200, B: 200},
|
||||
Bg: terminal.RGB{R: 25, G: 25, B: 35},
|
||||
}
|
||||
}
|
||||
|
||||
// Form renders a multi-field form with labels and editable text fields, returns height used
|
||||
func (r Region) Form(state *FormState, opts FormOpts) int {
|
||||
if len(state.Fields) == 0 || r.H < 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
style := opts.Style
|
||||
if style == (FormStyle{}) {
|
||||
style = DefaultFormStyle()
|
||||
}
|
||||
|
||||
labelW := opts.LabelWidth
|
||||
if labelW <= 0 {
|
||||
for _, f := range state.Fields {
|
||||
if w := RuneLen(f.Label); w > labelW {
|
||||
labelW = w
|
||||
}
|
||||
}
|
||||
labelW += 2
|
||||
}
|
||||
|
||||
spacing := opts.Spacing
|
||||
if spacing < 1 {
|
||||
spacing = 1
|
||||
}
|
||||
|
||||
y := 0
|
||||
for i, field := range state.Fields {
|
||||
if y >= r.H {
|
||||
break
|
||||
}
|
||||
|
||||
isFocused := i == state.Focus
|
||||
|
||||
// Label
|
||||
label := field.Label + ":"
|
||||
for j, ch := range label {
|
||||
if j >= labelW || j >= r.W {
|
||||
break
|
||||
}
|
||||
r.Cell(j, y, ch, style.LabelFg, style.Bg, terminal.AttrNone)
|
||||
}
|
||||
|
||||
// Field area
|
||||
fieldX := labelW
|
||||
fieldW := r.W - labelW
|
||||
if fieldW < 1 {
|
||||
y += spacing
|
||||
continue
|
||||
}
|
||||
|
||||
fieldBg := style.FieldBg
|
||||
if isFocused {
|
||||
fieldBg = style.FocusBg
|
||||
}
|
||||
|
||||
for x := fieldX; x < fieldX+fieldW && x < r.W; x++ {
|
||||
r.Cell(x, y, ' ', style.FieldFg, fieldBg, terminal.AttrNone)
|
||||
}
|
||||
|
||||
// Render text using existing TextField logic
|
||||
text := field.State.Text
|
||||
scroll := field.State.Scroll
|
||||
cursor := field.State.Cursor
|
||||
|
||||
field.State.AdjustScroll(fieldW)
|
||||
scroll = field.State.Scroll
|
||||
|
||||
for x := 0; x < fieldW; x++ {
|
||||
runeIdx := scroll + x
|
||||
ch := ' '
|
||||
if runeIdx < len(text) {
|
||||
ch = text[runeIdx]
|
||||
}
|
||||
|
||||
fg := style.FieldFg
|
||||
bg := fieldBg
|
||||
|
||||
if isFocused && runeIdx == cursor {
|
||||
fg = style.CursorFg
|
||||
bg = style.CursorBg
|
||||
}
|
||||
|
||||
if fieldX+x < r.W {
|
||||
r.Cell(fieldX+x, y, ch, fg, bg, terminal.AttrNone)
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor at end of text
|
||||
if isFocused && cursor == len(text) {
|
||||
cursorX := fieldX + cursor - scroll
|
||||
if cursorX >= fieldX && cursorX < fieldX+fieldW && cursorX < r.W {
|
||||
r.Cell(cursorX, y, ' ', style.CursorFg, style.CursorBg, terminal.AttrNone)
|
||||
}
|
||||
}
|
||||
|
||||
y += spacing
|
||||
}
|
||||
|
||||
return y
|
||||
}
|
||||
Reference in New Issue
Block a user