From d3334d73f65b574a92c814780c8235bab41e6fcc Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Thu, 25 Sep 2025 09:49:52 -0500 Subject: [PATCH] Some work --- helpers/number_helpers.go | 28 ++++- wdgt_button.go | 7 +- wdgt_field.go | 33 ++--- wdgt_form.go | 257 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 306 insertions(+), 19 deletions(-) create mode 100644 wdgt_form.go diff --git a/helpers/number_helpers.go b/helpers/number_helpers.go index 3210616..1540d8a 100644 --- a/helpers/number_helpers.go +++ b/helpers/number_helpers.go @@ -21,7 +21,9 @@ THE SOFTWARE. */ package helpers -import "cmp" +import ( + "cmp" +) const ( MaxUint = ^uint(0) @@ -30,6 +32,18 @@ const ( MinInt = -MaxInt - 1 ) +func MaxFor[O interface{}, E cmp.Ordered](g func(O) E, of ...O) O { + var m O + var mE E + for i := range of { + if g(of[i]) > mE { + m = of[i] + mE = g(of[i]) + } + } + return m +} + func Max[E cmp.Ordered](of ...E) E { var m E if len(of) > 0 { @@ -43,6 +57,18 @@ func Max[E cmp.Ordered](of ...E) E { return m } +func MinFor[O interface{}, E cmp.Ordered](g func(O) E, of ...O) O { + var m O + var mE E + for i := range of { + if g(of[i]) < mE { + m = of[i] + mE = g(of[i]) + } + } + return m +} + func Min[E cmp.Ordered](of ...E) E { var m E if len(of) > 0 { diff --git a/wdgt_button.go b/wdgt_button.go index 786ae27..fb4e7a2 100644 --- a/wdgt_button.go +++ b/wdgt_button.go @@ -63,8 +63,10 @@ func (w *Button) Init(id string, style tcell.Style) { w.onPressed = func() bool { return false } w.focusable = true } -func (w *Button) Id() string { return w.id } -func (w *Button) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() } +func (w *Button) Id() string { return w.id } +func (w *Button) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() +} func (w *Button) SetKeyMap(km KeyMap) { w.keyMap = km } func (w *Button) AddToKeyMap(km KeyMap) { w.keyMap.Merge(km) } @@ -113,6 +115,7 @@ func (w *Button) Draw(screen tcell.Screen) { wh.DrawText(w.x, w.y+1, fmt.Sprintf("╰%s╯", strings.Repeat("─", len(lbl)-2)), dStyle, screen) return } + if w.w < 2 { wh.DrawText(w.x, w.y, "╬", dStyle, screen) return diff --git a/wdgt_field.go b/wdgt_field.go index 5166fb7..4f2863b 100644 --- a/wdgt_field.go +++ b/wdgt_field.go @@ -113,10 +113,8 @@ func (w *Field) Draw(screen tcell.Screen) { if !w.visible { return } - useStyle := w.style - if w.active { - useStyle = w.style.Bold(true) - } + useStyle := w.style.Dim(!w.active) + valueStyle := useStyle.Bold(w.active) x := w.x labelW := len(w.label) if labelW > 0 { @@ -133,15 +131,15 @@ func (w *Field) Draw(screen tcell.Screen) { } } - wh.DrawText(x, w.y, pre, useStyle, screen) + wh.DrawText(x, w.y, pre, valueStyle, screen) x += len(pre) if w.active { - wh.DrawText(x, w.y, cursor, useStyle.Reverse(true).Blink(true), screen) + wh.DrawText(x, w.y, cursor, valueStyle.Reverse(true).Blink(true), screen) } else { - wh.DrawText(x, w.y, cursor, useStyle, screen) + wh.DrawText(x, w.y, cursor, valueStyle, screen) } x += 1 - wh.DrawText(x, w.y, post, useStyle, screen) + wh.DrawText(x, w.y, post, valueStyle, screen) } func (w *Field) Active() bool { return w.active } @@ -159,21 +157,24 @@ func (w *Field) SetH(h int) { w.h = h } func (w *Field) GetW() int { return w.w } func (w *Field) GetH() int { return w.h } func (w *Field) WantW() int { + vM := wh.Max(40, len(w.value)) if len(w.label) > 0 { - return len(w.label) + 2 + len(w.value) + 1 - } else { - return len(w.value) + return len(w.label) + vM + 3 } + return vM } -func (w *Field) WantH() int { - return 1 +func (w *Field) WantH() int { return 1 } +func (w *Field) SetSize(c Coord) { + w.SetW(c.X) + w.SetH(c.Y) } -func (w *Field) SetSize(c Coord) { w.w, w.h = c.X, c.Y } 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) + wh.Max(15, 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_form.go b/wdgt_form.go new file mode 100644 index 0000000..4ad5e73 --- /dev/null +++ b/wdgt_form.go @@ -0,0 +1,257 @@ +/* +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 ( + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" + "github.com/gdamore/tcell" +) + +type Form struct { + id string + style tcell.Style + + cursor int + visible bool + active bool + focusable bool + x, y int + w, h int + + title string + border []rune + fields []Widget + + submit *Button + cancel *Button + hasSubmit bool + hasCancel bool + + keyMap KeyMap +} + +var _ Widget = (*Form)(nil) + +func NewForm(id string, style tcell.Style) *Form { + f := &Form{} + f.Init(id, style) + return f +} + +func (w *Form) Init(id string, style tcell.Style) { + w.id = id + w.style = style + w.visible = true + w.focusable = true +} + +func (w *Form) Id() string { return w.id } +func (w *Form) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() + y := 0 + for i := range w.fields { + w.fields[i].SetPos(Coord{X: 0, Y: y}) + y += w.fields[i].WantH() + } + half := w.w / 2 + if w.hasCancel && w.hasSubmit { + w.cancel.SetPos(Coord{X: 0, Y: y}) + w.cancel.SetSize(Coord{X: half, Y: 3}) + w.submit.SetPos(Coord{X: 0, Y: y}) + w.submit.SetSize(Coord{X: half, Y: 3}) + } else if w.hasCancel { + w.cancel.SetPos(Coord{X: half, Y: y}) + w.cancel.SetSize(Coord{X: half, Y: 3}) + } else if w.hasSubmit { + w.submit.SetPos(Coord{X: w.w / 2, Y: y}) + w.submit.SetSize(Coord{X: half, Y: 3}) + } +} + +func (w *Form) SetKeyMap(km KeyMap) { w.keyMap = km } +func (w *Form) AddToKeyMap(km KeyMap) { w.keyMap.Merge(km) } +func (w *Form) RemoveFromKeyMap(km KeyMap) { + for k := range km.Keys { + w.keyMap.Remove(k) + } + for r := range km.Runes { + w.keyMap.RemoveRune(r) + } +} + +func (w *Form) HandleKey(ev *tcell.EventKey) bool { + if !w.active { + return false + } + if ev.Key() == tcell.KeyTab { + fldCnt := len(w.fields) + num := fldCnt + if w.hasCancel { + num += 1 + } + if w.hasSubmit { + num += 1 + } + if num < fldCnt { + } + } + return false +} +func (w *Form) HandleTime(ev *tcell.EventTime) {} +func (w *Form) Draw(screen tcell.Screen) { + if !w.visible { + return + } + useStyle := w.style.Dim(!w.active) + if len(w.title) > 0 { + wh.TitledBorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.title, w.border, useStyle, screen) + } else { + wh.BorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.border, useStyle, screen) + } + p := w.GetPos() + for _, wdgt := range w.fields { + p.DrawOffset(wdgt, screen) + } + if w.hasCancel { + p.DrawOffset(w.cancel, screen) + } + if w.hasSubmit { + p.DrawOffset(w.submit, screen) + } +} + +func (w *Form) Active() bool { return w.active } +func (w *Form) SetActive(a bool) { w.active = a } +func (w *Form) Visible() bool { return w.visible } +func (w *Form) SetVisible(a bool) { w.visible = a } +func (w *Form) SetX(x int) { w.x = x } +func (w *Form) SetY(y int) { w.y = y } +func (w *Form) GetX() int { return w.x } +func (w *Form) GetY() int { return w.y } +func (w *Form) GetPos() Coord { return Coord{X: w.x, Y: w.y} } +func (w *Form) SetPos(c Coord) { w.x, w.y = c.X, c.Y } +func (w *Form) SetW(wd int) { w.w = wd } +func (w *Form) SetH(h int) { w.h = h } +func (w *Form) GetW() int { return w.w } +func (w *Form) GetH() int { return w.h } +func (w *Form) WantW() int { + var ret int + if len(w.border) > 0 { + ret += 2 + } + maxFld := wh.MaxFor(func(f Widget) int { return f.WantW() }, w.fields...) + ret += maxFld.WantW() + + return 1 +} + +func (w *Form) WantH() int { + var ret int + if len(w.border) > 0 { + ret += 2 + } + for i := range w.fields { + ret += w.fields[i].WantH() + } + if w.hasSubmit || w.hasCancel { + ret += 3 + } + + return ret +} + +func (w *Form) SetSize(c Coord) { + w.SetW(c.X) + w.SetH(c.Y) +} +func (w *Form) Focusable() bool { return w.focusable } +func (w *Form) SetFocusable(b bool) { w.focusable = b } +func (w *Form) MinW() int { return 1 } +func (w *Form) MinH() int { return 1 } + +// Non-Widget Functions +func (w *Form) IndexOf(n Widget) int { + for i := range w.fields { + if w.fields[i] == n { + return i + } + } + return -1 +} +func (w *Form) Contains(n Widget) bool { return w.IndexOf(n) >= 0 } +func (w *Form) Replace(n, with Widget) { + idx := w.IndexOf(n) + if idx == -1 { + return + } + w.Delete(n) + w.Insert(with, idx) +} + +func (w *Form) Add(n Widget) { + if w.Contains(n) { + // If the widget is already in the layout, move it to the end + w.Delete(n) + } + w.fields = append(w.fields, n) +} + +func (w *Form) Insert(n Widget, idx int) { + if idx >= len(w.fields) { + w.Add(n) + return + } + if pos := w.IndexOf(n); pos >= 0 { + if pos < idx { + idx-- + } + w.Delete(n) + } + w.fields = append(w.fields[:idx], append([]Widget{n}, w.fields[idx:]...)...) +} + +func (w *Form) Delete(n Widget) { + for i := 0; i < len(w.fields); i++ { + if w.fields[i] == n { + w.DeleteIndex(i) + return + } + } +} + +func (w *Form) DeleteIndex(idx int) { + if idx < len(w.fields) { + w.fields = append(w.fields[:idx], w.fields[idx+1:]...) + } +} +func (w *Form) Clear() { w.fields = []Widget{} } + +func (w *Form) SetTitle(ttl string) { w.title = ttl } +func (w *Form) SetBorder(b []rune) { w.border = b } + +func (w *Form) SetSubmitLabel(lbl string) { w.submit.SetLabel(lbl) } +func (w *Form) SetOnSubmit(sb func() bool) { + w.submit.SetOnPressed(sb) + w.hasSubmit = sb != nil +} +func (w *Form) SetCancelLabel(lbl string) { w.cancel.SetLabel(lbl) } +func (w *Form) SetOnCancel(sb func() bool) { + w.cancel.SetOnPressed(sb) + w.hasCancel = sb != nil +}