Skip to content

Commit 36c5ee5

Browse files
feat: overlay paused & game over messages
1 parent a0fbd00 commit 36c5ee5

File tree

4 files changed

+231
-7
lines changed

4 files changed

+231
-7
lines changed

cmd/tui/helpers/helpers.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package helpers
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
7+
"github.com/charmbracelet/lipgloss"
8+
"github.com/mattn/go-runewidth"
9+
"github.com/muesli/ansi"
10+
"github.com/muesli/reflow/truncate"
11+
"github.com/muesli/termenv"
12+
)
13+
14+
// Most of this code is borrowed from
15+
// https://github.com/charmbracelet/lipgloss/pull/102
16+
// as well as the lipgloss library.
17+
18+
// Split a string into lines, additionally returning the size of the widest
19+
// line.
20+
func getLines(s string) (lines []string, widest int) {
21+
lines = strings.Split(s, "\n")
22+
23+
for _, l := range lines {
24+
w := ansi.PrintableRuneWidth(l)
25+
if widest < w {
26+
widest = w
27+
}
28+
}
29+
30+
return lines, widest
31+
}
32+
33+
func PlaceOverlayCenter(fg, bg string, opts ...WhitespaceOption) string {
34+
x := lipgloss.Width(bg) / 2
35+
y := lipgloss.Height(bg) / 2
36+
return PlaceOverlay(x, y, fg, bg, opts...)
37+
}
38+
39+
// PlaceOverlay places fg on top of bg.
40+
func PlaceOverlay(x, y int, fg, bg string, opts ...WhitespaceOption) string {
41+
fgLines, fgWidth := getLines(fg)
42+
bgLines, bgWidth := getLines(bg)
43+
bgHeight := len(bgLines)
44+
fgHeight := len(fgLines)
45+
46+
if fgWidth >= bgWidth && fgHeight >= bgHeight {
47+
// FIXME: return fg or bg?
48+
return fg
49+
}
50+
// TODO: allow placement outside of the bg box?
51+
x = clamp(x, 0, bgWidth-fgWidth)
52+
y = clamp(y, 0, bgHeight-fgHeight)
53+
54+
ws := &whitespace{}
55+
for _, opt := range opts {
56+
opt(ws)
57+
}
58+
59+
var b strings.Builder
60+
for i, bgLine := range bgLines {
61+
if i > 0 {
62+
b.WriteByte('\n')
63+
}
64+
if i < y || i >= y+fgHeight {
65+
b.WriteString(bgLine)
66+
continue
67+
}
68+
69+
pos := 0
70+
if x > 0 {
71+
left := truncate.String(bgLine, uint(x))
72+
pos = ansi.PrintableRuneWidth(left)
73+
b.WriteString(left)
74+
if pos < x {
75+
b.WriteString(ws.render(x - pos))
76+
pos = x
77+
}
78+
}
79+
80+
fgLine := fgLines[i-y]
81+
b.WriteString(fgLine)
82+
pos += ansi.PrintableRuneWidth(fgLine)
83+
84+
right := cutLeft(bgLine, pos)
85+
bgWidth := ansi.PrintableRuneWidth(bgLine)
86+
rightWidth := ansi.PrintableRuneWidth(right)
87+
if rightWidth <= bgWidth-pos {
88+
b.WriteString(ws.render(bgWidth - rightWidth - pos))
89+
}
90+
91+
b.WriteString(right)
92+
}
93+
94+
return b.String()
95+
}
96+
97+
// cutLeft cuts printable characters from the left.
98+
// This function is heavily based on muesli's ansi and truncate packages.
99+
func cutLeft(s string, cutWidth int) string {
100+
var (
101+
pos int
102+
isAnsi bool
103+
ab bytes.Buffer
104+
b bytes.Buffer
105+
)
106+
for _, c := range s {
107+
var w int
108+
if c == ansi.Marker || isAnsi {
109+
isAnsi = true
110+
ab.WriteRune(c)
111+
if ansi.IsTerminator(c) {
112+
isAnsi = false
113+
if bytes.HasSuffix(ab.Bytes(), []byte("[0m")) {
114+
ab.Reset()
115+
}
116+
}
117+
} else {
118+
w = runewidth.RuneWidth(c)
119+
}
120+
121+
if pos >= cutWidth {
122+
if b.Len() == 0 {
123+
if ab.Len() > 0 {
124+
b.Write(ab.Bytes())
125+
}
126+
if pos-cutWidth > 1 {
127+
b.WriteByte(' ')
128+
continue
129+
}
130+
}
131+
b.WriteRune(c)
132+
}
133+
pos += w
134+
}
135+
return b.String()
136+
}
137+
138+
func clamp(v, lower, upper int) int {
139+
return min(max(v, lower), upper)
140+
}
141+
142+
func max(a, b int) int {
143+
if a > b {
144+
return a
145+
}
146+
return b
147+
}
148+
149+
func min(a, b int) int {
150+
if a < b {
151+
return a
152+
}
153+
return b
154+
}
155+
156+
type whitespace struct {
157+
style termenv.Style
158+
chars string
159+
}
160+
161+
// Render whitespaces.
162+
func (w whitespace) render(width int) string {
163+
if w.chars == "" {
164+
w.chars = " "
165+
}
166+
167+
r := []rune(w.chars)
168+
j := 0
169+
b := strings.Builder{}
170+
171+
// Cycle through runes and print them into the whitespace.
172+
for i := 0; i < width; {
173+
b.WriteRune(r[j])
174+
j++
175+
if j >= len(r) {
176+
j = 0
177+
}
178+
i += ansi.PrintableRuneWidth(string(r[j]))
179+
}
180+
181+
// Fill any extra gaps white spaces. This might be necessary if any runes
182+
// are more than one cell wide, which could leave a one-rune gap.
183+
short := width - ansi.PrintableRuneWidth(b.String())
184+
if short > 0 {
185+
b.WriteString(strings.Repeat(" ", short))
186+
}
187+
188+
return w.style.Styled(b.String())
189+
}
190+
191+
// WhitespaceOption sets a styling rule for rendering whitespace.
192+
type WhitespaceOption func(*whitespace)

