1package core
2
3import (
4 "slices"
5 "time"
6
7 "github.com/bnema/neferwl/internal/ports"
8)
9
10type flashLevel uint8
11
12const (
13 flashWindow flashLevel = iota
14 flashColumn
15 flashMonitor
16 flashSettle = 150 * time.Millisecond
17 flashRecheck = 50 * time.Millisecond
18 flashCooldown = time.Second
19)
20
21type flashTarget struct {
22 level flashLevel
23 window WindowID
24 output string
25}
26
27type focusFlash struct {
28 pending *flashTarget
29 since time.Time
30 timerC <-chan time.Time
31 timerStop func() bool
32 running *flashTarget
33 rect Rect
34 radius int // Corner radius of the target, as SceneWindow.Radius.
35 start time.Time
36 value float64 // Retained between this output's own flips.
37 lastStarts map[flashTarget]time.Time
38 layout []Placement // Owner scratch, never shared with a published scene.
39 published *ports.FocusFlash // Immutable last sample, reused when unchanged.
40}
41
42func (c *Core) flashOn() bool { return c.animOn() && c.cfg.Focus.Flash && !c.protectionRequested() }
43
44func (c *Core) requestFlash(level flashLevel, window WindowID) {
45 if !c.flashOn() {
46 return
47 }
48 c.stopFlash()
49 f := &c.flash
50 f.pending = &flashTarget{level: level, window: window, output: c.cur().name()}
51 f.since = c.now()
52 f.timerC, f.timerStop = newTimer(c.opts.Clock, flashSettle)
53}
54
55func (c *Core) flashTick() bool {
56 f := &c.flash
57 f.timerC, f.timerStop = nil, nil
58 if f.pending == nil {
59 return false
60 }
61 target := *f.pending
62 f.pending = nil
63 if !c.flashOn() {
64 return false
65 }
66 i := c.screenIndex(target.output)
67 if i < 0 {
68 return false
69 }
70 sc := c.screens[i]
71 if sc.mon.ov.open {
72 return false
73 }
74 now := c.now()
75 if target.level != flashMonitor {
76 owner, _ := c.screenOf(target.window)
77 record := c.windows.lookup(target.window)
78 if owner != sc || record.client.ID == 0 {
79 return false
80 }
81 }
82 wait := flashSettle - now.Sub(f.since)
83 if wait > 0 || !sc.settled() {
84 if wait <= 0 {
85 wait = flashRecheck
86 }
87 f.pending = &target
88 f.timerC, f.timerStop = newTimer(c.opts.Clock, wait)
89 return false
90 }
91 rect, radius, ok := c.flashRect(target, sc)
92 if !ok {
93 return false
94 }
95 if at, ok := f.lastStarts[target]; ok && now.Sub(at) < flashCooldown {
96 return false
97 }
98 if f.lastStarts == nil {
99 f.lastStarts = make(map[flashTarget]time.Time)
100 }
101 for key, at := range f.lastStarts {
102 if now.Sub(at) >= flashCooldown {
103 delete(f.lastStarts, key)
104 }
105 }
106 f.lastStarts[target] = now
107 f.running, f.rect, f.radius, f.start = &target, rect, radius, now
108 f.value = c.cfg.Focus.FlashOpacity
109 return true
110}
111
112func (c *Core) advanceFlash(only *screen) {
113 f := &c.flash
114 if f.running == nil {
115 return
116 }
117 if !c.flashOn() || c.screenIndex(f.running.output) < 0 {
118 c.stopFlash()
119 return
120 }
121 sc := c.screens[c.screenIndex(f.running.output)]
122 if rect, radius, ok := c.flashRect(*f.running, sc); !ok || rect != f.rect || radius != f.radius {
123 c.stopFlash()
124 return
125 }
126 if only != nil && only.name() != f.running.output {
127 return
128 }
129 d := c.cfg.Focus.FlashDuration
130 elapsed := c.now().Sub(f.start)
131 if d <= 0 || elapsed >= d {
132 f.running = nil
133 f.value = 0
134 return
135 }
136 f.value = c.cfg.Focus.FlashOpacity * (1 - float64(max(elapsed, 0))/float64(d))
137}
138
139// flashRect resolves the current target, never retaining a border after its
140// visibility or geometry changes. stopFlash leaves the published sample intact
141// so the scene diff also damages the old border.
142func (c *Core) flashRect(target flashTarget, sc *screen) (Rect, int, bool) {
143 if sc.mon.ov.open {
144 return Rect{}, 0, false
145 }
146 if target.level == flashMonitor {
147 o := sc.mon.Output()
148 return Rect{W: o.W, H: o.H}, 0, true
149 }
150 owner, _ := c.screenOf(target.window)
151 if owner != sc || c.windows.lookup(target.window).client.ID == 0 {
152 return Rect{}, 0, false
153 }
154 view := sc.mon.viewport()
155 c.flash.layout = sc.mon.layoutInto(c.flash.layout)
156 var rect Rect
157 radius := 0
158 found := false
159 for _, p := range c.flash.layout {
160 if p.ID == target.window {
161 if p.Fullscreen || p.Preview != 0 || !view.shows(p) {
162 return Rect{}, 0, false
163 }
164 // The flash sits inside the drawn border, hugging the body.
165 rect, found = c.clientRect(p), true
166 radius = max(windowRadius(p, sc.mon.Current().gap(), c.cfg.Border.Radius)-c.cfg.Border.Width, 0)
167 break
168 }
169 }
170 if !found {
171 return Rect{}, 0, false
172 }
173 if target.level == flashColumn {
174 w := sc.mon.Current()
175 col := w.columnOf(target.window)
176 if col < 0 {
177 return Rect{}, 0, false
178 }
179 for _, p := range c.flash.layout {
180 if !p.Hidden && slices.Contains(w.Columns[col].Windows, p.ID) {
181 rect = union(rect, c.clientRect(p))
182 }
183 }
184 }
185 return rect, radius, true
186}
187
188func (c *Core) flashFor(sc *screen) *ports.FocusFlash {
189 f := &c.flash
190 if !c.flashing(sc) {
191 return nil
192 }
193 color := c.cfg.Focus.FlashColor
194 if color == "" {
195 color = c.cfg.Border.Active
196 }
197 sample := ports.FocusFlash{Rect: f.rect, Width: c.cfg.Focus.FlashWidth, Radius: f.radius, Color: color, Alpha: f.value}
198 if f.published == nil || *f.published != sample {
199 // Allocate only for a changed sample; sample itself stays on the stack.
200 published := new(ports.FocusFlash)
201 *published = sample
202 f.published = published
203 }
204 return f.published
205}
206
207func (c *Core) flashing(sc *screen) bool {
208 return c.flash.running != nil && c.flash.running.output == sc.name()
209}
210
211func (c *Core) stopFlashTimer() {
212 if c.flash.timerStop != nil {
213 c.flash.timerStop()
214 }
215 c.flash.timerC, c.flash.timerStop = nil, nil
216}
217
218func (c *Core) stopFlash() {
219 c.stopFlashTimer()
220 c.flash.pending, c.flash.running, c.flash.value = nil, nil, 0
221}
222
223func (m *Monitor) settled() bool {
224 if m.switchView.busy() {
225 return false
226 }
227 for w := range m.all() {
228 if w.view.busy() || w.stashView.busy() {
229 return false
230 }
231 }
232 return true
233}
234
235func (s *screen) settled() bool { return s.mon.settled() && len(s.rects) == 0 }