cascade.go RawHistoryFolder

1package core
2
3import "slices"
4
5// Cascade bands pack columns in order: a band ends when the next column
6// would take it past MaxColumns slots. A column takes one slot and a
7// maximized one all of them, so it sits alone in its band. Cells only
8// share a band's width (laneWidth); they never move a column to a band.
9
10// bandSlots is the number of slots column i takes in its band.
11func (w *Workspace) bandSlots(i int) int {
12	if w.Columns[i].FullWidth {
13		return max(w.MaxColumns, 1)
14	}
15	return 1
16}
17
18// walkBands calls f with each band and its column range, in order, until f
19// returns false.
20func (w *Workspace) walkBands(f func(b, first, last int) bool) {
21	k := max(w.MaxColumns, 1)
22	b, first, sum := 0, 0, 0
23	for i := range w.Columns {
24		c := w.bandSlots(i)
25		if i > first && sum+c > k {
26			if !f(b, first, i-1) {
27				return
28			}
29			b, first, sum = b+1, i, 0
30		}
31		sum += c
32	}
33	if len(w.Columns) > 0 {
34		f(b, first, len(w.Columns)-1)
35	}
36}
37
38// band is the cascade band of column i.
39func (w *Workspace) band(i int) (band int) {
40	w.walkBands(func(b, _, last int) bool {
41		band = b
42		return last < i
43	})
44	return band
45}
46
47// bandBounds is the column range of band b, first > last past the bands.
48func (w *Workspace) bandBounds(b int) (first, last int) {
49	first, last = 0, -1
50	if b < 0 {
51		return first, last
52	}
53	w.walkBands(func(n, f, l int) bool {
54		if n == b {
55			first, last = f, l
56		}
57		return n < b
58	})
59	return first, last
60}
61
62// cellX is the x of column i in its lane, from the usable area's left edge.
63func (w *Workspace) cellX(i int) int {
64	first, last := w.laneBounds(w.laneOf(i))
65	S, u, sum := w.laneStats(first, last)
66	x := w.gap()
67	for j := first; j < i; j++ {
68		x += w.laneWidth(j, first, last, S, u, sum) + w.gap()
69	}
70	return x
71}
72func (w *Workspace) cascadeBands() (n, spacing int) {
73	if len(w.Columns) == 0 || w.Usable.H <= 0 {
74		return 1, 0
75	}
76	return w.band(len(w.Columns)-1) + 1, w.Usable.H
77}
78
79// cellWidth is the width of column i in its lane: the columns of a band
80// (the whole row in fixed overflow) share the usable width by their cells,
81// auto columns sharing what the others leave, so a lone column fills it.
82func (w *Workspace) cellWidth(i int) int {
83	first, last := w.laneBounds(w.laneOf(i))
84	S, u, sum := w.laneStats(first, last)
85	return w.laneWidth(i, first, last, S, u, sum)
86}
87
88// laneStats measures the lane first..last: S is the sum of the cells of
89// its set columns, u the count of its auto columns and sum the total of
90// their weights (laneWeight).
91func (w *Workspace) laneStats(first, last int) (S, u, sum int) {
92	for i := first; i <= last; i++ {
93		if c := w.Columns[i].Cells; c > 0 {
94			S += c
95		} else {
96			u++
97		}
98	}
99	for i := first; i <= last; i++ {
100		sum += w.laneWeight(i, S, u)
101	}
102	return S, u, sum
103}
104
105// laneWeight is the share of column i in its lane, given its stats. With
106// room left for every auto column, they share the cells the set ones leave
107// and the set ones keep their cells; otherwise an auto column weighs one
108// cell.
109func (w *Workspace) laneWeight(i, S, u int) int {
110	k := max(w.MaxColumns, 1)
111	c := w.Columns[i].Cells
112	switch {
113	case u == 0:
114		return c
115	case k-S >= u:
116		if c > 0 {
117			return c * u
118		}
119		return k - S
120	case c > 0:
121		return c
122	}
123	return 1
124}
125
126// laneWidth is the width of column i of the lane first..last with the
127// stats of laneStats: the usable width after gaps, split by weight. The
128// remainder under one pixel per weight stays empty, as in equalShare.
129func (w *Workspace) laneWidth(i, first, last, S, u, sum int) int {
130	if sum <= 0 {
131		return 0
132	}
133	avail := max(w.Usable.W-w.gap()*(last-first+2), 0)
134	// floor(avail*weight/sum) without overflowing on a huge area.
135	weight := w.laneWeight(i, S, u)
136	return avail/sum*weight + avail%sum*weight/sum
137}
138
139// equalShare is the width of each of k columns sharing the usable width
140// after gaps; a remainder under k pixels stays empty.
141func (w *Workspace) equalShare(k int) int {
142	k = max(k, 1)
143	return max((w.Usable.W-w.gap()*(k+1))/k, 0)
144}
145
146// focusBand focuses the column in the band dir away; a maximized column
147// keeps its own band, so it stays maximized.
148func (w *Workspace) focusBand(dir int) bool {
149	i := w.laneNeighbor(dir)
150	if i < 0 {
151		return false
152	}
153	w.Focus = i
154	w.Columns[i].Focus = 0
155	if dir < 0 {
156		w.Columns[i].Focus = len(w.Columns[i].Windows) - 1
157	}
158	w.raiseColumns()
159	w.scroll()
160	return true
161}
162func (w *Workspace) cascadeRectsInto(dst []Rect, ignoreFullWidth bool) []Rect {
163	g := w.gap()
164	rects := slices.Grow(dst[:0], len(w.Columns))[:len(w.Columns)]
165	view := w.View + w.shiftPixels()
166	w.walkBands(func(lane, first, last int) bool {
167		S, u, sum := w.laneStats(first, last)
168		x := g
169		for i := first; i <= last; i++ {
170			cw := w.laneWidth(i, first, last, S, u, sum)
171			r := Rect{X: w.Usable.X + x, Y: w.Usable.Y + g + lane*w.Usable.H - view, W: cw, H: max(w.Usable.H-2*g, 0)}
172			if w.Columns[i].FullWidth && !ignoreFullWidth {
173				r.X = w.Usable.X + g
174				r.W = max(w.Usable.W-2*g, 0)
175			}
176			rects[i] = r
177			x += cw + g
178		}
179		return true
180	})
181	return rects
182}
183
184func (w *Workspace) cascadePoints() []float64 {
185	n, spacing := w.cascadeBands()
186	points := make([]float64, n)
187	for i := range points {
188		points[i] = float64(i * spacing)
189	}
190	return points
191}
192
193func (w *Workspace) cascadeFocus(view int) int {
194	if len(w.Columns) == 0 {
195		return 0
196	}
197	b := min(max(view/max(w.Usable.H, 1), 0), w.band(len(w.Columns)-1))
198	return w.laneNeighbor(b - w.band(w.Focus))
199}
200
201// laneOf is the lane of column i in the overview: its band, or the only
202// lane of a layout that does not wrap.
203func (w *Workspace) laneOf(i int) int {
204	if w.policy().wraps {
205		return w.band(i)
206	}
207	return 0
208}
209
210// laneBounds is the range of columns of a lane, first > last when the lane
211// does not exist. A layout that does not wrap has the whole row as lane 0.
212func (w *Workspace) laneBounds(lane int) (first, last int) {
213	if w.policy().wraps {
214		return w.bandBounds(lane)
215	}
216	if lane != 0 || len(w.Columns) == 0 {
217		return 0, -1
218	}
219	return 0, len(w.Columns) - 1
220}
221
222// leavesBand reports whether column i, where a left/right move from the
223// focused column goes, is past the workspace or in another cascade band:
224// the move then goes to the neighbor monitor first.
225func (w *Workspace) leavesBand(i int) bool {
226	return i < 0 || i >= len(w.Columns) || w.policy().wraps && w.band(i) != w.band(w.Focus)
227}
228
229// moveBand swaps the focused cascade column with the column in the band dir
230// away where focus-window-up/down goes; focus follows it. Past the first
231// or last band nothing moves. Columns keep their maximization: when the two
232// take a different number of slots, a swap would repack the bands around
233// them, so the focused column passes the whole adjacent band instead.
234func (w *Workspace) moveBand(dir int) {
235	i := w.laneNeighbor(dir)
236	if i < 0 {
237		return
238	}
239	if w.bandSlots(w.Focus) == w.bandSlots(i) {
240		w.Columns[w.Focus], w.Columns[i] = w.Columns[i], w.Columns[w.Focus]
241		w.Focus = i
242		w.scroll()
243		return
244	}
245	first, last := w.bandBounds(w.band(i))
246	col := w.Columns[w.Focus]
247	w.Columns = slices.Delete(w.Columns, w.Focus, w.Focus+1)
248	at := first // dir < 0: before the band above.
249	if dir > 0 {
250		at = last // After the band below, one index lower once removed.
251	}
252	w.Columns = slices.Insert(w.Columns, at, col)
253	w.Focus = at
254	w.scroll()
255}
256
257// laneNeighbor is the closest column to the focus in the lane d lanes away,
258// the focus's offset in its lane kept; -1 past the first or last lane.
259func (w *Workspace) laneNeighbor(d int) int {
260	lane := w.laneOf(w.Focus)
261	first, _ := w.laneBounds(lane)
262	nfirst, nlast := w.laneBounds(lane + d)
263	if nfirst > nlast {
264		return -1
265	}
266	return min(nfirst+w.Focus-first, nlast)
267}

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