mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-03 00:16:15 +00:00
io/input,io/router: [API] rename package io/router to io/input
The input name better matches its purpose, in particular when we introduce input.Source. Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package input
|
||||
|
||||
import (
|
||||
"gioui.org/io/event"
|
||||
)
|
||||
|
||||
type clipboardQueue struct {
|
||||
receivers map[event.Tag]struct{}
|
||||
// request avoid read clipboard every frame while waiting.
|
||||
requested bool
|
||||
text *string
|
||||
}
|
||||
|
||||
// WriteClipboard returns the most recent text to be copied
|
||||
// to the clipboard, if any.
|
||||
func (q *clipboardQueue) WriteClipboard() (string, bool) {
|
||||
if q.text == nil {
|
||||
return "", false
|
||||
}
|
||||
text := *q.text
|
||||
q.text = nil
|
||||
return text, true
|
||||
}
|
||||
|
||||
// ReadClipboard reports if any new handler is waiting
|
||||
// to read the clipboard.
|
||||
func (q *clipboardQueue) ReadClipboard() bool {
|
||||
if len(q.receivers) == 0 || q.requested {
|
||||
return false
|
||||
}
|
||||
q.requested = true
|
||||
return true
|
||||
}
|
||||
|
||||
func (q *clipboardQueue) Push(e event.Event, events *handlerEvents) {
|
||||
for r := range q.receivers {
|
||||
events.Add(r, e)
|
||||
delete(q.receivers, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (q *clipboardQueue) ProcessWriteClipboard(refs []interface{}) {
|
||||
q.text = refs[0].(*string)
|
||||
}
|
||||
|
||||
func (q *clipboardQueue) ProcessReadClipboard(refs []interface{}) {
|
||||
if q.receivers == nil {
|
||||
q.receivers = make(map[event.Tag]struct{})
|
||||
}
|
||||
tag := refs[0].(event.Tag)
|
||||
if _, ok := q.receivers[tag]; !ok {
|
||||
q.receivers[tag] = struct{}{}
|
||||
q.requested = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package input
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/op"
|
||||
)
|
||||
|
||||
func TestClipboardDuplicateEvent(t *testing.T) {
|
||||
ops, router, handler := new(op.Ops), new(Router), make([]int, 2)
|
||||
|
||||
// Both must receive the event once
|
||||
clipboard.ReadOp{Tag: &handler[0]}.Add(ops)
|
||||
clipboard.ReadOp{Tag: &handler[1]}.Add(ops)
|
||||
|
||||
router.Frame(ops)
|
||||
event := clipboard.Event{Text: "Test"}
|
||||
router.Queue(event)
|
||||
assertClipboardReadOp(t, router, 0)
|
||||
assertClipboardEvent(t, router.Events(&handler[0]), true)
|
||||
assertClipboardEvent(t, router.Events(&handler[1]), true)
|
||||
ops.Reset()
|
||||
|
||||
// No ReadOp
|
||||
|
||||
router.Frame(ops)
|
||||
assertClipboardReadOp(t, router, 0)
|
||||
assertClipboardEvent(t, router.Events(&handler[0]), false)
|
||||
assertClipboardEvent(t, router.Events(&handler[1]), false)
|
||||
ops.Reset()
|
||||
|
||||
clipboard.ReadOp{Tag: &handler[0]}.Add(ops)
|
||||
|
||||
router.Frame(ops)
|
||||
// No ClipboardEvent sent
|
||||
assertClipboardReadOp(t, router, 1)
|
||||
assertClipboardEvent(t, router.Events(&handler[0]), false)
|
||||
assertClipboardEvent(t, router.Events(&handler[1]), false)
|
||||
ops.Reset()
|
||||
}
|
||||
|
||||
func TestQueueProcessReadClipboard(t *testing.T) {
|
||||
ops, router, handler := new(op.Ops), new(Router), make([]int, 2)
|
||||
ops.Reset()
|
||||
|
||||
// Request read
|
||||
clipboard.ReadOp{Tag: &handler[0]}.Add(ops)
|
||||
|
||||
router.Frame(ops)
|
||||
assertClipboardReadOp(t, router, 1)
|
||||
ops.Reset()
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
// No ReadOp
|
||||
// One receiver must still wait for response
|
||||
|
||||
router.Frame(ops)
|
||||
assertClipboardReadOpDuplicated(t, router, 1)
|
||||
ops.Reset()
|
||||
}
|
||||
|
||||
router.Frame(ops)
|
||||
// Send the clipboard event
|
||||
event := clipboard.Event{Text: "Text 2"}
|
||||
router.Queue(event)
|
||||
assertClipboardReadOp(t, router, 0)
|
||||
assertClipboardEvent(t, router.Events(&handler[0]), true)
|
||||
ops.Reset()
|
||||
|
||||
// No ReadOp
|
||||
// There's no receiver waiting
|
||||
|
||||
router.Frame(ops)
|
||||
assertClipboardReadOp(t, router, 0)
|
||||
assertClipboardEvent(t, router.Events(&handler[0]), false)
|
||||
ops.Reset()
|
||||
}
|
||||
|
||||
func TestQueueProcessWriteClipboard(t *testing.T) {
|
||||
ops, router := new(op.Ops), new(Router)
|
||||
ops.Reset()
|
||||
|
||||
clipboard.WriteOp{Text: "Write 1"}.Add(ops)
|
||||
|
||||
router.Frame(ops)
|
||||
assertClipboardWriteOp(t, router, "Write 1")
|
||||
ops.Reset()
|
||||
|
||||
// No WriteOp
|
||||
|
||||
router.Frame(ops)
|
||||
assertClipboardWriteOp(t, router, "")
|
||||
ops.Reset()
|
||||
|
||||
clipboard.WriteOp{Text: "Write 2"}.Add(ops)
|
||||
|
||||
router.Frame(ops)
|
||||
assertClipboardReadOp(t, router, 0)
|
||||
assertClipboardWriteOp(t, router, "Write 2")
|
||||
ops.Reset()
|
||||
}
|
||||
|
||||
func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) {
|
||||
t.Helper()
|
||||
var evtClipboard int
|
||||
for _, e := range events {
|
||||
switch e.(type) {
|
||||
case clipboard.Event:
|
||||
evtClipboard++
|
||||
}
|
||||
}
|
||||
if evtClipboard <= 0 && expected {
|
||||
t.Error("expected to receive some event")
|
||||
}
|
||||
if evtClipboard > 0 && !expected {
|
||||
t.Error("unexpected event received")
|
||||
}
|
||||
}
|
||||
|
||||
func assertClipboardReadOp(t *testing.T, router *Router, expected int) {
|
||||
t.Helper()
|
||||
if len(router.cqueue.receivers) != expected {
|
||||
t.Error("unexpected number of receivers")
|
||||
}
|
||||
if router.cqueue.ReadClipboard() != (expected > 0) {
|
||||
t.Error("missing requests")
|
||||
}
|
||||
}
|
||||
|
||||
func assertClipboardReadOpDuplicated(t *testing.T, router *Router, expected int) {
|
||||
t.Helper()
|
||||
if len(router.cqueue.receivers) != expected {
|
||||
t.Error("receivers removed")
|
||||
}
|
||||
if router.cqueue.ReadClipboard() != false {
|
||||
t.Error("duplicated requests")
|
||||
}
|
||||
}
|
||||
|
||||
func assertClipboardWriteOp(t *testing.T, router *Router, expected string) {
|
||||
t.Helper()
|
||||
if (router.cqueue.text != nil) != (expected != "") {
|
||||
t.Error("text not defined")
|
||||
}
|
||||
text, ok := router.cqueue.WriteClipboard()
|
||||
if ok != (expected != "") {
|
||||
t.Error("duplicated requests")
|
||||
}
|
||||
if text != expected {
|
||||
t.Errorf("got text %s, expected %s", text, expected)
|
||||
}
|
||||
}
|
||||
+353
@@ -0,0 +1,353 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package input
|
||||
|
||||
import (
|
||||
"image"
|
||||
"sort"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
)
|
||||
|
||||
// EditorState represents the state of an editor needed by input handlers.
|
||||
type EditorState struct {
|
||||
Selection struct {
|
||||
Transform f32.Affine2D
|
||||
key.Range
|
||||
key.Caret
|
||||
}
|
||||
Snippet key.Snippet
|
||||
}
|
||||
|
||||
type TextInputState uint8
|
||||
|
||||
type keyQueue struct {
|
||||
focus event.Tag
|
||||
order []event.Tag
|
||||
dirOrder []dirFocusEntry
|
||||
handlers map[event.Tag]*keyHandler
|
||||
state TextInputState
|
||||
hint key.InputHint
|
||||
content EditorState
|
||||
}
|
||||
|
||||
type keyHandler struct {
|
||||
// visible will be true if the InputOp is present
|
||||
// in the current frame.
|
||||
visible bool
|
||||
new bool
|
||||
hint key.InputHint
|
||||
order int
|
||||
dirOrder int
|
||||
filter key.Set
|
||||
}
|
||||
|
||||
// keyCollector tracks state required to update a keyQueue
|
||||
// from key ops.
|
||||
type keyCollector struct {
|
||||
q *keyQueue
|
||||
focus event.Tag
|
||||
changed bool
|
||||
}
|
||||
|
||||
type dirFocusEntry struct {
|
||||
tag event.Tag
|
||||
row int
|
||||
area int
|
||||
bounds image.Rectangle
|
||||
}
|
||||
|
||||
const (
|
||||
TextInputKeep TextInputState = iota
|
||||
TextInputClose
|
||||
TextInputOpen
|
||||
)
|
||||
|
||||
type FocusDirection int
|
||||
|
||||
const (
|
||||
FocusRight FocusDirection = iota
|
||||
FocusLeft
|
||||
FocusUp
|
||||
FocusDown
|
||||
FocusForward
|
||||
FocusBackward
|
||||
)
|
||||
|
||||
// InputState returns the last text input state as
|
||||
// determined in Frame.
|
||||
func (q *keyQueue) InputState() TextInputState {
|
||||
state := q.state
|
||||
q.state = TextInputKeep
|
||||
return state
|
||||
}
|
||||
|
||||
// InputHint returns the input mode from the most recent key.InputOp.
|
||||
func (q *keyQueue) InputHint() (key.InputHint, bool) {
|
||||
if q.focus == nil {
|
||||
return q.hint, false
|
||||
}
|
||||
focused, ok := q.handlers[q.focus]
|
||||
if !ok {
|
||||
return q.hint, false
|
||||
}
|
||||
old := q.hint
|
||||
q.hint = focused.hint
|
||||
return q.hint, old != q.hint
|
||||
}
|
||||
|
||||
func (q *keyQueue) Reset() {
|
||||
if q.handlers == nil {
|
||||
q.handlers = make(map[event.Tag]*keyHandler)
|
||||
}
|
||||
for _, h := range q.handlers {
|
||||
h.visible, h.new = false, false
|
||||
h.order = -1
|
||||
}
|
||||
q.order = q.order[:0]
|
||||
q.dirOrder = q.dirOrder[:0]
|
||||
}
|
||||
|
||||
func (q *keyQueue) Frame(events *handlerEvents, collector keyCollector) {
|
||||
changed, focus := collector.changed, collector.focus
|
||||
for k, h := range q.handlers {
|
||||
if !h.visible {
|
||||
delete(q.handlers, k)
|
||||
if q.focus == k {
|
||||
// Remove focus from the handler that is no longer visible.
|
||||
q.focus = nil
|
||||
q.state = TextInputClose
|
||||
}
|
||||
} else if h.new && k != focus {
|
||||
// Reset the handler on (each) first appearance, but don't trigger redraw.
|
||||
events.AddNoRedraw(k, key.FocusEvent{Focus: false})
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
q.setFocus(focus, events)
|
||||
}
|
||||
q.updateFocusLayout()
|
||||
}
|
||||
|
||||
// updateFocusLayout partitions input handlers handlers into rows
|
||||
// for directional focus moves.
|
||||
//
|
||||
// The approach is greedy: pick the topmost handler and create a row
|
||||
// containing it. Then, extend the handler bounds to a horizontal beam
|
||||
// and add to the row every handler whose center intersect it. Repeat
|
||||
// until no handlers remain.
|
||||
func (q *keyQueue) updateFocusLayout() {
|
||||
order := q.dirOrder
|
||||
// Sort by ascending y position.
|
||||
sort.SliceStable(order, func(i, j int) bool {
|
||||
return order[i].bounds.Min.Y < order[j].bounds.Min.Y
|
||||
})
|
||||
row := 0
|
||||
for len(order) > 0 {
|
||||
h := &order[0]
|
||||
h.row = row
|
||||
bottom := h.bounds.Max.Y
|
||||
end := 1
|
||||
for ; end < len(order); end++ {
|
||||
h := &order[end]
|
||||
center := (h.bounds.Min.Y + h.bounds.Max.Y) / 2
|
||||
if center > bottom {
|
||||
break
|
||||
}
|
||||
h.row = row
|
||||
}
|
||||
// Sort row by ascending x position.
|
||||
sort.SliceStable(order[:end], func(i, j int) bool {
|
||||
return order[i].bounds.Min.X < order[j].bounds.Min.X
|
||||
})
|
||||
order = order[end:]
|
||||
row++
|
||||
}
|
||||
for i, o := range q.dirOrder {
|
||||
q.handlers[o.tag].dirOrder = i
|
||||
}
|
||||
}
|
||||
|
||||
// MoveFocus attempts to move the focus in the direction of dir, returning true if it succeeds.
|
||||
func (q *keyQueue) MoveFocus(dir FocusDirection, events *handlerEvents) bool {
|
||||
if len(q.dirOrder) == 0 {
|
||||
return false
|
||||
}
|
||||
order := 0
|
||||
if q.focus != nil {
|
||||
order = q.handlers[q.focus].dirOrder
|
||||
}
|
||||
focus := q.dirOrder[order]
|
||||
switch dir {
|
||||
case FocusForward, FocusBackward:
|
||||
if len(q.order) == 0 {
|
||||
break
|
||||
}
|
||||
order := 0
|
||||
if dir == FocusBackward {
|
||||
order = -1
|
||||
}
|
||||
if q.focus != nil {
|
||||
order = q.handlers[q.focus].order
|
||||
if dir == FocusForward {
|
||||
order++
|
||||
} else {
|
||||
order--
|
||||
}
|
||||
}
|
||||
order = (order + len(q.order)) % len(q.order)
|
||||
q.setFocus(q.order[order], events)
|
||||
return true
|
||||
case FocusRight, FocusLeft:
|
||||
next := order
|
||||
if q.focus != nil {
|
||||
next = order + 1
|
||||
if dir == FocusLeft {
|
||||
next = order - 1
|
||||
}
|
||||
}
|
||||
if 0 <= next && next < len(q.dirOrder) {
|
||||
newFocus := q.dirOrder[next]
|
||||
if newFocus.row == focus.row {
|
||||
q.setFocus(newFocus.tag, events)
|
||||
return true
|
||||
}
|
||||
}
|
||||
case FocusUp, FocusDown:
|
||||
delta := +1
|
||||
if dir == FocusUp {
|
||||
delta = -1
|
||||
}
|
||||
nextRow := 0
|
||||
if q.focus != nil {
|
||||
nextRow = focus.row + delta
|
||||
}
|
||||
var closest event.Tag
|
||||
dist := int(1e6)
|
||||
center := (focus.bounds.Min.X + focus.bounds.Max.X) / 2
|
||||
loop:
|
||||
for 0 <= order && order < len(q.dirOrder) {
|
||||
next := q.dirOrder[order]
|
||||
switch next.row {
|
||||
case nextRow:
|
||||
nextCenter := (next.bounds.Min.X + next.bounds.Max.X) / 2
|
||||
d := center - nextCenter
|
||||
if d < 0 {
|
||||
d = -d
|
||||
}
|
||||
if d > dist {
|
||||
break loop
|
||||
}
|
||||
dist = d
|
||||
closest = next.tag
|
||||
case nextRow + delta:
|
||||
break loop
|
||||
}
|
||||
order += delta
|
||||
}
|
||||
if closest != nil {
|
||||
q.setFocus(closest, events)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (q *keyQueue) BoundsFor(t event.Tag) image.Rectangle {
|
||||
order := q.handlers[t].dirOrder
|
||||
return q.dirOrder[order].bounds
|
||||
}
|
||||
|
||||
func (q *keyQueue) AreaFor(t event.Tag) int {
|
||||
order := q.handlers[t].dirOrder
|
||||
return q.dirOrder[order].area
|
||||
}
|
||||
|
||||
func (q *keyQueue) Accepts(t event.Tag, e key.Event) bool {
|
||||
return q.handlers[t].filter.Contains(e.Name, e.Modifiers)
|
||||
}
|
||||
|
||||
func (q *keyQueue) setFocus(focus event.Tag, events *handlerEvents) {
|
||||
if focus != nil {
|
||||
if _, exists := q.handlers[focus]; !exists {
|
||||
focus = nil
|
||||
}
|
||||
}
|
||||
if focus == q.focus {
|
||||
return
|
||||
}
|
||||
q.content = EditorState{}
|
||||
if q.focus != nil {
|
||||
events.Add(q.focus, key.FocusEvent{Focus: false})
|
||||
}
|
||||
q.focus = focus
|
||||
if q.focus != nil {
|
||||
events.Add(q.focus, key.FocusEvent{Focus: true})
|
||||
}
|
||||
if q.focus == nil || q.state == TextInputKeep {
|
||||
q.state = TextInputClose
|
||||
}
|
||||
}
|
||||
|
||||
func (k *keyCollector) focusOp(tag event.Tag) {
|
||||
k.focus = tag
|
||||
k.changed = true
|
||||
}
|
||||
|
||||
func (k *keyCollector) softKeyboard(show bool) {
|
||||
if show {
|
||||
k.q.state = TextInputOpen
|
||||
} else {
|
||||
k.q.state = TextInputClose
|
||||
}
|
||||
}
|
||||
|
||||
func (k *keyCollector) handlerFor(tag event.Tag, area int, bounds image.Rectangle) *keyHandler {
|
||||
h, ok := k.q.handlers[tag]
|
||||
if !ok {
|
||||
h = &keyHandler{new: true, order: -1}
|
||||
k.q.handlers[tag] = h
|
||||
}
|
||||
if h.order == -1 {
|
||||
h.order = len(k.q.order)
|
||||
k.q.order = append(k.q.order, tag)
|
||||
k.q.dirOrder = append(k.q.dirOrder, dirFocusEntry{tag: tag, area: area, bounds: bounds})
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (k *keyCollector) inputOp(op key.InputOp, area int, bounds image.Rectangle) {
|
||||
h := k.handlerFor(op.Tag, area, bounds)
|
||||
h.visible = true
|
||||
h.hint = op.Hint
|
||||
h.filter = op.Keys
|
||||
}
|
||||
|
||||
func (k *keyCollector) selectionOp(t f32.Affine2D, op key.SelectionOp) {
|
||||
if op.Tag == k.q.focus {
|
||||
k.q.content.Selection.Range = op.Range
|
||||
k.q.content.Selection.Caret = op.Caret
|
||||
k.q.content.Selection.Transform = t
|
||||
}
|
||||
}
|
||||
|
||||
func (k *keyCollector) snippetOp(op key.SnippetOp) {
|
||||
if op.Tag == k.q.focus {
|
||||
k.q.content.Snippet = op.Snippet
|
||||
}
|
||||
}
|
||||
|
||||
func (t TextInputState) String() string {
|
||||
switch t {
|
||||
case TextInputKeep:
|
||||
return "Keep"
|
||||
case TextInputClose:
|
||||
return "Close"
|
||||
case TextInputOpen:
|
||||
return "Open"
|
||||
default:
|
||||
panic("unexpected value")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package input
|
||||
|
||||
import (
|
||||
"image"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
)
|
||||
|
||||
func TestKeyWakeup(t *testing.T) {
|
||||
handler := new(int)
|
||||
var ops op.Ops
|
||||
key.InputOp{Tag: handler}.Add(&ops)
|
||||
|
||||
var r Router
|
||||
// Test that merely adding a handler doesn't trigger redraw.
|
||||
r.Frame(&ops)
|
||||
if _, wake := r.WakeupTime(); wake {
|
||||
t.Errorf("adding key.InputOp triggered a redraw")
|
||||
}
|
||||
// However, adding a handler queues a Focus(false) event.
|
||||
if evts := r.Events(handler); len(evts) != 1 {
|
||||
t.Errorf("no Focus event for newly registered key.InputOp")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyMultiples(t *testing.T) {
|
||||
handlers := make([]int, 3)
|
||||
ops := new(op.Ops)
|
||||
r := new(Router)
|
||||
|
||||
key.SoftKeyboardOp{Show: true}.Add(ops)
|
||||
key.InputOp{Tag: &handlers[0]}.Add(ops)
|
||||
key.FocusOp{Tag: &handlers[2]}.Add(ops)
|
||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
||||
|
||||
// The last one must be focused:
|
||||
key.InputOp{Tag: &handlers[2]}.Add(ops)
|
||||
|
||||
r.Frame(ops)
|
||||
|
||||
assertKeyEvent(t, r.Events(&handlers[0]), false)
|
||||
assertKeyEvent(t, r.Events(&handlers[1]), false)
|
||||
assertKeyEvent(t, r.Events(&handlers[2]), true)
|
||||
assertFocus(t, r, &handlers[2])
|
||||
assertKeyboard(t, r, TextInputOpen)
|
||||
}
|
||||
|
||||
func TestKeyStacked(t *testing.T) {
|
||||
handlers := make([]int, 4)
|
||||
ops := new(op.Ops)
|
||||
r := new(Router)
|
||||
|
||||
key.InputOp{Tag: &handlers[0]}.Add(ops)
|
||||
key.FocusOp{Tag: nil}.Add(ops)
|
||||
key.SoftKeyboardOp{Show: false}.Add(ops)
|
||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
||||
key.FocusOp{Tag: &handlers[1]}.Add(ops)
|
||||
key.InputOp{Tag: &handlers[2]}.Add(ops)
|
||||
key.SoftKeyboardOp{Show: true}.Add(ops)
|
||||
key.InputOp{Tag: &handlers[3]}.Add(ops)
|
||||
|
||||
r.Frame(ops)
|
||||
|
||||
assertKeyEvent(t, r.Events(&handlers[0]), false)
|
||||
assertKeyEvent(t, r.Events(&handlers[1]), true)
|
||||
assertKeyEvent(t, r.Events(&handlers[2]), false)
|
||||
assertKeyEvent(t, r.Events(&handlers[3]), false)
|
||||
assertFocus(t, r, &handlers[1])
|
||||
assertKeyboard(t, r, TextInputOpen)
|
||||
}
|
||||
|
||||
func TestKeySoftKeyboardNoFocus(t *testing.T) {
|
||||
ops := new(op.Ops)
|
||||
r := new(Router)
|
||||
|
||||
// It's possible to open the keyboard
|
||||
// without any active focus:
|
||||
key.SoftKeyboardOp{Show: true}.Add(ops)
|
||||
|
||||
r.Frame(ops)
|
||||
|
||||
assertFocus(t, r, nil)
|
||||
assertKeyboard(t, r, TextInputOpen)
|
||||
}
|
||||
|
||||
func TestKeyRemoveFocus(t *testing.T) {
|
||||
handlers := make([]int, 2)
|
||||
ops := new(op.Ops)
|
||||
r := new(Router)
|
||||
|
||||
// New InputOp with Focus and Keyboard:
|
||||
key.InputOp{Tag: &handlers[0], Keys: "Short-Tab"}.Add(ops)
|
||||
key.FocusOp{Tag: &handlers[0]}.Add(ops)
|
||||
key.SoftKeyboardOp{Show: true}.Add(ops)
|
||||
|
||||
// New InputOp without any focus:
|
||||
key.InputOp{Tag: &handlers[1], Keys: "Short-Tab"}.Add(ops)
|
||||
|
||||
r.Frame(ops)
|
||||
|
||||
// Add some key events:
|
||||
event := event.Event(key.Event{Name: key.NameTab, Modifiers: key.ModShortcut, State: key.Press})
|
||||
r.Queue(event)
|
||||
|
||||
assertKeyEvent(t, r.Events(&handlers[0]), true, event)
|
||||
assertKeyEvent(t, r.Events(&handlers[1]), false)
|
||||
assertFocus(t, r, &handlers[0])
|
||||
assertKeyboard(t, r, TextInputOpen)
|
||||
|
||||
ops.Reset()
|
||||
|
||||
// Will get the focus removed:
|
||||
key.InputOp{Tag: &handlers[0]}.Add(ops)
|
||||
|
||||
// Unchanged:
|
||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
||||
|
||||
// Remove focus by focusing on a tag that don't exist.
|
||||
key.FocusOp{Tag: new(int)}.Add(ops)
|
||||
|
||||
r.Frame(ops)
|
||||
|
||||
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
|
||||
assertFocus(t, r, nil)
|
||||
assertKeyboard(t, r, TextInputClose)
|
||||
|
||||
ops.Reset()
|
||||
|
||||
key.InputOp{Tag: &handlers[0]}.Add(ops)
|
||||
|
||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
||||
|
||||
r.Frame(ops)
|
||||
|
||||
assertKeyEventUnexpected(t, r.Events(&handlers[0]))
|
||||
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
|
||||
assertFocus(t, r, nil)
|
||||
assertKeyboard(t, r, TextInputClose)
|
||||
|
||||
ops.Reset()
|
||||
|
||||
// Set focus to InputOp which already
|
||||
// exists in the previous frame:
|
||||
key.FocusOp{Tag: &handlers[0]}.Add(ops)
|
||||
key.InputOp{Tag: &handlers[0]}.Add(ops)
|
||||
key.SoftKeyboardOp{Show: true}.Add(ops)
|
||||
|
||||
// Remove focus.
|
||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
||||
key.FocusOp{Tag: nil}.Add(ops)
|
||||
|
||||
r.Frame(ops)
|
||||
|
||||
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
|
||||
assertFocus(t, r, nil)
|
||||
assertKeyboard(t, r, TextInputOpen)
|
||||
}
|
||||
|
||||
func TestKeyFocusedInvisible(t *testing.T) {
|
||||
handlers := make([]int, 2)
|
||||
ops := new(op.Ops)
|
||||
r := new(Router)
|
||||
|
||||
// Set new InputOp with focus:
|
||||
key.FocusOp{Tag: &handlers[0]}.Add(ops)
|
||||
key.InputOp{Tag: &handlers[0]}.Add(ops)
|
||||
key.SoftKeyboardOp{Show: true}.Add(ops)
|
||||
|
||||
// Set new InputOp without focus:
|
||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
||||
|
||||
r.Frame(ops)
|
||||
|
||||
assertKeyEvent(t, r.Events(&handlers[0]), true)
|
||||
assertKeyEvent(t, r.Events(&handlers[1]), false)
|
||||
assertFocus(t, r, &handlers[0])
|
||||
assertKeyboard(t, r, TextInputOpen)
|
||||
|
||||
ops.Reset()
|
||||
|
||||
//
|
||||
// Removed first (focused) element!
|
||||
//
|
||||
|
||||
// Unchanged:
|
||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
||||
|
||||
r.Frame(ops)
|
||||
|
||||
assertKeyEventUnexpected(t, r.Events(&handlers[0]))
|
||||
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
|
||||
assertFocus(t, r, nil)
|
||||
assertKeyboard(t, r, TextInputClose)
|
||||
|
||||
ops.Reset()
|
||||
|
||||
// Respawn the first element:
|
||||
// It must receive one `Event{Focus: false}`.
|
||||
key.InputOp{Tag: &handlers[0]}.Add(ops)
|
||||
|
||||
// Unchanged
|
||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
||||
|
||||
r.Frame(ops)
|
||||
|
||||
assertKeyEvent(t, r.Events(&handlers[0]), false)
|
||||
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
|
||||
assertFocus(t, r, nil)
|
||||
assertKeyboard(t, r, TextInputClose)
|
||||
|
||||
}
|
||||
|
||||
func TestNoOps(t *testing.T) {
|
||||
r := new(Router)
|
||||
r.Frame(nil)
|
||||
}
|
||||
|
||||
func TestDirectionalFocus(t *testing.T) {
|
||||
ops := new(op.Ops)
|
||||
r := new(Router)
|
||||
handlers := []image.Rectangle{
|
||||
image.Rect(10, 10, 50, 50),
|
||||
image.Rect(50, 20, 100, 80),
|
||||
image.Rect(20, 26, 60, 80),
|
||||
image.Rect(10, 60, 50, 100),
|
||||
}
|
||||
|
||||
for i, bounds := range handlers {
|
||||
cl := clip.Rect(bounds).Push(ops)
|
||||
key.InputOp{Tag: &handlers[i]}.Add(ops)
|
||||
cl.Pop()
|
||||
}
|
||||
r.Frame(ops)
|
||||
|
||||
r.MoveFocus(FocusLeft)
|
||||
assertFocus(t, r, &handlers[0])
|
||||
r.MoveFocus(FocusLeft)
|
||||
assertFocus(t, r, &handlers[0])
|
||||
r.MoveFocus(FocusRight)
|
||||
assertFocus(t, r, &handlers[1])
|
||||
r.MoveFocus(FocusRight)
|
||||
assertFocus(t, r, &handlers[1])
|
||||
r.MoveFocus(FocusDown)
|
||||
assertFocus(t, r, &handlers[2])
|
||||
r.MoveFocus(FocusDown)
|
||||
assertFocus(t, r, &handlers[2])
|
||||
r.MoveFocus(FocusLeft)
|
||||
assertFocus(t, r, &handlers[3])
|
||||
r.MoveFocus(FocusUp)
|
||||
assertFocus(t, r, &handlers[0])
|
||||
|
||||
r.MoveFocus(FocusForward)
|
||||
assertFocus(t, r, &handlers[1])
|
||||
r.MoveFocus(FocusBackward)
|
||||
assertFocus(t, r, &handlers[0])
|
||||
}
|
||||
|
||||
func TestFocusScroll(t *testing.T) {
|
||||
ops := new(op.Ops)
|
||||
r := new(Router)
|
||||
h := new(int)
|
||||
|
||||
parent := clip.Rect(image.Rect(1, 1, 14, 39)).Push(ops)
|
||||
cl := clip.Rect(image.Rect(10, -20, 20, 30)).Push(ops)
|
||||
key.InputOp{Tag: h}.Add(ops)
|
||||
pointer.InputOp{
|
||||
Tag: h,
|
||||
Kinds: pointer.Scroll,
|
||||
ScrollBounds: image.Rect(-100, -100, 100, 100),
|
||||
}.Add(ops)
|
||||
// Test that h is scrolled even if behind another handler.
|
||||
pointer.InputOp{
|
||||
Tag: new(int),
|
||||
}.Add(ops)
|
||||
cl.Pop()
|
||||
parent.Pop()
|
||||
r.Frame(ops)
|
||||
|
||||
r.MoveFocus(FocusLeft)
|
||||
r.RevealFocus(image.Rect(0, 0, 15, 40))
|
||||
evts := r.Events(h)
|
||||
assertScrollEvent(t, evts[len(evts)-1], f32.Pt(6, -9))
|
||||
}
|
||||
|
||||
func TestFocusClick(t *testing.T) {
|
||||
ops := new(op.Ops)
|
||||
r := new(Router)
|
||||
h := new(int)
|
||||
|
||||
cl := clip.Rect(image.Rect(0, 0, 10, 10)).Push(ops)
|
||||
key.InputOp{Tag: h}.Add(ops)
|
||||
pointer.InputOp{
|
||||
Tag: h,
|
||||
Kinds: pointer.Press | pointer.Release,
|
||||
}.Add(ops)
|
||||
cl.Pop()
|
||||
r.Frame(ops)
|
||||
|
||||
r.MoveFocus(FocusLeft)
|
||||
r.ClickFocus()
|
||||
assertEventPointerTypeSequence(t, r.Events(h), pointer.Cancel, pointer.Press, pointer.Release)
|
||||
}
|
||||
|
||||
func TestNoFocus(t *testing.T) {
|
||||
r := new(Router)
|
||||
r.MoveFocus(FocusForward)
|
||||
}
|
||||
|
||||
func TestKeyRouting(t *testing.T) {
|
||||
handlers := make([]int, 5)
|
||||
ops := new(op.Ops)
|
||||
macroOps := new(op.Ops)
|
||||
r := new(Router)
|
||||
|
||||
rect := clip.Rect{Max: image.Pt(10, 10)}
|
||||
|
||||
macro := op.Record(macroOps)
|
||||
key.InputOp{Tag: &handlers[0], Keys: "A"}.Add(ops)
|
||||
cl1 := rect.Push(ops)
|
||||
key.InputOp{Tag: &handlers[1], Keys: "B"}.Add(ops)
|
||||
key.InputOp{Tag: &handlers[2], Keys: "A"}.Add(ops)
|
||||
cl1.Pop()
|
||||
cl2 := rect.Push(ops)
|
||||
key.InputOp{Tag: &handlers[3]}.Add(ops)
|
||||
key.InputOp{Tag: &handlers[4], Keys: "A"}.Add(ops)
|
||||
cl2.Pop()
|
||||
call := macro.Stop()
|
||||
call.Add(ops)
|
||||
|
||||
r.Frame(ops)
|
||||
|
||||
A, B := key.Event{Name: "A"}, key.Event{Name: "B"}
|
||||
r.Queue(A, B)
|
||||
|
||||
// With no focus, the events should traverse the final branch of the hit tree
|
||||
// searching for handlers.
|
||||
assertKeyEvent(t, r.Events(&handlers[4]), false, A)
|
||||
assertKeyEvent(t, r.Events(&handlers[3]), false)
|
||||
assertKeyEvent(t, r.Events(&handlers[2]), false)
|
||||
assertKeyEvent(t, r.Events(&handlers[1]), false, B)
|
||||
assertKeyEvent(t, r.Events(&handlers[0]), false)
|
||||
|
||||
r2 := new(Router)
|
||||
|
||||
call.Add(ops)
|
||||
key.FocusOp{Tag: &handlers[3]}.Add(ops)
|
||||
r2.Frame(ops)
|
||||
|
||||
r2.Queue(A, B)
|
||||
|
||||
// With focus, the events should traverse the branch of the hit tree
|
||||
// containing the focused element.
|
||||
assertKeyEvent(t, r2.Events(&handlers[4]), false)
|
||||
assertKeyEvent(t, r2.Events(&handlers[3]), true)
|
||||
assertKeyEvent(t, r2.Events(&handlers[2]), false)
|
||||
assertKeyEvent(t, r2.Events(&handlers[1]), false)
|
||||
assertKeyEvent(t, r2.Events(&handlers[0]), false, A)
|
||||
}
|
||||
|
||||
func assertKeyEvent(t *testing.T, events []event.Event, expectedFocus bool, expectedInputs ...event.Event) {
|
||||
t.Helper()
|
||||
var evtFocus int
|
||||
var evtKeyPress int
|
||||
for _, e := range events {
|
||||
switch ev := e.(type) {
|
||||
case key.FocusEvent:
|
||||
if ev.Focus != expectedFocus {
|
||||
t.Errorf("focus is expected to be %v, got %v", expectedFocus, ev.Focus)
|
||||
}
|
||||
evtFocus++
|
||||
case key.Event, key.EditEvent:
|
||||
if len(expectedInputs) <= evtKeyPress {
|
||||
t.Fatalf("unexpected key events")
|
||||
}
|
||||
if !reflect.DeepEqual(ev, expectedInputs[evtKeyPress]) {
|
||||
t.Errorf("expected %v events, got %v", expectedInputs[evtKeyPress], ev)
|
||||
}
|
||||
evtKeyPress++
|
||||
}
|
||||
}
|
||||
if evtFocus <= 0 {
|
||||
t.Errorf("expected focus event")
|
||||
}
|
||||
if evtFocus > 1 {
|
||||
t.Errorf("expected single focus event")
|
||||
}
|
||||
if evtKeyPress != len(expectedInputs) {
|
||||
t.Errorf("expected key events")
|
||||
}
|
||||
}
|
||||
|
||||
func assertKeyEventUnexpected(t *testing.T, events []event.Event) {
|
||||
t.Helper()
|
||||
var evtFocus int
|
||||
for _, e := range events {
|
||||
switch e.(type) {
|
||||
case key.FocusEvent:
|
||||
evtFocus++
|
||||
}
|
||||
}
|
||||
if evtFocus > 1 {
|
||||
t.Errorf("unexpected focus event")
|
||||
}
|
||||
}
|
||||
|
||||
func assertFocus(t *testing.T, router *Router, expected event.Tag) {
|
||||
t.Helper()
|
||||
if got := router.key.queue.focus; got != expected {
|
||||
t.Errorf("expected %v to be focused, got %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func assertKeyboard(t *testing.T, router *Router, expected TextInputState) {
|
||||
t.Helper()
|
||||
if got := router.key.queue.state; got != expected {
|
||||
t.Errorf("expected %v keyboard, got %v", expected, got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,977 @@
|
||||
// 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/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.Kind
|
||||
// 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{Kind: 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.Kinds&(pointer.Press|pointer.Release) != 0 {
|
||||
area.semantic.content.gestures |= ClickGesture
|
||||
}
|
||||
if op.Kinds&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.Kinds
|
||||
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) 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 (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) (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) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
|
||||
hits := q.scratch[:0]
|
||||
cursor := q.hitTest(pos, func(n *hitNode) bool {
|
||||
if n.tag != nil {
|
||||
if _, exists := q.handlers[n.tag]; exists {
|
||||
hits = addHandler(hits, n.tag)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
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{Kind: 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.Kind&h.types == 0 {
|
||||
continue
|
||||
}
|
||||
e := e
|
||||
if e.Kind == 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.Kind != 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.Kind == 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.Kind {
|
||||
case pointer.Press:
|
||||
q.deliverEnterLeaveEvents(p, events, e)
|
||||
p.pressed = true
|
||||
q.deliverEvent(p, events, e)
|
||||
case pointer.Move:
|
||||
if p.pressed {
|
||||
e.Kind = 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.Kind == 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.Kind&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.Kind != 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...)
|
||||
}
|
||||
}
|
||||
// Deliver Leave events.
|
||||
for _, k := range p.entered {
|
||||
if _, found := searchTag(hits, k); found {
|
||||
continue
|
||||
}
|
||||
h := q.handlers[k]
|
||||
e.Kind = pointer.Leave
|
||||
|
||||
if e.Kind&h.types != 0 {
|
||||
e := e
|
||||
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.Kind = pointer.Enter
|
||||
|
||||
if e.Kind&h.types != 0 {
|
||||
e := e
|
||||
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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,599 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
/*
|
||||
Package input implements Router, an event.Queue implementation
|
||||
that disambiguates and routes events to handlers declared
|
||||
in operation lists.
|
||||
|
||||
Router is used by app.Window and is otherwise only useful for
|
||||
using Gio with external window implementations.
|
||||
*/
|
||||
package input
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"image"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/f32"
|
||||
f32internal "gioui.org/internal/f32"
|
||||
"gioui.org/internal/ops"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/semantic"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/op"
|
||||
)
|
||||
|
||||
// Router is a Queue implementation that routes events
|
||||
// to handlers declared in operation lists.
|
||||
type Router struct {
|
||||
savedTrans []f32.Affine2D
|
||||
transStack []f32.Affine2D
|
||||
pointer struct {
|
||||
queue pointerQueue
|
||||
collector pointerCollector
|
||||
}
|
||||
key struct {
|
||||
queue keyQueue
|
||||
collector keyCollector
|
||||
}
|
||||
cqueue clipboardQueue
|
||||
|
||||
handlers handlerEvents
|
||||
|
||||
reader ops.Reader
|
||||
|
||||
// InvalidateOp summary.
|
||||
wakeup bool
|
||||
wakeupTime time.Time
|
||||
}
|
||||
|
||||
// SemanticNode represents a node in the tree describing the components
|
||||
// contained in a frame.
|
||||
type SemanticNode struct {
|
||||
ID SemanticID
|
||||
ParentID SemanticID
|
||||
Children []SemanticNode
|
||||
Desc SemanticDesc
|
||||
|
||||
areaIdx int
|
||||
}
|
||||
|
||||
// SemanticDesc provides a semantic description of a UI component.
|
||||
type SemanticDesc struct {
|
||||
Class semantic.ClassOp
|
||||
Description string
|
||||
Label string
|
||||
Selected bool
|
||||
Disabled bool
|
||||
Gestures SemanticGestures
|
||||
Bounds image.Rectangle
|
||||
}
|
||||
|
||||
// SemanticGestures is a bit-set of supported gestures.
|
||||
type SemanticGestures int
|
||||
|
||||
const (
|
||||
ClickGesture SemanticGestures = 1 << iota
|
||||
ScrollGesture
|
||||
)
|
||||
|
||||
// SemanticID uniquely identifies a SemanticDescription.
|
||||
//
|
||||
// By convention, the zero value denotes the non-existent ID.
|
||||
type SemanticID uint
|
||||
|
||||
type handlerEvents struct {
|
||||
handlers map[event.Tag][]event.Event
|
||||
hadEvents bool
|
||||
}
|
||||
|
||||
// Events returns the available events for the handler key.
|
||||
func (q *Router) Events(k event.Tag) []event.Event {
|
||||
events := q.handlers.Events(k)
|
||||
return events
|
||||
}
|
||||
|
||||
// Frame replaces the declared handlers from the supplied
|
||||
// operation list. The text input state, wakeup time and whether
|
||||
// there are active profile handlers is also saved.
|
||||
func (q *Router) Frame(frame *op.Ops) {
|
||||
q.handlers.Clear()
|
||||
q.wakeup = false
|
||||
var ops *ops.Ops
|
||||
if frame != nil {
|
||||
ops = &frame.Internal
|
||||
}
|
||||
q.reader.Reset(ops)
|
||||
q.collect()
|
||||
|
||||
q.pointer.queue.Frame(&q.handlers)
|
||||
q.key.queue.Frame(&q.handlers, q.key.collector)
|
||||
if q.handlers.HadEvents() {
|
||||
q.wakeup = true
|
||||
q.wakeupTime = time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
// Queue key events to the topmost handler.
|
||||
func (q *Router) QueueTopmost(events ...key.Event) bool {
|
||||
var topmost event.Tag
|
||||
pq := &q.pointer.queue
|
||||
for _, h := range pq.hitTree {
|
||||
if h.ktag != nil {
|
||||
topmost = h.ktag
|
||||
break
|
||||
}
|
||||
}
|
||||
if topmost == nil {
|
||||
return false
|
||||
}
|
||||
for _, e := range events {
|
||||
q.handlers.Add(topmost, e)
|
||||
}
|
||||
return q.handlers.HadEvents()
|
||||
}
|
||||
|
||||
// Queue events and report whether at least one handler had an event queued.
|
||||
func (q *Router) Queue(events ...event.Event) bool {
|
||||
for _, e := range events {
|
||||
switch e := e.(type) {
|
||||
case pointer.Event:
|
||||
q.pointer.queue.Push(e, &q.handlers)
|
||||
case key.Event:
|
||||
q.queueKeyEvent(e)
|
||||
case key.SnippetEvent:
|
||||
// Expand existing, overlapping snippet.
|
||||
if r := q.key.queue.content.Snippet.Range; rangeOverlaps(r, key.Range(e)) {
|
||||
if e.Start > r.Start {
|
||||
e.Start = r.Start
|
||||
}
|
||||
if e.End < r.End {
|
||||
e.End = r.End
|
||||
}
|
||||
}
|
||||
if f := q.key.queue.focus; f != nil {
|
||||
q.handlers.Add(f, e)
|
||||
}
|
||||
case key.EditEvent, key.FocusEvent, key.SelectionEvent:
|
||||
if f := q.key.queue.focus; f != nil {
|
||||
q.handlers.Add(f, e)
|
||||
}
|
||||
case clipboard.Event:
|
||||
q.cqueue.Push(e, &q.handlers)
|
||||
}
|
||||
}
|
||||
return q.handlers.HadEvents()
|
||||
}
|
||||
|
||||
func rangeOverlaps(r1, r2 key.Range) bool {
|
||||
r1 = rangeNorm(r1)
|
||||
r2 = rangeNorm(r2)
|
||||
return r1.Start <= r2.Start && r2.Start < r1.End ||
|
||||
r1.Start <= r2.End && r2.End < r1.End
|
||||
}
|
||||
|
||||
func rangeNorm(r key.Range) key.Range {
|
||||
if r.End < r.Start {
|
||||
r.End, r.Start = r.Start, r.End
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (q *Router) queueKeyEvent(e key.Event) {
|
||||
kq := &q.key.queue
|
||||
f := q.key.queue.focus
|
||||
if f != nil && kq.Accepts(f, e) {
|
||||
q.handlers.Add(f, e)
|
||||
return
|
||||
}
|
||||
pq := &q.pointer.queue
|
||||
idx := len(pq.hitTree) - 1
|
||||
focused := f != nil
|
||||
if focused {
|
||||
// If there is a focused tag, traverse its ancestry through the
|
||||
// hit tree to search for handlers.
|
||||
for ; pq.hitTree[idx].ktag != f; idx-- {
|
||||
}
|
||||
}
|
||||
for idx != -1 {
|
||||
n := &pq.hitTree[idx]
|
||||
if focused {
|
||||
idx = n.next
|
||||
} else {
|
||||
idx--
|
||||
}
|
||||
if n.ktag == nil {
|
||||
continue
|
||||
}
|
||||
if kq.Accepts(n.ktag, e) {
|
||||
q.handlers.Add(n.ktag, e)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Router) MoveFocus(dir FocusDirection) bool {
|
||||
return q.key.queue.MoveFocus(dir, &q.handlers)
|
||||
}
|
||||
|
||||
// RevealFocus scrolls the current focus (if any) into viewport
|
||||
// if there are scrollable parent handlers.
|
||||
func (q *Router) RevealFocus(viewport image.Rectangle) {
|
||||
focus := q.key.queue.focus
|
||||
if focus == nil {
|
||||
return
|
||||
}
|
||||
bounds := q.key.queue.BoundsFor(focus)
|
||||
area := q.key.queue.AreaFor(focus)
|
||||
viewport = q.pointer.queue.ClipFor(area, viewport)
|
||||
|
||||
topleft := bounds.Min.Sub(viewport.Min)
|
||||
topleft = max(topleft, bounds.Max.Sub(viewport.Max))
|
||||
topleft = min(image.Pt(0, 0), topleft)
|
||||
bottomright := bounds.Max.Sub(viewport.Max)
|
||||
bottomright = min(bottomright, bounds.Min.Sub(viewport.Min))
|
||||
bottomright = max(image.Pt(0, 0), bottomright)
|
||||
s := topleft
|
||||
if s.X == 0 {
|
||||
s.X = bottomright.X
|
||||
}
|
||||
if s.Y == 0 {
|
||||
s.Y = bottomright.Y
|
||||
}
|
||||
q.ScrollFocus(s)
|
||||
}
|
||||
|
||||
// ScrollFocus scrolls the focused widget, if any, by dist.
|
||||
func (q *Router) ScrollFocus(dist image.Point) {
|
||||
focus := q.key.queue.focus
|
||||
if focus == nil {
|
||||
return
|
||||
}
|
||||
area := q.key.queue.AreaFor(focus)
|
||||
q.pointer.queue.Deliver(area, pointer.Event{
|
||||
Kind: pointer.Scroll,
|
||||
Source: pointer.Touch,
|
||||
Scroll: f32internal.FPt(dist),
|
||||
}, &q.handlers)
|
||||
}
|
||||
|
||||
func max(p1, p2 image.Point) image.Point {
|
||||
m := p1
|
||||
if p2.X > m.X {
|
||||
m.X = p2.X
|
||||
}
|
||||
if p2.Y > m.Y {
|
||||
m.Y = p2.Y
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func min(p1, p2 image.Point) image.Point {
|
||||
m := p1
|
||||
if p2.X < m.X {
|
||||
m.X = p2.X
|
||||
}
|
||||
if p2.Y < m.Y {
|
||||
m.Y = p2.Y
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (q *Router) ActionAt(p f32.Point) (system.Action, bool) {
|
||||
return q.pointer.queue.ActionAt(p)
|
||||
}
|
||||
|
||||
func (q *Router) ClickFocus() {
|
||||
focus := q.key.queue.focus
|
||||
if focus == nil {
|
||||
return
|
||||
}
|
||||
bounds := q.key.queue.BoundsFor(focus)
|
||||
center := bounds.Max.Add(bounds.Min).Div(2)
|
||||
e := pointer.Event{
|
||||
Position: f32.Pt(float32(center.X), float32(center.Y)),
|
||||
Source: pointer.Touch,
|
||||
}
|
||||
area := q.key.queue.AreaFor(focus)
|
||||
e.Kind = pointer.Press
|
||||
q.pointer.queue.Deliver(area, e, &q.handlers)
|
||||
e.Kind = pointer.Release
|
||||
q.pointer.queue.Deliver(area, e, &q.handlers)
|
||||
}
|
||||
|
||||
// TextInputState returns the input state from the most recent
|
||||
// call to Frame.
|
||||
func (q *Router) TextInputState() TextInputState {
|
||||
return q.key.queue.InputState()
|
||||
}
|
||||
|
||||
// TextInputHint returns the input mode from the most recent key.InputOp.
|
||||
func (q *Router) TextInputHint() (key.InputHint, bool) {
|
||||
return q.key.queue.InputHint()
|
||||
}
|
||||
|
||||
// WriteClipboard returns the most recent text to be copied
|
||||
// to the clipboard, if any.
|
||||
func (q *Router) WriteClipboard() (string, bool) {
|
||||
return q.cqueue.WriteClipboard()
|
||||
}
|
||||
|
||||
// ReadClipboard reports if any new handler is waiting
|
||||
// to read the clipboard.
|
||||
func (q *Router) ReadClipboard() bool {
|
||||
return q.cqueue.ReadClipboard()
|
||||
}
|
||||
|
||||
// Cursor returns the last cursor set.
|
||||
func (q *Router) Cursor() pointer.Cursor {
|
||||
return q.pointer.queue.cursor
|
||||
}
|
||||
|
||||
// SemanticAt returns the first semantic description under pos, if any.
|
||||
func (q *Router) SemanticAt(pos f32.Point) (SemanticID, bool) {
|
||||
return q.pointer.queue.SemanticAt(pos)
|
||||
}
|
||||
|
||||
// AppendSemantics appends the semantic tree to nodes, and returns the result.
|
||||
// The root node is the first added.
|
||||
func (q *Router) AppendSemantics(nodes []SemanticNode) []SemanticNode {
|
||||
q.pointer.collector.q = &q.pointer.queue
|
||||
q.pointer.collector.ensureRoot()
|
||||
return q.pointer.queue.AppendSemantics(nodes)
|
||||
}
|
||||
|
||||
// EditorState returns the editor state for the focused handler, or the
|
||||
// zero value if there is none.
|
||||
func (q *Router) EditorState() EditorState {
|
||||
return q.key.queue.content
|
||||
}
|
||||
|
||||
func (q *Router) collect() {
|
||||
q.transStack = q.transStack[:0]
|
||||
pc := &q.pointer.collector
|
||||
pc.q = &q.pointer.queue
|
||||
pc.reset()
|
||||
kc := &q.key.collector
|
||||
*kc = keyCollector{q: &q.key.queue}
|
||||
q.key.queue.Reset()
|
||||
var t f32.Affine2D
|
||||
bo := binary.LittleEndian
|
||||
for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
|
||||
switch ops.OpType(encOp.Data[0]) {
|
||||
case ops.TypeInvalidate:
|
||||
op := decodeInvalidateOp(encOp.Data)
|
||||
if !q.wakeup || op.At.Before(q.wakeupTime) {
|
||||
q.wakeup = true
|
||||
q.wakeupTime = op.At
|
||||
}
|
||||
case ops.TypeClipboardRead:
|
||||
q.cqueue.ProcessReadClipboard(encOp.Refs)
|
||||
case ops.TypeClipboardWrite:
|
||||
q.cqueue.ProcessWriteClipboard(encOp.Refs)
|
||||
case ops.TypeSave:
|
||||
id := ops.DecodeSave(encOp.Data)
|
||||
if extra := id - len(q.savedTrans) + 1; extra > 0 {
|
||||
q.savedTrans = append(q.savedTrans, make([]f32.Affine2D, extra)...)
|
||||
}
|
||||
q.savedTrans[id] = t
|
||||
case ops.TypeLoad:
|
||||
id := ops.DecodeLoad(encOp.Data)
|
||||
t = q.savedTrans[id]
|
||||
pc.resetState()
|
||||
pc.setTrans(t)
|
||||
|
||||
case ops.TypeClip:
|
||||
var op ops.ClipOp
|
||||
op.Decode(encOp.Data)
|
||||
pc.clip(op)
|
||||
case ops.TypePopClip:
|
||||
pc.popArea()
|
||||
case ops.TypeTransform:
|
||||
t2, push := ops.DecodeTransform(encOp.Data)
|
||||
if push {
|
||||
q.transStack = append(q.transStack, t)
|
||||
}
|
||||
t = t.Mul(t2)
|
||||
pc.setTrans(t)
|
||||
case ops.TypePopTransform:
|
||||
n := len(q.transStack)
|
||||
t = q.transStack[n-1]
|
||||
q.transStack = q.transStack[:n-1]
|
||||
pc.setTrans(t)
|
||||
|
||||
// Pointer ops.
|
||||
case ops.TypePass:
|
||||
pc.pass()
|
||||
case ops.TypePopPass:
|
||||
pc.popPass()
|
||||
case ops.TypePointerInput:
|
||||
op := pointer.InputOp{
|
||||
Tag: encOp.Refs[0].(event.Tag),
|
||||
Grab: encOp.Data[1] != 0,
|
||||
Kinds: pointer.Kind(bo.Uint16(encOp.Data[2:])),
|
||||
ScrollBounds: image.Rectangle{
|
||||
Min: image.Point{
|
||||
X: int(int32(bo.Uint32(encOp.Data[4:]))),
|
||||
Y: int(int32(bo.Uint32(encOp.Data[8:]))),
|
||||
},
|
||||
Max: image.Point{
|
||||
X: int(int32(bo.Uint32(encOp.Data[12:]))),
|
||||
Y: int(int32(bo.Uint32(encOp.Data[16:]))),
|
||||
},
|
||||
},
|
||||
}
|
||||
pc.inputOp(op, &q.handlers)
|
||||
case ops.TypeCursor:
|
||||
name := pointer.Cursor(encOp.Data[1])
|
||||
pc.cursor(name)
|
||||
case ops.TypeSource:
|
||||
op := transfer.SourceOp{
|
||||
Tag: encOp.Refs[0].(event.Tag),
|
||||
Type: encOp.Refs[1].(string),
|
||||
}
|
||||
pc.sourceOp(op, &q.handlers)
|
||||
case ops.TypeTarget:
|
||||
op := transfer.TargetOp{
|
||||
Tag: encOp.Refs[0].(event.Tag),
|
||||
Type: encOp.Refs[1].(string),
|
||||
}
|
||||
pc.targetOp(op, &q.handlers)
|
||||
case ops.TypeOffer:
|
||||
op := transfer.OfferOp{
|
||||
Tag: encOp.Refs[0].(event.Tag),
|
||||
Type: encOp.Refs[1].(string),
|
||||
Data: encOp.Refs[2].(io.ReadCloser),
|
||||
}
|
||||
pc.offerOp(op, &q.handlers)
|
||||
case ops.TypeActionInput:
|
||||
act := system.Action(encOp.Data[1])
|
||||
pc.actionInputOp(act)
|
||||
|
||||
// Key ops.
|
||||
case ops.TypeKeyFocus:
|
||||
tag, _ := encOp.Refs[0].(event.Tag)
|
||||
op := key.FocusOp{
|
||||
Tag: tag,
|
||||
}
|
||||
kc.focusOp(op.Tag)
|
||||
case ops.TypeKeySoftKeyboard:
|
||||
op := key.SoftKeyboardOp{
|
||||
Show: encOp.Data[1] != 0,
|
||||
}
|
||||
kc.softKeyboard(op.Show)
|
||||
case ops.TypeKeyInput:
|
||||
filter := key.Set(*encOp.Refs[1].(*string))
|
||||
op := key.InputOp{
|
||||
Tag: encOp.Refs[0].(event.Tag),
|
||||
Hint: key.InputHint(encOp.Data[1]),
|
||||
Keys: filter,
|
||||
}
|
||||
a := pc.currentArea()
|
||||
b := pc.currentAreaBounds()
|
||||
pc.keyInputOp(op)
|
||||
kc.inputOp(op, a, b)
|
||||
case ops.TypeSnippet:
|
||||
op := key.SnippetOp{
|
||||
Tag: encOp.Refs[0].(event.Tag),
|
||||
Snippet: key.Snippet{
|
||||
Range: key.Range{
|
||||
Start: int(int32(bo.Uint32(encOp.Data[1:]))),
|
||||
End: int(int32(bo.Uint32(encOp.Data[5:]))),
|
||||
},
|
||||
Text: *(encOp.Refs[1].(*string)),
|
||||
},
|
||||
}
|
||||
kc.snippetOp(op)
|
||||
case ops.TypeSelection:
|
||||
op := key.SelectionOp{
|
||||
Tag: encOp.Refs[0].(event.Tag),
|
||||
Range: key.Range{
|
||||
Start: int(int32(bo.Uint32(encOp.Data[1:]))),
|
||||
End: int(int32(bo.Uint32(encOp.Data[5:]))),
|
||||
},
|
||||
Caret: key.Caret{
|
||||
Pos: f32.Point{
|
||||
X: math.Float32frombits(bo.Uint32(encOp.Data[9:])),
|
||||
Y: math.Float32frombits(bo.Uint32(encOp.Data[13:])),
|
||||
},
|
||||
Ascent: math.Float32frombits(bo.Uint32(encOp.Data[17:])),
|
||||
Descent: math.Float32frombits(bo.Uint32(encOp.Data[21:])),
|
||||
},
|
||||
}
|
||||
kc.selectionOp(t, op)
|
||||
|
||||
// Semantic ops.
|
||||
case ops.TypeSemanticLabel:
|
||||
lbl := *encOp.Refs[0].(*string)
|
||||
pc.semanticLabel(lbl)
|
||||
case ops.TypeSemanticDesc:
|
||||
desc := *encOp.Refs[0].(*string)
|
||||
pc.semanticDesc(desc)
|
||||
case ops.TypeSemanticClass:
|
||||
class := semantic.ClassOp(encOp.Data[1])
|
||||
pc.semanticClass(class)
|
||||
case ops.TypeSemanticSelected:
|
||||
if encOp.Data[1] != 0 {
|
||||
pc.semanticSelected(true)
|
||||
} else {
|
||||
pc.semanticSelected(false)
|
||||
}
|
||||
case ops.TypeSemanticEnabled:
|
||||
if encOp.Data[1] != 0 {
|
||||
pc.semanticEnabled(true)
|
||||
} else {
|
||||
pc.semanticEnabled(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WakeupTime returns the most recent time for doing another frame,
|
||||
// as determined from the last call to Frame.
|
||||
func (q *Router) WakeupTime() (time.Time, bool) {
|
||||
return q.wakeupTime, q.wakeup
|
||||
}
|
||||
|
||||
func (h *handlerEvents) init() {
|
||||
if h.handlers == nil {
|
||||
h.handlers = make(map[event.Tag][]event.Event)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handlerEvents) AddNoRedraw(k event.Tag, e event.Event) {
|
||||
h.init()
|
||||
h.handlers[k] = append(h.handlers[k], e)
|
||||
}
|
||||
|
||||
func (h *handlerEvents) Add(k event.Tag, e event.Event) {
|
||||
h.AddNoRedraw(k, e)
|
||||
h.hadEvents = true
|
||||
}
|
||||
|
||||
func (h *handlerEvents) HadEvents() bool {
|
||||
u := h.hadEvents
|
||||
h.hadEvents = false
|
||||
return u
|
||||
}
|
||||
|
||||
func (h *handlerEvents) Events(k event.Tag) []event.Event {
|
||||
if events, ok := h.handlers[k]; ok {
|
||||
h.handlers[k] = h.handlers[k][:0]
|
||||
return events
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *handlerEvents) Clear() {
|
||||
for k := range h.handlers {
|
||||
delete(h.handlers, k)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeInvalidateOp(d []byte) op.InvalidateOp {
|
||||
bo := binary.LittleEndian
|
||||
if ops.OpType(d[0]) != ops.TypeInvalidate {
|
||||
panic("invalid op")
|
||||
}
|
||||
var o op.InvalidateOp
|
||||
if nanos := bo.Uint64(d[1:]); nanos > 0 {
|
||||
o.At = time.Unix(0, int64(nanos))
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func (s SemanticGestures) String() string {
|
||||
var gestures []string
|
||||
if s&ClickGesture != 0 {
|
||||
gestures = append(gestures, "Click")
|
||||
}
|
||||
return strings.Join(gestures, ",")
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package input
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/semantic"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
)
|
||||
|
||||
func TestEmptySemantics(t *testing.T) {
|
||||
var r Router
|
||||
tree := r.AppendSemantics(nil)
|
||||
if len(tree) != 1 {
|
||||
t.Errorf("expected 1 semantic node for empty tree, got %d", len(tree))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSemanticTree(t *testing.T) {
|
||||
var (
|
||||
ops op.Ops
|
||||
r Router
|
||||
)
|
||||
t1 := clip.Rect(image.Rect(0, 0, 75, 75)).Push(&ops)
|
||||
semantic.DescriptionOp("child1").Add(&ops)
|
||||
t1.Pop()
|
||||
t2 := clip.Rect(image.Rect(25, 25, 100, 100)).Push(&ops)
|
||||
semantic.DescriptionOp("child2").Add(&ops)
|
||||
t2.Pop()
|
||||
r.Frame(&ops)
|
||||
tests := []struct {
|
||||
x, y float32
|
||||
desc string
|
||||
}{
|
||||
{24, 24, "child1"},
|
||||
{50, 50, "child2"},
|
||||
{100, 100, ""},
|
||||
}
|
||||
tree := r.AppendSemantics(nil)
|
||||
verifyTree(t, 0, tree[0])
|
||||
for _, test := range tests {
|
||||
p := f32.Pt(test.x, test.y)
|
||||
id, found := r.SemanticAt(p)
|
||||
if !found {
|
||||
t.Errorf("no semantic node at %v", p)
|
||||
}
|
||||
n, found := lookupNode(tree, id)
|
||||
if !found {
|
||||
t.Errorf("no id %d in semantic tree", id)
|
||||
}
|
||||
if got := n.Desc.Description; got != test.desc {
|
||||
t.Errorf("got semantic description %s at %v, expected %s", got, p, test.desc)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify stable IDs.
|
||||
r.Frame(&ops)
|
||||
tree2 := r.AppendSemantics(nil)
|
||||
if !reflect.DeepEqual(tree, tree2) {
|
||||
fmt.Println("First tree:")
|
||||
printTree(0, tree[0])
|
||||
fmt.Println("Second tree:")
|
||||
printTree(0, tree2[0])
|
||||
t.Error("same semantic description lead to differing trees")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSemanticDescription(t *testing.T) {
|
||||
var ops op.Ops
|
||||
pointer.InputOp{Tag: new(int), Kinds: pointer.Press | pointer.Release}.Add(&ops)
|
||||
semantic.DescriptionOp("description").Add(&ops)
|
||||
semantic.LabelOp("label").Add(&ops)
|
||||
semantic.Button.Add(&ops)
|
||||
semantic.EnabledOp(false).Add(&ops)
|
||||
semantic.SelectedOp(true).Add(&ops)
|
||||
var r Router
|
||||
r.Frame(&ops)
|
||||
tree := r.AppendSemantics(nil)
|
||||
got := tree[0].Desc
|
||||
exp := SemanticDesc{
|
||||
Class: 1,
|
||||
Description: "description",
|
||||
Label: "label",
|
||||
Selected: true,
|
||||
Disabled: true,
|
||||
Gestures: ClickGesture,
|
||||
Bounds: image.Rectangle{Min: image.Point{X: -1e+06, Y: -1e+06}, Max: image.Point{X: 1e+06, Y: 1e+06}},
|
||||
}
|
||||
if got != exp {
|
||||
t.Errorf("semantic description mismatch:\nGot: %+v\nWant: %+v", got, exp)
|
||||
}
|
||||
}
|
||||
|
||||
func lookupNode(tree []SemanticNode, id SemanticID) (SemanticNode, bool) {
|
||||
for _, n := range tree {
|
||||
if id == n.ID {
|
||||
return n, true
|
||||
}
|
||||
}
|
||||
return SemanticNode{}, false
|
||||
}
|
||||
|
||||
func verifyTree(t *testing.T, parent SemanticID, n SemanticNode) {
|
||||
t.Helper()
|
||||
if n.ParentID != parent {
|
||||
t.Errorf("node %d: got parent %d, want %d", n.ID, n.ParentID, parent)
|
||||
}
|
||||
for _, c := range n.Children {
|
||||
verifyTree(t, n.ID, c)
|
||||
}
|
||||
}
|
||||
|
||||
func printTree(indent int, n SemanticNode) {
|
||||
for i := 0; i < indent; i++ {
|
||||
fmt.Print("\t")
|
||||
}
|
||||
fmt.Printf("%d: %+v\n", n.ID, n.Desc)
|
||||
for _, c := range n.Children {
|
||||
printTree(indent+1, c)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user