So much work

This commit is contained in:
2025-08-06 16:15:08 -05:00
parent 1474caffaa
commit 81c7ec8324
20 changed files with 1386 additions and 376 deletions

View File

@@ -22,8 +22,6 @@ THE SOFTWARE.
package widgets
import (
"fmt"
"github.com/gdamore/tcell"
)
@@ -36,11 +34,16 @@ type AbsoluteLayout struct {
widgets []Widget
wCoords map[Widget]Coord
wAnchor map[Widget]AbsoluteAnchor
wManualSizes map[Widget]Coord
defAnchor AbsoluteAnchor
active bool
visible bool
tabbable bool
cursor int
disableTab bool
logger func(string)
}
@@ -48,14 +51,15 @@ type AbsoluteLayout struct {
type AbsoluteAnchor int
const (
AnchorTL = AbsoluteAnchor(iota) // x,y starts at <startX> <startY>
AnchorT // x,y starts at <middleX>, <startY>
AnchorTR // x,y starts at <endX>, <startY>
AnchorR // x,y starts at <endX>, <middleY>
AnchorBR // x,y starts at <endX>, <endY>
AnchorB // x,y starts at <middleX>, <endY>
AnchorBL // x,y starts at <endX>, <startY>
AnchorL // x,y starts at <startX>, <middleY>
AnchorTL = AbsoluteAnchor(iota) // widget starts at <startX>, <startY>
AnchorT // widget starts at <middleX>-<widget width/2>, <startY>
AnchorTR // widget starts at <endX>, <startY>
AnchorL // widget starts at <startX>, <middleY>
AnchorC // widget starts at <middleX>-<widget width/2>, <middleY>-<widget height/2>
AnchorR // widget starts at <endX>-<widget width>, <middleY>-<widget height/2>
AnchorBL // widget starts at <startX>, <endY>-<widget height>
AnchorB // widget starts at <middleX>-<widget width/2>, <endY>-<widget height>
AnchorBR // widget starts at <endX>-<widget width>, <endY>-<widget height>
AnchorErr
)
@@ -72,18 +76,46 @@ func (w *AbsoluteLayout) Init(id string, s tcell.Style) {
w.defAnchor = AnchorTL
w.wCoords = make(map[Widget]Coord)
w.wAnchor = make(map[Widget]AbsoluteAnchor)
w.wManualSizes = make(map[Widget]Coord)
w.tabbable = true
}
func (w *AbsoluteLayout) Id() string { return w.id }
func (w *AbsoluteLayout) HandleResize(ev *tcell.EventResize) {
for _, wi := range w.widgets {
wi.HandleResize(ev)
}
w.w, w.h = ev.Size()
w.updateWidgetLayouts()
}
func (w *AbsoluteLayout) HandleKey(ev *tcell.EventKey) bool {
if !w.disableTab && ev.Key() == tcell.KeyTab {
fndP := -1
for i := w.cursor; i < len(w.widgets); i++ {
if fndP == -1 {
if w.widgets[i].Active() {
fndP = i
w.widgets[i].SetActive(false)
continue
}
} else {
if w.widgets[i].Focusable() && w.widgets[i].Tabbable() {
w.widgets[i].SetActive(true)
return true
}
}
}
// If we're here, we hit the end.
if fndP == -1 { // But didn't even find the start
return false
}
for i := 0; i < fndP; i++ {
if w.widgets[i].Focusable() && w.widgets[i].Tabbable() {
w.widgets[i].SetActive(true)
return true
}
}
return false
}
for _, wi := range w.widgets {
w.Log(fmt.Sprintf("Passing key (%s) to %s", ev.Name(), wi.Id()))
if wi.HandleKey(ev) {
return true
}
@@ -101,46 +133,12 @@ func (w *AbsoluteLayout) Draw(screen tcell.Screen) {
if !w.visible {
return
}
for i := len(w.widgets) - 1; i >= 0; i-- {
var p Coord
var a AbsoluteAnchor
var ok bool
if p, ok = w.wCoords[w.widgets[i]]; !ok {
// Don't know where to put this widget
continue
}
if a, ok = w.wAnchor[w.widgets[i]]; !ok {
a = w.defAnchor
}
midX := (w.x + (w.x + w.w)) / 2
// midY := (w.y + (w.y + w.h)) / 2
switch a {
case AnchorTL:
w.widgets[i].SetPos(p.Add(Coord{X: w.x, Y: w.y}))
case AnchorT:
wrk := w.widgets[i].GetW() / 2
w.widgets[i].SetPos(p.Add(Coord{X: (midX - wrk), Y: w.y}))
case AnchorTR:
wrk := w.widgets[i].GetW()
w.widgets[i].SetPos(p.Add(Coord{X: w.x + w.w - wrk, Y: w.y}))
case AnchorR:
wrkYmid := w.widgets[i].GetH() / 2
wrkX := w.widgets[i].GetW()
w.widgets[i].SetPos(p.Add(Coord{X: w.x + w.w - wrkX, Y: w.y - (w.h / 2) - wrkYmid}))
case AnchorBR:
// TODO
w.widgets[i].SetPos(p.Add(Coord{X: w.x, Y: w.y}))
case AnchorB:
// TODO
w.widgets[i].SetPos(p.Add(Coord{X: w.x, Y: w.y}))
case AnchorBL:
wrkX, wrkY := (w.x+w.h)-w.widgets[i].GetH(), 0
w.widgets[i].SetPos(p.Add(Coord{X: wrkX, Y: wrkY}))
case AnchorL:
// TODO
w.widgets[i].SetPos(p.Add(Coord{X: w.x, Y: w.y}))
}
w.widgets[i].Draw(screen)
p := w.GetPos()
for _, wd := range w.widgets {
o := wd.GetPos()
wd.SetPos(p.Add(o))
wd.Draw(screen)
wd.SetPos(o)
}
}
func (w *AbsoluteLayout) Active() bool { return w.active }
@@ -148,10 +146,13 @@ func (w *AbsoluteLayout) SetActive(a bool) { w.active = a }
func (w *AbsoluteLayout) Visible() bool { return w.visible }
func (w *AbsoluteLayout) SetVisible(a bool) { w.visible = a }
func (w *AbsoluteLayout) Focusable() bool { return true }
func (w *AbsoluteLayout) SetTabbable(b bool) { w.tabbable = b }
func (w *AbsoluteLayout) Tabbable() bool { return w.tabbable }
func (w *AbsoluteLayout) SetX(x int) { w.x = x }
func (w *AbsoluteLayout) SetY(y int) { w.y = y }
func (w *AbsoluteLayout) GetX() int { return w.x }
func (w *AbsoluteLayout) GetY() int { return w.y }
func (w *AbsoluteLayout) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *AbsoluteLayout) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *AbsoluteLayout) GetW() int { return w.w }
func (w *AbsoluteLayout) GetH() int { return w.h }
@@ -160,6 +161,29 @@ func (w *AbsoluteLayout) SetH(h int) { w.h = h }
func (w *AbsoluteLayout) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *AbsoluteLayout) WantW() int { return w.w }
func (w *AbsoluteLayout) WantH() int { return w.h }
func (w *AbsoluteLayout) MinW() int {
// Find the highest value for x in all widgets GetX() + MinW()
var minW int
for _, wd := range w.widgets {
wrk := wd.GetX() + wd.MinW()
if wrk > minW {
minW = wrk
}
}
return minW
}
func (w *AbsoluteLayout) MinH() int {
// Find the highest value for y in all widgets GetY() + MinH()
var minH int
for _, wd := range w.widgets {
wrk := wd.GetY() + wd.MinH()
if wrk > minH {
minH = wrk
}
}
return minH
}
// Add a widget at x/y
func (w *AbsoluteLayout) Add(n Widget, pos Coord) { w.AddAnchored(n, pos, w.defAnchor) }
@@ -168,6 +192,8 @@ func (w *AbsoluteLayout) AddAnchored(n Widget, pos Coord, anchor AbsoluteAnchor)
w.widgets = append(w.widgets, n)
w.wCoords[n] = pos
w.wAnchor[n] = anchor
w.updateWidgetLayouts()
}
func (w *AbsoluteLayout) Clear() {
@@ -188,3 +214,100 @@ func (w *AbsoluteLayout) Log(txt string) {
w.logger(txt)
}
}
func (w *AbsoluteLayout) updateWidgetLayouts() {
// In an Absolute Layout, widgets are given a definite position and anchor.
// The anchor is a side of the layout (see AbsoluteAnchor type)
for _, wd := range w.widgets {
w.updateWidgetPos(wd)
w.updateWidgetSize(wd)
}
}
func (w *AbsoluteLayout) updateWidgetSize(wd Widget) {
if sz, ok := w.wManualSizes[wd]; ok {
wd.SetW(sz.X)
wd.SetH(sz.Y)
return
}
// The available space is:
// X: Layout Width - Widget X
// Y: Layout Height - Widget Y
available := Coord{X: w.GetW() - wd.GetX(), Y: w.GetH() - wd.GetY()}
ww := wd.WantW()
if ww < available.X {
wd.SetW(ww)
} else if wd.MinW() < available.X {
wd.SetW(available.X)
} else {
wd.SetW(wd.MinW())
}
wh := wd.WantH()
if wh < available.Y {
wd.SetH(wh)
} else if wd.MinH() < available.Y {
wd.SetH(available.Y)
} else {
wd.SetH(wd.MinH())
}
}
// Set a widgets position relative to the layout
func (w *AbsoluteLayout) updateWidgetPos(wd Widget) { wd.SetPos(w.getRelPos(wd)) }
// Manually set the size of a widget, the Layout won't override it
func (w *AbsoluteLayout) SetWidgetSize(wd Widget, sz Coord) { w.wManualSizes[wd] = sz }
func (w *AbsoluteLayout) ShrinkWrap(wd Widget) {
w.SetWidgetSize(wd, Coord{X: wd.MinW(), Y: wd.MinH()})
}
func (w *AbsoluteLayout) getRelPos(wd Widget) Coord {
var p Coord
var a AbsoluteAnchor
var ok bool
if p, ok = w.wCoords[wd]; !ok {
// Default to top-left corner
p = Coord{X: 0, Y: 0}
}
if a, ok = w.wAnchor[wd]; !ok {
a = w.defAnchor
}
midX, midY := (w.w / 2), (w.h / 2)
switch a {
case AnchorTL:
return p
case AnchorT:
return p.Add(Coord{X: midX - (wd.GetW() / 2), Y: 0})
case AnchorTR:
return p.Add(Coord{X: w.w - wd.GetW(), Y: 0})
case AnchorL:
return p.Add(Coord{X: 0, Y: midY - (wd.GetH() / 2)})
case AnchorC:
return p.Add(Coord{X: midX - (wd.GetW() / 2), Y: midY - (wd.GetH() / 2)})
case AnchorR:
return p.Add(Coord{X: w.w - wd.GetW(), Y: midY - (wd.GetH() / 2)})
case AnchorBR:
return p.Add(Coord{X: w.w - wd.GetW(), Y: w.h - wd.GetH()})
case AnchorB:
return p.Add(Coord{X: midX - (wd.GetW() / 2), Y: w.h - wd.GetH()})
case AnchorBL:
return p.Add(Coord{X: 0, Y: w.h - wd.GetH()})
}
return p
}
func (w *AbsoluteLayout) getAbsPos(wd Widget) Coord {
rel := w.getRelPos(wd)
return rel.Add(Coord{X: w.x, Y: w.y})
}

