1package core
2
3import (
4 "context"
5 "math"
6 "slices"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11// overviewState owns the provisional selection and the snapshot of each row
12// it showed. row is the workspace whose stack has a provisional front; nil
13// uses its real front.
14type overviewState struct {
15 open bool
16 from *Workspace
17 back *Workspace
18 rows map[*Workspace]rowSnapshot
19 row *Workspace
20 front stackItem
21 frontAt int
22 cardOf *Workspace
23 card WindowID
24 selected WindowID
25 selectedAt int
26 scrollX, scrollY float64
27 // scrolled is set once a three-finger gesture took its one step per
28 // gesture (up or down, a cascade's horizontal step, or a turn of the
29 // overview): it waits for the fingers to lift. sideways is set
30 // once it stepped to a column: it stays on columns and steps once per
31 // overviewScrollRepeat.
32 scrolled, sideways bool
33}
34
35// dropScroll forgets the scroll an overview axis accumulated, which belongs
36// to the old axis once it turns. The finger latch (scrolled) stays: it waits
37// for the fingers to lift.
38func (o *overviewState) dropScroll() { o.scrollX, o.scrollY, o.sideways = 0, 0, false }
39
40// rowSnapshot is a row as the overview first showed it: browsing moves its
41// focus and scroll, closing puts them back.
42type rowSnapshot struct {
43 focus WindowID
44 floats []Float
45 fullWidth WindowID
46 // FullWidth history is restored when active binds changed it.
47 maximized []WindowID
48}
49
50// The overview scales workspace rows without resizing clients. Covering
51// floats and the columns form a linear stack of cards; small floats stay
52// hidden. Moves along the workspace axis traverse the stack before changing
53// workspace. A pinned fullscreen window is the front card of its row, the
54// windows it hides behind it; choosing one of them leaves fullscreen.
55
56// A workspace's stash shows as a pile of cards, folded or unfolded. Folded
57// (not selected), the stash's own selection is in front at
58// overviewCardZoom, up to overviewCards-1 others behind it, each
59// overviewCardStep further up and right, dimmed. Selected, the pile
60// unfolds along the overview axis into a carousel centred on the selection
61// (unfold), framed, and the moves along that axis walk it round. A vertical
62// overview pins the pile to the left edge of its row, which takes the width
63// the pile leaves. A horizontal one centres it on its row, in a band
64// reserved under the selected lane (pileBand): the lane and the band are
65// centred together.
66const (
67 overviewCardZoom = 0.2
68 overviewCards = 4
69 overviewCardStep = 8
70 overviewPeekStep = 0.07
71)
72
73// overviewMaxZoom caps the current row so its neighbors show beside it
74// along the workspace axis; overviewMinZoom floors the shrink of wide
75// workspaces, which then scroll to keep the selected column in view.
76const overviewMaxZoom, overviewMinZoom = 0.6, 0.25
77
78// ToggleOverview opens the overview, or closes it on the selection. Over
79// a covering fullscreen window (a game) too: its row shows it, composed
80// rather than scanned out while the overview is open.
81func (m *Monitor) ToggleOverview() {
82 if m.ov.open {
83 m.closeOverview()
84 return
85 }
86 w := m.Current()
87 // A slide in progress lands at once: the overview lays out the
88 // settled state.
89 m.stopSwitch()
90 m.each(func(w *Workspace) { w.view.stop(); w.stashView.stop() })
91 m.ov.open, m.ov.from, m.ov.back = true, w, m.back
92 m.ov.scrollX, m.ov.scrollY, m.ov.scrolled, m.ov.sideways = 0, 0, false, false
93 m.overviewOpens++
94 m.selectRow()
95}
96
97// remember snapshots w the first time the overview shows it.
98func (m *Monitor) remember(w *Workspace) {
99 if _, ok := m.ov.rows[w]; ok {
100 return
101 }
102 if m.ov.rows == nil {
103 m.ov.rows = make(map[*Workspace]rowSnapshot)
104 }
105 s := rowSnapshot{
106 floats: append([]Float(nil), w.Floats...),
107 maximized: slices.Clone(w.maximized),
108 }
109 s.focus, _ = w.Focused()
110 if len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth {
111 c := w.Columns[w.Focus]
112 s.fullWidth = c.Windows[c.Focus]
113 }
114 m.ov.rows[w] = s
115}
116
117// selectRow resets provisional stack and stash selection for a new row.
118func (m *Monitor) selectRow() {
119 m.remember(m.Current())
120 m.clearCard()
121 m.ov.front, m.ov.row, m.ov.frontAt = stackItem{}, nil, 0
122 m.ov.selected, m.ov.selectedAt = 0, -1
123 w := m.Current()
124 // A fullscreen window has the focus: its card is in front.
125 w.focusCover()
126 if w.floatFocus && len(w.Columns) > 0 && (len(w.stack()) < 2 || m.stackFront(w).kind == stackColumns) {
127 w.floatFocus = false
128 }
129 if len(w.Stash) > 0 && (len(w.stack()) == 0 || w.stashFocused() && !w.floatFocus) {
130 m.selectCard(w, w.stashAt)
131 }
132}
133
134// selectCard selects stash entry i of w.
135func (m *Monitor) selectCard(w *Workspace, i int) {
136 m.ov.card, m.ov.cardOf = w.Stash[i].ID, w
137}
138
139// card is the selected stash card, 0 when there is none: the overview is
140// closed, the selection is on a column, or its window left the stash on
141// screen (unmapped, unstashed, or another workspace shown by a bind).
142func (m *Monitor) card() WindowID {
143 w := m.ov.cardOf
144 if !m.ov.open || w == nil || w != m.Current() || w.stashIndex(m.ov.card) < 0 {
145 return 0
146 }
147 return m.ov.card
148}
149
150// clearCard deselects the stash card.
151func (m *Monitor) clearCard() { m.ov.card, m.ov.cardOf = 0, nil }
152
153// cardAt is the pile entry of w that is selected, -1 when none.
154func (m *Monitor) cardAt(w *Workspace) int {
155 if id := m.card(); id != 0 && w == m.ov.cardOf {
156 return w.stashIndex(id)
157 }
158 return -1
159}
160
161// overviewTarget resolves the active preview to one window for validation
162// and window actions. Presentation state never falls back to a covering float.
163func (m *Monitor) overviewTarget() WindowID {
164 if id := m.card(); id != 0 {
165 return id
166 }
167 w := m.Current()
168 item := m.stackFront(w)
169 switch item.kind {
170 case stackFloat:
171 return item.id
172 case stackColumn:
173 i := w.columnOf(item.id)
174 if i >= 0 {
175 c := w.Columns[i]
176 return c.Windows[c.Focus]
177 }
178 case stackColumns:
179 i := w.Focus
180 if len(w.Columns) == 0 {
181 return 0
182 }
183 if w.overviewMaximized() {
184 i = w.columnOf(m.ov.selected)
185 if i < 0 || i == w.Focus {
186 i = m.hiddenColumn(w)
187 }
188 }
189 if i >= 0 {
190 c := w.Columns[i]
191 return c.Windows[c.Focus]
192 }
193 }
194 return 0
195}
196
197// overviewBlocks makes window mutations explicit: they are disabled while
198// selection is provisional. Workspace and monitor navigation and moves to
199// another workspace remain active.
200func overviewBlocks(a Action) bool {
201 if _, _, ok := ResizeArg(a); ok {
202 return true
203 }
204 switch a {
205 case ActionMoveColumnLeft, ActionMoveColumnRight, ActionCycleColumnWidth,
206 ActionMaximizeColumn, ActionToggleFullscreen, ActionToggleWindowStash,
207 ActionToggleStashVisible, ActionConsumeOrExpelLeft,
208 ActionConsumeOrExpelRight, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight,
209 ActionMoveWorkspaceToMonitorUp, ActionMoveWorkspaceToMonitorDown,
210 ActionMoveWindowUp, ActionMoveWindowDown, ActionMoveWorkspacePrev,
211 ActionMoveWorkspaceNext, ActionToggleFloating:
212 return true
213 }
214 return false
215}
216
217// overviewMoveTo moves the selected preview at once, like close-window: the
218// overview stays open and closing it does not undo the move. Nothing moves
219// without a selected preview, from a stash card, or when the bind has
220// nowhere to go.
221func (m *Monitor) overviewMoveTo(a Action) {
222 cur, id := m.Current(), m.overviewTarget()
223 i, column, ok := m.moveDest(a)
224 if m.card() != 0 || id == 0 || !ok || m.Workspaces[i] == cur {
225 return
226 }
227 if cur.pinned() && id != cur.cover() {
228 // Focused reports the covering window while it covers.
229 cur.leaveFullscreen()
230 }
231 cur.FocusID(id)
232 m.MoveToWorkspace(i, column)
233 // The row that received the window keeps it focused on close.
234 if to, _ := m.find(id); to != nil && to != cur {
235 delete(m.ov.rows, to)
236 }
237 m.selectRow()
238}
239
240// closeOverview closes the overview on the selection: a card shows the
241// stash on it. A fullscreen request another stashed window made while
242// the stash was hidden is dropped: the picked card stays in front. A
243// window a pinned fullscreen window hides leaves that fullscreen.
244func (m *Monitor) closeOverview() {
245 w := m.Current()
246 front, target := m.stackFront(w), m.overviewTarget()
247 if w.pinned() && target != w.cover() {
248 w.leaveFullscreen()
249 }
250 if i := m.cardAt(w); i >= 0 {
251 if w.hiddenFullscreen != w.Stash[i].ID {
252 w.hiddenFullscreen = 0
253 }
254 w.stashAt = i
255 w.showStash()
256 } else if front != (stackItem{}) {
257 w.apply(front, target)
258 }
259 for r, s := range m.ov.rows {
260 if r != w && m.has(r) {
261 restoreRow(r, s)
262 }
263 }
264 m.ov = overviewState{}
265}
266
267// CancelOverview closes the overview and returns to the workspace and
268// window it opened on, if they are still there. Every row it showed gets
269// its focus back.
270func (m *Monitor) CancelOverview() {
271 if from := m.ov.from; from != nil && m.has(from) {
272 m.show(from)
273 }
274 for r, s := range m.ov.rows {
275 if m.has(r) {
276 restoreRow(r, s)
277 }
278 }
279 m.back = m.ov.back
280 if m.back != nil && !m.has(m.back) {
281 m.back = nil
282 }
283 m.ov = overviewState{}
284}
285
286// restoreRow puts w back as snapshot s found it.
287func restoreRow(w *Workspace, s rowSnapshot) {
288 if s.focus != 0 {
289 w.FocusID(s.focus)
290 } else {
291 w.floatFocus, w.stashFocus = false, false
292 }
293 // FocusID on a float raises it; restore the initial order and below
294 // flags for floats still present, leaving new floats in their order.
295 current := w.Floats
296 w.Floats = nil
297 for _, f := range s.floats {
298 for i, now := range current {
299 if now.ID == f.ID {
300 now.below = f.below
301 w.Floats = append(w.Floats, now)
302 current = append(current[:i], current[i+1:]...)
303 break
304 }
305 }
306 }
307 w.Floats = append(w.Floats, current...)
308 // Fixed overflow maximizes at most one column: only the one holding
309 // the anchor window, wherever active binds moved it. Scroll overflow
310 // keeps each column's own width.
311 if w.policy().maximizeHides() {
312 w.unmaximize()
313 for i := range w.Columns {
314 if s.fullWidth != 0 && slices.Contains(w.Columns[i].Windows, s.fullWidth) {
315 w.maximize(i)
316 break
317 }
318 }
319 }
320 // Windows that left the row since leave its maximize history too.
321 w.maximized = slices.DeleteFunc(slices.Clone(s.maximized), func(id WindowID) bool { return !w.has(id) })
322 w.scroll()
323}
324
325// overviewWorkspaces inserts named rows below their invocation workspace,
326// in configured order within each group. Unattached rows follow the last
327// occupied numbered row (or the first). Empty invocation anchors stay visible;
328// other empty rows are skipped unless current.
329//
330// The result is the monitor's scratch, rebuilt by the next call: callers
331// read it and drop it before calling again (Clone it to keep it). It keeps
332// the last result's workspaces reachable until then.
333func (m *Monitor) overviewWorkspaces() []*Workspace {
334 fallback := m.Workspaces[0]
335 for _, w := range m.Workspaces {
336 if !w.empty() {
337 fallback = w
338 }
339 }
340 rank := func(w *Workspace) int {
341 for i, spec := range m.named {
342 if spec.Name == w.Name {
343 return i
344 }
345 }
346 return len(m.named)
347 }
348 cur := m.Current()
349 named := append(m.ovNamed[:0], m.hidden...)
350 named = slices.DeleteFunc(named, func(n *Workspace) bool { return n.empty() && n != cur })
351 m.ovNamed = named
352 slices.SortStableFunc(named, func(a, b *Workspace) int { return rank(a) - rank(b) })
353 rows := m.ovList[:0]
354 for _, w := range m.Workspaces {
355 // The row is kept when occupied, current or anchoring a named
356 // row: placed first, dropped again if none of those holds.
357 at := len(rows)
358 rows = append(rows, w)
359 for _, n := range named {
360 anchor := n.overviewAfter
361 if indexOf(m.Workspaces, anchor) < 0 {
362 anchor = fallback
363 }
364 if anchor == w {
365 rows = append(rows, n)
366 }
367 }
368 if len(rows) == at+1 && w.empty() && w != cur {
369 rows = rows[:at]
370 }
371 }
372 // named is only needed here; the rows' tail is cleared so the scratch
373 // holds no more than the last result.
374 clear(named[:cap(named)])
375 clear(rows[len(rows):cap(rows)])
376 m.ovList = rows
377 return rows
378}
379
380// showOverview changes rows without committing the provisional stack. Entering
381// a named group uses its invocation anchor as the toggle return target, not
382// rows merely browsed in the overview.
383func (m *Monitor) showOverview(w *Workspace) {
384 if w.policy().workspace != m.overviewAxis() {
385 m.ov.dropScroll()
386 }
387 if w != m.Current() && m.isHidden(w) && !m.isHidden(m.Current()) {
388 m.back = m.Current()
389 if indexOf(m.Workspaces, w.overviewAfter) >= 0 {
390 m.back = w.overviewAfter
391 }
392 }
393 m.show(w)
394 m.selectRow()
395}
396
397// OverviewMove shares navigation rules across keys, scrolls and swipes. It
398// is the physical entry point: it maps dx and dy to the overview's moves.
399func (m *Monitor) OverviewMove(dx, dy int) {
400 if m.overviewAxis() == horizontalAxis {
401 if dy != 0 {
402 m.overviewLane(dy)
403 } else if dx != 0 {
404 m.overviewAcross(dx)
405 }
406 return
407 }
408 // Workspaces stack vertically: vertical moves stay in a selected stash
409 // pile; left stays, and right deselects it.
410 if dy != 0 {
411 m.overviewVertical(dy)
412 } else if dx != 0 {
413 m.overviewHorizontal(dx)
414 }
415}
416
417// overviewWorkspaceStep is a focus-workspace bind. A vertical overview
418// walks the stack cards before changing workspace (overviewVertical); from
419// a selected stash card, which the move keys walk, it changes workspace
420// directly. A side-by-side one always changes workspace, since its stack
421// and unfolded stash run left and right.
422func (m *Monitor) overviewWorkspaceStep(d int) {
423 if m.overviewAxis() == verticalAxis && m.cardAt(m.Current()) < 0 {
424 m.overviewVertical(d)
425 return
426 }
427 m.overviewStep(d)
428}
429
430// cycleCard selects the stash card d entries from the selected one of w in
431// the unfolded pile, wrapping round (wrapsCards), else staying at either
432// end. It reports whether a card was selected.
433func (m *Monitor) cycleCard(w *Workspace, d int) bool {
434 at := m.cardAt(w)
435 if at < 0 {
436 return false
437 }
438 m.stepCard(w, at, d)
439 return true
440}
441
442// stepCard selects the stash card d entries from card at of w, wrapping
443// round when the carousel does (wrapsCards). It reports false, selecting
444// nothing, past an end of one that does not.
445func (m *Monitor) stepCard(w *Workspace, at, d int) bool {
446 n, next := len(w.Stash), at+d
447 if next < 0 || next >= n {
448 if !w.wrapsCards() {
449 return false
450 }
451 next = (next%n + n) % n
452 }
453 m.selectCard(w, next)
454 return true
455}
456
457// wrapsCards reports whether the unfolded stash of w wraps round, drawn
458// (unfold) and walked (stepCard). A carousel of fewer than three cards does
459// not, so that each side has the card its move selects; nor does the one
460// of a stash-only workspace, which has nothing beside its pile to leave it
461// for. Past the ends of one that does not, a move goes on as from a lane
462// (cascade) or stops (a vertical overview).
463func (w *Workspace) wrapsCards() bool {
464 _, k := w.stackHead()
465 return k > 0 && len(w.Stash) >= 3
466}
467
468// overviewLane is the vertical move of an overview whose workspaces sit side
469// by side (cascade). From the unfolded stash, up leaves it for the last lane
470// (leavePile) and down stays: the stash is the bottom. Else the selection
471// goes to the previous or next lane (band), at the closest column, stopping
472// at the first. Down past the last lane, or from a card that is not the
473// columns, enters the stash, which sits below. Covering floats stack along
474// the horizontal axis, not this one.
475func (m *Monitor) overviewLane(dy int) {
476 w := m.Current()
477 if m.cardAt(w) >= 0 {
478 if dy < 0 {
479 m.leavePile(w)
480 }
481 return
482 }
483 if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 {
484 if i := w.laneNeighbor(dy); i >= 0 {
485 m.selectLaneColumn(w, i)
486 return
487 }
488 }
489 if dy > 0 && len(w.Stash) > 0 {
490 m.selectCard(w, w.stashAt)
491 }
492}
493
494// leavePile moves the selection from a stash card back up to where the pile
495// sits below: the last lane, at the column the pile was entered from (the
496// focused column's offset in its lane, or the lane's last column when it is
497// shorter). With a covering float in front it only deselects the card; a
498// stash-only workspace has nothing above the pile, so the card stays.
499func (m *Monitor) leavePile(w *Workspace) {
500 if len(w.stack()) == 0 {
501 return
502 }
503 front := m.stackFront(w)
504 m.clearCard()
505 m.ov.selected = 0
506 if front.kind != stackColumns || len(w.Columns) == 0 {
507 return
508 }
509 first, _ := w.laneBounds(w.laneOf(w.Focus))
510 nfirst, nlast := w.laneBounds(w.laneOf(len(w.Columns) - 1))
511 m.selectLaneColumn(w, min(nfirst+w.Focus-first, nlast))
512}
513
514// selectLaneColumn selects column i of w.
515func (m *Monitor) selectLaneColumn(w *Workspace, i int) {
516 w.selectOverviewColumn(i)
517 m.ov.selected = w.Columns[i].Windows[w.Columns[i].Focus]
518 m.ov.selectedAt = i
519}
520
521// overviewAcross is the horizontal move of an overview whose workspaces sit
522// side by side (cascade): a column inside the selected lane, or a card of
523// the unfolded stash, which wraps round (stepCard). Past the first column
524// or a card that does not wrap, it goes to the previous workspace; past
525// the last, to the next workspace. Covering floats fan along this axis and
526// are traversed before a workspace: the card on the left is the one behind.
527func (m *Monitor) overviewAcross(dx int) {
528 w := m.Current()
529 if at := m.cardAt(w); at >= 0 {
530 if !m.stepCard(w, at, dx) {
531 m.overviewStep(dx)
532 }
533 return
534 }
535 if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 {
536 m.laneEdge(dx)
537 return
538 }
539 first, last := w.laneBounds(w.laneOf(w.Focus))
540 if next := w.Focus + dx; next >= first && next <= last {
541 m.selectLaneColumn(w, next)
542 return
543 }
544 m.laneEdge(dx)
545}
546
547// laneEdge is a horizontal move off the edge of the selected lane, or off a
548// card that is not the columns: the stack first, then a workspace.
549func (m *Monitor) laneEdge(dx int) {
550 if m.moveStack(-dx) {
551 return
552 }
553 m.overviewStep(dx)
554}
555
556// overviewVertical is the vertical move of an overview whose workspaces
557// stack vertically: through a selected stash pile, then the stack, then
558// to the next workspace.
559func (m *Monitor) overviewVertical(dy int) {
560 if m.cycleCard(m.Current(), dy) {
561 return
562 }
563 if m.moveStack(-dy) {
564 return
565 }
566 m.overviewStep(dy)
567}
568
569// overviewStep shows the workspace d rows from the current one; it stays
570// on the first or last row. The row selects its stack even when its stash
571// has the focus, if the stash wraps round (wrapsCards): the moves that
572// stepped there would only walk the carousel, never reaching the next row.
573func (m *Monitor) overviewStep(d int) {
574 rows := m.overviewWorkspaces()
575 i := indexOf(rows, m.Current()) + d
576 if i >= 0 && i < len(rows) {
577 m.showOverview(rows[i])
578 if w := m.Current(); m.cardAt(w) >= 0 && w.wrapsCards() {
579 m.clearCard()
580 }
581 }
582}
583
584// overviewHorizontal is the horizontal move of an overview whose workspaces
585// stack vertically: it never changes workspace.
586func (m *Monitor) overviewHorizontal(dx int) {
587 w := m.Current()
588 if m.cardAt(w) >= 0 {
589 if dx > 0 {
590 m.clearCard()
591 m.ov.selected = 0
592 item := m.stackFront(w)
593 if item.kind == stackColumns {
594 if w.overviewMaximized() {
595 i := m.hiddenColumn(w)
596 m.ov.selected, m.ov.selectedAt = w.Columns[i].Windows[w.Columns[i].Focus], i
597 } else {
598 w.selectOverviewColumn(0)
599 m.ov.selected = w.Columns[0].Windows[w.Columns[0].Focus]
600 }
601 }
602 }
603 return
604 }
605 front := m.stackFront(w)
606 if front.kind != stackColumns {
607 if dx < 0 && m.moveStack(1) {
608 return
609 }
610 if dx < 0 && len(w.Stash) > 0 {
611 m.selectCard(w, w.stashAt)
612 }
613 return
614 }
615 if len(w.Columns) == 0 {
616 return
617 }
618 i := w.columnOf(m.ov.selected)
619 if i < 0 || w.overviewMaximized() && i == w.Focus {
620 if w.overviewMaximized() {
621 i = m.hiddenColumn(w)
622 } else {
623 i = w.Focus
624 }
625 }
626 first := 0
627 if w.overviewMaximized() && w.Focus == 0 {
628 first = 1
629 }
630 if dx < 0 && i == first && len(w.Stash) > 0 {
631 m.selectCard(w, w.stashAt)
632 return
633 }
634 next := min(max(i+dx, 0), len(w.Columns)-1)
635 if w.overviewMaximized() && next == w.Focus {
636 next = min(max(next+dx, 0), len(w.Columns)-1)
637 if next == w.Focus {
638 return
639 }
640 }
641 m.ov.selected = w.Columns[next].Windows[w.Columns[next].Focus]
642 m.ov.selectedAt = next
643 if !w.overviewMaximized() {
644 // Ordinary group navigation retains its existing scroll-to-selection.
645 w.selectOverviewColumn(next)
646 }
647}
648
649// OverviewPick closes the overview on the clicked window.
650func (m *Monitor) OverviewPick(id WindowID) {
651 w, _ := m.find(id)
652 if w == nil {
653 return
654 }
655 if w.floatIndex(id) >= 0 && w.stashIndex(id) < 0 {
656 i := w.floatIndex(id)
657 if !w.coversFloat(w.Floats[i]) && !(id == w.cover() && w.pinned()) {
658 return
659 }
660 }
661 m.showOverview(w)
662 m.clearCard()
663 if i := w.stashIndex(id); i >= 0 {
664 m.selectCard(w, i)
665 } else if item, ok := w.itemOf(id); ok {
666 m.setFront(w, item)
667 if i := w.columnOf(id); i >= 0 {
668 // The clicked window first: a maximization moving to its
669 // column records it in the maximize history.
670 for j, v := range w.Columns[i].Windows {
671 if v == id {
672 w.Columns[i].Focus = j
673 break
674 }
675 }
676 if item.kind == stackColumns && !w.overviewMaximized() {
677 w.selectOverviewColumn(i)
678 }
679 m.ov.selected = id
680 }
681 }
682 m.closeOverview()
683}
684
685// overviewAxis is the axis the overview lays workspaces out along, and the
686// stacked cards of a row fan along: the navigation axis of the current
687// workspace's layout.
688func (m *Monitor) overviewAxis() layoutAxis { return m.Current().policy().workspace }
689
690// overviewSlots are the slots of the overview rows: the current workspace
691// and at most one neighbor on each side, in a fixed array (no allocation).
692type overviewSlots struct {
693 n int
694 w [3]*Workspace
695 r [3]Rect
696}
697
698func (s *overviewSlots) add(w *Workspace, r Rect) {
699 s.w[s.n], s.r[s.n] = w, r
700 s.n++
701}
702
703// lookup is the slot of w, if it has one.
704func (s *overviewSlots) lookup(w *Workspace) (Rect, bool) {
705 for i := range s.n {
706 if s.w[i] == w {
707 return s.r[i], true
708 }
709 }
710 return Rect{}, false
711}
712
713// overviewRows places the current workspace and its overview neighbors in
714// slots, in output-local pixels, along the overview axis; each workspace
715// places its previews in its slot. The numbered/named boundary gets extra
716// spacing and an inactive rule across the axis. The dividers are the
717// monitor's scratch, rewritten by the next call: callers copy them.
718//
719// Vertical axis: full-width slots, the current one centred; the rule is
720// horizontal. Horizontal axis: full-height slots as wide as their content,
721// stack peeks included (the stash pile takes a band of the slot), the current one
722// centred; the rule is vertical, in the spacing between two slots.
723func (m *Monitor) overviewRows() (overviewSlots, []ports.Separator) {
724 cur := m.Current()
725 u := cur.overviewArea()
726 gap := max(1, u.H*3/100)
727 axis := m.overviewAxis()
728 var cs Rect
729 if axis == horizontalAxis {
730 row := cur.rowWidth(axis)
731 cs = Rect{X: u.X + (u.W-row)/2, Y: u.Y, W: row, H: u.H}
732 } else {
733 hc := cur.rowHeight(axis)
734 cs = Rect{X: u.X, Y: u.Y + (u.H-hc)/2, W: u.W, H: hc}
735 }
736 var rows overviewSlots
737 rows.add(cur, cs)
738 dividers := 0
739 list := m.overviewWorkspaces()
740 i := indexOf(list, cur)
741 for _, next := range [2]int{i - 1, i + 1} {
742 if next < 0 || next >= len(list) {
743 continue
744 }
745 w := list[next]
746 spacing := gap
747 boundary := m.isHidden(w) != m.isHidden(cur)
748 if boundary {
749 spacing *= 2
750 }
751 slot, gapAt := neighborSlot(axis, u, cs, w, next < i, spacing)
752 rows.add(w, slot)
753 if boundary {
754 m.ovDividers[dividers] = overviewDivider(axis, u, gapAt+spacing/2)
755 dividers++
756 }
757 }
758 if dividers == 0 {
759 return rows, nil
760 }
761 return rows, m.ovDividers[:dividers]
762}
763
764// neighborSlot is the slot of w, the neighbor before or after the current
765// slot cs along axis, spacing away; gapAt is where the spacing begins.
766func neighborSlot(axis layoutAxis, u, cs Rect, w *Workspace, before bool, spacing int) (slot Rect, gapAt int) {
767 if axis == horizontalAxis {
768 slot = Rect{Y: u.Y, W: w.rowWidth(axis), H: u.H}
769 if before {
770 gapAt = cs.X - spacing
771 slot.X = gapAt - slot.W
772 } else {
773 gapAt = cs.X + cs.W
774 slot.X = gapAt + spacing
775 }
776 return slot, gapAt
777 }
778 h := w.rowHeight(axis)
779 slot = Rect{X: u.X, W: u.W, H: h}
780 if before {
781 gapAt = cs.Y - spacing
782 slot.Y = gapAt - h
783 } else {
784 gapAt = cs.Y + cs.H
785 slot.Y = gapAt + spacing
786 }
787 return slot, gapAt
788}
789
790// overviewDivider is the inactive rule across axis at pos, inset from the
791// edges of u.
792func overviewDivider(axis layoutAxis, u Rect, pos int) ports.Separator {
793 if axis == horizontalAxis {
794 return ports.Separator{Rect: Rect{X: pos, Y: u.Y + u.H*4/100, W: 1, H: u.H * 92 / 100}}
795 }
796 margin := u.W * 4 / 100
797 return ports.Separator{Rect: Rect{X: u.X + margin, Y: pos, W: u.W - 2*margin, H: 1}}
798}
799
800// overviewLayout places the current workspace and its overview neighbors,
801// each with its stash pile and covering-float stack. Other rows
802// stay hidden; numbered and named workspaces share the same preview geometry.
803func (m *Monitor) overviewLayout(result []Placement) []Placement {
804 cur := m.Current()
805 rows, _ := m.overviewRows()
806 axis := m.overviewAxis()
807 // The unfolded stash goes last: its strip may run over the neighbor
808 // rows, which must not cover it.
809 var unfolded *Workspace
810 var unfoldedSlot Rect
811 for w := range m.all() {
812 slot, shown := rows.lookup(w)
813 if shown {
814 // The pile band goes under the row: the row is centred
815 // with it.
816 slot.H -= w.pileBand(axis)
817 at := m.cardAt(w)
818 result = append(result, m.stackRow(w, axis, slot, w != cur, at < 0)...)
819 if at >= 0 {
820 unfolded, unfoldedSlot = w, slot
821 } else {
822 result = w.pile(result, axis, slot, w != cur, -1)
823 }
824 }
825 for _, f := range w.Floats {
826 if _, drawn := w.itemOf(f.ID); !shown || !drawn {
827 result = append(result, Placement{ID: f.ID, Hidden: true})
828 }
829 }
830 if !shown {
831 // Columns and stash only: the floats were listed above.
832 result = w.appendHidden(result, false)
833 }
834 }
835 if unfolded != nil {
836 result = unfolded.pile(result, axis, unfoldedSlot, false, m.cardAt(unfolded))
837 }
838 return result
839}
840
841// fan places front and neighboring cards, farthest visible first and front
842// last. A card may contain several tiles; omitted cards stay in the layout
843// hidden so their clients are not left showing on screen.
844func fan(cards [][]Placement, front, maxBehind, maxBefore int,
845 behind, before func(int) (int, int), dim, lit bool) []Placement {
846 n := len(cards)
847 if n == 0 {
848 return nil
849 }
850 front = min(max(front, 0), n-1)
851 out := make([]Placement, 0, n)
852 add := func(i, distance int, offset func(int) (int, int), peek bool) {
853 dx, dy := offset(distance)
854 for _, p := range cards[i] {
855 if !p.Hidden {
856 p.Rect.X += dx
857 p.Rect.Y += dy
858 if p.Peek || peek || dim {
859 p.peeking(1)
860 }
861 p.Focused = p.Focused && !peek && !dim && lit
862 }
863 out = append(out, p)
864 }
865 }
866 shown := make([]bool, n)
867 // Behind and before are in nearest-first order; paint farthest first.
868 for d := min(maxBehind, n-1); d >= 1; d-- {
869 i := front + d
870 if i >= n {
871 continue
872 }
873 add(i, d, behind, true)
874 shown[i] = true
875 }
876 for d := min(maxBefore, n-1); d >= 1; d-- {
877 i := front - d
878 if i < 0 || shown[i] {
879 continue
880 }
881 add(i, d, before, true)
882 shown[i] = true
883 }
884 add(front, 0, behind, false)
885 shown[front] = true
886 for i, card := range cards {
887 if shown[i] {
888 continue
889 }
890 for _, p := range card {
891 p.Hidden = true
892 p.Preview = 0
893 p.Focused = false
894 out = append(out, p)
895 }
896 }
897 return out
898}
899
900// pileWidth is the room the stash pile of w takes on the left of its row in
901// a vertical overview, its margin included; 0 without a stash.
902func (w *Workspace) pileWidth() int {
903 if len(w.Stash) == 0 {
904 return 0
905 }
906 card, _ := w.cardSize()
907 // Never more than a third of the width: the columns keep the rest.
908 a := w.overviewArea()
909 return min(card+(min(len(w.Stash), overviewCards)-1)*w.cardStep()+a.W*2/100, a.W/3)
910}
911
912// cardSize is the size of a stash card in the overview.
913func (w *Workspace) cardSize() (int, int) {
914 r := w.stashRect()
915 return int(math.Round(float64(r.W) * overviewCardZoom)), int(math.Round(float64(r.H) * overviewCardZoom))
916}
917
918// pileBand is the height a horizontal overview gives the stash of w under
919// its selected lane: the spacing, then the folded pile with its cards
920// behind. 0 without a stash, or in a vertical overview, where the pile
921// sits left of the row. The lane and the band are centred together.
922func (w *Workspace) pileBand(axis layoutAxis) int {
923 if axis != horizontalAxis || len(w.Stash) == 0 {
924 return 0
925 }
926 _, ch := w.cardSize()
927 return max(1, w.overviewArea().H*3/100) + ch + (min(len(w.Stash), overviewCards)-1)*w.cardStep()
928}
929
930// cardStep is the offset between stacked cards, smaller on tiny outputs.
931func (w *Workspace) cardStep() int {
932 a := w.overviewArea()
933 return min(overviewCardStep, a.W/100, a.H/50)
934}
935
936// pile appends the stash of w to dst as cards in its slot along axis (see
937// the overview's stash comment). With entry at selected (at >= 0, on the
938// current row: dim false) the pile unfolds; else it is folded: the stash
939// selection in front, painted last, up to overviewCards-1 next ones
940// (wrapping round) behind it, farthest first, dimmed; the rest hidden.
941func (w *Workspace) pile(dst []Placement, axis layoutAxis, slot Rect, dim bool, at int) []Placement {
942 n := len(w.Stash)
943 if n == 0 {
944 return dst
945 }
946 u := w.overviewArea()
947 cw, ch := w.cardSize()
948 rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom(axis)))
949 var x0, y0 int
950 if axis == horizontalAxis {
951 // Centred on the row, under the selected lane: slot is the
952 // lane's, the pile band (pileBand) taken off its height, so the
953 // band starts below the lane. The cards behind rise from the
954 // front one, which goes at the bottom of the band. Both scale
955 // with the area (the lane at most overviewMaxZoom of it), so
956 // the pile stays inside whatever its size.
957 x0 = slot.X + (slot.W-cw)/2
958 y0 = slot.Y + (slot.H-rowH)/2 + rowH + w.pileBand(axis) - ch
959 } else {
960 // Stack rows reserve two peek steps above the front card. Align
961 // the stash with that front band, not with the top of its envelope.
962 band := w.frontSlot(axis, slot)
963 x0, y0 = band.X+u.W/100, band.Y+(rowH-ch)/2
964 }
965 folded := Rect{X: x0, Y: y0, W: cw, H: ch}
966 if at >= 0 && !dim {
967 return w.unfold(dst, axis, folded, min(at, n-1))
968 }
969 front, behind, step := min(max(w.stashAt, 0), n-1), min(n, overviewCards)-1, w.cardStep()
970 card := func(i, d int) Placement {
971 p := Placement{ID: w.Stash[i].ID, Rect: folded, Preview: overviewCardZoom, Card: true}
972 p.Rect.X, p.Rect.Y = p.Rect.X+d*step, p.Rect.Y-d*step
973 if d > 0 || dim {
974 p.peeking(1)
975 }
976 return p
977 }
978 for d := behind; d >= 0; d-- {
979 dst = append(dst, card((front+d)%n, d))
980 }
981 for i := range w.Stash {
982 // The cards neither in front nor behind it: hidden, so their
983 // clients are not left showing.
984 if (i-front+n)%n > behind {
985 dst = append(dst, Placement{ID: w.Stash[i].ID, Rect: folded, Card: true, Hidden: true})
986 }
987 }
988 return dst
989}
990
991// unfold lays the stash of w out from the folded pile's card rect as a
992// carousel along axis: a row in a horizontal overview, a column in a
993// vertical one. The selected card at sits on the pile, in the middle; the
994// cards before it come on the left (above), the ones after it on the right
995// (below), in stash order, wrapping round (wrapsCards) with the oldest
996// following the newest (carouselSlot). A wider gap marks that seam. A
997// carousel that fits the overview area is shifted into it. Longer, one
998// that wraps keeps the selection in the middle; one that does not stops at
999// the area's edges, like a wide row. The cards past the area are hidden,
1000// the ones other than at dimmed. The cards are appended to dst.
1001func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int) []Placement {
1002 n := len(w.Stash)
1003 u := w.overviewArea()
1004 size, origin, lo, room := folded.W, folded.X, u.X, u.W
1005 if axis == verticalAxis {
1006 size, origin, lo, room = folded.H, folded.Y, u.Y, u.H
1007 }
1008 stride := size + 2*w.cardStep()
1009 // The extra room at the seam, on top of the stride.
1010 gap := size / 4
1011 wraps := w.wrapsCards()
1012 pos := func(s int) int {
1013 p := origin + s*stride
1014 switch {
1015 case s > 0 && at+s >= n:
1016 p += gap
1017 case s < 0 && at+s < 0:
1018 p -= gap
1019 }
1020 return p
1021 }
1022 // The slots of the ends.
1023 from, to := -at, n-1-at
1024 if wraps {
1025 from, to = -(n / 2), (n-1)/2
1026 }
1027 first, total := pos(from), pos(to)+size-pos(from)
1028 var shift int
1029 switch {
1030 case total <= room:
1031 shift = min(max(first, lo), lo+room-total) - first
1032 case !wraps:
1033 shift = min(max(first, lo+room-total), lo) - first
1034 }
1035 for i := range w.Stash {
1036 r := folded
1037 if p := pos(carouselSlot(i, at, n, wraps)) + shift; axis == verticalAxis {
1038 r.Y = p
1039 } else {
1040 r.X = p
1041 }
1042 dst = w.unfoldedCard(dst, i, r, at, u)
1043 }
1044 return dst
1045}
1046
1047// carouselSlot is the place of stash card i in a carousel of n cards
1048// centred on card at: 0 for at, negative on the left (above), positive on
1049// the right (below). Wrapping round, the left side takes the extra card of
1050// an even count; else the cards keep the stash order.
1051func carouselSlot(i, at, n int, wraps bool) int {
1052 if !wraps {
1053 return i - at
1054 }
1055 s := (i - at + n) % n
1056 if s > (n-1)/2 {
1057 s -= n
1058 }
1059 return s
1060}
1061
1062// unfoldedCard appends stash card i of an unfolded pile at r to dst: framed
1063// when it is the selected one at, dimmed otherwise, hidden out of the area u.
1064func (w *Workspace) unfoldedCard(dst []Placement, i int, r Rect, at int, u Rect) []Placement {
1065 p := Placement{ID: w.Stash[i].ID, Rect: r, Preview: overviewCardZoom, Card: true, Focused: i == at}
1066 if i != at {
1067 p.peeking(1)
1068 }
1069 if !r.Overlaps(u) {
1070 p.Hidden, p.Preview, p.Focused = true, 0, false
1071 }
1072 return append(dst, p)
1073}
1074
1075// overviewZoom is the scale that fits every column of w in the usable
1076// width, within the overview bounds, in an overview along axis: only a
1077// vertical one sets the stash pile beside the row.
1078func (w *Workspace) overviewZoom(axis layoutAxis) float64 {
1079 z, _ := w.zoomSpan(axis)
1080 return z
1081}
1082
1083// zoomSpan is overviewZoom with the unscaled span of the tiles it fits. A
1084// cascade workspace spans one screen whatever its band count.
1085func (w *Workspace) zoomSpan(axis layoutAxis) (float64, int) {
1086 // Only the span is read: the tiles go in the zoom scratch, dead on return.
1087 var span int
1088 w.zoomBuf, span, _ = w.previewTilesInto(w.zoomBuf[:0])
1089 if span <= 0 {
1090 return overviewMaxZoom, span
1091 }
1092 room := float64(w.overviewArea().W) * 0.96
1093 if axis == verticalAxis {
1094 room = float64(w.overviewArea().W-w.pileWidth()) * 0.96
1095 }
1096 z := min(max(room/float64(span), overviewMinZoom), overviewMaxZoom)
1097 if w.stacked() {
1098 // Reserve two clear peeks on either side of the front row.
1099 z = min(z, overviewMaxZoom/(1+4*overviewPeekStep))
1100 }
1101 return z, span
1102}
1103
1104// rowWidth is the width of the slot of w in a horizontal overview: what its
1105// tiles take (at most the area), plus the room for the covering floats
1106// peeking on either side of the front card.
1107func (w *Workspace) rowWidth(axis layoutAxis) int {
1108 z, span := w.zoomSpan(axis)
1109 if span <= 0 {
1110 // An empty workspace keeps the room of a screen.
1111 span = w.Usable.W
1112 }
1113 width := min(int(math.Round(float64(span)*z)), w.overviewArea().W)
1114 if w.stacked() {
1115 width += 4 * w.peekStep(axis)
1116 }
1117 return width
1118}
1119
1120// peekStep is the shared step for a stack row and its front-card inset.
1121func (w *Workspace) peekStep(axis layoutAxis) int {
1122 return max(1, int(math.Round(float64(w.Usable.H)*w.overviewZoom(axis)*overviewPeekStep)))
1123}
1124
1125// frontSlot is the part of slot the front card of w takes: slot shifted
1126// along axis past the room reserved for two cards peeking before it, and
1127// (along a horizontal axis, where the slot is as wide as its cards) without
1128// the room of the two after it. A single-card row keeps slot.
1129func (w *Workspace) frontSlot(axis layoutAxis, slot Rect) Rect {
1130 if w.stacked() {
1131 p := w.peekStep(axis)
1132 axis.offset(&slot, 2*p)
1133 if axis == horizontalAxis {
1134 slot.W -= 4 * p
1135 }
1136 }
1137 return slot
1138}
1139
1140// rowHeight includes the largest possible stack envelope (two peeks above
1141// and two below). Neighbor rows use the same measure as the current row.
1142func (w *Workspace) rowHeight(axis layoutAxis) int {
1143 h := int(math.Round(float64(w.Usable.H) * w.overviewZoom(axis)))
1144 if w.stacked() {
1145 h += 4 * w.peekStep(axis)
1146 }
1147 return h
1148}
1149
1150// previewTiles lays tiles out unscrolled, relative to the usable area's
1151// corner. Fixed overflow uses its on-screen geometry; scroll overflow uses
1152// an unscrolled row. span is the row's width; sel is the focused column.
1153func (w *Workspace) previewTiles() (tiles []Placement, span int, sel Rect) {
1154 return w.previewTilesInto(make([]Placement, 0, w.columnWindows()))
1155}
1156
1157// previewTilesInto is previewTiles appending to tiles (dst[:0] to reuse its
1158// storage: the result then aliases it). The column rects use w.colBuf, dead
1159// when it returns.
1160func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) {
1161 var span int
1162 var sel Rect
1163 if w.pinned() {
1164 c := w.cover()
1165 // The row shows the columns a pinned fullscreen window hides,
1166 // laid out as without it; its own tile stays marked fullscreen.
1167 v := *w
1168 v.fullscreen = 0
1169 tiles, span, sel = v.previewTilesInto(tiles)
1170 w.colBuf, w.rowBuf = v.colBuf, v.rowBuf
1171 for i := range tiles {
1172 tiles[i].Fullscreen = tiles[i].ID == c
1173 }
1174 return tiles, span, sel
1175 }
1176 g := w.gap()
1177 h := max(w.Usable.H-2*g, 0)
1178 var rects []Rect
1179 if w.Overflow == OverflowFixed {
1180 w.colBuf = w.columnRectsInto(w.colBuf, false)
1181 rects = w.colBuf
1182 span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile.
1183 }
1184 for i, c := range w.Columns {
1185 r := Rect{Y: g, H: h}
1186 if w.policy().wraps {
1187 // The band is the lane: it is encoded in Y, one screen apart.
1188 // A maximized column fills its band, alone, as on screen.
1189 r.X = w.cellX(i)
1190 r.Y = g + w.band(i)*w.Usable.H
1191 r.W = w.cellWidth(i)
1192 if c.FullWidth {
1193 r.X, r.W = g, max(w.Usable.W-2*g, 0)
1194 }
1195 } else if w.Overflow == OverflowFixed {
1196 r = rects[i]
1197 r.X, r.Y = r.X-w.Usable.X, r.Y-w.Usable.Y
1198 } else {
1199 r.X, r.W = w.columnX(i)-w.Usable.X, w.columnWidth(i)
1200 if w.fullscreenColumn(i) {
1201 // A fullscreen column shows at the width it had in the row.
1202 r.W = max(w.Usable.W-2*g, 0)
1203 }
1204 }
1205 if w.hiddenByMaximized(i) {
1206 // Fixed overflow shows the hidden columns on their own card
1207 // (previewItem). Their old buffers cannot fit their current
1208 // rects; show only the maximized column here.
1209 for _, id := range c.Windows {
1210 tiles = append(tiles, Placement{ID: id, Hidden: true})
1211 }
1212 continue
1213 }
1214 w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g)
1215 for j, t := range w.rowBuf {
1216 tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: i == w.Focus && j == c.Focus})
1217 }
1218 if w.Overflow != OverflowFixed && !w.policy().wraps {
1219 span = max(span, r.X+r.W+g)
1220 }
1221 if i == w.Focus {
1222 sel = r
1223 }
1224 }
1225 if w.policy().wraps && len(w.Columns) > 0 {
1226 span = w.Usable.W
1227 }
1228 return tiles, span, sel
1229}
1230
1231// previewRow scales the tiles of w into a row placed in slot, centred
1232// in the slot's width, or scrolled so the focused column shows when the
1233// row is wider (right of the stash pile in a vertical overview). Dimmed rows (neighbor
1234// workspaces) have no focus; neither has a row whose pile has it (lit
1235// false).
1236func (w *Workspace) previewRow(axis layoutAxis, slot Rect, dim, lit bool) []Placement {
1237 tiles, span, sel := w.previewTiles()
1238 return w.previewRowTiles(axis, slot, dim, lit, w.policy().wraps, tiles, span, sel)
1239}
1240
1241// previewRowTiles scales tiles, laid out unscrolled over span with sel the
1242// focused one, into slot of an overview along axis. Placement stays inside
1243// slot.
1244//
1245// With lanes set (the tiles of a cascade workspace), the tiles carry their
1246// band in Y, one Usable.H apart. The band of sel is the selected lane,
1247// centred across the axis; the other bands sit a lane away from it, dimmed,
1248// and are hidden when they leave the area. A workspace whose lanes run along
1249// the overview axis shows only its selected lane. In a horizontal overview,
1250// tiles entirely outside the area are hidden.
1251func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes bool, tiles []Placement, span int, sel Rect) []Placement {
1252 z := w.overviewZoom(axis)
1253 scale := func(v int) int { return int(math.Round(float64(v) * z)) }
1254 width := scale(span)
1255 u := w.overviewArea()
1256 top := slot.Y
1257 if axis == horizontalAxis {
1258 top = slot.Y + (slot.H-scale(w.Usable.H))/2
1259 }
1260 // Rows stay centred on the output like rows without a stash; in a
1261 // vertical overview the pile only pushes a row right when they would
1262 // overlap. A horizontal one gives the pile a band under the selected
1263 // lane (pileBand).
1264 left := 0
1265 if axis == verticalAxis {
1266 left = w.pileWidth()
1267 }
1268 x := max(slot.X+(slot.W-width)/2, slot.X+left)
1269 ux, uw := slot.X+left, slot.W-left
1270 if width > uw {
1271 x = ux + uw/2 - scale(sel.X+sel.W/2)
1272 x = min(max(x, ux+uw-width), ux)
1273 }
1274 stride := w.policy().content.span(w.Usable)
1275 lanes = lanes && stride > 0
1276 // Lanes along the overview axis would overlap the neighbor workspaces.
1277 otherLanes := w.policy().content != axis
1278 laneStep := scale(stride) + max(1, u.H*3/100)
1279 // The lanes below the selected one leave room for the stash pile.
1280 below := w.pileBand(axis)
1281 selLane := 0
1282 if lanes {
1283 selLane = max(sel.Y, 0) / stride
1284 }
1285 for i := range tiles {
1286 if tiles[i].Hidden {
1287 continue
1288 }
1289 r := tiles[i].Rect
1290 lane, ly := 0, r.Y
1291 if lanes {
1292 l := r.Y / stride
1293 lane, ly = l-selLane, r.Y-l*stride
1294 }
1295 x0, y0 := scale(r.X), scale(ly)
1296 y := top + y0 + lane*laneStep
1297 if lanes && lane > 0 {
1298 y += below
1299 }
1300 tiles[i].Rect = Rect{X: x + x0, Y: y, W: scale(r.X+r.W) - x0, H: scale(ly+r.H) - y0}
1301 tiles[i].Preview = z
1302 if dim || lane != 0 {
1303 tiles[i].peeking(1)
1304 }
1305 tiles[i].Focused = tiles[i].Focused && !dim && lit && lane == 0
1306 out := false
1307 if axis == horizontalAxis {
1308 // A row wider than its slot scrolls: what leaves the slot
1309 // would sit on the neighbor workspaces.
1310 out = tiles[i].Rect.X < ux || tiles[i].Rect.X+tiles[i].Rect.W > ux+uw
1311 }
1312 if lane != 0 && !otherLanes || out || (lanes || axis == horizontalAxis) && !tiles[i].Rect.Overlaps(u) {
1313 tiles[i].Hidden, tiles[i].Preview, tiles[i].Focused = true, 0, false
1314 }
1315 }
1316 return tiles
1317}
1318
1319// outlineLines is the lines of an uncut outline: one per edge.
1320const outlineLines = 4
1321
1322// overviewOutline frames the selected preview with active lines of width
1323// b, just outside it. The selection is one preview: a selected stash card
1324// wins over any other focused one, which must then not be framed. A tile's
1325// frame is cut where the cards of the pile sit over it, so it never runs
1326// across the pile; a selected card keeps its complete frame. The lines are
1327// appended to dst, which the caller sizes for them (room for outlineLines
1328// past its length) so the scene's separators are allocated once.
1329func overviewOutline(dst []ports.Separator, layout []Placement, b int) []ports.Separator {
1330 sel := -1
1331 for i, p := range layout {
1332 if p.Preview > 0 && p.Focused && !p.Hidden && (sel < 0 || p.Card && !layout[sel].Card) {
1333 sel = i
1334 }
1335 }
1336 if sel < 0 {
1337 return dst
1338 }
1339 dst = slices.Grow(dst, outlineLines)
1340 r := layout[sel].Rect
1341 edges := [4]Rect{
1342 {X: r.X - b, Y: r.Y - b, W: r.W + 2*b, H: b},
1343 {X: r.X - b, Y: r.Y + r.H, W: r.W + 2*b, H: b},
1344 {X: r.X - b, Y: r.Y, W: b, H: r.H},
1345 {X: r.X + r.W, Y: r.Y, W: b, H: r.H},
1346 }
1347 cut := false
1348 if !layout[sel].Card {
1349 for _, p := range layout {
1350 if p.Card && p.Preview > 0 && !p.Hidden {
1351 cut = true
1352 break
1353 }
1354 }
1355 }
1356 if !cut {
1357 for _, e := range edges {
1358 dst = append(dst, ports.Separator{Rect: e, Active: true})
1359 }
1360 return dst
1361 }
1362 // Stack buffers: the outline is rebuilt every overview frame.
1363 var cardBuf [overviewCards]Rect
1364 var pieceBuf [8]Rect
1365 cards := cardBuf[:0]
1366 for _, p := range layout {
1367 if p.Card && p.Preview > 0 && !p.Hidden {
1368 cards = append(cards, p.Rect)
1369 }
1370 }
1371 for _, e := range edges {
1372 for _, piece := range subtractRects(pieceBuf[:0], e, cards) {
1373 dst = append(dst, ports.Separator{Rect: piece, Active: true})
1374 }
1375 }
1376 return dst
1377}
1378
1379// subtractRects is what of r no rect of cuts covers, as rects. The edges of
1380// an outline are thin, so a cut splits one into at most two along its length
1381// (or drops it, or cuts across its thickness): the pieces are cut one rect
1382// at a time. The pieces are appended to dst; cuts that miss r allocate
1383// nothing.
1384func subtractRects(dst []Rect, r Rect, cuts []Rect) []Rect {
1385 var bufA, bufB [8]Rect
1386 pieces, spare := append(bufA[:0], r), bufB[:0]
1387 for _, c := range cuts {
1388 next := spare[:0]
1389 for _, p := range pieces {
1390 if !p.Overlaps(c) {
1391 next = append(next, p)
1392 continue
1393 }
1394 // The four bands of p around c: above, below, left, right.
1395 if c.Y > p.Y {
1396 next = append(next, Rect{X: p.X, Y: p.Y, W: p.W, H: c.Y - p.Y})
1397 }
1398 if c.Y+c.H < p.Y+p.H {
1399 next = append(next, Rect{X: p.X, Y: c.Y + c.H, W: p.W, H: p.Y + p.H - c.Y - c.H})
1400 }
1401 top, bottom := max(p.Y, c.Y), min(p.Y+p.H, c.Y+c.H)
1402 if c.X > p.X {
1403 next = append(next, Rect{X: p.X, Y: top, W: c.X - p.X, H: bottom - top})
1404 }
1405 if c.X+c.W < p.X+p.W {
1406 next = append(next, Rect{X: c.X + c.W, Y: top, W: p.X + p.W - c.X - c.W, H: bottom - top})
1407 }
1408 }
1409 // Swap the buffers: next becomes the pieces, the old pieces'
1410 // storage the next round's.
1411 pieces, spare = next, pieces
1412 }
1413 return append(dst, pieces...)
1414}
1415
1416// overviewClickFade is the fade from which a card takes no click.
1417const overviewClickFade = 0.5
1418
1419// overviewIn is the preview under the output-local point in layout, if
1420// any. Front cards take priority over peeks, including peeks appended after
1421// a front tile in another row's layout. The click path passes the shown
1422// layout, so a card is hit where it is drawn while it moves. A card that is
1423// nearly invisible (still fading in) is not clickable.
1424func overviewIn(layout []Placement, x, y float64) WindowID {
1425 for i := len(layout) - 1; i >= 0; i-- {
1426 p := layout[i]
1427 r := p.Rect
1428 if p.Preview > 0 && !p.Hidden && !p.Peek && p.Fade < overviewClickFade && x >= float64(r.X) && x < float64(r.X+r.W) && y >= float64(r.Y) && y < float64(r.Y+r.H) {
1429 return p.ID
1430 }
1431 }
1432 for i := len(layout) - 1; i >= 0; i-- {
1433 p := layout[i]
1434 r := p.Rect
1435 if p.Preview > 0 && !p.Hidden && p.Fade < overviewClickFade && x >= float64(r.X) && x < float64(r.X+r.W) && y >= float64(r.Y) && y < float64(r.Y+r.H) {
1436 return p.ID
1437 }
1438 }
1439 return 0
1440}
1441
1442// overviewHit is overviewIn with the stash piles first: the cards behind a
1443// folded pile's front one peek out where tiles may be drawn over them.
1444func (m *Monitor) overviewHit(layout []Placement, x, y float64) WindowID {
1445 var piles []Placement
1446 for _, p := range layout {
1447 if w, _ := m.find(p.ID); w != nil && w.stashIndex(p.ID) >= 0 {
1448 piles = append(piles, p)
1449 }
1450 }
1451 if id := overviewIn(piles, x, y); id != 0 {
1452 return id
1453 }
1454 return overviewIn(layout, x, y)
1455}
1456
1457// overviewClick closes the overview on the preview under the pointer, on
1458// the output it shows on. picked is false when no preview is there.
1459func (c *Core) overviewClick(ctx context.Context) (picked bool, err error) {
1460 i := c.screenAt(c.cursorX, c.cursorY)
1461 if i < 0 {
1462 return false, nil
1463 }
1464 sc := c.screens[i]
1465 if !sc.mon.ov.open {
1466 return false, nil
1467 }
1468 // The cards are hit where they are drawn: a click during the open or a
1469 // navigation animation picks what moves under the pointer.
1470 id := sc.mon.overviewHit(sc.shownLayout(), c.cursorX-float64(sc.x), c.cursorY-float64(sc.y))
1471 if id == 0 {
1472 return false, nil
1473 }
1474 now := c.now()
1475 shots := c.snapshot(now)
1476 sc.mon.OverviewPick(id)
1477 c.transition(shots, now)
1478 c.focusScreen = i
1479 c.requestFlash(flashWindow, id)
1480 c.keyboard.takeBack()
1481 if err := c.workspaceVisible(ctx, true); err != nil {
1482 return true, err
1483 }
1484 return true, c.publish(ctx)
1485}
1486
1487// overviewAction runs a bind while the overview is open: focus binds move
1488// the selection, close-window and move-to-workspace binds act on the
1489// selected preview, other mutations are dropped. handled is false for binds
1490// that run as usual.
1491func (m *Monitor) overviewAction(a Action) (e Effect, handled bool) {
1492 if !m.ov.open {
1493 return Effect{}, false
1494 }
1495 if a == ActionCloseWindow {
1496 return Effect{Close: m.overviewTarget()}, true
1497 }
1498 if movesToWorkspace(a) {
1499 m.overviewMoveTo(a)
1500 return Effect{}, true
1501 }
1502 return Effect{}, m.overviewFocus(a) || overviewBlocks(a)
1503}
1504
1505// overviewFocus runs a focus bind as an overview move, so the focus binds
1506// and the bare keys navigate the overview the same way: true when a was
1507// one of them.
1508func (m *Monitor) overviewFocus(a Action) bool {
1509 switch a {
1510 case ActionFocusColumnLeft:
1511 m.OverviewMove(-1, 0)
1512 case ActionFocusColumnRight:
1513 m.OverviewMove(1, 0)
1514 case ActionFocusWindowUp:
1515 m.OverviewMove(0, -1)
1516 case ActionFocusWindowDown:
1517 m.OverviewMove(0, 1)
1518 case ActionFocusWorkspacePrev:
1519 m.overviewWorkspaceStep(-1)
1520 case ActionFocusWorkspaceNext:
1521 m.overviewWorkspaceStep(1)
1522 default:
1523 return false
1524 }
1525 return true
1526}
1527
1528const (
1529 // overviewScrollStep is the scroll distance, in libinput's pointer
1530 // units, that moves the selection one column or workspace.
1531 overviewScrollStep = 60
1532 // overviewScrollRepeat is the three-finger distance of each further
1533 // column step in one scroll: longer than the first, so a short scroll
1534 // reaches the next column exactly and a long one goes on.
1535 overviewScrollRepeat = 150
1536)
1537
1538// overviewScroll moves the overview selection with a scroll frame: a row
1539// step when the vertical scroll reaches overviewScrollStep and leads, else
1540// a column step. Three-finger updates (encoded here as AxisFinger) step
1541// to the next column after overviewScrollStep, then once per
1542// overviewScrollRepeat, and stay on columns until they lift; up or down they
1543// step once per scroll, so a card in a stack is easy to pick. In a cascade
1544// overview they take one step per gesture on both axes: a band vertically,
1545// a card or workspace horizontally.
1546// Continuous scrolling steps once per overviewScrollStep and a wheel once
1547// per notch (high-resolution wheels add up their fractions of a notch).
1548// Vertical steps select cards first, then rows; natural scroll
1549// (touchpad.* or mouse.*) flips both axes as libinput reports. The end of
1550// a scroll resets it (scrollStop).
1551func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) {
1552 if a.Vertical.Stop || a.Horizontal.Stop {
1553 return false
1554 }
1555 finger := a.Source == ports.AxisFinger
1556 if finger && m.ov.scrolled {
1557 return false
1558 }
1559 // A sideways finger scroll stays on columns; other sources do not, and
1560 // start from zero rather than from its leftover.
1561 sideways := finger && m.ov.sideways
1562 if !finger && m.ov.sideways {
1563 m.ov.scrollX, m.ov.scrollY = 0, 0
1564 }
1565 stepX := float64(overviewScrollStep)
1566 if sideways {
1567 stepX = overviewScrollRepeat
1568 }
1569 add := func(acc *float64, ax ports.ScrollAxis) {
1570 switch {
1571 case !ax.Set:
1572 case a.Source == ports.AxisWheel:
1573 *acc += float64(ax.V120) / 120 * overviewScrollStep
1574 default:
1575 *acc += ax.Value
1576 }
1577 }
1578 if !sideways {
1579 add(&m.ov.scrollY, a.Vertical)
1580 }
1581 add(&m.ov.scrollX, a.Horizontal)
1582 // A step that turns the overview returns, so the axis holds for the loop.
1583 axis := m.overviewAxis()
1584 cascade := axis == horizontalAxis
1585 // A finger step reports a change only when the selection moved: at an
1586 // edge or over a float it does nothing, and the scene stays. The
1587 // snapshot is taken only before a step: stackFront allocates.
1588 var before overviewSelection
1589 for {
1590 switch {
1591 case math.Abs(m.ov.scrollY) >= overviewScrollStep && math.Abs(m.ov.scrollY) >= math.Abs(m.ov.scrollX):
1592 d := sign(m.ov.scrollY)
1593 // The step leaves before the move: a move that turns the
1594 // overview drops the scroll left (showOverview).
1595 m.ov.scrollY -= float64(d) * overviewScrollStep
1596 m.ov.scrollX = 0
1597 if finger {
1598 before = m.overviewSelection()
1599 }
1600 switch {
1601 case cascade && !finger && m.cardAt(m.Current()) >= 0:
1602 // A wheel walks the unfolded stash, a row.
1603 m.OverviewMove(d, 0)
1604 case cascade && !finger:
1605 m.overviewStep(d)
1606 default:
1607 m.OverviewMove(0, d)
1608 }
1609 if m.overviewTurned(axis, finger) {
1610 return true
1611 }
1612 if finger {
1613 m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true
1614 return m.overviewSelection() != before
1615 }
1616 case math.Abs(m.ov.scrollX) >= stepX:
1617 d := sign(m.ov.scrollX)
1618 if cascade && finger {
1619 before = m.overviewSelection()
1620 // One step per gesture: one card of the unfolded
1621 // stash, else one workspace.
1622 if m.cardAt(m.Current()) >= 0 {
1623 m.OverviewMove(d, 0)
1624 } else {
1625 m.overviewStep(d)
1626 }
1627 m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true
1628 return m.overviewSelection() != before
1629 }
1630 m.ov.scrollX -= float64(d) * stepX
1631 m.ov.scrollY = 0
1632 m.OverviewMove(d, 0)
1633 if m.overviewTurned(axis, finger) {
1634 return true
1635 }
1636 if finger {
1637 m.ov.sideways, stepX = true, overviewScrollRepeat
1638 }
1639 default:
1640 return changed
1641 }
1642 changed = true
1643 }
1644}
1645
1646// overviewSelection is what an overview step can move: the workspace, its
1647// stack front, stash card, focused column and selected window.
1648type overviewSelection struct {
1649 w *Workspace
1650 front stackItem
1651 card, selected WindowID
1652 focus int
1653}
1654
1655func (m *Monitor) overviewSelection() overviewSelection {
1656 w := m.Current()
1657 return overviewSelection{w: w, front: m.stackFront(w), card: m.card(), selected: m.ov.selected, focus: w.Focus}
1658}
1659
1660// overviewTurned reports whether a scroll step turned the overview from the
1661// axis before. A finger scroll then waits for the fingers to lift, so it
1662// cannot bounce back.
1663func (m *Monitor) overviewTurned(before layoutAxis, finger bool) bool {
1664 if m.overviewAxis() == before {
1665 return false
1666 }
1667 m.ov.scrolled = m.ov.scrolled || finger
1668 return true
1669}
1670
1671// scrollStop ends a scroll on every monitor: the fingers lifted, maybe
1672// after the pointer took the focus to another output, so the next scroll
1673// starts from zero wherever it goes.
1674func (c *Core) scrollStop(a ports.PointerAxis) {
1675 if !a.Vertical.Stop && !a.Horizontal.Stop {
1676 return
1677 }
1678 for _, sc := range c.screens {
1679 sc.mon.ov.scrollX, sc.mon.ov.scrollY, sc.mon.ov.scrolled, sc.mon.ov.sideways = 0, 0, false, false
1680 }
1681}
1682
1683func sign(v float64) int {
1684 if v < 0 {
1685 return -1
1686 }
1687 return 1
1688}
1689
1690// overviewKeyboardTaken reports whether a layer surface (a launcher) or a
1691// grabbing menu has the keyboard: it gets the keys, not the overview.
1692func (c *Core) overviewKeyboardTaken() bool {
1693 id := c.keyboardFocus()
1694 _, _, layer := c.layerOf(id)
1695 return layer || c.popups[id] != nil
1696}
1697
1698// overviewKey runs an overview key: true when the key was one.
1699func (m *Monitor) overviewKey(key ports.KeyEvent) bool {
1700 if key.Mods != 0 {
1701 return false
1702 }
1703 switch keyName(key.Keysym) {
1704 case "h", "Left":
1705 m.OverviewMove(-1, 0)
1706 case "l", "Right":
1707 m.OverviewMove(1, 0)
1708 case "k", "Up":
1709 m.OverviewMove(0, -1)
1710 case "j", "Down":
1711 m.OverviewMove(0, 1)
1712 case "Return", "KP_Enter":
1713 m.ToggleOverview()
1714 case "Escape":
1715 m.CancelOverview()
1716 default:
1717 return false
1718 }
1719 return true
1720}