From 4631cd73370bf39306af23c1e3598588faff4f46 Mon Sep 17 00:00:00 2001 From: Oliver <480930+rivo@users.noreply.github.com> Date: Wed, 20 Jun 2018 10:06:05 +0200 Subject: [PATCH] Added the tree view. --- README.md | 3 + demos/presentation/main.go | 7 +- demos/presentation/textview.go | 6 +- demos/presentation/treeview.go | 149 ++++++++ demos/treeview/main.go | 61 +++ demos/treeview/screenshot.png | Bin 0 -> 51097 bytes doc.go | 10 +- table.go | 1 - textview.go | 90 ++++- treeview.go | 663 +++++++++++++++++++++++++++++++++ util.go | 7 +- 11 files changed, 966 insertions(+), 31 deletions(-) create mode 100644 demos/presentation/treeview.go create mode 100644 demos/treeview/main.go create mode 100644 demos/treeview/screenshot.png create mode 100644 treeview.go diff --git a/README.md b/README.md index 1ebfc01..89b866c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Among these components are: - __Input forms__ (include __input/password fields__, __drop-down selections__, __checkboxes__, and __buttons__) - Navigable multi-color __text views__ - Sophisticated navigable __table views__ +- Flexible __tree views__ - Selectable __lists__ - __Grid__, __Flexbox__ and __page layouts__ - Modal __message windows__ @@ -64,6 +65,8 @@ Add your issue here on GitHub. Feel free to get in touch if you have any questio (There are no corresponding tags in the project. I only keep such a history in this README.) +- v0.17 (2018-06-20) + - Added `TreeView`. - v0.15 (2018-05-02) - `Flex` and `Grid` don't clear their background per default, thus allowing for custom modals. See the [Wiki](https://github.com/rivo/tview/wiki/Modal) for an example. - v0.14 (2018-04-13) diff --git a/demos/presentation/main.go b/demos/presentation/main.go index dabea18..c75fdc0 100644 --- a/demos/presentation/main.go +++ b/demos/presentation/main.go @@ -40,6 +40,7 @@ func main() { TextView1, TextView2, Table, + TreeView, Flex, Grid, Colors, @@ -58,12 +59,14 @@ func main() { pages := tview.NewPages() previousSlide := func() { currentSlide = (currentSlide - 1 + len(slides)) % len(slides) - info.Highlight(strconv.Itoa(currentSlide)) + info.Highlight(strconv.Itoa(currentSlide)). + ScrollToHighlight() pages.SwitchToPage(strconv.Itoa(currentSlide)) } nextSlide := func() { currentSlide = (currentSlide + 1) % len(slides) - info.Highlight(strconv.Itoa(currentSlide)) + info.Highlight(strconv.Itoa(currentSlide)). + ScrollToHighlight() pages.SwitchToPage(strconv.Itoa(currentSlide)) } for index, slide := range slides { diff --git a/demos/presentation/textview.go b/demos/presentation/textview.go index c0ba862..65f2d2c 100644 --- a/demos/presentation/textview.go +++ b/demos/presentation/textview.go @@ -68,7 +68,7 @@ const textView2 = `[green]package[white] main [yellow]SetDoneFunc[white]([yellow]func[white](key tcell.Key) { highlights := ["2"]textView[""].[yellow]GetHighlights[white]() hasHighlights := [yellow]len[white](highlights) > [red]0 -[white] [yellow]switch[white] key { + [yellow]switch[white] key { [yellow]case[white] tcell.KeyEnter: [yellow]if[white] hasHighlights { ["3"]textView[""].[yellow]Highlight[white]() @@ -80,14 +80,14 @@ const textView2 = `[green]package[white] main [yellow]if[white] hasHighlights { current, _ := strconv.[yellow]Atoi[white](highlights[[red]0[white]]) next := (current + [red]1[white]) % [red]9 -[white] ["5"]textView[""].[yellow]Highlight[white](strconv.[yellow]Itoa[white](next)). + ["5"]textView[""].[yellow]Highlight[white](strconv.[yellow]Itoa[white](next)). [yellow]ScrollToHighlight[white]() } [yellow]case[white] tcell.KeyBacktab: [yellow]if[white] hasHighlights { current, _ := strconv.[yellow]Atoi[white](highlights[[red]0[white]]) next := (current - [red]1[white] + [red]9[white]) % [red]9 -[white] ["6"]textView[""].[yellow]Highlight[white](strconv.[yellow]Itoa[white](next)). + ["6"]textView[""].[yellow]Highlight[white](strconv.[yellow]Itoa[white](next)). [yellow]ScrollToHighlight[white]() } } diff --git a/demos/presentation/treeview.go b/demos/presentation/treeview.go new file mode 100644 index 0000000..68349cd --- /dev/null +++ b/demos/presentation/treeview.go @@ -0,0 +1,149 @@ +package main + +import ( + "strings" + + "github.com/gdamore/tcell" + "github.com/rivo/tview" +) + +const treeAllCode = `[green]package[white] main + +[green]import[white] [red]"github.com/rivo/tview"[white] + +[green]func[white] [yellow]main[white]() { + $$$ + + root := tview.[yellow]NewTreeNode[white]([red]"Root"[white]). + [yellow]AddChild[white](tview.[yellow]NewTreeNode[white]([red]"First child"[white]). + [yellow]AddChild[white](tview.[yellow]NewTreeNode[white]([red]"Grandchild A"[white])). + [yellow]AddChild[white](tview.[yellow]NewTreeNode[white]([red]"Grandchild B"[white]))). + [yellow]AddChild[white](tview.[yellow]NewTreeNode[white]([red]"Second child"[white]). + [yellow]AddChild[white](tview.[yellow]NewTreeNode[white]([red]"Grandchild C"[white])). + [yellow]AddChild[white](tview.[yellow]NewTreeNode[white]([red]"Grandchild D"[white]))). + [yellow]AddChild[white](tview.[yellow]NewTreeNode[white]([red]"Third child"[white])) + + tree.[yellow]SetRoot[white](root). + [yellow]SetCurrentNode[white](root) + + tview.[yellow]NewApplication[white](). + [yellow]SetRoot[white](tree, true). + [yellow]Run[white]() +}` + +const treeBasicCode = `tree := tview.[yellow]NewTreeView[white]()` + +const treeTopLevelCode = `tree := tview.[yellow]NewTreeView[white](). + [yellow]SetTopLevel[white]([red]1[white])` + +const treeAlignCode = `tree := tview.[yellow]NewTreeView[white](). + [yellow]SetAlign[white](true)` + +const treePrefixCode = `tree := tview.[yellow]NewTreeView[white](). + [yellow]SetGraphics[white](false). + [yellow]SetTopLevel[white]([red]1[white]). + [yellow]SetPrefixes[white]([][green]string[white]{ + [red]"[red[]* "[white], + [red]"[darkcyan[]- "[white], + [red]"[darkmagenta[]- "[white], + })` + +type node struct { + text string + expand bool + selected func() + children []*node +} + +var ( + tree = tview.NewTreeView() + treeNextSlide func() + treeCode = tview.NewTextView().SetWrap(false).SetDynamicColors(true) +) + +var rootNode = &node{ + text: "Root", + children: []*node{ + {text: "Expand all", selected: func() { tree.GetRoot().ExpandAll() }}, + {text: "Collapse all", selected: func() { + for _, child := range tree.GetRoot().GetChildren() { + child.CollapseAll() + } + }}, + {text: "Hide root node", expand: true, children: []*node{ + {text: "Tree list starts one level down"}, + {text: "Works better for lists where no top node is needed"}, + {text: "Switch to this layout", selected: func() { + tree.SetAlign(false).SetTopLevel(1).SetGraphics(true).SetPrefixes(nil) + treeCode.SetText(strings.Replace(treeAllCode, "$$$", treeTopLevelCode, -1)) + }}, + }}, + {text: "Align node text", expand: true, children: []*node{ + {text: "For trees that are similar to lists"}, + {text: "Hierarchy shown only in line drawings"}, + {text: "Switch to this layout", selected: func() { + tree.SetAlign(true).SetTopLevel(0).SetGraphics(true).SetPrefixes(nil) + treeCode.SetText(strings.Replace(treeAllCode, "$$$", treeAlignCode, -1)) + }}, + }}, + {text: "Prefixes", expand: true, children: []*node{ + {text: "Best for hierarchical bullet point lists"}, + {text: "You can define your own prefixes per level"}, + {text: "Switch to this layout", selected: func() { + tree.SetAlign(false).SetTopLevel(1).SetGraphics(false).SetPrefixes([]string{"[red]* ", "[darkcyan]- ", "[darkmagenta]- "}) + treeCode.SetText(strings.Replace(treeAllCode, "$$$", treePrefixCode, -1)) + }}, + }}, + {text: "Basic tree with graphics", expand: true, children: []*node{ + {text: "Lines illustrate hierarchy"}, + {text: "Basic indentation"}, + {text: "Switch to this layout", selected: func() { + tree.SetAlign(false).SetTopLevel(0).SetGraphics(true).SetPrefixes(nil) + treeCode.SetText(strings.Replace(treeAllCode, "$$$", treeBasicCode, -1)) + }}, + }}, + {text: "Next slide", selected: func() { treeNextSlide() }}, + }} + +// TreeView demonstrates the tree view. +func TreeView(nextSlide func()) (title string, content tview.Primitive) { + treeNextSlide = nextSlide + tree.SetBorder(true). + SetTitle("TreeView") + + // Add nodes. + var add func(target *node) *tview.TreeNode + add = func(target *node) *tview.TreeNode { + node := tview.NewTreeNode(target.text). + SetSelectable(target.expand || target.selected != nil). + SetExpanded(target == rootNode). + SetReference(target) + if target.expand { + node.SetColor(tcell.ColorGreen) + } else if target.selected != nil { + node.SetColor(tcell.ColorRed) + } + for _, child := range target.children { + node.AddChild(add(child)) + } + return node + } + root := add(rootNode) + tree.SetRoot(root). + SetCurrentNode(root). + SetSelectedFunc(func(n *tview.TreeNode) { + original := n.GetReference().(*node) + if original.expand { + n.SetExpanded(!n.IsExpanded()) + } else if original.selected != nil { + original.selected() + } + }) + + treeCode.SetText(strings.Replace(treeAllCode, "$$$", treeBasicCode, -1)). + SetBorderPadding(1, 1, 2, 0) + + return "Tree", tview.NewFlex(). + AddItem(tree, 0, 1, true). + AddItem(treeCode, codeWidth, 1, false) +} diff --git a/demos/treeview/main.go b/demos/treeview/main.go new file mode 100644 index 0000000..6657b73 --- /dev/null +++ b/demos/treeview/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "io/ioutil" + "path/filepath" + + "github.com/gdamore/tcell" + "github.com/rivo/tview" +) + +// Show a navigable tree view of the current directory. +func main() { + rootDir := "." + root := tview.NewTreeNode(rootDir). + SetColor(tcell.ColorRed) + tree := tview.NewTreeView(). + SetRoot(root). + SetCurrentNode(root) + + // A helper function which adds the files and directories of the given path + // to the given target node. + add := func(target *tview.TreeNode, path string) { + files, err := ioutil.ReadDir(path) + if err != nil { + panic(err) + } + for _, file := range files { + node := tview.NewTreeNode(file.Name()). + SetReference(filepath.Join(path, file.Name())). + SetSelectable(file.IsDir()) + if file.IsDir() { + node.SetColor(tcell.ColorGreen) + } + target.AddChild(node) + } + } + + // Add the current directory to the root node. + add(root, rootDir) + + // If a directory was selected, open it. + tree.SetSelectedFunc(func(node *tview.TreeNode) { + reference := node.GetReference() + if reference == nil { + return // Selecting the root node does nothing. + } + children := node.GetChildren() + if len(children) == 0 { + // Load and show files in this directory. + path := reference.(string) + add(node, path) + } else { + // Collapse if visible, expand if collapsed. + node.SetExpanded(!node.IsExpanded()) + } + }) + + if err := tview.NewApplication().SetRoot(tree, true).Run(); err != nil { + panic(err) + } +} diff --git a/demos/treeview/screenshot.png b/demos/treeview/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..c25fcf5673b85c31770ed3ad3d98bc601f68bc1f GIT binary patch literal 51097 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 bf^P0U^uMw7>5IaL1Ab(lD@YbY^?d&W@G^z` literal 0 HcmV?d00001 diff --git a/doc.go b/doc.go index 211185f..1b51a27 100644 --- a/doc.go +++ b/doc.go @@ -7,10 +7,12 @@ Widgets The package implements the following widgets: - - TextView: Scrollable windows that display multi-colored text. Text may also + - TextView: A scrollable window that display multi-colored text. Text may also be highlighted. - - Table: Scrollable display of tabular data. Table cells, rows, or columns may - also be highlighted. + - Table: A scrollable display of tabular data. Table cells, rows, or columns + may also be highlighted. + - TreeView: A scrollable display for hierarchical data. Tree nodes can be + highlighted, collapsed, expanded, and more. - List: A navigable text list with optional keyboard shortcuts. - InputField: One-line input fields to enter text. - DropDown: Drop-down selection fields. @@ -83,7 +85,7 @@ tag is as follows: [::] -Each of the three fields can be left blank and trailing fields can be ommitted. +Each of the three fields can be left blank and trailing fields can be omitted. (Empty square brackets "[]", however, are not considered color tags.) Colors that are not specified will be left unchanged. A field with just a dash ("-") means "reset to default". diff --git a/table.go b/table.go index 10f8399..38f5006 100644 --- a/table.go +++ b/table.go @@ -644,7 +644,6 @@ ColumnLoop: } expWidth := toDistribute * expansion / expansionTotal widths[index] += expWidth - tableWidth += expWidth toDistribute -= expWidth expansionTotal -= expansion } diff --git a/textview.go b/textview.go index d49e7bd..a88791c 100644 --- a/textview.go +++ b/textview.go @@ -104,6 +104,10 @@ type TextView struct { // during re-indexing. Set to -1 if there is no current highlight. fromHighlight, toHighlight int + // The screen space column of the highlight in its first line. Set to -1 if + // there is no current highlight. + posHighlight int + // A set of region IDs that are currently highlighted. highlights map[string]struct{} @@ -171,6 +175,7 @@ func NewTextView() *TextView { align: AlignLeft, wrap: true, textColor: Styles.PrimaryTextColor, + regions: false, dynamicColors: false, } } @@ -503,7 +508,7 @@ func (t *TextView) reindexBuffer(width int) { return // Nothing has changed. We can still use the current index. } t.index = nil - t.fromHighlight, t.toHighlight = -1, -1 + t.fromHighlight, t.toHighlight, t.posHighlight = -1, -1, -1 // If there's no space, there's no index. if width < 1 { @@ -522,8 +527,9 @@ func (t *TextView) reindexBuffer(width int) { colorTags [][]string escapeIndices [][]int ) + strippedStr := str if t.dynamicColors { - colorTagIndices, colorTags, escapeIndices, str, _ = decomposeString(str) + colorTagIndices, colorTags, escapeIndices, strippedStr, _ = decomposeString(str) } // Find all regions in this line. Then remove them. @@ -534,14 +540,18 @@ func (t *TextView) reindexBuffer(width int) { if t.regions { regionIndices = regionPattern.FindAllStringIndex(str, -1) regions = regionPattern.FindAllStringSubmatch(str, -1) - str = regionPattern.ReplaceAllString(str, "") - if !t.dynamicColors { - // We haven't detected escape tags yet. Do it now. - escapeIndices = escapePattern.FindAllStringIndex(str, -1) - str = escapePattern.ReplaceAllString(str, "[$1$2]") - } + strippedStr = regionPattern.ReplaceAllString(strippedStr, "") } + // Find all escape tags in this line. Escape them. + if t.dynamicColors || t.regions { + escapeIndices = escapePattern.FindAllStringIndex(str, -1) + strippedStr = escapePattern.ReplaceAllString(strippedStr, "[$1$2]") + } + + // We don't need the original string anymore for now. + str = strippedStr + // Split the line if required. var splitLines []string if t.wrap && len(str) > 0 { @@ -585,15 +595,53 @@ func (t *TextView) reindexBuffer(width int) { // Shift original position with tags. lineLength := len(splitLine) + remainingLength := lineLength + tagEnd := originalPos + totalTagLength := 0 for { - if colorPos < len(colorTagIndices) && colorTagIndices[colorPos][0] <= originalPos+lineLength { + // Which tag comes next? + nextTag := make([][3]int, 0, 3) + if colorPos < len(colorTagIndices) { + nextTag = append(nextTag, [3]int{colorTagIndices[colorPos][0], colorTagIndices[colorPos][1], 0}) // 0 = color tag. + } + if regionPos < len(regionIndices) { + nextTag = append(nextTag, [3]int{regionIndices[regionPos][0], regionIndices[regionPos][1], 1}) // 1 = region tag. + } + if escapePos < len(escapeIndices) { + nextTag = append(nextTag, [3]int{escapeIndices[escapePos][0], escapeIndices[escapePos][1], 2}) // 2 = escape tag. + } + minPos := -1 + tagIndex := -1 + for index, pair := range nextTag { + if minPos < 0 || pair[0] < minPos { + minPos = pair[0] + tagIndex = index + } + } + + // Is the next tag in range? + if tagIndex < 0 || minPos >= tagEnd+remainingLength { + break // No. We're done with this line. + } + + // Advance. + strippedTagStart := nextTag[tagIndex][0] - originalPos - totalTagLength + tagEnd = nextTag[tagIndex][1] + tagLength := tagEnd - nextTag[tagIndex][0] + if nextTag[tagIndex][2] == 2 { + tagLength = 1 + } + totalTagLength += tagLength + remainingLength = lineLength - (tagEnd - originalPos - totalTagLength) + + // Process the tag. + switch nextTag[tagIndex][2] { + case 0: // Process color tags. - originalPos += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0] foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos]) colorPos++ - } else if regionPos < len(regionIndices) && regionIndices[regionPos][0] <= originalPos+lineLength { + case 1: // Process region tags. - originalPos += regionIndices[regionPos][1] - regionIndices[regionPos][0] regionID = regions[regionPos][1] _, highlighted = t.highlights[regionID] @@ -602,23 +650,21 @@ func (t *TextView) reindexBuffer(width int) { line := len(t.index) if t.fromHighlight < 0 { t.fromHighlight, t.toHighlight = line, line + t.posHighlight = runewidth.StringWidth(splitLine[:strippedTagStart]) } else if line > t.toHighlight { t.toHighlight = line } } regionPos++ - } else if escapePos < len(escapeIndices) && escapeIndices[escapePos][0] <= originalPos+lineLength { + case 2: // Process escape tags. - originalPos++ escapePos++ - } else { - break } } // Advance to next line. - originalPos += lineLength + originalPos += lineLength + totalTagLength // Append this line. line.NextPos = originalPos @@ -683,6 +729,16 @@ func (t *TextView) Draw(screen tcell.Screen) { // No, let's move to the start of the highlights. t.lineOffset = t.fromHighlight } + + // If the highlight is too far to the right, move it to the middle. + if t.posHighlight-t.columnOffset > 3*width/4 { + t.columnOffset = t.posHighlight - width/2 + } + + // If the highlight is off-screen on the left, move it on-screen. + if t.posHighlight-t.columnOffset < 0 { + t.columnOffset = t.posHighlight - width/4 + } } t.scrollToHighlights = false diff --git a/treeview.go b/treeview.go new file mode 100644 index 0000000..4f8d446 --- /dev/null +++ b/treeview.go @@ -0,0 +1,663 @@ +package tview + +import ( + "github.com/gdamore/tcell" +) + +// 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 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 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. +} + +// 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) *TreeNode { + 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]) + } + } + + return n +} + +// 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{}) *TreeNode { + n.reference = reference + return n +} + +// GetReference returns this node's reference object. +func (n *TreeNode) GetReference() interface{} { + return n.reference +} + +// SetChildren sets this node's child nodes. +func (n *TreeNode) SetChildren(childNodes []*TreeNode) *TreeNode { + n.children = childNodes + return n +} + +// GetChildren returns this node's children. +func (n *TreeNode) GetChildren() []*TreeNode { + return n.children +} + +// ClearChildren removes all child nodes from this node. +func (n *TreeNode) ClearChildren() *TreeNode { + n.children = nil + return n +} + +// AddChild adds a new child node to this node. +func (n *TreeNode) AddChild(node *TreeNode) *TreeNode { + n.children = append(n.children, node) + return n +} + +// SetSelectable sets a flag indicating whether this node can be selected by +// the user. +func (n *TreeNode) SetSelectable(selectable bool) *TreeNode { + n.selectable = selectable + return n +} + +// SetSelectedFunc sets a function which is called when the user selects this +// node by hitting Enter when it is selected. +func (n *TreeNode) SetSelectedFunc(handler func()) *TreeNode { + n.selected = handler + return n +} + +// SetExpanded sets whether or not this node's child nodes should be displayed. +func (n *TreeNode) SetExpanded(expanded bool) *TreeNode { + n.expanded = expanded + return n +} + +// Expand makes the child nodes of this node appear. +func (n *TreeNode) Expand() *TreeNode { + n.expanded = true + return n +} + +// Collapse makes the child nodes of this node disappear. +func (n *TreeNode) Collapse() *TreeNode { + n.expanded = false + return n +} + +// ExpandAll expands this node and all descendent nodes. +func (n *TreeNode) ExpandAll() *TreeNode { + n.Walk(func(node, parent *TreeNode) bool { + node.expanded = true + return true + }) + return n +} + +// CollapseAll collapses this node and all descendent nodes. +func (n *TreeNode) CollapseAll() *TreeNode { + n.Walk(func(node, parent *TreeNode) bool { + n.expanded = false + return true + }) + return n +} + +// IsExpanded returns whether the child nodes of this node are visible. +func (n *TreeNode) IsExpanded() bool { + return n.expanded +} + +// SetText sets the node's text which is displayed. +func (n *TreeNode) SetText(text string) *TreeNode { + n.text = text + return n +} + +// SetColor sets the node's text color. +func (n *TreeNode) SetColor(color tcell.Color) *TreeNode { + n.color = color + return n +} + +// 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) *TreeNode { + n.indent = indent + return n +} + +// 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 selected 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 skip +// 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 selected node or nil if no node is selected. + 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 []string + + // 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 color of the lines. + graphicsColor tcell.Color + + // An optional function which is called when the user has navigated to a new + // tree node. + changed func(node *TreeNode) + + // An optional function which is called when a tree item was selected. + selected func(node *TreeNode) +} + +// NewTreeView returns a new tree view. +func NewTreeView() *TreeView { + return &TreeView{ + Box: NewBox(), + graphics: true, + graphicsColor: Styles.GraphicsColor, + } +} + +// SetRoot sets the root node of the tree. +func (t *TreeView) SetRoot(root *TreeNode) *TreeView { + t.root = root + return t +} + +// GetRoot returns the root node of the tree. If no such node was previously +// set, nil is returned. +func (t *TreeView) GetRoot() *TreeNode { + return t.root +} + +// SetCurrentNode sets the currently selected node. Provide nil to clear all +// selections. 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) *TreeView { + t.currentNode = node + return t +} + +// GetCurrentNode returns the currently selected node or nil of no node is +// currently selected. +func (t *TreeView) GetCurrentNode() *TreeNode { + 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) *TreeView { + t.topLevel = topLevel + return t +} + +// 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) *TreeView { + t.prefixes = prefixes + return t +} + +// 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) *TreeView { + t.align = align + return t +} + +// 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) *TreeView { + t.graphics = showGraphics + return t +} + +// SetGraphicsColor sets the colors of the lines used to draw the tree structure. +func (t *TreeView) SetGraphicsColor(color tcell.Color) *TreeView { + t.graphicsColor = color + return t +} + +// SetChangedFunc sets the function which is called when the user navigates to +// a new tree node. +func (t *TreeView) SetChangedFunc(handler func(node *TreeNode)) *TreeView { + t.changed = handler + return t +} + +// 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)) *TreeView { + t.selected = handler + return t +} + +// Draw draws this primitive onto the screen. +func (t *TreeView) Draw(screen tcell.Screen) { + t.Box.Draw(screen) + if t.root == nil { + return + } + x, y, width, height := t.GetInnerRect() + + // Determine visible nodes and their placement. + var graphicsOffset, maxTextX int + var nodes []*TreeNode + 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 + } + if node.textX > maxTextX { + maxTextX = node.textX + } + if node == t.currentNode && node.selectable { + selectedIndex = len(nodes) + } + + // Maybe we want to skip this level. + if t.topLevel == node.level && (topLevelGraphicsX < 0 || node.graphicsX < topLevelGraphicsX) { + topLevelGraphicsX = node.graphicsX + } + + // Add and recurse (if desired). + if node.level >= t.topLevel { + nodes = append(nodes, node) + } + return node.expanded + }) + + // Post-process positions. + for _, node := range 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 nodes[newSelectedIndex].selectable { + break MovementSwitch + } + } + newSelectedIndex = selectedIndex + case treeDown: + for newSelectedIndex < len(nodes)-1 { + newSelectedIndex++ + if nodes[newSelectedIndex].selectable { + break MovementSwitch + } + } + newSelectedIndex = selectedIndex + case treeHome: + for newSelectedIndex = 0; newSelectedIndex < len(nodes); newSelectedIndex++ { + if nodes[newSelectedIndex].selectable { + break MovementSwitch + } + } + newSelectedIndex = selectedIndex + case treeEnd: + for newSelectedIndex = len(nodes) - 1; newSelectedIndex >= 0; newSelectedIndex-- { + if nodes[newSelectedIndex].selectable { + break MovementSwitch + } + } + newSelectedIndex = selectedIndex + case treePageUp: + if newSelectedIndex+height < len(nodes) { + newSelectedIndex += height + } else { + newSelectedIndex = len(nodes) - 1 + } + for ; newSelectedIndex < len(nodes); newSelectedIndex++ { + if nodes[newSelectedIndex].selectable { + break MovementSwitch + } + } + newSelectedIndex = selectedIndex + case treePageDown: + if newSelectedIndex >= height { + newSelectedIndex -= height + } else { + newSelectedIndex = 0 + } + for ; newSelectedIndex >= 0; newSelectedIndex-- { + if nodes[newSelectedIndex].selectable { + break MovementSwitch + } + } + newSelectedIndex = selectedIndex + } + t.currentNode = nodes[newSelectedIndex] + if newSelectedIndex != selectedIndex { + t.movement = treeNone + if t.changed != nil { + t.changed(t.currentNode) + } + } + 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 nodes { + if node.selectable { + selectedIndex = index + t.currentNode = node + break + } + } + } + if selectedIndex < 0 { + t.currentNode = nil + } + } + + // Scroll the tree. + switch t.movement { + case treeUp: + t.offsetY-- + case treeDown: + t.offsetY++ + case treeHome: + t.offsetY = 0 + case treeEnd: + t.offsetY = len(nodes) + case treePageUp: + t.offsetY -= height + case treePageDown: + t.offsetY += height + } + t.movement = treeNone + + // Fix invalid offsets. + if t.offsetY >= len(nodes)-height { + t.offsetY = len(nodes) - height + } + if t.offsetY < 0 { + t.offsetY = 0 + } + + // Draw the tree. + posY := y + lineStyle := tcell.StyleDefault.Foreground(t.graphicsColor) + for index, node := range nodes { + // Skip invisible parts. + if posY >= y+height+1 { + 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 && nodes[index-1].graphicsX <= node.graphicsX && 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 index == selectedIndex { + style = tcell.StyleDefault.Background(node.color).Foreground(t.backgroundColor) + } + printWithStyle(screen, node.text, x+node.textX+prefixWidth, posY, width-node.textX-prefixWidth, AlignLeft, style) + } + } + + // 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)) { + // Because the tree is flattened into a list only at drawing time, we also + // postpone the (selection) movement to drawing time. + switch key := event.Key(); key { + case tcell.KeyTab, tcell.KeyDown, tcell.KeyRight: + t.movement = treeDown + case tcell.KeyBacktab, tcell.KeyUp, tcell.KeyLeft: + t.movement = treeUp + case tcell.KeyHome: + t.movement = treeHome + case tcell.KeyEnd: + t.movement = treeEnd + case tcell.KeyPgDn, tcell.KeyCtrlF: + t.movement = treePageDown + case tcell.KeyPgUp, tcell.KeyCtrlB: + t.movement = treePageUp + case tcell.KeyRune: + switch event.Rune() { + case 'g': + t.movement = treeHome + case 'G': + t.movement = treeEnd + case 'j': + t.movement = treeDown + case 'k': + t.movement = treeUp + } + case tcell.KeyEnter: + if t.currentNode != nil { + if t.selected != nil { + t.selected(t.currentNode) + } + if t.currentNode.selected != nil { + t.currentNode.selected() + } + } + } + }) +} diff --git a/util.go b/util.go index 7ac8f74..44c8057 100644 --- a/util.go +++ b/util.go @@ -120,13 +120,12 @@ func overlayStyle(background tcell.Color, defaultStyle tcell.Style, fgColor, bgC defFg, defBg, defAttr := defaultStyle.Decompose() style := defaultStyle.Background(background) - if fgColor == "-" { - style = style.Foreground(defFg) - } else if fgColor != "" { + style = style.Foreground(defFg) + if fgColor != "" { style = style.Foreground(tcell.GetColor(fgColor)) } - if bgColor == "-" { + if bgColor == "-" || bgColor == "" && defBg != tcell.ColorDefault { style = style.Background(defBg) } else if bgColor != "" { style = style.Background(tcell.GetColor(bgColor))