Skip to content

Commit

Permalink
feat: add border func to set text in borders
Browse files Browse the repository at this point in the history
this adds callback functions to set text in borders when they are generated.
  • Loading branch information
pachecot committed Oct 3, 2024
1 parent eea5201 commit a8709aa
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 2 deletions.
64 changes: 62 additions & 2 deletions borders.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type Border struct {
MiddleBottom string
}

type BorderFunc func(width int, middle string) string

Check failure on line 29 in borders.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported type BorderFunc should have comment or be unexported (revive)

// 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.
Expand Down Expand Up @@ -248,6 +250,9 @@ func (s Style) applyBorder(str string) string {
rightBG = s.getAsColor(borderRightBackgroundKey)
bottomBG = s.getAsColor(borderBottomBackgroundKey)
leftBG = s.getAsColor(borderLeftBackgroundKey)

topFuncs = s.borderTopFunc
bottomFuncs = s.borderBottomFunc
)

// If a border is set and no sides have been specifically turned on or off
Expand Down Expand Up @@ -327,7 +332,12 @@ func (s Style) applyBorder(str string) string {

// Render top
if hasTop {
top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
var top string
if len(topFuncs) > 0 {
top = renderAnnotatedHorizontalEdge(border.TopLeft, border.Top, border.TopRight, topFuncs, width)
} else {
top = renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
}
top = s.styleBorder(top, topFG, topBG)
out.WriteString(top)
out.WriteRune('\n')
Expand Down Expand Up @@ -365,7 +375,12 @@ func (s Style) applyBorder(str string) string {

// Render bottom
if hasBottom {
bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width)
var bottom string
if len(bottomFuncs) > 0 {
bottom = renderAnnotatedHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, bottomFuncs, width)
} else {
bottom = renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width)
}
bottom = s.styleBorder(bottom, bottomFG, bottomBG)
out.WriteRune('\n')
out.WriteString(bottom)
Expand All @@ -374,6 +389,51 @@ func (s Style) applyBorder(str string) string {
return out.String()
}

// Render the horizontal (top or bottom) portion of a border.
func renderAnnotatedHorizontalEdge(left, middle, right string, bFuncs []BorderFunc, width int) string {
if middle == "" {
middle = " "
}

ts := make([]string, 3)

Check failure on line 398 in borders.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 3, in <argument> detected (gomnd)
ws := make([]int, 3)

Check failure on line 399 in borders.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 3, in <argument> detected (gomnd)
for i, f := range bFuncs {
if f == nil {
continue
}
ts[i] = f(width, middle)
ws[i] = ansi.StringWidth(ts[i])
}

if width <= ws[0]+ws[1]+ws[2]+2 {

Check failure on line 408 in borders.go

View workflow job for this annotation

GitHub Actions / lint

empty-block: this block is empty, you can remove it (revive)
// TODO fix length if the strings are too long
}

runes := []rune(middle)
j := 0

out := strings.Builder{}
out.WriteString(left)
out.WriteString(ts[0])

for i := ws[0]; i < width-ws[2]-1; {
if ws[1] > 0 && i == (width-1-ws[1])/2 {
out.WriteString(ts[1])
i += ws[1]
}
out.WriteRune(runes[j])
j++
if j >= len(runes) {
j = 0
}
i += ansi.StringWidth(string(runes[j]))
}
out.WriteString(ts[2])
out.WriteString(right)

return out.String()
}

// Render the horizontal (top or bottom) portion of a border.
func renderHorizontalEdge(left, middle, right string, width int) string {
if middle == "" {
Expand Down
140 changes: 140 additions & 0 deletions borders_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package lipgloss

import "testing"

func TestBorderFunc(t *testing.T) {

tt := []struct {
name string
text string
style Style
expected string
}{
{
name: "top left title",
text: "",
style: NewStyle().Width(10).Border(NormalBorder()).BorderTopFunc(Left, func(width int, middle string) string {
return "TITLE"
}),
expected: `┌TITLE─────┐
│ │
└──────────┘`,
},
{
name: "top center title",
text: "",
style: NewStyle().Width(10).Border(NormalBorder()).BorderTopFunc(Center, func(width int, middle string) string {
return "TITLE"
}),
expected: `┌──TITLE───┐
│ │
└──────────┘`,
},
{
name: "top center title even",
text: "",
style: NewStyle().Width(11).Border(NormalBorder()).BorderTopFunc(Center, func(width int, middle string) string {
return "TITLE"
}),
expected: `┌───TITLE───┐
│ │
└───────────┘`,
},
{
name: "top right title",
text: "",
style: NewStyle().Width(10).Border(NormalBorder()).BorderTopFunc(Right, func(width int, middle string) string {
return "TITLE"
}),
expected: `┌─────TITLE┐
│ │
└──────────┘`,
},
{
name: "bottom left title",
text: "",
style: NewStyle().Width(10).Border(NormalBorder()).BorderBottomFunc(Left, func(width int, middle string) string {
return "STATUS"
}),
expected: `┌──────────┐
│ │
└STATUS────┘`,
},
{
name: "bottom center title",
text: "",
style: NewStyle().Width(10).Border(NormalBorder()).BorderBottomFunc(Center, func(width int, middle string) string {
return "STATUS"
}),
expected: `┌──────────┐
│ │
└──STATUS──┘`,
},
{
name: "bottom center title odd",
text: "",
style: NewStyle().Width(11).Border(NormalBorder()).BorderBottomFunc(Center, func(width int, middle string) string {
return "STATUS"
}),
expected: `┌───────────┐
│ │
└──STATUS───┘`,
},
{
name: "bottom right title",
text: "",
style: NewStyle().Width(10).Border(NormalBorder()).BorderBottomFunc(Right, func(width int, middle string) string {
return "STATUS"
}),
expected: `┌──────────┐
│ │
└────STATUS┘`,
},
}

for i, tc := range tt {
res := tc.style.Render(tc.text)
if res != tc.expected {
t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
i, tc.expected, formatEscapes(tc.expected),
res, formatEscapes(res))
}
}

}

func TestBorders(t *testing.T) {
tt := []struct {
name string
text string
style Style
expected string
}{
{
name: "border with width",
text: "",
style: NewStyle().Width(10).Border(NormalBorder()),
expected: `┌──────────┐
│ │
└──────────┘`,
},
{
name: "top center title",
text: "HELLO",
style: NewStyle().Border(NormalBorder()),
expected: `┌─────┐
│HELLO│
└─────┘`,
},
}

for i, tc := range tt {
res := tc.style.Render(tc.text)
if res != tc.expected {
t.Errorf("Test %d, expected:\n\n`%s`\n`%s`\n\nActual output:\n\n`%s`\n`%s`\n\n",
i, tc.expected, formatEscapes(tc.expected),
res, formatEscapes(res))
}
}

}
51 changes: 51 additions & 0 deletions set.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ func (s *Style) set(key propKey, value interface{}) {
s.borderBottomBgColor = colorOrNil(value)
case borderLeftBackgroundKey:
s.borderLeftBgColor = colorOrNil(value)
case borderTopFuncKey:
s.borderTopFunc = mergeBorderFunc(s.borderTopFunc, value.([]BorderFunc))
case borderBottomFuncKey:
s.borderBottomFunc = mergeBorderFunc(s.borderBottomFunc, value.([]BorderFunc))
case maxWidthKey:
s.maxWidth = max(0, value.(int))
case maxHeightKey:
Expand Down Expand Up @@ -137,6 +141,12 @@ func (s *Style) setFrom(key propKey, i Style) {
s.set(borderBottomBackgroundKey, i.borderBottomBgColor)
case borderLeftBackgroundKey:
s.set(borderLeftBackgroundKey, i.borderLeftBgColor)
case borderTopFuncKey:
s.borderTopFunc = make([]BorderFunc, 3)

Check failure on line 145 in set.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 3, in <argument> detected (gomnd)
copy(s.borderTopFunc, i.borderTopFunc)
case borderBottomFuncKey:
s.borderBottomFunc = make([]BorderFunc, 3)

Check failure on line 148 in set.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 3, in <argument> detected (gomnd)
copy(s.borderBottomFunc, i.borderBottomFunc)
case maxWidthKey:
s.set(maxWidthKey, i.maxWidth)
case maxHeightKey:
Expand All @@ -158,6 +168,21 @@ func colorOrNil(c interface{}) TerminalColor {
return nil
}

func mergeBorderFunc(a, b []BorderFunc) []BorderFunc {
if len(a) < 3 {
aa := make([]BorderFunc, 3)

Check failure on line 173 in set.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 3, in <argument> detected (gomnd)
copy(aa, a)
a = aa
}
for i := range b {
if b[i] != nil {
a[i] = b[i]
break
}
}
return a
}

// Bold sets a bold formatting rule.
func (s Style) Bold(v bool) Style {
s.set(boldKey, v)
Expand Down Expand Up @@ -591,6 +616,32 @@ func (s Style) BorderLeftBackground(c TerminalColor) Style {
return s
}

func posIndex(p Position) int {
switch p {

Check failure on line 620 in set.go

View workflow job for this annotation

GitHub Actions / lint-soft

missing cases in switch of type lipgloss.Position: lipgloss.Top|lipgloss.Left (exhaustive)
case Center:
return 1
case Right:
return 2
}
return 0
}

// BorderTopFunc set the top func

Check failure on line 629 in set.go

View workflow job for this annotation

GitHub Actions / lint-soft

Comment should end in a period (godot)
func (s Style) BorderTopFunc(p Position, bf BorderFunc) Style {
fns := make([]BorderFunc, 3)

Check failure on line 631 in set.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 3, in <argument> detected (gomnd)
fns[posIndex(p)] = bf
s.set(borderTopFuncKey, fns)
return s
}

// BorderBottomFunc set the bottom func

Check failure on line 637 in set.go

View workflow job for this annotation

GitHub Actions / lint-soft

Comment should end in a period (godot)
func (s Style) BorderBottomFunc(p Position, bf BorderFunc) Style {
fns := make([]BorderFunc, 3)

Check failure on line 639 in set.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 3, in <argument> detected (gomnd)
fns[posIndex(p)] = bf
s.set(borderBottomFuncKey, fns)
return s
}

// Inline makes rendering output one line and disables the rendering of
// margins, padding and borders. This is useful when you need a style to apply
// only to font rendering and don't want it to change any physical dimensions.
Expand Down
5 changes: 5 additions & 0 deletions style.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ const (
borderBottomBackgroundKey
borderLeftBackgroundKey

borderTopFuncKey
borderBottomFuncKey

inlineKey
maxWidthKey
maxHeightKey
Expand Down Expand Up @@ -151,6 +154,8 @@ type Style struct {
borderRightBgColor TerminalColor
borderBottomBgColor TerminalColor
borderLeftBgColor TerminalColor
borderTopFunc []BorderFunc
borderBottomFunc []BorderFunc

maxWidth int
maxHeight int
Expand Down

0 comments on commit a8709aa

Please sign in to comment.