diff --git a/absolute_layout.go b/absolute_layout.go index ea18cec..a9af254 100644 --- a/absolute_layout.go +++ b/absolute_layout.go @@ -35,6 +35,9 @@ type AbsoluteLayout struct { w, h int widgets []Widget wCoords map[Widget]Coord + wAnchor map[Widget]AbsoluteAnchor + + defAnchor AbsoluteAnchor active bool visible bool @@ -42,22 +45,31 @@ type AbsoluteLayout struct { logger func(string) } -func (w *AbsoluteLayout) SetLogger(l func(string)) { w.logger = l } -func (w *AbsoluteLayout) Log(txt string) { - if w.logger != nil { - w.logger(txt) - } -} +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 , + AnchorErr +) func NewAbsoluteLayout(id string, s tcell.Style) *AbsoluteLayout { - ret := &AbsoluteLayout{style: s} - ret.Init(id) + ret := &AbsoluteLayout{} + ret.Init(id, s) return ret } -func (w *AbsoluteLayout) Init(id string) { +func (w *AbsoluteLayout) Init(id string, s tcell.Style) { w.id = id + w.style = s w.visible = true + w.defAnchor = AnchorTL w.wCoords = make(map[Widget]Coord) } @@ -90,12 +102,43 @@ func (w *AbsoluteLayout) Draw(screen tcell.Screen) { } 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 } - w.widgets[i].SetPos(p.Add(Coord{X: w.x, Y: w.y})) + 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) } } @@ -118,12 +161,29 @@ func (w *AbsoluteLayout) WantW() int { return w.w } func (w *AbsoluteLayout) WantH() int { return w.h } // Add a widget at x/y -func (w *AbsoluteLayout) Add(n Widget, pos Coord) { +func (w *AbsoluteLayout) Add(n Widget, pos Coord) { w.AddAnchored(n, pos, w.defAnchor) } + +func (w *AbsoluteLayout) AddAnchored(n Widget, pos Coord, anchor AbsoluteAnchor) { w.widgets = append(w.widgets, n) w.wCoords[n] = pos + w.wAnchor[n] = anchor } func (w *AbsoluteLayout) Clear() { w.widgets = []Widget{} w.wCoords = make(map[Widget]Coord) } + +func (w *AbsoluteLayout) SetDefaultAnchor(d AbsoluteAnchor) { + if d < 0 || d > AnchorErr { + d = AnchorTL + } + w.defAnchor = d +} + +func (w *AbsoluteLayout) SetLogger(l func(string)) { w.logger = l } +func (w *AbsoluteLayout) Log(txt string) { + if w.logger != nil { + w.logger(txt) + } +} diff --git a/bordered_widget.go b/bordered_widget.go index 129c62c..f698556 100644 --- a/bordered_widget.go +++ b/bordered_widget.go @@ -51,15 +51,15 @@ func (w *BorderedWidget) Log(txt string) { func NewBorderedWidget(id string, wd Widget, s tcell.Style) *BorderedWidget { ret := &BorderedWidget{ - style: s, widget: wd, } - ret.Init(id) + ret.Init(id, s) return ret } -func (w *BorderedWidget) Init(id string) { +func (w *BorderedWidget) Init(id string, s tcell.Style) { w.id = id + w.style = s w.visible = true w.border = h.BRD_CSIMPLE } diff --git a/button.go b/button.go index c365dba..3f0b6c1 100644 --- a/button.go +++ b/button.go @@ -45,13 +45,14 @@ type Button struct { var _ Widget = (*Button)(nil) func NewButton(id string, style tcell.Style) *Button { - b := &Button{style: style} - b.Init(id) + b := &Button{} + b.Init(id, style) return b } -func (w *Button) Init(id string) { +func (w *Button) Init(id string, style tcell.Style) { w.id = id + w.style = style w.visible = true w.onPressed = func() bool { return false } } diff --git a/checkbox.go b/checkbox.go index 502b299..e678901 100644 --- a/checkbox.go +++ b/checkbox.go @@ -50,13 +50,14 @@ type Checkbox struct { var _ Widget = (*Checkbox)(nil) func NewCheckbox(id string, style tcell.Style) *Checkbox { - ret := &Checkbox{style: style} - ret.Init(id) + ret := &Checkbox{} + ret.Init(id, style) return ret } -func (w *Checkbox) Init(id string) { +func (w *Checkbox) Init(id string, style tcell.Style) { w.id = id + w.style = style w.visible = true w.stateRunes = []rune{'X', ' ', '-'} } diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..3510728 --- /dev/null +++ b/cli.go @@ -0,0 +1,276 @@ +/* +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" + "sort" + "strings" + + h "git.bullercodeworks.com/brian/tcell-widgets/helpers" + "github.com/gdamore/tcell" +) + +type Cli struct { + id string + style tcell.Style + + x, y int + w, h int + active bool + visible bool + + rawLog []string + log []string + logPosition int + + value string + cursor int + + history []string + historyPosition int + + commands []string + commandMap map[string]cliCommand + commandGuessMap map[string]func(args ...string) string +} + +func NewCli(id string, s tcell.Style) *Cli { + ret := &Cli{} + ret.Init(id, s) + return ret +} + +func (w *Cli) Init(id string, s tcell.Style) { + w.id, w.style = id, s +} + +func (w *Cli) Id() string { return w.id } +func (w *Cli) HandleResize(ev *tcell.EventResize) {} +func (w *Cli) HandleKey(ev *tcell.EventKey) bool { + if !w.active { + return false + } + if h.IsKey(*ev, tcell.KeyEsc) { + w.SetActive(false) + return true + } + if h.IsBS(*ev) { + if w.cursor > 0 { + w.cursor-- + } + if w.cursor < len(w.value) { + w.value = w.value[:w.cursor] + w.value[w.cursor+1:] + } + } + if done := h.HandleKeys(*ev, map[tcell.Key]func() bool{ + tcell.KeyEsc: func() bool { + w.SetActive(false) + return true + }, + tcell.KeyUp: func() bool { + if w.historyPosition < len(w.history)-1 { + w.historyPosition++ + w.value = w.history[w.historyPosition] + w.cursor = len(w.value) + return true + } + return false + }, + tcell.KeyDown: func() bool { + if w.historyPosition > -1 { + w.historyPosition-- + if w.historyPosition == -1 { + w.value = "" + } else { + w.value = w.history[w.historyPosition] + } + w.cursor = len(w.value) + return true + } + return false + }, + tcell.KeyLeft: func() bool { + if w.cursor > 0 { + w.cursor-- + return true + } + return false + }, + tcell.KeyRight: func() bool { + if w.cursor < len(w.value) { + w.cursor++ + return true + } + return false + }, + tcell.KeyCtrlU: func() bool { + w.value = w.value[w.cursor:] + w.cursor = 0 + return true + }, + tcell.KeyTab: func() bool { + // Auto-complete + guess := w.findBestGuess() + if guess != "" { + w.value = guess + w.cursor = len(w.value) + return true + } + return false + }, + tcell.KeyEnter: func() bool { + cmds := strings.Split(w.value, " ") + if v, ok := w.commandMap[cmds[0]]; ok { + w.history = append(w.history, w.value) + w.historyPosition = -1 + w.value = "" + w.cursor = 0 + if len(cmds) > 1 { + return v(cmds[1:]...) + } else { + return v() + } + } + return true + }, + }); done { + return done + } + var ch string + if h.KeyIsDisplayable(*ev) { + ch = string(ev.Rune()) + if w.cursor < len(w.value) { + strPt1 := w.value[:w.cursor] + strPt2 := w.value[w.cursor:] + w.value = fmt.Sprintf("%s%s%s", strPt1, ch, strPt2) + } else { + w.value = fmt.Sprintf("%s%s", w.value, ch) + } + w.cursor++ + return true + } + return false +} +func (w *Cli) HandleTime(ev *tcell.EventTime) {} + +func (w *Cli) Draw(screen tcell.Screen) { + if !w.visible { + return + } + dStyle := w.style + if !w.active { + w.style = dStyle.Dim(true) + } + h.Border(w.x, w.y, w.x+w.w-1, w.y+w.h, h.BRD_SIMPLE, dStyle, screen) + x, y := w.x+1, w.y+1+w.h-3 + for i := 0; i < w.h-2; i++ { + if len(w.log) > i { + line := w.log[len(w.log)-i-1] + if len(line) > w.w-2 { + line = line[:w.w-2] + } + h.DrawText(x, y, line, dStyle, screen) + y-- + } + } + y = w.y + w.h - 1 + cursor := " " + pre := "> " + var post string + if len(w.value) > 0 { + pre = fmt.Sprintf("%s%s", pre, w.value[:w.cursor]) + if w.cursor < len(w.value) { + cursor = string(w.value[w.cursor]) + post = w.value[w.cursor+1:] + } + } + h.DrawText(x, y, pre, dStyle, screen) + x += len(pre) + h.DrawText(x, y, cursor, dStyle.Reverse(w.active), screen) + x += 1 + h.DrawText(x, y, post, dStyle, 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) SetValue(val string) { + w.value = val + w.cursor = len(val) +} + +func (w *Cli) AddComamnd(cmd string, do cliCommand) { + w.commands = append(w.commands, cmd) + sort.Strings(w.commands) + w.commandMap[cmd] = do +} + +func (w *Cli) RemoveCommand(cmd string) { + var idx int + for idx = range w.commands { + if w.commands[idx] == cmd { + break + } + } + w.commands = append(w.commands[:idx], w.commands[idx+1:]...) + delete(w.commandMap, cmd) +} + +func (w *Cli) findBestGuess() string { + if w.value == "" { + return "" + } + for _, v := range w.commands { + if strings.HasPrefix(v, w.value) { + return v + } + } + return "" +} + +func (w *Cli) DoLog(txt string) { + w.rawLog = append(w.rawLog, txt) + w.log = append(w.log, h.WrapStringToSlice(txt, w.w-2)...) +} + +type cliCommand func(args ...string) bool + +func (c *cliCommand) findBestGuess(args ...string) string { + return "" +} diff --git a/field.go b/field.go index 9b49c35..a8556c4 100644 --- a/field.go +++ b/field.go @@ -49,13 +49,14 @@ type Field struct { var _ Widget = (*Field)(nil) func NewField(id string, style tcell.Style) *Field { - f := &Field{style: style} - f.Init(id) + f := &Field{} + f.Init(id, style) return f } -func (w *Field) Init(id string) { +func (w *Field) Init(id string, style tcell.Style) { w.id = id + w.style = style w.visible = true w.filter = func(ev *tcell.EventKey) bool { return h.IsBS(*ev) || diff --git a/helpers/text_helpers.go b/helpers/text_helpers.go index ba53105..faa2abd 100644 --- a/helpers/text_helpers.go +++ b/helpers/text_helpers.go @@ -157,3 +157,7 @@ func Longest(of []string) int { } return ret } + +func WrapStringToSlice(s string, width int) []string { + return strings.Split(WrapText(s, width), "\n") +} diff --git a/menu.go b/menu.go index f5869ac..6d244d9 100644 --- a/menu.go +++ b/menu.go @@ -51,15 +51,17 @@ const ( ) func NewMenu(id string, style tcell.Style) *Menu { - ret := &Menu{style: style} - ret.Init(id) + ret := &Menu{} + ret.Init(id, style) return ret } -func (w *Menu) Id() string { return w.id } -func (w *Menu) Init(id string) { + +func (w *Menu) Init(id string, style tcell.Style) { w.id = id + w.style = style w.visible = true } +func (w *Menu) Id() string { return w.id } func (w *Menu) HandleResize(ev *tcell.EventResize) {} func (w *Menu) HandleKey(ev *tcell.EventKey) bool { if !w.active { diff --git a/menu_item.go b/menu_item.go index 80822a3..3a1e910 100644 --- a/menu_item.go +++ b/menu_item.go @@ -45,13 +45,14 @@ type MenuItem struct { } func NewMenuItem(id string, style tcell.Style) *MenuItem { - ret := &MenuItem{style: style} - ret.Init(id) + ret := &MenuItem{} + ret.Init(id, style) return ret } -func (w *MenuItem) Init(id string) { +func (w *MenuItem) Init(id string, style tcell.Style) { w.id = id + w.style = style w.visible = true } func (w *MenuItem) Id() string { return w.id } diff --git a/relative_layout.go b/relative_layout.go index 9c7f46f..7056f58 100644 --- a/relative_layout.go +++ b/relative_layout.go @@ -55,13 +55,14 @@ const ( ) func NewRelativeLayout(id string, s tcell.Style) *RelativeLayout { - ret := &RelativeLayout{style: s} - ret.Init(id) + ret := &RelativeLayout{} + ret.Init(id, s) return ret } -func (w *RelativeLayout) Init(id string) { +func (w *RelativeLayout) Init(id string, style tcell.Style) { w.id = id + w.style = style w.visible = true } func (w *RelativeLayout) Id() string { return w.id } diff --git a/searcher.go b/searcher.go index 095ad45..b0f1b28 100644 --- a/searcher.go +++ b/searcher.go @@ -53,15 +53,15 @@ var _ Widget = (*Searcher)(nil) func NewSearcher(id string, style tcell.Style) *Searcher { ret := &Searcher{ - style: style, search: NewField(fmt.Sprintf("%s-searcher-field", id), style), } - ret.Init(id) + ret.Init(id, style) return ret } -func (w *Searcher) Init(id string) { +func (w *Searcher) Init(id string, style tcell.Style) { w.id = id + w.style = style w.visible = true w.search.SetOnChange(func(prev, curr string) { w.updateFilter() diff --git a/table.go b/table.go index 5b5295c..9f14a1b 100644 --- a/table.go +++ b/table.go @@ -71,12 +71,13 @@ const ( func NewTable(id string, style tcell.Style) *Table { ret := &Table{style: style} - ret.Init(id) + ret.Init(id, style) return ret } -func (w *Table) Init(id string) { +func (w *Table) Init(id string, style tcell.Style) { w.id = id + w.style = style w.visible = true } func (w *Table) Id() string { return w.id } diff --git a/text.go b/text.go index 4b74e8e..b35a32c 100644 --- a/text.go +++ b/text.go @@ -38,13 +38,14 @@ type Text struct { var _ Widget = (*Text)(nil) func NewText(id string, style tcell.Style) *Text { - ret := &Text{style: style} - ret.Init(id) + ret := &Text{} + ret.Init(id, style) return ret } -func (w *Text) Init(id string) { +func (w *Text) Init(id string, style tcell.Style) { w.id = id + w.style = style w.visible = true } func (w *Text) Id() string { return w.id } diff --git a/widget.go b/widget.go index fc4e8c3..be17ad0 100644 --- a/widget.go +++ b/widget.go @@ -24,7 +24,7 @@ package widgets import "github.com/gdamore/tcell" type Widget interface { - Init(id string) + Init(string, tcell.Style) Id() string HandleResize(*tcell.EventResize) HandleKey(*tcell.EventKey) bool