cmd/tui/marathon/model.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
"github.com/Broderick-Westrope/tetrigo/cmd/tui/common"
9+
"github.com/Broderick-Westrope/tetrigo/cmd/tui/helpers"
910
"github.com/Broderick-Westrope/tetrigo/internal/config"
1011
"github.com/Broderick-Westrope/tetrigo/internal/data"
1112
"github.com/Broderick-Westrope/tetrigo/pkg/tetris"
@@ -17,6 +18,28 @@ import (
1718
"github.com/charmbracelet/lipgloss"
1819
)
1920

21+
var pausedMsg = `
22+
______ ___ _ _ _____ ___________
23+
| ___ \/ _ \| | | / ___| ___| _ \
24+
| |_/ / /_\ \ | | \ '--.| |__ | | | |
25+
| __/| _ | | | |'--. \ __|| | | |
26+
| | | | | | |_| /\__/ / |___| |/ /
27+
\_| \_| |_/\___/\____/\____/|___/
28+
Press PAUSE to continue or HOLD to exit.
29+
30+
`
31+
32+
var gameOverMsg = `
33+
_____ ___ ___ ___ _____ _____ _ _ ___________
34+
| __ \ / _ \ | \/ || ___| | _ || | | | ___| ___ \
35+
| | \// /_\ \| . . || |__ | | | || | | | |__ | |_/ /
36+
| | __ | _ || |\/| || __| | | | || | | | __|| /
37+
| |_\ \| | | || | | || |___ \ \_/ /\ \_/ / |___| |\ \
38+
\____/\_| |_/\_| |_/\____/ \___/ \___/\____/\_| \_|
39+
Press HOLD to continue.
40+
41+
`
42+
2043
var _ tea.Model = &Model{}
2144

2245
type Model struct {
@@ -257,11 +280,20 @@ func (m *Model) View() string {
257280
m.bagView(),
258281
)
259282

283+
if m.game.IsGameOver() {
284+
output = helpers.PlaceOverlayCenter(gameOverMsg, output)
285+
}
286+
287+
if m.isPaused {
288+
output = helpers.PlaceOverlayCenter(pausedMsg, output)
289+
}
290+
260291
output = lipgloss.JoinVertical(lipgloss.Left, output, m.help.View(m.keys))
261292

262293
if m.isFullscreen {
263-
return m.styles.ProgramFullscreen.Render(output)
294+
output = m.styles.ProgramFullscreen.Render(output)
264295
}
296+
265297
return output
266298
}
267299

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ require (
88
github.com/charmbracelet/bubbles v0.18.0
99
github.com/charmbracelet/bubbletea v0.25.0
1010
github.com/charmbracelet/lipgloss v0.9.1
11+
github.com/mattn/go-runewidth v0.0.16
1112
github.com/mattn/go-sqlite3 v1.14.22
13+
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b
14+
github.com/muesli/reflow v0.3.0
15+
github.com/muesli/termenv v0.15.2
1216
github.com/stretchr/testify v1.9.0
1317
)
1418

@@ -19,11 +23,7 @@ require (
1923
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
2024
github.com/mattn/go-isatty v0.0.18 // indirect
2125
github.com/mattn/go-localereader v0.0.1 // indirect
22-
github.com/mattn/go-runewidth v0.0.15 // indirect
23-
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
2426
github.com/muesli/cancelreader v0.2.2 // indirect
25-
github.com/muesli/reflow v0.3.0 // indirect
26-
github.com/muesli/termenv v0.15.2 // indirect
2727
github.com/pmezard/go-difflib v1.0.0 // indirect
2828
github.com/rivo/uniseg v0.4.6 // indirect
2929
golang.org/x/sync v0.1.0 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
2727
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
2828
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
2929
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
30-
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
31-
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
30+
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
31+
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
3232
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
3333
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
3434
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=

0 commit comments

Comments
 (0)