1
0
mirror of https://github.com/mum4k/termdash.git synced 2025-04-25 13:48:50 +08:00

Adding a grid builder.

This commit is contained in:
Jakub Sobon 2019-03-10 22:59:44 -04:00
parent e6ef3f7ed1
commit 2cb4cc9797
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
3 changed files with 993 additions and 0 deletions

View File

@ -39,6 +39,7 @@ project reaches version 1.0.0. Any breaking changes will be published in the
- Full support for terminal window resizing throughout the infrastructure. - Full support for terminal window resizing throughout the infrastructure.
- Customizable layout, widget placement, borders, margins, padding, colors, etc. - Customizable layout, widget placement, borders, margins, padding, colors, etc.
- Binary tree and Grid forms of setting up the layout.
- Focusable containers and widgets. - Focusable containers and widgets.
- Processing of keyboard and mouse events. - Processing of keyboard and mouse events.
- Periodic and event driven screen redraw. - Periodic and event driven screen redraw.

252
container/grid/grid.go Normal file
View File

@ -0,0 +1,252 @@
// Package grid helps to build grid layouts.
package grid
import (
"fmt"
"github.com/mum4k/termdash/container"
"github.com/mum4k/termdash/widgetapi"
)
// Builder builds grid layouts.
type Builder struct {
elems []Element
}
// New returns a new grid builder.
func New() *Builder {
return &Builder{}
}
// Add adds the specified elements.
// The subElements can be either a single Widget or any combination of Rows and
// Columns.
// Rows are created using RowHeightPerc() and Columns are created using
// ColWidthPerc().
// Can be called repeatedly, e.g. to add multiple Rows or Columns.
func (b *Builder) Add(subElements ...Element) {
b.elems = append(b.elems, subElements...)
}
// Build builds the grid layout and returns the corresponding container
// options.
func (b *Builder) Build() ([]container.Option, error) {
if err := validate(b.elems); err != nil {
return nil, err
}
return build(b.elems, 100, 100), nil
}
// validate recursively validates the elements that were added to the builder.
// Validates the following per each level of Rows or Columns.:
// The subElements are either exactly one Widget or any number of Rows and
// Columns.
// Each individual width or height is in the range 0 < v < 100.
// The sum of all widths is <= 100.
// The sum of all heights is <= 100.
func validate(elems []Element) error {
heightSum := 0
widthSum := 0
for _, elem := range elems {
switch e := elem.(type) {
case *row:
if min, max := 0, 100; e.heightPerc <= min || e.heightPerc >= max {
return fmt.Errorf("invalid row heightPerc(%d), must be a value in the range %d < v < %d", e.heightPerc, min, max)
}
heightSum += e.heightPerc
if err := validate(e.subElem); err != nil {
return err
}
case *col:
if min, max := 0, 100; e.widthPerc <= min || e.widthPerc >= max {
return fmt.Errorf("invalid column widthPerc(%d), must be a value in the range %d < v < %d", e.widthPerc, min, max)
}
widthSum += e.widthPerc
if err := validate(e.subElem); err != nil {
return err
}
case *widget:
if len(elems) > 1 {
return fmt.Errorf("when adding a widget, it must be the only added element at that level, got: %v", elems)
}
}
}
if max := 100; heightSum > max || widthSum > max {
return fmt.Errorf("the sum of all height percentages(%d) and width percentages(%d) at one element level cannot be larger than %d", heightSum, widthSum, max)
}
return nil
}
// build recursively builds the container options according to the elements
// that were added to the builder.
// The parentHeightPerc and parentWidthPerc percent indicate the relative size
// of the element we are building now in the parent element. See innerPerc()
// for more details.
func build(elems []Element, parentHeightPerc, parentWidthPerc int) []container.Option {
if len(elems) == 0 {
return nil
}
elem := elems[0]
elems = elems[1:]
switch e := elem.(type) {
case *row:
if len(elems) > 0 {
perc := innerPerc(e.heightPerc, parentHeightPerc)
childHeightPerc := parentHeightPerc - e.heightPerc
return []container.Option{
container.SplitHorizontal(
container.Top(build(e.subElem, 100, parentWidthPerc)...),
container.Bottom(build(elems, childHeightPerc, parentWidthPerc)...),
container.SplitPercent(perc),
),
}
} else {
return build(e.subElem, 100, parentWidthPerc)
}
case *col:
if len(elems) > 0 {
perc := innerPerc(e.widthPerc, parentWidthPerc)
childWidthPerc := parentWidthPerc - e.widthPerc
return []container.Option{
container.SplitVertical(
container.Left(build(e.subElem, parentHeightPerc, 100)...),
container.Right(build(elems, parentHeightPerc, childWidthPerc)...),
container.SplitPercent(perc),
),
}
} else {
return build(e.subElem, parentHeightPerc, 100)
}
case *widget:
opts := e.cOpts
opts = append(opts, container.PlaceWidget(e.widget))
return opts
}
return nil
}
// innerPerc translates the outer split percentage into the inner one.
// E.g. multiple rows would specify that they want the outer split percentage
// of 25% each, but we are representing them in a tree of containers so the
// inner splits vary:
// ╭─────────╮
// 25% │ 25% │
// │╭───────╮│ ---
// 25% ││ 33% ││
// ││╭─────╮││
// 25% │││ 50% │││
// ││├─────┤││ 75%
// 25% │││ 50% │││
// ││╰─────╯││
// │╰───────╯│
// ╰─────────╯ ---
//
// Argument outerPerc is the user specified percentage for the split, i.e. the
// 25% in the example above.
// Argument parentPerc is the percentage this container has in the parent, i.e.
// 75% for the first inner container in the example above.
func innerPerc(outerPerc, parentPerc int) int {
// parentPerc * parentHeightCells = childHeightCells
// innerPerc * childHeightCells = outerPerc * parentHeightCells
// innerPerc * parentPerc * parentHeightCells = outerPerc * parentHeightCells
// innerPerc * parentPerc = outerPerc
// innerPerc = outerPerc / parentPerc
return int(float64(outerPerc) / float64(parentPerc) * 100)
}
// Element is an element that can be added to the grid.
type Element interface {
isElement()
}
// row is a row in the grid.
// row implements Element.
type row struct {
// heightPerc is the height percentage this row occupies.
heightPerc int
// subElem are the sub Rows or Columns or a single widget.
subElem []Element
}
// isElement implements Element.isElement.
func (row) isElement() {}
// String implements fmt.Stringer.
func (r *row) String() string {
return fmt.Sprintf("row{height:%d, sub:%v}", r.heightPerc, r.subElem)
}
// col is a column in the grid.
// col implements Element.
type col struct {
// widthPerc is the width percentage this column occupies.
widthPerc int
// subElem are the sub Rows or Columns or a single widget.
subElem []Element
}
// isElement implements Element.isElement.
func (col) isElement() {}
// String implements fmt.Stringer.
func (c *col) String() string {
return fmt.Sprintf("col{width:%d, sub:%v}", c.widthPerc, c.subElem)
}
// widget is a widget placed into the grid.
// widget implements Element.
type widget struct {
// widget is the widget instance.
widget widgetapi.Widget
// cOpts are the options for the widget's container.
cOpts []container.Option
}
// String implements fmt.Stringer.
func (w *widget) String() string {
return fmt.Sprintf("widget{type:%T}", w.widget)
}
// isElement implements Element.isElement.
func (widget) isElement() {}
// RowHeightPerc creates a row of the specified height.
// The height is supplied as height percentage of the outer container.
// The subElements can be either a single Widget or any combination of Rows and
// Columns.
func RowHeightPerc(heightPerc int, subElements ...Element) Element {
return &row{
heightPerc: heightPerc,
subElem: subElements,
}
}
// ColWidthPerc creates a column of the specified width.
// The width is supplied as width percentage of the outer container.
// The subElements can be either a single Widget or any combination of Rows and
// Columns.
func ColWidthPerc(widthPerc int, subElements ...Element) Element {
return &col{
widthPerc: widthPerc,
subElem: subElements,
}
}
// Widget adds a widget into the Row or Column.
// The options will be applied to the container that directly holds this
// widget.
func Widget(w widgetapi.Widget, cOpts ...container.Option) Element {
return &widget{
widget: w,
cOpts: cOpts,
}
}

