diff options
| author | historia <[not public]> | 2026-06-25 15:40:48 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-25 15:40:48 -0400 |
| commit | abd612c15799f604e671e83dc7c410ed2b44185f (patch) | |
| tree | 0597ca92350de73aac2a7cf26b2f2d6595dac0cc /internal/ui | |
| parent | 2725e2927a1595c7b100d942d1f14146252adeb7 (diff) | |
| download | thehouseoficarus-abd612c15799f604e671e83dc7c410ed2b44185f.tar.gz | |
slop refactor
Diffstat (limited to 'internal/ui')
| -rw-r--r-- | internal/ui/bar.go | 110 | ||||
| -rw-r--r-- | internal/ui/doc.go | 5 | ||||
| -rw-r--r-- | internal/ui/table.go | 150 |
3 files changed, 265 insertions, 0 deletions
diff --git a/internal/ui/bar.go b/internal/ui/bar.go new file mode 100644 index 0000000..c1abce9 --- /dev/null +++ b/internal/ui/bar.go @@ -0,0 +1,110 @@ +package ui + +import ( + "math" + "strings" + + "thehouseoficarus/internal/color" +) + +type BarStyle struct { + FilledColor int + EmptyColor int + EmptyDim bool +} + +func RenderBar(current, max, width int, unicode bool) string { + if max <= 0 { + max = 1 + } + if current < 0 { + current = 0 + } + if current > max { + current = max + } + filled := int(math.Round(float64(current) * float64(width) / float64(max))) + if filled > width { + filled = width + } + if filled < 0 { + filled = 0 + } + + fillRune, emptyRune := barRunes(unicode) + return strings.Repeat(fillRune, filled) + strings.Repeat(emptyRune, width-filled) +} + +func RenderBarF(current, max float64, width int, unicode bool) string { + if max <= 0 { + max = 1 + } + ratio := current / max + if ratio > 1 { + ratio = 1 + } + if ratio < 0 { + ratio = 0 + } + filled := int(math.Round(ratio * float64(width))) + if filled > width { + filled = width + } + if filled < 0 { + filled = 0 + } + + fillRune, emptyRune := barRunes(unicode) + return strings.Repeat(fillRune, filled) + strings.Repeat(emptyRune, width-filled) +} + +func RenderColoredBar(current, max, width int, unicode bool, style *BarStyle, mode string) string { + if max <= 0 { + max = 1 + } + if current < 0 { + current = 0 + } + if current > max { + current = max + } + filled := int(math.Round(float64(current) * float64(width) / float64(max))) + if filled > width { + filled = width + } + if filled < 0 { + filled = 0 + } + + fillRune, emptyRune := barRunes(unicode) + fillStr := strings.Repeat(fillRune, filled) + emptyStr := strings.Repeat(emptyRune, width-filled) + + if style != nil && mode != "none" && mode != "" { + if style.FilledColor >= 0 { + fillStr = color.Render(mode, color.ColorSpec{Fg: style.FilledColor}, fillStr) + } + if style.EmptyColor >= 0 { + spec := color.ColorSpec{Fg: style.EmptyColor} + if style.EmptyDim { + spec.Dim = true + } + emptyStr = color.Render(mode, spec, emptyStr) + } + } + + return fillStr + emptyStr +} + +func barRunes(unicode bool) (fill, empty string) { + return BarRunes(unicode) +} + +// BarRunes returns the fill and empty runes used for progress bars, selecting +// Unicode block characters when unicode is true and ASCII otherwise. +func BarRunes(unicode bool) (fill, empty string) { + if unicode { + return "█", "░" + } + return "#", "." +} diff --git a/internal/ui/doc.go b/internal/ui/doc.go new file mode 100644 index 0000000..428ac51 --- /dev/null +++ b/internal/ui/doc.go @@ -0,0 +1,5 @@ +// Package ui provides pure rendering primitives (progress bars, Unicode box +// tables) that are independent of game state. Session-coupled rendering +// (colour resolution, prompt formatting, combat display) lives in the game +// package as render_*.go files. +package ui diff --git a/internal/ui/table.go b/internal/ui/table.go new file mode 100644 index 0000000..0ac41ad --- /dev/null +++ b/internal/ui/table.go @@ -0,0 +1,150 @@ +package ui + +import ( + "strings" + + "thehouseoficarus/internal/color" +) + +type tableGlyphs struct { + topLeft, topSep, topRight rune + side, colSep rune + sepLeft, sepCross, sepRight rune + botLeft, botSep, botRight rune + fillH, fillHSep rune + titleSep rune +} + +func tableGlyphSet(unicode bool) tableGlyphs { + if unicode { + return tableGlyphs{ + topLeft: '╔', topSep: '╤', topRight: '╗', side: '║', colSep: '│', + sepLeft: '╟', sepCross: '┼', sepRight: '╢', + botLeft: '╚', botSep: '╧', botRight: '╝', fillH: '═', fillHSep: '─', + titleSep: '┬', + } + } + return tableGlyphs{ + topLeft: '+', topSep: '+', topRight: '+', side: '|', colSep: '|', + sepLeft: '+', sepCross: '+', sepRight: '+', + botLeft: '+', botSep: '+', botRight: '+', fillH: '-', fillHSep: '-', + titleSep: '+', + } +} + +type Table struct { + Title string + Columns []string + Rows [][]string +} + +func (t *Table) Render(unicode bool) []string { + g := tableGlyphSet(unicode) + + nCols := len(t.Columns) + if nCols == 0 && len(t.Rows) > 0 { + nCols = len(t.Rows[0]) + } + if nCols == 0 { + return nil + } + + colWidths := make([]int, nCols) + for i, h := range t.Columns { + colWidths[i] = color.VisibleLen(h) + } + for _, row := range t.Rows { + for i, cell := range row { + if i >= nCols { + break + } + vl := color.VisibleLen(cell) + if vl > colWidths[i] { + colWidths[i] = vl + } + } + } + + makeSep := func(left, cross, right rune, fill rune) string { + var b strings.Builder + b.WriteRune(left) + for i := 0; i < nCols; i++ { + if i > 0 { + b.WriteRune(cross) + } + b.WriteString(strings.Repeat(string(fill), colWidths[i]+2)) + } + b.WriteRune(right) + return b.String() + } + + makeRow := func(cells []string) string { + var b strings.Builder + b.WriteRune(g.side) + for i := 0; i < nCols; i++ { + if i > 0 { + b.WriteRune(g.colSep) + } + cell := "" + if i < len(cells) { + cell = cells[i] + } + pad := colWidths[i] - color.VisibleLen(cell) + if pad < 0 { + pad = 0 + } + b.WriteString(" " + cell + strings.Repeat(" ", pad) + " ") + } + b.WriteRune(g.side) + return b.String() + } + + hasTitle := t.Title != "" + hasCols := len(t.Columns) > 0 + + var out []string + + if hasTitle { + innerWidth := 0 + for _, w := range colWidths { + innerWidth += w + 2 + } + innerWidth += nCols - 1 + + var tb strings.Builder + tb.WriteRune(g.topLeft) + tb.WriteString(strings.Repeat(string(g.fillH), innerWidth)) + tb.WriteRune(g.topRight) + out = append(out, tb.String()) + + var tr strings.Builder + tr.WriteRune(g.side) + tr.WriteString(" ") + tr.WriteString(t.Title) + padding := innerWidth - 2 - color.VisibleLen(t.Title) + if padding < 0 { + padding = 0 + } + tr.WriteString(strings.Repeat(" ", padding)) + tr.WriteString(" ") + tr.WriteRune(g.side) + out = append(out, tr.String()) + + out = append(out, makeSep(g.sepLeft, g.titleSep, g.sepRight, g.fillHSep)) + } else { + out = append(out, makeSep(g.topLeft, g.topSep, g.topRight, g.fillH)) + } + + if hasCols { + out = append(out, makeRow(t.Columns)) + out = append(out, makeSep(g.sepLeft, g.sepCross, g.sepRight, g.fillHSep)) + } + + for _, row := range t.Rows { + out = append(out, makeRow(row)) + } + + out = append(out, makeSep(g.botLeft, g.botSep, g.botRight, g.fillH)) + + return out +} |
