Files
gio/io/router/pointer.go
T
Elias Naur 3f38e67ce0 io/system: add ActionInputOp to register window move gesture areas
The app.Window.Perform(ActionMove) is the wrong abstraction for
initiating a move gesture: Windows needs to know the move gesture
area at pointer move, and macOS needs to know the pointer button
down event that triggers the move gesture. This change replaces
Perform(ActionMove) with a new system.ActionInputOp that marks an
area movable.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-25 19:40:46 +02:00

964 lines
22 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package router
import (
"image"
"io"
"gioui.org/f32"
f32internal "gioui.org/internal/f32"
"gioui.org/internal/ops"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/semantic"
"gioui.org/io/system"
"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
}
action system.Action
}
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,
})
}
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) actionInputOp(act system.Action) {
areaID := c.currentArea()
area := &c.q.areas[areaID]
area.action = act
}
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) ActionAt(pos f32.Point) (system.Action, bool) {
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]
return area.action, area.action != 0
}
return 0, false
}
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(f32internal.FPt(op.rect.Min))
size := f32internal.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 f32internal.Rectangle{
Min: a.trans.Transform(f32internal.FPt(a.area.rect.Min)),
Max: a.trans.Transform(f32internal.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
}