740
container/grid/grid_test.go Normal file
View File

@ -0,0 +1,740 @@
package grid
import (
"context"
"image"
"testing"
"time"
"github.com/mum4k/termdash"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/container"
"github.com/mum4k/termdash/internal/area"
"github.com/mum4k/termdash/internal/canvas/testcanvas"
"github.com/mum4k/termdash/internal/draw"
"github.com/mum4k/termdash/internal/draw/testdraw"
"github.com/mum4k/termdash/internal/faketerm"
"github.com/mum4k/termdash/internal/fakewidget"
"github.com/mum4k/termdash/linestyle"
"github.com/mum4k/termdash/terminal/termbox"
"github.com/mum4k/termdash/widgetapi"
"github.com/mum4k/termdash/widgets/barchart"
)
// Shows how to create a simple 4x4 grid with four widgets.
// All the cells in the grid contain the same widget in this example.
func Example() {
tbx, err := termbox.New()
if err != nil {
panic(err)
}
defer tbx.Close()
bc, err := barchart.New()
if err != nil {
panic(err)
}
builder := New()
builder.Add(
RowHeightPerc(
50,
ColWidthPerc(50, Widget(bc)),
ColWidthPerc(50, Widget(bc)),
),
RowHeightPerc(
50,
ColWidthPerc(50, Widget(bc)),
ColWidthPerc(50, Widget(bc)),
),
)
gridOpts, err := builder.Build()
if err != nil {
panic(err)
}
cont, err := container.New(tbx, gridOpts...)
if err != nil {
panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := termdash.Run(ctx, tbx, cont); err != nil {
panic(err)
}
}
// Shows how to create rows iteratively. Each row contains two columns and each
// column contains the same widget.
func Example_iterative() {
tbx, err := termbox.New()
if err != nil {
panic(err)
}
defer tbx.Close()
bc, err := barchart.New()
if err != nil {
panic(err)
}
builder := New()
for i := 0; i < 5; i++ {
builder.Add(
RowHeightPerc(
20,
ColWidthPerc(50, Widget(bc)),
ColWidthPerc(50, Widget(bc)),
),
)
}
gridOpts, err := builder.Build()
if err != nil {
panic(err)
}
cont, err := container.New(tbx, gridOpts...)
if err != nil {
panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := termdash.Run(ctx, tbx, cont); err != nil {
panic(err)
}
}
// mirror returns a new fake widget.
func mirror() *fakewidget.Mirror {
return fakewidget.New(widgetapi.Options{})
}
// mustHSplit splits the area or panics.
func mustHSplit(ar image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle) {
t, b, err := area.HSplit(ar, heightPerc)
if err != nil {
panic(err)
}
return t, b
}
// mustVSplit splits the area or panics.
func mustVSplit(ar image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle) {
l, r, err := area.VSplit(ar, widthPerc)
if err != nil {
panic(err)
}
return l, r
}
func TestBuilder(t *testing.T) {
tests := []struct {
desc string
termSize image.Point
builder *Builder
want func(size image.Point) *faketerm.Terminal
wantErr bool
}{
{
desc: "fails when Widget is mixed with Rows and Columns at top level",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(
RowHeightPerc(50),
Widget(mirror()),
)
return b
}(),
wantErr: true,
},
{
desc: "fails when Widget is mixed with Rows and Columns at sub level",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(
RowHeightPerc(
50,
RowHeightPerc(50),
Widget(mirror()),
),
)
return b
}(),
wantErr: true,
},
{
desc: "fails when Row heightPerc is too low at top level",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(
RowHeightPerc(0),
)
return b
}(),
wantErr: true,
},
{
desc: "fails when Row heightPerc is too low at sub level",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(
RowHeightPerc(
50,
RowHeightPerc(0),
),
)
return b
}(),
wantErr: true,
},
{
desc: "fails when Row heightPerc is too high at top level",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(
RowHeightPerc(100),
)
return b
}(),
wantErr: true,
},
{
desc: "fails when Row heightPerc is too high at sub level",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(
RowHeightPerc(
50,
RowHeightPerc(100),
),
)
return b
}(),
wantErr: true,
},
{
desc: "fails when Col widthPerc is too low at top level",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(
ColWidthPerc(0),
)
return b
}(),
wantErr: true,
},
{
desc: "fails when Col widthPerc is too low at sub level",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(
ColWidthPerc(
50,
ColWidthPerc(0),
),
)
return b
}(),
wantErr: true,
},
{
desc: "fails when Col widthPerc is too high at top level",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(
ColWidthPerc(100),
)
return b
}(),
wantErr: true,
},
{
desc: "fails when Col widthPerc is too high at sub level",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(
ColWidthPerc(
50,
ColWidthPerc(100),
),
)
return b
}(),
wantErr: true,
},
{
desc: "fails when height sum is too large at top level",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(
RowHeightPerc(50),
RowHeightPerc(50),
RowHeightPerc(1),
)
return b
}(),
wantErr: true,
},
{
desc: "fails when height sum is too large at sub level",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(
RowHeightPerc(
50,
RowHeightPerc(50),
RowHeightPerc(50),
RowHeightPerc(1),
),
)
return b
}(),
wantErr: true,
},
{
desc: "fails when width sum is too large at top level",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(
ColWidthPerc(50),
ColWidthPerc(50),
ColWidthPerc(1),
)
return b
}(),
wantErr: true,
},
{
desc: "fails when width sum is too large at sub level",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(
ColWidthPerc(
50,
ColWidthPerc(50),
ColWidthPerc(50),
ColWidthPerc(1),
),
)
return b
}(),
wantErr: true,
},
{
desc: "empty container when nothing is added",
termSize: image.Point{10, 10},
builder: func() *Builder {
return New()
}(),
},
{
desc: "widget in the outer most container",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(Widget(mirror()))
return b
}(),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := testcanvas.MustNew(ft.Area())
fakewidget.MustDraw(ft, cvs, widgetapi.Options{})
return ft
},
},
{
desc: "two equal rows",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(RowHeightPerc(50, Widget(mirror())))
b.Add(RowHeightPerc(50, Widget(mirror())))
return b
}(),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
top, bot := mustHSplit(ft.Area(), 50)
fakewidget.MustDraw(ft, testcanvas.MustNew(top), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(bot), widgetapi.Options{})
return ft
},
},
{
desc: "two unequal rows",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(RowHeightPerc(20, Widget(mirror())))
b.Add(RowHeightPerc(80, Widget(mirror())))
return b
}(),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
top, bot := mustHSplit(ft.Area(), 20)
fakewidget.MustDraw(ft, testcanvas.MustNew(top), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(bot), widgetapi.Options{})
return ft
},
},
{
desc: "two equal columns",
termSize: image.Point{20, 10},
builder: func() *Builder {
b := New()
b.Add(ColWidthPerc(50, Widget(mirror())))
b.Add(ColWidthPerc(50, Widget(mirror())))
return b
}(),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
left, right := mustVSplit(ft.Area(), 50)
fakewidget.MustDraw(ft, testcanvas.MustNew(left), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(right), widgetapi.Options{})
return ft
},
},
{
desc: "two unequal columns",
termSize: image.Point{40, 10},
builder: func() *Builder {
b := New()
b.Add(ColWidthPerc(20, Widget(mirror())))
b.Add(ColWidthPerc(80, Widget(mirror())))
return b
}(),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
left, right := mustVSplit(ft.Area(), 20)
fakewidget.MustDraw(ft, testcanvas.MustNew(left), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(right), widgetapi.Options{})
return ft
},
},
{
desc: "rows with columns (equal)",
termSize: image.Point{20, 20},
builder: func() *Builder {
b := New()
b.Add(
RowHeightPerc(
50,
ColWidthPerc(50, Widget(mirror())),
ColWidthPerc(50, Widget(mirror())),
),
RowHeightPerc(
50,
ColWidthPerc(50, Widget(mirror())),
ColWidthPerc(50, Widget(mirror())),
),
)
return b
}(),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
top, bot := mustHSplit(ft.Area(), 50)
topLeft, topRight := mustVSplit(top, 50)
botLeft, botRight := mustVSplit(bot, 50)
fakewidget.MustDraw(ft, testcanvas.MustNew(topLeft), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(botLeft), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), widgetapi.Options{})
return ft
},
},
{
desc: "rows with columns (unequal)",
termSize: image.Point{40, 20},
builder: func() *Builder {
b := New()
b.Add(
RowHeightPerc(
20,
ColWidthPerc(20, Widget(mirror())),
ColWidthPerc(80, Widget(mirror())),
),
RowHeightPerc(
80,
ColWidthPerc(80, Widget(mirror())),
ColWidthPerc(20, Widget(mirror())),
),
)
return b
}(),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
top, bot := mustHSplit(ft.Area(), 20)
topLeft, topRight := mustVSplit(top, 20)
botLeft, botRight := mustVSplit(bot, 80)
fakewidget.MustDraw(ft, testcanvas.MustNew(topLeft), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(botLeft), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), widgetapi.Options{})
return ft
},
},
{
desc: "columns with rows (equal)",
termSize: image.Point{20, 20},
builder: func() *Builder {
b := New()
b.Add(
ColWidthPerc(
50,
RowHeightPerc(50, Widget(mirror())),
RowHeightPerc(50, Widget(mirror())),
),
ColWidthPerc(
50,
RowHeightPerc(50, Widget(mirror())),
RowHeightPerc(50, Widget(mirror())),
),
)
return b
}(),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
top, bot := mustHSplit(ft.Area(), 50)
topLeft, topRight := mustVSplit(top, 50)
botLeft, botRight := mustVSplit(bot, 50)
fakewidget.MustDraw(ft, testcanvas.MustNew(topLeft), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(botLeft), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), widgetapi.Options{})
return ft
},
},
{
desc: "columns with rows (unequal)",
termSize: image.Point{40, 20},
builder: func() *Builder {
b := New()
b.Add(
ColWidthPerc(
20,
RowHeightPerc(20, Widget(mirror())),
RowHeightPerc(80, Widget(mirror())),
),
ColWidthPerc(
80,
RowHeightPerc(80, Widget(mirror())),
RowHeightPerc(20, Widget(mirror())),
),
)
return b
}(),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
left, right := mustVSplit(ft.Area(), 20)
topLeft, topRight := mustHSplit(left, 20)
botLeft, botRight := mustHSplit(right, 80)
fakewidget.MustDraw(ft, testcanvas.MustNew(topLeft), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(botLeft), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), widgetapi.Options{})
return ft
},
},
{
desc: "rows with rows with columns",
termSize: image.Point{40, 40},
builder: func() *Builder {
b := New()
b.Add(
RowHeightPerc(
50,
RowHeightPerc(
50,
ColWidthPerc(50, Widget(mirror())),
ColWidthPerc(50, Widget(mirror())),
),
RowHeightPerc(
50,
ColWidthPerc(50, Widget(mirror())),
ColWidthPerc(50, Widget(mirror())),
),
),
RowHeightPerc(
50,
RowHeightPerc(
50,
ColWidthPerc(50, Widget(mirror())),
ColWidthPerc(50, Widget(mirror())),
),
RowHeightPerc(
50,
ColWidthPerc(50, Widget(mirror())),
ColWidthPerc(50, Widget(mirror())),
),
),
)
return b
}(),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
top, bot := mustHSplit(ft.Area(), 50)
topTop, topBot := mustHSplit(top, 50)
botTop, botBot := mustHSplit(bot, 50)
topTopLeft, topTopRight := mustVSplit(topTop, 50)
topBotLeft, topBotRight := mustVSplit(topBot, 50)
botTopLeft, botTopRight := mustVSplit(botTop, 50)
botBotLeft, botBotRight := mustVSplit(botBot, 50)
fakewidget.MustDraw(ft, testcanvas.MustNew(topTopLeft), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(topTopRight), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(topBotLeft), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(topBotRight), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(botTopLeft), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(botTopRight), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(botBotLeft), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(botBotRight), widgetapi.Options{})
return ft
},
},
{
desc: "rows mixed with columns at top level",
termSize: image.Point{40, 30},
builder: func() *Builder {
b := New()
b.Add(
RowHeightPerc(20, Widget(mirror())),
ColWidthPerc(20, Widget(mirror())),
RowHeightPerc(20, Widget(mirror())),
ColWidthPerc(20, Widget(mirror())),
)
return b
}(),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
top, bot := mustHSplit(ft.Area(), 20)
left, right := mustVSplit(bot, 20)
topRight, botRight := mustHSplit(right, 25)
fakewidget.MustDraw(ft, testcanvas.MustNew(top), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(left), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), widgetapi.Options{})
return ft
},
},
{
desc: "rows mixed with columns at sub level",
termSize: image.Point{40, 30},
builder: func() *Builder {
b := New()
b.Add(
RowHeightPerc(
50,
RowHeightPerc(20, Widget(mirror())),
ColWidthPerc(20, Widget(mirror())),
RowHeightPerc(20, Widget(mirror())),
ColWidthPerc(20, Widget(mirror())),
),
RowHeightPerc(50, Widget(mirror())),
)
return b
}(),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
top, bot := mustHSplit(ft.Area(), 50)
fakewidget.MustDraw(ft, testcanvas.MustNew(bot), widgetapi.Options{})
topTop, topBot := mustHSplit(top, 20)
left, right := mustVSplit(topBot, 20)
topRight, botRight := mustHSplit(right, 25)
fakewidget.MustDraw(ft, testcanvas.MustNew(topTop), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(left), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(topRight), widgetapi.Options{})
fakewidget.MustDraw(ft, testcanvas.MustNew(botRight), widgetapi.Options{})
return ft
},
},
{
desc: "widget's container can have options",
termSize: image.Point{20, 20},
builder: func() *Builder {
b := New()
b.Add(
Widget(
mirror(),
container.Border(linestyle.Double),
),
)
return b
}(),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := testcanvas.MustNew(ft.Area())
testdraw.MustBorder(
cvs,
cvs.Area(),
draw.BorderLineStyle(linestyle.Double),
draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
)
wCvs := testcanvas.MustNew(area.ExcludeBorder(cvs.Area()))
fakewidget.MustDraw(ft, wCvs, widgetapi.Options{})
testcanvas.MustCopyTo(wCvs, cvs)
testcanvas.MustApply(cvs, ft)
return ft
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got, err := faketerm.New(tc.termSize)
if err != nil {
t.Fatalf("faketerm.New => unexpected error: %v", err)
}
gridOpts, err := tc.builder.Build()
if (err != nil) != tc.wantErr {
t.Errorf("tc.builder => unexpected error:%v, wantErr:%v", err, tc.wantErr)
}
if err != nil {
return
}
cont, err := container.New(got, gridOpts...)
if err != nil {
t.Fatalf("container.New => unexpected error: %v", err)
}
if err := cont.Draw(); err != nil {
t.Fatalf("Draw => unexpected error: %v", err)
}
var want *faketerm.Terminal
if tc.want != nil {
want = tc.want(tc.termSize)
} else {
w, err := faketerm.New(tc.termSize)
if err != nil {
t.Fatalf("faketerm.New => unexpected error: %v", err)
}
want = w
}
if diff := faketerm.Diff(want, got); diff != "" {
t.Errorf("Draw => %v", diff)
}
})
}
}