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

81
main.go
View file

@ -5,88 +5,61 @@ import (
"log"
"mvvasilev/last_light/render"
"os"
"time"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
)
func main() {
s, err := tcell.NewScreen()
c, err := render.CreateRenderContext()
if err != nil {
log.Fatalf("%~v", err)
}
if err := s.Init(); err != nil {
log.Fatalf("%+v", err)
}
// width, height := s.Size()
// if width < 50 || height < 50 {
// log.Fatalf("Your terminal must be at least 50x50")
// }
c.HandleInput(func(ev *tcell.EventKey) {
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
c.Stop()
os.Exit(0)
}
})
defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset)
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.Insert(0, rect)
layers.Insert(1, text)
//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()
// layers.Insert(1, text)
// layers.Insert(0, grid)
c.HandleRender(func(view views.View, deltaTime int64) {
fps := 1_000_000 / deltaTime
fpsText := render.CreateText(0, 0, 16, 1, fmt.Sprintf("%v FPS", fps), defStyle)
layers.Draw(s)
fpsText.Draw(s)
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:
}
}
layers.Draw(view)
fpsText.Draw(view)
})
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"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid"
)
@ -21,18 +22,44 @@ type grid struct {
eastBorder rune
southBorder rune
internalVerticalBorder rune
internalHorizontalBorder rune
nwCorner rune
swCorner rune
seCorner rune
neCorner rune
verticalTJunction rune
horizontalTJunction rune
crossJunction rune
verticalDownwardsTJunction rune
verticalUpwardsTJunction rune
horizontalLeftTJunction rune
horizontalRightTJunction rune
crossJunction 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(
x uint16,
y uint16,
@ -40,32 +67,36 @@ func CreateGrid(
cellHeight uint16,
numCellsHorizontal uint16,
numCellsVertical uint16,
nwCorner, northBorder, neCorner,
westBorder, fillRune, eastBorder,
swCorner, southBorder, seCorner,
verticalTJunction, horizontalTJunction,
crossJunction rune,
nwCorner, northBorder, verticalDownwardsTJunction, neCorner,
westBorder, fillRune, internalVerticalBorder, eastBorder,
horizontalRightTJunction, internalHorizontalBorder, crossJunction, horizontalLeftTJunction,
swCorner, southBorder, verticalUpwardsTJunction, seCorner rune,
style tcell.Style,
) grid {
return grid{
id: uuid.New(),
internalCellSize: util.SizeOf(cellWidth, cellHeight),
numCellsHorizontal: numCellsHorizontal,
numCellsVertical: numCellsVertical,
position: util.PositionAt(x, y),
style: style,
northBorder: northBorder,
eastBorder: eastBorder,
southBorder: southBorder,
westBorder: westBorder,
nwCorner: nwCorner,
seCorner: seCorner,
swCorner: swCorner,
neCorner: neCorner,
fillRune: fillRune,
verticalTJunction: verticalTJunction,
horizontalTJunction: horizontalTJunction,
crossJunction: crossJunction,
id: uuid.New(),
internalCellSize: util.SizeOf(cellWidth, cellHeight),
numCellsHorizontal: numCellsHorizontal,
numCellsVertical: numCellsVertical,
position: util.PositionAt(x, y),
style: style,
northBorder: northBorder,
eastBorder: eastBorder,
southBorder: southBorder,
westBorder: westBorder,
internalVerticalBorder: internalVerticalBorder,
internalHorizontalBorder: internalHorizontalBorder,
nwCorner: nwCorner,
seCorner: seCorner,
swCorner: swCorner,
neCorner: neCorner,
verticalDownwardsTJunction: verticalDownwardsTJunction,
verticalUpwardsTJunction: verticalUpwardsTJunction,
horizontalRightTJunction: horizontalRightTJunction,
horizontalLeftTJunction: horizontalLeftTJunction,
fillRune: fillRune,
crossJunction: crossJunction,
}
}
@ -73,6 +104,46 @@ func (g grid) UniqueId() uuid.UUID {
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 (
"slices"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"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 {
d.Draw(s)
}
@ -112,7 +112,7 @@ func (ldc *layeredDrawContainer) UniqueId() uuid.UUID {
return ldc.id
}
func (ldc *layeredDrawContainer) Draw(s tcell.Screen) {
func (ldc *layeredDrawContainer) Draw(s views.View) {
for _, d := range ldc.layers {
d.draw(s)
}

View file

@ -4,6 +4,7 @@ import (
"mvvasilev/last_light/util"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid"
)
@ -24,26 +25,50 @@ type rectangle struct {
seCorner rune
neCorner rune
isBorderless bool
isFilled bool
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 {
return CreateRectangle(
x, y, width, height,
borderRune, borderRune, borderRune,
borderRune, fillRune, borderRune,
borderRune, borderRune, borderRune,
style,
false, true, style,
)
}
// CreateRectangle(
//
// x, y, width, height,
// '┌', '─', '┐',
// '│', ' ', '│',
// '└', '─', '┘',
// style
// x, y, width, height,
// '┌', '─', '┐',
// '│', ' ', '│',
// '└', '─', '┘',
// false, true,
// style
//
// )
func CreateRectangle(
@ -54,22 +79,25 @@ func CreateRectangle(
nwCorner, northBorder, neCorner,
westBorder, fillRune, eastBorder,
swCorner, southBorder, seCorner rune,
isBorderless, isFilled bool,
style tcell.Style,
) rectangle {
return rectangle{
id: uuid.New(),
size: util.SizeOf(width, height),
position: util.PositionAt(x, y),
style: style,
northBorder: northBorder,
eastBorder: eastBorder,
southBorder: southBorder,
westBorder: westBorder,
nwCorner: nwCorner,
seCorner: seCorner,
swCorner: swCorner,
neCorner: neCorner,
fillRune: fillRune,
id: uuid.New(),
size: util.SizeOf(width, height),
position: util.PositionAt(x, y),
style: style,
northBorder: northBorder,
eastBorder: eastBorder,
southBorder: southBorder,
westBorder: westBorder,
nwCorner: nwCorner,
seCorner: seCorner,
swCorner: swCorner,
neCorner: neCorner,
isBorderless: isBorderless,
isFilled: isFilled,
fillRune: fillRune,
}
}
@ -77,64 +105,42 @@ func (rect rectangle) UniqueId() uuid.UUID {
return rect.id
}
func (rect rectangle) Draw(s tcell.Screen) {
func (rect rectangle) drawBorders(v views.View) {
width := rect.size.Width()
height := rect.size.Height()
x := rect.position.X()
y := rect.position.Y()
for h := range height {
for w := range width {
v.SetContent(x, y, rect.nwCorner, nil, rect.style)
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
if w == 0 && h == 0 {
s.SetContent(x+w, y+h, rect.nwCorner, nil, rect.style)
continue
}
for w := range width - 2 {
v.SetContent(1+w, y, rect.northBorder, nil, rect.style)
v.SetContent(1+w, y+height-1, rect.southBorder, nil, rect.style)
}
// ne corner
if w == (width-1) && h == 0 {
s.SetContent(x+w, y+h, rect.neCorner, nil, rect.style)
continue
}
for h := range height - 2 {
v.SetContent(x, 1+h, rect.westBorder, nil, rect.style)
v.SetContent(x+width-1, 1+h, rect.eastBorder, nil, rect.style)
}
}
// sw corner
if w == 0 && h == (height-1) {
s.SetContent(x+w, y+h, rect.swCorner, nil, rect.style)
continue
}
// 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) drawFill(v views.View) {
for w := range rect.size.Width() - 2 {
for h := range rect.size.Height() - 2 {
v.SetContent(1+w, 1+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"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid"
)
type Drawable interface {
UniqueId() uuid.UUID
Draw(s tcell.Screen)
}
type text struct {
id uuid.UUID
content []string
@ -41,7 +37,7 @@ func (t text) UniqueId() uuid.UUID {
return t.id
}
func (t text) Draw(s tcell.Screen) {
func (t text) Draw(s views.View) {
width := t.size.Width()
height := t.size.Height()
x := t.position.X()