From b476c7468371b772f58d385755a3592a173032cd Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Thu, 7 Aug 2025 11:18:03 -0500 Subject: [PATCH] Really figuring some things out --- absolute_layout.go | 5 + alert.go | 57 ++++----- bordered_widget.go | 13 +- button.go | 35 +++--- chat.go | 35 +++--- checkbox.go | 12 +- cli.go | 31 +++-- field.go | 29 +++-- filepicker.go | 1 + helpers/number_helpers.go | 7 ++ layout_flags.go | 59 +++++++++ linear_layout.go | 257 ++++++++++++++++++++++++++++++-------- list.go | 30 +++-- menu.go | 36 ++++-- menu_item.go | 21 +++- prompt.go | 6 + relative_layout.go | 20 ++- searcher.go | 34 +++-- table.go | 47 +++---- text.go | 48 ++++--- timefield.go | 39 +++--- top_menu_layout.go | 149 ++++++++++++++++++++++ widget.go | 15 ++- 23 files changed, 737 insertions(+), 249 deletions(-) create mode 100644 layout_flags.go create mode 100644 top_menu_layout.go diff --git a/absolute_layout.go b/absolute_layout.go index 08d00b3..c46af6a 100644 --- a/absolute_layout.go +++ b/absolute_layout.go @@ -22,6 +22,7 @@ THE SOFTWARE. package widgets import ( + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) @@ -63,6 +64,8 @@ const ( AnchorErr ) +var _ Widget = (*AbsoluteLayout)(nil) + func NewAbsoluteLayout(id string, s tcell.Style) *AbsoluteLayout { ret := &AbsoluteLayout{} ret.Init(id, s) @@ -83,6 +86,8 @@ func (w *AbsoluteLayout) Init(id string, s tcell.Style) { func (w *AbsoluteLayout) Id() string { return w.id } func (w *AbsoluteLayout) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) w.updateWidgetLayouts() } diff --git a/alert.go b/alert.go index 99093b1..2eb8b48 100644 --- a/alert.go +++ b/alert.go @@ -23,7 +23,6 @@ package widgets import ( "fmt" - "strings" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" @@ -45,6 +44,7 @@ type Alert struct { btnOk, btnCancel *Button keyMap KeyMap + logger func(string, ...any) } var _ Widget = (*Alert)(nil) @@ -59,17 +59,17 @@ func (w *Alert) Init(id string, style tcell.Style) { w.id = id w.style = style - w.layout = NewLinearLayout("alertlayout", tcell.StyleDefault) + w.layout = NewLinearLayout(fmt.Sprintf("%s-layout", id), tcell.StyleDefault) w.message = NewText(fmt.Sprintf("%s-text", id), style) w.layout.Add(w.message) btnLayout := NewLinearLayout("alertbtn-layout", tcell.StyleDefault) btnLayout.SetOrientation(LinLayH) + w.layout.Add(btnLayout) w.btnCancel = NewButton(fmt.Sprintf("%s-cancel", id), style) w.btnCancel.SetLabel("Cancel") - w.layout.Add(w.btnCancel) btnLayout.Add(w.btnCancel) w.btnOk = NewButton(fmt.Sprintf("%s-select", id), style) @@ -77,8 +77,6 @@ func (w *Alert) Init(id string, style tcell.Style) { w.btnOk.SetActive(true) btnLayout.Add(w.btnOk) - w.layout.Add(btnLayout) - w.keyMap = NewKeyMap(map[tcell.Key]func(ev *tcell.EventKey) bool{ tcell.KeyTab: w.SelectNext, tcell.KeyRight: w.SelectNext, @@ -92,31 +90,13 @@ func (w *Alert) Init(id string, style tcell.Style) { func (w *Alert) Id() string { return w.id } func (w *Alert) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() + + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) + // Trim space for the borders and pass on the size to the layout + w.layout.SetPos(Coord{X: 1, Y: 1}) w.layout.HandleResize(tcell.NewEventResize(w.w-2, w.h-2)) - /* - w.message.HandleResize(ev) - w.message.SetPos(Coord{X: w.x + 1, Y: w.y + 1}) - msgWantH := w.message.WantH() - if msgWantH > w.h { - // TODO message won't fit in alert window - } - w.message.SetSize(Coord{X: w.w - 2, Y: msgWantH}) - - w.btnCancel.HandleResize(ev) - w.btnCancel.SetPos(Coord{ - X: w.x + 2, - Y: w.y + w.h - 3, - }) - w.btnCancel.SetSize(Coord{X: 10, Y: 3}) - - w.btnOk.HandleResize(ev) - w.btnOk.SetPos(Coord{ - X: w.x + w.w - 12, - Y: w.y + w.h - 3, - }) - w.btnOk.SetSize(Coord{X: 10, Y: 3}) - */ } func (w *Alert) HandleKey(ev *tcell.EventKey) bool { @@ -164,15 +144,17 @@ func (w *Alert) SetSize(c Coord) { w.w, w.h = c.X, c.Y } func (w *Alert) Focusable() bool { return true } func (w *Alert) SetTabbable(b bool) { w.tabbable = b } func (w *Alert) Tabbable() bool { return w.tabbable } -func (w *Alert) WantW() int { return w.btnOk.WantW() + w.btnCancel.WantW() + 4 } +func (w *Alert) WantW() int { + return 4 + wh.Max(w.message.WantW(), (w.btnOk.WantW()+w.btnCancel.WantW())) +} + func (w *Alert) WantH() int { - msg := len(strings.Split(wh.WrapText(w.message.GetText(), w.WantW()), "\n")) - return 2 + w.btnOk.WantH() + msg + return 4 + w.btnOk.WantH() + w.message.WantH() } // Borders + Buttons func (w *Alert) MinW() int { - return 2 + w.message.MinW() + w.btnOk.MinW() + w.btnCancel.MinW() + return 2 + wh.Max(w.message.MinW(), (w.btnOk.MinW()+w.btnCancel.MinW())) } // Borders + Buttons + 2 lines for message @@ -204,3 +186,14 @@ func (w *Alert) Do(ev *tcell.EventKey) bool { } return false } + +func (w *Alert) SetLogger(l func(string, ...any)) { + w.logger = l + w.layout.SetLogger(l) +} + +func (w *Alert) Log(txt string, args ...any) { + if w.logger != nil { + w.logger(txt, args...) + } +} diff --git a/bordered_widget.go b/bordered_widget.go index e8c722b..62b4b5a 100644 --- a/bordered_widget.go +++ b/bordered_widget.go @@ -22,7 +22,7 @@ THE SOFTWARE. package widgets import ( - h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) @@ -44,6 +44,8 @@ type BorderedWidget struct { logger func(string) } +var _ Widget = (*BorderedWidget)(nil) + func (w *BorderedWidget) SetLogger(l func(string)) { w.logger = l } func (w *BorderedWidget) Log(txt string) { if w.logger != nil { @@ -63,7 +65,7 @@ func (w *BorderedWidget) Init(id string, s tcell.Style) { w.id = id w.style = s w.visible = true - w.border = h.BRD_CSIMPLE + w.border = wh.BRD_CSIMPLE w.tabbable = true } @@ -71,6 +73,9 @@ func (w *BorderedWidget) Id() string { return w.id } func (w *BorderedWidget) HandleResize(ev *tcell.EventResize) { // Trim space for border and pass the resize to the widget w.w, w.h = ev.Size() + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) + w.widget.HandleResize(tcell.NewEventResize(w.w-2, w.h-2)) } @@ -87,9 +92,9 @@ func (w *BorderedWidget) Draw(screen tcell.Screen) { return } if len(w.title) > 0 { - h.TitledBorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.title, w.border, w.style, screen) + wh.TitledBorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.title, w.border, w.style, screen) } else { - h.BorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.border, w.style, screen) + wh.BorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.border, w.style, screen) } w.widget.SetPos(Coord{X: w.x + 1, Y: w.y + 1}) w.widget.DrawOffset(w.GetPos(), screen) diff --git a/button.go b/button.go index c16c145..89c5512 100644 --- a/button.go +++ b/button.go @@ -25,7 +25,7 @@ import ( "fmt" "strings" - h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) @@ -58,8 +58,12 @@ func (w *Button) Init(id string, style tcell.Style) { w.onPressed = func() bool { return false } w.tabbable = 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() + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) +} func (w *Button) HandleKey(ev *tcell.EventKey) bool { if !w.active { @@ -79,38 +83,39 @@ func (w *Button) Draw(screen tcell.Screen) { if w.active { dStyle = w.style.Bold(true) } - if w.h == 1 { + switch w.h { + case 1: lbl := w.label if w.w < len(lbl) { lbl = lbl[:w.w] } else if w.w == len(w.label)+2 { lbl = fmt.Sprintf("[%s]", lbl) } else if w.w > len(w.label)+2 { - lbl = fmt.Sprintf("[%s]", h.Center(lbl, w.w-2)) + lbl = fmt.Sprintf("[%s]", wh.Center(lbl, w.w-2)) } - h.DrawText(w.x, w.y, lbl, dStyle, screen) - } else if w.h == 2 { + wh.DrawText(w.x, w.y, lbl, dStyle, screen) + case 2: lbl := w.label if w.w < len(lbl) { lbl = lbl[:w.w] } else if w.w == len(lbl)+2 { lbl = fmt.Sprintf("╭%s╮", lbl) } - h.DrawText(w.x, w.y, lbl, dStyle, screen) - h.DrawText(w.x, w.y+1, fmt.Sprintf("╰%s╯", strings.Repeat("─", len(lbl)-2)), dStyle, screen) + wh.DrawText(w.x, w.y, lbl, dStyle, screen) + wh.DrawText(w.x, w.y+1, fmt.Sprintf("╰%s╯", strings.Repeat("─", len(lbl)-2)), dStyle, screen) return } if w.w < 2 { - h.DrawText(w.x, w.y, "╬", dStyle, screen) + wh.DrawText(w.x, w.y, "╬", dStyle, screen) return } else if w.w == 2 { - h.DrawText(w.x, w.y, "[]", dStyle, screen) + wh.DrawText(w.x, w.y, "[]", dStyle, screen) return } - lbl := h.Center(w.label, w.w-2) - h.DrawText(w.x, w.y, fmt.Sprintf("╭%s╮", strings.Repeat("─", w.w-2)), dStyle, screen) - h.DrawText(w.x, w.y+1, fmt.Sprintf("│%s│", h.Center(lbl, w.w-2)), dStyle, screen) - h.DrawText(w.x, w.y+2, fmt.Sprintf("╰%s╯", strings.Repeat("─", w.w-2)), dStyle, screen) + lbl := wh.Center(w.label, w.w-2) + wh.DrawText(w.x, w.y, fmt.Sprintf("╭%s╮", strings.Repeat("─", w.w-2)), dStyle, screen) + wh.DrawText(w.x, w.y+1, fmt.Sprintf("│%s│", wh.Center(lbl, w.w-2)), dStyle, screen) + wh.DrawText(w.x, w.y+2, fmt.Sprintf("╰%s╯", strings.Repeat("─", w.w-2)), dStyle, screen) } func (w *Button) DrawOffset(c Coord, screen tcell.Screen) { diff --git a/chat.go b/chat.go index fdf3ac9..8c8dcfa 100644 --- a/chat.go +++ b/chat.go @@ -25,10 +25,11 @@ import ( "fmt" "time" - h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) +// Chat is a greedy widget and will consume all of the space you give it. type Chat struct { id string style tcell.Style @@ -53,6 +54,8 @@ type Chat struct { keyMap KeyMap } +var _ Widget = (*Chat)(nil) + func NewChat(id string, s tcell.Style) *Chat { ret := &Chat{} ret.Init(id, s) @@ -66,18 +69,22 @@ func (w *Chat) Init(id string, s tcell.Style) { w.tabbable = true } -func (w *Chat) Id() string { return w.id } -func (w *Chat) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() } +func (w *Chat) Id() string { return w.id } +func (w *Chat) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) +} func (w *Chat) HandleKey(ev *tcell.EventKey) bool { if !w.active { return false } - if h.IsKey(*ev, tcell.KeyEsc) { + if wh.IsKey(*ev, tcell.KeyEsc) { w.SetActive(false) return true } - if h.IsBS(*ev) { + if wh.IsBS(*ev) { if w.cursor > 0 { w.cursor-- } @@ -90,7 +97,7 @@ func (w *Chat) HandleKey(ev *tcell.EventKey) bool { } var ch string - if h.KeyIsDisplayable(*ev) { + if wh.KeyIsDisplayable(*ev) { ch = string(ev.Rune()) if w.cursor < len(w.value) { strPt1 := w.value[:w.cursor] @@ -115,9 +122,9 @@ func (w *Chat) Draw(screen tcell.Screen) { dStyle = dStyle.Dim(true) } if w.title != "" { - h.TitledBorderFilled(w.x, w.y, w.x+w.w-1, w.y+w.h, w.title, h.BRD_SIMPLE, dStyle, screen) + wh.TitledBorderFilled(w.x, w.y, w.x+w.w-1, w.y+w.h, w.title, wh.BRD_SIMPLE, dStyle, screen) } else { - h.BorderFilled(w.x, w.y, w.x+w.w-1, w.y+w.h, h.BRD_SIMPLE, dStyle, screen) + wh.BorderFilled(w.x, w.y, w.x+w.w-1, w.y+w.h, wh.BRD_SIMPLE, dStyle, screen) } x, y := w.x+1, w.y+1+w.h-3 @@ -127,7 +134,7 @@ func (w *Chat) Draw(screen tcell.Screen) { if len(line) > w.w-2 { line = line[:w.w-2] } - h.DrawText(x, y, h.PadR(line, w.w), dStyle, screen) + wh.DrawText(x, y, wh.PadR(line, w.w), dStyle, screen) y-- } } @@ -142,11 +149,11 @@ func (w *Chat) Draw(screen tcell.Screen) { post = w.value[w.cursor+1:] } } - h.DrawText(x, y, pre, dStyle, screen) + wh.DrawText(x, y, pre, dStyle, screen) x += len(pre) - h.DrawText(x, y, cursor, dStyle.Reverse(w.active), screen) + wh.DrawText(x, y, cursor, dStyle.Reverse(w.active), screen) x += 1 - h.DrawText(x, y, h.PadR(post, w.w-x), dStyle, screen) + wh.DrawText(x, y, wh.PadR(post, w.w-x), dStyle, screen) // x += len(post) - 1 } @@ -175,8 +182,8 @@ func (w *Chat) GetH() int { return w.h } func (w *Chat) SetW(wd int) { w.w = wd } func (w *Chat) SetH(h int) { w.h = h } func (w *Chat) SetSize(c Coord) { w.w, w.h = c.X, c.Y } -func (w *Chat) WantW() int { return w.w } -func (w *Chat) WantH() int { return w.h } +func (w *Chat) WantW() int { return wh.MaxInt } +func (w *Chat) WantH() int { return wh.MaxInt } func (w *Chat) MinW() int { return 2 + 20 } func (w *Chat) MinH() int { return 6 } diff --git a/checkbox.go b/checkbox.go index 7d67ec7..1351cb1 100644 --- a/checkbox.go +++ b/checkbox.go @@ -24,7 +24,7 @@ package widgets import ( "fmt" - h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) @@ -63,8 +63,12 @@ func (w *Checkbox) Init(id string, style tcell.Style) { w.stateRunes = []rune{'X', ' ', '-'} w.tabbable = true } -func (w *Checkbox) Id() string { return w.id } -func (w *Checkbox) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() } +func (w *Checkbox) Id() string { return w.id } +func (w *Checkbox) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) +} func (w *Checkbox) HandleKey(ev *tcell.EventKey) bool { if !w.active { @@ -89,7 +93,7 @@ func (w *Checkbox) Draw(screen tcell.Screen) { if w.active { dStyle = w.style.Bold(true) } - h.DrawText(w.x, w.y, fmt.Sprintf("[%s] %s", string(w.state), w.label), dStyle, screen) + wh.DrawText(w.x, w.y, fmt.Sprintf("[%s] %s", string(w.state), w.label), dStyle, screen) } func (w *Checkbox) DrawOffset(c Coord, screen tcell.Screen) { diff --git a/cli.go b/cli.go index 35088d6..6433aaf 100644 --- a/cli.go +++ b/cli.go @@ -26,10 +26,11 @@ import ( "sort" "strings" - h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) +// Cli is a greedy widget and will consume all of the space you give it. type Cli struct { id string style tcell.Style @@ -58,6 +59,8 @@ type Cli struct { keyMap KeyMap } +var _ Widget = (*Cli)(nil) + func NewCli(id string, s tcell.Style) *Cli { ret := &Cli{} ret.Init(id, s) @@ -77,11 +80,11 @@ func (w *Cli) HandleKey(ev *tcell.EventKey) bool { if !w.active { return false } - if h.IsKey(*ev, tcell.KeyEsc) { + if wh.IsKey(*ev, tcell.KeyEsc) { w.SetActive(false) return true } - if h.IsBS(*ev) { + if wh.IsBS(*ev) { if w.cursor > 0 { w.cursor-- } @@ -94,7 +97,7 @@ func (w *Cli) HandleKey(ev *tcell.EventKey) bool { } var ch string - if h.KeyIsDisplayable(*ev) { + if wh.KeyIsDisplayable(*ev) { ch = string(ev.Rune()) if w.cursor < len(w.value) { strPt1 := w.value[:w.cursor] @@ -119,9 +122,9 @@ func (w *Cli) Draw(screen tcell.Screen) { dStyle = dStyle.Dim(true) } if w.title != "" { - h.TitledBorderFilled(w.x, w.y, w.x+w.w-1, w.y+w.h, w.title, h.BRD_SIMPLE, dStyle, screen) + wh.TitledBorderFilled(w.x, w.y, w.x+w.w-1, w.y+w.h, w.title, wh.BRD_SIMPLE, dStyle, screen) } else { - h.BorderFilled(w.x, w.y, w.x+w.w-1, w.y+w.h, h.BRD_SIMPLE, dStyle, screen) + wh.BorderFilled(w.x, w.y, w.x+w.w-1, w.y+w.h, wh.BRD_SIMPLE, dStyle, screen) } x, y := w.x+1, w.y+1+w.h-3 @@ -131,7 +134,7 @@ func (w *Cli) Draw(screen tcell.Screen) { if len(line) > w.w-2 { line = line[:w.w-2] } - h.DrawText(x, y, h.PadR(line, w.w), dStyle, screen) + wh.DrawText(x, y, wh.PadR(line, w.w), dStyle, screen) y-- } } @@ -146,11 +149,11 @@ func (w *Cli) Draw(screen tcell.Screen) { post = w.value[w.cursor+1:] } } - h.DrawText(x, y, pre, dStyle, screen) + wh.DrawText(x, y, pre, dStyle, screen) x += len(pre) - h.DrawText(x, y, cursor, dStyle.Reverse(w.active), screen) + wh.DrawText(x, y, cursor, dStyle.Reverse(w.active), screen) x += 1 - h.DrawText(x, y, h.PadR(post, w.w-x), dStyle, screen) + wh.DrawText(x, y, wh.PadR(post, w.w-x), dStyle, screen) // x += len(post) - 1 } @@ -179,8 +182,8 @@ func (w *Cli) GetH() int { return w.h } func (w *Cli) SetW(wd int) { w.w = wd } func (w *Cli) SetH(h int) { w.h = h } func (w *Cli) SetSize(c Coord) { w.w, w.h = c.X, c.Y } -func (w *Cli) WantW() int { return w.w } -func (w *Cli) WantH() int { return w.h } +func (w *Cli) WantW() int { return wh.MaxInt } +func (w *Cli) WantH() int { return wh.MaxInt } func (w *Cli) MinW() int { return 20 } func (w *Cli) MinH() int { return 6 } @@ -297,7 +300,7 @@ func (w *Cli) findBestGuess() string { func (w *Cli) Log(txt string, args ...any) { val := fmt.Sprintf(txt, args...) w.rawLog = append(w.rawLog, val) - w.log = append(w.log, h.WrapStringToSlice(val, w.w-2)...) + w.log = append(w.log, wh.WrapStringToSlice(val, w.w-2)...) } func (w *Cli) Clear() { @@ -307,6 +310,8 @@ func (w *Cli) Clear() { type cliCommand func(args ...string) bool +// TODO func (c *cliCommand) findBestGuess(args ...string) string { + _ = args return "" } diff --git a/field.go b/field.go index 8cc019a..55517fe 100644 --- a/field.go +++ b/field.go @@ -24,7 +24,7 @@ package widgets import ( "fmt" - h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) @@ -61,8 +61,8 @@ func (w *Field) Init(id string, style tcell.Style) { w.style = style w.visible = true w.filter = func(ev *tcell.EventKey) bool { - return h.IsBS(*ev) || - h.KeyIsDisplayable(*ev) + return wh.IsBS(*ev) || + wh.KeyIsDisplayable(*ev) } w.keyMap = NewKeyMap(map[tcell.Key]func(ev *tcell.EventKey) bool{ tcell.KeyLeft: w.handleLeft, @@ -74,13 +74,18 @@ func (w *Field) Init(id string, style tcell.Style) { w.tabbable = true } -func (w *Field) Id() string { return w.id } -func (w *Field) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() } +func (w *Field) Id() string { return w.id } +func (w *Field) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) +} + func (w *Field) HandleKey(ev *tcell.EventKey) bool { if !w.active { return false } - if h.IsBS(*ev) { + if wh.IsBS(*ev) { return w.handleBackspace(ev) } if ok := w.keyMap.Handle(ev); ok { @@ -108,7 +113,7 @@ func (w *Field) Draw(screen tcell.Screen) { x := w.x labelW := len(w.label) if labelW > 0 { - h.DrawText(w.x, w.y, w.label+": ", useStyle, screen) + wh.DrawText(w.x, w.y, w.label+": ", useStyle, screen) x = x + labelW + 2 } cursor := " " @@ -121,15 +126,15 @@ func (w *Field) Draw(screen tcell.Screen) { } } - h.DrawText(x, w.y, pre, useStyle, screen) + wh.DrawText(x, w.y, pre, useStyle, screen) x += len(pre) if w.active { - h.DrawText(x, w.y, cursor, useStyle.Reverse(true).Blink(true), screen) + wh.DrawText(x, w.y, cursor, useStyle.Reverse(true).Blink(true), screen) } else { - h.DrawText(x, w.y, cursor, useStyle, screen) + wh.DrawText(x, w.y, cursor, useStyle, screen) } x += 1 - h.DrawText(x, w.y, post, useStyle, screen) + wh.DrawText(x, w.y, post, useStyle, screen) } func (w *Field) DrawOffset(c Coord, screen tcell.Screen) { @@ -167,7 +172,7 @@ func (w *Field) MinW() int { return len(w.label) + 15 } func (w *Field) MinH() int { return 1 } /* Non-Widget-Interface Functions */ -func (w *Field) handleBackspace(ev *tcell.EventKey) bool { +func (w *Field) handleBackspace(_ *tcell.EventKey) bool { st := w.cursor if w.cursor > 0 { w.cursor-- diff --git a/filepicker.go b/filepicker.go index 4062dfa..fae2fb6 100644 --- a/filepicker.go +++ b/filepicker.go @@ -29,6 +29,7 @@ import ( "github.com/gdamore/tcell" ) +// TODO: Rebuild using LinearLayouts type FilePicker struct { id string title string diff --git a/helpers/number_helpers.go b/helpers/number_helpers.go index ce80839..3210616 100644 --- a/helpers/number_helpers.go +++ b/helpers/number_helpers.go @@ -23,6 +23,13 @@ package helpers import "cmp" +const ( + MaxUint = ^uint(0) + MinUint = 0 + MaxInt = int(MaxUint >> 1) + MinInt = -MaxInt - 1 +) + func Max[E cmp.Ordered](of ...E) E { var m E if len(of) > 0 { diff --git a/layout_flags.go b/layout_flags.go new file mode 100644 index 0000000..d2e49f3 --- /dev/null +++ b/layout_flags.go @@ -0,0 +1,59 @@ +/* +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 + +type LayoutFlag int + +// Alignment Flags +// The default should be fully centered +const ( + _ = LayoutFlag(iota) + LFAlignHLeft // 01 + LFAlignHRight // 10 + LFAlignHCenter // 11 + + LFAlignH = LayoutFlag(0x11) +) + +const ( + _ = LayoutFlag(iota << 2) + LFAlignVTop // 0100 + LFAlignVBottom // 1000 + LFAlignVCenter // 1100 + + LFAlignV = LayoutFlag(0x1100) +) + +func (f LayoutFlag) Add(fl LayoutFlag) { f |= fl } +func (f LayoutFlag) AlignH() LayoutFlag { return f & LFAlignH } +func (f LayoutFlag) AlignV() LayoutFlag { return f & LFAlignV } +func (f LayoutFlag) IsAlignH() bool { return f.AlignH() > 0 } +func (f LayoutFlag) IsAlignV() bool { return f.AlignV() > 0 } +func (f LayoutFlag) ClearAlignH() { + f = f &^ LFAlignH + f.Add(LFAlignHCenter) +} + +func (f LayoutFlag) ClearAlignV() { + f = f &^ LFAlignV + f.Add(LFAlignVCenter) +} diff --git a/linear_layout.go b/linear_layout.go index beee673..8fa1c79 100644 --- a/linear_layout.go +++ b/linear_layout.go @@ -22,7 +22,7 @@ THE SOFTWARE. package widgets import ( - h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) @@ -33,9 +33,12 @@ type LinearLayout struct { orientation LinearLayoutOrient - x, y int - w, h int - widgets []Widget + x, y int + w, h int + widgets []Widget + layoutFlags map[Widget]LayoutFlag + layoutWeights map[Widget]int + totalWeight int active bool visible bool @@ -43,6 +46,7 @@ type LinearLayout struct { disableTab bool cursor int + logger func(string, ...any) } type LinearLayoutOrient int @@ -52,6 +56,8 @@ const ( LinLayH ) +var _ Widget = (*LinearLayout)(nil) + func NewLinearLayout(id string, s tcell.Style) *LinearLayout { ret := &LinearLayout{} ret.Init(id, s) @@ -62,11 +68,15 @@ func (w *LinearLayout) Init(id string, s tcell.Style) { w.id = id w.style = s w.visible = true + w.layoutFlags = make(map[Widget]LayoutFlag) + w.layoutWeights = make(map[Widget]int) } func (w *LinearLayout) Id() string { return w.id } func (w *LinearLayout) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) w.updateWidgetLayouts() } @@ -151,10 +161,11 @@ func (w *LinearLayout) SetSize(c Coord) { w.w, w.h = c.X, c.Y } func (w *LinearLayout) WantW() int { var wantW int for _, wd := range w.widgets { - if w.orientation == LinLayV { + switch w.orientation { + case LinLayV: // Find the highest want of all widgets - wantW = h.Max(wd.WantW(), wantW) - } else if w.orientation == LinLayH { + wantW = wh.Max(wd.WantW(), wantW) + case LinLayH: // Find the sum of all widget widgets wants wantW = wantW + wd.WantW() } @@ -165,12 +176,13 @@ func (w *LinearLayout) WantW() int { func (w *LinearLayout) WantH() int { var wantH int for _, wd := range w.widgets { - if w.orientation == LinLayV { + switch w.orientation { + case LinLayV: // Find the sum of all widget widgets wants wantH = wantH + wd.WantH() - } else if w.orientation == LinLayH { + case LinLayH: // Find the highest want of all widgets - wantH = h.Max(wd.WantH(), wantH) + wantH = wh.Max(wd.WantH(), wantH) } } return wantH @@ -179,10 +191,11 @@ func (w *LinearLayout) WantH() int { func (w *LinearLayout) MinW() int { var minW int for _, wd := range w.widgets { - if w.orientation == LinLayV { + switch w.orientation { + case LinLayV: // Find the highest minimum width of all widgets - minW = h.Max(wd.MinW(), minW) - } else if w.orientation == LinLayH { + minW = wh.Max(wd.MinW(), minW) + case LinLayH: // Find the sum of all widget minimum widgets minW = minW + wd.MinW() } @@ -193,17 +206,67 @@ func (w *LinearLayout) MinW() int { func (w *LinearLayout) MinH() int { var minH int for _, wd := range w.widgets { - if w.orientation == LinLayV { + switch w.orientation { + case LinLayV: minH = minH + wd.MinH() - } else if w.orientation == LinLayH { - minH = h.Max(wd.MinH(), minH) + case LinLayH: + minH = wh.Max(wd.MinH(), minH) } } return minH } func (w *LinearLayout) SetOrientation(o LinearLayoutOrient) { w.orientation = o } -func (w *LinearLayout) Add(n Widget) { w.widgets = append(w.widgets, n) } +func (w *LinearLayout) IndexOf(n Widget) int { + for i := range w.widgets { + if w.widgets[i] == n { + return i + } + } + return -1 +} + +func (w *LinearLayout) Contains(n Widget) bool { + return w.IndexOf(n) >= 0 +} + +func (w *LinearLayout) Add(n Widget) { + if w.Contains(n) { + // If the widget is already in the layout, move it to the end + pFlags, pWeight := w.layoutFlags[n], w.layoutWeights[n] + w.Delete(n) + w.layoutFlags[n], w.layoutWeights[n] = pFlags, pWeight + } + w.widgets = append(w.widgets, n) + // If we don't already have a weight set, set it to 1 + if _, ok := w.layoutWeights[n]; !ok { + w.layoutWeights[n] = 1 + } + w.updateTotalWeight() +} + +func (w *LinearLayout) 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-- + } + // Preserve the flags & weight + pFlags, pWeight := w.layoutFlags[n], w.layoutWeights[n] + w.Delete(n) + w.layoutFlags[n], w.layoutWeights[n] = pFlags, pWeight + } + w.widgets = append(w.widgets[:idx], append([]Widget{n}, w.widgets[idx:]...)...) + // If we don't already have a weight set, set it to 1 + if _, ok := w.layoutWeights[n]; !ok { + w.layoutWeights[n] = 1 + } + w.updateTotalWeight() +} + func (w *LinearLayout) Delete(n Widget) { for i := 0; i < len(w.widgets); i++ { if w.widgets[i] == n { @@ -214,40 +277,59 @@ func (w *LinearLayout) Delete(n Widget) { } func (w *LinearLayout) DeleteIndex(idx int) { - w.widgets = append(w.widgets[:idx], w.widgets[idx+1:]...) -} - -func (w *LinearLayout) Insert(n Widget, idx int) { - w.widgets = append(w.widgets[:idx], append([]Widget{n}, w.widgets[idx:]...)...) -} - -func (w *LinearLayout) updateWidgetLayouts() { - for _, wd := range w.widgets { - w.updateWidgetPos(wd) - w.updateWidgetSize(wd) + if idx < len(w.widgets) { + p := w.widgets[idx] + w.widgets = append(w.widgets[:idx], w.widgets[idx+1:]...) + delete(w.layoutFlags, p) + delete(w.layoutWeights, p) + w.updateTotalWeight() } } -// The Layout should have a static Size set at this point that we can use -// For now we're centering all views in the Layout (on the cross-axis) -// -// The position and size of each widget before this should be correct -// Find the position and size of the widget before this one -func (w *LinearLayout) updateWidgetPos(wd Widget) { - prevP, prevS := 0, 0 - for _, wrk := range w.widgets { - if w.orientation == LinLayV { - if wrk == wd { - wd.SetPos(Coord{X: w.w - (wd.GetW() / 2), Y: prevP + prevS + 1}) - return - } - prevP, prevS = wrk.GetY(), wrk.GetH() - } else if w.orientation == LinLayH { - if wrk == wd { - wd.SetPos(Coord{X: prevP + prevS + 1, Y: w.h - (wd.GetH() / 2)}) - return - } - prevP, prevS = wrk.GetX(), wrk.GetW() +func (w *LinearLayout) AddFlag(wd Widget, f LayoutFlag) { + if f.IsAlignH() { + w.layoutFlags[wd].ClearAlignH() + } else if f.IsAlignV() { + w.layoutFlags[wd].ClearAlignV() + } + w.layoutFlags[wd].Add(f) +} + +func (w *LinearLayout) RemoveFlag(wd Widget, f LayoutFlag) { + // Removing an alignment flag centers that direction + if f.IsAlignH() { + w.layoutFlags[wd].ClearAlignH() + } else if f.IsAlignV() { + w.layoutFlags[wd].ClearAlignV() + } +} + +func (w *LinearLayout) SetWeight(wd Widget, wt int) { + if !w.Contains(wd) { + return + } + w.layoutWeights[wd] = wt + w.updateTotalWeight() +} + +func (w *LinearLayout) updateTotalWeight() { + w.totalWeight = 0 + for _, v := range w.layoutWeights { + w.totalWeight += v + } +} + +func (w *LinearLayout) updateWidgetLayouts() { + switch w.orientation { + case LinLayV: + for _, wd := range w.widgets { + w.updateVerticalWidgetSize(wd) + w.updateVerticalWidgetPos(wd) + } + case LinLayH: + for _, wd := range w.widgets { + w.updateHorizontalWidgetSize(wd) + w.updateHorizontalWidgetPos(wd) } } } @@ -255,12 +337,64 @@ func (w *LinearLayout) updateWidgetPos(wd Widget) { // The Layout should have a static Size set at this point that we can use // For now we're centering all views in the Layout (on the cross-axis) // -// The position of this widget should be correct -func (w *LinearLayout) updateWidgetSize(wd Widget) { - // TODO +// We need to determine the allowed size of this widget so we can determine +// it's position +func (w *LinearLayout) updateVerticalWidgetSize(wd Widget) { + wd.HandleResize((&Coord{X: w.w, Y: w.getWeightedH(wd)}).ResizeEvent()) +} + +func (w *LinearLayout) updateHorizontalWidgetSize(wd Widget) { + wd.HandleResize((&Coord{X: w.getWeightedW(wd), Y: w.h}).ResizeEvent()) +} + +// The Layout should have a static Size set at this point that we can use +// For now we're centering all views in the Layout (on the cross-axis) +// TODO: Use LayoutFlags to determine alignment in each 'cell' +// +// 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 *LinearLayout) updateVerticalWidgetPos(wd Widget) { + w.Log("Calculating Widget Pos: %s", wd.Id()) + c := Coord{} + for i := range w.widgets { + if w.widgets[i] == wd { + break + } + c.Y += w.getWeightedH(w.widgets[i]) + } + w.Log("Set %s Y = %d", wd.Id(), c.Y) + c.X = (w.w / 2) - (wd.GetW() / 2) + wd.SetPos(c) +} + +func (w *LinearLayout) updateHorizontalWidgetPos(wd Widget) { + c := Coord{} + for i := range w.widgets { + c.X += w.getWeightedH(w.widgets[i]) + } + c.Y = (w.h / 2) - (wd.GetH() / 2) + wd.SetPos(c) +} + +func (w *LinearLayout) updateWidgetPos(wd Widget) { + prevP, prevS := 0, 0 + for _, wrk := range w.widgets { + c := Coord{} + switch w.orientation { + case LinLayV: + c.X, c.Y = w.w-(wd.GetW()/2), prevP+prevS+1 + prevP, prevS = wrk.GetY(), wrk.GetH() + case LinLayH: + c.X, c.Y = prevP+prevS+1, w.h-(wd.GetH()/2) + prevP, prevS = wrk.GetX(), wrk.GetW() + } + wd.SetPos(c) + } } func (w *LinearLayout) getRelPos(wd Widget) Coord { + _ = wd return Coord{} } @@ -268,3 +402,24 @@ func (w *LinearLayout) getAbsPos(wd Widget) Coord { rel := w.getRelPos(wd) return rel.Add(Coord{X: w.x, Y: w.y}) } + +func (w *LinearLayout) getWeightedH(wd Widget) int { + if !w.Contains(wd) { + return 0 + } + return w.h * (w.layoutWeights[wd] * w.totalWeight) +} + +func (w *LinearLayout) getWeightedW(wd Widget) int { + if !w.Contains(wd) { + return 0 + } + return w.w * (w.layoutWeights[wd] * w.totalWeight) +} + +func (w *LinearLayout) SetLogger(l func(string, ...any)) { w.logger = l } +func (w *LinearLayout) Log(txt string, args ...any) { + if w.logger != nil { + w.logger(txt, args...) + } +} diff --git a/list.go b/list.go index 93b11d1..0c91b1b 100644 --- a/list.go +++ b/list.go @@ -22,7 +22,7 @@ THE SOFTWARE. package widgets import ( - h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) @@ -35,9 +35,8 @@ type List struct { focusable bool tabbable bool - x, y int - w, h int - wantW, wantH int + x, y int + w, h int border []rune cursor int @@ -86,8 +85,13 @@ func (w *List) Init(id string, style tcell.Style) { w.itemsStyle = make(map[int]tcell.Style) w.tabbable = true } -func (w *List) Id() string { return w.id } -func (w *List) HandleResize(ev *tcell.EventResize) {} +func (w *List) Id() string { return w.id } +func (w *List) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() + + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) +} func (w *List) HandleKey(ev *tcell.EventKey) bool { if !w.active || !w.focusable { @@ -106,9 +110,9 @@ func (w *List) Draw(screen tcell.Screen) { if len(w.border) > 0 { brdSz = 2 if len(w.title) > 0 { - h.TitledBorderFilled(x, y, x+w.w, y+w.h, w.title, w.border, dS, screen) + wh.TitledBorderFilled(x, y, x+w.w, y+w.h, w.title, w.border, dS, screen) } else { - h.BorderFilled(x, y, x+w.w, y+w.h, w.border, dS, screen) + wh.BorderFilled(x, y, x+w.w, y+w.h, w.border, dS, screen) } } x, y = x+1, y+1 @@ -126,7 +130,7 @@ func (w *List) Draw(screen tcell.Screen) { if s, ok = w.itemsStyle[i]; !ok { s = dS } - h.DrawText(x, y, txt, s.Reverse(rev), screen) + wh.DrawText(x, y, txt, s.Reverse(rev), screen) y += 1 } } @@ -156,7 +160,7 @@ func (w *List) Focusable() bool { return w.focusable } func (w *List) SetTabbable(b bool) { w.tabbable = b } func (w *List) Tabbable() bool { return w.tabbable } func (w *List) WantW() int { - lng := h.Longest(w.list) + lng := wh.Longest(w.list) if len(w.border) > 0 { return lng + 2 } @@ -172,7 +176,7 @@ func (w *List) WantH() int { } func (w *List) MinW() int { - lng := h.Longest(w.list) + lng := wh.Longest(w.list) if lng > 80 { lng = 80 } @@ -224,9 +228,9 @@ func (w *List) Remove(l string) { func (w *List) SetBorder(brd []rune) { if len(brd) == 0 { - w.border = h.BRD_SIMPLE + w.border = wh.BRD_SIMPLE } else { - w.border = h.ValidateBorder(brd) + w.border = wh.ValidateBorder(brd) } } diff --git a/menu.go b/menu.go index ab79e70..21f5860 100644 --- a/menu.go +++ b/menu.go @@ -22,7 +22,9 @@ THE SOFTWARE. package widgets import ( - h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + "fmt" + + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) @@ -55,6 +57,8 @@ const ( MenuTypeV ) +var _ Widget = (*Menu)(nil) + func NewMenu(id string, style tcell.Style) *Menu { ret := &Menu{} ret.Init(id, style) @@ -79,8 +83,16 @@ func (w *Menu) Init(id string, style tcell.Style) { }) w.tabbable = true } -func (w *Menu) Id() string { return w.id } -func (w *Menu) HandleResize(ev *tcell.EventResize) {} +func (w *Menu) Id() string { return w.id } +func (w *Menu) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() + + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) + + // TODO: Trickle-down HandleResize +} + func (w *Menu) HandleKey(ev *tcell.EventKey) bool { if !w.active { return false @@ -118,7 +130,7 @@ func (w *Menu) drawHMenu(screen tcell.Screen) { } x, y := w.x, w.y if len(w.label) > 0 { - h.DrawText(x, y, w.label, st, screen) + wh.DrawText(x, y, w.label, st, screen) x = x + len(w.label) + 2 } x += 2 @@ -142,9 +154,9 @@ func (w *Menu) drawVMenu(screen tcell.Screen) { st := w.style if w.active { st = w.style.Reverse(true) - h.TitledBorderFilled(x-1, y, x+w.WantW(), y+w.WantH(), w.label, h.BRD_CSIMPLE, w.style, screen) + wh.TitledBorderFilled(x-1, y, x+w.WantW(), y+w.WantH(), w.label, wh.BRD_CSIMPLE, w.style, screen) } - h.DrawText(w.x, w.y, w.label, st, screen) + wh.DrawText(w.x, w.y, w.label, st, screen) if w.expanded || (w.active && !w.manualExpand) { // TODO: Use DrawOffset? for i := range w.items { @@ -214,7 +226,7 @@ func (w *Menu) MinW() int { } return wrk case MenuTypeV: - return h.Longest(labels) + return wh.Longest(labels) } return 0 } @@ -302,3 +314,13 @@ func (w *Menu) MoveDown(ev *tcell.EventKey) bool { w.cursor = (w.cursor + 1) % len(w.items) return true } + +func (w *Menu) CreateMenuItem(lbl string, do func() bool, subItems ...*MenuItem) *MenuItem { + d := NewMenuItem(fmt.Sprintf("menuitem-%s", lbl), tcell.StyleDefault) + d.SetLabel(lbl) + d.SetOnPressed(do) + if len(subItems) > 0 { + d.AddItems(subItems...) + } + return d +} diff --git a/menu_item.go b/menu_item.go index dca2e77..cfe8a50 100644 --- a/menu_item.go +++ b/menu_item.go @@ -22,7 +22,7 @@ THE SOFTWARE. package widgets import ( - h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) @@ -48,6 +48,8 @@ type MenuItem struct { keyMap KeyMap } +var _ Widget = (*MenuItem)(nil) + func NewMenuItem(id string, style tcell.Style) *MenuItem { ret := &MenuItem{} ret.Init(id, style) @@ -75,8 +77,15 @@ func (w *MenuItem) Init(id string, style tcell.Style) { } w.tabbable = true } -func (w *MenuItem) Id() string { return w.id } -func (w *MenuItem) HandleResize(ev *tcell.EventResize) {} +func (w *MenuItem) Id() string { return w.id } +func (w *MenuItem) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() + + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) + // TODO: Trickle-down HandleResize +} + func (w *MenuItem) HandleKey(ev *tcell.EventKey) bool { if !w.active { return false @@ -100,11 +109,11 @@ func (w *MenuItem) Draw(screen tcell.Screen) { x, y := w.x, w.y wd := w.w - h.DrawText(x, y, h.PadR(w.label, wd), st, screen) + wh.DrawText(x, y, wh.PadR(w.label, wd), st, screen) y += 1 if w.expanded { if len(w.items) > 0 { - h.TitledBorderFilled(w.x-1, w.y, w.x+w.WantW(), w.y+w.WantH(), w.label, h.BRD_CSIMPLE, w.style, screen) + 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 { @@ -141,6 +150,8 @@ func (w *MenuItem) SetW(x int) { w.w = x } func (w *MenuItem) SetH(y int) { w.h = y } func (w *MenuItem) GetW() int { return w.w } func (w *MenuItem) GetH() int { return w.y } +func (w *MenuItem) MinW() int { return w.w } +func (w *MenuItem) MinH() int { return w.y } func (w *MenuItem) WantW() int { ret := len(w.label) + 2 if len(w.items) > 0 { diff --git a/prompt.go b/prompt.go index 53f1d7e..9e937d6 100644 --- a/prompt.go +++ b/prompt.go @@ -29,6 +29,7 @@ import ( "github.com/gdamore/tcell" ) +// TODO: Rebuild with LinearLayouts type Prompt struct { id string style tcell.Style @@ -67,6 +68,11 @@ func (w *Prompt) Init(id string, style tcell.Style) { } func (w *Prompt) Id() string { return w.id } func (w *Prompt) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() + + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) + w.message.SetPos(Coord{X: w.x + 1, Y: w.y + 1}) w.field.SetPos(Coord{X: w.x + 1, Y: WidgetBottom(w.message)}) w.btnOk.SetPos(Coord{X: w.x + w.w - w.btnOk.WantW(), Y: w.y + w.h - 1}) diff --git a/relative_layout.go b/relative_layout.go index 205ad76..99d4b37 100644 --- a/relative_layout.go +++ b/relative_layout.go @@ -21,7 +21,10 @@ THE SOFTWARE. */ package widgets -import "github.com/gdamore/tcell" +import ( + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" + "github.com/gdamore/tcell" +) type RelativeLayout struct { id string @@ -74,10 +77,17 @@ func (w *RelativeLayout) Init(id string, style tcell.Style) { w.visible = true w.tabbable = true } -func (w *RelativeLayout) Id() string { return w.id } -func (w *RelativeLayout) HandleResize(ev *tcell.EventResize) {} -func (w *RelativeLayout) HandleKey(ev *tcell.EventKey) bool { return false } -func (w *RelativeLayout) HandleTime(ev *tcell.EventTime) {} +func (w *RelativeLayout) Id() string { return w.id } +func (w *RelativeLayout) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() + + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) + + // TODO: Trickle-down HandleResize +} +func (w *RelativeLayout) HandleKey(ev *tcell.EventKey) bool { return false } +func (w *RelativeLayout) HandleTime(ev *tcell.EventTime) {} func (w *RelativeLayout) Draw(screen tcell.Screen) { if !w.visible { return diff --git a/searcher.go b/searcher.go index 80a228b..8869683 100644 --- a/searcher.go +++ b/searcher.go @@ -25,7 +25,7 @@ import ( "fmt" "strings" - h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) @@ -47,7 +47,7 @@ type Searcher struct { selectFunc func(idx int, s string) bool hideOnSelect bool - logger func(string) + logger func(string, ...any) keyMap KeyMap } @@ -81,8 +81,16 @@ func (w *Searcher) Init(id string, style tcell.Style) { w.tabbable = true } -func (w *Searcher) Id() string { return w.id } -func (w *Searcher) HandleResize(ev *tcell.EventResize) {} +func (w *Searcher) Id() string { return w.id } +func (w *Searcher) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() + + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) + // TODO: Verify this is fine: + w.search.HandleResize(ev) +} + func (w *Searcher) HandleKey(ev *tcell.EventKey) bool { if !w.active { return false @@ -172,9 +180,9 @@ func (w *Searcher) Draw(screen tcell.Screen) { dStyle = dStyle.Dim(true) } if len(w.title) > 0 { - h.TitledBorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.title, h.BRD_CSIMPLE, dStyle, screen) + wh.TitledBorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.title, wh.BRD_CSIMPLE, dStyle, screen) } else { - h.BorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, h.BRD_CSIMPLE, dStyle, screen) + wh.BorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, wh.BRD_CSIMPLE, dStyle, screen) } w.search.DrawOffset(w.GetPos(), screen) x, y := w.x+1, w.y+2 @@ -186,13 +194,13 @@ func (w *Searcher) Draw(screen tcell.Screen) { if w.cursor+w.h/2 > fD { stIdx = fD - w.h/2 } - stIdx = h.Max(stIdx, 0) + stIdx = wh.Max(stIdx, 0) for i := stIdx; i < fD; i++ { st := dStyle if i == w.cursor { st = st.Reverse(true) } - h.DrawText(x, y, w.filteredData[i], st, screen) + wh.DrawText(x, y, w.filteredData[i], st, screen) y++ if y >= w.y+w.h { break @@ -222,7 +230,7 @@ func (w *Searcher) WantW() int { ret := 2 + w.search.WantW() var maxData int for i := range w.filteredData { - maxData = h.Max(maxData, len(w.filteredData[i])) + maxData = wh.Max(maxData, len(w.filteredData[i])) } return ret + maxData } @@ -258,7 +266,7 @@ func (w *Searcher) SetData(data []string) { func (w *Searcher) updateFilter() { var selVal string - var data []string + data := []string{} copy(data, w.filteredData) if len(data) > 0 && len(data) > w.cursor { selVal = data[w.cursor] @@ -308,9 +316,9 @@ func (w *Searcher) ClearSearch() { w.search.SetValue("") } -func (w *Searcher) SetLogger(l func(string)) { w.logger = l } -func (w *Searcher) Log(txt string) { +func (w *Searcher) SetLogger(l func(string, ...any)) { w.logger = l } +func (w *Searcher) Log(txt string, args ...any) { if w.logger != nil { - w.logger(txt) + w.logger(txt, args) } } diff --git a/table.go b/table.go index e31dd08..381b155 100644 --- a/table.go +++ b/table.go @@ -25,7 +25,7 @@ import ( "fmt" "strings" - h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) @@ -48,9 +48,8 @@ type Table struct { border []rune - x, y int - w, h int - wantW, wantH int + x, y int + w, h int columnWidths []int } @@ -82,11 +81,17 @@ func (w *Table) Init(id string, style tcell.Style) { w.id = id w.style = style w.visible = true - w.border = h.BRD_CSIMPLE + w.border = wh.BRD_CSIMPLE w.tabbable = true } -func (w *Table) Id() string { return w.id } -func (w *Table) HandleResize(ev *tcell.EventResize) {} +func (w *Table) Id() string { return w.id } +func (w *Table) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() + + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) +} + func (w *Table) HandleKey(ev *tcell.EventKey) bool { if !w.active { return false @@ -115,29 +120,29 @@ func (w *Table) Draw(screen tcell.Screen) { dStyle = dStyle.Dim(true) } if w.minimized { - h.DrawText(x, y, fmt.Sprintf("├%s (%d rows)┤", w.title, len(dat)), dStyle, screen) + wh.DrawText(x, y, fmt.Sprintf("├%s (%d rows)┤", w.title, len(dat)), dStyle, screen) return } if w.title != "" { - h.TitledBorder(x, y, w.x+width, w.y+height, w.title, h.BRD_CSIMPLE, dStyle, screen) + wh.TitledBorder(x, y, w.x+width, w.y+height, w.title, wh.BRD_CSIMPLE, dStyle, screen) } else { - h.Border(x, y, w.x+width, w.y+height, h.BRD_CSIMPLE, dStyle, screen) + wh.Border(x, y, w.x+width, w.y+height, wh.BRD_CSIMPLE, dStyle, screen) } if len(w.header) > 0 { - h.DrawText(x, y, fmt.Sprintf("│%s│", strings.Join(w.header, "│")), dStyle, screen) + wh.DrawText(x, y, fmt.Sprintf("│%s│", strings.Join(w.header, "│")), dStyle, screen) y++ } if len(dat) == 0 { - h.DrawText(x, y, "No data in table", dStyle, screen) + wh.DrawText(x, y, "No data in table", dStyle, screen) y++ } else { for i := range dat { - h.DrawText(x, y, fmt.Sprintf("│%s│", strings.Join(dat[i], "│")), dStyle, screen) + wh.DrawText(x, y, fmt.Sprintf("│%s│", strings.Join(dat[i], "│")), dStyle, screen) y++ } } if len(w.footer) > 0 { - h.DrawText(x, y, fmt.Sprintf("│%s│", strings.Join(w.header, "│")), dStyle, screen) + wh.DrawText(x, y, fmt.Sprintf("│%s│", strings.Join(w.header, "│")), dStyle, screen) y++ } } @@ -169,9 +174,9 @@ func (w *Table) WantW() int { } // For each column, find the longest (in header, data, and footer) var totalW int - colCnt := h.Max(len(w.header), len(w.footer)) + colCnt := wh.Max(len(w.header), len(w.footer)) for i := range w.data { - colCnt = h.Max(colCnt, len(w.data[i])) + colCnt = wh.Max(colCnt, len(w.data[i])) } for i := 0; i < colCnt; i++ { var cols []int @@ -186,7 +191,7 @@ func (w *Table) WantW() int { if len(w.footer) > i { cols = append(cols, len(w.footer[i])) } - totalW += h.Max(cols...) + totalW += wh.Max(cols...) } return totalW } @@ -214,9 +219,9 @@ func (w *Table) MinW() int { } // For each column, find the longest (in header, data, and footer) var totalW int - colCnt := h.Max(len(w.header), len(w.footer)) + colCnt := wh.Max(len(w.header), len(w.footer)) for i := range w.data { - colCnt = h.Max(colCnt, len(w.data[i])) + colCnt = wh.Max(colCnt, len(w.data[i])) } for i := 0; i < colCnt; i++ { var cols []int @@ -231,7 +236,7 @@ func (w *Table) MinW() int { if len(w.footer) > i { cols = append(cols, len(w.footer[i])) } - totalW += h.Max(cols...) + totalW += wh.Max(cols...) } return totalW } @@ -240,7 +245,7 @@ func (w *Table) MinH() int { if w.minimized { return 1 } - datLen := h.Min(len(w.data)+2, 7) // Data length + Border + datLen := wh.Min(len(w.data)+2, 7) // Data length + Border if len(w.header) > 0 { datLen += len(w.header) + 1 // Header length + separator } diff --git a/text.go b/text.go index 544f26a..78f2aaf 100644 --- a/text.go +++ b/text.go @@ -24,13 +24,16 @@ package widgets import ( "strings" - h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) +// Currently the text widget will only split up text on newlines +// TODO: Add 'wrap' mode, which will automatically split it into lines. type Text struct { id string text string + message []string style tcell.Style x, y int w, h int @@ -52,23 +55,21 @@ func (w *Text) Init(id string, style tcell.Style) { w.visible = true w.tabbable = false } -func (w *Text) Id() string { return w.id } -func (w *Text) HandleResize(ev *tcell.EventResize) {} -func (w *Text) HandleKey(ev *tcell.EventKey) bool { return false } -func (w *Text) HandleTime(ev *tcell.EventTime) {} +func (w *Text) Id() string { return w.id } +func (w *Text) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) +} +func (w *Text) HandleKey(ev *tcell.EventKey) bool { return false } +func (w *Text) HandleTime(ev *tcell.EventTime) {} func (w *Text) Draw(screen tcell.Screen) { if !w.visible { return } - var pts []string - if w.w < len(w.text) { - pts = strings.Split(h.WrapText(w.text, w.w), "\n") - } else { - pts = []string{w.text} - } y := w.y - for i := range pts { - h.DrawText(w.x, y, pts[i], w.style, screen) + for i := range w.message { + wh.DrawText(w.x-(len(w.message[i])/2), y, w.message[i], w.style, screen) y++ } } @@ -93,14 +94,23 @@ func (w *Text) SetW(x int) { w.w = x } func (w *Text) SetH(y int) { w.h = y } func (w *Text) GetW() int { return w.w } func (w *Text) GetH() int { return w.y } -func (w *Text) WantW() int { return h.Max(w.w, len(w.text)) } -func (w *Text) WantH() int { return h.Max(w.h, 1) } +func (w *Text) WantW() int { return wh.Longest(w.message) } +func (w *Text) WantH() int { return len(w.message) } func (w *Text) SetSize(c Coord) { w.w, w.h = c.X, c.Y } func (w *Text) Focusable() bool { return false } func (w *Text) SetTabbable(b bool) { w.tabbable = b } func (w *Text) Tabbable() bool { return w.tabbable } -func (w *Text) MinW() int { return len(w.text) } -func (w *Text) MinH() int { return 1 } +func (w *Text) MinW() int { return wh.Longest(w.message) } +func (w *Text) MinH() int { return len(w.message) } -func (w *Text) SetText(txt string) { w.text = txt } -func (w *Text) GetText() string { return w.text } +func (w *Text) SetText(txt string) { + w.text = txt + if strings.Contains(w.text, "\n") { + w.message = strings.Split(w.text, "\n") + } else { + w.message = []string{w.text} + } +} + +func (w *Text) GetText() string { return w.text } +func (w *Text) GetMessage() []string { return w.message } diff --git a/timefield.go b/timefield.go index 368130c..e8fd05a 100644 --- a/timefield.go +++ b/timefield.go @@ -25,7 +25,7 @@ import ( "fmt" "time" - h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) @@ -37,9 +37,8 @@ type TimeField struct { visible bool tabbable bool - x, y int - w, h int - wantW, wantH int + x, y int + w, h int value time.Time hasDate bool @@ -79,8 +78,14 @@ func (w *TimeField) Init(id string, style tcell.Style) { w.tabbable = true } -func (w *TimeField) Id() string { return w.id } -func (w *TimeField) HandleResize(ev *tcell.EventResize) {} +func (w *TimeField) Id() string { return w.id } +func (w *TimeField) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() + + w.w = wh.Min(w.w, w.WantW()) + w.h = wh.Min(w.h, w.WantH()) +} + func (w *TimeField) HandleKey(ev *tcell.EventKey) bool { if !w.active { return false @@ -99,24 +104,24 @@ func (w *TimeField) Draw(screen tcell.Screen) { x := w.x labelW := len(w.label) if labelW > 0 { - h.DrawText(w.x, w.y, w.label+": ", ds, screen) + wh.DrawText(w.x, w.y, w.label+": ", ds, screen) x = x + labelW + 2 } if w.hasDate { yr, mo, dy := w.value.Year(), w.value.Month(), w.value.Day() for idx, vl := range fmt.Sprintf("%4d%2d%2d", yr, mo, dy) { if idx == 4 || idx == 7 { - h.DrawText(x, w.y, "-", ds, screen) + wh.DrawText(x, w.y, "-", ds, screen) x++ } if idx == w.cursor && !w.nowBtnActive { if w.active { - h.DrawText(x, w.y, string(vl), ds.Reverse(true).Blink(true), screen) + wh.DrawText(x, w.y, string(vl), ds.Reverse(true).Blink(true), screen) } else { - h.DrawText(x, w.y, string(vl), ds, screen) + wh.DrawText(x, w.y, string(vl), ds, screen) } } else { - h.DrawText(x, w.y, string(vl), ds, screen) + wh.DrawText(x, w.y, string(vl), ds, screen) } x++ } @@ -129,26 +134,26 @@ func (w *TimeField) Draw(screen tcell.Screen) { } for idx, vl := range txt { if idx == 2 || idx == 5 { - h.DrawText(x, w.y, ":", ds, screen) + wh.DrawText(x, w.y, ":", ds, screen) x++ } if idx+8 == w.cursor && !w.nowBtnActive { if w.active { - h.DrawText(x, w.y, string(vl), ds.Reverse(true).Blink(true), screen) + wh.DrawText(x, w.y, string(vl), ds.Reverse(true).Blink(true), screen) } else { - h.DrawText(x, w.y, string(vl), ds, screen) + wh.DrawText(x, w.y, string(vl), ds, screen) } } else { - h.DrawText(x, w.y, string(vl), ds, screen) + wh.DrawText(x, w.y, string(vl), ds, screen) } x++ } } if w.showNowBtn { if w.nowBtnActive { - h.DrawText(x, w.y, "[ Now ]", ds.Reverse(true), screen) + wh.DrawText(x, w.y, "[ Now ]", ds.Reverse(true), screen) } else { - h.DrawText(x, w.y, "[ Now ]", ds, screen) + wh.DrawText(x, w.y, "[ Now ]", ds, screen) } } } diff --git a/top_menu_layout.go b/top_menu_layout.go new file mode 100644 index 0000000..6237631 --- /dev/null +++ b/top_menu_layout.go @@ -0,0 +1,149 @@ +/* +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" +) + +// TopMenuLayout is a greedy widget and will consume all of the space you +// give it +type TopMenuLayout struct { + id string + style tcell.Style + + x, y int + w, h int + menu *Menu + widget Widget + + active bool + visible bool +} + +var _ Widget = (*TopMenuLayout)(nil) + +func NewTopMenuLayout(id string, s tcell.Style) *TopMenuLayout { + ret := &TopMenuLayout{} + ret.Init(id, s) + return ret +} + +func (w *TopMenuLayout) Init(id string, s tcell.Style) { + w.id = id + w.style = s + w.visible = true + + w.menu = NewMenu("mainmenu", tcell.StyleDefault) + w.menu.SetActive(false) + w.menu.SetType(MenuTypeH) + w.menu.SetTabbable(false) +} + +func (w *TopMenuLayout) Id() string { return w.id } +func (w *TopMenuLayout) HandleResize(ev *tcell.EventResize) { + w.w, w.h = ev.Size() + availW, availH := w.w, w.h + if w.menu != nil { + w.menu.SetPos(Coord{X: 0, Y: 0}) + w.menu.SetSize(Coord{X: w.w, Y: 1}) + availH = w.h - 1 + } + if w.widget != nil { + w.widget.SetPos(Coord{X: 0, Y: 1}) + w.widget.HandleResize(tcell.NewEventResize(availW-1, availH-1)) + } +} + +func (w *TopMenuLayout) HandleKey(ev *tcell.EventKey) bool { + if !w.active { + return false + } + if ev.Key() == tcell.KeyEscape && w.menu != nil { + w.menu.SetActive(!w.menu.Active()) + return true + } + if w.menu.HandleKey(ev) { + return true + } + + // Pass the key through to the main widget + if w.widget != nil { + return w.widget.HandleKey(ev) + } + return false +} + +func (w *TopMenuLayout) HandleTime(ev *tcell.EventTime) { + w.menu.HandleTime(ev) + w.widget.HandleTime(ev) +} + +func (w *TopMenuLayout) Draw(screen tcell.Screen) { + if !w.visible { + return + } + p := w.GetPos() + if w.menu != nil { + w.menu.DrawOffset(p, screen) + } + if w.widget != nil { + w.widget.DrawOffset(p, screen) + } +} + +func (w *TopMenuLayout) DrawOffset(c Coord, screen tcell.Screen) { + p := w.GetPos() + w.SetPos(p.Add(c)) + w.Draw(screen) + w.SetPos(p) +} + +func (w *TopMenuLayout) Active() bool { return w.active } +func (w *TopMenuLayout) SetActive(a bool) { w.active = a } +func (w *TopMenuLayout) Visible() bool { return w.visible } +func (w *TopMenuLayout) SetVisible(a bool) { w.visible = a } +func (w *TopMenuLayout) SetX(x int) { w.x = x } +func (w *TopMenuLayout) SetY(y int) { w.y = y } +func (w *TopMenuLayout) GetX() int { return w.x } +func (w *TopMenuLayout) GetY() int { return w.y } +func (w *TopMenuLayout) GetPos() Coord { return Coord{X: w.x, Y: w.y} } +func (w *TopMenuLayout) SetPos(c Coord) { w.x, w.y = c.X, c.Y } +func (w *TopMenuLayout) SetW(x int) { w.w = x } +func (w *TopMenuLayout) SetH(y int) { w.h = y } +func (w *TopMenuLayout) GetW() int { return w.w } +func (w *TopMenuLayout) GetH() int { return w.y } +func (w *TopMenuLayout) SetSize(c Coord) { w.w, w.h = c.X, c.Y } +func (w *TopMenuLayout) Focusable() bool { return true } +func (w *TopMenuLayout) SetTabbable(b bool) {} +func (w *TopMenuLayout) Tabbable() bool { return true } + +func (w *TopMenuLayout) WantW() int { return wh.MaxInt } +func (w *TopMenuLayout) WantH() int { return wh.MaxInt } +func (w *TopMenuLayout) MinW() int { return 0 } +func (w *TopMenuLayout) MinH() int { return 0 } + +func (w *TopMenuLayout) Menu() *Menu { return w.menu } +func (w *TopMenuLayout) AddMenuItems(iL ...*MenuItem) { w.menu.AddItems(iL...) } +func (w *TopMenuLayout) RemoveMenuItems(iL ...*MenuItem) { w.menu.RemoveItems(iL...) } +func (w *TopMenuLayout) SetWidget(wd Widget) { w.widget = wd } diff --git a/widget.go b/widget.go index 5f93f34..00a94ce 100644 --- a/widget.go +++ b/widget.go @@ -26,8 +26,9 @@ import "github.com/gdamore/tcell" type Widget interface { Init(string, tcell.Style) Id() string - // HandleResize sets things up to be drawn based on the width and height - // given to it through SetW & SetH + // HandleResize receives a resize event from the parent with the amount of + // space available to the widget. + // If there is not enough space, the widget is allowed to freak out. HandleResize(*tcell.EventResize) HandleKey(*tcell.EventKey) bool HandleTime(*tcell.EventTime) @@ -46,20 +47,22 @@ type Widget interface { GetY() int GetPos() Coord SetPos(Coord) + SetSize(Coord) // Whatever is managing this widget (parent widget, screen, etc) should // tell it exactly what the Width & Height are. - // It _should_ try to base this off of WantW & WantH + // It _should_ try to base this off of WantW/WantH & MinW/MinH SetW(int) SetH(int) GetW() int GetH() int // Given infinite space, WantW & WantH are what this widget wants + // This should reflect the current state of the widget, not potential WantW() int WantH() int // MinW & MinH are what this widget must have to be functional. + // This should reflect the current state of the widget, not potential MinW() int MinH() int - SetSize(Coord) } func WidgetBottom(w Widget) int { @@ -81,6 +84,10 @@ func (p *Coord) Add(o Coord) Coord { } } +func (p *Coord) ResizeEvent() *tcell.EventResize { + return tcell.NewEventResize(p.X, p.Y) +} + // To validate that a struct satisfies this interface, you can do: // var _ Widget - (*)(nil) // where is the actual struct that you're validating.