~eliasnaur/gio

Mailing list for the Gio project for discussion and patches.

Send your message to ~eliasnaur/gio@lists.sr.ht; no account is required and you can post without being subscribed.

5 2

How does pointer.HandlerOp work?

Details
Message ID
<20190805223712.GA21004@larrymbp14.local>
DKIM signature
missing
Download raw message
I modified the GopherCon pointer.go demo to have two buttons, in
different places, and each with its own pointer listeners.  (Or at
least that was my intention.)  I'd inferred that a pointer.HandlerOp
knows what screen area to "listen in" (if that's the right words)
based on the immediately preceeding pointer.RectAreaOp.  But that
doesn't appear to be it.

In the below code, the 2nd button doesn't get any mouse events.  Can
you tell me why?

(Also note: This demo loops through all mouse events available during
a single draw event.  I found it quite possible to generate a click
event and a release event that were both delivered during a single
draw event, so that the rest of the Layout function didn't see a mouse
click (since "pressed" was false at the end).  That could trip up
beginners (such as moi!) that use pointer.go as a model for how to
process click events.  On the other hand, it's a demo, so I know you
want to keep it simple.  But it might bear a comment or something.)

-- Larry


package main

import (
	"fmt"
	"image"
	"image/color"
	"time"

	"gioui.org/ui"
	"gioui.org/ui/app"
	"gioui.org/ui/draw"
	"gioui.org/ui/f32"
	"gioui.org/ui/input"
	"gioui.org/ui/pointer"
)

func main() {
	go func() {
		w := app.NewWindow(nil)
		var button1, button2 Button
		ops := new(ui.Ops)
		for e := range w.Events() {
			if e, ok := e.(app.DrawEvent); ok {
				fmt.Printf("%v: draw: %+v\n", time.Now(), e)
				ops.Reset()
				queue := w.Queue()
				button1.Layout(queue, ops, 0, 0, 250, 250)
				button2.Layout(queue, ops, 250, 250, 500, 500)
				w.Draw(ops)
				fmt.Println()
			}
		}
	}()
	app.Main()
}

type Button struct {
	pressed bool
}

func (b *Button) Layout(queue input.Queue, ops *ui.Ops, x1, y1, x2, y2 int) {
	numEvents := 0
	for e, ok := queue.Next(b); ok; e, ok = queue.Next(b) {
		if e, ok := e.(pointer.Event); ok {
			numEvents++
			switch e.Type {
			case pointer.Press:
				b.pressed = true
			case pointer.Release:
				b.pressed = false
			}
			fmt.Printf("%v: pointer: %p: %d,%d,%d,%d: %t, %+v\n", time.Now(), b, x1, y1, x2, y2, b.pressed, e)
		}
	}
	fmt.Printf("%v: final pointer of %d: %p: %d,%d,%d,%d: %t\n", time.Now(), numEvents, b, x1, y1, x2, y2, b.pressed)

	col := color.RGBA{A: 0xff, R: 0xff}
	if b.pressed {
		col = color.RGBA{A: 0xff, G: 0xff}
	}
	pointer.RectAreaOp{
		Rect: image.Rectangle{Min: image.Point{X: x1, Y: y1}, Max: image.Point{X: x2, Y: y2}},
	}.Add(ops)
	pointer.HandlerOp{Key: b}.Add(ops)
	drawSquare(ops, col, x1, y1, x2, y2)
}

func drawSquare(ops *ui.Ops, color color.RGBA, x1, y1, x2, y2 int) {
	square := f32.Rectangle{
		f32.Point{float32(x1), float32(y1)},
		f32.Point{float32(x2), float32(y2)},
	}
	draw.ColorOp{Color: color}.Add(ops)
	draw.DrawOp{Rect: square}.Add(ops)
}
Details
Message ID
<CAMAFT9X=nirGT9oPqU0Jm+TZV9nG+-16cSMuWotcpDPZUKNCtw@mail.gmail.com>
In-Reply-To
<20190805223712.GA21004@larrymbp14.local> (view parent)
DKIM signature
pass
Download raw message
On Tue, Aug 6, 2019 at 12:37 AM Larry Clapp <larry@theclapp.org> wrote:
>
> I modified the GopherCon pointer.go demo to have two buttons, in
> different places, and each with its own pointer listeners.  (Or at
> least that was my intention.)  I'd inferred that a pointer.HandlerOp
> knows what screen area to "listen in" (if that's the right words)
> based on the immediately preceeding pointer.RectAreaOp.  But that
> doesn't appear to be it.
>

It took me too long to figure out myself, but you're right about
HandlerOp. The problem
is that the area ops intersect, which means that the effective area of
the second
button is the zero area (the interesection of the two square areas).

> In the below code, the 2nd button doesn't get any mouse events.  Can
> you tell me why?
>

>
>         pointer.RectAreaOp{
>                 Rect: image.Rectangle{Min: image.Point{X: x1, Y: y1}, Max: image.Point{X: x2, Y: y2}},
>         }.Add(ops)
>         pointer.HandlerOp{Key: b}.Add(ops)
> }
>

The fix is to use a StackOp to save and restore the current op state.
In our case we're interested in
resetting the the current set of areas. Like this:

      var stack ui.StackOp
      stack.Push(ops)  // Save state

      pointer.RectAreaOp{
          Rect: image.Rectangle{Min: image.Point{X: x1, Y: y1}, Max:
image.Point{X: x2, Y: y2}},
      }.Add(ops)
      pointer.HandlerOp{Key: b}.Add(ops)

      stack.Pop() // Restore state

 - elias
Details
Message ID
<20190806131241.GA24700@larrymbp14.local>
In-Reply-To
<CAMAFT9X=nirGT9oPqU0Jm+TZV9nG+-16cSMuWotcpDPZUKNCtw@mail.gmail.com> (view parent)
DKIM signature
missing
Download raw message
On Tue, Aug 06, 2019 at 12:02:14PM +0200, Elias Naur wrote:
> It took me too long to figure out myself, but you're right about
> HandlerOp. The problem is that the area ops intersect, which means
> that the effective area of the second button is the zero area (the
> interesection of the two square areas).

Thanks!

I don't think it's *just* that the two buttons intersect?

I tried (0,0)-(100,100) & (200,200)-(300,300) (and several other
clearly non-intersecting combinations) and none of them worked.

Also, I zoomed in on the intersection of (0,0)-(250,250) &
(250,250)-(500,500) and there's a clear (tiny) gap between them, which
(once I added the StackOp Push/Pop) I could even move the mouse
between and get no events.  So it doesn't look to me like they
actually intercept?  If the code thinks they do in some places, it
doesn't seem to in others, at the very least.  It looked like the
first button was actually *drawn* to (249,249), maybe?

I tried (0,0)-(300,300) and (200,200)-(500,500) (without the StackOp),
which clearly intersect, and then the first button got all the events
in its area, and the second button got events for the overlap (i.e.
(200,200)-(300,300)).  In that trial, when I clicked on the
intersecting bit, both buttons got the event (and both turned green).
When I clicked on the non-intersecting bit of button1, only it turned
green (except the intersection, which stayed red, which I guess just
indicates that the buttons are drawn back-to-front); when I clicked on
the non-intersecting bit of button2, it (of course) didn't get the
event at all.

Using the StackOp fixed everything.

===

Question about the StackOp: I tried two methods, one using a single
StackOp and Push/Pop+Push/Pop, and one using two StackOps, one per
button.  Both worked, mind you.  And from my reading of Push and Pop,
both looked the same from the point of view of whatever reads the
operations stream.  But, which would you prefer / recommend?  Does it
matter?  (But see below.)

===

I was going to ask: say you have many widgets on a page, do you have
to do a StackOp Push/Pop around each one?  But I looked at the gophers
demo, which in fact has many widgets, and it uses several nested
Layouts, all of which do a StackOp Push/Pop around each "child"
element.  So I guess that's the answer: Use layouts, and let them
handle it, and if you write your own layout, then yes, do a StackOp
Push/Pop around each child.

... Which I guess answers my previous question, too: Using a single
StackOp and doing several Push/Pops on it is probably best, if you can
manage it, but it probably don't matter *too* awful much.

===

So one thing that's kind of implicit in all of this is that some
documentation on the API would be, you know, helpful.  :)  In the past
I've found "go vet" useful for this; it flags exported
functions/variables/etc that don't have a doc comment attached.
Usually the answer is "write documentation", but sometimes the answer
is "that shouldn't be exported"!  :)

