v0.8.0 446d1e3

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().wraps && w.hiddenByMaximized(i) {
218		// A card behind a maximized cascade column takes the
219		// maximization; Escape gives it back (restoreRow).
220		w.transferMaximization(i)
221	} else if w.policy().equalCells && w.Focus != i && w.Focus < len(w.Columns) {
222		w.unmaximize()
223	}
224	w.Focus = i
225	w.scroll()
226}
227
228// apply commits the selected front card without changing other stack items.
229func (w *Workspace) apply(item stackItem, selected WindowID) {
230	// Picking a card is a user focus: a blocked parent gives it to its
231	// modal dialog.
232	defer w.focusModal(0)
233	switch item.kind {
234	case stackFloat:
235		w.FocusID(item.id)
236	case stackColumns:
237		i := w.columnOf(selected)
238		if w.overviewMaximized() && i == w.Focus {
239			i = -1
240		}
241		if i < 0 && len(w.Columns) > 0 {
242			i = w.Focus
243			if w.overviewMaximized() {
244				for next := range w.Columns {
245					if next != w.Focus {
246						i = next
247						break
248					}
249				}
250			}
251			c := w.Columns[i]
252			selected = c.Windows[c.Focus]
253		}
254		if selected != 0 {
255			if w.overviewMaximized() && i != w.Focus {
256				w.transferMaximization(i)
257			}
258			w.FocusID(selected)
259		}
260	case stackColumn:
261		i := w.columnOf(item.id)
262		if i < 0 {
263			return
264		}
265		if w.columnOf(selected) != i {
266			c := w.Columns[i]
267			selected = c.Windows[c.Focus]
268		}
269		if w.overviewMaximized() && i != w.Focus {
270			w.transferMaximization(i)
271		}
272		w.FocusID(selected)
273	}
274}
275
276// previewItem draws a group using screen geometry and a single card at its
277// configured size. A prior maximized column retains its full-width buffer.
278func (m *Monitor) previewItem(w *Workspace, item stackItem, axis layoutAxis, slot Rect, dim, lit bool) []Placement {
279	if item.kind == stackColumns {
280		if !w.overviewMaximized() {
281			return w.previewRow(axis, slot, dim, lit)
282		}
283		// The hidden columns fill the screen as if the maximized column,
284		// shown on its own card, were not there. In particular, an earlier
285		// maximized buffer scales into its unmaximized cell here.
286		g := w.gap()
287		rects := w.hiddenColumnRects()
288		var tiles []Placement
289		selected := m.hiddenColumn(w)
290		for i, c := range w.Columns {
291			if i == w.Focus {
292				continue
293			}
294			r := rects[i]
295			r.X -= w.Usable.X
296			r.Y -= w.Usable.Y
297			w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g)
298			for j, t := range w.rowBuf {
299				tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: i == selected && j == c.Focus})
300			}
301		}
302		sel := rects[selected]
303		sel.X -= w.Usable.X
304		return w.previewRowTiles(axis, slot, dim, lit, false, tiles, w.Usable.W, sel)
305	}
306	g := w.gap()
307	if item.kind == stackFloat && item.id == w.cover() {
308		// A fullscreen float shows at the size of a fullscreen column.
309		r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)}
310		return w.previewRowTiles(axis, slot, dim, lit, false, []Placement{{ID: item.id, Rect: r, Focused: true, Fullscreen: true}}, w.Usable.W, r)
311	}
312	if item.kind == stackFloat {
313		f := w.Floats[w.floatIndex(item.id)]
314		r := w.floatRect(f)
315		r.X -= w.Usable.X
316		r.Y -= w.Usable.Y
317		return w.previewRowTiles(axis, slot, dim, lit, false, []Placement{{ID: item.id, Rect: r, Floating: true, Focused: true}}, w.Usable.W, r)
318	}
319	i := w.columnOf(item.id)
320	if i < 0 {
321		return nil
322	}
323	c := w.Columns[i]
324	r := Rect{W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)}
325	r.X = (w.Usable.W - r.W) / 2
326	r.Y = (w.Usable.H - r.H) / 2
327	tiles := make([]Placement, 0, len(c.Windows))
328	w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g)
329	for j, t := range w.rowBuf {
330		tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus, Fullscreen: c.Windows[j] == w.cover()})
331	}
332	return w.previewRowTiles(axis, slot, dim, lit, false, tiles, w.Usable.W, r)
333}
334
335// hiddenColumnRects lays out the columns behind the maximized one with the
336// fixed layout rules, without the maximized column, so no hole is left
337// where it was. The maximized column's own entry is an empty Rect.
338func (w *Workspace) hiddenColumnRects() []Rect {
339	v := *w
340	v.Columns = slices.Delete(slices.Clone(w.Columns), w.Focus, w.Focus+1)
341	v.Focus = 0
342	return slices.Insert(v.columnRectsFor(true), w.Focus, Rect{})
343}
344
345// stackRow draws up to two cards on each side of the provisional front,
346// fanned along axis.
347func (m *Monitor) stackRow(w *Workspace, axis layoutAxis, slot Rect, dim, lit bool) []Placement {
348	first, n := w.stackHead()
349	if n == 0 {
350		return w.previewRow(axis, slot, dim, lit)
351	}
352	if n == 1 {
353		return m.previewItem(w, first, axis, slot, dim, lit)
354	}
355	items := w.stack()
356	at := 0
357	if w == m.Current() {
358		at = slices.Index(items, m.stackFront(w))
359	}
360	cards := make([][]Placement, len(items))
361	step := w.peekStep()
362	// along is the offset of d steps on the overview axis.
363	along := func(d int) (int, int) {
364		var r Rect
365		axis.offset(&r, d*step)
366		return r.X, r.Y
367	}
368	for i, item := range items {
369		cards[i] = m.previewItem(w, item, axis, w.frontSlot(axis, slot), dim, lit)
370	}
371
372	// fan's behind side advances through the linear stack.
373	return fan(cards, at, 2, 2, false,
374		func(d int) (int, int) { return along(-d) }, along, dim, lit)
375}

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 26

Tags 10

↑↓ moveenter openesc close