forked from joejulian/gio
8fb6d3da2b
Even when a command defers event delivery to the next frame, the already observed events must still be delivered in the current frame. This matters for pointer events that hit more than one event handler. Fixes: https://todo.sr.ht/~eliasnaur/gio/594 Signed-off-by: Elias Naur <mail@eliasnaur.com>
882 lines
22 KiB
Go
882 lines
22 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package input
|
|
|
|
import (
|
|
"image"
|
|
"io"
|
|
"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 tracks the [io/event.Tag] identifiers of user interface widgets
|
|
// and routes events to them. [Source] is its interface exposed to widgets.
|
|
type Router struct {
|
|
savedTrans []f32.Affine2D
|
|
transStack []f32.Affine2D
|
|
handlers map[event.Tag]*handler
|
|
pointer struct {
|
|
queue pointerQueue
|
|
collector pointerCollector
|
|
}
|
|
key struct {
|
|
queue keyQueue
|
|
// The following fields have the same purpose as the fields in
|
|
// type handler, but for key.Events.
|
|
filter keyFilter
|
|
nextFilter keyFilter
|
|
scratchFilter keyFilter
|
|
}
|
|
cqueue clipboardQueue
|
|
// states is the list of pending state changes resulting from
|
|
// incoming events. The first element, if present, contains the state
|
|
// and events for the current frame.
|
|
changes []stateChange
|
|
reader ops.Reader
|
|
// InvalidateCmd summary.
|
|
wakeup bool
|
|
wakeupTime time.Time
|
|
// Changes queued for next call to Frame.
|
|
commands []Command
|
|
// transfers is the pending transfer.DataEvent.Open functions.
|
|
transfers []io.ReadCloser
|
|
// deferring is set if command execution and event delivery is deferred
|
|
// to the next frame.
|
|
deferring bool
|
|
// scratchFilters is for garbage-free construction of ephemeral filters.
|
|
scratchFilters []taggedFilter
|
|
}
|
|
|
|
// Source implements the interface between a Router and user interface widgets.
|
|
// The value Source is disabled.
|
|
type Source struct {
|
|
r *Router
|
|
}
|
|
|
|
// Command represents a request such as moving the focus, or initiating a clipboard read.
|
|
// Commands are queued by calling [Source.Queue].
|
|
type Command interface {
|
|
ImplementsCommand()
|
|
}
|
|
|
|
// 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
|
|
|
|
// SystemEvent is a marker for events that have platform specific
|
|
// side-effects. SystemEvents are never matched by catch-all filters.
|
|
type SystemEvent struct {
|
|
Event event.Event
|
|
}
|
|
|
|
// handler contains the per-handler state tracked by a [Router].
|
|
type handler struct {
|
|
// active tracks whether the handler was active in the current
|
|
// frame. Router deletes state belonging to inactive handlers during Frame.
|
|
active bool
|
|
pointer pointerHandler
|
|
key keyHandler
|
|
// filter the handler has asked for through event handling
|
|
// in the previous frame. It is used for routing events in the
|
|
// current frame.
|
|
filter filter
|
|
// prevFilter is the filter being built in the current frame.
|
|
nextFilter filter
|
|
// processedFilter is the filters that have exhausted available events.
|
|
processedFilter filter
|
|
}
|
|
|
|
// filter is the union of a set of [io/event.Filters].
|
|
type filter struct {
|
|
pointer pointerFilter
|
|
focusable bool
|
|
}
|
|
|
|
// taggedFilter is a filter for a particular tag.
|
|
type taggedFilter struct {
|
|
tag event.Tag
|
|
filter filter
|
|
}
|
|
|
|
// stateChange represents the new state and outgoing events
|
|
// resulting from an incoming event.
|
|
type stateChange struct {
|
|
// event, if set, is the trigger for the change.
|
|
event event.Event
|
|
state inputState
|
|
events []taggedEvent
|
|
}
|
|
|
|
// inputState represent a immutable snapshot of the state required
|
|
// to route events.
|
|
type inputState struct {
|
|
clipboardState
|
|
keyState
|
|
pointerState
|
|
}
|
|
|
|
// taggedEvent represents an event and its target handler.
|
|
type taggedEvent struct {
|
|
event event.Event
|
|
tag event.Tag
|
|
}
|
|
|
|
// Source returns a Source backed by this Router.
|
|
func (q *Router) Source() Source {
|
|
return Source{r: q}
|
|
}
|
|
|
|
// Execute a command.
|
|
func (s Source) Execute(c Command) {
|
|
if !s.Enabled() {
|
|
return
|
|
}
|
|
s.r.execute(c)
|
|
}
|
|
|
|
// Enabled reports whether the source is enabled. Only enabled
|
|
// Sources deliver events and respond to commands.
|
|
func (s Source) Enabled() bool {
|
|
return s.r != nil
|
|
}
|
|
|
|
// Focused reports whether tag is focused, according to the most recent
|
|
// [key.FocusEvent] delivered.
|
|
func (s Source) Focused(tag event.Tag) bool {
|
|
if !s.Enabled() {
|
|
return false
|
|
}
|
|
return s.r.state().keyState.focus == tag
|
|
}
|
|
|
|
// Event returns the next event that matches at least one of filters.
|
|
func (s Source) Event(filters ...event.Filter) (event.Event, bool) {
|
|
if !s.Enabled() {
|
|
return nil, false
|
|
}
|
|
return s.r.Event(filters...)
|
|
}
|
|
|
|
func (q *Router) Event(filters ...event.Filter) (event.Event, bool) {
|
|
// Merge filters into scratch filters.
|
|
q.scratchFilters = q.scratchFilters[:0]
|
|
q.key.scratchFilter = q.key.scratchFilter[:0]
|
|
for _, f := range filters {
|
|
var t event.Tag
|
|
switch f := f.(type) {
|
|
case key.Filter:
|
|
q.key.scratchFilter = append(q.key.scratchFilter, f)
|
|
continue
|
|
case transfer.SourceFilter:
|
|
t = f.Target
|
|
case transfer.TargetFilter:
|
|
t = f.Target
|
|
case key.FocusFilter:
|
|
t = f.Target
|
|
case pointer.Filter:
|
|
t = f.Target
|
|
}
|
|
if t == nil {
|
|
continue
|
|
}
|
|
var filter *filter
|
|
for i := range q.scratchFilters {
|
|
s := &q.scratchFilters[i]
|
|
if s.tag == t {
|
|
filter = &s.filter
|
|
break
|
|
}
|
|
}
|
|
if filter == nil {
|
|
n := len(q.scratchFilters)
|
|
if n < cap(q.scratchFilters) {
|
|
// Re-use previously allocated filter.
|
|
q.scratchFilters = q.scratchFilters[:n+1]
|
|
tf := &q.scratchFilters[n]
|
|
tf.tag = t
|
|
filter = &tf.filter
|
|
filter.Reset()
|
|
} else {
|
|
q.scratchFilters = append(q.scratchFilters, taggedFilter{tag: t})
|
|
filter = &q.scratchFilters[n].filter
|
|
}
|
|
}
|
|
filter.Add(f)
|
|
}
|
|
for _, tf := range q.scratchFilters {
|
|
h := q.stateFor(tf.tag)
|
|
h.filter.Merge(tf.filter)
|
|
h.nextFilter.Merge(tf.filter)
|
|
}
|
|
q.key.filter = append(q.key.filter, q.key.scratchFilter...)
|
|
q.key.nextFilter = append(q.key.nextFilter, q.key.scratchFilter...)
|
|
// Deliver reset event, if any.
|
|
for _, f := range filters {
|
|
switch f := f.(type) {
|
|
case key.FocusFilter:
|
|
if f.Target == nil {
|
|
break
|
|
}
|
|
h := q.stateFor(f.Target)
|
|
if reset, ok := h.key.ResetEvent(); ok {
|
|
return reset, true
|
|
}
|
|
case pointer.Filter:
|
|
if f.Target == nil {
|
|
break
|
|
}
|
|
h := q.stateFor(f.Target)
|
|
if reset, ok := h.pointer.ResetEvent(); ok && h.filter.pointer.Matches(reset) {
|
|
return reset, true
|
|
}
|
|
}
|
|
}
|
|
for i := range q.changes {
|
|
if q.deferring && i > 0 {
|
|
break
|
|
}
|
|
change := &q.changes[i]
|
|
for j, evt := range change.events {
|
|
match := false
|
|
switch e := evt.event.(type) {
|
|
case key.Event:
|
|
match = q.key.scratchFilter.Matches(change.state.keyState.focus, e, false)
|
|
default:
|
|
for _, tf := range q.scratchFilters {
|
|
if evt.tag == tf.tag && tf.filter.Matches(evt.event) {
|
|
match = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if match {
|
|
change.events = append(change.events[:j], change.events[j+1:]...)
|
|
// Fast forward state to last matched.
|
|
q.collapseState(i)
|
|
return evt.event, true
|
|
}
|
|
}
|
|
}
|
|
for _, tf := range q.scratchFilters {
|
|
h := q.stateFor(tf.tag)
|
|
h.processedFilter.Merge(tf.filter)
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// collapseState in the interval [1;idx] into q.changes[0].
|
|
func (q *Router) collapseState(idx int) {
|
|
if idx == 0 {
|
|
return
|
|
}
|
|
first := &q.changes[0]
|
|
first.state = q.changes[idx].state
|
|
for _, ch := range q.changes[1 : idx+1] {
|
|
first.events = append(first.events, ch.events...)
|
|
}
|
|
q.changes = append(q.changes[:1], q.changes[idx+1:]...)
|
|
}
|
|
|
|
// Frame completes the current frame and starts a new with the
|
|
// handlers from the frame argument. Remaining events are discarded,
|
|
// unless they were deferred by a command.
|
|
func (q *Router) Frame(frame *op.Ops) {
|
|
var remaining []event.Event
|
|
if n := len(q.changes); n > 0 {
|
|
if q.deferring {
|
|
// Collect events for replay.
|
|
for _, ch := range q.changes[1:] {
|
|
remaining = append(remaining, ch.event)
|
|
}
|
|
q.changes = append(q.changes[:0], stateChange{state: q.changes[0].state})
|
|
} else {
|
|
// Collapse state.
|
|
state := q.changes[n-1].state
|
|
q.changes = append(q.changes[:0], stateChange{state: state})
|
|
}
|
|
}
|
|
for _, rc := range q.transfers {
|
|
if rc != nil {
|
|
rc.Close()
|
|
}
|
|
}
|
|
q.transfers = nil
|
|
q.deferring = false
|
|
for _, h := range q.handlers {
|
|
h.filter, h.nextFilter = h.nextFilter, h.filter
|
|
h.nextFilter.Reset()
|
|
h.processedFilter.Reset()
|
|
h.pointer.Reset()
|
|
h.key.Reset()
|
|
}
|
|
q.key.filter, q.key.nextFilter = q.key.nextFilter, q.key.filter
|
|
q.key.nextFilter = q.key.nextFilter[:0]
|
|
var ops *ops.Ops
|
|
if frame != nil {
|
|
ops = &frame.Internal
|
|
}
|
|
q.reader.Reset(ops)
|
|
q.collect()
|
|
for k, h := range q.handlers {
|
|
if !h.active {
|
|
delete(q.handlers, k)
|
|
} else {
|
|
h.active = false
|
|
}
|
|
}
|
|
q.executeCommands()
|
|
q.Queue(remaining...)
|
|
st := q.lastState()
|
|
pst, evts := q.pointer.queue.Frame(q.handlers, st.pointerState)
|
|
st.pointerState = pst
|
|
st.keyState = q.key.queue.Frame(q.handlers, q.lastState().keyState)
|
|
q.changeState(nil, st, evts)
|
|
|
|
// Collapse state and events.
|
|
q.collapseState(len(q.changes) - 1)
|
|
}
|
|
|
|
// Queue events to be routed.
|
|
func (q *Router) Queue(events ...event.Event) {
|
|
for _, e := range events {
|
|
se, system := e.(SystemEvent)
|
|
if system {
|
|
e = se.Event
|
|
}
|
|
q.processEvent(e, system)
|
|
}
|
|
}
|
|
|
|
func (f *filter) Add(flt event.Filter) {
|
|
switch flt := flt.(type) {
|
|
case key.FocusFilter:
|
|
f.focusable = true
|
|
case pointer.Filter:
|
|
f.pointer.Add(flt)
|
|
case transfer.SourceFilter, transfer.TargetFilter:
|
|
f.pointer.Add(flt)
|
|
}
|
|
}
|
|
|
|
// Merge f2 into f.
|
|
func (f *filter) Merge(f2 filter) {
|
|
f.focusable = f.focusable || f2.focusable
|
|
f.pointer.Merge(f2.pointer)
|
|
}
|
|
|
|
func (f *filter) Matches(e event.Event) bool {
|
|
switch e.(type) {
|
|
case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent:
|
|
return f.focusable
|
|
default:
|
|
return f.pointer.Matches(e)
|
|
}
|
|
}
|
|
|
|
func (f *filter) Reset() {
|
|
*f = filter{
|
|
pointer: pointerFilter{
|
|
sourceMimes: f.pointer.sourceMimes[:0],
|
|
targetMimes: f.pointer.targetMimes[:0],
|
|
},
|
|
}
|
|
}
|
|
|
|
func (q *Router) processEvent(e event.Event, system bool) {
|
|
state := q.lastState()
|
|
switch e := e.(type) {
|
|
case pointer.Event:
|
|
pstate, evts := q.pointer.queue.Push(q.handlers, state.pointerState, e)
|
|
state.pointerState = pstate
|
|
q.changeState(e, state, evts)
|
|
case key.Event:
|
|
var evts []taggedEvent
|
|
if q.key.filter.Matches(state.keyState.focus, e, system) {
|
|
evts = append(evts, taggedEvent{event: e})
|
|
}
|
|
q.changeState(e, state, evts)
|
|
case key.SnippetEvent:
|
|
// Expand existing, overlapping snippet.
|
|
if r := state.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
|
|
}
|
|
}
|
|
var evts []taggedEvent
|
|
if f := state.focus; f != nil {
|
|
evts = append(evts, taggedEvent{tag: f, event: e})
|
|
}
|
|
q.changeState(e, state, evts)
|
|
case key.EditEvent, key.FocusEvent, key.SelectionEvent:
|
|
var evts []taggedEvent
|
|
if f := state.focus; f != nil {
|
|
evts = append(evts, taggedEvent{tag: f, event: e})
|
|
}
|
|
q.changeState(e, state, evts)
|
|
case transfer.DataEvent:
|
|
cstate, evts := q.cqueue.Push(state.clipboardState, e)
|
|
state.clipboardState = cstate
|
|
q.changeState(e, state, evts)
|
|
default:
|
|
panic("unknown event type")
|
|
}
|
|
}
|
|
|
|
func (q *Router) execute(c Command) {
|
|
// The command can be executed immediately if event delivery is not frozen, and
|
|
// no event receiver has completed their event handling.
|
|
if !q.deferring {
|
|
ch := q.executeCommand(c)
|
|
immediate := true
|
|
for _, e := range ch.events {
|
|
h, ok := q.handlers[e.tag]
|
|
immediate = immediate && (!ok || !h.processedFilter.Matches(e.event))
|
|
}
|
|
if immediate {
|
|
// Hold on to the remaining events for state replay.
|
|
var evts []event.Event
|
|
for _, ch := range q.changes {
|
|
if ch.event != nil {
|
|
evts = append(evts, ch.event)
|
|
}
|
|
}
|
|
if len(q.changes) > 1 {
|
|
q.changes = q.changes[:1]
|
|
}
|
|
q.changeState(nil, ch.state, ch.events)
|
|
q.Queue(evts...)
|
|
return
|
|
}
|
|
}
|
|
q.deferring = true
|
|
q.commands = append(q.commands, c)
|
|
}
|
|
|
|
func (q *Router) state() inputState {
|
|
if len(q.changes) > 0 {
|
|
return q.changes[0].state
|
|
}
|
|
return inputState{}
|
|
}
|
|
|
|
func (q *Router) lastState() inputState {
|
|
if n := len(q.changes); n > 0 {
|
|
return q.changes[n-1].state
|
|
}
|
|
return inputState{}
|
|
}
|
|
|
|
func (q *Router) executeCommands() {
|
|
for _, c := range q.commands {
|
|
ch := q.executeCommand(c)
|
|
q.changeState(nil, ch.state, ch.events)
|
|
}
|
|
q.commands = nil
|
|
}
|
|
|
|
// executeCommand the command and return the resulting state change along with the
|
|
// tag the state change depended on, if any.
|
|
func (q *Router) executeCommand(c Command) stateChange {
|
|
state := q.state()
|
|
var evts []taggedEvent
|
|
switch req := c.(type) {
|
|
case key.SelectionCmd:
|
|
state.keyState = q.key.queue.setSelection(state.keyState, req)
|
|
case key.FocusCmd:
|
|
state.keyState, evts = q.key.queue.Focus(q.handlers, state.keyState, req.Tag)
|
|
case key.SoftKeyboardCmd:
|
|
state.keyState = state.keyState.softKeyboard(req.Show)
|
|
case key.SnippetCmd:
|
|
state.keyState = q.key.queue.setSnippet(state.keyState, req)
|
|
case transfer.OfferCmd:
|
|
state.pointerState, evts = q.pointer.queue.offerData(q.handlers, state.pointerState, req)
|
|
case clipboard.WriteCmd:
|
|
q.cqueue.ProcessWriteClipboard(req)
|
|
case clipboard.ReadCmd:
|
|
state.clipboardState = q.cqueue.ProcessReadClipboard(state.clipboardState, req.Tag)
|
|
case pointer.GrabCmd:
|
|
state.pointerState, evts = q.pointer.queue.grab(state.pointerState, req)
|
|
case op.InvalidateCmd:
|
|
if !q.wakeup || req.At.Before(q.wakeupTime) {
|
|
q.wakeup = true
|
|
q.wakeupTime = req.At
|
|
}
|
|
}
|
|
return stateChange{state: state, events: evts}
|
|
}
|
|
|
|
func (q *Router) changeState(e event.Event, state inputState, evts []taggedEvent) {
|
|
// Wrap pointer.DataEvent.Open functions to detect them not being called.
|
|
for i := range evts {
|
|
e := &evts[i]
|
|
if de, ok := e.event.(transfer.DataEvent); ok {
|
|
transferIdx := len(q.transfers)
|
|
data := de.Open()
|
|
q.transfers = append(q.transfers, data)
|
|
de.Open = func() io.ReadCloser {
|
|
q.transfers[transferIdx] = nil
|
|
return data
|
|
}
|
|
e.event = de
|
|
}
|
|
}
|
|
// Initialize the first change to contain the current state
|
|
// and events that are bound for the current frame.
|
|
if len(q.changes) == 0 {
|
|
q.changes = append(q.changes, stateChange{})
|
|
}
|
|
if e != nil && len(evts) > 0 {
|
|
// An event triggered events bound for user receivers. Add a state change to be
|
|
// able to redo the change in case of a command execution.
|
|
q.changes = append(q.changes, stateChange{event: e, state: state, events: evts})
|
|
} else {
|
|
// Otherwise, merge with previous change.
|
|
prev := &q.changes[len(q.changes)-1]
|
|
prev.state = state
|
|
prev.events = append(prev.events, evts...)
|
|
}
|
|
}
|
|
|
|
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) MoveFocus(dir key.FocusDirection) {
|
|
state := q.lastState()
|
|
kstate, evts := q.key.queue.MoveFocus(q.handlers, state.keyState, dir)
|
|
state.keyState = kstate
|
|
q.changeState(nil, state, evts)
|
|
}
|
|
|
|
// RevealFocus scrolls the current focus (if any) into viewport
|
|
// if there are scrollable parent handlers.
|
|
func (q *Router) RevealFocus(viewport image.Rectangle) {
|
|
state := q.lastState()
|
|
focus := state.focus
|
|
if focus == nil {
|
|
return
|
|
}
|
|
kh := &q.handlers[focus].key
|
|
bounds := q.key.queue.BoundsFor(kh)
|
|
area := q.key.queue.AreaFor(kh)
|
|
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) {
|
|
state := q.lastState()
|
|
focus := state.focus
|
|
if focus == nil {
|
|
return
|
|
}
|
|
kh := &q.handlers[focus].key
|
|
area := q.key.queue.AreaFor(kh)
|
|
q.changeState(nil, q.lastState(), q.pointer.queue.Deliver(q.handlers, area, pointer.Event{
|
|
Kind: pointer.Scroll,
|
|
Source: pointer.Touch,
|
|
Scroll: f32internal.FPt(dist),
|
|
}))
|
|
}
|
|
|
|
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.lastState().focus
|
|
if focus == nil {
|
|
return
|
|
}
|
|
kh := &q.handlers[focus].key
|
|
bounds := q.key.queue.BoundsFor(kh)
|
|
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(kh)
|
|
e.Kind = pointer.Press
|
|
state := q.lastState()
|
|
q.changeState(nil, state, q.pointer.queue.Deliver(q.handlers, area, e))
|
|
e.Kind = pointer.Release
|
|
q.changeState(nil, state, q.pointer.queue.Deliver(q.handlers, area, e))
|
|
}
|
|
|
|
// TextInputState returns the input state from the most recent
|
|
// call to Frame.
|
|
func (q *Router) TextInputState() TextInputState {
|
|
state := q.state()
|
|
kstate, s := state.InputState()
|
|
state.keyState = kstate
|
|
q.changeState(nil, state, nil)
|
|
return s
|
|
}
|
|
|
|
// TextInputHint returns the input mode from the most recent key.InputOp.
|
|
func (q *Router) TextInputHint() (key.InputHint, bool) {
|
|
return q.key.queue.InputHint(q.handlers, q.state().keyState)
|
|
}
|
|
|
|
// WriteClipboard returns the most recent content to be copied
|
|
// to the clipboard, if any.
|
|
func (q *Router) WriteClipboard() (mime string, content []byte, ok bool) {
|
|
return q.cqueue.WriteClipboard()
|
|
}
|
|
|
|
// ClipboardRequested reports if any new handler is waiting
|
|
// to read the clipboard.
|
|
func (q *Router) ClipboardRequested() bool {
|
|
return q.cqueue.ClipboardRequested(q.lastState().clipboardState)
|
|
}
|
|
|
|
// Cursor returns the last cursor set.
|
|
func (q *Router) Cursor() pointer.Cursor {
|
|
return q.state().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.editorState(q.handlers, q.state().keyState)
|
|
}
|
|
|
|
func (q *Router) stateFor(tag event.Tag) *handler {
|
|
if tag == nil {
|
|
panic("internal error: nil tag")
|
|
}
|
|
s, ok := q.handlers[tag]
|
|
if !ok {
|
|
s = new(handler)
|
|
if q.handlers == nil {
|
|
q.handlers = make(map[event.Tag]*handler)
|
|
}
|
|
q.handlers[tag] = s
|
|
}
|
|
s.active = true
|
|
return s
|
|
}
|
|
|
|
func (q *Router) collect() {
|
|
q.transStack = q.transStack[:0]
|
|
pc := &q.pointer.collector
|
|
pc.q = &q.pointer.queue
|
|
pc.Reset()
|
|
kq := &q.key.queue
|
|
q.key.queue.Reset()
|
|
var t f32.Affine2D
|
|
for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
|
|
switch ops.OpType(encOp.Data[0]) {
|
|
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)
|
|
|
|
case ops.TypeInput:
|
|
tag := encOp.Refs[0].(event.Tag)
|
|
s := q.stateFor(tag)
|
|
pc.inputOp(tag, &s.pointer)
|
|
a := pc.currentArea()
|
|
b := pc.currentAreaBounds()
|
|
if s.filter.focusable {
|
|
kq.inputOp(tag, &s.key, t, a, b)
|
|
}
|
|
|
|
// Pointer ops.
|
|
case ops.TypePass:
|
|
pc.pass()
|
|
case ops.TypePopPass:
|
|
pc.popPass()
|
|
case ops.TypeCursor:
|
|
name := pointer.Cursor(encOp.Data[1])
|
|
pc.cursor(name)
|
|
case ops.TypeActionInput:
|
|
act := system.Action(encOp.Data[1])
|
|
pc.actionInputOp(act)
|
|
case ops.TypeKeyInputHint:
|
|
op := key.InputHintOp{
|
|
Tag: encOp.Refs[0].(event.Tag),
|
|
Hint: key.InputHint(encOp.Data[1]),
|
|
}
|
|
s := q.stateFor(op.Tag)
|
|
s.key.inputHint(op.Hint)
|
|
|
|
// 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) {
|
|
t, w := q.wakeupTime, q.wakeup
|
|
q.wakeup = false
|
|
// Pending events always trigger wakeups.
|
|
if len(q.changes) > 1 || len(q.changes) == 1 && len(q.changes[0].events) > 0 {
|
|
t, w = time.Time{}, true
|
|
}
|
|
return t, w
|
|
}
|
|
|
|
func (s SemanticGestures) String() string {
|
|
var gestures []string
|
|
if s&ClickGesture != 0 {
|
|
gestures = append(gestures, "Click")
|
|
}
|
|
return strings.Join(gestures, ",")
|
|
}
|
|
|
|
func (SystemEvent) ImplementsEvent() {}
|