2020-08-15 08:28:02 +10:00
package boxlayout
2020-05-16 12:35:19 +10:00
2022-04-17 09:27:17 +10:00
import (
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
2020-05-16 12:35:19 +10:00
2020-08-15 08:28:02 +10:00
type Dimensions struct {
X0 int
X1 int
Y0 int
Y1 int
2020-05-17 21:32:17 +10:00
}
2021-03-31 23:55:06 +11:00
type Direction int
2020-05-16 12:35:19 +10:00
const (
2021-03-31 23:55:06 +11:00
ROW Direction = iota
2020-05-16 12:35:19 +10:00
COLUMN
)
2020-08-21 19:53:45 +10:00
// 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.
2020-05-18 22:00:07 +10:00
// If a box has children, it needs to specify how it wants to arrange those children: ROW or COLUMN.
2020-08-21 19:53:45 +10:00
// If a box represents a window, you can put the window name in the Window field.
2020-05-18 22:00:07 +10:00
// 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%
2020-08-15 08:28:02 +10:00
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.
2021-03-31 23:55:06 +11:00
Direction Direction
2020-05-16 12:35:19 +10:00
// function which takes the width and height assigned to the box and decides which orientation it will have
2021-03-31 23:55:06 +11:00
ConditionalDirection func ( width int , height int ) Direction
2020-05-16 12:35:19 +10:00
2020-08-15 08:28:02 +10:00
Children [ ] * Box
2020-05-16 12:35:19 +10:00
2020-05-17 21:32:17 +10:00
// function which takes the width and height assigned to the box and decides the layout of the children.
2020-08-15 08:28:02 +10:00
ConditionalChildren func ( width int , height int ) [ ] * Box
2020-05-17 21:32:17 +10:00
2020-08-21 19:53:45 +10:00
// Window refers to the name of the window this box represents, if there is one
Window string
2020-05-16 12:35:19 +10:00
2020-08-15 08:28:02 +10:00
// static Size. If parent box's direction is ROW this refers to height, otherwise width
Size int
2020-05-16 12:35:19 +10:00
2020-08-15 08:28:02 +10:00
// 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
2020-05-16 12:35:19 +10:00
}
2020-08-21 19:53:45 +10:00
func ArrangeWindows ( root * Box , x0 , y0 , width , height int ) map [ string ] Dimensions {
2020-05-17 21:44:59 +10:00
children := root . getChildren ( width , height )
if len ( children ) == 0 {
2020-05-16 12:35:19 +10:00
// leaf node
2020-08-21 19:53:45 +10:00
if root . Window != "" {
dimensionsForWindow := Dimensions { X0 : x0 , Y0 : y0 , X1 : x0 + width - 1 , Y1 : y0 + height - 1 }
return map [ string ] Dimensions { root . Window : dimensionsForWindow }
2020-05-16 12:35:19 +10:00
}
2020-08-15 08:28:02 +10:00
return map [ string ] Dimensions { }
2020-05-16 12:35:19 +10:00
}
direction := root . getDirection ( width , height )
var availableSize int
if direction == COLUMN {
availableSize = width
} else {
availableSize = height
}
2022-04-17 09:27:17 +10:00
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
2020-05-16 12:35:19 +10:00
}
2022-04-17 09:27:17 +10:00
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 ]
}
2020-05-16 12:35:19 +10:00
}
2022-04-17 09:27:17 +10:00
dynamicSpace := utils . Max ( 0 , availableSpace - reservedSpace )
2020-05-16 12:35:19 +10:00
unitSize := 0
2022-04-17 09:27:17 +10:00
extraSpace := 0
2020-05-16 12:35:19 +10:00
if totalWeight > 0 {
2022-04-17 09:27:17 +10:00
unitSize = dynamicSpace / totalWeight
extraSpace = dynamicSpace % totalWeight
2020-05-16 12:35:19 +10:00
}
2022-04-17 09:27:17 +10:00
result := make ( [ ] int , len ( boxes ) )
for i , box := range boxes {
if box . isStatic ( ) {
2021-04-11 12:12:26 +10:00
// 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
2022-04-17 09:27:17 +10:00
result [ i ] = utils . Min ( availableSpace , box . Size )
2020-05-16 12:35:19 +10:00
} else {
2022-04-17 09:27:17 +10:00
result [ i ] = unitSize * normalizedWeights [ i ]
2020-05-16 12:35:19 +10:00
}
2022-04-17 09:27:17 +10:00
}
2020-05-16 12:35:19 +10:00
2022-04-17 09:27:17 +10:00
// 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
}
}
2020-05-16 12:35:19 +10:00
}
}
return result
}
2022-04-17 09:27:17 +10:00
// 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
}
2020-08-15 08:58:58 +10:00
func ( b * Box ) isStatic ( ) bool {
return b . Size > 0
}
2021-03-31 23:55:06 +11:00
func ( b * Box ) getDirection ( width int , height int ) Direction {
2020-08-15 08:58:58 +10:00
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
}
2020-08-15 08:28:02 +10:00
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 } {
2020-05-16 12:35:19 +10:00
for k , v := range dimensionMap {
result [ k ] = v
}
}
return result
}