d40cb3d0f94cff998d16e2eeeb695f9bb5e7f7bf — Chris Waldon 4 months ago 6f814a1
architecture: add discussion of input tree with pointer example

This commit adds a discussion of the hierarchy of clip areas and
how pointer events propagate through it. It also adds an example
demonstrating the ways that pointer input areas interact in a

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
M content/doc/architecture/input.md => content/doc/architecture/input.md +23 -1
@@ 31,7 31,6 @@ It's convenient to use a Go pointer value for the input tag, as it's cheap to co

For more details take a look at [`gioui.org/io/pointer`](https://gioui.org/io/pointer) (pointer/mouse events) and [`gioui.org/io/key`](https://gioui.org/io/key) (keyboard events).

## External input

A single frame consists of getting input, registering for input and drawing the new state:

@@ 45,3 44,26 @@ Let's make the button change it's position every second. We can use a select to 
<pre style="min-height: 100px" data-run="wasm" data-pkg="architecture" data-args="external-changes" data-size="200x100"></pre>

Writing a program using these concepts could get really verbose, which is why Gio provides standard widgets for common look and behaviour. Most programs end up using widgets primarily and few low-level operations.

## Advanced Input Topics

Content below this heading explores more advanced usage of Gio's input operations. This content is mostly useful for people writing custom widgets, and isn't strictly necessary for using Gio's high-level widget and layout APIs.

### Input Tree

You may have noticed that the previous example uses a `clip.AreaOp` (constructed with `clip.Rect`) to describe where it wants pointer input. This is because Gio uses `clip.AreaOp`s both to describe drawing and input regions. As you can see above, often you want to both draw within a region and accept input within that region, so this reuse is convenient.

`clip.AreaOp`s form an implicit tree of input areas, each of which may be interested in pointer input, keyboard input, or both.

Here's an example to explore how pointer events interact with this tree structure.

<{{files/architecture/button.go}}[/START INPUTTREE OMIT/,/END INPUTTREE OMIT/]

<pre style="min-height: 100px" data-run="wasm" data-pkg="architecture" data-args="input-tree" data-size="200x100"></pre>

Try clicking each of the three blue rectangles. You should see that clicking the biggest rectangle only turns itself red, while clicking either of the two rectangles inside of it turns both the rectangle that you clicked _and_ the outermost rectangle red.

This happens because pointer input events propagate up the tree of `clip.AreaOp`s looking for `pointer.InputOp`s for that kind of event. They do not stop at the first interested `pointer.InputOp`, but continue all the way up to the root of the tree. This means that both the rectangle we clicked _and_ the rectangle that contains it receive the `pointer.Press` and `pointer.Release` from clicking on one of the nested rectangles.

Notice also that if you click on the area where the two child rectangles overlap, only the top-most (last drawn) rectangle receives the click. By default, Gio only considers the foremost area and its ancestors when routing pointer events. If you want to alter this, you can use `pointer.PassOp` to allow pointer events to pass through an input area to those underneath it. This is useful for laying out overlays and similar elements. See the [documentation for package `pointer`](https://pkg.go.dev/gioui.org/io/pointer#hdr-Pass_through) for details on this operation.

M include/files/architecture/button.go => include/files/architecture/button.go +76 -0
@@ 52,6 52,82 @@ func doButton(ops *op.Ops, q event.Queue) {


var (
	// Declare a number of variables to use both as state
	// and input tags.
	root, child1, child2 bool

// displayForTag adds a pointer.InputOp interested
// in press and release events to the given op.Ops using
// the given tag. It also paints a color based on the current
// value of the tag to the current clip.
func displayForTag(ops *op.Ops, tag *bool, rect clip.Rect) {
		Tag:   tag,
		Types: pointer.Press | pointer.Release,
	// Choose a color based on whether the tag is being pressed.
	c := color.NRGBA{B: 0xFF, A: 0xFF}
	if *tag {
		c = color.NRGBA{R: 0xFF, A: 0xFF}
	// Paint the current clipping area with a translucent color.
	translucent := c
	translucent.A = 0x44
	paint.ColorOp{Color: translucent}.Add(ops)

	// Reduce our clipping area to the outline of the rectangle, then
	// paint that outline. This should make it easier to see overlapping
	// rectangles.
	defer clip.Stroke{
		Path:  rect.Path(),
		Width: 5,
	paint.ColorOp{Color: c}.Add(ops)

func doPointerTree(ops *op.Ops, q event.Queue) {
	// Process events that arrived between the last frame and this one for every tag.
	for _, tag := range []*bool{&root, &child1, &child2} {
		for _, ev := range q.Events(tag) {
			if x, ok := ev.(pointer.Event); ok {
				switch x.Type {
				case pointer.Press:
					*tag = true
				case pointer.Release:
					*tag = false

	// Confine the rootArea of interest to a 200x200 rectangle.
	rootRect := clip.Rect(image.Rect(0, 0, 200, 200))
	rootArea := rootRect.Push(ops)
	displayForTag(ops, &root, rootRect)

	// Any clip areas we add before Pop-ing the root area
	// are considered its children.
	child1Rect := clip.Rect(image.Rect(25, 25, 175, 100))
	child1Area := child1Rect.Push(ops)
	displayForTag(ops, &child1, child1Rect)

	child2Rect := clip.Rect(image.Rect(100, 25, 175, 175))
	child2Area := child2Rect.Push(ops)
	displayForTag(ops, &child2, child2Rect)

	// Now anything we add is _not_ a child of the rootArea.


var buttonVisual ButtonVisual

func handleButtonVisual(gtx layout.Context) layout.Dimensions {

M include/files/architecture/main.go => include/files/architecture/main.go +2 -1
@@ 22,7 22,7 @@ func main() {
		run  func() error

	var commands = []*command{
	commands := []*command{
		// drawing section
		{name: "draw-operations", run: drawLoop(addColorOperation)},
		{name: "draw-paint", run: drawLoop(drawRedRect)},

@@ 39,6 39,7 @@ func main() {
		{name: "draw-image", run: drawLoop(drawImageInternal)},

		{name: "button-low", run: drawQueueLoop(doButton)},
		{name: "input-tree", run: drawQueueLoop(doPointerTree)},
		{name: "external-changes", run: externalChanges},
		{name: "button-visual", run: contextLoop(handleButtonVisual)},
		{name: "button", run: contextLoop(handleButton)},