105
alert.go
View File

@@ -37,10 +37,14 @@ type Alert struct {
w, h int
active bool
visible bool
tabbable bool
layout *AbsoluteLayout
title string
message *Text
btnOk, btnCancel *Button
keyMap KeyMap
}
var _ Widget = (*Alert)(nil)
@@ -54,23 +58,66 @@ func NewAlert(id string, style tcell.Style) *Alert {
func (w *Alert) Init(id string, style tcell.Style) {
w.id = id
w.style = style
w.layout = NewAbsoluteLayout("alertlayout", tcell.StyleDefault)
w.message = NewText(fmt.Sprintf("%s-text", id), style)
w.layout.AddAnchored(w.message, Coord{X: 0, Y: 0}, AnchorC)
w.btnOk = NewButton(fmt.Sprintf("%s-select", id), style)
w.btnOk.SetLabel("Ok")
w.btnOk.SetActive(true)
w.layout.AddAnchored(w.btnOk, Coord{X: -2, Y: 0}, AnchorBR)
w.btnCancel = NewButton(fmt.Sprintf("%s-cancel", id), style)
w.btnCancel.SetLabel("Cancel")
w.layout.AddAnchored(w.btnCancel, Coord{X: 2, Y: 0}, AnchorBL)
w.keyMap = NewKeyMap(map[tcell.Key]func(ev *tcell.EventKey) bool{
tcell.KeyTab: w.SelectNext,
tcell.KeyRight: w.SelectNext,
tcell.KeyDown: w.SelectNext,
tcell.KeyLeft: w.SelectNext,
tcell.KeyUp: w.SelectNext,
tcell.KeyEnter: w.Do,
})
w.tabbable = true
}
func (w *Alert) Id() string { return w.id }
func (w *Alert) HandleResize(ev *tcell.EventResize) {
w.btnOk.SetPos(Coord{X: w.x + w.w - w.btnOk.WantW(), Y: w.y + w.h - 1})
w.btnCancel.SetPos(Coord{X: w.x + 1, Y: w.y + w.h - 1})
w.w, w.h = ev.Size()
// Trim space for the borders and pass on the size to the layout
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 {
if !w.active {
return false
}
return false
return w.keyMap.Handle(ev)
}
func (w *Alert) HandleTime(ev *tcell.EventTime) {}
func (w *Alert) Draw(screen tcell.Screen) {
@@ -81,10 +128,12 @@ func (w *Alert) Draw(screen tcell.Screen) {
if !w.active {
dS = dS.Dim(true)
}
wh.TitledBorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.title, wh.BRD_SIMPLE, w.style, screen)
w.message.Draw(screen)
w.btnOk.Draw(screen)
w.btnCancel.Draw(screen)
w.layout.Draw(screen)
// w.message.Draw(screen)
// w.btnOk.Draw(screen)
// w.btnCancel.Draw(screen)
}
func (w *Alert) Active() bool { return w.active }
func (w *Alert) SetActive(a bool) { w.active = a }
@@ -94,6 +143,7 @@ func (w *Alert) SetX(x int) { w.x = x }
func (w *Alert) SetY(y int) { w.y = y }
func (w *Alert) GetX() int { return w.x }
func (w *Alert) GetY() int { return w.y }
func (w *Alert) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *Alert) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Alert) SetW(x int) { w.w = x }
func (w *Alert) SetH(y int) { w.h = y }
@@ -101,16 +151,45 @@ func (w *Alert) GetW() int { return w.w }
func (w *Alert) GetH() int { return w.y }
func (w *Alert) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Alert) Focusable() bool { return true }
func (w *Alert) WantW() int {
return w.btnOk.WantW() + w.btnCancel.WantW() + 4
}
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) WantH() int {
msg := len(strings.Split(wh.WrapText(w.message.GetText(), w.WantW()), "\n"))
return 2 + w.btnOk.WantH() + msg
}
func (w *Alert) SetTitle(ttl string) { w.title = ttl }
func (w *Alert) SetMessage(msg string) {
w.message.SetText(msg)
// Borders + Buttons
func (w *Alert) MinW() int {
return 2 + w.message.MinW() + w.btnOk.MinW() + w.btnCancel.MinW()
}
// Borders + Buttons + 2 lines for message
func (w *Alert) MinH() int {
return 2 + w.message.MinH() + w.btnOk.MinH()
}
func (w *Alert) SetTitle(ttl string) { w.title = ttl }
func (w *Alert) SetMessage(msg string) { w.message.SetText(msg) }
func (w *Alert) SetOkPressed(b func() bool) { w.btnOk.SetOnPressed(b) }
func (w *Alert) SetCancelPressed(b func() bool) { w.btnCancel.SetOnPressed(b) }
func (w *Alert) SelectNext(ev *tcell.EventKey) bool {
if w.btnOk.Active() {
w.btnOk.SetActive(false)
w.btnCancel.SetActive(true)
} else {
w.btnOk.SetActive(true)
w.btnCancel.SetActive(false)
}
return true
}
func (w *Alert) Do(ev *tcell.EventKey) bool {
if w.btnOk.Active() {
return w.btnOk.HandleKey(ev)
} else if w.btnCancel.Active() {
return w.btnCancel.HandleKey(ev)
}
return false
}

View File

@@ -26,6 +26,7 @@ import (
"github.com/gdamore/tcell"
)
// TODO: Make sure this works right... I don't think it does.
type BorderedWidget struct {
id string
style tcell.Style
@@ -38,6 +39,7 @@ type BorderedWidget struct {
title string
active bool
visible bool
tabbable bool
logger func(string)
}
@@ -62,11 +64,14 @@ func (w *BorderedWidget) Init(id string, s tcell.Style) {
w.style = s
w.visible = true
w.border = h.BRD_CSIMPLE
w.tabbable = true
}
func (w *BorderedWidget) Id() string { return w.id }
func (w *BorderedWidget) HandleResize(ev *tcell.EventResize) {
w.widget.HandleResize(ev)
// Trim space for border and pass the resize to the widget
w.w, w.h = ev.Size()
w.widget.HandleResize(tcell.NewEventResize(w.w-2, w.h-2))
}
func (w *BorderedWidget) HandleKey(ev *tcell.EventKey) bool {
@@ -94,10 +99,13 @@ func (w *BorderedWidget) SetActive(a bool) { w.active = a }
func (w *BorderedWidget) Visible() bool { return w.visible }
func (w *BorderedWidget) SetVisible(a bool) { w.visible = a }
func (w *BorderedWidget) Focusable() bool { return true }
func (w *BorderedWidget) SetTabbable(b bool) { w.tabbable = b }
func (w *BorderedWidget) Tabbable() bool { return w.tabbable }
func (w *BorderedWidget) SetX(x int) { w.x = x }
func (w *BorderedWidget) SetY(y int) { w.y = y }
func (w *BorderedWidget) GetX() int { return w.x }
func (w *BorderedWidget) GetY() int { return w.y }
func (w *BorderedWidget) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *BorderedWidget) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *BorderedWidget) GetW() int { return w.w }
func (w *BorderedWidget) GetH() int { return w.h }
@@ -106,5 +114,8 @@ func (w *BorderedWidget) SetH(h int) { w.h = h }
func (w *BorderedWidget) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *BorderedWidget) WantW() int { return w.w }
func (w *BorderedWidget) WantH() int { return w.h }
func (w *BorderedWidget) MinW() int { return 2 + w.widget.MinW() }
func (w *BorderedWidget) MinH() int { return 2 + w.widget.MinH() }
func (w *BorderedWidget) SetBorder(r []rune) { w.border = r }
func (w *BorderedWidget) SetTitle(ttl string) { w.title = ttl }

