~eliasnaur/gio#572: 
Fwd: Detecting mouse wheel

Detecting mouse scrolls is non intuitive.

In the example on https://gioui.org/doc/architecture/input,

// Declare tag as being one of the targets. event.Op(ops, tag)

is called before painting operations commence.

However, it this example [1], event.Op(&ops, tag) needs to be called at the very end of case app.FrameEvent:

If event.Op(&ops, tag) is called too early, for example around the lines of code where the tag is used and it contextually is natural to register it, behaviour is strange:

  • scrolls are not detected
  • however, scrolling while pressing (and not releasing) any mouse buttion works well. Strange

Finally, the scroll event in Y-direction only works if ScrollBounds has x distance as well:

 ScrollBounds: image.Rectangle{
          Min: image.Point{X: -1, Y: -1},
          Max: image.Point{X: +1, Y: +1},
        },

Thanks to JK for helping identify these

Best regards JE

[1] https://github.com/jonegil/gui-with-gio/blob/5c4a46b6760257a3a4dc95a4df49e79c621ebb00/teleprompter/code/main.go#L371

---------- Forwarded message --------- From: Jon Egil Strand jon.egil.strand@gmail.com Date: Tue, 19 Mar 2024 at 20:57 Subject: Re: Detecting mouse wheel To: jkvatne@online.no Cc: ~eliasnaur/gio@lists.sr.ht

Thank you for your reply.

To add mystery to the enigma:

Adding breadth to X in ScrollBounds work as you describe.

Min: image.Point{X: 0, Y: -100}, Max: image.Point{X: 1, Y: +100},

If I then press and hold Left, Right or Wheel, scrolling works as expected. No buttonpress = no scroll

Jon Egil

On Tue, 19 Mar 2024 at 09:32, Jan Kåre Vatne jkvatne@online.no wrote:

Hi Jon. Yes the scrolling is strange.

I have been able to make your app work by the following changes.

  1. Move the tag registration to just before the Finalize section 2a. Either change the ScrollBounds to { Min: image.Point{X: 0, Y: -100}, Max: image.Point{X: 1, Y: +100}} 2b. Or add an identical ScrollBound to the mouse button filter.

The last change is really strange. I have tried to debug the gio code, but it is very complicated. I think there must be an error in the gio code, concerning merging of filters. There is no reason the x range should be different from 0,0. And it fails only when there are two different scroll bounds.

A few more tips: The red stripe should be one line height, scaled with the font size. The maximum scroll delta (now 100) should also be one line height.

Jan Kåre Vatne

Status
RESOLVED FIXED
Submitter
~jonegil
Assigned to
No-one
Submitted
2 months ago
Updated
2 months ago
Labels
No labels applied.

~whereswaldon 2 months ago

Hmm. I tried making a test program like this:

// SPDX-License-Identifier: Unlicense OR MIT

package main

// A simple Gio program. See https://gioui.org for more information.

import (
	"image"
	"image/color"
	"log"
	"os"

	"gioui.org/app"
	"gioui.org/font/gofont"
	"gioui.org/io/event"
	"gioui.org/io/input"
	"gioui.org/io/pointer"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/text"
	"gioui.org/widget/material"
)

func main() {
	go func() {
		w := app.NewWindow()
		if err := loop(w); err != nil {
			log.Fatal(err)
		}
		os.Exit(0)
	}()
	app.Main()
}

var tag = new(bool)
var c = color.NRGBA{R: 0xff, A: 0xff}

func doButton(ops *op.Ops, q input.Source) {
	// Confine the area of interest to a 100x100 rectangle.
	defer clip.Rect{Max: image.Pt(100, 100)}.Push(ops).Pop()

	// Declare `tag` as being one of the targets.
	event.Op(ops, tag)

	// Process events that arrived between the last frame and this one.
	for {
		ev, ok := q.Event(pointer.Filter{
			Target: tag,
			Kinds:  pointer.Scroll,
			ScrollBounds: image.Rectangle{
				Min: image.Point{
					Y: -100,
				},
				Max: image.Point{
					Y: 100,
				},
			},
		})
		if !ok {
			break
		}

		if x, ok := ev.(pointer.Event); ok {
			switch x.Kind {
			case pointer.Scroll:
				c.G = uint8(x.Scroll.Y/100 + float32(c.G))
			}
		}
	}

	paint.ColorOp{Color: c}.Add(ops)
	paint.PaintOp{}.Add(ops)
}

