clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

refactor/root-tui-model
puzzletea / ui / mainmenu.go
mainmenu.go 2.6K
1package ui23import (4 "strings"56 "charm.land/lipgloss/v2"7)89const logo = "puzzletea"1011// MainMenu is a custom main menu component with an ASCII art logo12// and styled item list. It replaces the generic list.Model for the13// root menu to give the application a game-themed identity.14type MainMenu struct {15 items []MenuItem16 cursor int17}1819// NewMainMenu creates a main menu with an ASCII art logo and the given items.20func NewMainMenu(items []MenuItem) MainMenu {21 return MainMenu{22 items: items,23 }24}2526// CursorUp moves the selection cursor up, wrapping around.27func (m *MainMenu) CursorUp() {28 m.cursor--29 if m.cursor < 0 {30 m.cursor = len(m.items) - 131 }32}3334// CursorDown moves the selection cursor down, wrapping around.35func (m *MainMenu) CursorDown() {36 m.cursor++37 if m.cursor >= len(m.items) {38 m.cursor = 039 }40}4142func (m MainMenu) Selected() MenuItem {43 return m.items[m.cursor]44}4546func (m MainMenu) SelectedAction() string {47 return m.Selected().ActionID()48}4950// View renders the main menu as a framed panel with logo and items.51func (m MainMenu) View() string {52 styledLogo := LogoStyle().Render(logo)5354 items := m.RenderItems()5556 footer := FooterHint().Render("↑/↓ navigate • enter select • ctrl+c quit")5758 inner := lipgloss.JoinVertical(lipgloss.Left,59 styledLogo,60 "",61 items,62 "",63 footer,64 )6566 return HeavyPanel(inner)67}6869// ViewAsPanel renders the menu items inside a rounded-border Panel,70// suitable for submenus that should look visually distinct from the71// root main menu.72func (m MainMenu) ViewAsPanel(title string) string {73 return Panel(title, m.RenderItems(), "↑/↓ navigate • enter select • esc back")74}7576// RenderItems returns the styled item list shared by View and ViewAsPanel.77func (m MainMenu) RenderItems() string {78 // Pre-compute the widest item line so the block has a stable width79 // regardless of which item is selected.80 maxW := 081 for _, item := range m.items {82 // Selected cursor prefix ("⦾ ") is the widest variant at 3 chars.83 w := lipgloss.Width("⦾ " + item.ItemTitle)84 if dw := lipgloss.Width(" " + item.Desc); dw > w {85 w = dw86 }87 if w > maxW {88 maxW = w89 }90 }9192 var itemLines []string93 for i, item := range m.items {94 var line, cursor, title string95 if i == m.cursor {96 cursor = CursorStyle().Render("⦾ ")97 title = SelectedItemStyle().Render(item.ItemTitle)98 } else {99 cursor = CursorStyle().Render("◦ ")100 title = NormalItemStyle().Render(item.ItemTitle)101 }102 desc := DimItemStyle().Render("\n " + item.Desc)103 line = cursor + title + desc + "\n"104 itemLines = append(itemLines, line)105 }106107 content := strings.Join(itemLines, "\n")108 return lipgloss.NewStyle().Width(maxW).Render(content)109}