// SPDX-License-Identifier: Unlicense OR MIT package input import ( "image" "io" "gioui.org/f32" f32internal "gioui.org/internal/f32" "gioui.org/internal/ops" "gioui.org/io/event" "gioui.org/io/pointer" "gioui.org/io/semantic" "gioui.org/io/system" "gioui.org/io/transfer" ) type pointerQueue struct { hitTree []hitNode areas []areaNode 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 pass bool } // pointerState is the input state related to pointer events. type pointerState struct { cursor pointer.Cursor pointers []pointerInfo } 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 { // areaPlusOne is the index into the list of pointerQueue.areas, plus 1. areaPlusOne int // setup tracks whether the handler has received // the pointer.Cancel event that resets its state. setup bool } // pointerFilter represents the union of a set of pointer filters. type pointerFilter struct { kinds pointer.Kind // min and max horizontal/vertical scroll scrollX, scrollY pointer.ScrollRange sourceMimes []string targetMimes []string } 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, state *pointerHandler) { areaID := c.currentArea() c.addHitNode(hitNode{ area: areaID, tag: tag, pass: c.state.pass > 0, }) state.areaPlusOne = areaID + 1 } func (s *pointerHandler) Reset() { s.areaPlusOne = 0 } func (c *pointerCollector) actionInputOp(act system.Action) { areaID := c.currentArea() area := &c.q.areas[areaID] area.action = act } func (q *pointerQueue) grab(state pointerState, req pointer.GrabCmd) (pointerState, []taggedEvent) { var evts []taggedEvent for _, p := range state.pointers { if !p.pressed || p.id != req.ID { continue } // Drop other handlers that lost their grab. for i := len(p.handlers) - 1; i >= 0; i-- { if tag := p.handlers[i]; tag != req.Tag { evts = append(evts, taggedEvent{ tag: tag, event: pointer.Event{Kind: pointer.Cancel}, }) state = dropHandler(state, tag) } } break } return state, evts } func (c *pointerCollector) inputOp(tag event.Tag, state *pointerHandler) { areaID := c.currentArea() area := &c.q.areas[areaID] area.semantic.content.tag = tag c.newHandler(tag, state) } func (p *pointerFilter) Add(f event.Filter) { switch f := f.(type) { case transfer.SourceFilter: for _, m := range p.sourceMimes { if m == f.Type { return } } p.sourceMimes = append(p.sourceMimes, f.Type) case transfer.TargetFilter: for _, m := range p.targetMimes { if m == f.Type { return } } p.targetMimes = append(p.targetMimes, f.Type) case pointer.Filter: p.kinds = p.kinds | f.Kinds p.scrollX = p.scrollX.Union(f.ScrollX) p.scrollY = p.scrollY.Union(f.ScrollY) } } func (p *pointerFilter) Matches(e event.Event) bool { switch e := e.(type) { case pointer.Event: return e.Kind&p.kinds == e.Kind case transfer.CancelEvent, transfer.InitiateEvent: return len(p.sourceMimes) > 0 || len(p.targetMimes) > 0 case transfer.RequestEvent: for _, t := range p.sourceMimes { if t == e.Type { return true } } case transfer.DataEvent: for _, t := range p.targetMimes { if t == e.Type { return true } } } return false } func (p *pointerFilter) Merge(p2 pointerFilter) { p.kinds = p.kinds | p2.kinds p.scrollX = p.scrollX.Union(p2.scrollX) p.scrollY = p.scrollY.Union(p2.scrollY) p.sourceMimes = append(p.sourceMimes, p2.sourceMimes...) p.targetMimes = append(p.targetMimes, p2.targetMimes...) } // clampScroll splits a scroll distance in the remaining scroll and the // scroll accepted by the filter. func (p *pointerFilter) clampScroll(scroll f32.Point) (left, scrolled f32.Point) { left.X, scrolled.X = clampSplit(scroll.X, p.scrollX.Min, p.scrollX.Max) left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollY.Min, p.scrollY.Max) return } func clampSplit(v float32, min, max int) (float32, float32) { if m := float32(max); v > m { return v - m, m } if m := float32(min); v < m { return v - m, m } return 0, v } func (s *pointerHandler) ResetEvent() (event.Event, bool) { if s.setup { return nil, false } s.setup = true return pointer.Event{Kind: pointer.Cancel}, true } 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) semanticEnabled(enabled bool) { areaID := c.currentArea() area := &c.q.areas[areaID] area.semantic.valid = true area.semantic.content.disabled = !enabled } func (c *pointerCollector) cursor(cursor pointer.Cursor) { areaID := c.currentArea() area := &c.q.areas[areaID] area.cursor = cursor } func (q *pointerQueue) offerData(handlers map[event.Tag]*handler, state pointerState, req transfer.OfferCmd) (pointerState, []taggedEvent) { var evts []taggedEvent for i, p := range state.pointers { if p.dataSource != req.Tag { continue } if p.dataTarget != nil { evts = append(evts, taggedEvent{tag: p.dataTarget, event: transfer.DataEvent{ Type: req.Type, Open: func() io.ReadCloser { return req.Data }, }}) } state.pointers = append([]pointerInfo{}, state.pointers...) state.pointers[i], evts = q.deliverTransferCancelEvent(handlers, p, evts) break } return state, evts } 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) (action system.Action, hasAction bool) { q.hitTest(pos, func(n *hitNode) bool { area := q.areas[n.area] if area.action != 0 { action = area.action hasAction = true return false } return true }) return action, hasAction } func (q *pointerQueue) SemanticAt(pos f32.Point) (semID SemanticID, hasSemID bool) { q.assignSemIDs() q.hitTest(pos, func(n *hitNode) bool { area := q.areas[n.area] if area.semantic.id != 0 { semID = area.semantic.id hasSemID = true return false } return true }) return semID, hasSemID } // hitTest searches the hit tree for nodes matching pos. Any node matching pos will // have the onNode func invoked on it to allow the caller to extract whatever information // is necessary for further processing. onNode may return false to terminate the walk of // the hit tree, or true to continue. Providing this algorithm in this generic way // allows normal event routing and system action event routing to share the same traversal // logic even though they are interested in different aspects of hit nodes. func (q *pointerQueue) hitTest(pos f32.Point, onNode func(*hitNode) bool) pointer.Cursor { // Track whether we're passing through hits. pass := true 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 !onNode(n) { break } } return 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() { 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) } } } func (q *pointerQueue) Frame(handlers map[event.Tag]*handler, state pointerState) (pointerState, []taggedEvent) { for _, h := range handlers { if h.pointer.areaPlusOne != 0 { area := &q.areas[h.pointer.areaPlusOne-1] if h.filter.pointer.kinds&(pointer.Press|pointer.Release) != 0 { area.semantic.content.gestures |= ClickGesture } if h.filter.pointer.kinds&pointer.Scroll != 0 { area.semantic.content.gestures |= ScrollGesture } area.semantic.valid = area.semantic.content.gestures != 0 } } var evts []taggedEvent for i, p := range state.pointers { changed := false p, evts, state.cursor, changed = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, p.last) if changed { state.pointers = append([]pointerInfo{}, state.pointers...) state.pointers[i] = p } } return state, evts } func dropHandler(state pointerState, tag event.Tag) pointerState { pointers := state.pointers state.pointers = nil for _, p := range pointers { handlers := p.handlers p.handlers = nil for _, h := range handlers { if h != tag { p.handlers = append(p.handlers, h) } } entered := p.entered p.entered = nil for _, h := range entered { if h != tag { p.entered = append(p.entered, h) } } state.pointers = append(state.pointers, p) } return state } // pointerOf returns the pointerInfo index corresponding to the pointer in e. func (s pointerState) pointerOf(e pointer.Event) (pointerState, int) { for i, p := range s.pointers { if p.id == e.PointerID { return s, i } } n := len(s.pointers) s.pointers = append(s.pointers[:n:n], pointerInfo{id: e.PointerID}) return s, len(s.pointers) - 1 } // Deliver is like Push, but delivers an event to a particular area. func (q *pointerQueue) Deliver(handlers map[event.Tag]*handler, areaIdx int, e pointer.Event) []taggedEvent { scroll := e.Scroll idx := len(q.hitTree) - 1 // Locate first potential receiver. for idx != -1 { n := &q.hitTree[idx] if n.area == areaIdx { break } idx-- } var evts []taggedEvent for idx != -1 { n := &q.hitTree[idx] idx = n.next h, ok := handlers[n.tag] if !ok || !h.filter.pointer.Matches(e) { continue } e := e if e.Kind == pointer.Scroll { if scroll == (f32.Point{}) { break } scroll, e.Scroll = h.filter.pointer.clampScroll(scroll) } e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position) evts = append(evts, taggedEvent{tag: n.tag, event: e}) if e.Kind != pointer.Scroll { break } } return evts } // 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(handlers map[event.Tag]*handler, state pointerState, e pointer.Event) (pointerState, []taggedEvent) { var evts []taggedEvent if e.Kind == pointer.Cancel { for k := range handlers { evts = append(evts, taggedEvent{ event: pointer.Event{Kind: pointer.Cancel}, tag: k, }) } state.pointers = nil return state, evts } state, pidx := state.pointerOf(e) p := state.pointers[pidx] switch e.Kind { case pointer.Press: p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e) p.pressed = true evts = q.deliverEvent(handlers, p, evts, e) case pointer.Move: if p.pressed { e.Kind = pointer.Drag } p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e) evts = q.deliverEvent(handlers, p, evts, e) if p.pressed { p, evts = q.deliverDragEvent(handlers, p, evts) } case pointer.Release: evts = q.deliverEvent(handlers, p, evts, e) p.pressed = false p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e) p, evts = q.deliverDropEvent(handlers, p, evts) case pointer.Scroll: p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e) evts = q.deliverEvent(handlers, p, evts, e) default: panic("unsupported pointer event type") } p.last = e if !p.pressed && len(p.entered) == 0 { // No longer need to track pointer. state.pointers = append(state.pointers[:pidx:pidx], state.pointers[pidx+1:]...) } else { state.pointers = append([]pointerInfo{}, state.pointers...) state.pointers[pidx] = p } return state, evts } func (q *pointerQueue) deliverEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent, e pointer.Event) []taggedEvent { foremost := true if p.pressed && len(p.handlers) == 1 { e.Priority = pointer.Grabbed foremost = false } scroll := e.Scroll for _, k := range p.handlers { h, ok := handlers[k] if !ok { continue } f := h.filter.pointer if !f.Matches(e) { continue } if e.Kind == pointer.Scroll { if scroll == (f32.Point{}) { return evts } scroll, e.Scroll = f.clampScroll(scroll) } e := e if foremost { foremost = false e.Priority = pointer.Foremost } e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position) evts = append(evts, taggedEvent{event: e, tag: k}) } return evts } func (q *pointerQueue) deliverEnterLeaveEvents(handlers map[event.Tag]*handler, cursor pointer.Cursor, p pointerInfo, evts []taggedEvent, e pointer.Event) (pointerInfo, []taggedEvent, pointer.Cursor, bool) { changed := false var hits []event.Tag if e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press { // Consider non-mouse pointers leaving when they're released. } else { var transSrc *pointerFilter if p.dataSource != nil { transSrc = &handlers[p.dataSource].filter.pointer } cursor = q.hitTest(e.Position, func(n *hitNode) bool { h, ok := handlers[n.tag] if !ok { return true } add := true if p.pressed { add = false // Filter out non-participating handlers, // except potential transfer targets when a transfer has been initiated. if _, found := searchTag(p.handlers, n.tag); found { add = true } if transSrc != nil { if _, ok := firstMimeMatch(transSrc, &h.filter.pointer); ok { add = true } } } if add { hits = addHandler(hits, n.tag) } return true }) if !p.pressed { changed = true p.handlers = hits } } // Deliver Leave events. for _, k := range p.entered { if _, found := searchTag(hits, k); found { continue } h, ok := handlers[k] if !ok { continue } changed = true e := e e.Kind = pointer.Leave if h.filter.pointer.Matches(e) { e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position) evts = append(evts, taggedEvent{tag: k, event: e}) } } // Deliver Enter events. for _, k := range hits { if _, found := searchTag(p.entered, k); found { continue } h, ok := handlers[k] if !ok { continue } changed = true e := e e.Kind = pointer.Enter if h.filter.pointer.Matches(e) { e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position) evts = append(evts, taggedEvent{tag: k, event: e}) } } p.entered = hits return p, evts, cursor, changed } func (q *pointerQueue) deliverDragEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) { if p.dataSource != nil { return p, evts } // Identify the data source. for _, k := range p.entered { src := &handlers[k].filter.pointer if len(src.sourceMimes) == 0 { continue } // One data source handler per pointer. p.dataSource = k // Notify all potential targets. for k, tgt := range handlers { if _, ok := firstMimeMatch(src, &tgt.filter.pointer); ok { evts = append(evts, taggedEvent{tag: k, event: transfer.InitiateEvent{}}) } } break } return p, evts } func (q *pointerQueue) deliverDropEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) { if p.dataSource == nil { return p, evts } // Request data from the source. src := &handlers[p.dataSource].filter.pointer for _, k := range p.entered { h := handlers[k] if m, ok := firstMimeMatch(src, &h.filter.pointer); ok { p.dataTarget = k evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.RequestEvent{Type: m}}) return p, evts } } // No valid target found, abort. return q.deliverTransferCancelEvent(handlers, p, evts) } func (q *pointerQueue) deliverTransferCancelEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) { evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.CancelEvent{}}) // Cancel all potential targets. src := &handlers[p.dataSource].filter.pointer for k, h := range handlers { if _, ok := firstMimeMatch(src, &h.filter.pointer); ok { evts = append(evts, taggedEvent{tag: k, event: transfer.CancelEvent{}}) } } p.dataSource = nil p.dataTarget = nil return p, evts } // 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 *pointerFilter) (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() }