func loop(w *app.Window) error {
	th := material.NewTheme()
	th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
	var ops op.Ops
	for {
		switch e := w.NextEvent().(type) {
		case app.DestroyEvent:
			return e.Err
		case app.FrameEvent:
			gtx := app.NewContext(&ops, e)
			doButton(gtx.Ops, gtx.Source)
			e.Frame(gtx.Ops)
		}
	}
}

I don't see anything unexpected though. I can scroll up and down to gradually adjust the degree of green in the color. I didn't have to set an X coordinate on the scroll bounds or anything. Jon, does this program work for you?

~jonegil 2 months ago*

Thank you Chris!

Yes, your program works as intended without X coordinate. If I add an explicit X it works equally well with X:0 and X: 1 (or X:-1).

Your code gives clean events:

SCROLL: {Kind:Scroll Source:Mouse PointerID:0 Priority:Foremost Time:5h18m6.992s Buttons: Position:(64.63672,46.26953) Scroll:(0,100) Modifiers:}
SCROLL: {Kind:Scroll Source:Mouse PointerID:0 Priority:Foremost Time:5h18m7.52s Buttons: Position:(64.63672,46.26953) Scroll:(0,-100) Modifiers:}

Adding a print to my original progam [1 above] and removing X or setting X in Scrollbounds to 0 gives

SCROLL: {Kind:Scroll Source:Mouse PointerID:0 Priority:Foremost Time:5h19m1.32s Buttons: Position:(476.33203,382.47656) Scroll:(0,0) Modifiers:}
SCROLL: {Kind:Scroll Source:Mouse PointerID:0 Priority:Foremost Time:5h19m1.62s Buttons: Position:(476.33203,382.47656) Scroll:(0,0) Modifiers:}
SCROLL: {Kind:Scroll Source:Mouse PointerID:0 Priority:Foremost Time:5h19m1.688s Buttons: Position:(476.33203,382.47656) Scroll:(0,0) Modifiers:}

I.e. the event registers, but the Y-value is 0.

Best regards J

~eliasnaur 2 months ago

~jonegil, can I ask you to help me pinpoint the issue by minimizing your program as much as possible? As I understand it, Chris' program works as intended, so I'm trying to locate the breaking difference from his program to yours. Thank you.

~jonegil 2 months ago*

Thanks Elias

A minimalist version that reproduces the effect is available here. It uses gio 0.6. The program listens for scrolls in the window and prints in the terminal. https://github.com/jonegil/gui-with-gio/blob/bug-scrolling/teleprompter/code/main.go

If the event code only listens for scroll events, it catches the Y dimension as expected, including when X in ScrollBounds is 0

// Scrolled a mouse wheel?
for {
  ev, ok := gtx.Event(
    pointer.Filter{
      Target: tag,
      Kinds:  pointer.Scroll,
      ScrollBounds: image.Rectangle{
        Min: image.Point{X: 0, Y: -1},
        Max: image.Point{X: 0, Y: +1},
      },
    },
  )
  if !ok {
    break
  }
  fmt.Printf("SCROLL: %+v\n", ev)
}

Printed output, +/- 1 in Scroll.Y:

SCROLL: {Kind:Scroll Source:Mouse PointerID:0 Priority:Foremost Time:10h38m56.531s Buttons: Position:(471,276) Scroll:(0,1) Modifiers:}
SCROLL: {Kind:Scroll Source:Mouse PointerID:0 Priority:Foremost Time:10h38m56.921s Buttons: Position:(471,276) Scroll:(0,-1) Modifiers:}

However, if it also listens for pointer clicks, the Y dimension is ignored:

