Files
gio/ui/pointer/queue.go
T
Elias Naur 40856a244e ui/pointer: introduce OpArea for pointer hit testing
Split out OpArea from OpHandler to allow stacked areas in a followup.

Replace hit closures with static shapes (rectangles and ellipses) to
avoid allocations. If needed, generic hit functions can be introduced
again later.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2019-06-03 14:54:26 +02:00

242 lines
4.7 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package pointer
import (
"gioui.org/ui"
"gioui.org/ui/f32"
"gioui.org/ui/internal/ops"
)
type Queue struct {
hitTree []hitNode
handlers map[Key]*handler
pointers []pointerInfo
reader ui.OpsReader
scratch []Key
}
type hitNode struct {
// The layer depth.
level int
// The handler, or nil for a layer.
key Key
}
type pointerInfo struct {
id ID
pressed bool
handlers []Key
}
type handler struct {
area OpArea
active bool
transform ui.Transform
events []Event
wantsGrab bool
}
func (q *Queue) collectHandlers(r *ui.OpsReader, t ui.Transform, layer int, area OpArea) {
for {
encOp, ok := r.Decode()
if !ok {
return
}
switch ops.OpType(encOp.Data[0]) {
case ops.TypePush:
q.collectHandlers(r, t, layer, area)
case ops.TypePop:
return
case ops.TypeLayer:
layer++
q.hitTree = append(q.hitTree, hitNode{level: layer})
case ops.TypeArea:
var op OpArea
op.decode(encOp.Data)
area = op
case ops.TypeTransform:
var op ui.OpTransform
op.Decode(encOp.Data)
t = t.Mul(op.Transform)
case ops.TypePointerHandler:
var op OpHandler
op.Decode(encOp.Data, encOp.Refs)
q.hitTree = append(q.hitTree, hitNode{level: layer, key: op.Key})
h, ok := q.handlers[op.Key]
if !ok {
h = new(handler)
q.handlers[op.Key] = h
}
h.area = area
h.transform = t
h.wantsGrab = h.wantsGrab || op.Grab
}
}
}
func (q *Queue) opHit(handlers *[]Key, pos f32.Point) {
level := 1 << 30
opaque := false
for i := len(q.hitTree) - 1; i >= 0; i-- {
n := q.hitTree[i]
if n.key == nil {
// Layer
if opaque {
opaque = false
// Skip sibling handlers.
level = n.level - 1
}
} else if n.level <= level {
// Handler
h, ok := q.handlers[n.key]
if !ok {
continue
}
tpos := h.transform.InvTransform(pos)
res := h.area.Hit(tpos)
opaque = opaque || res == HitOpaque
if res != HitNone {
*handlers = append(*handlers, n.key)
}
}
}
}
func (q *Queue) init() {
if q.handlers == nil {
q.handlers = make(map[Key]*handler)
}
}
func (q *Queue) Frame(root *ui.Ops) {
q.init()
for k, h := range q.handlers {
if !h.active {
q.dropHandler(k)
} else {
// Reset handler.
h.events = h.events[:0]
}
}
q.hitTree = q.hitTree[:0]
q.reader.Reset(root)
q.collectHandlers(&q.reader, ui.Transform{}, 0, OpArea{})
}
func (q *Queue) For(k Key) []Event {
if k == nil {
panic("nil handler")
}
q.init()
h, ok := q.handlers[k]
if !ok {
h = new(handler)
q.handlers[k] = h
}
if !h.active {
h.active = true
// Prepend a Cancel.
h.events = append(h.events, Event{})
copy(h.events[1:], h.events)
h.events[0] = Event{Type: Cancel}
}
return h.events
}
func (q *Queue) dropHandler(k Key) {
delete(q.handlers, k)
for i := range q.pointers {
p := &q.pointers[i]
for i := len(p.handlers) - 1; i >= 0; i-- {
if p.handlers[i] == k {
p.handlers = append(p.handlers[:i], p.handlers[i+1:]...)
}
}
}
}
func (q *Queue) Push(e Event) {
q.init()
if e.Type == Cancel {
q.pointers = q.pointers[:0]
for k := range q.handlers {
q.dropHandler(k)
}
return
}
pidx := -1
for i, p := range q.pointers {
if p.id == e.PointerID {
pidx = i
break
}
}
if pidx == -1 {
q.pointers = append(q.pointers, pointerInfo{id: e.PointerID})
pidx = len(q.pointers) - 1
}
p := &q.pointers[pidx]
if !p.pressed && (e.Type == Move || e.Type == Press) {
p.handlers, q.scratch = q.scratch[:0], p.handlers
q.opHit(&p.handlers, e.Position)
// Drop handlers no longer hit.
loop:
for _, h := range q.scratch {
for _, h2 := range p.handlers {
if h == h2 {
continue loop
}
}
q.dropHandler(h)
}
if e.Type == Press {
p.pressed = true
}
}
if p.pressed {
// Resolve grabs.
q.scratch = q.scratch[:0]
for i, k := range p.handlers {
h := q.handlers[k]
if h.wantsGrab {
q.scratch = append(q.scratch, p.handlers[:i]...)
q.scratch = append(q.scratch, p.handlers[i+1:]...)
break
}
}
// Drop handlers that lost their grab.
for _, k := range q.scratch {
q.dropHandler(k)
}
}
if e.Type == Release {
q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...)
}
for i, k := range p.handlers {
h := q.handlers[k]
e := e
switch {
case p.pressed && len(p.handlers) == 1:
e.Priority = Grabbed
case i == 0:
e.Priority = Foremost
}
e.Position = h.transform.InvTransform(e.Position)
e.Hit = h.area.Hit(e.Position) != HitNone
h.events = append(h.events, e)
if e.Type == Release {
// Release grab when the number of grabs reaches zero.
grabs := 0
for _, p := range q.pointers {
if p.pressed && len(p.handlers) == 1 && p.handlers[0] == k {
grabs++
}
}
if grabs == 0 {
h.wantsGrab = false
}
}
}
}