diff --git a/absolute_layout.go b/absolute_layout.go index fe863ce..730477a 100644 --- a/absolute_layout.go +++ b/absolute_layout.go @@ -22,8 +22,6 @@ THE SOFTWARE. package widgets import ( - "fmt" - "github.com/gdamore/tcell" ) @@ -31,16 +29,21 @@ type AbsoluteLayout struct { id string style tcell.Style - x, y int - w, h int - widgets []Widget - wCoords map[Widget]Coord - wAnchor map[Widget]AbsoluteAnchor + x, y int + w, h int + widgets []Widget + wCoords map[Widget]Coord + wAnchor map[Widget]AbsoluteAnchor + wManualSizes map[Widget]Coord defAnchor AbsoluteAnchor - active bool - visible bool + 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 - AnchorT // x,y starts at , - AnchorTR // x,y starts at , - AnchorR // x,y starts at , - AnchorBR // x,y starts at , - AnchorB // x,y starts at , - AnchorBL // x,y starts at , - AnchorL // x,y starts at , + AnchorTL = AbsoluteAnchor(iota) // widget starts at , + AnchorT // widget starts at -, + AnchorTR // widget starts at , + AnchorL // widget starts at , + AnchorC // widget starts at -, - + AnchorR // widget starts at -, - + AnchorBL // widget starts at , - + AnchorB // widget starts at -, - + AnchorBR // widget starts at -, - 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,65 +133,57 @@ 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 } -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) 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) 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 } -func (w *AbsoluteLayout) SetW(wd int) { w.w = wd } -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) Active() bool { return w.active } +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 } +func (w *AbsoluteLayout) SetW(wd int) { w.w = wd } +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}) +} diff --git a/alert.go b/alert.go index a3fe3d9..b4f5a3f 100644 --- a/alert.go +++ b/alert.go @@ -33,14 +33,18 @@ type Alert struct { id string style tcell.Style - x, y int - w, h int - active bool - visible bool + x, y int + 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,36 +128,68 @@ 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) -} -func (w *Alert) Active() bool { return w.active } -func (w *Alert) SetActive(a bool) { w.active = a } -func (w *Alert) Visible() bool { return w.visible } -func (w *Alert) SetVisible(a bool) { w.visible = a } -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) 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 } -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 -} + wh.TitledBorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.title, wh.BRD_SIMPLE, w.style, 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 } +func (w *Alert) Visible() bool { return w.visible } +func (w *Alert) SetVisible(a bool) { w.visible = a } +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 } +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) 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 } diff --git a/bordered_widget.go b/bordered_widget.go index f698556..ec29013 100644 --- a/bordered_widget.go +++ b/bordered_widget.go @@ -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 @@ -35,9 +36,10 @@ type BorderedWidget struct { widget Widget border []rune - title string - active bool - visible bool + 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 { @@ -89,22 +94,28 @@ func (w *BorderedWidget) Draw(screen tcell.Screen) { w.widget.SetPos(Coord{X: w.x + 1, Y: w.y + 1}) w.widget.Draw(screen) } -func (w *BorderedWidget) Active() bool { return w.active } -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) 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) 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 } -func (w *BorderedWidget) SetW(wd int) { w.w = wd } -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) Active() bool { return w.active } +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 } +func (w *BorderedWidget) SetW(wd int) { w.w = wd } +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) SetBorder(r []rune) { w.border = r } +func (w *BorderedWidget) SetTitle(ttl string) { w.title = ttl } diff --git a/button.go b/button.go index 3f0b6c1..882a222 100644 --- a/button.go +++ b/button.go @@ -36,8 +36,9 @@ type Button struct { x, y int w, h int - active bool - visible bool + 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,26 +109,31 @@ 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 } -func (w *Button) Visible() bool { return w.visible } -func (w *Button) SetVisible(a bool) { w.visible = a } -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) 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) 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) Active() bool { return w.active } +func (w *Button) SetActive(a bool) { w.active = a } +func (w *Button) Visible() bool { return w.visible } +func (w *Button) SetVisible(a bool) { w.visible = a } +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 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 } diff --git a/chat.go b/chat.go index c9da3b8..15a5a7c 100644 --- a/chat.go +++ b/chat.go @@ -33,10 +33,11 @@ type Chat struct { id string style tcell.Style - x, y int - w, h int - active bool - visible bool + x, y int + 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 @@ -147,23 +150,28 @@ func (w *Chat) Draw(screen tcell.Screen) { // x += len(post) - 1 } -func (w *Chat) Active() bool { return w.active } -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) 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) 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 } -func (w *Chat) SetW(wd int) { w.w = wd } -func (w *Chat) SetH(h int) { w.h = h } -func (w *Chat) SetSize(c Coord) { w.w, w.h = c.X, c.Y } -func (w *Chat) WantW() int { return w.w } -func (w *Chat) WantH() int { return w.h } +func (w *Chat) Active() bool { return w.active } +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 } +func (w *Chat) SetW(wd int) { w.w = wd } +func (w *Chat) SetH(h int) { w.h = h } +func (w *Chat) SetSize(c Coord) { w.w, w.h = c.X, c.Y } +func (w *Chat) WantW() int { return w.w } +func (w *Chat) WantH() int { return w.h } +func (w *Chat) 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{ diff --git a/checkbox.go b/checkbox.go index e678901..2d83043 100644 --- a/checkbox.go +++ b/checkbox.go @@ -35,14 +35,15 @@ const ( ) type Checkbox struct { - id string - label string - style tcell.Style - active bool - visible bool - state int - x, y int - w, h int + id string + label string + style tcell.Style + active bool + visible bool + tabbable bool + state int + x, y int + w, h int stateRunes []rune } @@ -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) 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) { diff --git a/cli.go b/cli.go index d9cb9b7..4f02f13 100644 --- a/cli.go +++ b/cli.go @@ -34,10 +34,11 @@ type Cli struct { id string style tcell.Style - x, y int - w, h int - active bool - visible bool + x, y int + 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 @@ -152,23 +154,28 @@ func (w *Cli) Draw(screen tcell.Screen) { // x += len(post) - 1 } -func (w *Cli) Active() bool { return w.active } -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) 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) 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 } -func (w *Cli) SetW(wd int) { w.w = wd } -func (w *Cli) SetH(h int) { w.h = h } -func (w *Cli) SetSize(c Coord) { w.w, w.h = c.X, c.Y } -func (w *Cli) WantW() int { return w.w } -func (w *Cli) WantH() int { return w.h } +func (w *Cli) Active() bool { return w.active } +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 } +func (w *Cli) SetW(wd int) { w.w = wd } +func (w *Cli) SetH(h int) { w.h = h } +func (w *Cli) SetSize(c Coord) { w.w, w.h = c.X, c.Y } +func (w *Cli) WantW() int { return w.w } +func (w *Cli) WantH() int { return w.h } +func (w *Cli) 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{ diff --git a/field.go b/field.go index 7958c93..bc08045 100644 --- a/field.go +++ b/field.go @@ -35,11 +35,12 @@ type Field struct { label string value string - cursor int - visible bool - active bool - x, y int - w, h int + cursor int + visible bool + active bool + tabbable bool + x, y int + w, h int filter func(*tcell.EventKey) bool onChange func(prev, curr string) @@ -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 } @@ -149,8 +152,12 @@ func (w *Field) WantW() int { func (w *Field) WantH() int { return 1 } -func (w *Field) SetSize(c Coord) { w.w, w.h = c.X, c.Y } -func (w *Field) Focusable() bool { return true } +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 { diff --git a/filepicker.go b/filepicker.go index 467bfe1..67c29df 100644 --- a/filepicker.go +++ b/filepicker.go @@ -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}) } @@ -91,21 +102,24 @@ func (w *FilePicker) Draw(screen tcell.Screen) { w.btnSelect.Draw(screen) w.btnCancel.Draw(screen) } -func (w *FilePicker) Active() bool { return w.active } -func (w *FilePicker) SetActive(a bool) { w.active = a } -func (w *FilePicker) Visible() bool { return w.visible } -func (w *FilePicker) SetVisible(a bool) { w.visible = a } -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) 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 } -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) Active() bool { return w.active } +func (w *FilePicker) SetActive(a bool) { w.active = a } +func (w *FilePicker) Visible() bool { return w.visible } +func (w *FilePicker) SetVisible(a bool) { w.visible = a } +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 } +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 diff --git a/linear_layout.go b/linear_layout.go new file mode 100644 index 0000000..8c1d63c --- /dev/null +++ b/linear_layout.go @@ -0,0 +1,264 @@ +/* +Copyright © Brian Buller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package widgets + +import ( + 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}) +} diff --git a/list.go b/list.go index b6a97d6..1a1df42 100644 --- a/list.go +++ b/list.go @@ -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) {} @@ -128,21 +130,24 @@ func (w *List) Draw(screen tcell.Screen) { y += 1 } } -func (w *List) Active() bool { return w.active } -func (w *List) SetActive(a bool) { w.active = a } -func (w *List) Visible() bool { return w.visible } -func (w *List) SetVisible(a bool) { w.visible = a } -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) 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 } -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) Active() bool { return w.active } +func (w *List) SetActive(a bool) { w.active = a } +func (w *List) Visible() bool { return w.visible } +func (w *List) SetVisible(a bool) { w.visible = a } +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 } +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 } diff --git a/menu.go b/menu.go index 14f8395..7d0153f 100644 --- a/menu.go +++ b/menu.go @@ -27,13 +27,14 @@ import ( ) type Menu struct { - id string - label string - style tcell.Style - active bool - visible bool - x, y int - w, h int + id string + label string + style tcell.Style + active bool + visible bool + tabbable bool + x, y int + w, h int menuType MenuType cursor 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) {} @@ -146,21 +148,24 @@ func (w *Menu) drawVMenu(screen tcell.Screen) { y++ } } -func (w *Menu) Active() bool { return w.active } -func (w *Menu) SetActive(a bool) { w.active = a } -func (w *Menu) Visible() bool { return w.visible } -func (w *Menu) SetVisible(a bool) { w.visible = a } -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) 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 } -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) Active() bool { return w.active } +func (w *Menu) SetActive(a bool) { w.active = a } +func (w *Menu) Visible() bool { return w.visible } +func (w *Menu) SetVisible(a bool) { w.visible = a } +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 } +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) {} diff --git a/menu_item.go b/menu_item.go index e9926b4..d5ad664 100644 --- a/menu_item.go +++ b/menu_item.go @@ -27,13 +27,14 @@ import ( ) type MenuItem struct { - id string - label string - style tcell.Style - active bool - visible bool - x, y int - w, h int + id string + label string + style tcell.Style + active bool + visible bool + tabbable bool + x, y int + w, h int menuType MenuType cursor 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 } } @@ -149,8 +155,10 @@ func (w *MenuItem) WantH() int { } return ret } -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) 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) { diff --git a/prompt.go b/prompt.go index c6ef34f..a38cac2 100644 --- a/prompt.go +++ b/prompt.go @@ -33,10 +33,11 @@ type Prompt struct { id string style tcell.Style - x, y int - w, h int - active bool - visible bool + x, y int + 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) { @@ -91,21 +93,24 @@ func (w *Prompt) Draw(screen tcell.Screen) { w.btnOk.Draw(screen) w.btnCancel.Draw(screen) } -func (w *Prompt) Active() bool { return w.active } -func (w *Prompt) SetActive(a bool) { w.active = a } -func (w *Prompt) Visible() bool { return w.visible } -func (w *Prompt) SetVisible(a bool) { w.visible = a } -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) 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 } -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) Active() bool { return w.active } +func (w *Prompt) SetActive(a bool) { w.active = a } +func (w *Prompt) Visible() bool { return w.visible } +func (w *Prompt) SetVisible(a bool) { w.visible = a } +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 } +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 } diff --git a/relative_layout.go b/relative_layout.go index 7056f58..a01e311 100644 --- a/relative_layout.go +++ b/relative_layout.go @@ -26,12 +26,15 @@ 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 - x, y int - w, h int + active bool + visible bool + tabbable bool + + x, y int + w, h int } var _ Widget = (*RelativeLayout)(nil) @@ -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,33 +82,62 @@ func (w *RelativeLayout) Draw(screen tcell.Screen) { if !w.visible { return } - done := make(map[Widget]widgetRelation) - rem := make(map[Widget]widgetRelation) - for k, v := range w.widgetRelations { - if v.relTo == w { - done[k] = v - } else { - rem[k] = v + // All widgets should have correct (relative) positions + /* + done := make(map[Widget]widgetRelation) + rem := make(map[Widget]widgetRelation) + for k, v := range w.widgetRelations { + if v.relTo == w { + done[k] = v + } else { + 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 } +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) 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) 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) 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) 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 } -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) 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) { +} diff --git a/searcher.go b/searcher.go index 64a8672..eaf8da5 100644 --- a/searcher.go +++ b/searcher.go @@ -33,10 +33,11 @@ type Searcher struct { id string style tcell.Style - x, y int - w, h int - active bool - visible bool + x, y int + 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,13 +224,23 @@ func (w *Searcher) WantH() int { return 2 + w.search.WantH() + len(w.filteredData) // Border + Field + Data } -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 } -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) 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 } +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 } diff --git a/table.go b/table.go index 578af8a..0cadcbb 100644 --- a/table.go +++ b/table.go @@ -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) SetSize(c Coord) { w.w, w.h = c.X, c.Y } -func (w *Table) Focusable() bool { return w.focusable } +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 } diff --git a/text.go b/text.go index ae2b912..b7b3bd7 100644 --- a/text.go +++ b/text.go @@ -22,17 +22,20 @@ THE SOFTWARE. package widgets import ( + "strings" + h "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) type Text struct { - id string - text string - style tcell.Style - x, y int - w, h int - visible bool + id string + text string + style tcell.Style + 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,25 +60,40 @@ 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) {} -func (w *Text) Visible() bool { return w.visible } -func (w *Text) SetVisible(a bool) { w.visible = a } -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) 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 } -func (w *Text) GetW() int { return w.w } -func (w *Text) GetH() int { return w.y } -func (w *Text) WantW() int { return h.Max(w.w, len(w.text)) } -func (w *Text) WantH() int { return h.Max(w.h, 1) } -func (w *Text) SetSize(c Coord) { w.w, w.h = c.X, c.Y } -func (w *Text) Focusable() bool { return false } +func (w *Text) Active() bool { return false } +func (w *Text) SetActive(a bool) {} +func (w *Text) Visible() bool { return w.visible } +func (w *Text) SetVisible(a bool) { w.visible = a } +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 } +func (w *Text) GetW() int { return w.w } +func (w *Text) GetH() int { return w.y } +func (w *Text) WantW() int { return h.Max(w.w, len(w.text)) } +func (w *Text) WantH() int { return h.Max(w.h, 1) } +func (w *Text) 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 } diff --git a/timefield.go b/timefield.go new file mode 100644 index 0000000..5041d8b --- /dev/null +++ b/timefield.go @@ -0,0 +1,272 @@ +/* +Copyright © Brian Buller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package widgets + +import ( + "fmt" + "time" + + 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 +} diff --git a/widget.go b/widget.go index e8ca151..c10331a 100644 --- a/widget.go +++ b/widget.go @@ -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) }