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