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 package widgets
import ( import (
wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
@@ -63,6 +64,8 @@ const (
AnchorErr AnchorErr
) )
var _ Widget = (*AbsoluteLayout)(nil)
func NewAbsoluteLayout(id string, s tcell.Style) *AbsoluteLayout { func NewAbsoluteLayout(id string, s tcell.Style) *AbsoluteLayout {
ret := &AbsoluteLayout{} ret := &AbsoluteLayout{}
ret.Init(id, s) 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) Id() string { return w.id }
func (w *AbsoluteLayout) HandleResize(ev *tcell.EventResize) { func (w *AbsoluteLayout) HandleResize(ev *tcell.EventResize) {
w.w, w.h = ev.Size() w.w, w.h = ev.Size()
w.w = wh.Min(w.w, w.WantW())
w.h = wh.Min(w.h, w.WantH())
w.updateWidgetLayouts() w.updateWidgetLayouts()
} }

View File

@@ -23,7 +23,6 @@ package widgets
import ( import (
"fmt" "fmt"
"strings"
wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
@@ -45,6 +44,7 @@ type Alert struct {
btnOk, btnCancel *Button btnOk, btnCancel *Button
keyMap KeyMap keyMap KeyMap
logger func(string, ...any)
} }
var _ Widget = (*Alert)(nil) var _ Widget = (*Alert)(nil)
@@ -59,17 +59,17 @@ func (w *Alert) Init(id string, style tcell.Style) {
w.id = id w.id = id
w.style = style 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.message = NewText(fmt.Sprintf("%s-text", id), style)
w.layout.Add(w.message) w.layout.Add(w.message)
btnLayout := NewLinearLayout("alertbtn-layout", tcell.StyleDefault) btnLayout := NewLinearLayout("alertbtn-layout", tcell.StyleDefault)
btnLayout.SetOrientation(LinLayH) btnLayout.SetOrientation(LinLayH)
w.layout.Add(btnLayout)
w.btnCancel = NewButton(fmt.Sprintf("%s-cancel", id), style) w.btnCancel = NewButton(fmt.Sprintf("%s-cancel", id), style)
w.btnCancel.SetLabel("Cancel") w.btnCancel.SetLabel("Cancel")
w.layout.Add(w.btnCancel)
btnLayout.Add(w.btnCancel) btnLayout.Add(w.btnCancel)
w.btnOk = NewButton(fmt.Sprintf("%s-select", id), style) 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) w.btnOk.SetActive(true)
btnLayout.Add(w.btnOk) btnLayout.Add(w.btnOk)
w.layout.Add(btnLayout)
w.keyMap = NewKeyMap(map[tcell.Key]func(ev *tcell.EventKey) bool{ w.keyMap = NewKeyMap(map[tcell.Key]func(ev *tcell.EventKey) bool{
tcell.KeyTab: w.SelectNext, tcell.KeyTab: w.SelectNext,
tcell.KeyRight: 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) Id() string { return w.id }
func (w *Alert) HandleResize(ev *tcell.EventResize) { func (w *Alert) HandleResize(ev *tcell.EventResize) {
w.w, w.h = ev.Size() 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 // 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.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 { 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) Focusable() bool { return true }
func (w *Alert) SetTabbable(b bool) { w.tabbable = b } func (w *Alert) SetTabbable(b bool) { w.tabbable = b }
func (w *Alert) Tabbable() bool { return w.tabbable } 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 { func (w *Alert) WantH() int {
msg := len(strings.Split(wh.WrapText(w.message.GetText(), w.WantW()), "\n")) return 4 + w.btnOk.WantH() + w.message.WantH()
return 2 + w.btnOk.WantH() + msg
} }
// Borders + Buttons // Borders + Buttons
func (w *Alert) MinW() int { 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 // Borders + Buttons + 2 lines for message
@@ -204,3 +186,14 @@ func (w *Alert) Do(ev *tcell.EventKey) bool {
} }
return false 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 package widgets
import ( import (
h "git.bullercodeworks.com/brian/tcell-widgets/helpers" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
@@ -44,6 +44,8 @@ type BorderedWidget struct {
logger func(string) logger func(string)
} }
var _ Widget = (*BorderedWidget)(nil)
func (w *BorderedWidget) SetLogger(l func(string)) { w.logger = l } func (w *BorderedWidget) SetLogger(l func(string)) { w.logger = l }
func (w *BorderedWidget) Log(txt string) { func (w *BorderedWidget) Log(txt string) {
if w.logger != nil { if w.logger != nil {
@@ -63,7 +65,7 @@ func (w *BorderedWidget) Init(id string, s tcell.Style) {
w.id = id w.id = id
w.style = s w.style = s
w.visible = true w.visible = true
w.border = h.BRD_CSIMPLE w.border = wh.BRD_CSIMPLE
w.tabbable = true w.tabbable = true
} }
@@ -71,6 +73,9 @@ func (w *BorderedWidget) Id() string { return w.id }
func (w *BorderedWidget) HandleResize(ev *tcell.EventResize) { func (w *BorderedWidget) HandleResize(ev *tcell.EventResize) {
// Trim space for border and pass the resize to the widget // Trim space for border and pass the resize to the widget
w.w, w.h = ev.Size() 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)) w.widget.HandleResize(tcell.NewEventResize(w.w-2, w.h-2))
} }
@@ -87,9 +92,9 @@ func (w *BorderedWidget) Draw(screen tcell.Screen) {
return return
} }
if len(w.title) > 0 { 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 { } 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.SetPos(Coord{X: w.x + 1, Y: w.y + 1})
w.widget.DrawOffset(w.GetPos(), screen) w.widget.DrawOffset(w.GetPos(), screen)

View File

@@ -25,7 +25,7 @@ import (
"fmt" "fmt"
"strings" "strings"
h "git.bullercodeworks.com/brian/tcell-widgets/helpers" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
@@ -58,8 +58,12 @@ func (w *Button) Init(id string, style tcell.Style) {
w.onPressed = func() bool { return false } w.onPressed = func() bool { return false }
w.tabbable = true w.tabbable = true
} }
func (w *Button) Id() string { return w.id } func (w *Button) Id() string { return w.id }
func (w *Button) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() } 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 { func (w *Button) HandleKey(ev *tcell.EventKey) bool {
if !w.active { if !w.active {
@@ -79,38 +83,39 @@ func (w *Button) Draw(screen tcell.Screen) {
if w.active { if w.active {
dStyle = w.style.Bold(true) dStyle = w.style.Bold(true)
} }
if w.h == 1 { switch w.h {
case 1:
lbl := w.label lbl := w.label
if w.w < len(lbl) { if w.w < len(lbl) {
lbl = lbl[:w.w] lbl = lbl[:w.w]
} else if w.w == len(w.label)+2 { } else if w.w == len(w.label)+2 {
lbl = fmt.Sprintf("[%s]", lbl) lbl = fmt.Sprintf("[%s]", lbl)
} else if w.w > len(w.label)+2 { } 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) wh.DrawText(w.x, w.y, lbl, dStyle, screen)
} else if w.h == 2 { case 2:
lbl := w.label lbl := w.label
if w.w < len(lbl) { if w.w < len(lbl) {
lbl = lbl[:w.w] lbl = lbl[:w.w]
} else if w.w == len(lbl)+2 { } else if w.w == len(lbl)+2 {
lbl = fmt.Sprintf("╭%s╮", lbl) lbl = fmt.Sprintf("╭%s╮", lbl)
} }
h.DrawText(w.x, w.y, lbl, dStyle, screen) wh.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+1, fmt.Sprintf("╰%s╯", strings.Repeat("─", len(lbl)-2)), dStyle, screen)
return return
} }
if w.w < 2 { if w.w < 2 {
h.DrawText(w.x, w.y, "╬", dStyle, screen) wh.DrawText(w.x, w.y, "╬", dStyle, screen)
return return
} else if w.w == 2 { } else if w.w == 2 {
h.DrawText(w.x, w.y, "[]", dStyle, screen) wh.DrawText(w.x, w.y, "[]", dStyle, screen)
return return
} }
lbl := h.Center(w.label, w.w-2) lbl := wh.Center(w.label, w.w-2)
h.DrawText(w.x, w.y, fmt.Sprintf("╭%s╮", strings.Repeat("─", w.w-2)), dStyle, screen) wh.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) wh.DrawText(w.x, w.y+1, fmt.Sprintf("│%s│", wh.Center(lbl, w.w-2)), dStyle, screen)
h.DrawText(w.x, w.y+2, fmt.Sprintf("╰%s╯", strings.Repeat("─", 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) { func (w *Button) DrawOffset(c Coord, screen tcell.Screen) {

35
chat.go
View File

@@ -25,10 +25,11 @@ import (
"fmt" "fmt"
"time" "time"
h "git.bullercodeworks.com/brian/tcell-widgets/helpers" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
// Chat is a greedy widget and will consume all of the space you give it.
type Chat struct { type Chat struct {
id string id string
style tcell.Style style tcell.Style
@@ -53,6 +54,8 @@ type Chat struct {
keyMap KeyMap keyMap KeyMap
} }
var _ Widget = (*Chat)(nil)
func NewChat(id string, s tcell.Style) *Chat { func NewChat(id string, s tcell.Style) *Chat {
ret := &Chat{} ret := &Chat{}
ret.Init(id, s) ret.Init(id, s)
@@ -66,18 +69,22 @@ func (w *Chat) Init(id string, s tcell.Style) {
w.tabbable = true w.tabbable = true
} }
func (w *Chat) Id() string { return w.id } func (w *Chat) Id() string { return w.id }
func (w *Chat) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() } 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 { func (w *Chat) HandleKey(ev *tcell.EventKey) bool {
if !w.active { if !w.active {
return false return false
} }
if h.IsKey(*ev, tcell.KeyEsc) { if wh.IsKey(*ev, tcell.KeyEsc) {
w.SetActive(false) w.SetActive(false)
return true return true
} }
if h.IsBS(*ev) { if wh.IsBS(*ev) {
if w.cursor > 0 { if w.cursor > 0 {
w.cursor-- w.cursor--
} }
@@ -90,7 +97,7 @@ func (w *Chat) HandleKey(ev *tcell.EventKey) bool {
} }
var ch string var ch string
if h.KeyIsDisplayable(*ev) { if wh.KeyIsDisplayable(*ev) {
ch = string(ev.Rune()) ch = string(ev.Rune())
if w.cursor < len(w.value) { if w.cursor < len(w.value) {
strPt1 := w.value[:w.cursor] strPt1 := w.value[:w.cursor]
@@ -115,9 +122,9 @@ func (w *Chat) Draw(screen tcell.Screen) {
dStyle = dStyle.Dim(true) dStyle = dStyle.Dim(true)
} }
if w.title != "" { 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 { } 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 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 { if len(line) > w.w-2 {
line = 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-- y--
} }
} }
@@ -142,11 +149,11 @@ func (w *Chat) Draw(screen tcell.Screen) {
post = w.value[w.cursor+1:] post = w.value[w.cursor+1:]
} }
} }
h.DrawText(x, y, pre, dStyle, screen) wh.DrawText(x, y, pre, dStyle, screen)
x += len(pre) 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 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 // 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) SetW(wd int) { w.w = wd }
func (w *Chat) SetH(h int) { w.h = h } 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) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Chat) WantW() int { return w.w } func (w *Chat) WantW() int { return wh.MaxInt }
func (w *Chat) WantH() int { return w.h } func (w *Chat) WantH() int { return wh.MaxInt }
func (w *Chat) MinW() int { return 2 + 20 } func (w *Chat) MinW() int { return 2 + 20 }
func (w *Chat) MinH() int { return 6 } func (w *Chat) MinH() int { return 6 }

View File

@@ -24,7 +24,7 @@ package widgets
import ( import (
"fmt" "fmt"
h "git.bullercodeworks.com/brian/tcell-widgets/helpers" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
@@ -63,8 +63,12 @@ func (w *Checkbox) Init(id string, style tcell.Style) {
w.stateRunes = []rune{'X', ' ', '-'} w.stateRunes = []rune{'X', ' ', '-'}
w.tabbable = true w.tabbable = true
} }
func (w *Checkbox) Id() string { return w.id } func (w *Checkbox) Id() string { return w.id }
func (w *Checkbox) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() } 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 { func (w *Checkbox) HandleKey(ev *tcell.EventKey) bool {
if !w.active { if !w.active {
@@ -89,7 +93,7 @@ func (w *Checkbox) Draw(screen tcell.Screen) {
if w.active { if w.active {
dStyle = w.style.Bold(true) 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) { func (w *Checkbox) DrawOffset(c Coord, screen tcell.Screen) {

31
cli.go
View File

@@ -26,10 +26,11 @@ import (
"sort" "sort"
"strings" "strings"
h "git.bullercodeworks.com/brian/tcell-widgets/helpers" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
// Cli is a greedy widget and will consume all of the space you give it.
type Cli struct { type Cli struct {
id string id string
style tcell.Style style tcell.Style
@@ -58,6 +59,8 @@ type Cli struct {
keyMap KeyMap keyMap KeyMap
} }
var _ Widget = (*Cli)(nil)
func NewCli(id string, s tcell.Style) *Cli { func NewCli(id string, s tcell.Style) *Cli {
ret := &Cli{} ret := &Cli{}
ret.Init(id, s) ret.Init(id, s)
@@ -77,11 +80,11 @@ func (w *Cli) HandleKey(ev *tcell.EventKey) bool {
if !w.active { if !w.active {
return false return false
} }
if h.IsKey(*ev, tcell.KeyEsc) { if wh.IsKey(*ev, tcell.KeyEsc) {
w.SetActive(false) w.SetActive(false)
return true return true
} }
if h.IsBS(*ev) { if wh.IsBS(*ev) {
if w.cursor > 0 { if w.cursor > 0 {
w.cursor-- w.cursor--
} }
@@ -94,7 +97,7 @@ func (w *Cli) HandleKey(ev *tcell.EventKey) bool {
} }
var ch string var ch string
if h.KeyIsDisplayable(*ev) { if wh.KeyIsDisplayable(*ev) {
ch = string(ev.Rune()) ch = string(ev.Rune())
if w.cursor < len(w.value) { if w.cursor < len(w.value) {
strPt1 := w.value[:w.cursor] strPt1 := w.value[:w.cursor]
@@ -119,9 +122,9 @@ func (w *Cli) Draw(screen tcell.Screen) {
dStyle = dStyle.Dim(true) dStyle = dStyle.Dim(true)
} }
if w.title != "" { 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 { } 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 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 { if len(line) > w.w-2 {
line = 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-- y--
} }
} }
@@ -146,11 +149,11 @@ func (w *Cli) Draw(screen tcell.Screen) {
post = w.value[w.cursor+1:] post = w.value[w.cursor+1:]
} }
} }
h.DrawText(x, y, pre, dStyle, screen) wh.DrawText(x, y, pre, dStyle, screen)
x += len(pre) 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 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 // 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) SetW(wd int) { w.w = wd }
func (w *Cli) SetH(h int) { w.h = h } 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) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Cli) WantW() int { return w.w } func (w *Cli) WantW() int { return wh.MaxInt }
func (w *Cli) WantH() int { return w.h } func (w *Cli) WantH() int { return wh.MaxInt }
func (w *Cli) MinW() int { return 20 } func (w *Cli) MinW() int { return 20 }
func (w *Cli) MinH() int { return 6 } func (w *Cli) MinH() int { return 6 }
@@ -297,7 +300,7 @@ func (w *Cli) findBestGuess() string {
func (w *Cli) Log(txt string, args ...any) { func (w *Cli) Log(txt string, args ...any) {
val := fmt.Sprintf(txt, args...) val := fmt.Sprintf(txt, args...)
w.rawLog = append(w.rawLog, val) 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() { func (w *Cli) Clear() {
@@ -307,6 +310,8 @@ func (w *Cli) Clear() {
type cliCommand func(args ...string) bool type cliCommand func(args ...string) bool
// TODO
func (c *cliCommand) findBestGuess(args ...string) string { func (c *cliCommand) findBestGuess(args ...string) string {
_ = args
return "" return ""
} }

View File

@@ -24,7 +24,7 @@ package widgets
import ( import (
"fmt" "fmt"
h "git.bullercodeworks.com/brian/tcell-widgets/helpers" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
@@ -61,8 +61,8 @@ func (w *Field) Init(id string, style tcell.Style) {
w.style = style w.style = style
w.visible = true w.visible = true
w.filter = func(ev *tcell.EventKey) bool { w.filter = func(ev *tcell.EventKey) bool {
return h.IsBS(*ev) || return wh.IsBS(*ev) ||
h.KeyIsDisplayable(*ev) wh.KeyIsDisplayable(*ev)
} }
w.keyMap = NewKeyMap(map[tcell.Key]func(ev *tcell.EventKey) bool{ w.keyMap = NewKeyMap(map[tcell.Key]func(ev *tcell.EventKey) bool{
tcell.KeyLeft: w.handleLeft, tcell.KeyLeft: w.handleLeft,
@@ -74,13 +74,18 @@ func (w *Field) Init(id string, style tcell.Style) {
w.tabbable = true w.tabbable = true
} }
func (w *Field) Id() string { return w.id } func (w *Field) Id() string { return w.id }
func (w *Field) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() } 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 { func (w *Field) HandleKey(ev *tcell.EventKey) bool {
if !w.active { if !w.active {
return false return false
} }
if h.IsBS(*ev) { if wh.IsBS(*ev) {
return w.handleBackspace(ev) return w.handleBackspace(ev)
} }
if ok := w.keyMap.Handle(ev); ok { if ok := w.keyMap.Handle(ev); ok {
@@ -108,7 +113,7 @@ func (w *Field) Draw(screen tcell.Screen) {
x := w.x x := w.x
labelW := len(w.label) labelW := len(w.label)
if labelW > 0 { 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 x = x + labelW + 2
} }
cursor := " " 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) x += len(pre)
if w.active { 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 { } else {
h.DrawText(x, w.y, cursor, useStyle, screen) wh.DrawText(x, w.y, cursor, useStyle, screen)
} }
x += 1 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) { 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 } func (w *Field) MinH() int { return 1 }
/* Non-Widget-Interface Functions */ /* Non-Widget-Interface Functions */
func (w *Field) handleBackspace(ev *tcell.EventKey) bool { func (w *Field) handleBackspace(_ *tcell.EventKey) bool {
st := w.cursor st := w.cursor
if w.cursor > 0 { if w.cursor > 0 {
w.cursor-- w.cursor--

View File

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

View File

@@ -23,6 +23,13 @@ package helpers
import "cmp" import "cmp"
const (
MaxUint = ^uint(0)
MinUint = 0
MaxInt = int(MaxUint >> 1)
MinInt = -MaxInt - 1
)
func Max[E cmp.Ordered](of ...E) E { func Max[E cmp.Ordered](of ...E) E {
var m E var m E
if len(of) > 0 { 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 package widgets
import ( import (
h "git.bullercodeworks.com/brian/tcell-widgets/helpers" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
@@ -33,9 +33,12 @@ type LinearLayout struct {
orientation LinearLayoutOrient orientation LinearLayoutOrient
x, y int x, y int
w, h int w, h int
widgets []Widget widgets []Widget
layoutFlags map[Widget]LayoutFlag
layoutWeights map[Widget]int
totalWeight int
active bool active bool
visible bool visible bool
@@ -43,6 +46,7 @@ type LinearLayout struct {
disableTab bool disableTab bool
cursor int cursor int
logger func(string, ...any)
} }
type LinearLayoutOrient int type LinearLayoutOrient int
@@ -52,6 +56,8 @@ const (
LinLayH LinLayH
) )
var _ Widget = (*LinearLayout)(nil)
func NewLinearLayout(id string, s tcell.Style) *LinearLayout { func NewLinearLayout(id string, s tcell.Style) *LinearLayout {
ret := &LinearLayout{} ret := &LinearLayout{}
ret.Init(id, s) ret.Init(id, s)
@@ -62,11 +68,15 @@ func (w *LinearLayout) Init(id string, s tcell.Style) {
w.id = id w.id = id
w.style = s w.style = s
w.visible = true 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) Id() string { return w.id }
func (w *LinearLayout) HandleResize(ev *tcell.EventResize) { func (w *LinearLayout) HandleResize(ev *tcell.EventResize) {
w.w, w.h = ev.Size() w.w, w.h = ev.Size()
w.w = wh.Min(w.w, w.WantW())
w.h = wh.Min(w.h, w.WantH())
w.updateWidgetLayouts() 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 { func (w *LinearLayout) WantW() int {
var wantW int var wantW int
for _, wd := range w.widgets { for _, wd := range w.widgets {
if w.orientation == LinLayV { switch w.orientation {
case LinLayV:
// Find the highest want of all widgets // Find the highest want of all widgets
wantW = h.Max(wd.WantW(), wantW) wantW = wh.Max(wd.WantW(), wantW)
} else if w.orientation == LinLayH { case LinLayH:
// Find the sum of all widget widgets wants // Find the sum of all widget widgets wants
wantW = wantW + wd.WantW() wantW = wantW + wd.WantW()
} }
@@ -165,12 +176,13 @@ func (w *LinearLayout) WantW() int {
func (w *LinearLayout) WantH() int { func (w *LinearLayout) WantH() int {
var wantH int var wantH int
for _, wd := range w.widgets { for _, wd := range w.widgets {
if w.orientation == LinLayV { switch w.orientation {
case LinLayV:
// Find the sum of all widget widgets wants // Find the sum of all widget widgets wants
wantH = wantH + wd.WantH() wantH = wantH + wd.WantH()
} else if w.orientation == LinLayH { case LinLayH:
// Find the highest want of all widgets // Find the highest want of all widgets
wantH = h.Max(wd.WantH(), wantH) wantH = wh.Max(wd.WantH(), wantH)
} }
} }
return wantH return wantH
@@ -179,10 +191,11 @@ func (w *LinearLayout) WantH() int {
func (w *LinearLayout) MinW() int { func (w *LinearLayout) MinW() int {
var minW int var minW int
for _, wd := range w.widgets { for _, wd := range w.widgets {
if w.orientation == LinLayV { switch w.orientation {
case LinLayV:
// Find the highest minimum width of all widgets // Find the highest minimum width of all widgets
minW = h.Max(wd.MinW(), minW) minW = wh.Max(wd.MinW(), minW)
} else if w.orientation == LinLayH { case LinLayH:
// Find the sum of all widget minimum widgets // Find the sum of all widget minimum widgets
minW = minW + wd.MinW() minW = minW + wd.MinW()
} }
@@ -193,17 +206,67 @@ func (w *LinearLayout) MinW() int {
func (w *LinearLayout) MinH() int { func (w *LinearLayout) MinH() int {
var minH int var minH int
for _, wd := range w.widgets { for _, wd := range w.widgets {
if w.orientation == LinLayV { switch w.orientation {
case LinLayV:
minH = minH + wd.MinH() minH = minH + wd.MinH()
} else if w.orientation == LinLayH { case LinLayH:
minH = h.Max(wd.MinH(), minH) minH = wh.Max(wd.MinH(), minH)
} }
} }
return minH return minH
} }
func (w *LinearLayout) SetOrientation(o LinearLayoutOrient) { w.orientation = o } 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) { func (w *LinearLayout) Delete(n Widget) {
for i := 0; i < len(w.widgets); i++ { for i := 0; i < len(w.widgets); i++ {
if w.widgets[i] == n { if w.widgets[i] == n {
@@ -214,40 +277,59 @@ func (w *LinearLayout) Delete(n Widget) {
} }
func (w *LinearLayout) DeleteIndex(idx int) { func (w *LinearLayout) DeleteIndex(idx int) {
w.widgets = append(w.widgets[:idx], w.widgets[idx+1:]...) if idx < len(w.widgets) {
} p := w.widgets[idx]
w.widgets = append(w.widgets[:idx], w.widgets[idx+1:]...)
func (w *LinearLayout) Insert(n Widget, idx int) { delete(w.layoutFlags, p)
w.widgets = append(w.widgets[:idx], append([]Widget{n}, w.widgets[idx:]...)...) delete(w.layoutWeights, p)
} w.updateTotalWeight()
func (w *LinearLayout) updateWidgetLayouts() {
for _, wd := range w.widgets {
w.updateWidgetPos(wd)
w.updateWidgetSize(wd)
} }
} }
// The Layout should have a static Size set at this point that we can use func (w *LinearLayout) AddFlag(wd Widget, f LayoutFlag) {
// For now we're centering all views in the Layout (on the cross-axis) if f.IsAlignH() {
// w.layoutFlags[wd].ClearAlignH()
// The position and size of each widget before this should be correct } else if f.IsAlignV() {
// Find the position and size of the widget before this one w.layoutFlags[wd].ClearAlignV()
func (w *LinearLayout) updateWidgetPos(wd Widget) { }
prevP, prevS := 0, 0 w.layoutFlags[wd].Add(f)
for _, wrk := range w.widgets { }
if w.orientation == LinLayV {
if wrk == wd { func (w *LinearLayout) RemoveFlag(wd Widget, f LayoutFlag) {
wd.SetPos(Coord{X: w.w - (wd.GetW() / 2), Y: prevP + prevS + 1}) // Removing an alignment flag centers that direction
return if f.IsAlignH() {
} w.layoutFlags[wd].ClearAlignH()
prevP, prevS = wrk.GetY(), wrk.GetH() } else if f.IsAlignV() {
} else if w.orientation == LinLayH { w.layoutFlags[wd].ClearAlignV()
if wrk == wd { }
wd.SetPos(Coord{X: prevP + prevS + 1, Y: w.h - (wd.GetH() / 2)}) }
return
} func (w *LinearLayout) SetWeight(wd Widget, wt int) {
prevP, prevS = wrk.GetX(), wrk.GetW() 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 // 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) // For now we're centering all views in the Layout (on the cross-axis)
// //
// The position of this widget should be correct // We need to determine the allowed size of this widget so we can determine
func (w *LinearLayout) updateWidgetSize(wd Widget) { // it's position
// TODO 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 { func (w *LinearLayout) getRelPos(wd Widget) Coord {
_ = wd
return Coord{} return Coord{}
} }
@@ -268,3 +402,24 @@ func (w *LinearLayout) getAbsPos(wd Widget) Coord {
rel := w.getRelPos(wd) rel := w.getRelPos(wd)
return rel.Add(Coord{X: w.x, Y: w.y}) 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 package widgets
import ( import (
h "git.bullercodeworks.com/brian/tcell-widgets/helpers" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
@@ -35,9 +35,8 @@ type List struct {
focusable bool focusable bool
tabbable bool tabbable bool
x, y int x, y int
w, h int w, h int
wantW, wantH int
border []rune border []rune
cursor int cursor int
@@ -86,8 +85,13 @@ func (w *List) Init(id string, style tcell.Style) {
w.itemsStyle = make(map[int]tcell.Style) w.itemsStyle = make(map[int]tcell.Style)
w.tabbable = true w.tabbable = true
} }
func (w *List) Id() string { return w.id } func (w *List) Id() string { return w.id }
func (w *List) HandleResize(ev *tcell.EventResize) {} 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 { func (w *List) HandleKey(ev *tcell.EventKey) bool {
if !w.active || !w.focusable { if !w.active || !w.focusable {
@@ -106,9 +110,9 @@ func (w *List) Draw(screen tcell.Screen) {
if len(w.border) > 0 { if len(w.border) > 0 {
brdSz = 2 brdSz = 2
if len(w.title) > 0 { 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 { } 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 x, y = x+1, y+1
@@ -126,7 +130,7 @@ func (w *List) Draw(screen tcell.Screen) {
if s, ok = w.itemsStyle[i]; !ok { if s, ok = w.itemsStyle[i]; !ok {
s = dS s = dS
} }
h.DrawText(x, y, txt, s.Reverse(rev), screen) wh.DrawText(x, y, txt, s.Reverse(rev), screen)
y += 1 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) SetTabbable(b bool) { w.tabbable = b }
func (w *List) Tabbable() bool { return w.tabbable } func (w *List) Tabbable() bool { return w.tabbable }
func (w *List) WantW() int { func (w *List) WantW() int {
lng := h.Longest(w.list) lng := wh.Longest(w.list)
if len(w.border) > 0 { if len(w.border) > 0 {
return lng + 2 return lng + 2
} }
@@ -172,7 +176,7 @@ func (w *List) WantH() int {
} }
func (w *List) MinW() int { func (w *List) MinW() int {
lng := h.Longest(w.list) lng := wh.Longest(w.list)
if lng > 80 { if lng > 80 {
lng = 80 lng = 80
} }
@@ -224,9 +228,9 @@ func (w *List) Remove(l string) {
func (w *List) SetBorder(brd []rune) { func (w *List) SetBorder(brd []rune) {
if len(brd) == 0 { if len(brd) == 0 {
w.border = h.BRD_SIMPLE w.border = wh.BRD_SIMPLE
} else { } else {
w.border = h.ValidateBorder(brd) w.border = wh.ValidateBorder(brd)
} }
} }

36
menu.go
View File

@@ -22,7 +22,9 @@ THE SOFTWARE.
package widgets package widgets
import ( import (
h "git.bullercodeworks.com/brian/tcell-widgets/helpers" "fmt"
wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
@@ -55,6 +57,8 @@ const (
MenuTypeV MenuTypeV
) )
var _ Widget = (*Menu)(nil)
func NewMenu(id string, style tcell.Style) *Menu { func NewMenu(id string, style tcell.Style) *Menu {
ret := &Menu{} ret := &Menu{}
ret.Init(id, style) ret.Init(id, style)
@@ -79,8 +83,16 @@ func (w *Menu) Init(id string, style tcell.Style) {
}) })
w.tabbable = true w.tabbable = true
} }
func (w *Menu) Id() string { return w.id } func (w *Menu) Id() string { return w.id }
func (w *Menu) HandleResize(ev *tcell.EventResize) {} 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 { func (w *Menu) HandleKey(ev *tcell.EventKey) bool {
if !w.active { if !w.active {
return false return false
@@ -118,7 +130,7 @@ func (w *Menu) drawHMenu(screen tcell.Screen) {
} }
x, y := w.x, w.y x, y := w.x, w.y
if len(w.label) > 0 { 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 = x + len(w.label) + 2
} }
x += 2 x += 2
@@ -142,9 +154,9 @@ func (w *Menu) drawVMenu(screen tcell.Screen) {
st := w.style st := w.style
if w.active { if w.active {
st = w.style.Reverse(true) 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) { if w.expanded || (w.active && !w.manualExpand) {
// TODO: Use DrawOffset? // TODO: Use DrawOffset?
for i := range w.items { for i := range w.items {
@@ -214,7 +226,7 @@ func (w *Menu) MinW() int {
} }
return wrk return wrk
case MenuTypeV: case MenuTypeV:
return h.Longest(labels) return wh.Longest(labels)
} }
return 0 return 0
} }
@@ -302,3 +314,13 @@ func (w *Menu) MoveDown(ev *tcell.EventKey) bool {
w.cursor = (w.cursor + 1) % len(w.items) w.cursor = (w.cursor + 1) % len(w.items)
return true 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 package widgets
import ( import (
h "git.bullercodeworks.com/brian/tcell-widgets/helpers" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
@@ -48,6 +48,8 @@ type MenuItem struct {
keyMap KeyMap keyMap KeyMap
} }
var _ Widget = (*MenuItem)(nil)
func NewMenuItem(id string, style tcell.Style) *MenuItem { func NewMenuItem(id string, style tcell.Style) *MenuItem {
ret := &MenuItem{} ret := &MenuItem{}
ret.Init(id, style) ret.Init(id, style)
@@ -75,8 +77,15 @@ func (w *MenuItem) Init(id string, style tcell.Style) {
} }
w.tabbable = true w.tabbable = true
} }
func (w *MenuItem) Id() string { return w.id } func (w *MenuItem) Id() string { return w.id }
func (w *MenuItem) HandleResize(ev *tcell.EventResize) {} 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 { func (w *MenuItem) HandleKey(ev *tcell.EventKey) bool {
if !w.active { if !w.active {
return false return false
@@ -100,11 +109,11 @@ func (w *MenuItem) Draw(screen tcell.Screen) {
x, y := w.x, w.y x, y := w.x, w.y
wd := w.w 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 y += 1
if w.expanded { if w.expanded {
if len(w.items) > 0 { 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 x += 1
for i := range w.items { 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) SetH(y int) { w.h = y }
func (w *MenuItem) GetW() int { return w.w } func (w *MenuItem) GetW() int { return w.w }
func (w *MenuItem) GetH() int { return w.y } 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 { func (w *MenuItem) WantW() int {
ret := len(w.label) + 2 ret := len(w.label) + 2
if len(w.items) > 0 { if len(w.items) > 0 {

View File

@@ -29,6 +29,7 @@ import (
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
// TODO: Rebuild with LinearLayouts
type Prompt struct { type Prompt struct {
id string id string
style tcell.Style 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) Id() string { return w.id }
func (w *Prompt) HandleResize(ev *tcell.EventResize) { 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.message.SetPos(Coord{X: w.x + 1, Y: w.y + 1})
w.field.SetPos(Coord{X: w.x + 1, Y: WidgetBottom(w.message)}) 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}) 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 package widgets
import "github.com/gdamore/tcell" import (
wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell"
)
type RelativeLayout struct { type RelativeLayout struct {
id string id string
@@ -74,10 +77,17 @@ func (w *RelativeLayout) Init(id string, style tcell.Style) {
w.visible = true w.visible = true
w.tabbable = true w.tabbable = true
} }
func (w *RelativeLayout) Id() string { return w.id } func (w *RelativeLayout) Id() string { return w.id }
func (w *RelativeLayout) HandleResize(ev *tcell.EventResize) {} func (w *RelativeLayout) HandleResize(ev *tcell.EventResize) {
func (w *RelativeLayout) HandleKey(ev *tcell.EventKey) bool { return false } w.w, w.h = ev.Size()
func (w *RelativeLayout) HandleTime(ev *tcell.EventTime) {}
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) { func (w *RelativeLayout) Draw(screen tcell.Screen) {
if !w.visible { if !w.visible {
return return

View File

@@ -25,7 +25,7 @@ import (
"fmt" "fmt"
"strings" "strings"
h "git.bullercodeworks.com/brian/tcell-widgets/helpers" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
@@ -47,7 +47,7 @@ type Searcher struct {
selectFunc func(idx int, s string) bool selectFunc func(idx int, s string) bool
hideOnSelect bool hideOnSelect bool
logger func(string) logger func(string, ...any)
keyMap KeyMap keyMap KeyMap
} }
@@ -81,8 +81,16 @@ func (w *Searcher) Init(id string, style tcell.Style) {
w.tabbable = true w.tabbable = true
} }
func (w *Searcher) Id() string { return w.id } func (w *Searcher) Id() string { return w.id }
func (w *Searcher) HandleResize(ev *tcell.EventResize) {} 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 { func (w *Searcher) HandleKey(ev *tcell.EventKey) bool {
if !w.active { if !w.active {
return false return false
@@ -172,9 +180,9 @@ func (w *Searcher) Draw(screen tcell.Screen) {
dStyle = dStyle.Dim(true) dStyle = dStyle.Dim(true)
} }
if len(w.title) > 0 { 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 { } 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) w.search.DrawOffset(w.GetPos(), screen)
x, y := w.x+1, w.y+2 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 { if w.cursor+w.h/2 > fD {
stIdx = fD - w.h/2 stIdx = fD - w.h/2
} }
stIdx = h.Max(stIdx, 0) stIdx = wh.Max(stIdx, 0)
for i := stIdx; i < fD; i++ { for i := stIdx; i < fD; i++ {
st := dStyle st := dStyle
if i == w.cursor { if i == w.cursor {
st = st.Reverse(true) st = st.Reverse(true)
} }
h.DrawText(x, y, w.filteredData[i], st, screen) wh.DrawText(x, y, w.filteredData[i], st, screen)
y++ y++
if y >= w.y+w.h { if y >= w.y+w.h {
break break
@@ -222,7 +230,7 @@ func (w *Searcher) WantW() int {
ret := 2 + w.search.WantW() ret := 2 + w.search.WantW()
var maxData int var maxData int
for i := range w.filteredData { for i := range w.filteredData {
maxData = h.Max(maxData, len(w.filteredData[i])) maxData = wh.Max(maxData, len(w.filteredData[i]))
} }
return ret + maxData return ret + maxData
} }
@@ -258,7 +266,7 @@ func (w *Searcher) SetData(data []string) {
func (w *Searcher) updateFilter() { func (w *Searcher) updateFilter() {
var selVal string var selVal string
var data []string data := []string{}
copy(data, w.filteredData) copy(data, w.filteredData)
if len(data) > 0 && len(data) > w.cursor { if len(data) > 0 && len(data) > w.cursor {
selVal = data[w.cursor] selVal = data[w.cursor]
@@ -308,9 +316,9 @@ func (w *Searcher) ClearSearch() {
w.search.SetValue("") w.search.SetValue("")
} }
func (w *Searcher) SetLogger(l func(string)) { w.logger = l } func (w *Searcher) SetLogger(l func(string, ...any)) { w.logger = l }
func (w *Searcher) Log(txt string) { func (w *Searcher) Log(txt string, args ...any) {
if w.logger != nil { if w.logger != nil {
w.logger(txt) w.logger(txt, args)
} }
} }

View File

@@ -25,7 +25,7 @@ import (
"fmt" "fmt"
"strings" "strings"
h "git.bullercodeworks.com/brian/tcell-widgets/helpers" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
@@ -48,9 +48,8 @@ type Table struct {
border []rune border []rune
x, y int x, y int
w, h int w, h int
wantW, wantH int
columnWidths []int columnWidths []int
} }
@@ -82,11 +81,17 @@ func (w *Table) Init(id string, style tcell.Style) {
w.id = id w.id = id
w.style = style w.style = style
w.visible = true w.visible = true
w.border = h.BRD_CSIMPLE w.border = wh.BRD_CSIMPLE
w.tabbable = true w.tabbable = true
} }
func (w *Table) Id() string { return w.id } func (w *Table) Id() string { return w.id }
func (w *Table) HandleResize(ev *tcell.EventResize) {} 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 { func (w *Table) HandleKey(ev *tcell.EventKey) bool {
if !w.active { if !w.active {
return false return false
@@ -115,29 +120,29 @@ func (w *Table) Draw(screen tcell.Screen) {
dStyle = dStyle.Dim(true) dStyle = dStyle.Dim(true)
} }
if w.minimized { 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 return
} }
if w.title != "" { 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 { } 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 { 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++ y++
} }
if len(dat) == 0 { 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++ y++
} else { } else {
for i := range dat { 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++ y++
} }
} }
if len(w.footer) > 0 { 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++ y++
} }
} }
@@ -169,9 +174,9 @@ func (w *Table) WantW() int {
} }
// For each column, find the longest (in header, data, and footer) // For each column, find the longest (in header, data, and footer)
var totalW int 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 { 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++ { for i := 0; i < colCnt; i++ {
var cols []int var cols []int
@@ -186,7 +191,7 @@ func (w *Table) WantW() int {
if len(w.footer) > i { if len(w.footer) > i {
cols = append(cols, len(w.footer[i])) cols = append(cols, len(w.footer[i]))
} }
totalW += h.Max(cols...) totalW += wh.Max(cols...)
} }
return totalW return totalW
} }
@@ -214,9 +219,9 @@ func (w *Table) MinW() int {
} }
// For each column, find the longest (in header, data, and footer) // For each column, find the longest (in header, data, and footer)
var totalW int 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 { 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++ { for i := 0; i < colCnt; i++ {
var cols []int var cols []int
@@ -231,7 +236,7 @@ func (w *Table) MinW() int {
if len(w.footer) > i { if len(w.footer) > i {
cols = append(cols, len(w.footer[i])) cols = append(cols, len(w.footer[i]))
} }
totalW += h.Max(cols...) totalW += wh.Max(cols...)
} }
return totalW return totalW
} }
@@ -240,7 +245,7 @@ func (w *Table) MinH() int {
if w.minimized { if w.minimized {
return 1 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 { if len(w.header) > 0 {
datLen += len(w.header) + 1 // Header length + separator datLen += len(w.header) + 1 // Header length + separator
} }

48
text.go
View File

@@ -24,13 +24,16 @@ package widgets
import ( import (
"strings" "strings"
h "git.bullercodeworks.com/brian/tcell-widgets/helpers" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "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 { type Text struct {
id string id string
text string text string
message []string
style tcell.Style style tcell.Style
x, y int x, y int
w, h int w, h int
@@ -52,23 +55,21 @@ func (w *Text) Init(id string, style tcell.Style) {
w.visible = true w.visible = true
w.tabbable = false w.tabbable = false
} }
func (w *Text) Id() string { return w.id } func (w *Text) Id() string { return w.id }
func (w *Text) HandleResize(ev *tcell.EventResize) {} func (w *Text) HandleResize(ev *tcell.EventResize) {
func (w *Text) HandleKey(ev *tcell.EventKey) bool { return false } w.w, w.h = ev.Size()
func (w *Text) HandleTime(ev *tcell.EventTime) {} 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) { func (w *Text) Draw(screen tcell.Screen) {
if !w.visible { if !w.visible {
return 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 y := w.y
for i := range pts { for i := range w.message {
h.DrawText(w.x, y, pts[i], w.style, screen) wh.DrawText(w.x-(len(w.message[i])/2), y, w.message[i], w.style, screen)
y++ 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) SetH(y int) { w.h = y }
func (w *Text) GetW() int { return w.w } func (w *Text) GetW() int { return w.w }
func (w *Text) GetH() int { return w.y } func (w *Text) GetH() int { return w.y }
func (w *Text) WantW() int { return h.Max(w.w, len(w.text)) } func (w *Text) WantW() int { return wh.Longest(w.message) }
func (w *Text) WantH() int { return h.Max(w.h, 1) } 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) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Text) Focusable() bool { return false } func (w *Text) Focusable() bool { return false }
func (w *Text) SetTabbable(b bool) { w.tabbable = b } func (w *Text) SetTabbable(b bool) { w.tabbable = b }
func (w *Text) Tabbable() bool { return w.tabbable } func (w *Text) Tabbable() bool { return w.tabbable }
func (w *Text) MinW() int { return len(w.text) } func (w *Text) MinW() int { return wh.Longest(w.message) }
func (w *Text) MinH() int { return 1 } func (w *Text) MinH() int { return len(w.message) }
func (w *Text) SetText(txt string) { w.text = txt } func (w *Text) SetText(txt string) {
func (w *Text) GetText() string { return w.text } 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" "fmt"
"time" "time"
h "git.bullercodeworks.com/brian/tcell-widgets/helpers" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
@@ -37,9 +37,8 @@ type TimeField struct {
visible bool visible bool
tabbable bool tabbable bool
x, y int x, y int
w, h int w, h int
wantW, wantH int
value time.Time value time.Time
hasDate bool hasDate bool
@@ -79,8 +78,14 @@ func (w *TimeField) Init(id string, style tcell.Style) {
w.tabbable = true w.tabbable = true
} }
func (w *TimeField) Id() string { return w.id } func (w *TimeField) Id() string { return w.id }
func (w *TimeField) HandleResize(ev *tcell.EventResize) {} 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 { func (w *TimeField) HandleKey(ev *tcell.EventKey) bool {
if !w.active { if !w.active {
return false return false
@@ -99,24 +104,24 @@ func (w *TimeField) Draw(screen tcell.Screen) {
x := w.x x := w.x
labelW := len(w.label) labelW := len(w.label)
if labelW > 0 { 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 x = x + labelW + 2
} }
if w.hasDate { if w.hasDate {
yr, mo, dy := w.value.Year(), w.value.Month(), w.value.Day() yr, mo, dy := w.value.Year(), w.value.Month(), w.value.Day()
for idx, vl := range fmt.Sprintf("%4d%2d%2d", yr, mo, dy) { for idx, vl := range fmt.Sprintf("%4d%2d%2d", yr, mo, dy) {
if idx == 4 || idx == 7 { if idx == 4 || idx == 7 {
h.DrawText(x, w.y, "-", ds, screen) wh.DrawText(x, w.y, "-", ds, screen)
x++ x++
} }
if idx == w.cursor && !w.nowBtnActive { if idx == w.cursor && !w.nowBtnActive {
if w.active { 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 { } else {
h.DrawText(x, w.y, string(vl), ds, screen) wh.DrawText(x, w.y, string(vl), ds, screen)
} }
} else { } else {
h.DrawText(x, w.y, string(vl), ds, screen) wh.DrawText(x, w.y, string(vl), ds, screen)
} }
x++ x++
} }
@@ -129,26 +134,26 @@ func (w *TimeField) Draw(screen tcell.Screen) {
} }
for idx, vl := range txt { for idx, vl := range txt {
if idx == 2 || idx == 5 { if idx == 2 || idx == 5 {
h.DrawText(x, w.y, ":", ds, screen) wh.DrawText(x, w.y, ":", ds, screen)
x++ x++
} }
if idx+8 == w.cursor && !w.nowBtnActive { if idx+8 == w.cursor && !w.nowBtnActive {
if w.active { 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 { } else {
h.DrawText(x, w.y, string(vl), ds, screen) wh.DrawText(x, w.y, string(vl), ds, screen)
} }
} else { } else {
h.DrawText(x, w.y, string(vl), ds, screen) wh.DrawText(x, w.y, string(vl), ds, screen)
} }
x++ x++
} }
} }
if w.showNowBtn { if w.showNowBtn {
if w.nowBtnActive { if w.nowBtnActive {
h.DrawText(x, w.y, "[ Now ]", ds.Reverse(true), screen) wh.DrawText(x, w.y, "[ Now ]", ds.Reverse(true), screen)
} else { } 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 { type Widget interface {
Init(string, tcell.Style) Init(string, tcell.Style)
Id() string Id() string
// HandleResize sets things up to be drawn based on the width and height // HandleResize receives a resize event from the parent with the amount of
// given to it through SetW & SetH // space available to the widget.
// If there is not enough space, the widget is allowed to freak out.
HandleResize(*tcell.EventResize) HandleResize(*tcell.EventResize)
HandleKey(*tcell.EventKey) bool HandleKey(*tcell.EventKey) bool
HandleTime(*tcell.EventTime) HandleTime(*tcell.EventTime)
@@ -46,20 +47,22 @@ type Widget interface {
GetY() int GetY() int
GetPos() Coord GetPos() Coord
SetPos(Coord) SetPos(Coord)
SetSize(Coord)
// Whatever is managing this widget (parent widget, screen, etc) should // Whatever is managing this widget (parent widget, screen, etc) should
// tell it exactly what the Width & Height are. // 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) SetW(int)
SetH(int) SetH(int)
GetW() int GetW() int
GetH() int GetH() int
// Given infinite space, WantW & WantH are what this widget wants // Given infinite space, WantW & WantH are what this widget wants
// This should reflect the current state of the widget, not potential
WantW() int WantW() int
WantH() int WantH() int
// MinW & MinH are what this widget must have to be functional. // MinW & MinH are what this widget must have to be functional.
// This should reflect the current state of the widget, not potential
MinW() int MinW() int
MinH() int MinH() int
SetSize(Coord)
} }
func WidgetBottom(w Widget) int { 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: // To validate that a struct satisfies this interface, you can do:
// var _ Widget - (*<struct>)(nil) // var _ Widget - (*<struct>)(nil)
// where <struct> is the actual struct that you're validating. // where <struct> is the actual struct that you're validating.