mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
29cea1db49
Pointer hit areas and paint clip areas are separate concepts, but similar enough to warrant merging. This change replaces pointer hit areas with clip areas, so Gio is left with just one area concept (in package op/clip). The reason for separating the concepts in the original Gio release was because of my being unsure general path/stroke hit areas would ever be implemented, let alone efficient. This change represents a change of mind, in the sense that it's better to have an incomplete API than two separate area concepts. Leave the deprecated pointer.Rect, pointer.Ellipse for temporary backwards compatibility. This is an API change. Most existing programs should continue to build with this change, but may have to adjust to having all clip.Ops participate in InputOp hit areas. Signed-off-by: Elias Naur <mail@eliasnaur.com>
528 lines
12 KiB
Go
528 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
|
|
pass bool
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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) clip(op ops.ClipOp) {
|
|
area := -1
|
|
if i := c.state.nodePlusOne - 1; i != -1 {
|
|
n := c.q.hitTree[i]
|
|
area = n.area
|
|
}
|
|
kind := areaRect
|
|
if op.Shape == ops.Ellipse {
|
|
kind = areaEllipse
|
|
}
|
|
areaOp := areaOp{kind: kind, rect: frect(op.Bounds)}
|
|
c.q.areas = append(c.q.areas, areaNode{trans: c.state.t, next: area, area: areaOp})
|
|
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,
|
|
pass: true,
|
|
})
|
|
c.state.nodePlusOne = len(c.q.hitTree) - 1 + 1
|
|
}
|
|
|
|
// frect converts a rectangle to a f32.Rectangle.
|
|
func frect(r image.Rectangle) f32.Rectangle {
|
|
return f32.Rectangle{
|
|
Min: fpt(r.Min), Max: fpt(r.Max),
|
|
}
|
|
}
|
|
|
|
// fpt converts an point to a f32.Point.
|
|
func fpt(p image.Point) f32.Point {
|
|
return f32.Point{
|
|
X: float32(p.X), Y: float32(p.Y),
|
|
}
|
|
}
|
|
|
|
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,
|
|
pass: c.state.pass > 0,
|
|
})
|
|
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 := q.hit(n.area, pos, n.pass)
|
|
if !hit {
|
|
idx--
|
|
continue
|
|
}
|
|
pass = pass && n.pass
|
|
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, pass bool) bool {
|
|
for areaIdx != -1 {
|
|
a := &q.areas[areaIdx]
|
|
p := a.trans.Invert().Transform(p)
|
|
if !a.area.Hit(p) {
|
|
return false
|
|
}
|
|
areaIdx = a.next
|
|
}
|
|
return true
|
|
}
|
|
|
|
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) 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
|
|
}
|