diff --git a/README.md b/README.md index 1160705..0bb0c97 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,31 @@ __Demo:__ demo -__Grid layout:(incomplete)__ +__Grid layout:__ -grid +Expressive syntax, using [12 columns grid system](http://www.w3schools.com/bootstrap/bootstrap_grid_system.asp) +```go + 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) +``` +[demo code:](https://github.com/gizak/termui/blob/master/example/grid.go) + +grid ## Installation @@ -58,7 +80,7 @@ Note that components can be overlapped (I'd rather call this a feature...), `Ren ## Themes -_All_ colors in _all_ components _can_ be changed at _any_ time, while there provides some predefined color schemes: +_All_ colors in _all_ components can be changed at _any_ time, while there provides some predefined color schemes: ```go // for now there are only two themes: default and helloworld @@ -114,8 +136,11 @@ The `helloworld` color scheme drops in some colors! ## TODO -- [ ] Grid layout +- [x] Grid layout - [ ] Event system +- [ ] Canvas widget +- [ ] Refine APIs +- [ ] Focusable widgets ## License This library is under the [MIT License](http://opensource.org/licenses/MIT) diff --git a/example/grid.gif b/example/grid.gif index 52ee501..7490043 100644 Binary files a/example/grid.gif and b/example/grid.gif differ diff --git a/example/grid.go b/example/grid.go index a40f767..41f5bc9 100644 --- a/example/grid.go +++ b/example/grid.go @@ -1,6 +1,5 @@ package main -/* import ui "github.com/gizak/termui" import tm "github.com/nsf/termbox-go" import "math" @@ -50,10 +49,43 @@ func main() { lc.AxesColor = ui.ColorWhite lc.LineColor = ui.ColorYellow | ui.AttrBold - ui.Body.Rows = []ui.Row{ + gs := make([]*ui.Gauge, 3) + for i := range gs { + gs[i] = ui.NewGauge() + gs[i].Height = 2 + gs[i].HasBorder = false + gs[i].Percent = i * 10 + gs[i].PaddingBottom = 1 + gs[i].BarColor = ui.ColorRed + } + + ls := ui.NewList() + ls.HasBorder = false + ls.Items = []string{ + "[1] Downloading File 1", + "", // == \newline + "[2] Downloading File 2", + "", + "[3] Uploading File 3", + } + ls.Height = 5 + + par := ui.NewPar("<> This row has 3 columns\n<- Widgets can be stacked up like left side\n<- Stacked widgets are treated as a single widget") + par.Height = 5 + par.Border.Label = "Demonstration" + + // build layout + ui.Body.AddRows( ui.NewRow( - ui.NewCol(sp, 6, 0, true), - ui.NewCol(lc, 6, 0, true))} + ui.NewCol(6, 0, sp), + ui.NewCol(6, 0, lc)), + ui.NewRow( + ui.NewCol(3, 0, ls), + ui.NewCol(3, 0, gs[0], gs[1], gs[2]), + ui.NewCol(6, 0, par))) + + // calculate layout + ui.Body.Align() draw := func(t int) { sp.Lines[0].Data = spdata[t:] @@ -75,7 +107,15 @@ func main() { if e.Type == tm.EventKey && e.Ch == 'q' { return } + if e.Type == tm.EventResize { + ui.Body.Width = ui.TermWidth() + ui.Body.Align() + } default: + for _, g := range gs { + g.Percent = (g.Percent + 3) % 100 + } + draw(i) i++ if i == 102 { @@ -85,4 +125,3 @@ func main() { } } } -*/ diff --git a/grid.go b/grid.go index 8108906..e4f8e3a 100644 --- a/grid.go +++ b/grid.go @@ -1,125 +1,6 @@ package termui -import tm "github.com/nsf/termbox-go" - -/* -type container struct { - height int - width int - BgColor Attribute - Rows []Row -} - -type Row []Col - -type Col struct { - Blocks []ColumnBufferer - Offset int // 0 ~ 11 - Span int // 1 ~ 12 -} - -type ColumnBufferer interface { - Bufferer - GetHeight() int - SetWidth(int) - SetX(int) - SetY(int) -} - -func NewRow(cols ...Col) Row { - return cols -} - -func NewCol(span, offset int, blocks ...ColumnBufferer) Col { - return Col{Blocks: blocks, Span: span, Offset: offset} -} - -// Highest col is the height of a Row -func (r Row) GetHeight() int { - h := 0 - for _, v := range r { - if nh := v.GetHeight(); nh > h { - h = nh - } - } - return h -} - -// Set width according to its span -func (r Row) SetWidth(w int) { - for _, c := range r { - c.SetWidth(int(float64(w*c.Span) / 12.0)) - } -} - -// Set x y -func (r Row) SetX(x int) { - for i := range r { - r[i].SetX(x) - } -} - -func (r Row) SetY(y int) { - for i := range r { - r[i].SetY(y) - } -} - -// GetHeight recursively retrieves height of each children, then add them up. -func (c Col) GetHeight() int { - h := 0 - for _, v := range c.Blocks { - h += c.GetHeight() - } - return h -} - -func (c Col) GetWidth() int { - w := 0 - for _, v := range c.Blocks { - if nw := v.GetWidth(); nw > w { - w = nw - } - } - return w -} - -func (c Col) SetWidth(w int) { - for i := range c.Blocks { - c.SetWidth(w) - } -} - -func (c container) Buffer() []Point { - ps := []Point{} - maxw, _ := tm.Size() - - y := 0 - for _, row := range c.Rows { - x := 0 - maxHeight := 0 - - for _, col := range row { - if h := col.GetHeight(); h > maxHeight { - maxHeight = h - } - - w := int(float64(maxw*(col.Span+col.Offset)) / 12.0) - if col.GetWidth() > w { - col.SetWidth(w) - } - - col.SetY(y) - col.SetX(x) - ps = append(ps, col.Buffer()...) - x += w + int(float64(maxw*col.Offset)/12) - } - y += maxHeight - } - return ps -} -*/ - +// Bufferers that can be manipulated by Grid type LayoutBufferer interface { Bufferer GetHeight() int @@ -130,8 +11,8 @@ type LayoutBufferer interface { // build a layout tree type row struct { - Cols []*row - Widget LayoutBufferer // only leaves hold this + Cols []*row //children + Widget LayoutBufferer // root X int Y int Width int @@ -140,15 +21,9 @@ type row struct { Offset int } -func newContainer() *row { - w, _ := tm.Size() - r := &row{Width: w, Span: 12, X: 0, Y: 0, Cols: []*row{}} - return r -} - -func (r *row) layout() { +func (r *row) calcLayout() { r.assignWidth(r.Width) - r.solveHeight() + r.Height = r.solveHeight() r.assignX(r.X) r.assignY(r.Y) } @@ -163,14 +38,14 @@ func (r *row) isRenderableLeaf() bool { func (r *row) assignWidth(w int) { cw := int(float64(w*r.Span) / 12) - r.Width = cw + r.SetWidth(cw) for i, _ := range r.Cols { r.Cols[i].assignWidth(cw) } } -// bottom up +// bottom up, return r's total height func (r *row) solveHeight() int { if r.isRenderableLeaf() { r.Height = r.Widget.GetHeight() @@ -196,9 +71,7 @@ func (r *row) solveHeight() int { } func (r *row) assignX(x int) { - if r.isRenderableLeaf() { - r.Widget.SetX(x) - } + r.SetX(x) if !r.isLeaf() { acc := 0 @@ -210,14 +83,12 @@ func (r *row) assignX(x int) { } } } - r.X = x } func (r *row) assignY(y int) { - r.Y = y + r.SetY(y) - if r.isRenderableLeaf() { - r.Widget.SetY(y) + if r.isLeaf() { return } @@ -231,6 +102,31 @@ func (r *row) assignY(y int) { } +func (r row) GetHeight() int { + return r.Height +} + +func (r *row) SetX(x int) { + r.X = x + if r.Widget != nil { + r.Widget.SetX(x) + } +} + +func (r *row) SetY(y int) { + r.Y = y + if r.Widget != nil { + r.Widget.SetY(y) + } +} + +func (r *row) SetWidth(w int) { + r.Width = w + if r.Widget != nil { + r.Widget.SetWidth(w) + } +} + // recursively merge all widgets buffer func (r *row) Buffer() []Point { merged := []Point{} @@ -239,6 +135,12 @@ func (r *row) Buffer() []Point { return r.Widget.Buffer() } + // for those are not leaves but have a renderable widget + if r.Widget != nil { + merged = append(merged, r.Widget.Buffer()...) + } + + // collect buffer from children if !r.isLeaf() { for _, c := range r.Cols { merged = append(merged, c.Buffer()...) @@ -248,4 +150,73 @@ func (r *row) Buffer() []Point { return merged } -//var Body container +type Grid struct { + Rows []*row + Width int + X int + Y int + BgColor Attribute +} + +func NewGrid(rows ...*row) *Grid { + return &Grid{Rows: rows} +} + +func (g *Grid) AddRows(rs ...*row) { + g.Rows = append(g.Rows, rs...) +} + +func NewRow(cols ...*row) *row { + rs := &row{Span: 12, Cols: cols} + return rs +} + +// NewCol accepts: widgets are LayoutBufferer or +// widgets is A NewRow +// Note that if multiple widgets are provided, they will stack up in the col +func NewCol(span, offset int, widgets ...LayoutBufferer) *row { + r := &row{Span: span, Offset: offset} + + if widgets != nil && len(widgets) == 1 { + wgt := widgets[0] + nw, isRow := wgt.(*row) + if isRow { + r.Cols = nw.Cols + } else { + r.Widget = wgt + } + return r + } + + r.Cols = []*row{} + ir := r + for _, w := range widgets { + nr := &row{Span: 12, Widget: w} + ir.Cols = []*row{nr} + ir = nr + } + + return r +} + +// Calculate each rows' layout +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() + } +} + +func (g Grid) Buffer() []Point { + ps := []Point{} + for _, r := range g.Rows { + ps = append(ps, r.Buffer()...) + } + return ps +} + +var Body *Grid diff --git a/grid_test.go b/grid_test.go index cd906bb..7712f3f 100644 --- a/grid_test.go +++ b/grid_test.go @@ -30,22 +30,32 @@ func TestRowWidth(t *testing.T) { / 1100:w */ - r = &row{ - Span: 12, - Cols: []*row{ - &row{Widget: p0, Span: 6}, - &row{ - Span: 6, - Cols: []*row{ - &row{Widget: p1, Span: 6}, - &row{ - Span: 6, - Cols: []*row{ - &row{ - Span: 12, - Widget: p2, - Cols: []*row{ - &row{Span: 12, Widget: p3}}}}}}}}} + /* + r = &row{ + Span: 12, + Cols: []*row{ + &row{Widget: p0, Span: 6}, + &row{ + Span: 6, + Cols: []*row{ + &row{Widget: p1, Span: 6}, + &row{ + Span: 6, + Cols: []*row{ + &row{ + Span: 12, + Widget: p2, + Cols: []*row{ + &row{Span: 12, Widget: p3}}}}}}}}} + */ + + r = NewRow( + NewCol(6, 0, p0), + NewCol(6, 0, + NewRow( + NewCol(6, 0, p1), + NewCol(6, 0, p2, p3)))) + r.assignWidth(100) if r.Width != 100 || (r.Cols[0].Width) != 50 || @@ -60,6 +70,7 @@ func TestRowWidth(t *testing.T) { func TestRowHeight(t *testing.T) { spew.Dump() + if (r.solveHeight()) != 2 || (r.Cols[1].Cols[1].Height) != 2 || (r.Cols[1].Cols[1].Cols[0].Height) != 2 || diff --git a/render.go b/render.go index c701f0b..c0f3100 100644 --- a/render.go +++ b/render.go @@ -8,10 +8,14 @@ type Bufferer interface { } func Init() error { - // Body = container{ - // BgColor: theme.BodyBg, - // Rows: []Row{}, - // } + Body = NewGrid() + Body.X = 0 + Body.Y = 0 + Body.BgColor = theme.BodyBg + defer (func() { + w, _ := tm.Size() + Body.Width = w + })() return tm.Init() } @@ -19,6 +23,16 @@ func Close() { tm.Close() } +func TermWidth() int { + w, _ := tm.Size() + return w +} + +func TermHeight() int { + _, h := tm.Size() + return h +} + // render all from left to right, right could overlap on left ones func Render(rs ...Bufferer) { tm.Clear(tm.ColorDefault, toTmAttr(theme.BodyBg))