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)