Render context, centered window, better rectangle drawing

This commit is contained in:
Miroslav Vasilev 2024-04-21 21:51:43 +03:00
parent 422840fc7b
commit dce7d29a99
6 changed files with 352 additions and 159 deletions

79
main.go
View file

@ -5,88 +5,61 @@ import (
"log" "log"
"mvvasilev/last_light/render" "mvvasilev/last_light/render"
"os" "os"
"time"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
) )
func main() { func main() {
s, err := tcell.NewScreen() c, err := render.CreateRenderContext()
if err != nil { if err != nil {
log.Fatalf("%~v", err) log.Fatalf("%~v", err)
} }
if err := s.Init(); err != nil { c.HandleInput(func(ev *tcell.EventKey) {
log.Fatalf("%+v", err) if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
c.Stop()
os.Exit(0)
} }
})
// width, height := s.Size()
// if width < 50 || height < 50 {
// log.Fatalf("Your terminal must be at least 50x50")
// }
defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset) defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset)
rect := render.CreateRectangle( rect := render.CreateRectangle(
0, 1, 10, 10, 0, 0, 80, 24,
'┌', '─', '┐', '┌', '─', '┐',
'│', '#', '│', '│', '#', '│',
'└', '─', '┘', '└', '─', '┘',
defStyle, false, true, defStyle,
) )
text := render.CreateText(1, 2, 8, 8, "Hello World! How are you today?", defStyle) // text := render.CreateText(1, 2, 8, 8, "Hello World! How are you today?", defStyle)
// grid := render.CreateGrid(
// 11, 1, 3, 3, 3, 3,
// '┌', '─', '┬', '┐',
// '│', '#', '│', '│',
// '├', '─', '┼', '┤',
// '└', '─', '┴', '┘',
// defStyle,
// )
layers := render.CreateLayeredDrawContainer() layers := render.CreateLayeredDrawContainer()
layers.Insert(0, rect) layers.Insert(0, rect)
layers.Insert(1, text) // layers.Insert(1, text)
// layers.Insert(0, grid)
//layers.Remove(text.UniqueId())
events := make(chan tcell.Event)
quit := make(chan struct{})
go s.ChannelEvents(events, quit)
lastTime := time.Now()
for {
deltaTime := 1 + time.Since(lastTime).Microseconds()
lastTime = time.Now()
s.Clear()
c.HandleRender(func(view views.View, deltaTime int64) {
fps := 1_000_000 / deltaTime fps := 1_000_000 / deltaTime
fpsText := render.CreateText(0, 0, 16, 1, fmt.Sprintf("%v FPS", fps), defStyle) fpsText := render.CreateText(0, 0, 16, 1, fmt.Sprintf("%v FPS", fps), defStyle)
layers.Draw(s) layers.Draw(view)
fpsText.Draw(s) fpsText.Draw(view)
})
s.Show()
select {
case ev, ok := <-events:
if !ok {
break
}
switch ev := ev.(type) {
case *tcell.EventResize:
s.Sync()
case *tcell.EventKey:
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
s.Fini()
os.Exit(0)
}
}
default:
}
}
c.BeginRendering()
} }

147
render/context.go Normal file
View file

