summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go')
-rw-r--r--vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go212
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
+}