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:
parent
e6ef3f7ed1
commit
2cb4cc9797
@ -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.
|
||||
- Customizable layout, widget placement, borders, margins, padding, colors, etc.
|
||||
- Binary tree and Grid forms of setting up the layout.
|
||||
- Focusable containers and widgets.
|
||||
- Processing of keyboard and mouse events.
|
||||
- Periodic and event driven screen redraw.
|
||||
|
252
container/grid/grid.go
Normal file
252
container/grid/grid.go
Normal 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
740
container/grid/grid_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user