Browse Source

add style management

pull/1/head
David Ashby 2 months ago
parent
commit
c2d2fe25c0
  1. 5
      Makefile
  2. 16
      cmd/manage/main.go
  3. 92
      ui/ui.go
  4. 24
      ui/ui_test.go

5
Makefile

@ -1,4 +1,4 @@
.PHONY: up down run
.PHONY: up down run-server run-manager test
GOFILES=$(shell find . -name '*.go' -o -name 'go.*')
STATICFILES=$(shell find . -name '*.js' -o -name '*.css' -o -name '*.html')
@ -23,6 +23,9 @@ server: $(GOFILES) $(STATICFILES)
manager: $(GOFILES) $(SQLFILES)
go build -o manager ./cmd/manage
test:
go test ./... -cover
# dev dependencies
up:
docker compose up -d

16
cmd/manage/main.go

@ -125,6 +125,8 @@ func main() {
Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: -2, Right: -2},
Container: l,
}},
ui.StyleActive,
false,
)
book := ui.NewBookDetails(&book.Book{
Title: "test title",
@ -136,6 +138,8 @@ func main() {
Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: 0, Right: 0},
Container: book,
}},
ui.StyleInactive,
false,
)
container := ui.NewContainer(
@ -155,6 +159,7 @@ func main() {
// init UI state
state.Set("ui_state", IN_MENU)
screen.PostEvent(NewEventLoadBook(l.SelectedID()))
// UI loop
for {
@ -168,21 +173,26 @@ func main() {
if curr == IN_MENU {
if v.Key() == tcell.KeyUp && l.Selected() > 0 {
l.SetSelected(l.Selected() - 1)
screen.PostEvent(NewEventLoadBook(l.SelectedID()))
}
if v.Key() == tcell.KeyDown && l.Selected() < len(l.ListMembers())-1 {
l.SetSelected(l.Selected() + 1)
screen.PostEvent(NewEventLoadBook(l.SelectedID()))
}
if v.Rune() == 'q' {
screen.Fini()
fmt.Printf("Thank you for playing Wing Commander!\n\n")
return
}
if v.Key() == tcell.KeyRight {
screen.PostEvent(NewEventLoadBook(l.SelectedID()))
if v.Key() == tcell.KeyEnter {
activeBook.SetStyle(ui.StyleActive)
menu.SetStyle(ui.StyleInactive)
state.Set("ui_state", IN_BOOK)
}
} else if curr == IN_BOOK {
if v.Key() == tcell.KeyLeft {
if v.Key() == tcell.KeyEsc {
activeBook.SetStyle(ui.StyleInactive)
menu.SetStyle(ui.StyleActive)
state.Set("ui_state", IN_MENU)
}
}

92
ui/ui.go

@ -11,6 +11,7 @@ import (
type Drawable interface {
Draw(tcell.Screen)
SetSize(x, y, h, w int)
SetStyle(tcell.Style)
}
type Offsets struct {
@ -34,6 +35,11 @@ const (
LayoutVerticalPercent
)
var (
StyleActive = tcell.Style(0).Foreground(tcell.ColorWhite).Background(tcell.ColorBlack)
StyleInactive = tcell.Style(0).Foreground(tcell.ColorGray).Background(tcell.ColorBlack)
)
// 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
@ -155,6 +161,10 @@ func (c *Container) SetSize(x, y, h, w int) {
}
}
func (c *Container) SetStyle(s tcell.Style) {
// containers have no visible elements to style
}
func (c *Container) Contents() Contents {
return c.contents
}
@ -170,13 +180,17 @@ type Box struct {
title Drawable
menuItems Drawable
contents Contents
style tcell.Style
cascade bool
}
func NewBox(title string, menuItems []string, contents Contents) *Box {
func NewBox(title string, menuItems []string, contents Contents, initialStyle tcell.Style, cascade bool) *Box {
return &Box{
title: NewPaddedText(title),
menuItems: NewPaddedText(strings.Join(menuItems, " ")),
contents: contents,
style: initialStyle,
cascade: cascade,
}
}
@ -195,17 +209,17 @@ func (b *Box) SetSize(x, y, h, w int) {
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)
s.SetContent(m, b.y, tcell.RuneHLine, nil, b.style)
s.SetContent(m, b.y+b.h-1, tcell.RuneHLine, nil, b.style)
}
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, m, tcell.RuneVLine, nil, b.style)
s.SetContent(b.x+b.w-1, m, tcell.RuneVLine, nil, b.style)
}
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)
s.SetContent(b.x, b.y, tcell.RuneULCorner, nil, b.style)
s.SetContent(b.x+b.w-1, b.y, tcell.RuneURCorner, nil, b.style)
s.SetContent(b.x, b.y+b.h-1, tcell.RuneLLCorner, nil, b.style)
s.SetContent(b.x+b.w-1, b.y+b.h-1, tcell.RuneLRCorner, nil, b.style)
if b.title != nil {
b.title.Draw(s)
@ -218,6 +232,17 @@ func (b *Box) Draw(s tcell.Screen) {
}
}
func (b *Box) SetStyle(s tcell.Style) {
b.style = s
b.title.SetStyle(s)
b.menuItems.SetStyle(s)
if b.cascade {
for c := range b.contents {
b.contents[c].Container.SetStyle(s)
}
}
}
func (b *Box) Contents() Contents {
return b.contents
}
@ -232,6 +257,7 @@ type List struct {
h, w int
selected int
listItems []ListKeyValue
style tcell.Style
}
type ListKeyValue struct {
@ -253,14 +279,18 @@ func (l *List) SetSize(x, y, h, w int) {
func (l *List) Draw(s tcell.Screen) {
for i := range l.listItems {
for j, r := range l.listItems[i].Value {
s.SetContent(l.x+j, l.y+i, r, nil, tcell.StyleDefault)
s.SetContent(l.x+j, l.y+i, r, nil, l.style)
}
if i == l.selected {
s.SetContent(l.x+len(l.listItems[i].Value)+1, l.y+i, '<', nil, tcell.StyleDefault)
s.SetContent(l.x+len(l.listItems[i].Value)+1, l.y+i, '<', nil, l.style)
}
}
}
func (l *List) SetStyle(s tcell.Style) {
l.style = s
}
func (l *List) Selected() int {
return l.selected
}
@ -279,9 +309,10 @@ func (l *List) ListMembers() []ListKeyValue {
// BookDetails displays an editable list of book details
type BookDetails struct {
x, y int
h, w int
book *book.Book
x, y int
h, w int
book *book.Book
style tcell.Style
}
func NewBookDetails(b *book.Book) *BookDetails {
@ -327,17 +358,23 @@ func (l *BookDetails) Draw(s tcell.Screen) {
if i < l.h-2 {
kv := NewKeyValue(items[i].label, ": ", items[i].value)
kv.SetSize(l.x, l.y+i, 0, 0)
kv.SetStyle(l.style)
kv.Draw(s)
}
}
}
func (l *BookDetails) SetStyle(s tcell.Style) {
l.style = s
}
// 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
x, y int
h, w int
text string
style tcell.Style
}
func NewPaddedText(text string) *PaddedText {
@ -348,18 +385,22 @@ 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) SetStyle(s tcell.Style) {
p.style = s
}
func (p *PaddedText) Draw(s tcell.Screen) {
if p.text == "" {
return
}
t := p.x
s.SetContent(t, p.y, ' ', nil, tcell.StyleDefault)
s.SetContent(t, p.y, ' ', nil, p.style)
t++
for _, r := range p.text {
s.SetContent(t, p.y, r, nil, tcell.StyleDefault)
s.SetContent(t, p.y, r, nil, p.style)
t++
}
s.SetContent(t, p.y, ' ', nil, tcell.StyleDefault)
s.SetContent(t, p.y, ' ', nil, p.style)
}
type KeyValue struct {
@ -368,6 +409,7 @@ type KeyValue struct {
key string
value string
separator string
style tcell.Style
}
func NewKeyValue(key, separator, value string) *KeyValue {
@ -382,15 +424,19 @@ func (p *KeyValue) SetSize(x, y, _, _ int) {
p.x, p.y, p.h, p.w = x, y, 1, len(p.key)+len(p.separator)+len(p.value)
}
func (p *KeyValue) SetStyle(s tcell.Style) {
p.style = s
}
func (p *KeyValue) Draw(s tcell.Screen) {
for j, r := range p.key {
s.SetContent(p.x+j, p.y, r, nil, tcell.StyleDefault)
s.SetContent(p.x+j, p.y, r, nil, p.style)
}
for j, r := range p.separator {
s.SetContent(p.x+len(p.key)+j, p.y, r, nil, tcell.StyleDefault)
s.SetContent(p.x+len(p.key)+j, p.y, r, nil, p.style)
}
for j, r := range p.value {
s.SetContent(p.x+len(p.key)+len(p.separator)+j, p.y, r, nil, tcell.StyleDefault)
s.SetContent(p.x+len(p.key)+len(p.separator)+j, p.y, r, nil, p.style)
}
}

24
ui/ui_test.go

@ -13,7 +13,7 @@ func TestContainerOneBox(t *testing.T) {
`
m := &MockScreen{}
one := NewBox("box one", nil, Contents{})
one := NewBox("box one", nil, Contents{}, 0, false)
container := NewContainer(
Contents{{Container: one}},
LayoutHorizontalEven,
@ -38,8 +38,8 @@ func TestContainerTwoBoxesHStack(t *testing.T) {
`
m := &MockScreen{}
one := NewBox("one", nil, Contents{})
two := NewBox("two", nil, Contents{})
one := NewBox("one", nil, Contents{}, 0, false)
two := NewBox("two", nil, Contents{}, 0, false)
container := NewContainer(
Contents{{Container: one}, {Container: two}},
LayoutHorizontalEven,
@ -64,9 +64,9 @@ func TestContainerThreeBoxesUnevenHStack(t *testing.T) {
`
m := &MockScreen{}
one := NewBox("one", nil, Contents{})
two := NewBox("two", nil, Contents{})
three := NewBox("three", nil, Contents{})
one := NewBox("one", nil, Contents{}, 0, false)
two := NewBox("two", nil, Contents{}, 0, false)
three := NewBox("three", nil, Contents{}, 0, false)
container := NewContainer(
Contents{{Container: one}, {Container: two}, {Container: three}},
LayoutHorizontalEven,
@ -91,8 +91,8 @@ func TestContainerTwoBoxesHPercentStack(t *testing.T) {
`
m := &MockScreen{}
one := NewBox("one", nil, Contents{})
two := NewBox("two", nil, Contents{})
one := NewBox("one", nil, Contents{}, 0, false)
two := NewBox("two", nil, Contents{}, 0, false)
container := NewContainer(
Contents{
{Container: one, Offsets: Offsets{Percent: 2}},
@ -124,8 +124,8 @@ func TestContainerTwoBoxesVStack(t *testing.T) {
`
m := &MockScreen{}
one := NewBox("one", nil, Contents{})
two := NewBox("two", nil, Contents{})
one := NewBox("one", nil, Contents{}, 0, false)
two := NewBox("two", nil, Contents{}, 0, false)
container := NewContainer(
Contents{{Container: one}, {Container: two}},
LayoutVerticalEven,
@ -155,8 +155,8 @@ func TestContainerTwoBoxesPercentageVStack(t *testing.T) {
`
m := &MockScreen{}
one := NewBox("one", nil, Contents{})
two := NewBox("two", nil, Contents{})
one := NewBox("one", nil, Contents{}, 0, false)
two := NewBox("two", nil, Contents{}, 0, false)
container := NewContainer(
Contents{
{Container: one, Offsets: Offsets{Percent: 2}},

Loading…
Cancel
Save