gesture: support multiple click actions

Double click or tap actions are common in user interfaces and this
commit adds support for it in the gesture package.
ClickEvent has now a new field NumClicks that contains the number of
successive clicks that occurred within 200ms.

Fixes gio#101

Signed-off-by: Pierre.Curto <pierre.curto@gmail.com>
This commit is contained in:
Pierre.Curto
2020-04-29 12:44:35 +02:00
committed by Elias Naur
parent b862f4f174
commit 4db3cb2afb
2 changed files with 106 additions and 1 deletions
+19 -1
View File
@@ -22,11 +22,20 @@ import (
"gioui.org/unit"
)
// The duration is somewhat arbitrary.
const doubleClickDuration = 200 * time.Millisecond
// Click detects click gestures in the form
// of ClickEvents.
type Click struct {
// state tracks the gesture state.
state ClickState
// clickedAt is the timestamp at which
// the last click occurred.
clickedAt time.Duration
// clicks is incremented if successive clicks
// are performed within a fixed duration.
clicks int
}
type ClickState uint8
@@ -39,6 +48,9 @@ type ClickEvent struct {
Position f32.Point
Source pointer.Source
Modifiers key.Modifiers
// NumClicks records successive clicks occurring
// within a short duration of each other.
NumClicks int
}
type ClickType uint8
@@ -122,7 +134,13 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
wasPressed := c.state == StatePressed
c.state = StateNormal
if wasPressed {
events = append(events, ClickEvent{Type: TypeClick, Position: e.Position, Source: e.Source, Modifiers: e.Modifiers})
if e.Time-c.clickedAt < doubleClickDuration {
c.clicks++
} else {
c.clicks = 1
}
c.clickedAt = e.Time
events = append(events, ClickEvent{Type: TypeClick, Position: e.Position, Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
}
case pointer.Cancel:
c.state = StateNormal
+87
View File
@@ -0,0 +1,87 @@
package gesture
import (
"testing"
"time"
"gioui.org/io/event"
"gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/op"
)
func TestMouseClicks(t *testing.T) {
for _, tc := range []struct {
label string
events []event.Event
clicks []int // number of combined clicks per click (single, double...)
}{
{
label: "single click",
events: mouseClickEvents(200 * time.Millisecond),
clicks: []int{1},
},
{
label: "double click",
events: mouseClickEvents(
100*time.Millisecond,
100*time.Millisecond+doubleClickDuration-1),
clicks: []int{1, 2},
},
{
label: "two single clicks",
events: mouseClickEvents(
100*time.Millisecond,
100*time.Millisecond+doubleClickDuration+1),
clicks: []int{1, 1},
},
} {
t.Run(tc.label, func(t *testing.T) {
var click Click
var ops op.Ops
click.Add(&ops)
var r router.Router
r.Frame(&ops)
r.Add(tc.events...)
events := click.Events(&r)
clicks := filterMouseClicks(events)
if got, want := len(clicks), len(tc.clicks); got != want {
t.Fatalf("got %d mouse clicks, expected %d", got, want)
}
for i, click := range clicks {
if got, want := click.NumClicks, tc.clicks[i]; got != want {
t.Errorf("got %d combined mouse clicks, expected %d", got, want)
}
}
})
}
}
func mouseClickEvents(times ...time.Duration) []event.Event {
press := pointer.Event{
Type: pointer.Press,
Source: pointer.Mouse,
Buttons: pointer.ButtonLeft,
}
events := make([]event.Event, 0, 2*len(times))
for _, t := range times {
release := press
release.Type = pointer.Release
release.Time = t
events = append(events, press, release)
}
return events
}
func filterMouseClicks(events []ClickEvent) []ClickEvent {
var clicks []ClickEvent
for _, ev := range events {
if ev.Type == TypeClick {
clicks = append(clicks, ev)
}
}
return clicks
}