Skip to content

Commit 060dfc4

Browse files
committed
feat(tui): add overlay
1 parent ced432a commit 060dfc4

File tree

1 file changed

+124
-0
lines changed

1 file changed

+124
-0
lines changed

internal/tui/overlay.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// This code is from https://github.com/charmbracelet/lipgloss/pull/102 & https://github.com/yorukot/superfile
2+
package tui
3+
4+
import (
5+
"strings"
6+
7+
charmansi "github.com/charmbracelet/x/ansi"
8+
ansi "github.com/muesli/reflow/ansi"
9+
"github.com/muesli/reflow/truncate"
10+
"github.com/muesli/termenv"
11+
)
12+
13+
type whitespace struct {
14+
style termenv.Style
15+
chars string
16+
}
17+
18+
type WhitespaceOption func(*whitespace)
19+
20+
func (w whitespace) render(width int) string {
21+
if w.chars == "" {
22+
w.chars = " "
23+
}
24+
25+
r := []rune(w.chars)
26+
j := 0
27+
b := strings.Builder{}
28+
29+
// Cycle through runes and print them into the whitespace.
30+
for i := 0; i < width; {
31+
b.WriteRune(r[j])
32+
j++
33+
if j >= len(r) {
34+
j = 0
35+
}
36+
i += charmansi.StringWidth(string(r[j]))
37+
}
38+
39+
// Fill any extra gaps white spaces. This might be necessary if any runes
40+
// are more than one cell wide, which could leave a one-rune gap.
41+
short := width - charmansi.StringWidth(b.String())
42+
if short > 0 {
43+
b.WriteString(strings.Repeat(" ", short))
44+
}
45+
46+
return w.style.Styled(b.String())
47+
}
48+
49+
// placeOverlay places fg on top of bg.
50+
func placeOverlay(x, y int, fg, bg string, opts ...WhitespaceOption) string {
51+
fgLines, fgWidth := getLines(fg)
52+
bgLines, bgWidth := getLines(bg)
53+
bgHeight := len(bgLines)
54+
fgHeight := len(fgLines)
55+
56+
if fgWidth >= bgWidth && fgHeight >= bgHeight {
57+
// FIXME: return fg or bg?
58+
return fg
59+
}
60+
// TODO: allow placement outside of the bg box?
61+
x = clamp(x, 0, bgWidth-fgWidth)
62+
y = clamp(y, 0, bgHeight-fgHeight)
63+
64+
ws := &whitespace{}
65+
for _, opt := range opts {
66+
opt(ws)
67+
}
68+
69+
var b strings.Builder
70+
for i, bgLine := range bgLines {
71+
if i > 0 {
72+
b.WriteByte('\n')
73+
}
74+
if i < y || i >= y+fgHeight {
75+
b.WriteString(bgLine)
76+
continue
77+
}
78+
79+
pos := 0
80+
if x > 0 {
81+
left := truncate.String(bgLine, uint(x))
82+
pos = ansi.PrintableRuneWidth(left)
83+
b.WriteString(left)
84+
if pos < x {
85+
b.WriteString(ws.render(x - pos))
86+
pos = x
87+
}
88+
}
89+
90+
fgLine := fgLines[i-y]
91+
b.WriteString(fgLine)
92+
pos += ansi.PrintableRuneWidth(fgLine)
93+
94+
right := charmansi.TruncateLeft(bgLine, pos, "")
95+
bgWidth = ansi.PrintableRuneWidth(bgLine)
96+
rightWidth := ansi.PrintableRuneWidth(right)
97+
if rightWidth <= bgWidth-pos {
98+
b.WriteString(ws.render(bgWidth - rightWidth - pos))
99+
}
100+
101+
b.WriteString(right)
102+
}
103+
104+
return b.String()
105+
}
106+
107+
func clamp(v, lower, upper int) int {
108+
return min(max(v, lower), upper)
109+
}
110+
111+
// Split a string into lines, additionally returning the size of the widest
112+
// line.
113+
func getLines(s string) ([]string, int) {
114+
lines := strings.Split(s, "\n")
115+
widest := 0
116+
for _, l := range lines {
117+
w := charmansi.StringWidth(l)
118+
if widest < w {
119+
widest = w
120+
}
121+
}
122+
123+
return lines, widest
124+
}

0 commit comments

Comments
 (0)