From f18b53809d6d57f1a850daead2748f5cfe81a368 Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Thu, 26 Feb 2026 11:23:51 -0600 Subject: [PATCH] Working on TextArea Field --- wdgt_field.go | 6 +- wdgt_textarea_field.go | 338 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 340 insertions(+), 4 deletions(-) create mode 100644 wdgt_textarea_field.go diff --git a/wdgt_field.go b/wdgt_field.go index 848d3a3..d73ddef 100644 --- a/wdgt_field.go +++ b/wdgt_field.go @@ -188,10 +188,8 @@ func (w *Field) SetSize(c Coord) { } func (w *Field) Focusable() bool { return w.focusable } func (w *Field) SetFocusable(b bool) { w.focusable = b } -func (w *Field) MinW() int { - return len(w.label) + len(w.value) -} -func (w *Field) MinH() int { return 1 } +func (w *Field) MinW() int { return len(w.label) + len(w.value) } +func (w *Field) MinH() int { return 1 } /* Non-Widget-Interface Functions */ func (w *Field) handleBackspace(_ *tcell.EventKey) bool { diff --git a/wdgt_textarea_field.go b/wdgt_textarea_field.go new file mode 100644 index 0000000..0af6a2f --- /dev/null +++ b/wdgt_textarea_field.go @@ -0,0 +1,338 @@ +/* +Copyright © Brian Buller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package widgets + +import ( + "fmt" + "strings" + + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" + "github.com/gdamore/tcell" +) + +type TextAreaField struct { + id string + style tcell.Style + + label string + value string + + overwriteMode bool + + cursor int + visible bool + active bool + focusable bool + x, y int + w, h int + + filter func(tcell.EventKey) bool + onChange func(prev, curr string) + + keyMap *KeyMap +} + +var _ Widget = (*TextAreaField)(nil) + +func NewTextAreaField(id string, style tcell.Style) *TextAreaField { + f := &TextAreaField{} + f.Init(id, style) + return f +} + +func (w *TextAreaField) Init(id string, style tcell.Style) { + w.id = id + w.style = style + w.visible = true + w.filter = func(ev tcell.EventKey) bool { + return wh.IsBS(ev) || + wh.KeyIsDisplayable(ev) || + wh.IsKey(tcell.KeyEnter) + } + w.keyMap = NewKeyMap( + NewKey(BuildEK(tcell.KeyLeft), w.handleLeft), + NewKey(BuildEK(tcell.KeyRight), w.handleRight), + NewKey(BuildEK(tcell.KeyUp), w.handleUp), + NewKey(BuildEK(tcell.KeyDown), w.handleDown), + NewKey(BuildEK(tcell.KeyHome), w.handleHome), + NewKey(BuildEK(tcell.KeyEnd), w.handleEnd), + NewKey(BuildEK(tcell.KeyCtrlU), w.clearValueBeforeCursor), + ) + w.focusable = true +} + +func (w *TextAreaField) Id() string { return w.id } +func (w *TextAreaField) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() } + +func (w *TextAreaField) GetKeyMap() *KeyMap { return w.keyMap } +func (w *TextAreaField) SetKeyMap(km *KeyMap) { w.keyMap = km } + +func (w *TextAreaField) HandleKey(ev *tcell.EventKey) bool { + if !w.active { + return false + } + if wh.IsBS(*ev) { + if w.overwriteMode { + w.cursor-- + return true + } + return w.handleBackspace(ev) + } + if wh.IsKey(tcell.KeyEnter) { + return w.handleEnter(ev) + } + if ev.Key() == tcell.KeyDelete { + if w.cursor < len(w.value) { + w.cursor++ + return w.handleBackspace(ev) + } + } + if w.keyMap.Handle(ev) { + return true + } + if w.filter != nil && !w.filter(*ev) { + return false + } + if ev.Key() == tcell.KeyRune { + var val string + if w.overwriteMode { + if w.value[w.cursor] == '\n' { + // At a new line, we just insert + val = fmt.Sprintf("%s%s\n", w.value[:w.cursor-1], string(ev.Rune())) + } else { + val = fmt.Sprintf("%s%s", w.value[:w.cursor], string(ev.Rune())) + } + if len(w.value) > w.cursor+1 { + val = fmt.Sprintf("%s%s", val, w.value[w.cursor+1:]) + } + } else { + val = fmt.Sprintf("%s%s%s", w.value[:w.cursor], string(ev.Rune()), w.value[w.cursor:]) + } + w.SetValue(val) + w.cursor++ + return true + } + return false +} +func (w *TextAreaField) HandleTime(ev *tcell.EventTime) {} +func (w *TextAreaField) Draw(screen tcell.Screen) { + if !w.visible { + return + } + useStyle := w.style.Dim(!w.active) + valueStyle := useStyle.Bold(w.active) + x, y := w.x, w.y + labelW := len(w.label) + if labelW > 0 { + wh.DrawText(w.x, w.y, w.label+": ", useStyle, screen) + x = x + labelW + 2 + y++ + } + lines := strings.Split(w.value, "\n") + var byteCount int + for li := range lines { + ll := len(lines[li]) + if w.cursor >= byteCount && w.cursor < byteCount+ll { + // Cursor is on this line + crsPos := w.cursor - byteCount + cursor := " " + var pre, post string + if ll > 0 { + pre = lines[li][:crsPos] + if crsPos < ll { + cursor = string(lines[li][crsPos]) + post = lines[li][crsPos+1:] + } + } + wh.DrawText(x, y, pre, valueStyle, screen) + x += len(pre) + if w.active { + wh.DrawText(x, y, cursor, valueStyle.Reverse(true).Blink(true), screen) + } else { + wh.DrawText(x, y, cursor, valueStyle, screen) + } + x += 1 + wh.DrawText(x, y, post, valueStyle, screen) + } else { + // Cursor is not on this line + wh.DrawText(x, y, w.value[i], valueStyle, screen) + } + byteCount += ll + y++ + x = w.x + } +} + +func (w *TextAreaField) SetStyle(s tcell.Style) { w.style = s } +func (w *TextAreaField) Active() bool { return w.active } +func (w *TextAreaField) SetActive(a bool) bool { + w.active = a + return w.active +} +func (w *TextAreaField) Visible() bool { return w.visible } +func (w *TextAreaField) SetVisible(a bool) { w.visible = a } +func (w *TextAreaField) SetX(x int) { w.x = x } +func (w *TextAreaField) SetY(y int) { w.y = y } +func (w *TextAreaField) GetX() int { return w.x } +func (w *TextAreaField) GetY() int { return w.y } +func (w *TextAreaField) GetPos() Coord { return Coord{X: w.x, Y: w.y} } +func (w *TextAreaField) SetPos(c Coord) { w.x, w.y = c.X, c.Y } +func (w *TextAreaField) SetW(wd int) { w.w = wd } +func (w *TextAreaField) SetH(h int) { w.h = h } +func (w *TextAreaField) GetW() int { return w.w } +func (w *TextAreaField) GetH() int { return w.h } +func (w *TextAreaField) WantW() int { + vM := wh.Max(40, len(w.value)) + if len(w.label) > 0 { + return len(w.label) + vM + 3 + } + return vM +} + +func (w *TextAreaField) WantH() int { return 1 } +func (w *TextAreaField) SetSize(c Coord) { + w.SetW(c.X) + w.SetH(c.Y) +} +func (w *TextAreaField) Focusable() bool { return w.focusable } +func (w *TextAreaField) SetFocusable(b bool) { w.focusable = b } +func (w *TextAreaField) MinW() int { return len(w.label) + len(w.value) } +func (w *TextAreaField) MinH() int { return 1 } + +/* Non-Widget-Interface Functions */ +func (w *TextAreaField) handleEnter(_ *tcell.EventKey) bool { + st := w.cursor + w.SetValue(w.value[:w.cursor] + "\n" + w.value[w.cursor+1:]) + return true +} +func (w *TextAreaField) handleBackspace(_ *tcell.EventKey) bool { + st := w.cursor + if w.cursor > 0 { + w.cursor-- + } + if w.cursor < len(w.value) { + w.SetValue(w.value[:w.cursor] + w.value[w.cursor+1:]) + } + if st != w.cursor { + return true + } + return false +} + +func (w *TextAreaField) getLine(n int) (string, error) { + if n < 0 { + return "", errors.New("invalid line specified") + } + pts := strings.Split(w.value, "\n") + if len( +} + +func (w *TextAreaField) getCursorPosOnLine() (int, string) { + pts := strings.Split(w.value, "\n") + var wrk int + for i := range pts { + if w.cursor < wrk+len(pts[i]) { + return w.cursor - wrk, pts[i] + } + } + return -1, "" +} + +func (w *TextAreaField) handleUp(ev *tcell.EventKey) bool { + // TODO: Split up value into lines, find current line, find equivilant 'x' on previous line + pos, ln := w.getCursorPosOnLine() + if pos == w.cursor { + w.cursor = 0 + return true + } + + return false +} +func (w *TextAreaField) handleDown(ev *tcell.EventKey) bool { + // TODO: Split up value into lines, find current line, find equivilant 'x' on next line + return false +} + +func (w *TextAreaField) handleLeft(ev *tcell.EventKey) bool { + // TODO: Split up value into lines, find current line, if pos on line is 0, go to end of previous line + return false +} + +func (w *TextAreaField) handleRight(ev *tcell.EventKey) bool { + // TODO: Split up value into lines, find current line, if pos on line is end, go to beg of next line + return false +} + +func (w *TextAreaField) clearValueBeforeCursor(ev *tcell.EventKey) bool { + // TODO: Split up value into lines, find current line, clear before cursor + return true +} + +func (w *TextAreaField) handleHome(ev *tcell.EventKey) bool { + // TODO: Split up value into lines, find current line, go to beg of it + w.cursorX = 0 + return true +} + +func (w *TextAreaField) handleEnd(ev *tcell.EventKey) bool { + // TODO: Split up value into lines, find current line, go to end of it + w.cursorX = len(w.value[w.cursorY]) + return true +} + +func (w *TextAreaField) SetLabel(l string) { w.label = l } +func (w *TextAreaField) Label() string { return w.label } +func (w *TextAreaField) SetValue(v string) { + prev := w.value + w.value = v + w.doOnChange(prev, v) + if w.cursor > len(v) { + w.cursor = len(v) + } + if w.cursor < 0 { + w.cursor = 0 + } +} +func (w *TextAreaField) Value() string { return w.value } + +func (w *TextAreaField) SetOnChange(v func(prev, curr string)) { w.onChange = v } +func (w *TextAreaField) doOnChange(prev, curr string) { + if w.onChange != nil { + w.onChange(prev, curr) + } +} + +func (w *TextAreaField) SetFilter(f func(ev tcell.EventKey) bool) { + w.filter = func(ev tcell.EventKey) bool { + // We always want to make sure we allow backspace. + if wh.IsBS(ev) || wh.IsKey(tcell.KeyEnter) { + return true + } + + // We also always want to make sure it's displayable + if !wh.KeyIsDisplayable(ev) { + return false + } + return f(ev) + } +} +func (w *TextAreaField) SetOverwrite(b bool) { w.overwriteMode = b }