From c0f80f0e0c74985c6a6e47564e3d2e8fb1284ea4 Mon Sep 17 00:00:00 2001 From: mvvasilev Date: Wed, 24 Apr 2024 17:11:33 +0300 Subject: [PATCH] Main Menu, main play state, pause menu --- game/game.go | 41 +++++++++++++ game/model/entity.go | 30 ++++++++++ game/model/player.go | 76 ++++++++++++++++++++++++ game/state/gameState.go | 20 +++++++ game/state/mainMenuState.go | 87 ++++++++++++++++++++++++++++ game/state/pauseGameState.go | 108 +++++++++++++++++++++++++++++++++++ game/state/playingState.go | 56 ++++++++++++++++++ game/state/quitState.go | 21 +++++++ layer.go | 1 - main.go | 42 +++++--------- render/context.go | 16 +++--- render/grid.go | 66 +++++++++++++++------ render/layers.go | 6 +- render/raw.go | 46 +++++++++++++++ render/rectangle.go | 66 +++++++++++++++------ render/text.go | 44 ++++++++++---- terminal.go | 1 - ui/borderButton.go | 62 ++++++++++++++++++++ ui/container.go | 82 ++++++++++++++++++++++++++ ui/label.go | 56 ++++++++++++++++++ ui/simpleButton.go | 102 +++++++++++++++++++++++++++++++++ ui/ui.go | 20 +++++++ ui/window.go | 62 ++++++++++++++++++++ util/util.go | 28 +++++++++ 24 files changed, 1053 insertions(+), 86 deletions(-) create mode 100644 game/game.go create mode 100644 game/model/entity.go create mode 100644 game/model/player.go create mode 100644 game/state/gameState.go create mode 100644 game/state/mainMenuState.go create mode 100644 game/state/pauseGameState.go create mode 100644 game/state/playingState.go create mode 100644 game/state/quitState.go delete mode 100644 layer.go create mode 100644 render/raw.go delete mode 100644 terminal.go create mode 100644 ui/borderButton.go create mode 100644 ui/container.go create mode 100644 ui/label.go create mode 100644 ui/simpleButton.go create mode 100644 ui/window.go diff --git a/game/game.go b/game/game.go new file mode 100644 index 0000000..a4522df --- /dev/null +++ b/game/game.go @@ -0,0 +1,41 @@ +package game + +import ( + "mvvasilev/last_light/game/state" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" +) + +type Game struct { + state state.GameState +} + +func CreateGame() *Game { + game := new(Game) + + game.state = state.NewMainMenuState() + + return game +} + +func (g *Game) Input(ev *tcell.EventKey) { + g.state.OnInput(ev) +} + +func (g *Game) Tick(dt int64) bool { + s := g.state.OnTick(dt) + + switch s.(type) { + case *state.QuitState: + return false + } + + g.state = s + + return true +} + +func (g *Game) Draw(v views.View) { + g.state.OnDraw(v) +} diff --git a/game/model/entity.go b/game/model/entity.go new file mode 100644 index 0000000..c6da679 --- /dev/null +++ b/game/model/entity.go @@ -0,0 +1,30 @@ +package model + +import ( + "mvvasilev/last_light/util" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" + "github.com/google/uuid" +) + +type Direction int + +const ( + Up Direction = iota + Down + Left + Right +) + +type Entity interface { + UniqueId() uuid.UUID + Draw(v views.View) + Input(e *tcell.EventKey) + Tick(dt int64) +} + +type MovableEntity interface { + Position() util.Position + Move(dir Direction) +} diff --git a/game/model/player.go b/game/model/player.go new file mode 100644 index 0000000..ba60c0e --- /dev/null +++ b/game/model/player.go @@ -0,0 +1,76 @@ +package model + +import ( + "mvvasilev/last_light/util" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" + "github.com/google/uuid" +) + +type Player struct { + id uuid.UUID + position util.Position + style tcell.Style +} + +func CreatePlayer(x, y uint16, style tcell.Style) *Player { + p := new(Player) + + p.id = uuid.New() + p.position = util.PositionAt(x, y) + + return p +} + +func (p *Player) UniqueId() uuid.UUID { + return p.id +} + +func (p *Player) Move(dir Direction) { + x, y := p.position.XYUint16() + + switch dir { + case Up: + p.position = util.PositionAt(x, y-1) + case Down: + p.position = util.PositionAt(x, y+1) + case Left: + p.position = util.PositionAt(x-1, y) + case Right: + p.position = util.PositionAt(x+1, y) + } +} + +func (p *Player) Draw(v views.View) { + x, y := p.position.XY() + v.SetContent(x, y, '@', nil, p.style) +} + +func (p *Player) Input(e *tcell.EventKey) { + switch e.Key() { + case tcell.KeyUp: + p.Move(Up) + case tcell.KeyDown: + p.Move(Down) + case tcell.KeyLeft: + p.Move(Left) + case tcell.KeyRight: + p.Move(Right) + case tcell.KeyRune: + switch e.Rune() { + case 'w': + p.Move(Up) + case 'a': + p.Move(Left) + case 's': + p.Move(Down) + case 'd': + p.Move(Right) + } + } +} + +func (p *Player) Tick(dt int64) { + +} diff --git a/game/state/gameState.go b/game/state/gameState.go new file mode 100644 index 0000000..2d606b2 --- /dev/null +++ b/game/state/gameState.go @@ -0,0 +1,20 @@ +package state + +import ( + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" +) + +type GameState interface { + OnInput(e *tcell.EventKey) + OnTick(dt int64) GameState + OnDraw(c views.View) +} + +type PausableState interface { + Pause() + Unpause() + SetPaused(paused bool) + + GameState +} diff --git a/game/state/mainMenuState.go b/game/state/mainMenuState.go new file mode 100644 index 0000000..fba766b --- /dev/null +++ b/game/state/mainMenuState.go @@ -0,0 +1,87 @@ +package state + +import ( + "mvvasilev/last_light/render" + "mvvasilev/last_light/ui" + "mvvasilev/last_light/util" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" +) + +type MainMenuState struct { + menuTitle *render.Raw + buttons []*ui.UISimpleButton + currButtonSelected int + + quitGame bool + startNewGame bool +} + +func NewMainMenuState() *MainMenuState { + state := new(MainMenuState) + + highlightStyle := tcell.StyleDefault.Attributes(tcell.AttrBold) + + state.menuTitle = render.CreateRawDrawable( + 11, 1, tcell.StyleDefault.Attributes(tcell.AttrBold).Foreground(tcell.ColorYellow), + " | | | _) | | ", + " | _` | __| __| | | _` | __ \\ __|", + " | ( | \\__ \\ | | | ( | | | | | ", + "_____| \\__,_| ____/ \\__| _____| _| \\__, | _| |_| \\__|", + " |___/ ", + ) + state.buttons = make([]*ui.UISimpleButton, 0) + state.buttons = append(state.buttons, ui.CreateSimpleButton(11, 7, "New Game", tcell.StyleDefault, highlightStyle, func() { + state.startNewGame = true + })) + state.buttons = append(state.buttons, ui.CreateSimpleButton(11, 9, "Load Game", tcell.StyleDefault, highlightStyle, func() { + + })) + state.buttons = append(state.buttons, ui.CreateSimpleButton(11, 11, "Quit", tcell.StyleDefault, highlightStyle, func() { + state.quitGame = true + })) + + state.currButtonSelected = 0 + state.buttons[state.currButtonSelected].Highlight() + + return state +} + +func (mms *MainMenuState) OnInput(e *tcell.EventKey) { + if e.Key() == tcell.KeyDown { + mms.buttons[mms.currButtonSelected].Unhighlight() + mms.currButtonSelected = util.LimitIncrement(mms.currButtonSelected, 2) + mms.buttons[mms.currButtonSelected].Highlight() + } + + if e.Key() == tcell.KeyUp { + mms.buttons[mms.currButtonSelected].Unhighlight() + mms.currButtonSelected = util.LimitDecrement(mms.currButtonSelected, 0) + mms.buttons[mms.currButtonSelected].Highlight() + } + + if e.Key() == tcell.KeyEnter { + mms.buttons[mms.currButtonSelected].Select() + } +} + +func (mms *MainMenuState) OnTick(dt int64) GameState { + if mms.quitGame { + return &QuitState{} + } + + if mms.startNewGame { + return BeginPlayingState() + } + + return mms +} + +func (mms *MainMenuState) OnDraw(c views.View) { + mms.menuTitle.Draw(c) + + for _, b := range mms.buttons { + b.Draw(c) + } +} diff --git a/game/state/pauseGameState.go b/game/state/pauseGameState.go new file mode 100644 index 0000000..13a3453 --- /dev/null +++ b/game/state/pauseGameState.go @@ -0,0 +1,108 @@ +package state + +import ( + "mvvasilev/last_light/render" + "mvvasilev/last_light/ui" + "mvvasilev/last_light/util" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" +) + +type PauseGameState struct { + prevState PausableState + + unpauseGame bool + returnToMainMenu bool + + pauseMenuWindow *ui.UIWindow + buttons []*ui.UISimpleButton + currButtonSelected int +} + +func PauseGame(prevState PausableState) *PauseGameState { + s := new(PauseGameState) + + s.prevState = prevState + + highlightStyle := tcell.StyleDefault.Attributes(tcell.AttrBold) + + s.pauseMenuWindow = ui.CreateWindow(uint16(render.TERMINAL_SIZE_WIDTH)/2-15, uint16(render.TERMINAL_SIZE_HEIGHT)/2-7, 30, 14, "PAUSED", tcell.StyleDefault) + s.buttons = make([]*ui.UISimpleButton, 0) + s.buttons = append( + s.buttons, + ui.CreateSimpleButton( + uint16(s.pauseMenuWindow.Position().X())+3, + uint16(s.pauseMenuWindow.Position().Y())+1, + "Resume", + tcell.StyleDefault, + highlightStyle, + func() { + s.unpauseGame = true + }, + ), + ) + s.buttons = append( + s.buttons, + ui.CreateSimpleButton( + uint16(s.pauseMenuWindow.Position().X())+3, + uint16(s.pauseMenuWindow.Position().Y())+3, + "Exit To Main Menu", + tcell.StyleDefault, + highlightStyle, + func() { + s.returnToMainMenu = true + }, + ), + ) + + s.currButtonSelected = 0 + s.buttons[s.currButtonSelected].Highlight() + + return s +} + +func (pg *PauseGameState) OnInput(e *tcell.EventKey) { + if e.Key() == tcell.KeyEsc { + pg.unpauseGame = true + } + + if e.Key() == tcell.KeyDown { + pg.buttons[pg.currButtonSelected].Unhighlight() + pg.currButtonSelected = util.LimitIncrement(pg.currButtonSelected, 1) + pg.buttons[pg.currButtonSelected].Highlight() + } + + if e.Key() == tcell.KeyUp { + pg.buttons[pg.currButtonSelected].Unhighlight() + pg.currButtonSelected = util.LimitDecrement(pg.currButtonSelected, 0) + pg.buttons[pg.currButtonSelected].Highlight() + } + + if e.Key() == tcell.KeyEnter { + pg.buttons[pg.currButtonSelected].Select() + } +} + +func (pg *PauseGameState) OnTick(dt int64) GameState { + if pg.unpauseGame { + pg.prevState.Unpause() + return pg.prevState + } + + if pg.returnToMainMenu { + return NewMainMenuState() + } + + return pg +} + +func (pg *PauseGameState) OnDraw(c views.View) { + pg.prevState.OnDraw(c) + + pg.pauseMenuWindow.Draw(c) + + for _, b := range pg.buttons { + b.Draw(c) + } +} diff --git a/game/state/playingState.go b/game/state/playingState.go new file mode 100644 index 0000000..0cd6e3d --- /dev/null +++ b/game/state/playingState.go @@ -0,0 +1,56 @@ +package state + +import ( + "mvvasilev/last_light/game/model" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" +) + +type PlayingState struct { + player *model.Player + + pauseGame bool +} + +func BeginPlayingState() *PlayingState { + s := new(PlayingState) + + s.player = model.CreatePlayer(10, 10, tcell.StyleDefault) + + return s +} + +func (ps *PlayingState) Pause() { + ps.pauseGame = true +} + +func (ps *PlayingState) Unpause() { + ps.pauseGame = false +} + +func (ps *PlayingState) SetPaused(paused bool) { + ps.pauseGame = paused +} + +func (ps *PlayingState) OnInput(e *tcell.EventKey) { + ps.player.Input(e) + + if e.Key() == tcell.KeyEsc { + ps.pauseGame = true + } +} + +func (ps *PlayingState) OnTick(dt int64) GameState { + ps.player.Tick(dt) + + if ps.pauseGame { + return PauseGame(ps) + } + + return ps +} + +func (ps *PlayingState) OnDraw(c views.View) { + ps.player.Draw(c) +} diff --git a/game/state/quitState.go b/game/state/quitState.go new file mode 100644 index 0000000..37e0d46 --- /dev/null +++ b/game/state/quitState.go @@ -0,0 +1,21 @@ +package state + +import ( + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" +) + +type QuitState struct { +} + +func (q *QuitState) OnInput(e *tcell.EventKey) { + +} + +func (q *QuitState) OnTick(dt int64) GameState { + return q +} + +func (q *QuitState) OnDraw(c views.View) { + +} diff --git a/layer.go b/layer.go deleted file mode 100644 index 06ab7d0..0000000 --- a/layer.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/main.go b/main.go index 418213c..2520b47 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "log" + "mvvasilev/last_light/game" "mvvasilev/last_light/render" "os" @@ -18,46 +19,33 @@ func main() { log.Fatalf("%~v", err) } + g := game.CreateGame() + c.HandleInput(func(ev *tcell.EventKey) { - if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC { + if ev.Key() == tcell.KeyCtrlC { c.Stop() os.Exit(0) } + + g.Input(ev) }) defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset) - rect := render.CreateRectangle( - 0, 0, 80, 24, - '┌', '─', '┐', - '│', '#', '│', - '└', '─', '┘', - false, true, 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.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(view) + keepGoing := g.Tick(deltaTime) + + if !keepGoing { + c.Stop() + os.Exit(0) + } + + g.Draw(view) + fpsText.Draw(view) }) diff --git a/render/context.go b/render/context.go index 134f272..dbd243d 100644 --- a/render/context.go +++ b/render/context.go @@ -22,7 +22,7 @@ type Drawable interface { Draw(v views.View) } -type renderContext struct { +type RenderContext struct { screen tcell.Screen view *views.ViewPort defaultStyle tcell.Style @@ -36,7 +36,7 @@ type renderContext struct { inputHandler func(ev *tcell.EventKey) } -func CreateRenderContext() (*renderContext, error) { +func CreateRenderContext() (*RenderContext, error) { s, err := tcell.NewScreen() if err != nil { @@ -77,7 +77,7 @@ func CreateRenderContext() (*renderContext, error) { go s.ChannelEvents(events, quit) - context := new(renderContext) + context := new(RenderContext) context.screen = s context.defaultStyle = defStyle @@ -88,19 +88,19 @@ func CreateRenderContext() (*renderContext, error) { return context, nil } -func (c *renderContext) Stop() { +func (c *RenderContext) Stop() { c.screen.Fini() } -func (c *renderContext) HandleRender(renderHandler func(view views.View, deltaTime int64)) { +func (c *RenderContext) HandleRender(renderHandler func(view views.View, deltaTime int64)) { c.renderHandler = renderHandler } -func (c *renderContext) HandleInput(inputHandler func(ev *tcell.EventKey)) { +func (c *RenderContext) HandleInput(inputHandler func(ev *tcell.EventKey)) { c.inputHandler = inputHandler } -func (c *renderContext) onResize(ev *tcell.EventResize) { +func (c *RenderContext) onResize(ev *tcell.EventResize) { width, height := ev.Size() c.screen.Clear() @@ -115,7 +115,7 @@ func (c *renderContext) onResize(ev *tcell.EventResize) { c.screen.Sync() } -func (c *renderContext) BeginRendering() { +func (c *RenderContext) BeginRendering() { c.lastRenderTime = time.Now() for { diff --git a/render/grid.go b/render/grid.go index c452a01..b21097c 100644 --- a/render/grid.go +++ b/render/grid.go @@ -8,7 +8,7 @@ import ( "github.com/google/uuid" ) -type grid struct { +type Grid struct { id uuid.UUID internalCellSize util.Size @@ -45,7 +45,7 @@ func CreateSimpleGrid( numCellsHorizontal, numCellsVertical uint16, borderRune, fillRune rune, style tcell.Style, -) grid { +) Grid { return CreateGrid( x, y, cellWidth, cellHeight, numCellsHorizontal, numCellsVertical, borderRune, borderRune, borderRune, borderRune, @@ -72,8 +72,8 @@ func CreateGrid( horizontalRightTJunction, internalHorizontalBorder, crossJunction, horizontalLeftTJunction, swCorner, southBorder, verticalUpwardsTJunction, seCorner rune, style tcell.Style, -) grid { - return grid{ +) Grid { + return Grid{ id: uuid.New(), internalCellSize: util.SizeOf(cellWidth, cellHeight), numCellsHorizontal: numCellsHorizontal, @@ -100,7 +100,7 @@ func CreateGrid( } } -func (g grid) UniqueId() uuid.UUID { +func (g Grid) UniqueId() uuid.UUID { return g.id } @@ -117,9 +117,11 @@ func (g grid) UniqueId() uuid.UUID { // # # # # // # # # # // 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) +func (g Grid) drawBorders(v views.View) { + iCellSizeWidth := g.internalCellSize.Width() + iCellSizeHeight := g.internalCellSize.Height() + width := 1 + (iCellSizeWidth * int(g.numCellsHorizontal)) + (int(g.numCellsHorizontal)) + height := 1 + (iCellSizeHeight * int(g.numCellsVertical)) + (int(g.numCellsVertical)) x := g.position.X() y := g.position.Y() @@ -128,22 +130,54 @@ func (g grid) drawBorders(v views.View) { 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 w := 1; w < width-1; w++ { + + for iw := 1; iw < int(g.numCellsVertical); iw++ { + if w%(iCellSizeWidth+1) == 0 { + v.SetContent(x+w, y+(iw*iCellSizeHeight+iw), g.crossJunction, nil, g.style) + continue + } + + v.SetContent(x+w, y+(iw*iCellSizeHeight+iw), g.internalHorizontalBorder, nil, g.style) + } + + if w%(iCellSizeWidth+1) == 0 { + v.SetContent(x+w, y, g.verticalDownwardsTJunction, nil, g.style) + v.SetContent(x+w, y+height-1, g.verticalUpwardsTJunction, nil, g.style) + continue + } + + v.SetContent(x+w, y, g.northBorder, nil, g.style) + v.SetContent(x+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) + for h := 1; h < height-1; h++ { + + for ih := 1; ih < int(g.numCellsHorizontal); ih++ { + if h%(iCellSizeHeight+1) == 0 { + v.SetContent(x+(ih*iCellSizeHeight+ih), y+h, g.crossJunction, nil, g.style) + continue + } + + v.SetContent(x+(ih*iCellSizeHeight+ih), y+h, g.internalVerticalBorder, nil, g.style) + } + + if h%(iCellSizeHeight+1) == 0 { + v.SetContent(x, y+h, g.horizontalRightTJunction, nil, g.style) + v.SetContent(x+width-1, y+h, g.horizontalLeftTJunction, nil, g.style) + continue + } + + v.SetContent(x, y+h, g.westBorder, nil, g.style) + v.SetContent(x+width-1, y+h, g.eastBorder, nil, g.style) } } -func (g grid) drawFill(v views.View) { +func (g Grid) drawFill(v views.View) { } -func (g grid) Draw(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 524b541..98a069f 100644 --- a/render/layers.go +++ b/render/layers.go @@ -37,13 +37,13 @@ func (l *layer) draw(s views.View) { } } -type unorderedDrawContainer struct { +type UnorderedDrawContainer struct { id uuid.UUID contents []Drawable } -func CreateUnorderedDrawContainer(contents []Drawable) unorderedDrawContainer { - return unorderedDrawContainer{ +func CreateUnorderedDrawContainer(contents []Drawable) UnorderedDrawContainer { + return UnorderedDrawContainer{ id: uuid.New(), contents: contents, } diff --git a/render/raw.go b/render/raw.go new file mode 100644 index 0000000..63c810b --- /dev/null +++ b/render/raw.go @@ -0,0 +1,46 @@ +package render + +import ( + "mvvasilev/last_light/util" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" + "github.com/google/uuid" +) + +type Raw struct { + id uuid.UUID + buffer [][]rune + position util.Position + style tcell.Style +} + +func CreateRawDrawable(x, y uint16, style tcell.Style, buffer ...string) *Raw { + r := new(Raw) + + r.position = util.PositionAt(x, y) + r.buffer = make([][]rune, 0) + + for _, row := range buffer { + r.buffer = append(r.buffer, []rune(row)) + } + + r.style = style + + return r +} + +func (r *Raw) UniqueId() uuid.UUID { + return r.id +} + +func (r *Raw) Draw(v views.View) { + x := r.position.X() + y := r.position.Y() + + for h, row := range r.buffer { + for i, ru := range row { + v.SetContent(x+i, y+h, ru, nil, r.style) + } + } +} diff --git a/render/rectangle.go b/render/rectangle.go index 6663dc8..b550be6 100644 --- a/render/rectangle.go +++ b/render/rectangle.go @@ -8,7 +8,7 @@ import ( "github.com/google/uuid" ) -type rectangle struct { +type Rectangle struct { id uuid.UUID size util.Size @@ -31,7 +31,7 @@ type rectangle struct { fillRune rune } -func CreateBorderlessRectangle(x, y uint16, width, height uint16, fillRune rune, style tcell.Style) rectangle { +func CreateBorderlessRectangle(x, y uint16, width, height uint16, fillRune rune, style tcell.Style) Rectangle { return CreateRectangle( x, y, width, height, 0, 0, 0, @@ -41,7 +41,7 @@ func CreateBorderlessRectangle(x, y uint16, width, height uint16, fillRune rune, ) } -func CreateSimpleEmptyRectangle(x, y uint16, width, height uint16, borderRune rune, style tcell.Style) rectangle { +func CreateSimpleEmptyRectangle(x, y uint16, width, height uint16, borderRune rune, style tcell.Style) Rectangle { return CreateRectangle( x, y, width, height, borderRune, borderRune, borderRune, @@ -51,7 +51,7 @@ func CreateSimpleEmptyRectangle(x, y uint16, width, height uint16, borderRune ru ) } -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( x, y, width, height, borderRune, borderRune, borderRune, @@ -61,6 +61,25 @@ func CreateSimpleRectangle(x uint16, y uint16, width uint16, height uint16, bord ) } +func CreateRectangleV2( + x, y uint16, width, height uint16, + upper, middle, lower string, + isBorderless, isFilled bool, + style tcell.Style, +) Rectangle { + upperRunes := []rune(upper) + middleRunes := []rune(middle) + lowerRunes := []rune(lower) + + return CreateRectangle( + x, y, width, height, + upperRunes[0], upperRunes[1], upperRunes[2], + middleRunes[0], middleRunes[1], middleRunes[2], + lowerRunes[0], lowerRunes[1], lowerRunes[2], + isBorderless, isFilled, style, + ) +} + // CreateRectangle( // // x, y, width, height, @@ -81,8 +100,8 @@ func CreateRectangle( swCorner, southBorder, seCorner rune, isBorderless, isFilled bool, style tcell.Style, -) rectangle { - return rectangle{ +) Rectangle { + return Rectangle{ id: uuid.New(), size: util.SizeOf(width, height), position: util.PositionAt(x, y), @@ -101,11 +120,15 @@ func CreateRectangle( } } -func (rect rectangle) UniqueId() uuid.UUID { +func (rect Rectangle) UniqueId() uuid.UUID { return rect.id } -func (rect rectangle) drawBorders(v views.View) { +func (rect Rectangle) Position() util.Position { + return rect.position +} + +func (rect Rectangle) drawBorders(v views.View) { width := rect.size.Width() height := rect.size.Height() x := rect.position.X() @@ -116,26 +139,31 @@ func (rect rectangle) drawBorders(v views.View) { v.SetContent(x, y+height-1, rect.swCorner, nil, rect.style) v.SetContent(x+width-1, y+height-1, rect.seCorner, nil, rect.style) - 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) + for w := 1; w < width-1; w++ { + v.SetContent(x+w, y, rect.northBorder, nil, rect.style) + v.SetContent(x+w, y+height-1, rect.southBorder, nil, rect.style) } - 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) + for h := 1; h < height-1; h++ { + v.SetContent(x, y+h, rect.westBorder, nil, rect.style) + v.SetContent(x+width-1, y+h, rect.eastBorder, 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) drawFill(v views.View) { + width := rect.size.Width() + height := rect.size.Height() + x := rect.position.X() + y := rect.position.Y() + + for w := 1; w < width-1; w++ { + for h := 1; h < height-1; h++ { + v.SetContent(x+w, y+h, rect.fillRune, nil, rect.style) } } } -func (rect rectangle) Draw(v views.View) { +func (rect Rectangle) Draw(v views.View) { if !rect.isBorderless { rect.drawBorders(v) } diff --git a/render/text.go b/render/text.go index d588406..06552f2 100644 --- a/render/text.go +++ b/render/text.go @@ -10,7 +10,7 @@ import ( "github.com/google/uuid" ) -type text struct { +type Text struct { id uuid.UUID content []string position util.Position @@ -23,21 +23,43 @@ func CreateText( width, height uint16, content string, style tcell.Style, -) text { - return text{ - id: uuid.New(), - content: strings.Split(content, " "), - style: style, - size: util.SizeOf(width, height), - position: util.PositionAt(x, y), - } +) *Text { + text := new(Text) + + text.id = uuid.New() + text.content = strings.Split(content, " ") + text.style = style + text.size = util.SizeOf(width, height) + text.position = util.PositionAt(x, y) + + return text } -func (t text) UniqueId() uuid.UUID { +func (t *Text) UniqueId() uuid.UUID { return t.id } -func (t text) Draw(s views.View) { +func (t *Text) Position() util.Position { + return t.position +} + +func (t *Text) Content() string { + return strings.Join(t.content, " ") +} + +func (t *Text) Size() util.Size { + return t.size +} + +func (t *Text) SetStyle(style tcell.Style) { + t.style = style +} + +func (t *Text) Style() tcell.Style { + return t.style +} + +func (t *Text) Draw(s views.View) { width := t.size.Width() height := t.size.Height() x := t.position.X() diff --git a/terminal.go b/terminal.go deleted file mode 100644 index 06ab7d0..0000000 --- a/terminal.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/ui/borderButton.go b/ui/borderButton.go new file mode 100644 index 0000000..bb45d7f --- /dev/null +++ b/ui/borderButton.go @@ -0,0 +1,62 @@ +package ui + +import ( + "mvvasilev/last_light/render" + "mvvasilev/last_light/util" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" + "github.com/google/uuid" +) + +type UIBorderedButton struct { + id uuid.UUID + + text render.Text + border render.Rectangle + + isSelected bool + + unselectedStyle tcell.Style + selectedStyle tcell.Style +} + +func (b *UIBorderedButton) IsSelected() bool { + return b.isSelected +} + +func (b *UIBorderedButton) Select() { + b.isSelected = true +} + +func (b *UIBorderedButton) Deselect() { + b.isSelected = false +} + +func (b *UIBorderedButton) SetSelected(selected bool) { + b.isSelected = selected +} + +func (b *UIBorderedButton) UniqueId() uuid.UUID { + return b.id +} + +func (b *UIBorderedButton) MoveTo(x uint16, y uint16) { + panic("not implemented") // TODO: Implement +} + +func (b *UIBorderedButton) Position() util.Position { + panic("not implemented") // TODO: Implement +} + +func (b *UIBorderedButton) Size() util.Size { + panic("not implemented") // TODO: Implement +} + +func (b *UIBorderedButton) Draw(v views.View) { + panic("not implemented") // TODO: Implement +} + +func (b *UIBorderedButton) Input(e *tcell.EventKey) { + panic("not implemented") // TODO: Implement +} diff --git a/ui/container.go b/ui/container.go new file mode 100644 index 0000000..4de591a --- /dev/null +++ b/ui/container.go @@ -0,0 +1,82 @@ +package ui + +import ( + "mvvasilev/last_light/util" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" + "github.com/google/uuid" +) + +type UIContainerLayout int + +const ( + // These change the provided ui positions + UpperLeft UIContainerLayout = iota + MiddleLeft + LowerLeft + UpperRight + MiddleRight + LowerRight + UpperCenter + MiddleCenter + LowerCenter + // This uses the positions as provided in the ui elements + Manual +) + +type UIContainer struct { + id uuid.UUID + layout UIContainerLayout + position util.Position + size util.Size + elements []UIElement +} + +func CreateUIContainer(x, y uint16, width, height uint16, layout UIContainerLayout) *UIContainer { + container := new(UIContainer) + + container.id = uuid.New() + container.layout = layout + container.position = util.PositionAt(x, y) + container.size = util.SizeOf(width, height) + container.elements = make([]UIElement, 0) + + return container +} + +func (uic *UIContainer) Push(element UIElement) { + uic.elements = append(uic.elements, element) +} + +func (uic *UIContainer) Clear() { + uic.elements = make([]UIElement, 0) +} + +func (uic *UIContainer) UniqueId() uuid.UUID { + return uic.id +} + +func (uic *UIContainer) MoveTo(x, y uint16) { + uic.position = util.PositionAt(x, y) +} + +func (uic *UIContainer) Position() util.Position { + return uic.position +} + +func (uic *UIContainer) Size() util.Size { + return uic.size +} + +func (uic *UIContainer) Draw(v views.View) { + for _, e := range uic.elements { + e.Draw(v) + } +} + +func (uic *UIContainer) Input(ev *tcell.EventKey) { + for _, e := range uic.elements { + e.Input(ev) + } +} diff --git a/ui/label.go b/ui/label.go new file mode 100644 index 0000000..0f828c7 --- /dev/null +++ b/ui/label.go @@ -0,0 +1,56 @@ +package ui + +import ( + "mvvasilev/last_light/render" + "mvvasilev/last_light/util" + "unicode/utf8" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" + "github.com/google/uuid" +) + +type UILabel struct { + id uuid.UUID + text *render.Text +} + +func CreateUILabel(x, y uint16, width, height uint16, content string, style tcell.Style) *UILabel { + label := new(UILabel) + + label.id = uuid.New() + label.text = render.CreateText(x, y, width, height, content, style) + + return label +} + +func CreateSingleLineUILabel(x, y uint16, content string, style tcell.Style) *UILabel { + label := new(UILabel) + + label.id = uuid.New() + label.text = render.CreateText(x, y, uint16(utf8.RuneCountInString(content)), 1, content, style) + + return label +} + +func (t *UILabel) UniqueId() uuid.UUID { + return t.id +} + +func (t *UILabel) MoveTo(x uint16, y uint16) { + t.text = render.CreateText(x, y, uint16(t.text.Size().Width()), uint16(t.Size().Height()), t.text.Content(), t.text.Style()) +} + +func (t *UILabel) Position() util.Position { + return t.text.Position() +} + +func (t *UILabel) Size() util.Size { + return t.text.Size() +} + +func (t *UILabel) Draw(v views.View) { + t.text.Draw(v) +} + +func (t *UILabel) Input(e *tcell.EventKey) {} diff --git a/ui/simpleButton.go b/ui/simpleButton.go new file mode 100644 index 0000000..b84af8b --- /dev/null +++ b/ui/simpleButton.go @@ -0,0 +1,102 @@ +package ui + +import ( + "mvvasilev/last_light/render" + "mvvasilev/last_light/util" + "strings" + "unicode/utf8" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" + "github.com/google/uuid" +) + +type UISimpleButton struct { + id uuid.UUID + isHighlighted bool + text *render.Text + selectHandler func() + unhighlightedStyle tcell.Style + highlightedStyle tcell.Style +} + +func CreateSimpleButton(x, y uint16, text string, unhighlightedStyle, highlightedStyle tcell.Style, onSelect func()) *UISimpleButton { + sb := new(UISimpleButton) + + sb.id = uuid.New() + sb.text = render.CreateText(x, y, uint16(utf8.RuneCountInString(text)), 1, text, unhighlightedStyle) + sb.isHighlighted = false + sb.selectHandler = onSelect + sb.highlightedStyle = highlightedStyle + sb.unhighlightedStyle = unhighlightedStyle + + return sb +} + +func (sb *UISimpleButton) Select() { + sb.selectHandler() +} + +func (sb *UISimpleButton) OnSelect(f func()) { + sb.selectHandler = f +} + +func (sb *UISimpleButton) IsHighlighted() bool { + return sb.isHighlighted +} + +func (sb *UISimpleButton) Highlight() { + sb.isHighlighted = true + + newContent := "[ " + sb.text.Content() + " ]" + + sb.text = render.CreateText( + uint16(sb.Position().X()-2), uint16(sb.Position().Y()), + uint16(utf8.RuneCountInString(newContent)), 1, + newContent, + sb.highlightedStyle, + ) +} + +func (sb *UISimpleButton) Unhighlight() { + sb.isHighlighted = false + + content := strings.Trim(sb.text.Content(), " ]") + content = strings.Trim(content, "[ ") + contentLen := utf8.RuneCountInString(content) + + sb.text = render.CreateText( + uint16(sb.Position().X()+2), uint16(sb.Position().Y()), + uint16(contentLen), 1, + content, + sb.unhighlightedStyle, + ) +} + +func (sb *UISimpleButton) SetHighlighted(highlighted bool) { + sb.isHighlighted = highlighted +} + +func (sb *UISimpleButton) UniqueId() uuid.UUID { + return sb.id +} + +func (sb *UISimpleButton) MoveTo(x uint16, y uint16) { + sb.text = render.CreateText(x, y, uint16(utf8.RuneCountInString(sb.text.Content())), 1, sb.text.Content(), sb.highlightedStyle) +} + +func (sb *UISimpleButton) Position() util.Position { + return sb.text.Position() +} + +func (sb *UISimpleButton) Size() util.Size { + return sb.text.Size() +} + +func (sb *UISimpleButton) Draw(v views.View) { + sb.text.Draw(v) +} + +func (sb *UISimpleButton) Input(e *tcell.EventKey) { + +} diff --git a/ui/ui.go b/ui/ui.go index 48ecf65..0b5f39b 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -3,10 +3,30 @@ package ui import ( "mvvasilev/last_light/util" + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" "github.com/google/uuid" ) type UIElement interface { UniqueId() uuid.UUID + MoveTo(x, y uint16) Position() util.Position + Size() util.Size + Draw(v views.View) + Input(e *tcell.EventKey) +} + +type UIHighlightableElement interface { + IsHighlighted() bool + Highlight() + Unhighlight() + SetHighlighted(highlighted bool) + UIElement +} + +type UISelectableElement interface { + Select() + OnSelect(func()) + UIHighlightableElement } diff --git a/ui/window.go b/ui/window.go new file mode 100644 index 0000000..1f36fab --- /dev/null +++ b/ui/window.go @@ -0,0 +1,62 @@ +package ui + +import ( + "mvvasilev/last_light/render" + "mvvasilev/last_light/util" + "unicode/utf8" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" + "github.com/google/uuid" +) + +type UIWindow struct { + id uuid.UUID + + title *render.Text + box render.Rectangle +} + +func CreateWindow(x, y, width, height uint16, title string, style tcell.Style) *UIWindow { + w := new(UIWindow) + + titleLen := utf8.RuneCountInString(title) + + titlePos := (width / 2) - uint16(titleLen/2) + + w.title = render.CreateText(x+titlePos, y, uint16(titleLen), 1, title, style) + + w.box = render.CreateRectangle( + x, y, width, height, + '┌', '─', '┐', + '│', ' ', '│', + '└', '─', '┘', + false, true, style, + ) + + return w +} + +func (w *UIWindow) UniqueId() uuid.UUID { + return w.id +} + +func (w *UIWindow) MoveTo(x uint16, y uint16) { + +} + +func (w *UIWindow) Position() util.Position { + return w.box.Position() +} + +func (w *UIWindow) Size() util.Size { + return util.SizeOf(0, 0) +} + +func (w *UIWindow) Draw(v views.View) { + w.box.Draw(v) + w.title.Draw(v) +} + +func (w *UIWindow) Input(e *tcell.EventKey) { +} diff --git a/util/util.go b/util/util.go index da1ac52..23aa9e0 100644 --- a/util/util.go +++ b/util/util.go @@ -17,6 +17,14 @@ func (p Position) Y() int { return p.y } +func (p Position) XY() (int, int) { + return p.x, p.y +} + +func (p Position) XYUint16() (uint16, uint16) { + return uint16(p.x), uint16(p.y) +} + type Size struct { width int height int @@ -33,3 +41,23 @@ func (s Size) Width() int { func (s Size) Height() int { return s.height } + +func (s Size) WHUint16() (uint16, uint16) { + return uint16(s.width), uint16(s.height) +} + +func LimitIncrement(i int, limit int) int { + if (i + 1) > limit { + return i + } + + return i + 1 +} + +func LimitDecrement(i int, limit int) int { + if (i - 1) < limit { + return i + } + + return i - 1 +}