Skip to content

Commit

Permalink
feat: add BorderTitle API
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmdm committed Jan 14, 2025
1 parent ecc1bd0 commit 5cc8b83
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 13 deletions.
37 changes: 25 additions & 12 deletions borders.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lipgloss

import (
"fmt"
"strings"

"github.com/charmbracelet/x/ansi"
Expand Down Expand Up @@ -229,6 +230,7 @@ func HiddenBorder() Border {
func (s Style) applyBorder(str string) string {
var (
border = s.getBorderStyle()
title = s.getBorderTitle()
hasTop = s.getAsBool(borderTopKey, false)
hasRight = s.getAsBool(borderRightKey, false)
hasBottom = s.getAsBool(borderBottomKey, false)
Expand Down Expand Up @@ -322,7 +324,7 @@ func (s Style) applyBorder(str string) string {

// Render top
if hasTop {
top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, title, width)
top = s.styleBorder(top, topFG, topBG)
out.WriteString(top)
out.WriteRune('\n')
Expand Down Expand Up @@ -360,7 +362,7 @@ func (s Style) applyBorder(str string) string {

// Render bottom
if hasBottom {
bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width)
bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, "", width)
bottom = s.styleBorder(bottom, bottomFG, bottomBG)
out.WriteRune('\n')
out.WriteString(bottom)
Expand All @@ -370,27 +372,38 @@ func (s Style) applyBorder(str string) string {
}

// Render the horizontal (top or bottom) portion of a border.
func renderHorizontalEdge(left, middle, right string, width int) string {
func renderHorizontalEdge(left, middle, right, title string, width int) string {
if middle == "" {
middle = " "
}

leftWidth := ansi.StringWidth(left)
rightWidth := ansi.StringWidth(right)
var (
leftWidth = ansi.StringWidth(left)
midWidth = ansi.StringWidth(middle)
runes = []rune(middle)
j = 0
)

runes := []rune(middle)
j := 0
absWidth := width - leftWidth

out := strings.Builder{}
out.WriteString(left)
for i := leftWidth + rightWidth; i < width+rightWidth; {
out.WriteRune(runes[j])
j++
if j >= len(runes) {
j = 0

// If there is enough space to print the middle segment a space, the title, a space and middle segment
// Print that and remove it from the absolute length of the border.
if title != "" {
if titleLen := ansi.StringWidth(title) + 2 + 2*midWidth; titleLen < absWidth {
out.WriteString(fmt.Sprintf("%s %s %s", middle, title, middle))
absWidth -= titleLen
}
}

for i := 0; i < absWidth; {
out.WriteRune(runes[j])
j = (j + 1) % len(runes)
i += ansi.StringWidth(string(runes[j]))
}

out.WriteString(right)

return out.String()
Expand Down
69 changes: 68 additions & 1 deletion borders_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package lipgloss

import "testing"
import (
"strings"
"testing"

"github.com/charmbracelet/x/ansi"
)

func TestStyle_GetBorderSizes(t *testing.T) {
tests := []struct {
Expand Down Expand Up @@ -94,3 +99,65 @@ func TestStyle_GetBorderSizes(t *testing.T) {
})
}
}

func TestBorderStyle(t *testing.T) {
tests := []struct {
name string
title string
expected string
}{
{
name: "standard case",
title: "Test",
expected: strings.TrimSpace(`
┌─ Test ───┐
│ │
│ │
│ │
│ │
└──────────┘
`),
},
{
name: "ignores title if does not fit",
title: "Title is too long a string and exceeds width",
expected: strings.TrimSpace(`
┌──────────┐
│ │
│ │
│ │
│ │
└──────────┘
`),
},
{
name: "works with ansi escapes",
title: NewStyle().Foreground(Color("#0ff")).Render("Test"),
expected: strings.TrimSpace(`
┌─ Test ───┐
│ │
│ │
│ │
│ │
└──────────┘
`),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := NewStyle().
Width(10).
Height(4).
Border(NormalBorder()).
BorderTitle(tt.title).
Render()

actual = ansi.Strip(actual)

if actual != tt.expected {
t.Errorf("expected:\n%s\n but got:\n%s", tt.expected, actual)
}
})
}
}
4 changes: 4 additions & 0 deletions get.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,10 @@ func (s Style) getBorderStyle() Border {
return s.borderStyle
}

func (s Style) getBorderTitle() string {
return s.borderTitle
}

// Returns whether or not the style has implicit borders. This happens when
// a border style has been set but no border sides have been explicitly turned
// on or off.
Expand Down
9 changes: 9 additions & 0 deletions set.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ func (s *Style) set(key propKey, value interface{}) {
s.marginBgColor = colorOrNil(value)
case borderStyleKey:
s.borderStyle = value.(Border)
case borderTitleKey:
s.borderTitle = value.(string)
case borderTopForegroundKey:
s.borderTopFgColor = colorOrNil(value)
case borderRightForegroundKey:
Expand Down Expand Up @@ -429,6 +431,13 @@ func (s Style) Border(b Border, sides ...bool) Style {
return s
}

// BorderTitle sets a title on the top border if top border is present and if
// the title fits within the width of the border. Otherwise this has no effect.
func (s Style) BorderTitle(title string) Style {
s.set(borderTitleKey, title)
return s
}

// BorderStyle defines the Border on a style. A Border contains a series of
// definitions for the sides and corners of a border.
//
Expand Down
4 changes: 4 additions & 0 deletions style.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ const (
// Border runes.
borderStyleKey

// Border title.
borderTitleKey

// Border edges.
borderTopKey
borderRightKey
Expand Down Expand Up @@ -143,6 +146,7 @@ type Style struct {
marginBgColor TerminalColor

borderStyle Border
borderTitle string
borderTopFgColor TerminalColor
borderRightFgColor TerminalColor
borderBottomFgColor TerminalColor
Expand Down

0 comments on commit 5cc8b83

Please sign in to comment.