Files
gio/ui/input/pointer.go
T
Elias Naur 4fadf71992 ui/pointer: split AreaOp into RectAreaOp and EllipseAreaOp
Now that the pass through mode is moved into its own PassOp op,
it doesn't make sense to collect all area types into one exported
type.

Add RectAreaOp and EllipseAreaOp for clients; the internal
representation is still a single op. It can be changed later without
breaking clients.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2019-07-10 22:55:16 +02:00

316 lines
6.1 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package input
import (
"encoding/binary"
"image"
"gioui.org/ui"
"gioui.org/ui/f32"
"gioui.org/ui/internal/ops"
"gioui.org/ui/pointer"
)
type pointerQueue struct {
hitTree []hitNode
areas []areaNode
handlers map[Key]*pointerHandler
pointers []pointerInfo
reader ui.OpsReader
scratch []Key
}
type hitNode struct {
next int
area int
// Pass tracks the most recent PassOp mode.
pass bool
// For handler nodes.
key Key
}
type pointerInfo struct {
id pointer.ID
pressed bool
handlers []Key
}
type pointerHandler struct {
area int
active bool
transform ui.Transform
wantsGrab bool
}
type areaOp struct {
kind areaKind
size image.Point
}
type areaNode struct {
trans ui.Transform
next int
area areaOp
}
type areaKind uint8
const (
areaRect areaKind = iota
areaEllipse
)
func (q *pointerQueue) collectHandlers(r *ui.OpsReader, events handlerEvents, t ui.Transform, area, node int, pass bool) {
for {
encOp, ok := r.Decode()
if !ok {
return
}
switch ops.OpType(encOp.Data[0]) {
case ops.TypePush:
q.collectHandlers(r, events, t, area, node, pass)
case ops.TypePop:
return
case ops.TypePass:
var op pointer.PassOp
op.Decode(encOp.Data)
pass = op.Pass
case ops.TypeArea:
var op areaOp
op.Decode(encOp.Data)
q.areas = append(q.areas, areaNode{trans: t, next: area, area: op})
area = len(q.areas) - 1
q.hitTree = append(q.hitTree, hitNode{
next: node,
area: area,
pass: pass,
})
node = len(q.hitTree) - 1
case ops.TypeTransform:
var op ui.TransformOp
op.Decode(encOp.Data)
t = t.Mul(op.Transform)
case ops.TypePointerHandler:
var op pointer.HandlerOp
op.Decode(encOp.Data, encOp.Refs)
q.hitTree = append(q.hitTree, hitNode{
next: node,
area: area,
pass: pass,
key: op.Key,
})
node = len(q.hitTree) - 1
h, ok := q.handlers[op.Key]
if !ok {
h = new(pointerHandler)
q.handlers[op.Key] = h
events[op.Key] = []Event{pointer.Event{Type: pointer.Cancel}}
}
h.active = true
h.area = area
h.transform = t
h.wantsGrab = h.wantsGrab || op.Grab
}
}
}
func (q *pointerQueue) opHit(handlers *[]Key, pos f32.Point) {
// Track whether we're passing through hits.
pass := true
idx := len(q.hitTree) - 1
for idx >= 0 {
n := &q.hitTree[idx]
if !q.hit(n.area, pos) {
idx--
continue
}
pass = pass && n.pass
if pass {
idx--
} else {
idx = n.next
}
if n.key != nil {
*handlers = append(*handlers, n.key)
}
}
}
func (q *pointerQueue) hit(areaIdx int, p f32.Point) bool {
for areaIdx != -1 {
a := &q.areas[areaIdx]
if !a.hit(p) {
return false
}
areaIdx = a.next
}
return true
}
func (a *areaNode) hit(p f32.Point) bool {
p = a.trans.InvTransform(p)
return a.area.Hit(p)
}
func (q *pointerQueue) init() {
if q.handlers == nil {
q.handlers = make(map[Key]*pointerHandler)
}
}
func (q *pointerQueue) Frame(root *ui.Ops, events handlerEvents) {
q.init()
for _, h := range q.handlers {
// Reset handler.
h.active = false
}
q.hitTree = q.hitTree[:0]
q.areas = q.areas[:0]
q.reader.Reset(root)
q.collectHandlers(&q.reader, events, ui.Transform{}, -1, -1, false)
for k, h := range q.handlers {
if !h.active {
q.dropHandler(k)
}
}
}
func (q *pointerQueue) 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 *pointerQueue) Push(e pointer.Event, events handlerEvents) {
q.init()
if e.Type == pointer.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 == pointer.Move || e.Type == pointer.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 == pointer.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 == pointer.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 = pointer.Grabbed
case i == 0:
e.Priority = pointer.Foremost
}
e.Hit = q.hit(h.area, e.Position)
e.Position = h.transform.InvTransform(e.Position)
events[k] = append(events[k], e)
if e.Type == pointer.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
}
}
}
}
func (op *areaOp) Decode(d []byte) {
if ops.OpType(d[0]) != ops.TypeArea {
panic("invalid op")
}
bo := binary.LittleEndian
size := image.Point{
X: int(bo.Uint32(d[2:])),
Y: int(bo.Uint32(d[6:])),
}
*op = areaOp{
kind: areaKind(d[1]),
size: size,
}
}
func (op *areaOp) Hit(pos f32.Point) bool {
switch op.kind {
case areaRect:
if 0 <= pos.X && pos.X < float32(op.size.X) &&
0 <= pos.Y && pos.Y < float32(op.size.Y) {
return true
} else {
return false
}
case areaEllipse:
rx := float32(op.size.X) / 2
ry := float32(op.size.Y) / 2
rx2 := rx * rx
ry2 := ry * ry
xh := pos.X - rx
yk := pos.Y - ry
if xh*xh*ry2+yk*yk*rx2 <= rx2*ry2 {
return true
} else {
return false
}
default:
panic("invalid area kind")
}
}