Files
gio/io/router/pointer.go
T
Elias Naur 60846d112b io/router: introduce pointerCollector to avoid an ops decode
The router package decodes the entire ops list thrice: once for pointer
ops, once for key ops, once for other ops. This change removes one
decode round by merging other ops and pointer ops decoding.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2021-10-21 18:16:12 +02:00

529 lines
12 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package router
import (
"encoding/binary"
"image"
"gioui.org/f32"
"gioui.org/internal/ops"
"gioui.org/io/event"
"gioui.org/io/pointer"
)
type pointerQueue struct {
hitTree []hitNode
areas []areaNode
cursors []cursorNode
cursor pointer.CursorName
handlers map[event.Tag]*pointerHandler
pointers []pointerInfo
scratch []event.Tag
}
type hitNode struct {
next int
area int
// For handler nodes.
tag event.Tag
}
type cursorNode struct {
name pointer.CursorName
area int
}
type pointerInfo struct {
id pointer.ID
pressed bool
handlers []event.Tag
// last tracks the last pointer event received,
// used while processing frame events.
last pointer.Event
// entered tracks the tags that contain the pointer.
entered []event.Tag
}
type pointerHandler struct {
area int
active bool
wantsGrab bool
types pointer.Type
// min and max horizontal/vertical scroll
scrollRange image.Rectangle
}
type areaOp struct {
kind areaKind
rect f32.Rectangle
}
type areaNode struct {
trans f32.Affine2D
next int
area areaOp
pass bool
}
type areaKind uint8
// collectState represents the state for pointerCollector.
type collectState struct {
t f32.Affine2D
// nodePlusOne is the current node index, plus one to
// make the zero value collectState the initial state.
nodePlusOne int
pass int
}
// pointerCollector tracks the state needed to update an pointerQueue
// from pointer ops.
type pointerCollector struct {
q *pointerQueue
state collectState
nodeStack []int
transStack []f32.Affine2D
// states holds the storage for save/restore ops.
states []f32.Affine2D
}
const (
areaRect areaKind = iota
areaEllipse
)
func (c *pointerCollector) save(id int) {
if extra := id - len(c.states) + 1; extra > 0 {
c.states = append(c.states, make([]f32.Affine2D, extra)...)
}
c.states[id] = c.state.t
}
func (c *pointerCollector) load(id int) {
c.state = collectState{t: c.states[id]}
}
func (c *pointerCollector) area(op areaOp) {
area := -1
if i := c.state.nodePlusOne - 1; i != -1 {
n := c.q.hitTree[i]
area = n.area
}
c.q.areas = append(c.q.areas, areaNode{trans: c.state.t, next: area, area: op, pass: c.state.pass > 0})
c.nodeStack = append(c.nodeStack, c.state.nodePlusOne-1)
c.q.hitTree = append(c.q.hitTree, hitNode{
next: c.state.nodePlusOne - 1,
area: len(c.q.areas) - 1,
})
c.state.nodePlusOne = len(c.q.hitTree) - 1 + 1
}
func (c *pointerCollector) popArea() {
n := len(c.nodeStack)
c.state.nodePlusOne = c.nodeStack[n-1] + 1
c.nodeStack = c.nodeStack[:n-1]
}
func (c *pointerCollector) pass() {
c.state.pass++
}
func (c *pointerCollector) popPass() {
c.state.pass--
}
func (c *pointerCollector) transform(t f32.Affine2D, push bool) {
if push {
c.transStack = append(c.transStack, c.state.t)
}
c.state.t = c.state.t.Mul(t)
}
func (c *pointerCollector) popTransform() {
n := len(c.transStack)
c.state.t = c.transStack[n-1]
c.transStack = c.transStack[:n-1]
}
func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) {
area := -1
if i := c.state.nodePlusOne - 1; i != -1 {
n := c.q.hitTree[i]
area = n.area
}
c.q.hitTree = append(c.q.hitTree, hitNode{
next: c.state.nodePlusOne - 1,
area: area,
tag: op.Tag,
})
c.state.nodePlusOne = len(c.q.hitTree) - 1 + 1
h, ok := c.q.handlers[op.Tag]
if !ok {
h = new(pointerHandler)
c.q.handlers[op.Tag] = h
// Cancel handlers on (each) first appearance, but don't
// trigger redraw.
events.AddNoRedraw(op.Tag, pointer.Event{Type: pointer.Cancel})
}
h.active = true
h.area = area
h.wantsGrab = h.wantsGrab || op.Grab
h.types = h.types | op.Types
h.scrollRange = op.ScrollBounds
}
func (c *pointerCollector) cursor(name pointer.CursorName) {
c.q.cursors = append(c.q.cursors, cursorNode{
name: name,
area: len(c.q.areas) - 1,
})
}
func (c *pointerCollector) reset(q *pointerQueue) {
q.reset()
c.state = collectState{}
c.nodeStack = c.nodeStack[:0]
c.transStack = c.transStack[:0]
c.q = q
}
func (q *pointerQueue) opHit(handlers *[]event.Tag, pos f32.Point) {
// Track whether we're passing through hits.
pass := true
idx := len(q.hitTree) - 1
for idx >= 0 {
n := &q.hitTree[idx]
hit, areaPass := q.hit(n.area, pos)
if !hit {
idx--
continue
}
pass = pass && areaPass
if pass {
idx--
} else {
idx = n.next
}
if n.tag != nil {
if _, exists := q.handlers[n.tag]; exists {
*handlers = addHandler(*handlers, n.tag)
}
}
}
}
func (q *pointerQueue) invTransform(areaIdx int, p f32.Point) f32.Point {
if areaIdx == -1 {
return p
}
return q.areas[areaIdx].trans.Invert().Transform(p)
}
func (q *pointerQueue) hit(areaIdx int, p f32.Point) (bool, bool) {
pass := false
for areaIdx != -1 {
a := &q.areas[areaIdx]
p := a.trans.Invert().Transform(p)
if !a.area.Hit(p) {
return false, false
}
areaIdx = a.next
pass = pass || a.pass
}
return true, pass
}
func (q *pointerQueue) reset() {
if q.handlers == nil {
q.handlers = make(map[event.Tag]*pointerHandler)
}
for _, h := range q.handlers {
// Reset handler.
h.active = false
h.wantsGrab = false
h.types = 0
}
q.hitTree = q.hitTree[:0]
q.areas = q.areas[:0]
q.cursors = q.cursors[:0]
}
func (q *pointerQueue) Frame(events *handlerEvents) {
for k, h := range q.handlers {
if !h.active {
q.dropHandler(nil, k)
delete(q.handlers, k)
}
if h.wantsGrab {
for _, p := range q.pointers {
if !p.pressed {
continue
}
for i, k2 := range p.handlers {
if k2 == k {
// Drop other handlers that lost their grab.
dropped := q.scratch[:0]
dropped = append(dropped, p.handlers[:i]...)
dropped = append(dropped, p.handlers[i+1:]...)
for _, tag := range dropped {
q.dropHandler(events, tag)
}
break
}
}
}
}
}
for i := range q.pointers {
p := &q.pointers[i]
q.deliverEnterLeaveEvents(p, events, p.last)
}
}
func (q *pointerQueue) dropHandler(events *handlerEvents, tag event.Tag) {
if events != nil {
events.Add(tag, pointer.Event{Type: pointer.Cancel})
}
for i := range q.pointers {
p := &q.pointers[i]
for i := len(p.handlers) - 1; i >= 0; i-- {
if p.handlers[i] == tag {
p.handlers = append(p.handlers[:i], p.handlers[i+1:]...)
}
}
for i := len(p.entered) - 1; i >= 0; i-- {
if p.entered[i] == tag {
p.entered = append(p.entered[:i], p.entered[i+1:]...)
}
}
}
}
// pointerOf returns the pointerInfo index corresponding to the pointer in e.
func (q *pointerQueue) pointerOf(e pointer.Event) int {
for i, p := range q.pointers {
if p.id == e.PointerID {
return i
}
}
q.pointers = append(q.pointers, pointerInfo{id: e.PointerID})
return len(q.pointers) - 1
}
func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
if e.Type == pointer.Cancel {
q.pointers = q.pointers[:0]
for k := range q.handlers {
q.dropHandler(events, k)
}
return
}
pidx := q.pointerOf(e)
p := &q.pointers[pidx]
p.last = e
switch e.Type {
case pointer.Press:
q.deliverEnterLeaveEvents(p, events, e)
p.pressed = true
q.deliverEvent(p, events, e)
case pointer.Move:
if p.pressed {
e.Type = pointer.Drag
}
q.deliverEnterLeaveEvents(p, events, e)
q.deliverEvent(p, events, e)
case pointer.Release:
q.deliverEvent(p, events, e)
p.pressed = false
q.deliverEnterLeaveEvents(p, events, e)
case pointer.Scroll:
q.deliverEnterLeaveEvents(p, events, e)
q.deliverScrollEvent(p, events, e)
default:
panic("unsupported pointer event type")
}
if !p.pressed && len(p.entered) == 0 {
// No longer need to track pointer.
q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...)
}
}
func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, e pointer.Event) {
foremost := true
if p.pressed && len(p.handlers) == 1 {
e.Priority = pointer.Grabbed
foremost = false
}
for _, k := range p.handlers {
h := q.handlers[k]
if e.Type&h.types == 0 {
continue
}
e := e
if foremost {
foremost = false
e.Priority = pointer.Foremost
}
e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e)
}
}
func (q *pointerQueue) deliverScrollEvent(p *pointerInfo, events *handlerEvents, e pointer.Event) {
foremost := true
if p.pressed && len(p.handlers) == 1 {
e.Priority = pointer.Grabbed
foremost = false
}
var sx, sy = e.Scroll.X, e.Scroll.Y
for _, k := range p.handlers {
if sx == 0 && sy == 0 {
return
}
h := q.handlers[k]
// Distribute the scroll to the handler based on its ScrollRange.
sx, e.Scroll.X = setScrollEvent(sx, h.scrollRange.Min.X, h.scrollRange.Max.X)
sy, e.Scroll.Y = setScrollEvent(sy, h.scrollRange.Min.Y, h.scrollRange.Max.Y)
e := e
if foremost {
foremost = false
e.Priority = pointer.Foremost
}
e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e)
}
}
func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEvents, e pointer.Event) {
q.scratch = q.scratch[:0]
q.opHit(&q.scratch, e.Position)
if p.pressed {
// Filter out non-participating handlers.
for i := len(q.scratch) - 1; i >= 0; i-- {
if _, found := searchTag(p.handlers, q.scratch[i]); !found {
q.scratch = append(q.scratch[:i], q.scratch[i+1:]...)
}
}
} else {
p.handlers = append(p.handlers[:0], q.scratch...)
}
hits := q.scratch
if e.Source != pointer.Mouse && !p.pressed && e.Type != pointer.Press {
// Consider non-mouse pointers leaving when they're released.
hits = nil
}
// Deliver Leave events.
for _, k := range p.entered {
if _, found := searchTag(hits, k); found {
continue
}
h := q.handlers[k]
e.Type = pointer.Leave
if e.Type&h.types != 0 {
e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e)
}
}
// Deliver Enter events and update cursor.
q.cursor = pointer.CursorDefault
for _, k := range hits {
h := q.handlers[k]
for i := len(q.cursors) - 1; i >= 0; i-- {
if c := q.cursors[i]; c.area == h.area {
q.cursor = c.name
break
}
}
if _, found := searchTag(p.entered, k); found {
continue
}
e.Type = pointer.Enter
if e.Type&h.types != 0 {
e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e)
}
}
p.entered = append(p.entered[:0], hits...)
}
func searchTag(tags []event.Tag, tag event.Tag) (int, bool) {
for i, t := range tags {
if t == tag {
return i, true
}
}
return 0, false
}
// addHandler adds tag to the slice if not present.
func addHandler(tags []event.Tag, tag event.Tag) []event.Tag {
for _, t := range tags {
if t == tag {
return tags
}
}
return append(tags, tag)
}
func opDecodeFloat32(d []byte) float32 {
return float32(int32(binary.LittleEndian.Uint32(d)))
}
func (op *areaOp) Decode(d []byte) {
if ops.OpType(d[0]) != ops.TypeArea {
panic("invalid op")
}
rect := f32.Rectangle{
Min: f32.Point{
X: opDecodeFloat32(d[2:]),
Y: opDecodeFloat32(d[6:]),
},
Max: f32.Point{
X: opDecodeFloat32(d[10:]),
Y: opDecodeFloat32(d[14:]),
},
}
*op = areaOp{
kind: areaKind(d[1]),
rect: rect,
}
}
func (op *areaOp) Hit(pos f32.Point) bool {
pos = pos.Sub(op.rect.Min)
size := op.rect.Size()
switch op.kind {
case areaRect:
return 0 <= pos.X && pos.X < size.X &&
0 <= pos.Y && pos.Y < size.Y
case areaEllipse:
rx := size.X / 2
ry := size.Y / 2
xh := pos.X - rx
yk := pos.Y - ry
// The ellipse function works in all cases because
// 0/0 is not <= 1.
return (xh*xh)/(rx*rx)+(yk*yk)/(ry*ry) <= 1
default:
panic("invalid area kind")
}
}
func setScrollEvent(scroll float32, min, max int) (left, scrolled float32) {
if v := float32(max); scroll > v {
return scroll - v, v
}
if v := float32(min); scroll < v {
return scroll - v, v
}
return 0, scroll
}