overlay.go 3.6 KB

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