View File

@@ -38,6 +38,7 @@ type Button struct {
active bool
visible bool
tabbable bool
onPressed func() bool
}
@@ -55,9 +56,11 @@ func (w *Button) Init(id string, style tcell.Style) {
w.style = style
w.visible = true
w.onPressed = func() bool { return false }
w.tabbable = true
}
func (w *Button) Id() string { return w.id }
func (w *Button) HandleResize(ev *tcell.EventResize) {}
func (w *Button) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() }
func (w *Button) HandleKey(ev *tcell.EventKey) bool {
if !w.active {
return false
@@ -106,8 +109,8 @@ func (w *Button) Draw(screen tcell.Screen) {
}
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, fmt.Sprintf("│%s│", h.Center(lbl, w.w-2)), dStyle, screen)
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)
}
func (w *Button) Active() bool { return w.active }
func (w *Button) SetActive(a bool) { w.active = a }
@@ -117,15 +120,20 @@ func (w *Button) SetX(x int) { w.x = x }
func (w *Button) SetY(y int) { w.y = y }
func (w *Button) GetX() int { return w.x }
func (w *Button) GetY() int { return w.y }
func (w *Button) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *Button) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Button) SetW(x int) { w.w = x }
func (w *Button) SetH(y int) { w.h = y }
func (w *Button) GetW() int { return w.w }
func (w *Button) GetH() int { return w.y }
func (w *Button) WantW() int { return 2 + len(w.label) }
func (w *Button) WantW() int { return 4 + len(w.label) }
func (w *Button) WantH() int { return 3 }
func (w *Button) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Button) Focusable() bool { return true }
func (w *Button) SetTabbable(b bool) { w.tabbable = b }
func (w *Button) Tabbable() bool { return w.tabbable }
func (w *Button) MinW() int { return len(w.label) + 2 }
func (w *Button) MinH() int { return 1 }
func (w *Button) SetLabel(l string) { w.label = l }
func (w *Button) SetOnPressed(p func() bool) { w.onPressed = p }

10
chat.go
View File

