position.go

  1package lipgloss
  2
  3import (
  4	"math"
  5	"strings"
  6
  7	"github.com/charmbracelet/x/ansi"
  8)
  9
 10// Position represents a position along a horizontal or vertical axis. It's in
 11// situations where an axis is involved, like alignment, joining, placement and
 12// so on.
 13//
 14// A value of 0 represents the start (the left or top) and 1 represents the end
 15// (the right or bottom). 0.5 represents the center.
 16//
 17// There are constants Top, Bottom, Center, Left and Right in this package that
 18// can be used to aid readability.
 19type Position float64
 20
 21func (p Position) value() float64 {
 22	return math.Min(1, math.Max(0, float64(p)))
 23}
 24
 25// Position aliases.
 26const (
 27	Top    Position = 0.0
 28	Bottom Position = 1.0
 29	Center Position = 0.5
 30	Left   Position = 0.0
 31	Right  Position = 1.0
 32)
 33
 34// Place places a string or text block vertically in an unstyled box of a given
 35// width or height.
 36func Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string {
 37	return PlaceVertical(height, vPos, PlaceHorizontal(width, hPos, str, opts...), opts...)
 38}
 39
 40// PlaceHorizontal places a string or text block horizontally in an unstyled
 41// block of a given width. If the given width is shorter than the max width of
 42// the string (measured by its longest line) this will be a noop.
 43func PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string {
 44	lines, contentWidth := getLines(str)
 45	gap := width - contentWidth
 46
 47	if gap <= 0 {
 48		return str
 49	}
 50
 51	ws := newWhitespace(opts...)
 52
 53	var b strings.Builder
 54	for i, l := range lines {
 55		// Is this line shorter than the longest line?
 56		short := max(0, contentWidth-ansi.StringWidth(l))
 57
 58		switch pos { //nolint:exhaustive
 59		case Left:
 60			b.WriteString(l)
 61			b.WriteString(ws.render(gap + short))
 62
 63		case Right:
 64			b.WriteString(ws.render(gap + short))
 65			b.WriteString(l)
 66
 67		default: // somewhere in the middle
 68			totalGap := gap + short
 69
 70			split := int(math.Round(float64(totalGap) * pos.value()))
 71			left := totalGap - split
 72			right := totalGap - left
 73
 74			b.WriteString(ws.render(left))
 75			b.WriteString(l)
 76			b.WriteString(ws.render(right))
 77		}
 78
 79		if i < len(lines)-1 {
 80			b.WriteRune('\n')
 81		}
 82	}
 83
 84	return b.String()
 85}
 86
 87// PlaceVertical places a string or text block vertically in an unstyled block
 88// of a given height. If the given height is shorter than the height of the
 89// string (measured by its newlines) then this will be a noop.
 90func PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOption) string {
 91	contentHeight := strings.Count(str, "\n") + 1
 92	gap := height - contentHeight
 93
 94	if gap <= 0 {
 95		return str
 96	}
 97
 98	ws := newWhitespace(opts...)
 99
100	_, width := getLines(str)
101	emptyLine := ws.render(width)
102	b := strings.Builder{}
103
104	switch pos { //nolint:exhaustive
105	case Top:
106		b.WriteString(str)
107		b.WriteRune('\n')
108		for i := range gap {
109			b.WriteString(emptyLine)
110			if i < gap-1 {
111				b.WriteRune('\n')
112			}
113		}
114
115	case Bottom:
116		b.WriteString(strings.Repeat(emptyLine+"\n", gap))
117		b.WriteString(str)
118
119	default: // Somewhere in the middle
120		split := int(math.Round(float64(gap) * pos.value()))
121		top := gap - split
122		bottom := gap - top
123
124		b.WriteString(strings.Repeat(emptyLine+"\n", top))
125		b.WriteString(str)
126
127		for range bottom {
128			b.WriteRune('\n')
129			b.WriteString(emptyLine)
130		}
131	}
132
133	return b.String()
134}