1package bar
2
3import (
4 "math"
5 "slices"
6 "strconv"
7 "time"
8
9 "github.com/bnema/neferclient"
10
11 "github.com/bnema/neferbar/internal/layout"
12 "github.com/bnema/neferbar/internal/module"
13 "github.com/bnema/neferbar/internal/popup"
14)
15
16// Evdev button codes.
17const (
18 btnLeft = 0x110
19 btnRight = 0x111
20 btnMiddle = 0x112
21)
22
23// pxPerStep is how far a continuous scroll (touchpad) moves for one step.
24const pxPerStep = 15
25
26// press is the latest button press sent to a module.
27type press struct {
28 m *module.Module // the module the press was sent to
29 token uint32 // the number the module echoes to refer to this press
30 serial uint32 // the pointer serial of the press, for popup grabs
31 at time.Time
32 used bool // a menu already answered this press
33}
34
35// menuPressTTL is how long after a press a module may still answer it with a
36// menu.
37const menuPressTTL = 5 * time.Second
38
39// takeMenuPress accepts a menu that module m opens in answer to the press
40// numbered token: it must be the latest press, sent to m, not older than
41// menuPressTTL, and not answered yet. The serial of the press is what the
42// popup grabs with. A press answers one menu.
43func (p *pointerState) takeMenuPress(m *module.Module, token uint32, now time.Time) (serial uint32, ok bool) {
44 pr := &p.press
45 if m == nil || pr.m != m || token == 0 || pr.token != token || pr.used || pr.serial == 0 || now.Sub(pr.at) > menuPressTTL {
46 return 0, false
47 }
48 pr.used = true
49 return pr.serial, true
50}
51
52// pointerState turns pointer events over the bar into the lines of the module
53// protocol. It owns no Wayland object: the bar loop feeds it columns.
54type pointerState struct {
55 hover *module.Module // the interactive module under the pointer
56 hoverOff int // its cell column
57 hoverGen uint64 // hover.Gen() when the hover line was sent
58 token uint32 // last click token
59 press press
60 wheel wheelAcc
61 buf [64]byte // the line being built
62}
63
64// send hands a line built in p.buf to m. A full queue drops the line: the
65// script is not reading and pointer events are not worth waiting for.
66func (p *pointerState) send(m *module.Module, line []byte) { m.Send(line) }
67
68// line starts a line in p.buf.
69func (p *pointerState) line(verb string) []byte { return append(p.buf[:0], verb...) }
70
71func appendInt(b []byte, v int) []byte {
72 b = append(b, ' ')
73 return strconv.AppendInt(b, int64(v), 10)
74}
75
76// forgetRestarted drops the hover of a module whose script restarted: the new
77// process was never told about it, so it gets no leave, and the next motion
78// sends it a hover.
79func (p *pointerState) forgetRestarted() {
80 if p.hover != nil && p.hover.Gen() != p.hoverGen {
81 p.hover, p.hoverOff = nil, 0
82 }
83}
84
85// motion handles Enter and Motion at cell column col of the bar row.
86func (p *pointerState) motion(lay *layout.Layout, col int) {
87 p.forgetRestarted()
88 m, off := interactiveAt(lay, col)
89 if m == p.hover && (m == nil || off == p.hoverOff) {
90 return
91 }
92 if p.hover != nil && m != p.hover {
93 p.send(p.hover, p.line("leave"))
94 }
95 p.hover, p.hoverOff = m, off
96 if m != nil {
97 p.hoverGen = m.Gen()
98 p.send(m, appendInt(p.line("hover"), off))
99 }
100}
101
102// leave handles the pointer leaving the bar.
103func (p *pointerState) leave() {
104 p.forgetRestarted()
105 if p.hover != nil {
106 p.send(p.hover, p.line("leave"))
107 }
108 p.hover, p.hoverOff = nil, 0
109 p.wheel.reset()
110}
111
112// button handles a button press at column col. serial is the press serial.
113func (p *pointerState) button(lay *layout.Layout, col int, code uint32, serial uint32) {
114 var name string
115 switch code {
116 case btnLeft:
117 name = "left"
118 case btnMiddle:
119 name = "middle"
120 case btnRight:
121 name = "right"
122 default:
123 return
124 }
125 m, off := interactiveAt(lay, col)
126 if m == nil {
127 return
128 }
129 p.token++
130 p.press = press{m: m, token: p.token, serial: serial, at: time.Now()}
131 b := append(p.line("click "), name...)
132 b = appendInt(b, off)
133 b = append(b, ' ')
134 b = strconv.AppendUint(b, uint64(p.token), 10)
135 p.send(m, b)
136}
137
138// axis handles one scroll event at column col.
139func (p *pointerState) axis(lay *layout.Layout, col int, axis uint32, value120 int32, delta float64) {
140 steps := p.wheel.add(axis, value120, delta)
141 if steps == 0 {
142 return
143 }
144 m, off := interactiveAt(lay, col)
145 if m == nil {
146 return
147 }
148 var dir string
149 switch {
150 case axis == neferclient.AxisVertical && steps > 0:
151 dir = "down"
152 case axis == neferclient.AxisVertical:
153 dir = "up"
154 case steps > 0:
155 dir = "right"
156 default:
157 dir = "left"
158 }
159 if steps < 0 {
160 steps = -steps
161 }
162 b := append(p.line("scroll "), dir...)
163 b = appendInt(b, int(steps))
164 b = appendInt(b, off)
165 p.send(m, b)
166}
167
168// interactiveAt is the interactive module at column col, and the column inside it.
169func interactiveAt(lay *layout.Layout, col int) (*module.Module, int) {
170 m, off, ok := lay.At(col)
171 if !ok || !m.Interactive {
172 return nil, 0
173 }
174 return m, off
175}
176
177// wheelAcc counts scroll steps. A wheel reports notches in 1/120 units; a
178// touchpad reports pixels. A compositor may send both for a wheel, so once a
179// 1/120 value was seen the pixels are ignored until the pointer leaves.
180type wheelAcc struct {
181 wheel bool
182 v120 [2]int32
183 px [2]float64
184}
185
186func (a *wheelAcc) reset() { *a = wheelAcc{} }
187
188// stop ends a touchpad scroll on axis: the pixels left over do not carry into
189// the next gesture.
190func (a *wheelAcc) stop(axis uint32) {
191 if axis <= 1 {
192 a.px[axis] = 0
193 }
194}
195
196// cellColumn is the cell column under logical x on a surface of the given
197// scale, for cells cellW physical pixels wide.
198func cellColumn(x, scale float64, cellW int) int {
199 return int(math.Floor(x * scale / float64(cellW)))
200}
201
202// add accumulates one event and returns the whole steps it completed:
203// positive for down or right, negative for up or left.
204func (a *wheelAcc) add(axis uint32, value120 int32, delta float64) int32 {
205 if axis > 1 {
206 return 0
207 }
208 if value120 != 0 {
209 a.wheel = true
210 a.v120[axis] += value120
211 steps := a.v120[axis] / 120
212 a.v120[axis] -= steps * 120
213 return steps
214 }
215 if a.wheel {
216 return 0
217 }
218 a.px[axis] += delta
219 steps := int32(a.px[axis] / pxPerStep)
220 a.px[axis] -= float64(steps) * pxPerStep
221 return steps
222}
223
224// inputRect is the logical-pixel rectangle of a span of cells: rounded
225// outward, full surface height.
226func inputRect(start, width, cellW int, scale float64, height int32) neferclient.Rect {
227 return popup.CellRect(start, width, cellW, scale, height)
228}
229
230// inputKey is everything but the spans that the input rectangles depend on.
231type inputKey struct {
232 cellW int
233 scale float64
234 height int32
235}
236
237// inputRegion tracks the spans the compositor was last told about.
238type inputRegion struct {
239 spans, last []layout.Span
240 rects []neferclient.Rect
241 key inputKey
242}
243
244// reset forgets what the compositor knows: a new surface starts click-through.
245func (r *inputRegion) reset() {
246 r.last = r.last[:0]
247 r.key = inputKey{}
248}
249
250// update collects the spans of the interactive modules and reports whether the
251// input rectangles differ from the last ones; if so rects holds the new ones
252// (non-nil, possibly empty). It allocates only while the slices grow.
253func (r *inputRegion) update(lay *layout.Layout, key inputKey) (rects []neferclient.Rect, changed bool) {
254 r.spans = lay.Spans(r.spans[:0])
255 r.spans = slices.DeleteFunc(r.spans, func(s layout.Span) bool { return !s.M.Interactive })
256 if key == r.key && slices.Equal(r.spans, r.last) {
257 return nil, false
258 }
259 if len(r.spans) == 0 && len(r.last) == 0 {
260 r.key = key
261 return nil, false
262 }
263 r.rects = r.rects[:0]
264 for _, s := range r.spans {
265 r.rects = append(r.rects, inputRect(s.Start, s.Width, key.cellW, key.scale, key.height))
266 }
267 if r.rects == nil {
268 r.rects = []neferclient.Rect{}
269 }
270 r.last = append(r.last[:0], r.spans...)
271 r.key = key
272 return r.rects, true
273}