(I think it was "go vet".  It might've been some other Go linter.)

===

Thanks again for your help, and for reading this far!

-- Larry
Details
Message ID
<CAMAFT9Us0cWvYTRqHR+wTgEqunBUoKhyn=zZ6g7=bM+G0BR1ww@mail.gmail.com>
In-Reply-To
<20190806131241.GA24700@larrymbp14.local> (view parent)
DKIM signature
pass
Download raw message
On Tue, Aug 6, 2019 at 3:12 PM Larry Clapp <larry@theclapp.org> wrote:
>
> On Tue, Aug 06, 2019 at 12:02:14PM +0200, Elias Naur wrote:
> > It took me too long to figure out myself, but you're right about
> > HandlerOp. The problem is that the area ops intersect, which means
> > that the effective area of the second button is the zero area (the
> > interesection of the two square areas).
>
> Thanks!
>
> I don't think it's *just* that the two buttons intersect?
>

Ah, I think I explained it poorly.

I meant that area ops intersect by design and that that feature
explained your problem.
When you called the two button Layout methods, the ops added
to the ops buffer were:

    // button1
    // Set the current area to the intersection of the previous area
    // (whole screen) and button1's area, which is just button1's area.
    pointer.RectAreaOp{...}.Add(ops)
    pointer.HandlerOp{Key: &button1}.Add(ops)
    // button2
    // Set the current area to the intersection of the previous area
    // (button1's area) and button2's area, which is the empty area.
    pointer.RectAreaOp{...}.Add(ops)
    pointer.HandlerOp{Key: &button2}.Add(ops)

Since areas intersect, the effective area for button2's HandlerOp
is the intersection between the button1's RectAreaOp and button2's
ReactAreaOp. Since the two areas are disjoint, the resulting intersection
is the empty area.

With the StackOps, the effect of the first RectAreaOp is undone:

    // button1
    // Save all state, including current area (whole screen).
    stack.Push()
    // Update area...
    pointer.RectAreaOp{...}.Add(ops)
    pointer.HandlerOp{Key: &button1}.Add(ops)
    // Restore all state, including the area back to the whole screen.
    stack.Pop()

I hope that makes more sense.

> It looked like the
> first button was actually *drawn* to (249,249), maybe?

That would be wrong, but from a zoomed screenshot, I don't *think* that
is the case. To me, the first button is drawn to (250,250).

> Question about the StackOp: I tried two methods, one using a single
> StackOp and Push/Pop+Push/Pop, and one using two StackOps, one per
> button.  Both worked, mind you.  And from my reading of Push and Pop,
> both looked the same from the point of view of whatever reads the
> operations stream.  But, which would you prefer / recommend?  Does it
> matter?  (But see below.)

It doesn't matter. The StackOps are stack allocated and garbage free. The
only reason you need a StackOp is to check you're matching the Pop
with the correct Push. A previous version of Gio had separate PushOp
and PopOp, which made it hard to debug unbalanced push and pops.

> I was going to ask: say you have many widgets on a page, do you have
> to do a StackOp Push/Pop around each one?  But I looked at the gophers
> demo, which in fact has many widgets, and it uses several nested
> Layouts, all of which do a StackOp Push/Pop around each "child"
> element.  So I guess that's the answer: Use layouts, and let them
> handle it, and if you write your own layout, then yes, do a StackOp
> Push/Pop around each child.

That pretty much sums it up, yes. And you use MacroOps for recording
a series of "child" operations that you want to manipulate after they're
added. The centering.go example from the talk repository demonstrate
this nicely, although it didn't make it to the talk.

> ... Which I guess answers my previous question, too: Using a single
> StackOp and doing several Push/Pops on it is probably best, if you can
> manage it, but it probably don't matter *too* awful much.
>

Many StackOps are as efficient as few. The only reason layouts store them
is to be able to Pop the same StackOp they Pushed.

> So one thing that's kind of implicit in all of this is that some
> documentation on the API would be, you know, helpful.  :)  In the past
> I've found "go vet" useful for this; it flags exported
> functions/variables/etc that don't have a doc comment attached.
> Usually the answer is "write documentation", but sometimes the answer
> is "that shouldn't be exported"!  :)
>
> (I think it was "go vet".  It might've been some other Go linter.)

Couldn't agree more. And tests - Gio contains no tests currently :)

My excuse up until now has been time pressure for the presentation and
that I wasn't fully happy with the design of the initial public
release in March.

I'm much happier with the current design, and it's definitely time to add
documentation and tests.

Thanks for working with Gio in spite of lack of documentation. To be honest,
I hadn't expected someone to work with it at this low level so soon :)

 - elias
Details
Message ID
<20190807003804.GA28227@larrymbp14.local>
In-Reply-To
<CAMAFT9Us0cWvYTRqHR+wTgEqunBUoKhyn=zZ6g7=bM+G0BR1ww@mail.gmail.com> (view parent)
DKIM signature
missing
Download raw message
On Tue, Aug 06, 2019 at 10:47:59PM +0200, Elias Naur wrote:
> Since areas intersect, the effective area for button2's HandlerOp is
> the intersection between the button1's RectAreaOp and button2's
> ReactAreaOp. Since the two areas are disjoint, the resulting
> intersection is the empty area.

Ah, I think I get it now.  Thanks!

> > It looked like the first button was actually *drawn* to (249,249),
> > maybe?
> 
> That would be wrong, but from a zoomed screenshot, I don't *think*
> that is the case. To me, the first button is drawn to (250,250).

It's a subtle thing either way.  Perhaps I'm mistaken.  But trying it
again, with more-obviously-overlapping rectangles, it sure doesn't
look like (0,0)-(250,400) overlaps at all with (250,100)-(500,500),
and it should, right? by one pixel?

But perhaps my understanding is again incomplete.  Is a square from
(0,0)-(10,10) 10x10 pixels, or 11x11?  If the former, that would
explain what I see, I think.

> > So one thing that's kind of implicit in all of this is that some
> > documentation on the API would be, you know, helpful.
> 
> Couldn't agree more. And tests

:)

