1package core
2
3import (
4 "math"
5 "time"
6
7 "github.com/bnema/neferwl/internal/ports"
8)
9
10// The focus pulse is the pulse animation of the focus indicator
11// (focus.animation): it briefly shows the indicator's effect (focus.effect, a
12// light screen blend visible on black backgrounds too) on a window that just
13// got the keyboard focus. Only the last focus counts: it must hold for
14// pulseSettle before the pulse starts, a focus change stops a running one, and
15// the window that pulsed last does not pulse again within pulseCooldown
16// (switching back after another window held the focus is a new change).
17// Fast switching therefore shows nothing. Fullscreen windows, overview
18// previews, a window focused as it maps and a protected session never pulse.
19
20const (
21 pulseSettle = 150 * time.Millisecond
22 pulseRise = 90 * time.Millisecond
23 pulseFall = 230 * time.Millisecond
24 pulseCooldown = time.Second
25)
26
27// focusPulse is the pulse state. target is the window focus last seen and
28// since when; id is the window pulsing from start (0: none). last and lastAt
29// are the previous pulse, for the cooldown. drawable is whether the last
30// scene could show a pulse on target.
31type focusPulse struct {
32 target WindowID
33 since time.Time
34 id WindowID
35 start time.Time
36 last WindowID
37 lastAt time.Time
38 drawable bool
39 timerC <-chan time.Time
40 timerStop func() bool
41}
42
43func (c *Core) pulseOn() bool { return c.cfg.Focus.Animation == ports.FocusAnimationPulse }
44
45// pulseFocus follows the window focus. held is false while something else
46// (a popup grab, a layer, a lock surface) has the keyboard: the running pulse
47// stops, and the window getting it back is not a new focus. A new window
48// stops the running pulse and waits pulseSettle before one starts. With the
49// pulse off the focus is still followed, so turning it on pulses nothing.
50func (c *Core) pulseFocus(window WindowID, held bool) {
51 p := &c.pulse
52 if !held || !c.pulseOn() {
53 p.id = 0
54 c.stopPulseTimer()
55 if held {
56 p.target = window
57 }
58 return
59 }
60 if window == p.target {
61 return
62 }
63 p.target, p.id = window, 0
64 c.stopPulseTimer()
65 if window == 0 {
66 return
67 }
68 p.since = c.now()
69 if c.justMapped(window) {
70 return
71 }
72 p.timerC, p.timerStop = newTimer(c.ch.Clock, pulseSettle)
73}
74
75// justMapped reports whether the window got the focus as it mapped: opening
76// an app is not a focus change, and its first frames may still be on their
77// way. Its focus counts from the next change.
78func (c *Core) justMapped(id WindowID) bool {
79 p := &c.pulse
80 return p.target == id && p.since.Sub(c.windows.lookup(id).mappedAt) < pulseSettle
81}
82
83// pulseTick runs when the settle timer fired. It reports whether a pulse
84// started.
85func (c *Core) pulseTick() bool {
86 p := &c.pulse
87 p.timerC, p.timerStop = nil, nil
88 if p.target == 0 || !c.pulseOn() || !p.drawable || c.justMapped(p.target) {
89 return false
90 }
91 now := c.now()
92 if wait := pulseSettle - now.Sub(p.since); wait > 0 {
93 p.timerC, p.timerStop = newTimer(c.ch.Clock, wait)
94 return false
95 }
96 if p.target == p.last && now.Sub(p.lastAt) < pulseCooldown {
97 return false
98 }
99 p.id, p.start, p.last, p.lastAt = p.target, now, p.target, now
100 return true
101}
102
103// advancePulse is the effect of the running pulse at now: a quick sine rise
104// to focus.strength, then a smooth fall. It ends the pulse once over.
105func (c *Core) advancePulse(now time.Time) float64 {
106 p := &c.pulse
107 if p.id == 0 {
108 return 0
109 }
110 peak := c.cfg.Focus.Strength
111 t := now.Sub(p.start)
112 switch {
113 case t < 0:
114 return 0
115 case t < pulseRise:
116 return peak * math.Sin(float64(t)/float64(pulseRise)*math.Pi/2)
117 case t < pulseRise+pulseFall:
118 x := 1 - float64(t-pulseRise)/float64(pulseFall)
119 return peak * x * x * (3 - 2*x)
120 }
121 p.id = 0
122 return 0
123}
124
125// pulsing reports whether a pulse runs on the screen.
126func (c *Core) pulsing(sc *screen) bool {
127 return c.pulse.id != 0 && sc == c.cur()
128}
129
130func (c *Core) stopPulseTimer() {
131 p := &c.pulse
132 if p.timerStop != nil {
133 p.timerStop()
134 }
135 p.timerC, p.timerStop = nil, nil
136}
137
138// stopPulse ends the running pulse and its timer (security change). The
139// focus is kept: the window focused again after an unlock does not pulse.
140func (c *Core) stopPulse() {
141 c.stopPulseTimer()
142 c.pulse.id, c.pulse.drawable = 0, false
143}