Skip to content

Commit d974049

Browse files
committed
feat(style): add BorderTitle API
1 parent ecc1bd0 commit d974049

File tree

5 files changed

+110
-13
lines changed

5 files changed

+110
-13
lines changed

borders.go

+25-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package lipgloss
22

33
import (
4+
"fmt"
45
"strings"
56

67
"github.com/charmbracelet/x/ansi"
@@ -229,6 +230,7 @@ func HiddenBorder() Border {
229230
func (s Style) applyBorder(str string) string {
230231
var (
231232
border = s.getBorderStyle()
233+
title = s.getBorderTitle()
232234
hasTop = s.getAsBool(borderTopKey, false)
233235
hasRight = s.getAsBool(borderRightKey, false)
234236
hasBottom = s.getAsBool(borderBottomKey, false)
@@ -322,7 +324,7 @@ func (s Style) applyBorder(str string) string {
322324

323325
// Render top
324326
if hasTop {
325-
top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
327+
top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, title, width)
326328
top = s.styleBorder(top, topFG, topBG)
327329
out.WriteString(top)
328330
out.WriteRune('\n')
@@ -360,7 +362,7 @@ func (s Style) applyBorder(str string) string {
360362

361363
// Render bottom
362364
if hasBottom {
363-
bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width)
365+
bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, "", width)
364366
bottom = s.styleBorder(bottom, bottomFG, bottomBG)
365367
out.WriteRune('\n')
366368
out.WriteString(bottom)
@@ -370,27 +372,38 @@ func (s Style) applyBorder(str string) string {
370372
}
371373

372374
// Render the horizontal (top or bottom) portion of a border.
373-
func renderHorizontalEdge(left, middle, right string, width int) string {
375+
func renderHorizontalEdge(left, middle, right, title string, width int) string {
374376
if middle == "" {
375377
middle = " "
376378
}
377379

378-
leftWidth := ansi.StringWidth(left)
379-
rightWidth := ansi.StringWidth(right)
380+
var (
381+
leftWidth = ansi.StringWidth(left)
382+
midWidth = ansi.StringWidth(middle)
383+
runes = []rune(middle)
384+
j = 0
385+
)
380386

381-
runes := []rune(middle)
382-
j := 0
387+
absWidth := width - leftWidth
383388

384389
out := strings.Builder{}
385390
out.WriteString(left)
386-
for i := leftWidth + rightWidth; i < width+rightWidth; {
387-
out.WriteRune(runes[j])
388-
j++
389-
if j >= len(runes) {
390-
j = 0
391+
392+
// If there is enough space to print the middle segment a space, the title, a space and middle segment
393+
// Print that and remove it from the absolute length of the border.
394+
if title != "" {
395+
if titleLen := ansi.StringWidth(title) + 2 + 2*midWidth; titleLen < absWidth {
396+
out.WriteString(fmt.Sprintf("%s %s %s", middle, title, middle))
397+
absWidth -= titleLen
391398
}
399+
}
400+
401+
for i := 0; i < absWidth; {
402+
out.WriteRune(runes[j])
403+
j = (j + 1) % len(runes)
392404
i += ansi.StringWidth(string(runes[j]))
393405
}
406+
394407
out.WriteString(right)
395408

396409
return out.String()

borders_test.go

+68-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package lipgloss
22

3-
import "testing"
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/charmbracelet/x/ansi"
8+
)
49

510
func TestStyle_GetBorderSizes(t *testing.T) {
611
tests := []struct {
@@ -94,3 +99,65 @@ func TestStyle_GetBorderSizes(t *testing.T) {
9499
})
95100
}
96101
}
102+
103+
func TestBorderStyle(t *testing.T) {
104+
tests := []struct {
105+
name string
106+
title string
107+
expected string
108+
}{
109+
{
110+
name: "standard case",
111+
title: "Test",
112+
expected: strings.TrimSpace(`
113+
┌─ Test ───┐
114+
│ │
115+
│ │
116+
│ │
117+
│ │
118+
└──────────┘
119+
`),
120+
},
121+
{
122+
name: "ignores title if does not fit",
123+
title: "Title is too long a string and exceeds width",
124+
expected: strings.TrimSpace(`
125+
┌──────────┐
126+
│ │
127+
│ │
128+
│ │
129+
│ │
130+
└──────────┘
131+
`),
132+
},
133+
{
134+
name: "works with ansi escapes",
135+
title: NewStyle().Foreground(Color("#0ff")).Render("Test"),
136+
expected: strings.TrimSpace(`
137+
┌─ Test ───┐
138+
│ │
139+
│ │
140+
│ │
141+
│ │
142+
└──────────┘
143+
`),
144+
},
145+
}
146+
147+
for _, tt := range tests {
148+
t.Run(tt.name, func(t *testing.T) {
149+
actual := NewStyle().
150+
Width(10).
151+
Height(4).
152+
Border(NormalBorder()).
153+
BorderTitle(tt.title).
154+
Render()
155+
156+
actual = ansi.Strip(actual)
157+
158+
if actual != tt.expected {
159+
t.Errorf("expected:\n%s\n but got:\n%s", tt.expected, actual)
160+
}
161+
})
162+
}
163+
}

get.go

+4
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,10 @@ func (s Style) getBorderStyle() Border {
519519
return s.borderStyle
520520
}
521521

522+
func (s Style) getBorderTitle() string {
523+
return s.borderTitle
524+
}
525+
522526
// Returns whether or not the style has implicit borders. This happens when
523527
// a border style has been set but no border sides have been explicitly turned
524528
// on or off.

set.go

+9
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ func (s *Style) set(key propKey, value interface{}) {
3939
s.marginBgColor = colorOrNil(value)
4040
case borderStyleKey:
4141
s.borderStyle = value.(Border)
42+
case borderTitleKey:
43+
s.borderTitle = value.(string)
4244
case borderTopForegroundKey:
4345
s.borderTopFgColor = colorOrNil(value)
4446
case borderRightForegroundKey:
@@ -429,6 +431,13 @@ func (s Style) Border(b Border, sides ...bool) Style {
429431
return s
430432
}
431433

434+
// BorderTitle sets a title on the top border if top border is present and if
435+
// the title fits within the width of the border. Otherwise this has no effect.
436+
func (s Style) BorderTitle(title string) Style {
437+
s.set(borderTitleKey, title)
438+
return s
439+
}
440+
432441
// BorderStyle defines the Border on a style. A Border contains a series of
433442
// definitions for the sides and corners of a border.
434443
//

style.go

+4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ const (
5151
// Border runes.
5252
borderStyleKey
5353

54+
// Border title.
55+
borderTitleKey
56+
5457
// Border edges.
5558
borderTopKey
5659
borderRightKey
@@ -143,6 +146,7 @@ type Style struct {
143146
marginBgColor TerminalColor
144147

145148
borderStyle Border
149+
borderTitle string
146150
borderTopFgColor TerminalColor
147151
borderRightFgColor TerminalColor
148152
borderBottomFgColor TerminalColor

0 commit comments

Comments
 (0)