summaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2022-10-09 08:31:14 -0700
committerJesse Duffield <jessedduffield@gmail.com>2022-10-09 08:31:14 -0700
commitdba0edb9987b16738fb4b1c07e681388a84ce52d (patch)
tree935efd9877ca12c7757d98632d5bc89d159ab358 /pkg
parent7b4b42abd6f458eb667d322ec870bc6b20024812 (diff)
use boxlayout from lazycore
Diffstat (limited to 'pkg')
-rw-r--r--pkg/gui/arrangement.go2
-rw-r--r--pkg/gui/boxlayout/boxlayout.go212
-rw-r--r--pkg/gui/boxlayout/boxlayout_test.go380
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))
- })
- }
-}