1package selection
2
3import (
4 "context"
5 "math"
6 "sync/atomic"
7 "time"
8
9 "github.com/bnema/nefercap/internal/core"
10 "github.com/bnema/nefercap/internal/ports"
11)
12
13// Short effects confirm a key press: a toast names the new mode or target and
14// the selection border pulses. They only exist while the selector is open, so
15// they never reach a capture.
16const (
17 fxStep = 30 * time.Millisecond
18 toastFade = 9 * fxStep // o1..o9 after the hold
19 modeHold = 600 * time.Millisecond
20 targetHold = 400 * time.Millisecond
21 warnHold = 1500 * time.Millisecond
22 pulseSpan = 210 * time.Millisecond
23 pulseExtra = 3.0 // logical pixels added to the border at the pulse start
24 hudSteps = 4 // HUD fade length in fxSteps: 120 ms each way
25)
26
27// hudOpacity indexes opacityClasses by HUD fade level; level hudSteps is hidden.
28var hudOpacity = [hudSteps]int{0, 3, 5, 7}
29
30// opacityClasses are the toast steps of style.css, from opaque to faint.
31var opacityClasses = [...]string{"o0", "o1", "o2", "o3", "o4", "o5", "o6", "o7", "o8", "o9"}
32
33// Toast texts are constants so triggering one never allocates.
34const (
35 toastShot = "Screenshot mode"
36 toastRec = "Record mode"
37 toastRegion = "Region · drag to select"
38 toastMonitor = "Monitor"
39 toastWorkspace = "Workspace"
40 toastNoWS = "No workspace found · using monitor"
41 toastFrozen = "Frozen screen"
42 toastLive = "Live screen"
43)
44
45// toast is one centered message. start is zero until the first frame draws
46// it, so a slow frame does not eat the hold.
47type toast struct {
48 text string
49 warn bool
50 hold time.Duration
51 start time.Time
52 step int // index in opacityClasses; valid while on
53 on bool
54}
55
56// pulse thickens the selection border and lets it shrink back.
57type pulse struct {
58 start time.Time
59 on bool
60}
61
62// hudFade fades the chips, header, footer and toast out while a region is
63// dragged, and back in after. level counts steps away from fully shown.
64type hudFade struct {
65 hiding bool
66 from int // level when the current fade started
67 start time.Time // zero until the first frame of the fade
68 level int
69}
70
71// animator wakes its overlay every fxStep while an effect runs and sleeps
72// otherwise, so an idle selector does not redraw. until is written by the
73// overlay's owner loop and read by the animator goroutine.
74type animator struct {
75 kick chan struct{}
76 until atomic.Int64 // unix nanoseconds
77}
78
79func newAnimator() *animator { return &animator{kick: make(chan struct{}, 1)} }
80
81// extend keeps the overlay redrawing until t. A nil animator does nothing.
82func (a *animator) extend(t time.Time) {
83 if a == nil {
84 return
85 }
86 if n := t.UnixNano(); n > a.until.Load() {
87 a.until.Store(n)
88 }
89 select {
90 case a.kick <- struct{}{}:
91 default:
92 }
93}
94
95// run sends a non-blocking wake every fxStep until the effects end, then waits
96// for the next kick. It returns when ctx ends.
97func (a *animator) run(ctx context.Context, wake chan<- struct{}) {
98 t := time.NewTicker(fxStep)
99 t.Stop()
100 defer t.Stop()
101 for {
102 select {
103 case <-ctx.Done():
104 return
105 case <-a.kick:
106 }
107 t.Reset(fxStep)
108 // One step past the end draws the frame that removes the effect.
109 for time.Now().UnixNano() < a.until.Load()+int64(fxStep) {
110 select {
111 case <-ctx.Done():
112 return
113 case <-t.C:
114 select {
115 case wake <- struct{}{}:
116 default:
117 }
118 }
119 }
120 t.Stop()
121 }
122}
123
124// showToast replaces the current toast. It never allocates.
125func (m *model) showToast(text string, hold time.Duration, warn bool) {
126 m.toast = toast{text: text, warn: warn, hold: hold, on: true}
127 m.fx++
128}
129
130// pulseBorder restarts the border pulse.
131func (m *model) pulseBorder() {
132 m.pulse = pulse{on: true}
133 m.fx++
134}
135
136// targetToast is the toast that confirms a target key.
137func targetToast(kind core.PickKind) string {
138 switch kind {
139 case core.PickMonitor:
140 return toastMonitor
141 case core.PickWorkspace:
142 return toastWorkspace
143 }
144 return toastRegion
145}
146
147// freezeToast confirms an F press.
148func freezeToast(live bool) string {
149 if live {
150 return toastLive
151 }
152 return toastFrozen
153}
154
155func modeToast(mode ports.Mode) string {
156 if mode == ports.Record {
157 return toastRec
158 }
159 return toastShot
160}
161
162// animate advances the effects to now. The view calls it before every scene;
163// it never allocates.
164func (m *model) animate(now time.Time) {
165 m.edgeW = edge
166 m.fadeHUD(now)
167 if t := &m.toast; t.on {
168 if t.start.IsZero() {
169 t.start = now
170 m.anim.extend(now.Add(t.hold + toastFade))
171 }
172 el := now.Sub(t.start)
173 switch {
174 case el >= t.hold+toastFade:
175 t.on = false
176 case el <= t.hold:
177 t.step = 0
178 default:
179 t.step = min(1+int((el-t.hold)/fxStep), len(opacityClasses)-1)
180 }
181 }
182 if p := &m.pulse; p.on {
183 if p.start.IsZero() {
184 p.start = now
185 m.anim.extend(now.Add(pulseSpan))
186 }
187 el := now.Sub(p.start)
188 if el >= pulseSpan {
189 p.on = false
190 } else {
191 m.edgeW = edge + math.Round(pulseExtra*float64(pulseSpan-el)/float64(pulseSpan))
192 }
193 }
194}
195
196// fadeHUD moves the HUD fade one step per fxStep toward hidden while a region
197// is dragged and toward shown otherwise. A reversal starts from the current
198// level, so a short drag never jumps.
199func (m *model) fadeHUD(now time.Time) {
200 h := &m.hud
201 if hide := m.picker.Dragging(); hide != h.hiding {
202 h.hiding, h.from, h.start = hide, h.level, time.Time{}
203 }
204 target := 0
205 if h.hiding {
206 target = hudSteps
207 }
208 if h.level == target {
209 return
210 }
211 if h.start.IsZero() {
212 h.start = now
213 m.anim.extend(now.Add(hudSteps * fxStep))
214 }
215 n := 1 + int(now.Sub(h.start)/fxStep)
216 if target > h.from {
217 h.level = min(h.from+n, target)
218 } else {
219 h.level = max(h.from-n, target)
220 }
221}
222
223// hudStep is the opacityClasses index of the HUD this frame.
224func (m *model) hudStep() int { return hudOpacity[min(m.hud.level, hudSteps-1)] }
225
226// hudClass is the opacity class of the HUD this frame.
227func (m *model) hudClass() string { return opacityClasses[m.hudStep()] }
228
229// toastClass is the fainter of the toast's own fade and the HUD fade.
230func (m *model) toastClass() string { return opacityClasses[max(m.toast.step, m.hudStep())] }
231
232// hudHidden reports a fully faded HUD, which is not laid out or drawn.
233func (m *model) hudHidden() bool { return m.hud.level >= hudSteps }