package cview
import (
"regexp"
"slices"
"strings"
"sync"
"github.com/gdamore/tcell/v2"
)
const (
treeNone int = iota
treeHome
treeEnd
treeUp
treeDown
treePageUp
treePageDown
treeScrollUp
treeScrollDown
)
type TreeNode struct {
reference interface {}
children []*TreeNode
text string
bold bool
underline bool
color tcell .Color
highlighted bool
selectable bool
expanded bool
indent int
focused func ()
selected func ()
parent *TreeNode
level int
graphicsX int
textX int
sync .RWMutex
}
func NewTreeNode (text string ) *TreeNode {
return &TreeNode {
text : text ,
color : Styles .PrimaryTextColor ,
indent : 2 ,
expanded : true ,
selectable : true ,
}
}
func (n *TreeNode ) Walk (callback func (node , parent *TreeNode , depth int ) bool ) {
n .walk (callback )
}
func (n *TreeNode ) GetParent () *TreeNode {
n .RLock ()
defer n .RUnlock ()
return n .parent
}
func (n *TreeNode ) walk (callback func (
node , parent *TreeNode , depth int ) bool ) {
type nodeInfo struct {
node *TreeNode
level int
children int
parent *TreeNode
}
n .RLock ()
nodes := []*nodeInfo {{node : n , level : n .level , parent : n .parent }}
n .RUnlock ()
var processedNodes []*nodeInfo
for len (nodes ) > 0 {
current := nodes [len (nodes )-1 ]
nodes = nodes [:len (nodes )-1 ]
processedNodes = append (processedNodes , current )
current .node .RLock ()
children := current .node .children
for index := len (children ) - 1 ; index >= 0 ; index -- {
ch := children [index ]
nodes = append (nodes , &nodeInfo {
node : ch ,
level : current .level + 1 ,
children : len (ch .children ),
parent : current .node ,
})
}
current .node .RUnlock ()
}
nodes = processedNodes
for i := 0 ; i < len (nodes ); i ++ {
current := nodes [i ]
if !callback (current .node , current .parent , current .level ) {
if current .children == 0 {
continue
}
found := false
for ii := i + current .children ; ii < len (nodes ); ii ++ {
if nodes [ii ].level <= current .level {
i = ii - 1
found = true
break
}
}
if !found {
i = len (nodes ) - 1
}
}
}
}
func (n *TreeNode ) SetReference (reference interface {}) {
n .Lock ()
defer n .Unlock ()
n .reference = reference
}
func (n *TreeNode ) GetReference () interface {} {
n .RLock ()
defer n .RUnlock ()
return n .reference
}
func (n *TreeNode ) SetChildren (childNodes []*TreeNode ) {
n .Lock ()
defer n .Unlock ()
n .children = childNodes
}
var styleRegex = regexp .MustCompile (`\[([a-zA-Z:-]*)\]` )
func (n *TreeNode ) VisibleLength () int {
text := n .GetText ()
visible := styleRegex .ReplaceAllString (text , "" )
return len (visible )
}
func (n *TreeNode ) GetText () string {
n .RLock ()
defer n .RUnlock ()
return n .text
}
func (n *TreeNode ) GetChildren () []*TreeNode {
n .RLock ()
defer n .RUnlock ()
return n .children
}
func (n *TreeNode ) ClearChildren () {
n .Lock ()
defer n .Unlock ()
n .children = nil
}
func (n *TreeNode ) AddChild (node *TreeNode ) {
n .Lock ()
defer n .Unlock ()
n .children = append (n .children , node )
for _ , child := range n .children {
child .parent = n
}
}
func (n *TreeNode ) SetSelectable (selectable bool ) {
n .Lock ()
defer n .Unlock ()
n .selectable = selectable
}
func (n *TreeNode ) RemoveChild (node *TreeNode ) bool {
for index , child := range n .children {
if child == node {
n .children = append (n .children [:index ], n .children [index +1 :]...)
return true
}
}
return false
}
func (n *TreeNode ) SetFocusedFunc (handler func ()) {
n .Lock ()
defer n .Unlock ()
n .focused = handler
}
func (n *TreeNode ) SetSelectedFunc (handler func ()) {
n .Lock ()
defer n .Unlock ()
n .selected = handler
}
func (n *TreeNode ) SetExpanded (expanded bool ) {
n .Lock ()
defer n .Unlock ()
n .expanded = expanded
}
func (n *TreeNode ) Expand () {
n .Lock ()
defer n .Unlock ()
n .expanded = true
}
func (n *TreeNode ) Collapse () {
n .Lock ()
defer n .Unlock ()
n .expanded = false
}
func (n *TreeNode ) ExpandAll () {
n .Walk (func (node , parent *TreeNode , _ int ) bool {
node .Lock ()
defer node .Unlock ()
node .expanded = true
return true
})
}
func (n *TreeNode ) CollapseAll () {
n .Walk (func (node , parent *TreeNode , _ int ) bool {
node .Lock ()
defer node .Unlock ()
n .expanded = false
return true
})
}
func (n *TreeNode ) IsExpanded () bool {
n .RLock ()
defer n .RUnlock ()
return n .expanded
}
func (n *TreeNode ) SetText (text string ) {
n .Lock ()
defer n .Unlock ()
n .text = text
}
func (n *TreeNode ) SetBold (bold bool ) {
n .Lock ()
defer n .Unlock ()
n .bold = bold
}
func (n *TreeNode ) SetUnderline (underline bool ) {
n .Lock ()
defer n .Unlock ()
n .underline = underline
}
func (n *TreeNode ) GetColor () tcell .Color {
n .RLock ()
defer n .RUnlock ()
return n .color
}
func (n *TreeNode ) SetColor (color tcell .Color ) {
n .Lock ()
defer n .Unlock ()
n .color = color
}
func (n *TreeNode ) GetHighlighted () bool {
n .RLock ()
defer n .RUnlock ()
return n .highlighted
}
func (n *TreeNode ) SetHighlighted (state bool ) {
n .Lock ()
defer n .Unlock ()
n .highlighted = state
}
func (n *TreeNode ) SetIndent (indent int ) {
n .Lock ()
defer n .Unlock ()
n .indent = indent
}
func (n *TreeNode ) GetIndent () int {
n .Lock ()
defer n .Unlock ()
return n .indent
}
type TreeView struct {
*Box
root *TreeNode
currentNode *TreeNode
movement int
step int
topLevel int
prefixes [][]byte
offsetY int
align bool
graphics bool
highlightColor *tcell .Color
selectedTextColor *tcell .Color
selectedBackgroundColor *tcell .Color
graphicsColor tcell .Color
scrollBarVisibility ScrollBarVisibility
scrollBarColor tcell .Color
changed func (node *TreeNode )
selected func (node *TreeNode )
done func (key tcell .Key )
nodes []*TreeNode
stableNodes bool
sync .RWMutex
}
func NewTreeView () *TreeView {
return &TreeView {
Box : NewBox (),
scrollBarVisibility : ScrollBarAuto ,
graphics : true ,
graphicsColor : Styles .GraphicsColor ,
scrollBarColor : Styles .ScrollBarColor ,
}
}
func (t *TreeView ) SetRoot (root *TreeNode ) {
t .Lock ()
defer t .Unlock ()
t .root = root
}
func (t *TreeView ) GetRoot () *TreeNode {
t .RLock ()
defer t .RUnlock ()
return t .root
}
func (t *TreeView ) SetCurrentNode (node *TreeNode ) {
t .Lock ()
defer t .Unlock ()
t .currentNode = node
if t .currentNode .focused != nil {
t .Unlock ()
t .currentNode .focused ()
t .Lock ()
}
t .scrollCurrentIntoView (true )
}
func (t *TreeView ) ScrollCurrentIntoView (soft bool ) {
t .Lock ()
defer t .Unlock ()
t .scrollCurrentIntoView (soft )
}
func (t *TreeView ) scrollCurrentIntoView (soft bool ) {
currIdx := slices .Index (t .nodes , t .currentNode )
_ , _ , _ , height := t .GetInnerRect ()
height = max (height -1 , 0 )
if t .offsetY > currIdx {
correctedY := min (t .offsetY , currIdx )
t .offsetY = correctedY
} else if t .offsetY +height < currIdx {
correctedY := max (t .offsetY , currIdx )
if soft {
t .offsetY = correctedY - height /2
} else {
t .offsetY = correctedY
}
}
}
func (t *TreeView ) GetCurrentNode () *TreeNode {
t .RLock ()
defer t .RUnlock ()
return t .currentNode
}
func (t *TreeView ) GetPath (node *TreeNode ) []*TreeNode {
if t .root == nil {
return nil
}
var f func (current *TreeNode , path []*TreeNode ) []*TreeNode
f = func (current *TreeNode , path []*TreeNode ) []*TreeNode {
if current == node {
return path
}
for _ , child := range current .children {
newPath := make ([]*TreeNode , len (path ), len (path )+1 )
copy (newPath , path )
if p := f (child , append (newPath , child )); p != nil {
return p
}
}
return nil
}
return f (t .root , []*TreeNode {t .root })
}
func (t *TreeView ) SetTopLevel (topLevel int ) {
t .Lock ()
defer t .Unlock ()
t .topLevel = topLevel
}
func (t *TreeView ) SetPrefixes (prefixes []string ) {
t .Lock ()
defer t .Unlock ()
t .prefixes = make ([][]byte , len (prefixes ))
for i := range prefixes {
t .prefixes [i ] = []byte (prefixes [i ])
}
}
func (t *TreeView ) SetAlign (align bool ) {
t .Lock ()
defer t .Unlock ()
t .align = align
}
func (t *TreeView ) SetGraphics (showGraphics bool ) {
t .Lock ()
defer t .Unlock ()
t .graphics = showGraphics
}
func (t *TreeView ) SetHighlightColor (color tcell .Color ) {
t .Lock ()
defer t .Unlock ()
t .highlightColor = &color
}
func (t *TreeView ) SetSelectedTextColor (color tcell .Color ) {
t .Lock ()
defer t .Unlock ()
t .selectedTextColor = &color
}
func (t *TreeView ) SetSelectedBackgroundColor (color tcell .Color ) {
t .Lock ()
defer t .Unlock ()
t .selectedBackgroundColor = &color
}
func (t *TreeView ) SetGraphicsColor (color tcell .Color ) {
t .Lock ()
defer t .Unlock ()
t .graphicsColor = color
}
func (t *TreeView ) SetScrollBarVisibility (visibility ScrollBarVisibility ) {
t .Lock ()
defer t .Unlock ()
t .scrollBarVisibility = visibility
}
func (t *TreeView ) SetScrollBarColor (color tcell .Color ) {
t .Lock ()
defer t .Unlock ()
t .scrollBarColor = color
}
func (t *TreeView ) SetChangedFunc (handler func (node *TreeNode )) {
t .Lock ()
defer t .Unlock ()
t .changed = handler
}
func (t *TreeView ) SetSelectedFunc (handler func (node *TreeNode )) {
t .Lock ()
defer t .Unlock ()
t .selected = handler
}
func (t *TreeView ) SetDoneFunc (handler func (key tcell .Key )) {
t .Lock ()
defer t .Unlock ()
t .done = handler
}
func (t *TreeView ) GetScrollOffset () int {
t .RLock ()
defer t .RUnlock ()
return t .offsetY
}
func (t *TreeView ) GetRowCount () int {
t .RLock ()
defer t .RUnlock ()
return len (t .nodes )
}
func (t *TreeView ) Transform (tr Transformation ) {
t .Lock ()
defer t .Unlock ()
softScroll := false
switch tr {
case TransformFirstItem :
t .movement = treeHome
case TransformLastItem :
t .movement = treeEnd
case TransformPreviousItem :
t .movement = treeUp
softScroll = true
case TransformNextItem :
t .movement = treeDown
softScroll = true
case TransformPreviousPage :
t .movement = treePageUp
case TransformNextPage :
t .movement = treePageDown
}
t .process ()
t .scrollCurrentIntoView (softScroll )
}
func (t *TreeView ) process () {
_ , _ , _ , height := t .GetInnerRect ()
var graphicsOffset , maxTextX int
t .nodes = nil
selectedIndex := -1
topLevelGraphicsX := -1
if t .graphics {
graphicsOffset = 1
}
t .root .Walk (func (node , parent *TreeNode , _ int ) bool {
node .Lock ()
defer node .Unlock ()
if parent == nil {
node .level = 0
node .graphicsX = 0
node .textX = 0
} else {
node .level = parent .level + 1
node .graphicsX = parent .textX
node .textX = node .graphicsX + graphicsOffset + node .indent
}
if !t .graphics && t .align {
node .textX = 0
}
if node .level == t .topLevel {
node .graphicsX = 0
node .textX = 0
}
if node .level >= t .topLevel {
if node .textX > maxTextX {
maxTextX = node .textX
}
if node == t .currentNode && node .selectable {
selectedIndex = len (t .nodes )
}
if t .topLevel == node .level && (topLevelGraphicsX < 0 || node .graphicsX < topLevelGraphicsX ) {
topLevelGraphicsX = node .graphicsX
}
t .nodes = append (t .nodes , node )
}
return node .expanded
})
for _ , node := range t .nodes {
if t .align && node .level > t .topLevel {
node .textX = maxTextX
}
if topLevelGraphicsX > 0 {
node .graphicsX -= topLevelGraphicsX
node .textX -= topLevelGraphicsX
}
}
if selectedIndex >= 0 {
newSelectedIndex := selectedIndex
MovementSwitch :
switch t .movement {
case treeUp :
for newSelectedIndex > 0 {
newSelectedIndex --
if t .nodes [newSelectedIndex ].selectable {
break MovementSwitch
}
}
newSelectedIndex = selectedIndex
case treeDown :
for newSelectedIndex < len (t .nodes )-1 {
newSelectedIndex ++
if t .nodes [newSelectedIndex ].selectable {
break MovementSwitch
}
}
newSelectedIndex = selectedIndex
case treeHome :
for newSelectedIndex = 0 ; newSelectedIndex < len (t .nodes ); newSelectedIndex ++ {
if t .nodes [newSelectedIndex ].selectable {
break MovementSwitch
}
}
newSelectedIndex = selectedIndex
case treeEnd :
for newSelectedIndex = len (t .nodes ) - 1 ; newSelectedIndex >= 0 ; newSelectedIndex -- {
if t .nodes [newSelectedIndex ].selectable {
break MovementSwitch
}
}
newSelectedIndex = selectedIndex
case treePageDown :
if newSelectedIndex +height < len (t .nodes ) {
newSelectedIndex += height
} else {
newSelectedIndex = len (t .nodes ) - 1
}
for ; newSelectedIndex < len (t .nodes ); newSelectedIndex ++ {
if t .nodes [newSelectedIndex ].selectable {
break MovementSwitch
}
}
newSelectedIndex = selectedIndex
case treePageUp :
if newSelectedIndex >= height {
newSelectedIndex -= height
} else {
newSelectedIndex = 0
}
for ; newSelectedIndex >= 0 ; newSelectedIndex -- {
if t .nodes [newSelectedIndex ].selectable {
break MovementSwitch
}
}
newSelectedIndex = selectedIndex
}
t .currentNode = t .nodes [newSelectedIndex ]
moved := false
if newSelectedIndex != selectedIndex {
t .movement = treeNone
moved = true
if t .changed != nil {
t .Unlock ()
t .changed (t .currentNode )
t .Lock ()
}
if t .currentNode .focused != nil {
t .Unlock ()
t .currentNode .focused ()
t .Lock ()
}
}
selectedIndex = newSelectedIndex
if t .movement != treeScrollUp && t .movement != treeScrollDown && t .movement != treeNone && !moved {
if selectedIndex -t .offsetY >= height {
t .offsetY = selectedIndex - height + 1
}
if selectedIndex < t .offsetY {
t .offsetY = selectedIndex
}
}
} else {
if t .currentNode != nil {
for index , node := range t .nodes {
if node .selectable {
selectedIndex = index
t .currentNode = node
break
}
}
}
if selectedIndex < 0 {
t .currentNode = nil
}
}
}
func (t *TreeView ) Draw (screen tcell .Screen ) {
if !t .GetVisible () {
return
}
t .Box .Draw (screen )
t .Lock ()
defer t .Unlock ()
if t .root == nil {
return
}
t .process ()
x , y , width , height := t .GetInnerRect ()
switch t .movement {
case treeScrollUp :
t .offsetY -= height / 3
case treeScrollDown :
t .offsetY += height / 3
case treeUp :
t .offsetY --
case treeDown :
t .offsetY ++
case treeHome :
t .offsetY = 0
case treeEnd :
t .offsetY = len (t .nodes )
case treePageUp :
t .offsetY -= height
case treePageDown :
t .offsetY += height
}
t .movement = treeNone
if t .offsetY >= len (t .nodes )-height {
t .offsetY = len (t .nodes ) - height
}
if t .offsetY < 0 {
t .offsetY = 0
}
rows := len (t .nodes )
cursor := int (float64 (rows ) * (float64 (t .offsetY ) / float64 (rows -height )))
posY := y
for index , node := range t .nodes {
doBreak := false
func () {
node .Lock ()
defer node .Unlock ()
lineStyle := tcell .StyleDefault .Background (t .backgroundColor ).Foreground (t .graphicsColor )
if node .highlighted {
lineStyle = lineStyle .Background (*t .highlightColor )
}
if posY >= y +height {
doBreak = true
return
}
if index < t .offsetY {
return
}
space := ""
if width > 0 {
space = strings .Repeat (" " , width -1 )
}
PrintStyle (screen , []byte (space ), x , posY , width -1 , AlignLeft , lineStyle )
if t .graphics {
ancestor := node .parent
for ancestor != nil && ancestor .parent != nil && ancestor .parent .level >= t .topLevel {
if ancestor .graphicsX >= width {
return
}
idx := len (ancestor .parent .children ) - 1
if idx >= 0 && ancestor .parent .children [idx ] != ancestor {
if posY -1 >= y && ancestor .textX > ancestor .graphicsX {
PrintJoinedSemigraphics (screen , x +ancestor .graphicsX , posY -1 , Borders .Vertical , t .graphicsColor )
}
if posY < y +height {
screen .SetContent (x +ancestor .graphicsX , posY , Borders .Vertical , nil , lineStyle )
}
}
ancestor = ancestor .parent
}
if node .textX > node .graphicsX && node .graphicsX < width {
if posY -1 >= y && t .nodes [index -1 ].graphicsX <= node .graphicsX && t .nodes [index -1 ].textX > node .graphicsX {
PrintJoinedSemigraphics (screen , x +node .graphicsX , posY -1 , Borders .TopLeft , t .graphicsColor )
}
if posY < y +height {
screen .SetContent (x +node .graphicsX , posY , Borders .BottomLeft , nil , lineStyle )
for pos := node .graphicsX + 1 ; pos < node .textX && pos < width ; pos ++ {
screen .SetContent (x +pos , posY , Borders .Horizontal , nil , lineStyle )
}
}
}
}
if node .textX < width && posY < y +height {
var prefixWidth int
if len (t .prefixes ) > 0 {
_, prefixWidth = PrintStyle (screen , t .prefixes [(node .level -t .topLevel )%len (t .prefixes )], x +node .textX , posY , width -node .textX , AlignLeft , lineStyle .Foreground (node .color ))
}
if node .textX +prefixWidth < width {
style := tcell .StyleDefault .Foreground (node .color ).Bold (node .bold ).Underline (node .underline )
if node == t .currentNode {
backgroundColor := node .color
foregroundColor := t .backgroundColor
if t .selectedTextColor != nil {
foregroundColor = *t .selectedTextColor
}
if t .selectedBackgroundColor != nil {
backgroundColor = *t .selectedBackgroundColor
}
style = tcell .StyleDefault .Background (backgroundColor ).Foreground (foregroundColor )
}
PrintStyle (screen , []byte (node .text ), x +node .textX +prefixWidth , posY , width -node .textX -prefixWidth , AlignLeft , style )
}
}
RenderScrollBar (screen , t .scrollBarVisibility , x +(width -1 ), posY , height , rows , cursor , posY -y , t .hasFocus , t .scrollBarColor )
posY ++
}()
if doBreak {
break
}
}
}
func (t *TreeView ) SelectNode (node *TreeNode ) {
if node == nil {
return
}
t .Lock ()
defer t .Unlock ()
if t .selected != nil {
t .selected (node )
}
if node .focused != nil {
node .focused ()
}
if node .selected != nil {
node .selected ()
}
}
func (t *TreeView ) InputHandler () func (event *tcell .EventKey , setFocus func (p Primitive )) {
return t .WrapInputHandler (func (event *tcell .EventKey , setFocus func (p Primitive )) {
selectNode := func () {
t .Lock ()
currentNode := t .currentNode
t .Unlock ()
if currentNode == nil {
return
}
if t .selected != nil {
t .selected (currentNode )
}
if currentNode .focused != nil {
currentNode .focused ()
}
if currentNode .selected != nil {
currentNode .selected ()
}
}
t .Lock ()
defer t .Unlock ()
softScroll := false
if HitShortcut (event , Keys .Cancel , Keys .MovePreviousField , Keys .MoveNextField ) {
if t .done != nil {
t .Unlock ()
t .done (event .Key ())
t .Lock ()
}
} else if HitShortcut (event , Keys .MoveFirst , Keys .MoveFirst2 ) {
t .movement = treeHome
} else if HitShortcut (event , Keys .MoveLast , Keys .MoveLast2 ) {
t .movement = treeEnd
} else if HitShortcut (event , Keys .MoveUp , Keys .MoveUp2 ) {
t .movement = treeUp
softScroll = true
} else if HitShortcut (event , Keys .MoveDown , Keys .MoveDown2 ) {
t .movement = treeDown
softScroll = true
} else if HitShortcut (event , Keys .MovePreviousPage ) {
t .movement = treePageUp
} else if HitShortcut (event , Keys .MoveNextPage ) {
t .movement = treePageDown
} else if HitShortcut (event , Keys .Select , Keys .Select2 ) {
t .Unlock ()
selectNode ()
t .Lock ()
}
t .process ()
t .scrollCurrentIntoView (softScroll )
})
}
func (t *TreeView ) MouseHandler () func (
action MouseAction , event *tcell .EventMouse , setFocus func (p Primitive )) (consumed bool , capture Primitive ,
) {
return t .WrapMouseHandler (func (
action MouseAction , event *tcell .EventMouse , setFocus func (p Primitive ),
) (consumed bool , capture Primitive ) {
x , y := event .Position ()
if !t .InRect (x , y ) {
return false , nil
}
switch action {
case MouseLeftClick :
_ , rectY , _ , _ := t .GetInnerRect ()
y -= rectY
y += t .offsetY
if y >= 0 && y < len (t .nodes ) {
node := t .nodes [y ]
if node .selectable {
if t .currentNode != node && t .changed != nil {
t .changed (node )
}
if t .selected != nil {
t .selected (node )
}
t .currentNode = node
}
}
consumed = true
setFocus (t )
case MouseScrollUp :
t .movement = treeScrollUp
consumed = true
case MouseScrollDown :
t .movement = treeScrollDown
consumed = true
}
return
})
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .