overlay.go 4.0 KB

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