diff options
| author | Dax Raad <[email protected]> | 2025-05-18 22:30:41 -0400 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2025-05-26 12:40:17 -0400 |
| commit | 99af6146d5def31c59993636d60eb75a483a283b (patch) | |
| tree | eb57ed227c15cf9be54bc9f231c83895d812f881 /pkg/tui/layout/overlay.go | |
| parent | 020e0ca039287b73fa33041fbd1bb214e6ccb396 (diff) | |
| download | opencode-99af6146d5def31c59993636d60eb75a483a283b.tar.gz opencode-99af6146d5def31c59993636d60eb75a483a283b.zip | |
openapi
Diffstat (limited to 'pkg/tui/layout/overlay.go')
| -rw-r--r-- | pkg/tui/layout/overlay.go | 169 |
1 files changed, 169 insertions, 0 deletions
diff --git a/pkg/tui/layout/overlay.go b/pkg/tui/layout/overlay.go new file mode 100644 index 000000000..64836463d --- /dev/null +++ b/pkg/tui/layout/overlay.go @@ -0,0 +1,169 @@ +package layout + +import ( + "strings" + + "github.com/charmbracelet/lipgloss" + chAnsi "github.com/charmbracelet/x/ansi" + "github.com/muesli/ansi" + "github.com/muesli/reflow/truncate" + "github.com/muesli/termenv" + "github.com/sst/opencode/internal/tui/styles" + "github.com/sst/opencode/internal/tui/theme" + "github.com/sst/opencode/internal/tui/util" +) + +// Most of this code is borrowed from +// https://github.com/charmbracelet/lipgloss/pull/102 +// as well as the lipgloss library, with some modification for what I needed. + +// Split a string into lines, additionally returning the size of the widest +// line. +func getLines(s string) (lines []string, widest int) { + lines = strings.Split(s, "\n") + + for _, l := range lines { + w := ansi.PrintableRuneWidth(l) + if widest < w { + widest = w + } + } + + return lines, widest +} + +// PlaceOverlay places fg on top of bg. +func PlaceOverlay( + x, y int, + fg, bg string, + shadow bool, opts ...WhitespaceOption, +) string { + fgLines, fgWidth := getLines(fg) + bgLines, bgWidth := getLines(bg) + bgHeight := len(bgLines) + fgHeight := len(fgLines) + + if shadow { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + var shadowbg string = "" + shadowchar := lipgloss.NewStyle(). + Background(t.BackgroundDarker()). + Foreground(t.Background()). + Render("░") + bgchar := baseStyle.Render(" ") + for i := 0; i <= fgHeight; i++ { + if i == 0 { + shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n" + } else { + shadowbg += bgchar + strings.Repeat(shadowchar, fgWidth) + "\n" + } + } + + fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...) + fgLines, fgWidth = getLines(fg) + fgHeight = len(fgLines) + } + + if fgWidth >= bgWidth && fgHeight >= bgHeight { + // FIXME: return fg or bg? + return fg + } + // TODO: allow placement outside of the bg box? + x = util.Clamp(x, 0, bgWidth-fgWidth) + y = util.Clamp(y, 0, bgHeight-fgHeight) + + ws := &whitespace{} + for _, opt := range opts { + opt(ws) + } + + var b strings.Builder + for i, bgLine := range bgLines { + if i > 0 { + b.WriteByte('\n') + } + if i < y || i >= y+fgHeight { + b.WriteString(bgLine) + continue + } + + pos := 0 + if x > 0 { + left := truncate.String(bgLine, uint(x)) + pos = ansi.PrintableRuneWidth(left) + b.WriteString(left) + if pos < x { + b.WriteString(ws.render(x - pos)) + pos = x + } + } + + fgLine := fgLines[i-y] + b.WriteString(fgLine) + pos += ansi.PrintableRuneWidth(fgLine) + + right := cutLeft(bgLine, pos) + bgWidth := ansi.PrintableRuneWidth(bgLine) + rightWidth := ansi.PrintableRuneWidth(right) + if rightWidth <= bgWidth-pos { + b.WriteString(ws.render(bgWidth - rightWidth - pos)) + } + + b.WriteString(right) + } + + return b.String() +} + +// cutLeft cuts printable characters from the left. +// This function is heavily based on muesli's ansi and truncate packages. +func cutLeft(s string, cutWidth int) string { + return chAnsi.Cut(s, cutWidth, lipgloss.Width(s)) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +type whitespace struct { + style termenv.Style + chars string +} + +// Render whitespaces. +func (w whitespace) render(width int) string { + if w.chars == "" { + w.chars = " " + } + + r := []rune(w.chars) + j := 0 + b := strings.Builder{} + + // Cycle through runes and print them into the whitespace. + for i := 0; i < width; { + b.WriteRune(r[j]) + j++ + if j >= len(r) { + j = 0 + } + i += ansi.PrintableRuneWidth(string(r[j])) + } + + // Fill any extra gaps white spaces. This might be necessary if any runes + // are more than one cell wide, which could leave a one-rune gap. + short := width - ansi.PrintableRuneWidth(b.String()) + if short > 0 { + b.WriteString(strings.Repeat(" ", short)) + } + + return w.style.Styled(b.String()) +} + +// WhitespaceOption sets a styling rule for rendering whitespace. +type WhitespaceOption func(*whitespace) |
