Refactor `SplitFixed()` to use SplitCells

* Add basic tests to `SplitFixed()`
* Panic when both `SplitFixed()` and `SplitPercent()` are used
* Refactor `validateOptions()` into two smaller functions
This commit is contained in:
nijynot 2019-05-08 21:48:57 +02:00
parent 4aa60fe8e7
commit 70a5255d5e
7 changed files with 136 additions and 39 deletions

View File

@ -171,10 +171,17 @@ func (c *Container) split() (image.Rectangle, image.Rectangle, error) {
if err != nil { if err != nil {
return image.ZR, image.ZR, err return image.ZR, image.ZR, err
} }
if c.opts.split == splitTypeVertical { if c.opts.splitFixed > -1 {
return area.VSplit(ar, c.opts.splitPercent, c.opts.splitFixed) if c.opts.split == splitTypeVertical {
return area.VSplitCells(ar, c.opts.splitFixed)
}
return area.HSplitCells(ar, c.opts.splitFixed)
} }
return area.HSplit(ar, c.opts.splitPercent, c.opts.splitFixed)
if c.opts.split == splitTypeVertical {
return area.VSplit(ar, c.opts.splitPercent)
}
return area.HSplit(ar, c.opts.splitPercent)
} }
// createFirst creates and returns the first sub container of this container. // createFirst creates and returns the first sub container of this container.

View File

