aboutsummaryrefslogtreecommitdiff
path: root/internal/ui
diff options
context:
space:
mode:
Diffstat (limited to 'internal/ui')
-rw-r--r--internal/ui/bar.go110
-rw-r--r--internal/ui/doc.go5
-rw-r--r--internal/ui/table.go150
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
+}