From 998531f95d732fb1cc47031fc93d8e1e1e59be9d Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Wed, 1 Oct 2025 15:37:31 -0500 Subject: [PATCH] Added Datepicker Some other work too --- wdgt_bordered.go | 57 +++++++---- wdgt_datepicker.go | 216 ++++++++++++++++++++++++++++++++++++++++++ wdgt_form.go | 69 ++++++++++++-- wdgt_linear_layout.go | 35 ++++++- 4 files changed, 343 insertions(+), 34 deletions(-) create mode 100644 wdgt_datepicker.go diff --git a/wdgt_bordered.go b/wdgt_bordered.go index 37c6cd9..e964034 100644 --- a/wdgt_bordered.go +++ b/wdgt_bordered.go @@ -22,6 +22,9 @@ THE SOFTWARE. package widgets import ( + "fmt" + "time" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) @@ -36,12 +39,10 @@ type BorderedWidget struct { widget Widget border []rune - title string - active bool - visible bool - focusable bool + title string // The 'title' string is over the top of the border + help string // The 'help' string is over the bottom of the border - logger func(string) + logger func(string, ...any) } var _ Widget = (*BorderedWidget)(nil) @@ -55,9 +56,9 @@ func NewBorderedWidget(id string, s tcell.Style, wd Widget) *BorderedWidget { func (w *BorderedWidget) Init(id string, s tcell.Style) { w.id = id w.style = s - w.visible = true + w.widget.SetVisible(true) w.border = wh.BRD_CSIMPLE - w.focusable = true + w.widget.SetFocusable(true) } func (w *BorderedWidget) Id() string { return w.id } @@ -68,15 +69,18 @@ func (w *BorderedWidget) HandleResize(ev *tcell.EventResize) { w.widget.HandleResize(tcell.NewEventResize(w.w-2, w.h-2)) } -func (w *BorderedWidget) SetKeyMap(km KeyMap) { w.widget.SetKeyMap(km) } -func (w *BorderedWidget) AddToKeyMap(km KeyMap) { w.widget.AddToKeyMap(km) } -func (w *BorderedWidget) RemoveFromKeyMap(km KeyMap) { w.widget.RemoveFromKeyMap(km) } -func (w *BorderedWidget) HandleKey(ev *tcell.EventKey) bool { return w.HandleKey(ev) } +func (w *BorderedWidget) SetKeyMap(km KeyMap) { w.widget.SetKeyMap(km) } +func (w *BorderedWidget) AddToKeyMap(km KeyMap) { w.widget.AddToKeyMap(km) } +func (w *BorderedWidget) RemoveFromKeyMap(km KeyMap) { w.widget.RemoveFromKeyMap(km) } +func (w *BorderedWidget) HandleKey(ev *tcell.EventKey) bool { + w.Log("BW(%s) Active(%s) Handlekey", w.Id(), w.widget.Id()) + return w.widget.HandleKey(ev) +} func (w *BorderedWidget) HandleTime(ev *tcell.EventTime) { w.widget.HandleTime(ev) } func (w *BorderedWidget) Draw(screen tcell.Screen) { - if !w.visible { + if !w.widget.Visible() { return } if len(w.title) > 0 { @@ -84,15 +88,18 @@ func (w *BorderedWidget) Draw(screen tcell.Screen) { } else { wh.BorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.border, w.style, screen) } - w.GetPos().DrawOffset(w, screen) + if len(w.help) > 0 { + wh.DrawText(w.x+w.w-len(w.help)-2, w.y+w.h, w.help, w.style, screen) + } + w.GetPos().DrawOffset(w.widget, screen) } -func (w *BorderedWidget) Active() bool { return w.active } -func (w *BorderedWidget) SetActive(a bool) { w.active = a } -func (w *BorderedWidget) Visible() bool { return w.visible } -func (w *BorderedWidget) SetVisible(a bool) { w.visible = a } -func (w *BorderedWidget) Focusable() bool { return w.focusable } -func (w *BorderedWidget) SetFocusable(b bool) { w.focusable = b } +func (w *BorderedWidget) Active() bool { return w.widget.Active() } +func (w *BorderedWidget) SetActive(a bool) { w.widget.SetActive(a) } +func (w *BorderedWidget) Visible() bool { return w.widget.Visible() } +func (w *BorderedWidget) SetVisible(a bool) { w.SetVisible(a) } +func (w *BorderedWidget) Focusable() bool { return w.widget.Focusable() } +func (w *BorderedWidget) SetFocusable(b bool) { w.widget.SetFocusable(b) } func (w *BorderedWidget) SetX(x int) { w.x = x } func (w *BorderedWidget) SetY(y int) { w.y = y } func (w *BorderedWidget) GetX() int { return w.x } @@ -109,5 +116,13 @@ func (w *BorderedWidget) WantH() int { return w.h } func (w *BorderedWidget) MinW() int { return 2 + w.widget.MinW() } func (w *BorderedWidget) MinH() int { return 2 + w.widget.MinH() } -func (w *BorderedWidget) SetBorder(r []rune) { w.border = r } -func (w *BorderedWidget) SetTitle(ttl string) { w.title = ttl } +func (w *BorderedWidget) SetBorder(r []rune) { w.border = r } +func (w *BorderedWidget) SetTitle(ttl string) { w.title = ttl } +func (w *BorderedWidget) SetHelp(hlp string) { w.help = hlp } +func (w *BorderedWidget) SetLogger(l func(string, ...any)) { w.logger = l } +func (w *BorderedWidget) Log(txt string, args ...any) { + if w.logger != nil { + txt = fmt.Sprintf("%s:%s", time.Now().Format(time.TimeOnly), txt) + w.logger(txt, args...) + } +} diff --git a/wdgt_datepicker.go b/wdgt_datepicker.go new file mode 100644 index 0000000..d31a3bf --- /dev/null +++ b/wdgt_datepicker.go @@ -0,0 +1,216 @@ +/* +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" + "time" + + "github.com/gdamore/tcell" +) + +type DatePicker struct { + id string + style tcell.Style + + x, y int + w, h int + lookActive bool + active bool + visible bool + focusable bool + + date time.Time + format string + + dateFld *Field + dateNow *Button + + keyMap KeyMap + + logger func(string, ...any) +} + +var _ Widget = (*DatePicker)(nil) + +func NewDatePicker(id string, s tcell.Style) *DatePicker { + ret := &DatePicker{} + ret.Init(id, s) + return ret +} + +func (w *DatePicker) Init(id string, s tcell.Style) { + w.id = id + w.style = s + w.visible = true + w.focusable = true + + w.format = time.RFC3339 + + w.dateFld = NewField(fmt.Sprintf("%s-date", id), s) + w.dateFld.SetValue(w.date.Format(w.format)) + w.dateFld.SetH(1) + + w.dateNow = NewButton(fmt.Sprintf("%s-now", id), s) + w.dateNow.SetLabel("Now") + w.dateNow.SetOnPressed(func() bool { + w.SetValue(time.Now()) + return true + }) + w.dateNow.SetW(5) + w.dateNow.SetH(1) + + w.initKeyMap() +} + +func (w *DatePicker) Id() string { return w.id } +func (w *DatePicker) HandleResize(ev *tcell.EventResize) { + wd, ht := ev.Size() + w.SetW(wd) + w.SetH(ht) + if wd >= w.WantW() { + w.dateFld.HandleResize(Coord{X: w.dateFld.WantW(), Y: ht}.ResizeEvent()) + w.dateNow.HandleResize(Coord{X: w.dateNow.WantW(), Y: ht}.ResizeEvent()) + } + w.dateFld.SetPos(Coord{X: 0, Y: 0}) + w.dateNow.SetPos(Coord{X: wd - 6, Y: 0}) +} + +func (w *DatePicker) SetKeyMap(km KeyMap) { w.keyMap = km } +func (w *DatePicker) AddToKeyMap(km KeyMap) { w.keyMap.Merge(km) } +func (w *DatePicker) RemoveFromKeyMap(km KeyMap) { + for k := range km.Keys { + w.keyMap.Remove(k) + } + for r := range km.Runes { + w.keyMap.RemoveRune(r) + } +} + +func (w *DatePicker) HandleKey(ev *tcell.EventKey) bool { + if !w.active { + return false + } + if ev.Key() == tcell.KeyTab { + if w.dateFld.Active() { + w.dateFld.SetActive(false) + w.dateNow.SetActive(true) + return true + } else if w.dateNow.Active() { + w.dateFld.SetActive(false) + w.dateNow.SetActive(false) + return false + } + return false + } + if w.dateFld.Active() { + if w.dateFld.HandleKey(ev) { + // Date Field updated, see if we can parse it. + dt, err := time.Parse(w.format, w.dateFld.Value()) + if err == nil { + w.date = dt + } + } + } else if w.dateNow.Active() { + return w.dateNow.HandleKey(ev) + } + return w.keyMap.Handle(ev) +} +func (w *DatePicker) HandleTime(ev *tcell.EventTime) {} +func (w *DatePicker) Draw(screen tcell.Screen) { + if !w.visible { + return + } + + w.GetPos().DrawOffset(w.dateFld, screen) + w.GetPos().DrawOffset(w.dateNow, screen) +} + +func (w *DatePicker) Active() bool { return w.active } +func (w *DatePicker) SetActive(a bool) { + if !w.active && a { + // Wasn't active, but turning on + w.dateFld.SetActive(true) + } + w.active = a +} +func (w *DatePicker) Visible() bool { return w.visible } +func (w *DatePicker) SetVisible(a bool) { w.visible = a } +func (w *DatePicker) Focusable() bool { return w.focusable } +func (w *DatePicker) SetFocusable(b bool) { w.focusable = b } +func (w *DatePicker) SetX(x int) { w.x = x } +func (w *DatePicker) SetY(y int) { w.y = y } +func (w *DatePicker) GetX() int { return w.x } +func (w *DatePicker) GetY() int { return w.y } +func (w *DatePicker) GetPos() Coord { return Coord{X: w.x, Y: w.y} } +func (w *DatePicker) SetPos(c Coord) { w.x, w.y = c.X, c.Y } +func (w *DatePicker) GetW() int { return w.w } +func (w *DatePicker) GetH() int { return w.h } +func (w *DatePicker) SetW(wd int) { w.w = wd } +func (w *DatePicker) SetH(h int) { w.h = h } +func (w *DatePicker) SetSize(c Coord) { + w.SetW(c.X) + w.SetH(c.Y) +} + +func (w *DatePicker) WantW() int { + return w.dateFld.WantW() + w.dateNow.WantW() + 2 +} + +func (w *DatePicker) WantH() int { + return 1 +} +func (w *DatePicker) MinW() int { return w.dateFld.MinW() + w.dateNow.MinW() + 2 } +func (w *DatePicker) MinH() int { return 1 } + +func (w *DatePicker) updateUI() { + w.dateFld.SetValue(w.date.Format(w.format)) + w.dateFld.SetSize(Coord{X: len(w.dateFld.Label()) + len(w.format) + 3, Y: 1}) + w.dateNow.SetSize(Coord{X: 5, Y: 1}) +} + +func (w *DatePicker) initKeyMap() { + w.keyMap = NewKeyMap(map[tcell.Key]func(ev *tcell.EventKey) bool{}) +} + +func (w *DatePicker) Format() string { return w.format } +func (w *DatePicker) SetFormat(fmt string) { + w.format = fmt + w.updateUI() +} +func (w *DatePicker) Label() string { return w.dateFld.Label() } +func (w *DatePicker) SetLabel(lbl string) { + w.dateFld.SetLabel(lbl) + w.updateUI() +} +func (w *DatePicker) Value() time.Time { return w.date } +func (w *DatePicker) SetValue(t time.Time) { + w.date = t + w.updateUI() +} + +func (w *DatePicker) SetLogger(l func(string, ...any)) { w.logger = l } +func (w *DatePicker) Log(t string, a ...any) { + if w.logger != nil { + w.logger(t, a...) + } +} diff --git a/wdgt_form.go b/wdgt_form.go index 4ad5e73..f05f366 100644 --- a/wdgt_form.go +++ b/wdgt_form.go @@ -19,6 +19,8 @@ THE SOFTWARE. package widgets import ( + "fmt" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) @@ -59,6 +61,8 @@ func (w *Form) Init(id string, style tcell.Style) { w.style = style w.visible = true w.focusable = true + w.submit = NewButton(fmt.Sprintf("%s-submit", id), style) + w.cancel = NewButton(fmt.Sprintf("%s-cancel", id), style) } func (w *Form) Id() string { return w.id } @@ -67,7 +71,9 @@ func (w *Form) HandleResize(ev *tcell.EventResize) { y := 0 for i := range w.fields { w.fields[i].SetPos(Coord{X: 0, Y: y}) - y += w.fields[i].WantH() + wantH := w.fields[i].WantH() + w.fields[i].HandleResize(Coord{X: w.w, Y: wantH}.ResizeEvent()) + y += wantH } half := w.w / 2 if w.hasCancel && w.hasSubmit { @@ -99,17 +105,21 @@ func (w *Form) HandleKey(ev *tcell.EventKey) bool { if !w.active { return false } + if w.getActive().HandleKey(ev) { + return true + } if ev.Key() == tcell.KeyTab { fldCnt := len(w.fields) - num := fldCnt if w.hasCancel { - num += 1 + fldCnt += 1 } if w.hasSubmit { - num += 1 - } - if num < fldCnt { + fldCnt += 1 } + pre := w.cursor + w.cursor = (w.cursor + 1) % fldCnt + w.updateWidgets() + return w.cursor > pre } return false } @@ -126,7 +136,14 @@ func (w *Form) Draw(screen tcell.Screen) { } p := w.GetPos() for _, wdgt := range w.fields { - p.DrawOffset(wdgt, screen) + if !w.Active() { + wdAct := wdgt.Active() + wdgt.SetActive(false) + p.DrawOffset(wdgt, screen) + wdgt.SetActive(wdAct) + } else { + p.DrawOffset(wdgt, screen) + } } if w.hasCancel { p.DrawOffset(w.cancel, screen) @@ -136,8 +153,11 @@ func (w *Form) Draw(screen tcell.Screen) { } } -func (w *Form) Active() bool { return w.active } -func (w *Form) SetActive(a bool) { w.active = a } +func (w *Form) Active() bool { return w.active } +func (w *Form) SetActive(a bool) { + w.active = a + w.updateWidgets() +} 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 } @@ -186,6 +206,37 @@ func (w *Form) MinW() int { return 1 } func (w *Form) MinH() int { return 1 } // Non-Widget Functions +func (w *Form) updateWidgets() { + for i := 0; i < len(w.fields); i++ { + w.fields[i].SetActive(w.Active() && i == w.cursor) + } + if w.hasCancel { + w.cancel.SetActive(w.Active() && w.cursor == len(w.fields)) + if w.hasSubmit { + w.submit.SetActive(w.Active() && w.cursor == len(w.fields)+1) + } + } else if w.hasSubmit { + w.submit.SetActive(w.Active() && w.cursor == len(w.fields)) + } +} + +func (w *Form) getActive() Widget { + if w.cursor < len(w.fields) { + return w.fields[w.cursor] + } + if w.cursor == len(w.fields) { + if w.hasCancel { + return w.cancel + } + if w.hasSubmit { + return w.submit + } + } else if w.cursor > len(w.fields) && w.hasSubmit { + return w.submit + } + return nil +} + func (w *Form) IndexOf(n Widget) int { for i := range w.fields { if w.fields[i] == n { diff --git a/wdgt_linear_layout.go b/wdgt_linear_layout.go index 9d70982..a05736b 100644 --- a/wdgt_linear_layout.go +++ b/wdgt_linear_layout.go @@ -88,14 +88,15 @@ func (w *LinearLayout) Init(id string, s tcell.Style) { w.keyMap.Add(tcell.KeyTab, func(ev *tcell.EventKey) bool { active := w.findActive() if active == nil && len(w.widgets) > 0 { - // No widget is active + // No widget is active, but we do have some + if w.widgets[0].Focusable() { w.widgets[0].SetActive(true) return true } return false } - return w.activateNext() + return w.ActivateNext() }) } @@ -122,13 +123,16 @@ func (w *LinearLayout) HandleKey(ev *tcell.EventKey) bool { } active := w.findActive() if active != nil { + w.Log("LL(%s) Active(%s) Handlekey", w.Id(), active.Id()) if active.HandleKey(ev) { return true } } return w.keyMap.Handle(ev) } -func (w *LinearLayout) GetActive() Widget { return w.findActive() } + +func (w *LinearLayout) GetActiveWidgetIdx() int { return w.findActiveIdx() } +func (w *LinearLayout) GetActiveWidget() Widget { return w.findActive() } func (w *LinearLayout) HandleTime(ev *tcell.EventTime) { for _, wi := range w.widgets { @@ -242,6 +246,15 @@ func (w *LinearLayout) MinH() int { } // Find the currently active widget, there should be only one. +func (w *LinearLayout) findActiveIdx() int { + for i := range w.widgets { + if w.widgets[i].Active() { + return i + } + } + return -1 +} + func (w *LinearLayout) findActive() Widget { for i := range w.widgets { if w.widgets[i].Active() { @@ -262,7 +275,7 @@ func (w *LinearLayout) findActiveOrFirst() Widget { return nil } -func (w *LinearLayout) activateNext() bool { +func (w *LinearLayout) ActivateNext() bool { var found bool for i := range w.widgets { if found && w.widgets[i].Focusable() { @@ -276,6 +289,20 @@ func (w *LinearLayout) activateNext() bool { return false } +func (w *LinearLayout) ActivatePrev() bool { + var found bool + for i := len(w.widgets) - 1; i >= 0; i-- { + if found && w.widgets[i].Focusable() { + w.widgets[i].SetActive(true) + return true + } else if w.widgets[i].Active() { + found = true + w.widgets[i].SetActive(false) + } + } + return false +} + func (w *LinearLayout) SetOrientation(o LinearLayoutOrient) { w.orientation = o } func (w *LinearLayout) IndexOf(n Widget) int { for i := range w.widgets {