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

Making the container API easier to use.

This commit is contained in:
Jakub Sobon 2018-04-01 16:00:20 +02:00
parent af6c5e9c81
commit 1911e2190a
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
4 changed files with 393 additions and 328 deletions

View File

@ -2,7 +2,7 @@
Package container defines a type that wraps other containers or widgets.
The container supports splitting container into sub containers, defining
container styles and placing widgets. The container also creates and manages
container styles and placing widgets. The container also creates and manages
canvases assigned to the placed widgets.
*/
package container
@ -47,50 +47,27 @@ func (c *Container) String() string {
// New returns a new root container that will use the provided terminal and
// applies the provided options.
func New(t terminalapi.Terminal, opts ...Option) *Container {
o := &options{}
for _, opt := range opts {
opt.set(o)
}
size := t.Size()
return &Container{
root := &Container{
term: t,
// The root container has access to the entire terminal.
area: image.Rect(0, 0, size.X, size.Y),
opts: o,
opts: &options{},
}
applyOptions(root, opts...)
return root
}
// newChild creates a new child container of the given parent.
func newChild(parent *Container, area image.Rectangle, opts ...Option) *Container {
o := &options{}
for _, opt := range opts {
opt.set(o)
}
func newChild(parent *Container, area image.Rectangle) *Container {
return &Container{
parent: parent,
term: parent.term,
area: area,
opts: o,
opts: &options{},
}
}
// Returns the parent container of this container and applies the provided
// options to the parent container. Returns nil if this container is the root
// of the container tree.
func (c *Container) Parent(opts ...Option) *Container {
if c.parent == nil {
return nil
}
p := c.parent
for _, opt := range opts {
opt.set(p.opts)
}
return p
}
// hasBorder determines if this container has a border.
func (c *Container) hasBorder() bool {
return c.opts.border != draw.LineStyleNone
@ -107,75 +84,29 @@ func (c *Container) usable() image.Rectangle {
}
// split splits the container's usable area into child areas.
// Panics if the container isn't configured for a split.
func (c *Container) split() (image.Rectangle, image.Rectangle) {
if ar := c.usable(); c.opts.split == splitTypeHorizontal {
return area.HSplit(ar)
} else {
if ar := c.usable(); c.opts.split == splitTypeVertical {
return area.VSplit(ar)
} else {
return area.HSplit(ar)
}
}
// First returns the first sub container of this container.
// This is the left sub container when using SplitVertical() or the top sub
// container when using SplitHorizontal().
// If this container doesn't have the first sub container yet, it will be
// created. Applies the provided options to the first sub container.
// Returns nil if this container contains a widget, containers with widgets
// cannot have sub containers.
func (c *Container) First(opts ...Option) *Container {
if c.opts.widget != nil {
return nil
}
if child := c.first; child != nil {
for _, opt := range opts {
opt.set(child.opts)
}
return child
}
// createFirst creates and returns the first sub container of this container.
func (c *Container) createFirst() *Container {
ar, _ := c.split()
c.first = newChild(c, ar, opts...)
c.first = newChild(c, ar)
return c.first
}
// Second returns the second sub container of this container.
// This is the left sub container when using SplitVertical() or the top sub
// container when using SplitHorizontal().
// If this container doesn't have the second sub container yet, it will be
// created. Applies the provided options to the second sub container.
// Returns nil if this container contains a widget, containers with widgets
// cannot have sub containers.
func (c *Container) Second(opts ...Option) *Container {
if c.opts.widget != nil {
return nil
}
if child := c.second; child != nil {
for _, opt := range opts {
opt.set(child.opts)
}
return child
}
// createSecond creates and returns the second sub container of this container.
func (c *Container) createSecond() *Container {
_, ar := c.split()
c.second = newChild(c, ar, opts...)
c.second = newChild(c, ar)
return c.second
}
// Root returns the root container and applies the provided options to the root
// container.
func (c *Container) Root(opts ...Option) *Container {
for p := c.Parent(); p != nil; p = c.Parent() {
c = p
}
for _, opt := range opts {
opt.set(c.opts)
}
return c
}
// draw draws this container and its widget.
// TODO(mum4k): Draw the widget.
func (c *Container) draw() error {

View File

@ -13,55 +13,34 @@ import (
// Example demonstrates how to use the Container API.
func Example() {
New( // Create the root container.
New(
/* terminal = */ nil,
SplitHorizontal(),
).First( // This is the top half part of the terminal.
SplitVertical(),
).First( // Left side on the top.
VerticalAlignTop(),
PlaceWidget( /* widget = */ nil),
).Parent().Second( // Right side on the top.
HorizontalAlignRight(),
PlaceWidget( /* widget = */ nil),
).Root().Second( // Bottom half of the terminal.
PlaceWidget( /* widget = */ nil),
).Root()
// TODO(mum4k): Don't require .Root() at the end.
SplitVertical(
Left(
SplitHorizontal(
Top(
Border(draw.LineStyleLight),
),
Bottom(
SplitHorizontal(
Top(
Border(draw.LineStyleLight),
),
Bottom(
Border(draw.LineStyleLight),
),
),
),
),
),
Right(
Border(draw.LineStyleLight),
),
),
)
// TODO(mum4k): Allow splits on different ratios.
}
func TestParentAndRoot(t *testing.T) {
ft := faketerm.MustNew(image.Point{1, 1})
tests := []struct {
desc string
container *Container
// Arg is the container defined above.
want func(c *Container) *Container
}{
{
desc: "root container has no parent",
container: New(ft),
want: func(c *Container) *Container {
return nil
},
},
{
desc: "returns the parent",
container: New(ft).First(),
want: func(c *Container) *Container {
return c.Root()
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
if got := tc.container.Parent(); got != tc.want(tc.container) {
t.Errorf("Parent => unexpected container\n got: %v\n want: %v", got, tc.want)
}
})
}
// TODO(mum4k): Include an example with a widget.
}
// mustCanvas returns a new canvas or panics.
@ -95,145 +74,166 @@ func TestDraw(t *testing.T) {
want func(size image.Point) *faketerm.Terminal
wantErr bool
}{
// {
// desc: "empty container",
// termSize: image.Point{10, 10},
// container: func(ft *faketerm.Terminal) *Container {
// return New(ft)
// },
// want: func(size image.Point) *faketerm.Terminal {
// return faketerm.MustNew(size)
// },
// },
// {
// desc: "container with a border",
// termSize: image.Point{10, 10},
// container: func(ft *faketerm.Terminal) *Container {
// return New(
// ft,
// Border(draw.LineStyleLight),
// )
// },
// want: func(size image.Point) *faketerm.Terminal {
// ft := faketerm.MustNew(size)
// cvs := mustCanvas(image.Rect(0, 0, 10, 10))
// mustBox(cvs, image.Rect(0, 0, 10, 10), draw.LineStyleLight)
// mustApply(cvs, ft)
// return ft
// },
// },
// {
// desc: "horizontal split, children have borders",
// termSize: image.Point{10, 10},
// container: func(ft *faketerm.Terminal) *Container {
// return New(
// ft,
// SplitHorizontal(),
// ).First(
// Border(draw.LineStyleLight),
// ).Root().Second(
// Border(draw.LineStyleLight),
// ).Root()
// },
// want: func(size image.Point) *faketerm.Terminal {
// ft := faketerm.MustNew(size)
// cvs := mustCanvas(image.Rect(0, 0, 10, 10))
// mustBox(cvs, image.Rect(0, 0, 10, 5), draw.LineStyleLight)
// mustBox(cvs, image.Rect(0, 5, 10, 10), draw.LineStyleLight)
// mustApply(cvs, ft)
// return ft
// },
// },
// {
// desc: "horizontal split, parent and children have borders",
// termSize: image.Point{10, 10},
// container: func(ft *faketerm.Terminal) *Container {
// return New(
// ft,
// SplitHorizontal(),
// Border(draw.LineStyleLight),
// ).First(
// Border(draw.LineStyleLight),
// ).Root().Second(
// Border(draw.LineStyleLight),
// ).Root()
// },
// want: func(size image.Point) *faketerm.Terminal {
// ft := faketerm.MustNew(size)
// cvs := mustCanvas(image.Rect(0, 0, 10, 10))
// mustBox(cvs, image.Rect(0, 0, 10, 10), draw.LineStyleLight)
// mustBox(cvs, image.Rect(1, 1, 9, 5), draw.LineStyleLight)
// mustBox(cvs, image.Rect(1, 5, 9, 9), draw.LineStyleLight)
// mustApply(cvs, ft)
// return ft
// },
// },
// {
// desc: "vertical split, children have borders",
// termSize: image.Point{10, 10},
// container: func(ft *faketerm.Terminal) *Container {
// return New(
// ft,
// SplitVertical(),
// ).First(
// Border(draw.LineStyleLight),
// ).Root().Second(
// Border(draw.LineStyleLight),
// ).Root()
// },
// want: func(size image.Point) *faketerm.Terminal {
// ft := faketerm.MustNew(size)
// cvs := mustCanvas(image.Rect(0, 0, 10, 10))
// mustBox(cvs, image.Rect(0, 0, 5, 10), draw.LineStyleLight)
// mustBox(cvs, image.Rect(5, 0, 10, 10), draw.LineStyleLight)
// mustApply(cvs, ft)
// return ft
// },
// },
// {
// desc: "vertical split, parent and children have borders",
// termSize: image.Point{10, 10},
// container: func(ft *faketerm.Terminal) *Container {
// return New(
// ft,
// SplitVertical(),
// Border(draw.LineStyleLight),
// ).First(
// Border(draw.LineStyleLight),
// ).Root().Second(
// Border(draw.LineStyleLight),
// ).Root()
// },
// want: func(size image.Point) *faketerm.Terminal {
// ft := faketerm.MustNew(size)
// cvs := mustCanvas(image.Rect(0, 0, 10, 10))
// mustBox(cvs, image.Rect(0, 0, 10, 10), draw.LineStyleLight)
// mustBox(cvs, image.Rect(1, 1, 5, 9), draw.LineStyleLight)
// mustBox(cvs, image.Rect(5, 1, 9, 9), draw.LineStyleLight)
// mustApply(cvs, ft)
// return ft
// },
// },
{
desc: "empty container",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) *Container {
return New(ft)
},
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
},
{
desc: "container with a border",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) *Container {
return New(
ft,
Border(draw.LineStyleLight),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := mustCanvas(image.Rect(0, 0, 10, 10))
mustBox(cvs, image.Rect(0, 0, 10, 10), draw.LineStyleLight)
mustApply(cvs, ft)
return ft
},
},
{
desc: "horizontal split, children have borders",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) *Container {
return New(
ft,
SplitHorizontal(
Top(
Border(draw.LineStyleLight),
),
Bottom(
Border(draw.LineStyleLight),
),
),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := mustCanvas(image.Rect(0, 0, 10, 10))
mustBox(cvs, image.Rect(0, 0, 10, 5), draw.LineStyleLight)
mustBox(cvs, image.Rect(0, 5, 10, 10), draw.LineStyleLight)
mustApply(cvs, ft)
return ft
},
},
{
desc: "horizontal split, parent and children have borders",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) *Container {
return New(
ft,
Border(draw.LineStyleLight),
SplitHorizontal(
Top(
Border(draw.LineStyleLight),
),
Bottom(
Border(draw.LineStyleLight),
),
),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := mustCanvas(image.Rect(0, 0, 10, 10))
mustBox(cvs, image.Rect(0, 0, 10, 10), draw.LineStyleLight)
mustBox(cvs, image.Rect(1, 1, 9, 5), draw.LineStyleLight)
mustBox(cvs, image.Rect(1, 5, 9, 9), draw.LineStyleLight)
mustApply(cvs, ft)
return ft
},
},
{
desc: "vertical split, children have borders",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) *Container {
return New(
ft,
SplitVertical(
Left(
Border(draw.LineStyleLight),
),
Right(
Border(draw.LineStyleLight),
),
),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := mustCanvas(image.Rect(0, 0, 10, 10))
mustBox(cvs, image.Rect(0, 0, 5, 10), draw.LineStyleLight)
mustBox(cvs, image.Rect(5, 0, 10, 10), draw.LineStyleLight)
mustApply(cvs, ft)
return ft
},
},
{
desc: "vertical split, parent and children have borders",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) *Container {
return New(
ft,
Border(draw.LineStyleLight),
SplitVertical(
Left(
Border(draw.LineStyleLight),
),
Right(
Border(draw.LineStyleLight),
),
),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := mustCanvas(image.Rect(0, 0, 10, 10))
mustBox(cvs, image.Rect(0, 0, 10, 10), draw.LineStyleLight)
mustBox(cvs, image.Rect(1, 1, 5, 9), draw.LineStyleLight)
mustBox(cvs, image.Rect(5, 1, 9, 9), draw.LineStyleLight)
mustApply(cvs, ft)
return ft
},
},
{
desc: "multi level split",
termSize: image.Point{10, 11},
container: func(ft *faketerm.Terminal) *Container {
return New(
ft,
SplitVertical(),
).First(
SplitHorizontal(),
).First(
Border(draw.LineStyleLight),
).Parent().Second(
SplitHorizontal(),
).First(
Border(draw.LineStyleLight),
).Parent().Second(
Border(draw.LineStyleLight),
).Root().Second(
Border(draw.LineStyleLight),
).Root()
SplitVertical(
Left(
SplitHorizontal(
Top(
Border(draw.LineStyleLight),
),
Bottom(
SplitHorizontal(
Top(
Border(draw.LineStyleLight),
),
Bottom(
Border(draw.LineStyleLight),
),
),
),
),
),
Right(
Border(draw.LineStyleLight),
),
),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
@ -252,16 +252,22 @@ func TestDraw(t *testing.T) {
container: func(ft *faketerm.Terminal) *Container {
return New(
ft,
SplitHorizontal(),
).First(
Border(draw.LineStyleLight),
).Parent().Second(
SplitHorizontal(),
).First(
Border(draw.LineStyleLight),
).Parent().Second(
Border(draw.LineStyleLight),
).Root()
SplitHorizontal(
Top(
Border(draw.LineStyleLight),
),
Bottom(
SplitHorizontal(
Top(
Border(draw.LineStyleLight),
),
Bottom(
Border(draw.LineStyleLight),
),
),
),
),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)

View File

@ -7,10 +7,17 @@ import (
"github.com/mum4k/termdash/widget"
)
// Option is used to provide options.
// applyOptions applies the options to the container.
func applyOptions(c *Container, opts ...Option) {
for _, opt := range opts {
opt.set(c)
}
}
// Option is used to provide options to a container.
type Option interface {
// set sets the provided option.
set(*options)
set(*Container)
}
// options stores the options provided to the container.
@ -32,33 +39,45 @@ type options struct {
}
// option implements Option.
type option func(*options)
type option func(*Container)
// set implements Option.set.
func (o option) set(opts *options) {
o(opts)
func (o option) set(c *Container) {
o(c)
}
// SplitVertical splits the container along the vertical axis into two sub
// containers. The use of this option removes any widget placed at this
// container, containers with sub containers cannot contain widgets.
func SplitVertical(l LeftOption, r RightOption) Option {
return option(func(c *Container) {
c.opts.split = splitTypeVertical
c.opts.widget = nil
applyOptions(c.createFirst(), l.lOpts()...)
applyOptions(c.createSecond(), r.rOpts()...)
})
}
// SplitHorizontal splits the container along the horizontal axis into two sub
// containers. The use of this option removes any widget placed at this
// container, containers with sub containers cannot contain widgets.
func SplitHorizontal(t TopOption, b BottomOption) Option {
return option(func(c *Container) {
c.opts.split = splitTypeHorizontal
c.opts.widget = nil
applyOptions(c.createFirst(), t.tOpts()...)
applyOptions(c.createSecond(), b.bOpts()...)
})
}
// PlaceWidget places the provided widget into the container.
// The use of this option removes any sub containers. Containers with sub
// containers cannot have widgets.
func PlaceWidget(w widget.Widget) Option {
return option(func(opts *options) {
opts.widget = w
})
}
// SplitHorizontal configures the container for a horizontal split.
func SplitHorizontal() Option {
return option(func(opts *options) {
opts.split = splitTypeHorizontal
})
}
// SplitVertical configures the container for a vertical split.
// This is the default split type if neither if SplitHorizontal() or
// SplitVertical() is specified.
func SplitVertical() Option {
return option(func(opts *options) {
opts.split = splitTypeVertical
return option(func(c *Container) {
c.opts.widget = w
c.first = nil
c.second = nil
})
}
@ -66,8 +85,8 @@ func SplitVertical() Option {
// container along the horizontal axis. Has no effect if the container contains
// no widget. This is the default horizontal alignment if no other is specified.
func HorizontalAlignLeft() Option {
return option(func(opts *options) {
opts.hAlign = hAlignTypeLeft
return option(func(c *Container) {
c.opts.hAlign = hAlignTypeLeft
})
}
@ -75,8 +94,8 @@ func HorizontalAlignLeft() Option {
// container along the horizontal axis. Has no effect if the container contains
// no widget.
func HorizontalAlignCenter() Option {
return option(func(opts *options) {
opts.hAlign = hAlignTypeCenter
return option(func(c *Container) {
c.opts.hAlign = hAlignTypeCenter
})
}
@ -84,8 +103,8 @@ func HorizontalAlignCenter() Option {
// container along the horizontal axis. Has no effect if the container contains
// no widget.
func HorizontalAlignRight() Option {
return option(func(opts *options) {
opts.hAlign = hAlignTypeRight
return option(func(c *Container) {
c.opts.hAlign = hAlignTypeRight
})
}
@ -93,8 +112,8 @@ func HorizontalAlignRight() Option {
// container along the vertical axis. Has no effect if the container contains
// no widget. This is the default vertical alignment if no other is specified.
func VerticalAlignTop() Option {
return option(func(opts *options) {
opts.vAlign = vAlignTypeTop
return option(func(c *Container) {
c.opts.vAlign = vAlignTypeTop
})
}
@ -102,8 +121,8 @@ func VerticalAlignTop() Option {
// container along the vertical axis. Has no effect if the container contains
// no widget.
func VerticalAlignMiddle() Option {
return option(func(opts *options) {
opts.vAlign = vAlignTypeMiddle
return option(func(c *Container) {
c.opts.vAlign = vAlignTypeMiddle
})
}
@ -111,15 +130,15 @@ func VerticalAlignMiddle() Option {
// container along the vertical axis. Has no effect if the container contains
// no widget.
func VerticalAlignBottom() Option {
return option(func(opts *options) {
opts.vAlign = vAlignTypeBottom
return option(func(c *Container) {
c.opts.vAlign = vAlignTypeBottom
})
}
// Border configures the container to have a border of the specified style.
func Border(ls draw.LineStyle) Option {
return option(func(opts *options) {
opts.border = ls
return option(func(c *Container) {
c.opts.border = ls
})
}
@ -192,3 +211,103 @@ const (
vAlignTypeMiddle
vAlignTypeBottom
)
// LeftOption is used to provide options to the left sub container after a
// vertical split of the parent.
type LeftOption interface {
// lOpts returns the options.
lOpts() []Option
}
// leftOption implements LeftOption.
type leftOption func() []Option
// lOpts implements LeftOption.lOpts.
func (lo leftOption) lOpts() []Option {
if lo == nil {
return nil
}
return lo()
}
// Left applies options to the left sub container after a vertical split of the parent.
func Left(opts ...Option) LeftOption {
return leftOption(func() []Option {
return opts
})
}
// RightOption is used to provide options to the right sub container after a
// vertical split of the parent.
type RightOption interface {
// rOpts returns the options.
rOpts() []Option
}
// rightOption implements RightOption.
type rightOption func() []Option
// rOpts implements RightOption.rOpts.
func (lo rightOption) rOpts() []Option {
if lo == nil {
return nil
}
return lo()
}
// Right applies options to the right sub container after a vertical split of the parent.
func Right(opts ...Option) RightOption {
return rightOption(func() []Option {
return opts
})
}
// TopOption is used to provide options to the top sub container after a
// horizontal split of the parent.
type TopOption interface {
// tOpts returns the options.
tOpts() []Option
}
// topOption implements TopOption.
type topOption func() []Option
// tOpts implements TopOption.tOpts.
func (lo topOption) tOpts() []Option {
if lo == nil {
return nil
}
return lo()
}
// Top applies options to the top sub container after a horizontal split of the parent.
func Top(opts ...Option) TopOption {
return topOption(func() []Option {
return opts
})
}
// BottomOption is used to provide options to the bottom sub container after a
// horizontal split of the parent.
type BottomOption interface {
// bOpts returns the options.
bOpts() []Option
}
// bottomOption implements BottomOption.
type bottomOption func() []Option
// bOpts implements BottomOption.bOpts.
func (lo bottomOption) bOpts() []Option {
if lo == nil {
return nil
}
return lo()
}
// Bottom applies options to the bottom sub container after a horizontal split of the parent.
func Bottom(opts ...Option) BottomOption {
return bottomOption(func() []Option {
return opts
})
}

View File

@ -18,20 +18,29 @@ func main() {
c := container.New(
t,
container.SplitVertical(),
).First(
container.SplitHorizontal(),
).First(
container.Border(draw.LineStyleLight),
).Parent().Second(
container.SplitHorizontal(),
).First(
container.Border(draw.LineStyleLight),
).Parent().Second(
container.Border(draw.LineStyleLight),
).Root().Second(
container.Border(draw.LineStyleLight),
).Root()
container.SplitVertical(
container.Left(
container.SplitHorizontal(
container.Top(
container.Border(draw.LineStyleLight),
),
container.Bottom(
container.SplitHorizontal(
container.Top(
container.Border(draw.LineStyleLight),
),
container.Bottom(
container.Border(draw.LineStyleLight),
),
),
),
),
),
container.Right(
container.Border(draw.LineStyleLight),
),
),
)
if err := c.Draw(); err != nil {
panic(err)
@ -40,5 +49,5 @@ func main() {
if err := t.Flush(); err != nil {
panic(err)
}
time.Sleep(30 * time.Second)
time.Sleep(3 * time.Second)
}