~eliasnaur/gio#620: 
swallowing clicks by accident

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)
		}
	}
}
Status
REPORTED
Submitter
~andybalholm
Assigned to
No-one
Submitted
5 months ago
Updated
4 months ago
Labels
No labels applied.

~andybalholm referenced this from #613 5 months ago

~eliasnaur 5 months ago

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, the event.Op really should have a proper clip area.

What did I miss?

~andybalholm 5 months ago

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.

~eliasnaur 5 months ago

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.

~andybalholm 5 months ago

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.)

~eliasnaur 5 months ago

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.Ops 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).

~andybalholm 5 months ago

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.

~eliasnaur 4 months ago

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 by Add'ing the macro.

~andybalholm 4 months ago

Yes, that works well. Probably the only change that's needed is better documentation…

Register here or Log in to comment, or comment via email.