1package tview
2
3import (
4	"github.com/gdamore/tcell"
5)
6
7// Tree navigation events.
8const (
9	treeNone int = iota
10	treeHome
11	treeEnd
12	treeUp
13	treeDown
14	treePageUp
15	treePageDown
16)
17
18// TreeNode represents one node in a tree view.
19type TreeNode struct {
20	// The reference object.
21	reference interface{}
22
23	// This node's child nodes.
24	children []*TreeNode
25
26	// The item's text.
27	text string
28
29	// The text color.
30	color tcell.Color
31
32	// Whether or not this node can be selected.
33	selectable bool
34
35	// Whether or not this node's children should be displayed.
36	expanded bool
37
38	// The additional horizontal indent of this node's text.
39	indent int
40
41	// An optional function which is called when the user selects this node.
42	selected func()
43
44	// Temporary member variables.
45	parent    *TreeNode // The parent node (nil for the root).
46	level     int       // The hierarchy level (0 for the root, 1 for its children, and so on).
47	graphicsX int       // The x-coordinate of the left-most graphics rune.
48	textX     int       // The x-coordinate of the first rune of the text.
49}
50
51// NewTreeNode returns a new tree node.
52func NewTreeNode(text string) *TreeNode {
53	return &TreeNode{
54		text:       text,
55		color:      Styles.PrimaryTextColor,
56		indent:     2,
57		expanded:   true,
58		selectable: true,
59	}
60}
61
62// Walk traverses this node's subtree in depth-first, pre-order (NLR) order and
63// calls the provided callback function on each traversed node (which includes
64// this node) with the traversed node and its parent node (nil for this node).
65// The callback returns whether traversal should continue with the traversed
66// node's child nodes (true) or not recurse any deeper (false).
67func (n *TreeNode) Walk(callback func(node, parent *TreeNode) bool) *TreeNode {
68	n.parent = nil
69	nodes := []*TreeNode{n}
70	for len(nodes) > 0 {
71		// Pop the top node and process it.
72		node := nodes[len(nodes)-1]
73		nodes = nodes[:len(nodes)-1]
74		if !callback(node, node.parent) {
75			// Don't add any children.
76			continue
77		}
78
79		// Add children in reverse order.
80		for index := len(node.children) - 1; index >= 0; index-- {
81			node.children[index].parent = node
82			nodes = append(nodes, node.children[index])
83		}
84	}
85
86	return n
87}
88
89// SetReference allows you to store a reference of any type in this node. This
90// will allow you to establish a mapping between the TreeView hierarchy and your
91// internal tree structure.
92func (n *TreeNode) SetReference(reference interface{}) *TreeNode {
93	n.reference = reference
94	return n
95}
96
97// GetReference returns this node's reference object.
98func (n *TreeNode) GetReference() interface{} {
99	return n.reference
100}
101
102// SetChildren sets this node's child nodes.
103func (n *TreeNode) SetChildren(childNodes []*TreeNode) *TreeNode {
104	n.children = childNodes
105	return n
106}
107
108// GetText returns this node's text.
109func (n *TreeNode) GetText() string {
110	return n.text
111}
112
113// GetChildren returns this node's children.
114func (n *TreeNode) GetChildren() []*TreeNode {
115	return n.children
116}
117
118// ClearChildren removes all child nodes from this node.
119func (n *TreeNode) ClearChildren() *TreeNode {
120	n.children = nil
121	return n
122}
123
124// AddChild adds a new child node to this node.
125func (n *TreeNode) AddChild(node *TreeNode) *TreeNode {
126	n.children = append(n.children, node)
127	return n
128}
129
130// SetSelectable sets a flag indicating whether this node can be selected by
131// the user.
132func (n *TreeNode) SetSelectable(selectable bool) *TreeNode {
133	n.selectable = selectable
134	return n
135}
136
137// SetSelectedFunc sets a function which is called when the user selects this
138// node by hitting Enter when it is selected.
139func (n *TreeNode) SetSelectedFunc(handler func()) *TreeNode {
140	n.selected = handler
141	return n
142}
143
144// SetExpanded sets whether or not this node's child nodes should be displayed.
145func (n *TreeNode) SetExpanded(expanded bool) *TreeNode {
146	n.expanded = expanded
147	return n
148}
149
150// Expand makes the child nodes of this node appear.
151func (n *TreeNode) Expand() *TreeNode {
152	n.expanded = true
153	return n
154}
155
156// Collapse makes the child nodes of this node disappear.
157func (n *TreeNode) Collapse() *TreeNode {
158	n.expanded = false
159	return n
160}
161
162// ExpandAll expands this node and all descendent nodes.
163func (n *TreeNode) ExpandAll() *TreeNode {
164	n.Walk(func(node, parent *TreeNode) bool {
165		node.expanded = true
166		return true
167	})
168	return n
169}
170
171// CollapseAll collapses this node and all descendent nodes.
172func (n *TreeNode) CollapseAll() *TreeNode {
173	n.Walk(func(node, parent *TreeNode) bool {
174		n.expanded = false
175		return true
176	})
177	return n
178}
179
180// IsExpanded returns whether the child nodes of this node are visible.
181func (n *TreeNode) IsExpanded() bool {
182	return n.expanded
183}
184
185// SetText sets the node's text which is displayed.
186func (n *TreeNode) SetText(text string) *TreeNode {
187	n.text = text
188	return n
189}
190
191// GetColor returns the node's color.
192func (n *TreeNode) GetColor() tcell.Color {
193	return n.color
194}
195
196// SetColor sets the node's text color.
197func (n *TreeNode) SetColor(color tcell.Color) *TreeNode {
198	n.color = color
199	return n
200}
201
202// SetIndent sets an additional indentation for this node's text. A value of 0
203// keeps the text as far left as possible with a minimum of line graphics. Any
204// value greater than that moves the text to the right.
205func (n *TreeNode) SetIndent(indent int) *TreeNode {
206	n.indent = indent
207	return n
208}
209
210// TreeView displays tree structures. A tree consists of nodes (TreeNode
211// objects) where each node has zero or more child nodes and exactly one parent
212// node (except for the root node which has no parent node).
213//
214// The SetRoot() function is used to specify the root of the tree. Other nodes
215// are added locally to the root node or any of its descendents. See the
216// TreeNode documentation for details on node attributes. (You can use
217// SetReference() to store a reference to nodes of your own tree structure.)
218//
219// Nodes can be selected by calling SetCurrentNode(). The user can navigate the
220// selection or the tree by using the following keys:
221//
222//   - j, down arrow, right arrow: Move (the selection) down by one node.
223//   - k, up arrow, left arrow: Move (the selection) up by one node.
224//   - g, home: Move (the selection) to the top.
225//   - G, end: Move (the selection) to the bottom.
226//   - Ctrl-F, page down: Move (the selection) down by one page.
227//   - Ctrl-B, page up: Move (the selection) up by one page.
228//
229// Selected nodes can trigger the "selected" callback when the user hits Enter.
230//
231// The root node corresponds to level 0, its children correspond to level 1,
232// their children to level 2, and so on. Per default, the first level that is
233// displayed is 0, i.e. the root node. You can call SetTopLevel() to hide
234// levels.
235//
236// If graphics are turned on (see SetGraphics()), lines indicate the tree's
237// hierarchy. Alternative (or additionally), you can set different prefixes
238// using SetPrefixes() for different levels, for example to display hierarchical
239// bullet point lists.
240//
241// See https://github.com/rivo/tview/wiki/TreeView for an example.
242type TreeView struct {
243	*Box
244
245	// The root node.
246	root *TreeNode
247
248	// The currently selected node or nil if no node is selected.
249	currentNode *TreeNode
250
251	// The movement to be performed during the call to Draw(), one of the
252	// constants defined above.
253	movement int
254
255	// The top hierarchical level shown. (0 corresponds to the root level.)
256	topLevel int
257
258	// Strings drawn before the nodes, based on their level.
259	prefixes []string
260
261	// Vertical scroll offset.
262	offsetY int
263
264	// If set to true, all node texts will be aligned horizontally.
265	align bool
266
267	// If set to true, the tree structure is drawn using lines.
268	graphics bool
269
270	// The color of the lines.
271	graphicsColor tcell.Color
272
273	// An optional function which is called when the user has navigated to a new
274	// tree node.
275	changed func(node *TreeNode)
276
277	// An optional function which is called when a tree item was selected.
278	selected func(node *TreeNode)
279
280	// The visible nodes, top-down, as set by process().
281	nodes []*TreeNode
282}
283
284// NewTreeView returns a new tree view.
285func NewTreeView() *TreeView {
286	return &TreeView{
287		Box:           NewBox(),
288		graphics:      true,
289		graphicsColor: Styles.GraphicsColor,
290	}
291}
292
293// SetRoot sets the root node of the tree.
294func (t *TreeView) SetRoot(root *TreeNode) *TreeView {
295	t.root = root
296	return t
297}
298
299// GetRoot returns the root node of the tree. If no such node was previously
300// set, nil is returned.
301func (t *TreeView) GetRoot() *TreeNode {
302	return t.root
303}
304
305// SetCurrentNode sets the currently selected node. Provide nil to clear all
306// selections. Selected nodes must be visible and selectable, or else the
307// selection will be changed to the top-most selectable and visible node.
308//
309// This function does NOT trigger the "changed" callback.
310func (t *TreeView) SetCurrentNode(node *TreeNode) *TreeView {
311	t.currentNode = node
312	return t
313}
314
315// GetCurrentNode returns the currently selected node or nil of no node is
316// currently selected.
317func (t *TreeView) GetCurrentNode() *TreeNode {
318	return t.currentNode
319}
320
321// SetTopLevel sets the first tree level that is visible with 0 referring to the
322// root, 1 to the root's child nodes, and so on. Nodes above the top level are
323// not displayed.
324func (t *TreeView) SetTopLevel(topLevel int) *TreeView {
325	t.topLevel = topLevel
326	return t
327}
328
329// SetPrefixes defines the strings drawn before the nodes' texts. This is a
330// slice of strings where each element corresponds to a node's hierarchy level,
331// i.e. 0 for the root, 1 for the root's children, and so on (levels will
332// cycle).
333//
334// For example, to display a hierarchical list with bullet points:
335//
336//   treeView.SetGraphics(false).
337//     SetPrefixes([]string{"* ", "- ", "x "})
338func (t *TreeView) SetPrefixes(prefixes []string) *TreeView {
339	t.prefixes = prefixes
340	return t
341}
342
343// SetAlign controls the horizontal alignment of the node texts. If set to true,
344// all texts except that of top-level nodes will be placed in the same column.
345// If set to false, they will indent with the hierarchy.
346func (t *TreeView) SetAlign(align bool) *TreeView {
347	t.align = align
348	return t
349}
350
351// SetGraphics sets a flag which determines whether or not line graphics are
352// drawn to illustrate the tree's hierarchy.
353func (t *TreeView) SetGraphics(showGraphics bool) *TreeView {
354	t.graphics = showGraphics
355	return t
356}
357
358// SetGraphicsColor sets the colors of the lines used to draw the tree structure.
359func (t *TreeView) SetGraphicsColor(color tcell.Color) *TreeView {
360	t.graphicsColor = color
361	return t
362}
363
364// SetChangedFunc sets the function which is called when the user navigates to
365// a new tree node.
366func (t *TreeView) SetChangedFunc(handler func(node *TreeNode)) *TreeView {
367	t.changed = handler
368	return t
369}
370
371// SetSelectedFunc sets the function which is called when the user selects a
372// node by pressing Enter on the current selection.
373func (t *TreeView) SetSelectedFunc(handler func(node *TreeNode)) *TreeView {
374	t.selected = handler
375	return t
376}
377
378// GetScrollOffset returns the number of node rows that were skipped at the top
379// of the tree view. Note that when the user navigates the tree view, this value
380// is only updated after the tree view has been redrawn.
381func (t *TreeView) GetScrollOffset() int {
382	return t.offsetY
383}
384
385// GetRowCount returns the number of "visible" nodes. This includes nodes which
386// fall outside the tree view's box but notably does not include the children
387// of collapsed nodes. Note that this value is only up to date after the tree
388// view has been drawn.
389func (t *TreeView) GetRowCount() int {
390	return len(t.nodes)
391}
392
393// process builds the visible tree, populates the "nodes" slice, and processes
394// pending selection actions.
395func (t *TreeView) process() {
396	_, _, _, height := t.GetInnerRect()
397
398	// Determine visible nodes and their placement.
399	var graphicsOffset, maxTextX int
400	t.nodes = nil
401	selectedIndex := -1
402	topLevelGraphicsX := -1
403	if t.graphics {
404		graphicsOffset = 1
405	}
406	t.root.Walk(func(node, parent *TreeNode) bool {
407		// Set node attributes.
408		node.parent = parent
409		if parent == nil {
410			node.level = 0
411			node.graphicsX = 0
412			node.textX = 0
413		} else {
414			node.level = parent.level + 1
415			node.graphicsX = parent.textX
416			node.textX = node.graphicsX + graphicsOffset + node.indent
417		}
418		if !t.graphics && t.align {
419			// Without graphics, we align nodes on the first column.
420			node.textX = 0
421		}
422		if node.level == t.topLevel {
423			// No graphics for top level nodes.
424			node.graphicsX = 0
425			node.textX = 0
426		}
427
428		// Add the node to the list.
429		if node.level >= t.topLevel {
430			// This node will be visible.
431			if node.textX > maxTextX {
432				maxTextX = node.textX
433			}
434			if node == t.currentNode && node.selectable {
435				selectedIndex = len(t.nodes)
436			}
437
438			// Maybe we want to skip this level.
439			if t.topLevel == node.level && (topLevelGraphicsX < 0 || node.graphicsX < topLevelGraphicsX) {
440				topLevelGraphicsX = node.graphicsX
441			}
442
443			t.nodes = append(t.nodes, node)
444		}
445
446		// Recurse if desired.
447		return node.expanded
448	})
449
450	// Post-process positions.
451	for _, node := range t.nodes {
452		// If text must align, we correct the positions.
453		if t.align && node.level > t.topLevel {
454			node.textX = maxTextX
455		}
456
457		// If we skipped levels, shift to the left.
458		if topLevelGraphicsX > 0 {
459			node.graphicsX -= topLevelGraphicsX
460			node.textX -= topLevelGraphicsX
461		}
462	}
463
464	// Process selection. (Also trigger events if necessary.)
465	if selectedIndex >= 0 {
466		// Move the selection.
467		newSelectedIndex := selectedIndex
468	MovementSwitch:
469		switch t.movement {
470		case treeUp:
471			for newSelectedIndex > 0 {
472				newSelectedIndex--
473				if t.nodes[newSelectedIndex].selectable {
474					break MovementSwitch
475				}
476			}
477			newSelectedIndex = selectedIndex
478		case treeDown:
479			for newSelectedIndex < len(t.nodes)-1 {
480				newSelectedIndex++
481				if t.nodes[newSelectedIndex].selectable {
482					break MovementSwitch
483				}
484			}
485			newSelectedIndex = selectedIndex
486		case treeHome:
487			for newSelectedIndex = 0; newSelectedIndex < len(t.nodes); newSelectedIndex++ {
488				if t.nodes[newSelectedIndex].selectable {
489					break MovementSwitch
490				}
491			}
492			newSelectedIndex = selectedIndex
493		case treeEnd:
494			for newSelectedIndex = len(t.nodes) - 1; newSelectedIndex >= 0; newSelectedIndex-- {
495				if t.nodes[newSelectedIndex].selectable {
496					break MovementSwitch
497				}
498			}
499			newSelectedIndex = selectedIndex
500		case treePageDown:
501			if newSelectedIndex+height < len(t.nodes) {
502				newSelectedIndex += height
503			} else {
504				newSelectedIndex = len(t.nodes) - 1
505			}
506			for ; newSelectedIndex < len(t.nodes); newSelectedIndex++ {
507				if t.nodes[newSelectedIndex].selectable {
508					break MovementSwitch
509				}
510			}
511			newSelectedIndex = selectedIndex
512		case treePageUp:
513			if newSelectedIndex >= height {
514				newSelectedIndex -= height
515			} else {
516				newSelectedIndex = 0
517			}
518			for ; newSelectedIndex >= 0; newSelectedIndex-- {
519				if t.nodes[newSelectedIndex].selectable {
520					break MovementSwitch
521				}
522			}
523			newSelectedIndex = selectedIndex
524		}
525		t.currentNode = t.nodes[newSelectedIndex]
526		if newSelectedIndex != selectedIndex {
527			t.movement = treeNone
528			if t.changed != nil {
529				t.changed(t.currentNode)
530			}
531		}
532		selectedIndex = newSelectedIndex
533
534		// Move selection into viewport.
535		if selectedIndex-t.offsetY >= height {
536			t.offsetY = selectedIndex - height + 1
537		}
538		if selectedIndex < t.offsetY {
539			t.offsetY = selectedIndex
540		}
541	} else {
542		// If selection is not visible or selectable, select the first candidate.
543		if t.currentNode != nil {
544			for index, node := range t.nodes {
545				if node.selectable {
546					selectedIndex = index
547					t.currentNode = node
548					break
549				}
550			}
551		}
552		if selectedIndex < 0 {
553			t.currentNode = nil
554		}
555	}
556}
557
558// Draw draws this primitive onto the screen.
559func (t *TreeView) Draw(screen tcell.Screen) {
560	t.Box.Draw(screen)
561	if t.root == nil {
562		return
563	}
564
565	t.process()
566
567	// Scroll the tree.
568	x, y, width, height := t.GetInnerRect()
569	switch t.movement {
570	case treeUp:
571		t.offsetY--
572	case treeDown:
573		t.offsetY++
574	case treeHome:
575		t.offsetY = 0
576	case treeEnd:
577		t.offsetY = len(t.nodes)
578	case treePageUp:
579		t.offsetY -= height
580	case treePageDown:
581		t.offsetY += height
582	}
583	t.movement = treeNone
584
585	// Fix invalid offsets.
586	if t.offsetY >= len(t.nodes)-height {
587		t.offsetY = len(t.nodes) - height
588	}
589	if t.offsetY < 0 {
590		t.offsetY = 0
591	}
592
593	// Draw the tree.
594	posY := y
595	lineStyle := tcell.StyleDefault.Background(t.backgroundColor).Foreground(t.graphicsColor)
596	for index, node := range t.nodes {
597		// Skip invisible parts.
598		if posY >= y+height+1 {
599			break
600		}
601		if index < t.offsetY {
602			continue
603		}
604
605		// Draw the graphics.
606		if t.graphics {
607			// Draw ancestor branches.
608			ancestor := node.parent
609			for ancestor != nil && ancestor.parent != nil && ancestor.parent.level >= t.topLevel {
610				if ancestor.graphicsX >= width {
611					continue
612				}
613
614				// Draw a branch if this ancestor is not a last child.
615				if ancestor.parent.children[len(ancestor.parent.children)-1] != ancestor {
616					if posY-1 >= y && ancestor.textX > ancestor.graphicsX {
617						PrintJoinedSemigraphics(screen, x+ancestor.graphicsX, posY-1, Borders.Vertical, t.graphicsColor)
618					}
619					if posY < y+height {
620						screen.SetContent(x+ancestor.graphicsX, posY, Borders.Vertical, nil, lineStyle)
621					}
622				}
623				ancestor = ancestor.parent
624			}
625
626			if node.textX > node.graphicsX && node.graphicsX < width {
627				// Connect to the node above.
628				if posY-1 >= y && t.nodes[index-1].graphicsX <= node.graphicsX && t.nodes[index-1].textX > node.graphicsX {
629					PrintJoinedSemigraphics(screen, x+node.graphicsX, posY-1, Borders.TopLeft, t.graphicsColor)
630				}
631
632				// Join this node.
633				if posY < y+height {
634					screen.SetContent(x+node.graphicsX, posY, Borders.BottomLeft, nil, lineStyle)
635					for pos := node.graphicsX + 1; pos < node.textX && pos < width; pos++ {
636						screen.SetContent(x+pos, posY, Borders.Horizontal, nil, lineStyle)
637					}
638				}
639			}
640		}
641
642		// Draw the prefix and the text.
643		if node.textX < width && posY < y+height {
644			// Prefix.
645			var prefixWidth int
646			if len(t.prefixes) > 0 {
647				_, prefixWidth = Print(screen, t.prefixes[(node.level-t.topLevel)%len(t.prefixes)], x+node.textX, posY, width-node.textX, AlignLeft, node.color)
648			}
649
650			// Text.
651			if node.textX+prefixWidth < width {
652				style := tcell.StyleDefault.Foreground(node.color)
653				if node == t.currentNode {
654					style = tcell.StyleDefault.Background(node.color).Foreground(t.backgroundColor)
655				}
656				printWithStyle(screen, node.text, x+node.textX+prefixWidth, posY, width-node.textX-prefixWidth, AlignLeft, style)
657			}
658		}
659
660		// Advance.
661		posY++
662	}
663}
664
665// InputHandler returns the handler for this primitive.
666func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
667	return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
668		selectNode := func() {
669			if t.currentNode != nil {
670				if t.selected != nil {
671					t.selected(t.currentNode)
672				}
673				if t.currentNode.selected != nil {
674					t.currentNode.selected()
675				}
676			}
677		}
678
679		// Because the tree is flattened into a list only at drawing time, we also
680		// postpone the (selection) movement to drawing time.
681		switch key := event.Key(); key {
682		case tcell.KeyTab, tcell.KeyDown, tcell.KeyRight:
683			t.movement = treeDown
684		case tcell.KeyBacktab, tcell.KeyUp, tcell.KeyLeft:
685			t.movement = treeUp
686		case tcell.KeyHome:
687			t.movement = treeHome
688		case tcell.KeyEnd:
689			t.movement = treeEnd
690		case tcell.KeyPgDn, tcell.KeyCtrlF:
691			t.movement = treePageDown
692		case tcell.KeyPgUp, tcell.KeyCtrlB:
693			t.movement = treePageUp
694		case tcell.KeyRune:
695			switch event.Rune() {
696			case 'g':
697				t.movement = treeHome
698			case 'G':
699				t.movement = treeEnd
700			case 'j':
701				t.movement = treeDown
702			case 'k':
703				t.movement = treeUp
704			case ' ':
705				selectNode()
706			}
707		case tcell.KeyEnter:
708			selectNode()
709		}
710
711		t.process()
712	})
713}
714