join.go

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