2015-03-20 14:21:50 -06:00
|
|
|
// Copyright 2015 Zack Guo <gizak@icloud.com>. All rights reserved.
|
|
|
|
// Use of this source code is governed by a MIT license that can
|
|
|
|
// be found in the LICENSE file.
|
|
|
|
|
2015-03-15 13:56:38 -06:00
|
|
|
package termui
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// GridBufferer introduces a Bufferer that can be manipulated by Grid.
|
|
|
|
type GridBufferer interface {
|
2015-03-19 10:00:38 -06:00
|
|
|
Bufferer
|
|
|
|
GetHeight() int
|
|
|
|
SetWidth(int)
|
|
|
|
SetX(int)
|
|
|
|
SetY(int)
|
|
|
|
}
|
|
|
|
|
2015-05-05 09:08:45 -06:00
|
|
|
// Row builds a layout tree
|
|
|
|
type Row struct {
|
|
|
|
Cols []*Row //children
|
2015-03-24 15:16:43 -06:00
|
|
|
Widget GridBufferer // root
|
2015-03-19 10:00:38 -06:00
|
|
|
X int
|
|
|
|
Y int
|
|
|
|
Width int
|
|
|
|
Height int
|
|
|
|
Span int
|
|
|
|
Offset int
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// calculate and set the underlying layout tree's x, y, height and width.
|
2015-05-05 09:08:45 -06:00
|
|
|
func (r *Row) calcLayout() {
|
2015-03-19 10:00:38 -06:00
|
|
|
r.assignWidth(r.Width)
|
2015-03-20 06:24:48 -06:00
|
|
|
r.Height = r.solveHeight()
|
2015-03-19 10:00:38 -06:00
|
|
|
r.assignX(r.X)
|
|
|
|
r.assignY(r.Y)
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// tell if the node is leaf in the tree.
|
2015-05-05 09:08:45 -06:00
|
|
|
func (r *Row) isLeaf() bool {
|
2015-03-19 10:00:38 -06:00
|
|
|
return r.Cols == nil || len(r.Cols) == 0
|
|
|
|
}
|
|
|
|
|
2015-05-05 09:08:45 -06:00
|
|
|
func (r *Row) isRenderableLeaf() bool {
|
2015-03-19 10:00:38 -06:00
|
|
|
return r.isLeaf() && r.Widget != nil
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// assign widgets' (and their parent rows') width recursively.
|
2015-05-05 09:08:45 -06:00
|
|
|
func (r *Row) assignWidth(w int) {
|
2015-04-10 07:57:34 -06:00
|
|
|
r.SetWidth(w)
|
2015-03-19 10:00:38 -06:00
|
|
|
|
2015-04-10 07:57:34 -06:00
|
|
|
accW := 0 // acc span and offset
|
|
|
|
calcW := make([]int, len(r.Cols)) // calculated width
|
|
|
|
calcOftX := make([]int, len(r.Cols)) // computated start position of x
|
|
|
|
|
|
|
|
for i, c := range r.Cols {
|
|
|
|
accW += c.Span + c.Offset
|
|
|
|
cw := int(float64(c.Span*r.Width) / 12.0)
|
|
|
|
|
|
|
|
if i >= 1 {
|
|
|
|
calcOftX[i] = calcOftX[i-1] +
|
|
|
|
calcW[i-1] +
|
|
|
|
int(float64(r.Cols[i-1].Offset*r.Width)/12.0)
|
|
|
|
}
|
|
|
|
|
|
|
|
// use up the space if it is the last col
|
|
|
|
if i == len(r.Cols)-1 && accW == 12 {
|
|
|
|
cw = r.Width - calcOftX[i]
|
|
|
|
}
|
|
|
|
calcW[i] = cw
|
2015-03-19 10:00:38 -06:00
|
|
|
r.Cols[i].assignWidth(cw)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// bottom up calc and set rows' (and their widgets') height,
|
|
|
|
// return r's total height.
|
2015-05-05 09:08:45 -06:00
|
|
|
func (r *Row) solveHeight() int {
|
2015-03-19 10:00:38 -06:00
|
|
|
if r.isRenderableLeaf() {
|
|
|
|
r.Height = r.Widget.GetHeight()
|
|
|
|
return r.Widget.GetHeight()
|
|
|
|
}
|
|
|
|
|
|
|
|
maxh := 0
|
|
|
|
if !r.isLeaf() {
|
|
|
|
for _, c := range r.Cols {
|
|
|
|
nh := c.solveHeight()
|
|
|
|
// when embed rows in Cols, row widgets stack up
|
|
|
|
if r.Widget != nil {
|
|
|
|
nh += r.Widget.GetHeight()
|
|
|
|
}
|
|
|
|
if nh > maxh {
|
|
|
|
maxh = nh
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
r.Height = maxh
|
|
|
|
return maxh
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// recursively assign x position for r tree.
|
2015-05-05 09:08:45 -06:00
|
|
|
func (r *Row) assignX(x int) {
|
2015-03-20 06:24:48 -06:00
|
|
|
r.SetX(x)
|
2015-03-19 10:00:38 -06:00
|
|
|
|
|
|
|
if !r.isLeaf() {
|
|
|
|
acc := 0
|
|
|
|
for i, c := range r.Cols {
|
|
|
|
if c.Offset != 0 {
|
2015-04-10 07:57:34 -06:00
|
|
|
acc += int(float64(c.Offset*r.Width) / 12.0)
|
2015-03-19 10:00:38 -06:00
|
|
|
}
|
2015-04-09 15:39:46 -06:00
|
|
|
r.Cols[i].assignX(x + acc)
|
|
|
|
acc += c.Width
|
2015-03-19 10:00:38 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// recursively assign y position to r.
|
2015-05-05 09:08:45 -06:00
|
|
|
func (r *Row) assignY(y int) {
|
2015-03-20 06:24:48 -06:00
|
|
|
r.SetY(y)
|
2015-03-19 10:00:38 -06:00
|
|
|
|
2015-03-20 06:24:48 -06:00
|
|
|
if r.isLeaf() {
|
2015-03-19 10:00:38 -06:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := range r.Cols {
|
|
|
|
acc := 0
|
|
|
|
if r.Widget != nil {
|
|
|
|
acc = r.Widget.GetHeight()
|
|
|
|
}
|
|
|
|
r.Cols[i].assignY(y + acc)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// GetHeight implements GridBufferer interface.
|
2015-05-05 09:08:45 -06:00
|
|
|
func (r Row) GetHeight() int {
|
2015-03-20 06:24:48 -06:00
|
|
|
return r.Height
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// SetX implements GridBufferer interface.
|
2015-05-05 09:08:45 -06:00
|
|
|
func (r *Row) SetX(x int) {
|
2015-03-20 06:24:48 -06:00
|
|
|
r.X = x
|
|
|
|
if r.Widget != nil {
|
|
|
|
r.Widget.SetX(x)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// SetY implements GridBufferer interface.
|
2015-05-05 09:08:45 -06:00
|
|
|
func (r *Row) SetY(y int) {
|
2015-03-20 06:24:48 -06:00
|
|
|
r.Y = y
|
|
|
|
if r.Widget != nil {
|
|
|
|
r.Widget.SetY(y)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// SetWidth implements GridBufferer interface.
|
2015-05-05 09:08:45 -06:00
|
|
|
func (r *Row) SetWidth(w int) {
|
2015-03-20 06:24:48 -06:00
|
|
|
r.Width = w
|
|
|
|
if r.Widget != nil {
|
|
|
|
r.Widget.SetWidth(w)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// Buffer implements Bufferer interface,
|
2015-03-19 10:00:38 -06:00
|
|
|
// recursively merge all widgets buffer
|
2015-05-05 09:08:45 -06:00
|
|
|
func (r *Row) Buffer() []Point {
|
2015-03-19 10:00:38 -06:00
|
|
|
merged := []Point{}
|
|
|
|
|
|
|
|
if r.isRenderableLeaf() {
|
|
|
|
return r.Widget.Buffer()
|
|
|
|
}
|
|
|
|
|
2015-03-20 06:24:48 -06:00
|
|
|
// for those are not leaves but have a renderable widget
|
|
|
|
if r.Widget != nil {
|
|
|
|
merged = append(merged, r.Widget.Buffer()...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// collect buffer from children
|
2015-03-19 10:00:38 -06:00
|
|
|
if !r.isLeaf() {
|
|
|
|
for _, c := range r.Cols {
|
|
|
|
merged = append(merged, c.Buffer()...)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return merged
|
|
|
|
}
|
2015-03-15 13:56:38 -06:00
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// Grid implements 12 columns system.
|
|
|
|
// A simple example:
|
|
|
|
/*
|
|
|
|
import ui "github.com/gizak/termui"
|
|
|
|
// init and create widgets...
|
|
|
|
|
|
|
|
// build
|
|
|
|
ui.Body.AddRows(
|
|
|
|
ui.NewRow(
|
|
|
|
ui.NewCol(6, 0, widget0),
|
|
|
|
ui.NewCol(6, 0, widget1)),
|
|
|
|
ui.NewRow(
|
|
|
|
ui.NewCol(3, 0, widget2),
|
|
|
|
ui.NewCol(3, 0, widget30, widget31, widget32),
|
|
|
|
ui.NewCol(6, 0, widget4)))
|
|
|
|
|
|
|
|
// calculate layout
|
|
|
|
ui.Body.Align()
|
|
|
|
|
|
|
|
ui.Render(ui.Body)
|
|
|
|
*/
|
2015-03-20 06:24:48 -06:00
|
|
|
type Grid struct {
|
2015-05-05 09:08:45 -06:00
|
|
|
Rows []*Row
|
2015-03-20 06:24:48 -06:00
|
|
|
Width int
|
|
|
|
X int
|
|
|
|
Y int
|
|
|
|
BgColor Attribute
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// NewGrid returns *Grid with given rows.
|
2015-05-05 09:08:45 -06:00
|
|
|
func NewGrid(rows ...*Row) *Grid {
|
2015-03-20 06:24:48 -06:00
|
|
|
return &Grid{Rows: rows}
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// AddRows appends given rows to Grid.
|
2015-05-05 09:08:45 -06:00
|
|
|
func (g *Grid) AddRows(rs ...*Row) {
|
2015-03-20 06:24:48 -06:00
|
|
|
g.Rows = append(g.Rows, rs...)
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// NewRow creates a new row out of given columns.
|
2015-05-05 09:08:45 -06:00
|
|
|
func NewRow(cols ...*Row) *Row {
|
|
|
|
rs := &Row{Span: 12, Cols: cols}
|
2015-03-20 06:24:48 -06:00
|
|
|
return rs
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// NewCol accepts: widgets are LayoutBufferer or widgets is A NewRow.
|
|
|
|
// Note that if multiple widgets are provided, they will stack up in the col.
|
2015-05-05 09:08:45 -06:00
|
|
|
func NewCol(span, offset int, widgets ...GridBufferer) *Row {
|
|
|
|
r := &Row{Span: span, Offset: offset}
|
2015-03-20 06:24:48 -06:00
|
|
|
|
|
|
|
if widgets != nil && len(widgets) == 1 {
|
|
|
|
wgt := widgets[0]
|
2015-05-05 09:08:45 -06:00
|
|
|
nw, isRow := wgt.(*Row)
|
2015-03-20 06:24:48 -06:00
|
|
|
if isRow {
|
|
|
|
r.Cols = nw.Cols
|
|
|
|
} else {
|
|
|
|
r.Widget = wgt
|
|
|
|
}
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2015-05-05 09:08:45 -06:00
|
|
|
r.Cols = []*Row{}
|
2015-03-20 06:24:48 -06:00
|
|
|
ir := r
|
|
|
|
for _, w := range widgets {
|
2015-05-05 09:08:45 -06:00
|
|
|
nr := &Row{Span: 12, Widget: w}
|
|
|
|
ir.Cols = []*Row{nr}
|
2015-03-20 06:24:48 -06:00
|
|
|
ir = nr
|
|
|
|
}
|
|
|
|
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// Align calculate each rows' layout.
|
2015-03-20 06:24:48 -06:00
|
|
|
func (g *Grid) Align() {
|
|
|
|
h := 0
|
|
|
|
for _, r := range g.Rows {
|
|
|
|
r.SetWidth(g.Width)
|
|
|
|
r.SetX(g.X)
|
|
|
|
r.SetY(g.Y + h)
|
|
|
|
r.calcLayout()
|
|
|
|
h += r.GetHeight()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// Buffer implments Bufferer interface.
|
2015-03-20 06:24:48 -06:00
|
|
|
func (g Grid) Buffer() []Point {
|
|
|
|
ps := []Point{}
|
|
|
|
for _, r := range g.Rows {
|
|
|
|
ps = append(ps, r.Buffer()...)
|
|
|
|
}
|
|
|
|
return ps
|
|
|
|
}
|
|
|
|
|
2015-03-24 15:16:43 -06:00
|
|
|
// Body corresponds to the entire terminal display region.
|
2015-03-20 06:24:48 -06:00
|
|
|
var Body *Grid
|