> Thanks for working with Gio in spite of lack of documentation. To be
> honest, I hadn't expected someone to work with it at this low level
> so soon :)

Well, at first I really really wanted to understand how widgets got
mouse clicks in immediate mode, 'cause I just couldn't see it.
Eventually I gave up and decided "for now, I'll accept that it's just
magic, and I'll get it later".  And then I wanted to try just writing
a single button.  So I added a button to the pointer.go demo.  And
then it didn't work.  So I ended up digging around in the internals a
lot.  :)  On the plus side, I think now I'm a lot closer to
understanding how widgets get mouse clicks!  I don't know if you
invented the hitTree thing, but I think it's a nice piece of work.

-- Larry
Details
Message ID
<CAMAFT9U7iZ1GCFq5qP_cEN_Pc4M+rHdYcht7SzrQMSkV=hzSRw@mail.gmail.com>
In-Reply-To
<20190807003804.GA28227@larrymbp14.local> (view parent)
DKIM signature
pass
Download raw message
> But perhaps my understanding is again incomplete.  Is a square from
> (0,0)-(10,10) 10x10 pixels, or 11x11?  If the former, that would
> explain what I see, I think.
>

A (0,0)-(10,10) rectangle takes up 10x10 pixels so I believe that
matches what you see.

> I don't know if you
> invented the hitTree thing, but I think it's a nice piece of work.
>

Thanks! Designing event handling and pointer routing in particular was
the hardest part to get right. I didn't find any other immediate mode library
that attempts to tackle the complicated touch input case with multiple fingers
and overlapping hit areas. For example, it took quite a few attempts to get the
scrollable list with clickable child elements right.

 - elias