Menus

The Menu type handles the boilerplate of D-pad-navigated option lists: cursor tracking, wrapping, disabled items, and confirmation callbacks. Use it inside any GameState for title screens, option screens, pause menus, and game-over screens.

Quick start

type TitleState struct {
    sm   *gosprite64.StateMachine
    menu *gosprite64.Menu
}

func (s *TitleState) Enter() {
    s.menu = gosprite64.NewMenu([]gosprite64.MenuItem{
        {Label: "Start Game", OnConfirm: func() {
            s.sm.Switch(&GameplayState{sm: s.sm})
        }},
        {Label: "Options", OnConfirm: func() {
            s.sm.Push(&OptionsState{sm: s.sm})
        }},
        {Label: "Quit", Disabled: true},
    })
    s.menu.X = 100
    s.menu.Y = 100
    s.menu.Wrap = true
}

func (s *TitleState) Update() {
    s.menu.HandleInput()
}

func (s *TitleState) Draw() {
    gosprite64.ClearScreen()
    gosprite64.DrawText("MY GAME", 112, 40, gosprite64.White)
    s.menu.Draw()
}

HandleInput reads D-pad Up/Down for navigation and A for confirmation. Draw renders the menu with a cursor indicator. That's the entire integration.

Each item has a label, an optional callback, and a disabled flag:

type MenuItem struct {
    Label     string      // displayed text
    Disabled  bool        // grayed out, skipped by cursor, can't confirm
    OnConfirm func()      // called when A is pressed on this item
}

OnConfirm can do anything: switch states, change settings, start gameplay. If OnConfirm is nil or the item is disabled, pressing A does nothing.

The cursor moves with D-pad Up and Down. Disabled items are automatically skipped:

menu := gosprite64.NewMenu([]gosprite64.MenuItem{
    {Label: "Play"},
    {Label: "Locked", Disabled: true},
    {Label: "Settings"},
})
// Pressing Down from "Play" skips "Locked" and lands on "Settings"

If all items are disabled, the cursor stays where it is.

Wrapping

By default, the cursor stops at the first and last item. Set Wrap = true to wrap around:

menu.Wrap = true
// Pressing Down on the last item goes to the first
// Pressing Up on the first item goes to the last

Manual control

If HandleInput doesn't fit your needs (e.g. you want analog stick support or different buttons), use the navigation methods directly:

func (s *MyState) Update() {
    if gosprite64.IsButtonJustPressed(gosprite64.ButtonDPadDown) {
        s.menu.MoveDown()
    }
    if gosprite64.IsButtonJustPressed(gosprite64.ButtonDPadUp) {
        s.menu.MoveUp()
    }
    if gosprite64.IsButtonJustPressed(gosprite64.ButtonStart) {
        s.menu.Confirm()
    }
}

Other useful methods:

menu.Cursor()       // current index (0-based)
menu.SetCursor(2)   // jump to index (clamped to valid range)
menu.Selected()     // get the highlighted MenuItem
menu.Count()        // number of items

Customizing appearance

The built-in Draw uses the 8x8 DrawText font. Customize position, spacing, colors, and cursor character:

menu.X = 80           // left edge
menu.Y = 60           // top edge
menu.LineHeight = 14   // pixels between items (default 12)
menu.Color = gosprite64.Yellow          // text color
menu.CursorChar = "-> "                // cursor prefix (default "> ")

Disabled items are drawn in DarkGray regardless of Color.

Custom rendering

For full control (custom fonts, icons, backgrounds), skip Draw and render manually using Cursor():

func (s *MyState) Draw() {
    for i, item := range items {
        y := 60 + i*16
        if i == s.menu.Cursor() {
            gosprite64.FillRect(78, y-2, 220, y+10, gosprite64.DarkBlue)
        }
        c := gosprite64.White
        if item.Disabled {
            c = gosprite64.DarkGray
        }
        myFont.DrawTextEx(item.Label, 84, y, gosprite64.AlignLeft)
    }
}

Multiple menus

A state can have multiple menus. Track which is active:

type OptionsState struct {
    difficultyMenu *gosprite64.Menu
    speedMenu      *gosprite64.Menu
    activeMenu     int  // 0 = difficulty, 1 = speed
}

func (s *OptionsState) Update() {
    switch s.activeMenu {
    case 0:
        s.difficultyMenu.HandleInput()
    case 1:
        s.speedMenu.HandleInput()
    }
    if gosprite64.IsButtonJustPressed(gosprite64.ButtonR) {
        s.activeMenu = (s.activeMenu + 1) % 2
    }
}

Complete example

See examples/splitscreen_demo which uses Menu for the title and result screens, combined with Timer for countdowns and SetDrawRegion for split-screen viewports.