mirror of
https://github.com/jesseduffield/lazygit.git
synced 2024-12-04 10:34:55 +02:00
213 lines
5.9 KiB
Go
213 lines
5.9 KiB
Go
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
|
|
}
|