mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
ed0d5d5767
Replace the key.Filter.Target field with a Focus field that matches only of the specified tag has focus. This has the advantage of simpler event delivery and for lower latency in delivering key events to new handlers. For example, consider a UI where a button is activated by a key press, which is turn displays a dialog with another button activated by the same key. This change allows two button press(+releases) in the same frame to arrive at the intended targets: one key press(+release) for each button. Signed-off-by: Elias Naur <mail@eliasnaur.com>
867 lines
22 KiB
Go
867 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
|
|
processedFilter 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
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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 {
|
|
return reset, true
|
|
}
|
|
}
|
|
}
|
|
if !q.deferring {
|
|
for i := range q.changes {
|
|
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)
|
|
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)
|
|
}
|
|
q.key.processedFilter = append(q.key.processedFilter, q.key.scratchFilter...)
|
|
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 i := 1; i <= idx; i++ {
|
|
first.events = append(first.events, q.changes[i].events...)
|
|
}
|
|
q.changes = append(q.changes[:1], q.changes[idx+1:]...)
|
|
}
|
|
|
|
// 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) {
|
|
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)
|
|
|
|
if len(q.changes) > 0 && len(q.changes[0].events) > 0 {
|
|
q.wakeup = true
|
|
q.wakeupTime = time.Time{}
|
|
}
|
|
}
|
|
|
|
// Queue events and report whether at least one event matched a handler.
|
|
func (q *Router) Queue(events ...event.Event) bool {
|
|
matched := false
|
|
for _, e := range events {
|
|
hadEvents := q.processEvent(e)
|
|
matched = matched || hadEvents
|
|
}
|
|
return matched
|
|
}
|
|
|
|
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) 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
|
|
return q.changeState(e, state, evts)
|
|
case key.Event:
|
|
var evts []taggedEvent
|
|
if q.key.filter.Matches(state.keyState.focus, e) {
|
|
evts = append(evts, taggedEvent{event: e})
|
|
}
|
|
return 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})
|
|
}
|
|
return 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})
|
|
}
|
|
return q.changeState(e, state, evts)
|
|
case transfer.DataEvent:
|
|
cstate, evts := q.cqueue.Push(state.clipboardState, e)
|
|
state.clipboardState = cstate
|
|
return 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) bool {
|
|
// 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...)
|
|
}
|
|
return len(evts) > 0
|
|
}
|
|
|
|
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) bool {
|
|
state := q.lastState()
|
|
kstate, evts := q.key.queue.MoveFocus(q.handlers, state.keyState, dir)
|
|
state.keyState = kstate
|
|
return 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) {
|
|
w := q.wakeup
|
|
q.wakeup = false
|
|
return q.wakeupTime, w
|
|
}
|
|
|
|
func (s SemanticGestures) String() string {
|
|
var gestures []string
|
|
if s&ClickGesture != 0 {
|
|
gestures = append(gestures, "Click")
|
|
}
|
|
return strings.Join(gestures, ",")
|
|
}
|