v0.8.0 446d1e3

cascade.go RawHistoryFolder

1package core
2
3import "slices"
4
5func (w *Workspace) band(i int) int         { return i / max(w.MaxColumns, 1) }
6func (w *Workspace) sameBand(a, b int) bool { return !w.policy().wraps || w.band(a) == w.band(b) }
7
8// cellX is the x of column i in its lane, from the usable area's left edge.
9func (w *Workspace) cellX(i int) int {
10	first, last := w.laneBounds(w.laneOf(i))
11	S, u, sum := w.laneStats(first, last)
12	x := w.gap()
13	for j := first; j < i; j++ {
14		x += w.laneWidth(j, first, last, S, u, sum) + w.gap()
15	}
16	return x
17}
18func (w *Workspace) cascadeBands() (n, spacing int) {
19	if len(w.Columns) == 0 || w.Usable.H <= 0 {
20		return 1, 0
21	}
22	return w.band(len(w.Columns)-1) + 1, w.Usable.H
23}
24
25// cellWidth is the width of column i in its lane: the columns of a band
26// (the whole row in fixed overflow) share the usable width by their cells,
27// auto columns sharing what the others leave, so a lone column fills it.
28func (w *Workspace) cellWidth(i int) int {
29	first, last := w.laneBounds(w.laneOf(i))
30	S, u, sum := w.laneStats(first, last)
31	return w.laneWidth(i, first, last, S, u, sum)
32}
33
34// laneStats measures the lane first..last: S is the sum of the cells of
35// its set columns, u the count of its auto columns and sum the total of
36// their weights (laneWeight).
37func (w *Workspace) laneStats(first, last int) (S, u, sum int) {
38	for i := first; i <= last; i++ {
39		if c := w.Columns[i].Cells; c > 0 {
40			S += c
41		} else {
42			u++
43		}
44	}
45	for i := first; i <= last; i++ {
46		sum += w.laneWeight(i, S, u)
47	}
48	return S, u, sum
49}
50
51// laneWeight is the share of column i in its lane, given its stats. With
52// room left for every auto column, they share the cells the set ones leave
53// and the set ones keep their cells; otherwise an auto column weighs one
54// cell.
55func (w *Workspace) laneWeight(i, S, u int) int {
56	k := max(w.MaxColumns, 1)
57	c := w.Columns[i].Cells
58	switch {
59	case u == 0:
60		return c
61	case k-S >= u:
62		if c > 0 {
63			return c * u
64		}
65		return k - S
66	case c > 0:
67		return c
68	}
69	return 1
70}
71
72// laneWidth is the width of column i of the lane first..last with the
73// stats of laneStats: the usable width after gaps, split by weight. The
74// remainder under one pixel per weight stays empty, as in equalShare.
75func (w *Workspace) laneWidth(i, first, last, S, u, sum int) int {
76	if sum <= 0 {
77		return 0
78	}
79	avail := max(w.Usable.W-w.gap()*(last-first+2), 0)
80	// floor(avail*weight/sum) without overflowing on a huge area.
81	weight := w.laneWeight(i, S, u)
82	return avail/sum*weight + avail%sum*weight/sum
83}
84
85// equalShare is the width of each of k columns sharing the usable width
86// after gaps; a remainder under k pixels stays empty.
87func (w *Workspace) equalShare(k int) int {
88	k = max(k, 1)
89	return max((w.Usable.W-w.gap()*(k+1))/k, 0)
90}
91func (w *Workspace) cellInBand(b int) int {
92	k := max(w.MaxColumns, 1)
93	return min(b*k+w.Focus%k, len(w.Columns)-1)
94}
95func (w *Workspace) focusBand(dir int) bool {
96	i := w.laneNeighbor(dir)
97	if i < 0 {
98		return false
99	}
100	w.unmaximize()
101	w.Focus = i
102	w.Columns[i].Focus = 0
103	if dir < 0 {
104		w.Columns[i].Focus = len(w.Columns[i].Windows) - 1
105	}
106	w.raiseColumns()
107	w.scroll()
108	return true
109}
110func (w *Workspace) cascadeRectsInto(dst []Rect, ignoreFullWidth bool) []Rect {
111	g := w.gap()
112	rects := slices.Grow(dst[:0], len(w.Columns))[:len(w.Columns)]
113	view := w.View + w.shiftPixels()
114	for lane := 0; ; lane++ {
115		first, last := w.laneBounds(lane)
116		if first > last {
117			break
118		}
119		S, u, sum := w.laneStats(first, last)
120		x := g
121		for i := first; i <= last; i++ {
122			cw := w.laneWidth(i, first, last, S, u, sum)
123			r := Rect{X: w.Usable.X + x, Y: w.Usable.Y + g + w.band(i)*w.Usable.H - view, W: cw, H: max(w.Usable.H-2*g, 0)}
124			if w.Columns[i].FullWidth && !ignoreFullWidth {
125				r.X = w.Usable.X + g
126				r.W = max(w.Usable.W-2*g, 0)
127			}
128			rects[i] = r
129			x += cw + g
130		}
131	}
132	return rects
133}
134
135func (w *Workspace) cascadePoints() []float64 {
136	n, spacing := w.cascadeBands()
137	points := make([]float64, n)
138	for i := range points {
139		points[i] = float64(i * spacing)
140	}
141	return points
142}
143
144func (w *Workspace) cascadeFocus(view int) int {
145	if len(w.Columns) == 0 {
146		return 0
147	}
148	b := min(max(view/max(w.Usable.H, 1), 0), w.band(len(w.Columns)-1))
149	return w.cellInBand(b)
150}
151
152// laneOf is the lane of column i in the overview: its band, or the only
153// lane of a layout that does not wrap.
154func (w *Workspace) laneOf(i int) int {
155	if w.policy().wraps {
156		return w.band(i)
157	}
158	return 0
159}
160
161// laneBounds is the range of columns of a lane, first > last when the lane
162// does not exist. A layout that does not wrap has the whole row as lane 0.
163func (w *Workspace) laneBounds(lane int) (first, last int) {
164	k := len(w.Columns)
165	if w.policy().wraps {
166		k = max(w.MaxColumns, 1)
167	}
168	first = lane * k
169	if lane < 0 || first >= len(w.Columns) {
170		return 0, -1
171	}
172	return first, min(first+k, len(w.Columns)) - 1
173}
174
175// leavesBand reports whether column i, where a left/right move from the
176// focused column goes, is past the workspace or in another cascade band:
177// the move then goes to the neighbor monitor first.
178func (w *Workspace) leavesBand(i int) bool {
179	return i < 0 || i >= len(w.Columns) || !w.sameBand(i, w.Focus)
180}
181
182// moveBand swaps the focused cascade column with the column in the band dir
183// away where focus-window-up/down goes; focus follows it. Past the first
184// or last band nothing moves.
185func (w *Workspace) moveBand(dir int) {
186	i := w.laneNeighbor(dir)
187	if i < 0 {
188		return
189	}
190	w.unmaximize()
191	w.Columns[w.Focus], w.Columns[i] = w.Columns[i], w.Columns[w.Focus]
192	w.Focus = i
193	w.scroll()
194}
195
196// laneNeighbor is the closest column to the focus in the lane d lanes away,
197// the focus's offset in its lane kept; -1 past the first or last lane.
198func (w *Workspace) laneNeighbor(d int) int {
199	lane := w.laneOf(w.Focus)
200	first, _ := w.laneBounds(lane)
201	nfirst, nlast := w.laneBounds(lane + d)
202	if nfirst > nlast {
203		return -1
204	}
205	return min(nfirst+w.Focus-first, nlast)
206}

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