@ -490,6 +490,26 @@ func TestNew(t *testing.T) {
}, },
wantContainerErr: true, wantContainerErr: true,
}, },
{
desc: "fails when both SplitFixed and SplitPercent are specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitHorizontal(
Top(
Border(linestyle.Light),
),
Bottom(
Border(linestyle.Light),
),
SplitFixed(4),
SplitPercent(20),
),
)
},
wantContainerErr: true,
},
{ {
desc: "empty container", desc: "empty container",
termSize: image.Point{10, 10}, termSize: image.Point{10, 10},
@ -616,6 +636,32 @@ func TestNew(t *testing.T) {
return ft return ft
}, },
}, },
{
desc: "horizontal unequal split",
termSize: image.Point{10, 20},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitHorizontal(
Top(
Border(linestyle.Light),
),
Bottom(
Border(linestyle.Light),
),
SplitFixed(4),
),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := testcanvas.MustNew(ft.Area())
testdraw.MustBorder(cvs, image.Rect(0, 0, 10, 4))
testdraw.MustBorder(cvs, image.Rect(0, 4, 10, 20))
testcanvas.MustApply(cvs, ft)
return ft
},
},
{ {
desc: "horizontal split, parent and children have borders", desc: "horizontal split, parent and children have borders",
termSize: image.Point{10, 10}, termSize: image.Point{10, 10},
@ -742,6 +788,32 @@ func TestNew(t *testing.T) {
return ft return ft
}, },
}, },
{
desc: "vertical fixed splits",
termSize: image.Point{20, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(
Border(linestyle.Light),
),
Right(
Border(linestyle.Light),
),
SplitFixed(4),
),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := testcanvas.MustNew(ft.Area())
testdraw.MustBorder(cvs, image.Rect(0, 0, 4, 10))
testdraw.MustBorder(cvs, image.Rect(4, 0, 20, 10))
testcanvas.MustApply(cvs, ft)
return ft
},
},
{ {
desc: "vertical split, parent and children have borders", desc: "vertical split, parent and children have borders",
termSize: image.Point{10, 10}, termSize: image.Point{10, 10},

View File

@ -127,7 +127,7 @@ func mirror() *fakewidget.Mirror {
// mustHSplit splits the area or panics. // mustHSplit splits the area or panics.
func mustHSplit(ar image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle) { func mustHSplit(ar image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle) {
t, b, err := area.HSplit(ar, heightPerc, -1) t, b, err := area.HSplit(ar, heightPerc)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -136,7 +136,7 @@ func mustHSplit(ar image.Rectangle, heightPerc int) (top image.Rectangle, bottom
// mustVSplit splits the area or panics. // mustVSplit splits the area or panics.
func mustVSplit(ar image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle) { func mustVSplit(ar image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle) {
l, r, err := area.VSplit(ar, widthPerc, -1) l, r, err := area.VSplit(ar, widthPerc)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -38,9 +38,8 @@ func applyOptions(c *Container, opts ...Option) error {
return nil return nil
} }
// validateOptions validates options set in the container tree. // ensure all the container identifiers are either empty or unique.
func validateOptions(c *Container) error { func validateIds(c *Container) error {
// ensure all the container identifiers are either empty or unique.
var errStr string var errStr string
seenID := map[string]bool{} seenID := map[string]bool{}
preOrder(c, &errStr, func(c *Container) error { preOrder(c, &errStr, func(c *Container) error {
@ -60,6 +59,43 @@ func validateOptions(c *Container) error {
return nil return nil
} }
// ensure all the container only have one split modifier.
func validateSplits(c *Container) error {
var errStr string
preOrder(c, &errStr, func(c *Container) error {
if c.opts.splitFixed > -1 && c.opts.splitPercent != 50 {
return fmt.Errorf(
"only one of splitFixed `%v` and splitPercent `%v` is allowed to be set per container",
c.opts.splitFixed,
c.opts.splitPercent,
)
}
return nil
})
if errStr != "" {
return errors.New(errStr)
}
return nil
}
// validateOptions validates options set in the container tree.
func validateOptions(c *Container) error {
// ensure all the container identifiers are either empty or unique.
errIds := validateIds(c)
if errIds != nil {
return errIds
}
// ensure that max one split modifier is used per container
errSplits := validateSplits(c)
if errSplits != nil {
return errSplits
}
return nil
}
// Option is used to provide options to a container. // Option is used to provide options to a container.
type Option interface { type Option interface {
// set sets the provided option. // set sets the provided option.
@ -218,8 +254,8 @@ func SplitPercent(p int) SplitOption {
}) })
} }
// SplitFixed sets the heights of the top and the bottom containers. // SplitFixed sets the size as a fixed value of the available space.
// The first container will be fixed and the second one will fill the rest of the space. // FIXME: More comments
// Allows values greater or equal to zero. // Allows values greater or equal to zero.
func SplitFixed(cells int) SplitOption { func SplitFixed(cells int) SplitOption {
return splitOption(func(opts *options) error { return splitOption(func(opts *options) error {

View File

@ -45,23 +45,14 @@ func FromSize(size image.Point) (image.Rectangle, error) {
// //
// The fixed value must by in the range -1 <= heightFixed. // The fixed value must by in the range -1 <= heightFixed.
// If fixed value is -1 (default value), heightPerc is used instead. // If fixed value is -1 (default value), heightPerc is used instead.
func HSplit(area image.Rectangle, heightPerc int, heightFixed int) (top image.Rectangle, bottom image.Rectangle, err error) { func HSplit(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) {
if min, max := 0, 100; heightPerc < min || heightPerc > max { if min, max := 0, 100; heightPerc < min || heightPerc > max {
return image.ZR, image.ZR, fmt.Errorf("invalid heightPerc %d, must be in range %d <= heightPerc <= %d", heightPerc, min, max) return image.ZR, image.ZR, fmt.Errorf("invalid heightPerc %d, must be in range %d <= heightPerc <= %d", heightPerc, min, max)
} }
if heightFixed < -1 {
return image.ZR, image.ZR, fmt.Errorf("invalid heightFixed %d, must be in range %d <= heightFixed", heightFixed, -1)
}
// Prioritize `SplitFixed()` over `SplitPercent()`. height := area.Dy() * heightPerc / 100
if heightFixed >= 0 { top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+height)
top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+heightFixed) bottom = image.Rect(area.Min.X, area.Min.Y+height, area.Max.X, area.Max.Y)
bottom = image.Rect(area.Min.X, area.Min.Y+heightFixed, area.Max.X, area.Max.Y)
} else {
height := area.Dy() * heightPerc / 100
top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+height)
bottom = image.Rect(area.Min.X, area.Min.Y+height, area.Max.X, area.Max.Y)
}
if top.Dy() == 0 { if top.Dy() == 0 {
top = image.ZR top = image.ZR
} }
@ -78,23 +69,14 @@ func HSplit(area image.Rectangle, heightPerc int, heightFixed int) (top image.Re
// //
// The fixed value must by in the range -1 <= heightFixed. // The fixed value must by in the range -1 <= heightFixed.
// If fixed value is -1 (default value), heightPerc is used instead. // If fixed value is -1 (default value), heightPerc is used instead.
func VSplit(area image.Rectangle, widthPerc int, widthFixed int) (left image.Rectangle, right image.Rectangle, err error) { func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) {
if min, max := 0, 100; widthPerc < min || widthPerc > max { if min, max := 0, 100; widthPerc < min || widthPerc > max {
return image.ZR, image.ZR, fmt.Errorf("invalid widthPerc %d, must be in range %d <= widthPerc <= %d", widthPerc, min, max) return image.ZR, image.ZR, fmt.Errorf("invalid widthPerc %d, must be in range %d <= widthPerc <= %d", widthPerc, min, max)
} }
if widthFixed < -1 {
return image.ZR, image.ZR, fmt.Errorf("invalid widthFixed %d, must be in range %d <= widthFixed", widthFixed, -1)
}
// Prioritize `SplitFixed()` over `SplitPercent()`. width := area.Dx() * widthPerc / 100
if widthFixed >= 0 { left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+width, area.Max.Y)
left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+widthFixed, area.Max.Y) right = image.Rect(area.Min.X+width, area.Min.Y, area.Max.X, area.Max.Y)
right = image.Rect(area.Min.X+widthFixed, area.Min.Y, area.Max.X, area.Max.Y)
} else {
width := area.Dx() * widthPerc / 100
left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+width, area.Max.Y)
right = image.Rect(area.Min.X+width, area.Min.Y, area.Max.X, area.Max.Y)
}
if left.Dx() == 0 { if left.Dx() == 0 {
left = image.ZR left = image.ZR
} }

View File

@ -184,7 +184,7 @@ func TestHSplit(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) { t.Run(tc.desc, func(t *testing.T) {
gotTop, gotBot, err := HSplit(tc.area, tc.heightPerc, -1) gotTop, gotBot, err := HSplit(tc.area, tc.heightPerc)
if (err != nil) != tc.wantErr { if (err != nil) != tc.wantErr {
t.Errorf("VSplit => unexpected error:%v, wantErr:%v", err, tc.wantErr) t.Errorf("VSplit => unexpected error:%v, wantErr:%v", err, tc.wantErr)
} }
@ -265,7 +265,7 @@ func TestVSplit(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) { t.Run(tc.desc, func(t *testing.T) {
gotLeft, gotRight, err := VSplit(tc.area, tc.widthPerc, -1) gotLeft, gotRight, err := VSplit(tc.area, tc.widthPerc)
if (err != nil) != tc.wantErr { if (err != nil) != tc.wantErr {
t.Errorf("VSplit => unexpected error:%v, wantErr:%v", err, tc.wantErr) t.Errorf("VSplit => unexpected error:%v, wantErr:%v", err, tc.wantErr)
} }

View File

@ -330,7 +330,7 @@ func split(cvsAr image.Rectangle, label string, widthPerc *int) (labelAr, textAr
switch { switch {
case widthPerc != nil: case widthPerc != nil:
splitP := 100 - *widthPerc splitP := 100 - *widthPerc
labelAr, textAr, err := area.VSplit(cvsAr, splitP, -1) labelAr, textAr, err := area.VSplit(cvsAr, splitP)
if err != nil { if err != nil {
return image.ZR, image.ZR, err return image.ZR, image.ZR, err
} }