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