From 4465700c5dbbbe9a0dd2c45ead0e06973fccf42e Mon Sep 17 00:00:00 2001 From: Matthew Coleman Date: Tue, 5 Mar 2024 11:00:54 -0500 Subject: [PATCH 01/11] Allow reverse horizontal area splits --- container/container.go | 2 +- private/area/area.go | 16 ++++++++++++---- private/area/area_test.go | 19 ++++++++++++++++++- widgets/donut/donut.go | 2 +- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/container/container.go b/container/container.go index 6070efe..0bfa7c5 100644 --- a/container/container.go +++ b/container/container.go @@ -182,7 +182,7 @@ func (c *Container) split() (image.Rectangle, image.Rectangle, error) { if c.opts.split == splitTypeVertical { return area.VSplitCells(ar, c.opts.splitFixed) } - return area.HSplitCells(ar, c.opts.splitFixed) + return area.HSplitCells(ar, c.opts.splitFixed, false) } if c.opts.split == splitTypeVertical { diff --git a/private/area/area.go b/private/area/area.go index 34b21a1..9e2040c 100644 --- a/private/area/area.go +++ b/private/area/area.go @@ -39,7 +39,7 @@ func FromSize(size image.Point) (image.Rectangle, error) { } // HSplit returns two new areas created by splitting the provided area at the -// specified percentage of its width. The percentage must be in the range +// specified percentage of its height. The percentage must be in the range // 0 <= heightPerc <= 100. // Can return zero size areas. func HSplit(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) { @@ -106,7 +106,7 @@ func VSplitCells(area image.Rectangle, cells int) (left image.Rectangle, right i // be a zero or a positive integer. Providing a zero returns top=image.ZR, // bottom=area. Providing a number equal or larger to area's height returns // top=area, bottom=image.ZR. -func HSplitCells(area image.Rectangle, cells int) (top image.Rectangle, bottom image.Rectangle, err error) { +func HSplitCells(area image.Rectangle, cells int, fromMax bool) (top image.Rectangle, bottom image.Rectangle, err error) { if min := 0; cells < min { return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells) } @@ -119,8 +119,16 @@ func HSplitCells(area image.Rectangle, cells int) (top image.Rectangle, bottom i return area, image.ZR, nil } - top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+cells) - bottom = image.Rect(area.Min.X, area.Min.Y+cells, area.Max.X, area.Max.Y) + splitY := area.Min.Y + if fromMax { + splitY = area.Max.Y - cells + } else { + splitY = area.Min.Y + cells + } + + top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, splitY) + bottom = image.Rect(area.Min.X, splitY, area.Max.X, area.Max.Y) + return top, bottom, nil } diff --git a/private/area/area_test.go b/private/area/area_test.go index f4a46eb..62141ce 100644 --- a/private/area/area_test.go +++ b/private/area/area_test.go @@ -372,6 +372,7 @@ func TestHSplitCells(t *testing.T) { desc string area image.Rectangle cells int + fromMax bool wantTop image.Rectangle wantBottom image.Rectangle wantErr bool @@ -417,6 +418,14 @@ func TestHSplitCells(t *testing.T) { wantTop: image.Rect(1, 1, 3, 2), wantBottom: image.Rect(1, 2, 3, 3), }, + { + desc: "splits area with even height from max", + area: image.Rect(1, 1, 3, 3), + cells: 1, + fromMax: true, + wantTop: image.Rect(1, 1, 3, 2), + wantBottom: image.Rect(1, 2, 3, 3), + }, { desc: "splits area with odd width", area: image.Rect(1, 1, 4, 4), @@ -431,11 +440,19 @@ func TestHSplitCells(t *testing.T) { wantTop: image.Rect(0, 0, 4, 3), wantBottom: image.Rect(0, 3, 4, 4), }, + { + desc: "splits to unequal areas from max", + area: image.Rect(0, 0, 4, 4), + cells: 3, + fromMax: true, + wantTop: image.Rect(0, 0, 4, 1), + wantBottom: image.Rect(0, 1, 4, 4), + }, } for _, tc := range tests { t.Run(tc.desc, func(t *testing.T) { - gotTop, gotBottom, err := HSplitCells(tc.area, tc.cells) + gotTop, gotBottom, err := HSplitCells(tc.area, tc.cells, tc.fromMax) if (err != nil) != tc.wantErr { t.Errorf("HSplitCells => unexpected error:%v, wantErr:%v", err, tc.wantErr) } diff --git a/widgets/donut/donut.go b/widgets/donut/donut.go index 7d98792..a95268d 100644 --- a/widgets/donut/donut.go +++ b/widgets/donut/donut.go @@ -306,7 +306,7 @@ func donutAndLabel(cvsAr image.Rectangle) (donAr, labelAr image.Rectangle, err e // Two lines for the text label at the bottom. // One for the text itself and one for visual space between the donut and // the label. - donAr, labelAr, err = area.HSplitCells(cvsAr, height-2) + donAr, labelAr, err = area.HSplitCells(cvsAr, height-2, false) if err != nil { return image.ZR, image.ZR, err } From 275c62fc47cbe1b997ffc6b3a70ece29998434d7 Mon Sep 17 00:00:00 2001 From: Matthew Coleman Date: Thu, 7 Mar 2024 17:14:50 -0500 Subject: [PATCH 02/11] Restore HSplitCells to original function signature --- container/container.go | 2 +- private/area/area.go | 76 +++++++++++++++++++++++++-------------- private/area/area_test.go | 19 +--------- widgets/donut/donut.go | 2 +- 4 files changed, 52 insertions(+), 47 deletions(-) diff --git a/container/container.go b/container/container.go index 0bfa7c5..6070efe 100644 --- a/container/container.go +++ b/container/container.go @@ -182,7 +182,7 @@ func (c *Container) split() (image.Rectangle, image.Rectangle, error) { if c.opts.split == splitTypeVertical { return area.VSplitCells(ar, c.opts.splitFixed) } - return area.HSplitCells(ar, c.opts.splitFixed, false) + return area.HSplitCells(ar, c.opts.splitFixed) } if c.opts.split == splitTypeVertical { diff --git a/private/area/area.go b/private/area/area.go index 9e2040c..e497628 100644 --- a/private/area/area.go +++ b/private/area/area.go @@ -22,6 +22,41 @@ import ( "github.com/mum4k/termdash/private/numbers" ) +// TODO +func hSplitCells(area image.Rectangle, cells int, fromMax bool) (top image.Rectangle, bottom image.Rectangle, err error) { + if min := 0; cells < min { + return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells) + } + if cells == 0 { + if fromMax { + return area, image.ZR, nil + } else { + return image.ZR, area, nil + } + } + + height := area.Dy() + if cells >= height { + if fromMax { + return image.ZR, area, nil + } else { + return area, image.ZR, nil + } + } + + splitY := area.Min.Y + if fromMax { + splitY = area.Max.Y - cells + } else { + splitY = area.Min.Y + cells + } + + top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, splitY) + bottom = image.Rect(area.Min.X, splitY, area.Max.X, area.Max.Y) + + return top, bottom, nil +} + // Size returns the size of the provided area. func Size(area image.Rectangle) image.Point { return image.Point{ @@ -102,34 +137,21 @@ func VSplitCells(area image.Rectangle, cells int) (left image.Rectangle, right i } // HSplitCells returns two new areas created by splitting the provided area -// after the specified amount of cells of its height. The number of cells must -// be a zero or a positive integer. Providing a zero returns top=image.ZR, -// bottom=area. Providing a number equal or larger to area's height returns -// top=area, bottom=image.ZR. -func HSplitCells(area image.Rectangle, cells int, fromMax bool) (top image.Rectangle, bottom image.Rectangle, err error) { - if min := 0; cells < min { - return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells) - } - if cells == 0 { - return image.ZR, area, nil - } +// after the specified amount of cells of its height, as applied to the first +// area. The number of cells must be a zero or a positive integer. Providing a +// zero returns top=image.ZR, bottom=area. Providing a number equal or larger to +// area's height returns top=area, bottom=image.ZR. +func HSplitCells(area image.Rectangle, cells int) (top image.Rectangle, bottom image.Rectangle, err error) { + return hSplitCells(area, cells, false) +} - height := area.Dy() - if cells >= height { - return area, image.ZR, nil - } - - splitY := area.Min.Y - if fromMax { - splitY = area.Max.Y - cells - } else { - splitY = area.Min.Y + cells - } - - top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, splitY) - bottom = image.Rect(area.Min.X, splitY, area.Max.X, area.Max.Y) - - return top, bottom, nil +// HSplitCells returns two new areas created by splitting the provided area +// after the specified amount of cells of its height, as applied to the second +// area. The number of cells must be a zero or a positive integer. Providing a +// zero returns top=area, bottom=image.ZR. Providing a number equal or larger to +// area's height returns top=image.ZR, bottom=area. +func HSplitCellsReversed(area image.Rectangle, cells int) (top image.Rectangle, bottom image.Rectangle, err error) { + return hSplitCells(area, cells, true) } // ExcludeBorder returns a new area created by subtracting a border around the diff --git a/private/area/area_test.go b/private/area/area_test.go index 62141ce..f4a46eb 100644 --- a/private/area/area_test.go +++ b/private/area/area_test.go @@ -372,7 +372,6 @@ func TestHSplitCells(t *testing.T) { desc string area image.Rectangle cells int - fromMax bool wantTop image.Rectangle wantBottom image.Rectangle wantErr bool @@ -418,14 +417,6 @@ func TestHSplitCells(t *testing.T) { wantTop: image.Rect(1, 1, 3, 2), wantBottom: image.Rect(1, 2, 3, 3), }, - { - desc: "splits area with even height from max", - area: image.Rect(1, 1, 3, 3), - cells: 1, - fromMax: true, - wantTop: image.Rect(1, 1, 3, 2), - wantBottom: image.Rect(1, 2, 3, 3), - }, { desc: "splits area with odd width", area: image.Rect(1, 1, 4, 4), @@ -440,19 +431,11 @@ func TestHSplitCells(t *testing.T) { wantTop: image.Rect(0, 0, 4, 3), wantBottom: image.Rect(0, 3, 4, 4), }, - { - desc: "splits to unequal areas from max", - area: image.Rect(0, 0, 4, 4), - cells: 3, - fromMax: true, - wantTop: image.Rect(0, 0, 4, 1), - wantBottom: image.Rect(0, 1, 4, 4), - }, } for _, tc := range tests { t.Run(tc.desc, func(t *testing.T) { - gotTop, gotBottom, err := HSplitCells(tc.area, tc.cells, tc.fromMax) + gotTop, gotBottom, err := HSplitCells(tc.area, tc.cells) if (err != nil) != tc.wantErr { t.Errorf("HSplitCells => unexpected error:%v, wantErr:%v", err, tc.wantErr) } diff --git a/widgets/donut/donut.go b/widgets/donut/donut.go index a95268d..7d98792 100644 --- a/widgets/donut/donut.go +++ b/widgets/donut/donut.go @@ -306,7 +306,7 @@ func donutAndLabel(cvsAr image.Rectangle) (donAr, labelAr image.Rectangle, err e // Two lines for the text label at the bottom. // One for the text itself and one for visual space between the donut and // the label. - donAr, labelAr, err = area.HSplitCells(cvsAr, height-2, false) + donAr, labelAr, err = area.HSplitCells(cvsAr, height-2) if err != nil { return image.ZR, image.ZR, err } From 610a070aab7ab83ffbce53879be3eb4e58c664d0 Mon Sep 17 00:00:00 2001 From: Matthew Coleman Date: Thu, 7 Mar 2024 17:26:16 -0500 Subject: [PATCH 03/11] Add test for HSplitCellsReversed --- private/area/area_test.go | 85 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/private/area/area_test.go b/private/area/area_test.go index f4a46eb..1105b26 100644 --- a/private/area/area_test.go +++ b/private/area/area_test.go @@ -452,6 +452,91 @@ func TestHSplitCells(t *testing.T) { } } +func TestHSplitCellsReversed(t *testing.T) { + tests := []struct { + desc string + area image.Rectangle + cells int + wantTop image.Rectangle + wantBottom image.Rectangle + wantErr bool + }{ + { + desc: "fails on negative cells", + area: image.Rect(1, 1, 2, 2), + cells: -1, + wantErr: true, + }, + { + desc: "returns area as bottom on cells too large", + area: image.Rect(1, 1, 2, 2), + cells: 2, + wantTop: image.ZR, + wantBottom: image.Rect(1, 1, 2, 2), + }, + { + desc: "returns area as bottom on cells equal area width", + area: image.Rect(1, 1, 2, 2), + cells: 1, + wantTop: image.ZR, + wantBottom: image.Rect(1, 1, 2, 2), + }, + { + desc: "returns area as top on zero cells", + area: image.Rect(1, 1, 2, 2), + cells: 0, + wantBottom: image.ZR, + wantTop: image.Rect(1, 1, 2, 2), + }, + { + desc: "zero area to begin with", + area: image.ZR, + cells: 0, + wantTop: image.ZR, + wantBottom: image.ZR, + }, + { + desc: "splits area with even height", + area: image.Rect(1, 1, 3, 3), + cells: 1, + wantTop: image.Rect(1, 1, 3, 2), + wantBottom: image.Rect(1, 2, 3, 3), + }, + { + desc: "splits area with odd width", + area: image.Rect(1, 1, 4, 4), + cells: 1, + wantTop: image.Rect(1, 1, 4, 3), + wantBottom: image.Rect(1, 3, 4, 4), + }, + { + desc: "splits to unequal areas", + area: image.Rect(0, 0, 4, 4), + cells: 3, + wantTop: image.Rect(0, 0, 4, 1), + wantBottom: image.Rect(0, 1, 4, 4), + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + gotTop, gotBottom, err := HSplitCellsReversed(tc.area, tc.cells) + if (err != nil) != tc.wantErr { + t.Errorf("HSplitCells => unexpected error:%v, wantErr:%v", err, tc.wantErr) + } + if err != nil { + return + } + if diff := pretty.Compare(tc.wantTop, gotTop); diff != "" { + t.Errorf("HSplitCells => left value unexpected diff (-want, +got):\n%s", diff) + } + if diff := pretty.Compare(tc.wantBottom, gotBottom); diff != "" { + t.Errorf("HSplitCells => right value unexpected diff (-want, +got):\n%s", diff) + } + }) + } +} + func TestExcludeBorder(t *testing.T) { tests := []struct { desc string From ca93ed8334d533822f6df78f6728b6826d9af161 Mon Sep 17 00:00:00 2001 From: Matthew Coleman Date: Thu, 7 Mar 2024 17:55:44 -0500 Subject: [PATCH 04/11] Define shared logic for vertical cell splits --- private/area/area.go | 73 +++++++++++++++++++++++---------- private/area/area_test.go | 85 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 22 deletions(-) diff --git a/private/area/area.go b/private/area/area.go index e497628..9c7076a 100644 --- a/private/area/area.go +++ b/private/area/area.go @@ -23,12 +23,46 @@ import ( ) // TODO -func hSplitCells(area image.Rectangle, cells int, fromMax bool) (top image.Rectangle, bottom image.Rectangle, err error) { +func vSplitCells(area image.Rectangle, cells int, reversed bool) (left image.Rectangle, right image.Rectangle, err error) { if min := 0; cells < min { return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells) } if cells == 0 { - if fromMax { + if reversed { + return area, image.ZR, nil + } else { + return image.ZR, area, nil + } + } + + width := area.Dx() + if cells >= width { + if reversed { + return image.ZR, area, nil + } else { + return area, image.ZR, nil + } + } + + splitX := area.Min.X + if reversed { + splitX = area.Max.X - cells + } else { + splitX = area.Min.X + cells + } + + left = image.Rect(area.Min.X, area.Min.Y, splitX, area.Max.Y) + right = image.Rect(splitX, area.Min.Y, area.Max.X, area.Max.Y) + return left, right, nil +} + +// TODO +func hSplitCells(area image.Rectangle, cells int, reversed bool) (top image.Rectangle, bottom image.Rectangle, err error) { + if min := 0; cells < min { + return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells) + } + if cells == 0 { + if reversed { return area, image.ZR, nil } else { return image.ZR, area, nil @@ -37,7 +71,7 @@ func hSplitCells(area image.Rectangle, cells int, fromMax bool) (top image.Recta height := area.Dy() if cells >= height { - if fromMax { + if reversed { return image.ZR, area, nil } else { return area, image.ZR, nil @@ -45,7 +79,7 @@ func hSplitCells(area image.Rectangle, cells int, fromMax bool) (top image.Recta } splitY := area.Min.Y - if fromMax { + if reversed { splitY = area.Max.Y - cells } else { splitY = area.Min.Y + cells @@ -114,26 +148,21 @@ func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right im } // VSplitCells returns two new areas created by splitting the provided area -// after the specified amount of cells of its width. The number of cells must -// be a zero or a positive integer. Providing a zero returns left=image.ZR, -// right=area. Providing a number equal or larger to area's width returns -// left=area, right=image.ZR. +// after the specified amount of cells of its width, as applied to the first +// area. The number of cells must be a zero or a positive integer. Providing a +// zero returns left=image.ZR, right=area. Providing a number equal or larger to +// area's width returns left=area, right=image.ZR. func VSplitCells(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) { - if min := 0; cells < min { - return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells) - } - if cells == 0 { - return image.ZR, area, nil - } + return vSplitCells(area, cells, false) +} - width := area.Dx() - if cells >= width { - return area, image.ZR, nil - } - - left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+cells, area.Max.Y) - right = image.Rect(area.Min.X+cells, area.Min.Y, area.Max.X, area.Max.Y) - return left, right, nil +// VSplitCells returns two new areas created by splitting the provided area +// after the specified amount of cells of its width, as applied to the second +// area. The number of cells must be a zero or a positive integer. Providing a +// zero returns left=image.ZR, right=area. Providing a number equal or larger to +// area's width returns left=area, right=image.ZR. +func VSplitCellsReversed(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) { + return vSplitCells(area, cells, true) } // HSplitCells returns two new areas created by splitting the provided area diff --git a/private/area/area_test.go b/private/area/area_test.go index 1105b26..e324230 100644 --- a/private/area/area_test.go +++ b/private/area/area_test.go @@ -367,6 +367,91 @@ func TestVSplitCells(t *testing.T) { } } +func TestVSplitCellsReversed(t *testing.T) { + tests := []struct { + desc string + area image.Rectangle + cells int + wantLeft image.Rectangle + wantRight image.Rectangle + wantErr bool + }{ + { + desc: "fails on negative cells", + area: image.Rect(1, 1, 2, 2), + cells: -1, + wantErr: true, + }, + { + desc: "returns area as left on cells too large", + area: image.Rect(1, 1, 2, 2), + cells: 2, + wantLeft: image.ZR, + wantRight: image.Rect(1, 1, 2, 2), + }, + { + desc: "returns area as left on cells equal area width", + area: image.Rect(1, 1, 2, 2), + cells: 1, + wantLeft: image.ZR, + wantRight: image.Rect(1, 1, 2, 2), + }, + { + desc: "returns area as right on zero cells", + area: image.Rect(1, 1, 2, 2), + cells: 0, + wantRight: image.ZR, + wantLeft: image.Rect(1, 1, 2, 2), + }, + { + desc: "zero area to begin with", + area: image.ZR, + cells: 0, + wantLeft: image.ZR, + wantRight: image.ZR, + }, + { + desc: "splits area with even width", + area: image.Rect(1, 1, 3, 3), + cells: 1, + wantLeft: image.Rect(1, 1, 2, 3), + wantRight: image.Rect(2, 1, 3, 3), + }, + { + desc: "splits area with odd width", + area: image.Rect(1, 1, 4, 4), + cells: 1, + wantLeft: image.Rect(1, 1, 3, 4), + wantRight: image.Rect(3, 1, 4, 4), + }, + { + desc: "splits to unequal areas", + area: image.Rect(0, 0, 4, 4), + cells: 3, + wantLeft: image.Rect(0, 0, 1, 4), + wantRight: image.Rect(1, 0, 4, 4), + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + gotLeft, gotRight, err := VSplitCellsReversed(tc.area, tc.cells) + if (err != nil) != tc.wantErr { + t.Errorf("VSplitCells => unexpected error:%v, wantErr:%v", err, tc.wantErr) + } + if err != nil { + return + } + if diff := pretty.Compare(tc.wantLeft, gotLeft); diff != "" { + t.Errorf("VSplitCells => left value unexpected diff (-want, +got):\n%s", diff) + } + if diff := pretty.Compare(tc.wantRight, gotRight); diff != "" { + t.Errorf("VSplitCells => right value unexpected diff (-want, +got):\n%s", diff) + } + }) + } +} + func TestHSplitCells(t *testing.T) { tests := []struct { desc string From 992d7f3bfcea164bf38d808871462b9e8d509fa9 Mon Sep 17 00:00:00 2001 From: Matthew Coleman Date: Thu, 7 Mar 2024 17:58:36 -0500 Subject: [PATCH 05/11] Lint fixes --- private/area/area.go | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/private/area/area.go b/private/area/area.go index 9c7076a..4ee97f3 100644 --- a/private/area/area.go +++ b/private/area/area.go @@ -30,18 +30,16 @@ func vSplitCells(area image.Rectangle, cells int, reversed bool) (left image.Rec if cells == 0 { if reversed { return area, image.ZR, nil - } else { - return image.ZR, area, nil } + return image.ZR, area, nil } width := area.Dx() if cells >= width { if reversed { return image.ZR, area, nil - } else { - return area, image.ZR, nil } + return area, image.ZR, nil } splitX := area.Min.X @@ -64,18 +62,16 @@ func hSplitCells(area image.Rectangle, cells int, reversed bool) (top image.Rect if cells == 0 { if reversed { return area, image.ZR, nil - } else { - return image.ZR, area, nil } + return image.ZR, area, nil } height := area.Dy() if cells >= height { if reversed { return image.ZR, area, nil - } else { - return area, image.ZR, nil } + return area, image.ZR, nil } splitY := area.Min.Y @@ -156,11 +152,11 @@ func VSplitCells(area image.Rectangle, cells int) (left image.Rectangle, right i return vSplitCells(area, cells, false) } -// VSplitCells returns two new areas created by splitting the provided area -// after the specified amount of cells of its width, as applied to the second -// area. The number of cells must be a zero or a positive integer. Providing a -// zero returns left=image.ZR, right=area. Providing a number equal or larger to -// area's width returns left=area, right=image.ZR. +// VSplitCellsReversed returns two new areas created by splitting the provided +// area after the specified amount of cells of its width, as applied to the +// second area. The number of cells must be a zero or a positive integer. +// Providing a zero returns left=image.ZR, right=area. Providing a number equal +// or larger to area's width returns left=area, right=image.ZR. func VSplitCellsReversed(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) { return vSplitCells(area, cells, true) } @@ -174,11 +170,11 @@ func HSplitCells(area image.Rectangle, cells int) (top image.Rectangle, bottom i return hSplitCells(area, cells, false) } -// HSplitCells returns two new areas created by splitting the provided area -// after the specified amount of cells of its height, as applied to the second -// area. The number of cells must be a zero or a positive integer. Providing a -// zero returns top=area, bottom=image.ZR. Providing a number equal or larger to -// area's height returns top=image.ZR, bottom=area. +// HSplitCellsReversed returns two new areas created by splitting the provided +// area after the specified amount of cells of its height, as applied to the +// second area. The number of cells must be a zero or a positive integer. +// Providing a zero returns top=area, bottom=image.ZR. Providing a number equal +// or larger to area's height returns top=image.ZR, bottom=area. func HSplitCellsReversed(area image.Rectangle, cells int) (top image.Rectangle, bottom image.Rectangle, err error) { return hSplitCells(area, cells, true) } From 5b664845a5896d26453cc74ce54b2768ea869a08 Mon Sep 17 00:00:00 2001 From: Matthew Coleman Date: Thu, 7 Mar 2024 18:47:11 -0500 Subject: [PATCH 06/11] Reversed split for percentages --- private/area/area.go | 105 +++++++++++++++++------- private/area/area_test.go | 165 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+), 30 deletions(-) diff --git a/private/area/area.go b/private/area/area.go index 4ee97f3..4ecea7d 100644 --- a/private/area/area.go +++ b/private/area/area.go @@ -22,6 +22,32 @@ import ( "github.com/mum4k/termdash/private/numbers" ) +// TODO +func vSplit(area image.Rectangle, widthPerc int, reversed bool) (left image.Rectangle, right image.Rectangle, err error) { + 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) + } + + width := area.Dx() * widthPerc / 100 + + if reversed { + left = image.Rect(area.Min.X, area.Min.Y, area.Max.X-width, area.Max.Y) + right = image.Rect(area.Max.X-width, area.Min.Y, area.Max.X, area.Max.Y) + } else { + 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 { + left = image.ZR + } + if right.Dx() == 0 { + right = image.ZR + } + + return left, right, nil +} + // TODO func vSplitCells(area image.Rectangle, cells int, reversed bool) (left image.Rectangle, right image.Rectangle, err error) { if min := 0; cells < min { @@ -51,9 +77,36 @@ func vSplitCells(area image.Rectangle, cells int, reversed bool) (left image.Rec left = image.Rect(area.Min.X, area.Min.Y, splitX, area.Max.Y) right = image.Rect(splitX, area.Min.Y, area.Max.X, area.Max.Y) + return left, right, nil } +// TODO +func hSplit(area image.Rectangle, heightPerc int, reversed bool) (top image.Rectangle, bottom image.Rectangle, err error) { + 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) + } + + height := area.Dy() * heightPerc / 100 + + if reversed { + top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Max.Y-height) + bottom = image.Rect(area.Min.X, area.Max.Y-height, area.Max.X, area.Max.Y) + } else { + 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 { + top = image.ZR + } + if bottom.Dy() == 0 { + bottom = image.ZR + } + + return top, bottom, nil +} + // TODO func hSplitCells(area image.Rectangle, cells int, reversed bool) (top image.Rectangle, bottom image.Rectangle, err error) { if min := 0; cells < min { @@ -104,43 +157,35 @@ func FromSize(size image.Point) (image.Rectangle, error) { } // HSplit returns two new areas created by splitting the provided area at the -// specified percentage of its height. The percentage must be in the range -// 0 <= heightPerc <= 100. +// specified percentage of its height, applying the percentage to the first +// area. The percentage must be in the range 0 <= heightPerc <= 100. // Can return zero size areas. func HSplit(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) { - 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) - } - height := area.Dy() * heightPerc / 100 - top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+height) - if top.Dy() == 0 { - top = image.ZR - } - bottom = image.Rect(area.Min.X, area.Min.Y+height, area.Max.X, area.Max.Y) - if bottom.Dy() == 0 { - bottom = image.ZR - } - return top, bottom, nil + return hSplit(area, heightPerc, false) +} + +// HSplit returns two new areas created by splitting the provided area at the +// specified percentage of its height, applying the percentage to the second +// area. The percentage must be in the range 0 <= heightPerc <= 100. +// Can return zero size areas. +func HSplitReversed(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) { + return hSplit(area, heightPerc, true) } // VSplit returns two new areas created by splitting the provided area at the -// specified percentage of its width. The percentage must be in the range -// 0 <= widthPerc <= 100. +// specified percentage of its width, applying the percentage to the first area. +// The percentage must be in the range 0 <= widthPerc <= 100. // Can return zero size areas. func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) { - 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) - } - width := area.Dx() * widthPerc / 100 - left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+width, area.Max.Y) - if left.Dx() == 0 { - left = image.ZR - } - right = image.Rect(area.Min.X+width, area.Min.Y, area.Max.X, area.Max.Y) - if right.Dx() == 0 { - right = image.ZR - } - return left, right, nil + return vSplit(area, widthPerc, false) +} + +// VSplit returns two new areas created by splitting the provided area at the +// specified percentage of its width, applying the percentage to the second area. +// The percentage must be in the range 0 <= widthPerc <= 100. +// Can return zero size areas. +func VSplitReversed(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) { + return vSplit(area, widthPerc, true) } // VSplitCells returns two new areas created by splitting the provided area diff --git a/private/area/area_test.go b/private/area/area_test.go index e324230..fcf2ae4 100644 --- a/private/area/area_test.go +++ b/private/area/area_test.go @@ -198,6 +198,87 @@ func TestHSplit(t *testing.T) { } } +func TestHSplitReversed(t *testing.T) { + tests := []struct { + desc string + area image.Rectangle + heightPerc int + wantTop image.Rectangle + wantBot image.Rectangle + wantErr bool + }{ + { + desc: "fails on heightPerc too small", + area: image.Rect(1, 1, 2, 2), + heightPerc: -1, + wantErr: true, + }, + { + desc: "fails on heightPerc too large", + area: image.Rect(1, 1, 2, 2), + heightPerc: 101, + wantErr: true, + }, + { + desc: "zero area to begin with", + area: image.ZR, + heightPerc: 50, + wantTop: image.ZR, + wantBot: image.ZR, + }, + { + desc: "splitting results in zero height area on the bottom", + area: image.Rect(1, 1, 2, 2), + heightPerc: 0, + wantTop: image.Rect(1, 1, 2, 2), + wantBot: image.ZR, + }, + { + desc: "splitting results in 100 height area on the top", + area: image.Rect(1, 1, 2, 2), + heightPerc: 100, + wantTop: image.ZR, + wantBot: image.Rect(1, 1, 2, 2), + }, + { + desc: "splits area with even height", + area: image.Rect(1, 1, 3, 3), + heightPerc: 50, + wantTop: image.Rect(1, 1, 3, 2), + wantBot: image.Rect(1, 2, 3, 3), + }, + { + desc: "splits area with odd height", + area: image.Rect(1, 1, 4, 4), + heightPerc: 50, + wantTop: image.Rect(1, 1, 4, 3), + wantBot: image.Rect(1, 3, 4, 4), + }, + { + desc: "splits to unequal areas", + area: image.Rect(0, 0, 4, 4), + heightPerc: 25, + wantTop: image.Rect(0, 0, 4, 3), + wantBot: image.Rect(0, 3, 4, 4), + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + gotTop, gotBot, err := HSplitReversed(tc.area, tc.heightPerc) + if (err != nil) != tc.wantErr { + t.Errorf("VSplit => unexpected error:%v, wantErr:%v", err, tc.wantErr) + } + if diff := pretty.Compare(tc.wantTop, gotTop); diff != "" { + t.Errorf("HSplit => first value unexpected diff (-want, +got):\n%s", diff) + } + if diff := pretty.Compare(tc.wantBot, gotBot); diff != "" { + t.Errorf("HSplit => second value unexpected diff (-want, +got):\n%s", diff) + } + }) + } +} + func TestVSplit(t *testing.T) { tests := []struct { desc string @@ -282,6 +363,90 @@ func TestVSplit(t *testing.T) { } } +func TestVSplitReversed(t *testing.T) { + tests := []struct { + desc string + area image.Rectangle + widthPerc int + wantLeft image.Rectangle + wantRight image.Rectangle + wantErr bool + }{ + { + desc: "fails on widthPerc too small", + area: image.Rect(1, 1, 2, 2), + widthPerc: -1, + wantErr: true, + }, + { + desc: "fails on widthPerc too large", + area: image.Rect(1, 1, 2, 2), + widthPerc: 101, + wantErr: true, + }, + { + desc: "zero area to begin with", + area: image.ZR, + widthPerc: 50, + wantLeft: image.ZR, + wantRight: image.ZR, + }, + { + desc: "splitting results in zero width area on the right", + area: image.Rect(1, 1, 2, 2), + widthPerc: 0, + wantLeft: image.Rect(1, 1, 2, 2), + wantRight: image.ZR, + }, + { + desc: "splitting results in zero width area on the left", + area: image.Rect(1, 1, 2, 2), + widthPerc: 100, + wantLeft: image.ZR, + wantRight: image.Rect(1, 1, 2, 2), + }, + { + desc: "splits area with even width", + area: image.Rect(1, 1, 3, 3), + widthPerc: 50, + wantLeft: image.Rect(1, 1, 2, 3), + wantRight: image.Rect(2, 1, 3, 3), + }, + { + desc: "splits area with odd width", + area: image.Rect(1, 1, 4, 4), + widthPerc: 50, + wantLeft: image.Rect(1, 1, 3, 4), + wantRight: image.Rect(3, 1, 4, 4), + }, + { + desc: "splits to unequal areas", + area: image.Rect(0, 0, 4, 4), + widthPerc: 25, + wantLeft: image.Rect(0, 0, 3, 4), + wantRight: image.Rect(3, 0, 4, 4), + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + gotLeft, gotRight, err := VSplitReversed(tc.area, tc.widthPerc) + if (err != nil) != tc.wantErr { + t.Errorf("VSplit => unexpected error:%v, wantErr:%v", err, tc.wantErr) + } + if err != nil { + return + } + if diff := pretty.Compare(tc.wantLeft, gotLeft); diff != "" { + t.Errorf("VSplit => left value unexpected diff (-want, +got):\n%s", diff) + } + if diff := pretty.Compare(tc.wantRight, gotRight); diff != "" { + t.Errorf("VSplit => right value unexpected diff (-want, +got):\n%s", diff) + } + }) + } +} + func TestVSplitCells(t *testing.T) { tests := []struct { desc string From 029ce611e8e34d0bce11b652b86a30b7e41e1b86 Mon Sep 17 00:00:00 2001 From: Matthew Coleman Date: Thu, 7 Mar 2024 18:49:29 -0500 Subject: [PATCH 07/11] Linting adjustments --- private/area/area.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/private/area/area.go b/private/area/area.go index 4ecea7d..e54e5d5 100644 --- a/private/area/area.go +++ b/private/area/area.go @@ -164,9 +164,9 @@ func HSplit(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom i return hSplit(area, heightPerc, false) } -// HSplit returns two new areas created by splitting the provided area at the -// specified percentage of its height, applying the percentage to the second -// area. The percentage must be in the range 0 <= heightPerc <= 100. +// HSplitReversed returns two new areas created by splitting the provided area +// at the specified percentage of its height, applying the percentage to the +// second area. The percentage must be in the range 0 <= heightPerc <= 100. // Can return zero size areas. func HSplitReversed(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) { return hSplit(area, heightPerc, true) @@ -180,9 +180,9 @@ func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right im return vSplit(area, widthPerc, false) } -// VSplit returns two new areas created by splitting the provided area at the -// specified percentage of its width, applying the percentage to the second area. -// The percentage must be in the range 0 <= widthPerc <= 100. +// VSplitReversed returns two new areas created by splitting the provided area +// at the specified percentage of its width, applying the percentage to the +// second area. The percentage must be in the range 0 <= widthPerc <= 100. // Can return zero size areas. func VSplitReversed(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) { return vSplit(area, widthPerc, true) From edab304013374a1168c0d4e35d996997b5e49fb4 Mon Sep 17 00:00:00 2001 From: Matthew Coleman Date: Thu, 7 Mar 2024 18:57:20 -0500 Subject: [PATCH 08/11] Function re-ordering --- private/area/area.go | 178 +++++++++++++++++++++---------------------- 1 file changed, 89 insertions(+), 89 deletions(-) diff --git a/private/area/area.go b/private/area/area.go index e54e5d5..d8dfaa0 100644 --- a/private/area/area.go +++ b/private/area/area.go @@ -22,6 +22,64 @@ import ( "github.com/mum4k/termdash/private/numbers" ) +// Size returns the size of the provided area. +func Size(area image.Rectangle) image.Point { + return image.Point{ + area.Dx(), + area.Dy(), + } +} + +// FromSize returns the corresponding area for the provided size. +func FromSize(size image.Point) (image.Rectangle, error) { + if size.X < 0 || size.Y < 0 { + return image.Rectangle{}, fmt.Errorf("cannot convert zero or negative size to an area, got: %+v", size) + } + return image.Rect(0, 0, size.X, size.Y), nil +} + +// TODO +func hSplit(area image.Rectangle, heightPerc int, reversed bool) (top image.Rectangle, bottom image.Rectangle, err error) { + 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) + } + + height := area.Dy() * heightPerc / 100 + + if reversed { + top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Max.Y-height) + bottom = image.Rect(area.Min.X, area.Max.Y-height, area.Max.X, area.Max.Y) + } else { + 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 { + top = image.ZR + } + if bottom.Dy() == 0 { + bottom = image.ZR + } + + return top, bottom, nil +} + +// HSplit returns two new areas created by splitting the provided area at the +// specified percentage of its height, applying the percentage to the first +// area. The percentage must be in the range 0 <= heightPerc <= 100. +// Can return zero size areas. +func HSplit(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) { + return hSplit(area, heightPerc, false) +} + +// HSplitReversed returns two new areas created by splitting the provided area +// at the specified percentage of its height, applying the percentage to the +// second area. The percentage must be in the range 0 <= heightPerc <= 100. +// Can return zero size areas. +func HSplitReversed(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) { + return hSplit(area, heightPerc, true) +} + // TODO func vSplit(area image.Rectangle, widthPerc int, reversed bool) (left image.Rectangle, right image.Rectangle, err error) { if min, max := 0, 100; widthPerc < min || widthPerc > max { @@ -48,6 +106,22 @@ func vSplit(area image.Rectangle, widthPerc int, reversed bool) (left image.Rect return left, right, nil } +// VSplit returns two new areas created by splitting the provided area at the +// specified percentage of its width, applying the percentage to the first area. +// The percentage must be in the range 0 <= widthPerc <= 100. +// Can return zero size areas. +func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) { + return vSplit(area, widthPerc, false) +} + +// VSplitReversed returns two new areas created by splitting the provided area +// at the specified percentage of its width, applying the percentage to the +// second area. The percentage must be in the range 0 <= widthPerc <= 100. +// Can return zero size areas. +func VSplitReversed(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) { + return vSplit(area, widthPerc, true) +} + // TODO func vSplitCells(area image.Rectangle, cells int, reversed bool) (left image.Rectangle, right image.Rectangle, err error) { if min := 0; cells < min { @@ -81,30 +155,22 @@ func vSplitCells(area image.Rectangle, cells int, reversed bool) (left image.Rec return left, right, nil } -// TODO -func hSplit(area image.Rectangle, heightPerc int, reversed bool) (top image.Rectangle, bottom image.Rectangle, err error) { - 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) - } +// VSplitCells returns two new areas created by splitting the provided area +// after the specified amount of cells of its width, as applied to the first +// area. The number of cells must be a zero or a positive integer. Providing a +// zero returns left=image.ZR, right=area. Providing a number equal or larger to +// area's width returns left=area, right=image.ZR. +func VSplitCells(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) { + return vSplitCells(area, cells, false) +} - height := area.Dy() * heightPerc / 100 - - if reversed { - top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Max.Y-height) - bottom = image.Rect(area.Min.X, area.Max.Y-height, area.Max.X, area.Max.Y) - } else { - 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 { - top = image.ZR - } - if bottom.Dy() == 0 { - bottom = image.ZR - } - - return top, bottom, nil +// VSplitCellsReversed returns two new areas created by splitting the provided +// area after the specified amount of cells of its width, as applied to the +// second area. The number of cells must be a zero or a positive integer. +// Providing a zero returns left=image.ZR, right=area. Providing a number equal +// or larger to area's width returns left=area, right=image.ZR. +func VSplitCellsReversed(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) { + return vSplitCells(area, cells, true) } // TODO @@ -140,72 +206,6 @@ func hSplitCells(area image.Rectangle, cells int, reversed bool) (top image.Rect return top, bottom, nil } -// Size returns the size of the provided area. -func Size(area image.Rectangle) image.Point { - return image.Point{ - area.Dx(), - area.Dy(), - } -} - -// FromSize returns the corresponding area for the provided size. -func FromSize(size image.Point) (image.Rectangle, error) { - if size.X < 0 || size.Y < 0 { - return image.Rectangle{}, fmt.Errorf("cannot convert zero or negative size to an area, got: %+v", size) - } - return image.Rect(0, 0, size.X, size.Y), nil -} - -// HSplit returns two new areas created by splitting the provided area at the -// specified percentage of its height, applying the percentage to the first -// area. The percentage must be in the range 0 <= heightPerc <= 100. -// Can return zero size areas. -func HSplit(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) { - return hSplit(area, heightPerc, false) -} - -// HSplitReversed returns two new areas created by splitting the provided area -// at the specified percentage of its height, applying the percentage to the -// second area. The percentage must be in the range 0 <= heightPerc <= 100. -// Can return zero size areas. -func HSplitReversed(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) { - return hSplit(area, heightPerc, true) -} - -// VSplit returns two new areas created by splitting the provided area at the -// specified percentage of its width, applying the percentage to the first area. -// The percentage must be in the range 0 <= widthPerc <= 100. -// Can return zero size areas. -func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) { - return vSplit(area, widthPerc, false) -} - -// VSplitReversed returns two new areas created by splitting the provided area -// at the specified percentage of its width, applying the percentage to the -// second area. The percentage must be in the range 0 <= widthPerc <= 100. -// Can return zero size areas. -func VSplitReversed(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) { - return vSplit(area, widthPerc, true) -} - -// VSplitCells returns two new areas created by splitting the provided area -// after the specified amount of cells of its width, as applied to the first -// area. The number of cells must be a zero or a positive integer. Providing a -// zero returns left=image.ZR, right=area. Providing a number equal or larger to -// area's width returns left=area, right=image.ZR. -func VSplitCells(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) { - return vSplitCells(area, cells, false) -} - -// VSplitCellsReversed returns two new areas created by splitting the provided -// area after the specified amount of cells of its width, as applied to the -// second area. The number of cells must be a zero or a positive integer. -// Providing a zero returns left=image.ZR, right=area. Providing a number equal -// or larger to area's width returns left=area, right=image.ZR. -func VSplitCellsReversed(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) { - return vSplitCells(area, cells, true) -} - // HSplitCells returns two new areas created by splitting the provided area // after the specified amount of cells of its height, as applied to the first // area. The number of cells must be a zero or a positive integer. Providing a From ec14443ef984b765491e70ef45c22a0655c7b3fb Mon Sep 17 00:00:00 2001 From: Matthew Coleman Date: Thu, 7 Mar 2024 19:26:59 -0500 Subject: [PATCH 09/11] More documentation updates --- private/area/area.go | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/private/area/area.go b/private/area/area.go index d8dfaa0..94fceca 100644 --- a/private/area/area.go +++ b/private/area/area.go @@ -38,7 +38,11 @@ func FromSize(size image.Point) (image.Rectangle, error) { return image.Rect(0, 0, size.X, size.Y), nil } -// TODO +// hSplit returns two new areas created by splitting the provided area at the +// specified percentage of its height, applying the percentage to the top or +// bottom area, depending on the reversed flag. The percentage must be in the +// range 0 <= heightPerc <= 100. +// Can return zero size areas. func hSplit(area image.Rectangle, heightPerc int, reversed bool) (top image.Rectangle, bottom image.Rectangle, err error) { 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) @@ -65,8 +69,8 @@ func hSplit(area image.Rectangle, heightPerc int, reversed bool) (top image.Rect } // HSplit returns two new areas created by splitting the provided area at the -// specified percentage of its height, applying the percentage to the first -// area. The percentage must be in the range 0 <= heightPerc <= 100. +// specified percentage of its height, applying the percentage to the top area. +// The percentage must be in the range 0 <= heightPerc <= 100. // Can return zero size areas. func HSplit(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) { return hSplit(area, heightPerc, false) @@ -74,13 +78,17 @@ func HSplit(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom i // HSplitReversed returns two new areas created by splitting the provided area // at the specified percentage of its height, applying the percentage to the -// second area. The percentage must be in the range 0 <= heightPerc <= 100. +// bottom area. The percentage must be in the range 0 <= heightPerc <= 100. // Can return zero size areas. func HSplitReversed(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) { return hSplit(area, heightPerc, true) } -// TODO +// vSplit returns two new areas created by splitting the provided area at the +// specified percentage of its width, applying the percentage to the left or +// right area, depending on the reversed flag. The percentage must be in the +// range 0 <= widthPerc <= 100. +// Can return zero size areas. func vSplit(area image.Rectangle, widthPerc int, reversed bool) (left image.Rectangle, right image.Rectangle, err error) { 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) @@ -107,7 +115,7 @@ func vSplit(area image.Rectangle, widthPerc int, reversed bool) (left image.Rect } // VSplit returns two new areas created by splitting the provided area at the -// specified percentage of its width, applying the percentage to the first area. +// specified percentage of its width, applying the percentage to the left area. // The percentage must be in the range 0 <= widthPerc <= 100. // Can return zero size areas. func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) { @@ -116,13 +124,18 @@ func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right im // VSplitReversed returns two new areas created by splitting the provided area // at the specified percentage of its width, applying the percentage to the -// second area. The percentage must be in the range 0 <= widthPerc <= 100. +// right area. The percentage must be in the range 0 <= widthPerc <= 100. // Can return zero size areas. func VSplitReversed(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) { return vSplit(area, widthPerc, true) } -// TODO +// vSplitCells returns two new areas created by splitting the provided area +// after the specified amount of cells of its width, applied to the left or +// right area, depending on the reversed flag. The number of cells must be a +// zero or a positive integer. Providing a zero returns left=image.ZR, +// right=area. Providing a number equal or larger to area's width returns +// left=area, right=image.ZR. func vSplitCells(area image.Rectangle, cells int, reversed bool) (left image.Rectangle, right image.Rectangle, err error) { if min := 0; cells < min { return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells) @@ -156,7 +169,7 @@ func vSplitCells(area image.Rectangle, cells int, reversed bool) (left image.Rec } // VSplitCells returns two new areas created by splitting the provided area -// after the specified amount of cells of its width, as applied to the first +// after the specified amount of cells of its width, as applied to the left // area. The number of cells must be a zero or a positive integer. Providing a // zero returns left=image.ZR, right=area. Providing a number equal or larger to // area's width returns left=area, right=image.ZR. @@ -166,14 +179,19 @@ func VSplitCells(area image.Rectangle, cells int) (left image.Rectangle, right i // VSplitCellsReversed returns two new areas created by splitting the provided // area after the specified amount of cells of its width, as applied to the -// second area. The number of cells must be a zero or a positive integer. +// right area. The number of cells must be a zero or a positive integer. // Providing a zero returns left=image.ZR, right=area. Providing a number equal // or larger to area's width returns left=area, right=image.ZR. func VSplitCellsReversed(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) { return vSplitCells(area, cells, true) } -// TODO +// hSplitCells returns two new areas created by splitting the provided area +// after the specified amount of cells of its height, applied to the top or +// bottom area, depending on the reversed flag. The number of cells must be a +// zero or a positive integer. Providing a zero returns top=image.ZR, +// bottom=area. Providing a number equal or larger to area's height returns +// top=area, bottom=image.ZR. func hSplitCells(area image.Rectangle, cells int, reversed bool) (top image.Rectangle, bottom image.Rectangle, err error) { if min := 0; cells < min { return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells) @@ -207,7 +225,7 @@ func hSplitCells(area image.Rectangle, cells int, reversed bool) (top image.Rect } // HSplitCells returns two new areas created by splitting the provided area -// after the specified amount of cells of its height, as applied to the first +// after the specified amount of cells of its height, as applied to the top // area. The number of cells must be a zero or a positive integer. Providing a // zero returns top=image.ZR, bottom=area. Providing a number equal or larger to // area's height returns top=area, bottom=image.ZR. @@ -217,7 +235,7 @@ func HSplitCells(area image.Rectangle, cells int) (top image.Rectangle, bottom i // HSplitCellsReversed returns two new areas created by splitting the provided // area after the specified amount of cells of its height, as applied to the -// second area. The number of cells must be a zero or a positive integer. +// bottom area. The number of cells must be a zero or a positive integer. // Providing a zero returns top=area, bottom=image.ZR. Providing a number equal // or larger to area's height returns top=image.ZR, bottom=area. func HSplitCellsReversed(area image.Rectangle, cells int) (top image.Rectangle, bottom image.Rectangle, err error) { From 4b7343b987b23bea2c9ae208d81f10a69a20632f Mon Sep 17 00:00:00 2001 From: Matthew Coleman Date: Fri, 8 Mar 2024 00:50:54 -0500 Subject: [PATCH 10/11] Split*FromEnd functions --- container/container.go | 12 +++++++ container/container_test.go | 52 ++++++++++++++++++++++++++++ container/options.go | 67 +++++++++++++++++++++++++++++++------ 3 files changed, 121 insertions(+), 10 deletions(-) diff --git a/container/container.go b/container/container.go index 6070efe..fc41caf 100644 --- a/container/container.go +++ b/container/container.go @@ -180,14 +180,26 @@ func (c *Container) split() (image.Rectangle, image.Rectangle, error) { } if c.opts.splitFixed > DefaultSplitFixed { if c.opts.split == splitTypeVertical { + if c.opts.splitReversed { + return area.VSplitCellsReversed(ar, c.opts.splitFixed) + } return area.VSplitCells(ar, c.opts.splitFixed) } + if c.opts.splitReversed { + return area.HSplitCellsReversed(ar, c.opts.splitFixed) + } return area.HSplitCells(ar, c.opts.splitFixed) } if c.opts.split == splitTypeVertical { + if c.opts.splitReversed { + return area.VSplitReversed(ar, c.opts.splitPercent) + } return area.VSplit(ar, c.opts.splitPercent) } + if c.opts.splitReversed { + return area.HSplitReversed(ar, c.opts.splitPercent) + } return area.HSplit(ar, c.opts.splitPercent) } diff --git a/container/container_test.go b/container/container_test.go index f16c315..f502654 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -864,6 +864,32 @@ func TestNew(t *testing.T) { return ft }, }, + { + desc: "vertical, reversed unequal split", + termSize: image.Point{20, 10}, + container: func(ft *faketerm.Terminal) (*Container, error) { + return New( + ft, + SplitVertical( + Left( + Border(linestyle.Light), + ), + Right( + Border(linestyle.Light), + ), + SplitPercentFromEnd(20), + ), + ) + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + cvs := testcanvas.MustNew(ft.Area()) + testdraw.MustBorder(cvs, image.Rect(0, 0, 16, 10)) + testdraw.MustBorder(cvs, image.Rect(16, 0, 20, 10)) + testcanvas.MustApply(cvs, ft) + return ft + }, + }, { desc: "vertical fixed splits", termSize: image.Point{20, 10}, @@ -890,6 +916,32 @@ func TestNew(t *testing.T) { return ft }, }, + { + desc: "vertical, reversed 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), + ), + SplitFixedFromEnd(4), + ), + ) + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + cvs := testcanvas.MustNew(ft.Area()) + testdraw.MustBorder(cvs, image.Rect(0, 0, 16, 10)) + testdraw.MustBorder(cvs, image.Rect(16, 0, 20, 10)) + testcanvas.MustApply(cvs, ft) + return ft + }, + }, { desc: "vertical split, parent and children have borders", termSize: image.Point{10, 10}, diff --git a/container/options.go b/container/options.go index 1fa718a..84726bc 100644 --- a/container/options.go +++ b/container/options.go @@ -108,9 +108,10 @@ type options struct { inherited inherited // split identifies how is this container split. - split splitType - splitPercent int - splitFixed int + split splitType + splitReversed bool + splitPercent int + splitFixed int // widget is the widget in the container. // A container can have either two sub containers (left and right) or a @@ -247,10 +248,11 @@ func newOptions(parent *options) *options { inherited: inherited{ focusedColor: cell.ColorYellow, }, - hAlign: align.HorizontalCenter, - vAlign: align.VerticalMiddle, - splitPercent: DefaultSplitPercent, - splitFixed: DefaultSplitFixed, + hAlign: align.HorizontalCenter, + vAlign: align.VerticalMiddle, + splitReversed: DefaultSplitReversed, + splitPercent: DefaultSplitPercent, + splitFixed: DefaultSplitFixed, } if parent != nil { opts.global = parent.global @@ -281,13 +283,17 @@ func (so splitOption) setSplit(opts *options) error { return so(opts) } +// DefaultSplitReversed is the default value for the SplitReversed option. +const DefaultSplitReversed = false + // DefaultSplitPercent is the default value for the SplitPercent option. const DefaultSplitPercent = 50 // DefaultSplitFixed is the default value for the SplitFixed option. const DefaultSplitFixed = -1 -// SplitPercent sets the relative size of the split as percentage of the available space. +// SplitPercent sets the relative size of the split as percentage of the +// available space. // When using SplitVertical, the provided size is applied to the new left // container, the new right container gets the reminder of the size. // When using SplitHorizontal, the provided size is applied to the new top @@ -304,6 +310,25 @@ func SplitPercent(p int) SplitOption { }) } +// SplitPercentFromEnd sets the relative size of the split as percentage of the +// available space. +// When using SplitVertical, the provided size is applied to the new right +// container, the new leftcontainer gets the reminder of the size. +// When using SplitHorizontal, the provided size is applied to the new bottom +// container, the new top container gets the reminder of the size. +// The provided value must be a positive number in the range 0 < p < 100. +// If not provided, defaults to using SplitPercent with DefaultSplitPercent. +func SplitPercentFromEnd(p int) SplitOption { + return splitOption(func(opts *options) error { + if min, max := 0, 100; p <= min || p >= max { + return fmt.Errorf("invalid split percentage %d, must be in range %d < p < %d", p, min, max) + } + opts.splitReversed = true + opts.splitPercent = p + return nil + }) +} + // SplitFixed sets the size of the first container to be a fixed value // and makes the second container take up the remaining space. // When using SplitVertical, the provided size is applied to the new left @@ -311,8 +336,9 @@ func SplitPercent(p int) SplitOption { // When using SplitHorizontal, the provided size is applied to the new top // container, the new bottom container gets the reminder of the size. // The provided value must be a positive number in the range 0 <= cells. -// If SplitFixed() is not specified, it defaults to SplitPercent() and its given value. -// Only one of SplitFixed() and SplitPercent() can be specified per container. +// If SplitFixed* or SplitPercent* is not specified, it defaults to +// SplitPercent() and its given value. +// Only one SplitFixed* or SplitPercent* may be specified per container. func SplitFixed(cells int) SplitOption { return splitOption(func(opts *options) error { if cells < 0 { @@ -323,6 +349,27 @@ func SplitFixed(cells int) SplitOption { }) } +// SplitFixedFromEnd sets the size of the second container to be a fixed value +// and makes the first container take up the remaining space. +// When using SplitVertical, the provided size is applied to the new right +// container, the new left container gets the reminder of the size. +// When using SplitHorizontal, the provided size is applied to the new bottom +// container, the new top container gets the reminder of the size. +// The provided value must be a positive number in the range 0 <= cells. +// If SplitFixed* or SplitPercent* is not specified, it defaults to +// SplitPercent() and its given value. +// Only one SplitFixed* or SplitPercent* may be specified per container. +func SplitFixedFromEnd(cells int) SplitOption { + return splitOption(func(opts *options) error { + if cells < 0 { + return fmt.Errorf("invalid fixed value %d, must be in range %d <= cells", cells, 0) + } + opts.splitFixed = cells + opts.splitReversed = true + return nil + }) +} + // 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. From e980c4bb2b196245994ee6afdf62924ea2b7bdab Mon Sep 17 00:00:00 2001 From: Matthew Coleman Date: Fri, 8 Mar 2024 13:02:01 -0500 Subject: [PATCH 11/11] Add tests, clean-up --- container/container_test.go | 54 ++++++++++++++++++++++++++++++++++++- container/options.go | 2 +- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/container/container_test.go b/container/container_test.go index f502654..efe2af5 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -713,7 +713,33 @@ func TestNew(t *testing.T) { }, }, { - desc: "horizontal unequal split", + desc: "horizontal, reversed 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), + ), + SplitPercentFromEnd(20), + ), + ) + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + cvs := testcanvas.MustNew(ft.Area()) + testdraw.MustBorder(cvs, image.Rect(0, 0, 10, 16)) + testdraw.MustBorder(cvs, image.Rect(0, 16, 10, 20)) + testcanvas.MustApply(cvs, ft) + return ft + }, + }, + { + desc: "horizontal fixed splits", termSize: image.Point{10, 20}, container: func(ft *faketerm.Terminal) (*Container, error) { return New( @@ -738,6 +764,32 @@ func TestNew(t *testing.T) { return ft }, }, + { + desc: "horizontal, reversed fixed splits", + termSize: image.Point{10, 20}, + container: func(ft *faketerm.Terminal) (*Container, error) { + return New( + ft, + SplitHorizontal( + Top( + Border(linestyle.Light), + ), + Bottom( + Border(linestyle.Light), + ), + SplitFixedFromEnd(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, 16)) + testdraw.MustBorder(cvs, image.Rect(0, 16, 10, 20)) + testcanvas.MustApply(cvs, ft) + return ft + }, + }, { desc: "horizontal split, parent and children have borders", termSize: image.Point{10, 10}, diff --git a/container/options.go b/container/options.go index 84726bc..764429f 100644 --- a/container/options.go +++ b/container/options.go @@ -313,7 +313,7 @@ func SplitPercent(p int) SplitOption { // SplitPercentFromEnd sets the relative size of the split as percentage of the // available space. // When using SplitVertical, the provided size is applied to the new right -// container, the new leftcontainer gets the reminder of the size. +// container, the new left container gets the reminder of the size. // When using SplitHorizontal, the provided size is applied to the new bottom // container, the new top container gets the reminder of the size. // The provided value must be a positive number in the range 0 < p < 100.