diff --git a/examples/tree/background/main.go b/examples/tree/background/main.go index a38ca626..3e989d96 100644 --- a/examples/tree/background/main.go +++ b/examples/tree/background/main.go @@ -8,7 +8,7 @@ import ( ) func main() { - enumeratorStyle := lipgloss.NewStyle(). + darkBg := lipgloss.NewStyle(). Background(lipgloss.Color("0")). Padding(0, 1) @@ -23,7 +23,8 @@ func main() { t := tree.Root("# Table of Contents"). RootStyle(itemStyle). ItemStyle(itemStyle). - EnumeratorStyle(enumeratorStyle). + EnumeratorStyle(darkBg). + IndenterStyle(darkBg). Child( tree.Root("## Chapter 1"). Child("Chapter 1.1"). diff --git a/examples/tree/files/main.go b/examples/tree/files/main.go index 4435c5d1..1f79006f 100644 --- a/examples/tree/files/main.go +++ b/examples/tree/files/main.go @@ -59,6 +59,7 @@ func main() { } t := tree.Root(pwd). + IndenterStyle(enumeratorStyle). EnumeratorStyle(enumeratorStyle). RootStyle(itemStyle). ItemStyle(itemStyle) diff --git a/examples/tree/makeup/main.go b/examples/tree/makeup/main.go index b6dca57d..a3aa6b70 100644 --- a/examples/tree/makeup/main.go +++ b/examples/tree/makeup/main.go @@ -27,6 +27,7 @@ func main() { ). Enumerator(tree.RoundedEnumerator). EnumeratorStyle(enumeratorStyle). + IndenterStyle(enumeratorStyle). RootStyle(rootStyle). ItemStyle(itemStyle) diff --git a/examples/tree/rounded/main.go b/examples/tree/rounded/main.go index c32dae3a..d2aa7ec4 100644 --- a/examples/tree/rounded/main.go +++ b/examples/tree/rounded/main.go @@ -31,7 +31,10 @@ func main() { "Leek", "Artichoke", ), - ).ItemStyle(itemStyle).EnumeratorStyle(enumeratorStyle).Enumerator(tree.RoundedEnumerator) + ).ItemStyle(itemStyle). + EnumeratorStyle(enumeratorStyle). + Enumerator(tree.RoundedEnumerator). + IndenterStyle(enumeratorStyle) fmt.Println(t) } diff --git a/examples/tree/selection/main.go b/examples/tree/selection/main.go new file mode 100644 index 00000000..9fb9b8cf --- /dev/null +++ b/examples/tree/selection/main.go @@ -0,0 +1,143 @@ +package main + +import ( + "fmt" + "path" + + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/tree" +) + +const selected = "/Users/bash/.config/doom-emacs" + +type styles struct { + base, + container, + dir, + selected, + dimmed, + toggle lipgloss.Style +} + +func defaultStyles() styles { + var s styles + s.base = lipgloss.NewStyle(). + Background(lipgloss.Color("235")) + s.container = s.base. + Margin(1, 2). + Padding(1, 0) + s.dir = s.base. + Inline(true) + s.toggle = s.base. + Foreground(lipgloss.Color("5")). + PaddingRight(1) + s.selected = s.base. + Background(lipgloss.Color("8")). + Foreground(lipgloss.Color("207")). + Bold(true) + s.dimmed = s.base. + Foreground(lipgloss.Color("241")) + return s +} + +type dir struct { + name string + open bool + styles styles +} + +func (d dir) String() string { + t := d.styles.toggle.PaddingLeft(1).Render + n := d.styles.dir.Render + if d.open { + return t("▼") + n(d.name) + } + return t("▶") + n(d.name) +} + +// file implements the Node interface. +type file struct { + name string + styles styles +} + +func (s file) String() string { + return path.Base(s.name) +} + +func (s file) Hidden() bool { + return false +} + +func (s file) Children() tree.Children { + return tree.NodeChildren(nil) +} + +func (s file) Value() string { + return s.String() +} + +func isItemSelected(children tree.Children, index int) bool { + child := children.At(index) + if file, ok := child.(file); ok && file.name == selected { + return true + } + + return false +} + +func itemStyle(children tree.Children, index int) lipgloss.Style { + s := defaultStyles() + if isItemSelected(children, index) { + return s.selected + } + + return s.base +} + +func indenterStyle(children tree.Children, index int) lipgloss.Style { + s := defaultStyles() + if isItemSelected(children, index) { + return s.dimmed.Background(s.selected.GetBackground()) + } + + return s.dimmed +} + +func main() { + s := defaultStyles() + + t := tree.Root(dir{"~/charm", true, s}). + Child( + dir{"ayman", false, s}, + tree.Root(dir{"bash", true, s}). + Child( + file{"/Users/bash/.config/doom-emacs", s}, + ), + tree.Root(dir{"carlos", true, s}). + Child( + tree.Root(dir{"emotes", true, s}). + Child( + file{"/home/caarlos0/Pictures/chefkiss.png", s}, + file{"/home/caarlos0/Pictures/kekw.png", s}, + ), + ), + dir{"maas", false, s}, + ). + Width(30). + Indenter(Indenter). + Enumerator(Enumerator). + EnumeratorStyleFunc(indenterStyle). + IndenterStyleFunc(indenterStyle). + ItemStyleFunc(itemStyle) + + fmt.Println(s.container.Render(t.String())) +} + +func Enumerator(children tree.Children, index int) string { + return " │ " +} + +func Indenter(children tree.Children, index int) string { + return " │ " +} diff --git a/examples/tree/styles/main.go b/examples/tree/styles/main.go index 9950b1f4..4c9eebf5 100644 --- a/examples/tree/styles/main.go +++ b/examples/tree/styles/main.go @@ -17,10 +17,12 @@ func main() { "Claire’s Boutique", tree.Root("Nyx"). Child("Lip Gloss", "Foundation"). - EnumeratorStyle(pink), + EnumeratorStyle(pink). + IndenterStyle(purple), "Mac", "Milk", ). - EnumeratorStyle(purple) + EnumeratorStyle(purple). + IndenterStyle(purple) fmt.Println(t) } diff --git a/examples/tree/toggle/main.go b/examples/tree/toggle/main.go index 0664e98f..027d432b 100644 --- a/examples/tree/toggle/main.go +++ b/examples/tree/toggle/main.go @@ -10,7 +10,7 @@ import ( type styles struct { base, block, - enumerator, + pink, dir, toggle, file lipgloss.Style @@ -25,7 +25,7 @@ func defaultStyles() styles { Padding(1, 3). Margin(1, 3). Width(40) - s.enumerator = s.base. + s.pink = s.base. Foreground(lipgloss.Color("212")). PaddingRight(1) s.dir = s.base. @@ -66,7 +66,8 @@ func main() { t := tree.Root(dir{"~/charm", true, s}). Enumerator(tree.RoundedEnumerator). - EnumeratorStyle(s.enumerator). + IndenterStyle(s.pink). + EnumeratorStyle(s.pink). Child( dir{"ayman", false, s}, tree.Root(dir{"bash", true, s}). diff --git a/tree/renderer.go b/tree/renderer.go index 8fd86930..a6557677 100644 --- a/tree/renderer.go +++ b/tree/renderer.go @@ -12,6 +12,7 @@ type StyleFunc func(children Children, i int) lipgloss.Style // Style is the styling applied to the tree. type Style struct { enumeratorFunc StyleFunc + indenterFunc StyleFunc itemFunc StyleFunc root lipgloss.Style } @@ -23,6 +24,9 @@ func newRenderer() *renderer { enumeratorFunc: func(Children, int) lipgloss.Style { return lipgloss.NewStyle().PaddingRight(1) }, + indenterFunc: func(Children, int) lipgloss.Style { + return lipgloss.NewStyle() + }, itemFunc: func(Children, int) lipgloss.Style { return lipgloss.NewStyle() }, @@ -36,6 +40,7 @@ type renderer struct { style Style enumerator Enumerator indenter Indenter + width int } // render is responsible for actually rendering the tree. @@ -65,18 +70,22 @@ func (r *renderer) render(node Node, root bool, prefix string) string { if child.Hidden() { continue } - indent := indenter(children, i) - nodePrefix := enumerator(children, i) + indentStyle := r.style.indenterFunc(children, i) enumStyle := r.style.enumeratorFunc(children, i) itemStyle := r.style.itemFunc(children, i) - nodePrefix = enumStyle.Render(nodePrefix) + indent := indenter(children, i) + nodeIndent := indentStyle.Render(indent) + nodePrefix := enumStyle.Render(enumerator(children, i)) if l := maxLen - lipgloss.Width(nodePrefix); l > 0 { - nodePrefix = strings.Repeat(" ", l) + nodePrefix + nodePrefix = enumStyle.Render(strings.Repeat(" ", l)) + nodePrefix } item := itemStyle.Render(child.Value()) multineLinePrefix := prefix + if multineLinePrefix != "" { + multineLinePrefix = indentStyle.Render(multineLinePrefix) + } // This dance below is to account for multiline prefixes, e.g. "|\n|". // In that case, we need to make sure that both the parent prefix and @@ -85,32 +94,39 @@ func (r *renderer) render(node Node, root bool, prefix string) string { nodePrefix = lipgloss.JoinVertical( lipgloss.Left, nodePrefix, - enumStyle.Render(indent), + nodeIndent, ) } for lipgloss.Height(nodePrefix) > lipgloss.Height(multineLinePrefix) { multineLinePrefix = lipgloss.JoinVertical( lipgloss.Left, multineLinePrefix, - prefix, + indentStyle.Render(prefix), ) } + line := lipgloss.JoinHorizontal( + lipgloss.Top, + multineLinePrefix, + nodePrefix, + item, + ) + + // If the line is shorter than the desired width, we pad it with spaces. + if pad := r.width - lipgloss.Width(line); pad > 0 { + line = line + itemStyle.Render(strings.Repeat(" ", pad)) + } strs = append( strs, - lipgloss.JoinHorizontal( - lipgloss.Top, - multineLinePrefix, - nodePrefix, - item, - ), + line, ) if children.Length() > 0 { - // here we see if the child has a custom renderer, which means the - // user set a custom enumerator, style, etc. - // if it has one, we'll use it to render itself. + // Here we see if the child has a custom renderer, which means the + // user set a custom enumerator/indenter/item style, etc. + // If it has one, we'll use it to render itself. // otherwise, we keep using the current renderer. + // Note that the renderer doesn't inherit its parent's styles. renderer := r switch child := child.(type) { case *Tree: @@ -121,7 +137,7 @@ func (r *renderer) render(node Node, root bool, prefix string) string { if s := renderer.render( child, false, - prefix+enumStyle.Render(indent), + prefix+indent, ); s != "" { strs = append(strs, s) } diff --git a/tree/tree.go b/tree/tree.go index 94f5dd08..87e3eff0 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -51,7 +51,8 @@ func (Leaf) Children() Children { return NodeChildren(nil) } -// Value of a leaf node returns its value. +// Value of a leaf node returns its string representation. +// If the Leaf implements fmt.Stringer, it will return the value returned by it. func (s Leaf) Value() string { return s.value } @@ -62,6 +63,7 @@ func (s Leaf) Hidden() bool { } // String returns the string representation of a Leaf node. +// For leaf nodes, this is the same as Value. func (s Leaf) String() string { return s.Value() } @@ -109,6 +111,7 @@ func (t *Tree) Offset(start, end int) *Tree { } // Value returns the root name of this node. +// If the root implements fmt.Stringer, it will return the value returned by it. func (t *Tree) Value() string { return t.value } @@ -223,6 +226,34 @@ func (t *Tree) EnumeratorStyleFunc(fn StyleFunc) *Tree { return t } +// IndenterStyle sets a static style for all indenters. +// +// Use IndenterStyleFunc to conditionally set styles based on the tree node. +func (t *Tree) IndenterStyle(style lipgloss.Style) *Tree { + t.ensureRenderer().style.indenterFunc = func(Children, int) lipgloss.Style { + return style + } + return t +} + +// IndenterStyleFunc sets the indentation style function. Use this function +// for conditional styling. +// +// t := tree.New(). +// IndenterStyleFunc(func(_ tree.Children, i int) lipgloss.Style { +// if selected == i { +// return lipgloss.NewStyle().Foreground(hightlightColor) +// } +// return lipgloss.NewStyle().Foreground(dimColor) +// }) +func (t *Tree) IndenterStyleFunc(fn StyleFunc) *Tree { + if fn == nil { + fn = func(Children, int) lipgloss.Style { return lipgloss.NewStyle() } + } + t.ensureRenderer().style.indenterFunc = fn + return t +} + // RootStyle sets a style for the root element. func (t *Tree) RootStyle(style lipgloss.Style) *Tree { t.ensureRenderer().style.root = style @@ -292,6 +323,14 @@ func (t *Tree) Indenter(indenter Indenter) *Tree { return t } +// Width sets the tree width. +// +// Items will be padded to account for the entire width of the tree. +func (t *Tree) Width(width int) *Tree { + t.ensureRenderer().width = width + return t +} + // Children returns the children of a node. func (t *Tree) Children() Children { var data []Node