Merge pull request #237 from namreg/tree-widget
This commit is contained in:
commit
14f6f658b8
@ -61,6 +61,7 @@ func main() {
|
|||||||
- [Gauge](./_examples/gauge.go)
|
- [Gauge](./_examples/gauge.go)
|
||||||
- [Image](./_examples/image.go)
|
- [Image](./_examples/image.go)
|
||||||
- [List](./_examples/list.go)
|
- [List](./_examples/list.go)
|
||||||
|
- [Tree](./_examples/tree.go)
|
||||||
- [Paragraph](./_examples/paragraph.go)
|
- [Paragraph](./_examples/paragraph.go)
|
||||||
- [PieChart](./_examples/piechart.go)
|
- [PieChart](./_examples/piechart.go)
|
||||||
- [Plot](./_examples/plot.go) (for scatterplots and linecharts)
|
- [Plot](./_examples/plot.go) (for scatterplots and linecharts)
|
||||||
|
127
_examples/tree.go
Normal file
127
_examples/tree.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
/// +build ignore
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
ui "github.com/gizak/termui/v3"
|
||||||
|
"github.com/gizak/termui/v3/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nodeValue string
|
||||||
|
|
||||||
|
func (nv nodeValue) String() string {
|
||||||
|
return string(nv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := ui.Init(); err != nil {
|
||||||
|
log.Fatalf("failed to initialize termui: %v", err)
|
||||||
|
}
|
||||||
|
defer ui.Close()
|
||||||
|
|
||||||
|
nodes := []*widgets.TreeNode{
|
||||||
|
{
|
||||||
|
Value: nodeValue("Key 1"),
|
||||||
|
Nodes: []*widgets.TreeNode{
|
||||||
|
{
|
||||||
|
Value: nodeValue("Key 1.1"),
|
||||||
|
Nodes: []*widgets.TreeNode{
|
||||||
|
{
|
||||||
|
Value: nodeValue("Key 1.1.1"),
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: nodeValue("Key 1.1.2"),
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: nodeValue("Key 1.2"),
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: nodeValue("Key 2"),
|
||||||
|
Nodes: []*widgets.TreeNode{
|
||||||
|
{
|
||||||
|
Value: nodeValue("Key 2.1"),
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: nodeValue("Key 2.2"),
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: nodeValue("Key 2.3"),
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: nodeValue("Key 3"),
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
l := widgets.NewTree()
|
||||||
|
l.TextStyle = ui.NewStyle(ui.ColorYellow)
|
||||||
|
l.WrapText = false
|
||||||
|
l.SetNodes(nodes)
|
||||||
|
|
||||||
|
x, y := ui.TerminalDimensions()
|
||||||
|
|
||||||
|
l.SetRect(0, 0, x, y)
|
||||||
|
|
||||||
|
ui.Render(l)
|
||||||
|
|
||||||
|
previousKey := ""
|
||||||
|
uiEvents := ui.PollEvents()
|
||||||
|
for {
|
||||||
|
e := <-uiEvents
|
||||||
|
switch e.ID {
|
||||||
|
case "q", "<C-c>":
|
||||||
|
return
|
||||||
|
case "j", "<Down>":
|
||||||
|
l.ScrollDown()
|
||||||
|
case "k", "<Up>":
|
||||||
|
l.ScrollUp()
|
||||||
|
case "<C-d>":
|
||||||
|
l.ScrollHalfPageDown()
|
||||||
|
case "<C-u>":
|
||||||
|
l.ScrollHalfPageUp()
|
||||||
|
case "<C-f>":
|
||||||
|
l.ScrollPageDown()
|
||||||
|
case "<C-b>":
|
||||||
|
l.ScrollPageUp()
|
||||||
|
case "g":
|
||||||
|
if previousKey == "g" {
|
||||||
|
l.ScrollTop()
|
||||||
|
}
|
||||||
|
case "<Home>":
|
||||||
|
l.ScrollTop()
|
||||||
|
case "<Enter>":
|
||||||
|
l.ToggleExpand()
|
||||||
|
case "G", "<End>":
|
||||||
|
l.ScrollBottom()
|
||||||
|
case "E":
|
||||||
|
l.ExpandAll()
|
||||||
|
case "C":
|
||||||
|
l.CollapseAll()
|
||||||
|
case "<Resize>":
|
||||||
|
x, y := ui.TerminalDimensions()
|
||||||
|
l.SetRect(0, 0, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
if previousKey == "g" {
|
||||||
|
previousKey = ""
|
||||||
|
} else {
|
||||||
|
previousKey = e.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Render(l)
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,9 @@ const (
|
|||||||
|
|
||||||
UP_ARROW = '▲'
|
UP_ARROW = '▲'
|
||||||
DOWN_ARROW = '▼'
|
DOWN_ARROW = '▼'
|
||||||
|
|
||||||
|
COLLAPSED = '+'
|
||||||
|
EXPANDED = '−'
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
13
v3/theme.go
13
v3/theme.go
@ -33,6 +33,7 @@ type RootTheme struct {
|
|||||||
Gauge GaugeTheme
|
Gauge GaugeTheme
|
||||||
Plot PlotTheme
|
Plot PlotTheme
|
||||||
List ListTheme
|
List ListTheme
|
||||||
|
Tree TreeTheme
|
||||||
Paragraph ParagraphTheme
|
Paragraph ParagraphTheme
|
||||||
PieChart PieChartTheme
|
PieChart PieChartTheme
|
||||||
Sparkline SparklineTheme
|
Sparkline SparklineTheme
|
||||||
@ -66,6 +67,12 @@ type ListTheme struct {
|
|||||||
Text Style
|
Text Style
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TreeTheme struct {
|
||||||
|
Text Style
|
||||||
|
Collapsed rune
|
||||||
|
Expanded rune
|
||||||
|
}
|
||||||
|
|
||||||
type ParagraphTheme struct {
|
type ParagraphTheme struct {
|
||||||
Text Style
|
Text Style
|
||||||
}
|
}
|
||||||
@ -122,6 +129,12 @@ var Theme = RootTheme{
|
|||||||
Text: NewStyle(ColorWhite),
|
Text: NewStyle(ColorWhite),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Tree: TreeTheme{
|
||||||
|
Text: NewStyle(ColorWhite),
|
||||||
|
Collapsed: COLLAPSED,
|
||||||
|
Expanded: EXPANDED,
|
||||||
|
},
|
||||||
|
|
||||||
StackedBarChart: StackedBarChartTheme{
|
StackedBarChart: StackedBarChartTheme{
|
||||||
Bars: StandardColors,
|
Bars: StandardColors,
|
||||||
Nums: StandardStyles,
|
Nums: StandardStyles,
|
||||||
|
257
v3/widgets/tree.go
Normal file
257
v3/widgets/tree.go
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
. "github.com/gizak/termui/v3"
|
||||||
|
rw "github.com/mattn/go-runewidth"
|
||||||
|
)
|
||||||
|
|
||||||
|
const treeIndent = " "
|
||||||
|
|
||||||
|
// TreeNode is a tree node.
|
||||||
|
type TreeNode struct {
|
||||||
|
Value fmt.Stringer
|
||||||
|
Expanded bool
|
||||||
|
Nodes []*TreeNode
|
||||||
|
|
||||||
|
// level stores the node level in the tree.
|
||||||
|
level int
|
||||||
|
}
|
||||||
|
|
||||||
|
// TreeWalkFn is a function used for walking a Tree.
|
||||||
|
// To interrupt the walking process function should return false.
|
||||||
|
type TreeWalkFn func(*TreeNode) bool
|
||||||
|
|
||||||
|
func (self *TreeNode) parseStyles(style Style) []Cell {
|
||||||
|
var sb strings.Builder
|
||||||
|
if len(self.Nodes) == 0 {
|
||||||
|
sb.WriteString(strings.Repeat(treeIndent, self.level+1))
|
||||||
|
} else {
|
||||||
|
sb.WriteString(strings.Repeat(treeIndent, self.level))
|
||||||
|
if self.Expanded {
|
||||||
|
sb.WriteRune(Theme.Tree.Expanded)
|
||||||
|
} else {
|
||||||
|
sb.WriteRune(Theme.Tree.Collapsed)
|
||||||
|
}
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
}
|
||||||
|
sb.WriteString(self.Value.String())
|
||||||
|
return ParseStyles(sb.String(), style)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tree is a tree widget.
|
||||||
|
type Tree struct {
|
||||||
|
Block
|
||||||
|
TextStyle Style
|
||||||
|
SelectedRowStyle Style
|
||||||
|
WrapText bool
|
||||||
|
SelectedRow int
|
||||||
|
|
||||||
|
nodes []*TreeNode
|
||||||
|
// rows is flatten nodes for rendering.
|
||||||
|
rows []*TreeNode
|
||||||
|
topRow int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTree creates a new Tree widget.
|
||||||
|
func NewTree() *Tree {
|
||||||
|
return &Tree{
|
||||||
|
Block: *NewBlock(),
|
||||||
|
TextStyle: Theme.Tree.Text,
|
||||||
|
SelectedRowStyle: Theme.Tree.Text,
|
||||||
|
WrapText: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) SetNodes(nodes []*TreeNode) {
|
||||||
|
self.nodes = nodes
|
||||||
|
self.prepareNodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) prepareNodes() {
|
||||||
|
self.rows = make([]*TreeNode, 0)
|
||||||
|
for _, node := range self.nodes {
|
||||||
|
self.prepareNode(node, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) prepareNode(node *TreeNode, level int) {
|
||||||
|
self.rows = append(self.rows, node)
|
||||||
|
node.level = level
|
||||||
|
|
||||||
|
if node.Expanded {
|
||||||
|
for _, n := range node.Nodes {
|
||||||
|
self.prepareNode(n, level+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) Walk(fn TreeWalkFn) {
|
||||||
|
for _, n := range self.nodes {
|
||||||
|
if !self.walk(n, fn) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) walk(n *TreeNode, fn TreeWalkFn) bool {
|
||||||
|
if !fn(n) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, node := range n.Nodes {
|
||||||
|
if !self.walk(node, fn) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) Draw(buf *Buffer) {
|
||||||
|
self.Block.Draw(buf)
|
||||||
|
point := self.Inner.Min
|
||||||
|
|
||||||
|
// adjusts view into widget
|
||||||
|
if self.SelectedRow >= self.Inner.Dy()+self.topRow {
|
||||||
|
self.topRow = self.SelectedRow - self.Inner.Dy() + 1
|
||||||
|
} else if self.SelectedRow < self.topRow {
|
||||||
|
self.topRow = self.SelectedRow
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw rows
|
||||||
|
for row := self.topRow; row < len(self.rows) && point.Y < self.Inner.Max.Y; row++ {
|
||||||
|
cells := self.rows[row].parseStyles(self.TextStyle)
|
||||||
|
if self.WrapText {
|
||||||
|
cells = WrapCells(cells, uint(self.Inner.Dx()))
|
||||||
|
}
|
||||||
|
for j := 0; j < len(cells) && point.Y < self.Inner.Max.Y; j++ {
|
||||||
|
style := cells[j].Style
|
||||||
|
if row == self.SelectedRow {
|
||||||
|
style = self.SelectedRowStyle
|
||||||
|
}
|
||||||
|
if point.X+1 == self.Inner.Max.X+1 && len(cells) > self.Inner.Dx() {
|
||||||
|
buf.SetCell(NewCell(ELLIPSES, style), point.Add(image.Pt(-1, 0)))
|
||||||
|
} else {
|
||||||
|
buf.SetCell(NewCell(cells[j].Rune, style), point)
|
||||||
|
point = point.Add(image.Pt(rw.RuneWidth(cells[j].Rune), 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
point = image.Pt(self.Inner.Min.X, point.Y+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw UP_ARROW if needed
|
||||||
|
if self.topRow > 0 {
|
||||||
|
buf.SetCell(
|
||||||
|
NewCell(UP_ARROW, NewStyle(ColorWhite)),
|
||||||
|
image.Pt(self.Inner.Max.X-1, self.Inner.Min.Y),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw DOWN_ARROW if needed
|
||||||
|
if len(self.rows) > int(self.topRow)+self.Inner.Dy() {
|
||||||
|
buf.SetCell(
|
||||||
|
NewCell(DOWN_ARROW, NewStyle(ColorWhite)),
|
||||||
|
image.Pt(self.Inner.Max.X-1, self.Inner.Max.Y-1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollAmount scrolls by amount given. If amount is < 0, then scroll up.
|
||||||
|
// There is no need to set self.topRow, as this will be set automatically when drawn,
|
||||||
|
// since if the selected item is off screen then the topRow variable will change accordingly.
|
||||||
|
func (self *Tree) ScrollAmount(amount int) {
|
||||||
|
if len(self.rows)-int(self.SelectedRow) <= amount {
|
||||||
|
self.SelectedRow = len(self.rows) - 1
|
||||||
|
} else if int(self.SelectedRow)+amount < 0 {
|
||||||
|
self.SelectedRow = 0
|
||||||
|
} else {
|
||||||
|
self.SelectedRow += amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) SelectedNode() *TreeNode {
|
||||||
|
if len(self.rows) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return self.rows[self.SelectedRow]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) ScrollUp() {
|
||||||
|
self.ScrollAmount(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) ScrollDown() {
|
||||||
|
self.ScrollAmount(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) ScrollPageUp() {
|
||||||
|
// If an item is selected below top row, then go to the top row.
|
||||||
|
if self.SelectedRow > self.topRow {
|
||||||
|
self.SelectedRow = self.topRow
|
||||||
|
} else {
|
||||||
|
self.ScrollAmount(-self.Inner.Dy())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) ScrollPageDown() {
|
||||||
|
self.ScrollAmount(self.Inner.Dy())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) ScrollHalfPageUp() {
|
||||||
|
self.ScrollAmount(-int(FloorFloat64(float64(self.Inner.Dy()) / 2)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) ScrollHalfPageDown() {
|
||||||
|
self.ScrollAmount(int(FloorFloat64(float64(self.Inner.Dy()) / 2)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) ScrollTop() {
|
||||||
|
self.SelectedRow = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) ScrollBottom() {
|
||||||
|
self.SelectedRow = len(self.rows) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) Collapse() {
|
||||||
|
self.rows[self.SelectedRow].Expanded = false
|
||||||
|
self.prepareNodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) Expand() {
|
||||||
|
node := self.rows[self.SelectedRow]
|
||||||
|
if len(node.Nodes) > 0 {
|
||||||
|
self.rows[self.SelectedRow].Expanded = true
|
||||||
|
}
|
||||||
|
self.prepareNodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) ToggleExpand() {
|
||||||
|
node := self.rows[self.SelectedRow]
|
||||||
|
if len(node.Nodes) > 0 {
|
||||||
|
node.Expanded = !node.Expanded
|
||||||
|
}
|
||||||
|
self.prepareNodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) ExpandAll() {
|
||||||
|
self.Walk(func(n *TreeNode) bool {
|
||||||
|
if len(n.Nodes) > 0 {
|
||||||
|
n.Expanded = true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
self.prepareNodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Tree) CollapseAll() {
|
||||||
|
self.Walk(func(n *TreeNode) bool {
|
||||||
|
n.Expanded = false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
self.prepareNodes()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user