From 6cd94df5212831684a223fb9c60d46fc0f1f2e84 Mon Sep 17 00:00:00 2001 From: David Ashby Date: Sat, 3 Jul 2021 13:30:08 -0400 Subject: [PATCH] doesn't quite work yet --- cmd/manage/interface.go | 135 ------------------------ cmd/manage/main.go | 41 +++++--- ui/ui.go | 226 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 252 insertions(+), 150 deletions(-) delete mode 100644 cmd/manage/interface.go create mode 100644 ui/ui.go diff --git a/cmd/manage/interface.go b/cmd/manage/interface.go deleted file mode 100644 index 167bbd2..0000000 --- a/cmd/manage/interface.go +++ /dev/null @@ -1,135 +0,0 @@ -package main - -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 -} - -type box struct { - x, y int - h, w int - title Drawable - menuItems Drawable - contents []struct { - offsets offsets - container Drawable - } -} - -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 := 1; m < b.w-1; m++ { - s.SetContent(m, b.y, tcell.RuneHLine, nil, tcell.StyleDefault) - s.SetContent(m, b.h-1, tcell.RuneHLine, nil, tcell.StyleDefault) - } - for m := 1; m < b.h-1; m++ { - s.SetContent(b.x, m, tcell.RuneVLine, nil, tcell.StyleDefault) - s.SetContent(b.w-1, m, tcell.RuneVLine, nil, tcell.StyleDefault) - } - s.SetContent(b.x, b.y, tcell.RuneULCorner, nil, tcell.StyleDefault) - s.SetContent(b.w-1, b.y, tcell.RuneURCorner, nil, tcell.StyleDefault) - s.SetContent(b.x, b.h-1, tcell.RuneLLCorner, nil, tcell.StyleDefault) - s.SetContent(b.w-1, 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) - } -} - -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) - } - } -} - -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) { - 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) -} diff --git a/cmd/manage/main.go b/cmd/manage/main.go index 43a043f..da4e15a 100644 --- a/cmd/manage/main.go +++ b/cmd/manage/main.go @@ -5,6 +5,7 @@ import ( "log" "git.yetaga.in/alazyreader/library/config" + "git.yetaga.in/alazyreader/library/ui" "github.com/gdamore/tcell" "github.com/kelseyhightower/envconfig" ) @@ -24,21 +25,31 @@ func main() { log.Fatalln(err) } - l := NewList([]string{"foo", "bar", "baz"}, 0) - b := NewBox( + l := ui.NewList([]string{"foo", "bar", "baz"}, 0) + menu := ui.NewBox( "library", []string{"(e)dit", "(q)uit"}, - contents{{ - offsets: offsets{top: 1, left: 2, bottom: -2, right: -2}, - container: l, + ui.Contents{{ + Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: -2, Right: -2}, + Container: l, }}, ) + activeBook := ui.NewBox( + "book", + []string{"test"}, + ui.Contents{}, + ) + + container := ui.NewContainer( + ui.Contents{{Container: menu}, {Container: activeBook}}, + ui.LayoutHorizontalEven, + ) // init screen.Clear() w, h := screen.Size() - b.SetSize(0, 0, h, w) - b.Draw(screen) + container.SetSize(0, 0, h, w) + container.Draw(screen) screen.Sync() // UI loop @@ -48,11 +59,11 @@ func main() { case *tcell.EventError: screen.Beep() case *tcell.EventKey: // input handling - if v.Key() == tcell.KeyUp && l.selected > 0 { - l.selected = l.selected - 1 + if v.Key() == tcell.KeyUp && l.Selected() > 0 { + l.SetSelected(l.Selected() - 1) } - if v.Key() == tcell.KeyDown && l.selected < len(l.listItems)-1 { - l.selected = l.selected + 1 + if v.Key() == tcell.KeyDown && l.Selected() < len(l.ListMembers())-1 { + l.SetSelected(l.Selected() + 1) } if v.Rune() == 'q' { screen.Fini() @@ -60,17 +71,17 @@ func main() { return } screen.Clear() - b.Draw(screen) + container.Draw(screen) case *tcell.EventResize: // screen redraw w, h := screen.Size() - b.SetSize(0, 0, h, w) + container.SetSize(0, 0, h, w) screen.Clear() - b.Draw(screen) + container.Draw(screen) case *tcell.EventInterrupt: case *tcell.EventMouse: case *tcell.EventTime: default: } - screen.Sync() // repaint + screen.Show() // repaint } } diff --git a/ui/ui.go b/ui/ui.go new file mode 100644 index 0000000..3048535 --- /dev/null +++ b/ui/ui.go @@ -0,0 +1,226 @@ +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 + if c.layoutMethod == LayoutVerticalEven { + num := len(c.contents) + for r := range c.contents { + x := c.x + y := c.y + (num * (r + 1)) + h := c.h / num + w := c.w + c.contents[r].Container.SetSize(x, y, h, w) + } + } else if c.layoutMethod == LayoutHorizontalEven { + num := len(c.contents) + for r := range c.contents { + w := c.w / num + h := c.h + x := c.x + (w * r) + y := c.y + 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 := 1; m < b.w-1; m++ { + s.SetContent(m, b.y, tcell.RuneHLine, nil, tcell.StyleDefault) + s.SetContent(m, b.h-1, tcell.RuneHLine, nil, tcell.StyleDefault) + } + for m := 1; m < b.h-1; m++ { + s.SetContent(b.x, m, tcell.RuneVLine, nil, tcell.StyleDefault) + s.SetContent(b.w-1, m, tcell.RuneVLine, nil, tcell.StyleDefault) + } + s.SetContent(b.x, b.y, tcell.RuneULCorner, nil, tcell.StyleDefault) + s.SetContent(b.w-1, b.y, tcell.RuneURCorner, nil, tcell.StyleDefault) + s.SetContent(b.x, b.h-1, tcell.RuneLLCorner, nil, tcell.StyleDefault) + s.SetContent(b.w-1, 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) { + 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) +}