overlay.go

  1package layout
  2
  3import (
  4	"strings"
  5
  6	"github.com/charmbracelet/lipgloss"
  7	chAnsi "github.com/charmbracelet/x/ansi"
  8	"github.com/muesli/ansi"
  9	"github.com/muesli/reflow/truncate"
 10	"github.com/muesli/termenv"
 11	"github.com/opencode-ai/opencode/internal/tui/styles"
 12	"github.com/opencode-ai/opencode/internal/tui/theme"
 13	"github.com/opencode-ai/opencode/internal/tui/util"
 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		t := theme.CurrentTheme()
 48		baseStyle := styles.BaseStyle()
 49
 50		var shadowbg string = ""
 51		shadowchar := lipgloss.NewStyle().
 52			Background(t.BackgroundDarker()).
 53			Foreground(t.Background()).
 54			Render("░")
 55		bgchar := baseStyle.Render(" ")
 56		for i := 0; i <= fgHeight; i++ {
 57			if i == 0 {
 58				shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n"
 59			} else {
 60				shadowbg += bgchar + strings.Repeat(shadowchar, fgWidth) + "\n"
 61			}
 62		}
 63
 64		fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...)
 65		fgLines, fgWidth = getLines(fg)
 66		fgHeight = len(fgLines)
 67	}
 68
 69	if fgWidth >= bgWidth && fgHeight >= bgHeight {
 70		// FIXME: return fg or bg?
 71		return fg
 72	}
 73	// TODO: allow placement outside of the bg box?
 74	x = util.Clamp(x, 0, bgWidth-fgWidth)
 75	y = util.Clamp(y, 0, bgHeight-fgHeight)
 76
 77	ws := &whitespace{}
 78	for _, opt := range opts {
 79		opt(ws)
 80	}
 81
 82	var b strings.Builder
 83	for i, bgLine := range bgLines {
 84		if i > 0 {
 85			b.WriteByte('\n')
 86		}
 87		if i < y || i >= y+fgHeight {
 88			b.WriteString(bgLine)
 89			continue
 90		}
 91
 92		pos := 0
 93		if x > 0 {
 94			left := truncate.String(bgLine, uint(x))
 95			pos = ansi.PrintableRuneWidth(left)
 96			b.WriteString(left)
 97			if pos < x {
 98				b.WriteString(ws.render(x - pos))
 99				pos = x
100			}
101		}
102
103		fgLine := fgLines[i-y]
104		b.WriteString(fgLine)
105		pos += ansi.PrintableRuneWidth(fgLine)
106
107		right := cutLeft(bgLine, pos)
108		bgWidth := ansi.PrintableRuneWidth(bgLine)
109		rightWidth := ansi.PrintableRuneWidth(right)
110		if rightWidth <= bgWidth-pos {
111			b.WriteString(ws.render(bgWidth - rightWidth - pos))
112		}
113
114		b.WriteString(right)
115	}
116
117	return b.String()
118}
119
120// cutLeft cuts printable characters from the left.
121// This function is heavily based on muesli's ansi and truncate packages.
122func cutLeft(s string, cutWidth int) string {
123	return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
124}
125
126func max(a, b int) int {
127	if a > b {
128		return a
129	}
130	return b
131}
132
133type whitespace struct {
134	style termenv.Style
135	chars string
136}
137
138// Render whitespaces.
139func (w whitespace) render(width int) string {
140	if w.chars == "" {
141		w.chars = " "
142	}
143
144	r := []rune(w.chars)
145	j := 0
146	b := strings.Builder{}
147
148	// Cycle through runes and print them into the whitespace.
149	for i := 0; i < width; {
150		b.WriteRune(r[j])
151		j++
152		if j >= len(r) {
153			j = 0
154		}
155		i += ansi.PrintableRuneWidth(string(r[j]))
156	}
157
158	// Fill any extra gaps white spaces. This might be necessary if any runes
159	// are more than one cell wide, which could leave a one-rune gap.
160	short := width - ansi.PrintableRuneWidth(b.String())
161	if short > 0 {
162		b.WriteString(strings.Repeat(" ", short))
163	}
164
165	return w.style.Styled(b.String())
166}
167
168// WhitespaceOption sets a styling rule for rendering whitespace.
169type WhitespaceOption func(*whitespace)