overview_stack.go RawHistoryFolder

1package core
2
3import "slices"
4
5type stackKind uint8
6
7const (
8	stackFloat stackKind = iota
9	stackColumns
10	stackColumn
11)
12
13// A stackItem identifies a covering float, a column group (all columns, or
14// all except the maximized column), or the maximized column by one of its
15// windows. Only stackColumns has a zero ID.
16type stackItem struct {
17	kind stackKind
18	id   WindowID
19}
20
21// overviewMaximized reports a fixed row whose focused column hides the
22// others: maximized, or holding the covering fullscreen tile. Its card
23// shows in front of the hidden columns' card.
24func (w *Workspace) overviewMaximized() bool {
25	if w.Overflow != OverflowFixed || len(w.Columns) < 2 || w.Focus < 0 || w.Focus >= len(w.Columns) {
26		return false
27	}
28	if c := w.cover(); c != 0 && !w.isFloat(c) {
29		return w.columnOf(c) == w.Focus
30	}
31	return w.Columns[w.Focus].FullWidth
32}
33
34func (w *Workspace) columnOf(id WindowID) int {
35	c, _ := w.cell(id)
36	return c - 1
37}
38
39// stack lists what is on screen followed by what it hides, top to bottom.
40// A pinned fullscreen window is the front card, a float on its own, a
41// fixed tile with its column, so every window it hides stays reachable; a
42// stashed one is its card in the pile.
43func (w *Workspace) stack() []stackItem {
44	var items []stackItem
45	w.walkStack(func(item stackItem) bool {
46		items = append(items, item)
47		return true
48	})
49	return items
50}
51
52// stacked reports whether the stack has several cards, without building it:
53// the render path asks for every workspace of every frame.
54func (w *Workspace) stacked() bool {
55	_, n := w.stackHead()
56	return n > 1
57}
58
59// stackHead is the first item of the stack and the number of items, counted
60// up to 2, without building the stack.
61func (w *Workspace) stackHead() (first stackItem, n int) {
62	w.walkStack(func(item stackItem) bool {
63		if n == 0 {
64			first = item
65		}
66		n++
67		return n < 2
68	})
69	return first, n
70}
71
72// walkStack yields the items of stack in order until yield returns false.
73func (w *Workspace) walkStack(yield func(stackItem) bool) {
74	c, columns := WindowID(0), len(w.Columns) > 0
75	if w.pinned() {
76		c = w.cover()
77	}
78	switch {
79	case c == 0, w.stashIndex(c) >= 0:
80	case w.floatIndex(c) >= 0:
81		if !yield(stackItem{stackFloat, c}) {
82			return
83		}
84	case w.overviewMaximized():
85		if !yield(stackItem{stackColumn, c}) {
86			return
87		}
88	default:
89		// One card of all columns, its own marked fullscreen.
90		if !yield(stackItem{kind: stackColumns}) {
91			return
92		}
93		columns = false
94	}
95	for i := len(w.Floats) - 1; i >= 0; i-- {
96		f := w.Floats[i]
97		if !f.below && w.coversFloat(f) && !yield(stackItem{stackFloat, f.ID}) {
98			return
99		}
100	}
101	if (c == 0 || w.isFloat(c)) && w.overviewMaximized() && !yield(stackItem{stackColumn, w.Columns[w.Focus].Windows[0]}) {
102		return
103	}
104	if columns && !yield(stackItem{kind: stackColumns}) {
105		return
106	}
107	for i := len(w.Floats) - 1; i >= 0; i-- {
108		f := w.Floats[i]
109		if f.below && w.coversFloat(f) && !yield(stackItem{stackFloat, f.ID}) {
110			return
111		}
112	}
113}
114
115// itemOf resolves a clicked tile to the card currently containing it.
116func (w *Workspace) itemOf(id WindowID) (stackItem, bool) {
117	for _, item := range w.stack() {
118		switch item.kind {
119		case stackFloat:
120			if item.id == id {
121				return item, true
122			}
123		case stackColumn:
124			if i := w.columnOf(id); i >= 0 && i == w.columnOf(item.id) {
125				return item, true
126			}
127		case stackColumns:
128			if i := w.columnOf(id); i >= 0 && (!w.overviewMaximized() || i != w.Focus) {
129				return item, true
130			}
131		}
132	}
133	return stackItem{}, false
134}
135
136// stackFront resolves the provisional item against fresh workspace contents;
137// an invalidated selection falls back to the nearest surviving stack index.
138func (m *Monitor) stackFront(w *Workspace) stackItem {
139	items := w.stack()
140	if len(items) == 0 {
141		return stackItem{}
142	}
143	if m.ov.row == w {
144		for _, item := range items {
145			if item == m.ov.front || item.kind == stackColumn && m.ov.front.kind == stackColumn && w.columnOf(item.id) >= 0 && w.columnOf(item.id) == w.columnOf(m.ov.front.id) {
146				return item
147			}
148		}
149		return items[min(max(m.ov.frontAt, 0), len(items)-1)]
150	}
151	return items[0]
152}
153
154// setFront records a provisional card and its index for removal fallback.
155func (m *Monitor) setFront(w *Workspace, item stackItem) {
156	m.ov.row, m.ov.front = w, item
157	m.ov.frontAt = slices.Index(w.stack(), item)
158}
159
160// moveStack moves only within the current row. The caller changes workspace
161// when the linear stack has no item in that direction.
162func (m *Monitor) moveStack(d int) bool {
163	w := m.Current()
164	items := w.stack()
165	if len(items) < 2 {
166		return false
167	}
168	at := slices.Index(items, m.stackFront(w)) + d
169	if at < 0 || at >= len(items) {
170		return false
171	}
172	m.setFront(w, items[at])
173	m.clearCard()
174	return true
175}
176
177// hiddenColumn resolves the provisional selection by window, not column
178// index. With no surviving selection, prefer the most recently maximized
179// hidden column, then the nearest prior non-maximized focus, then first.
180func (m *Monitor) hiddenColumn(w *Workspace) int {
181	if !w.overviewMaximized() {
182		return -1
183	}
184	if i := w.columnOf(m.ov.selected); i >= 0 && i != w.Focus && m.ov.row == w {
185		return i
186	}
187	if m.ov.row == w && m.ov.selectedAt >= 0 {
188		i := min(m.ov.selectedAt, len(w.Columns)-1)
189		if i == w.Focus {
190			if i > 0 {
191				i--
192			} else {
193				i++
194			}
195		}
196		return i
197	}
198	for _, id := range w.maximized {
199		if i := w.columnOf(id); i >= 0 && i != w.Focus {
200			return i
201		}
202	}
203	if i := w.columnOf(m.ov.rows[m.ov.from].focus); w == m.ov.from && i >= 0 && i != w.Focus {
204		return i
205	}
206	for i := range w.Columns {
207		if i != w.Focus {
208			return i
209		}
210	}
211	return -1
212}
213
214// selectOverviewColumn retains the existing ordinary-row live focus semantics.
215func (w *Workspace) selectOverviewColumn(i int) {
216	w.floatFocus, w.stashFocus = false, false
217	if w.policy().maximizeHides() && w.Focus != i && w.Focus < len(w.Columns) {
218		w.unmaximize()
219	}
220	w.Focus = i
221	w.scroll()
222}
223
224// apply commits the selected front card without changing other stack items.
225func (w *Workspace) apply(item stackItem, selected WindowID) {
226	// Picking a card is a user focus: a blocked parent gives it to its
227	// modal dialog.
228	defer w.focusModal(0)
229	switch item.kind {
230	case stackFloat:
231		w.FocusID(item.id)
232	case stackColumns:
233		i := w.columnOf(selected)
234		if w.overviewMaximized() && i == w.Focus {
235			i = -1
236		}
237		if i < 0 && len(w.Columns) > 0 {
238			i = w.Focus
239			if w.overviewMaximized() {
240				for next := range w.Columns {
241					if next != w.Focus {
242						i = next
243						break
244					}
245				}
246			}
247			c := w.Columns[i]
248			selected = c.Windows[c.Focus]
249		}
250		if selected != 0 {
251			if w.overviewMaximized() && i != w.Focus {
252				w.transferMaximization(i)
253			}
254			w.FocusID(selected)
255		}
256	case stackColumn:
257		i := w.columnOf(item.id)
258		if i < 0 {
259			return
260		}
261		if w.columnOf(selected) != i {
262			c := w.Columns[i]
263			selected = c.Windows[c.Focus]
264		}
265		if w.overviewMaximized() && i != w.Focus {
266			w.transferMaximization(i)
267		}
268		w.FocusID(selected)
269	}
270}
271
272// previewItem draws a group using screen geometry and a single card at its
273// configured size. A prior maximized column retains its full-width buffer.
274func (m *Monitor) previewItem(w *Workspace, item stackItem, axis layoutAxis, slot Rect, dim, lit bool) []Placement {
275	if item.kind == stackColumns {
276		if !w.overviewMaximized() {
277			return w.previewRow(axis, slot, dim, lit)
278		}
279		// The hidden columns fill the screen as if the maximized column,
280		// shown on its own card, were not there. In particular, an earlier
281		// maximized buffer scales into its unmaximized cell here.
282		g := w.gap()
283		rects := w.hiddenColumnRects()
284		var tiles []Placement
285		selected := m.hiddenColumn(w)
286		for i, c := range w.Columns {
287			if i == w.Focus {
288				continue
289			}
290			r := rects[i]
291			r.X -= w.Usable.X
292			r.Y -= w.Usable.Y
293			w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g)
294			for j, t := range w.rowBuf {
295				tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: i == selected && j == c.Focus})
296			}
297		}
298		sel := rects[selected]
299		sel.X -= w.Usable.X
300		return w.previewRowTiles(axis, slot, dim, lit, false, tiles, w.Usable.W, sel)
301	}
302	g := w.gap()
303	if item.kind == stackFloat && item.id == w.cover() {
304		// A fullscreen float shows at the size of a fullscreen column.
305		r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)}
306		return w.previewRowTiles(axis, slot, dim, lit, false, []Placement{{ID: item.id, Rect: r, Focused: true, Fullscreen: true}}, w.Usable.W, r)
307	}
308	if item.kind == stackFloat {
309		f := w.Floats[w.floatIndex(item.id)]
310		r := w.floatRect(f)
311		r.X -= w.Usable.X
312		r.Y -= w.Usable.Y
313		return w.previewRowTiles(axis, slot, dim, lit, false, []Placement{{ID: item.id, Rect: r, Floating: true, Focused: true}}, w.Usable.W, r)
314	}
315	i := w.columnOf(item.id)
316	if i < 0 {
317		return nil
318	}
319	c := w.Columns[i]
320	r := Rect{W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)}
321	r.X = (w.Usable.W - r.W) / 2
322	r.Y = (w.Usable.H - r.H) / 2
323	tiles := make([]Placement, 0, len(c.Windows))
324	w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g)
325	for j, t := range w.rowBuf {
326		tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus, Fullscreen: c.Windows[j] == w.cover()})
327	}
328	return w.previewRowTiles(axis, slot, dim, lit, false, tiles, w.Usable.W, r)
329}
330
331// hiddenColumnRects lays out the columns behind the maximized one with the
332// fixed layout rules, without the maximized column, so no hole is left
333// where it was. The maximized column's own entry is an empty Rect.
334func (w *Workspace) hiddenColumnRects() []Rect {
335	v := *w
336	v.Columns = slices.Delete(slices.Clone(w.Columns), w.Focus, w.Focus+1)
337	v.Focus = 0
338	return slices.Insert(v.columnRectsFor(true), w.Focus, Rect{})
339}
340
341// stackRow draws up to two cards on each side of the provisional front,
342// fanned along axis.
343func (m *Monitor) stackRow(w *Workspace, axis layoutAxis, slot Rect, dim, lit bool) []Placement {
344	first, n := w.stackHead()
345	if n == 0 {
346		return w.previewRow(axis, slot, dim, lit)
347	}
348	if n == 1 {
349		return m.previewItem(w, first, axis, slot, dim, lit)
350	}
351	items := w.stack()
352	at := 0
353	if w == m.Current() {
354		at = slices.Index(items, m.stackFront(w))
355	}
356	cards := make([][]Placement, len(items))
357	step := w.peekStep()
358	// along is the offset of d steps on the overview axis.
359	along := func(d int) (int, int) {
360		var r Rect
361		axis.offset(&r, d*step)
362		return r.X, r.Y
363	}
364	for i, item := range items {
365		cards[i] = m.previewItem(w, item, axis, w.frontSlot(axis, slot), dim, lit)
366	}
367
368	// fan's behind side advances through the linear stack.
369	return fan(cards, at, 2, 2, false,
370		func(d int) (int, int) { return along(-d) }, along, dim, lit)
371}

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