mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
380f96b3fc
Before this change, every Event would be passed to the focused InputOp tag, making it impossible to implement, say, program-wide shortcuts. This change implements key.Event routing similar to how pointer.Events are routed: every InputOp describes the set of keys it can handle, and the router use that information to deliver an Event to the matching handler. This is an API change, because every InputOp must now include a filter matching the keys it wants to handle. Fixes: https://todo.sr.ht/~eliasnaur/gio/395 Signed-off-by: Elias Naur <mail@eliasnaur.com>
956 lines
22 KiB
Go
956 lines
22 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package router
|
|
|
|
import (
|
|
"image"
|
|
"io"
|
|
|
|
"gioui.org/f32"
|
|
"gioui.org/internal/ops"
|
|
"gioui.org/io/event"
|
|
"gioui.org/io/key"
|
|
"gioui.org/io/pointer"
|
|
"gioui.org/io/semantic"
|
|
"gioui.org/io/transfer"
|
|
)
|
|
|
|
type pointerQueue struct {
|
|
hitTree []hitNode
|
|
areas []areaNode
|
|
cursor pointer.Cursor
|
|
handlers map[event.Tag]*pointerHandler
|
|
pointers []pointerInfo
|
|
transfers []io.ReadCloser // pending data transfers
|
|
|
|
scratch []event.Tag
|
|
|
|
semantic struct {
|
|
idsAssigned bool
|
|
lastID SemanticID
|
|
// contentIDs maps semantic content to a list of semantic IDs
|
|
// previously assigned. It is used to maintain stable IDs across
|
|
// frames.
|
|
contentIDs map[semanticContent][]semanticID
|
|
}
|
|
}
|
|
|
|
type hitNode struct {
|
|
next int
|
|
area int
|
|
|
|
// For handler nodes.
|
|
tag event.Tag
|
|
ktag event.Tag
|
|
pass bool
|
|
}
|
|
|
|
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
|
|
|
|
dataSource event.Tag // dragging source tag
|
|
dataTarget event.Tag // dragging target tag
|
|
}
|
|
|
|
type pointerHandler struct {
|
|
area int
|
|
active bool
|
|
wantsGrab bool
|
|
types pointer.Type
|
|
// min and max horizontal/vertical scroll
|
|
scrollRange image.Rectangle
|
|
|
|
sourceMimes []string
|
|
targetMimes []string
|
|
offeredMime string
|
|
data io.ReadCloser
|
|
}
|
|
|
|
type areaOp struct {
|
|
kind areaKind
|
|
rect image.Rectangle
|
|
}
|
|
|
|
type areaNode struct {
|
|
trans f32.Affine2D
|
|
area areaOp
|
|
|
|
cursor pointer.Cursor
|
|
|
|
// Tree indices, with -1 being the sentinel.
|
|
parent int
|
|
firstChild int
|
|
lastChild int
|
|
sibling int
|
|
|
|
semantic struct {
|
|
valid bool
|
|
id SemanticID
|
|
content semanticContent
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type semanticContent struct {
|
|
tag event.Tag
|
|
label string
|
|
desc string
|
|
class semantic.ClassOp
|
|
gestures SemanticGestures
|
|
selected bool
|
|
disabled bool
|
|
}
|
|
|
|
type semanticID struct {
|
|
id SemanticID
|
|
used bool
|
|
}
|
|
|
|
const (
|
|
areaRect areaKind = iota
|
|
areaEllipse
|
|
)
|
|
|
|
func (c *pointerCollector) resetState() {
|
|
c.state = collectState{}
|
|
c.nodeStack = c.nodeStack[:0]
|
|
// Pop every node except the root.
|
|
if len(c.q.hitTree) > 0 {
|
|
c.state.nodePlusOne = 0 + 1
|
|
}
|
|
}
|
|
|
|
func (c *pointerCollector) setTrans(t f32.Affine2D) {
|
|
c.state.t = t
|
|
}
|
|
|
|
func (c *pointerCollector) clip(op ops.ClipOp) {
|
|
kind := areaRect
|
|
if op.Shape == ops.Ellipse {
|
|
kind = areaEllipse
|
|
}
|
|
c.pushArea(kind, op.Bounds)
|
|
}
|
|
|
|
func (c *pointerCollector) pushArea(kind areaKind, bounds image.Rectangle) {
|
|
parentID := c.currentArea()
|
|
areaID := len(c.q.areas)
|
|
areaOp := areaOp{kind: kind, rect: bounds}
|
|
if parentID != -1 {
|
|
parent := &c.q.areas[parentID]
|
|
if parent.firstChild == -1 {
|
|
parent.firstChild = areaID
|
|
}
|
|
if siblingID := parent.lastChild; siblingID != -1 {
|
|
c.q.areas[siblingID].sibling = areaID
|
|
}
|
|
parent.lastChild = areaID
|
|
}
|
|
an := areaNode{
|
|
trans: c.state.t,
|
|
area: areaOp,
|
|
parent: parentID,
|
|
sibling: -1,
|
|
firstChild: -1,
|
|
lastChild: -1,
|
|
}
|
|
|
|
c.q.areas = append(c.q.areas, an)
|
|
c.nodeStack = append(c.nodeStack, c.state.nodePlusOne-1)
|
|
c.addHitNode(hitNode{
|
|
area: areaID,
|
|
pass: true,
|
|
})
|
|
}
|
|
|
|
// 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 a 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) currentArea() int {
|
|
if i := c.state.nodePlusOne - 1; i != -1 {
|
|
n := c.q.hitTree[i]
|
|
return n.area
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func (c *pointerCollector) currentAreaBounds() image.Rectangle {
|
|
a := c.currentArea()
|
|
if a == -1 {
|
|
panic("no root area")
|
|
}
|
|
return c.q.areas[a].bounds()
|
|
}
|
|
|
|
func (c *pointerCollector) addHitNode(n hitNode) {
|
|
n.next = c.state.nodePlusOne - 1
|
|
c.q.hitTree = append(c.q.hitTree, n)
|
|
c.state.nodePlusOne = len(c.q.hitTree) - 1 + 1
|
|
}
|
|
|
|
// newHandler returns the current handler or a new one for tag.
|
|
func (c *pointerCollector) newHandler(tag event.Tag, events *handlerEvents) *pointerHandler {
|
|
areaID := c.currentArea()
|
|
c.addHitNode(hitNode{
|
|
area: areaID,
|
|
tag: tag,
|
|
pass: c.state.pass > 0,
|
|
})
|
|
h, ok := c.q.handlers[tag]
|
|
if !ok {
|
|
h = new(pointerHandler)
|
|
c.q.handlers[tag] = h
|
|
// Cancel handlers on (each) first appearance, but don't
|
|
// trigger redraw.
|
|
events.AddNoRedraw(tag, pointer.Event{Type: pointer.Cancel})
|
|
}
|
|
h.active = true
|
|
h.area = areaID
|
|
return h
|
|
}
|
|
|
|
func (c *pointerCollector) keyInputOp(op key.InputOp) {
|
|
areaID := c.currentArea()
|
|
c.addHitNode(hitNode{
|
|
area: areaID,
|
|
ktag: op.Tag,
|
|
pass: true,
|
|
})
|
|
}
|
|
|
|
func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) {
|
|
areaID := c.currentArea()
|
|
area := &c.q.areas[areaID]
|
|
area.semantic.content.tag = op.Tag
|
|
if op.Types&(pointer.Press|pointer.Release) != 0 {
|
|
area.semantic.content.gestures |= ClickGesture
|
|
}
|
|
if op.Types&pointer.Scroll != 0 {
|
|
area.semantic.content.gestures |= ScrollGesture
|
|
}
|
|
area.semantic.valid = area.semantic.content.gestures != 0
|
|
h := c.newHandler(op.Tag, events)
|
|
h.wantsGrab = h.wantsGrab || op.Grab
|
|
h.types = h.types | op.Types
|
|
h.scrollRange = op.ScrollBounds
|
|
}
|
|
|
|
func (c *pointerCollector) semanticLabel(lbl string) {
|
|
areaID := c.currentArea()
|
|
area := &c.q.areas[areaID]
|
|
area.semantic.valid = true
|
|
area.semantic.content.label = lbl
|
|
}
|
|
|
|
func (c *pointerCollector) semanticDesc(desc string) {
|
|
areaID := c.currentArea()
|
|
area := &c.q.areas[areaID]
|
|
area.semantic.valid = true
|
|
area.semantic.content.desc = desc
|
|
}
|
|
|
|
func (c *pointerCollector) semanticClass(class semantic.ClassOp) {
|
|
areaID := c.currentArea()
|
|
area := &c.q.areas[areaID]
|
|
area.semantic.valid = true
|
|
area.semantic.content.class = class
|
|
}
|
|
|
|
func (c *pointerCollector) semanticSelected(selected bool) {
|
|
areaID := c.currentArea()
|
|
area := &c.q.areas[areaID]
|
|
area.semantic.valid = true
|
|
area.semantic.content.selected = selected
|
|
}
|
|
|
|
func (c *pointerCollector) semanticDisabled(disabled bool) {
|
|
areaID := c.currentArea()
|
|
area := &c.q.areas[areaID]
|
|
area.semantic.valid = true
|
|
area.semantic.content.disabled = disabled
|
|
}
|
|
|
|
func (c *pointerCollector) cursor(cursor pointer.Cursor) {
|
|
areaID := c.currentArea()
|
|
area := &c.q.areas[areaID]
|
|
area.cursor = cursor
|
|
}
|
|
|
|
func (c *pointerCollector) sourceOp(op transfer.SourceOp, events *handlerEvents) {
|
|
h := c.newHandler(op.Tag, events)
|
|
h.sourceMimes = append(h.sourceMimes, op.Type)
|
|
}
|
|
|
|
func (c *pointerCollector) targetOp(op transfer.TargetOp, events *handlerEvents) {
|
|
h := c.newHandler(op.Tag, events)
|
|
h.targetMimes = append(h.targetMimes, op.Type)
|
|
}
|
|
|
|
func (c *pointerCollector) offerOp(op transfer.OfferOp, events *handlerEvents) {
|
|
h := c.newHandler(op.Tag, events)
|
|
h.offeredMime = op.Type
|
|
h.data = op.Data
|
|
}
|
|
|
|
func (c *pointerCollector) reset() {
|
|
c.q.reset()
|
|
c.resetState()
|
|
c.ensureRoot()
|
|
}
|
|
|
|
// Ensure implicit root area for semantic descriptions to hang onto.
|
|
func (c *pointerCollector) ensureRoot() {
|
|
if len(c.q.areas) > 0 {
|
|
return
|
|
}
|
|
c.pushArea(areaRect, image.Rect(-1e6, -1e6, 1e6, 1e6))
|
|
// Make it semantic to ensure a single semantic root.
|
|
c.q.areas[0].semantic.valid = true
|
|
}
|
|
|
|
func (q *pointerQueue) assignSemIDs() {
|
|
if q.semantic.idsAssigned {
|
|
return
|
|
}
|
|
q.semantic.idsAssigned = true
|
|
for i, a := range q.areas {
|
|
if a.semantic.valid {
|
|
q.areas[i].semantic.id = q.semanticIDFor(a.semantic.content)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (q *pointerQueue) AppendSemantics(nodes []SemanticNode) []SemanticNode {
|
|
q.assignSemIDs()
|
|
nodes = q.appendSemanticChildren(nodes, 0)
|
|
nodes = q.appendSemanticArea(nodes, 0, 0)
|
|
return nodes
|
|
}
|
|
|
|
func (q *pointerQueue) appendSemanticArea(nodes []SemanticNode, parentID SemanticID, nodeIdx int) []SemanticNode {
|
|
areaIdx := nodes[nodeIdx].areaIdx
|
|
a := q.areas[areaIdx]
|
|
childStart := len(nodes)
|
|
nodes = q.appendSemanticChildren(nodes, a.firstChild)
|
|
childEnd := len(nodes)
|
|
for i := childStart; i < childEnd; i++ {
|
|
nodes = q.appendSemanticArea(nodes, a.semantic.id, i)
|
|
}
|
|
n := &nodes[nodeIdx]
|
|
n.ParentID = parentID
|
|
n.Children = nodes[childStart:childEnd]
|
|
return nodes
|
|
}
|
|
|
|
func (q *pointerQueue) appendSemanticChildren(nodes []SemanticNode, areaIdx int) []SemanticNode {
|
|
if areaIdx == -1 {
|
|
return nodes
|
|
}
|
|
a := q.areas[areaIdx]
|
|
if semID := a.semantic.id; semID != 0 {
|
|
cnt := a.semantic.content
|
|
nodes = append(nodes, SemanticNode{
|
|
ID: semID,
|
|
Desc: SemanticDesc{
|
|
Bounds: a.bounds(),
|
|
Label: cnt.label,
|
|
Description: cnt.desc,
|
|
Class: cnt.class,
|
|
Gestures: cnt.gestures,
|
|
Selected: cnt.selected,
|
|
Disabled: cnt.disabled,
|
|
},
|
|
areaIdx: areaIdx,
|
|
})
|
|
} else {
|
|
nodes = q.appendSemanticChildren(nodes, a.firstChild)
|
|
}
|
|
return q.appendSemanticChildren(nodes, a.sibling)
|
|
}
|
|
|
|
func (q *pointerQueue) semanticIDFor(content semanticContent) SemanticID {
|
|
ids := q.semantic.contentIDs[content]
|
|
for i, id := range ids {
|
|
if !id.used {
|
|
ids[i].used = true
|
|
return id.id
|
|
}
|
|
}
|
|
// No prior assigned ID; allocate a new one.
|
|
q.semantic.lastID++
|
|
id := semanticID{id: q.semantic.lastID, used: true}
|
|
if q.semantic.contentIDs == nil {
|
|
q.semantic.contentIDs = make(map[semanticContent][]semanticID)
|
|
}
|
|
q.semantic.contentIDs[content] = append(q.semantic.contentIDs[content], id)
|
|
return id.id
|
|
}
|
|
|
|
func (q *pointerQueue) SemanticAt(pos f32.Point) (SemanticID, bool) {
|
|
q.assignSemIDs()
|
|
for i := len(q.hitTree) - 1; i >= 0; i-- {
|
|
n := &q.hitTree[i]
|
|
hit, _ := q.hit(n.area, pos)
|
|
if !hit {
|
|
continue
|
|
}
|
|
area := q.areas[n.area]
|
|
if area.semantic.id != 0 {
|
|
return area.semantic.id, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
|
|
// Track whether we're passing through hits.
|
|
pass := true
|
|
hits := q.scratch[:0]
|
|
idx := len(q.hitTree) - 1
|
|
cursor := pointer.CursorDefault
|
|
for idx >= 0 {
|
|
n := &q.hitTree[idx]
|
|
hit, c := q.hit(n.area, pos)
|
|
if !hit {
|
|
idx--
|
|
continue
|
|
}
|
|
if cursor == pointer.CursorDefault {
|
|
cursor = c
|
|
}
|
|
pass = pass && n.pass
|
|
if pass {
|
|
idx--
|
|
} else {
|
|
idx = n.next
|
|
}
|
|
if n.tag != nil {
|
|
if _, exists := q.handlers[n.tag]; exists {
|
|
hits = addHandler(hits, n.tag)
|
|
}
|
|
}
|
|
}
|
|
q.scratch = hits[:0]
|
|
return hits, cursor
|
|
}
|
|
|
|
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, pointer.Cursor) {
|
|
c := pointer.CursorDefault
|
|
for areaIdx != -1 {
|
|
a := &q.areas[areaIdx]
|
|
if c == pointer.CursorDefault {
|
|
c = a.cursor
|
|
}
|
|
p := a.trans.Invert().Transform(p)
|
|
if !a.area.Hit(p) {
|
|
return false, c
|
|
}
|
|
areaIdx = a.parent
|
|
}
|
|
return true, c
|
|
}
|
|
|
|
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
|
|
h.sourceMimes = h.sourceMimes[:0]
|
|
h.targetMimes = h.targetMimes[:0]
|
|
}
|
|
q.hitTree = q.hitTree[:0]
|
|
q.areas = q.areas[:0]
|
|
q.semantic.idsAssigned = false
|
|
for k, ids := range q.semantic.contentIDs {
|
|
for i := len(ids) - 1; i >= 0; i-- {
|
|
if !ids[i].used {
|
|
ids = append(ids[:i], ids[i+1:]...)
|
|
} else {
|
|
ids[i].used = false
|
|
}
|
|
}
|
|
if len(ids) > 0 {
|
|
q.semantic.contentIDs[k] = ids
|
|
} else {
|
|
delete(q.semantic.contentIDs, k)
|
|
}
|
|
}
|
|
for _, rc := range q.transfers {
|
|
if rc != nil {
|
|
rc.Close()
|
|
}
|
|
}
|
|
q.transfers = nil
|
|
}
|
|
|
|
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)
|
|
q.deliverTransferDataEvent(p, events)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Deliver is like Push, but delivers an event to a particular area.
|
|
func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEvents) {
|
|
var sx, sy = e.Scroll.X, e.Scroll.Y
|
|
idx := len(q.hitTree) - 1
|
|
// Locate first potential receiver.
|
|
for idx != -1 {
|
|
n := &q.hitTree[idx]
|
|
if n.area == areaIdx {
|
|
break
|
|
}
|
|
idx--
|
|
}
|
|
for idx != -1 {
|
|
n := &q.hitTree[idx]
|
|
idx = n.next
|
|
if n.tag == nil {
|
|
continue
|
|
}
|
|
h := q.handlers[n.tag]
|
|
if e.Type&h.types == 0 {
|
|
continue
|
|
}
|
|
e := e
|
|
if e.Type == pointer.Scroll {
|
|
if sx == 0 && sy == 0 {
|
|
break
|
|
}
|
|
// 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.Position = q.invTransform(h.area, e.Position)
|
|
events.Add(n.tag, e)
|
|
if e.Type != pointer.Scroll {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// SemanticArea returns the sematic content for area, and its parent area.
|
|
func (q *pointerQueue) SemanticArea(areaIdx int) (semanticContent, int) {
|
|
for areaIdx != -1 {
|
|
a := &q.areas[areaIdx]
|
|
areaIdx = a.parent
|
|
if !a.semantic.valid {
|
|
continue
|
|
}
|
|
return a.semantic.content, areaIdx
|
|
}
|
|
return semanticContent{}, -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)
|
|
if p.pressed {
|
|
q.deliverDragEvent(p, events)
|
|
}
|
|
case pointer.Release:
|
|
q.deliverEvent(p, events, e)
|
|
p.pressed = false
|
|
q.deliverEnterLeaveEvents(p, events, e)
|
|
q.deliverDropEvent(p, events)
|
|
case pointer.Scroll:
|
|
q.deliverEnterLeaveEvents(p, events, e)
|
|
q.deliverEvent(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
|
|
}
|
|
var sx, sy = e.Scroll.X, e.Scroll.Y
|
|
for _, k := range p.handlers {
|
|
h := q.handlers[k]
|
|
if e.Type == pointer.Scroll {
|
|
if sx == 0 && sy == 0 {
|
|
return
|
|
}
|
|
// 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)
|
|
}
|
|
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) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEvents, e pointer.Event) {
|
|
var hits []event.Tag
|
|
if e.Source != pointer.Mouse && !p.pressed && e.Type != pointer.Press {
|
|
// Consider non-mouse pointers leaving when they're released.
|
|
} else {
|
|
hits, q.cursor = q.opHit(e.Position)
|
|
if p.pressed {
|
|
// Filter out non-participating handlers,
|
|
// except potential transfer targets when a transfer has been initiated.
|
|
var hitsHaveTarget bool
|
|
if p.dataSource != nil {
|
|
transferSource := q.handlers[p.dataSource]
|
|
for _, hit := range hits {
|
|
if _, ok := firstMimeMatch(transferSource, q.handlers[hit]); ok {
|
|
hitsHaveTarget = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
for i := len(hits) - 1; i >= 0; i-- {
|
|
if _, found := searchTag(p.handlers, hits[i]); !found && !hitsHaveTarget {
|
|
hits = append(hits[:i], hits[i+1:]...)
|
|
}
|
|
}
|
|
} else {
|
|
p.handlers = append(p.handlers[:0], hits...)
|
|
}
|
|
}
|
|
if e.Source == pointer.Mouse {
|
|
// 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.
|
|
for _, k := range hits {
|
|
h := q.handlers[k]
|
|
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 (q *pointerQueue) deliverDragEvent(p *pointerInfo, events *handlerEvents) {
|
|
if p.dataSource != nil {
|
|
return
|
|
}
|
|
// Identify the data source.
|
|
for _, k := range p.entered {
|
|
src := q.handlers[k]
|
|
if len(src.sourceMimes) == 0 {
|
|
continue
|
|
}
|
|
// One data source handler per pointer.
|
|
p.dataSource = k
|
|
// Notify all potential targets.
|
|
for k, tgt := range q.handlers {
|
|
if _, ok := firstMimeMatch(src, tgt); ok {
|
|
events.Add(k, transfer.InitiateEvent{})
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
func (q *pointerQueue) deliverDropEvent(p *pointerInfo, events *handlerEvents) {
|
|
if p.dataSource == nil {
|
|
return
|
|
}
|
|
// Request data from the source.
|
|
src := q.handlers[p.dataSource]
|
|
for _, k := range p.entered {
|
|
h := q.handlers[k]
|
|
if m, ok := firstMimeMatch(src, h); ok {
|
|
p.dataTarget = k
|
|
events.Add(p.dataSource, transfer.RequestEvent{Type: m})
|
|
return
|
|
}
|
|
}
|
|
// No valid target found, abort.
|
|
q.deliverTransferCancelEvent(p, events)
|
|
}
|
|
|
|
func (q *pointerQueue) deliverTransferDataEvent(p *pointerInfo, events *handlerEvents) {
|
|
if p.dataSource == nil {
|
|
return
|
|
}
|
|
src := q.handlers[p.dataSource]
|
|
if src.data == nil {
|
|
// Data not received yet.
|
|
return
|
|
}
|
|
if p.dataTarget == nil {
|
|
q.deliverTransferCancelEvent(p, events)
|
|
return
|
|
}
|
|
// Send the offered data to the target.
|
|
transferIdx := len(q.transfers)
|
|
events.Add(p.dataTarget, transfer.DataEvent{
|
|
Type: src.offeredMime,
|
|
Open: func() io.ReadCloser {
|
|
q.transfers[transferIdx] = nil
|
|
return src.data
|
|
},
|
|
})
|
|
q.transfers = append(q.transfers, src.data)
|
|
p.dataTarget = nil
|
|
}
|
|
|
|
func (q *pointerQueue) deliverTransferCancelEvent(p *pointerInfo, events *handlerEvents) {
|
|
events.Add(p.dataSource, transfer.CancelEvent{})
|
|
// Cancel all potential targets.
|
|
src := q.handlers[p.dataSource]
|
|
for k, h := range q.handlers {
|
|
if _, ok := firstMimeMatch(src, h); ok {
|
|
events.Add(k, transfer.CancelEvent{})
|
|
}
|
|
}
|
|
src.offeredMime = ""
|
|
src.data = nil
|
|
p.dataSource = nil
|
|
p.dataTarget = nil
|
|
}
|
|
|
|
// ClipFor clips r to the parents of area.
|
|
func (q *pointerQueue) ClipFor(area int, r image.Rectangle) image.Rectangle {
|
|
a := &q.areas[area]
|
|
parent := a.parent
|
|
for parent != -1 {
|
|
a := &q.areas[parent]
|
|
r = r.Intersect(a.bounds())
|
|
parent = a.parent
|
|
}
|
|
return r
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// firstMimeMatch returns the first type match between src and tgt.
|
|
func firstMimeMatch(src, tgt *pointerHandler) (first string, matched bool) {
|
|
for _, m1 := range tgt.targetMimes {
|
|
for _, m2 := range src.sourceMimes {
|
|
if m1 == m2 {
|
|
return m1, true
|
|
}
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (op *areaOp) Hit(pos f32.Point) bool {
|
|
pos = pos.Sub(fpt(op.rect.Min))
|
|
size := fpt(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 (a *areaNode) bounds() image.Rectangle {
|
|
return f32.Rectangle{
|
|
Min: a.trans.Transform(fpt(a.area.rect.Min)),
|
|
Max: a.trans.Transform(fpt(a.area.rect.Max)),
|
|
}.Round()
|
|
}
|
|
|
|
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
|
|
}
|