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 c, columns := WindowID(0), len(w.Columns) > 0
46 if w.pinned() {
47 c = w.cover()
48 }
49 switch {
50 case c == 0, w.stashIndex(c) >= 0:
51 case w.floatIndex(c) >= 0:
52 items = append(items, stackItem{stackFloat, c})
53 case w.overviewMaximized():
54 items = append(items, stackItem{stackColumn, c})
55 default:
56 // One card of all columns, its own marked fullscreen.
57 items = append(items, stackItem{kind: stackColumns})
58 columns = false
59 }
60 for i := len(w.Floats) - 1; i >= 0; i-- {
61 f := w.Floats[i]
62 if !f.below && w.coversFloat(f) {
63 items = append(items, stackItem{stackFloat, f.ID})
64 }
65 }
66 if (c == 0 || w.isFloat(c)) && w.overviewMaximized() {
67 items = append(items, stackItem{stackColumn, w.Columns[w.Focus].Windows[0]})
68 }
69 if columns {
70 items = append(items, stackItem{kind: stackColumns})
71 }
72 for i := len(w.Floats) - 1; i >= 0; i-- {
73 f := w.Floats[i]
74 if f.below && w.coversFloat(f) {
75 items = append(items, stackItem{stackFloat, f.ID})
76 }
77 }
78 return items
79}
80
81// itemOf resolves a clicked tile to the card currently containing it.
82func (w *Workspace) itemOf(id WindowID) (stackItem, bool) {
83 for _, item := range w.stack() {
84 switch item.kind {
85 case stackFloat:
86 if item.id == id {
87 return item, true
88 }
89 case stackColumn:
90 if i := w.columnOf(id); i >= 0 && i == w.columnOf(item.id) {
91 return item, true
92 }
93 case stackColumns:
94 if i := w.columnOf(id); i >= 0 && (!w.overviewMaximized() || i != w.Focus) {
95 return item, true
96 }
97 }
98 }
99 return stackItem{}, false
100}
101
102// stackFront resolves the provisional item against fresh workspace contents;
103// an invalidated selection falls back to the nearest surviving stack index.
104func (m *Monitor) stackFront(w *Workspace) stackItem {
105 items := w.stack()
106 if len(items) == 0 {
107 return stackItem{}
108 }
109 if m.ov.row == w {
110 for _, item := range items {
111 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) {
112 return item
113 }
114 }
115 return items[min(max(m.ov.frontAt, 0), len(items)-1)]
116 }
117 return items[0]
118}
119
120// setFront records a provisional card and its index for removal fallback.
121func (m *Monitor) setFront(w *Workspace, item stackItem) {
122 m.ov.row, m.ov.front = w, item
123 m.ov.frontAt = slices.Index(w.stack(), item)
124}
125
126// moveStack moves only within the current row. The caller changes workspace
127// when the linear stack has no item in that direction.
128func (m *Monitor) moveStack(d int) bool {
129 w := m.Current()
130 items := w.stack()
131 if len(items) < 2 {
132 return false
133 }
134 at := slices.Index(items, m.stackFront(w)) + d
135 if at < 0 || at >= len(items) {
136 return false
137 }
138 m.setFront(w, items[at])
139 m.ov.cardOf, m.ov.card = nil, 0
140 return true
141}
142
143// hiddenColumn resolves the provisional selection by window, not column
144// index. With no surviving selection, prefer the most recently maximized
145// hidden column, then the nearest prior non-maximized focus, then first.
146func (m *Monitor) hiddenColumn(w *Workspace) int {
147 if !w.overviewMaximized() {
148 return -1
149 }
150 if i := w.columnOf(m.ov.selected); i >= 0 && i != w.Focus && m.ov.row == w {
151 return i
152 }
153 if m.ov.row == w && m.ov.selectedAt >= 0 {
154 i := min(m.ov.selectedAt, len(w.Columns)-1)
155 if i == w.Focus {
156 if i > 0 {
157 i--
158 } else {
159 i++
160 }
161 }
162 return i
163 }
164 for _, id := range w.maximized {
165 if i := w.columnOf(id); i >= 0 && i != w.Focus {
166 return i
167 }
168 }
169 if i := w.columnOf(m.ov.rows[m.ov.from].focus); w == m.ov.from && i >= 0 && i != w.Focus {
170 return i
171 }
172 for i := range w.Columns {
173 if i != w.Focus {
174 return i
175 }
176 }
177 return -1
178}
179
180// selectOverviewColumn retains the existing ordinary-row live focus semantics.
181func (w *Workspace) selectOverviewColumn(i int) {
182 w.floatFocus, w.stashFocus = false, false
183 if w.Overflow == OverflowFixed && w.Focus != i && w.Focus < len(w.Columns) {
184 w.unmaximize()
185 }
186 w.Focus = i
187 w.scroll()
188}
189
190// apply commits the selected front card without changing other stack items.
191func (w *Workspace) apply(item stackItem, selected WindowID) {
192 switch item.kind {
193 case stackFloat:
194 w.FocusID(item.id)
195 case stackColumns:
196 i := w.columnOf(selected)
197 if w.overviewMaximized() && i == w.Focus {
198 i = -1
199 }
200 if i < 0 && len(w.Columns) > 0 {
201 i = w.Focus
202 if w.overviewMaximized() {
203 for next := range w.Columns {
204 if next != w.Focus {
205 i = next
206 break
207 }
208 }
209 }
210 c := w.Columns[i]
211 selected = c.Windows[c.Focus]
212 }
213 if selected != 0 {
214 if w.overviewMaximized() && i != w.Focus {
215 w.transferMaximization(i)
216 }
217 w.FocusID(selected)
218 }
219 case stackColumn:
220 i := w.columnOf(item.id)
221 if i < 0 {
222 return
223 }
224 if w.columnOf(selected) != i {
225 c := w.Columns[i]
226 selected = c.Windows[c.Focus]
227 }
228 if w.overviewMaximized() && i != w.Focus {
229 w.transferMaximization(i)
230 }
231 w.FocusID(selected)
232 }
233}
234
235// previewItem draws a group using screen geometry and a single card at its
236// configured size. A prior maximized column retains its full-width buffer.
237func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool) []Placement {
238 if item.kind == stackColumns {
239 if !w.overviewMaximized() {
240 return w.previewRow(y, dim, lit)
241 }
242 // The hidden columns fill the screen as if the maximized column,
243 // shown on its own card, were not there. In particular, an earlier
244 // maximized buffer scales into its unmaximized cell here.
245 g := w.gap()
246 rects := w.hiddenColumnRects()
247 var tiles []Placement
248 selected := m.hiddenColumn(w)
249 for i, c := range w.Columns {
250 if i == w.Focus {
251 continue
252 }
253 r := rects[i]
254 r.X -= w.Usable.X
255 r.Y -= w.Usable.Y
256 w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g)
257 for j, t := range w.rowBuf {
258 tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: i == selected && j == c.Focus})
259 }
260 }
261 sel := rects[selected]
262 sel.X -= w.Usable.X
263 return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, sel)
264 }
265 g := w.gap()
266 if item.kind == stackFloat && item.id == w.cover() {
267 // A fullscreen float shows at the size of a fullscreen column.
268 r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)}
269 return w.previewRowTiles(y, dim, lit, []Placement{{ID: item.id, Rect: r, Focused: true, Fullscreen: true}}, w.Usable.W, r)
270 }
271 if item.kind == stackFloat {
272 f := w.Floats[w.floatIndex(item.id)]
273 r := w.floatRect(f)
274 r.X -= w.Usable.X
275 r.Y -= w.Usable.Y
276 return w.previewRowTiles(y, dim, lit, []Placement{{ID: item.id, Rect: r, Floating: true, Focused: true}}, w.Usable.W, r)
277 }
278 i := w.columnOf(item.id)
279 if i < 0 {
280 return nil
281 }
282 c := w.Columns[i]
283 r := Rect{W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)}
284 r.X = (w.Usable.W - r.W) / 2
285 r.Y = (w.Usable.H - r.H) / 2
286 tiles := make([]Placement, 0, len(c.Windows))
287 w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g)
288 for j, t := range w.rowBuf {
289 tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus, Fullscreen: c.Windows[j] == w.cover()})
290 }
291 return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, r)
292}
293
294// hiddenColumnRects lays out the columns behind the maximized one with the
295// fixed layout rules, without the maximized column, so no hole is left
296// where it was. The maximized column's own entry is an empty Rect.
297func (w *Workspace) hiddenColumnRects() []Rect {
298 v := *w
299 v.Columns = slices.Delete(slices.Clone(w.Columns), w.Focus, w.Focus+1)
300 v.Focus = 0
301 return slices.Insert(v.columnRectsFor(true), w.Focus, Rect{})
302}
303
304// stackRow draws up to two cards on each side of the provisional front.
305func (m *Monitor) stackRow(w *Workspace, y int, dim, lit bool) []Placement {
306 items := w.stack()
307 if len(items) == 0 {
308 return w.previewRow(y, dim, lit)
309 }
310 if len(items) == 1 {
311 return m.previewItem(w, items[0], y, dim, lit)
312 }
313 at := 0
314 if w == m.Current() {
315 at = slices.Index(items, m.stackFront(w))
316 }
317 cards := make([][]Placement, len(items))
318 step := w.peekStep()
319 for i, item := range items {
320 cards[i] = m.previewItem(w, item, w.frontRowY(y), dim, lit)
321 }
322
323 // fan's behind side advances through the linear stack.
324 return fan(cards, at, 2, 2, false,
325 func(d int) (int, int) { return 0, -d * step },
326 func(d int) (int, int) { return 0, d * step }, dim, lit)
327}