io/input: implement lazy event routing

This change defers event routing from the time the event is queued until
the time Events is called. This allows a future change to execute
commands immediately and to react to event order changes during a frame.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2023-11-15 19:14:14 -06:00
parent 651094d692
commit 9dfada745c
8 changed files with 390 additions and 230 deletions
+1 -1
View File
@@ -319,7 +319,7 @@ func (w *Window) processFrame(d driver) {
if mime, txt, ok := q.WriteClipboard(); ok { if mime, txt, ok := q.WriteClipboard(); ok {
d.WriteClipboard(mime, txt) d.WriteClipboard(mime, txt)
} }
if q.ReadClipboard() { if q.ClipboardRequested() {
d.ReadClipboard() d.ReadClipboard()
} }
oldState := w.imeState oldState := w.imeState
+24 -19
View File
@@ -9,8 +9,12 @@ import (
"gioui.org/io/event" "gioui.org/io/event"
) )
// clipboardState contains the state for clipboard event routing.
type clipboardState struct {
receivers []event.Tag
}
type clipboardQueue struct { type clipboardQueue struct {
receivers map[event.Tag]struct{}
// request avoid read clipboard every frame while waiting. // request avoid read clipboard every frame while waiting.
requested bool requested bool
mime string mime string
@@ -28,22 +32,21 @@ func (q *clipboardQueue) WriteClipboard() (mime string, content []byte, ok bool)
return q.mime, content, true return q.mime, content, true
} }
// ReadClipboard reports if any new handler is waiting // ClipboardRequested reports if any new handler is waiting
// to read the clipboard. // to read the clipboard.
func (q *clipboardQueue) ReadClipboard() bool { func (q *clipboardQueue) ClipboardRequested(state clipboardState) bool {
if len(q.receivers) == 0 || q.requested { req := len(state.receivers) > 0 && q.requested
return false q.requested = false
} return req
q.requested = true
return true
} }
func (q *clipboardQueue) Push(evts []taggedEvent, e event.Event) []taggedEvent { func (q *clipboardQueue) Push(state clipboardState, e event.Event) (clipboardState, []taggedEvent) {
for r := range q.receivers { var evts []taggedEvent
for _, r := range state.receivers {
evts = append(evts, taggedEvent{tag: r, event: e}) evts = append(evts, taggedEvent{tag: r, event: e})
delete(q.receivers, r)
} }
return evts state.receivers = nil
return state, evts
} }
func (q *clipboardQueue) ProcessWriteClipboard(req clipboard.WriteCmd) { func (q *clipboardQueue) ProcessWriteClipboard(req clipboard.WriteCmd) {
@@ -56,12 +59,14 @@ func (q *clipboardQueue) ProcessWriteClipboard(req clipboard.WriteCmd) {
q.text = content q.text = content
} }
func (q *clipboardQueue) ProcessReadClipboard(tag event.Tag) { func (q *clipboardQueue) ProcessReadClipboard(state clipboardState, tag event.Tag) clipboardState {
if q.receivers == nil { for _, k := range state.receivers {
q.receivers = make(map[event.Tag]struct{}) if k == tag {
} return state
if _, ok := q.receivers[tag]; !ok { }
q.receivers[tag] = struct{}{}
q.requested = false
} }
n := len(state.receivers)
state.receivers = append(state.receivers[:n:n], tag)
q.requested = true
return state
} }
+7 -7
View File
@@ -28,9 +28,9 @@ func TestClipboardDuplicateEvent(t *testing.T) {
}, },
} }
router.Queue(event) router.Queue(event)
assertClipboardReadCmd(t, router, 0)
assertClipboardEvent(t, router.Events(&handler[0], transfer.TargetFilter{Type: "application/text"}), true) assertClipboardEvent(t, router.Events(&handler[0], transfer.TargetFilter{Type: "application/text"}), true)
assertClipboardEvent(t, router.Events(&handler[1], transfer.TargetFilter{Type: "application/text"}), true) assertClipboardEvent(t, router.Events(&handler[1], transfer.TargetFilter{Type: "application/text"}), true)
assertClipboardReadCmd(t, router, 0)
ops.Reset() ops.Reset()
// No ReadCmd // No ReadCmd
@@ -80,8 +80,8 @@ func TestQueueProcessReadClipboard(t *testing.T) {
}, },
} }
router.Queue(event) router.Queue(event)
assertClipboardReadCmd(t, router, 0)
assertClipboardEvent(t, router.Events(&handler[0], transfer.TargetFilter{Type: "application/text"}), true) assertClipboardEvent(t, router.Events(&handler[0], transfer.TargetFilter{Type: "application/text"}), true)
assertClipboardReadCmd(t, router, 0)
ops.Reset() ops.Reset()
// No ReadCmd // No ReadCmd
@@ -137,20 +137,20 @@ func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) {
func assertClipboardReadCmd(t *testing.T, router *Router, expected int) { func assertClipboardReadCmd(t *testing.T, router *Router, expected int) {
t.Helper() t.Helper()
if len(router.cqueue.receivers) != expected { if got := len(router.lastState().receivers); got != expected {
t.Error("unexpected number of receivers") t.Errorf("unexpected %d receivers, got %d", expected, got)
} }
if router.cqueue.ReadClipboard() != (expected > 0) { if router.ClipboardRequested() != (expected > 0) {
t.Error("missing requests") t.Error("missing requests")
} }
} }
func assertClipboardReadDuplicated(t *testing.T, router *Router, expected int) { func assertClipboardReadDuplicated(t *testing.T, router *Router, expected int) {
t.Helper() t.Helper()
if len(router.cqueue.receivers) != expected { if len(router.lastState().receivers) != expected {
t.Error("receivers removed") t.Error("receivers removed")
} }
if router.cqueue.ReadClipboard() != false { if router.ClipboardRequested() != false {
t.Error("duplicated requests") t.Error("duplicated requests")
} }
} }
+62 -56
View File
@@ -24,13 +24,17 @@ type EditorState struct {
type TextInputState uint8 type TextInputState uint8
type keyQueue struct { type keyQueue struct {
focus event.Tag
order []event.Tag order []event.Tag
dirOrder []dirFocusEntry dirOrder []dirFocusEntry
handlers map[event.Tag]*keyHandler handlers map[event.Tag]*keyHandler
state TextInputState
hint key.InputHint hint key.InputHint
content EditorState }
// keyState is the input state related to key events.
type keyState struct {
focus event.Tag
state TextInputState
content EditorState
} }
type keyHandler struct { type keyHandler struct {
@@ -67,21 +71,18 @@ func (q *keyQueue) inputHint(op key.InputHintOp) {
h.hint = op.Hint h.hint = op.Hint
} }
// InputState returns the last text input state as // InputState returns the input state and returns a state
// determined in Frame. // reset to [TextInputKeep].
func (q *keyQueue) InputState() TextInputState { func (s keyState) InputState() (keyState, TextInputState) {
state := q.state state := s.state
q.state = TextInputKeep s.state = TextInputKeep
return state return s, state
} }
// InputHint returns the input hint from the focused handler and whether it was // InputHint returns the input hint from the focused handler and whether it was
// changed since the last call. // changed since the last call.
func (q *keyQueue) InputHint() (key.InputHint, bool) { func (q *keyQueue) InputHint(state keyState) (key.InputHint, bool) {
if q.focus == nil { focused, ok := q.handlers[state.focus]
return q.hint, false
}
focused, ok := q.handlers[q.focus]
if !ok { if !ok {
return q.hint, false return q.hint, false
} }
@@ -108,13 +109,13 @@ func (q *keyQueue) ResetEvent(k event.Tag) (event.Event, bool) {
return key.FocusEvent{Focus: false}, true return key.FocusEvent{Focus: false}, true
} }
func (q *keyQueue) Frame() { func (q *keyQueue) Frame(state keyState) keyState {
for k, h := range q.handlers { for k, h := range q.handlers {
if !h.visible || !h.focusable { if !h.visible || !h.focusable {
if q.focus == k { if state.focus == k {
// Remove focus from the handler that is no longer focusable. // Remove focus from the handler that is no longer focusable.
q.focus = nil state.focus = nil
q.state = TextInputClose state.state = TextInputClose
} }
if !h.visible && !h.focusable { if !h.visible && !h.focusable {
delete(q.handlers, k) delete(q.handlers, k)
@@ -126,6 +127,7 @@ func (q *keyQueue) Frame() {
h.active = false h.active = false
} }
q.updateFocusLayout() q.updateFocusLayout()
return state
} }
// updateFocusLayout partitions input handlers handlers into rows // updateFocusLayout partitions input handlers handlers into rows
@@ -168,13 +170,13 @@ func (q *keyQueue) updateFocusLayout() {
} }
// MoveFocus attempts to move the focus in the direction of dir. // MoveFocus attempts to move the focus in the direction of dir.
func (q *keyQueue) MoveFocus(evts []taggedEvent, dir key.FocusDirection) []taggedEvent { func (q *keyQueue) MoveFocus(state keyState, dir key.FocusDirection) (keyState, []taggedEvent) {
if len(q.dirOrder) == 0 { if len(q.dirOrder) == 0 {
return nil return state, nil
} }
order := 0 order := 0
if q.focus != nil { if state.focus != nil {
order = q.handlers[q.focus].dirOrder order = q.handlers[state.focus].dirOrder
} }
focus := q.dirOrder[order] focus := q.dirOrder[order]
switch dir { switch dir {
@@ -186,8 +188,8 @@ func (q *keyQueue) MoveFocus(evts []taggedEvent, dir key.FocusDirection) []tagge
if dir == key.FocusBackward { if dir == key.FocusBackward {
order = -1 order = -1
} }
if q.focus != nil { if state.focus != nil {
order = q.handlers[q.focus].order order = q.handlers[state.focus].order
if dir == key.FocusForward { if dir == key.FocusForward {
order++ order++
} else { } else {
@@ -195,10 +197,10 @@ func (q *keyQueue) MoveFocus(evts []taggedEvent, dir key.FocusDirection) []tagge
} }
} }
order = (order + len(q.order)) % len(q.order) order = (order + len(q.order)) % len(q.order)
return q.Focus(evts, q.order[order]) return q.Focus(state, q.order[order])
case key.FocusRight, key.FocusLeft: case key.FocusRight, key.FocusLeft:
next := order next := order
if q.focus != nil { if state.focus != nil {
next = order + 1 next = order + 1
if dir == key.FocusLeft { if dir == key.FocusLeft {
next = order - 1 next = order - 1
@@ -207,7 +209,7 @@ func (q *keyQueue) MoveFocus(evts []taggedEvent, dir key.FocusDirection) []tagge
if 0 <= next && next < len(q.dirOrder) { if 0 <= next && next < len(q.dirOrder) {
newFocus := q.dirOrder[next] newFocus := q.dirOrder[next]
if newFocus.row == focus.row { if newFocus.row == focus.row {
return q.Focus(evts, newFocus.tag) return q.Focus(state, newFocus.tag)
} }
} }
case key.FocusUp, key.FocusDown: case key.FocusUp, key.FocusDown:
@@ -216,7 +218,7 @@ func (q *keyQueue) MoveFocus(evts []taggedEvent, dir key.FocusDirection) []tagge
delta = -1 delta = -1
} }
nextRow := 0 nextRow := 0
if q.focus != nil { if state.focus != nil {
nextRow = focus.row + delta nextRow = focus.row + delta
} }
var closest event.Tag var closest event.Tag
@@ -243,10 +245,10 @@ func (q *keyQueue) MoveFocus(evts []taggedEvent, dir key.FocusDirection) []tagge
order += delta order += delta
} }
if closest != nil { if closest != nil {
return q.Focus(evts, closest) return q.Focus(state, closest)
} }
} }
return nil return state, nil
} }
func (q *keyQueue) BoundsFor(t event.Tag) image.Rectangle { func (q *keyQueue) BoundsFor(t event.Tag) image.Rectangle {
@@ -281,35 +283,37 @@ func keyFilterMatch(f key.Filter, e key.Event) bool {
return true return true
} }
func (q *keyQueue) Focus(evts []taggedEvent, focus event.Tag) []taggedEvent { func (q *keyQueue) Focus(state keyState, focus event.Tag) (keyState, []taggedEvent) {
if focus != nil { if focus != nil {
if _, exists := q.handlers[focus]; !exists { if _, exists := q.handlers[focus]; !exists {
focus = nil focus = nil
} }
} }
if focus == q.focus { if focus == state.focus {
return evts return state, nil
} }
q.content = EditorState{} state.content = EditorState{}
if q.focus != nil { var evts []taggedEvent
evts = append(evts, taggedEvent{tag: q.focus, event: key.FocusEvent{Focus: false}}) if state.focus != nil {
evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: false}})
} }
q.focus = focus state.focus = focus
if q.focus != nil { if state.focus != nil {
evts = append(evts, taggedEvent{tag: q.focus, event: key.FocusEvent{Focus: true}}) evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: true}})
} }
if q.focus == nil || q.state == TextInputKeep { if state.focus == nil || state.state == TextInputKeep {
q.state = TextInputClose state.state = TextInputClose
} }
return evts return state, evts
} }
func (q *keyQueue) softKeyboard(show bool) { func (s keyState) softKeyboard(show bool) keyState {
if show { if show {
q.state = TextInputOpen s.state = TextInputOpen
} else { } else {
q.state = TextInputClose s.state = TextInputClose
} }
return s
} }
func (q *keyQueue) filter(tag event.Tag, f key.Filter) { func (q *keyQueue) filter(tag event.Tag, f key.Filter) {
@@ -349,26 +353,28 @@ func (q *keyQueue) inputOp(tag event.Tag, t f32.Affine2D, area int, bounds image
h.trans = t h.trans = t
} }
func (q *keyQueue) setSelection(req key.SelectionCmd) { func (q *keyQueue) setSelection(state keyState, req key.SelectionCmd) keyState {
if req.Tag != q.focus { if req.Tag != state.focus {
return return state
} }
q.content.Selection.Range = req.Range state.content.Selection.Range = req.Range
q.content.Selection.Caret = req.Caret state.content.Selection.Caret = req.Caret
return state
} }
func (q *keyQueue) editorState() EditorState { func (q *keyQueue) editorState(state keyState) EditorState {
s := q.content s := state.content
if f := q.focus; f != nil { if f := state.focus; f != nil {
s.Selection.Transform = q.handlers[f].trans s.Selection.Transform = q.handlers[f].trans
} }
return s return s
} }
func (q *keyQueue) setSnippet(req key.SnippetCmd) { func (q *keyQueue) setSnippet(state keyState, req key.SnippetCmd) keyState {
if req.Tag == q.focus { if req.Tag == state.focus {
q.content.Snippet = req.Snippet state.content.Snippet = req.Snippet
} }
return state
} }
func (t TextInputState) String() string { func (t TextInputState) String() string {
+2 -2
View File
@@ -464,14 +464,14 @@ func assertKeyEventUnexpected(t *testing.T, events []event.Event) {
func assertFocus(t *testing.T, router *Router, expected event.Tag) { func assertFocus(t *testing.T, router *Router, expected event.Tag) {
t.Helper() t.Helper()
if got := router.key.queue.focus; got != expected { if got := router.lastState().focus; got != expected {
t.Errorf("expected %v to be focused, got %v", expected, got) t.Errorf("expected %v to be focused, got %v", expected, got)
} }
} }
func assertKeyboard(t *testing.T, router *Router, expected TextInputState) { func assertKeyboard(t *testing.T, router *Router, expected TextInputState) {
t.Helper() t.Helper()
if got := router.key.queue.state; got != expected { if got := router.lastState().state; got != expected {
t.Errorf("expected %v keyboard, got %v", expected, got) t.Errorf("expected %v keyboard, got %v", expected, got)
} }
} }
+82 -71
View File
@@ -17,12 +17,9 @@ import (
) )
type pointerQueue struct { type pointerQueue struct {
hitTree []hitNode hitTree []hitNode
areas []areaNode areas []areaNode
cursor pointer.Cursor handlers map[event.Tag]*pointerHandler
handlers map[event.Tag]*pointerHandler
pointers []pointerInfo
transfers []io.ReadCloser // pending data transfers
semantic struct { semantic struct {
idsAssigned bool idsAssigned bool
@@ -43,6 +40,12 @@ type hitNode struct {
pass bool pass bool
} }
// pointerState is the input state related to pointer events.
type pointerState struct {
cursor pointer.Cursor
pointers []pointerInfo
}
type pointerInfo struct { type pointerInfo struct {
id pointer.ID id pointer.ID
pressed bool pressed bool
@@ -264,8 +267,9 @@ func (c *pointerCollector) actionInputOp(act system.Action) {
area.action = act area.action = act
} }
func (q *pointerQueue) grab(evts []taggedEvent, req pointer.GrabCmd) []taggedEvent { func (q *pointerQueue) grab(state pointerState, req pointer.GrabCmd) (pointerState, []taggedEvent) {
for _, p := range q.pointers { var evts []taggedEvent
for _, p := range state.pointers {
if !p.pressed || p.id != req.ID { if !p.pressed || p.id != req.ID {
continue continue
} }
@@ -276,12 +280,12 @@ func (q *pointerQueue) grab(evts []taggedEvent, req pointer.GrabCmd) []taggedEve
tag: tag, tag: tag,
event: pointer.Event{Kind: pointer.Cancel}, event: pointer.Event{Kind: pointer.Cancel},
}) })
q.dropHandler(tag) state = dropHandler(state, tag)
} }
} }
break break
} }
return evts return state, evts
} }
func (c *pointerCollector) inputOp(tag event.Tag) { func (c *pointerCollector) inputOp(tag event.Tag) {
@@ -357,29 +361,25 @@ func (q *pointerQueue) targetFilter(tag event.Tag, f transfer.TargetFilter) {
h.targetMimes = append(h.targetMimes, f.Type) h.targetMimes = append(h.targetMimes, f.Type)
} }
func (q *pointerQueue) offerData(evts []taggedEvent, req transfer.OfferCmd) []taggedEvent { func (q *pointerQueue) offerData(state pointerState, req transfer.OfferCmd) (pointerState, []taggedEvent) {
transferIdx := len(q.transfers) var evts []taggedEvent
q.transfers = append(q.transfers, req.Data) for i, p := range state.pointers {
for i := range q.pointers {
p := q.pointers[i]
if p.dataSource != req.Tag { if p.dataSource != req.Tag {
continue continue
} }
if p.dataTarget == nil { if p.dataTarget != nil {
q.pointers[i], evts = q.deliverTransferCancelEvent(p, evts) evts = append(evts, taggedEvent{tag: p.dataTarget, event: transfer.DataEvent{
break Type: req.Type,
Open: func() io.ReadCloser {
return req.Data
},
}})
} }
evts = append(evts, taggedEvent{tag: p.dataTarget, event: transfer.DataEvent{ state.pointers = append([]pointerInfo{}, state.pointers...)
Type: req.Type, state.pointers[i], evts = q.deliverTransferCancelEvent(p, evts)
Open: func() io.ReadCloser {
q.transfers[transferIdx] = nil
return req.Data
},
}})
q.pointers[i], evts = q.deliverTransferCancelEvent(p, evts)
break break
} }
return evts return state, evts
} }
func (c *pointerCollector) reset() { func (c *pointerCollector) reset() {
@@ -595,18 +595,12 @@ func (q *pointerQueue) reset() {
delete(q.semantic.contentIDs, k) delete(q.semantic.contentIDs, k)
} }
} }
for _, rc := range q.transfers {
if rc != nil {
rc.Close()
}
}
q.transfers = nil
} }
func (q *pointerQueue) Frame(evts []taggedEvent) []taggedEvent { func (q *pointerQueue) Frame(state pointerState) (pointerState, []taggedEvent) {
for k, h := range q.handlers { for k, h := range q.handlers {
if !h.active { if !h.active {
q.dropHandler(k) state = dropHandler(state, k)
delete(q.handlers, k) delete(q.handlers, k)
continue continue
} }
@@ -622,38 +616,51 @@ func (q *pointerQueue) Frame(evts []taggedEvent) []taggedEvent {
area.semantic.valid = area.semantic.content.gestures != 0 area.semantic.valid = area.semantic.content.gestures != 0
} }
} }
for i := range q.pointers { var evts []taggedEvent
p := q.pointers[i] for i, p := range state.pointers {
q.pointers[i], evts = q.deliverEnterLeaveEvents(p, evts, p.last) changed := false
p, evts, state.cursor, changed = q.deliverEnterLeaveEvents(state.cursor, p, evts, p.last)
if changed {
state.pointers = append([]pointerInfo{}, state.pointers...)
state.pointers[i] = p
}
} }
return evts return state, evts
} }
func (q *pointerQueue) dropHandler(tag event.Tag) { func dropHandler(state pointerState, tag event.Tag) pointerState {
for i := range q.pointers { pointers := state.pointers
p := &q.pointers[i] state.pointers = nil
for i := len(p.handlers) - 1; i >= 0; i-- { for _, p := range pointers {
if p.handlers[i] == tag { handlers := p.handlers
p.handlers = append(p.handlers[:i], p.handlers[i+1:]...) p.handlers = nil
for _, h := range handlers {
if h != tag {
p.handlers = append(p.handlers, h)
} }
} }
for i := len(p.entered) - 1; i >= 0; i-- { entered := p.entered
if p.entered[i] == tag { p.entered = nil
p.entered = append(p.entered[:i], p.entered[i+1:]...) for _, h := range entered {
if h != tag {
p.entered = append(p.entered, h)
} }
} }
state.pointers = append(state.pointers, p)
} }
return state
} }
// pointerOf returns the pointerInfo index corresponding to the pointer in e. // pointerOf returns the pointerInfo index corresponding to the pointer in e.
func (q *pointerQueue) pointerOf(e pointer.Event) int { func (s pointerState) pointerOf(e pointer.Event) (pointerState, int) {
for i, p := range q.pointers { for i, p := range s.pointers {
if p.id == e.PointerID { if p.id == e.PointerID {
return i return s, i
} }
} }
q.pointers = append(q.pointers, pointerInfo{id: e.PointerID}) n := len(s.pointers)
return len(q.pointers) - 1 s.pointers = append(s.pointers[:n:n], pointerInfo{id: e.PointerID})
return s, len(s.pointers) - 1
} }
// Deliver is like Push, but delivers an event to a particular area. // Deliver is like Push, but delivers an event to a particular area.
@@ -710,7 +717,8 @@ func (q *pointerQueue) SemanticArea(areaIdx int) (semanticContent, int) {
return semanticContent{}, -1 return semanticContent{}, -1
} }
func (q *pointerQueue) Push(evts []taggedEvent, e pointer.Event) []taggedEvent { func (q *pointerQueue) Push(state pointerState, e pointer.Event) (pointerState, []taggedEvent) {
var evts []taggedEvent
if e.Kind == pointer.Cancel { if e.Kind == pointer.Cancel {
for k := range q.handlers { for k := range q.handlers {
evts = append(evts, taggedEvent{ evts = append(evts, taggedEvent{
@@ -718,25 +726,22 @@ func (q *pointerQueue) Push(evts []taggedEvent, e pointer.Event) []taggedEvent {
tag: k, tag: k,
}) })
} }
q.pointers = q.pointers[:0] state.pointers = nil
for k := range q.handlers { return state, evts
q.dropHandler(k)
}
return evts
} }
pidx := q.pointerOf(e) state, pidx := state.pointerOf(e)
p := q.pointers[pidx] p := state.pointers[pidx]
switch e.Kind { switch e.Kind {
case pointer.Press: case pointer.Press:
p, evts = q.deliverEnterLeaveEvents(p, evts, e) p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(state.cursor, p, evts, e)
p.pressed = true p.pressed = true
evts = q.deliverEvent(p, evts, e) evts = q.deliverEvent(p, evts, e)
case pointer.Move: case pointer.Move:
if p.pressed { if p.pressed {
e.Kind = pointer.Drag e.Kind = pointer.Drag
} }
p, evts = q.deliverEnterLeaveEvents(p, evts, e) p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(state.cursor, p, evts, e)
evts = q.deliverEvent(p, evts, e) evts = q.deliverEvent(p, evts, e)
if p.pressed { if p.pressed {
p, evts = q.deliverDragEvent(p, evts) p, evts = q.deliverDragEvent(p, evts)
@@ -744,23 +749,25 @@ func (q *pointerQueue) Push(evts []taggedEvent, e pointer.Event) []taggedEvent {
case pointer.Release: case pointer.Release:
evts = q.deliverEvent(p, evts, e) evts = q.deliverEvent(p, evts, e)
p.pressed = false p.pressed = false
p, evts = q.deliverEnterLeaveEvents(p, evts, e) p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(state.cursor, p, evts, e)
p, evts = q.deliverDropEvent(p, evts) p, evts = q.deliverDropEvent(p, evts)
case pointer.Scroll: case pointer.Scroll:
p, evts = q.deliverEnterLeaveEvents(p, evts, e) p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(state.cursor, p, evts, e)
evts = q.deliverEvent(p, evts, e) evts = q.deliverEvent(p, evts, e)
default: default:
panic("unsupported pointer event type") panic("unsupported pointer event type")
} }
q.pointers[pidx] = p
p.last = e p.last = e
if !p.pressed && len(p.entered) == 0 { if !p.pressed && len(p.entered) == 0 {
// No longer need to track pointer. // No longer need to track pointer.
q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...) state.pointers = append(state.pointers[:pidx:pidx], state.pointers[pidx+1:]...)
} else {
state.pointers = append([]pointerInfo{}, state.pointers...)
state.pointers[pidx] = p
} }
return evts return state, evts
} }
func (q *pointerQueue) deliverEvent(p pointerInfo, evts []taggedEvent, e pointer.Event) []taggedEvent { func (q *pointerQueue) deliverEvent(p pointerInfo, evts []taggedEvent, e pointer.Event) []taggedEvent {
@@ -794,12 +801,13 @@ func (q *pointerQueue) deliverEvent(p pointerInfo, evts []taggedEvent, e pointer
return evts return evts
} }
func (q *pointerQueue) deliverEnterLeaveEvents(p pointerInfo, evts []taggedEvent, e pointer.Event) (pointerInfo, []taggedEvent) { func (q *pointerQueue) deliverEnterLeaveEvents(cursor pointer.Cursor, p pointerInfo, evts []taggedEvent, e pointer.Event) (pointerInfo, []taggedEvent, pointer.Cursor, bool) {
changed := false
var hits []event.Tag var hits []event.Tag
if e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press { if e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press {
// Consider non-mouse pointers leaving when they're released. // Consider non-mouse pointers leaving when they're released.
} else { } else {
hits, q.cursor = q.opHit(e.Position) hits, cursor = q.opHit(e.Position)
if p.pressed { if p.pressed {
// Filter out non-participating handlers, // Filter out non-participating handlers,
// except potential transfer targets when a transfer has been initiated. // except potential transfer targets when a transfer has been initiated.
@@ -819,6 +827,7 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p pointerInfo, evts []taggedEvent
} }
} }
} else { } else {
changed = true
p.handlers = hits p.handlers = hits
} }
} }
@@ -827,6 +836,7 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p pointerInfo, evts []taggedEvent
if _, found := searchTag(hits, k); found { if _, found := searchTag(hits, k); found {
continue continue
} }
changed = true
h := q.handlers[k] h := q.handlers[k]
e := e e := e
e.Kind = pointer.Leave e.Kind = pointer.Leave
@@ -842,6 +852,7 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p pointerInfo, evts []taggedEvent
if _, found := searchTag(p.entered, k); found { if _, found := searchTag(p.entered, k); found {
continue continue
} }
changed = true
e := e e := e
e.Kind = pointer.Enter e.Kind = pointer.Enter
@@ -851,7 +862,7 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p pointerInfo, evts []taggedEvent
} }
} }
p.entered = hits p.entered = hits
return p, evts return p, evts, cursor, changed
} }
func (q *pointerQueue) deliverDragEvent(p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) { func (q *pointerQueue) deliverDragEvent(p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) {
+1
View File
@@ -1037,6 +1037,7 @@ func TestPassCursor(t *testing.T) {
Position: f32.Pt(10, 10), Position: f32.Pt(10, 10),
Kind: pointer.Move, Kind: pointer.Move,
}) })
r.Frame(&ops)
if got := r.Cursor(); want != got { if got := r.Cursor(); want != got {
t.Errorf("got cursor %v, want %v", got, want) t.Errorf("got cursor %v, want %v", got, want)
} }
+211 -74
View File
@@ -5,6 +5,7 @@ package input
import ( import (
"encoding/binary" "encoding/binary"
"image" "image"
"io"
"strings" "strings"
"time" "time"
@@ -35,7 +36,10 @@ type Router struct {
} }
cqueue clipboardQueue cqueue clipboardQueue
events []taggedEvent // states is the list of pending state changes resulting from
// incoming events. The first element is the current state,
// if any.
changes []stateChange
reader ops.Reader reader ops.Reader
@@ -45,6 +49,9 @@ type Router struct {
// Changes queued for next call to Frame. // Changes queued for next call to Frame.
commands []Command commands []Command
// transfers is the pending transfer.DataEvent.Open functions.
transfers []io.ReadCloser
} }
// Source implements the interface between a Router and user interface widgets. // Source implements the interface between a Router and user interface widgets.
@@ -94,6 +101,21 @@ const (
// By convention, the zero value denotes the non-existent ID. // By convention, the zero value denotes the non-existent ID.
type SemanticID uint type SemanticID uint
// stateChange represents the new state and outgoing events
// resulting from an incoming event.
type stateChange struct {
state inputState
events []taggedEvent
}
// inputState represent a immutable snapshot of the state required
// to route events.
type inputState struct {
clipboardState
keyState
pointerState
}
// taggedEvent represents an event and its target handler. // taggedEvent represents an event and its target handler.
type taggedEvent struct { type taggedEvent struct {
event event.Event event event.Event
@@ -130,7 +152,8 @@ func (s Source) Events(k event.Tag, filters ...event.Filter) []event.Event {
} }
func (q *Router) Events(k event.Tag, filters ...event.Filter) []event.Event { func (q *Router) Events(k event.Tag, filters ...event.Filter) []event.Event {
var evts []event.Event var events []event.Event
// Record handler filters and add reset events.
for _, f := range filters { for _, f := range filters {
switch f := f.(type) { switch f := f.(type) {
case key.Filter: case key.Filter:
@@ -138,12 +161,12 @@ func (q *Router) Events(k event.Tag, filters ...event.Filter) []event.Event {
case key.FocusFilter: case key.FocusFilter:
q.key.queue.focusable(k) q.key.queue.focusable(k)
if reset, ok := q.key.queue.ResetEvent(k); ok { if reset, ok := q.key.queue.ResetEvent(k); ok {
evts = append(evts, reset) events = append(events, reset)
} }
case pointer.Filter: case pointer.Filter:
q.pointer.queue.filterTag(k, f) q.pointer.queue.filterTag(k, f)
if reset, ok := q.pointer.queue.ResetEvent(k); ok { if reset, ok := q.pointer.queue.ResetEvent(k); ok {
evts = append(evts, reset) events = append(events, reset)
} }
case transfer.SourceFilter: case transfer.SourceFilter:
q.pointer.queue.sourceFilter(k, f) q.pointer.queue.sourceFilter(k, f)
@@ -151,39 +174,71 @@ func (q *Router) Events(k event.Tag, filters ...event.Filter) []event.Event {
q.pointer.queue.targetFilter(k, f) q.pointer.queue.targetFilter(k, f)
} }
} }
i := 0 // Accumulate events from state changes until there are no more
for i < len(q.events) { // matching events.
e := q.events[i] matchedIdx := 0
if e.tag == k { for i := range q.changes {
q.events = append(q.events[:i], q.events[i+1:]...) change := &q.changes[i]
if filtersMatches(filters, e.event) { j := 0
evts = append(evts, e.event) for j < len(change.events) {
evt := change.events[j]
if evt.tag != k || !filtersMatches(filters, evt.event) {
j++
continue
} }
} else { events = append(events, evt.event)
i++ change.events = append(change.events[:j], change.events[j+1:]...)
matchedIdx = i
} }
} }
return evts // Fast forward state to last matched.
q.collapseState(matchedIdx)
return events
}
// collapseState in the interval [1;idx] into q.changes[0].
func (q *Router) collapseState(idx int) {
if idx == 0 {
return
}
first := &q.changes[0]
first.state = q.changes[idx].state
for i := 1; i <= idx; i++ {
first.events = append(first.events, q.changes[i].events...)
}
q.changes = append(q.changes[:1], q.changes[idx+1:]...)
} }
// Frame replaces the declared handlers from the supplied // Frame replaces the declared handlers from the supplied
// operation list. The text input state, wakeup time and whether // operation list. The text input state, wakeup time and whether
// there are active profile handlers is also saved. // there are active profile handlers is also saved.
func (q *Router) Frame(frame *op.Ops) { func (q *Router) Frame(frame *op.Ops) {
q.events = q.events[:0] for _, rc := range q.transfers {
if rc != nil {
rc.Close()
}
}
q.transfers = nil
q.wakeup = false q.wakeup = false
// Collapse state and clear events.
if n := len(q.changes); n > 1 {
state := q.changes[n-1].state
q.changes = append(q.changes[:0], stateChange{state: state})
}
var ops *ops.Ops var ops *ops.Ops
if frame != nil { if frame != nil {
ops = &frame.Internal ops = &frame.Internal
} }
q.reader.Reset(ops) q.reader.Reset(ops)
q.collect() q.collect()
evts := q.executeCommands(nil) q.executeCommands()
evts = q.pointer.queue.Frame(evts) q.changePointerState(q.pointer.queue.Frame(q.lastState().pointerState))
q.key.queue.Frame() kstate := q.key.queue.Frame(q.lastState().keyState)
q.addEvents(evts) q.changeKeyState(kstate, nil)
// Collapse state and events.
q.collapseState(len(q.changes) - 1)
if len(evts) > 0 { if len(q.changes) > 0 && len(q.changes[0].events) > 0 {
q.wakeup = true q.wakeup = true
q.wakeupTime = time.Time{} q.wakeupTime = time.Time{}
} }
@@ -191,69 +246,147 @@ func (q *Router) Frame(frame *op.Ops) {
// Queue events and report whether at least one event matched a handler. // Queue events and report whether at least one event matched a handler.
func (q *Router) Queue(events ...event.Event) bool { func (q *Router) Queue(events ...event.Event) bool {
var evts []taggedEvent matched := false
for _, e := range events { for _, e := range events {
switch e := e.(type) { hadEvents := q.processEvent(e)
case pointer.Event: matched = matched || hadEvents
evts = q.pointer.queue.Push(evts, e) }
case key.Event: return matched
evts = q.queueKeyEvent(evts, e) }
case key.SnippetEvent:
// Expand existing, overlapping snippet. func (q *Router) processEvent(e event.Event) bool {
if r := q.key.queue.content.Snippet.Range; rangeOverlaps(r, key.Range(e)) { state := q.lastState()
if e.Start > r.Start { switch e := e.(type) {
e.Start = r.Start case pointer.Event:
} return q.changePointerState(q.pointer.queue.Push(state.pointerState, e))
if e.End < r.End { case key.Event:
e.End = r.End return q.addEvents(q.queueKeyEvent(state.keyState, e))
} case key.SnippetEvent:
} // Expand existing, overlapping snippet.
if f := q.key.queue.focus; f != nil { if r := state.content.Snippet.Range; rangeOverlaps(r, key.Range(e)) {
evts = append(evts, taggedEvent{tag: f, event: e}) if e.Start > r.Start {
} e.Start = r.Start
case key.EditEvent, key.FocusEvent, key.SelectionEvent: }
if f := q.key.queue.focus; f != nil { if e.End < r.End {
evts = append(evts, taggedEvent{tag: f, event: e}) e.End = r.End
} }
case transfer.DataEvent: }
evts = q.cqueue.Push(evts, e) var evts []taggedEvent
} if f := state.focus; f != nil {
evts = append(evts, taggedEvent{tag: f, event: e})
}
return q.addEvents(evts)
case key.EditEvent, key.FocusEvent, key.SelectionEvent:
var evts []taggedEvent
if f := state.focus; f != nil {
evts = append(evts, taggedEvent{tag: f, event: e})
}
return q.addEvents(evts)
case transfer.DataEvent:
return q.changeClipboardState(q.cqueue.Push(state.clipboardState, e))
default:
panic("unknown event type")
} }
q.addEvents(evts)
return len(evts) > 0
} }
func (q *Router) queue(f Command) { func (q *Router) queue(f Command) {
q.commands = append(q.commands, f) q.commands = append(q.commands, f)
} }
func (q *Router) executeCommands(evts []taggedEvent) []taggedEvent { func (q *Router) state() inputState {
if len(q.changes) > 0 {
return q.changes[0].state
}
return inputState{}
}
func (q *Router) lastState() inputState {
if n := len(q.changes); n > 0 {
return q.changes[n-1].state
}
return inputState{}
}
func (q *Router) changeClipboardState(cstate clipboardState, evts []taggedEvent) bool {
state := q.lastState()
state.clipboardState = cstate
return q.changeState(state, evts)
}
func (q *Router) changeKeyState(kstate keyState, evts []taggedEvent) bool {
state := q.lastState()
state.keyState = kstate
return q.changeState(state, evts)
}
func (q *Router) changePointerState(pstate pointerState, evts []taggedEvent) bool {
state := q.lastState()
state.pointerState = pstate
return q.changeState(state, evts)
}
func (q *Router) executeCommands() {
for _, req := range q.commands { for _, req := range q.commands {
state := q.lastState()
switch req := req.(type) { switch req := req.(type) {
case key.SelectionCmd: case key.SelectionCmd:
q.key.queue.setSelection(req) kstate := q.key.queue.setSelection(state.keyState, req)
q.changeKeyState(kstate, nil)
case key.FocusCmd: case key.FocusCmd:
evts = q.key.queue.Focus(evts, req.Tag) q.changeKeyState(q.key.queue.Focus(state.keyState, req.Tag))
case key.SoftKeyboardCmd: case key.SoftKeyboardCmd:
q.key.queue.softKeyboard(req.Show) kstate := state.keyState.softKeyboard(req.Show)
q.changeKeyState(kstate, nil)
case key.SnippetCmd: case key.SnippetCmd:
q.key.queue.setSnippet(req) kstate := q.key.queue.setSnippet(state.keyState, req)
q.changeKeyState(kstate, nil)
case transfer.OfferCmd: case transfer.OfferCmd:
evts = q.pointer.queue.offerData(evts, req) q.changePointerState(q.pointer.queue.offerData(state.pointerState, req))
case clipboard.WriteCmd: case clipboard.WriteCmd:
q.cqueue.ProcessWriteClipboard(req) q.cqueue.ProcessWriteClipboard(req)
case clipboard.ReadCmd: case clipboard.ReadCmd:
q.cqueue.ProcessReadClipboard(req.Tag) cstate := q.cqueue.ProcessReadClipboard(state.clipboardState, req.Tag)
q.changeClipboardState(cstate, nil)
case pointer.GrabCmd: case pointer.GrabCmd:
evts = q.pointer.queue.grab(evts, req) q.changePointerState(q.pointer.queue.grab(state.pointerState, req))
} }
} }
q.commands = nil q.commands = nil
return evts
} }
func (q *Router) addEvents(evts []taggedEvent) { func (q *Router) addEvents(evts []taggedEvent) bool {
q.events = append(q.events, evts...) return q.changeState(q.lastState(), evts)
}
func (q *Router) changeState(state inputState, evts []taggedEvent) bool {
// Wrap pointer.DataEvent.Open functions to detect them not being called.
for i := range evts {
e := &evts[i]
if de, ok := e.event.(transfer.DataEvent); ok {
transferIdx := len(q.transfers)
data := de.Open()
q.transfers = append(q.transfers, data)
de.Open = func() io.ReadCloser {
q.transfers[transferIdx] = nil
return data
}
e.event = de
}
}
n := len(q.changes)
// We must add a new state change if
//
// - there is no first state change, or
// - the state change is not atomic from the perspective of the handlers.
if len(q.changes) == 0 || (len(evts) > 0 && len(q.changes[n-1].events) > 0) {
q.changes = append(q.changes, stateChange{state: state, events: evts})
} else {
// Otherwise, merge with previous change.
prev := &q.changes[n-1]
prev.state = state
prev.events = append(prev.events, evts...)
}
return len(evts) > 0
} }
func rangeOverlaps(r1, r2 key.Range) bool { func rangeOverlaps(r1, r2 key.Range) bool {
@@ -270,9 +403,10 @@ func rangeNorm(r key.Range) key.Range {
return r return r
} }
func (q *Router) queueKeyEvent(evts []taggedEvent, e key.Event) []taggedEvent { func (q *Router) queueKeyEvent(state keyState, e key.Event) []taggedEvent {
kq := &q.key.queue kq := &q.key.queue
f := q.key.queue.focus f := state.focus
var evts []taggedEvent
if f != nil && kq.Accepts(f, e) { if f != nil && kq.Accepts(f, e) {
evts = append(evts, taggedEvent{tag: f, event: e}) evts = append(evts, taggedEvent{tag: f, event: e})
return evts return evts
@@ -305,15 +439,15 @@ func (q *Router) queueKeyEvent(evts []taggedEvent, e key.Event) []taggedEvent {
} }
func (q *Router) MoveFocus(dir key.FocusDirection) bool { func (q *Router) MoveFocus(dir key.FocusDirection) bool {
evts := q.key.queue.MoveFocus(nil, dir) ks, evts := q.key.queue.MoveFocus(q.lastState().keyState, dir)
q.addEvents(evts) return q.changeKeyState(ks, evts)
return len(evts) > 0
} }
// RevealFocus scrolls the current focus (if any) into viewport // RevealFocus scrolls the current focus (if any) into viewport
// if there are scrollable parent handlers. // if there are scrollable parent handlers.
func (q *Router) RevealFocus(viewport image.Rectangle) { func (q *Router) RevealFocus(viewport image.Rectangle) {
focus := q.key.queue.focus state := q.lastState()
focus := state.focus
if focus == nil { if focus == nil {
return return
} }
@@ -339,7 +473,8 @@ func (q *Router) RevealFocus(viewport image.Rectangle) {
// ScrollFocus scrolls the focused widget, if any, by dist. // ScrollFocus scrolls the focused widget, if any, by dist.
func (q *Router) ScrollFocus(dist image.Point) { func (q *Router) ScrollFocus(dist image.Point) {
focus := q.key.queue.focus state := q.lastState()
focus := state.focus
if focus == nil { if focus == nil {
return return
} }
@@ -378,7 +513,7 @@ func (q *Router) ActionAt(p f32.Point) (system.Action, bool) {
} }
func (q *Router) ClickFocus() { func (q *Router) ClickFocus() {
focus := q.key.queue.focus focus := q.lastState().focus
if focus == nil { if focus == nil {
return return
} }
@@ -398,12 +533,14 @@ func (q *Router) ClickFocus() {
// TextInputState returns the input state from the most recent // TextInputState returns the input state from the most recent
// call to Frame. // call to Frame.
func (q *Router) TextInputState() TextInputState { func (q *Router) TextInputState() TextInputState {
return q.key.queue.InputState() kstate, s := q.state().InputState()
q.changeKeyState(kstate, nil)
return s
} }
// TextInputHint returns the input mode from the most recent key.InputOp. // TextInputHint returns the input mode from the most recent key.InputOp.
func (q *Router) TextInputHint() (key.InputHint, bool) { func (q *Router) TextInputHint() (key.InputHint, bool) {
return q.key.queue.InputHint() return q.key.queue.InputHint(q.state().keyState)
} }
// WriteClipboard returns the most recent content to be copied // WriteClipboard returns the most recent content to be copied
@@ -412,15 +549,15 @@ func (q *Router) WriteClipboard() (mime string, content []byte, ok bool) {
return q.cqueue.WriteClipboard() return q.cqueue.WriteClipboard()
} }
// ReadClipboard reports if any new handler is waiting // ClipboardRequested reports if any new handler is waiting
// to read the clipboard. // to read the clipboard.
func (q *Router) ReadClipboard() bool { func (q *Router) ClipboardRequested() bool {
return q.cqueue.ReadClipboard() return q.cqueue.ClipboardRequested(q.lastState().clipboardState)
} }
// Cursor returns the last cursor set. // Cursor returns the last cursor set.
func (q *Router) Cursor() pointer.Cursor { func (q *Router) Cursor() pointer.Cursor {
return q.pointer.queue.cursor return q.state().cursor
} }
// SemanticAt returns the first semantic description under pos, if any. // SemanticAt returns the first semantic description under pos, if any.
@@ -439,7 +576,7 @@ func (q *Router) AppendSemantics(nodes []SemanticNode) []SemanticNode {
// EditorState returns the editor state for the focused handler, or the // EditorState returns the editor state for the focused handler, or the
// zero value if there is none. // zero value if there is none.
func (q *Router) EditorState() EditorState { func (q *Router) EditorState() EditorState {
return q.key.queue.editorState() return q.key.queue.editorState(q.state().keyState)
} }
func (q *Router) collect() { func (q *Router) collect() {