join.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. package lipgloss
  2. import (
  3. "math"
  4. "strings"
  5. "github.com/muesli/reflow/ansi"
  6. )
  7. // JoinHorizontal is a utility function for horizontally joining two
  8. // potentially multi-lined strings along a vertical axis. The first argument is
  9. // the position, with 0 being all the way at the top and 1 being all the way
  10. // at the bottom.
  11. //
  12. // If you just want to align to the left, right or center you may as well just
  13. // use the helper constants Top, Center, and Bottom.
  14. //
  15. // Example:
  16. //
  17. // blockB := "...\n...\n..."
  18. // blockA := "...\n...\n...\n...\n..."
  19. //
  20. // // Join 20% from the top
  21. // str := lipgloss.JoinHorizontal(0.2, blockA, blockB)
  22. //
  23. // // Join on the top edge
  24. // str := lipgloss.JoinHorizontal(lipgloss.Top, blockA, blockB)
  25. func JoinHorizontal(pos Position, strs ...string) string {
  26. if len(strs) == 0 {
  27. return ""
  28. }
  29. if len(strs) == 1 {
  30. return strs[0]
  31. }
  32. var (
  33. // Groups of strings broken into multiple lines
  34. blocks = make([][]string, len(strs))
  35. // Max line widths for the above text blocks
  36. maxWidths = make([]int, len(strs))
  37. // Height of the tallest block
  38. maxHeight int
  39. )
  40. // Break text blocks into lines and get max widths for each text block
  41. for i, str := range strs {
  42. blocks[i], maxWidths[i] = getLines(str)
  43. if len(blocks[i]) > maxHeight {
  44. maxHeight = len(blocks[i])
  45. }
  46. }
  47. // Add extra lines to make each side the same height
  48. for i := range blocks {
  49. if len(blocks[i]) >= maxHeight {
  50. continue
  51. }
  52. extraLines := make([]string, maxHeight-len(blocks[i]))
  53. switch pos {
  54. case Top:
  55. blocks[i] = append(blocks[i], extraLines...)
  56. case Bottom:
  57. blocks[i] = append(extraLines, blocks[i]...)
  58. default: // Somewhere in the middle
  59. n := len(extraLines)
  60. split := int(math.Round(float64(n) * pos.value()))
  61. top := n - split
  62. bottom := n - top
  63. blocks[i] = append(extraLines[top:], blocks[i]...)
  64. blocks[i] = append(blocks[i], extraLines[bottom:]...)
  65. }
  66. }
  67. // Merge lines
  68. var b strings.Builder
  69. for i := range blocks[0] { // remember, all blocks have the same number of members now
  70. for j, block := range blocks {
  71. b.WriteString(block[i])
  72. // Also make lines the same length
  73. b.WriteString(strings.Repeat(" ", maxWidths[j]-ansi.PrintableRuneWidth(block[i])))
  74. }
  75. if i < len(blocks[0])-1 {
  76. b.WriteRune('\n')
  77. }
  78. }
  79. return b.String()
  80. }
  81. // JoinVertical is a utility function for vertically joining two potentially
  82. // multi-lined strings along a horizontal axis. The first argument is the
  83. // position, with 0 being all the way to the left and 1 being all the way to
  84. // the right.
  85. //
  86. // If you just want to align to the left, right or center you may as well just
  87. // use the helper constants Left, Center, and Right.
  88. //
  89. // Example:
  90. //
  91. // blockB := "...\n...\n..."
  92. // blockA := "...\n...\n...\n...\n..."
  93. //
  94. // // Join 20% from the top
  95. // str := lipgloss.JoinVertical(0.2, blockA, blockB)
  96. //
  97. // // Join on the right edge
  98. // str := lipgloss.JoinVertical(lipgloss.Right, blockA, blockB)
  99. func JoinVertical(pos Position, strs ...string) string {
  100. if len(strs) == 0 {
  101. return ""
  102. }
  103. if len(strs) == 1 {
  104. return strs[0]
  105. }
  106. var (
  107. blocks = make([][]string, len(strs))
  108. maxWidth int
  109. )
  110. for i := range strs {
  111. var w int
  112. blocks[i], w = getLines(strs[i])
  113. if w > maxWidth {
  114. maxWidth = w
  115. }
  116. }
  117. var b strings.Builder
  118. for i, block := range blocks {
  119. for j, line := range block {
  120. w := maxWidth - ansi.PrintableRuneWidth(line)
  121. switch pos {
  122. case Left:
  123. b.WriteString(line)
  124. b.WriteString(strings.Repeat(" ", w))
  125. case Right:
  126. b.WriteString(strings.Repeat(" ", w))
  127. b.WriteString(line)
  128. default: // Somewhere in the middle
  129. if w < 1 {
  130. b.WriteString(line)
  131. break
  132. }
  133. split := int(math.Round(float64(w) * pos.value()))
  134. right := w - split
  135. left := w - right
  136. b.WriteString(strings.Repeat(" ", left))
  137. b.WriteString(line)
  138. b.WriteString(strings.Repeat(" ", right))
  139. }
  140. // Write a newline as long as we're not on the last line of the
  141. // last block.
  142. if !(i == len(blocks)-1 && j == len(block)-1) {
  143. b.WriteRune('\n')
  144. }
  145. }
  146. }
  147. return b.String()
  148. }