v0.2.0 13c6a90

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

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