When I call event.Op
without specifying a clipping area, no previously-declared event handlers can receive mouse events, because it creates a click-receiving area that covers the whole window—even if I don't intend to handle any mouse events with this tag.
In the real program where I ran into this, I had a complex widget wrapping some other widgets. I wanted the complex widget as a whole to be able to receive keyboard focus and respond to key presses, but mouse input to go directly to some clickable areas within the widget. But as soon as I registered the event handler for the keyboard events, the mouse clicks didn't work any more.
This is an unfortunate result of unifying pointer.InputOp
and key.InputOp
into event.Op
. (So now registering for events registers for both mouse and keyboard events by default.) All the parts make sense in isolation, but in combination, the result is surprising and can be difficult to debug. (The solution was to set an empty clipping area before calling event.Op
.)
Here is an example program that reproduces the problem. In this example, I don't actually handle any events from the tag that I registered, for simplicity:
package main
import (
"log"
"os"
"gioui.org/app"
"gioui.org/io/event"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/widget"
"gioui.org/widget/material"
)
// This program demonstrates a Gio event-routing issue where
// an event handler without a clipping area swallows all clicks.
var th = material.NewTheme()
func main() {
go func() {
var w app.Window
w.Option(app.Title("Click Swallower"))
w.Option(app.Size(640, 480))
Home(&w)
}()
app.Main()
}
func exit(e app.DestroyEvent) {
if e.Err != nil {
log.Fatal(e.Err)
}
os.Exit(0)
}
func Home(w *app.Window) {
var quitButton widget.Clickable
var ops op.Ops
for {
switch e := w.Event().(type) {
case app.DestroyEvent:
exit(e)
case app.FrameEvent:
gtx := app.NewContext(&ops, e)
if quitButton.Clicked(gtx) {
os.Exit(0)
}
layout.UniformInset(20).Layout(gtx,
material.Button(th, &quitButton, "Quit").Layout,
)
event.Op(gtx.Ops, 0)
e.Frame(gtx.Ops)
}
}
}
I see, thank you for the detailed report.
Why not leave the
event.Op
out altogether? You can set the focus and listen for focus and key events without it. You lose the ability for the user to tab to your widget, but if that matters, theevent.Op
really should have a proper clip area.What did I miss?
Do you mean that if we set the focus to a tag that doesn't have an event.Op, it will receive key events?
Anyway, I do want the user to be able to tab to the widget. I solved the issue by setting a zero-sized rectangle as the clip area. But it still seems rather counter-intuitive, even though it's perfectly logical considering how all the parts are defined.
Do you mean that if we set the focus to a tag that doesn't have an event.Op, it will receive key events?
Yes, programmatically setting the focus doesn't (shouldn't) require an
event.Op
.Anyway, I do want the user to be able to tab to the widget. I solved the issue by setting a zero-sized rectangle as the clip area. But it still seems rather counter-intuitive, even though it's perfectly logical considering how all the parts are defined.
I see. If you allow tab to the widget don't you want to have a graphical representation for the widget? If so, your a zero-area is just as wrong as the implicit full window area.
The widget is definitely visible, and contains many clickable areas. But it's not useful for it to be clickable as a whole; the clicks need to be passed through to the content.
(It's a table with clickable rows. The keyboard focus is to allow selecting a row with the arrow keys and activating it with enter instead of clicking on it.)
Ok, so a main area with several clickable children? It seems to me such a widget should be modelled as a large clip area with a key-event-only
event.Op
, with the child clickable areas as children.(I'm tempted to just ignore
event.Op
s without pointer event filter for pointer event handling, but I also want to encourage proper clip areas. Algorithms for focus switching and accessibility depend on the position and size of even key-only widgets).
To avoid blocking clicks that should go to the widget's children, the whole-widget event.Op would need to be called before drawing the children. But the correct dimensions to set for its clip area aren't known till afterward, because they are the return value of the Layout method.
I see. This is what macros are designed to handle. That is, you can put your children inside a macro, add the
event.Op
with the correct dimensions and then add the children byAdd
'ing the macro.
Yes, that works well. Probably the only change that's needed is better documentation…