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.
|
- 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
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