1package core
2
3import "slices"
4
5// Cascade bands pack columns in order: a band ends when the next column
6// would take it past MaxColumns slots. A column takes one slot and a
7// maximized one all of them, so it sits alone in its band. Cells only
8// share a band's width (laneWidth); they never move a column to a band.
9
10// bandSlots is the number of slots column i takes in its band.
11func (w *Workspace) bandSlots(i int) int {
12 if w.Columns[i].FullWidth {
13 return max(w.MaxColumns, 1)
14 }
15 return 1
16}
17
18// walkBands calls f with each band and its column range, in order, until f
19// returns false.
20func (w *Workspace) walkBands(f func(b, first, last int) bool) {
21 k := max(w.MaxColumns, 1)
22 b, first, sum := 0, 0, 0
23 for i := range w.Columns {
24 c := w.bandSlots(i)
25 if i > first && sum+c > k {
26 if !f(b, first, i-1) {
27 return
28 }
29 b, first, sum = b+1, i, 0
30 }
31 sum += c
32 }
33 if len(w.Columns) > 0 {
34 f(b, first, len(w.Columns)-1)
35 }
36}
37
38// band is the cascade band of column i.
39func (w *Workspace) band(i int) (band int) {
40 w.walkBands(func(b, _, last int) bool {
41 band = b
42 return last < i
43 })
44 return band
45}
46
47// bandBounds is the column range of band b, first > last past the bands.
48func (w *Workspace) bandBounds(b int) (first, last int) {
49 first, last = 0, -1
50 if b < 0 {
51 return first, last
52 }
53 w.walkBands(func(n, f, l int) bool {
54 if n == b {
55 first, last = f, l
56 }
57 return n < b
58 })
59 return first, last
60}
61
62// cellX is the x of column i in its lane, from the usable area's left edge.
63func (w *Workspace) cellX(i int) int {
64 first, last := w.laneBounds(w.laneOf(i))
65 S, u, sum := w.laneStats(first, last)
66 x := w.gap()
67 for j := first; j < i; j++ {
68 x += w.laneWidth(j, first, last, S, u, sum) + w.gap()
69 }
70 return x
71}
72func (w *Workspace) cascadeBands() (n, spacing int) {
73 if len(w.Columns) == 0 || w.Usable.H <= 0 {
74 return 1, 0
75 }
76 return w.band(len(w.Columns)-1) + 1, w.Usable.H
77}
78
79// cellWidth is the width of column i in its lane: the columns of a band
80// (the whole row in fixed overflow) share the usable width by their cells,
81// auto columns sharing what the others leave, so a lone column fills it.
82func (w *Workspace) cellWidth(i int) int {
83 first, last := w.laneBounds(w.laneOf(i))
84 S, u, sum := w.laneStats(first, last)
85 return w.laneWidth(i, first, last, S, u, sum)
86}
87
88// laneStats measures the lane first..last: S is the sum of the cells of
89// its set columns, u the count of its auto columns and sum the total of
90// their weights (laneWeight).
91func (w *Workspace) laneStats(first, last int) (S, u, sum int) {
92 for i := first; i <= last; i++ {
93 if c := w.Columns[i].Cells; c > 0 {
94 S += c
95 } else {
96 u++
97 }
98 }
99 for i := first; i <= last; i++ {
100 sum += w.laneWeight(i, S, u)
101 }
102 return S, u, sum
103}
104
105// laneWeight is the share of column i in its lane, given its stats. With
106// room left for every auto column, they share the cells the set ones leave
107// and the set ones keep their cells; otherwise an auto column weighs one
108// cell.
109func (w *Workspace) laneWeight(i, S, u int) int {
110 k := max(w.MaxColumns, 1)
111 c := w.Columns[i].Cells
112 switch {
113 case u == 0:
114 return c
115 case k-S >= u:
116 if c > 0 {
117 return c * u
118 }
119 return k - S
120 case c > 0:
121 return c
122 }
123 return 1
124}
125
126// laneWidth is the width of column i of the lane first..last with the
127// stats of laneStats: the usable width after gaps, split by weight. The
128// remainder under one pixel per weight stays empty, as in equalShare.
129func (w *Workspace) laneWidth(i, first, last, S, u, sum int) int {
130 if sum <= 0 {
131 return 0
132 }
133 avail := max(w.Usable.W-w.gap()*(last-first+2), 0)
134 // floor(avail*weight/sum) without overflowing on a huge area.
135 weight := w.laneWeight(i, S, u)
136 return avail/sum*weight + avail%sum*weight/sum
137}
138
139// equalShare is the width of each of k columns sharing the usable width
140// after gaps; a remainder under k pixels stays empty.
141func (w *Workspace) equalShare(k int) int {
142 k = max(k, 1)
143 return max((w.Usable.W-w.gap()*(k+1))/k, 0)
144}
145
146// focusBand focuses the column in the band dir away; a maximized column
147// keeps its own band, so it stays maximized.
148func (w *Workspace) focusBand(dir int) bool {
149 i := w.laneNeighbor(dir)
150 if i < 0 {
151 return false
152 }
153 w.Focus = i
154 w.Columns[i].Focus = 0
155 if dir < 0 {
156 w.Columns[i].Focus = len(w.Columns[i].Windows) - 1
157 }
158 w.raiseColumns()
159 w.scroll()
160 return true
161}
162func (w *Workspace) cascadeRectsInto(dst []Rect, ignoreFullWidth bool) []Rect {
163 g := w.gap()
164 rects := slices.Grow(dst[:0], len(w.Columns))[:len(w.Columns)]
165 view := w.View + w.shiftPixels()
166 w.walkBands(func(lane, first, last int) bool {
167 S, u, sum := w.laneStats(first, last)
168 x := g
169 for i := first; i <= last; i++ {
170 cw := w.laneWidth(i, first, last, S, u, sum)
171 r := Rect{X: w.Usable.X + x, Y: w.Usable.Y + g + lane*w.Usable.H - view, W: cw, H: max(w.Usable.H-2*g, 0)}
172 if w.Columns[i].FullWidth && !ignoreFullWidth {
173 r.X = w.Usable.X + g
174 r.W = max(w.Usable.W-2*g, 0)
175 }
176 rects[i] = r
177 x += cw + g
178 }
179 return true
180 })
181 return rects
182}
183
184func (w *Workspace) cascadePoints() []float64 {
185 n, spacing := w.cascadeBands()
186 points := make([]float64, n)
187 for i := range points {
188 points[i] = float64(i * spacing)
189 }
190 return points
191}
192
193func (w *Workspace) cascadeFocus(view int) int {
194 if len(w.Columns) == 0 {
195 return 0
196 }
197 b := min(max(view/max(w.Usable.H, 1), 0), w.band(len(w.Columns)-1))
198 return w.laneNeighbor(b - w.band(w.Focus))
199}
200
201// laneOf is the lane of column i in the overview: its band, or the only
202// lane of a layout that does not wrap.
203func (w *Workspace) laneOf(i int) int {
204 if w.policy().wraps {
205 return w.band(i)
206 }
207 return 0
208}
209
210// laneBounds is the range of columns of a lane, first > last when the lane
211// does not exist. A layout that does not wrap has the whole row as lane 0.
212func (w *Workspace) laneBounds(lane int) (first, last int) {
213 if w.policy().wraps {
214 return w.bandBounds(lane)
215 }
216 if lane != 0 || len(w.Columns) == 0 {
217 return 0, -1
218 }
219 return 0, len(w.Columns) - 1
220}
221
222// leavesBand reports whether column i, where a left/right move from the
223// focused column goes, is past the workspace or in another cascade band:
224// the move then goes to the neighbor monitor first.
225func (w *Workspace) leavesBand(i int) bool {
226 return i < 0 || i >= len(w.Columns) || w.policy().wraps && w.band(i) != w.band(w.Focus)
227}
228
229// moveBand swaps the focused cascade column with the column in the band dir
230// away where focus-window-up/down goes; focus follows it. Past the first
231// or last band nothing moves. Columns keep their maximization: when the two
232// take a different number of slots, a swap would repack the bands around
233// them, so the focused column passes the whole adjacent band instead.
234func (w *Workspace) moveBand(dir int) {
235 i := w.laneNeighbor(dir)
236 if i < 0 {
237 return
238 }
239 if w.bandSlots(w.Focus) == w.bandSlots(i) {
240 w.Columns[w.Focus], w.Columns[i] = w.Columns[i], w.Columns[w.Focus]
241 w.Focus = i
242 w.scroll()
243 return
244 }
245 first, last := w.bandBounds(w.band(i))
246 col := w.Columns[w.Focus]
247 w.Columns = slices.Delete(w.Columns, w.Focus, w.Focus+1)
248 at := first // dir < 0: before the band above.
249 if dir > 0 {
250 at = last // After the band below, one index lower once removed.
251 }
252 w.Columns = slices.Insert(w.Columns, at, col)
253 w.Focus = at
254 w.scroll()
255}
256
257// laneNeighbor is the closest column to the focus in the lane d lanes away,
258// the focus's offset in its lane kept; -1 past the first or last lane.
259func (w *Workspace) laneNeighbor(d int) int {
260 lane := w.laneOf(w.Focus)
261 first, _ := w.laneBounds(lane)
262 nfirst, nlast := w.laneBounds(lane + d)
263 if nfirst > nlast {
264 return -1
265 }
266 return min(nfirst+w.Focus-first, nlast)
267}