@ -0,0 +1,147 @@
package render
import (
"errors"
"log"
"time"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid"
)
const (
TERMINAL_SIZE_WIDTH int = 80
TERMINAL_SIZE_HEIGHT int = 24
DEFAULT_STYLE_BACKGROUND tcell.Color = tcell.ColorReset
DEFAULT_STYLE_FOREGROUND tcell.Color = tcell.ColorReset
)
type Drawable interface {
UniqueId() uuid.UUID
Draw(v views.View)
}
type renderContext struct {
screen tcell.Screen
view *views.ViewPort
defaultStyle tcell.Style
events chan tcell.Event
quit chan struct{}
lastRenderTime time.Time
renderHandler func(view views.View, deltaTime int64)
inputHandler func(ev *tcell.EventKey)
}
func CreateRenderContext() (*renderContext, error) {
s, err := tcell.NewScreen()
if err != nil {
log.Fatal(err)
return nil, err
}
stopScreen := func() {
s.Fini()
}
if err := s.Init(); err != nil {
stopScreen()
log.Fatal(err)
return nil, err
}
width, height := s.Size()
if width < TERMINAL_SIZE_WIDTH || height < TERMINAL_SIZE_HEIGHT {
stopScreen()
log.Fatal("Unable to start; Terminal must be at least 80x24")
return nil, errors.New("Terminal is undersized; must be at least 80x24")
}
view := views.NewViewPort(
s,
(width/2)-(TERMINAL_SIZE_WIDTH/2),
(height/2)-(TERMINAL_SIZE_HEIGHT/2),
TERMINAL_SIZE_WIDTH,
TERMINAL_SIZE_HEIGHT,
)
defStyle := tcell.StyleDefault.Background(DEFAULT_STYLE_BACKGROUND).Foreground(DEFAULT_STYLE_FOREGROUND)
events := make(chan tcell.Event)
quit := make(chan struct{})
go s.ChannelEvents(events, quit)
context := new(renderContext)
context.screen = s
context.defaultStyle = defStyle
context.events = events
context.quit = quit
context.view = view
return context, nil
}
func (c *renderContext) Stop() {
c.screen.Fini()
}
func (c *renderContext) HandleRender(renderHandler func(view views.View, deltaTime int64)) {
c.renderHandler = renderHandler
}
func (c *renderContext) HandleInput(inputHandler func(ev *tcell.EventKey)) {
c.inputHandler = inputHandler
}
func (c *renderContext) onResize(ev *tcell.EventResize) {
width, height := ev.Size()
c.screen.Clear()
c.view.Resize(
(width/2)-(TERMINAL_SIZE_WIDTH/2),
(height/2)-(TERMINAL_SIZE_HEIGHT/2),
TERMINAL_SIZE_WIDTH,
TERMINAL_SIZE_HEIGHT,
)
c.screen.Sync()
}
func (c *renderContext) BeginRendering() {
c.lastRenderTime = time.Now()
for {
deltaTime := 1 + time.Since(c.lastRenderTime).Microseconds()
c.lastRenderTime = time.Now()
c.screen.Clear()
c.renderHandler(c.view, deltaTime)
c.screen.Show()
select {
case ev, ok := <-c.events:
if !ok {
break
}
switch ev := ev.(type) {
case *tcell.EventResize:
c.onResize(ev)
case *tcell.EventKey:
c.inputHandler(ev)
}
default:
}
}
}

View file

