overlay.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. package layout
  2. import (
  3. "bytes"
  4. "strings"
  5. "github.com/charmbracelet/lipgloss"
  6. "github.com/kujtimiihoxha/opencode/internal/tui/styles"
  7. "github.com/kujtimiihoxha/opencode/internal/tui/util"
  8. "github.com/mattn/go-runewidth"
  9. "github.com/muesli/ansi"
  10. "github.com/muesli/reflow/truncate"
  11. "github.com/muesli/termenv"
  12. )
  13. // Most of this code is borrowed from
  14. // https://github.com/charmbracelet/lipgloss/pull/102
  15. // as well as the lipgloss library, with some modification for what I needed.
  16. // Split a string into lines, additionally returning the size of the widest
  17. // line.
  18. func getLines(s string) (lines []string, widest int) {
  19. lines = strings.Split(s, "\n")
  20. for _, l := range lines {
  21. w := ansi.PrintableRuneWidth(l)
  22. if widest < w {
  23. widest = w
  24. }
  25. }
  26. return lines, widest
  27. }
  28. // PlaceOverlay places fg on top of bg.
  29. func PlaceOverlay(
  30. x, y int,
  31. fg, bg string,
  32. shadow bool, opts ...WhitespaceOption,
  33. ) string {
  34. fgLines, fgWidth := getLines(fg)
  35. bgLines, bgWidth := getLines(bg)
  36. bgHeight := len(bgLines)
  37. fgHeight := len(fgLines)
  38. if shadow {
  39. var shadowbg string = ""
  40. shadowchar := lipgloss.NewStyle().
  41. Background(styles.BackgroundDarker).
  42. Foreground(styles.Background).
  43. Render("░")
  44. bgchar := styles.BaseStyle.Render(" ")
  45. for i := 0; i <= fgHeight; i++ {
  46. if i == 0 {
  47. shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n"
  48. } else {
  49. shadowbg += bgchar + strings.Repeat(shadowchar, fgWidth) + "\n"
  50. }
  51. }
  52. fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...)
  53. fgLines, fgWidth = getLines(fg)
  54. fgHeight = len(fgLines)
  55. }
  56. if fgWidth >= bgWidth && fgHeight >= bgHeight {
  57. // FIXME: return fg or bg?
  58. return fg
  59. }
  60. // TODO: allow placement outside of the bg box?
  61. x = util.Clamp(x, 0, bgWidth-fgWidth)
  62. y = util.Clamp(y, 0, bgHeight-fgHeight)
  63. ws := &whitespace{}
  64. for _, opt := range opts {
  65. opt(ws)
  66. }
  67. var b strings.Builder
  68. for i, bgLine := range bgLines {
  69. if i > 0 {
  70. b.WriteByte('\n')
  71. }
  72. if i < y || i >= y+fgHeight {
  73. b.WriteString(bgLine)
  74. continue
  75. }
  76. pos := 0
  77. if x > 0 {
  78. left := truncate.String(bgLine, uint(x))
  79. pos = ansi.PrintableRuneWidth(left)
  80. b.WriteString(left)
  81. if pos < x {
  82. b.WriteString(ws.render(x - pos))
  83. pos = x
  84. }
  85. }
  86. fgLine := fgLines[i-y]
  87. b.WriteString(fgLine)
  88. pos += ansi.PrintableRuneWidth(fgLine)
  89. right := cutLeft(bgLine, pos)
  90. bgWidth := ansi.PrintableRuneWidth(bgLine)
  91. rightWidth := ansi.PrintableRuneWidth(right)
  92. if rightWidth <= bgWidth-pos {
  93. b.WriteString(ws.render(bgWidth - rightWidth - pos))
  94. }
  95. b.WriteString(right)
  96. }
  97. return b.String()
  98. }
  99. // cutLeft cuts printable characters from the left.
  100. // This function is heavily based on muesli's ansi and truncate packages.
  101. func cutLeft(s string, cutWidth int) string {
  102. var (
  103. pos int
  104. isAnsi bool
  105. ab bytes.Buffer
  106. b bytes.Buffer
  107. )
  108. for _, c := range s {
  109. var w int
  110. if c == ansi.Marker || isAnsi {
  111. isAnsi = true
  112. ab.WriteRune(c)
  113. if ansi.IsTerminator(c) {
  114. isAnsi = false
  115. if bytes.HasSuffix(ab.Bytes(), []byte("[0m")) {
  116. ab.Reset()
  117. }
  118. }
  119. } else {
  120. w = runewidth.RuneWidth(c)
  121. }
  122. if pos >= cutWidth {
  123. if b.Len() == 0 {
  124. if ab.Len() > 0 {
  125. b.Write(ab.Bytes())
  126. }
  127. if pos-cutWidth > 1 {
  128. b.WriteByte(' ')
  129. continue
  130. }
  131. }
  132. b.WriteRune(c)
  133. }
  134. pos += w
  135. }
  136. return b.String()
  137. }
  138. func max(a, b int) int {
  139. if a > b {
  140. return a
  141. }
  142. return b
  143. }
  144. type whitespace struct {
  145. style termenv.Style
  146. chars string
  147. }
  148. // Render whitespaces.
  149. func (w whitespace) render(width int) string {
  150. if w.chars == "" {
  151. w.chars = " "
  152. }
  153. r := []rune(w.chars)
  154. j := 0
  155. b := strings.Builder{}
  156. // Cycle through runes and print them into the whitespace.
  157. for i := 0; i < width; {
  158. b.WriteRune(r[j])
  159. j++
  160. if j >= len(r) {
  161. j = 0
  162. }
  163. i += ansi.PrintableRuneWidth(string(r[j]))
  164. }
  165. // Fill any extra gaps white spaces. This might be necessary if any runes
  166. // are more than one cell wide, which could leave a one-rune gap.
  167. short := width - ansi.PrintableRuneWidth(b.String())
  168. if short > 0 {
  169. b.WriteString(strings.Repeat(" ", short))
  170. }
  171. return w.style.Styled(b.String())
  172. }
  173. // WhitespaceOption sets a styling rule for rendering whitespace.
  174. type WhitespaceOption func(*whitespace)