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}