// Scrolled a mouse wheel?
      for {
        ev, ok := gtx.Event(
          pointer.Filter{
            Target: tag,
            Kinds:  pointer.Scroll,
            ScrollBounds: image.Rectangle{
              Min: image.Point{X: 0, Y: -1},
              Max: image.Point{X: 0, Y: +1},
            },
          },
        )
        if !ok {
          break
        }
        fmt.Printf("SCROLL: %+v\n", ev)
      }

      // Pressed a mouse button?
      for {
        ev, ok := gtx.Event(
          pointer.Filter{
            Target: tag,
            Kinds:  pointer.Press,
          },
        )
        if !ok {
          break
        }
        fmt.Printf("PRESS : %+v\n", ev)
      }

Printed output, 0 Scroll.Y

SCROLL: {Kind:Scroll Source:Mouse PointerID:0 Priority:Foremost Time:10h39m12.312s Buttons: Position:(540,422) Scroll:(0,0) Modifiers:}
SCROLL: {Kind:Scroll Source:Mouse PointerID:0 Priority:Foremost Time:10h39m12.671s Buttons: Position:(540,422) Scroll:(0,0) Modifiers:}

However, by changing the ScrollBounds to have a +/-1 X-dimension, things start working again

ScrollBounds: image.Rectangle{
  Min: image.Point{X: -1, Y: -1},
  Max: image.Point{X: +1, Y: +1},
},

the output again shows non-zero values in Scroll.Y even when I listen both for scrolls and for clicks.

SCROLL: {Kind:Scroll Source:Mouse PointerID:0 Priority:Foremost Time:10h50m26.484s Buttons: Position:(488,334) Scroll:(0,1) Modifiers:}
SCROLL: {Kind:Scroll Source:Mouse PointerID:0 Priority:Foremost Time:10h50m26.609s Buttons: Position:(488,334) Scroll:(0,-1) Modifiers:}

~egonelbre 2 months ago

I think I've figured out the issue, the scrollRange calculations are using Rectangle.Union (one example https://git.sr.ht/~eliasnaur/gio/tree/main/item/io/input/pointer.go#L300)

Rectangle.Union is implemented as:

func (r Rectangle) Union(s Rectangle) Rectangle {
if r.Empty() { return s }
if s.Empty() { return r }

The fix is to replace image.Rectangle with a different type, e.g. ScrollRange, that implements Union without collapsing the ranges when one of the dimensions is empty.

~eliasnaur 2 months ago

~egonelbre is right, of course. The collapsing behaviour of Union suggests to me that scroll ranges should be independent. Say:

package pointer

type Filter struct {
    ....
    ScrollX ScrollRange
    ScrollY ScrollRange
}

type ScrollRange struct {
    Min, Max int
}

WDYT?

~jonegil 2 months ago

It makes sense to not use image.Rectangle for ScrollBounds,

But does that explain why the behaviour for Scrolls changes when I add a handler to listen for Clicks as well?

~eliasnaur 2 months ago

But does that explain why the behaviour for Scrolls changes when I add a handler to listen for Clicks as well?

Yes, the bug is that the two filters you register merge their scrollbounds with image.Rectangle.Union which collapses rectangles with zero area to (0,0,0,0).

The question remains how scroll bounds are best represented, while we're here and fixing the bug.

~egonelbre 2 months ago

Actually, there already exists layout.Constraints that solves a similar problem. However, that seems to assume that the ranges are >= 0.

As for whether to use ScrollX / ScrollY -- I don't have a strong opinion. However, it might be easier within the gio internal codebase to have a single value rather than two different ones. Similarly the scroll result in pointer.Event is returned as f32.Point -- so I kind of would expect the Scroll to be a single field at least. e.g.

   Scroll ScrollRanges
}
type ScrollRanges { X, Y ScrollRange }
type ScrollRange { Min, Max int }

Though, it does introduce some extra types when declaring the filters. The separate fields might be a good compromise.

~eliasnaur REPORTED FIXED 2 months ago

Elias Naur referenced this ticket in commit ee6cdec.

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