diff --git a/assets/sprites/button-9slice1.png b/assets/sprites/button-9slice1.png index c0731c2..6a5804d 100644 Binary files a/assets/sprites/button-9slice1.png and b/assets/sprites/button-9slice1.png differ diff --git a/assets/sprites/button-9slice2.png b/assets/sprites/button-9slice2.png index f2454f9..6a7a525 100644 Binary files a/assets/sprites/button-9slice2.png and b/assets/sprites/button-9slice2.png differ diff --git a/assets/sprites/button-9slice3.png b/assets/sprites/button-9slice3.png index ab324da..f633e3a 100644 Binary files a/assets/sprites/button-9slice3.png and b/assets/sprites/button-9slice3.png differ diff --git a/assets/sprites/checkbox-9slice3.png b/assets/sprites/checkbox-9slice3.png deleted file mode 100644 index ab324da..0000000 Binary files a/assets/sprites/checkbox-9slice3.png and /dev/null differ diff --git a/assets/src/button-9slice.ase b/assets/src/button-9slice.ase index 8f00914..d11bb86 100644 Binary files a/assets/src/button-9slice.ase and b/assets/src/button-9slice.ase differ diff --git a/assets/src/button-9slice1.ase b/assets/src/button-9slice1.ase deleted file mode 100644 index fbb734f..0000000 Binary files a/assets/src/button-9slice1.ase and /dev/null differ diff --git a/config.go b/config.go index 584c626..4a27bbe 100644 --- a/config.go +++ b/config.go @@ -15,18 +15,18 @@ import ( // all the settings comming from commandline, but maybe tweaked later from the UI type Config struct { - Width, Height, Cellsize, Density int // measurements - ScreenWidth, ScreenHeight int - TPG int // ticks per generation/game speed, 1==max - Debug, Empty, Invert, Paused bool // game modi - ShowEvolution, NoGrid, RunOneStep bool // flags - Rule *Rule // which rule to use, default: B3/S23 - RLE *rle.RLE // loaded GOL pattern from RLE file - Statefile string // load game state from it if non-nil - StateGrid *Grid // a grid from a statefile - Wrap bool // wether wraparound mode is in place or not - ShowVersion bool - UseShader bool // to use a shader to render alife cells + Width, Height, Cellsize, Density int // measurements + ScreenWidth, ScreenHeight int + TPG int // ticks per generation/game speed, 1==max + Debug, Empty, Invert, Paused, Markmode bool // game modi + ShowEvolution, NoGrid, RunOneStep bool // flags + Rule *Rule // which rule to use, default: B3/S23 + RLE *rle.RLE // loaded GOL pattern from RLE file + Statefile string // load game state from it if non-nil + StateGrid *Grid // a grid from a statefile + Wrap bool // wether wraparound mode is in place or not + ShowVersion bool + UseShader bool // to use a shader to render alife cells // for internal profiling ProfileFile string @@ -205,3 +205,7 @@ func ParseCommandline() (*Config, error) { return &config, nil } + +func (config *Config) TogglePaused() { + config.Paused = !config.Paused +} diff --git a/game.go b/game.go index 4bd7842..c4075aa 100644 --- a/game.go +++ b/game.go @@ -23,12 +23,13 @@ func NewGame(config *Config, startscene SceneName) *Game { // setup scene[s] game.CurrentScene = startscene game.Scenes[Play] = NewPlayScene(game, config) + game.Scenes[Menu] = NewMenuScene(game, config) // setup environment ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight) ebiten.SetWindowTitle("golsky - conway's game of life") ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) - + ebiten.SetScreenClearedEveryFrame(false) return game } @@ -61,12 +62,5 @@ func (game *Game) Update() error { func (game *Game) Draw(screen *ebiten.Image) { scene := game.GetCurrentScene() - - if scene.Clearscreen() { - ebiten.SetScreenClearedEveryFrame(true) - } else { - ebiten.SetScreenClearedEveryFrame(false) - } - scene.Draw(screen) } diff --git a/go.mod b/go.mod index 2837d0b..aad108f 100644 --- a/go.mod +++ b/go.mod @@ -13,9 +13,11 @@ require ( github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect github.com/ebitengine/hideconsole v1.0.0 // indirect github.com/ebitengine/purego v0.7.0 // indirect + github.com/ebitenui/ebitenui v0.5.6 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/jezek/xgb v1.1.1 // indirect github.com/tinne26/etxt v0.0.8 // indirect + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect diff --git a/go.sum b/go.sum index 094afd4..a59462a 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= github.com/ebitengine/purego v0.7.0 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc= github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= +github.com/ebitenui/ebitenui v0.5.6 h1:qyJRU5j+lQo1lamxB48IBwMxMfz1xNb5iWUayCtA0Wk= +github.com/ebitenui/ebitenui v0.5.6/go.mod h1:I0rVbTOUi7gWKTPet2gzbvhOdkHp5pJXMM6c6b3dRoE= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/hajimehoshi/ebiten/v2 v2.7.4 h1:X+heODRQ3Ie9F9QFjm24gEZqQd5FSfR9XuT2XfHwgf8= @@ -18,6 +20,8 @@ github.com/tinne26/etxt v0.0.8 h1:rjb58jkMkapRGLmhBMWnT76E/nMTXC5P1Q956BRZkoc= github.com/tinne26/etxt v0.0.8/go.mod h1:QM/hlNkstsKC39elTFNKAR34xsMb9QoVosf+g9wlYxM= github.com/tinne26/etxt v0.0.9-alpha.6.0.20240409152929-91bfc562becc h1:+USGSXbkrRAy6bz3Qm4GUczhqeXe7XlRfkRexCSFxkw= github.com/tinne26/etxt v0.0.9-alpha.6.0.20240409152929-91bfc562becc/go.mod h1:Icbd4bDjrXag1oYIhB51CrkMYqRb7YMv0AsrOSfNKfU= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw= golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= diff --git a/scene-menu.go b/scene-menu.go new file mode 100644 index 0000000..a4c45ab --- /dev/null +++ b/scene-menu.go @@ -0,0 +1,94 @@ +package main + +import ( + "image/color" + + "github.com/ebitenui/ebitenui" + "github.com/ebitenui/ebitenui/widget" + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" +) + +type SceneMenu struct { + Game *Game + Config *Config + Next SceneName + Whoami SceneName + Ui *ebitenui.UI + FontColor color.RGBA +} + +func NewMenuScene(game *Game, config *Config) Scene { + scene := &SceneMenu{ + Whoami: Menu, + Game: game, + Next: Menu, + Config: config, + FontColor: color.RGBA{255, 30, 30, 0xff}, + } + + scene.Init() + + return scene +} + +func (scene *SceneMenu) GetNext() SceneName { + return scene.Next +} + +func (scene *SceneMenu) ResetNext() { + scene.Next = scene.Whoami +} + +func (scene *SceneMenu) SetNext(next SceneName) { + scene.Next = next +} + +func (scene *SceneMenu) Clearscreen() bool { + return false +} + +func (scene *SceneMenu) Update() error { + scene.Ui.Update() + + if inpututil.IsKeyJustPressed(ebiten.KeyEscape) || inpututil.IsKeyJustPressed(ebiten.KeyQ) { + scene.SetNext(Play) + } + + return nil + +} + +func (scene *SceneMenu) Draw(screen *ebiten.Image) { + scene.Ui.Draw(screen) +} + +func (scene *SceneMenu) Init() { + rowContainer := NewRowContainer() + + pause := NewCheckbox("Pause", *FontRenderer.FontSmall, + func(args *widget.CheckboxChangedEventArgs) { + scene.Config.TogglePaused() + }) + + copy := NewMenuButton("Save Copy as RLE", *FontRenderer.FontSmall, + func(args *widget.ButtonClickedEventArgs) { + scene.Config.Markmode = true + scene.Config.Paused = true + scene.SetNext(Play) + }) + + label := widget.NewText( + widget.TextOpts.Text("Menu", *FontRenderer.FontNormal, scene.FontColor), + widget.TextOpts.Position(widget.TextPositionCenter, widget.TextPositionCenter), + ) + + rowContainer.AddChild(label) + rowContainer.AddChild(pause) + rowContainer.AddChild(copy) + + scene.Ui = &ebitenui.UI{ + Container: rowContainer.Container(), + } + +} diff --git a/scene-play.go b/scene-play.go index c3d2c99..e654081 100644 --- a/scene-play.go +++ b/scene-play.go @@ -37,11 +37,10 @@ type ScenePlay struct { WheelTurned bool // when user turns wheel multiple times, zoom faster Dragging bool // middle mouse is pressed, move canvas LastCursorPos []int // used to check if the user is dragging - Markmode bool // enabled with 'c' MarkTaken bool // true when mouse1 pressed MarkDone bool // true when mouse1 released, copy cells between Mark+Point Mark, Point image.Point // area to marks+save - Paused, RunOneStep bool // mutable flags from config + RunOneStep bool // mutable flags from config TPG int } @@ -51,7 +50,6 @@ func NewPlayScene(game *Game, config *Config) Scene { Game: game, Next: Play, Config: config, - Paused: config.Paused, TPG: config.TPG, RunOneStep: config.RunOneStep, } @@ -147,9 +145,9 @@ func (scene *ScenePlay) UpdateCells() { } func (scene *ScenePlay) Reset() { - scene.Paused = true + scene.Config.Paused = true scene.InitGrid(nil) - scene.Paused = false + scene.Config.Paused = false } // check user input @@ -160,26 +158,30 @@ func (scene *ScenePlay) CheckInput() { if inpututil.IsKeyJustPressed(ebiten.KeyC) { fmt.Println("mark mode on") - scene.Markmode = true - scene.Paused = true + scene.Config.Markmode = true + scene.Config.Paused = true } - if scene.Markmode { + if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { + scene.SetNext(Menu) + } + + if scene.Config.Markmode { return } if inpututil.IsKeyJustPressed(ebiten.KeySpace) || inpututil.IsKeyJustPressed(ebiten.KeyEnter) { - scene.Paused = !scene.Paused + scene.Config.TogglePaused() } if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { scene.ToggleCellOnCursorPos(Alive) - scene.Paused = true // drawing while running makes no sense + scene.Config.Paused = true // drawing while running makes no sense } if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) { scene.ToggleCellOnCursorPos(Dead) - scene.Paused = true // drawing while running makes no sense + scene.Config.Paused = true // drawing while running makes no sense } if inpututil.IsKeyJustPressed(ebiten.KeyPageDown) { @@ -206,7 +208,7 @@ func (scene *ScenePlay) CheckInput() { scene.Config.Debug = !scene.Config.Debug } - if scene.Paused { + if scene.Config.Paused { if inpututil.IsKeyJustPressed(ebiten.KeyN) { scene.Config.RunOneStep = true } @@ -216,7 +218,7 @@ func (scene *ScenePlay) CheckInput() { // Check dragging input. move the canvas with the mouse while pressing // the middle mouse button, zoom in and out using the wheel. func (scene *ScenePlay) CheckDraggingInput() { - if scene.Markmode { + if scene.Config.Markmode { return } @@ -280,7 +282,7 @@ func (scene *ScenePlay) GetWorldCursorPos() image.Point { } func (scene *ScenePlay) CheckMarkInput() { - if !scene.Markmode { + if !scene.Config.Markmode { return } @@ -294,7 +296,7 @@ func (scene *ScenePlay) CheckMarkInput() { scene.Point = scene.GetWorldCursorPos() //fmt.Printf("Mark: %v, Point: %v\n", scene.Mark, scene.Point) } else if inpututil.IsMouseButtonJustReleased(ebiten.MouseButton0) { - scene.Markmode = false + scene.Config.Markmode = false scene.MarkTaken = false scene.MarkDone = true @@ -368,7 +370,7 @@ func (scene *ScenePlay) Update() error { scene.CheckDraggingInput() scene.CheckMarkInput() - if !scene.Paused || scene.RunOneStep { + if !scene.Config.Paused || scene.RunOneStep { scene.UpdateCells() } @@ -446,7 +448,7 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) { } func (scene *ScenePlay) DrawMark(screen *ebiten.Image) { - if scene.Markmode && scene.MarkTaken { + if scene.Config.Markmode && scene.MarkTaken { x := float32(scene.Mark.X * scene.Config.Cellsize) y := float32(scene.Mark.Y * scene.Config.Cellsize) w := float32((scene.Point.X - scene.Mark.X) * scene.Config.Cellsize) @@ -464,7 +466,7 @@ func (scene *ScenePlay) DrawMark(screen *ebiten.Image) { func (scene *ScenePlay) DrawDebug(screen *ebiten.Image) { if scene.Config.Debug { paused := "" - if scene.Paused { + if scene.Config.Paused { paused = "-- paused --" } diff --git a/widgets.go b/widgets.go new file mode 100644 index 0000000..8e565d8 --- /dev/null +++ b/widgets.go @@ -0,0 +1,137 @@ +package main + +import ( + "image/color" + + "github.com/ebitenui/ebitenui/image" + "github.com/ebitenui/ebitenui/widget" + "golang.org/x/image/font" +) + +func NewMenuButton( + text string, + face font.Face, + action func(args *widget.ButtonClickedEventArgs)) *widget.Button { + + buttonImage, _ := LoadButtonImage() + + return widget.NewButton( + widget.ButtonOpts.WidgetOpts( + widget.WidgetOpts.LayoutData(widget.RowLayoutData{ + Position: widget.RowLayoutPositionCenter, + Stretch: true, + MaxWidth: 200, + MaxHeight: 100, + }), + ), + + widget.ButtonOpts.Image(buttonImage), + + widget.ButtonOpts.Text(text, face, &widget.ButtonTextColor{ + Idle: color.NRGBA{0xdf, 0xf4, 0xff, 0xff}, + }), + + widget.ButtonOpts.TextPadding(widget.Insets{ + Left: 5, + Right: 5, + Top: 5, + Bottom: 5, + }), + + widget.ButtonOpts.ClickedHandler(action), + ) +} + +func NewCheckbox( + text string, + face font.Face, + action func(args *widget.CheckboxChangedEventArgs)) *widget.LabeledCheckbox { + + checkboxImage, _ := LoadCheckboxImage() + buttonImage, _ := LoadButtonImage() + + return widget.NewLabeledCheckbox( + widget.LabeledCheckboxOpts.CheckboxOpts( + widget.CheckboxOpts.ButtonOpts(widget.ButtonOpts.Image(buttonImage)), + widget.CheckboxOpts.Image(checkboxImage), + widget.CheckboxOpts.StateChangedHandler(action), + ), + widget.LabeledCheckboxOpts.LabelOpts( + widget.LabelOpts.Text(text, face, + &widget.LabelColor{ + Idle: color.NRGBA{0xdf, 0xf4, 0xff, 0xff}, + }), + ), + ) +} + +type RowContainer struct { + Root *widget.Container + Row *widget.Container +} + +func (container *RowContainer) AddChild(child widget.PreferredSizeLocateableWidget) { + container.Row.AddChild(child) +} + +func (container *RowContainer) Container() *widget.Container { + return container.Root +} + +// set arg to false if no background needed +func NewRowContainer() *RowContainer { + uiContainer := widget.NewContainer( + widget.ContainerOpts.Layout(widget.NewAnchorLayout()), + ) + + rowContainer := widget.NewContainer( + widget.ContainerOpts.WidgetOpts( + widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{ + HorizontalPosition: widget.AnchorLayoutPositionCenter, + VerticalPosition: widget.AnchorLayoutPositionCenter, + }), + ), + widget.ContainerOpts.Layout(widget.NewRowLayout( + widget.RowLayoutOpts.Direction(widget.DirectionVertical), + widget.RowLayoutOpts.Padding(widget.NewInsetsSimple(20)), + widget.RowLayoutOpts.Spacing(0), + )), + ) + + uiContainer.AddChild(rowContainer) + + return &RowContainer{ + Root: uiContainer, + Row: rowContainer, + } +} + +func LoadButtonImage() (*widget.ButtonImage, error) { + idle := image.NewNineSlice(Assets["button-9slice2"], [3]int{3, 3, 3}, [3]int{3, 3, 3}) + hover := image.NewNineSlice(Assets["button-9slice3"], [3]int{3, 3, 3}, [3]int{3, 3, 3}) + pressed := image.NewNineSlice(Assets["button-9slice1"], [3]int{3, 3, 3}, [3]int{3, 3, 3}) + + return &widget.ButtonImage{ + Idle: idle, + Hover: hover, + Pressed: pressed, + }, nil +} + +func LoadCheckboxImage() (*widget.CheckboxGraphicImage, error) { + unchecked := &widget.ButtonImageImage{ + Idle: Assets["checkbox-9slice2"], + Disabled: Assets["checkbox-9slice2"], + } + + checked := &widget.ButtonImageImage{ + Idle: Assets["checkbox-9slice1"], + Disabled: Assets["checkbox-9slice1"], + } + + return &widget.CheckboxGraphicImage{ + Checked: checked, + Unchecked: unchecked, + Greyed: unchecked, + }, nil +}