diff --git a/internal/app/buffer.go b/internal/app/buffer.go index 2e9f9d2..542ba28 100644 --- a/internal/app/buffer.go +++ b/internal/app/buffer.go @@ -21,29 +21,20 @@ func newBuffer(width, height int) *buffer { return b } -func (b *buffer) set(x, y int, t tile) bool { +func (b *buffer) setTile(x, y int, t tile) { n := y*b.width + x if n >= len(b.tiles) { - return false + return } b.tiles[n] = t - return true } -func (b *buffer) get(x, y int) (tile, bool) { +func (b *buffer) getTile(x, y int) tile { n := y*b.width + x if n >= len(b.tiles) { - return tile{}, false - } - return b.tiles[n], true -} - -func (b *buffer) writeString(s string, start math.Vec, style tcell.Style) { - for i, r := range []rune(s) { - if !b.set(start.X+i, start.Y, tile{r: r, style: style}) { - break - } + return tile{} } + return b.tiles[n] } func (b *buffer) clear() { @@ -55,10 +46,8 @@ func (b *buffer) clear() { func (b *buffer) blit(s tcell.Screen, offset math.Vec) { for x := 0; x < b.width; x++ { for y := 0; y < b.height; y++ { - t, ok := b.get(x, y) - if ok { - s.SetContent(x+offset.X, y+offset.Y, t.r, nil, t.style) - } + t := b.getTile(x, y) + s.SetContent(x+offset.X, y+offset.Y, t.r, nil, t.style) } } } @@ -66,7 +55,9 @@ func (b *buffer) blit(s tcell.Screen, offset math.Vec) { func (b *buffer) fill(style tcell.Style) { for x := 0; x < b.width; x++ { for y := 0; y < b.height; y++ { - b.set(x, y, tile{r: ' ', style: style}) + b.setTile(x, y, tile{r: ' ', style: style}) } } } + +func (b *buffer) bounds() math.Rect { return math.Rect{Width: b.width, Height: b.height} } diff --git a/internal/app/canvas.go b/internal/app/canvas.go new file mode 100644 index 0000000..fca0234 --- /dev/null +++ b/internal/app/canvas.go @@ -0,0 +1,43 @@ +package app + +import ( + "github.com/gdamore/tcell/v2" + "github.com/jordanorelli/astro-domu/internal/math" +) + +type canvas interface { + getTile(x, y int) tile + setTile(int, int, tile) + bounds() math.Rect +} + +func writeString(img canvas, s string, start math.Vec, style tcell.Style) { + for i, r := range []rune(s) { + img.setTile(start.X+i, start.Y, tile{r: r, style: style}) + } +} + +func cut(img canvas, bounds math.Rect) canvas { + return §ion{img: img, frame: bounds} +} + +type section struct { + img canvas + frame math.Rect +} + +func (s *section) getTile(x, y int) tile { + if x < 0 || x >= s.frame.Width || y < 0 || y >= s.frame.Height { + return tile{} + } + return s.img.getTile(x+s.frame.Origin.X, y+s.frame.Origin.Y) +} + +func (s *section) setTile(x, y int, t tile) { + if x < 0 || x >= s.frame.Width || y < 0 || y >= s.frame.Height { + return + } + s.img.setTile(x+s.frame.Origin.X, y+s.frame.Origin.Y, t) +} + +func (s *section) bounds() math.Rect { return s.frame } diff --git a/internal/app/chat_view.go b/internal/app/chat_view.go index c6d8b1d..b6b9988 100644 --- a/internal/app/chat_view.go +++ b/internal/app/chat_view.go @@ -16,7 +16,7 @@ type chatView struct { history []sim.ChatMessage } -func (c *chatView) handleEvent(ui *UI, e tcell.Event) bool { +func (c *chatView) handleEvent(e tcell.Event) change { switch t := e.(type) { case *tcell.EventKey: key := t.Key() @@ -31,10 +31,11 @@ func (c *chatView) handleEvent(ui *UI, e tcell.Event) bool { } if key == tcell.KeyEnter { - // ugh lol - go ui.client.Send(sim.SendChatMessage{Text: c.composing}) c.composing = "" - break + return changeFn(func(ui *UI) { + // ugh lol + go ui.client.Send(sim.SendChatMessage{Text: c.composing}) + }) } if key == tcell.KeyRune { @@ -45,22 +46,23 @@ func (c *chatView) handleEvent(ui *UI, e tcell.Event) bool { default: // ui.Debug("screen saw unhandled event of type %T", e) } - return false + return nil } -func (c *chatView) draw(b *buffer) { - chatHeight := b.height - 1 +func (c *chatView) draw(img canvas, st *state) { + bounds := img.bounds() + chatHeight := bounds.Height - 1 for i := 0; i < math.Min(chatHeight, len(c.history)); i++ { msg := c.history[len(c.history)-1-i] s := fmt.Sprintf("%12s: %s", msg.From, msg.Text) - b.writeString(s, math.Vec{0, b.height - 2 - i}, tcell.StyleDefault) + writeString(img, s, math.Vec{0, bounds.Height - 2 - i}, tcell.StyleDefault) } - b.writeString(c.composing, math.Vec{0, b.height - 1}, tcell.StyleDefault) + writeString(img, c.composing, math.Vec{0, bounds.Height - 1}, tcell.StyleDefault) if c.inFocus { cursor := tile{r: tcell.RuneBlock, style: tcell.StyleDefault.Blink(true)} - b.set(len([]rune(c.composing)), b.height-1, cursor) + img.setTile(len([]rune(c.composing)), bounds.Height-1, cursor) } } diff --git a/internal/app/container_view.go b/internal/app/container_view.go new file mode 100644 index 0000000..3417361 --- /dev/null +++ b/internal/app/container_view.go @@ -0,0 +1,34 @@ +package app + +import ( + "github.com/gdamore/tcell/v2" + "github.com/jordanorelli/astro-domu/internal/math" +) + +type containerView struct { + children []*node + focussed int +} + +func (c *containerView) handleEvent(e tcell.Event) change { + switch e.(type) { + case *tcell.EventKey: + return c.children[c.focussed].handleEvent(e) + + default: + // ui.Debug("screen saw unhandled event of type %T", e) + } + + return nil +} + +func (c *containerView) draw(img canvas, st *state) { + for _, n := range c.children { + n.draw(cut(img, n.frame), st) + } +} + +type node struct { + view + frame math.Rect +} diff --git a/internal/app/form.go b/internal/app/form.go index 9e1de04..29fcf55 100644 --- a/internal/app/form.go +++ b/internal/app/form.go @@ -22,10 +22,10 @@ func (f *form) handleEvent(e tcell.Event) change { return f.fields[0].handleEvent(e) } -func (f *form) draw(b *buffer, _ *state) { +func (f *form) draw(img canvas, _ *state) { for i, field := range f.fields { - b.writeString(field.label, math.Vec{0, i * 2}, tcell.StyleDefault) - b.writeString(field.prompt+field.entered, math.Vec{0, i*2 + 1}, tcell.StyleDefault) + writeString(img, field.label, math.Vec{0, i * 2}, tcell.StyleDefault) + writeString(img, field.prompt+field.entered, math.Vec{0, i*2 + 1}, tcell.StyleDefault) } } diff --git a/internal/app/game_view.go b/internal/app/game_view.go index 3a91f1b..48e8b5d 100644 --- a/internal/app/game_view.go +++ b/internal/app/game_view.go @@ -33,35 +33,34 @@ func (v *gameView) handleEvent(e tcell.Event) change { return nil } -func (v *gameView) draw(b *buffer, st *state) { - v.drawHeader(b, st) +func (v *gameView) draw(img canvas, st *state) { + v.drawHeader(img, st) // fill in background dots first for x := 0; x < st.room.Width; x++ { for y := 0; y < st.room.Height; y++ { - b.set(x+1, y+2, tile{r: '·', style: tcell.StyleDefault}) + img.setTile(x+1, y+2, tile{r: '·', style: tcell.StyleDefault}) } } - b.set(0, 1, tile{r: '┌'}) - b.set(st.room.Width+1, 1, tile{r: '┐'}) - b.set(0, st.room.Height+2, tile{r: '└'}) - b.set(st.room.Width+1, st.room.Height+2, tile{r: '┘'}) + img.setTile(0, 1, tile{r: '┌'}) + img.setTile(st.room.Width+1, 1, tile{r: '┐'}) + img.setTile(0, st.room.Height+2, tile{r: '└'}) + img.setTile(st.room.Width+1, st.room.Height+2, tile{r: '┘'}) for x := 0; x < st.room.Width; x++ { - b.set(x+1, 1, tile{r: '─'}) - b.set(x+1, st.room.Height+2, tile{r: '─'}) + img.setTile(x+1, 1, tile{r: '─'}) + img.setTile(x+1, st.room.Height+2, tile{r: '─'}) } for y := 0; y < st.room.Height; y++ { - b.set(0, y+2, tile{r: '│'}) - b.set(st.room.Width+1, y+2, tile{r: '│'}) + img.setTile(0, y+2, tile{r: '│'}) + img.setTile(st.room.Width+1, y+2, tile{r: '│'}) } for _, e := range st.room.Entities { pos := e.Position - b.set(pos.X+1, pos.Y+2, tile{r: e.Glyph, style: tcell.StyleDefault}) + img.setTile(pos.X+1, pos.Y+2, tile{r: e.Glyph, style: tcell.StyleDefault}) } - } -func (v *gameView) drawHeader(b *buffer, st *state) { +func (v *gameView) drawHeader(img canvas, st *state) { // the first row is the name of the room that we're currently in var style tcell.Style style = style.Background(tcell.NewRGBColor(64, 64, 128)) @@ -69,11 +68,12 @@ func (v *gameView) drawHeader(b *buffer, st *state) { runes := []rune(st.room.Name) - for i := 0; i < b.width; i++ { + bounds := img.bounds() + for i := 0; i < bounds.Width; i++ { if i < len(runes) { - b.set(i, 0, tile{r: runes[i], style: style}) + img.setTile(i, 0, tile{r: runes[i], style: style}) } else { - b.set(i, 0, tile{r: ' ', style: style}) + img.setTile(i, 0, tile{r: ' ', style: style}) } } } @@ -86,5 +86,5 @@ type move struct { } func (m move) exec(ui *UI) { - ui.client.Send(sim.Move{m.x, m.y}) + go ui.client.Send(sim.Move{m.x, m.y}) } diff --git a/internal/app/login.go b/internal/app/login.go index fe0eedd..aba51a1 100644 --- a/internal/app/login.go +++ b/internal/app/login.go @@ -1,6 +1,7 @@ package app import ( + "github.com/jordanorelli/astro-domu/internal/math" "github.com/jordanorelli/astro-domu/internal/wire" ) @@ -37,9 +38,18 @@ func (l login) exec(ui *UI) { // e := room.Entities[p.Avatar] ui.state.room = &room - ui.root = &node{ - view: &gameView{ - Log: ui.Child("game-view"), + ui.root = &containerView{ + children: []*node{ + { + frame: math.Rect{math.Vec{0, 0}, 20, 20}, + view: &gameView{ + Log: ui.Child("game-view"), + }, + }, + { + frame: math.Rect{math.Vec{0, 20}, 40, 20}, + view: &chatView{}, + }, }, } ui.Info("done logging in, we replaced the root view whaduheck") diff --git a/internal/app/menu_list.go b/internal/app/menu_list.go index 35800df..753c16c 100644 --- a/internal/app/menu_list.go +++ b/internal/app/menu_list.go @@ -32,12 +32,12 @@ func (m *menuList) handleEvent(e tcell.Event) change { return nil } -func (m *menuList) draw(b *buffer, _ *state) { +func (m *menuList) draw(img canvas, _ *state) { for i, choice := range m.choices { if i == m.highlight { - b.writeString("▷ "+choice.name, math.Vec{0, i}, tcell.StyleDefault) + writeString(img, "▷ "+choice.name, math.Vec{0, i}, tcell.StyleDefault) } else { - b.writeString(" "+choice.name, math.Vec{0, i}, tcell.StyleDefault) + writeString(img, " "+choice.name, math.Vec{0, i}, tcell.StyleDefault) } } } diff --git a/internal/app/node.go b/internal/app/node.go deleted file mode 100644 index 9250a27..0000000 --- a/internal/app/node.go +++ /dev/null @@ -1,6 +0,0 @@ -package app - -type node struct { - view - children []*node -} diff --git a/internal/app/status_bar.go b/internal/app/status_bar.go index 8b6f571..37537b7 100644 --- a/internal/app/status_bar.go +++ b/internal/app/status_bar.go @@ -16,9 +16,9 @@ type statusBar struct { func (s *statusBar) handleEvent(ui *UI, e tcell.Event) bool { return false } -func (s *statusBar) draw(b *buffer) { +func (s *statusBar) draw(img canvas, st *state) { line := fmt.Sprintf("shown: %08d cleared: %08d messages: %08d", s.showCount, s.clearCount, s.msgCount) - b.writeString(line, math.Vec{0, 0}, tcell.StyleDefault) + writeString(img, line, math.Vec{0, 0}, tcell.StyleDefault) } func (s *statusBar) setFocus(enabled bool) { s.inFocus = enabled } diff --git a/internal/app/text.go b/internal/app/text.go index 2156521..f440748 100644 --- a/internal/app/text.go +++ b/internal/app/text.go @@ -36,6 +36,6 @@ func (t *textInput) handleEvent(e tcell.Event) change { return nil } -func (t *textInput) draw(b *buffer, _ *state) { - b.writeString(t.prompt+t.entered, math.Vec{0, 0}, tcell.StyleDefault) +func (t *textInput) draw(img canvas, _ *state) { + writeString(img, t.prompt+t.entered, math.Vec{0, 0}, tcell.StyleDefault) } diff --git a/internal/app/ui.go b/internal/app/ui.go index 41d6fa6..47fa6c6 100644 --- a/internal/app/ui.go +++ b/internal/app/ui.go @@ -18,7 +18,7 @@ type UI struct { notifications <-chan wire.Response state state - root *node + root view } func (ui *UI) Run() { @@ -164,33 +164,29 @@ func (ui *UI) pollInput(c chan tcell.Event) { } } -var mainMenu = &node{ - view: &menuList{ - choices: []menuItem{ - menuItem{ - name: "join", - onSelect: changeFn(func(ui *UI) { - ui.root = joinForm - }), - }, - menuItem{ - name: "exit", - onSelect: changeFn(func(ui *UI) { - panic("this is bad programming") - }), - }, +var mainMenu = &menuList{ + choices: []menuItem{ + menuItem{ + name: "join", + onSelect: changeFn(func(ui *UI) { + ui.root = joinForm + }), + }, + menuItem{ + name: "exit", + onSelect: changeFn(func(ui *UI) { + panic("this is bad programming") + }), }, }, } -var joinForm = &node{ - view: &form{ - fields: []textField{ - textField{ - label: "What is your name?", - textInput: textInput{ - prompt: "> ", - }, +var joinForm = &form{ + fields: []textField{ + textField{ + label: "What is your name?", + textInput: textInput{ + prompt: "> ", }, }, }, diff --git a/internal/app/view.go b/internal/app/view.go index 38540bb..be49cca 100644 --- a/internal/app/view.go +++ b/internal/app/view.go @@ -6,7 +6,7 @@ import ( type view interface { handleEvent(tcell.Event) change - draw(*buffer, *state) + draw(canvas, *state) } type focusable interface {