From eea5201de24105d7cbd8d17cd3f2c69a3e05475f Mon Sep 17 00:00:00 2001 From: Tom Pacheco Date: Wed, 2 Oct 2024 18:47:59 -0400 Subject: [PATCH 01/14] fix: removed unneeded widths --- borders.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/borders.go b/borders.go index deb6b35a..03a24c19 100644 --- a/borders.go +++ b/borders.go @@ -380,15 +380,12 @@ func renderHorizontalEdge(left, middle, right string, width int) string { middle = " " } - leftWidth := ansi.StringWidth(left) - rightWidth := ansi.StringWidth(right) - runes := []rune(middle) j := 0 out := strings.Builder{} out.WriteString(left) - for i := leftWidth + rightWidth; i < width+rightWidth; { + for i := 0; i < width-1; { out.WriteRune(runes[j]) j++ if j >= len(runes) { From a8709aa7debe27c39acfd4f15164f556a222c770 Mon Sep 17 00:00:00 2001 From: Tom Pacheco Date: Wed, 2 Oct 2024 20:12:36 -0400 Subject: [PATCH 02/14] feat: add border func to set text in borders this adds callback functions to set text in borders when they are generated. --- borders.go | 64 +++++++++++++++++++++- borders_test.go | 140 ++++++++++++++++++++++++++++++++++++++++++++++++ set.go | 51 ++++++++++++++++++ style.go | 5 ++ 4 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 borders_test.go diff --git a/borders.go b/borders.go index 03a24c19..97ff7ee8 100644 --- a/borders.go +++ b/borders.go @@ -26,6 +26,8 @@ type Border struct { MiddleBottom string } +type BorderFunc func(width int, middle string) 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. @@ -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 @@ -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') @@ -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) @@ -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) + ws := make([]int, 3) + 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 { + // 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 == "" { diff --git a/borders_test.go b/borders_test.go new file mode 100644 index 00000000..ee2a4f24 --- /dev/null +++ b/borders_test.go @@ -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)) + } + } + +} diff --git a/set.go b/set.go index ed6e272c..01e0943f 100644 --- a/set.go +++ b/set.go @@ -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: @@ -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) + copy(s.borderTopFunc, i.borderTopFunc) + case borderBottomFuncKey: + s.borderBottomFunc = make([]BorderFunc, 3) + copy(s.borderBottomFunc, i.borderBottomFunc) case maxWidthKey: s.set(maxWidthKey, i.maxWidth) case maxHeightKey: @@ -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) + 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) @@ -591,6 +616,32 @@ func (s Style) BorderLeftBackground(c TerminalColor) Style { return s } +func posIndex(p Position) int { + switch p { + case Center: + return 1 + case Right: + return 2 + } + return 0 +} + +// BorderTopFunc set the top func +func (s Style) BorderTopFunc(p Position, bf BorderFunc) Style { + fns := make([]BorderFunc, 3) + fns[posIndex(p)] = bf + s.set(borderTopFuncKey, fns) + return s +} + +// BorderBottomFunc set the bottom func +func (s Style) BorderBottomFunc(p Position, bf BorderFunc) Style { + fns := make([]BorderFunc, 3) + 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. diff --git a/style.go b/style.go index 2aba56b0..87d4545e 100644 --- a/style.go +++ b/style.go @@ -69,6 +69,9 @@ const ( borderBottomBackgroundKey borderLeftBackgroundKey + borderTopFuncKey + borderBottomFuncKey + inlineKey maxWidthKey maxHeightKey @@ -151,6 +154,8 @@ type Style struct { borderRightBgColor TerminalColor borderBottomBgColor TerminalColor borderLeftBgColor TerminalColor + borderTopFunc []BorderFunc + borderBottomFunc []BorderFunc maxWidth int maxHeight int From 582eb0a32ba102a26382219b010cefb7e40a40cc Mon Sep 17 00:00:00 2001 From: Tom Pacheco Date: Thu, 3 Oct 2024 19:16:30 -0400 Subject: [PATCH 03/14] fix: remove extra width offset --- borders.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/borders.go b/borders.go index 97ff7ee8..4671f1b5 100644 --- a/borders.go +++ b/borders.go @@ -275,7 +275,6 @@ func (s Style) applyBorder(str string) string { if border.Left == "" { border.Left = " " } - width += maxRuneWidth(border.Left) } if hasRight && border.Right == "" { @@ -416,8 +415,8 @@ func renderAnnotatedHorizontalEdge(left, middle, right string, bFuncs []BorderFu 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 { + for i := ws[0]; i < width-ws[2]; { + if ws[1] > 0 && i == (width-ws[1])/2 { out.WriteString(ts[1]) i += ws[1] } @@ -445,7 +444,7 @@ func renderHorizontalEdge(left, middle, right string, width int) string { out := strings.Builder{} out.WriteString(left) - for i := 0; i < width-1; { + for i := 0; i < width; { out.WriteRune(runes[j]) j++ if j >= len(runes) { From eb0979ee7c8da703bdb2d944df133164e4445d53 Mon Sep 17 00:00:00 2001 From: Tom Pacheco Date: Fri, 4 Oct 2024 20:00:31 -0400 Subject: [PATCH 04/14] fix: add missing Unset functions for Border Funcs --- unset.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/unset.go b/unset.go index 1086e722..7c073413 100644 --- a/unset.go +++ b/unset.go @@ -282,6 +282,16 @@ func (s Style) UnsetBorderLeftBackground() Style { return s } +func (s Style) UnsetBorderBottomFunc() Style { + s.unset(borderBottomFuncKey) + return s +} + +func (s Style) UnsetBorderTopFunc() Style { + s.unset(borderBottomFuncKey) + return s +} + // UnsetInline removes the inline style rule, if set. func (s Style) UnsetInline() Style { s.unset(inlineKey) From 7f057ec98e4c2208c35629923314ee1b0f6a6d6c Mon Sep 17 00:00:00 2001 From: Tom Pacheco Date: Fri, 4 Oct 2024 20:05:58 -0400 Subject: [PATCH 05/14] docs: updated BorderFunc docs --- borders.go | 20 ++++++++++++++++++++ set.go | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/borders.go b/borders.go index 4671f1b5..2542ccd6 100644 --- a/borders.go +++ b/borders.go @@ -26,6 +26,26 @@ type Border struct { MiddleBottom string } +// BorderFunc is border function that sets horizontal border text +// at the given position. +// +// It takes the width of the border and the Top/Bottom border string +// and determines the string for that position. +// +// Example: +// +// bStyle := lipgloss.NewStyle().Reverse(true) +// t := lipgloss.NewStyle(). +// Border(lipgloss.NormalBorder()). +// BorderTopFunc(lipgloss.Center, func(w int, m string) string { +// return bStyle.Render(" BIG TITLE ") +// }). +// BorderBottomFunc(lipgloss.Right, func(width int, middle string) string { +// return bStyle.Render(fmt.Sprintf(" %d/%d ", m.index + 1, m.count)) + middle +// }). +// BorderBottomFunc(lipgloss.Left, func(width int, middle string) string { +// return middle + bStyle.Render(fmt.Sprintf("Status: %s", m.status)) +// }) type BorderFunc func(width int, middle string) string // GetTopSize returns the width of the top border. If borders contain runes of diff --git a/set.go b/set.go index 01e0943f..c11f4863 100644 --- a/set.go +++ b/set.go @@ -626,7 +626,22 @@ func posIndex(p Position) int { return 0 } -// BorderTopFunc set the top func +// BorderTopFunc set the top border decoration such as a title. +// The first argument is the position, it accepts Left, Right, and Center. +// +// the second argument is +// func(width int, middle string) string +// +// examples: +// +// // Set a title with dynamic text from a function +// lipgloss.NewStyle(). +// Border(lipgloss.NormalBorder()). +// BorderTopFunc(lipgloss.Center, +// func(width int, middle string) string { +// return fmt.Sprintf(" %d/%d ", index + 1, count) +// }) +// func (s Style) BorderTopFunc(p Position, bf BorderFunc) Style { fns := make([]BorderFunc, 3) fns[posIndex(p)] = bf @@ -634,7 +649,22 @@ func (s Style) BorderTopFunc(p Position, bf BorderFunc) Style { return s } -// BorderBottomFunc set the bottom func +// BorderBottomFunc set the bottom border decoration such as a status. +// The first argument is the position, it accepts Left, Right, and Center. +// +// the second argument is +// func(width int, middle string) string +// +// examples: +// +// // Set a title with dynamic text from a function +// lipgloss.NewStyle(). +// Border(lipgloss.NormalBorder()). +// BorderBottomFunc(lipgloss.Right, +// func(width int, middle string) string { +// return fmt.Sprintf(" %d/%d ", index + 1, count) +// }) +// func (s Style) BorderBottomFunc(p Position, bf BorderFunc) Style { fns := make([]BorderFunc, 3) fns[posIndex(p)] = bf From b96a4eef00a59b0c166c46948034e5b314aa83fa Mon Sep 17 00:00:00 2001 From: Tom Pacheco Date: Fri, 4 Oct 2024 20:17:56 -0400 Subject: [PATCH 06/14] fix: change internals to BorderFunc and limit width of returned border text changed internals to use interface{} so will work with string or String(). border length was not being checked before, now it is limited to the width of the border. --- borders.go | 33 ++++++++++++++++++++++++++------- set.go | 24 ++++++++++++++---------- style.go | 4 ++-- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/borders.go b/borders.go index 2542ccd6..847471fb 100644 --- a/borders.go +++ b/borders.go @@ -1,6 +1,7 @@ package lipgloss import ( + "fmt" "strings" "github.com/charmbracelet/x/ansi" @@ -409,7 +410,7 @@ func (s Style) applyBorder(str string) string { } // Render the horizontal (top or bottom) portion of a border. -func renderAnnotatedHorizontalEdge(left, middle, right string, bFuncs []BorderFunc, width int) string { +func renderAnnotatedHorizontalEdge(left, middle, right string, bFuncs []interface{}, width int) string { if middle == "" { middle = " " } @@ -420,12 +421,30 @@ func renderAnnotatedHorizontalEdge(left, middle, right string, bFuncs []BorderFu if f == nil { continue } - ts[i] = f(width, middle) - ws[i] = ansi.StringWidth(ts[i]) - } - - if width <= ws[0]+ws[1]+ws[2]+2 { - // TODO fix length if the strings are too long + remainingWidth := width + if remainingWidth < 1 { + break + } + switch f := f.(type) { + case string: + ts[i] = ansi.Truncate(f, remainingWidth, "") + ws[i] = ansi.StringWidth(ts[i]) + remainingWidth -= ws[i] + case func(int, string) string: + ts[i] = f(remainingWidth, middle) + ts[i] = ansi.Truncate(ts[i], remainingWidth, "") + ws[i] = ansi.StringWidth(ts[i]) + remainingWidth -= ws[i] + case BorderFunc: + ts[i] = f(remainingWidth, middle) + ts[i] = ansi.Truncate(ts[i], remainingWidth, "") + ws[i] = ansi.StringWidth(ts[i]) + remainingWidth -= ws[i] + case fmt.Stringer: + ts[i] = ansi.Truncate(f.String(), remainingWidth, "") + ws[i] = ansi.StringWidth(ts[i]) + remainingWidth -= ws[i] + } } runes := []rune(middle) diff --git a/set.go b/set.go index c11f4863..49f05202 100644 --- a/set.go +++ b/set.go @@ -56,9 +56,15 @@ func (s *Style) set(key propKey, value interface{}) { case borderLeftBackgroundKey: s.borderLeftBgColor = colorOrNil(value) case borderTopFuncKey: - s.borderTopFunc = mergeBorderFunc(s.borderTopFunc, value.([]BorderFunc)) + s.borderTopFunc = mergeBorderFunc( + s.borderTopFunc, + value.([]interface{}), + ) case borderBottomFuncKey: - s.borderBottomFunc = mergeBorderFunc(s.borderBottomFunc, value.([]BorderFunc)) + s.borderBottomFunc = mergeBorderFunc( + s.borderBottomFunc, + value.([]interface{}), + ) case maxWidthKey: s.maxWidth = max(0, value.(int)) case maxHeightKey: @@ -142,11 +148,9 @@ func (s *Style) setFrom(key propKey, i Style) { case borderLeftBackgroundKey: s.set(borderLeftBackgroundKey, i.borderLeftBgColor) case borderTopFuncKey: - s.borderTopFunc = make([]BorderFunc, 3) - copy(s.borderTopFunc, i.borderTopFunc) + s.borderTopFunc = mergeBorderFunc(s.borderTopFunc, i.borderTopFunc) case borderBottomFuncKey: - s.borderBottomFunc = make([]BorderFunc, 3) - copy(s.borderBottomFunc, i.borderBottomFunc) + s.borderBottomFunc = mergeBorderFunc(s.borderBottomFunc, i.borderBottomFunc) case maxWidthKey: s.set(maxWidthKey, i.maxWidth) case maxHeightKey: @@ -168,9 +172,9 @@ func colorOrNil(c interface{}) TerminalColor { return nil } -func mergeBorderFunc(a, b []BorderFunc) []BorderFunc { +func mergeBorderFunc(a, b []interface{}) []interface{} { if len(a) < 3 { - aa := make([]BorderFunc, 3) + aa := make([]interface{}, 3) copy(aa, a) a = aa } @@ -643,7 +647,7 @@ func posIndex(p Position) int { // }) // func (s Style) BorderTopFunc(p Position, bf BorderFunc) Style { - fns := make([]BorderFunc, 3) + fns := make([]interface{}, 3) fns[posIndex(p)] = bf s.set(borderTopFuncKey, fns) return s @@ -666,7 +670,7 @@ func (s Style) BorderTopFunc(p Position, bf BorderFunc) Style { // }) // func (s Style) BorderBottomFunc(p Position, bf BorderFunc) Style { - fns := make([]BorderFunc, 3) + fns := make([]interface{}, 3) fns[posIndex(p)] = bf s.set(borderBottomFuncKey, fns) return s diff --git a/style.go b/style.go index 87d4545e..e01e304e 100644 --- a/style.go +++ b/style.go @@ -154,8 +154,8 @@ type Style struct { borderRightBgColor TerminalColor borderBottomBgColor TerminalColor borderLeftBgColor TerminalColor - borderTopFunc []BorderFunc - borderBottomFunc []BorderFunc + borderTopFunc []interface{} + borderBottomFunc []interface{} maxWidth int maxHeight int From 6459d6283a1371bf2a79fce662cdc345c936523c Mon Sep 17 00:00:00 2001 From: Tom Pacheco Date: Sat, 5 Oct 2024 19:38:33 -0400 Subject: [PATCH 07/14] refactor: renamed methods and changed api. --- borders.go | 81 ++++++++++++--- borders_test.go | 154 +++++++++++++++++++++++----- examples/border/decorations/main.go | 49 +++++++++ set.go | 106 +++++++++---------- style.go | 4 +- unset.go | 17 ++- 6 files changed, 314 insertions(+), 97 deletions(-) create mode 100644 examples/border/decorations/main.go diff --git a/borders.go b/borders.go index 847471fb..7e1c485a 100644 --- a/borders.go +++ b/borders.go @@ -1,7 +1,6 @@ package lipgloss import ( - "fmt" "strings" "github.com/charmbracelet/x/ansi" @@ -27,27 +26,84 @@ type Border struct { MiddleBottom string } -// BorderFunc is border function that sets horizontal border text -// at the given position. +// BorderHorizontalFunc is border function that sets horizontal border text +// at the configured position. // // It takes the width of the border and the Top/Bottom border string // and determines the string for that position. // // Example: // -// bStyle := lipgloss.NewStyle().Reverse(true) +// reverseStyle := lipgloss.NewStyle().Reverse(true) // t := lipgloss.NewStyle(). // Border(lipgloss.NormalBorder()). // BorderTopFunc(lipgloss.Center, func(w int, m string) string { -// return bStyle.Render(" BIG TITLE ") +// return reverseStyle.Render(" BIG TITLE ") // }). // BorderBottomFunc(lipgloss.Right, func(width int, middle string) string { -// return bStyle.Render(fmt.Sprintf(" %d/%d ", m.index + 1, m.count)) + middle +// return reverseStyle.Render(fmt.Sprintf(" %d/%d ", m.index + 1, m.count)) + middle // }). // BorderBottomFunc(lipgloss.Left, func(width int, middle string) string { -// return middle + bStyle.Render(fmt.Sprintf("Status: %s", m.status)) +// return middle + reverseStyle.Render(fmt.Sprintf("Status: %s", m.status)) // }) -type BorderFunc func(width int, middle string) string +type BorderHorizontalFunc interface { + func(width int, middle string) string +} + +// BorderDecoration is type used by Border to set text or decorate the border. +type BorderDecoration struct { + side Position + align Position + st interface{} +} + +// BorderDecorator is constraint type for a string or function that is used +// to decorate a border. +type BorderDecorator interface { + string | func() string | BorderHorizontalFunc +} + +// NewBorderDecoration is function that sets creates a decoration for the border. +// +// It takes the side of the border (Top|Bottom), the alignment (Left|Center|Right) of the +// decoration, and the decoration. +// +// the decoration can be any of +// - string +// - func() string +// - func(width int, middle string) string where width is the size of the border and middle +// is the border string +// +// Example: +// +// reverseStyle := lipgloss.NewStyle().Reverse(true) +// +// t := lipgloss.NewStyle(). +// Border(lipgloss.NormalBorder()). +// BorderDecoration(lipgloss.NewBorderDecoration( +// lipgloss.Top, +// lipgloss.Center, +// reverseStyle.Padding(0, 1).Render("BIG TITLE"), +// )). +// BorderDecoration(lipgloss.NewBorderDecoration( +// lipgloss.Bottom, +// lipgloss.Right, +// func(width int, middle string) string { +// return reverseStyle.Render(fmt.Sprintf(" %d/%d ", m.index + 1, m.count)) + middle +// }, +// )). +// BorderDecoration(lipgloss.NewBorderDecoration( +// lipgloss.Bottom, +// lipgloss.Left, +// reverseStyle.SetString(fmt.Sprintf("Status: %s", m.status)).String, +// )) +func NewBorderDecoration[S BorderDecorator](side, align Position, st S) BorderDecoration { + return BorderDecoration{ + align: align, + side: side, + st: st, + } +} // 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 @@ -435,13 +491,8 @@ func renderAnnotatedHorizontalEdge(left, middle, right string, bFuncs []interfac ts[i] = ansi.Truncate(ts[i], remainingWidth, "") ws[i] = ansi.StringWidth(ts[i]) remainingWidth -= ws[i] - case BorderFunc: - ts[i] = f(remainingWidth, middle) - ts[i] = ansi.Truncate(ts[i], remainingWidth, "") - ws[i] = ansi.StringWidth(ts[i]) - remainingWidth -= ws[i] - case fmt.Stringer: - ts[i] = ansi.Truncate(f.String(), remainingWidth, "") + case func() string: + ts[i] = ansi.Truncate(f(), remainingWidth, "") ws[i] = ansi.StringWidth(ts[i]) remainingWidth -= ws[i] } diff --git a/borders_test.go b/borders_test.go index ee2a4f24..1c15fc9d 100644 --- a/borders_test.go +++ b/borders_test.go @@ -10,12 +10,52 @@ func TestBorderFunc(t *testing.T) { style Style expected string }{ + { + name: "top left title string", + text: "", + style: NewStyle(). + Width(10). + Border(NormalBorder()). + BorderDecoration(NewBorderDecoration(Top, Left, "TITLE")), + expected: `┌TITLE─────┐ +│ │ +└──────────┘`, + }, + { + name: "top left title stringer", + text: "", + style: NewStyle(). + Width(10). + Border(NormalBorder()). + BorderDecoration(NewBorderDecoration(Top, Left, NewStyle().SetString("TITLE").String)), + expected: `┌TITLE─────┐ +│ │ +└──────────┘`, + }, + { + name: "top left very long title stringer", + text: "", + style: NewStyle(). + Width(10). + Border(NormalBorder()). + BorderDecoration(NewBorderDecoration(Top, Left, NewStyle().SetString("TitleTitleTitle").String)), + expected: `┌TitleTitle┐ +│ │ +└──────────┘`, + }, { name: "top left title", text: "", - style: NewStyle().Width(10).Border(NormalBorder()).BorderTopFunc(Left, func(width int, middle string) string { - return "TITLE" - }), + style: NewStyle(). + Width(10). + Border(NormalBorder()). + BorderDecoration(NewBorderDecoration( + Top, + Left, + func(width int, middle string) string { + return "TITLE" + }, + )), expected: `┌TITLE─────┐ │ │ └──────────┘`, @@ -23,9 +63,16 @@ func TestBorderFunc(t *testing.T) { { name: "top center title", text: "", - style: NewStyle().Width(10).Border(NormalBorder()).BorderTopFunc(Center, func(width int, middle string) string { - return "TITLE" - }), + style: NewStyle(). + Width(10). + Border(NormalBorder()). + BorderDecoration(NewBorderDecoration( + Top, + Center, + func(width int, middle string) string { + return "TITLE" + }, + )), expected: `┌──TITLE───┐ │ │ └──────────┘`, @@ -33,9 +80,16 @@ func TestBorderFunc(t *testing.T) { { name: "top center title even", text: "", - style: NewStyle().Width(11).Border(NormalBorder()).BorderTopFunc(Center, func(width int, middle string) string { - return "TITLE" - }), + style: NewStyle(). + Width(11). + Border(NormalBorder()). + BorderDecoration(NewBorderDecoration( + Top, + Center, + func(width int, middle string) string { + return "TITLE" + }, + )), expected: `┌───TITLE───┐ │ │ └───────────┘`, @@ -43,9 +97,16 @@ func TestBorderFunc(t *testing.T) { { name: "top right title", text: "", - style: NewStyle().Width(10).Border(NormalBorder()).BorderTopFunc(Right, func(width int, middle string) string { - return "TITLE" - }), + style: NewStyle(). + Width(10). + Border(NormalBorder()). + BorderDecoration(NewBorderDecoration( + Top, + Right, + func(width int, middle string) string { + return "TITLE" + }, + )), expected: `┌─────TITLE┐ │ │ └──────────┘`, @@ -53,9 +114,16 @@ func TestBorderFunc(t *testing.T) { { name: "bottom left title", text: "", - style: NewStyle().Width(10).Border(NormalBorder()).BorderBottomFunc(Left, func(width int, middle string) string { - return "STATUS" - }), + style: NewStyle(). + Width(10). + Border(NormalBorder()). + BorderDecoration(NewBorderDecoration( + Bottom, + Left, + func(width int, middle string) string { + return "STATUS" + }, + )), expected: `┌──────────┐ │ │ └STATUS────┘`, @@ -63,9 +131,16 @@ func TestBorderFunc(t *testing.T) { { name: "bottom center title", text: "", - style: NewStyle().Width(10).Border(NormalBorder()).BorderBottomFunc(Center, func(width int, middle string) string { - return "STATUS" - }), + style: NewStyle(). + Width(10). + Border(NormalBorder()). + BorderDecoration(NewBorderDecoration( + Bottom, + Center, + func(width int, middle string) string { + return "STATUS" + }, + )), expected: `┌──────────┐ │ │ └──STATUS──┘`, @@ -73,9 +148,16 @@ func TestBorderFunc(t *testing.T) { { name: "bottom center title odd", text: "", - style: NewStyle().Width(11).Border(NormalBorder()).BorderBottomFunc(Center, func(width int, middle string) string { - return "STATUS" - }), + style: NewStyle(). + Width(11). + Border(NormalBorder()). + BorderDecoration(NewBorderDecoration( + Bottom, + Center, + func(width int, middle string) string { + return "STATUS" + }, + )), expected: `┌───────────┐ │ │ └──STATUS───┘`, @@ -83,13 +165,37 @@ func TestBorderFunc(t *testing.T) { { name: "bottom right title", text: "", - style: NewStyle().Width(10).Border(NormalBorder()).BorderBottomFunc(Right, func(width int, middle string) string { - return "STATUS" - }), + style: NewStyle(). + Width(10). + Border(NormalBorder()). + BorderDecoration(NewBorderDecoration( + Bottom, + Right, + func(width int, middle string) string { + return "STATUS" + }, + )), expected: `┌──────────┐ │ │ └────STATUS┘`, }, + { + name: "bottom right padded title", + text: "", + style: NewStyle(). + Width(12). + Border(NormalBorder()). + BorderDecoration(NewBorderDecoration( + Bottom, + Right, + func(width int, middle string) string { + return "│STATUS│" + middle + }, + )), + expected: `┌────────────┐ +│ │ +└───│STATUS│─┘`, + }, } for i, tc := range tt { diff --git a/examples/border/decorations/main.go b/examples/border/decorations/main.go new file mode 100644 index 00000000..e09367a4 --- /dev/null +++ b/examples/border/decorations/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + + "github.com/charmbracelet/lipgloss" +) + +func main() { + + m := struct { + index int + count int + status string + }{ + index: 5, + count: 10, + status: ":)", + } + + reverseStyle := lipgloss.NewStyle().Reverse(true) + + t := lipgloss.NewStyle(). + Width(40). + Height(10). + Border(lipgloss.NormalBorder()). + BorderDecoration(lipgloss.NewBorderDecoration( + lipgloss.Top, + lipgloss.Center, + reverseStyle.Padding(0, 1).Render("BIG TITLE"), + )). + BorderDecoration(lipgloss.NewBorderDecoration( + lipgloss.Bottom, + lipgloss.Right, + func(width int, middle string) string { + return reverseStyle.Render(fmt.Sprintf("[%d/%d]", m.index+1, m.count)) + middle + }, + )). + BorderDecoration(lipgloss.NewBorderDecoration( + lipgloss.Bottom, + lipgloss.Left, + reverseStyle.Padding(0, 1).SetString(fmt.Sprintf("Status: %s", m.status)).String, + )) + + fmt.Println() + fmt.Println(t) + fmt.Println() + +} diff --git a/set.go b/set.go index 49f05202..922d483b 100644 --- a/set.go +++ b/set.go @@ -55,16 +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.([]interface{}), - ) - case borderBottomFuncKey: - s.borderBottomFunc = mergeBorderFunc( - s.borderBottomFunc, - value.([]interface{}), - ) + case borderTopDecorationKey: + s.borderTopFunc = addBorderFunc(s.borderTopFunc, value.(BorderDecoration)) + case borderBottomDecorationKey: + s.borderBottomFunc = addBorderFunc(s.borderBottomFunc, value.(BorderDecoration)) case maxWidthKey: s.maxWidth = max(0, value.(int)) case maxHeightKey: @@ -147,9 +141,9 @@ func (s *Style) setFrom(key propKey, i Style) { s.set(borderBottomBackgroundKey, i.borderBottomBgColor) case borderLeftBackgroundKey: s.set(borderLeftBackgroundKey, i.borderLeftBgColor) - case borderTopFuncKey: + case borderTopDecorationKey: s.borderTopFunc = mergeBorderFunc(s.borderTopFunc, i.borderTopFunc) - case borderBottomFuncKey: + case borderBottomDecorationKey: s.borderBottomFunc = mergeBorderFunc(s.borderBottomFunc, i.borderBottomFunc) case maxWidthKey: s.set(maxWidthKey, i.maxWidth) @@ -172,6 +166,17 @@ func colorOrNil(c interface{}) TerminalColor { return nil } +func addBorderFunc(a []interface{}, b BorderDecoration) []interface{} { + if len(a) < 3 { + aa := make([]interface{}, 3) + copy(aa, a) + a = aa + } + i := posIndex(b.align) + a[i] = b.st + return a +} + func mergeBorderFunc(a, b []interface{}) []interface{} { if len(a) < 3 { aa := make([]interface{}, 3) @@ -630,49 +635,46 @@ func posIndex(p Position) int { return 0 } -// BorderTopFunc set the top border decoration such as a title. -// The first argument is the position, it accepts Left, Right, and Center. -// -// the second argument is -// func(width int, middle string) string -// -// examples: -// -// // Set a title with dynamic text from a function -// lipgloss.NewStyle(). -// Border(lipgloss.NormalBorder()). -// BorderTopFunc(lipgloss.Center, -// func(width int, middle string) string { -// return fmt.Sprintf(" %d/%d ", index + 1, count) -// }) -// -func (s Style) BorderTopFunc(p Position, bf BorderFunc) Style { - fns := make([]interface{}, 3) - fns[posIndex(p)] = bf - s.set(borderTopFuncKey, fns) - return s -} - -// BorderBottomFunc set the bottom border decoration such as a status. -// The first argument is the position, it accepts Left, Right, and Center. -// -// the second argument is -// func(width int, middle string) string +// BorderDecoration set the border decoration such as a title or status. +// The argument is a BorderDecoration. // // examples: // -// // Set a title with dynamic text from a function -// lipgloss.NewStyle(). -// Border(lipgloss.NormalBorder()). -// BorderBottomFunc(lipgloss.Right, -// func(width int, middle string) string { -// return fmt.Sprintf(" %d/%d ", index + 1, count) -// }) -// -func (s Style) BorderBottomFunc(p Position, bf BorderFunc) Style { - fns := make([]interface{}, 3) - fns[posIndex(p)] = bf - s.set(borderBottomFuncKey, fns) +// // Set a title with plain text +// lipgloss.NewStyle(). +// Border(lipgloss.NormalBorder()). +// BorderDecoration(lipgloss.NewBorderDecoration( +// lipgloss.Top, +// lipgloss.Center, +// "Title", +// )) +// +// // Set a title with reverse styled text +// lipgloss.NewStyle(). +// Border(lipgloss.NormalBorder()). +// BorderDecoration(lipgloss.NewBorderDecoration( +// lipgloss.Top, +// lipgloss.Center, +// lipgloss.NewStyle().Reverse(true).SetString("Title").String, +// )) +// +// // Set a title with dynamic text from a function +// lipgloss.NewStyle(). +// Border(lipgloss.NormalBorder()). +// BorderDecoration(lipgloss.NewBorderDecoration( +// lipgloss.Bottom, +// lipgloss.Center, +// func(width int, middle string) string { +// return fmt.Sprintf(" %d/%d ", index + 1, count) +// }, +// )) +func (s Style) BorderDecoration(bd BorderDecoration) Style { + switch bd.side { + case Top: + s.set(borderTopDecorationKey, bd) + case Bottom: + s.set(borderBottomDecorationKey, bd) + } return s } diff --git a/style.go b/style.go index e01e304e..3738aeb9 100644 --- a/style.go +++ b/style.go @@ -69,8 +69,8 @@ const ( borderBottomBackgroundKey borderLeftBackgroundKey - borderTopFuncKey - borderBottomFuncKey + borderTopDecorationKey + borderBottomDecorationKey inlineKey maxWidthKey diff --git a/unset.go b/unset.go index 7c073413..7aec783a 100644 --- a/unset.go +++ b/unset.go @@ -282,13 +282,22 @@ func (s Style) UnsetBorderLeftBackground() Style { return s } -func (s Style) UnsetBorderBottomFunc() Style { - s.unset(borderBottomFuncKey) +// UnsetBorderDecorationTop removes all the border decorations. +func (s Style) UnsetBorderDecoration() Style { + s.unset(borderTopDecorationKey) + s.unset(borderBottomDecorationKey) return s } -func (s Style) UnsetBorderTopFunc() Style { - s.unset(borderBottomFuncKey) +// UnsetBorderDecorationTop removes the border bottom decoration. +func (s Style) UnsetBorderBottomDecoration() Style { + s.unset(borderBottomDecorationKey) + return s +} + +// UnsetBorderTopDecoration removes the border top decoration. +func (s Style) UnsetBorderTopDecoration() Style { + s.unset(borderBottomDecorationKey) return s } From ca0259d89f73aa182d4ff099e75aa2fd08d1089d Mon Sep 17 00:00:00 2001 From: Tom Pacheco Date: Mon, 7 Oct 2024 19:08:44 -0400 Subject: [PATCH 08/14] fix: collisions and truncation rules in decorators. --- borders.go | 102 +++++++++++++++++++++++++++++++++++++----------- borders_test.go | 76 ++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 23 deletions(-) diff --git a/borders.go b/borders.go index 7e1c485a..e6bb25c6 100644 --- a/borders.go +++ b/borders.go @@ -410,7 +410,13 @@ func (s Style) applyBorder(str string) string { if hasTop { var top string if len(topFuncs) > 0 { - top = renderAnnotatedHorizontalEdge(border.TopLeft, border.Top, border.TopRight, topFuncs, width) + top = renderAnnotatedHorizontalEdge( + border.TopLeft, + border.Top, + border.TopRight, + topFuncs, + width, + ) } else { top = renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width) } @@ -453,7 +459,13 @@ func (s Style) applyBorder(str string) string { if hasBottom { var bottom string if len(bottomFuncs) > 0 { - bottom = renderAnnotatedHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, bottomFuncs, width) + bottom = renderAnnotatedHorizontalEdge( + border.BottomLeft, + border.Bottom, + border.BottomRight, + bottomFuncs, + width, + ) } else { bottom = renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width) } @@ -465,6 +477,51 @@ func (s Style) applyBorder(str string) string { return out.String() } +// truncateWidths return the widths truncated to fit in the given +// length. +func truncateWidths(leftWidth, centerWidth, rightWidth, length int) (int, int, int) { + + leftWidth = min(leftWidth, length) + centerWidth = min(centerWidth, length) + rightWidth = min(rightWidth, length) + + if leftWidth == 0 && rightWidth == 0 { + return leftWidth, centerWidth, rightWidth + } + + if centerWidth == 0 { + if leftWidth == 0 || rightWidth == 0 || leftWidth+rightWidth < length { + return leftWidth, centerWidth, rightWidth + } + for leftWidth+rightWidth >= length { + if leftWidth > rightWidth { + leftWidth-- + } else { + rightWidth-- + } + } + return leftWidth, centerWidth, rightWidth + } + + for leftWidth >= length/2-(centerWidth+1)/2 { + if leftWidth > centerWidth { + leftWidth-- + } else { + centerWidth-- + } + } + + for rightWidth >= (length+1)/2-centerWidth/2 { + if rightWidth > centerWidth { + rightWidth-- + } else { + centerWidth-- + } + } + + return leftWidth, centerWidth, rightWidth +} + // Render the horizontal (top or bottom) portion of a border. func renderAnnotatedHorizontalEdge(left, middle, right string, bFuncs []interface{}, width int) string { if middle == "" { @@ -473,28 +530,27 @@ func renderAnnotatedHorizontalEdge(left, middle, right string, bFuncs []interfac ts := make([]string, 3) ws := make([]int, 3) - for i, f := range bFuncs { - if f == nil { - continue - } - remainingWidth := width - if remainingWidth < 1 { - break - } - switch f := f.(type) { - case string: - ts[i] = ansi.Truncate(f, remainingWidth, "") - ws[i] = ansi.StringWidth(ts[i]) - remainingWidth -= ws[i] - case func(int, string) string: - ts[i] = f(remainingWidth, middle) - ts[i] = ansi.Truncate(ts[i], remainingWidth, "") - ws[i] = ansi.StringWidth(ts[i]) - remainingWidth -= ws[i] - case func() string: - ts[i] = ansi.Truncate(f(), remainingWidth, "") + + // get the decoration strings and truncate to fit within + // the width. + { + for i, f := range bFuncs { + if f == nil { + continue + } + switch f := f.(type) { + case string: + ts[i] = f + case func() string: + ts[i] = f() + case func(int, string) string: + ts[i] = f(width, middle) + } ws[i] = ansi.StringWidth(ts[i]) - remainingWidth -= ws[i] + } + ws[0], ws[1], ws[2] = truncateWidths(ws[0], ws[1], ws[2], width) + for i := range ts { + ts[i] = ansi.Truncate(ts[i], ws[i], "") } } diff --git a/borders_test.go b/borders_test.go index 1c15fc9d..6e4c6b67 100644 --- a/borders_test.go +++ b/borders_test.go @@ -10,6 +10,19 @@ func TestBorderFunc(t *testing.T) { style Style expected string }{ + { + name: "trunc all string", + text: "", + style: NewStyle(). + Width(16). + Border(NormalBorder()). + BorderDecoration(NewBorderDecoration(Top, Left, "LeftLeftLeftLeft")). + BorderDecoration(NewBorderDecoration(Top, Center, "CenterCenterCenter")). + BorderDecoration(NewBorderDecoration(Top, Right, "RightRightRightRight")), + expected: `┌LeftL─Cent─Right┐ +│ │ +└────────────────┘`, + }, { name: "top left title string", text: "", @@ -244,3 +257,66 @@ func TestBorders(t *testing.T) { } } + +func TestTruncateWidths(t *testing.T) { + + tt := []struct { + name string + widths [3]int + width int + expected [3]int + }{ + { + name: "lll-cc-rrr", + widths: [3]int{10, 10, 10}, + width: 10, + expected: [3]int{3, 2, 3}, + }, + { + name: "lll-ccc-rrr", + widths: [3]int{10, 10, 10}, + width: 12, + expected: [3]int{3, 3, 4}, + }, + { + name: "lllll-rrrr", + widths: [3]int{10, 0, 10}, + width: 10, + expected: [3]int{5, 0, 4}, + }, + { + name: "lllllll-rr", + widths: [3]int{10, 0, 2}, + width: 10, + expected: [3]int{7, 0, 2}, + }, + { + name: "ll-rrrrrrr", + widths: [3]int{2, 0, 20}, + width: 10, + expected: [3]int{2, 0, 7}, + }, + { + name: "lll-cc----", + widths: [3]int{10, 10, 0}, + width: 10, + expected: [3]int{3, 2, 0}, + }, + { + name: "----cc-rrr", + widths: [3]int{0, 10, 10}, + width: 10, + expected: [3]int{0, 3, 3}, + }, + } + + for i, tc := range tt { + var result [3]int + + result[0], result[1], result[2] = truncateWidths(tc.widths[0], tc.widths[1], tc.widths[2], tc.width) + if result != tc.expected { + t.Errorf("Test %d, expected:`%v`Actual output:`%v`", i, tc.expected, result) + } + } + +} From 31377717b5d1aa4865b4b0c3c1a0ebf69dcb7633 Mon Sep 17 00:00:00 2001 From: Tom Pacheco Date: Mon, 7 Oct 2024 19:15:49 -0400 Subject: [PATCH 09/14] style: remove extra whitespace --- borders.go | 1 - 1 file changed, 1 deletion(-) diff --git a/borders.go b/borders.go index e6bb25c6..d2e95120 100644 --- a/borders.go +++ b/borders.go @@ -480,7 +480,6 @@ func (s Style) applyBorder(str string) string { // truncateWidths return the widths truncated to fit in the given // length. func truncateWidths(leftWidth, centerWidth, rightWidth, length int) (int, int, int) { - leftWidth = min(leftWidth, length) centerWidth = min(centerWidth, length) rightWidth = min(rightWidth, length) From 55d80be4538bb83e413262237537f4bf617d3420 Mon Sep 17 00:00:00 2001 From: Tom Pacheco Date: Tue, 8 Oct 2024 18:12:04 -0400 Subject: [PATCH 10/14] doc: fix example to current api --- borders.go | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/borders.go b/borders.go index d2e95120..95e66398 100644 --- a/borders.go +++ b/borders.go @@ -37,15 +37,27 @@ type Border struct { // reverseStyle := lipgloss.NewStyle().Reverse(true) // t := lipgloss.NewStyle(). // Border(lipgloss.NormalBorder()). -// BorderTopFunc(lipgloss.Center, func(w int, m string) string { -// return reverseStyle.Render(" BIG TITLE ") -// }). -// BorderBottomFunc(lipgloss.Right, func(width int, middle string) string { -// return reverseStyle.Render(fmt.Sprintf(" %d/%d ", m.index + 1, m.count)) + middle -// }). -// BorderBottomFunc(lipgloss.Left, func(width int, middle string) string { -// return middle + reverseStyle.Render(fmt.Sprintf("Status: %s", m.status)) -// }) +// BorderDecoration(lipgloss.NewBorderDecoration( +// lipgloss.Top, +// lipgloss.Center, +// func(w int, m string) string { +// return reverseStyle.Render(" BIG TITLE ") +// }, +// )). +// BorderDecoration(lipgloss.NewBorderDecoration( +// lipgloss.Bottom, +// lipgloss.Right, +// func(width int, middle string) string { +// return reverseStyle.Render(fmt.Sprintf(" %d/%d ", m.index + 1, m.count)) + middle +// }, +// )). +// BorderDecoration(lipgloss.NewBorderDecoration( +// lipgloss.Bottom, +// lipgloss.Left, +// func(width int, middle string) string { +// return middle + reverseStyle.Render(fmt.Sprintf("Status: %s", m.status)) +// }, +// )) type BorderHorizontalFunc interface { func(width int, middle string) string } From 42b149dfc1da5ee330429733e7fcaf2822d92e86 Mon Sep 17 00:00:00 2001 From: Tom Pacheco Date: Wed, 9 Oct 2024 19:04:13 -0400 Subject: [PATCH 11/14] refactor: introduce BorderSide for BorderDecorations location. Position was not specific enough, could not distinguish between Top and Left, so could not use for side borders. BorderSide currently enum, could possibly be a flag. --- borders.go | 28 ++++++++++++++++++++-------- borders_test.go | 30 +++++++++++++++--------------- set.go | 4 ++-- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/borders.go b/borders.go index 95e66398..4b743b16 100644 --- a/borders.go +++ b/borders.go @@ -26,6 +26,18 @@ type Border struct { MiddleBottom string } +// BorderSide represents a side of the block. It's used in selection, alignment, +// joining, placement and so on. +type BorderSide int + +// BorderSide instances. +const ( + BorderTop BorderSide = iota + BorderRight + BorderBottom + BorderLeft +) + // BorderHorizontalFunc is border function that sets horizontal border text // at the configured position. // @@ -38,21 +50,21 @@ type Border struct { // t := lipgloss.NewStyle(). // Border(lipgloss.NormalBorder()). // BorderDecoration(lipgloss.NewBorderDecoration( -// lipgloss.Top, +// lipgloss.BorderTop, // lipgloss.Center, // func(w int, m string) string { // return reverseStyle.Render(" BIG TITLE ") // }, // )). // BorderDecoration(lipgloss.NewBorderDecoration( -// lipgloss.Bottom, +// lipgloss.BorderBottom, // lipgloss.Right, // func(width int, middle string) string { // return reverseStyle.Render(fmt.Sprintf(" %d/%d ", m.index + 1, m.count)) + middle // }, // )). // BorderDecoration(lipgloss.NewBorderDecoration( -// lipgloss.Bottom, +// lipgloss.BorderBottom, // lipgloss.Left, // func(width int, middle string) string { // return middle + reverseStyle.Render(fmt.Sprintf("Status: %s", m.status)) @@ -64,7 +76,7 @@ type BorderHorizontalFunc interface { // BorderDecoration is type used by Border to set text or decorate the border. type BorderDecoration struct { - side Position + side BorderSide align Position st interface{} } @@ -93,23 +105,23 @@ type BorderDecorator interface { // t := lipgloss.NewStyle(). // Border(lipgloss.NormalBorder()). // BorderDecoration(lipgloss.NewBorderDecoration( -// lipgloss.Top, +// lipgloss.BorderTop, // lipgloss.Center, // reverseStyle.Padding(0, 1).Render("BIG TITLE"), // )). // BorderDecoration(lipgloss.NewBorderDecoration( -// lipgloss.Bottom, +// lipgloss.BorderBottom, // lipgloss.Right, // func(width int, middle string) string { // return reverseStyle.Render(fmt.Sprintf(" %d/%d ", m.index + 1, m.count)) + middle // }, // )). // BorderDecoration(lipgloss.NewBorderDecoration( -// lipgloss.Bottom, +// lipgloss.BorderBottom, // lipgloss.Left, // reverseStyle.SetString(fmt.Sprintf("Status: %s", m.status)).String, // )) -func NewBorderDecoration[S BorderDecorator](side, align Position, st S) BorderDecoration { +func NewBorderDecoration[S BorderDecorator](side BorderSide, align Position, st S) BorderDecoration { return BorderDecoration{ align: align, side: side, diff --git a/borders_test.go b/borders_test.go index 6e4c6b67..b39599ce 100644 --- a/borders_test.go +++ b/borders_test.go @@ -16,9 +16,9 @@ func TestBorderFunc(t *testing.T) { style: NewStyle(). Width(16). Border(NormalBorder()). - BorderDecoration(NewBorderDecoration(Top, Left, "LeftLeftLeftLeft")). - BorderDecoration(NewBorderDecoration(Top, Center, "CenterCenterCenter")). - BorderDecoration(NewBorderDecoration(Top, Right, "RightRightRightRight")), + BorderDecoration(NewBorderDecoration(BorderTop, Left, "LeftLeftLeftLeft")). + BorderDecoration(NewBorderDecoration(BorderTop, Center, "CenterCenterCenter")). + BorderDecoration(NewBorderDecoration(BorderTop, Right, "RightRightRightRight")), expected: `┌LeftL─Cent─Right┐ │ │ └────────────────┘`, @@ -29,7 +29,7 @@ func TestBorderFunc(t *testing.T) { style: NewStyle(). Width(10). Border(NormalBorder()). - BorderDecoration(NewBorderDecoration(Top, Left, "TITLE")), + BorderDecoration(NewBorderDecoration(BorderTop, Left, "TITLE")), expected: `┌TITLE─────┐ │ │ └──────────┘`, @@ -40,7 +40,7 @@ func TestBorderFunc(t *testing.T) { style: NewStyle(). Width(10). Border(NormalBorder()). - BorderDecoration(NewBorderDecoration(Top, Left, NewStyle().SetString("TITLE").String)), + BorderDecoration(NewBorderDecoration(BorderTop, Left, NewStyle().SetString("TITLE").String)), expected: `┌TITLE─────┐ │ │ └──────────┘`, @@ -51,7 +51,7 @@ func TestBorderFunc(t *testing.T) { style: NewStyle(). Width(10). Border(NormalBorder()). - BorderDecoration(NewBorderDecoration(Top, Left, NewStyle().SetString("TitleTitleTitle").String)), + BorderDecoration(NewBorderDecoration(BorderTop, Left, NewStyle().SetString("TitleTitleTitle").String)), expected: `┌TitleTitle┐ │ │ └──────────┘`, @@ -63,7 +63,7 @@ func TestBorderFunc(t *testing.T) { Width(10). Border(NormalBorder()). BorderDecoration(NewBorderDecoration( - Top, + BorderTop, Left, func(width int, middle string) string { return "TITLE" @@ -80,7 +80,7 @@ func TestBorderFunc(t *testing.T) { Width(10). Border(NormalBorder()). BorderDecoration(NewBorderDecoration( - Top, + BorderTop, Center, func(width int, middle string) string { return "TITLE" @@ -97,7 +97,7 @@ func TestBorderFunc(t *testing.T) { Width(11). Border(NormalBorder()). BorderDecoration(NewBorderDecoration( - Top, + BorderTop, Center, func(width int, middle string) string { return "TITLE" @@ -114,7 +114,7 @@ func TestBorderFunc(t *testing.T) { Width(10). Border(NormalBorder()). BorderDecoration(NewBorderDecoration( - Top, + BorderTop, Right, func(width int, middle string) string { return "TITLE" @@ -131,7 +131,7 @@ func TestBorderFunc(t *testing.T) { Width(10). Border(NormalBorder()). BorderDecoration(NewBorderDecoration( - Bottom, + BorderBottom, Left, func(width int, middle string) string { return "STATUS" @@ -148,7 +148,7 @@ func TestBorderFunc(t *testing.T) { Width(10). Border(NormalBorder()). BorderDecoration(NewBorderDecoration( - Bottom, + BorderBottom, Center, func(width int, middle string) string { return "STATUS" @@ -165,7 +165,7 @@ func TestBorderFunc(t *testing.T) { Width(11). Border(NormalBorder()). BorderDecoration(NewBorderDecoration( - Bottom, + BorderBottom, Center, func(width int, middle string) string { return "STATUS" @@ -182,7 +182,7 @@ func TestBorderFunc(t *testing.T) { Width(10). Border(NormalBorder()). BorderDecoration(NewBorderDecoration( - Bottom, + BorderBottom, Right, func(width int, middle string) string { return "STATUS" @@ -199,7 +199,7 @@ func TestBorderFunc(t *testing.T) { Width(12). Border(NormalBorder()). BorderDecoration(NewBorderDecoration( - Bottom, + BorderBottom, Right, func(width int, middle string) string { return "│STATUS│" + middle diff --git a/set.go b/set.go index 922d483b..e5f805fb 100644 --- a/set.go +++ b/set.go @@ -670,9 +670,9 @@ func posIndex(p Position) int { // )) func (s Style) BorderDecoration(bd BorderDecoration) Style { switch bd.side { - case Top: + case BorderTop: s.set(borderTopDecorationKey, bd) - case Bottom: + case BorderBottom: s.set(borderBottomDecorationKey, bd) } return s From 4d34ffed622984aef40df17754b7ea48efe538fe Mon Sep 17 00:00:00 2001 From: Tom Pacheco Date: Wed, 9 Oct 2024 19:06:32 -0400 Subject: [PATCH 12/14] fix: update border decorations sample --- examples/border/decorations/main.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/border/decorations/main.go b/examples/border/decorations/main.go index e09367a4..c69e0705 100644 --- a/examples/border/decorations/main.go +++ b/examples/border/decorations/main.go @@ -25,19 +25,19 @@ func main() { Height(10). Border(lipgloss.NormalBorder()). BorderDecoration(lipgloss.NewBorderDecoration( - lipgloss.Top, + lipgloss.BorderTop, lipgloss.Center, reverseStyle.Padding(0, 1).Render("BIG TITLE"), )). BorderDecoration(lipgloss.NewBorderDecoration( - lipgloss.Bottom, + lipgloss.BorderBottom, lipgloss.Right, func(width int, middle string) string { return reverseStyle.Render(fmt.Sprintf("[%d/%d]", m.index+1, m.count)) + middle }, )). BorderDecoration(lipgloss.NewBorderDecoration( - lipgloss.Bottom, + lipgloss.BorderBottom, lipgloss.Left, reverseStyle.Padding(0, 1).SetString(fmt.Sprintf("Status: %s", m.status)).String, )) @@ -45,5 +45,4 @@ func main() { fmt.Println() fmt.Println(t) fmt.Println() - } From 28c86f486d0a93846f832a880b89697c590adf91 Mon Sep 17 00:00:00 2001 From: Tom Pacheco Date: Fri, 11 Oct 2024 07:50:06 -0400 Subject: [PATCH 13/14] feat: added support for decorating vertical borders --- borders.go | 225 +++++++++++++++++++++++++--- borders_test.go | 33 ++++ examples/border/decorations/main.go | 15 ++ set.go | 12 ++ style.go | 4 + 5 files changed, 269 insertions(+), 20 deletions(-) diff --git a/borders.go b/borders.go index 4b743b16..3b33188a 100644 --- a/borders.go +++ b/borders.go @@ -74,6 +74,42 @@ type BorderHorizontalFunc interface { func(width int, middle string) string } +// BorderVerticalFunc is border function that sets vertical border text +// at the configured position. +// +// The first argument is the current row index, the second argument is +// the height of the border and the third is the Left/Right border string. +// It should return the border string for the given row. +// +// Example: +// +// reverseStyle := lipgloss.NewStyle().Reverse(true) +// t := lipgloss.NewStyle(). +// Border(lipgloss.NormalBorder()). +// BorderDecoration(lipgloss.NewBorderDecoration( +// lipgloss.BorderLeft, +// lipgloss.Top, +// func(row, height int, m string) string { +// if row % 2 == 1 { +// return "X" +// } +// return m +// }, +// )). +// BorderDecoration(lipgloss.NewBorderDecoration( +// lipgloss.BorderRight, +// lipgloss.Top, +// func(row, height int, middle string) string { +// if row == index { +// return "<" +// } +// return middle +// }, +// )) +type BorderVerticalFunc interface { + func(row, height int, middle string) string +} + // BorderDecoration is type used by Border to set text or decorate the border. type BorderDecoration struct { side BorderSide @@ -84,7 +120,7 @@ type BorderDecoration struct { // BorderDecorator is constraint type for a string or function that is used // to decorate a border. type BorderDecorator interface { - string | func() string | BorderHorizontalFunc + string | func() string | BorderHorizontalFunc | BorderVerticalFunc } // NewBorderDecoration is function that sets creates a decoration for the border. @@ -354,6 +390,8 @@ func (s Style) applyBorder(str string) string { topFuncs = s.borderTopFunc bottomFuncs = s.borderBottomFunc + leftFuncs = s.borderLeftFunc + rightFuncs = s.borderRightFunc ) // If a border is set and no sides have been specifically turned on or off @@ -449,33 +487,58 @@ func (s Style) applyBorder(str string) string { out.WriteRune('\n') } - leftRunes := []rune(border.Left) - leftIndex := 0 + leftBorder := make([]string, len(lines)) + rightBorder := make([]string, len(lines)) + + if hasLeft { + leftRunes := make([]rune, 0, len(lines)) + for len(leftRunes) < len(lines) { + left := []rune(border.Left) + leftRunes = append(leftRunes, left...) + } + leftRunes = leftRunes[:len(lines)] + for i := range leftRunes { + leftBorder[i] = string(leftRunes[i]) + } + if len(leftFuncs) > 0 { + leftBorder = renderVerticalEdge( + leftBorder, + border.Left, + leftFuncs, + ) + } + } - rightRunes := []rune(border.Right) - rightIndex := 0 + if hasRight { + rightRunes := make([]rune, 0, len(lines)) + right := []rune(border.Right) + for len(rightRunes) < len(lines) { + rightRunes = append(rightRunes, right...) + } + rightRunes = rightRunes[:len(lines)] + for i := range rightRunes { + rightBorder[i] = string(rightRunes[i]) + } + if len(rightFuncs) > 0 { + rightBorder = renderVerticalEdge( + rightBorder, + border.Right, + rightFuncs, + ) + } + } // Render sides for i, l := range lines { + if i > 0 { + out.WriteRune('\n') + } if hasLeft { - r := string(leftRunes[leftIndex]) - leftIndex++ - if leftIndex >= len(leftRunes) { - leftIndex = 0 - } - out.WriteString(s.styleBorder(r, leftFG, leftBG)) + out.WriteString(s.styleBorder(leftBorder[i], 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') + out.WriteString(s.styleBorder(rightBorder[i], rightFG, rightBG)) } } @@ -545,6 +608,62 @@ func truncateWidths(leftWidth, centerWidth, rightWidth, length int) (int, int, i return leftWidth, centerWidth, rightWidth } +func renderVerticalEdge(edge []string, middle string, bFuncs []interface{}) []string { + + height := len(edge) + + var transformer func(int, int, string) string + + ts := make([]string, 3) + ws := make([]int, 3) + + // get the decoration strings and truncate to fit within + // the width. + { + for i, f := range bFuncs { + if f == nil { + continue + } + switch f := f.(type) { + case string: + ts[i] = f + case func() string: + ts[i] = f() + case func(int, string) string: + ts[i] = f(height, middle) + case func(int, int, string) string: + transformer = f + } + ws[i] = ansi.StringWidth(ts[i]) + } + ws[0], ws[1], ws[2] = truncateWidths(ws[0], ws[1], ws[2], height) + for i := range ts { + ts[i] = ansi.Truncate(ts[i], ws[i], "") + } + } + + if ws[0] > 0 { + copy(edge[0:], splitStyledString(ts[0])) + } + if ws[1] > 0 { + copy(edge[(height-ws[1])/2:], splitStyledString(ts[1])) + } + if ws[2] > 0 { + copy(edge[height-ws[2]:], splitStyledString(ts[2])) + } + + if transformer != nil { + // transform + for i := range edge { + w := ansi.StringWidth(edge[i]) + edge[i] = transformer(i, height, edge[i]) + edge[i] = ansi.Truncate(edge[i], w, "") + } + } + + return edge +} + // Render the horizontal (top or bottom) portion of a border. func renderAnnotatedHorizontalEdge(left, middle, right string, bFuncs []interface{}, width int) string { if middle == "" { @@ -666,3 +785,69 @@ func getFirstRuneAsString(str string) string { r := []rune(str) return string(r[0]) } + +// splitStyledString wraps a string to lines of width 1. +// If there are styles they copied to each line. +// Style support is very simple and assumes a single style is applied +// to the entire string. Internal styles are stripped. +func splitStyledString(s string) []string { + + x := ansi.Strip(s) + if x == s { + // string has no styles so can just split it. + return strings.Split(s, "") + } + + lines := strings.Split(ansi.Wrap(s, 1, ""), "\n") + + { + // temporary until ansi.Wrap is fixed. + // + // ansi.Wrap has issues wrapping a limit of 1 + // this is to split the 2 characters + // + + allLines := make([]string, len(x)) + for i := range lines { + line := ansi.Strip(lines[i]) + if len(line) == 0 { + // there was a trailing \n with possible styles + // so append the to last item + n := len(allLines) - 1 + allLines[n] += lines[i] + continue + } + if len(line) == 1 { + allLines[i*2] = lines[i] + continue + } + if line == lines[i] { // no styles + allLines[i*2] = line[:1] + allLines[i*2+1] = line[1:] + continue + } + j := strings.Index(lines[i], line) + allLines[i*2] = lines[i][:j+1] + allLines[i*2+1] = lines[i][j+1:] + } + lines = allLines + } + + prefix := "" + if i := strings.Index(lines[0], ansi.Strip(lines[0])); i > 0 { + prefix = lines[0][:i] + lines[0] = lines[0][i:] + } + + suffix := "" + n := len(lines) - 1 + if i := len(ansi.Strip(lines[n])); i < len(lines[n]) { + suffix = lines[n][i:] + lines[n] = lines[n][:i] + } + + for i := range lines { + lines[i] = prefix + ansi.Strip(lines[i]) + suffix + } + return lines +} diff --git a/borders_test.go b/borders_test.go index b39599ce..77d3bbf3 100644 --- a/borders_test.go +++ b/borders_test.go @@ -320,3 +320,36 @@ func TestTruncateWidths(t *testing.T) { } } + +func TestSplitStyledString(t *testing.T) { + + tt := []struct { + input string + expected []string + }{ + { + input: "abc", + expected: []string{"a", "b", "c"}, + }, + { + input: "\x1b[41mabc\x1b[0m", + expected: []string{"\x1b[41ma\x1b[0m", "\x1b[41mb\x1b[0m", "\x1b[41mc\x1b[0m"}, + }, + { + input: "VERTICAL", + expected: []string{"V", "E", "R", "T", "I", "C", "A", "L"}, + }, + } + + for i, tc := range tt { + got := splitStyledString(tc.input) + if len(got) != len(tc.expected) { + t.Errorf("Test %d expected:`%v`Actual output:`%v`", i, tc.expected, got) + } + for i := range got { + if got[i] != tc.expected[i] { + t.Errorf("Item %d, expected:`%q`Actual output:`%q`", i, tc.expected[i], got[i]) + } + } + } +} diff --git a/examples/border/decorations/main.go b/examples/border/decorations/main.go index c69e0705..f6efe133 100644 --- a/examples/border/decorations/main.go +++ b/examples/border/decorations/main.go @@ -40,6 +40,21 @@ func main() { lipgloss.BorderBottom, lipgloss.Left, reverseStyle.Padding(0, 1).SetString(fmt.Sprintf("Status: %s", m.status)).String, + )). + BorderDecoration(lipgloss.NewBorderDecoration( + lipgloss.BorderLeft, + lipgloss.Center, + reverseStyle.SetString("VERTICAL").String, + )). + BorderDecoration(lipgloss.NewBorderDecoration( + lipgloss.BorderRight, + lipgloss.Top, + func(row int, width int, middle string) string { + if row == 6 { + return "▶" + } + return middle + }, )) fmt.Println() diff --git a/set.go b/set.go index e5f805fb..a8b03e68 100644 --- a/set.go +++ b/set.go @@ -59,6 +59,10 @@ func (s *Style) set(key propKey, value interface{}) { s.borderTopFunc = addBorderFunc(s.borderTopFunc, value.(BorderDecoration)) case borderBottomDecorationKey: s.borderBottomFunc = addBorderFunc(s.borderBottomFunc, value.(BorderDecoration)) + case borderLeftDecorationKey: + s.borderLeftFunc = addBorderFunc(s.borderLeftFunc, value.(BorderDecoration)) + case borderRightDecorationKey: + s.borderRightFunc = addBorderFunc(s.borderRightFunc, value.(BorderDecoration)) case maxWidthKey: s.maxWidth = max(0, value.(int)) case maxHeightKey: @@ -145,6 +149,10 @@ func (s *Style) setFrom(key propKey, i Style) { s.borderTopFunc = mergeBorderFunc(s.borderTopFunc, i.borderTopFunc) case borderBottomDecorationKey: s.borderBottomFunc = mergeBorderFunc(s.borderBottomFunc, i.borderBottomFunc) + case borderLeftDecorationKey: + s.borderLeftFunc = mergeBorderFunc(s.borderLeftFunc, i.borderLeftFunc) + case borderRightDecorationKey: + s.borderRightFunc = mergeBorderFunc(s.borderRightFunc, i.borderRightFunc) case maxWidthKey: s.set(maxWidthKey, i.maxWidth) case maxHeightKey: @@ -674,6 +682,10 @@ func (s Style) BorderDecoration(bd BorderDecoration) Style { s.set(borderTopDecorationKey, bd) case BorderBottom: s.set(borderBottomDecorationKey, bd) + case BorderLeft: + s.set(borderLeftDecorationKey, bd) + case BorderRight: + s.set(borderRightDecorationKey, bd) } return s } diff --git a/style.go b/style.go index 3738aeb9..da01b3e4 100644 --- a/style.go +++ b/style.go @@ -71,6 +71,8 @@ const ( borderTopDecorationKey borderBottomDecorationKey + borderRightDecorationKey + borderLeftDecorationKey inlineKey maxWidthKey @@ -156,6 +158,8 @@ type Style struct { borderLeftBgColor TerminalColor borderTopFunc []interface{} borderBottomFunc []interface{} + borderLeftFunc []interface{} + borderRightFunc []interface{} maxWidth int maxHeight int From 12a1304e7bebea919f47db9310ce0c98200c8931 Mon Sep 17 00:00:00 2001 From: Tom Pacheco Date: Fri, 11 Oct 2024 09:17:01 -0400 Subject: [PATCH 14/14] style: fix linter issues --- borders.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/borders.go b/borders.go index 3b33188a..6f3cd5a0 100644 --- a/borders.go +++ b/borders.go @@ -609,7 +609,6 @@ func truncateWidths(leftWidth, centerWidth, rightWidth, length int) (int, int, i } func renderVerticalEdge(edge []string, middle string, bFuncs []interface{}) []string { - height := len(edge) var transformer func(int, int, string) string @@ -791,7 +790,6 @@ func getFirstRuneAsString(str string) string { // Style support is very simple and assumes a single style is applied // to the entire string. Internal styles are stripped. func splitStyledString(s string) []string { - x := ansi.Strip(s) if x == s { // string has no styles so can just split it.