package cview import ( "sync" "github.com/gdamore/tcell/v2" ) // Tree navigation events. const ( treeNone int = iota treeHome treeEnd treeUp treeDown treePageUp treePageDown ) // TreeNode represents one node in a tree view. type TreeNode struct { // The reference object. reference interface{} // This node's child nodes. children []*TreeNode // The item's text. text string // The text color. color tcell.Color // Whether or not this node can be focused and selected. selectable bool // Whether or not this node's children should be displayed. expanded bool // The additional horizontal indent of this node's text. indent int // An optional function which is called when the user focuses this node. focused func() // An optional function which is called when the user selects this node. selected func() // Temporary member variables. parent *TreeNode // The parent node (nil for the root). level int // The hierarchy level (0 for the root, 1 for its children, and so on). graphicsX int // The x-coordinate of the left-most graphics rune. textX int // The x-coordinate of the first rune of the text. sync.RWMutex } // NewTreeNode returns a new tree node. func NewTreeNode(text string) *TreeNode { return &TreeNode{ text: text, color: Styles.PrimaryTextColor, indent: 2, expanded: true, selectable: true, } } // Walk traverses this node's subtree in depth-first, pre-order (NLR) order and // calls the provided callback function on each traversed node (which includes // this node) with the traversed node and its parent node (nil for this node). // The callback returns whether traversal should continue with the traversed // node's child nodes (true) or not recurse any deeper (false). func (n *TreeNode) Walk(callback func(node, parent *TreeNode) bool) { n.Lock() defer n.Unlock() n.walk(callback) } func (n *TreeNode) walk(callback func(node, parent *TreeNode) bool) { n.parent = nil nodes := []*TreeNode{n} for len(nodes) > 0 { // Pop the top node and process it. node := nodes[len(nodes)-1] nodes = nodes[:len(nodes)-1] if !callback(node, node.parent) { // Don't add any children. continue } // Add children in reverse order. for index := len(node.children) - 1; index >= 0; index-- { node.children[index].parent = node nodes = append(nodes, node.children[index]) } } } // SetReference allows you to store a reference of any type in this node. This // will allow you to establish a mapping between the TreeView hierarchy and your // internal tree structure. func (n *TreeNode) SetReference(reference interface{}) { n.Lock() defer n.Unlock() n.reference = reference } // GetReference returns this node's reference object. func (n *TreeNode) GetReference() interface{} { n.RLock() defer n.RUnlock() return n.reference } // SetChildren sets this node's child nodes. func (n *TreeNode) SetChildren(childNodes []*TreeNode) { n.Lock() defer n.Unlock() n.children = childNodes } // GetText returns this node's text. func (n *TreeNode) GetText() string { n.RLock() defer n.RUnlock() return n.text } // GetChildren returns this node's children. func (n *TreeNode) GetChildren() []*TreeNode { n.RLock() defer n.RUnlock() return n.children } // ClearChildren removes all child nodes from this node. func (n *TreeNode) ClearChildren() { n.Lock() defer n.Unlock() n.children = nil } // AddChild adds a new child node to this node. func (n *TreeNode) AddChild(node *TreeNode) { n.Lock() defer n.Unlock() n.children = append(n.children, node) } // SetSelectable sets a flag indicating whether this node can be focused and // selected by the user. func (n *TreeNode) SetSelectable(selectable bool) { n.Lock() defer n.Unlock() n.selectable = selectable } // SetFocusedFunc sets the function which is called when the user navigates to // this node. // // This function is also called when the user selects this node. func (n *TreeNode) SetFocusedFunc(handler func()) { n.Lock() defer n.Unlock() n.focused = handler } // SetSelectedFunc sets a function which is called when the user selects this // node by hitting Enter when it is focused. func (n *TreeNode) SetSelectedFunc(handler func()) { n.Lock() defer n.Unlock() n.selected = handler } // SetExpanded sets whether or not this node's child nodes should be displayed. func (n *TreeNode) SetExpanded(expanded bool) { n.Lock() defer n.Unlock() n.expanded = expanded } // Expand makes the child nodes of this node appear. func (n *TreeNode) Expand() { n.Lock() defer n.Unlock() n.expanded = true } // Collapse makes the child nodes of this node disappear. func (n *TreeNode) Collapse() { n.Lock() defer n.Unlock() n.expanded = false } // ExpandAll expands this node and all descendent nodes. func (n *TreeNode) ExpandAll() { n.Walk(func(node, parent *TreeNode) bool { node.expanded = true return true }) } // CollapseAll collapses this node and all descendent nodes. func (n *TreeNode) CollapseAll() { n.Walk(func(node, parent *TreeNode) bool { n.expanded = false return true }) } // IsExpanded returns whether the child nodes of this node are visible. func (n *TreeNode) IsExpanded() bool { n.RLock() defer n.RUnlock() return n.expanded } // SetText sets the node's text which is displayed. func (n *TreeNode) SetText(text string) { n.Lock() defer n.Unlock() n.text = text } // GetColor returns the node's color. func (n *TreeNode) GetColor() tcell.Color { n.RLock() defer n.RUnlock() return n.color } // SetColor sets the node's text color. func (n *TreeNode) SetColor(color tcell.Color) { n.Lock() defer n.Unlock() n.color = color } // SetIndent sets an additional indentation for this node's text. A value of 0 // keeps the text as far left as possible with a minimum of line graphics. Any // value greater than that moves the text to the right. func (n *TreeNode) SetIndent(indent int) { n.Lock() defer n.Unlock() n.indent = indent } // TreeView displays tree structures. A tree consists of nodes (TreeNode // objects) where each node has zero or more child nodes and exactly one parent // node (except for the root node which has no parent node). // // The SetRoot() function is used to specify the root of the tree. Other nodes // are added locally to the root node or any of its descendents. See the // TreeNode documentation for details on node attributes. (You can use // SetReference() to store a reference to nodes of your own tree structure.) // // Nodes can be focused by calling SetCurrentNode(). The user can navigate the // selection or the tree by using the following keys: // // - j, down arrow, right arrow: Move (the selection) down by one node. // - k, up arrow, left arrow: Move (the selection) up by one node. // - g, home: Move (the selection) to the top. // - G, end: Move (the selection) to the bottom. // - Ctrl-F, page down: Move (the selection) down by one page. // - Ctrl-B, page up: Move (the selection) up by one page. // // Selected nodes can trigger the "selected" callback when the user hits Enter. // // The root node corresponds to level 0, its children correspond to level 1, // their children to level 2, and so on. Per default, the first level that is // displayed is 0, i.e. the root node. You can call SetTopLevel() to hide // levels. // // If graphics are turned on (see SetGraphics()), lines indicate the tree's // hierarchy. Alternative (or additionally), you can set different prefixes // using SetPrefixes() for different levels, for example to display hierarchical // bullet point lists. type TreeView struct { *Box // The root node. root *TreeNode // The currently focused node or nil if no node is focused. currentNode *TreeNode // The movement to be performed during the call to Draw(), one of the // constants defined above. movement int // The top hierarchical level shown. (0 corresponds to the root level.) topLevel int // Strings drawn before the nodes, based on their level. prefixes [][]byte // Vertical scroll offset. offsetY int // If set to true, all node texts will be aligned horizontally. align bool // If set to true, the tree structure is drawn using lines. graphics bool // The text color for selected items. selectedTextColor *tcell.Color // The background color for selected items. selectedBackgroundColor *tcell.Color // The color of the lines. graphicsColor tcell.Color // Visibility of the scroll bar. scrollBarVisibility ScrollBarVisibility // The scroll bar color. scrollBarColor tcell.Color // An optional function called when the focused tree item changes. changed func(node *TreeNode) // An optional function called when a tree item is selected. selected func(node *TreeNode) // An optional function called when the user moves away from this primitive. done func(key tcell.Key) // The visible nodes, top-down, as set by process(). nodes []*TreeNode sync.RWMutex } // NewTreeView returns a new tree view. func NewTreeView() *TreeView { return &TreeView{ Box: NewBox(), scrollBarVisibility: ScrollBarAuto, graphics: true, graphicsColor: Styles.GraphicsColor, scrollBarColor: Styles.ScrollBarColor, } } // SetRoot sets the root node of the tree. func (t *TreeView) SetRoot(root *TreeNode) { t.Lock() defer t.Unlock() t.root = root } // GetRoot returns the root node of the tree. If no such node was previously // set, nil is returned. func (t *TreeView) GetRoot() *TreeNode { t.RLock() defer t.RUnlock() return t.root } // SetCurrentNode focuses a node or, when provided with nil, clears focus. // Selected nodes must be visible and selectable, or else the selection will be // changed to the top-most selectable and visible node. // // This function does NOT trigger the "changed" callback. 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() } } // GetCurrentNode returns the currently selected node or nil of no node is // currently selected. func (t *TreeView) GetCurrentNode() *TreeNode { t.RLock() defer t.RUnlock() return t.currentNode } // SetTopLevel sets the first tree level that is visible with 0 referring to the // root, 1 to the root's child nodes, and so on. Nodes above the top level are // not displayed. func (t *TreeView) SetTopLevel(topLevel int) { t.Lock() defer t.Unlock() t.topLevel = topLevel } // SetPrefixes defines the strings drawn before the nodes' texts. This is a // slice of strings where each element corresponds to a node's hierarchy level, // i.e. 0 for the root, 1 for the root's children, and so on (levels will // cycle). // // For example, to display a hierarchical list with bullet points: // // treeView.SetGraphics(false). // SetPrefixes([]string{"* ", "- ", "x "}) 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]) } } // SetAlign controls the horizontal alignment of the node texts. If set to true, // all texts except that of top-level nodes will be placed in the same column. // If set to false, they will indent with the hierarchy. func (t *TreeView) SetAlign(align bool) { t.Lock() defer t.Unlock() t.align = align } // SetGraphics sets a flag which determines whether or not line graphics are // drawn to illustrate the tree's hierarchy. func (t *TreeView) SetGraphics(showGraphics bool) { t.Lock() defer t.Unlock() t.graphics = showGraphics } // SetSelectedTextColor sets the text color of selected items. func (t *TreeView) SetSelectedTextColor(color tcell.Color) { t.Lock() defer t.Unlock() t.selectedTextColor = &color } // SetSelectedBackgroundColor sets the background color of selected items. func (t *TreeView) SetSelectedBackgroundColor(color tcell.Color) { t.Lock() defer t.Unlock() t.selectedBackgroundColor = &color } // SetGraphicsColor sets the colors of the lines used to draw the tree structure. func (t *TreeView) SetGraphicsColor(color tcell.Color) { t.Lock() defer t.Unlock() t.graphicsColor = color } // SetScrollBarVisibility specifies the display of the scroll bar. func (t *TreeView) SetScrollBarVisibility(visibility ScrollBarVisibility) { t.Lock() defer t.Unlock() t.scrollBarVisibility = visibility } // SetScrollBarColor sets the color of the scroll bar. func (t *TreeView) SetScrollBarColor(color tcell.Color) { t.Lock() defer t.Unlock() t.scrollBarColor = color } // SetChangedFunc sets the function which is called when the user navigates to // a new tree node. func (t *TreeView) SetChangedFunc(handler func(node *TreeNode)) { t.Lock() defer t.Unlock() t.changed = handler } // SetSelectedFunc sets the function which is called when the user selects a // node by pressing Enter on the current selection. func (t *TreeView) SetSelectedFunc(handler func(node *TreeNode)) { t.Lock() defer t.Unlock() t.selected = handler } // SetDoneFunc sets a handler which is called whenever the user presses the // Escape, Tab, or Backtab key. func (t *TreeView) SetDoneFunc(handler func(key tcell.Key)) { t.Lock() defer t.Unlock() t.done = handler } // GetScrollOffset returns the number of node rows that were skipped at the top // of the tree view. Note that when the user navigates the tree view, this value // is only updated after the tree view has been redrawn. func (t *TreeView) GetScrollOffset() int { t.RLock() defer t.RUnlock() return t.offsetY } // GetRowCount returns the number of "visible" nodes. This includes nodes which // fall outside the tree view's box but notably does not include the children // of collapsed nodes. Note that this value is only up to date after the tree // view has been drawn. func (t *TreeView) GetRowCount() int { t.RLock() defer t.RUnlock() return len(t.nodes) } // Transform modifies the current selection. func (t *TreeView) Transform(tr Transformation) { t.Lock() defer t.Unlock() switch tr { case TransformFirstItem: t.movement = treeHome case TransformLastItem: t.movement = treeEnd case TransformPreviousItem: t.movement = treeUp case TransformNextItem: t.movement = treeDown case TransformPreviousPage: t.movement = treePageUp case TransformNextPage: t.movement = treePageDown } t.process() } // process builds the visible tree, populates the "nodes" slice, and processes // pending selection actions. func (t *TreeView) process() { _, _, _, height := t.GetInnerRect() // Determine visible nodes and their placement. var graphicsOffset, maxTextX int t.nodes = nil selectedIndex := -1 topLevelGraphicsX := -1 if t.graphics { graphicsOffset = 1 } t.root.walk(func(node, parent *TreeNode) bool { // Set node attributes. node.parent = parent 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 { // Without graphics, we align nodes on the first column. node.textX = 0 } if node.level == t.topLevel { // No graphics for top level nodes. node.graphicsX = 0 node.textX = 0 } // Add the node to the list. if node.level >= t.topLevel { // This node will be visible. if node.textX > maxTextX { maxTextX = node.textX } if node == t.currentNode && node.selectable { selectedIndex = len(t.nodes) } // Maybe we want to skip this level. if t.topLevel == node.level && (topLevelGraphicsX < 0 || node.graphicsX < topLevelGraphicsX) { topLevelGraphicsX = node.graphicsX } t.nodes = append(t.nodes, node) } // Recurse if desired. return node.expanded }) // Post-process positions. for _, node := range t.nodes { // If text must align, we correct the positions. if t.align && node.level > t.topLevel { node.textX = maxTextX } // If we skipped levels, shift to the left. if topLevelGraphicsX > 0 { node.graphicsX -= topLevelGraphicsX node.textX -= topLevelGraphicsX } } // Process selection. (Also trigger events if necessary.) if selectedIndex >= 0 { // Move the selection. 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] if newSelectedIndex != selectedIndex { t.movement = treeNone 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 // Move selection into viewport. if selectedIndex-t.offsetY >= height { t.offsetY = selectedIndex - height + 1 } if selectedIndex < t.offsetY { t.offsetY = selectedIndex } } else { // If selection is not visible or selectable, select the first candidate. 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 } } } // Draw draws this primitive onto the screen. 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() // Scroll the tree. x, y, width, height := t.GetInnerRect() switch t.movement { 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 // Fix invalid offsets. if t.offsetY >= len(t.nodes)-height { t.offsetY = len(t.nodes) - height } if t.offsetY < 0 { t.offsetY = 0 } // Calculate scroll bar position. rows := len(t.nodes) cursor := int(float64(rows) * (float64(t.offsetY) / float64(rows-height))) // Draw the tree. posY := y lineStyle := tcell.StyleDefault.Background(t.backgroundColor).Foreground(t.graphicsColor) for index, node := range t.nodes { // Skip invisible parts. if posY >= y+height { break } if index < t.offsetY { continue } // Draw the graphics. if t.graphics { // Draw ancestor branches. ancestor := node.parent for ancestor != nil && ancestor.parent != nil && ancestor.parent.level >= t.topLevel { if ancestor.graphicsX >= width { continue } // Draw a branch if this ancestor is not a last child. if ancestor.parent.children[len(ancestor.parent.children)-1] != 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 { // Connect to the node above. 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) } // Join this node. 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) } } } } // Draw the prefix and the text. if node.textX < width && posY < y+height { // Prefix. var prefixWidth int if len(t.prefixes) > 0 { _, prefixWidth = Print(screen, t.prefixes[(node.level-t.topLevel)%len(t.prefixes)], x+node.textX, posY, width-node.textX, AlignLeft, node.color) } // Text. if node.textX+prefixWidth < width { style := tcell.StyleDefault.Foreground(node.color) 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) } } // Draw scroll bar. RenderScrollBar(screen, t.scrollBarVisibility, x+(width-1), posY, height, rows, cursor, posY-y, t.hasFocus, t.scrollBarColor) // Advance. posY++ } } // InputHandler returns the handler for this primitive. 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() // Because the tree is flattened into a list only at drawing time, we also // postpone the (selection) movement to drawing time. 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, Keys.MovePreviousField) { t.movement = treeUp } else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2, Keys.MoveNextField) { t.movement = treeDown } 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() }) } // MouseHandler returns the mouse handler for this primitive. 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 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 = treeUp consumed = true case MouseScrollDown: t.movement = treeDown consumed = true } return }) }