v0.8.1 1f54f61

copy_scroll_animation.go RawHistoryFolder

1package daemon
2
3import (
4	"time"
5
6	"github.com/bnema/vev/internal/ports"
7)
8
9const (
10	copyScrollFrame = 16 * time.Millisecond
11	copyScrollTail  = 120 * time.Millisecond
12	// copyScrollMaxStep bounds each frame drained after the animation was held
13	// on a full output window, so the backlog that built up during the stall
14	// resumes as motion rather than one catch-up jump.
15	copyScrollMaxStep = 9
16	// copyScrollMaxHeldFrames ends an animation held on a full output window
17	// for about one second: the link is effectively gone, and scrolling on its
18	// own once acknowledgements finally return would surprise the user.
19	copyScrollMaxHeldFrames = int(time.Second / copyScrollFrame)
20)
21
22// copyMu owns the animation and its single cancellable overlay-input timer.
23// Easing distributes requested rows over frames; it never invents extra travel.
24type copyScrollAnimation struct {
25	remaining int
26	lastInput time.Time
27	timer     pendingByteTimer
28	// heldFrames counts consecutive frames held on a full output window.
29	heldFrames int
30	// recovering caps the steps that drain the backlog left by a hold.
31	recovering bool
32}
33
34func (rt *overlayRuntime) stopCopyScrollLocked() {
35	rt.copyScroll.timer.stop()
36	rt.copyScroll = copyScrollAnimation{}
37}
38
39func (d *Daemon) smoothCopyWheel(sess *session, ac *attachedClient, delta int) {
40	if delta == 0 {
41		return
42	}
43	rt := ac.overlays
44	if d.clock == nil || d.currentCopyConfig().ReduceMotion {
45		rt.copyMu.Lock()
46		rt.stopCopyScrollLocked()
47		rt.copyMu.Unlock()
48		d.copyWheel(sess, ac, delta)
49		return
50	}
51	rt.copyMu.Lock()
52	if rt.copyMode == nil || rt.copyDocument == nil || rt.copyPane == nil {
53		rt.copyMu.Unlock()
54		return
55	}
56	// Selection stays exact and immediate. Reversal cancels the old tail so
57	// the next frame always follows the user's latest direction.
58	if rt.copyMode.Selection().Enabled || rt.copyDocument.Len() <= rt.copyDocument.Height() {
59		rt.stopCopyScrollLocked()
60		rt.copyMu.Unlock()
61		d.copyWheel(sess, ac, delta)
62		return
63	}
64	if rt.copyScroll.remaining != 0 && (rt.copyScroll.remaining < 0) != (delta < 0) {
65		rt.stopCopyScrollLocked()
66	}
67	motion := &rt.copyScroll
68	limit := rt.copyDocument.Len()
69	motion.remaining += min(max(delta, -limit-motion.remaining), limit-motion.remaining)
70	motion.lastInput = d.clock.Now()
71	changed, exit := false, false
72	if motion.timer.timer == nil {
73		changed, exit = d.advanceCopyScrollLocked(sess, ac)
74	}
75	rt.copyMu.Unlock()
76	if changed || exit {
77		d.invalidateRender(sess, ac, exit, "copy-scroll-animation")
78	}
79}
80
81// advanceCopyScrollLocked requires ac.overlays.copyMu, not d.mu.
82func (d *Daemon) advanceCopyScrollLocked(sess *session, ac *attachedClient) (bool, bool) {
83	rt := ac.overlays
84	motion := &rt.copyScroll
85	remaining := motion.remaining
86	if remaining == 0 {
87		return false, false
88	}
89	// While the client has a full window of unacknowledged frames, moving the
90	// viewport would only pile rows into one catch-up jump when the window
91	// reopens. Hold the animation and keep polling at frame pace instead.
92	changed, exit := false, false
93	if ac.output.atCapacity() {
94		motion.heldFrames++
95		motion.recovering = true
96		if motion.heldFrames > copyScrollMaxHeldFrames {
97			rt.stopCopyScrollLocked()
98			return false, false
99		}
100	} else {
101		motion.heldFrames = 0
102		magnitude := remaining
103		if magnitude < 0 {
104			magnitude = -magnitude
105		}
106		step := (magnitude + 3) / 4
107		if d.clock.Now().Sub(motion.lastInput) >= copyScrollTail {
108			step = magnitude
109		}
110		if motion.recovering {
111			step = min(step, copyScrollMaxStep)
112		}
113		if remaining < 0 {
114			step = -step
115		}
116		motion.remaining -= step
117		changed, exit = rt.moveCopyWheelLocked(step)
118		if !changed || exit {
119			rt.stopCopyScrollLocked()
120			return changed, exit
121		}
122		if motion.remaining == 0 {
123			// The held backlog is drained: the next scroll eases freely again.
124			motion.recovering = false
125		}
126	}
127	if motion.remaining != 0 {
128		mode := rt.copyMode
129		motion.timer.retain(d.clock, copyScrollFrame, func(timer ports.Timer) {
130			// Check presentation ownership before entering the copy lock. A
131			// callback never locks another overlay or a session under copyMu.
132			visible := ac.currentSession() == sess && !rt.promptActive() && !rt.paletteActive() &&
133				!rt.pickerClientActive() && !rt.noticesActive() && !rt.resizeModeActive()
134			rt.copyMu.Lock()
135			if rt.copyScroll.timer.timer != timer {
136				rt.copyMu.Unlock()
137				return
138			}
139			rt.copyScroll.timer.timer = nil
140			rt.copyScroll.timer.done = nil
141			if !visible || rt.copyMode != mode || rt.copySearch != nil {
142				rt.stopCopyScrollLocked()
143				rt.copyMu.Unlock()
144				return
145			}
146			changed, exit := d.advanceCopyScrollLocked(sess, ac)
147			rt.copyMu.Unlock()
148			if changed || exit {
149				d.invalidateRender(sess, ac, exit, "copy-scroll-animation")
150			}
151		})
152	}
153	return changed, exit
154}

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 12

Tags 11

↑↓ moveenter openesc close