diff --git a/gesture/gesture.go b/gesture/gesture.go index fd764645..ff3ced65 100644 --- a/gesture/gesture.go +++ b/gesture/gesture.go @@ -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 diff --git a/gesture/gesture_test.go b/gesture/gesture_test.go new file mode 100644 index 00000000..93752fab --- /dev/null +++ b/gesture/gesture_test.go @@ -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 +}