feat/hints a471fa4

overview.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close