mirror of https://github.com/gizak/termui.git
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)
|
||||
- [Image](./_examples/image.go)
|
||||
- [List](./_examples/list.go)
|
||||
- [Tree](./_examples/tree.go)
|
||||
- [Paragraph](./_examples/paragraph.go)
|
||||
- [PieChart](./_examples/piechart.go)
|
||||
- [Plot](./_examples/plot.go) (for scatterplots and linecharts)
|
||||
|
|
|
@ -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 = '▲'
|
||||
DOWN_ARROW = '▼'
|
||||
|
||||
COLLAPSED = '+'
|
||||
EXPANDED = '−'
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
13
v3/theme.go
13
v3/theme.go
|
@ -33,6 +33,7 @@ type RootTheme struct {
|
|||
Gauge GaugeTheme
|
||||
Plot PlotTheme
|
||||
List ListTheme
|
||||
Tree TreeTheme
|
||||
Paragraph ParagraphTheme
|
||||
PieChart PieChartTheme
|
||||
Sparkline SparklineTheme
|
||||
|
@ -66,6 +67,12 @@ type ListTheme struct {
|
|||
Text Style
|
||||
}
|
||||
|
||||
type TreeTheme struct {
|
||||
Text Style
|
||||
Collapsed rune
|
||||
Expanded rune
|
||||
}
|
||||
|
||||
type ParagraphTheme struct {
|
||||
Text Style
|
||||
}
|
||||
|
@ -122,6 +129,12 @@ var Theme = RootTheme{
|
|||
Text: NewStyle(ColorWhite),
|
||||
},
|
||||
|
||||
Tree: TreeTheme{
|
||||
Text: NewStyle(ColorWhite),
|
||||
Collapsed: COLLAPSED,
|
||||
Expanded: EXPANDED,
|
||||
},
|
||||
|
||||
StackedBarChart: StackedBarChartTheme{
|
||||
Bars: StandardColors,
|
||||
Nums: StandardStyles,
|
||||
|
|
|
@ -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