1package core
2
3import "slices"
4
5// Cascade bands are explicit: Column.BandStart marks the first column of
6// each band, and moves keep the other bands as they are. A band also ends
7// where it would pass MaxColumns columns (max-columns shrank, or a column
8// was inserted into a full band), and a maximized column sits alone in its
9// band. scroll freezes those ends into BandStart, so the bands stay when
10// max-columns grows again or the column is unmaximized. Cells only share
11// a band's width (laneWidth); they never move a column to a band.
12
13// startsBand reports whether column i starts a band, n being the count of
14// columns in the band before it.
15func (w *Workspace) startsBand(i, n int) bool {
16 if i == 0 {
17 return true
18 }
19 c := w.Columns
20 return c[i].BandStart || n >= max(w.MaxColumns, 1) || c[i].FullWidth || c[i-1].FullWidth
21}
22
23// walkBands calls f with each band and its column range, in order, until f
24// returns false.
25func (w *Workspace) walkBands(f func(b, first, last int) bool) {
26 b, first, n := 0, 0, 0
27 for i := range w.Columns {
28 if i > first && w.startsBand(i, n) {
29 if !f(b, first, i-1) {
30 return
31 }
32 b, first, n = b+1, i, 0
33 }
34 n++
35 }
36 if len(w.Columns) > 0 {
37 f(b, first, len(w.Columns)-1)
38 }
39}
40
41// markBands freezes the band starts into BandStart.
42func (w *Workspace) markBands() {
43 n := 0
44 for i := range w.Columns {
45 s := w.startsBand(i, n)
46 w.Columns[i].BandStart = s
47 if s {
48 n = 0
49 }
50 n++
51 }
52}
53
54// band is the cascade band of column i.
55func (w *Workspace) band(i int) (band int) {
56 w.walkBands(func(b, _, last int) bool {
57 band = b
58 return last < i
59 })
60 return band
61}
62
63// bandBounds is the column range of band b, first > last past the bands.
64func (w *Workspace) bandBounds(b int) (first, last int) {
65 first, last = 0, -1
66 if b < 0 {
67 return first, last
68 }
69 w.walkBands(func(n, f, l int) bool {
70 if n == b {
71 first, last = f, l
72 }
73 return n < b
74 })
75 return first, last
76}
77
78// cellX is the x of column i in its lane, from the usable area's left edge.
79func (w *Workspace) cellX(i int) int {
80 first, last := w.laneBounds(w.laneOf(i))
81 S, u, sum := w.laneStats(first, last)
82 x := w.gap()
83 for j := first; j < i; j++ {
84 x += w.laneWidth(j, first, last, S, u, sum) + w.gap()
85 }
86 return x
87}
88func (w *Workspace) cascadeBands() (n, spacing int) {
89 if len(w.Columns) == 0 || w.Usable.H <= 0 {
90 return 1, 0
91 }
92 return w.band(len(w.Columns)-1) + 1, w.Usable.H
93}
94
95// cellWidth is the width of column i in its lane: the columns of a band
96// (the whole row in fixed overflow) share the usable width by their cells,
97// auto columns sharing what the others leave, so a lone column fills it.
98func (w *Workspace) cellWidth(i int) int {
99 first, last := w.laneBounds(w.laneOf(i))
100 S, u, sum := w.laneStats(first, last)
101 return w.laneWidth(i, first, last, S, u, sum)
102}
103
104// laneStats measures the lane first..last: S is the sum of the cells of
105// its set columns, u the count of its auto columns and sum the total of
106// their weights (laneWeight).
107func (w *Workspace) laneStats(first, last int) (S, u, sum int) {
108 for i := first; i <= last; i++ {
109 if c := w.Columns[i].Cells; c > 0 {
110 S += c
111 } else {
112 u++
113 }
114 }
115 for i := first; i <= last; i++ {
116 sum += w.laneWeight(i, S, u)
117 }
118 return S, u, sum
119}
120
121// laneWeight is the share of column i in its lane, given its stats. With
122// room left for every auto column, they share the cells the set ones leave
123// and the set ones keep their cells; otherwise an auto column weighs one
124// cell.
125func (w *Workspace) laneWeight(i, S, u int) int {
126 k := max(w.MaxColumns, 1)
127 c := w.Columns[i].Cells
128 switch {
129 case u == 0:
130 return c
131 case k-S >= u:
132 if c > 0 {
133 return c * u
134 }
135 return k - S
136 case c > 0:
137 return c
138 }
139 return 1
140}
141
142// laneWidth is the width of column i of the lane first..last with the
143// stats of laneStats: the usable width after gaps, split by weight. The
144// remainder under one pixel per weight stays empty, as in equalShare.
145func (w *Workspace) laneWidth(i, first, last, S, u, sum int) int {
146 if sum <= 0 {
147 return 0
148 }
149 avail := max(w.Usable.W-w.gap()*(last-first+2), 0)
150 // floor(avail*weight/sum) without overflowing on a huge area.
151 weight := w.laneWeight(i, S, u)
152 return avail/sum*weight + avail%sum*weight/sum
153}
154
155// equalShare is the width of each of k columns sharing the usable width
156// after gaps; a remainder under k pixels stays empty.
157func (w *Workspace) equalShare(k int) int {
158 k = max(k, 1)
159 return max((w.Usable.W-w.gap()*(k+1))/k, 0)
160}
161
162// focusBand focuses the column in the band dir away; a maximized column
163// keeps its own band, so it stays maximized.
164func (w *Workspace) focusBand(dir int) bool {
165 i := w.laneNeighbor(dir)
166 if i < 0 {
167 return false
168 }
169 w.Focus = i
170 w.Columns[i].Focus = 0
171 if dir < 0 {
172 w.Columns[i].Focus = len(w.Columns[i].Windows) - 1
173 }
174 w.raiseColumns()
175 w.scroll()
176 return true
177}
178func (w *Workspace) cascadeRectsInto(dst []Rect, ignoreFullWidth bool) []Rect {
179 g := w.gap()
180 rects := slices.Grow(dst[:0], len(w.Columns))[:len(w.Columns)]
181 view := w.View + w.shiftPixels()
182 w.walkBands(func(lane, first, last int) bool {
183 S, u, sum := w.laneStats(first, last)
184 x := g
185 for i := first; i <= last; i++ {
186 cw := w.laneWidth(i, first, last, S, u, sum)
187 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)}
188 if w.Columns[i].FullWidth && !ignoreFullWidth {
189 r.X = w.Usable.X + g
190 r.W = max(w.Usable.W-2*g, 0)
191 }
192 rects[i] = r
193 x += cw + g
194 }
195 return true
196 })
197 return rects
198}
199
200func (w *Workspace) cascadePoints() []float64 {
201 n, spacing := w.cascadeBands()
202 points := make([]float64, n)
203 for i := range points {
204 points[i] = float64(i * spacing)
205 }
206 return points
207}
208
209func (w *Workspace) cascadeFocus(view int) int {
210 if len(w.Columns) == 0 {
211 return 0
212 }
213 b := min(max(view/max(w.Usable.H, 1), 0), w.band(len(w.Columns)-1))
214 return w.laneNeighbor(b - w.band(w.Focus))
215}
216
217// laneOf is the lane of column i in the overview: its band, or the only
218// lane of a layout that does not wrap.
219func (w *Workspace) laneOf(i int) int {
220 if w.policy().wraps {
221 return w.band(i)
222 }
223 return 0
224}
225
226// laneBounds is the range of columns of a lane, first > last when the lane
227// does not exist. A layout that does not wrap has the whole row as lane 0.
228func (w *Workspace) laneBounds(lane int) (first, last int) {
229 if w.policy().wraps {
230 return w.bandBounds(lane)
231 }
232 if lane != 0 || len(w.Columns) == 0 {
233 return 0, -1
234 }
235 return 0, len(w.Columns) - 1
236}
237
238// leavesBand reports whether column i, where a left/right move from the
239// focused column goes, is past the workspace or in another cascade band:
240// the move then goes to the neighbor monitor first.
241func (w *Workspace) leavesBand(i int) bool {
242 return i < 0 || i >= len(w.Columns) || w.policy().wraps && w.band(i) != w.band(w.Focus)
243}
244
245// moveBand moves the focused cascade column to the band dir away
246// (move-window-up/down past its column), keeping its offset in the band;
247// focus follows it. See shiftBand.
248func (w *Workspace) moveBand(dir int) {
249 if !w.policy().wraps || len(w.Columns) == 0 {
250 return
251 }
252 first, _ := w.bandBounds(w.band(w.Focus))
253 if w.shiftBand(w.Focus, w.Focus, dir, w.Focus-first) {
254 w.scroll()
255 }
256}
257
258// laneNeighbor is the closest column to the focus in the lane d lanes away,
259// the focus's offset in its lane kept; -1 past the first or last lane.
260func (w *Workspace) laneNeighbor(d int) int {
261 lane := w.laneOf(w.Focus)
262 first, _ := w.laneBounds(lane)
263 nfirst, nlast := w.laneBounds(lane + d)
264 if nfirst > nlast {
265 return -1
266 }
267 return min(nfirst+w.Focus-first, nlast)
268}
269
270// bandList copies the cascade columns into their bands.
271func (w *Workspace) bandList() [][]Column {
272 var bands [][]Column
273 w.walkBands(func(_, first, last int) bool {
274 bands = append(bands, slices.Clone(w.Columns[first:last+1]))
275 return true
276 })
277 return bands
278}
279
280// setBands stores bands as the columns, empty bands dropped, and focuses
281// the column holding window focus.
282func (w *Workspace) setBands(bands [][]Column, focus WindowID) {
283 cols := w.Columns[:0]
284 for _, b := range bands {
285 for i, c := range b {
286 c.BandStart = i == 0
287 cols = append(cols, c)
288 }
289 }
290 w.Columns = cols
291 if i := w.columnOf(focus); i >= 0 {
292 w.Focus = i
293 }
294}
295
296// fits reports whether cols can join band b: it has room for them and
297// neither side holds a maximized column, which keeps a band to itself.
298func (w *Workspace) fits(bands [][]Column, b int, cols []Column) bool {
299 if b < 0 || b >= len(bands) || len(bands[b])+len(cols) > max(w.MaxColumns, 1) {
300 return false
301 }
302 full := func(c Column) bool { return c.FullWidth }
303 return !slices.ContainsFunc(bands[b], full) && !slices.ContainsFunc(cols, full)
304}
305
306// sendBand moves cols, already taken out of band b, to the band dir away:
307// they join it at index at (clamped) when they fit, else they make a new
308// band between the two, or past the outer band. When cols were alone in
309// band b, it goes away and a band that cannot take them is passed: the
310// new band goes on its far side, so repeated moves always progress.
311func (w *Workspace) sendBand(bands [][]Column, b, dir, at int, cols []Column) [][]Column {
312 t, nb := b+dir, b // A new band between b and t, before b going up.
313 if dir > 0 {
314 nb = b + 1
315 }
316 if len(bands[b]) == 0 {
317 bands = slices.Delete(bands, b, b+1)
318 t, nb = b-1, b-1 // Going up: before band t.
319 if dir > 0 {
320 t, nb = b, b+1 // Going down: after band t.
321 }
322 }
323 if w.fits(bands, t, cols) {
324 at = min(max(at, 0), len(bands[t]))
325 bands[t] = slices.Insert(bands[t], at, cols...)
326 return bands
327 }
328 return slices.Insert(bands, min(max(nb, 0), len(bands)), slices.Clone(cols))
329}
330
331// shiftBand moves the columns lo..hi of one cascade band to the band dir
332// away (see sendBand), at offset at in it. It reports whether they moved.
333func (w *Workspace) shiftBand(lo, hi, dir, at int) bool {
334 if len(w.Columns) == 0 || lo < 0 || hi >= len(w.Columns) || lo > hi {
335 return false
336 }
337 b := w.band(lo)
338 if w.band(hi) != b {
339 return false
340 }
341 first, last := w.bandBounds(b)
342 if lo == first && hi == last && (b+dir < 0 || b+dir > w.band(len(w.Columns)-1)) {
343 return false // Alone at the outer band: nothing to cross.
344 }
345 id, _ := w.Focused()
346 bands := w.bandList()
347 cols := slices.Clone(bands[b][lo-first : hi-first+1])
348 bands[b] = slices.Delete(bands[b], lo-first, hi-first+1)
349 w.setBands(w.sendBand(bands, b, dir, at, cols), id)
350 return true
351}
352
353// columnIDs lists the windows of each column, in order: its identity for
354// restoreBand. The column is the one headed by the first of them still
355// heading a column, so it is found after its first window closed or its
356// stack changed order, and a window that left it for a column of its own
357// comes after the one that stayed first.
358func columnIDs(cols []Column) [][]WindowID {
359 ids := make([][]WindowID, len(cols))
360 for i, c := range cols {
361 ids[i] = slices.Clone(c.Windows)
362 }
363 return ids
364}
365
366// isolateBand gives the focused cascade column a band of its own: the
367// columns on its left go to the band above, the ones on its right to the
368// band below (sendBand), and the column remembers them for restoreBand.
369func (w *Workspace) isolateBand() {
370 b := w.band(w.Focus)
371 first, last := w.bandBounds(b)
372 if first == last {
373 return
374 }
375 id, _ := w.Focused()
376 bands := w.bandList()
377 p := w.Focus - first
378 left := slices.Clone(bands[b][:p])
379 right := slices.Clone(bands[b][p+1:])
380 c := bands[b][p]
381 // Both lists keep the band's left-to-right order. Earlier pushes
382 // (cycle-column-width) took the far ends of the band, so the new ones
383 // were nearer the column.
384 c.pushedUp = append(c.pushedUp, columnIDs(left)...)
385 c.pushedDown = append(columnIDs(right), c.pushedDown...)
386 bands[b] = []Column{c}
387 if len(right) > 0 {
388 bands = w.sendBand(bands, b, 1, 0, right)
389 }
390 if len(left) > 0 {
391 bands = w.sendBand(bands, b, -1, max(w.MaxColumns, 1), left)
392 }
393 w.setBands(bands, id)
394}
395
396// pushNeighbor makes room in the focused column's full cascade band for a
397// wider column: the column at the far end of the band leaves it. The
398// rightmost goes to the band below; from the last column, the leftmost
399// goes to the band above, at its right end. The column remembers it.
400func (w *Workspace) pushNeighbor() {
401 first, last := w.bandBounds(w.band(w.Focus))
402 if first == last {
403 return
404 }
405 id, _ := w.Focused()
406 c := &w.Columns[w.Focus]
407 if w.Focus < last {
408 c.pushedDown = append(c.pushedDown, columnIDs(w.Columns[last:last+1])...)
409 w.shiftBand(last, last, 1, 0)
410 } else {
411 c.pushedUp = append(columnIDs(w.Columns[first:first+1]), c.pushedUp...)
412 w.shiftBand(first, first, -1, max(w.MaxColumns, 1))
413 }
414 w.Focus = w.columnOf(id)
415}
416
417// restoreBand brings back the columns isolateBand or pushNeighbor sent
418// away, those still in the adjacent band, as many as the band has room
419// for, nearest to the focused column first: the ones sent up at the start
420// of the band, the ones sent down at its end, in their former order. The
421// others stay where they are. It reports whether the column remembered
422// any.
423func (w *Workspace) restoreBand() bool {
424 c := &w.Columns[w.Focus]
425 up, down := c.pushedUp, c.pushedDown
426 c.pushedUp, c.pushedDown = nil, nil
427 if len(up) == 0 && len(down) == 0 {
428 return false
429 }
430 id, _ := w.Focused()
431 bands := w.bandList()
432 b := w.band(w.Focus)
433 room := max(max(w.MaxColumns, 1)-len(bands[b]), 0)
434 // take removes from band nb the remembered columns found there, at
435 // most room of them, in the order of ids walked from the focused
436 // column outward (rev: ids end nearest).
437 take := func(nb int, ids [][]WindowID, rev bool) []Column {
438 if nb < 0 || nb >= len(bands) {
439 return nil
440 }
441 var got []Column
442 for k := range ids {
443 if room == 0 {
444 break
445 }
446 e := ids[k]
447 if rev {
448 e = ids[len(ids)-1-k]
449 }
450 // The column is named across the workspace first: one that
451 // moved farther away stays there, with no stand-in.
452 head := slices.IndexFunc(e, func(v WindowID) bool {
453 return slices.ContainsFunc(w.Columns, func(c Column) bool { return c.Windows[0] == v })
454 })
455 if head < 0 {
456 continue
457 }
458 i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] })
459 if i < 0 {
460 continue
461 }
462 got = append(got, bands[nb][i])
463 bands[nb] = slices.Delete(bands[nb], i, i+1)
464 room--
465 }
466 if rev {
467 slices.Reverse(got)
468 }
469 return got
470 }
471 above := take(b-1, up, true)
472 below := take(b+1, down, false)
473 bands[b] = slices.Concat(above, bands[b], below)
474 w.setBands(bands, id)
475 return true
476}
477
478// spawnIndex is where a new cascade column goes: after the focused band,
479// which it joins when it has room; otherwise it starts a band of its own
480// between it and the next (newBand).
481func (w *Workspace) spawnIndex() (at int, newBand bool) {
482 if len(w.Columns) == 0 {
483 return 0, true
484 }
485 b := w.band(w.Focus)
486 first, last := w.bandBounds(b)
487 bands := [][]Column{w.Columns[first : last+1]}
488 return last + 1, !w.fits(bands, 0, []Column{{}})
489}
490
491// deleteColumn removes column i; a band it started keeps its start.
492func (w *Workspace) deleteColumn(i int) {
493 if w.policy().wraps && w.Columns[i].BandStart && i+1 < len(w.Columns) {
494 w.Columns[i+1].BandStart = true
495 }
496 w.Columns = slices.Delete(w.Columns, i, i+1)
497}
498
499// forgetBands drops the cascade band starts and pushed columns: the bands
500// pack again in column order.
501func (w *Workspace) forgetBands() {
502 for i := range w.Columns {
503 c := &w.Columns[i]
504 c.BandStart, c.pushedUp, c.pushedDown = false, nil, nil
505 }
506}