-
Notifications
You must be signed in to change notification settings - Fork 134
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
io/input: ensure full event delivery per input event
This commit tries to ensure that if any Gio event resulting from a given user input event is delivered, the rest of the Gio events from the same user input are delivered on the same frame. The alternative could lead to rare cases in which Gio events were entirely dropped because a subset of the resulting Gio events were delivered before a widget issued a command. Fixes: https://todo.sr.ht/~eliasnaur/gio/594 Signed-off-by: Chris Waldon <[email protected]>
- Loading branch information
1 parent
38fca9a
commit f8029f2
Showing
2 changed files
with
179 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
package input | ||
|
||
import ( | ||
"image" | ||
"image/color" | ||
"testing" | ||
|
||
"gioui.org/f32" | ||
"gioui.org/io/event" | ||
"gioui.org/io/key" | ||
"gioui.org/io/pointer" | ||
"gioui.org/op" | ||
"gioui.org/op/clip" | ||
"gioui.org/op/paint" | ||
) | ||
|
||
// TestRouterEventPartialDelivery ensures that the router delivers all events | ||
// associated with a given user input event in the same frame, even if delivery | ||
// of those events is interrupted by a command. | ||
// | ||
// In particular, this test builds a UI with keyboard focus defaulting elsewhere | ||
// (which gives us an easy command to use to defer event delivery) and a | ||
// button beneath an overlay that both want pointer press events. The test clicks | ||
// on the button/overlay, generating press events for both, and then checks that | ||
// the overlay actually receives a press (even though the button is laid out first | ||
// and issues a command that defers event delivery). | ||
func TestRouterEventPartialDelivery(t *testing.T) { | ||
var ui ui | ||
router := new(Router) | ||
source := router.Source() | ||
|
||
nextFrame := func() { | ||
router.Frame(&ui.ops) | ||
ui.ops.Reset() | ||
} | ||
ui.layout(&ui.ops, source) | ||
nextFrame() | ||
router.Queue( | ||
// Click on the overlay. This ought to activate it. | ||
pointer.Event{ | ||
Kind: pointer.Press, | ||
Buttons: pointer.ButtonPrimary, | ||
Position: f32.Point{X: 50, Y: 50}, | ||
}, | ||
) | ||
ui.layout(&ui.ops, source) | ||
if !ui.o { | ||
t.Error("overlay failed to activate on the first click") | ||
} | ||
} | ||
|
||
// btn implements a crude button which requests focus when clicked. | ||
// It does not *use* the keyboard focus for anything, but this focus | ||
// requesting behavior is necessary to trigger this bug. | ||
type btn int | ||
|
||
func (b *btn) layout(ops *op.Ops, source Source, c color.NRGBA) { | ||
for { | ||
ev, ok := source.Event( | ||
pointer.Filter{ | ||
Target: b, | ||
Kinds: pointer.Press | pointer.Release, | ||
}, | ||
key.FocusFilter{ | ||
Target: b, | ||
}, | ||
) | ||
if !ok { | ||
break | ||
} | ||
switch ev := ev.(type) { | ||
case pointer.Event: | ||
if ev.Kind == pointer.Press { | ||
source.Execute(key.FocusCmd{ | ||
Tag: b, | ||
}) | ||
} | ||
} | ||
} | ||
defer clip.Rect{Max: image.Pt(100, 100)}.Push(ops).Pop() | ||
event.Op(ops, b) | ||
paint.Fill(ops, c) | ||
} | ||
|
||
// overlay displays a toggleable color on top of its child widget. | ||
type overlay bool | ||
|
||
func (o *overlay) layout(ops *op.Ops, source Source, c color.NRGBA, child func(*op.Ops, Source)) { | ||
// We must update the state of the child widget *before* doing our own | ||
// update, otherwise we can't reproduce the bug. | ||
mac := op.Record(ops) | ||
child(ops, source) | ||
call := mac.Stop() | ||
for { | ||
ev, ok := source.Event( | ||
pointer.Filter{ | ||
Target: o, | ||
Kinds: pointer.Press | pointer.Release, | ||
}, | ||
) | ||
if !ok { | ||
break | ||
} | ||
switch ev := ev.(type) { | ||
case pointer.Event: | ||
if ev.Kind == pointer.Press { | ||
*o = !*o | ||
} | ||
} | ||
} | ||
defer clip.Rect{Max: image.Pt(100, 100)}.Push(ops).Pop() | ||
event.Op(ops, o) | ||
call.Add(ops) | ||
if *o { | ||
paint.Fill(ops, c) | ||
} | ||
} | ||
|
||
// focusable steals focus the first time it is laid out. | ||
type focusable bool | ||
|
||
func (f *focusable) layout(ops *op.Ops, source Source) { | ||
for { | ||
_, ok := source.Event(key.FocusFilter{Target: f}) | ||
if !ok { | ||
break | ||
} | ||
} | ||
event.Op(ops, f) | ||
if !*f { | ||
source.Execute(key.FocusCmd{Tag: f}) | ||
*f = true | ||
} | ||
} | ||
|
||
// UI bundles the state of this reproducer together to make it easier to use in both an | ||
// interactive program and a test case. | ||
type ui struct { | ||
ops op.Ops | ||
b btn | ||
o overlay | ||
f focusable | ||
} | ||
|
||
func (ui *ui) layout(ops *op.Ops, source Source) { | ||
// Lay out a focusable to grab the keyboard focus. | ||
ui.f.layout(ops, source) | ||
// Lay out an invisible overlay atop a button. | ||
ui.o.layout(ops, source, color.NRGBA{R: 255, A: 255}, func(ops *op.Ops, source Source) { | ||
ui.b.layout(ops, source, color.NRGBA{G: 255, A: 100}) | ||
}) | ||
} |