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