1package boxlayout
2
3import "math"
4
5type Dimensions struct {
6	X0 int
7	X1 int
8	Y0 int
9	Y1 int
10}
11
12type Direction int
13
14const (
15	ROW Direction = iota
16	COLUMN
17)
18
19// 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.
20// If a box has children, it needs to specify how it wants to arrange those children: ROW or COLUMN.
21// If a box represents a window, you can put the window name in the Window field.
22// When determining how to divvy-up the available height (for row children) or width (for column children), we first
23// give the boxes with a static `size` the space that they want. Then we apportion
24// the remaining space based on the weights of the dynamic boxes (you can't define
25// both size and weight at the same time: you gotta pick one). If there are two
26// boxes, one with weight 1 and the other with weight 2, the first one gets 33%
27// of the available space and the second one gets the remaining 66%
28
29type Box struct {
30	// 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.
31	Direction Direction
32
33	// function which takes the width and height assigned to the box and decides which orientation it will have
34	ConditionalDirection func(width int, height int) Direction
35
36	Children []*Box
37
38	// function which takes the width and height assigned to the box and decides the layout of the children.
39	ConditionalChildren func(width int, height int) []*Box
40
41	// Window refers to the name of the window this box represents, if there is one
42	Window string
43
44	// static Size. If parent box's direction is ROW this refers to height, otherwise width
45	Size int
46
47	// 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
48	// TODO: consider making there be one int and a type enum so we can't have size and Weight simultaneously defined
49	Weight int
50}
51
52func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions {
53	children := root.getChildren(width, height)
54	if len(children) == 0 {
55		// leaf node
56		if root.Window != "" {
57			dimensionsForWindow := Dimensions{X0: x0, Y0: y0, X1: x0 + width - 1, Y1: y0 + height - 1}
58			return map[string]Dimensions{root.Window: dimensionsForWindow}
59		}
60		return map[string]Dimensions{}
61	}
62
63	direction := root.getDirection(width, height)
64
65	var availableSize int
66	if direction == COLUMN {
67		availableSize = width
68	} else {
69		availableSize = height
70	}
71
72	// work out size taken up by children
73	reservedSize := 0
74	totalWeight := 0
75	for _, child := range children {
76		// assuming either size or weight are non-zero
77		reservedSize += child.Size
78		totalWeight += child.Weight
79	}
80
81	remainingSize := availableSize - reservedSize
82	if remainingSize < 0 {
83		remainingSize = 0
84	}
85
86	unitSize := 0
87	extraSize := 0
88	if totalWeight > 0 {
89		unitSize = remainingSize / totalWeight
90		extraSize = remainingSize % totalWeight
91	}
92
93	result := map[string]Dimensions{}
94	offset := 0
95	for _, child := range children {
96		var boxSize int
97		if child.isStatic() {
98			boxSize = child.Size
99			// assuming that only one static child can have a size greater than the
100			// available space. In that case we just crop the size to what's available
101			if boxSize > availableSize {
102				boxSize = availableSize
103			}
104		} else {
105			// TODO: consider more evenly distributing the remainder
106			boxSize = unitSize * child.Weight
107			boxExtraSize := int(math.Min(float64(extraSize), float64(child.Weight)))
108			boxSize += boxExtraSize
109			extraSize -= boxExtraSize
110		}
111
112		var resultForChild map[string]Dimensions
113		if direction == COLUMN {
114			resultForChild = ArrangeWindows(child, x0+offset, y0, boxSize, height)
115		} else {
116			resultForChild = ArrangeWindows(child, x0, y0+offset, width, boxSize)
117		}
118
119		result = mergeDimensionMaps(result, resultForChild)
120		offset += boxSize
121	}
122
123	return result
124}
125
126func (b *Box) isStatic() bool {
127	return b.Size > 0
128}
129
130func (b *Box) getDirection(width int, height int) Direction {
131	if b.ConditionalDirection != nil {
132		return b.ConditionalDirection(width, height)
133	}
134	return b.Direction
135}
136
137func (b *Box) getChildren(width int, height int) []*Box {
138	if b.ConditionalChildren != nil {
139		return b.ConditionalChildren(width, height)
140	}
141	return b.Children
142}
143
144func mergeDimensionMaps(a map[string]Dimensions, b map[string]Dimensions) map[string]Dimensions {
145	result := map[string]Dimensions{}
146	for _, dimensionMap := range []map[string]Dimensions{a, b} {
147		for k, v := range dimensionMap {
148			result[k] = v
149		}
150	}
151	return result
152}
153