From 250e33f8e21fb11775e880cee2175c966bcc05b4 Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Wed, 29 Oct 2025 16:54:38 -0500 Subject: [PATCH] Hopefully making good choices --- coord.go | 5 + layout_flags.go | 8 +- wdgt_linear_layout.go | 8 - wdgt_simple_list.go | 1 + wdgt_stacked_layout.go | 473 +++++++++++++++++++++++++++++++++++++++++ wdgt_text.go | 9 +- 6 files changed, 494 insertions(+), 10 deletions(-) create mode 100644 wdgt_stacked_layout.go diff --git a/coord.go b/coord.go index 53787a2..9c2c46f 100644 --- a/coord.go +++ b/coord.go @@ -27,6 +27,11 @@ type Coord struct { X, Y int } +func CoordFromER(ev *tcell.EventResize) Coord { + w, h := ev.Size() + return Coord{X: w, Y: h} +} + func (p Coord) Add(o Coord) Coord { return Coord{ X: p.X + o.X, diff --git a/layout_flags.go b/layout_flags.go index 02f0eb5..4c317ab 100644 --- a/layout_flags.go +++ b/layout_flags.go @@ -75,7 +75,8 @@ const ( LFSizeAll = LayoutFlag(0x11110000) ) -func (f LayoutFlag) Add(fl LayoutFlag) { f |= fl } +func (f LayoutFlag) Add(fl LayoutFlag) { f |= fl } +func (f LayoutFlag) Remove(fl LayoutFlag) { f = f &^ fl } func (f LayoutFlag) ClearAll() { f.ClearAllAlign() f.ClearAllSize() @@ -105,6 +106,7 @@ func (f LayoutFlag) ClearAlignV() { f = f &^ LFAlignV f.Add(LFAlignVCenter) } + func (f LayoutFlag) ClearAllAlign() { f.ClearAlignH() f.ClearAlignV() @@ -115,16 +117,20 @@ func (f LayoutFlag) SetSizeWrap() { f.ClearAllSize() f = f | LFSizeWrap } + func (f LayoutFlag) SetSizeFull() { f.ClearAllSize() f = f | LFSizeFull } + func (f LayoutFlag) ClearAllSize() { f = f &^ LFSizeAll } + func (f LayoutFlag) ClearSizeW() { f = f &^ (LFSizeFullW | LFSizeWrapW) } + func (f LayoutFlag) ClearSizeH() { f = f &^ (LFSizeFullH | LFSizeWrapH) } diff --git a/wdgt_linear_layout.go b/wdgt_linear_layout.go index 91d56fa..142ebc3 100644 --- a/wdgt_linear_layout.go +++ b/wdgt_linear_layout.go @@ -502,7 +502,6 @@ func (w *LinearLayout) updateLLHWidgetSize(wd Widget) { if w.stacked { rH = wd.MinH() } - w.Log("(%s) Resize (%s): X:%d, Y:%d", w.Id(), wd.Id(), w.getWeightedW(wd), rH) wd.HandleResize((&Coord{X: w.getWeightedW(wd), Y: rH}).ResizeEvent()) } @@ -561,11 +560,6 @@ func (w *LinearLayout) updateLLVWidgetPos(wd Widget) { } func (w *LinearLayout) updateLLHWidgetPos(wd Widget) { - debug := func(wd Widget, txt string, args ...any) { - if wd.Id() == "mngenc.selectadversary" { - w.Log(txt, args...) - } - } c := Coord{} for i := range w.widgets { if w.widgets[i] == wd { @@ -576,7 +570,6 @@ func (w *LinearLayout) updateLLHWidgetPos(wd Widget) { } if w.widgets[i].Visible() { c.X = w.widgets[i].GetX() + w.widgets[i].GetW() - debug(wd, "Bumping X: %d + %d = %d", w.widgets[i].GetX(), w.widgets[i].GetW(), c.X) } } @@ -613,7 +606,6 @@ func (w *LinearLayout) updateLLHWidgetPos(wd Widget) { c.X += 1 c.Y += 1 } - w.Log("(%s) SetPos (%s): X:%d, Y:%d", w.Id(), wd.Id(), c.X, c.Y) wd.SetPos(c) } diff --git a/wdgt_simple_list.go b/wdgt_simple_list.go index ad00176..85bb408 100644 --- a/wdgt_simple_list.go +++ b/wdgt_simple_list.go @@ -285,6 +285,7 @@ func (w *SimpleList) PageDn() bool { } return true } +func (w *SimpleList) Title() string { return w.title } func (w *SimpleList) SetTitle(ttl string) { w.title = ttl } func (w *SimpleList) SetList(l []string) { w.list = l } func (w *SimpleList) Clear() { diff --git a/wdgt_stacked_layout.go b/wdgt_stacked_layout.go new file mode 100644 index 0000000..80a6bf8 --- /dev/null +++ b/wdgt_stacked_layout.go @@ -0,0 +1,473 @@ +/* +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" + + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" + "github.com/gdamore/tcell" +) + +// ScrollingWidgetList lays out all widgets added one after the other +// It will fill as much space as you give it +type ScrollingWidgetList struct { + id string + style tcell.Style + + orientation ScrollingWidgetListOrient + + x, y int + w, h int + widgets []Widget + + emptyWidget Widget + + active bool + visible bool + focusable bool + disableTab bool + insetBorder bool + + keyMap *KeyMap + + logger func(string, ...any) +} + +type ScrollingWidgetListOrient int + +const ( + SWLLayV = ScrollingWidgetListOrient(iota) + SWLLayH +) + +var _ Widget = (*ScrollingWidgetList)(nil) + +func NewScrollingWidgetList(id string, s tcell.Style) *ScrollingWidgetList { + ret := &ScrollingWidgetList{} + ret.Init(id, s) + return ret +} + +func (w *ScrollingWidgetList) Init(id string, s tcell.Style) { + w.id = id + w.style = s + w.visible = true + w.focusable = true + w.keyMap = NewKeyMap(NewKey(BuildEK(tcell.KeyTab), func(ev *tcell.EventKey) bool { + active := w.findActive() + if active == nil && len(w.widgets) > 0 { + // No widget is active, but we do have some + for i := range w.widgets { + if w.widgets[i].Focusable() { + w.widgets[i].SetActive(true) + return true + } + } + return false + } + return w.ActivateNext() + })) + w.keyMap.Add(NewKey(BuildEK(tcell.KeyBacktab), func(ev *tcell.EventKey) bool { + active := w.findActive() + if active == nil && len(w.widgets) > 0 { + // No widget is active, but we do have some + for i := len(w.widgets) - 1; i >= 0; i-- { + if w.widgets[i].Focusable() { + w.widgets[i].SetActive(true) + return true + } + } + return false + } + return w.ActivatePrev() + })) + e := NewText(fmt.Sprintf("%s-empty", w.id), w.style) + e.SetText("Nothing to Display") + w.emptyWidget = e +} + +func (w *ScrollingWidgetList) Id() string { return w.id } +func (w *ScrollingWidgetList) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() + w.updateWidgetLayouts() +} + +func (w *ScrollingWidgetList) GetKeyMap() *KeyMap { return w.keyMap } +func (w *ScrollingWidgetList) SetKeyMap(km *KeyMap) { w.keyMap = km } + +func (w *ScrollingWidgetList) HandleKey(ev *tcell.EventKey) bool { + if !w.active || w.disableTab { + return false + } + active := w.findActive() + if active != nil { + if active.HandleKey(ev) { + return true + } + } + return w.keyMap.Handle(ev) +} + +func (w *ScrollingWidgetList) GetActiveWidgetIdx() int { return w.findActiveIdx() } +func (w *ScrollingWidgetList) GetActiveWidget() Widget { return w.findActive() } + +func (w *ScrollingWidgetList) HandleTime(ev *tcell.EventTime) { + for _, wi := range w.widgets { + wi.HandleTime(ev) + } +} + +func (w *ScrollingWidgetList) Draw(screen tcell.Screen) { + if !w.visible { + return + } + + pos := w.GetPos() + if w.insetBorder { + wh.Border(pos.X, pos.Y, pos.X+w.w, pos.Y+w.h, wh.BRD_CSIMPLE, w.style, screen) + } + + for _, wd := range w.widgets { + pos.DrawOffset(wd, screen) + } +} + +func (w *ScrollingWidgetList) Active() bool { return w.active } +func (w *ScrollingWidgetList) SetActive(a bool) { + w.active = a + if w.active { + act := w.findActiveOrFirst() + if act != nil { + act.SetActive(true) + } + } +} +func (w *ScrollingWidgetList) Visible() bool { return w.visible } +func (w *ScrollingWidgetList) SetVisible(a bool) { w.visible = a } +func (w *ScrollingWidgetList) Focusable() bool { return w.focusable } +func (w *ScrollingWidgetList) SetFocusable(b bool) { w.focusable = b } +func (w *ScrollingWidgetList) SetX(x int) { w.x = x } +func (w *ScrollingWidgetList) SetY(y int) { w.y = y } +func (w *ScrollingWidgetList) GetX() int { return w.x } +func (w *ScrollingWidgetList) GetY() int { return w.y } +func (w *ScrollingWidgetList) GetPos() Coord { return Coord{X: w.x, Y: w.y} } +func (w *ScrollingWidgetList) SetPos(c Coord) { w.x, w.y = c.X, c.Y } +func (w *ScrollingWidgetList) GetW() int { return w.w } +func (w *ScrollingWidgetList) GetH() int { return w.h } +func (w *ScrollingWidgetList) SetW(wd int) { w.w = wd } +func (w *ScrollingWidgetList) SetH(h int) { w.h = h } +func (w *ScrollingWidgetList) getSize() Coord { return Coord{X: w.w, Y: w.h} } +func (w *ScrollingWidgetList) SetSize(c Coord) { w.w, w.h = c.X, c.Y } +func (w *ScrollingWidgetList) WantW() int { + var wantW int + for _, wd := range w.widgets { + switch w.orientation { + case SWLLayV: + // Find the highest want of all widgets + wantW = wh.Max(wd.WantW(), wantW) + case SWLLayH: + // Find the sum of all widgets wants + wantW = wantW + wd.WantW() + } + } + if w.insetBorder { + wantW += 2 + } + return wantW +} + +func (w *ScrollingWidgetList) WantH() int { + var wantH int + for _, wd := range w.widgets { + switch w.orientation { + case SWLLayV: + // Find the sum of all widgets wants + wantH = wantH + wd.WantH() + case SWLLayH: + // Find the highest want of all widgets + wantH = wh.Max(wd.WantH(), wantH) + } + } + if w.insetBorder { + wantH += 2 + } + return wantH +} + +func (w *ScrollingWidgetList) MinW() int { + var minW int + for _, wd := range w.widgets { + switch w.orientation { + case SWLLayV: + // Find the highest minimum width of all widgets + minW = wh.Max(wd.MinW(), minW) + case SWLLayH: + // Find the sum of all widget minimum widgets + minW = minW + wd.MinW() + } + } + return minW +} + +func (w *ScrollingWidgetList) MinH() int { + var minH int + for _, wd := range w.widgets { + switch w.orientation { + case SWLLayV: + minH = minH + wd.MinH() + case SWLLayH: + minH = wh.Max(wd.MinH(), minH) + } + } + return minH +} + +// Find the currently active widget, there should be only one. +func (w *ScrollingWidgetList) findActiveIdx() int { + for i := range w.widgets { + if w.widgets[i].Active() { + return i + } + } + return -1 +} + +func (w *ScrollingWidgetList) findActive() Widget { + for i := range w.widgets { + if w.widgets[i].Active() { + return w.widgets[i] + } + } + return nil +} + +func (w *ScrollingWidgetList) findActiveOrFirst() Widget { + if act := w.findActive(); act != nil { + return act + } + // Didn't find one, return the first + if len(w.widgets) > 0 { + return w.widgets[0] + } + return nil +} + +func (w *ScrollingWidgetList) ActivateWidget(n Widget) { + for i := range w.widgets { + w.widgets[i].SetActive(w.widgets[i] == n) + } +} + +func (w *ScrollingWidgetList) ActivateNext() bool { + var found bool + for i := range w.widgets { + 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 *ScrollingWidgetList) 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 *ScrollingWidgetList) SetOrientation(o ScrollingWidgetListOrient) { w.orientation = o } +func (w *ScrollingWidgetList) WidgetCount() int { return len(w.widgets) } +func (w *ScrollingWidgetList) IndexOf(n Widget) int { + for i := range w.widgets { + if w.widgets[i] == n { + return i + } + } + return -1 +} + +func (w *ScrollingWidgetList) FindById(id string) Widget { + for i := range w.widgets { + if w.widgets[i].Id() == id { + return w.widgets[i] + } + } + return nil +} + +func (w *ScrollingWidgetList) Contains(n Widget) bool { + return w.IndexOf(n) >= 0 +} + +func (w *ScrollingWidgetList) Replace(n, with Widget) { + idx := w.IndexOf(n) + if idx == -1 { + // 'n' isn't in layout. Bail out. + return + } + w.Delete(n) + w.Insert(with, idx) +} + +func (w *ScrollingWidgetList) AddAll(n Widget, more ...Widget) { + w.Add(n) + for i := range more { + w.Add(more[i]) + } +} + +func (w *ScrollingWidgetList) Add(n Widget) { + if w.Contains(n) { + // If the widget is already in the layout, move it to the end + w.Delete(n) + } + w.widgets = append(w.widgets, n) + w.updateWidgetLayouts() +} + +func (w *ScrollingWidgetList) Insert(n Widget, idx int) { + if idx >= len(w.widgets) { + w.Add(n) + return + } + if pos := w.IndexOf(n); pos >= 0 { + if pos < idx { + idx-- + } + w.Delete(n) + } + w.widgets = append(w.widgets[:idx], append([]Widget{n}, w.widgets[idx:]...)...) +} + +// Remove all children from this widget +func (w *ScrollingWidgetList) Clear() { w.widgets = []Widget{} } + +func (w *ScrollingWidgetList) Delete(n Widget) { + for i := 0; i < len(w.widgets); i++ { + if w.widgets[i] == n { + w.DeleteIndex(i) + return + } + } +} + +func (w *ScrollingWidgetList) DeleteIndex(idx int) { + if idx < len(w.widgets) { + w.widgets = append(w.widgets[:idx], w.widgets[idx+1:]...) + } +} + +func (w *ScrollingWidgetList) updateWidgetLayouts() { + switch w.orientation { + case SWLLayV: + for _, wd := range w.widgets { + wd.HandleResize((&Coord{X: w.w, Y: wd.WantH()}).ResizeEvent()) + w.updateSWLVWidgetPos(wd) + } + case SWLLayH: + for _, wd := range w.widgets { + wd.HandleResize((&Coord{X: wd.WantW(), Y: w.h}).ResizeEvent()) + w.updateSWLHWidgetPos(wd) + } + } +} + +// The Layout should have a static Size set at this point that we can use +// All widgets are being aligned to the Top-Left +// +// The position and size of each widget before this should be correct +// This widget should also know its size by now. We just need to +// position it relative to the layout. +func (w *ScrollingWidgetList) updateSWLVWidgetPos(wd Widget) { + c := Coord{} + for i := range w.widgets { + if w.widgets[i] == wd { + if i > 0 { + c.Y += 1 + } + break + } + // We only care about the Y & H of the widget before this one. + if w.widgets[i].Visible() { + c.Y = w.widgets[i].GetY() + w.widgets[i].GetH() + } + } + + if wd.GetW() < w.w { + c.X = int((float64(w.w) / 2) - (float64(wd.GetW()) / 2)) + } else { + c.X = 0 + } + if w.insetBorder { + c.X += 1 + c.Y += 1 + } + wd.SetPos(c) +} + +func (w *ScrollingWidgetList) updateSWLHWidgetPos(wd Widget) { + c := Coord{} + for i := range w.widgets { + if w.widgets[i] == wd { + if i > 0 { + c.X += 1 + } + break + } + if w.widgets[i].Visible() { + c.X = w.widgets[i].GetX() + w.widgets[i].GetW() + } + } + + // Is the Y of this tricky? + c.Y = 0 + if w.insetBorder { + c.X += 1 + c.Y += 1 + } + wd.SetPos(c) +} + +func (w *ScrollingWidgetList) SetTabDisabled(b bool) { w.disableTab = b } +func (w *ScrollingWidgetList) SetBordered(b bool) { w.insetBorder = b } +func (w *ScrollingWidgetList) SetLogger(l func(string, ...any)) { w.logger = l } +func (w *ScrollingWidgetList) Log(txt string, args ...any) { + if w.logger != nil { + txt = fmt.Sprintf("%s:%s", time.Now().Format(time.TimeOnly), txt) + w.logger(txt, args...) + } +} + +func (w *ScrollingWidgetList) SetEmptyWidget(wd Widget) { w.emptyWidget = wd } diff --git a/wdgt_text.go b/wdgt_text.go index 046e063..6710565 100644 --- a/wdgt_text.go +++ b/wdgt_text.go @@ -41,6 +41,8 @@ type Text struct { active bool focusable bool keyMap *KeyMap + + flags LayoutFlag } var _ Widget = (*Text)(nil) @@ -73,7 +75,9 @@ func (w *Text) Draw(screen tcell.Screen) { } y := w.y for i := range w.message { - wh.DrawText(w.x+(w.w/2)-(len(w.message[i])/2), y, w.message[i], w.style, screen) + // wh.DrawText(w.x+(w.w/2)-(len(w.message[i])/2), y, w.message[i], w.style, screen) + wh.DrawText(w.x, y, w.message[i], w.style, screen) + // wh.DrawText(w.x+w.w-(len(w.message[i])), y, w.message[i], w.style, screen) y++ } } @@ -111,3 +115,6 @@ func (w *Text) SetText(txt string) { func (w *Text) GetText() string { return w.text } func (w *Text) GetMessage() []string { return w.message } + +func (w *Text) AddFlag(f LayoutFlag) { w.flags.Add(f) } +func (w *Text) RemoveFlag(f LayoutFlag) { w.flags.Remove(f) }