mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 15:45:38 +00:00
4fadf71992
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>
316 lines
6.1 KiB
Go
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")
|
|
}
|
|
}
|