diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2022-10-09 08:31:14 -0700 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2022-10-09 08:31:14 -0700 |
commit | dba0edb9987b16738fb4b1c07e681388a84ce52d (patch) | |
tree | 935efd9877ca12c7757d98632d5bc89d159ab358 /pkg | |
parent | 7b4b42abd6f458eb667d322ec870bc6b20024812 (diff) |
use boxlayout from lazycore
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/gui/arrangement.go | 2 | ||||
-rw-r--r-- | pkg/gui/boxlayout/boxlayout.go | 212 | ||||
-rw-r--r-- | pkg/gui/boxlayout/boxlayout_test.go | 380 |
3 files changed, 1 insertions, 593 deletions
diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index 89236ae5c..ecff17893 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -1,7 +1,7 @@ package gui import ( - "github.com/jesseduffield/lazygit/pkg/gui/boxlayout" + "github.com/jesseduffield/lazycore/pkg/boxlayout" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" diff --git a/pkg/gui/boxlayout/boxlayout.go b/pkg/gui/boxlayout/boxlayout.go deleted file mode 100644 index 4eb6f15e6..000000000 --- a/pkg/gui/boxlayout/boxlayout.go +++ /dev/null @@ -1,212 +0,0 @@ -package boxlayout - -import ( - "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/lazygit/pkg/utils" - "github.com/samber/lo" -) - -type Dimensions struct { - X0 int - X1 int - Y0 int - Y1 int -} - -type Direction int - -const ( - ROW Direction = iota - COLUMN -) - -// to give a high-level explanation of what's going on here. We layout our windows by arranging a bunch of boxes in the available space. -// If a box has children, it needs to specify how it wants to arrange those children: ROW or COLUMN. -// If a box represents a window, you can put the window name in the Window field. -// When determining how to divvy-up the available height (for row children) or width (for column children), we first -// give the boxes with a static `size` the space that they want. Then we apportion -// the remaining space based on the weights of the dynamic boxes (you can't define -// both size and weight at the same time: you gotta pick one). If there are two -// boxes, one with weight 1 and the other with weight 2, the first one gets 33% -// of the available space and the second one gets the remaining 66% - -type Box struct { - // Direction decides how the children boxes are laid out. ROW means the children will each form a row i.e. that they will be stacked on top of eachother. - Direction Direction - - // function which takes the width and height assigned to the box and decides which orientation it will have - ConditionalDirection func(width int, height int) Direction - - Children []*Box - - // function which takes the width and height assigned to the box and decides the layout of the children. - ConditionalChildren func(width int, height int) []*Box - - // Window refers to the name of the window this box represents, if there is one - Window string - - // static Size. If parent box's direction is ROW this refers to height, otherwise width - Size int - - // dynamic size. Once all statically sized children have been considered, Weight decides how much of the remaining space will be taken up by the box - // TODO: consider making there be one int and a type enum so we can't have size and Weight simultaneously defined - Weight int -} - -func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions { - children := root.getChildren(width, height) - if len(children) == 0 { - // leaf node - if root.Window != "" { - dimensionsForWindow := Dimensions{X0: x0, Y0: y0, X1: x0 + width - 1, Y1: y0 + height - 1} - return map[string]Dimensions{root.Window: dimensionsForWindow} - } - return map[string]Dimensions{} - } - - direction := root.getDirection(width, height) - - var availableSize int - if direction == COLUMN { - availableSize = width - } else { - availableSize = height - } - - sizes := calcSizes(children, availableSize) - - result := map[string]Dimensions{} - offset := 0 - for i, child := range children { - boxSize := sizes[i] - - var resultForChild map[string]Dimensions - if direction == COLUMN { - resultForChild = ArrangeWindows(child, x0+offset, y0, boxSize, height) - } else { - resultForChild = ArrangeWindows(child, x0, y0+offset, width, boxSize) - } - - result = mergeDimensionMaps(result, resultForChild) - offset += boxSize - } - - return result -} - -func calcSizes(boxes []*Box, availableSpace int) []int { - normalizedWeights := normalizeWeights(slices.Map(boxes, func(box *Box) int { return box.Weight })) - - totalWeight := 0 - reservedSpace := 0 - for i, box := range boxes { - if box.isStatic() { - reservedSpace += box.Size - } else { - totalWeight += normalizedWeights[i] - } - } - - dynamicSpace := utils.Max(0, availableSpace-reservedSpace) - - unitSize := 0 - extraSpace := 0 - if totalWeight > 0 { - unitSize = dynamicSpace / totalWeight - extraSpace = dynamicSpace % totalWeight - } - - result := make([]int, len(boxes)) - for i, box := range boxes { - if box.isStatic() { - // assuming that only one static child can have a size greater than the - // available space. In that case we just crop the size to what's available - result[i] = utils.Min(availableSpace, box.Size) - } else { - result[i] = unitSize * normalizedWeights[i] - } - } - - // distribute the remainder across dynamic boxes. - for extraSpace > 0 { - for i, weight := range normalizedWeights { - if weight > 0 { - result[i]++ - extraSpace-- - normalizedWeights[i]-- - - if extraSpace == 0 { - break - } - } - } - } - - return result -} - -// removes common multiple from weights e.g. if we get 2, 4, 4 we return 1, 2, 2. -func normalizeWeights(weights []int) []int { - if len(weights) == 0 { - return []int{} - } - - // to spare us some computation we'll exit early if any of our weights is 1 - if slices.Some(weights, func(weight int) bool { return weight == 1 }) { - return weights - } - - // map weights to factorSlices and find the lowest common factor - positiveWeights := slices.Filter(weights, func(weight int) bool { return weight > 0 }) - factorSlices := slices.Map(positiveWeights, func(weight int) []int { return calcFactors(weight) }) - commonFactors := factorSlices[0] - for _, factors := range factorSlices { - commonFactors = lo.Intersect(commonFactors, factors) - } - - if len(commonFactors) == 0 { - return weights - } - - newWeights := slices.Map(weights, func(weight int) int { return weight / commonFactors[0] }) - - return normalizeWeights(newWeights) -} - -func calcFactors(n int) []int { - factors := []int{} - for i := 2; i <= n; i++ { - if n%i == 0 { - factors = append(factors, i) - } - } - return factors -} - -func (b *Box) isStatic() bool { - return b.Size > 0 -} - -func (b *Box) getDirection(width int, height int) Direction { - if b.ConditionalDirection != nil { - return b.ConditionalDirection(width, height) - } - return b.Direction -} - -func (b *Box) getChildren(width int, height int) []*Box { - if b.ConditionalChildren != nil { - return b.ConditionalChildren(width, height) - } - return b.Children -} - -func mergeDimensionMaps(a map[string]Dimensions, b map[string]Dimensions) map[string]Dimensions { - result := map[string]Dimensions{} - for _, dimensionMap := range []map[string]Dimensions{a, b} { - for k, v := range dimensionMap { - result[k] = v - } - } - return result -} diff --git a/pkg/gui/boxlayout/boxlayout_test.go b/pkg/gui/boxlayout/boxlayout_test.go deleted file mode 100644 index c2c0bc9e4..000000000 --- a/pkg/gui/boxlayout/boxlayout_test.go +++ /dev/null @@ -1,380 +0,0 @@ -package boxlayout - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestArrangeWindows(t *testing.T) { - type scenario struct { - testName string - root *Box - x0 int - y0 int - width int - height int - test func(result map[string]Dimensions) - } - - scenarios := []scenario{ - { - testName: "Empty box", - root: &Box{}, - x0: 0, - y0: 0, - width: 10, - height: 10, - test: func(result map[string]Dimensions) { - assert.EqualValues(t, result, map[string]Dimensions{}) - }, - }, - { - testName: "Box with static and dynamic panel", - root: &Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic"}}}, - x0: 0, - y0: 0, - width: 10, - height: 10, - test: func(result map[string]Dimensions) { - assert.EqualValues( - t, - result, - map[string]Dimensions{ - "dynamic": {X0: 0, X1: 9, Y0: 1, Y1: 9}, - "static": {X0: 0, X1: 9, Y0: 0, Y1: 0}, - }, - ) - }, - }, - { - testName: "Box with static and two dynamic panels", - root: &Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}}, - x0: 0, - y0: 0, - width: 10, - height: 10, - test: func(result map[string]Dimensions) { - assert.EqualValues( - t, - result, - map[string]Dimensions{ - "static": {X0: 0, X1: 9, Y0: 0, Y1: 0}, - "dynamic1": {X0: 0, X1: 9, Y0: 1, Y1: 3}, - "dynamic2": {X0: 0, X1: 9, Y0: 4, Y1: 9}, - }, - ) - }, - }, - { - testName: "Box with COLUMN direction", - root: &Box{Direction: COLUMN, Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}}, - x0: 0, - y0: 0, - width: 10, - height: 10, - test: func(result map[string]Dimensions) { - assert.EqualValues( - t, - result, - map[string]Dimensions{ - "static": {X0: 0, X1: 0, Y0: 0, Y1: 9}, - "dynamic1": {X0: 1, X1: 3, Y0: 0, Y1: 9}, - "dynamic2": {X0: 4, X1: 9, Y0: 0, Y1: 9}, - }, - ) - }, - }, - { - testName: "Box with COLUMN direction only on wide boxes with narrow box", - root: &Box{ConditionalDirection: func(width int, height int) Direction { - if width > 4 { - return COLUMN - } else { - return ROW - } - }, Children: []*Box{{Weight: 1, Window: "dynamic1"}, {Weight: 1, Window: "dynamic2"}}}, - x0: 0, - y0: 0, - width: 4, - height: 4, - test: func(result map[string]Dimensions) { - assert.EqualValues( - t, - result, - map[string]Dimensions{ - "dynamic1": {X0: 0, X1: 3, Y0: 0, Y1: 1}, - "dynamic2": {X0: 0, X1: 3, Y0: 2, Y1: 3}, - }, - ) - }, - }, - { - testName: "Box with COLUMN direction only on wide boxes with wide box", - root: &Box{ConditionalDirection: func(width int, height int) Direction { - if width > 4 { - return COLUMN - } else { - return ROW - } - }, Children: []*Box{{Weight: 1, Window: "dynamic1"}, {Weight: 1, Window: "dynamic2"}}}, - // 5 / 2 = 2 remainder 1. That remainder goes to the first box. - x0: 0, - y0: 0, - width: 5, - height: 5, - test: func(result map[string]Dimensions) { - assert.EqualValues( - t, - result, - map[string]Dimensions{ - "dynamic1": {X0: 0, X1: 2, Y0: 0, Y1: 4}, - "dynamic2": {X0: 3, X1: 4, Y0: 0, Y1: 4}, - }, - ) - }, - }, - { - testName: "Box with conditional children where box is wide", - root: &Box{ConditionalChildren: func(width int, height int) []*Box { - if width > 4 { - return []*Box{{Window: "wide", Weight: 1}} - } else { - return []*Box{{Window: "narrow", Weight: 1}} - } - }}, - x0: 0, - y0: 0, - width: 5, - height: 5, - test: func(result map[string]Dimensions) { - assert.EqualValues( - t, - result, - map[string]Dimensions{ - "wide": {X0: 0, X1: 4, Y0: 0, Y1: 4}, - }, - ) - }, - }, - { - testName: "Box with conditional children where box is narrow", - root: &Box{ConditionalChildren: func(width int, height int) []*Box { - if width > 4 { - return []*Box{{Window: "wide", Weight: 1}} - } else { - return []*Box{{Window: "narrow", Weight: 1}} - } - }}, - x0: 0, - y0: 0, - width: 4, - height: 4, - test: func(result map[string]Dimensions) { - assert.EqualValues( - t, - result, - map[string]Dimensions{ - "narrow": {X0: 0, X1: 3, Y0: 0, Y1: 3}, - }, - ) - }, - }, - { - testName: "Box with static child with size too large", - root: &Box{Direction: COLUMN, Children: []*Box{{Size: 11, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}}, - x0: 0, - y0: 0, - width: 10, - height: 10, - test: func(result map[string]Dimensions) { - assert.EqualValues( - t, - result, - map[string]Dimensions{ - "static": {X0: 0, X1: 9, Y0: 0, Y1: 9}, - // not sure if X0: 10, X1: 9 makes any sense, but testing this in the - // actual GUI it seems harmless - "dynamic1": {X0: 10, X1: 9, Y0: 0, Y1: 9}, - "dynamic2": {X0: 10, X1: 9, Y0: 0, Y1: 9}, - }, - ) - }, - }, - { - // 10 total space minus 2 from the status box leaves us with 8. - // Total weight is 3, 8 / 3 = 2 with 2 remainder. - // We want to end up with 2, 3, 5 (one unit from remainder to each dynamic box) - testName: "Distributing remainder across weighted boxes", - root: &Box{Direction: COLUMN, Children: []*Box{{Size: 2, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}}, - x0: 0, - y0: 0, - width: 10, - height: 10, - test: func(result map[string]Dimensions) { - assert.EqualValues( - t, - result, - map[string]Dimensions{ - "static": {X0: 0, X1: 1, Y0: 0, Y1: 9}, // 2 - "dynamic1": {X0: 2, X1: 4, Y0: 0, Y1: 9}, // 3 - "dynamic2": {X0: 5, X1: 9, Y0: 0, Y1: 9}, // 5 - }, - ) - }, - }, - { - // 9 total space. - // total weight is 5, 9 / 5 = 1 with 4 remainder - // we want to give 2 of that remainder to the first, 1 to the second, and 1 to the last. - // Reason being that we just give units to each box evenly and consider weight in subsequent passes. - testName: "Distributing remainder across weighted boxes 2", - root: &Box{Direction: COLUMN, Children: []*Box{{Weight: 2, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}, {Weight: 1, Window: "dynamic3"}}}, - x0: 0, - y0: 0, - width: 9, - height: 10, - test: func(result map[string]Dimensions) { - assert.EqualValues( - t, - result, - map[string]Dimensions{ - "dynamic1": {X0: 0, X1: 3, Y0: 0, Y1: 9}, // 4 - "dynamic2": {X0: 4, X1: 6, Y0: 0, Y1: 9}, // 3 - "dynamic3": {X0: 7, X1: 8, Y0: 0, Y1: 9}, // 2 - }, - ) - }, - }, - { - // 9 total space. - // total weight is 5, 9 / 5 = 1 with 4 remainder - // we want to give 2 of that remainder to the first, 1 to the second, and 1 to the last. - // Reason being that we just give units to each box evenly and consider weight in subsequent passes. - testName: "Distributing remainder across weighted boxes with unnormalized weights", - root: &Box{Direction: COLUMN, Children: []*Box{{Weight: 4, Window: "dynamic1"}, {Weight: 4, Window: "dynamic2"}, {Weight: 2, Window: "dynamic3"}}}, - x0: 0, - y0: 0, - width: 9, - height: 10, - test: func(result map[string]Dimensions) { - assert.EqualValues( - t, - result, - map[string]Dimensions{ - "dynamic1": {X0: 0, X1: 3, Y0: 0, Y1: 9}, // 4 - "dynamic2": {X0: 4, X1: 6, Y0: 0, Y1: 9}, // 3 - "dynamic3": {X0: 7, X1: 8, Y0: 0, Y1: 9}, // 2 - }, - ) - }, - }, - { - testName: "Another distribution test", - root: &Box{Direction: COLUMN, Children: []*Box{ - {Weight: 3, Window: "dynamic1"}, - {Weight: 1, Window: "dynamic2"}, - {Weight: 1, Window: "dynamic3"}, - }}, - x0: 0, - y0: 0, - width: 9, - height: 10, - test: func(result map[string]Dimensions) { - assert.EqualValues( - t, - result, - map[string]Dimensions{ - "dynamic1": {X0: 0, X1: 4, Y0: 0, Y1: 9}, // 5 - "dynamic2": {X0: 5, X1: 6, Y0: 0, Y1: 9}, // 2 - "dynamic3": {X0: 7, X1: 8, Y0: 0, Y1: 9}, // 2 - }, - ) - }, - }, - { - testName: "Box with zero weight", - root: &Box{Direction: COLUMN, Children: []*Box{ - {Weight: 1, Window: "dynamic1"}, - {Weight: 0, Window: "dynamic2"}, - }}, - x0: 0, - y0: 0, - width: 10, - height: 10, - test: func(result map[string]Dimensions) { - assert.EqualValues( - t, - result, - map[string]Dimensions{ - "dynamic1": {X0: 0, X1: 9, Y0: 0, Y1: 9}, - "dynamic2": {X0: 10, X1: 9, Y0: 0, Y1: 9}, // when X0 > X1, we will hide the window - }, - ) - }, - }, - } - - for _, s := range scenarios { - s := s - t.Run(s.testName, func(t *testing.T) { - s.test(ArrangeWindows(s.root, s.x0, s.y0, s.width, s.height)) - }) - } -} - -func TestNormalizeWeights(t *testing.T) { - scenarios := []struct { - testName string - input []int - expected []int - }{ - { - testName: "empty", - input: []int{}, - expected: []int{}, - }, - { - testName: "one item of value 1", - input: []int{1}, - expected: []int{1}, - }, - { - testName: "one item of value greater than 1", - input: []int{2}, - expected: []int{1}, - }, - { - testName: "slice contains 1", - input: []int{2, 1}, - expected: []int{2, 1}, - }, - { - testName: "slice contains 2 and 2", - input: []int{2, 2}, - expected: []int{1, 1}, - }, - { - testName: "no common multiple", - input: []int{2, 3}, - expected: []int{2, 3}, - }, - { - testName: "complex case", - input: []int{10, 10, 20}, - expected: []int{1, 1, 2}, - }, - { - testName: "when a zero weight is included it is ignored", - input: []int{10, 10, 20, 0}, - expected: []int{1, 1, 2, 0}, - }, - } - - for _, s := range scenarios { - s := s - t.Run(s.testName, func(t *testing.T) { - assert.EqualValues(t, s.expected, normalizeWeights(s.input)) - }) - } -} |