From 10bdd958d4888dd6a9352e4c82047d1642267dbc Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Mon, 8 Sep 2025 21:18:22 -0500 Subject: [PATCH] Menu tweaks Spinner? --- wdgt_menu.go | 38 ++++++++++++++++- wdgt_menu_item.go | 50 ++++++++++++++++------ wdgt_spinner.go | 106 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 16 deletions(-) create mode 100644 wdgt_spinner.go diff --git a/wdgt_menu.go b/wdgt_menu.go index a0d15ea..70005c6 100644 --- a/wdgt_menu.go +++ b/wdgt_menu.go @@ -86,7 +86,41 @@ func (w *Menu) Init(id string, style tcell.Style) { func (w *Menu) Id() string { return w.id } func (w *Menu) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() - // TODO: Trickle-down HandleResize + switch w.menuType { + case MenuTypeH: + w.handleResizeH(ev) + case MenuTypeV: + w.handleResizeV(ev) + } +} + +func (w *Menu) handleResizeH(_ *tcell.EventResize) { + x := 0 + for i := range w.items { + lblW := len(w.items[i].Label()) + c := Coord{X: lblW + 2, Y: 1} + if w.items[i].Expanded() { + c.X, c.Y = w.items[i].WantW(), w.items[i].WantH() + } + w.items[i].HandleResize(c.ResizeEvent()) + // TODO: Make sure we don't overflow the passed Event + w.items[i].SetPos(Coord{X: lblW + 2, Y: 0}) + x += lblW + 2 + } +} + +func (w *Menu) handleResizeV(_ *tcell.EventResize) { + y := 0 + for i := range w.items { + c := Coord{X: 0, Y: y} + if w.items[i].Expanded() { + c.X, c.Y = w.items[i].WantW(), w.items[i].WantH() + } + w.items[i].HandleResize(c.ResizeEvent()) + // TODO: Make sure we don't overflow the passed Event + w.items[i].SetPos(Coord{X: 0, Y: y}) + y++ + } } func (w *Menu) SetKeyMap(km KeyMap) { w.keyMap = km } @@ -163,7 +197,7 @@ func (w *Menu) drawVMenu(screen tcell.Screen) { st := w.style if w.active { st = w.style.Reverse(true) - wh.TitledBorderFilled(x-1, y, x+w.WantW(), y+w.WantH(), w.label, wh.BRD_CSIMPLE, w.style, screen) + wh.BorderFilled(x-1, y+1, x+w.WantW(), y+1+w.WantH(), wh.BRD_CSIMPLE, w.style, screen) } wh.DrawText(w.x, w.y, w.label, st, screen) if w.expanded || (w.active && !w.manualExpand) { diff --git a/wdgt_menu_item.go b/wdgt_menu_item.go index d413dff..969a607 100644 --- a/wdgt_menu_item.go +++ b/wdgt_menu_item.go @@ -22,6 +22,9 @@ THE SOFTWARE. package widgets import ( + "fmt" + "strings" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) @@ -36,7 +39,7 @@ type MenuItem struct { x, y int w, h int - menuType MenuType + menuType MenuType // TODO: Handle Horizontal cursor int items []*MenuItem onPressed func() bool @@ -112,22 +115,33 @@ func (w *MenuItem) Draw(screen tcell.Screen) { if !w.visible { return } - st := w.style.Reverse(w.active).Dim(w.disabled).Italic(w.disabled) + st := w.style.Dim(w.disabled).Italic(w.disabled) + // st := w.style.Reverse(w.active).Dim(w.disabled).Italic(w.disabled) x, y := w.x, w.y - wd := w.w - wh.DrawText(x, y, wh.PadR(w.label, wd), st, screen) - y += 1 + // wd := w.w if w.expanded { if len(w.items) > 0 { - wh.TitledBorderFilled(w.x-1, w.y, w.x+w.WantW(), w.y+w.WantH(), w.label, wh.BRD_CSIMPLE, w.style, screen) - } - x += 1 - for i := range w.items { - // TODO: Use DrawOffset - w.items[i].SetPos(Coord{X: x, Y: y}) - w.items[i].Draw(screen) - y++ + wh.DrawText(x, y, fmt.Sprintf("╭%s╮", w.label), st, screen) + wh.DrawText(x+1, y, fmt.Sprintf("%s", w.label), st.Reverse(true), screen) + y += 1 + if len(w.items) > 0 { + wh.TitledBorderFilled(w.x-1, y, w.x+w.WantW(), y+w.WantH(), fmt.Sprintf("╯%s╰", strings.Repeat(" ", len(w.label))), wh.BRD_CSIMPLE, w.style, screen) + } + x += 1 + y += 1 + // pos := w.GetPos() + for i := range w.items { + // TODO: Use DrawOffset + // pos.DrawOffset(w.items[i], screen) + w.items[i].SetPos(Coord{X: x, Y: y}) + w.items[i].Draw(screen) + y++ + } + } else { + wh.DrawText(x, y, fmt.Sprintf(" %s ", w.label), st.Reverse(true), screen) } + } else { + wh.DrawText(x, y, fmt.Sprintf(" %s ", w.label), st, screen) } } @@ -157,7 +171,7 @@ func (w *MenuItem) WantW() int { if len(w.items) > 0 { for i := range w.items { if w.items[i].WantW() > ret { - ret = w.items[i].WantW() + 1 + ret = w.items[i].WantW() + 2 // TODO: Figure offset of subitems } } @@ -184,6 +198,7 @@ func (w *MenuItem) Expand(e bool) { w.expanded = e } } +func (w *MenuItem) Expanded() bool { return w.expanded } func (w *MenuItem) updateActive() { for i := range w.items { @@ -192,6 +207,9 @@ func (w *MenuItem) updateActive() { } func (w *MenuItem) MoveUp(ev *tcell.EventKey) bool { + if len(w.items) == 0 { + return false + } // Look for a previous enabled item st := w.cursor i := (w.cursor - 1 + len(w.items)) % len(w.items) @@ -206,6 +224,9 @@ func (w *MenuItem) MoveUp(ev *tcell.EventKey) bool { } func (w *MenuItem) MoveDown(ev *tcell.EventKey) bool { + if len(w.items) == 0 { + return false + } // Look for next enabled item st := w.cursor i := (st + 1) % len(w.items) @@ -218,6 +239,7 @@ func (w *MenuItem) MoveDown(ev *tcell.EventKey) bool { w.updateActive() return true } +func (w *MenuItem) Label() string { return w.label } func (w *MenuItem) SetLabel(lbl string) { w.label = lbl } func (w *MenuItem) SetDisabled(d bool) { w.disabled = d } func (w *MenuItem) IsDisabled() bool { return w.disabled } diff --git a/wdgt_spinner.go b/wdgt_spinner.go new file mode 100644 index 0000000..e0feaa0 --- /dev/null +++ b/wdgt_spinner.go @@ -0,0 +1,106 @@ +/* +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 ( + "time" + + "github.com/gdamore/tcell" +) + +// Spinner is just blank. It's a good placeholder, if needed. +type Spinner struct { + id string + x, y int + style tcell.Style + currentFrame int + frames []rune + lastTick time.Time + tickInterval time.Duration + keyMap KeyMap +} + +var _ Widget = (*Spinner)(nil) + +func NewSpinner(id string, style tcell.Style) *Spinner { + ret := &Spinner{id: id} + ret.Init(id, style) + return ret +} + +func (w *Spinner) Init(id string, st tcell.Style) { + w.frames = []rune{'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'} + w.tickInterval = time.Second + w.keyMap = BlankKeyMap() +} +func (w *Spinner) Id() string { return w.id } +func (w *Spinner) HandleResize(ev *tcell.EventResize) {} +func (w *Spinner) SetKeyMap(km KeyMap) { w.keyMap = km } +func (w *Spinner) AddToKeyMap(km KeyMap) { w.keyMap.Merge(km) } +func (w *Spinner) RemoveFromKeyMap(km KeyMap) { + for k := range km.Keys { + w.keyMap.Remove(k) + } + for r := range km.Runes { + w.keyMap.RemoveRune(r) + } +} +func (w *Spinner) HandleKey(ev *tcell.EventKey) bool { return false } +func (w *Spinner) HandleTime(ev *tcell.EventTime) { + for w.lastTick.Before(ev.When()) { + w.lastTick = w.lastTick.Add(w.tickInterval) + w.currentFrame = (w.currentFrame + 1) % len(w.frames) + } +} + +func (w *Spinner) Draw(screen tcell.Screen) { + screen.SetContent(w.x, w.y, w.frames[w.currentFrame], nil, w.style) +} + +func (w *Spinner) Active() bool { return false } +func (w *Spinner) SetActive(a bool) {} +func (w *Spinner) Visible() bool { return true } +func (w *Spinner) SetVisible(v bool) {} +func (w *Spinner) Focusable() bool { return false } +func (w *Spinner) SetFocusable(t bool) {} +func (w *Spinner) SetX(x int) {} +func (w *Spinner) SetY(y int) {} +func (w *Spinner) GetX() int { return w.x } +func (w *Spinner) GetY() int { return w.y } +func (w *Spinner) GetPos() Coord { return Coord{X: w.x, Y: w.y} } +func (w *Spinner) SetPos(pos Coord) {} +func (w *Spinner) SetSize(size Coord) {} +func (w *Spinner) SetW(wd int) {} +func (w *Spinner) SetH(h int) {} +func (w *Spinner) GetW() int { return 0 } +func (w *Spinner) GetH() int { return 0 } +func (w *Spinner) WantW() int { return 0 } +func (w *Spinner) WantH() int { return 0 } +func (w *Spinner) MinW() int { return 0 } +func (w *Spinner) MinH() int { return 0 } + +func (w *Spinner) SetFrames(frames []rune) { + w.frames = frames + w.currentFrame = 0 +} + +func (w *Spinner) SetTickInterface(d time.Duration) { w.tickInterval = d }