summaryrefslogtreecommitdiffhomepage
path: root/pkg/tui/layout/overlay.go
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-05-18 22:30:41 -0400
committerDax Raad <[email protected]>2025-05-26 12:40:17 -0400
commit99af6146d5def31c59993636d60eb75a483a283b (patch)
treeeb57ed227c15cf9be54bc9f231c83895d812f881 /pkg/tui/layout/overlay.go
parent020e0ca039287b73fa33041fbd1bb214e6ccb396 (diff)
downloadopencode-99af6146d5def31c59993636d60eb75a483a283b.tar.gz
opencode-99af6146d5def31c59993636d60eb75a483a283b.zip
openapi
Diffstat (limited to 'pkg/tui/layout/overlay.go')
-rw-r--r--pkg/tui/layout/overlay.go169
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)