borders.go

  1package lipgloss
  2
  3import (
  4	"image/color"
  5	"strings"
  6
  7	"github.com/charmbracelet/x/ansi"
  8	"github.com/rivo/uniseg"
  9)
 10
 11// Border contains a series of values which comprise the various parts of a
 12// border.
 13type Border struct {
 14	Top          string
 15	Bottom       string
 16	Left         string
 17	Right        string
 18	TopLeft      string
 19	TopRight     string
 20	BottomLeft   string
 21	BottomRight  string
 22	MiddleLeft   string
 23	MiddleRight  string
 24	Middle       string
 25	MiddleTop    string
 26	MiddleBottom string
 27}
 28
 29// GetTopSize returns the width of the top border. If borders contain runes of
 30// varying widths, the widest rune is returned. If no border exists on the top
 31// edge, 0 is returned.
 32func (b Border) GetTopSize() int {
 33	return getBorderEdgeWidth(b.TopLeft, b.Top, b.TopRight)
 34}
 35
 36// GetRightSize returns the width of the right border. If borders contain
 37// runes of varying widths, the widest rune is returned. If no border exists on
 38// the right edge, 0 is returned.
 39func (b Border) GetRightSize() int {
 40	return getBorderEdgeWidth(b.TopRight, b.Right, b.BottomRight)
 41}
 42
 43// GetBottomSize returns the width of the bottom border. If borders contain
 44// runes of varying widths, the widest rune is returned. If no border exists on
 45// the bottom edge, 0 is returned.
 46func (b Border) GetBottomSize() int {
 47	return getBorderEdgeWidth(b.BottomLeft, b.Bottom, b.BottomRight)
 48}
 49
 50// GetLeftSize returns the width of the left border. If borders contain runes
 51// of varying widths, the widest rune is returned. If no border exists on the
 52// left edge, 0 is returned.
 53func (b Border) GetLeftSize() int {
 54	return getBorderEdgeWidth(b.TopLeft, b.Left, b.BottomLeft)
 55}
 56
 57func getBorderEdgeWidth(borderParts ...string) (maxWidth int) {
 58	for _, piece := range borderParts {
 59		w := maxRuneWidth(piece)
 60		if w > maxWidth {
 61			maxWidth = w
 62		}
 63	}
 64	return maxWidth
 65}
 66
 67var (
 68	noBorder = Border{}
 69
 70	normalBorder = Border{
 71		Top:          "─",
 72		Bottom:       "─",
 73		Left:         "│",
 74		Right:        "│",
 75		TopLeft:      "┌",
 76		TopRight:     "┐",
 77		BottomLeft:   "└",
 78		BottomRight:  "┘",
 79		MiddleLeft:   "├",
 80		MiddleRight:  "┤",
 81		Middle:       "┼",
 82		MiddleTop:    "┬",
 83		MiddleBottom: "┴",
 84	}
 85
 86	roundedBorder = Border{
 87		Top:          "─",
 88		Bottom:       "─",
 89		Left:         "│",
 90		Right:        "│",
 91		TopLeft:      "╭",
 92		TopRight:     "╮",
 93		BottomLeft:   "╰",
 94		BottomRight:  "╯",
 95		MiddleLeft:   "├",
 96		MiddleRight:  "┤",
 97		Middle:       "┼",
 98		MiddleTop:    "┬",
 99		MiddleBottom: "┴",
100	}
101
102	blockBorder = Border{
103		Top:          "█",
104		Bottom:       "█",
105		Left:         "█",
106		Right:        "█",
107		TopLeft:      "█",
108		TopRight:     "█",
109		BottomLeft:   "█",
110		BottomRight:  "█",
111		MiddleLeft:   "█",
112		MiddleRight:  "█",
113		Middle:       "█",
114		MiddleTop:    "█",
115		MiddleBottom: "█",
116	}
117
118	outerHalfBlockBorder = Border{
119		Top:         "▀",
120		Bottom:      "▄",
121		Left:        "▌",
122		Right:       "▐",
123		TopLeft:     "▛",
124		TopRight:    "▜",
125		BottomLeft:  "▙",
126		BottomRight: "▟",
127	}
128
129	innerHalfBlockBorder = Border{
130		Top:         "▄",
131		Bottom:      "▀",
132		Left:        "▐",
133		Right:       "▌",
134		TopLeft:     "▗",
135		TopRight:    "▖",
136		BottomLeft:  "▝",
137		BottomRight: "▘",
138	}
139
140	thickBorder = Border{
141		Top:          "━",
142		Bottom:       "━",
143		Left:         "┃",
144		Right:        "┃",
145		TopLeft:      "┏",
146		TopRight:     "┓",
147		BottomLeft:   "┗",
148		BottomRight:  "┛",
149		MiddleLeft:   "┣",
150		MiddleRight:  "┫",
151		Middle:       "╋",
152		MiddleTop:    "┳",
153		MiddleBottom: "┻",
154	}
155
156	doubleBorder = Border{
157		Top:          "═",
158		Bottom:       "═",
159		Left:         "║",
160		Right:        "║",
161		TopLeft:      "╔",
162		TopRight:     "╗",
163		BottomLeft:   "╚",
164		BottomRight:  "╝",
165		MiddleLeft:   "╠",
166		MiddleRight:  "╣",
167		Middle:       "╬",
168		MiddleTop:    "╦",
169		MiddleBottom: "╩",
170	}
171
172	hiddenBorder = Border{
173		Top:          " ",
174		Bottom:       " ",
175		Left:         " ",
176		Right:        " ",
177		TopLeft:      " ",
178		TopRight:     " ",
179		BottomLeft:   " ",
180		BottomRight:  " ",
181		MiddleLeft:   " ",
182		MiddleRight:  " ",
183		Middle:       " ",
184		MiddleTop:    " ",
185		MiddleBottom: " ",
186	}
187
188	markdownBorder = Border{
189		Top:          "-",
190		Bottom:       "-",
191		Left:         "|",
192		Right:        "|",
193		TopLeft:      "|",
194		TopRight:     "|",
195		BottomLeft:   "|",
196		BottomRight:  "|",
197		MiddleLeft:   "|",
198		MiddleRight:  "|",
199		Middle:       "|",
200		MiddleTop:    "|",
201		MiddleBottom: "|",
202	}
203
204	asciiBorder = Border{
205		Top:          "-",
206		Bottom:       "-",
207		Left:         "|",
208		Right:        "|",
209		TopLeft:      "+",
210		TopRight:     "+",
211		BottomLeft:   "+",
212		BottomRight:  "+",
213		MiddleLeft:   "+",
214		MiddleRight:  "+",
215		Middle:       "+",
216		MiddleTop:    "+",
217		MiddleBottom: "+",
218	}
219)
220
221// NormalBorder returns a standard-type border with a normal weight and 90
222// degree corners.
223func NormalBorder() Border {
224	return normalBorder
225}
226
227// RoundedBorder returns a border with rounded corners.
228func RoundedBorder() Border {
229	return roundedBorder
230}
231
232// BlockBorder returns a border that takes the whole block.
233func BlockBorder() Border {
234	return blockBorder
235}
236
237// OuterHalfBlockBorder returns a half-block border that sits outside the frame.
238func OuterHalfBlockBorder() Border {
239	return outerHalfBlockBorder
240}
241
242// InnerHalfBlockBorder returns a half-block border that sits inside the frame.
243func InnerHalfBlockBorder() Border {
244	return innerHalfBlockBorder
245}
246
247// ThickBorder returns a border that's thicker than the one returned by
248// NormalBorder.
249func ThickBorder() Border {
250	return thickBorder
251}
252
253// DoubleBorder returns a border comprised of two thin strokes.
254func DoubleBorder() Border {
255	return doubleBorder
256}
257
258// HiddenBorder returns a border that renders as a series of single-cell
259// spaces. It's useful for cases when you want to remove a standard border but
260// maintain layout positioning. This said, you can still apply a background
261// color to a hidden border.
262func HiddenBorder() Border {
263	return hiddenBorder
264}
265
266// MarkdownBorder return a table border in markdown style.
267//
268// Make sure to disable top and bottom border for the best result. This will
269// ensure that the output is valid markdown.
270//
271//	table.New().Border(lipgloss.MarkdownBorder()).BorderTop(false).BorderBottom(false)
272func MarkdownBorder() Border {
273	return markdownBorder
274}
275
276// ASCIIBorder returns a table border with ASCII characters.
277func ASCIIBorder() Border {
278	return asciiBorder
279}
280
281func (s Style) applyBorder(str string) string {
282	var (
283		border    = s.getBorderStyle()
284		hasTop    = s.getAsBool(borderTopKey, false)
285		hasRight  = s.getAsBool(borderRightKey, false)
286		hasBottom = s.getAsBool(borderBottomKey, false)
287		hasLeft   = s.getAsBool(borderLeftKey, false)
288
289		topFG    = s.getAsColor(borderTopForegroundKey)
290		rightFG  = s.getAsColor(borderRightForegroundKey)
291		bottomFG = s.getAsColor(borderBottomForegroundKey)
292		leftFG   = s.getAsColor(borderLeftForegroundKey)
293
294		topBG    = s.getAsColor(borderTopBackgroundKey)
295		rightBG  = s.getAsColor(borderRightBackgroundKey)
296		bottomBG = s.getAsColor(borderBottomBackgroundKey)
297		leftBG   = s.getAsColor(borderLeftBackgroundKey)
298	)
299
300	// If a border is set and no sides have been specifically turned on or off
301	// render borders on all sides.
302	if s.isBorderStyleSetWithoutSides() {
303		hasTop = true
304		hasRight = true
305		hasBottom = true
306		hasLeft = true
307	}
308
309	// If no border is set or all borders are been disabled, abort.
310	if border == noBorder || (!hasTop && !hasRight && !hasBottom && !hasLeft) {
311		return str
312	}
313
314	lines, width := getLines(str)
315
316	if hasLeft {
317		if border.Left == "" {
318			border.Left = " "
319		}
320		width += maxRuneWidth(border.Left)
321	}
322
323	if hasRight && border.Right == "" {
324		border.Right = " "
325	}
326
327	// If corners should be rendered but are set with the empty string, fill them
328	// with a single space.
329	if hasTop && hasLeft && border.TopLeft == "" {
330		border.TopLeft = " "
331	}
332	if hasTop && hasRight && border.TopRight == "" {
333		border.TopRight = " "
334	}
335	if hasBottom && hasLeft && border.BottomLeft == "" {
336		border.BottomLeft = " "
337	}
338	if hasBottom && hasRight && border.BottomRight == "" {
339		border.BottomRight = " "
340	}
341
342	// Figure out which corners we should actually be using based on which
343	// sides are set to show.
344	if hasTop {
345		switch {
346		case !hasLeft && !hasRight:
347			border.TopLeft = ""
348			border.TopRight = ""
349		case !hasLeft:
350			border.TopLeft = ""
351		case !hasRight:
352			border.TopRight = ""
353		}
354	}
355	if hasBottom {
356		switch {
357		case !hasLeft && !hasRight:
358			border.BottomLeft = ""
359			border.BottomRight = ""
360		case !hasLeft:
361			border.BottomLeft = ""
362		case !hasRight:
363			border.BottomRight = ""
364		}
365	}
366
367	// For now, limit corners to one rune.
368	border.TopLeft = getFirstRuneAsString(border.TopLeft)
369	border.TopRight = getFirstRuneAsString(border.TopRight)
370	border.BottomRight = getFirstRuneAsString(border.BottomRight)
371	border.BottomLeft = getFirstRuneAsString(border.BottomLeft)
372
373	var out strings.Builder
374
375	// Render top
376	if hasTop {
377		top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
378		top = s.styleBorder(top, topFG, topBG)
379		out.WriteString(top)
380		out.WriteRune('\n')
381	}
382
383	leftRunes := []rune(border.Left)
384	leftIndex := 0
385
386	rightRunes := []rune(border.Right)
387	rightIndex := 0
388
389	// Render sides
390	for i, l := range lines {
391		if hasLeft {
392			r := string(leftRunes[leftIndex])
393			leftIndex++
394			if leftIndex >= len(leftRunes) {
395				leftIndex = 0
396			}
397			out.WriteString(s.styleBorder(r, leftFG, leftBG))
398		}
399		out.WriteString(l)
400		if hasRight {
401			r := string(rightRunes[rightIndex])
402			rightIndex++
403			if rightIndex >= len(rightRunes) {
404				rightIndex = 0
405			}
406			out.WriteString(s.styleBorder(r, rightFG, rightBG))
407		}
408		if i < len(lines)-1 {
409			out.WriteRune('\n')
410		}
411	}
412
413	// Render bottom
414	if hasBottom {
415		bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width)
416		bottom = s.styleBorder(bottom, bottomFG, bottomBG)
417		out.WriteRune('\n')
418		out.WriteString(bottom)
419	}
420
421	return out.String()
422}
423
424// Render the horizontal (top or bottom) portion of a border.
425func renderHorizontalEdge(left, middle, right string, width int) string {
426	if middle == "" {
427		middle = " "
428	}
429
430	leftWidth := ansi.StringWidth(left)
431	rightWidth := ansi.StringWidth(right)
432
433	runes := []rune(middle)
434	j := 0
435
436	out := strings.Builder{}
437	out.WriteString(left)
438	for i := leftWidth + rightWidth; i < width+rightWidth; {
439		out.WriteRune(runes[j])
440		j++
441		if j >= len(runes) {
442			j = 0
443		}
444		i += ansi.StringWidth(string(runes[j]))
445	}
446	out.WriteString(right)
447
448	return out.String()
449}
450
451// Apply foreground and background styling to a border.
452func (s Style) styleBorder(border string, fg, bg color.Color) string {
453	if fg == noColor && bg == noColor {
454		return border
455	}
456
457	var style ansi.Style
458	if fg != noColor {
459		style = style.ForegroundColor(fg)
460	}
461	if bg != noColor {
462		style = style.BackgroundColor(bg)
463	}
464
465	return style.Styled(border)
466}
467
468func maxRuneWidth(str string) int {
469	var width int
470
471	state := -1
472	for len(str) > 0 {
473		var w int
474		_, str, w, state = uniseg.FirstGraphemeClusterInString(str, state)
475		if w > width {
476			width = w
477		}
478	}
479
480	return width
481}
482
483func getFirstRuneAsString(str string) string {
484	if str == "" {
485		return str
486	}
487	r := []rune(str)
488	return string(r[0])
489}