@ -4,6 +4,7 @@ import (
"mvvasilev/last_light/util" "mvvasilev/last_light/util"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -21,18 +22,44 @@ type grid struct {
eastBorder rune eastBorder rune
southBorder rune southBorder rune
internalVerticalBorder rune
internalHorizontalBorder rune
nwCorner rune nwCorner rune
swCorner rune swCorner rune
seCorner rune seCorner rune
neCorner rune neCorner rune
verticalTJunction rune verticalDownwardsTJunction rune
horizontalTJunction rune verticalUpwardsTJunction rune
horizontalLeftTJunction rune
horizontalRightTJunction rune
crossJunction rune crossJunction rune
fillRune rune fillRune rune
} }
func CreateSimpleGrid(
x, y uint16,
cellWidth, cellHeight uint16,
numCellsHorizontal, numCellsVertical uint16,
borderRune, fillRune rune,
style tcell.Style,
) grid {
return CreateGrid(
x, y, cellWidth, cellHeight, numCellsHorizontal, numCellsVertical,
borderRune, borderRune, borderRune, borderRune,
borderRune, fillRune, borderRune, borderRune,
borderRune, borderRune, borderRune, borderRune,
borderRune, borderRune, borderRune, borderRune,
style,
)
}
// '┌', '─', '┬', '┐',
// '│', '#', '│', '│',
// '├', '─', '┼', '┤',
// '└', '─', '┴', '┘',
func CreateGrid( func CreateGrid(
x uint16, x uint16,
y uint16, y uint16,
@ -40,11 +67,10 @@ func CreateGrid(
cellHeight uint16, cellHeight uint16,
numCellsHorizontal uint16, numCellsHorizontal uint16,
numCellsVertical uint16, numCellsVertical uint16,
nwCorner, northBorder, neCorner, nwCorner, northBorder, verticalDownwardsTJunction, neCorner,
westBorder, fillRune, eastBorder, westBorder, fillRune, internalVerticalBorder, eastBorder,
swCorner, southBorder, seCorner, horizontalRightTJunction, internalHorizontalBorder, crossJunction, horizontalLeftTJunction,
verticalTJunction, horizontalTJunction, swCorner, southBorder, verticalUpwardsTJunction, seCorner rune,
crossJunction rune,
style tcell.Style, style tcell.Style,
) grid { ) grid {
return grid{ return grid{
@ -58,13 +84,18 @@ func CreateGrid(
eastBorder: eastBorder, eastBorder: eastBorder,
southBorder: southBorder, southBorder: southBorder,
westBorder: westBorder, westBorder: westBorder,
internalVerticalBorder: internalVerticalBorder,
internalHorizontalBorder: internalHorizontalBorder,
nwCorner: nwCorner, nwCorner: nwCorner,
seCorner: seCorner, seCorner: seCorner,
swCorner: swCorner, swCorner: swCorner,
neCorner: neCorner, neCorner: neCorner,
verticalDownwardsTJunction: verticalDownwardsTJunction,
verticalUpwardsTJunction: verticalUpwardsTJunction,
horizontalRightTJunction: horizontalRightTJunction,
horizontalLeftTJunction: horizontalLeftTJunction,
fillRune: fillRune, fillRune: fillRune,
verticalTJunction: verticalTJunction,
horizontalTJunction: horizontalTJunction,
crossJunction: crossJunction, crossJunction: crossJunction,
} }
} }
@ -73,6 +104,46 @@ func (g grid) UniqueId() uuid.UUID {
return g.id return g.id
} }
func (g grid) Draw(s tcell.Screen) { // C###T###T###C
// # # # #
// # # # #
// # # # #
// T###X###X###T
// # # # #
// # # # #
// # # # #
// T###X###X###T
// # # # #
// # # # #
// # # # #
// C###T###T###C
func (g grid) drawBorders(v views.View) {
width := 2 + (g.internalCellSize.Width() * int(g.numCellsHorizontal)) + (int(g.numCellsHorizontal) - 1)
height := 2 + (g.internalCellSize.Height() * int(g.numCellsVertical)) + (int(g.numCellsVertical) - 1)
x := g.position.X()
y := g.position.Y()
v.SetContent(x, y, g.nwCorner, nil, g.style)
v.SetContent(x+width-1, y, g.neCorner, nil, g.style)
v.SetContent(x, y+height-1, g.swCorner, nil, g.style)
v.SetContent(x+width-1, y+height-1, g.seCorner, nil, g.style)
for w := range width - 2 {
v.SetContent(1+w, y, g.northBorder, nil, g.style)
v.SetContent(1+w, y+height-1, g.southBorder, nil, g.style)
}
for h := range height - 2 {
v.SetContent(x, 1+h, g.westBorder, nil, g.style)
v.SetContent(x+width-1, 1+h, g.eastBorder, nil, g.style)
}
}
func (g grid) drawFill(v views.View) {
} }
func (g grid) Draw(v views.View) {
g.drawBorders(v)
g.drawFill(v)
}

View file

@ -3,7 +3,7 @@ package render
import ( import (
"slices" "slices"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2/views"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -31,7 +31,7 @@ func (l *layer) remove(uuid uuid.UUID) {
}) })
} }
func (l *layer) draw(s tcell.Screen) { func (l *layer) draw(s views.View) {
for _, d := range l.contents { for _, d := range l.contents {
d.Draw(s) d.Draw(s)
} }
@ -112,7 +112,7 @@ func (ldc *layeredDrawContainer) UniqueId() uuid.UUID {
return ldc.id return ldc.id
} }
func (ldc *layeredDrawContainer) Draw(s tcell.Screen) { func (ldc *layeredDrawContainer) Draw(s views.View) {
for _, d := range ldc.layers { for _, d := range ldc.layers {
d.draw(s) d.draw(s)
} }

View file

@ -4,6 +4,7 @@ import (
"mvvasilev/last_light/util" "mvvasilev/last_light/util"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -24,16 +25,39 @@ type rectangle struct {
seCorner rune seCorner rune
neCorner rune neCorner rune
isBorderless bool
isFilled bool
fillRune rune fillRune rune
} }
func CreateBorderlessRectangle(x, y uint16, width, height uint16, fillRune rune, style tcell.Style) rectangle {
return CreateRectangle(
x, y, width, height,
0, 0, 0,
0, fillRune, 0,
0, 0, 0,
true, true, style,
)
}
func CreateSimpleEmptyRectangle(x, y uint16, width, height uint16, borderRune rune, style tcell.Style) rectangle {
return CreateRectangle(
x, y, width, height,
borderRune, borderRune, borderRune,
borderRune, 0, borderRune,
borderRune, borderRune, borderRune,
false, false, style,
)
}
func CreateSimpleRectangle(x uint16, y uint16, width uint16, height uint16, borderRune rune, fillRune rune, style tcell.Style) rectangle { func CreateSimpleRectangle(x uint16, y uint16, width uint16, height uint16, borderRune rune, fillRune rune, style tcell.Style) rectangle {
return CreateRectangle( return CreateRectangle(
x, y, width, height, x, y, width, height,
borderRune, borderRune, borderRune, borderRune, borderRune, borderRune,
borderRune, fillRune, borderRune, borderRune, fillRune, borderRune,
borderRune, borderRune, borderRune, borderRune, borderRune, borderRune,
style, false, true, style,
) )
} }
@ -43,6 +67,7 @@ func CreateSimpleRectangle(x uint16, y uint16, width uint16, height uint16, bord
// '┌', '─', '┐', // '┌', '─', '┐',
// '│', ' ', '│', // '│', ' ', '│',
// '└', '─', '┘', // '└', '─', '┘',
// false, true,
// style // style
// //
// ) // )
@ -54,6 +79,7 @@ func CreateRectangle(
nwCorner, northBorder, neCorner, nwCorner, northBorder, neCorner,
westBorder, fillRune, eastBorder, westBorder, fillRune, eastBorder,
swCorner, southBorder, seCorner rune, swCorner, southBorder, seCorner rune,
isBorderless, isFilled bool,
style tcell.Style, style tcell.Style,
) rectangle { ) rectangle {
return rectangle{ return rectangle{
@ -69,6 +95,8 @@ func CreateRectangle(
seCorner: seCorner, seCorner: seCorner,
swCorner: swCorner, swCorner: swCorner,
neCorner: neCorner, neCorner: neCorner,
isBorderless: isBorderless,
isFilled: isFilled,
fillRune: fillRune, fillRune: fillRune,
} }
} }
@ -77,64 +105,42 @@ func (rect rectangle) UniqueId() uuid.UUID {
return rect.id return rect.id
} }
func (rect rectangle) Draw(s tcell.Screen) { func (rect rectangle) drawBorders(v views.View) {
width := rect.size.Width() width := rect.size.Width()
height := rect.size.Height() height := rect.size.Height()
x := rect.position.X() x := rect.position.X()
y := rect.position.Y() y := rect.position.Y()
for h := range height { v.SetContent(x, y, rect.nwCorner, nil, rect.style)
for w := range width { v.SetContent(x+width-1, y, rect.neCorner, nil, rect.style)
v.SetContent(x, y+height-1, rect.swCorner, nil, rect.style)
v.SetContent(x+width-1, y+height-1, rect.seCorner, nil, rect.style)
// nw corner for w := range width - 2 {
if w == 0 && h == 0 { v.SetContent(1+w, y, rect.northBorder, nil, rect.style)
s.SetContent(x+w, y+h, rect.nwCorner, nil, rect.style) v.SetContent(1+w, y+height-1, rect.southBorder, nil, rect.style)
continue
} }
// ne corner for h := range height - 2 {
if w == (width-1) && h == 0 { v.SetContent(x, 1+h, rect.westBorder, nil, rect.style)
s.SetContent(x+w, y+h, rect.neCorner, nil, rect.style) v.SetContent(x+width-1, 1+h, rect.eastBorder, nil, rect.style)
continue
} }
}
// sw corner func (rect rectangle) drawFill(v views.View) {
if w == 0 && h == (height-1) { for w := range rect.size.Width() - 2 {
s.SetContent(x+w, y+h, rect.swCorner, nil, rect.style) for h := range rect.size.Height() - 2 {
continue v.SetContent(1+w, 1+h, rect.fillRune, nil, rect.style)
}
// se corner
if w == (width-1) && h == (height-1) {
s.SetContent(x+w, y+h, rect.seCorner, nil, rect.style)
continue
}
// north border
if h == 0 && (w != 0 && w != (width-1)) {
s.SetContent(x+w, y+h, rect.northBorder, nil, rect.style)
continue
}
// south border
if h == (height-1) && (w != 0 && w != (width-1)) {
s.SetContent(x+w, y+h, rect.southBorder, nil, rect.style)
continue
}
// west border
if w == 0 && (h != 0 && h != (height-1)) {
s.SetContent(x+w, y+h, rect.westBorder, nil, rect.style)
continue
}
// east border
if w == (width-1) && (h != 0 && h != (height-1)) {
s.SetContent(x+w, y+h, rect.eastBorder, nil, rect.style)
continue
}
s.SetContent(x+w, y+h, rect.fillRune, nil, rect.style)
} }
} }
} }
func (rect rectangle) Draw(v views.View) {
if !rect.isBorderless {
rect.drawBorders(v)
}
if rect.isFilled {
rect.drawFill(v)
}
}

View file

@ -6,14 +6,10 @@ import (
"unicode/utf8" "unicode/utf8"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid" "github.com/google/uuid"
) )
type Drawable interface {
UniqueId() uuid.UUID
Draw(s tcell.Screen)
}
type text struct { type text struct {
id uuid.UUID id uuid.UUID
content []string content []string
@ -41,7 +37,7 @@ func (t text) UniqueId() uuid.UUID {
return t.id return t.id
} }
func (t text) Draw(s tcell.Screen) { func (t text) Draw(s views.View) {
width := t.size.Width() width := t.size.Width()
height := t.size.Height() height := t.size.Height()
x := t.position.X() x := t.position.X()