From 1911e2190aa948be3c6e529df717ad6b2c699be4 Mon Sep 17 00:00:00 2001 From: Jakub Sobon Date: Sun, 1 Apr 2018 16:00:20 +0200 Subject: [PATCH] Making the container API easier to use. --- container/container.go | 103 ++-------- container/container_test.go | 386 ++++++++++++++++++------------------ container/options.go | 193 ++++++++++++++---- experimental/boxes.go | 39 ++-- 4 files changed, 393 insertions(+), 328 deletions(-) diff --git a/container/container.go b/container/container.go index 24e83f2..8902f5a 100644 --- a/container/container.go +++ b/container/container.go @@ -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 { diff --git a/container/container_test.go b/container/container_test.go index 641af35..d111306 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -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) diff --git a/container/options.go b/container/options.go index 0d526d1..6f06703 100644 --- a/container/options.go +++ b/container/options.go @@ -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 + }) +} diff --git a/experimental/boxes.go b/experimental/boxes.go index 6b68373..1397899 100644 --- a/experimental/boxes.go +++ b/experimental/boxes.go @@ -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) }