Really figuring some things out

This commit is contained in:
2025-08-07 11:18:03 -05:00
parent 7c96fbb187
commit b476c74683
23 changed files with 737 additions and 249 deletions

View File

@@ -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()
}

View File

@@ -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...)
}
}

View File

@@ -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)

View File

@@ -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) {

35
chat.go
View File

@@ -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 }

View File

@@ -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) {

31
cli.go
View File

@@ -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 ""
}

View File

@@ -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--

View File

@@ -29,6 +29,7 @@ import (
"github.com/gdamore/tcell"
)
// TODO: Rebuild using LinearLayouts
type FilePicker struct {
id string
title string

View File

@@ -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 {

59
layout_flags.go Normal file
View File

@@ -0,0 +1,59 @@
/*
Copyright © Brian Buller <brian@bullercodeworks.com>
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)
}

View File

@@ -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...)
}
}

30
list.go
View File

@@ -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)
}
}

36
menu.go
View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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})

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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
}

48
text.go
View File

@@ -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 }

View File

@@ -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)
}
}
}

149
top_menu_layout.go Normal file
View File

@@ -0,0 +1,149 @@
/*
Copyright © Brian Buller <brian@bullercodeworks.com>
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 }

View File

@@ -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 - (*<struct>)(nil)
// where <struct> is the actual struct that you're validating.