termui-tree/tree.go

180 lines
5.1 KiB
Go

package main
import (
"image"
"github.com/gizak/termui/v3"
)
const indentWidth = 4
// TreeWidget is a renderable tree
type TreeWidget struct {
termui.Block
rootNodes []*Node
selectedNode *Node
}
// Node represents an item in the tree widget
type Node struct {
Text string
children []*Node
tree *TreeWidget
parent *Node
next *Node
prev *Node
}
// NewTreeWidget makes a new tree widget
func NewTreeWidget(rootNodes []*Node) *TreeWidget {
tree := &TreeWidget{
Block: *termui.NewBlock(),
}
tree.selectedNode = rootNodes[0]
for _, rootNode := range rootNodes {
tree.AddRootNode(rootNode)
}
return tree
}
// Draw draws the tree widget
func (tree *TreeWidget) Draw(buffer *termui.Buffer) {
tree.Block.Draw(buffer)
row := tree.Block.Inner.Min.Y
for _, item := range tree.rootNodes {
row = tree.renderSingleTree(item, buffer, row)
}
}
// AddRootNode adds a new top-level node to the tree
func (tree *TreeWidget) AddRootNode(node *Node) {
if len(tree.rootNodes) > 0 {
node.prev = tree.rootNodes[len(tree.rootNodes)-1]
node.prev.next = node
} else if len(tree.rootNodes) == 0 {
// If this is our first node, then we need to mark this as the current node
tree.selectedNode = node
}
tree.rootNodes = append(tree.rootNodes, node)
node.setTree(tree)
}
// SelectPrev marks the previous node as the currently selected node
func (tree *TreeWidget) SelectPrev() {
if tree.selectedNode.prev != nil && len(tree.selectedNode.prev.children) == 0 {
// If there's a previous node with no children, we can go straight to it.
tree.selectedNode = tree.selectedNode.prev
} else if tree.selectedNode.prev == nil && tree.selectedNode.parent != nil {
// If we can go right up to the next parent node, we're good - we go there.
tree.selectedNode = tree.selectedNode.parent
} else if tree.selectedNode.prev != nil {
// If we have a node before us, we're going to want to go to the absolute bottom of it.
cursorNode := tree.selectedNode.prev
for len(cursorNode.children) > 0 {
cursorNode = cursorNode.children[len(cursorNode.children)-1]
}
tree.selectedNode = cursorNode
}
}
// SelectNext marks the next node as the currently selected node
func (tree *TreeWidget) SelectNext() {
if len(tree.selectedNode.children) > 0 {
tree.selectedNode = tree.selectedNode.children[0]
} else if tree.selectedNode.next != nil {
tree.selectedNode = tree.selectedNode.next
} else if tree.selectedNode.next == nil && tree.selectedNode.parent != nil {
cursorNode := tree.selectedNode.parent
// Go up to the next adjacent node to the first parent node with another node adjacent to it.
for cursorNode != nil {
if cursorNode.next != nil {
tree.selectedNode = cursorNode.next
return
}
cursorNode = cursorNode.parent
}
}
// If all else fails, we must be at the end of the list, and there's nothing else to select.
}
// renderSingleTree renders a node and all its descendents
// Returns the number of rows needed to render it (inculding wrapping)
func (tree *TreeWidget) renderSingleTree(root *Node, buffer *termui.Buffer, startRow int) int {
baseWidth := tree.Inner.Max.X - tree.Inner.Min.X
depths := map[*Node]int{root: 0}
toVisit := []*Node{root}
row := startRow
for len(toVisit) > 0 {
var node *Node
toVisit, node = toVisit[:len(toVisit)-1], toVisit[len(toVisit)-1]
currentDepth := depths[node]
// Add the children to explore next in reverse order.
// The next off the stack should be the direct descendent of the parent.
for i := len(node.children) - 1; i >= 0; i-- {
child := node.children[i]
toVisit = append(toVisit, child)
depths[child] = currentDepth + 1
}
// Render this level of tree
width := baseWidth - currentDepth*indentWidth
startingCol := tree.Inner.Min.X + currentDepth*indentWidth
row += node.render(buffer, row, startingCol, width)
}
return row
}
// render renders this node in the term ui, and return the number of rows that were required to render it
func (node *Node) render(buffer *termui.Buffer, row, col, width int) int {
style := termui.Style{
Fg: termui.ColorRed,
Bg: termui.ColorClear,
}
if node.tree.selectedNode == node {
style.Fg, style.Bg = style.Bg, style.Fg
}
listItem := termui.ParseStyles(node.Text, style)
wrappedItem := termui.WrapCells(listItem, uint(width))
x := col
rowOffset := 0
for _, cell := range wrappedItem {
// If we're wrapped, we don't need to render anything, just go to the next line
if cell.Rune == '\n' {
rowOffset++
x = col
} else {
buffer.SetCell(cell, image.Point{x, row + rowOffset})
x++
}
}
return rowOffset + 1
}
// AddChild marks the given node as a child, and itself as the child node's parent
func (node *Node) AddChild(child *Node) {
child.parent = node
if node.children == nil {
node.children = []*Node{child}
} else {
node.children = append(node.children, child)
}
child.tree = node.tree
if len(node.children) > 1 {
lastNode := node.children[len(node.children)-2]
lastNode.next = child
child.prev = lastNode
}
}
func (node *Node) setTree(tree *TreeWidget) {
node.tree = tree
for _, child := range node.children {
child.setTree(tree)
}
}