171 lines
3.9 KiB
Go
171 lines
3.9 KiB
Go
package tui
|
|
|
|
import (
|
|
"github.com/lixenwraith/terminal"
|
|
)
|
|
|
|
// TextFieldOpts configures text field rendering
|
|
type TextFieldOpts struct {
|
|
Placeholder string // Shown when empty
|
|
Prefix string // Left prompt (e.g., "> ")
|
|
Mask rune // Password mask, 0 = none
|
|
MaxLen int // Max runes, 0 = unlimited
|
|
Border LineType // Border style, LineNone = no border
|
|
Focused bool // Show cursor and accept input
|
|
Style TextFieldStyle
|
|
}
|
|
|
|
// DefaultTextFieldStyle returns default colors
|
|
func DefaultTextFieldStyle() TextFieldStyle {
|
|
return TextFieldStyle{
|
|
TextFg: terminal.RGB{R: 220, G: 220, B: 220},
|
|
TextBg: terminal.RGB{R: 30, G: 30, B: 40},
|
|
CursorFg: terminal.RGB{R: 0, G: 0, B: 0},
|
|
CursorBg: terminal.RGB{R: 200, G: 200, B: 200},
|
|
PlaceholderFg: terminal.RGB{R: 100, G: 100, B: 110},
|
|
PrefixFg: terminal.RGB{R: 150, G: 150, B: 180},
|
|
BorderFg: terminal.RGB{R: 80, G: 80, B: 100},
|
|
}
|
|
}
|
|
|
|
// TextFieldStyle defines text field colors
|
|
type TextFieldStyle struct {
|
|
TextFg terminal.RGB
|
|
TextBg terminal.RGB
|
|
CursorFg terminal.RGB
|
|
CursorBg terminal.RGB
|
|
PlaceholderFg terminal.RGB
|
|
PrefixFg terminal.RGB
|
|
BorderFg terminal.RGB
|
|
}
|
|
|
|
// TextField renders text field and returns content height used
|
|
func (r Region) TextField(state *TextFieldState, opts TextFieldOpts) int {
|
|
if r.W < 3 || r.H < 1 {
|
|
return 0
|
|
}
|
|
|
|
style := opts.Style
|
|
if style == (TextFieldStyle{}) {
|
|
style = DefaultTextFieldStyle()
|
|
}
|
|
|
|
// Calculate content area
|
|
contentY := 0
|
|
contentX := 0
|
|
contentW := r.W
|
|
contentH := 1
|
|
|
|
if opts.Border != LineNone {
|
|
if r.H < 3 {
|
|
return 0
|
|
}
|
|
r.Box(opts.Border, style.BorderFg)
|
|
contentY = 1
|
|
contentX = 1
|
|
contentW = r.W - 2
|
|
contentH = r.H - 2
|
|
if contentH > 1 {
|
|
contentH = 1
|
|
}
|
|
}
|
|
|
|
// Fill background
|
|
for x := contentX; x < contentX+contentW; x++ {
|
|
r.Cell(x, contentY, ' ', style.TextFg, style.TextBg, terminal.AttrNone)
|
|
}
|
|
|
|
x := contentX
|
|
|
|
// Prefix
|
|
if opts.Prefix != "" {
|
|
for _, ch := range opts.Prefix {
|
|
if x >= contentX+contentW {
|
|
break
|
|
}
|
|
r.Cell(x, contentY, ch, style.PrefixFg, style.TextBg, terminal.AttrNone)
|
|
x++
|
|
}
|
|
}
|
|
|
|
// Calculate viewport
|
|
viewportW := contentX + contentW - x
|
|
if viewportW < 1 {
|
|
return contentH + 2*boolToInt(opts.Border != LineNone)
|
|
}
|
|
|
|
// Adjust scroll
|
|
state.AdjustScroll(viewportW)
|
|
|
|
// Render text or placeholder
|
|
text := state.Text
|
|
isEmpty := len(text) == 0
|
|
|
|
if isEmpty && opts.Placeholder != "" && !opts.Focused {
|
|
// Placeholder
|
|
placeholder := opts.Placeholder
|
|
if RuneLen(placeholder) > viewportW {
|
|
placeholder = Truncate(placeholder, viewportW)
|
|
}
|
|
for i, ch := range placeholder {
|
|
if x+i >= contentX+contentW {
|
|
break
|
|
}
|
|
r.Cell(x+i, contentY, ch, style.PlaceholderFg, style.TextBg, terminal.AttrDim)
|
|
}
|
|
} else {
|
|
// Scroll indicators
|
|
if state.Scroll > 0 && x > contentX {
|
|
r.Cell(x-1, contentY, '◀', style.PlaceholderFg, style.TextBg, terminal.AttrNone)
|
|
}
|
|
|
|
// Text content
|
|
for i := 0; i < viewportW; i++ {
|
|
runeIdx := state.Scroll + i
|
|
ch := ' '
|
|
if runeIdx < len(text) {
|
|
ch = text[runeIdx]
|
|
if opts.Mask != 0 {
|
|
ch = opts.Mask
|
|
}
|
|
}
|
|
|
|
fg := style.TextFg
|
|
bg := style.TextBg
|
|
|
|
// Cursor highlighting
|
|
if opts.Focused && runeIdx == state.Cursor {
|
|
fg = style.CursorFg
|
|
bg = style.CursorBg
|
|
}
|
|
|
|
r.Cell(x+i, contentY, ch, fg, bg, terminal.AttrNone)
|
|
}
|
|
|
|
// Cursor at end
|
|
if opts.Focused && state.Cursor == len(text) {
|
|
cursorX := x + state.Cursor - state.Scroll
|
|
if cursorX >= x && cursorX < contentX+contentW {
|
|
r.Cell(cursorX, contentY, ' ', style.CursorFg, style.CursorBg, terminal.AttrNone)
|
|
}
|
|
}
|
|
|
|
// Right scroll indicator
|
|
if state.Scroll+viewportW < len(text) {
|
|
r.Cell(contentX+contentW-1, contentY, '▶', style.PlaceholderFg, style.TextBg, terminal.AttrNone)
|
|
}
|
|
}
|
|
|
|
if opts.Border != LineNone {
|
|
return 3
|
|
}
|
|
return 1
|
|
}
|
|
|
|
// boolToInt converts boolean to integer (0 or 1)
|
|
func boolToInt(b bool) int {
|
|
if b {
|
|
return 1
|
|
}
|
|
return 0
|
|
} |