1package core
2
3import "slices"
4
5type stackKind uint8
6
7const (
8 stackFloat stackKind = iota
9 stackColumns
10 stackColumn
11)
12
13// A stackItem identifies a covering float, a column group (all columns, or
14// all except the maximized column), or the maximized column by one of its
15// windows. Only stackColumns has a zero ID.
16type stackItem struct {
17 kind stackKind
18 id WindowID
19}
20
21// overviewMaximized reports a fixed row whose focused column hides the
22// others: maximized, or holding the covering fullscreen tile. Its card
23// shows in front of the hidden columns' card.
24func (w *Workspace) overviewMaximized() bool {
25 if w.Overflow != OverflowFixed || len(w.Columns) < 2 || w.Focus < 0 || w.Focus >= len(w.Columns) {
26 return false
27 }
28 if c := w.cover(); c != 0 && !w.isFloat(c) {
29 return w.columnOf(c) == w.Focus
30 }
31 return w.Columns[w.Focus].FullWidth
32}
33
34func (w *Workspace) columnOf(id WindowID) int {
35 c, _ := w.cell(id)
36 return c - 1
37}
38
39// stack lists what is on screen followed by what it hides, top to bottom.
40// A pinned fullscreen window is the front card, a float on its own, a
41// fixed tile with its column, so every window it hides stays reachable; a
42// stashed one is its card in the pile.
43func (w *Workspace) stack() []stackItem {
44 var items []stackItem
45 w.walkStack(func(item stackItem) bool {
46 items = append(items, item)
47 return true
48 })
49 return items
50}
51
52// stacked reports whether the stack has several cards, without building it:
53// the render path asks for every workspace of every frame.
54func (w *Workspace) stacked() bool {
55 _, n := w.stackHead()
56 return n > 1
57}
58
59// stackHead is the first item of the stack and the number of items, counted
60// up to 2, without building the stack.
61func (w *Workspace) stackHead() (first stackItem, n int) {
62 w.walkStack(func(item stackItem) bool {
63 if n == 0 {
64 first = item
65 }
66 n++
67 return n < 2
68 })
69 return first, n
70}
71
72// walkStack yields the items of stack in order until yield returns false.
73func (w *Workspace) walkStack(yield func(stackItem) bool) {
74 c, columns := WindowID(0), len(w.Columns) > 0
75 if w.pinned() {
76 c = w.cover()
77 }
78 switch {
79 case c == 0, w.stashIndex(c) >= 0:
80 case w.floatIndex(c) >= 0:
81 if !yield(stackItem{stackFloat, c}) {
82 return
83 }
84 case w.overviewMaximized():
85 if !yield(stackItem{stackColumn, c}) {
86 return
87 }
88 default:
89 // One card of all columns, its own marked fullscreen.
90 if !yield(stackItem{kind: stackColumns}) {
91 return
92 }
93 columns = false
94 }
95 for i := len(w.Floats) - 1; i >= 0; i-- {
96 f := w.Floats[i]
97 if !f.below && w.coversFloat(f) && !yield(stackItem{stackFloat, f.ID}) {
98 return
99 }
100 }
101 if (c == 0 || w.isFloat(c)) && w.overviewMaximized() && !yield(stackItem{stackColumn, w.Columns[w.Focus].Windows[0]}) {
102 return
103 }
104 if columns && !yield(stackItem{kind: stackColumns}) {
105 return
106 }
107 for i := len(w.Floats) - 1; i >= 0; i-- {
108 f := w.Floats[i]
109 if f.below && w.coversFloat(f) && !yield(stackItem{stackFloat, f.ID}) {
110 return
111 }
112 }
113}
114
115// itemOf resolves a clicked tile to the card currently containing it.
116func (w *Workspace) itemOf(id WindowID) (stackItem, bool) {
117 for _, item := range w.stack() {
118 switch item.kind {
119 case stackFloat:
120 if item.id == id {
121 return item, true
122 }
123 case stackColumn:
124 if i := w.columnOf(id); i >= 0 && i == w.columnOf(item.id) {
125 return item, true
126 }
127 case stackColumns:
128 if i := w.columnOf(id); i >= 0 && (!w.overviewMaximized() || i != w.Focus) {
129 return item, true
130 }
131 }
132 }
133 return stackItem{}, false
134}
135
136// stackFront resolves the provisional item against fresh workspace contents;
137// an invalidated selection falls back to the nearest surviving stack index.
138func (m *Monitor) stackFront(w *Workspace) stackItem {
139 items := w.stack()
140 if len(items) == 0 {
141 return stackItem{}
142 }
143 if m.ov.row == w {
144 for _, item := range items {
145 if item == m.ov.front || item.kind == stackColumn && m.ov.front.kind == stackColumn && w.columnOf(item.id) >= 0 && w.columnOf(item.id) == w.columnOf(m.ov.front.id) {
146 return item
147 }
148 }
149 return items[min(max(m.ov.frontAt, 0), len(items)-1)]
150 }
151 return items[0]
152}
153
154// setFront records a provisional card and its index for removal fallback.
155func (m *Monitor) setFront(w *Workspace, item stackItem) {
156 m.ov.row, m.ov.front = w, item
157 m.ov.frontAt = slices.Index(w.stack(), item)
158}
159
160// moveStack moves only within the current row. The caller changes workspace
161// when the linear stack has no item in that direction.
162func (m *Monitor) moveStack(d int) bool {
163 w := m.Current()
164 items := w.stack()
165 if len(items) < 2 {
166 return false
167 }
168 at := slices.Index(items, m.stackFront(w)) + d
169 if at < 0 || at >= len(items) {
170 return false
171 }
172 m.setFront(w, items[at])
173 m.ov.cardOf, m.ov.card = nil, 0
174 return true
175}
176
177// hiddenColumn resolves the provisional selection by window, not column
178// index. With no surviving selection, prefer the most recently maximized
179// hidden column, then the nearest prior non-maximized focus, then first.
180func (m *Monitor) hiddenColumn(w *Workspace) int {
181 if !w.overviewMaximized() {
182 return -1
183 }
184 if i := w.columnOf(m.ov.selected); i >= 0 && i != w.Focus && m.ov.row == w {
185 return i
186 }
187 if m.ov.row == w && m.ov.selectedAt >= 0 {
188 i := min(m.ov.selectedAt, len(w.Columns)-1)
189 if i == w.Focus {
190 if i > 0 {
191 i--
192 } else {
193 i++
194 }
195 }
196 return i
197 }
198 for _, id := range w.maximized {
199 if i := w.columnOf(id); i >= 0 && i != w.Focus {
200 return i
201 }
202 }
203 if i := w.columnOf(m.ov.rows[m.ov.from].focus); w == m.ov.from && i >= 0 && i != w.Focus {
204 return i
205 }
206 for i := range w.Columns {
207 if i != w.Focus {
208 return i
209 }
210 }
211 return -1
212}
213
214// selectOverviewColumn retains the existing ordinary-row live focus semantics.
215func (w *Workspace) selectOverviewColumn(i int) {
216 w.floatFocus, w.stashFocus = false, false
217 if w.policy().equalCells && w.Focus != i && w.Focus < len(w.Columns) {
218 w.unmaximize()
219 }
220 w.Focus = i
221 w.scroll()
222}
223
224// apply commits the selected front card without changing other stack items.
225func (w *Workspace) apply(item stackItem, selected WindowID) {
226 switch item.kind {
227 case stackFloat:
228 w.FocusID(item.id)
229 case stackColumns:
230 i := w.columnOf(selected)
231 if w.overviewMaximized() && i == w.Focus {
232 i = -1
233 }
234 if i < 0 && len(w.Columns) > 0 {
235 i = w.Focus
236 if w.overviewMaximized() {
237 for next := range w.Columns {
238 if next != w.Focus {
239 i = next
240 break
241 }
242 }
243 }
244 c := w.Columns[i]
245 selected = c.Windows[c.Focus]
246 }
247 if selected != 0 {
248 if w.overviewMaximized() && i != w.Focus {
249 w.transferMaximization(i)
250 }
251 w.FocusID(selected)
252 }
253 case stackColumn:
254 i := w.columnOf(item.id)
255 if i < 0 {
256 return
257 }
258 if w.columnOf(selected) != i {
259 c := w.Columns[i]
260 selected = c.Windows[c.Focus]
261 }
262 if w.overviewMaximized() && i != w.Focus {
263 w.transferMaximization(i)
264 }
265 w.FocusID(selected)
266 }
267}
268
269// previewItem draws a group using screen geometry and a single card at its
270// configured size. A prior maximized column retains its full-width buffer.
271func (m *Monitor) previewItem(w *Workspace, item stackItem, axis layoutAxis, slot Rect, dim, lit bool) []Placement {
272 if item.kind == stackColumns {
273 if !w.overviewMaximized() {
274 return w.previewRow(axis, slot, dim, lit)
275 }
276 // The hidden columns fill the screen as if the maximized column,
277 // shown on its own card, were not there. In particular, an earlier
278 // maximized buffer scales into its unmaximized cell here.
279 g := w.gap()
280 rects := w.hiddenColumnRects()
281 var tiles []Placement
282 selected := m.hiddenColumn(w)
283 for i, c := range w.Columns {
284 if i == w.Focus {
285 continue
286 }
287 r := rects[i]
288 r.X -= w.Usable.X
289 r.Y -= w.Usable.Y
290 w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g)
291 for j, t := range w.rowBuf {
292 tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: i == selected && j == c.Focus})
293 }
294 }
295 sel := rects[selected]
296 sel.X -= w.Usable.X
297 return w.previewRowTiles(axis, slot, dim, lit, false, tiles, w.Usable.W, sel)
298 }
299 g := w.gap()
300 if item.kind == stackFloat && item.id == w.cover() {
301 // A fullscreen float shows at the size of a fullscreen column.
302 r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)}
303 return w.previewRowTiles(axis, slot, dim, lit, false, []Placement{{ID: item.id, Rect: r, Focused: true, Fullscreen: true}}, w.Usable.W, r)
304 }
305 if item.kind == stackFloat {
306 f := w.Floats[w.floatIndex(item.id)]
307 r := w.floatRect(f)
308 r.X -= w.Usable.X
309 r.Y -= w.Usable.Y
310 return w.previewRowTiles(axis, slot, dim, lit, false, []Placement{{ID: item.id, Rect: r, Floating: true, Focused: true}}, w.Usable.W, r)
311 }
312 i := w.columnOf(item.id)
313 if i < 0 {
314 return nil
315 }
316 c := w.Columns[i]
317 r := Rect{W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)}
318 r.X = (w.Usable.W - r.W) / 2
319 r.Y = (w.Usable.H - r.H) / 2
320 tiles := make([]Placement, 0, len(c.Windows))
321 w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g)
322 for j, t := range w.rowBuf {
323 tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus, Fullscreen: c.Windows[j] == w.cover()})
324 }
325 return w.previewRowTiles(axis, slot, dim, lit, false, tiles, w.Usable.W, r)
326}
327
328// hiddenColumnRects lays out the columns behind the maximized one with the
329// fixed layout rules, without the maximized column, so no hole is left
330// where it was. The maximized column's own entry is an empty Rect.
331func (w *Workspace) hiddenColumnRects() []Rect {
332 v := *w
333 v.Columns = slices.Delete(slices.Clone(w.Columns), w.Focus, w.Focus+1)
334 v.Focus = 0
335 return slices.Insert(v.columnRectsFor(true), w.Focus, Rect{})
336}
337
338// stackRow draws up to two cards on each side of the provisional front,
339// fanned along axis.
340func (m *Monitor) stackRow(w *Workspace, axis layoutAxis, slot Rect, dim, lit bool) []Placement {
341 first, n := w.stackHead()
342 if n == 0 {
343 return w.previewRow(axis, slot, dim, lit)
344 }
345 if n == 1 {
346 return m.previewItem(w, first, axis, slot, dim, lit)
347 }
348 items := w.stack()
349 at := 0
350 if w == m.Current() {
351 at = slices.Index(items, m.stackFront(w))
352 }
353 cards := make([][]Placement, len(items))
354 step := w.peekStep()
355 // along is the offset of d steps on the overview axis.
356 along := func(d int) (int, int) {
357 var r Rect
358 axis.offset(&r, d*step)
359 return r.X, r.Y
360 }
361 for i, item := range items {
362 cards[i] = m.previewItem(w, item, axis, w.frontSlot(axis, slot), dim, lit)
363 }
364
365 // fan's behind side advances through the linear stack.
366 return fan(cards, at, 2, 2, false,
367 func(d int) (int, int) { return along(-d) }, along, dim, lit)
368}