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, a window alone
18// in the usable area (e.g. a maximized column), overview previews, a window focused as
19// it maps and a protected session never pulse.
20
21const (
22 pulseSettle = 150 * time.Millisecond
23 pulseRise = 90 * time.Millisecond
24 pulseFall = 230 * time.Millisecond
25 pulseCooldown = time.Second
26 // pulseRecheck is how often a pulse waiting for a slide to land checks again.
27 pulseRecheck = pulseSettle / 3
28)
29
30// focusPulse is the pulse state. target is the window focus last seen and
31// since when; id is the window pulsing from start (0: none). last and lastAt
32// are the previous pulse, for the cooldown. drawable is whether the last
33// scene could show a pulse on target.
34type focusPulse struct {
35 target WindowID
36 since time.Time
37 id WindowID
38 start time.Time
39 last WindowID
40 lastAt time.Time
41 drawable bool
42 // value is the effect of the last sample, kept for scenes published
43 // between the focused output's own flips.
44 value float64
45 timerC <-chan time.Time
46 timerStop func() bool
47}
48
49func (c *Core) pulseOn() bool {
50 return c.animOn() && c.cfg.Focus.Animation == ports.FocusAnimationPulse
51}
52
53// pulseFocus follows the window focus. held is false while something else
54// (a popup grab, a layer, a lock surface) has the keyboard: the running pulse
55// stops, and the window getting it back is not a new focus. A new window
56// stops the running pulse and waits pulseSettle before one starts. With the
57// pulse off the focus is still followed, so turning it on pulses nothing.
58func (c *Core) pulseFocus(window WindowID, held bool) {
59 p := &c.pulse
60 if !held || !c.pulseOn() {
61 p.id = 0
62 c.stopPulseTimer()
63 if held {
64 p.target = window
65 }
66 return
67 }
68 if window == p.target {
69 return
70 }
71 p.target, p.id = window, 0
72 c.stopPulseTimer()
73 if window == 0 {
74 return
75 }
76 p.since = c.now()
77 if c.justMapped(window) {
78 return
79 }
80 p.timerC, p.timerStop = newTimer(c.opts.Clock, pulseSettle)
81}
82
83// justMapped reports whether the window got the focus as it mapped: opening
84// an app is not a focus change, and its first frames may still be on their
85// way. Its focus counts from the next change.
86func (c *Core) justMapped(id WindowID) bool {
87 p := &c.pulse
88 return p.target == id && p.since.Sub(c.windows.lookup(id).mappedAt) < pulseSettle
89}
90
91// pulseTick runs when the settle timer fired. It reports whether a pulse
92// started.
93func (c *Core) pulseTick() bool {
94 p := &c.pulse
95 p.timerC, p.timerStop = nil, nil
96 if p.target == 0 || !c.pulseOn() || !p.drawable || c.justMapped(p.target) {
97 return false
98 }
99 now := c.now()
100 if wait := pulseSettle - now.Sub(p.since); wait > 0 {
101 p.timerC, p.timerStop = newTimer(c.opts.Clock, wait)
102 return false
103 }
104 // A swipe or its landing slide still shows the neighbours: decide on
105 // the settled layout, or a window left alone would start a pulse cut
106 // short.
107 if !c.cur().settled() {
108 p.timerC, p.timerStop = newTimer(c.opts.Clock, pulseRecheck)
109 return false
110 }
111 if p.target == p.last && now.Sub(p.lastAt) < pulseCooldown {
112 return false
113 }
114 p.id, p.start, p.last, p.lastAt = p.target, now, p.target, now
115 return true
116}
117
118// advancePulse is the effect of the running pulse at now: a quick sine rise
119// to focus.strength, then a smooth fall. It ends the pulse once over.
120func (c *Core) advancePulse(now time.Time) float64 {
121 p := &c.pulse
122 if p.id == 0 {
123 return 0
124 }
125 peak := c.cfg.Focus.Strength
126 t := now.Sub(p.start)
127 switch {
128 case t < 0:
129 return 0
130 case t < pulseRise:
131 return peak * math.Sin(float64(t)/float64(pulseRise)*math.Pi/2)
132 case t < pulseRise+pulseFall:
133 x := 1 - float64(t-pulseRise)/float64(pulseFall)
134 return peak * x * x * (3 - 2*x)
135 }
136 p.id = 0
137 return 0
138}
139
140// settled reports whether no swipe or slide moves the monitor's layout
141// (screen.settled adds the rect motions).
142func (m *Monitor) settled() bool {
143 if m.switchView.busy() {
144 return false
145 }
146 for w := range m.all() {
147 if w.view.busy() || w.stashView.busy() {
148 return false
149 }
150 }
151 return true
152}
153
154// visibleCount is the number of windows of layout seen in v.
155func visibleCount(layout []Placement, v viewport) int {
156 n := 0
157 for _, p := range layout {
158 if p.Preview == 0 && v.shows(p) {
159 n++
160 }
161 }
162 return n
163}
164
165// pulsing reports whether a pulse runs on the screen.
166func (c *Core) pulsing(sc *screen) bool {
167 return c.pulse.id != 0 && sc == c.cur()
168}
169
170func (c *Core) stopPulseTimer() {
171 p := &c.pulse
172 if p.timerStop != nil {
173 p.timerStop()
174 }
175 p.timerC, p.timerStop = nil, nil
176}
177
178// stopPulse ends the running pulse and its timer (security change). The
179// focus is kept: the window focused again after an unlock does not pulse.
180func (c *Core) stopPulse() {
181 c.stopPulseTimer()
182 c.pulse.id, c.pulse.drawable = 0, false
183}