clone

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

feat/elo-difficulty
puzzletea / ui / list.go
list.go 4.3K
1package ui23import (4 "io"56 "charm.land/bubbles/v2/list"7 "github.com/FelineStateMachine/puzzletea/theme"8)910type categoryColorDelegate struct {11 list.DefaultDelegate12}1314func (d categoryColorDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {15 delegate := d.DefaultDelegate16 palette := theme.Current().ThemeColors()17 if len(palette) > 0 {18 barColor := palette[index%len(palette)]19 delegate.Styles.SelectedTitle = delegate.Styles.SelectedTitle.20 BorderLeftForeground(barColor)21 delegate.Styles.SelectedDesc = delegate.Styles.SelectedDesc.22 BorderLeftForeground(barColor)23 }24 delegate.Render(w, m, index, item)25}2627func newMenuDelegate(p theme.Palette) list.DefaultDelegate {28 d := list.NewDefaultDelegate()29 d.Styles.SelectedTitle = d.Styles.SelectedTitle.30 Foreground(p.Accent).31 BorderLeftForeground(p.Accent)32 d.Styles.SelectedDesc = d.Styles.SelectedDesc.33 Foreground(p.AccentSoft).34 BorderLeftForeground(p.Accent)35 d.Styles.NormalTitle = d.Styles.NormalTitle.36 Foreground(p.FG)37 d.Styles.NormalDesc = d.Styles.NormalDesc.38 Foreground(p.TextDim)39 return d40}4142func configureMenuList(l *list.Model, title string) {43 l.Title = title44 l.SetShowTitle(false)45 l.DisableQuitKeybindings()46 l.SetFilteringEnabled(false)47 l.SetShowHelp(false)48 l.SetShowStatusBar(false)49 l.SetShowPagination(false)50}5152func applyThemeListStyles(d *list.DefaultDelegate, p theme.Palette, width int) {53 // Title styles intentionally avoid fixed Width: bubbles uses them for54 // rune-level filter highlighting and Width can force wrapped segments.55 d.Styles.SelectedTitle = d.Styles.SelectedTitle.56 Foreground(p.Accent).57 BorderLeftForeground(p.Accent)58 d.Styles.NormalTitle = d.Styles.NormalTitle.59 Foreground(p.FG)6061 // Description lines carry the fixed width so the list column remains stable.62 d.Styles.SelectedDesc = d.Styles.SelectedDesc.63 Foreground(p.AccentSoft).64 BorderLeftForeground(p.Accent).65 Width(width)66 d.Styles.NormalDesc = d.Styles.NormalDesc.67 Foreground(p.TextDim).68 Width(width)69}7071// InitList creates a styled list widget with the active theme's colors.72// The list title is hidden because lists are rendered inside Panel frames73// that provide their own styled title.74func InitList(items []list.Item, title string) list.Model {75 p := theme.Current()76 d := newMenuDelegate(p)7778 l := list.New(items, d, 64, 64)79 configureMenuList(&l, title)80 return l81}8283// InitCategoryList creates the category picker list with per-item hover bar84// colors sourced from the active theme color set.85func InitCategoryList(items []list.Item, title string) list.Model {86 p := theme.Current()87 base := newMenuDelegate(p)88 base.ShowDescription = false89 base.SetHeight(1)90 base.SetSpacing(0)91 d := categoryColorDelegate{DefaultDelegate: base}9293 l := list.New(items, d, 64, 64)94 configureMenuList(&l, title)95 l.SetFilteringEnabled(true)96 return l97}9899// InitThemeList creates a styled list widget for the theme picker with100// a fixed-width delegate so the list column doesn't shift as the cursor101// moves across names of different lengths.102func InitThemeList(items []list.Item, width, height int) list.Model {103 p := theme.Current()104 d := list.NewDefaultDelegate()105 applyThemeListStyles(&d, p, width)106107 l := list.New(items, d, width, height)108 l.SetShowTitle(false)109 l.DisableQuitKeybindings()110 l.SetFilteringEnabled(true)111 l.SetShowHelp(false)112 l.SetShowStatusBar(false)113 return l114}115116// UpdateThemeListStyles refreshes the delegate colors on an existing theme117// list so that live-preview theme changes are reflected immediately.118func UpdateThemeListStyles(l *list.Model) {119 p := theme.Current()120 d := list.NewDefaultDelegate()121122 // Preserve the width that was set when the list was created.123 w := l.Width()124 applyThemeListStyles(&d, p, w)125126 l.SetDelegate(d)127}128129// ListHeight returns the minimum height needed to display all items in a130// single page, avoiding pagination. The bubbles list component has a131// circular dependency between pagination visibility and PerPage calculation132// that causes rendering glitches when pages change, so we size the list to133// prevent pagination entirely when the terminal is tall enough.134func ListHeight(l list.Model) int {135 n := len(l.Items())136 // Default delegate: height=2, spacing=1 → 3 lines per item.137 // Title is hidden (rendered by Panel), so no extra title height needed.138 const itemSlot = 3139 return itemSlot * n140}