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 rect, ok := c.flashRect(target, sc)
91 if !ok {
92 return false
93 }
94 if at, ok := f.lastStarts[target]; ok && now.Sub(at) < flashCooldown {
95 return false
96 }
97 if f.lastStarts == nil {
98 f.lastStarts = make(map[flashTarget]time.Time)
99 }
100 for key, at := range f.lastStarts {
101 if now.Sub(at) >= flashCooldown {
102 delete(f.lastStarts, key)
103 }
104 }
105 f.lastStarts[target] = now
106 f.running, f.rect, f.start = &target, rect, now
107 f.value = c.cfg.Focus.FlashOpacity
108 return true
109}
110
111func (c *Core) advanceFlash(only *screen) {
112 f := &c.flash
113 if f.running == nil {
114 return
115 }
116 if !c.flashOn() || c.screenIndex(f.running.output) < 0 {
117 c.stopFlash()
118 return
119 }
120 sc := c.screens[c.screenIndex(f.running.output)]
121 if rect, ok := c.flashRect(*f.running, sc); !ok || rect != f.rect {
122 c.stopFlash()
123 return
124 }
125 if only != nil && only.name() != f.running.output {
126 return
127 }
128 d := c.cfg.Focus.FlashDuration
129 elapsed := c.now().Sub(f.start)
130 if d <= 0 || elapsed >= d {
131 f.running = nil
132 f.value = 0
133 return
134 }
135 f.value = c.cfg.Focus.FlashOpacity * (1 - float64(max(elapsed, 0))/float64(d))
136}
137
138// flashRect resolves the current target, never retaining a border after its
139// visibility or geometry changes. stopFlash leaves the published sample intact
140// so the scene diff also damages the old border.
141func (c *Core) flashRect(target flashTarget, sc *screen) (Rect, bool) {
142 if sc.mon.ov.open {
143 return Rect{}, false
144 }
145 if target.level == flashMonitor {
146 o := sc.mon.Output()
147 return Rect{W: o.W, H: o.H}, true
148 }
149 owner, _ := c.screenOf(target.window)
150 if owner != sc || c.windows.lookup(target.window).client.ID == 0 {
151 return Rect{}, false
152 }
153 view := sc.mon.viewport()
154 c.flash.layout = sc.mon.layoutInto(c.flash.layout)
155 var rect Rect
156 found := false
157 for _, p := range c.flash.layout {
158 if p.ID == target.window {
159 if p.Fullscreen || p.Preview != 0 || !view.shows(p) {
160 return Rect{}, false
161 }
162 rect, found = p.Rect, true
163 break
164 }
165 }
166 if !found {
167 return Rect{}, false
168 }
169 if target.level == flashColumn {
170 w := sc.mon.Current()
171 col := w.columnOf(target.window)
172 if col < 0 {
173 return Rect{}, false
174 }
175 for _, p := range c.flash.layout {
176 if !p.Hidden && slices.Contains(w.Columns[col].Windows, p.ID) {
177 rect = union(rect, p.Rect)
178 }
179 }
180 }
181 return rect, true
182}
183
184func (c *Core) flashFor(sc *screen) *ports.FocusFlash {
185 f := &c.flash
186 if !c.flashing(sc) {
187 return nil
188 }
189 color := c.cfg.Focus.FlashColor
190 if color == "" {
191 color = c.cfg.Border.Active
192 }
193 sample := ports.FocusFlash{Rect: f.rect, Width: c.cfg.Focus.FlashWidth, Color: color, Alpha: f.value}
194 if f.published == nil || *f.published != sample {
195 f.published = &ports.FocusFlash{Rect: sample.Rect, Width: sample.Width, Color: sample.Color, Alpha: sample.Alpha}
196 }
197 return f.published
198}
199
200func (c *Core) flashing(sc *screen) bool {
201 return c.flash.running != nil && c.flash.running.output == sc.name()
202}
203
204func (c *Core) stopFlashTimer() {
205 if c.flash.timerStop != nil {
206 c.flash.timerStop()
207 }
208 c.flash.timerC, c.flash.timerStop = nil, nil
209}
210
211func (c *Core) stopFlash() {
212 c.stopFlashTimer()
213 c.flash.pending, c.flash.running, c.flash.value = nil, nil, 0
214}
215
216func (m *Monitor) settled() bool {
217 if m.switchView.busy() {
218 return false
219 }
220 for w := range m.all() {
221 if w.view.busy() || w.stashView.busy() {
222 return false
223 }
224 }
225 return true
226}
227
228func (s *screen) settled() bool { return s.mon.settled() && len(s.rects) == 0 }