fix(bar): reset pointer state on surface recreation, axis stop and script restart
- commit
- e357010b852643435b56c9dc7e7bc0141db4257a
- author
- brice · · 2026-10-09 04:00 UTC
- parent
- 487baa8
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +190 -2
M internal/bar/bar.go +21 -2
| @@ -506,12 +506,20 @@ func (b *Bar) createSurface(h int32) error { | |||
| 506 | 506 | } | |
| 507 | 507 | b.surf, b.sid = surf, surf.ID() | |
| 508 | 508 | b.mappedW = 0 | |
| 509 | - | b.input.reset() // the new surface is click-through | |
| 509 | + | b.resetInput() | |
| 510 | 510 | b.fresh = true | |
| 511 | 511 | b.configured, b.canPresent = false, false | |
| 512 | 512 | return nil | |
| 513 | 513 | } | |
| 514 | 514 | ||
| 515 | + | // resetInput forgets the input state of the surface being replaced: the new | |
| 516 | + | // surface starts click-through, and the module the pointer was over is told it | |
| 517 | + | // left, since no leave will come from a surface that is gone. | |
| 518 | + | func (b *Bar) resetInput() { | |
| 519 | + | b.input.reset() | |
| 520 | + | b.ptr.leave() | |
| 521 | + | } | |
| 522 | + | ||
| 515 | 523 | // step reparses changed modules and draws when something is ready. | |
| 516 | 524 | func (b *Bar) step() error { | |
| 517 | 525 | if b.lay != nil && b.lay.Update() { | |
| @@ -533,6 +541,9 @@ func (b *Bar) draw() error { | |||
| 533 | 541 | b.Stats.NoSlot++ // Released retries | |
| 534 | 542 | return nil | |
| 535 | 543 | } | |
| 544 | + | // The spans come from the row just drawn, and the input region follows | |
| 545 | + | // them only when the draw succeeded: between a layout change and the next | |
| 546 | + | // successful draw, the compositor may still route input by the older spans. | |
| 536 | 547 | b.syncInputRegion() | |
| 537 | 548 | if err = b.present(); err != nil { | |
| 538 | 549 | return err | |
| @@ -842,6 +853,7 @@ func (b *Bar) syncInputRegion() { | |||
| 842 | 853 | } | |
| 843 | 854 | if err := b.surf.SetInputRegion(rects); err != nil { | |
| 844 | 855 | b.log.Warn("cannot set the input region", "err", err) | |
| 856 | + | b.input.reset() // the compositor kept the old one: the next draw tries again | |
| 845 | 857 | } | |
| 846 | 858 | } | |
| 847 | 859 | ||
| @@ -851,7 +863,12 @@ func (b *Bar) Pointer(ev *neferclient.PointerEvent) { | |||
| 851 | 863 | return | |
| 852 | 864 | } | |
| 853 | 865 | _, _, scale := b.surf.Size() | |
| 854 | - | col := int(math.Floor(ev.X * scale / float64(b.face.CellW))) | |
| 866 | + | b.handlePointer(ev, scale, b.face.CellW) | |
| 867 | + | } | |
| 868 | + | ||
| 869 | + | // handlePointer is Pointer for an event over the bar surface. | |
| 870 | + | func (b *Bar) handlePointer(ev *neferclient.PointerEvent, scale float64, cellW int) { | |
| 871 | + | col := cellColumn(ev.X, scale, cellW) | |
| 855 | 872 | switch ev.Kind { | |
| 856 | 873 | case neferclient.PointerEnter, neferclient.PointerMotion: | |
| 857 | 874 | b.ptr.motion(b.lay, col) | |
| @@ -867,6 +884,8 @@ func (b *Bar) Pointer(ev *neferclient.PointerEvent) { | |||
| 867 | 884 | delta = ev.DX | |
| 868 | 885 | } | |
| 869 | 886 | b.ptr.axis(b.lay, col, ev.Axis, ev.Value120, delta) | |
| 887 | + | case neferclient.PointerAxisStop: | |
| 888 | + | b.ptr.wheel.stop(ev.Axis) | |
| 870 | 889 | } | |
| 871 | 890 | } | |
| 872 | 891 | ||
| @@ -506,12 +506,20 @@ func (b *Bar) createSurface(h int32) error { | |||||
| 506 | } | 506 | } | ||
| 507 | b.surf, b.sid = surf, surf.ID() | 507 | b.surf, b.sid = surf, surf.ID() | ||
| 508 | b.mappedW = 0 | 508 | b.mappedW = 0 | ||
| 509 | - | b.input.reset() // the new surface is click-through | 509 | + | b.resetInput() |
| 510 | b.fresh = true | 510 | b.fresh = true | ||
| 511 | b.configured, b.canPresent = false, false | 511 | b.configured, b.canPresent = false, false | ||
| 512 | return nil | 512 | return nil | ||
| 513 | } | 513 | } | ||
| 514 | 514 | ||||
| 515 | + | // resetInput forgets the input state of the surface being replaced: the new | |||
| 516 | + | // surface starts click-through, and the module the pointer was over is told it | |||
| 517 | + | // left, since no leave will come from a surface that is gone. | |||
| 518 | + | func (b *Bar) resetInput() { | |||
| 519 | + | b.input.reset() | |||
| 520 | + | b.ptr.leave() | |||
| 521 | + | } | |||
| 522 | + | ||||
| 515 | // step reparses changed modules and draws when something is ready. | 523 | // step reparses changed modules and draws when something is ready. | ||
| 516 | func (b *Bar) step() error { | 524 | func (b *Bar) step() error { | ||
| 517 | if b.lay != nil && b.lay.Update() { | 525 | if b.lay != nil && b.lay.Update() { | ||
| @@ -533,6 +541,9 @@ func (b *Bar) draw() error { | |||||
| 533 | b.Stats.NoSlot++ // Released retries | 541 | b.Stats.NoSlot++ // Released retries | ||
| 534 | return nil | 542 | return nil | ||
| 535 | } | 543 | } | ||
| 544 | + | // The spans come from the row just drawn, and the input region follows | |||
| 545 | + | // them only when the draw succeeded: between a layout change and the next | |||
| 546 | + | // successful draw, the compositor may still route input by the older spans. | |||
| 536 | b.syncInputRegion() | 547 | b.syncInputRegion() | ||
| 537 | if err = b.present(); err != nil { | 548 | if err = b.present(); err != nil { | ||
| 538 | return err | 549 | return err | ||
| @@ -842,6 +853,7 @@ func (b *Bar) syncInputRegion() { | |||||
| 842 | } | 853 | } | ||
| 843 | if err := b.surf.SetInputRegion(rects); err != nil { | 854 | if err := b.surf.SetInputRegion(rects); err != nil { | ||
| 844 | b.log.Warn("cannot set the input region", "err", err) | 855 | b.log.Warn("cannot set the input region", "err", err) | ||
| 856 | + | b.input.reset() // the compositor kept the old one: the next draw tries again | |||
| 845 | } | 857 | } | ||
| 846 | } | 858 | } | ||
| 847 | 859 | ||||
| @@ -851,7 +863,12 @@ func (b *Bar) Pointer(ev *neferclient.PointerEvent) { | |||||
| 851 | return | 863 | return | ||
| 852 | } | 864 | } | ||
| 853 | _, _, scale := b.surf.Size() | 865 | _, _, scale := b.surf.Size() | ||
| 854 | - | col := int(math.Floor(ev.X * scale / float64(b.face.CellW))) | 866 | + | b.handlePointer(ev, scale, b.face.CellW) |
| 867 | + | } | |||
| 868 | + | ||||
| 869 | + | // handlePointer is Pointer for an event over the bar surface. | |||
| 870 | + | func (b *Bar) handlePointer(ev *neferclient.PointerEvent, scale float64, cellW int) { | |||
| 871 | + | col := cellColumn(ev.X, scale, cellW) | |||
| 855 | switch ev.Kind { | 872 | switch ev.Kind { | ||
| 856 | case neferclient.PointerEnter, neferclient.PointerMotion: | 873 | case neferclient.PointerEnter, neferclient.PointerMotion: | ||
| 857 | b.ptr.motion(b.lay, col) | 874 | b.ptr.motion(b.lay, col) | ||
| @@ -867,6 +884,8 @@ func (b *Bar) Pointer(ev *neferclient.PointerEvent) { | |||||
| 867 | delta = ev.DX | 884 | delta = ev.DX | ||
| 868 | } | 885 | } | ||
| 869 | b.ptr.axis(b.lay, col, ev.Axis, ev.Value120, delta) | 886 | b.ptr.axis(b.lay, col, ev.Axis, ev.Value120, delta) | ||
| 887 | + | case neferclient.PointerAxisStop: | |||
| 888 | + | b.ptr.wheel.stop(ev.Axis) | |||
| 870 | } | 889 | } | ||
| 871 | } | 890 | } | ||
| 872 | 891 | ||||
M internal/bar/input.go +27 -0
| @@ -34,6 +34,7 @@ type press struct { | |||
| 34 | 34 | type pointerState struct { | |
| 35 | 35 | hover *module.Module // the interactive module under the pointer | |
| 36 | 36 | hoverOff int // its cell column | |
| 37 | + | hoverGen uint64 // hover.Gen() when the hover line was sent | |
| 37 | 38 | token uint32 // last click token | |
| 38 | 39 | press press | |
| 39 | 40 | wheel wheelAcc | |
| @@ -52,8 +53,18 @@ func appendInt(b []byte, v int) []byte { | |||
| 52 | 53 | return strconv.AppendInt(b, int64(v), 10) | |
| 53 | 54 | } | |
| 54 | 55 | ||
| 56 | + | // forgetRestarted drops the hover of a module whose script restarted: the new | |
| 57 | + | // process was never told about it, so it gets no leave, and the next motion | |
| 58 | + | // sends it a hover. | |
| 59 | + | func (p *pointerState) forgetRestarted() { | |
| 60 | + | if p.hover != nil && p.hover.Gen() != p.hoverGen { | |
| 61 | + | p.hover, p.hoverOff = nil, 0 | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 55 | 65 | // motion handles Enter and Motion at cell column col of the bar row. | |
| 56 | 66 | func (p *pointerState) motion(lay *layout.Layout, col int) { | |
| 67 | + | p.forgetRestarted() | |
| 57 | 68 | m, off := interactiveAt(lay, col) | |
| 58 | 69 | if m == p.hover && (m == nil || off == p.hoverOff) { | |
| 59 | 70 | return | |
| @@ -63,12 +74,14 @@ func (p *pointerState) motion(lay *layout.Layout, col int) { | |||
| 63 | 74 | } | |
| 64 | 75 | p.hover, p.hoverOff = m, off | |
| 65 | 76 | if m != nil { | |
| 77 | + | p.hoverGen = m.Gen() | |
| 66 | 78 | p.send(m, appendInt(p.line("hover"), off)) | |
| 67 | 79 | } | |
| 68 | 80 | } | |
| 69 | 81 | ||
| 70 | 82 | // leave handles the pointer leaving the bar. | |
| 71 | 83 | func (p *pointerState) leave() { | |
| 84 | + | p.forgetRestarted() | |
| 72 | 85 | if p.hover != nil { | |
| 73 | 86 | p.send(p.hover, p.line("leave")) | |
| 74 | 87 | } | |
| @@ -152,6 +165,20 @@ type wheelAcc struct { | |||
| 152 | 165 | ||
| 153 | 166 | func (a *wheelAcc) reset() { *a = wheelAcc{} } | |
| 154 | 167 | ||
| 168 | + | // stop ends a touchpad scroll on axis: the pixels left over do not carry into | |
| 169 | + | // the next gesture. | |
| 170 | + | func (a *wheelAcc) stop(axis uint32) { | |
| 171 | + | if axis <= 1 { | |
| 172 | + | a.px[axis] = 0 | |
| 173 | + | } | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | // cellColumn is the cell column under logical x on a surface of the given | |
| 177 | + | // scale, for cells cellW physical pixels wide. | |
| 178 | + | func cellColumn(x, scale float64, cellW int) int { | |
| 179 | + | return int(math.Floor(x * scale / float64(cellW))) | |
| 180 | + | } | |
| 181 | + | ||
| 155 | 182 | // add accumulates one event and returns the whole steps it completed: | |
| 156 | 183 | // positive for down or right, negative for up or left. | |
| 157 | 184 | func (a *wheelAcc) add(axis uint32, value120 int32, delta float64) int32 { | |
| @@ -34,6 +34,7 @@ type press struct { | |||||
| 34 | type pointerState struct { | 34 | type pointerState struct { | ||
| 35 | hover *module.Module // the interactive module under the pointer | 35 | hover *module.Module // the interactive module under the pointer | ||
| 36 | hoverOff int // its cell column | 36 | hoverOff int // its cell column | ||
| 37 | + | hoverGen uint64 // hover.Gen() when the hover line was sent | |||
| 37 | token uint32 // last click token | 38 | token uint32 // last click token | ||
| 38 | press press | 39 | press press | ||
| 39 | wheel wheelAcc | 40 | wheel wheelAcc | ||
| @@ -52,8 +53,18 @@ func appendInt(b []byte, v int) []byte { | |||||
| 52 | return strconv.AppendInt(b, int64(v), 10) | 53 | return strconv.AppendInt(b, int64(v), 10) | ||
| 53 | } | 54 | } | ||
| 54 | 55 | ||||
| 56 | + | // forgetRestarted drops the hover of a module whose script restarted: the new | |||
| 57 | + | // process was never told about it, so it gets no leave, and the next motion | |||
| 58 | + | // sends it a hover. | |||
| 59 | + | func (p *pointerState) forgetRestarted() { | |||
| 60 | + | if p.hover != nil && p.hover.Gen() != p.hoverGen { | |||
| 61 | + | p.hover, p.hoverOff = nil, 0 | |||
| 62 | + | } | |||
| 63 | + | } | |||
| 64 | + | ||||
| 55 | // motion handles Enter and Motion at cell column col of the bar row. | 65 | // motion handles Enter and Motion at cell column col of the bar row. | ||
| 56 | func (p *pointerState) motion(lay *layout.Layout, col int) { | 66 | func (p *pointerState) motion(lay *layout.Layout, col int) { | ||
| 67 | + | p.forgetRestarted() | |||
| 57 | m, off := interactiveAt(lay, col) | 68 | m, off := interactiveAt(lay, col) | ||
| 58 | if m == p.hover && (m == nil || off == p.hoverOff) { | 69 | if m == p.hover && (m == nil || off == p.hoverOff) { | ||
| 59 | return | 70 | return | ||
| @@ -63,12 +74,14 @@ func (p *pointerState) motion(lay *layout.Layout, col int) { | |||||
| 63 | } | 74 | } | ||
| 64 | p.hover, p.hoverOff = m, off | 75 | p.hover, p.hoverOff = m, off | ||
| 65 | if m != nil { | 76 | if m != nil { | ||
| 77 | + | p.hoverGen = m.Gen() | |||
| 66 | p.send(m, appendInt(p.line("hover"), off)) | 78 | p.send(m, appendInt(p.line("hover"), off)) | ||
| 67 | } | 79 | } | ||
| 68 | } | 80 | } | ||
| 69 | 81 | ||||
| 70 | // leave handles the pointer leaving the bar. | 82 | // leave handles the pointer leaving the bar. | ||
| 71 | func (p *pointerState) leave() { | 83 | func (p *pointerState) leave() { | ||
| 84 | + | p.forgetRestarted() | |||
| 72 | if p.hover != nil { | 85 | if p.hover != nil { | ||
| 73 | p.send(p.hover, p.line("leave")) | 86 | p.send(p.hover, p.line("leave")) | ||
| 74 | } | 87 | } | ||
| @@ -152,6 +165,20 @@ type wheelAcc struct { | |||||
| 152 | 165 | ||||
| 153 | func (a *wheelAcc) reset() { *a = wheelAcc{} } | 166 | func (a *wheelAcc) reset() { *a = wheelAcc{} } | ||
| 154 | 167 | ||||
| 168 | + | // stop ends a touchpad scroll on axis: the pixels left over do not carry into | |||
| 169 | + | // the next gesture. | |||
| 170 | + | func (a *wheelAcc) stop(axis uint32) { | |||
| 171 | + | if axis <= 1 { | |||
| 172 | + | a.px[axis] = 0 | |||
| 173 | + | } | |||
| 174 | + | } | |||
| 175 | + | ||||
| 176 | + | // cellColumn is the cell column under logical x on a surface of the given | |||
| 177 | + | // scale, for cells cellW physical pixels wide. | |||
| 178 | + | func cellColumn(x, scale float64, cellW int) int { | |||
| 179 | + | return int(math.Floor(x * scale / float64(cellW))) | |||
| 180 | + | } | |||
| 181 | + | ||||
| 155 | // add accumulates one event and returns the whole steps it completed: | 182 | // add accumulates one event and returns the whole steps it completed: | ||
| 156 | // positive for down or right, negative for up or left. | 183 | // positive for down or right, negative for up or left. | ||
| 157 | func (a *wheelAcc) add(axis uint32, value120 int32, delta float64) int32 { | 184 | func (a *wheelAcc) add(axis uint32, value120 int32, delta float64) int32 { | ||
M internal/bar/input_test.go +139 -0
| @@ -200,3 +200,142 @@ func TestPointerAllocs(t *testing.T) { | |||
| 200 | 200 | t.Errorf("an unchanged input region allocates %.1f objects; want 0", got) | |
| 201 | 201 | } | |
| 202 | 202 | } | |
| 203 | + | ||
| 204 | + | func TestRecreatingTheSurfaceResetsPointerState(t *testing.T) { | |
| 205 | + | r := newInputRig(t) | |
| 206 | + | b := &Bar{lay: r.lay} | |
| 207 | + | b.ptr.motion(r.lay, 2) | |
| 208 | + | b.ptr.wheel.add(neferclient.AxisVertical, 120, 0) | |
| 209 | + | b.input.update(r.lay, inputKey{cellW: 10, scale: 1, height: 20}) | |
| 210 | + | drain(r.a) | |
| 211 | + | ||
| 212 | + | b.resetInput() | |
| 213 | + | ||
| 214 | + | if got := drain(r.a); !slices.Equal(got, []string{"leave"}) { | |
| 215 | + | t.Fatalf("lines = %q, want one leave", got) | |
| 216 | + | } | |
| 217 | + | if b.ptr.hover != nil || b.ptr.wheel != (wheelAcc{}) { | |
| 218 | + | t.Fatalf("pointer state kept: hover %v, wheel %+v", b.ptr.hover, b.ptr.wheel) | |
| 219 | + | } | |
| 220 | + | if _, changed := b.input.update(r.lay, inputKey{cellW: 10, scale: 1, height: 20}); !changed { | |
| 221 | + | t.Fatal("the new surface needs its input region again") | |
| 222 | + | } | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | // A failed SetInputRegion resets the tracking, so the next draw tries again. | |
| 226 | + | func TestFailedInputRegionIsRetried(t *testing.T) { | |
| 227 | + | r := newInputRig(t) | |
| 228 | + | var in inputRegion | |
| 229 | + | key := inputKey{cellW: 10, scale: 1, height: 20} | |
| 230 | + | if _, changed := in.update(r.lay, key); !changed { | |
| 231 | + | t.Fatal("first update must change") | |
| 232 | + | } | |
| 233 | + | in.reset() // what syncInputRegion does when the request fails | |
| 234 | + | if _, changed := in.update(r.lay, key); !changed { | |
| 235 | + | t.Fatal("after a failure the same region must be sent again") | |
| 236 | + | } | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | func TestAxisStopClearsTheTouchpadRemainder(t *testing.T) { | |
| 240 | + | r := newInputRig(t) | |
| 241 | + | b := &Bar{lay: r.lay, sid: 1} | |
| 242 | + | axis := func(kind neferclient.PointerKind, ax uint32, dy float64) { | |
| 243 | + | b.handlePointer(&neferclient.PointerEvent{Surface: 1, Kind: kind, X: 1, Axis: ax, DY: dy}, 1, 10) | |
| 244 | + | } | |
| 245 | + | axis(neferclient.PointerAxis, neferclient.AxisVertical, 10) // 10 of 15 px: no step yet | |
| 246 | + | axis(neferclient.PointerAxisStop, neferclient.AxisVertical, 0) | |
| 247 | + | axis(neferclient.PointerAxis, neferclient.AxisVertical, 10) // would complete a step if 10 were kept | |
| 248 | + | if got := drain(r.a); len(got) != 0 { | |
| 249 | + | t.Fatalf("lines = %q; the remainder crossed a gesture", got) | |
| 250 | + | } | |
| 251 | + | axis(neferclient.PointerAxis, neferclient.AxisVertical, 10) | |
| 252 | + | if got := drain(r.a); !slices.Equal(got, []string{"scroll down 1 0"}) { | |
| 253 | + | t.Fatalf("lines = %q, want one step", got) | |
| 254 | + | } | |
| 255 | + | // Stopping the other axis leaves this one alone. | |
| 256 | + | axis(neferclient.PointerAxis, neferclient.AxisVertical, 10) | |
| 257 | + | axis(neferclient.PointerAxisStop, neferclient.AxisHorizontal, 0) | |
| 258 | + | axis(neferclient.PointerAxis, neferclient.AxisVertical, 5) | |
| 259 | + | if got := drain(r.a); !slices.Equal(got, []string{"scroll down 1 0"}) { | |
| 260 | + | t.Fatalf("lines = %q; the other axis's stop cleared this one", got) | |
| 261 | + | } | |
| 262 | + | var a wheelAcc | |
| 263 | + | a.stop(7) // unknown axis: no panic | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | func TestRestartedScriptGetsNoLeaveAndAHoverAgain(t *testing.T) { | |
| 267 | + | r := newInputRig(t) | |
| 268 | + | var p pointerState | |
| 269 | + | p.motion(r.lay, 1) | |
| 270 | + | if got := drain(r.a); !slices.Equal(got, []string{"hover 1"}) { | |
| 271 | + | t.Fatalf("lines = %q", got) | |
| 272 | + | } | |
| 273 | + | // The script restarts: the new process knows nothing of the hover. | |
| 274 | + | module.SetGenForTest(r.a, 7) | |
| 275 | + | p.motion(r.lay, 5) // moves off the module: the new process gets no leave | |
| 276 | + | if got := drain(r.a); len(got) != 0 { | |
| 277 | + | t.Fatalf("lines = %q, want no leave for the new process", got) | |
| 278 | + | } | |
| 279 | + | // The same move onto the module again sends it a hover. | |
| 280 | + | p.motion(r.lay, 1) | |
| 281 | + | module.SetGenForTest(r.a, 8) | |
| 282 | + | p.motion(r.lay, 1) // still over it, but the script restarted: hover again | |
| 283 | + | if got := drain(r.a); !slices.Equal(got, []string{"hover 1", "hover 1"}) { | |
| 284 | + | t.Fatalf("lines = %q, want a fresh hover after the restart", got) | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | // leave after a restart sends nothing to the new process. | |
| 288 | + | p.motion(r.lay, 2) | |
| 289 | + | drain(r.a) | |
| 290 | + | module.SetGenForTest(r.a, 9) | |
| 291 | + | p.leave() | |
| 292 | + | if got := drain(r.a); len(got) != 0 { | |
| 293 | + | t.Fatalf("lines = %q, want none", got) | |
| 294 | + | } | |
| 295 | + | if p.hover != nil { | |
| 296 | + | t.Fatal("hover not cleared") | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | // Without a restart nothing changes. | |
| 300 | + | p.motion(r.lay, 2) | |
| 301 | + | p.motion(r.lay, 6) | |
| 302 | + | if got := drain(r.a); !slices.Equal(got, []string{"hover 2", "leave"}) { | |
| 303 | + | t.Fatalf("lines = %q", got) | |
| 304 | + | } | |
| 305 | + | if got := testing.AllocsPerRun(100, func() { p.motion(r.lay, 1); p.motion(r.lay, 6) }); got > 0 && !racecheck.Enabled { | |
| 306 | + | t.Errorf("motion allocates %.1f", got) | |
| 307 | + | } | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | func TestCellColumnAtFractionalScale(t *testing.T) { | |
| 311 | + | // Scale 1.6, 13 px cells: a cell is 8.125 logical px wide. | |
| 312 | + | for _, c := range []struct { | |
| 313 | + | x float64 | |
| 314 | + | want int | |
| 315 | + | }{{0, 0}, {8.1, 0}, {8.125, 1}, {16.24, 1}, {16.25, 2}, {40.6, 4}, {40.625, 5}, {-0.1, -1}} { | |
| 316 | + | if got := cellColumn(c.x, 1.6, 13); got != c.want { | |
| 317 | + | t.Errorf("cellColumn(%v) = %d, want %d", c.x, got, c.want) | |
| 318 | + | } | |
| 319 | + | } | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | func TestPointerFiltersSurfaceAndUsesFractionalScale(t *testing.T) { | |
| 323 | + | r := newInputRig(t) // a: columns 0-3, b: 4-7 | |
| 324 | + | b := &Bar{lay: r.lay, sid: 5} | |
| 325 | + | // Another surface (a popup): ignored before the bar's size is even read. | |
| 326 | + | b.Pointer(&neferclient.PointerEvent{Surface: 9, Kind: neferclient.PointerMotion, X: 1}) | |
| 327 | + | if got := drain(r.a); len(got) != 0 || b.ptr.hover != nil { | |
| 328 | + | t.Fatalf("a foreign surface moved the hover: %q", got) | |
| 329 | + | } | |
| 330 | + | // Logical x 30 at scale 1.6 and 13 px cells is column 3 (physical px 48). | |
| 331 | + | b.handlePointer(&neferclient.PointerEvent{Surface: 5, Kind: neferclient.PointerMotion, X: 30}, 1.6, 13) | |
| 332 | + | // Logical x 33 is column 4: the non-interactive module, so a leave. | |
| 333 | + | b.handlePointer(&neferclient.PointerEvent{Surface: 5, Kind: neferclient.PointerMotion, X: 33}, 1.6, 13) | |
| 334 | + | b.handlePointer(&neferclient.PointerEvent{Surface: 5, Kind: neferclient.PointerMotion, X: 8.2}, 1.6, 13) | |
| 335 | + | b.handlePointer(&neferclient.PointerEvent{Surface: 5, Kind: neferclient.PointerButton, Button: btnRight, Pressed: true, X: 8.2}, 1.6, 13) | |
| 336 | + | b.handlePointer(&neferclient.PointerEvent{Surface: 5, Kind: neferclient.PointerButton, Button: btnRight, Pressed: false, X: 8.2}, 1.6, 13) | |
| 337 | + | want := []string{"hover 3", "leave", "hover 1", "click right 1 1"} | |
| 338 | + | if got := drain(r.a); !slices.Equal(got, want) { | |
| 339 | + | t.Fatalf("lines = %q, want %q", got, want) | |
| 340 | + | } | |
| 341 | + | } | |
| @@ -200,3 +200,142 @@ func TestPointerAllocs(t *testing.T) { | |||||
| 200 | t.Errorf("an unchanged input region allocates %.1f objects; want 0", got) | 200 | t.Errorf("an unchanged input region allocates %.1f objects; want 0", got) | ||
| 201 | } | 201 | } | ||
| 202 | } | 202 | } | ||
| 203 | + | ||||
| 204 | + | func TestRecreatingTheSurfaceResetsPointerState(t *testing.T) { | |||
| 205 | + | r := newInputRig(t) | |||
| 206 | + | b := &Bar{lay: r.lay} | |||
| 207 | + | b.ptr.motion(r.lay, 2) | |||
| 208 | + | b.ptr.wheel.add(neferclient.AxisVertical, 120, 0) | |||
| 209 | + | b.input.update(r.lay, inputKey{cellW: 10, scale: 1, height: 20}) | |||
| 210 | + | drain(r.a) | |||
| 211 | + | ||||
| 212 | + | b.resetInput() | |||
| 213 | + | ||||
| 214 | + | if got := drain(r.a); !slices.Equal(got, []string{"leave"}) { | |||
| 215 | + | t.Fatalf("lines = %q, want one leave", got) | |||
| 216 | + | } | |||
| 217 | + | if b.ptr.hover != nil || b.ptr.wheel != (wheelAcc{}) { | |||
| 218 | + | t.Fatalf("pointer state kept: hover %v, wheel %+v", b.ptr.hover, b.ptr.wheel) | |||
| 219 | + | } | |||
| 220 | + | if _, changed := b.input.update(r.lay, inputKey{cellW: 10, scale: 1, height: 20}); !changed { | |||
| 221 | + | t.Fatal("the new surface needs its input region again") | |||
| 222 | + | } | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | // A failed SetInputRegion resets the tracking, so the next draw tries again. | |||
| 226 | + | func TestFailedInputRegionIsRetried(t *testing.T) { | |||
| 227 | + | r := newInputRig(t) | |||
| 228 | + | var in inputRegion | |||
| 229 | + | key := inputKey{cellW: 10, scale: 1, height: 20} | |||
| 230 | + | if _, changed := in.update(r.lay, key); !changed { | |||
| 231 | + | t.Fatal("first update must change") | |||
| 232 | + | } | |||
| 233 | + | in.reset() // what syncInputRegion does when the request fails | |||
| 234 | + | if _, changed := in.update(r.lay, key); !changed { | |||
| 235 | + | t.Fatal("after a failure the same region must be sent again") | |||
| 236 | + | } | |||
| 237 | + | } | |||
| 238 | + | ||||
| 239 | + | func TestAxisStopClearsTheTouchpadRemainder(t *testing.T) { | |||
| 240 | + | r := newInputRig(t) | |||
| 241 | + | b := &Bar{lay: r.lay, sid: 1} | |||
| 242 | + | axis := func(kind neferclient.PointerKind, ax uint32, dy float64) { | |||
| 243 | + | b.handlePointer(&neferclient.PointerEvent{Surface: 1, Kind: kind, X: 1, Axis: ax, DY: dy}, 1, 10) | |||
| 244 | + | } | |||
| 245 | + | axis(neferclient.PointerAxis, neferclient.AxisVertical, 10) // 10 of 15 px: no step yet | |||
| 246 | + | axis(neferclient.PointerAxisStop, neferclient.AxisVertical, 0) | |||
| 247 | + | axis(neferclient.PointerAxis, neferclient.AxisVertical, 10) // would complete a step if 10 were kept | |||
| 248 | + | if got := drain(r.a); len(got) != 0 { | |||
| 249 | + | t.Fatalf("lines = %q; the remainder crossed a gesture", got) | |||
| 250 | + | } | |||
| 251 | + | axis(neferclient.PointerAxis, neferclient.AxisVertical, 10) | |||
| 252 | + | if got := drain(r.a); !slices.Equal(got, []string{"scroll down 1 0"}) { | |||
| 253 | + | t.Fatalf("lines = %q, want one step", got) | |||
| 254 | + | } | |||
| 255 | + | // Stopping the other axis leaves this one alone. | |||
| 256 | + | axis(neferclient.PointerAxis, neferclient.AxisVertical, 10) | |||
| 257 | + | axis(neferclient.PointerAxisStop, neferclient.AxisHorizontal, 0) | |||
| 258 | + | axis(neferclient.PointerAxis, neferclient.AxisVertical, 5) | |||
| 259 | + | if got := drain(r.a); !slices.Equal(got, []string{"scroll down 1 0"}) { | |||
| 260 | + | t.Fatalf("lines = %q; the other axis's stop cleared this one", got) | |||
| 261 | + | } | |||
| 262 | + | var a wheelAcc | |||
| 263 | + | a.stop(7) // unknown axis: no panic | |||
| 264 | + | } | |||
| 265 | + | ||||
| 266 | + | func TestRestartedScriptGetsNoLeaveAndAHoverAgain(t *testing.T) { | |||
| 267 | + | r := newInputRig(t) | |||
| 268 | + | var p pointerState | |||
| 269 | + | p.motion(r.lay, 1) | |||
| 270 | + | if got := drain(r.a); !slices.Equal(got, []string{"hover 1"}) { | |||
| 271 | + | t.Fatalf("lines = %q", got) | |||
| 272 | + | } | |||
| 273 | + | // The script restarts: the new process knows nothing of the hover. | |||
| 274 | + | module.SetGenForTest(r.a, 7) | |||
| 275 | + | p.motion(r.lay, 5) // moves off the module: the new process gets no leave | |||
| 276 | + | if got := drain(r.a); len(got) != 0 { | |||
| 277 | + | t.Fatalf("lines = %q, want no leave for the new process", got) | |||
| 278 | + | } | |||
| 279 | + | // The same move onto the module again sends it a hover. | |||
| 280 | + | p.motion(r.lay, 1) | |||
| 281 | + | module.SetGenForTest(r.a, 8) | |||
| 282 | + | p.motion(r.lay, 1) // still over it, but the script restarted: hover again | |||
| 283 | + | if got := drain(r.a); !slices.Equal(got, []string{"hover 1", "hover 1"}) { | |||
| 284 | + | t.Fatalf("lines = %q, want a fresh hover after the restart", got) | |||
| 285 | + | } | |||
| 286 | + | ||||
| 287 | + | // leave after a restart sends nothing to the new process. | |||
| 288 | + | p.motion(r.lay, 2) | |||
| 289 | + | drain(r.a) | |||
| 290 | + | module.SetGenForTest(r.a, 9) | |||
| 291 | + | p.leave() | |||
| 292 | + | if got := drain(r.a); len(got) != 0 { | |||
| 293 | + | t.Fatalf("lines = %q, want none", got) | |||
| 294 | + | } | |||
| 295 | + | if p.hover != nil { | |||
| 296 | + | t.Fatal("hover not cleared") | |||
| 297 | + | } | |||
| 298 | + | ||||
| 299 | + | // Without a restart nothing changes. | |||
| 300 | + | p.motion(r.lay, 2) | |||
| 301 | + | p.motion(r.lay, 6) | |||
| 302 | + | if got := drain(r.a); !slices.Equal(got, []string{"hover 2", "leave"}) { | |||
| 303 | + | t.Fatalf("lines = %q", got) | |||
| 304 | + | } | |||
| 305 | + | if got := testing.AllocsPerRun(100, func() { p.motion(r.lay, 1); p.motion(r.lay, 6) }); got > 0 && !racecheck.Enabled { | |||
| 306 | + | t.Errorf("motion allocates %.1f", got) | |||
| 307 | + | } | |||
| 308 | + | } | |||
| 309 | + | ||||
| 310 | + | func TestCellColumnAtFractionalScale(t *testing.T) { | |||
| 311 | + | // Scale 1.6, 13 px cells: a cell is 8.125 logical px wide. | |||
| 312 | + | for _, c := range []struct { | |||
| 313 | + | x float64 | |||
| 314 | + | want int | |||
| 315 | + | }{{0, 0}, {8.1, 0}, {8.125, 1}, {16.24, 1}, {16.25, 2}, {40.6, 4}, {40.625, 5}, {-0.1, -1}} { | |||
| 316 | + | if got := cellColumn(c.x, 1.6, 13); got != c.want { | |||
| 317 | + | t.Errorf("cellColumn(%v) = %d, want %d", c.x, got, c.want) | |||
| 318 | + | } | |||
| 319 | + | } | |||
| 320 | + | } | |||
| 321 | + | ||||
| 322 | + | func TestPointerFiltersSurfaceAndUsesFractionalScale(t *testing.T) { | |||
| 323 | + | r := newInputRig(t) // a: columns 0-3, b: 4-7 | |||
| 324 | + | b := &Bar{lay: r.lay, sid: 5} | |||
| 325 | + | // Another surface (a popup): ignored before the bar's size is even read. | |||
| 326 | + | b.Pointer(&neferclient.PointerEvent{Surface: 9, Kind: neferclient.PointerMotion, X: 1}) | |||
| 327 | + | if got := drain(r.a); len(got) != 0 || b.ptr.hover != nil { | |||
| 328 | + | t.Fatalf("a foreign surface moved the hover: %q", got) | |||
| 329 | + | } | |||
| 330 | + | // Logical x 30 at scale 1.6 and 13 px cells is column 3 (physical px 48). | |||
| 331 | + | b.handlePointer(&neferclient.PointerEvent{Surface: 5, Kind: neferclient.PointerMotion, X: 30}, 1.6, 13) | |||
| 332 | + | // Logical x 33 is column 4: the non-interactive module, so a leave. | |||
| 333 | + | b.handlePointer(&neferclient.PointerEvent{Surface: 5, Kind: neferclient.PointerMotion, X: 33}, 1.6, 13) | |||
| 334 | + | b.handlePointer(&neferclient.PointerEvent{Surface: 5, Kind: neferclient.PointerMotion, X: 8.2}, 1.6, 13) | |||
| 335 | + | b.handlePointer(&neferclient.PointerEvent{Surface: 5, Kind: neferclient.PointerButton, Button: btnRight, Pressed: true, X: 8.2}, 1.6, 13) | |||
| 336 | + | b.handlePointer(&neferclient.PointerEvent{Surface: 5, Kind: neferclient.PointerButton, Button: btnRight, Pressed: false, X: 8.2}, 1.6, 13) | |||
| 337 | + | want := []string{"hover 3", "leave", "hover 1", "click right 1 1"} | |||
| 338 | + | if got := drain(r.a); !slices.Equal(got, want) { | |||
| 339 | + | t.Fatalf("lines = %q, want %q", got, want) | |||
| 340 | + | } | |||
| 341 | + | } | |||
M internal/module/module.go +3 -0
| @@ -308,6 +308,9 @@ func (w *logWriter) Write(p []byte) (int, error) { | |||
| 308 | 308 | return len(p), nil | |
| 309 | 309 | } | |
| 310 | 310 | ||
| 311 | + | // SetGenForTest makes the module look as if its script was restarted. | |
| 312 | + | func SetGenForTest(m *Module, gen uint64) { m.gen.Store(gen) } | |
| 313 | + | ||
| 311 | 314 | // DrainForTest empties the queue of Send lines and returns them without their | |
| 312 | 315 | // newlines. | |
| 313 | 316 | func DrainForTest(m *Module) []string { | |
| @@ -308,6 +308,9 @@ func (w *logWriter) Write(p []byte) (int, error) { | |||||
| 308 | return len(p), nil | 308 | return len(p), nil | ||
| 309 | } | 309 | } | ||
| 310 | 310 | ||||
| 311 | + | // SetGenForTest makes the module look as if its script was restarted. | |||
| 312 | + | func SetGenForTest(m *Module, gen uint64) { m.gen.Store(gen) } | |||
| 313 | + | ||||
| 311 | // DrainForTest empties the queue of Send lines and returns them without their | 314 | // DrainForTest empties the queue of Send lines and returns them without their | ||
| 312 | // newlines. | 315 | // newlines. | ||
| 313 | func DrainForTest(m *Module) []string { | 316 | func DrainForTest(m *Module) []string { | ||