mirror of
https://github.com/charmbracelet/lipgloss.git
synced 2024-08-17 18:00:46 +03:00
When a style has a border set and an empty string is rendered, it should still render the border. When the left border is set, width is incremented by 1, so in most cases the width will not be 0. When we render an empty string, with a double border and no left border we expect the following: ╗ ║ ╝ But if we don't render a horizontal edge when the string width is less than 1 we see this: ║ The string width can't be lower than 0, so we can safely remove the (width < 1) check to create the expected behavior. Co-authored-by: Daan Schoone <daaschoone@gmail.com>
444 lines
10 KiB
Go
444 lines
10 KiB
Go
package lipgloss
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/muesli/reflow/ansi"
|
|
"github.com/muesli/termenv"
|
|
"github.com/rivo/uniseg"
|
|
)
|
|
|
|
// Border contains a series of values which comprise the various parts of a
|
|
// border.
|
|
type Border struct {
|
|
Top string
|
|
Bottom string
|
|
Left string
|
|
Right string
|
|
TopLeft string
|
|
TopRight string
|
|
BottomLeft string
|
|
BottomRight string
|
|
MiddleLeft string
|
|
MiddleRight string
|
|
Middle string
|
|
MiddleTop string
|
|
MiddleBottom string
|
|
}
|
|
|
|
// GetTopSize returns the width of the top border. If borders contain runes of
|
|
// varying widths, the widest rune is returned. If no border exists on the top
|
|
// edge, 0 is returned.
|
|
func (b Border) GetTopSize() int {
|
|
return getBorderEdgeWidth(b.TopLeft, b.Top, b.TopRight)
|
|
}
|
|
|
|
// GetRightSize returns the width of the right border. If borders contain
|
|
// runes of varying widths, the widest rune is returned. If no border exists on
|
|
// the right edge, 0 is returned.
|
|
func (b Border) GetRightSize() int {
|
|
return getBorderEdgeWidth(b.TopRight, b.Right, b.BottomRight)
|
|
}
|
|
|
|
// GetBottomSize returns the width of the bottom border. If borders contain
|
|
// runes of varying widths, the widest rune is returned. If no border exists on
|
|
// the bottom edge, 0 is returned.
|
|
func (b Border) GetBottomSize() int {
|
|
return getBorderEdgeWidth(b.BottomLeft, b.Bottom, b.BottomRight)
|
|
}
|
|
|
|
// GetLeftSize returns the width of the left border. If borders contain runes
|
|
// of varying widths, the widest rune is returned. If no border exists on the
|
|
// left edge, 0 is returned.
|
|
func (b Border) GetLeftSize() int {
|
|
return getBorderEdgeWidth(b.TopLeft, b.Left, b.BottomLeft)
|
|
}
|
|
|
|
func getBorderEdgeWidth(borderParts ...string) (maxWidth int) {
|
|
for _, piece := range borderParts {
|
|
w := maxRuneWidth(piece)
|
|
if w > maxWidth {
|
|
maxWidth = w
|
|
}
|
|
}
|
|
return maxWidth
|
|
}
|
|
|
|
var (
|
|
noBorder = Border{}
|
|
|
|
normalBorder = Border{
|
|
Top: "─",
|
|
Bottom: "─",
|
|
Left: "│",
|
|
Right: "│",
|
|
TopLeft: "┌",
|
|
TopRight: "┐",
|
|
BottomLeft: "└",
|
|
BottomRight: "┘",
|
|
MiddleLeft: "├",
|
|
MiddleRight: "┤",
|
|
Middle: "┼",
|
|
MiddleTop: "┬",
|
|
MiddleBottom: "┴",
|
|
}
|
|
|
|
roundedBorder = Border{
|
|
Top: "─",
|
|
Bottom: "─",
|
|
Left: "│",
|
|
Right: "│",
|
|
TopLeft: "╭",
|
|
TopRight: "╮",
|
|
BottomLeft: "╰",
|
|
BottomRight: "╯",
|
|
MiddleLeft: "├",
|
|
MiddleRight: "┤",
|
|
Middle: "┼",
|
|
MiddleTop: "┬",
|
|
MiddleBottom: "┴",
|
|
}
|
|
|
|
blockBorder = Border{
|
|
Top: "█",
|
|
Bottom: "█",
|
|
Left: "█",
|
|
Right: "█",
|
|
TopLeft: "█",
|
|
TopRight: "█",
|
|
BottomLeft: "█",
|
|
BottomRight: "█",
|
|
}
|
|
|
|
outerHalfBlockBorder = Border{
|
|
Top: "▀",
|
|
Bottom: "▄",
|
|
Left: "▌",
|
|
Right: "▐",
|
|
TopLeft: "▛",
|
|
TopRight: "▜",
|
|
BottomLeft: "▙",
|
|
BottomRight: "▟",
|
|
}
|
|
|
|
innerHalfBlockBorder = Border{
|
|
Top: "▄",
|
|
Bottom: "▀",
|
|
Left: "▐",
|
|
Right: "▌",
|
|
TopLeft: "▗",
|
|
TopRight: "▖",
|
|
BottomLeft: "▝",
|
|
BottomRight: "▘",
|
|
}
|
|
|
|
thickBorder = Border{
|
|
Top: "━",
|
|
Bottom: "━",
|
|
Left: "┃",
|
|
Right: "┃",
|
|
TopLeft: "┏",
|
|
TopRight: "┓",
|
|
BottomLeft: "┗",
|
|
BottomRight: "┛",
|
|
MiddleLeft: "┣",
|
|
MiddleRight: "┫",
|
|
Middle: "╋",
|
|
MiddleTop: "┳",
|
|
MiddleBottom: "┻",
|
|
}
|
|
|
|
doubleBorder = Border{
|
|
Top: "═",
|
|
Bottom: "═",
|
|
Left: "║",
|
|
Right: "║",
|
|
TopLeft: "╔",
|
|
TopRight: "╗",
|
|
BottomLeft: "╚",
|
|
BottomRight: "╝",
|
|
MiddleLeft: "╠",
|
|
MiddleRight: "╣",
|
|
Middle: "╬",
|
|
MiddleTop: "╦",
|
|
MiddleBottom: "╩",
|
|
}
|
|
|
|
hiddenBorder = Border{
|
|
Top: " ",
|
|
Bottom: " ",
|
|
Left: " ",
|
|
Right: " ",
|
|
TopLeft: " ",
|
|
TopRight: " ",
|
|
BottomLeft: " ",
|
|
BottomRight: " ",
|
|
MiddleLeft: " ",
|
|
MiddleRight: " ",
|
|
Middle: " ",
|
|
MiddleTop: " ",
|
|
MiddleBottom: " ",
|
|
}
|
|
)
|
|
|
|
// NormalBorder returns a standard-type border with a normal weight and 90
|
|
// degree corners.
|
|
func NormalBorder() Border {
|
|
return normalBorder
|
|
}
|
|
|
|
// RoundedBorder returns a border with rounded corners.
|
|
func RoundedBorder() Border {
|
|
return roundedBorder
|
|
}
|
|
|
|
// BlockBorder returns a border that takes the whole block.
|
|
func BlockBorder() Border {
|
|
return blockBorder
|
|
}
|
|
|
|
// OuterHalfBlockBorder returns a half-block border that sits outside the frame.
|
|
func OuterHalfBlockBorder() Border {
|
|
return outerHalfBlockBorder
|
|
}
|
|
|
|
// InnerHalfBlockBorder returns a half-block border that sits inside the frame.
|
|
func InnerHalfBlockBorder() Border {
|
|
return innerHalfBlockBorder
|
|
}
|
|
|
|
// ThickBorder returns a border that's thicker than the one returned by
|
|
// NormalBorder.
|
|
func ThickBorder() Border {
|
|
return thickBorder
|
|
}
|
|
|
|
// DoubleBorder returns a border comprised of two thin strokes.
|
|
func DoubleBorder() Border {
|
|
return doubleBorder
|
|
}
|
|
|
|
// HiddenBorder returns a border that renders as a series of single-cell
|
|
// spaces. It's useful for cases when you want to remove a standard border but
|
|
// maintain layout positioning. This said, you can still apply a background
|
|
// color to a hidden border.
|
|
func HiddenBorder() Border {
|
|
return hiddenBorder
|
|
}
|
|
|
|
func (s Style) applyBorder(str string) string {
|
|
var (
|
|
topSet = s.isSet(borderTopKey)
|
|
rightSet = s.isSet(borderRightKey)
|
|
bottomSet = s.isSet(borderBottomKey)
|
|
leftSet = s.isSet(borderLeftKey)
|
|
|
|
border = s.getBorderStyle()
|
|
hasTop = s.getAsBool(borderTopKey, false)
|
|
hasRight = s.getAsBool(borderRightKey, false)
|
|
hasBottom = s.getAsBool(borderBottomKey, false)
|
|
hasLeft = s.getAsBool(borderLeftKey, false)
|
|
|
|
topFG = s.getAsColor(borderTopForegroundKey)
|
|
rightFG = s.getAsColor(borderRightForegroundKey)
|
|
bottomFG = s.getAsColor(borderBottomForegroundKey)
|
|
leftFG = s.getAsColor(borderLeftForegroundKey)
|
|
|
|
topBG = s.getAsColor(borderTopBackgroundKey)
|
|
rightBG = s.getAsColor(borderRightBackgroundKey)
|
|
bottomBG = s.getAsColor(borderBottomBackgroundKey)
|
|
leftBG = s.getAsColor(borderLeftBackgroundKey)
|
|
)
|
|
|
|
// If a border is set and no sides have been specifically turned on or off
|
|
// render borders on all sides.
|
|
if border != noBorder && !(topSet || rightSet || bottomSet || leftSet) {
|
|
hasTop = true
|
|
hasRight = true
|
|
hasBottom = true
|
|
hasLeft = true
|
|
}
|
|
|
|
// If no border is set or all borders are been disabled, abort.
|
|
if border == noBorder || (!hasTop && !hasRight && !hasBottom && !hasLeft) {
|
|
return str
|
|
}
|
|
|
|
lines, width := getLines(str)
|
|
|
|
if hasLeft {
|
|
if border.Left == "" {
|
|
border.Left = " "
|
|
}
|
|
width += maxRuneWidth(border.Left)
|
|
}
|
|
|
|
if hasRight && border.Right == "" {
|
|
border.Right = " "
|
|
}
|
|
|
|
// If corners should be rendered but are set with the empty string, fill them
|
|
// with a single space.
|
|
if hasTop && hasLeft && border.TopLeft == "" {
|
|
border.TopLeft = " "
|
|
}
|
|
if hasTop && hasRight && border.TopRight == "" {
|
|
border.TopRight = " "
|
|
}
|
|
if hasBottom && hasLeft && border.BottomLeft == "" {
|
|
border.BottomLeft = " "
|
|
}
|
|
if hasBottom && hasRight && border.BottomRight == "" {
|
|
border.BottomRight = " "
|
|
}
|
|
|
|
// Figure out which corners we should actually be using based on which
|
|
// sides are set to show.
|
|
if hasTop {
|
|
switch {
|
|
case !hasLeft && !hasRight:
|
|
border.TopLeft = ""
|
|
border.TopRight = ""
|
|
case !hasLeft:
|
|
border.TopLeft = ""
|
|
case !hasRight:
|
|
border.TopRight = ""
|
|
}
|
|
}
|
|
if hasBottom {
|
|
switch {
|
|
case !hasLeft && !hasRight:
|
|
border.BottomLeft = ""
|
|
border.BottomRight = ""
|
|
case !hasLeft:
|
|
border.BottomLeft = ""
|
|
case !hasRight:
|
|
border.BottomRight = ""
|
|
}
|
|
}
|
|
|
|
// For now, limit corners to one rune.
|
|
border.TopLeft = getFirstRuneAsString(border.TopLeft)
|
|
border.TopRight = getFirstRuneAsString(border.TopRight)
|
|
border.BottomRight = getFirstRuneAsString(border.BottomRight)
|
|
border.BottomLeft = getFirstRuneAsString(border.BottomLeft)
|
|
|
|
var out strings.Builder
|
|
|
|
// Render top
|
|
if hasTop {
|
|
top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
|
|
top = s.styleBorder(top, topFG, topBG)
|
|
out.WriteString(top)
|
|
out.WriteRune('\n')
|
|
}
|
|
|
|
leftRunes := []rune(border.Left)
|
|
leftIndex := 0
|
|
|
|
rightRunes := []rune(border.Right)
|
|
rightIndex := 0
|
|
|
|
// Render sides
|
|
for i, l := range lines {
|
|
if hasLeft {
|
|
r := string(leftRunes[leftIndex])
|
|
leftIndex++
|
|
if leftIndex >= len(leftRunes) {
|
|
leftIndex = 0
|
|
}
|
|
out.WriteString(s.styleBorder(r, leftFG, leftBG))
|
|
}
|
|
out.WriteString(l)
|
|
if hasRight {
|
|
r := string(rightRunes[rightIndex])
|
|
rightIndex++
|
|
if rightIndex >= len(rightRunes) {
|
|
rightIndex = 0
|
|
}
|
|
out.WriteString(s.styleBorder(r, rightFG, rightBG))
|
|
}
|
|
if i < len(lines)-1 {
|
|
out.WriteRune('\n')
|
|
}
|
|
}
|
|
|
|
// Render bottom
|
|
if hasBottom {
|
|
bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width)
|
|
bottom = s.styleBorder(bottom, bottomFG, bottomBG)
|
|
out.WriteRune('\n')
|
|
out.WriteString(bottom)
|
|
}
|
|
|
|
return out.String()
|
|
}
|
|
|
|
// Render the horizontal (top or bottom) portion of a border.
|
|
func renderHorizontalEdge(left, middle, right string, width int) string {
|
|
if middle == "" {
|
|
middle = " "
|
|
}
|
|
|
|
leftWidth := ansi.PrintableRuneWidth(left)
|
|
rightWidth := ansi.PrintableRuneWidth(right)
|
|
|
|
runes := []rune(middle)
|
|
j := 0
|
|
|
|
out := strings.Builder{}
|
|
out.WriteString(left)
|
|
for i := leftWidth + rightWidth; i < width+rightWidth; {
|
|
out.WriteRune(runes[j])
|
|
j++
|
|
if j >= len(runes) {
|
|
j = 0
|
|
}
|
|
i += ansi.PrintableRuneWidth(string(runes[j]))
|
|
}
|
|
out.WriteString(right)
|
|
|
|
return out.String()
|
|
}
|
|
|
|
// Apply foreground and background styling to a border.
|
|
func (s Style) styleBorder(border string, fg, bg TerminalColor) string {
|
|
if fg == noColor && bg == noColor {
|
|
return border
|
|
}
|
|
|
|
style := termenv.Style{}
|
|
|
|
if fg != noColor {
|
|
style = style.Foreground(fg.color(s.r))
|
|
}
|
|
if bg != noColor {
|
|
style = style.Background(bg.color(s.r))
|
|
}
|
|
|
|
return style.Styled(border)
|
|
}
|
|
|
|
func maxRuneWidth(str string) int {
|
|
var width int
|
|
|
|
state := -1
|
|
for len(str) > 0 {
|
|
var w int
|
|
_, str, w, state = uniseg.FirstGraphemeClusterInString(str, state)
|
|
if w > width {
|
|
width = w
|
|
}
|
|
}
|
|
|
|
return width
|
|
}
|
|
|
|
func getFirstRuneAsString(str string) string {
|
|
if str == "" {
|
|
return str
|
|
}
|
|
r := []rune(str)
|
|
return string(r[0])
|
|
}
|