detached fdd661a

border.go RawHistoryFolder

1package core
2
3type borderKind int
4
5const (
6	borderColumn borderKind = iota + 1
7	borderRow
8)
9
10// border is an output-local hit zone between consecutive columns or rows.
11type border struct {
12	kind     borderKind
13	col, row int
14	rect     Rect
15}
16
17// borderMinWidth is the minimum logical pixel width for a border resize.
18// The effective minimum also includes 5% of the lane and four border widths.
19const borderMinWidth = 48
20
21func borderZone(a, b, pad int) (int, int) {
22	mid := (a + b) / 2
23	lo, hi := min(a, mid-pad), max(b, mid+pad)
24	return lo, max(hi-lo, 1)
25}
26
27func (w *Workspace) borderAt(x, y, pad int) (border, bool) {
28	if w.fullscreen != 0 || !contains(w.Usable, x, y) {
29		return border{}, false
30	}
31	// Overview and preview are Monitor presentation, not Workspace state. Their
32	// caller must not ask for workspace borders while presenting those scenes.
33	for _, p := range w.Layout() {
34		if p.Floating && !p.Hidden && !p.Below && contains(p.Rect, x, y) {
35			return border{}, false
36		}
37	}
38	rs := w.columnRects()
39	fixedFull := w.Overflow == OverflowFixed && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth
40	for i, r := range rs {
41		if fixedFull && i != w.Focus {
42			continue
43		}
44		if r.X+r.W <= w.Usable.X || r.X >= w.Usable.X+w.Usable.W || r.Y+r.H <= w.Usable.Y || r.Y >= w.Usable.Y+w.Usable.H {
45			continue
46		}
47		rows := rowRects(r, w.Columns[i], w.gap())
48		for j := 0; j+1 < len(rows); j++ {
49			lo, size := borderZone(rows[j].Y+rows[j].H, rows[j+1].Y, pad)
50			b := border{kind: borderRow, col: i, row: j, rect: Rect{X: r.X, Y: lo, W: r.W, H: size}}
51			if contains(b.rect, x, y) {
52				return b, true
53			}
54		}
55		if fixedFull || w.Overflow == OverflowFixed && !w.laneRow() || i+1 >= len(rs) || w.laneOf(i) != w.laneOf(i+1) || w.Columns[i].FullWidth || w.Columns[i+1].FullWidth {
56			continue
57		}
58		next := rs[i+1]
59		if next.X >= w.Usable.X+w.Usable.W || next.X+next.W <= w.Usable.X {
60			continue
61		}
62		top, bottom := max(r.Y, next.Y), min(r.Y+r.H, next.Y+next.H)
63		lo, size := borderZone(r.X+r.W, next.X, pad)
64		b := border{kind: borderColumn, col: i, rect: Rect{X: lo, Y: top, W: size, H: max(bottom-top, 0)}}
65		if contains(b.rect, x, y) {
66			return b, true
67		}
68	}
69	return border{}, false
70}
71
72// pixelShare rounds upward so a stamped width reproduces its integer pixels.
73func pixelShare(pixels, total int) int {
74	if total <= 0 {
75		return 0
76	}
77	return (pixels*10000 + total - 1) / total
78}
79
80// setBorder moves border b to pos and reports whether any span or share
81// changed (stamping a lane or a column's shares counts as a change).
82func (w *Workspace) setBorder(b border, pos int) bool {
83	if b.col < 0 || b.col >= len(w.Columns) || w.fullscreen != 0 {
84		return false
85	}
86	rs := w.columnRects()
87	r := rs[b.col]
88	if b.kind == borderRow {
89		c := &w.Columns[b.col]
90		if b.row < 0 || b.row+1 >= len(c.Windows) || len(c.Windows)*minShare > 10000 {
91			return false
92		}
93		rows := rowRects(r, *c, w.gap())
94		avail := max(r.H-(len(rows)-1)*w.gap(), 0)
95		if avail == 0 {
96			return false
97		}
98		stamped := !validShares(*c)
99		if stamped {
100			c.Shares = make([]int, len(rows))
101			used := 0
102			for i := range rows {
103				// Stamp the middle of the pixel bucket, leaving room for
104				// pair-preserving rounding on subsequent resizes.
105				c.Shares[i] = (rows[i].H*10000 + 5000) / avail
106				used += c.Shares[i]
107			}
108			c.Shares[len(rows)-1] += 10000 - used
109		}
110		pair := c.Shares[b.row] + c.Shares[b.row+1]
111		if pair < 2*minShare {
112			return stamped
113		}
114		target := pixelShare(pos-w.gap()/2-rows[b.row].Y, avail)
115		target = min(max(target, minShare), pair-minShare)
116		if b.row+1 < len(rows)-1 {
117			target = pairPixelShare(target, pair, avail, rows[b.row].H+rows[b.row+1].H, minShare)
118		}
119		changed := stamped || c.Shares[b.row] != target
120		c.Shares[b.row], c.Shares[b.row+1] = target, pair-target
121		return changed
122	}
123	if b.kind != borderColumn || b.col+1 >= len(w.Columns) || w.laneOf(b.col) != w.laneOf(b.col+1) || w.Overflow == OverflowFixed && !w.laneRow() || w.Columns[b.col].FullWidth || w.Columns[b.col+1].FullWidth {
124		return false
125	}
126	first, last := w.laneBounds(w.laneOf(b.col))
127	avail := max(w.Usable.W-2*w.gap(), 0)
128	if w.policy().equalCells {
129		avail = max(w.Usable.W-w.gap()*(last-first+2), 0)
130	}
131	if avail == 0 {
132		return false
133	}
134	stamped := !w.validSpans(first, last)
135	if stamped {
136		// Mutates spans, so it must not cache validity across this loop.
137		w.spanSeq++
138		used := 0
139		for i := first; i <= last; i++ {
140			w.Columns[i].Span = max((w.columnWidthFor(i, true)*10000+5000)/avail, 1)
141			w.Columns[i].spanStamp = w.spanSeq
142			used += w.Columns[i].Span
143		}
144		if w.policy().equalCells {
145			w.Columns[last].Span += 10000 - used
146		}
147	}
148	minimum := pixelShare(max(avail/20, 4*w.border, borderMinWidth), avail)
149	target := pixelShare(pos-w.gap()/2-r.X, avail)
150	if !w.policy().equalCells {
151		span := min(max(target, min(minimum, 10000)), 10000)
152		changed := stamped || w.Columns[b.col].Span != span
153		w.Columns[b.col].Span = span
154		return changed
155	}
156	pair := w.Columns[b.col].Span + w.Columns[b.col+1].Span
157	if pair < 2*minimum {
158		return stamped
159	}
160	target = min(max(target, minimum), pair-minimum)
161	if b.col+1 < last {
162		target = pairPixelShare(target, pair, avail, rs[b.col].W+rs[b.col+1].W, minimum)
163	}
164	changed := stamped || w.Columns[b.col].Span != target
165	w.Columns[b.col].Span, w.Columns[b.col+1].Span = target, pair-target
166	return changed
167}
168
169func (w *Workspace) resetBorder(b border) {
170	if b.col < 0 || b.col >= len(w.Columns) {
171		return
172	}
173	switch b.kind {
174	case borderColumn:
175		w.clearSpans(b.col)
176	case borderRow:
177		w.Columns[b.col].Shares = nil
178	}
179}
180
181// pairPixelShare keeps the pair's combined integer width unchanged as well as
182// its basis-point sum. Independent floors can otherwise shift later tiles by
183// one pixel even when the sum of the two shares is constant.
184func pairPixelShare(target, pair, avail, pixels, minimum int) int {
185	left := avail * target / 10000
186	right := pixels - left
187	lo := max(pixelShare(left, avail), pair-pixelShare(right+1, avail)+1, minimum)
188	hi := min(pixelShare(left+1, avail)-1, pair-pixelShare(right, avail), pair-minimum)
189	if lo <= hi {
190		return min(max(target, lo), hi)
191	}
192	return target
193}

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 23

Tags 10

↑↓ moveenter openesc close