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
69// advanceCopyScrollLocked requires ac.overlays.copyMu, not d.mu.
70func (d *Daemon) advanceCopyScrollLocked(sess *session, ac *attachedClient) (bool, bool) {
71 rt := ac.overlays
72 motion := &rt.copyScroll
73 remaining := motion.remaining
74 if remaining == 0 {
75 return false, false
76 }
77 magnitude := remaining
78 if magnitude < 0 {
79 magnitude = -magnitude
80 }
81 step := (magnitude + 3) / 4
82 if d.clock.Now().Sub(motion.lastInput) >= copyScrollTail {
83 step = magnitude
84 }
85 if remaining < 0 {
86 step = -step
87 }
88 motion.remaining -= step
89 changed, exit := rt.moveCopyWheelLocked(step)
90 if !changed || exit {
91 rt.stopCopyScrollLocked()
92 return changed, exit
93 }
94 if motion.remaining != 0 {
95 mode := rt.copyMode
96 motion.timer.retain(d.clock, copyScrollFrame, func(timer ports.Timer) {
97 // Check presentation ownership before entering the copy lock. A
98 // callback never locks another overlay or a session under copyMu.
99 visible := ac.currentSession() == sess && !rt.promptActive() && !rt.paletteActive() &&
100 !rt.pickerClientActive() && !rt.noticesActive() && !rt.resizeModeActive()
101 rt.copyMu.Lock()
102 if rt.copyScroll.timer.timer != timer {
103 rt.copyMu.Unlock()
104 return
105 }
106 rt.copyScroll.timer.timer = nil
107 rt.copyScroll.timer.done = nil
108 if !visible || rt.copyMode != mode || rt.copySearch != nil {
109 rt.stopCopyScrollLocked()
110 rt.copyMu.Unlock()
111 return
112 }
113 changed, exit := d.advanceCopyScrollLocked(sess, ac)
114 rt.copyMu.Unlock()
115 if changed || exit {
116 d.invalidateRender(sess, ac, exit, "copy-scroll-animation")
117 }
118 })
119 }
120 return changed, exit
121}