From d9a007586c2aa538776e3030a636269d38653e98 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Sun, 26 Nov 2023 12:58:10 -0600 Subject: [PATCH] all: [API] replace tag parameter of Source.Event with per-filter tags Until now, every event has had a particular target. We're about to simplify key event delivery to match the first matching filter, so there is no longer a global meaning to the tag argument to Source.Event. Add fields to filters to specify their target tags. Signed-off-by: Elias Naur --- gesture/gesture.go | 9 ++-- io/input/key_test.go | 4 +- io/input/pointer_test.go | 33 +++++++++++- io/input/router.go | 109 +++++++++++++++++++++++++++------------ io/input/router_test.go | 2 +- io/key/key.go | 11 ++-- io/pointer/doc.go | 31 +++-------- io/pointer/pointer.go | 5 +- io/transfer/transfer.go | 4 ++ widget/button.go | 9 ++-- widget/dnd.go | 2 +- widget/dnd_test.go | 8 +-- widget/editor.go | 46 ++++++++--------- widget/enum.go | 9 ++-- widget/example_test.go | 2 +- widget/selectable.go | 26 +++++----- 16 files changed, 195 insertions(+), 115 deletions(-) diff --git a/gesture/gesture.go b/gesture/gesture.go index 8167f7d6..c9a289eb 100644 --- a/gesture/gesture.go +++ b/gesture/gesture.go @@ -44,7 +44,7 @@ func (h *Hover) Add(ops *op.Ops) { // Update state and report whether a pointer is inside the area. func (h *Hover) Update(q input.Source) bool { for { - ev, ok := q.Event(h, pointer.Filter{Kinds: pointer.Enter | pointer.Leave}) + ev, ok := q.Event(pointer.Filter{Target: h, Kinds: pointer.Enter | pointer.Leave}) if !ok { break } @@ -176,7 +176,7 @@ func (c *Click) Pressed() bool { // Update state and return the next click events, if any. func (c *Click) Update(q input.Source) (ClickEvent, bool) { for { - evt, ok := q.Event(c, pointer.Filter{Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave}) + evt, ok := q.Event(pointer.Filter{Target: c, Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave}) if !ok { break } @@ -268,11 +268,12 @@ func (s *Scroll) Stop() { func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis, bounds image.Rectangle) int { total := 0 f := pointer.Filter{ + Target: s, Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll, ScrollBounds: bounds, } for { - evt, ok := q.Event(s, f) + evt, ok := q.Event(f) if !ok { break } @@ -372,7 +373,7 @@ func (d *Drag) Add(ops *op.Ops) { // Update state and return the next drag event, if any. func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) (pointer.Event, bool) { for { - ev, ok := q.Event(d, pointer.Filter{Kinds: pointer.Press | pointer.Drag | pointer.Release}) + ev, ok := q.Event(pointer.Filter{Target: d, Kinds: pointer.Press | pointer.Drag | pointer.Release}) if !ok { break } diff --git a/io/input/key_test.go b/io/input/key_test.go index 14d0b8d3..5c88f1bc 100644 --- a/io/input/key_test.go +++ b/io/input/key_test.go @@ -294,6 +294,7 @@ func TestFocusScroll(t *testing.T) { filters := []event.Filter{ key.FocusFilter{}, pointer.Filter{ + Target: h, Kinds: pointer.Scroll, ScrollBounds: image.Rect(-100, -100, 100, 100), }, @@ -322,7 +323,8 @@ func TestFocusClick(t *testing.T) { filters := []event.Filter{ key.FocusFilter{}, pointer.Filter{ - Kinds: pointer.Press | pointer.Release, + Target: h, + Kinds: pointer.Press | pointer.Release, }, } assertEventPointerTypeSequence(t, events(r, h, filters...), pointer.Cancel) diff --git a/io/input/pointer_test.go b/io/input/pointer_test.go index 4b4ecbf8..b9bccc4c 100644 --- a/io/input/pointer_test.go +++ b/io/input/pointer_test.go @@ -19,6 +19,17 @@ import ( "gioui.org/op/clip" ) +func TestPointerNilTarget(t *testing.T) { + r := new(Router) + r.Event(pointer.Filter{Kinds: pointer.Press}) + r.Frame(new(op.Ops)) + r.Queue(pointer.Event{Kind: pointer.Press}) + // Nil Targets should not receive events. + if _, ok := r.Event(pointer.Filter{Kinds: pointer.Press}); ok { + t.Errorf("nil target received event") + } +} + func TestPointerWakeup(t *testing.T) { handler := new(int) var ops op.Ops @@ -55,9 +66,29 @@ func TestPointerDrag(t *testing.T) { } func events(r *Router, h event.Tag, filters ...event.Filter) []event.Event { + // Hack to facilitate transition to per-filter tags. + for i, f := range filters { + switch f := f.(type) { + case key.Filter: + f.Target = h + filters[i] = f + case key.FocusFilter: + f.Target = h + filters[i] = f + case transfer.SourceFilter: + f.Target = h + filters[i] = f + case transfer.TargetFilter: + f.Target = h + filters[i] = f + case pointer.Filter: + f.Target = h + filters[i] = f + } + } var events []event.Event for { - e, ok := r.Event(h, filters...) + e, ok := r.Event(filters...) if !ok { break } diff --git a/io/input/router.go b/io/input/router.go index f00e896a..209ed3c3 100644 --- a/io/input/router.go +++ b/io/input/router.go @@ -35,30 +35,23 @@ type Router struct { queue keyQueue } cqueue clipboardQueue - // states is the list of pending state changes resulting from // incoming events. The first element, if present, contains the state // and events for the current frame. changes []stateChange - - reader ops.Reader - + reader ops.Reader // InvalidateCmd summary. wakeup bool wakeupTime time.Time - // Changes queued for next call to Frame. commands []Command - // transfers is the pending transfer.DataEvent.Open functions. transfers []io.ReadCloser - // deferring is set if command execution and event delivery is deferred // to the next frame. deferring bool - - // scratchFilter is for garbage-free construction of ephemeral filters. - scratchFilter filter + // scratchFilters is for garbage-free construction of ephemeral filters. + scratchFilters []taggedFilter } // Source implements the interface between a Router and user interface widgets. @@ -131,6 +124,12 @@ type filter struct { key keyFilter } +// taggedFilter is a filter for a particular tag. +type taggedFilter struct { + tag event.Tag + filter filter +} + // stateChange represents the new state and outgoing events // resulting from an incoming event. type stateChange struct { @@ -173,51 +172,94 @@ func (s Source) Enabled() bool { return s.r != nil } -// Event returns the next event for the handler tag that matches one -// or more of filters. -func (s Source) Event(k event.Tag, filters ...event.Filter) (event.Event, bool) { +// Event returns the next event that matches at least one of filters. +func (s Source) Event(filters ...event.Filter) (event.Event, bool) { if !s.Enabled() { return nil, false } - return s.r.Event(k, filters...) + return s.r.Event(filters...) } -func (q *Router) Event(k event.Tag, filters ...event.Filter) (event.Event, bool) { - h := q.stateFor(k) - q.scratchFilter.Reset() - // Record handler filters and add reset events. +func (q *Router) Event(filters ...event.Filter) (event.Event, bool) { + // Merge filters into scratch filters. + q.scratchFilters = q.scratchFilters[:0] for _, f := range filters { - q.scratchFilter.Add(f) - switch f.(type) { + var t event.Tag + switch f := f.(type) { + case key.Filter: + t = f.Target + case transfer.SourceFilter: + t = f.Target + case transfer.TargetFilter: + t = f.Target case key.FocusFilter: + t = f.Target + case pointer.Filter: + t = f.Target + } + if t == nil { + continue + } + var filter *filter + for i := range q.scratchFilters { + s := &q.scratchFilters[i] + if s.tag == t { + filter = &s.filter + break + } + } + if filter == nil { + n := len(q.scratchFilters) + q.scratchFilters = append(q.scratchFilters, taggedFilter{tag: t}) + filter = &q.scratchFilters[n].filter + } + filter.Add(f) + } + for _, tf := range q.scratchFilters { + h := q.stateFor(tf.tag) + h.filter.Merge(tf.filter) + h.nextFilter.Merge(tf.filter) + } + // Deliver reset event, if any. + for _, f := range filters { + switch f := f.(type) { + case key.FocusFilter: + if f.Target == nil { + break + } + h := q.stateFor(f.Target) if reset, ok := h.key.ResetEvent(); ok { return reset, true } case pointer.Filter: + if f.Target == nil { + break + } + h := q.stateFor(f.Target) if reset, ok := h.pointer.ResetEvent(); ok { return reset, true } } } - h.nextFilter.Merge(q.scratchFilter) if !q.deferring { for i := range q.changes { change := &q.changes[i] - j := 0 - for j < len(change.events) { - evt := change.events[j] - if evt.tag != k || !q.scratchFilter.Matches(evt.event) { - j++ - continue + for j, evt := range change.events { + for _, tf := range q.scratchFilters { + if evt.tag == tf.tag && tf.filter.Matches(evt.event) { + change.events = append(change.events[:j], change.events[j+1:]...) + // Fast forward state to last matched. + q.collapseState(i) + return evt.event, true + } } - change.events = append(change.events[:j], change.events[j+1:]...) - // Fast forward state to last matched. - q.collapseState(i) - return evt.event, true } } } - h.processedFilter.Merge(q.scratchFilter) + for _, tf := range q.scratchFilters { + h := q.stateFor(tf.tag) + h.processedFilter.Merge(tf.filter) + } return nil, false } @@ -691,6 +733,9 @@ func (q *Router) EditorState() EditorState { } func (q *Router) stateFor(tag event.Tag) *handler { + if tag == nil { + panic("internal error: nil tag") + } s, ok := q.handlers[tag] if !ok { s = new(handler) diff --git a/io/input/router_test.go b/io/input/router_test.go index 0f856360..b8928829 100644 --- a/io/input/router_test.go +++ b/io/input/router_test.go @@ -16,7 +16,7 @@ func TestNoFilterAllocs(t *testing.T) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - s.Event(nil, pointer.Filter{}) + s.Event(pointer.Filter{}) } }) if allocs := b.AllocsPerOp(); allocs != 0 { diff --git a/io/key/key.go b/io/key/key.go index 7848eb74..07dd19e4 100644 --- a/io/key/key.go +++ b/io/key/key.go @@ -18,8 +18,9 @@ import ( "gioui.org/op" ) -// Filter matches [Event]s. +// Filter matches any [Event] that matches the parameters. type Filter struct { + Target event.Tag // Required is the set of modifiers that must be included in events matched. Required Modifiers // Optional is the set of modifiers that may be included in events matched. @@ -108,8 +109,12 @@ type EditEvent struct { Text string } -// FocusFilter matches [FocusEvent]s. -type FocusFilter struct{} +// FocusFilter matches any [FocusEvent], [EditEvent], [SnippetEvent], +// or [SelectionEvent] with the specified target. +type FocusFilter struct { + // Target is a tag specified in a previous event.Op. + Target event.Tag +} // InputHint changes the on-screen-keyboard type. That hints the // type of data that might be entered by the user. diff --git a/io/pointer/doc.go b/io/pointer/doc.go index d10127ab..c70ef8ae 100644 --- a/io/pointer/doc.go +++ b/io/pointer/doc.go @@ -5,36 +5,19 @@ Package pointer implements pointer events and operations. A pointer is either a mouse controlled cursor or a touch object such as a finger. -The InputOp operation is used to declare a handler ready for pointer -events. Use an event.Queue to receive events. - -# Kinds - -Only events that match a specified list of types are delivered to a handler. - -For example, to receive Press, Drag, and Release events (but not Move, Enter, -Leave, or Scroll): - - var ops op.Ops - var h *Handler = ... - - pointer.InputOp{ - Tag: h, - Kinds: pointer.Press | pointer.Drag | pointer.Release, - }.Add(ops) - -Cancel events are always delivered. +The [event.Op] operation is used to declare a handler ready for pointer +events. # Hit areas -Clip operations from package op/clip are used for specifying -hit areas where subsequent InputOps are active. +Clip operations from package [op/clip] are used for specifying +hit areas where handlers may receive events. For example, to set up a handler with a rectangular hit area: r := image.Rectangle{...} area := clip.Rect(r).Push(ops) - pointer.InputOp{Tag: h}.Add(ops) + event.Op{Tag: h}.Add(ops) area.Pop() Note that hit areas behave similar to painting: the effective area of a stack @@ -54,11 +37,11 @@ For example: var h1, h2 *Handler area := clip.Rect(...).Push(ops) - pointer.InputOp{Tag: h1}.Add(Ops) + event.Op{Tag: h1}.Add(Ops) area.Pop() area := clip.Rect(...).Push(ops) - pointer.InputOp{Tag: h2}.Add(ops) + event.Op{Tag: h2}.Add(ops) area.Pop() implies a tree of two inner nodes, each with one pointer handler attached. diff --git a/io/pointer/pointer.go b/io/pointer/pointer.go index 493d08bb..08a4e6a5 100644 --- a/io/pointer/pointer.go +++ b/io/pointer/pointer.go @@ -54,8 +54,11 @@ type PassStack struct { macroID uint32 } -// Filter matches [Event]s. +// Filter matches every [Event] that target the Tag and whose kind is +// included in Kinds. Note that only tags specified in [event.Op] can +// be targeted by pointer events. type Filter struct { + Target event.Tag // Kinds is a bitwise-or of event types to match. Kinds Kind // ScrollBounds describe the maximum scrollable distances in both diff --git a/io/transfer/transfer.go b/io/transfer/transfer.go index 00f0089f..b4d633f5 100644 --- a/io/transfer/transfer.go +++ b/io/transfer/transfer.go @@ -42,6 +42,8 @@ func (OfferCmd) ImplementsCommand() {} // as well as [InitiateEvent] and [CancelEvent]. // Use multiple filters to offer multiple types. type SourceFilter struct { + // Target is a tag included in a previous event.Op. + Target event.Tag // Type is the MIME type supported by this source. Type string } @@ -49,6 +51,8 @@ type SourceFilter struct { // TargetFilter filters for any [DataEvent] whose type matches a MIME type // as well as [CancelEvent]. Use multiple filters to accept multiple types. type TargetFilter struct { + // Target is a tag included in a previous event.Op. + Target event.Tag // Type is the MIME type accepted by this target. Type string } diff --git a/widget/button.go b/widget/button.go index 4888166d..8f7a5327 100644 --- a/widget/button.go +++ b/widget/button.go @@ -148,13 +148,16 @@ func (b *Clickable) Update(gtx layout.Context) (Click, bool) { } } filters := []event.Filter{ - key.FocusFilter{}, + key.FocusFilter{Target: b}, } if b.focused { - filters = append(filters, key.Filter{Name: key.NameReturn}, key.Filter{Name: key.NameSpace}) + filters = append(filters, + key.Filter{Target: b, Name: key.NameReturn}, + key.Filter{Target: b, Name: key.NameSpace}, + ) } for { - e, ok := gtx.Event(b, filters...) + e, ok := gtx.Event(filters...) if !ok { break } diff --git a/widget/dnd.go b/widget/dnd.go index afc2dda2..0932e9b2 100644 --- a/widget/dnd.go +++ b/widget/dnd.go @@ -69,7 +69,7 @@ func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) { d.pos = pos for { - e, ok := gtx.Event(d, transfer.SourceFilter{Type: d.Type}) + e, ok := gtx.Event(transfer.SourceFilter{Target: d, Type: d.Type}) if !ok { break } diff --git a/widget/dnd_test.go b/widget/dnd_test.go index 705cec4c..750a0308 100644 --- a/widget/dnd_test.go +++ b/widget/dnd_test.go @@ -35,7 +35,7 @@ func TestDraggable(t *testing.T) { stack.Pop() drag.Update(gtx) - r.Event(tgt, transfer.TargetFilter{Type: drag.Type}) + r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type}) r.Frame(gtx.Ops) r.Queue( pointer.Event{ @@ -53,10 +53,10 @@ func TestDraggable(t *testing.T) { ) ofr := &offer{data: "hello"} drag.Update(gtx) - r.Event(tgt, transfer.TargetFilter{Type: drag.Type}) + r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type}) drag.Offer(gtx, "file", ofr) - e, ok := r.Event(tgt, transfer.TargetFilter{Type: drag.Type}) + e, ok := r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type}) if !ok { t.Fatalf("expected event") } @@ -67,7 +67,7 @@ func TestDraggable(t *testing.T) { if ofr.closed { t.Error("offer closed prematurely") } - e, ok = r.Event(tgt, transfer.TargetFilter{Type: drag.Type}) + e, ok = r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type}) if !ok { t.Fatalf("expected event") } diff --git a/widget/editor.go b/widget/editor.go index 79408879..ab0d5ac2 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -337,50 +337,50 @@ func (e *Editor) processKey(gtx layout.Context) { if e.text.Changed() { e.events = append(e.events, ChangeEvent{}) } - filters := []event.Filter{key.FocusFilter{}, transfer.TargetFilter{Type: "application/text"}} + filters := []event.Filter{key.FocusFilter{Target: e}, transfer.TargetFilter{Target: e, Type: "application/text"}} if e.focused { filters = append(filters, - key.Filter{Name: key.NameEnter, Optional: key.ModShift}, - key.Filter{Name: key.NameReturn, Optional: key.ModShift}, + key.Filter{Target: e, Name: key.NameEnter, Optional: key.ModShift}, + key.Filter{Target: e, Name: key.NameReturn, Optional: key.ModShift}, - key.Filter{Name: "Z", Required: key.ModShortcut, Optional: key.ModShift}, - key.Filter{Name: "C", Required: key.ModShortcut}, - key.Filter{Name: "V", Required: key.ModShortcut}, - key.Filter{Name: "X", Required: key.ModShortcut}, - key.Filter{Name: "A", Required: key.ModShortcut}, + key.Filter{Target: e, Name: "Z", Required: key.ModShortcut, Optional: key.ModShift}, + key.Filter{Target: e, Name: "C", Required: key.ModShortcut}, + key.Filter{Target: e, Name: "V", Required: key.ModShortcut}, + key.Filter{Target: e, Name: "X", Required: key.ModShortcut}, + key.Filter{Target: e, Name: "A", Required: key.ModShortcut}, - key.Filter{Name: key.NameDeleteBackward, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Name: key.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Target: e, Name: key.NameDeleteBackward, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Target: e, Name: key.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Name: key.NameHome, Optional: key.ModShift}, - key.Filter{Name: key.NameEnd, Optional: key.ModShift}, - key.Filter{Name: key.NamePageDown, Optional: key.ModShift}, - key.Filter{Name: key.NamePageUp, Optional: key.ModShift}, + key.Filter{Target: e, Name: key.NameHome, Optional: key.ModShift}, + key.Filter{Target: e, Name: key.NameEnd, Optional: key.ModShift}, + key.Filter{Target: e, Name: key.NamePageDown, Optional: key.ModShift}, + key.Filter{Target: e, Name: key.NamePageUp, Optional: key.ModShift}, ) caret, _ := e.text.Selection() if caret > 0 { if gtx.Locale.Direction.Progression() == system.FromOrigin { filters = append(filters, - key.Filter{Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Target: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Target: e, Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}, ) } else { filters = append(filters, - key.Filter{Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Target: e, Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Target: e, Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}, ) } } if caret < e.text.Len() { if gtx.Locale.Direction.Progression() == system.FromOrigin { filters = append(filters, - key.Filter{Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Target: e, Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Target: e, Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}, ) } else { filters = append(filters, - key.Filter{Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Target: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Target: e, Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}, ) } } @@ -388,7 +388,7 @@ func (e *Editor) processKey(gtx layout.Context) { // adjust keeps track of runes dropped because of MaxLen. var adjust int for { - ke, ok := gtx.Event(e, filters...) + ke, ok := gtx.Event(filters...) if !ok { break } diff --git a/widget/enum.go b/widget/enum.go index 2f208381..ecf4eaf6 100644 --- a/widget/enum.go +++ b/widget/enum.go @@ -65,13 +65,16 @@ func (e *Enum) Update(gtx layout.Context) bool { } } filters := []event.Filter{ - key.FocusFilter{}, + key.FocusFilter{Target: &state.tag}, } if e.focused && e.focus == state.key { - filters = append(filters, key.Filter{Name: key.NameReturn}, key.Filter{Name: key.NameSpace}) + filters = append(filters, + key.Filter{Target: &state.tag, Name: key.NameReturn}, + key.Filter{Target: &state.tag, Name: key.NameSpace}, + ) } for { - ev, ok := gtx.Event(&state.tag, filters...) + ev, ok := gtx.Event(filters...) if !ok { break } diff --git a/widget/example_test.go b/widget/example_test.go index c6e16c21..c2910fff 100644 --- a/widget/example_test.go +++ b/widget/example_test.go @@ -110,7 +110,7 @@ func ExampleDraggable_Layout() { // Check for the received data. for { - ev, ok := gtx.Event(&drop, transfer.TargetFilter{Type: mime}) + ev, ok := gtx.Event(transfer.TargetFilter{Target: &drop, Type: mime}) if !ok { break } diff --git a/widget/selectable.go b/widget/selectable.go index 4486cf10..af3d04c8 100644 --- a/widget/selectable.go +++ b/widget/selectable.go @@ -302,27 +302,27 @@ func (e *Selectable) clickDragEvents(gtx layout.Context) []event.Event { func (e *Selectable) processKey(gtx layout.Context) { filters := []event.Filter{ - key.FocusFilter{}, + key.FocusFilter{Target: e}, } if e.focused { filters = append(filters, - key.Filter{Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Target: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Target: e, Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Target: e, Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Target: e, Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Name: key.NamePageUp, Optional: key.ModShift}, - key.Filter{Name: key.NamePageDown, Optional: key.ModShift}, - key.Filter{Name: key.NameEnd, Optional: key.ModShift}, - key.Filter{Name: key.NameHome, Optional: key.ModShift}, + key.Filter{Target: e, Name: key.NamePageUp, Optional: key.ModShift}, + key.Filter{Target: e, Name: key.NamePageDown, Optional: key.ModShift}, + key.Filter{Target: e, Name: key.NameEnd, Optional: key.ModShift}, + key.Filter{Target: e, Name: key.NameHome, Optional: key.ModShift}, - key.Filter{Name: "C", Required: key.ModShortcut}, - key.Filter{Name: "X", Required: key.ModShortcut}, - key.Filter{Name: "A", Required: key.ModShortcut}, + key.Filter{Target: e, Name: "C", Required: key.ModShortcut}, + key.Filter{Target: e, Name: "X", Required: key.ModShortcut}, + key.Filter{Target: e, Name: "A", Required: key.ModShortcut}, ) } for { - ke, ok := gtx.Event(e, filters...) + ke, ok := gtx.Event(filters...) if !ok { break }