summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go
blob: c516cbba751f175c573ebedbee2987ebbf7776f2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
package boxlayout

import (
	"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(lo.Map(boxes, func(box *Box, _ int) 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 lo.SomeBy(weights, func(weight int) bool { return weight == 1 }) {
		return weights
	}

	// map weights to factorSlices and find the lowest common factor
	positiveWeights := lo.Filter(weights, func(weight int, _ int) bool { return weight > 0 })
	factorSlices := lo.Map(positiveWeights, func(weight int, _ int) []int { return calcFactors(weight) })
	commonFactors := factorSlices[0]
	for _, factors := range factorSlices {
		commonFactors = lo.Intersect(commonFactors, factors)
	}

	if len(commonFactors) == 0 {
		return weights
	}

	newWeights := lo.Map(weights, func(weight int, _ 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
}