180 lines
5.1 KiB
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)
|
|
}
|
|
}
|