diff options
Diffstat (limited to 'vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go')
-rw-r--r-- | vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go | 212 |
1 files changed, 212 insertions, 0 deletions
diff --git a/vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go b/vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go new file mode 100644 index 000000000..7cacfff98 --- /dev/null +++ b/vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go @@ -0,0 +1,212 @@ +package boxlayout + +import ( + "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/lazycore/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 +} |