v0.2.0 13c6a90

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	for i, c := range w.Columns {
36		if slices.Contains(c.Windows, id) {
37			return i
38		}
39	}
40	return -1
41}
42
43// stack lists what is on screen followed by what it hides, top to bottom.
44// A pinned fullscreen window is the front card, a float on its own, a
45// fixed tile with its column, so every window it hides stays reachable; a
46// stashed one is its card in the pile.
47func (w *Workspace) stack() []stackItem {
48	var items []stackItem
49	c, columns := WindowID(0), len(w.Columns) > 0
50	if w.pinned() {
51		c = w.cover()
52	}
53	switch {
54	case c == 0, w.stashIndex(c) >= 0:
55	case w.floatIndex(c) >= 0:
56		items = append(items, stackItem{stackFloat, c})
57	case w.overviewMaximized():
58		items = append(items, stackItem{stackColumn, c})
59	default:
60		// One card of all columns, its own marked fullscreen.
61		items = append(items, stackItem{kind: stackColumns})
62		columns = false
63	}
64	for i := len(w.Floats) - 1; i >= 0; i-- {
65		f := w.Floats[i]
66		if !f.below && w.coversFloat(f) {
67			items = append(items, stackItem{stackFloat, f.ID})
68		}
69	}
70	if (c == 0 || w.isFloat(c)) && w.overviewMaximized() {
71		items = append(items, stackItem{stackColumn, w.Columns[w.Focus].Windows[0]})
72	}
73	if columns {
74		items = append(items, stackItem{kind: stackColumns})
75	}
76	for i := len(w.Floats) - 1; i >= 0; i-- {
77		f := w.Floats[i]
78		if f.below && w.coversFloat(f) {
79			items = append(items, stackItem{stackFloat, f.ID})
80		}
81	}
82	return items
83}
84
85// itemOf resolves a clicked tile to the card currently containing it.
86func (w *Workspace) itemOf(id WindowID) (stackItem, bool) {
87	for _, item := range w.stack() {
88		switch item.kind {
89		case stackFloat:
90			if item.id == id {
91				return item, true
92			}
93		case stackColumn:
94			if i := w.columnOf(id); i >= 0 && i == w.columnOf(item.id) {
95				return item, true
96			}
97		case stackColumns:
98			if i := w.columnOf(id); i >= 0 && (!w.overviewMaximized() || i != w.Focus) {
99				return item, true
100			}
101		}
102	}
103	return stackItem{}, false
104}
105
106// stackFront resolves the provisional item against fresh workspace contents;
107// an invalidated selection falls back to the nearest surviving stack index.
108func (m *Monitor) stackFront(w *Workspace) stackItem {
109	items := w.stack()
110	if len(items) == 0 {
111		return stackItem{}
112	}
113	if m.ov.row == w {
114		for _, item := range items {
115			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) {
116				return item
117			}
118		}
119		return items[min(max(m.ov.frontAt, 0), len(items)-1)]
120	}
121	return items[0]
122}
123
124// setFront records a provisional card and its index for removal fallback.
125func (m *Monitor) setFront(w *Workspace, item stackItem) {
126	m.ov.row, m.ov.front = w, item
127	m.ov.frontAt = slices.Index(w.stack(), item)
128}
129
130// moveStack moves only within the current row. The caller changes workspace
131// when the linear stack has no item in that direction.
132func (m *Monitor) moveStack(d int) bool {
133	w := m.Current()
134	items := w.stack()
135	if len(items) < 2 {
136		return false
137	}
138	at := slices.Index(items, m.stackFront(w)) + d
139	if at < 0 || at >= len(items) {
140		return false
141	}
142	m.setFront(w, items[at])
143	m.ov.cardOf, m.ov.card = nil, 0
144	return true
145}
146
147// hiddenColumn resolves the provisional selection by window, not column
148// index. With no surviving selection, prefer the most recently maximized
149// hidden column, then the nearest prior non-maximized focus, then first.
150func (m *Monitor) hiddenColumn(w *Workspace) int {
151	if !w.overviewMaximized() {
152		return -1
153	}
154	if i := w.columnOf(m.ov.selected); i >= 0 && i != w.Focus && m.ov.row == w {
155		return i
156	}
157	if m.ov.row == w && m.ov.selectedAt >= 0 {
158		i := min(m.ov.selectedAt, len(w.Columns)-1)
159		if i == w.Focus {
160			if i > 0 {
161				i--
162			} else {
163				i++
164			}
165		}
166		return i
167	}
168	for _, id := range w.maximized {
169		if i := w.columnOf(id); i >= 0 && i != w.Focus {
170			return i
171		}
172	}
173	if i := w.columnOf(m.ov.rows[m.ov.from].focus); w == m.ov.from && i >= 0 && i != w.Focus {
174		return i
175	}
176	for i := range w.Columns {
177		if i != w.Focus {
178			return i
179		}
180	}
181	return -1
182}
183
184// selectOverviewColumn retains the existing ordinary-row live focus semantics.
185func (w *Workspace) selectOverviewColumn(i int) {
186	w.floatFocus, w.stashFocus = false, false
187	if w.Overflow == OverflowFixed && w.Focus != i && w.Focus < len(w.Columns) {
188		w.unmaximize()
189	}
190	w.Focus = i
191	w.scroll()
192}
193
194// apply commits the selected front card without changing other stack items.
195func (w *Workspace) apply(item stackItem, selected WindowID) {
196	switch item.kind {
197	case stackFloat:
198		w.FocusID(item.id)
199	case stackColumns:
200		i := w.columnOf(selected)
201		if w.overviewMaximized() && i == w.Focus {
202			i = -1
203		}
204		if i < 0 && len(w.Columns) > 0 {
205			i = w.Focus
206			if w.overviewMaximized() {
207				for next := range w.Columns {
208					if next != w.Focus {
209						i = next
210						break
211					}
212				}
213			}
214			c := w.Columns[i]
215			selected = c.Windows[c.Focus]
216		}
217		if selected != 0 {
218			if w.overviewMaximized() && i != w.Focus {
219				w.transferMaximization(i)
220			}
221			w.FocusID(selected)
222		}
223	case stackColumn:
224		i := w.columnOf(item.id)
225		if i < 0 {
226			return
227		}
228		if w.columnOf(selected) != i {
229			c := w.Columns[i]
230			selected = c.Windows[c.Focus]
231		}
232		if w.overviewMaximized() && i != w.Focus {
233			w.transferMaximization(i)
234		}
235		w.FocusID(selected)
236	}
237}
238
239// previewItem draws a group using screen geometry and a single card at its
240// configured size. A prior maximized column retains its full-width buffer.
241func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool) []Placement {
242	if item.kind == stackColumns {
243		if !w.overviewMaximized() {
244			return w.previewRow(y, dim, lit)
245		}
246		// The hidden columns fill the screen as if the maximized column,
247		// shown on its own card, were not there. In particular, an earlier
248		// maximized buffer scales into its unmaximized cell here.
249		g := w.gap()
250		rects := w.hiddenColumnRects()
251		var tiles []Placement
252		selected := m.hiddenColumn(w)
253		for i, c := range w.Columns {
254			if i == w.Focus {
255				continue
256			}
257			r := rects[i]
258			r.X -= w.Usable.X
259			r.Y -= w.Usable.Y
260			for j, t := range rowRects(r, c, g) {
261				tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: i == selected && j == c.Focus})
262			}
263		}
264		sel := rects[selected]
265		sel.X -= w.Usable.X
266		return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, sel)
267	}
268	g := w.gap()
269	if item.kind == stackFloat && item.id == w.cover() {
270		// A fullscreen float shows at the size of a fullscreen column.
271		r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)}
272		return w.previewRowTiles(y, dim, lit, []Placement{{ID: item.id, Rect: r, Focused: true, Fullscreen: true}}, w.Usable.W, r)
273	}
274	if item.kind == stackFloat {
275		f := w.Floats[w.floatIndex(item.id)]
276		r := w.floatRect(f)
277		r.X -= w.Usable.X
278		r.Y -= w.Usable.Y
279		return w.previewRowTiles(y, dim, lit, []Placement{{ID: item.id, Rect: r, Floating: true, Focused: true}}, w.Usable.W, r)
280	}
281	i := w.columnOf(item.id)
282	if i < 0 {
283		return nil
284	}
285	c := w.Columns[i]
286	r := Rect{W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)}
287	r.X = (w.Usable.W - r.W) / 2
288	r.Y = (w.Usable.H - r.H) / 2
289	tiles := make([]Placement, 0, len(c.Windows))
290	for j, t := range rowRects(r, c, g) {
291		tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus, Fullscreen: c.Windows[j] == w.cover()})
292	}
293	return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, r)
294}
295
296// hiddenColumnRects lays out the columns behind the maximized one with the
297// fixed layout rules, without the maximized column, so no hole is left
298// where it was. The maximized column's own entry is an empty Rect.
299func (w *Workspace) hiddenColumnRects() []Rect {
300	v := *w
301	v.Columns = slices.Delete(slices.Clone(w.Columns), w.Focus, w.Focus+1)
302	v.Focus = 0
303	return slices.Insert(v.columnRectsFor(true), w.Focus, Rect{})
304}
305
306// stackRow draws up to two cards on each side of the provisional front.
307func (m *Monitor) stackRow(w *Workspace, y int, dim, lit bool) []Placement {
308	items := w.stack()
309	if len(items) == 0 {
310		return w.previewRow(y, dim, lit)
311	}
312	if len(items) == 1 {
313		return m.previewItem(w, items[0], y, dim, lit)
314	}
315	at := 0
316	if w == m.Current() {
317		at = slices.Index(items, m.stackFront(w))
318	}
319	cards := make([][]Placement, len(items))
320	step := w.peekStep()
321	for i, item := range items {
322		cards[i] = m.previewItem(w, item, w.frontRowY(y), dim, lit)
323	}
324
325	// fan's behind side advances through the linear stack.
326	return fan(cards, at, 2, 2, false,
327		func(d int) (int, int) { return 0, -d * step },
328		func(d int) (int, int) { return 0, d * step }, dim, lit)
329}

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