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

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