overlay.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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/sst/opencode/internal/tui/styles"
  10. "github.com/sst/opencode/internal/tui/theme"
  11. "github.com/sst/opencode/internal/tui/util"
  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. t := theme.CurrentTheme()
  40. baseStyle := styles.BaseStyle()
  41. var shadowbg string = ""
  42. shadowchar := lipgloss.NewStyle().
  43. Background(t.BackgroundDarker()).
  44. Foreground(t.Background()).
  45. Render("░")
  46. bgchar := baseStyle.Render(" ")
  47. for i := 0; i <= fgHeight; i++ {
  48. if i == 0 {
  49. shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n"
  50. } else {
  51. shadowbg += bgchar + strings.Repeat(shadowchar, fgWidth) + "\n"
  52. }
  53. }
  54. fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...)
  55. fgLines, fgWidth = getLines(fg)
  56. fgHeight = len(fgLines)
  57. }
  58. if fgWidth >= bgWidth && fgHeight >= bgHeight {
  59. // FIXME: return fg or bg?
  60. return fg
  61. }
  62. // TODO: allow placement outside of the bg box?
  63. x = util.Clamp(x, 0, bgWidth-fgWidth)
  64. y = util.Clamp(y, 0, bgHeight-fgHeight)
  65. ws := &whitespace{}
  66. for _, opt := range opts {
  67. opt(ws)
  68. }
  69. var b strings.Builder
  70. for i, bgLine := range bgLines {
  71. if i > 0 {
  72. b.WriteByte('\n')
  73. }
  74. if i < y || i >= y+fgHeight {
  75. b.WriteString(bgLine)
  76. continue
  77. }
  78. pos := 0
  79. if x > 0 {
  80. left := truncate.String(bgLine, uint(x))
  81. pos = ansi.PrintableRuneWidth(left)
  82. b.WriteString(left)
  83. if pos < x {
  84. b.WriteString(ws.render(x - pos))
  85. pos = x
  86. }
  87. }
  88. fgLine := fgLines[i-y]
  89. b.WriteString(fgLine)
  90. pos += ansi.PrintableRuneWidth(fgLine)
  91. right := cutLeft(bgLine, pos)
  92. bgWidth := ansi.PrintableRuneWidth(bgLine)
  93. rightWidth := ansi.PrintableRuneWidth(right)
  94. if rightWidth <= bgWidth-pos {
  95. b.WriteString(ws.render(bgWidth - rightWidth - pos))
  96. }
  97. b.WriteString(right)
  98. }
  99. return b.String()
  100. }
  101. // cutLeft cuts printable characters from the left.
  102. // This function is heavily based on muesli's ansi and truncate packages.
  103. func cutLeft(s string, cutWidth int) string {
  104. return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
  105. }
  106. func max(a, b int) int {
  107. if a > b {
  108. return a
  109. }
  110. return b
  111. }
  112. type whitespace struct {
  113. style termenv.Style
  114. chars string
  115. }
  116. // Render whitespaces.
  117. func (w whitespace) render(width int) string {
  118. if w.chars == "" {
  119. w.chars = " "
  120. }
  121. r := []rune(w.chars)
  122. j := 0
  123. b := strings.Builder{}
  124. // Cycle through runes and print them into the whitespace.
  125. for i := 0; i < width; {
  126. b.WriteRune(r[j])
  127. j++
  128. if j >= len(r) {
  129. j = 0
  130. }
  131. i += ansi.PrintableRuneWidth(string(r[j]))
  132. }
  133. // Fill any extra gaps white spaces. This might be necessary if any runes
  134. // are more than one cell wide, which could leave a one-rune gap.
  135. short := width - ansi.PrintableRuneWidth(b.String())
  136. if short > 0 {
  137. b.WriteString(strings.Repeat(" ", short))
  138. }
  139. return w.style.Styled(b.String())
  140. }
  141. // WhitespaceOption sets a styling rule for rendering whitespace.
  142. type WhitespaceOption func(*whitespace)