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 two-finger scroll 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// rowSnapshot is a row as the overview first showed it: browsing moves its
35// focus and scroll, closing puts them back.
36type rowSnapshot struct {
37 focus WindowID
38 floats []Float
39 fullWidth WindowID
40 // FullWidth history is restored when active binds changed it.
41 maximized []WindowID
42}
43
44// The overview scales workspace rows without resizing clients. Covering
45// floats and the columns form a linear stack of cards; small floats stay
46// hidden. Up/down traverse the stack before changing workspace. A pinned
47// fullscreen window is the front card of its row, the windows it hides
48// behind it; choosing one of them leaves fullscreen.
49
50// A workspace's stash shows as a pile of cards pinned to the left edge of
51// its row: the selected stashed window in front at overviewCardZoom, up to
52// overviewCards-1 others behind it, each overviewCardStep further up and
53// right, dimmed. The row takes the width the pile leaves.
54const (
55 overviewCardZoom = 0.2
56 overviewCards = 4
57 overviewCardStep = 8
58 overviewPeekStep = 0.07
59)
60
61// overviewMaxZoom caps the current row so its neighbors show above and
62// below; overviewMinZoom floors the shrink of wide workspaces, which then
63// scroll to keep the selected column in view.
64const overviewMaxZoom, overviewMinZoom = 0.6, 0.25
65
66// ToggleOverview opens the overview, or closes it on the selection. Over
67// a covering fullscreen window (a game) too: its row shows it, composed
68// rather than scanned out while the overview is open.
69func (m *Monitor) ToggleOverview() {
70 if m.ov.open {
71 m.closeOverview()
72 return
73 }
74 w := m.Current()
75 // A slide in progress lands at once: the overview lays out the
76 // settled state.
77 m.stopSwitch()
78 m.each(func(w *Workspace) { w.stopSlide() })
79 m.ov.open, m.ov.from, m.ov.back = true, w, m.back
80 m.ov.scrollX, m.ov.scrollY, m.ov.scrolled, m.ov.sideways = 0, 0, false, false
81 m.overviewOpens++
82 m.selectRow()
83}
84
85// remember snapshots w the first time the overview shows it.
86func (m *Monitor) remember(w *Workspace) {
87 if _, ok := m.ov.rows[w]; ok {
88 return
89 }
90 if m.ov.rows == nil {
91 m.ov.rows = make(map[*Workspace]rowSnapshot)
92 }
93 s := rowSnapshot{
94 floats: append([]Float(nil), w.Floats...),
95 maximized: slices.Clone(w.maximized),
96 }
97 s.focus, _ = w.Focused()
98 if len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth {
99 c := w.Columns[w.Focus]
100 s.fullWidth = c.Windows[c.Focus]
101 }
102 m.ov.rows[w] = s
103}
104
105// selectRow resets provisional stack and stash selection for a new row.
106func (m *Monitor) selectRow() {
107 m.remember(m.Current())
108 m.ov.card, m.ov.cardOf = 0, nil
109 m.ov.front, m.ov.row, m.ov.frontAt = stackItem{}, nil, 0
110 m.ov.selected, m.ov.selectedAt = 0, -1
111 w := m.Current()
112 // A fullscreen window has the focus: its card is in front.
113 w.focusCover()
114 if w.floatFocus && len(w.Columns) > 0 && (len(w.stack()) < 2 || m.stackFront(w).kind == stackColumns) {
115 w.floatFocus = false
116 }
117 if len(w.Stash) > 0 && (len(w.stack()) == 0 || w.stashFocused() && !w.floatFocus) {
118 m.selectCard(w, w.stashAt)
119 }
120}
121
122// selectCard selects stash entry i of w.
123func (m *Monitor) selectCard(w *Workspace, i int) {
124 m.ov.card, m.ov.cardOf = w.Stash[i].ID, w
125}
126
127// card is the selected stash card, 0 when there is none: the overview is
128// closed, the selection is on a column, or its window left the stash on
129// screen (unmapped, unstashed, or another workspace shown by a bind).
130func (m *Monitor) card() WindowID {
131 w := m.ov.cardOf
132 if !m.ov.open || w == nil || w != m.Current() || w.stashIndex(m.ov.card) < 0 {
133 return 0
134 }
135 return m.ov.card
136}
137
138// cardAt is the pile entry of w that is selected, -1 when none.
139func (m *Monitor) cardAt(w *Workspace) int {
140 if id := m.card(); id != 0 && w == m.ov.cardOf {
141 return w.stashIndex(id)
142 }
143 return -1
144}
145
146// overviewTarget resolves the active preview to one window for validation
147// and window actions. Presentation state never falls back to a covering float.
148func (m *Monitor) overviewTarget() WindowID {
149 if id := m.card(); id != 0 {
150 return id
151 }
152 w := m.Current()
153 item := m.stackFront(w)
154 switch item.kind {
155 case stackFloat:
156 return item.id
157 case stackColumn:
158 i := w.columnOf(item.id)
159 if i >= 0 {
160 c := w.Columns[i]
161 return c.Windows[c.Focus]
162 }
163 case stackColumns:
164 i := w.Focus
165 if len(w.Columns) == 0 {
166 return 0
167 }
168 if w.overviewMaximized() {
169 i = w.columnOf(m.ov.selected)
170 if i < 0 || i == w.Focus {
171 i = m.hiddenColumn(w)
172 }
173 }
174 if i >= 0 {
175 c := w.Columns[i]
176 return c.Windows[c.Focus]
177 }
178 }
179 return 0
180}
181
182// overviewBlocks makes window mutations explicit: they are disabled while
183// selection is provisional. Workspace and monitor navigation and moves to
184// another workspace remain active.
185func overviewBlocks(a Action) bool {
186 if _, _, ok := ResizeArg(a); ok {
187 return true
188 }
189 switch a {
190 case ActionMoveColumnLeft, ActionMoveColumnRight, ActionCycleColumnWidth,
191 ActionMaximizeColumn, ActionToggleFullscreen, ActionToggleWindowStash,
192 ActionToggleStashVisible, ActionConsumeOrExpelLeft,
193 ActionConsumeOrExpelRight, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight,
194 ActionMoveWorkspaceToMonitorUp, ActionMoveWorkspaceToMonitorDown,
195 ActionMoveWindowUp, ActionMoveWindowDown, ActionMoveWorkspaceUp,
196 ActionMoveWorkspaceDown, ActionToggleFloating:
197 return true
198 }
199 return false
200}
201
202// overviewMoveTo moves the selected preview at once, like close-window: the
203// overview stays open and closing it does not undo the move. Nothing moves
204// without a selected preview, from a stash card, or when the bind has
205// nowhere to go.
206func (m *Monitor) overviewMoveTo(a Action) {
207 cur, id := m.Current(), m.overviewTarget()
208 i, column, ok := m.moveDest(a)
209 if m.card() != 0 || id == 0 || !ok || m.Workspaces[i] == cur {
210 return
211 }
212 if cur.pinned() && id != cur.cover() {
213 // Focused reports the covering window while it covers.
214 cur.leaveFullscreen()
215 }
216 cur.FocusID(id)
217 m.MoveToWorkspace(i, column)
218 // The row that received the window keeps it focused on close.
219 if to, _ := m.find(id); to != nil && to != cur {
220 delete(m.ov.rows, to)
221 }
222 m.selectRow()
223}
224
225// closeOverview closes the overview on the selection: a card shows the
226// stash on it. A fullscreen request another stashed window made while
227// the stash was hidden is dropped: the picked card stays in front. A
228// window a pinned fullscreen window hides leaves that fullscreen.
229func (m *Monitor) closeOverview() {
230 w := m.Current()
231 front, target := m.stackFront(w), m.overviewTarget()
232 if w.pinned() && target != w.cover() {
233 w.leaveFullscreen()
234 }
235 if i := m.cardAt(w); i >= 0 {
236 if w.hiddenFullscreen != w.Stash[i].ID {
237 w.hiddenFullscreen = 0
238 }
239 w.stashAt = i
240 w.showStash()
241 } else if front != (stackItem{}) {
242 w.apply(front, target)
243 }
244 for r, s := range m.ov.rows {
245 if r != w && m.has(r) {
246 restoreRow(r, s)
247 }
248 }
249 m.ov = overviewState{}
250}
251
252// CancelOverview closes the overview and returns to the workspace and
253// window it opened on, if they are still there. Every row it showed gets
254// its focus back.
255func (m *Monitor) CancelOverview() {
256 if from := m.ov.from; from != nil && m.has(from) {
257 m.show(from)
258 }
259 for r, s := range m.ov.rows {
260 if m.has(r) {
261 restoreRow(r, s)
262 }
263 }
264 m.back = m.ov.back
265 if m.back != nil && !m.has(m.back) {
266 m.back = nil
267 }
268 m.ov = overviewState{}
269}
270
271// restoreRow puts w back as snapshot s found it.
272func restoreRow(w *Workspace, s rowSnapshot) {
273 if s.focus != 0 {
274 w.FocusID(s.focus)
275 } else {
276 w.floatFocus, w.stashFocus = false, false
277 }
278 // FocusID on a float raises it; restore the initial order and below
279 // flags for floats still present, leaving new floats in their order.
280 current := w.Floats
281 w.Floats = nil
282 for _, f := range s.floats {
283 for i, now := range current {
284 if now.ID == f.ID {
285 now.below = f.below
286 w.Floats = append(w.Floats, now)
287 current = append(current[:i], current[i+1:]...)
288 break
289 }
290 }
291 }
292 w.Floats = append(w.Floats, current...)
293 // Fixed overflow maximizes at most one column: only the one holding
294 // the anchor window, wherever active binds moved it. Scroll overflow
295 // keeps each column's own width.
296 if w.Overflow == OverflowFixed {
297 w.unmaximize()
298 for i := range w.Columns {
299 if s.fullWidth != 0 && slices.Contains(w.Columns[i].Windows, s.fullWidth) {
300 w.maximize(i)
301 break
302 }
303 }
304 }
305 // Windows that left the row since leave its maximize history too.
306 w.maximized = slices.DeleteFunc(slices.Clone(s.maximized), func(id WindowID) bool { return !w.has(id) })
307 w.scroll()
308}
309
310// overviewWorkspaces inserts named rows below their invocation workspace,
311// in configured order within each group. Unattached rows follow the last
312// occupied numbered row (or the first). Empty invocation anchors stay visible;
313// other empty rows are skipped unless current.
314func (m *Monitor) overviewWorkspaces() []*Workspace {
315 fallback := m.Workspaces[0]
316 for _, w := range m.Workspaces {
317 if !w.empty() {
318 fallback = w
319 }
320 }
321 groups := make(map[*Workspace][]*Workspace)
322 named := slices.Clone(m.hidden)
323 rank := func(w *Workspace) int {
324 for i, spec := range m.named {
325 if spec.Name == w.Name {
326 return i
327 }
328 }
329 return len(m.named)
330 }
331 slices.SortStableFunc(named, func(a, b *Workspace) int { return rank(a) - rank(b) })
332 for _, w := range named {
333 if w.empty() && w != m.Current() {
334 continue
335 }
336 anchor := w.overviewAfter
337 if indexOf(m.Workspaces, anchor) < 0 {
338 anchor = fallback
339 }
340 groups[anchor] = append(groups[anchor], w)
341 }
342 var rows []*Workspace
343 for _, w := range m.Workspaces {
344 if !w.empty() || w == m.Current() || len(groups[w]) > 0 {
345 rows = append(rows, w)
346 rows = append(rows, groups[w]...)
347 }
348 }
349 return rows
350}
351
352// showOverview changes rows without committing the provisional stack. Entering
353// a named group uses its invocation anchor as the toggle return target, not
354// rows merely browsed in the overview.
355func (m *Monitor) showOverview(w *Workspace) {
356 if w != m.Current() && m.isHidden(w) && !m.isHidden(m.Current()) {
357 m.back = m.Current()
358 if indexOf(m.Workspaces, w.overviewAfter) >= 0 {
359 m.back = w.overviewAfter
360 }
361 }
362 m.show(w)
363 m.selectRow()
364}
365
366// OverviewMove routes every vertical input through moveStack, then falls
367// through to workspace navigation at the stack boundary. Horizontal moves
368// stay inside a column group; left on a single card sends it behind.
369func (m *Monitor) OverviewMove(dx, dy int) {
370 w := m.Current()
371 if dy != 0 {
372 if m.moveStack(-dy) {
373 return
374 }
375 rows := m.overviewWorkspaces()
376 i := indexOf(rows, w) + dy
377 if i < 0 || i >= len(rows) {
378 return
379 }
380 m.showOverview(rows[i])
381 return
382 }
383 if at := m.cardAt(w); at >= 0 {
384 switch {
385 case at+dx >= 0 && at+dx < len(w.Stash):
386 m.selectCard(w, at+dx)
387 case dx > 0 && len(w.stack()) > 0:
388 m.ov.card, m.ov.cardOf = 0, nil
389 item := m.stackFront(w)
390 if item.kind == stackColumns {
391 if w.overviewMaximized() {
392 i := m.hiddenColumn(w)
393 m.ov.selected, m.ov.selectedAt = w.Columns[i].Windows[w.Columns[i].Focus], i
394 } else {
395 w.selectOverviewColumn(0)
396 m.ov.selected = w.Columns[0].Windows[w.Columns[0].Focus]
397 }
398 }
399 }
400 return
401 }
402 front := m.stackFront(w)
403 if front.kind != stackColumns {
404 if dx < 0 && m.moveStack(1) {
405 return
406 }
407 if dx < 0 && len(w.Stash) > 0 {
408 m.selectCard(w, w.stashAt)
409 }
410 return
411 }
412 if len(w.Columns) == 0 {
413 return
414 }
415 i := w.columnOf(m.ov.selected)
416 if i < 0 || w.overviewMaximized() && i == w.Focus {
417 if w.overviewMaximized() {
418 i = m.hiddenColumn(w)
419 } else {
420 i = w.Focus
421 }
422 }
423 first := 0
424 if w.overviewMaximized() && w.Focus == 0 {
425 first = 1
426 }
427 if dx < 0 && i == first && len(w.Stash) > 0 {
428 m.selectCard(w, w.stashAt)
429 return
430 }
431 next := min(max(i+dx, 0), len(w.Columns)-1)
432 if w.overviewMaximized() && next == w.Focus {
433 next = min(max(next+dx, 0), len(w.Columns)-1)
434 if next == w.Focus {
435 return
436 }
437 }
438 m.ov.selected = w.Columns[next].Windows[w.Columns[next].Focus]
439 m.ov.selectedAt = next
440 if !w.overviewMaximized() {
441 // Ordinary group navigation retains its existing scroll-to-selection.
442 w.selectOverviewColumn(next)
443 }
444}
445
446// OverviewPick closes the overview on the clicked window.
447func (m *Monitor) OverviewPick(id WindowID) {
448 w, _ := m.find(id)
449 if w == nil {
450 return
451 }
452 if w.floatIndex(id) >= 0 && w.stashIndex(id) < 0 {
453 i := w.floatIndex(id)
454 if !w.coversFloat(w.Floats[i]) && !(id == w.cover() && w.pinned()) {
455 return
456 }
457 }
458 m.showOverview(w)
459 m.ov.card, m.ov.cardOf = 0, nil
460 if i := w.stashIndex(id); i >= 0 {
461 m.selectCard(w, i)
462 } else if item, ok := w.itemOf(id); ok {
463 m.setFront(w, item)
464 if i := w.columnOf(id); i >= 0 {
465 if item.kind == stackColumns && !w.overviewMaximized() {
466 w.selectOverviewColumn(i)
467 }
468 m.ov.selected = id
469 for j, v := range w.Columns[i].Windows {
470 if v == id {
471 w.Columns[i].Focus = j
472 break
473 }
474 }
475 }
476 }
477 m.closeOverview()
478}
479
480// overviewRows centers the selected row with its neighbors above and below.
481// The numbered/named boundary gets extra spacing and an inactive horizontal
482// rule. Row widths, stash piles and preview scales remain unchanged.
483func (m *Monitor) overviewRows() (map[*Workspace]int, []ports.Separator) {
484 cur := m.Current()
485 u := cur.overviewArea()
486 gap := max(1, u.H*3/100)
487 hc := cur.rowHeight()
488 y := u.Y + (u.H-hc)/2
489 rows := map[*Workspace]int{cur: y}
490 var dividers []ports.Separator
491 list := m.overviewWorkspaces()
492 i := indexOf(list, cur)
493 for _, next := range []int{i - 1, i + 1} {
494 if next < 0 || next >= len(list) {
495 continue
496 }
497 w := list[next]
498 spacing := gap
499 boundary := m.isHidden(w) != m.isHidden(cur)
500 if boundary {
501 spacing *= 2
502 }
503 top := y + hc
504 if next < i {
505 rows[w] = y - spacing - w.rowHeight()
506 top = y - spacing
507 } else {
508 rows[w] = y + hc + spacing
509 }
510 if boundary {
511 margin := u.W * 4 / 100
512 dividers = append(dividers, ports.Separator{Rect: Rect{X: u.X + margin, Y: top + spacing/2, W: u.W - 2*margin, H: 1}})
513 }
514 }
515 return rows, dividers
516}
517
518// overviewLayout places the current workspace and its overview neighbors,
519// each with its full-width stash pile and covering-float stack. Other rows
520// stay hidden; numbered and named workspaces share the same preview geometry.
521func (m *Monitor) overviewLayout() []Placement {
522 cur := m.Current()
523 rows, _ := m.overviewRows()
524 var result []Placement
525 for w := range m.all() {
526 ry, shown := rows[w]
527 if shown {
528 at := m.cardAt(w)
529 result = append(result, m.stackRow(w, ry, w != cur, at < 0)...)
530 result = append(result, w.pile(ry, w != cur, at)...)
531 }
532 for _, f := range w.Floats {
533 if _, drawn := w.itemOf(f.ID); !shown || !drawn {
534 result = append(result, Placement{ID: f.ID, Hidden: true})
535 }
536 }
537 if !shown {
538 for _, id := range w.windows() {
539 if w.floatIndex(id) < 0 {
540 result = append(result, Placement{ID: id, Hidden: true})
541 }
542 }
543 }
544 }
545 return result
546}
547
548// fan places front and neighboring cards, farthest visible first and front
549// last. A card may contain several tiles; omitted cards stay in the layout
550// hidden so their clients are not left showing on screen.
551func fan(cards [][]Placement, front, maxBehind, maxBefore int, circular bool,
552 behind, before func(int) (int, int), dim, lit bool) []Placement {
553 n := len(cards)
554 if n == 0 {
555 return nil
556 }
557 front = min(max(front, 0), n-1)
558 out := make([]Placement, 0, n)
559 add := func(i, distance int, offset func(int) (int, int), peek bool) {
560 dx, dy := offset(distance)
561 for _, p := range cards[i] {
562 if !p.Hidden {
563 p.Rect.X += dx
564 p.Rect.Y += dy
565 p.Peek = p.Peek || peek || dim
566 p.Focused = p.Focused && !peek && !dim && lit
567 }
568 out = append(out, p)
569 }
570 }
571 shown := make([]bool, n)
572 // Behind and before are in nearest-first order; paint farthest first.
573 for d := min(maxBehind, n-1); d >= 1; d-- {
574 i := front + d
575 if circular {
576 i = (front + d) % n
577 }
578 if i >= n {
579 continue
580 }
581 add(i, d, behind, true)
582 shown[i] = true
583 }
584 for d := min(maxBefore, n-1); d >= 1; d-- {
585 i := front - d
586 if circular {
587 i = (front - d + n) % n
588 }
589 if i < 0 {
590 continue
591 }
592 if shown[i] {
593 continue
594 }
595 add(i, d, before, true)
596 shown[i] = true
597 }
598 add(front, 0, behind, false)
599 shown[front] = true
600 for i, card := range cards {
601 if shown[i] {
602 continue
603 }
604 for _, p := range card {
605 p.Hidden = true
606 p.Preview = 0
607 p.Focused = false
608 out = append(out, p)
609 }
610 }
611 return out
612}
613
614// pileWidth is the room the stash pile of w takes on the left of its row,
615// its margin included; 0 without a stash.
616func (w *Workspace) pileWidth() int {
617 if len(w.Stash) == 0 {
618 return 0
619 }
620 card := int(math.Round(float64(w.stashRect().W) * overviewCardZoom))
621 // Never more than a third of the width: the columns keep the rest.
622 a := w.overviewArea()
623 return min(card+(min(len(w.Stash), overviewCards)-1)*w.cardStep()+a.W*2/100, a.W/3)
624}
625
626// cardStep is the offset between stacked cards, smaller on tiny outputs.
627func (w *Workspace) cardStep() int {
628 a := w.overviewArea()
629 return min(overviewCardStep, a.W/100, a.H/50)
630}
631
632// pile places the stash of w as cards on the left of the row whose top is
633// at y: entry front (the stash selection when -1) in front, the next ones
634// behind it, dimmed, the rest hidden. Only a front card of the current row
635// (dim false) with front set is selected.
636func (w *Workspace) pile(y int, dim bool, front int) []Placement {
637 n := len(w.Stash)
638 if n == 0 {
639 return nil
640 }
641 selected := front >= 0 && !dim
642 if front < 0 {
643 front = w.stashAt
644 }
645 front = min(max(front, 0), n-1)
646 r := w.stashRect()
647 u := w.overviewArea()
648 cw := int(math.Round(float64(r.W) * overviewCardZoom))
649 ch := int(math.Round(float64(r.H) * overviewCardZoom))
650 rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom()))
651 // Stack rows reserve two peek steps above the front card. Align the
652 // stash with that front band, not with the top of its envelope.
653 x0, y0 := u.X+u.W/100, w.frontRowY(y)+(rowH-ch)/2
654 cards := make([][]Placement, n)
655 for i := range w.Stash {
656 cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: Rect{X: x0, Y: y0, W: cw, H: ch}, Preview: overviewCardZoom, Focused: selected}}
657 }
658 step := w.cardStep()
659 return fan(cards, front, min(n, overviewCards)-1, 0, true,
660 func(d int) (int, int) { return d * step, -d * step },
661 func(int) (int, int) { return 0, 0 }, dim, selected)
662}
663
664// overviewZoom is the scale that fits every column of w in the usable
665// width, within the overview bounds.
666func (w *Workspace) overviewZoom() float64 {
667 _, span, _ := w.previewTiles()
668 if span <= 0 {
669 return overviewMaxZoom
670 }
671 room := float64(w.overviewArea().W-w.pileWidth()) * 0.96
672 z := min(max(room/float64(span), overviewMinZoom), overviewMaxZoom)
673 if len(w.stack()) > 1 {
674 // Reserve two clear peeks on either side of the front row.
675 z = min(z, overviewMaxZoom/(1+4*overviewPeekStep))
676 }
677 return z
678}
679
680// peekStep is the shared step for a stack row and its front-card inset.
681func (w *Workspace) peekStep() int {
682 return max(1, int(math.Round(float64(w.Usable.H)*w.overviewZoom()*overviewPeekStep)))
683}
684
685// frontRowY skips the space reserved for two cards peeking above the front.
686// A single-card row starts at y, as before.
687func (w *Workspace) frontRowY(y int) int {
688 if len(w.stack()) > 1 {
689 return y + 2*w.peekStep()
690 }
691 return y
692}
693
694// rowHeight includes the largest possible stack envelope (two peeks above
695// and two below). Neighbor rows use the same measure as the current row.
696func (w *Workspace) rowHeight() int {
697 h := int(math.Round(float64(w.Usable.H) * w.overviewZoom()))
698 if len(w.stack()) > 1 {
699 h += 4 * w.peekStep()
700 }
701 return h
702}
703
704// previewTiles lays tiles out unscrolled, relative to the usable area's
705// corner. Fixed overflow uses its on-screen geometry; scroll overflow uses
706// an unscrolled row. span is the row's width; sel is the focused column.
707func (w *Workspace) previewTiles() (tiles []Placement, span int, sel Rect) {
708 if w.pinned() {
709 c := w.cover()
710 // The row shows the columns a pinned fullscreen window hides,
711 // laid out as without it; its own tile stays marked fullscreen.
712 v := *w
713 v.fullscreen = 0
714 tiles, span, sel = v.previewTiles()
715 for i := range tiles {
716 tiles[i].Fullscreen = tiles[i].ID == c
717 }
718 return tiles, span, sel
719 }
720 g := w.gap()
721 h := max(w.Usable.H-2*g, 0)
722 var rects []Rect
723 if w.Overflow == OverflowFixed {
724 rects = w.columnRects()
725 span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile.
726 }
727 maximized := w.Overflow == OverflowFixed && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth
728 for i, c := range w.Columns {
729 r := Rect{X: w.columnX(i) - w.Usable.X, Y: g, W: w.columnWidth(i), H: h}
730 if w.Overflow == OverflowFixed {
731 r = rects[i]
732 r.X, r.Y = r.X-w.Usable.X, r.Y-w.Usable.Y
733 } else if w.fullscreenColumn(i) {
734 // A fullscreen column shows at the width it had in the row.
735 r.W = max(w.Usable.W-2*g, 0)
736 }
737 if maximized && i != w.Focus {
738 // FullWidth hides other columns on screen. Their old buffers
739 // cannot fit their current spiral rects; show only the maximized
740 // column until navigation restores the normal layout.
741 for _, id := range c.Windows {
742 tiles = append(tiles, Placement{ID: id, Hidden: true})
743 }
744 continue
745 }
746 for j, t := range rowRects(r, c, g) {
747 tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: i == w.Focus && j == c.Focus})
748 }
749 if w.Overflow != OverflowFixed {
750 span = max(span, r.X+r.W+g)
751 }
752 if i == w.Focus {
753 sel = r
754 }
755 }
756 return tiles, span, sel
757}
758
759// previewRow scales the tiles of w into a row whose top is at y, centred
760// in the usable width, or scrolled so the focused column shows when the
761// row is wider, right of the stash pile. Dimmed rows (neighbor
762// workspaces) have no focus; neither has a row whose pile has it (lit
763// false).
764func (w *Workspace) previewRow(y int, dim, lit bool) []Placement {
765 tiles, span, sel := w.previewTiles()
766 return w.previewRowTiles(y, dim, lit, tiles, span, sel)
767}
768
769func (w *Workspace) previewRowTiles(y int, dim, lit bool, tiles []Placement, span int, sel Rect) []Placement {
770 u := w.overviewArea()
771 z := w.overviewZoom()
772 scale := func(v int) int { return int(math.Round(float64(v) * z)) }
773 width := scale(span)
774 // Rows stay centred on the output like rows without a stash; the pile
775 // only pushes a row right when they would overlap.
776 x := max(u.X+(u.W-width)/2, u.X+w.pileWidth())
777 left := w.pileWidth()
778 u.X, u.W = u.X+left, u.W-left
779 if width > u.W {
780 x = u.X + u.W/2 - scale(sel.X+sel.W/2)
781 x = min(max(x, u.X+u.W-width), u.X)
782 }
783 for i := range tiles {
784 if tiles[i].Hidden {
785 continue
786 }
787 r := tiles[i].Rect
788 x0, y0 := scale(r.X), scale(r.Y)
789 tiles[i].Rect = Rect{X: x + x0, Y: y + y0, W: scale(r.X+r.W) - x0, H: scale(r.Y+r.H) - y0}
790 tiles[i].Preview, tiles[i].Peek = z, dim
791 tiles[i].Focused = tiles[i].Focused && !dim && lit
792 }
793 return tiles
794}
795
796// overviewOutline frames the selected preview with active lines of width
797// b, just outside it.
798func overviewOutline(layout []Placement, b int) []ports.Separator {
799 for _, p := range layout {
800 if p.Preview > 0 && p.Focused && !p.Hidden {
801 r := p.Rect
802 return []ports.Separator{
803 {Rect: Rect{X: r.X - b, Y: r.Y - b, W: r.W + 2*b, H: b}, Active: true},
804 {Rect: Rect{X: r.X - b, Y: r.Y + r.H, W: r.W + 2*b, H: b}, Active: true},
805 {Rect: Rect{X: r.X - b, Y: r.Y, W: b, H: r.H}, Active: true},
806 {Rect: Rect{X: r.X + r.W, Y: r.Y, W: b, H: r.H}, Active: true},
807 }
808 }
809 }
810 return nil
811}
812
813// overviewAt is the preview under the output-local point, if any. Front
814// cards take priority over peeks, including peeks appended after a front
815// tile in another row's layout.
816func (m *Monitor) overviewAt(x, y float64) WindowID {
817 layout := m.Layout()
818 for i := len(layout) - 1; i >= 0; i-- {
819 p := layout[i]
820 r := p.Rect
821 if p.Preview > 0 && !p.Hidden && !p.Peek && x >= float64(r.X) && x < float64(r.X+r.W) && y >= float64(r.Y) && y < float64(r.Y+r.H) {
822 return p.ID
823 }
824 }
825 for i := len(layout) - 1; i >= 0; i-- {
826 p := layout[i]
827 r := p.Rect
828 if p.Preview > 0 && !p.Hidden && x >= float64(r.X) && x < float64(r.X+r.W) && y >= float64(r.Y) && y < float64(r.Y+r.H) {
829 return p.ID
830 }
831 }
832 return 0
833}
834
835// overviewClick closes the overview on the preview under the pointer, on
836// the output it shows on. picked is false when no preview is there.
837func (c *Core) overviewClick(ctx context.Context) (picked bool, err error) {
838 i := c.screenAt(c.cursorX, c.cursorY)
839 if i < 0 {
840 return false, nil
841 }
842 sc := c.screens[i]
843 if !sc.mon.ov.open {
844 return false, nil
845 }
846 id := sc.mon.overviewAt(c.cursorX-float64(sc.x), c.cursorY-float64(sc.y))
847 if id == 0 {
848 return false, nil
849 }
850 sc.mon.OverviewPick(id)
851 c.focusScreen = i
852 c.keyboard.takeBack()
853 if err := c.workspaceVisible(ctx, true); err != nil {
854 return true, err
855 }
856 return true, c.publish(ctx)
857}
858
859// overviewAction runs a bind while the overview is open: focus binds move
860// the selection, close-window and move-to-workspace binds act on the
861// selected preview, other mutations are dropped. handled is false for binds
862// that run as usual.
863func (m *Monitor) overviewAction(a Action) (e Effect, handled bool) {
864 if !m.ov.open {
865 return Effect{}, false
866 }
867 if a == ActionCloseWindow {
868 return Effect{Close: m.overviewTarget()}, true
869 }
870 if movesToWorkspace(a) {
871 m.overviewMoveTo(a)
872 return Effect{}, true
873 }
874 return Effect{}, m.overviewFocus(a) || overviewBlocks(a)
875}
876
877// overviewFocus runs a focus bind as an overview move, so the focus binds
878// and the bare keys navigate the overview the same way: true when a was
879// one of them.
880func (m *Monitor) overviewFocus(a Action) bool {
881 switch a {
882 case ActionFocusColumnLeft:
883 m.OverviewMove(-1, 0)
884 case ActionFocusColumnRight:
885 m.OverviewMove(1, 0)
886 case ActionFocusWindowUp, ActionFocusWorkspaceUp:
887 m.OverviewMove(0, -1)
888 case ActionFocusWindowDown, ActionFocusWorkspaceDown:
889 m.OverviewMove(0, 1)
890 default:
891 return false
892 }
893 return true
894}
895
896const (
897 // overviewScrollStep is the scroll distance, in libinput's pointer
898 // units, that moves the selection one column or workspace.
899 overviewScrollStep = 60
900 // overviewScrollRepeat is the two-finger distance of each further
901 // column step in one scroll: longer than the first, so a short scroll
902 // reaches the next column exactly and a long one goes on.
903 overviewScrollRepeat = 150
904)
905
906// overviewScroll moves the overview selection with a scroll frame: a row
907// step when the vertical scroll reaches overviewScrollStep and leads, else
908// a column step. Two fingers step to the next column after
909// overviewScrollStep, then once per overviewScrollRepeat, and stay on
910// columns until they lift; up or down they step once per scroll, so a card
911// in a stack is easy to pick.
912// Continuous scrolling steps once per overviewScrollStep and a wheel once
913// per notch (high-resolution wheels add up their fractions of a notch).
914// Vertical steps select cards first, then rows; natural scroll
915// (touchpad.* or mouse.*) flips both axes as libinput reports. The end of
916// a scroll resets it (scrollStop).
917func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) {
918 if a.Vertical.Stop || a.Horizontal.Stop {
919 return false
920 }
921 finger := a.Source == ports.AxisFinger
922 if finger && m.ov.scrolled {
923 return false
924 }
925 // A sideways finger scroll stays on columns; other sources do not, and
926 // start from zero rather than from its leftover.
927 sideways := finger && m.ov.sideways
928 if !finger && m.ov.sideways {
929 m.ov.scrollX, m.ov.scrollY = 0, 0
930 }
931 stepX := float64(overviewScrollStep)
932 if sideways {
933 stepX = overviewScrollRepeat
934 }
935 add := func(acc *float64, ax ports.ScrollAxis) {
936 switch {
937 case !ax.Set:
938 case a.Source == ports.AxisWheel:
939 *acc += float64(ax.V120) / 120 * overviewScrollStep
940 default:
941 *acc += ax.Value
942 }
943 }
944 if !sideways {
945 add(&m.ov.scrollY, a.Vertical)
946 }
947 add(&m.ov.scrollX, a.Horizontal)
948 for {
949 switch {
950 case math.Abs(m.ov.scrollY) >= overviewScrollStep && math.Abs(m.ov.scrollY) >= math.Abs(m.ov.scrollX):
951 m.OverviewMove(0, sign(m.ov.scrollY))
952 m.ov.scrollY -= float64(sign(m.ov.scrollY)) * overviewScrollStep
953 m.ov.scrollX = 0
954 if finger {
955 m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true
956 return true
957 }
958 case math.Abs(m.ov.scrollX) >= stepX:
959 m.OverviewMove(sign(m.ov.scrollX), 0)
960 m.ov.scrollX -= float64(sign(m.ov.scrollX)) * stepX
961 m.ov.scrollY = 0
962 if finger {
963 m.ov.sideways, stepX = true, overviewScrollRepeat
964 }
965 default:
966 return changed
967 }
968 changed = true
969 }
970}
971
972// scrollStop ends a scroll on every monitor: the fingers lifted, maybe
973// after the pointer took the focus to another output, so the next scroll
974// starts from zero wherever it goes.
975func (c *Core) scrollStop(a ports.PointerAxis) {
976 if !a.Vertical.Stop && !a.Horizontal.Stop {
977 return
978 }
979 for _, sc := range c.screens {
980 sc.mon.ov.scrollX, sc.mon.ov.scrollY, sc.mon.ov.scrolled, sc.mon.ov.sideways = 0, 0, false, false
981 }
982}
983
984func sign(v float64) int {
985 if v < 0 {
986 return -1
987 }
988 return 1
989}
990
991// overviewKeyboardTaken reports whether a layer surface (a launcher) or a
992// grabbing menu has the keyboard: it gets the keys, not the overview.
993func (c *Core) overviewKeyboardTaken() bool {
994 id := c.keyboardFocus()
995 _, _, layer := c.layerOf(id)
996 return layer || c.popups[id] != nil
997}
998
999// overviewKey runs an overview key: true when the key was one.
1000func (m *Monitor) overviewKey(key ports.KeyEvent) bool {
1001 if key.Mods != 0 {
1002 return false
1003 }
1004 switch keyName(key.Keysym) {
1005 case "h", "Left":
1006 m.OverviewMove(-1, 0)
1007 case "l", "Right":
1008 m.OverviewMove(1, 0)
1009 case "k", "Up":
1010 m.OverviewMove(0, -1)
1011 case "j", "Down":
1012 m.OverviewMove(0, 1)
1013 case "Return", "KP_Enter":
1014 m.ToggleOverview()
1015 case "Escape":
1016 m.CancelOverview()
1017 default:
1018 return false
1019 }
1020 return true
1021}