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)
13
14// copyMu owns the animation and its single cancellable overlay-input timer.
15// Easing distributes requested rows over frames; it never invents extra travel.
16type copyScrollAnimation struct {
17 remaining int
18 lastInput time.Time
19 timer pendingByteTimer
20}
21
22func (rt *overlayRuntime) stopCopyScrollLocked() {
23 rt.copyScroll.timer.stop()
24 rt.copyScroll = copyScrollAnimation{}
25}
26
27func (d *Daemon) smoothCopyWheel(sess *session, ac *attachedClient, delta int) {
28 if delta == 0 {
29 return
30 }
31 rt := ac.overlays
32 if d.clock == nil || d.currentCopyConfig().ReduceMotion {
33 rt.copyMu.Lock()
34 rt.stopCopyScrollLocked()
35 rt.copyMu.Unlock()
36 d.copyWheel(sess, ac, delta)
37 return
38 }
39 rt.copyMu.Lock()
40 if rt.copyMode == nil || rt.copyDocument == nil || rt.copyPane == nil {
41 rt.copyMu.Unlock()
42 return
43 }
44 // Selection stays exact and immediate. Reversal cancels the old tail so
45 // the next frame always follows the user's latest direction.
46 if rt.copyMode.Selection().Enabled || rt.copyDocument.Len() <= rt.copyDocument.Height() {
47 rt.stopCopyScrollLocked()
48 rt.copyMu.Unlock()
49 d.copyWheel(sess, ac, delta)
50 return
51 }
52 if rt.copyScroll.remaining != 0 && (rt.copyScroll.remaining < 0) != (delta < 0) {
53 rt.stopCopyScrollLocked()
54 }
55 motion := &rt.copyScroll
56 limit := rt.copyDocument.Len()
57 motion.remaining += min(max(delta, -limit-motion.remaining), limit-motion.remaining)
58 motion.lastInput = d.clock.Now()
59 changed, exit := false, false
60 if motion.timer.timer == nil {
61 changed, exit = d.advanceCopyScrollLocked(sess, ac)
62 }
63 rt.copyMu.Unlock()
64 if changed || exit {
65 d.invalidateRender(sess, ac, exit, "copy-scroll-animation")
66 }
67}
68
69func (d *Daemon) advanceCopyScrollLocked(sess *session, ac *attachedClient) (bool, bool) {
70 rt := ac.overlays
71 motion := &rt.copyScroll
72 remaining := motion.remaining
73 if remaining == 0 {
74 return false, false
75 }
76 magnitude := remaining
77 if magnitude < 0 {
78 magnitude = -magnitude
79 }
80 step := (magnitude + 3) / 4
81 if d.clock.Now().Sub(motion.lastInput) >= copyScrollTail {
82 step = magnitude
83 }
84 if remaining < 0 {
85 step = -step
86 }
87 motion.remaining -= step
88 changed, exit := rt.moveCopyWheelLocked(step)
89 if !changed || exit {
90 rt.stopCopyScrollLocked()
91 return changed, exit
92 }
93 if motion.remaining != 0 {
94 mode := rt.copyMode
95 motion.timer.retain(d.clock, copyScrollFrame, func(timer ports.Timer) {
96 // Check presentation ownership before entering the copy lock. A
97 // callback never locks another overlay or a session under copyMu.
98 visible := ac.currentSession() == sess && !rt.promptActive() && !rt.paletteActive() &&
99 !rt.pickerClientActive() && !rt.noticesActive() && !rt.resizeModeActive()
100 rt.copyMu.Lock()
101 if rt.copyScroll.timer.timer != timer {
102 rt.copyMu.Unlock()
103 return
104 }
105 rt.copyScroll.timer.timer = nil
106 rt.copyScroll.timer.done = nil
107 if !visible || rt.copyMode != mode || rt.copySearch != nil {
108 rt.stopCopyScrollLocked()
109 rt.copyMu.Unlock()
110 return
111 }
112 changed, exit := d.advanceCopyScrollLocked(sess, ac)
113 rt.copyMu.Unlock()
114 if changed || exit {
115 d.invalidateRender(sess, ac, exit, "copy-scroll-animation")
116 }
117 })
118 }
119 return changed, exit
120}