refactor/kvconf 3462c76

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

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 23

Tags 10

↑↓ moveenter openesc close