overlay.go

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