@@ -37,6 +37,7 @@ type Chat struct {
w, h int
active bool
visible bool
tabbable bool
title string
rawLog []string
@@ -62,10 +63,12 @@ func (w *Chat) Init(id string, s tcell.Style) {
w.id, w.style = id, s
w.visible = true
w.initKeyMap()
w.tabbable = true
}
func (w *Chat) Id() string { return w.id }
func (w *Chat) HandleResize(ev *tcell.EventResize) {}
func (w *Chat) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() }
func (w *Chat) HandleKey(ev *tcell.EventKey) bool {
if !w.active {
return false
@@ -152,10 +155,13 @@ func (w *Chat) SetActive(a bool) { w.active = a }
func (w *Chat) Visible() bool { return w.visible }
func (w *Chat) SetVisible(a bool) { w.visible = a }
func (w *Chat) Focusable() bool { return true }
func (w *Chat) SetTabbable(b bool) { w.tabbable = b }
func (w *Chat) Tabbable() bool { return w.tabbable }
func (w *Chat) SetX(x int) { w.x = x }
func (w *Chat) SetY(y int) { w.y = y }
func (w *Chat) GetX() int { return w.x }
func (w *Chat) GetY() int { return w.y }
func (w *Chat) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *Chat) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Chat) GetW() int { return w.w }
func (w *Chat) GetH() int { return w.h }
@@ -164,6 +170,8 @@ 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) MinW() int { return 2 + 20 }
func (w *Chat) MinH() int { return 6 }
func (w *Chat) initKeyMap() {
w.keyMap = NewKeyMap(map[tcell.Key]func(ev *tcell.EventKey) bool{

View File

@@ -40,6 +40,7 @@ type Checkbox struct {
style tcell.Style
active bool
visible bool
tabbable bool
state int
x, y int
w, h int
@@ -60,9 +61,11 @@ func (w *Checkbox) Init(id string, style tcell.Style) {
w.style = style
w.visible = true
w.stateRunes = []rune{'X', ' ', '-'}
w.tabbable = true
}
func (w *Checkbox) Id() string { return w.id }
func (w *Checkbox) HandleResize(ev *tcell.EventResize) {}
func (w *Checkbox) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() }
func (w *Checkbox) HandleKey(ev *tcell.EventKey) bool {
if !w.active {
return false
@@ -96,6 +99,7 @@ func (w *Checkbox) SetX(x int) { w.x = x }
func (w *Checkbox) SetY(y int) { w.y = y }
func (w *Checkbox) GetX() int { return w.x }
func (w *Checkbox) GetY() int { return w.y }
func (w *Checkbox) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *Checkbox) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Checkbox) SetW(x int) { w.w = x }
func (w *Checkbox) SetH(y int) { w.h = y }
@@ -104,10 +108,15 @@ func (w *Checkbox) GetH() int { return w.y }
func (w *Checkbox) WantW() int {
return len(fmt.Sprintf("[%s] %s", string(w.state), w.label))
}
func (w *Checkbox) WantH() int { return 1 }
func (w *Checkbox) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Checkbox) Focusable() bool { return true }
func (w *Checkbox) SetTabbable(b bool) { w.tabbable = b }
func (w *Checkbox) Tabbable() bool { return w.tabbable }
func (w *Checkbox) MinW() int {
return len(fmt.Sprintf("[%s] %s", string(w.state), w.label))
}
func (w *Checkbox) MinH() int { return 1 }
func (w *Checkbox) SetLabel(l string) { w.label = l }
func (w *Checkbox) SetChecked(v bool) {

9
cli.go
View File

@@ -38,6 +38,7 @@ type Cli struct {
w, h int
active bool
visible bool
tabbable bool
title string
rawLog []string
@@ -67,10 +68,11 @@ func (w *Cli) Init(id string, s tcell.Style) {
w.id, w.style = id, s
w.visible = true
w.initKeyMap()
w.tabbable = true
}
func (w *Cli) Id() string { return w.id }
func (w *Cli) HandleResize(ev *tcell.EventResize) {}
func (w *Cli) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() }
func (w *Cli) HandleKey(ev *tcell.EventKey) bool {
if !w.active {
return false
@@ -157,10 +159,13 @@ func (w *Cli) SetActive(a bool) { w.active = a }
func (w *Cli) Visible() bool { return w.visible }
func (w *Cli) SetVisible(a bool) { w.visible = a }
func (w *Cli) Focusable() bool { return true }
func (w *Cli) SetTabbable(b bool) { w.tabbable = b }
func (w *Cli) Tabbable() bool { return w.tabbable }
func (w *Cli) SetX(x int) { w.x = x }
func (w *Cli) SetY(y int) { w.y = y }
func (w *Cli) GetX() int { return w.x }
func (w *Cli) GetY() int { return w.y }
func (w *Cli) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *Cli) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Cli) GetW() int { return w.w }
func (w *Cli) GetH() int { return w.h }
@@ -169,6 +174,8 @@ 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) MinW() int { return 20 }
func (w *Cli) MinH() int { return 6 }
func (w *Cli) initKeyMap() {
w.keyMap = NewKeyMap(map[tcell.Key]func(ev *tcell.EventKey) bool{

View File

@@ -38,6 +38,7 @@ type Field struct {
cursor int
visible bool
active bool
tabbable bool
x, y int
w, h int
@@ -70,10 +71,11 @@ func (w *Field) Init(id string, style tcell.Style) {
tcell.KeyEnd: w.handleEnd,
tcell.KeyCtrlU: w.clearValueBeforeCursor,
})
w.tabbable = true
}
func (w *Field) Id() string { return w.id }
func (w *Field) HandleResize(ev *tcell.EventResize) {}
func (w *Field) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() }
func (w *Field) HandleKey(ev *tcell.EventKey) bool {
if !w.active {
return false
@@ -137,6 +139,7 @@ func (w *Field) SetX(x int) { w.x = x }
func (w *Field) SetY(y int) { w.y = y }
func (w *Field) GetX() int { return w.x }
func (w *Field) GetY() int { return w.y }
func (w *Field) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *Field) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Field) SetW(wd int) { w.w = wd }
func (w *Field) SetH(h int) { w.h = h }
@@ -151,6 +154,10 @@ func (w *Field) WantH() int {
}
func (w *Field) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Field) Focusable() bool { return true }
func (w *Field) SetTabbable(b bool) { w.tabbable = b }
func (w *Field) Tabbable() bool { return w.tabbable }
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 {

View File

@@ -36,6 +36,7 @@ type FilePicker struct {
active bool
visible bool
focusable bool
tabbable bool
x, y int
w, h int
@@ -44,6 +45,8 @@ type FilePicker struct {
path string
wrkDir *os.File
layout *RelativeLayout
fileList *List
btnSelect, btnCancel *Button
}
@@ -59,13 +62,21 @@ func NewFilePicker(id string, style tcell.Style) *FilePicker {
func (w *FilePicker) Init(id string, style tcell.Style) {
w.id = id
w.style = style
w.layout = NewRelativeLayout(fmt.Sprintf("%s-layout", id), style)
w.btnSelect = NewButton(fmt.Sprintf("%s-select", id), style)
w.btnSelect.SetLabel("Select")
w.layout.Add(w.btnSelect, nil, RelAncBR)
w.btnCancel = NewButton(fmt.Sprintf("%s-cancel", id), style)
w.btnCancel.SetLabel("Cancel")
w.layout.Add(w.btnCancel, nil, RelAncBL)
w.tabbable = true
}
func (w *FilePicker) Id() string { return w.id }
func (w *FilePicker) HandleResize(ev *tcell.EventResize) {
w.w, w.h = ev.Size()
// ww, wh := w.w-2, w.h-2 // Trim border space
w.btnSelect.SetPos(Coord{X: w.x + w.w - w.btnSelect.WantW(), Y: w.y + w.h - 1})
w.btnCancel.SetPos(Coord{X: w.x + 1, Y: w.y + w.h - 1})
}
@@ -99,6 +110,7 @@ func (w *FilePicker) SetX(x int) { w.x = x }
func (w *FilePicker) SetY(y int) { w.y = y }
func (w *FilePicker) GetX() int { return w.x }
func (w *FilePicker) GetY() int { return w.y }
func (w *FilePicker) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *FilePicker) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *FilePicker) SetW(x int) { w.w = x }
func (w *FilePicker) SetH(y int) { w.h = y }
@@ -106,6 +118,8 @@ func (w *FilePicker) GetW() int { return w.w }
func (w *FilePicker) GetH() int { return w.y }
func (w *FilePicker) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *FilePicker) Focusable() bool { return w.focusable }
func (w *FilePicker) SetTabbable(b bool) { w.tabbable = b }
func (w *FilePicker) Tabbable() bool { return w.tabbable }
func (w *FilePicker) WantW() int {
// borders + the greater of the buttons next to each other or the list width
return wh.Max((w.btnSelect.WantW()+w.btnCancel.WantW()), w.fileList.WantW()) + 2
@@ -116,6 +130,14 @@ func (w *FilePicker) WantH() int {
return 2 + w.fileList.WantH() + w.btnSelect.WantH()
}
func (w *FilePicker) MinW() int {
return 2 + w.fileList.MinW() + w.btnSelect.MinW() + w.btnCancel.MinW()
}
func (w *FilePicker) MinH() int {
return 2 + w.fileList.MinH() + w.btnSelect.MinH()
}
func (w *FilePicker) SetTitle(ttl string) { w.title = ttl }
func (w *FilePicker) SetPath(path string) error {
var err error

264
linear_layout.go Normal file
View File

@@ -0,0 +1,264 @@
/*
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 (
h "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell"
)
// LinearLayout lays out all widgets added one after the other
type LinearLayout struct {
id string
style tcell.Style
orientation LinearLayoutOrient
x, y int
w, h int
widgets []Widget
active bool
visible bool
tabbable bool
disableTab bool
cursor int
}
type LinearLayoutOrient int
const (
LinLayV = LinearLayoutOrient(iota)
LinLayH
)
func NewLinearLayout(id string, s tcell.Style) *LinearLayout {
ret := &LinearLayout{}
ret.Init(id, s)
return ret
}
func (w *LinearLayout) Init(id string, s tcell.Style) {
w.id = id
w.style = s
w.visible = true
}
func (w *LinearLayout) Id() string { return w.id }
func (w *LinearLayout) HandleResize(ev *tcell.EventResize) {
w.w, w.h = ev.Size()
w.updateWidgetLayouts()
}
func (w *LinearLayout) HandleKey(ev *tcell.EventKey) bool {
if !w.disableTab && ev.Key() == tcell.KeyTab {
fndP := -1
for i := w.cursor; i < len(w.widgets); i++ {
if fndP == -1 {
if w.widgets[i].Active() {
fndP = i
w.widgets[i].SetActive(false)
continue
}
} else {
if w.widgets[i].Focusable() && w.widgets[i].Tabbable() {
w.widgets[i].SetActive(true)
return true
}
}
}
// If we're here, we hit the last widget, loop
if fndP == -1 { // But didn't even find the active one
return false
}
for i := 0; i < fndP; i++ {
if w.widgets[i].Focusable() && w.widgets[i].Tabbable() {
w.widgets[i].SetActive(true)
return true
}
}
return false
}
for _, wi := range w.widgets {
if wi.HandleKey(ev) {
return true
}
}
return false
}
func (w *LinearLayout) HandleTime(ev *tcell.EventTime) {
for _, wi := range w.widgets {
wi.HandleTime(ev)
}
}
func (w *LinearLayout) Draw(screen tcell.Screen) {
if !w.visible {
return
}
p := w.GetPos()
for _, wd := range w.widgets {
o := wd.GetPos()
wd.SetPos(p.Add(o))
wd.Draw(screen)
wd.SetPos(o)
}
}
func (w *LinearLayout) Active() bool { return w.active }
func (w *LinearLayout) SetActive(a bool) { w.active = a }
func (w *LinearLayout) Visible() bool { return w.visible }
func (w *LinearLayout) SetVisible(a bool) { w.visible = a }
func (w *LinearLayout) Focusable() bool { return true }
func (w *LinearLayout) SetTabbable(b bool) { w.tabbable = b }
func (w *LinearLayout) Tabbable() bool { return w.tabbable }
func (w *LinearLayout) SetX(x int) { w.x = x }
func (w *LinearLayout) SetY(y int) { w.y = y }
func (w *LinearLayout) GetX() int { return w.x }
func (w *LinearLayout) GetY() int { return w.y }
func (w *LinearLayout) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *LinearLayout) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *LinearLayout) GetW() int { return w.w }
func (w *LinearLayout) GetH() int { return w.h }
func (w *LinearLayout) SetW(wd int) { w.w = wd }
func (w *LinearLayout) SetH(h int) { w.h = h }
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 {
// Find the highest want of all widgets
wantW = h.Max(wd.WantW(), wantW)
} else if w.orientation == LinLayH {
// Find the sum of all widget widgets wants
wantW = wantW + wd.WantW()
}
}
return wantW
}
func (w *LinearLayout) WantH() int {
var wantH int
for _, wd := range w.widgets {
if w.orientation == LinLayV {
// Find the sum of all widget widgets wants
wantH = wantH + wd.WantH()
} else if w.orientation == LinLayH {
// Find the highest want of all widgets
wantH = h.Max(wd.WantH(), wantH)
}
}
return wantH
}
func (w *LinearLayout) MinW() int {
var minW int
for _, wd := range w.widgets {
if w.orientation == LinLayV {
// Find the highest minimum width of all widgets
minW = h.Max(wd.MinW(), minW)
} else if w.orientation == LinLayH {
// Find the sum of all widget minimum widgets
minW = minW + wd.MinW()
}
}
return minW
}
func (w *LinearLayout) MinH() int {
var minH int
for _, wd := range w.widgets {
if w.orientation == LinLayV {
minH = minH + wd.MinH()
} else if w.orientation == LinLayH {
minH = h.Max(wd.MinH(), minH)
}
}
return minH
}
func (w *LinearLayout) Append(n Widget) { w.widgets = append(w.widgets, n) }
func (w *LinearLayout) Delete(n Widget) {
for i := 0; i < len(w.widgets); i++ {
if w.widgets[i] == n {
w.DeleteIndex(i)
return
}
}
}
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)
}
}
// 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()
}
}
}
// 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
}
func (w *LinearLayout) getRelPos(wd Widget) Coord {
return Coord{}
}
func (w *LinearLayout) getAbsPos(wd Widget) Coord {
rel := w.getRelPos(wd)
return rel.Add(Coord{X: w.x, Y: w.y})
}

15
list.go
View File

@@ -33,6 +33,7 @@ type List struct {
active bool
visible bool
focusable bool
tabbable bool
x, y int
w, h int
@@ -83,6 +84,7 @@ func (w *List) Init(id string, style tcell.Style) {
return false
})
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) {}
@@ -136,6 +138,7 @@ func (w *List) SetX(x int) { w.x = x }
func (w *List) SetY(y int) { w.y = y }
func (w *List) GetX() int { return w.x }
func (w *List) GetY() int { return w.y }
func (w *List) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *List) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *List) SetW(x int) { w.w = x }
func (w *List) SetH(y int) { w.h = y }
@@ -143,6 +146,8 @@ func (w *List) GetW() int { return w.w }
func (w *List) GetH() int { return w.y }
func (w *List) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
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)
if len(w.border) > 0 {
@@ -159,6 +164,16 @@ func (w *List) WantH() int {
return lng
}
func (w *List) MinW() int {
lng := h.Longest(w.list)
if lng > 80 {
lng = 80
}
return 2 + lng
}
func (w *List) MinH() int { return 4 }
func (w *List) SetFocusable(f bool) { w.focusable = f }
func (w *List) SetCursorWrap(b bool) { w.cursorWrap = b }

36
menu.go
View File

@@ -32,6 +32,7 @@ type Menu struct {
style tcell.Style
active bool
visible bool
tabbable bool
x, y int
w, h int
@@ -76,6 +77,7 @@ func (w *Menu) Init(id string, style tcell.Style) {
return false
},
})
w.tabbable = true
}
func (w *Menu) Id() string { return w.id }
func (w *Menu) HandleResize(ev *tcell.EventResize) {}
@@ -154,6 +156,7 @@ func (w *Menu) SetX(x int) { w.x = x }
func (w *Menu) SetY(y int) { w.y = y }
func (w *Menu) GetX() int { return w.x }
func (w *Menu) GetY() int { return w.y }
func (w *Menu) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *Menu) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Menu) SetW(x int) { w.w = x }
func (w *Menu) SetH(y int) { w.h = y }
@@ -161,6 +164,8 @@ func (w *Menu) GetW() int { return w.w }
func (w *Menu) GetH() int { return w.y }
func (w *Menu) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Menu) Focusable() bool { return true }
func (w *Menu) SetTabbable(b bool) { w.tabbable = b }
func (w *Menu) Tabbable() bool { return w.tabbable }
func (w *Menu) WantW() int {
var maxW int
@@ -186,6 +191,37 @@ func (w *Menu) WantH() int {
return ret + len(w.items)
}
}
func (w *Menu) MinW() int {
labels := []string{w.label}
for i := range w.items {
labels = append(labels, w.items[i].label)
}
switch w.menuType {
case MenuTypeH:
wrk := 0
for i := range labels {
wrk += len(labels[i])
}
return wrk
case MenuTypeV:
return h.Longest(labels)
}
return 0
}
func (w *Menu) MinH() int {
switch w.menuType {
case MenuTypeH:
return 1
case MenuTypeV:
if len(w.label) > 0 {
return 1 + len(w.items)
}
return len(w.items)
}
return 0
}
func (w *Menu) SetType(tp MenuType) { w.menuType = tp }
func (w *Menu) SetLabel(lbl string) { w.label = lbl }
func (w *Menu) SetFocusable(f bool) {}

View File

@@ -32,6 +32,7 @@ type MenuItem struct {
style tcell.Style
active bool
visible bool
tabbable bool
x, y int
w, h int
@@ -72,6 +73,7 @@ func (w *MenuItem) Init(id string, style tcell.Style) {
for i := range w.items {
w.items[i].SetActive(i == w.cursor)
}
w.tabbable = true
}
func (w *MenuItem) Id() string { return w.id }
func (w *MenuItem) HandleResize(ev *tcell.EventResize) {}
@@ -101,7 +103,10 @@ func (w *MenuItem) Draw(screen tcell.Screen) {
h.DrawText(x, y, h.PadR(w.label, wd), st, screen)
y += 1
if w.expanded {
x += 2
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)
}
x += 1
for i := range w.items {
w.items[i].SetPos(Coord{X: x, Y: y})
w.items[i].Draw(screen)
@@ -122,6 +127,7 @@ func (w *MenuItem) SetX(x int) { w.x = x }
func (w *MenuItem) SetY(y int) { w.y = y }
func (w *MenuItem) GetX() int { return w.x }
func (w *MenuItem) GetY() int { return w.y }
func (w *MenuItem) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *MenuItem) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *MenuItem) SetW(x int) { w.w = x }
func (w *MenuItem) SetH(y int) { w.h = y }
@@ -132,7 +138,7 @@ func (w *MenuItem) WantW() int {
if len(w.items) > 0 {
for i := range w.items {
if w.items[i].WantW() > ret {
ret = w.items[i].WantW()
ret = w.items[i].WantW() + 1
// TODO: Figure offset of subitems
}
}
@@ -151,6 +157,8 @@ func (w *MenuItem) WantH() int {
}
func (w *MenuItem) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *MenuItem) Focusable() bool { return !w.disabled }
func (w *MenuItem) SetTabbable(b bool) { w.tabbable = b }
func (w *MenuItem) Tabbable() bool { return w.tabbable }
// How much width this item wants
func (w *MenuItem) Expand(e bool) {

View File

@@ -37,6 +37,7 @@ type Prompt struct {
w, h int
active bool
visible bool
tabbable bool
title string
message *Text
@@ -62,6 +63,7 @@ func (w *Prompt) Init(id string, style tcell.Style) {
w.btnOk.SetLabel("Ok")
w.btnCancel = NewButton(fmt.Sprintf("%s-cancel", id), style)
w.btnCancel.SetLabel("Cancel")
w.tabbable = true
}
func (w *Prompt) Id() string { return w.id }
func (w *Prompt) HandleResize(ev *tcell.EventResize) {
@@ -99,6 +101,7 @@ func (w *Prompt) SetX(x int) { w.x = x }
func (w *Prompt) SetY(y int) { w.y = y }
func (w *Prompt) GetX() int { return w.x }
func (w *Prompt) GetY() int { return w.y }
func (w *Prompt) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *Prompt) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Prompt) SetW(x int) { w.w = x }
func (w *Prompt) SetH(y int) { w.h = y }
@@ -106,6 +109,8 @@ func (w *Prompt) GetW() int { return w.w }
func (w *Prompt) GetH() int { return w.y }
func (w *Prompt) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Prompt) Focusable() bool { return true }
func (w *Prompt) SetTabbable(b bool) { w.tabbable = b }
func (w *Prompt) Tabbable() bool { return w.tabbable }
func (w *Prompt) WantW() int {
return w.btnOk.WantW() + w.btnCancel.WantW() + 4
}
@@ -120,4 +125,12 @@ func (w *Prompt) SetMessage(msg string) {
w.message.SetText(msg)
}
func (w *Prompt) MinW() int {
return 2 + w.field.MinW() + w.btnOk.MinW() + w.btnCancel.MinW() + w.message.MinW()
}
func (w *Prompt) MinH() int {
return 2 + w.field.MinH() + w.btnOk.MinH() + w.message.MinH()
}
func (w *Prompt) SetOnOk(f func(string) bool) { w.onOk = f }

View File

@@ -26,10 +26,13 @@ import "github.com/gdamore/tcell"
type RelativeLayout struct {
id string
style tcell.Style
widgetRelations map[Widget]widgetRelation
widgetRelations map[Widget][]widgetRelation
widgets []Widget
active bool
visible bool
tabbable bool
x, y int
w, h int
}
@@ -44,14 +47,19 @@ type widgetRelation struct {
type RelativeRelation int
const (
RRAbove = iota // Above Widget
RRToRightOf // To Right of Widget
RRBelow // Below Widget
RRToLeftOf // To Left of Widget
RATop // Anchored to parent Top
RARight // Anchored to parent Right
RABottom // Anchored to parent Bottom
RALeft // Anchored to parent Left
RelRelAbove = iota // Above Widget
RelRelToRightOf // To Right of Widget
RelRelBelow // Below Widget
RelRelToLeftOf // To Left of Widget
RelAncTL // Anchored to parent Top-Left
RelAncT // Anchored to parent Top
RelAncTR // Anchored to parent Top-Right
RelAncL // Anchored to parent Left
RelAncC // Anchored to parent Center
RelAncR // Anchored to parent Right
RelAncBL // Anchored to parent Bottom-Left
RelAncB // Anchored to parent Bottom
RelAncBR // Anchored to parent Bottom-Right
)
func NewRelativeLayout(id string, s tcell.Style) *RelativeLayout {
@@ -64,6 +72,7 @@ func (w *RelativeLayout) Init(id string, style tcell.Style) {
w.id = id
w.style = style
w.visible = true
w.tabbable = true
}
func (w *RelativeLayout) Id() string { return w.id }
func (w *RelativeLayout) HandleResize(ev *tcell.EventResize) {}
@@ -73,6 +82,8 @@ func (w *RelativeLayout) Draw(screen tcell.Screen) {
if !w.visible {
return
}
// All widgets should have correct (relative) positions
/*
done := make(map[Widget]widgetRelation)
rem := make(map[Widget]widgetRelation)
for k, v := range w.widgetRelations {
@@ -82,16 +93,21 @@ func (w *RelativeLayout) Draw(screen tcell.Screen) {
rem[k] = v
}
}
*/
}
func (w *RelativeLayout) Active() bool { return w.active }
func (w *RelativeLayout) SetActive(a bool) { w.active = a }
func (w *RelativeLayout) Visible() bool { return w.visible }
func (w *RelativeLayout) SetVisible(a bool) { w.visible = a }
func (w *RelativeLayout) Focusable() bool { return true }
func (w *RelativeLayout) SetTabbable(b bool) { w.tabbable = b }
func (w *RelativeLayout) Tabbable() bool { return w.tabbable }
func (w *RelativeLayout) SetX(x int) { w.x = x }
func (w *RelativeLayout) SetY(y int) { w.y = y }
func (w *RelativeLayout) GetX() int { return w.x }
func (w *RelativeLayout) GetY() int { return w.y }
func (w *RelativeLayout) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *RelativeLayout) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *RelativeLayout) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *RelativeLayout) SetW(wd int) { w.w = wd }
func (w *RelativeLayout) SetH(h int) { w.h = h }
@@ -99,7 +115,29 @@ func (w *RelativeLayout) GetW() int { return w.w }
func (w *RelativeLayout) GetH() int { return w.h }
func (w *RelativeLayout) WantW() int { return 1 }
func (w *RelativeLayout) WantH() int { return 1 }
func (w *RelativeLayout) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *RelativeLayout) MinW() int {
// Find the highest value for x in all widgets GetX() + MinW()
var minW int
for _, wd := range w.widgets {
wrk := wd.GetX() + wd.MinW()
if wrk > minW {
minW = wrk
}
}
return minW
}
func (w *RelativeLayout) MinH() int {
// Find the highest value for y in all widgets GetY() + MinH()
var minH int
for _, wd := range w.widgets {
wrk := wd.GetY() + wd.MinH()
if wrk > minH {
minH = wrk
}
}
return minH
}
func (w *RelativeLayout) Add(n Widget, relTo Widget, relation RelativeRelation) {
w.widgetRelations[n] = widgetRelation{
@@ -107,3 +145,6 @@ func (w *RelativeLayout) Add(n Widget, relTo Widget, relation RelativeRelation)
relTo: relTo,
}
}
func (w *RelativeLayout) AddRelation(n Widget, relation RelativeRelation, relTo Widget) {
}

