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