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