View File

@@ -37,6 +37,7 @@ type Searcher struct {
w, h int
active bool
visible bool
tabbable bool
title string
search *Field
@@ -77,6 +78,7 @@ func (w *Searcher) Init(id string, style tcell.Style) {
tcell.KeyPgDn: w.handleKeyPgDn,
tcell.KeyEnter: w.handleKeyEnter,
})
w.tabbable = true
}
func (w *Searcher) Id() string { return w.id }
@@ -222,6 +224,7 @@ func (w *Searcher) WantH() int {
return 2 + w.search.WantH() + len(w.filteredData) // Border + Field + Data
}
func (w *Searcher) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *Searcher) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Searcher) SetW(x int) { w.w = x }
func (w *Searcher) SetH(y int) { w.h = y }
@@ -229,6 +232,15 @@ func (w *Searcher) GetW() int { return w.w }
func (w *Searcher) GetH() int { return w.y }
func (w *Searcher) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Searcher) Focusable() bool { return true }
func (w *Searcher) SetTabbable(b bool) { w.tabbable = b }
func (w *Searcher) Tabbable() bool { return w.tabbable }
func (w *Searcher) MinW() int {
return 2 + w.search.MinW()
}
func (w *Searcher) MinH() int {
return 2 + w.search.MinH() + 5
}
func (w *Searcher) SetHideOnSelect(t bool) { w.hideOnSelect = t }
func (w *Searcher) SetTitle(ttl string) { w.title = ttl }

