package menu import ( "image/color" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/text/v2" "github.com/hajimehoshi/ebiten/v2/vector" "golang.org/x/image/font/basicfont" ) var ( basicFace = text.NewGoXFace(basicfont.Face7x13) basicFaceAscent = basicFace.Metrics().HAscent overlayImage *ebiten.Image overlaySize struct { width int height int } ) func getOverlayImage(width, height int) *ebiten.Image { if width <= 0 || height <= 0 { return nil } if overlayImage == nil || overlaySize.width != width || overlaySize.height != height { overlayImage = ebiten.NewImage(width, height) overlayImage.Fill(color.RGBA{R: 0, G: 0, B: 0, A: 180}) overlaySize.width = width overlaySize.height = height } return overlayImage } type MenuOption int const ( OptionResume MenuOption = iota OptionSettings OptionQuit optionCount ) type menuScreen int const ( screenMain menuScreen = iota screenSettings ) type PauseMenu struct { selectedIndex int currentScreen menuScreen fpsMonitorValue *bool fpsCapValue FPSCapSetting } type FPSCapSetting interface { String() string Cycle() } func NewPauseMenu() *PauseMenu { return &PauseMenu{ selectedIndex: 0, currentScreen: screenMain, } } func (m *PauseMenu) SetFPSMonitor(enabled *bool) { m.fpsMonitorValue = enabled } func (m *PauseMenu) SetFPSCap(cap FPSCapSetting) { m.fpsCapValue = cap } // Returns the selected option if one was chosen, nil otherwise func (m *PauseMenu) Update() *MenuOption { if m.currentScreen == screenSettings { return m.updateSettings() } return m.updateMain() } func (m *PauseMenu) updateMain() *MenuOption { if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) { m.selectedIndex-- if m.selectedIndex < 0 { m.selectedIndex = int(optionCount) - 1 } } if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) || inpututil.IsKeyJustPressed(ebiten.KeyS) { m.selectedIndex++ if m.selectedIndex >= int(optionCount) { m.selectedIndex = 0 } } if inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeySpace) { selected := MenuOption(m.selectedIndex) if selected == OptionSettings { m.currentScreen = screenSettings m.selectedIndex = 0 return nil } return &selected } return nil } func (m *PauseMenu) updateSettings() *MenuOption { if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { m.currentScreen = screenMain m.selectedIndex = 0 return nil } settingsCount := 2 if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) { m.selectedIndex-- if m.selectedIndex < 0 { m.selectedIndex = 0 } } if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) || inpututil.IsKeyJustPressed(ebiten.KeyS) { m.selectedIndex++ if m.selectedIndex >= settingsCount { m.selectedIndex = settingsCount - 1 } } if inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeySpace) { if m.selectedIndex == 0 && m.fpsMonitorValue != nil { *m.fpsMonitorValue = !*m.fpsMonitorValue } else if m.selectedIndex == 1 && m.fpsCapValue != nil { m.fpsCapValue.Cycle() } } return nil } func (m *PauseMenu) Draw(screen *ebiten.Image, screenWidth, screenHeight int) { if overlay := getOverlayImage(screenWidth, screenHeight); overlay != nil { screen.DrawImage(overlay, nil) } menuWidth := 400 menuHeight := 300 menuX := (screenWidth - menuWidth) / 2 menuY := (screenHeight - menuHeight) / 2 vector.DrawFilledRect(screen, float32(menuX), float32(menuY), float32(menuWidth), float32(menuHeight), color.RGBA{R: 40, G: 40, B: 50, A: 255}, false, ) vector.StrokeRect(screen, float32(menuX), float32(menuY), float32(menuWidth), float32(menuHeight), 2, color.RGBA{R: 100, G: 100, B: 120, A: 255}, false, ) if m.currentScreen == screenSettings { m.drawSettings(screen, menuX, menuY, menuWidth, menuHeight) } else { m.drawMain(screen, menuX, menuY, menuWidth, menuHeight) } } func (m *PauseMenu) drawMain(screen *ebiten.Image, menuX, menuY, menuWidth, menuHeight int) { titleText := "PAUSED" titleX := menuX + (menuWidth / 2) - (len(titleText) * 7 / 2) titleY := menuY + 50 m.drawText(screen, titleText, color.White, titleX, titleY) options := []string{"Resume", "Settings", "Quit"} startY := menuY + 110 for i, option := range options { optionY := startY + (i * 40) optionX := menuX + (menuWidth / 2) - (len(option) * 7 / 2) if i == m.selectedIndex { indicatorX := optionX - 20 m.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, optionY) m.drawText(screen, option, color.RGBA{R: 255, G: 255, B: 100, A: 255}, optionX, optionY) } else { m.drawText(screen, option, color.RGBA{R: 180, G: 180, B: 180, A: 255}, optionX, optionY) } } hintText := "Use Arrow Keys/WASD to navigate, Enter/Space to select" hintX := menuX + (menuWidth / 2) - (len(hintText) * 7 / 2) hintY := menuY + menuHeight - 30 m.drawText(screen, hintText, color.RGBA{R: 150, G: 150, B: 150, A: 255}, hintX, hintY) } func (m *PauseMenu) drawSettings(screen *ebiten.Image, menuX, menuY, menuWidth, menuHeight int) { titleText := "SETTINGS" titleX := menuX + (menuWidth / 2) - (len(titleText) * 7 / 2) titleY := menuY + 50 m.drawText(screen, titleText, color.White, titleX, titleY) startY := menuY + 110 leftMargin := menuX + 40 fpsMonitorText := "FPS Monitor: " if m.fpsMonitorValue != nil && *m.fpsMonitorValue { fpsMonitorText += "ON" } else { fpsMonitorText += "OFF" } if m.selectedIndex == 0 { indicatorX := leftMargin - 20 m.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, startY) m.drawText(screen, fpsMonitorText, color.RGBA{R: 255, G: 255, B: 100, A: 255}, leftMargin, startY) } else { m.drawText(screen, fpsMonitorText, color.RGBA{R: 180, G: 180, B: 180, A: 255}, leftMargin, startY) } fpsCapText := "FPS Cap: " if m.fpsCapValue != nil { fpsCapText += m.fpsCapValue.String() } else { fpsCapText += "60 FPS" } capY := startY + 30 if m.selectedIndex == 1 { indicatorX := leftMargin - 20 m.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, capY) m.drawText(screen, fpsCapText, color.RGBA{R: 255, G: 255, B: 100, A: 255}, leftMargin, capY) } else { m.drawText(screen, fpsCapText, color.RGBA{R: 180, G: 180, B: 180, A: 255}, leftMargin, capY) } hintText := "Enter/Space to toggle, ESC to go back" hintX := menuX + (menuWidth / 2) - (len(hintText) * 7 / 2) hintY := menuY + menuHeight - 30 m.drawText(screen, hintText, color.RGBA{R: 150, G: 150, B: 150, A: 255}, hintX, hintY) } func (m *PauseMenu) drawText(screen *ebiten.Image, txt string, clr color.Color, x, y int) { op := &text.DrawOptions{} op.GeoM.Translate(float64(x), float64(y)-basicFaceAscent) op.ColorScale.ScaleWithColor(clr) text.Draw(screen, txt, basicFace, op) } func (m *PauseMenu) Reset() { m.selectedIndex = 0 m.currentScreen = screenMain }