diff --git a/README.md b/README.md index c4c282ef..c6c741e4 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,37 @@ lipgloss.NewStyle(). lipgloss.NewStyle(). Border(lipgloss.DoubleBorder(), true, false, false, true) ``` +It is also possible to set and style a border title +```go + + // create bold italic title horizontally aligned + // to center of the border + titleStyle := lipgloss.NewStyle(). + Background(lipgloss.Color("#6124DF")). + Align(lipgloss.Center). + Bold(true). + Italic(true) + + // dialog box style with its title styled + dialogBoxStyle := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#874BFD")). + BorderTitleStyle(titleStyle). + Padding(1, 0). + BorderTop(true). + BorderLeft(true). + BorderRight(true). + BorderBottom(true) + + + // Use the title with the dialog box style + dialog := lipgloss.Place(width, 9, + lipgloss.Center, lipgloss.Center, + dialogBoxStyle.Copy().BorderTitle(" Question ").Render(ui), + lipgloss.WithWhitespaceChars("猫咪"), + lipgloss.WithWhitespaceForeground(subtle), + ) +``` For more on borders see [the docs][docs]. diff --git a/borders.go b/borders.go index 23372808..b2a83c07 100644 --- a/borders.go +++ b/borders.go @@ -294,11 +294,45 @@ func (s Style) applyBorder(str string) string { border.BottomLeft = getFirstRuneAsString(border.BottomLeft) var out strings.Builder + const sideCount = 2 // Render top if hasTop { - top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width) - top = styleBorder(top, topFG, topBG) + top := "" + + // sanitize title style + titleStyle := s.GetBorderTitleStyle().Copy().MaxWidth(width) + + // set default padding if one is not set + if titleStyle.GetHorizontalPadding() == 0 { + titleStyle = titleStyle.Padding(0, 1) + } + + title := s.GetBorderTitle() + + if len(strings.TrimSpace(title)) > 0 { + titleLen := titleStyle.GetHorizontalFrameSize() + len(title) + topBeforeTitle := border.TopLeft + topAfterTitle := border.TopRight + switch titleStyle.GetAlignHorizontal() { + case Right: + topBeforeTitle = border.TopLeft + strings.Repeat(border.Top, max(0, width-1-titleLen)) + case Center: + noTitleLen := width - 1 - titleLen + noTitleLen2 := noTitleLen / sideCount + topBeforeTitle = border.TopLeft + strings.Repeat(border.Top, max(0, noTitleLen2)) + topAfterTitle = strings.Repeat(border.Top, max(0, noTitleLen-noTitleLen2)) + border.TopRight + case Left: + topAfterTitle = strings.Repeat(border.Top, max(0, width-1-titleLen)) + border.TopRight + } + + top = styleBorder(topBeforeTitle, topFG, topBG) + + titleStyle.Render(title) + + styleBorder(topAfterTitle, topFG, topBG) + } else { + top = renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width) + top = styleBorder(top, topFG, topBG) + } out.WriteString(top) out.WriteRune('\n') } @@ -362,6 +396,7 @@ func renderHorizontalEdge(left, middle, right string, width int) string { out := strings.Builder{} out.WriteString(left) + for i := leftWidth + rightWidth; i < width+rightWidth; { out.WriteRune(runes[j]) j++ diff --git a/example/main.go b/example/main.go index 8ec832b8..ff25932c 100644 --- a/example/main.go +++ b/example/main.go @@ -92,9 +92,17 @@ var ( // Dialog. + dialogTitleStyle = lipgloss.NewStyle(). + Background(lipgloss.Color("#6124DF")). + Align(lipgloss.Center). + Bold(true). + Italic(true). + Padding(0, 5) + dialogBoxStyle = lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("#874BFD")). + BorderTitleStyle(dialogTitleStyle). Padding(1, 0). BorderTop(true). BorderLeft(true). @@ -239,7 +247,7 @@ func main() { dialog := lipgloss.Place(width, 9, lipgloss.Center, lipgloss.Center, - dialogBoxStyle.Render(ui), + dialogBoxStyle.Copy().BorderTitle("Question").Render(ui), lipgloss.WithWhitespaceChars("猫咪"), lipgloss.WithWhitespaceForeground(subtle), ) diff --git a/get.go b/get.go index 2613750f..5d42928a 100644 --- a/get.go +++ b/get.go @@ -434,6 +434,24 @@ func (s Style) getAsInt(k propKey) int { return 0 } +func (s Style) getAsString(k propKey) string { + if v, ok := s.rules[k]; ok { + if s, ok := v.(string); ok { + return s + } + } + return "" +} + +func (s Style) getAsStyle(k propKey) Style { + if v, ok := s.rules[k]; ok { + if s, ok := v.(Style); ok { + return s + } + } + return NewStyle() +} + func (s Style) getAsPosition(k propKey) Position { v, ok := s.rules[k] if !ok { @@ -456,7 +474,19 @@ func (s Style) getBorderStyle() Border { return noBorder } -// Split a string into lines, additionally returning the size of the widest +// GetBorderTitleStyle returns border title style if set, +// otherwise returns empty style. +func (s Style) GetBorderTitleStyle() Style { + return s.getAsStyle(borderTitleStyleKey) +} + +// GetBorderTitle returns border title if set, +// otherwise returns empty string. +func (s Style) GetBorderTitle() string { + return s.getAsString(borderTitleKey) +} + +// Split a string into lines, additionally returning the size of the widest. // line. func getLines(s string) (lines []string, widest int) { lines = strings.Split(s, "\n") diff --git a/set.go b/set.go index 21a9ce4f..104167ac 100644 --- a/set.go +++ b/set.go @@ -455,6 +455,18 @@ func (s Style) BorderLeftBackground(c TerminalColor) Style { return s } +// BorderTitleStyle set border title style +func (s Style) BorderTitleStyle(style Style) Style { + s.set(borderTitleStyleKey, style) + return s +} + +// BorderTitle set border title +func (s Style) BorderTitle(title string) Style { + s.set(borderTitleKey, title) + 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. diff --git a/style.go b/style.go index 4c149326..446f4b26 100644 --- a/style.go +++ b/style.go @@ -65,6 +65,10 @@ const ( borderBottomBackgroundKey borderLeftBackgroundKey + // Border title. + borderTitleStyleKey + borderTitleKey + inlineKey maxWidthKey maxHeightKey