View File

@@ -36,6 +36,7 @@ type Table struct {
active bool
visible bool
focusable bool
tabbable bool
header []string
footer []string
@@ -82,6 +83,7 @@ func (w *Table) Init(id string, style tcell.Style) {
w.style = style
w.visible = true
w.border = h.BRD_CSIMPLE
w.tabbable = true
}
func (w *Table) Id() string { return w.id }
func (w *Table) HandleResize(ev *tcell.EventResize) {}
@@ -147,6 +149,7 @@ func (w *Table) SetX(x int) { w.x = x }
func (w *Table) SetY(y int) { w.y = y }
func (w *Table) GetX() int { return w.x }
func (w *Table) GetY() int { return w.y }
func (w *Table) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *Table) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Table) SetW(x int) { w.w = x }
func (w *Table) SetH(y int) { w.h = y }
@@ -198,8 +201,55 @@ func (w *Table) WantH() int {
return datLen
}
func (w *Table) MinW() int {
if w.minimized {
return len(fmt.Sprintf("├%s (%d rows)┤", w.title, len(w.data)))
}
// For each column, find the longest (in header, data, and footer)
var totalW int
colCnt := h.Max(len(w.header), len(w.footer))
for i := range w.data {
colCnt = h.Max(colCnt, len(w.data[i]))
}
for i := 0; i < colCnt; i++ {
var cols []int
if len(w.header) > i {
cols = append(cols, len(w.header[i]))
}
for j := range w.data {
if len(w.data[j]) > i {
cols = append(cols, len(w.data[j][i]))
}
}
if len(w.footer) > i {
cols = append(cols, len(w.footer[i]))
}
totalW += h.Max(cols...)
}
return totalW
}
func (w *Table) MinH() int {
if w.minimized {
return 1
}
datLen := h.Min(len(w.data)+2, 7) // Data length + Border
if len(w.header) > 0 {
datLen += len(w.header) + 1 // Header length + separator
}
if datLen == 0 {
datLen = 1
}
if len(w.footer) > 0 {
datLen += len(w.footer) + 1 // Footer length + separator
}
return datLen
}
func (w *Table) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *Table) Focusable() bool { return w.focusable }
func (w *Table) SetTabbable(b bool) { w.tabbable = b }
func (w *Table) Tabbable() bool { return w.tabbable }
func (w *Table) SetTitle(ttl string) { w.title = ttl }
func (w *Table) SetFocusable(f bool) { w.focusable = f }

