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