feat/data-dnd e05cb15

stash.go RawHistoryFolder

1package core
2
3import (
4	"cmp"
5	"math"
6	"slices"
7
8	"github.com/bnema/neferwl/internal/ports"
9)
10
11// The stash is a workspace's strip of windows set aside with
12// toggle-window-stash, left to right in arrival order. The selected one
13// is centred, stash.width% of the usable width and stashHeight% of its
14// height; its neighbors fill the margins beside it, stash.gap% away,
15// the sides, dimmed; the others wait off screen. toggle-stash-visible
16// hides and shows the whole strip. Native floats (dialogs) are not in it.
17
18// stashWidth is the default share of the usable width, in percent, a
19// stashed window takes (stash.width); stashHeight is its share of the
20// usable height.
21const stashWidth, stashHeight = 80, 80
22
23func (w *Workspace) stashIndex(id WindowID) int {
24	return slices.IndexFunc(w.Stash, func(f Float) bool { return f.ID == id })
25}
26
27// isFloat reports whether the window floats: native or stashed.
28func (w *Workspace) isFloat(id WindowID) bool {
29	return w.floatIndex(id) >= 0 || w.stashIndex(id) >= 0
30}
31
32// stashFocused reports whether the stash has the focus, under any native
33// float that has it. A shown stash without tiles always has it.
34func (w *Workspace) stashFocused() bool {
35	return (w.stashFocus || len(w.Columns) == 0) && !w.stashHidden && len(w.Stash) > 0
36}
37
38// onFloat reports whether the focus is on a floating window: column
39// actions do nothing then.
40func (w *Workspace) onFloat() bool { return w.floatFocus || w.stashFocused() }
41
42// stashWindow moves the focused tile id to the end of the stash,
43// remembering its place in its column to return there.
44func (w *Workspace) stashWindow(id WindowID) {
45	back := w.tilePlace(id)
46	w.RemoveWindow(id)
47	w.addStash(Float{ID: id, back: back})
48}
49
50// tilePlace is where the focused tile id is in its column, for restore.
51func (w *Workspace) tilePlace(id WindowID) *origPlace {
52	c := w.Columns[w.Focus]
53	lone := len(c.Windows) == 1
54	back := &origPlace{col: w.Focus, row: c.Focus, slot: c.Slot, cells: c.Cells, fullWidth: c.FullWidth && lone}
55	for _, v := range c.Windows {
56		if v != id {
57			back.stacked = append(back.stacked, v)
58		}
59	}
60	return back
61}
62
63// addStash appends a window to the stash, shows the stash and selects it.
64func (w *Workspace) addStash(f Float) {
65	w.Stash = append(w.Stash, f)
66	w.stashAt = len(w.Stash) - 1
67	w.showStash()
68}
69
70// captureStash puts a new window id in the shown stash, selected, and
71// reports whether it did. Like a new tile, it leaves a focused native
72// float focused. Under a covering fullscreen window the stash is not on
73// screen: the window tiles as usual.
74func (w *Workspace) captureStash(id WindowID) bool {
75	if id == 0 || w.stashHidden || len(w.Stash) == 0 || w.cover() != 0 {
76		return false
77	}
78	// No former column: toggle-window-stash tiles it as a new column.
79	w.Stash = append(w.Stash, Float{ID: id})
80	w.stashAt, w.stashFocus = len(w.Stash)-1, true
81	return true
82}
83
84// stashSpawned puts a window opened by a stash spawn in the stash, selected
85// and focused, with no former column: toggle-window-stash tiles it as a new
86// column. A fullscreen stash member, current or waiting for the stash to
87// show, ends first, so the newcomer is the window on screen. Under a
88// covering fullscreen window outside the stash, the stash shows over it.
89func (w *Workspace) stashSpawned(id WindowID) {
90	if w.fullscreen != 0 && w.stashIndex(w.fullscreen) >= 0 {
91		w.endFullscreen()
92	}
93	w.hiddenFullscreen = 0
94	w.addStash(Float{ID: id})
95	if w.cover() != 0 {
96		w.showStashOver()
97	}
98}
99
100// showStash shows the stash and gives it the focus; a member whose
101// fullscreen waited while hidden is fullscreen again, selected, as if it
102// asked now.
103func (w *Workspace) showStash() {
104	w.stashHidden, w.stashFocus, w.floatFocus = false, true, false
105	if i := w.stashIndex(w.hiddenFullscreen); i >= 0 {
106		w.fullscreen, w.stashAt = w.hiddenFullscreen, i
107	}
108	w.hiddenFullscreen = 0
109}
110
111// unstash returns stash entry i to its former column, focused; a free
112// float floats again where it was.
113func (w *Workspace) unstash(i int) {
114	f := w.Stash[i]
115	w.RemoveWindow(f.ID)
116	if f.free {
117		w.Floats = append(w.Floats, f)
118		w.floatFocus, w.stashFocus = true, false
119		return
120	}
121	w.restore(f.ID, f.back)
122}
123
124// holdsStack reports whether c holds a window of the stack left behind.
125func (p origPlace) holdsStack(c Column) bool {
126	return slices.ContainsFunc(c.Windows, func(v WindowID) bool { return slices.Contains(p.stacked, v) })
127}
128
129// restore tiles id back where back says: its row in the column that still
130// holds its former neighbors, else a column of its own at its former
131// index. It takes the focus.
132func (w *Workspace) restore(id WindowID, back *origPlace) {
133	w.dropStashOver()
134	w.floatFocus, w.stashFocus = false, false
135	w.raiseColumns()
136	if back == nil {
137		w.addColumn(Column{Windows: []WindowID{id}})
138		return
139	}
140	at := min(back.col, len(w.Columns))
141	if i := slices.IndexFunc(w.Columns, back.holdsStack); i >= 0 {
142		c := &w.Columns[i]
143		row := 0
144		for _, v := range back.stacked[:back.row] {
145			if slices.Contains(c.Windows, v) {
146				row++
147			}
148		}
149		c.Windows = slices.Insert(c.Windows, row, id)
150		c.Focus = row
151		if w.policy().maximizeHides() && w.Focus != i {
152			w.unmaximize()
153		}
154		w.Focus = i
155		w.scroll()
156		return
157	}
158	// Its column is gone: it comes back at its width.
159	w.insertColumn(at, Column{Windows: []WindowID{id}, Cells: back.cells, Slot: back.slot})
160	if back.fullWidth {
161		w.maximize(w.Focus)
162	}
163}
164
165// rehome is a stashed window moved to another workspace: its former column
166// is not there, so it returns as the last column, at its width.
167func (f Float) rehome() Float {
168	back := &origPlace{col: math.MaxInt}
169	if f.back != nil {
170		back.cells = f.back.cells
171	}
172	f.back = back
173	return f
174}
175
176// ToggleStashVisible hides the stash, or shows it and gives it the
177// focus. Native floats stay as they are. Over a covering fullscreen
178// window it is a user override: the stash shows over it, which stays
179// fullscreen behind (composed, no scanout) and returns when the stash
180// hides. A stashed fullscreen window is the stash on screen: no-op.
181func (w *Workspace) ToggleStashVisible() {
182	if len(w.Stash) == 0 {
183		return
184	}
185	if c := w.cover(); c != 0 {
186		if w.stashIndex(c) >= 0 {
187			return
188		}
189		if w.stashOverCover() {
190			w.dropStashOver()
191			return
192		}
193		w.showStashOver()
194		return
195	}
196	if w.stashHidden {
197		w.showStash()
198		return
199	}
200	w.stashHidden, w.stashFocus = true, false
201}
202
203// showStashOver shows the stash over the covering fullscreen window,
204// focused. Not showStash: a pending stashed fullscreen must not replace
205// the covering window.
206func (w *Workspace) showStashOver() {
207	w.stashOver, w.stashHidden, w.stashFocus, w.floatFocus = w.cover(), false, true, false
208}
209
210// stashOverCover reports whether the stash shows over the covering
211// fullscreen window (ToggleStashVisible, Activate).
212func (w *Workspace) stashOverCover() bool {
213	return w.stashOver != 0 && w.stashOver == w.cover() && !w.stashHidden && len(w.Stash) > 0
214}
215
216// stashShown reports whether the stash is on screen: it holds windows, is
217// not hidden, and no covering fullscreen window hides it.
218func (w *Workspace) stashShown() bool {
219	return len(w.Stash) > 0 && !w.stashHidden && (w.cover() == 0 || w.stashOverCover())
220}
221
222// dropStashOver ends the stash override when the focus leaves it: the
223// stash hides again under the covering window, as it was.
224func (w *Workspace) dropStashOver() {
225	if w.stashOverCover() {
226		w.stashHidden, w.stashFocus = true, false
227	}
228	w.stashOver = 0
229}
230
231// Click focuses the clicked window. A click on a tile behind the shown
232// stash hides the stash, as a click outside a menu closes it.
233func (w *Workspace) Click(id WindowID) {
234	if !w.stashHidden && len(w.Stash) > 0 && !w.isFloat(id) && w.cover() == 0 {
235		w.stashHidden = true
236	}
237	w.FocusID(id)
238	w.focusModal(0)
239}
240
241// stashRect is where the selected stashed window is, border included.
242func (w *Workspace) stashRect() Rect {
243	u := w.Usable
244	fw := min(max(u.W*cmp.Or(w.stashWidth, stashWidth)/100-2*w.border, 0)+2*w.border, u.W)
245	fh := min(max(u.H*stashHeight/100-2*w.border, 0)+2*w.border, u.H)
246	return Rect{X: u.X + (u.W-fw)/2, Y: u.Y + (u.H-fh)/2, W: fw, H: fh}
247}
248
249// appendStash appends the placements of the stash to out: the selected window centred, its
250// neighbors gap% of the usable width away, showing what the margins
251// leave of them, the others hidden. cover is the covering fullscreen
252// window, if any.
253func (w *Workspace) appendStash(out []Placement, focusedID, cover WindowID) []Placement {
254	u := w.Usable
255	center := w.stashRect()
256	fw, fh := center.W, center.H
257	// Neighbors rest in the margins, never over the selected window: the
258	// side with the least room says whether there is one.
259	margin := (u.W-fw)/2 - w.stashGapPx()
260	// The view is moved by stashView.off pitches, rounded once so every
261	// window shares the offset.
262	pitch := w.stashPitch(fw)
263	shift := int(math.Round(w.stashView.off * float64(pitch)))
264	for i, f := range w.Stash {
265		p := Placement{ID: f.ID, Floating: true, Focused: f.ID == focusedID, Inset: ports.SideAll}
266		d := i - w.stashAt
267		r := Rect{X: center.X + d*pitch - shift, Y: center.Y, W: fw, H: fh}
268		switch {
269		case f.ID == cover:
270			p.Rect, p.Fullscreen, p.Inset = w.Output, true, 0
271		case w.stashHidden || cover != 0 && !w.stashOverCover():
272			p.Hidden = true
273		case d == 0:
274			// The selection is no peek, but its veil grows as it slides.
275			p.Rect, p.Veil = r, w.stashVeil(d)
276		case (shift != 0 || margin > 0 && (d == -1 || d == 1)) && r.Overlaps(u):
277			// A neighbor shows in its margin, if it leaves one. A slide
278			// shows what the view moves in, and keeps what it moves out
279			// until it is gone.
280			p.Rect = r
281			p.peeking(w.stashVeil(d))
282		default:
283			p.Hidden = true
284		}
285		out = append(out, p)
286	}
287	return out
288}
289
290// stashGapPx is the space between the selected stash window and its
291// neighbors, in pixels.
292func (w *Workspace) stashGapPx() int { return w.Usable.W * w.stashGap / 100 }
293
294// stashPitch is the distance between two stash windows of width fw.
295func (w *Workspace) stashPitch(fw int) int { return max(fw+w.stashGapPx(), 1) }
296
297// stashSpring lands a stash slide from off (in stash windows) with the
298// speed velocity; it snaps under half a pixel like the other slides.
299func (w *Workspace) stashSpring(off, velocity float64) spring {
300	s := workspaceSpring(off, velocity)
301	s.Snap = pixelSnap / float64(w.stashPitch(w.stashRect().W))
302	return s
303}
304
305// stashVeil is how much of the peek veil the stash window d places away
306// from stashAt draws: none at the centre of the view, all of it one window
307// away or more.
308func (w *Workspace) stashVeil(d int) float64 {
309	return min(math.Abs(float64(d)-w.stashView.off), 1)
310}

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