diff --git a/main.go b/main.go index 416f6f3..418213c 100644 --- a/main.go +++ b/main.go @@ -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() } diff --git a/render/context.go b/render/context.go new file mode 100644 index 0000000..134f272 --- /dev/null +++ b/render/context.go @@ -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: + } + } +} diff --git a/render/grid.go b/render/grid.go index 318a4fa..c452a01 100644 --- a/render/grid.go +++ b/render/grid.go @@ -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) +} diff --git a/render/layers.go b/render/layers.go index bdfb574..524b541 100644 --- a/render/layers.go +++ b/render/layers.go @@ -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) } diff --git a/render/rectangle.go b/render/rectangle.go index ed7f3fa..6663dc8 100644 --- a/render/rectangle.go +++ b/render/rectangle.go @@ -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) + } +} diff --git a/render/text.go b/render/text.go index a8181a1..d588406 100644 --- a/render/text.go +++ b/render/text.go @@ -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()