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 start time.Time
35 value float64 // Retained between this output's own flips.
36 lastStarts map[flashTarget]time.Time
37 layout []Placement // Owner scratch, never shared with a published scene.
38 published *ports.FocusFlash // Immutable last sample, reused when unchanged.
39}
40
41func (c *Core) flashOn() bool { return c.animOn() && c.cfg.Focus.Flash && !c.protectionRequested() }
42
43func (c *Core) requestFlash(level flashLevel, window WindowID) {
44 if !c.flashOn() {
45 return
46 }
47 c.stopFlash()
48 f := &c.flash
49 f.pending = &flashTarget{level: level, window: window, output: c.cur().name()}
50 f.since = c.now()
51 f.timerC, f.timerStop = newTimer(c.opts.Clock, flashSettle)
52}
53
54func (c *Core) flashTick() bool {
55 f := &c.flash
56 f.timerC, f.timerStop = nil, nil
57 if f.pending == nil {
58 return false
59 }
60 target := *f.pending
61 f.pending = nil
62 if !c.flashOn() {
63 return false
64 }
65 i := c.screenIndex(target.output)
66 if i < 0 {
67 return false
68 }
69 sc := c.screens[i]
70 if sc.mon.ov.open {
71 return false
72 }
73 now := c.now()
74 if target.level != flashMonitor {
75 owner, _ := c.screenOf(target.window)
76 record := c.windows.lookup(target.window)
77 if owner != sc || record.client.ID == 0 {
78 return false
79 }
80 }
81 wait := flashSettle - now.Sub(f.since)
82 if wait > 0 || !sc.settled() {
83 if wait <= 0 {
84 wait = flashRecheck
85 }
86 f.pending = &target
87 f.timerC, f.timerStop = newTimer(c.opts.Clock, wait)
88 return false
89 }
90 layout := sc.mon.Layout()
91 var rect Rect
92 if target.level == flashMonitor {
93 o := sc.mon.Output()
94 rect = Rect{W: o.W, H: o.H}
95 } else {
96 at := slices.IndexFunc(layout, func(p Placement) bool { return p.ID == target.window && !p.Hidden && p.Preview == 0 })
97 if at < 0 || layout[at].Fullscreen {
98 return false
99 }
100 rect = layout[at].Rect
101 if target.level == flashColumn {
102 w := sc.mon.Current()
103 col := w.columnOf(target.window)
104 if col < 0 {
105 return false
106 }
107 for _, p := range layout {
108 if p.Hidden || !slices.Contains(w.Columns[col].Windows, p.ID) {
109 continue
110 }
111 rect = union(rect, p.Rect)
112 }
113 }
114 }
115 if at, ok := f.lastStarts[target]; ok && now.Sub(at) < flashCooldown {
116 return false
117 }
118 if f.lastStarts == nil {
119 f.lastStarts = make(map[flashTarget]time.Time)
120 }
121 for key, at := range f.lastStarts {
122 if now.Sub(at) >= flashCooldown {
123 delete(f.lastStarts, key)
124 }
125 }
126 f.lastStarts[target] = now
127 f.running, f.rect, f.start = &target, rect, now
128 f.value = c.cfg.Focus.FlashOpacity
129 return true
130}
131
132func (c *Core) advanceFlash(only *screen) {
133 f := &c.flash
134 if f.running == nil {
135 return
136 }
137 if !c.flashOn() || c.screenIndex(f.running.output) < 0 {
138 c.stopFlash()
139 return
140 }
141 sc := c.screens[c.screenIndex(f.running.output)]
142 if !c.flashVisible(*f.running, sc) {
143 c.stopFlash()
144 return
145 }
146 if only != nil && only.name() != f.running.output {
147 return
148 }
149 d := c.cfg.Focus.FlashDuration
150 elapsed := c.now().Sub(f.start)
151 if d <= 0 || elapsed >= d {
152 f.running = nil
153 f.value = 0
154 return
155 }
156 f.value = c.cfg.Focus.FlashOpacity * (1 - float64(max(elapsed, 0))/float64(d))
157}
158
159// flashVisible checks the current layout, not leaving placements retained for
160// transitions: an old border must not outlive its target's visibility.
161func (c *Core) flashVisible(target flashTarget, sc *screen) bool {
162 if sc.mon.ov.open {
163 return false
164 }
165 if target.level == flashMonitor {
166 return true
167 }
168 owner, _ := c.screenOf(target.window)
169 if owner != sc || c.windows.lookup(target.window).client.ID == 0 {
170 return false
171 }
172 view := sc.mon.viewport()
173 c.flash.layout = sc.mon.layoutInto(c.flash.layout)
174 for _, p := range c.flash.layout {
175 if p.ID == target.window {
176 return !p.Fullscreen && p.Preview == 0 && view.shows(p)
177 }
178 }
179 return false
180}
181
182func (c *Core) flashFor(sc *screen) *ports.FocusFlash {
183 f := &c.flash
184 if !c.flashing(sc) {
185 return nil
186 }
187 color := c.cfg.Focus.FlashColor
188 if color == "" {
189 color = c.cfg.Border.Active
190 }
191 sample := ports.FocusFlash{Rect: f.rect, Width: c.cfg.Focus.FlashWidth, Color: color, Alpha: f.value}
192 if f.published == nil || *f.published != sample {
193 f.published = &ports.FocusFlash{Rect: sample.Rect, Width: sample.Width, Color: sample.Color, Alpha: sample.Alpha}
194 }
195 return f.published
196}
197
198func (c *Core) flashing(sc *screen) bool {
199 return c.flash.running != nil && c.flash.running.output == sc.name()
200}
201
202func (c *Core) stopFlashTimer() {
203 if c.flash.timerStop != nil {
204 c.flash.timerStop()
205 }
206 c.flash.timerC, c.flash.timerStop = nil, nil
207}
208
209func (c *Core) stopFlash() {
210 c.stopFlashTimer()
211 c.flash.pending, c.flash.running, c.flash.value = nil, nil, 0
212}
213
214func (m *Monitor) settled() bool {
215 if m.switchView.busy() {
216 return false
217 }
218 for w := range m.all() {
219 if w.view.busy() || w.stashView.busy() {
220 return false
221 }
222 }
223 return true
224}
225
226func (s *screen) settled() bool { return s.mon.settled() && len(s.rects) == 0 }