package ui import ( "strings" "github.com/gdamore/tcell" ) type Drawable interface { Draw(tcell.Screen) SetSize(x, y, h, w int) } type Offsets struct { Top int Bottom int Left int Right int } type Contents []struct { Offsets Offsets Container Drawable } const ( LayoutUnmanaged = iota LayoutHorizontalEven LayoutVerticalEven ) // A Container has no visible UI of its own, but arranges sub-components on the screen. // layoutMethod manages how subcomponents are organized. If `LayoutUnmanaged`, it just uses the offsets // in contents to paint on the screen. Otherwise, `LayoutHorizontalEven` and `LayoutVerticalEven` will // have it compute even distributions of space for all components either horizontally or vertically, // filling the container. type Container struct { x, y int h, w int layoutMethod int contents Contents } func NewContainer(contents Contents, layoutMethod int) *Container { return &Container{ layoutMethod: layoutMethod, contents: contents, } } func (c *Container) Draw(s tcell.Screen) { for i := range c.contents { c.contents[i].Container.Draw(s) } } func (c *Container) SetSize(x, y, h, w int) { c.x, c.y, c.h, c.w = x, y, h, w carry := 0 if c.layoutMethod == LayoutVerticalEven { num := len(c.contents) extra := c.h % num for r := range c.contents { w := c.w h := c.h / num x := c.x y := c.y + (h * r) + carry if extra > 0 { // distribute "extra" space to containers as we have some left h++ extra-- carry++ } c.contents[r].Container.SetSize(x, y, h, w) } } else if c.layoutMethod == LayoutHorizontalEven { num := len(c.contents) extra := c.w % num for r := range c.contents { w := c.w / num h := c.h x := c.x + (w * r) + carry y := c.y if extra > 0 { // distribute "extra" space to containers as we have some left w++ extra-- carry++ } c.contents[r].Container.SetSize(x, y, h, w) } } else { for r := range c.contents { x := c.x + c.contents[r].Offsets.Left y := c.y + c.contents[r].Offsets.Top h := c.h - c.contents[r].Offsets.Bottom w := c.w - c.contents[r].Offsets.Right c.contents[r].Container.SetSize(x, y, h, w) } } } func (c *Container) Contents() Contents { return c.contents } func (c *Container) SetContents(con Contents) { c.contents = con } // A Box draws a ASCII box around its contents, with an optional title and footer. type Box struct { x, y int h, w int title Drawable menuItems Drawable contents Contents } func NewBox(title string, menuItems []string, contents Contents) *Box { return &Box{ title: NewPaddedText(title), menuItems: NewPaddedText(strings.Join(menuItems, " ")), contents: contents, } } func (b *Box) SetSize(x, y, h, w int) { b.x, b.y, b.h, b.w = x, y, h, w b.title.SetSize(b.x+2, b.y, 0, 0) b.menuItems.SetSize(b.x+2, b.y+b.h-1, 0, 0) for c := range b.contents { x := b.x + b.contents[c].Offsets.Left y := b.y + b.contents[c].Offsets.Top h := b.h - b.contents[c].Offsets.Bottom w := b.w - b.contents[c].Offsets.Right b.contents[c].Container.SetSize(x, y, h, w) } } func (b *Box) Draw(s tcell.Screen) { for m := b.x + 1; m < b.x+b.w-1; m++ { s.SetContent(m, b.y, tcell.RuneHLine, nil, tcell.StyleDefault) s.SetContent(m, b.y+b.h-1, tcell.RuneHLine, nil, tcell.StyleDefault) } for m := b.y + 1; m < b.y+b.h-1; m++ { s.SetContent(b.x, m, tcell.RuneVLine, nil, tcell.StyleDefault) s.SetContent(b.x+b.w-1, m, tcell.RuneVLine, nil, tcell.StyleDefault) } s.SetContent(b.x, b.y, tcell.RuneULCorner, nil, tcell.StyleDefault) s.SetContent(b.x+b.w-1, b.y, tcell.RuneURCorner, nil, tcell.StyleDefault) s.SetContent(b.x, b.y+b.h-1, tcell.RuneLLCorner, nil, tcell.StyleDefault) s.SetContent(b.x+b.w-1, b.y+b.h-1, tcell.RuneLRCorner, nil, tcell.StyleDefault) if b.title != nil { b.title.Draw(s) } if b.menuItems != nil { b.menuItems.Draw(s) } for c := range b.contents { b.contents[c].Container.Draw(s) } } func (b *Box) Contents() Contents { return b.contents } func (b *Box) SetContents(c Contents) { b.contents = c } // A List is a scrollable, pageable list with a selector token. type List struct { x, y int h, w int selected int listItems []string } func NewList(listItems []string, initialSelected int) *List { return &List{ listItems: listItems, selected: initialSelected, } } func (l *List) SetSize(x, y, h, w int) { l.x, l.y, l.h, l.w = x, y, h, w } func (l *List) Draw(s tcell.Screen) { for i := range l.listItems { for j, r := range l.listItems[i] { s.SetContent(l.x+j, l.y+i, r, nil, tcell.StyleDefault) } if i == l.selected { s.SetContent(l.x+len(l.listItems[i])+1, l.y+i, '<', nil, tcell.StyleDefault) } } } func (l *List) Selected() int { return l.selected } func (l *List) SetSelected(i int) { l.selected = i } func (l *List) ListMembers() []string { return l.listItems } // PaddedText outputs strings with a space on both sides. // Useful for generating headings, footers, etc. Used by Box. type PaddedText struct { x, y int h, w int text string } func NewPaddedText(text string) *PaddedText { return &PaddedText{text: text} } func (p *PaddedText) SetSize(x, y, _, _ int) { p.x, p.y, p.h, p.w = x, y, 1, len(p.text)+2 } func (p *PaddedText) Draw(s tcell.Screen) { if p.text == "" { return } t := p.x s.SetContent(t, p.y, ' ', nil, tcell.StyleDefault) t++ for _, r := range p.text { s.SetContent(t, p.y, r, nil, tcell.StyleDefault) t++ } s.SetContent(t, p.y, ' ', nil, tcell.StyleDefault) }