21
text.go
View File

@@ -22,6 +22,8 @@ THE SOFTWARE.
package widgets
import (
"strings"
h "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell"
)
@@ -33,6 +35,7 @@ type Text struct {
x, y int
w, h int
visible bool
tabbable bool
}
var _ Widget = (*Text)(nil)
@@ -47,6 +50,7 @@ func (w *Text) Init(id string, style tcell.Style) {
w.id = id
w.style = style
w.visible = true
w.tabbable = false
}
func (w *Text) Id() string { return w.id }
func (w *Text) HandleResize(ev *tcell.EventResize) {}
@@ -56,7 +60,17 @@ func (w *Text) Draw(screen tcell.Screen) {
if !w.visible {
return
}
h.DrawText(w.x, w.y, w.text, w.style, screen)
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)
y++
}
}
func (w *Text) Active() bool { return false }
func (w *Text) SetActive(a bool) {}
@@ -66,6 +80,7 @@ func (w *Text) SetX(x int) { w.x = x }
func (w *Text) SetY(y int) { w.y = y }
func (w *Text) GetX() int { return w.x }
func (w *Text) GetY() int { return w.y }
func (w *Text) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *Text) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *Text) SetW(x int) { w.w = x }
func (w *Text) SetH(y int) { w.h = y }
@@ -75,6 +90,10 @@ 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) 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) SetText(txt string) { w.text = txt }
func (w *Text) GetText() string { return w.text }

