overlay.go

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