272
timefield.go Normal file
View File

@@ -0,0 +1,272 @@
/*
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 (
"fmt"
"time"
h "git.bullercodeworks.com/brian/tcell-widgets/helpers"
"github.com/gdamore/tcell"
)
type TimeField struct {
id string
label string
style tcell.Style
active bool
visible bool
tabbable bool
x, y int
w, h int
wantW, wantH int
value time.Time
hasDate bool
hasTime bool
hasSeconds bool
showNowBtn bool
nowBtnActive bool
cursor int
keyMap KeyMap
}
// TODO: Allow changing the format.
// For now it's just yyyy-mm-dd hh:mm:ss
var _ Widget = (*TimeField)(nil)
func NewTimeField(id string, style tcell.Style) *TimeField {
ret := &TimeField{style: style}
ret.Init(id, style)
return ret
}
func (w *TimeField) Init(id string, style tcell.Style) {
w.id = id
w.style = style
w.hasDate = true
w.hasTime = true
w.showNowBtn = true
w.keyMap = NewKeyMap(map[tcell.Key]func(ev *tcell.EventKey) bool{
tcell.KeyLeft: w.handleLeft,
tcell.KeyRight: w.handleRight,
tcell.KeyHome: w.handleHome,
tcell.KeyEnd: w.handleEnd,
})
w.tabbable = true
}
func (w *TimeField) Id() string { return w.id }
func (w *TimeField) HandleResize(ev *tcell.EventResize) {}
func (w *TimeField) HandleKey(ev *tcell.EventKey) bool {
if !w.active {
return false
}
return false
}
func (w *TimeField) HandleTime(ev *tcell.EventTime) {}
func (w *TimeField) Draw(screen tcell.Screen) {
if !w.visible {
return
}
ds := w.style
if !w.active {
ds = ds.Dim(true)
}
x := w.x
labelW := len(w.label)
if labelW > 0 {
h.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)
x++
}
if idx == w.cursor && !w.nowBtnActive {
if w.active {
h.DrawText(x, w.y, string(vl), ds.Reverse(true).Blink(true), screen)
} else {
h.DrawText(x, w.y, string(vl), ds, screen)
}
} else {
h.DrawText(x, w.y, string(vl), ds, screen)
}
x++
}
}
if w.hasTime {
hr, mn, sc := w.value.Hour(), w.value.Minute(), w.value.Second()
txt := fmt.Sprintf("%2d%2d", hr, mn)
if w.hasSeconds {
txt = fmt.Sprintf("%s%2d", txt, sc)
}
for idx, vl := range txt {
if idx == 2 || idx == 5 {
h.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)
} else {
h.DrawText(x, w.y, string(vl), ds, screen)
}
} else {
h.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)
} else {
h.DrawText(x, w.y, "[ Now ]", ds, screen)
}
}
}
func (w *TimeField) Active() bool { return w.active }
func (w *TimeField) SetActive(a bool) { w.active = a }
func (w *TimeField) Visible() bool { return w.visible }
func (w *TimeField) SetVisible(a bool) { w.visible = a }
func (w *TimeField) SetX(x int) { w.x = x }
func (w *TimeField) SetY(y int) { w.y = y }
func (w *TimeField) GetX() int { return w.x }
func (w *TimeField) GetY() int { return w.y }
func (w *TimeField) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
func (w *TimeField) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
func (w *TimeField) SetW(x int) { w.w = x }
func (w *TimeField) SetH(y int) { w.h = y }
func (w *TimeField) GetW() int { return w.w }
func (w *TimeField) GetH() int { return w.y }
func (w *TimeField) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
func (w *TimeField) Focusable() bool { return true }
func (w *TimeField) SetTabbable(b bool) { w.tabbable = b }
func (w *TimeField) Tabbable() bool { return w.tabbable }
func (w *TimeField) WantW() int {
wdt := 0
if w.hasDate {
wdt = 10 // yyyy-mm-dd
}
if w.hasTime {
if w.hasDate {
wdt += 1 // space between date & time
}
if w.hasSeconds {
wdt += 8 // hh:mm:ss
} else {
wdt += 5 // hh:mm
}
}
return wdt
}
func (w *TimeField) WantH() int { return 1 }
func (w *TimeField) MinW() int {
wdt := 0
if w.hasDate {
wdt = 10 // yyyy-mm-dd
}
if w.hasTime {
if w.hasDate {
wdt += 1 // space between date & time
}
if w.hasSeconds {
wdt += 8 // hh:mm:ss
} else {
wdt += 5 // hh:mm
}
}
return wdt
}
func (w *TimeField) MinH() int { return 1 }
func (w *TimeField) SetLabel(lbl string) { w.label = lbl }
func (w *TimeField) SetTime(tm time.Time) { w.value = tm }
func (w *TimeField) SetHasSeconds(b bool) { w.hasSeconds = b }
func (w *TimeField) SetHasTime(b bool) { w.hasTime = b }
func (w *TimeField) SetHasDate(b bool) { w.hasDate = b }
func (w *TimeField) SetValue(v time.Time) { w.value = v }
func (w *TimeField) Value() time.Time { return w.value }
func (w *TimeField) handleLeft(ev *tcell.EventKey) bool {
if w.nowBtnActive {
w.cursor = w.cursorLength()
return true
} else if w.cursor > 0 {
w.cursor--
return true
}
return false
}
func (w *TimeField) handleRight(ev *tcell.EventKey) bool {
if w.cursor < w.cursorLength() {
w.cursor++
return true
} else if !w.nowBtnActive {
w.nowBtnActive = true
return true
}
return false
}
func (w *TimeField) handleHome(ev *tcell.EventKey) bool {
if w.cursor != 0 {
w.cursor = 0
return true
}
return false
}
func (w *TimeField) handleEnd(ev *tcell.EventKey) bool {
l := w.cursorLength()
if w.cursor != l {
w.cursor = w.cursorLength()
return true
}
return false
}
func (w *TimeField) cursorLength() int {
var ret int
if w.hasDate {
ret += 8
}
if w.hasTime {
ret += 4
if w.hasSeconds {
ret += 2
}
}
return ret
}

View File

@@ -37,10 +37,13 @@ type Widget interface {
Visible() bool
SetVisible(bool)
Focusable() bool
Tabbable() bool
SetTabbable(bool)
SetX(int)
SetY(int)
GetX() int
GetY() int
GetPos() Coord
SetPos(Coord)
// Whatever is managing this widget (parent widget, screen, etc) should
// tell it exactly what the Width & Height are.
@@ -52,6 +55,9 @@ type Widget interface {
// Given infinite space, WantW & WantH are what this widget wants
WantW() int
WantH() int
// MinW & MinH are what this widget must have to be functional.
MinW() int
MinH() int
SetSize(Coord)
}