chore(v2): vendor dependencies for offline/China builds

go mod vendor pins onnxruntime_go v1.12.1, Gio and the rest into v2/vendor
so go run/build work without hitting proxy.golang.org (blocked/slow in
China). Verified: CGO_ENABLED=1 go build -mod=vendor ./internal/spike and
GOOS=windows go build -mod=vendor ./internal/ui both pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-23 16:35:01 +08:00
co-authored by Claude Opus 4.8
parent 97c1c4a974
commit f58728cddd
972 changed files with 597802 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: Unlicense OR MIT
package clipboard
import (
"io"
"gioui.org/io/event"
)
// WriteCmd copies Text to the clipboard.
type WriteCmd struct {
Type string
Data io.ReadCloser
}
// ReadCmd requests the text of the clipboard, delivered to
// the handler through an [io/transfer.DataEvent].
type ReadCmd struct {
Tag event.Tag
}
func (WriteCmd) ImplementsCommand() {}
func (ReadCmd) ImplementsCommand() {}
+33
View File
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Unlicense OR MIT
// Package event contains types for event handling.
package event
import (
"gioui.org/internal/ops"
"gioui.org/op"
)
// Tag is the stable identifier for an event handler.
// For a handler h, the tag is typically &h.
type Tag any
// Event is the marker interface for events.
type Event interface {
ImplementsEvent()
}
// Filter represents a filter for [Event] types.
type Filter interface {
ImplementsFilter()
}
// Op declares a tag for input routing at the current transformation
// and clip area hierarchy. It panics if tag is nil.
func Op(o *op.Ops, tag Tag) {
if tag == nil {
panic("Tag must be non-nil")
}
data := ops.Write1(&o.Internal, ops.TypeInputLen, tag)
data[0] = byte(ops.TypeInput)
}
+71
View File
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: Unlicense OR MIT
package input
import (
"io"
"slices"
"gioui.org/io/clipboard"
"gioui.org/io/event"
)
// clipboardState contains the state for clipboard event routing.
type clipboardState struct {
receivers []event.Tag
}
type clipboardQueue struct {
// request avoid read clipboard every frame while waiting.
requested bool
mime string
text []byte
}
// WriteClipboard returns the most recent data to be copied
// to the clipboard, if any.
func (q *clipboardQueue) WriteClipboard() (mime string, content []byte, ok bool) {
if q.text == nil {
return "", nil, false
}
content = q.text
q.text = nil
return q.mime, content, true
}
// ClipboardRequested reports if any new handler is waiting
// to read the clipboard.
func (q *clipboardQueue) ClipboardRequested(state clipboardState) bool {
req := len(state.receivers) > 0 && q.requested
q.requested = false
return req
}
func (q *clipboardQueue) Push(state clipboardState, e event.Event) (clipboardState, []taggedEvent) {
var evts []taggedEvent
for _, r := range state.receivers {
evts = append(evts, taggedEvent{tag: r, event: e})
}
state.receivers = nil
return state, evts
}
func (q *clipboardQueue) ProcessWriteClipboard(req clipboard.WriteCmd) {
defer req.Data.Close()
content, err := io.ReadAll(req.Data)
if err != nil {
return
}
q.mime = req.Type
q.text = content
}
func (q *clipboardQueue) ProcessReadClipboard(state clipboardState, tag event.Tag) clipboardState {
if slices.Contains(state.receivers, tag) {
return state
}
n := len(state.receivers)
state.receivers = append(state.receivers[:n:n], tag)
q.requested = true
return state
}
+15
View File
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Unlicense OR MIT
/*
Package input implements input routing and tracking of interface
state for a window.
The [Source] is the interface between the window and the widgets
of a user interface and is exposed by [gioui.org/app.FrameEvent]
received from windows.
The [Router] is used by [gioui.org/app.Window] to track window state and route
events from the platform to event handlers. It is otherwise only
useful for using Gio with external window implementations.
*/
package input
+364
View File
@@ -0,0 +1,364 @@
// SPDX-License-Identifier: Unlicense OR MIT
package input
import (
"image"
"slices"
"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 {
order []event.Tag
dirOrder []dirFocusEntry
hint key.InputHint
}
// keyState is the input state related to key events.
type keyState struct {
focus event.Tag
state TextInputState
content EditorState
}
type keyHandler struct {
// visible will be true if the InputOp is present
// in the current frame.
visible bool
// reset tracks whether the handler has seen a
// focus reset.
reset bool
hint key.InputHint
orderPlusOne int
dirOrder int
trans f32.Affine2D
}
type keyFilter []key.Filter
type dirFocusEntry struct {
tag event.Tag
row int
area int
bounds image.Rectangle
}
const (
TextInputKeep TextInputState = iota
TextInputClose
TextInputOpen
)
func (k *keyHandler) inputHint(hint key.InputHint) {
k.hint = hint
}
// InputState returns the input state and returns a state
// reset to [TextInputKeep].
func (s keyState) InputState() (keyState, TextInputState) {
state := s.state
s.state = TextInputKeep
return s, state
}
// InputHint returns the input hint from the focused handler and whether it was
// changed since the last call.
func (q *keyQueue) InputHint(handlers map[event.Tag]*handler, state keyState) (key.InputHint, bool) {
focused, ok := handlers[state.focus]
if !ok {
return q.hint, false
}
old := q.hint
q.hint = focused.key.hint
return q.hint, old != q.hint
}
func (k *keyHandler) Reset() {
k.visible = false
k.orderPlusOne = 0
k.hint = key.HintAny
}
func (q *keyQueue) Reset() {
q.order = q.order[:0]
q.dirOrder = q.dirOrder[:0]
}
func (k *keyHandler) ResetEvent() (event.Event, bool) {
if k.reset {
return nil, false
}
k.reset = true
return key.FocusEvent{Focus: false}, true
}
func (q *keyQueue) Frame(handlers map[event.Tag]*handler, state keyState) keyState {
if state.focus != nil {
if h, ok := handlers[state.focus]; !ok || !h.filter.focusable || !h.key.visible {
// Remove focus from the handler that is no longer focusable.
state.focus = nil
state.state = TextInputClose
}
}
q.updateFocusLayout(handlers)
return state
}
// 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(handlers map[event.Tag]*handler) {
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 {
handlers[o.tag].key.dirOrder = i
}
}
// MoveFocus attempts to move the focus in the direction of dir.
func (q *keyQueue) MoveFocus(handlers map[event.Tag]*handler, state keyState, dir key.FocusDirection) (keyState, []taggedEvent) {
if len(q.dirOrder) == 0 {
return state, nil
}
order := 0
if state.focus != nil {
order = handlers[state.focus].key.dirOrder
}
focus := q.dirOrder[order]
switch dir {
case key.FocusForward, key.FocusBackward:
if len(q.order) == 0 {
break
}
order := 0
if dir == key.FocusBackward {
order = -1
}
if state.focus != nil {
order = handlers[state.focus].key.orderPlusOne - 1
if dir == key.FocusForward {
order++
} else {
order--
}
}
order = (order + len(q.order)) % len(q.order)
return q.Focus(handlers, state, q.order[order])
case key.FocusRight, key.FocusLeft:
next := order
if state.focus != nil {
next = order + 1
if dir == key.FocusLeft {
next = order - 1
}
}
if 0 <= next && next < len(q.dirOrder) {
newFocus := q.dirOrder[next]
if newFocus.row == focus.row {
return q.Focus(handlers, state, newFocus.tag)
}
}
case key.FocusUp, key.FocusDown:
delta := +1
if dir == key.FocusUp {
delta = -1
}
nextRow := 0
if state.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 {
return q.Focus(handlers, state, closest)
}
}
return state, nil
}
func (q *keyQueue) BoundsFor(k *keyHandler) image.Rectangle {
order := k.dirOrder
return q.dirOrder[order].bounds
}
func (q *keyQueue) AreaFor(k *keyHandler) int {
order := k.dirOrder
return q.dirOrder[order].area
}
func (k *keyFilter) Matches(focus event.Tag, e key.Event, system bool) bool {
for _, f := range *k {
if keyFilterMatch(focus, f, e, system) {
return true
}
}
return false
}
func keyFilterMatch(focus event.Tag, f key.Filter, e key.Event, system bool) bool {
if f.Focus != nil && f.Focus != focus {
return false
}
if (f.Name != "" || system) && f.Name != e.Name {
return false
}
if e.Modifiers&f.Required != f.Required {
return false
}
if e.Modifiers&^(f.Required|f.Optional) != 0 {
return false
}
return true
}
func (q *keyQueue) Focus(handlers map[event.Tag]*handler, state keyState, focus event.Tag) (keyState, []taggedEvent) {
if focus == state.focus {
return state, nil
}
state.content = EditorState{}
state.content.Selection.Transform = f32.AffineId()
var evts []taggedEvent
if state.focus != nil {
evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: false}})
}
state.focus = focus
if state.focus != nil {
evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: true}})
}
if state.focus == nil || state.state == TextInputKeep {
state.state = TextInputClose
}
return state, evts
}
func (s keyState) softKeyboard(show bool) keyState {
if show {
s.state = TextInputOpen
} else {
s.state = TextInputClose
}
return s
}
func (k *keyFilter) Add(f key.Filter) {
if slices.Contains(*k, f) {
return
}
*k = append(*k, f)
}
func (k *keyFilter) Merge(k2 keyFilter) {
*k = append(*k, k2...)
}
func (q *keyQueue) inputOp(tag event.Tag, state *keyHandler, t f32.Affine2D, area int, bounds image.Rectangle) {
state.visible = true
if state.orderPlusOne == 0 {
state.orderPlusOne = len(q.order) + 1
q.order = append(q.order, tag)
q.dirOrder = append(q.dirOrder, dirFocusEntry{tag: tag, area: area, bounds: bounds})
}
state.trans = t
}
func (q *keyQueue) setSelection(state keyState, req key.SelectionCmd) keyState {
if req.Tag != state.focus {
return state
}
state.content.Selection.Range = req.Range
state.content.Selection.Caret = req.Caret
return state
}
func (q *keyQueue) editorState(handlers map[event.Tag]*handler, state keyState) EditorState {
s := state.content
if f := state.focus; f != nil {
s.Selection.Transform = handlers[f].key.trans
}
return s
}
func (q *keyQueue) setSnippet(state keyState, req key.SnippetCmd) keyState {
if req.Tag == state.focus {
state.content.Snippet = req.Snippet
}
return state
}
func (t TextInputState) String() string {
switch t {
case TextInputKeep:
return "Keep"
case TextInputClose:
return "Close"
case TextInputOpen:
return "Open"
default:
panic("unexpected value")
}
}
+1023
View File
File diff suppressed because it is too large Load Diff
+900
View File
@@ -0,0 +1,900 @@
// SPDX-License-Identifier: Unlicense OR MIT
package input
import (
"image"
"io"
"slices"
"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 zero-value Source is disabled.
type Source struct {
r *Router
disabled bool
}
// 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)
}
// Disabled returns a copy of this source that don't deliver any events.
func (s Source) Disabled() Source {
s2 := s
s2.disabled = true
return s2
}
// Enabled reports whether the source is enabled. Only enabled
// Sources deliver events.
func (s Source) Enabled() bool {
return s.r != nil && !s.disabled
}
// 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.
// If the source is disabled, no events will be reported.
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 = slices.Delete(change.events, j, 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, key.CompositionEvent:
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.CompositionEvent:
e = key.CompositionEvent(rangeNorm(key.Range(e)))
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 = maxPoint(topleft, bounds.Max.Sub(viewport.Max))
topleft = minPoint(image.Pt(0, 0), topleft)
bottomright := bounds.Max.Sub(viewport.Max)
bottomright = minPoint(bottomright, bounds.Min.Sub(viewport.Min))
bottomright = maxPoint(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 maxPoint(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 minPoint(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()
t := f32.AffineId()
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 {
for range extra {
q.savedTrans = append(q.savedTrans, f32.AffineId())
}
}
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() {}
+293
View File
@@ -0,0 +1,293 @@
// SPDX-License-Identifier: Unlicense OR MIT
// Package key implements key and text events and operations.
package key
import (
"strings"
"gioui.org/f32"
"gioui.org/internal/ops"
"gioui.org/io/event"
"gioui.org/op"
)
// Filter matches any [Event] that matches the parameters.
type Filter struct {
// Focus is the tag that must be focused for the filter to match. It has no effect
// if it is nil.
Focus event.Tag
// Required is the set of modifiers that must be included in events matched.
Required Modifiers
// Optional is the set of modifiers that may be included in events matched.
Optional Modifiers
// Name of the key to be matched. As a special case, the empty
// Name matches every key not matched by any other filter.
Name Name
}
// InputHintOp describes the type of text expected by a tag.
type InputHintOp struct {
Tag event.Tag
Hint InputHint
}
// SoftKeyboardCmd shows or hides the on-screen keyboard, if available.
type SoftKeyboardCmd struct {
Show bool
}
// SelectionCmd updates the selection for an input handler.
type SelectionCmd struct {
Tag event.Tag
Range
Caret
}
// SnippetCmd updates the content snippet for an input handler.
type SnippetCmd struct {
Tag event.Tag
Snippet
}
// Range represents a range of text, such as an editor's selection.
// Start and End are in runes.
type Range struct {
Start int
End int
}
// Snippet represents a snippet of text content used for communicating between
// an editor and an input method.
type Snippet struct {
Range
Text string
}
// Caret represents the position of a caret.
type Caret struct {
// Pos is the intersection point of the caret and its baseline.
Pos f32.Point
// Ascent is the length of the caret above its baseline.
Ascent float32
// Descent is the length of the caret below its baseline.
Descent float32
}
// SelectionEvent is generated when an input method changes the selection.
type SelectionEvent Range
// CompositionEvent is generated when an input method changes the composing range.
type CompositionEvent Range
// SnippetEvent is generated when the snippet range is updated by an
// input method.
type SnippetEvent Range
// A FocusEvent is generated when a handler gains or loses
// focus.
type FocusEvent struct {
Focus bool
}
// An Event is generated when a key is pressed. For text input
// use EditEvent.
type Event struct {
// Name of the key.
Name Name
// Modifiers is the set of active modifiers when the key was pressed.
Modifiers Modifiers
// State is the state of the key when the event was fired.
State State
}
// An EditEvent requests an edit by an input method.
type EditEvent struct {
// Range specifies the range to replace with Text.
Range Range
Text string
}
// FocusFilter matches any [FocusEvent], [EditEvent], [SnippetEvent],
// or [SelectionEvent] with the specified target.
type FocusFilter struct {
// Target is a tag specified in a previous event.Op.
Target event.Tag
}
// InputHint changes the on-screen-keyboard type. That hints the
// type of data that might be entered by the user.
type InputHint uint8
const (
// HintAny hints that any input is expected.
HintAny InputHint = iota
// HintText hints that text input is expected. It may activate auto-correction and suggestions.
HintText
// HintNumeric hints that numeric input is expected. It may activate shortcuts for 0-9, "." and ",".
HintNumeric
// HintEmail hints that email input is expected. It may activate shortcuts for common email characters, such as "@" and ".com".
HintEmail
// HintURL hints that URL input is expected. It may activate shortcuts for common URL fragments such as "/" and ".com".
HintURL
// HintTelephone hints that telephone number input is expected. It may activate shortcuts for 0-9, "#" and "*".
HintTelephone
// HintPassword hints that password input is expected. It may disable autocorrection and enable password autofill.
HintPassword
)
// State is the state of a key during an event.
type State uint8
const (
// Press is the state of a pressed key.
Press State = iota
// Release is the state of a key that has been released.
//
// Note: release events are only implemented on the following platforms:
// macOS, Linux, Windows, WebAssembly.
Release
)
// Modifiers
type Modifiers uint32
const (
// ModCtrl is the ctrl modifier key.
ModCtrl Modifiers = 1 << iota
// ModCommand is the command modifier key
// found on Apple keyboards.
ModCommand
// ModShift is the shift modifier key.
ModShift
// ModAlt is the alt modifier key, or the option
// key on Apple keyboards.
ModAlt
// ModSuper is the "logo" modifier key, often
// represented by a Windows logo.
ModSuper
)
// Name is the identifier for a keyboard key.
//
// For letters, the upper case form is used, via unicode.ToUpper.
// The shift modifier is taken into account, all other
// modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1"
// combinations both give the Name "!" with the US keyboard layout.
type Name string
const (
// Names for special keys.
NameLeftArrow Name = "←"
NameRightArrow Name = "→"
NameUpArrow Name = "↑"
NameDownArrow Name = "↓"
NameReturn Name = "⏎"
NameEnter Name = "⌤"
NameEscape Name = "⎋"
NameHome Name = "⇱"
NameEnd Name = "⇲"
NameDeleteBackward Name = "⌫"
NameDeleteForward Name = "⌦"
NamePageUp Name = "⇞"
NamePageDown Name = "⇟"
NameTab Name = "Tab"
NameSpace Name = "Space"
NameCtrl Name = "Ctrl"
NameShift Name = "Shift"
NameAlt Name = "Alt"
NameSuper Name = "Super"
NameCommand Name = "⌘"
NameF1 Name = "F1"
NameF2 Name = "F2"
NameF3 Name = "F3"
NameF4 Name = "F4"
NameF5 Name = "F5"
NameF6 Name = "F6"
NameF7 Name = "F7"
NameF8 Name = "F8"
NameF9 Name = "F9"
NameF10 Name = "F10"
NameF11 Name = "F11"
NameF12 Name = "F12"
NameBack Name = "Back"
)
type FocusDirection int
const (
FocusRight FocusDirection = iota
FocusLeft
FocusUp
FocusDown
FocusForward
FocusBackward
)
// Contain reports whether m contains all modifiers
// in m2.
func (m Modifiers) Contain(m2 Modifiers) bool {
return m&m2 == m2
}
// FocusCmd requests to set or clear the keyboard focus.
type FocusCmd struct {
// Tag is the new focus. The focus is cleared if Tag is nil, or if Tag
// has no [event.Op] references.
Tag event.Tag
}
func (h InputHintOp) Add(o *op.Ops) {
if h.Tag == nil {
panic("Tag must be non-nil")
}
data := ops.Write1(&o.Internal, ops.TypeKeyInputHintLen, h.Tag)
data[0] = byte(ops.TypeKeyInputHint)
data[1] = byte(h.Hint)
}
func (EditEvent) ImplementsEvent() {}
func (Event) ImplementsEvent() {}
func (FocusEvent) ImplementsEvent() {}
func (CompositionEvent) ImplementsEvent() {}
func (SnippetEvent) ImplementsEvent() {}
func (SelectionEvent) ImplementsEvent() {}
func (FocusCmd) ImplementsCommand() {}
func (SoftKeyboardCmd) ImplementsCommand() {}
func (SelectionCmd) ImplementsCommand() {}
func (SnippetCmd) ImplementsCommand() {}
func (Filter) ImplementsFilter() {}
func (FocusFilter) ImplementsFilter() {}
func (m Modifiers) String() string {
var strs []string
if m.Contain(ModCtrl) {
strs = append(strs, string(NameCtrl))
}
if m.Contain(ModCommand) {
strs = append(strs, string(NameCommand))
}
if m.Contain(ModShift) {
strs = append(strs, string(NameShift))
}
if m.Contain(ModAlt) {
strs = append(strs, string(NameAlt))
}
if m.Contain(ModSuper) {
strs = append(strs, string(NameSuper))
}
return strings.Join(strs, "-")
}
func (s State) String() string {
switch s {
case Press:
return "Press"
case Release:
return "Release"
default:
panic("invalid State")
}
}
+13
View File
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: Unlicense OR MIT
//go:build !darwin && !js
package key
// ModShortcut is the platform's shortcut modifier, usually the ctrl
// modifier. On Apple platforms it is the cmd key.
const ModShortcut = ModCtrl
// ModShortcutAlt is the platform's alternative shortcut modifier,
// usually the ctrl modifier. On Apple platforms it is the alt modifier.
const ModShortcutAlt = ModCtrl
+11
View File
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: Unlicense OR MIT
package key
// ModShortcut is the platform's shortcut modifier, usually the ctrl
// modifier. On Apple platforms it is the cmd key.
const ModShortcut = ModCommand
// ModShortcut is the platform's alternative shortcut modifier,
// usually the ctrl modifier. On Apple platforms it is the alt modifier.
const ModShortcutAlt = ModAlt
+38
View File
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: Unlicense OR MIT
package key
import (
"strings"
"syscall/js"
)
// ModShortcut is the platform's shortcut modifier, usually the ctrl
// modifier. On Apple platforms it is the cmd key.
var ModShortcut = ModCtrl
// ModShortcut is the platform's alternative shortcut modifier,
// usually the ctrl modifier. On Apple platforms it is the alt modifier.
var ModShortcutAlt = ModCtrl
func init() {
nav := js.Global().Get("navigator")
if !nav.Truthy() {
return // Almost impossible to happen
}
platform := ""
if p := nav.Get("platform"); p.Truthy() {
platform = p.String()
}
platform = strings.ToLower(platform)
// Based on https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform#examples
for _, darwinPlatform := range []string{"mac", "iphone", "ipad", "ipod"} {
if strings.HasPrefix(platform, darwinPlatform) {
ModShortcut = ModCommand
ModShortcutAlt = ModAlt
return
}
}
}
+118
View File
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: Unlicense OR MIT
/*
Package pointer implements pointer events and operations.
A pointer is either a mouse controlled cursor or a touch
object such as a finger.
The [event.Op] operation is used to declare a handler ready for pointer
events.
# Hit areas
Clip operations from package [op/clip] are used for specifying
hit areas where handlers may receive events.
For example, to set up a handler with a rectangular hit area:
r := image.Rectangle{...}
area := clip.Rect(r).Push(ops)
event.Op{Tag: h}.Add(ops)
area.Pop()
Note that hit areas behave similar to painting: the effective area of a stack
of multiple area operations is the intersection of the areas.
BUG: Clip operations other than clip.Rect and clip.Ellipse are approximated
with their bounding boxes.
# Matching events
Areas form an implicit tree, with input handlers as leaves. The children of
an area is every area and handler added between its Push and corresponding Pop.
For example:
ops := new(op.Ops)
var h1, h2 *Handler
area := clip.Rect(...).Push(ops)
event.Op(Ops, h1)
area.Pop()
area := clip.Rect(...).Push(ops)
event.Op(Ops, h2)
area.Pop()
implies a tree of two inner nodes, each with one pointer handler attached.
The matching proceeds as follows.
First, the foremost area that contains the event is found. Only areas whose
parent areas all contain the event is considered.
Then, every handler attached to the area is matched with the event.
If all attached handlers are marked pass-through or if no handlers are
attached, the matching repeats with the next foremost (sibling) area. Otherwise
the matching repeats with the parent area.
In the example above, all events will go to h2 because it and h1 are siblings
and none are pass-through.
# Pass-through
The PassOp operations controls the pass-through setting. All handlers added
inside one or more PassOp scopes are marked pass-through.
Pass-through is useful for overlay widgets. Consider a hidden side drawer: when
the user touches the side, both the (transparent) drawer handle and the
interface below should receive pointer events. This effect is achieved by
marking the drawer handle pass-through.
# Disambiguation
When more than one handler matches a pointer event, the event queue
follows a set of rules for distributing the event.
As long as the pointer has not received a Press event, all
matching handlers receive all events.
When a pointer is pressed, the set of matching handlers is
recorded. The set is not updated according to the pointer position
and hit areas. Rather, handlers stay in the matching set until they
no longer appear in a InputOp or when another handler in the set
grabs the pointer.
A handler can exclude all other handler from its matching sets
by setting the Grab flag in its InputOp. The Grab flag is sticky
and stays in effect until the handler no longer appears in any
matching sets.
The losing handlers are notified by a Cancel event.
For multiple grabbing handlers, the foremost handler wins.
# Priorities
Handlers know their position in a matching set of a pointer through
event priorities. The Shared priority is for matching sets with
multiple handlers; the Grabbed priority indicate exclusive access.
Priorities are useful for deferred gesture matching.
Consider a scrollable list of clickable elements. When the user touches an
element, it is unknown whether the gesture is a click on the element
or a drag (scroll) of the list. While the click handler might light up
the element in anticipation of a click, the scrolling handler does not
scroll on finger movements with lower than Grabbed priority.
Should the user release the finger, the click handler registers a click.
However, if the finger moves beyond a threshold, the scrolling handler
determines that the gesture is a drag and sets its Grab flag. The
click handler receives a Cancel (removing the highlight) and further
movements for the scroll handler has priority Grabbed, scrolling the
list.
*/
package pointer
+409
View File
@@ -0,0 +1,409 @@
// SPDX-License-Identifier: Unlicense OR MIT
package pointer
import (
"strings"
"time"
"gioui.org/f32"
"gioui.org/internal/ops"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/op"
)
// Event is a pointer event.
type Event struct {
Kind Kind
Source Source
// PointerID is the id for the pointer and can be used
// to track a particular pointer from Press to
// Release. Populated for Press, Release, Move, Drag,
// Enter, Leave, and Cancel; Scroll events are not
// bound to a tracked pointer and leave it zero.
PointerID ID
// Priority is the priority of the receiving handler
// for this event.
Priority Priority
// Time is when the event was received. The
// timestamp is relative to an undefined base.
Time time.Duration
// Buttons are the set of pressed mouse buttons for this event.
Buttons Buttons
// Position is the coordinates of the event in the local coordinate
// system of the receiving tag. The transformation from global window
// coordinates to local coordinates is performed by the inverse of
// the effective transformation of the tag.
Position f32.Point
// Scroll is the scroll amount, if any.
Scroll f32.Point
// Modifiers is the set of active modifiers when
// the mouse button was pressed.
Modifiers key.Modifiers
}
// PassOp sets the pass-through mode. InputOps added while the pass-through
// mode is set don't block events to siblings.
type PassOp struct{}
// PassStack represents a PassOp on the pass stack.
type PassStack struct {
ops *ops.Ops
id ops.StackID
macroID uint32
}
// Filter matches every [Event] that target the Tag and whose kind is
// included in Kinds. Note that only tags specified in [event.Op] can
// be targeted by pointer events.
type Filter struct {
Target event.Tag
// Kinds is a bitwise-or of event types to match.
Kinds Kind
// ScrollX and ScrollY constrain the range of scrolling events delivered
// to Target. Specifically, any Event e delivered to Tag will satisfy
//
// ScrollX.Min <= e.Scroll.X <= ScrollX.Max (horizontal axis)
// ScrollY.Min <= e.Scroll.Y <= ScrollY.Max (vertical axis)
ScrollX ScrollRange
ScrollY ScrollRange
}
// ScrollRange describes the range of scrolling distances in an
// axis.
type ScrollRange struct {
Min, Max int
}
// GrabCmd requests a pointer grab on the pointer identified by ID.
type GrabCmd struct {
Tag event.Tag
ID ID
}
type ID uint16
// Kind of an Event.
type Kind uint
// Priority of an Event.
type Priority uint8
// Source of an Event.
type Source uint8
// Buttons is a set of mouse buttons
type Buttons uint8
// Cursor denotes a pre-defined cursor shape. Its Add method adds an
// operation that sets the cursor shape for the current clip area.
type Cursor byte
// The cursors correspond to CSS pointer naming.
const (
// CursorDefault is the default cursor.
CursorDefault Cursor = iota
// CursorNone hides the cursor. To show it again, use any other cursor.
CursorNone
// CursorText is for selecting and inserting text.
CursorText
// CursorVerticalText is for selecting and inserting vertical text.
CursorVerticalText
// CursorPointer is for a link.
// Usually displayed as a pointing hand.
CursorPointer
// CursorCrosshair is for a precise location.
CursorCrosshair
// CursorAllScroll is for indicating scrolling in all directions.
// Usually displayed as arrows to all four directions.
CursorAllScroll
// CursorColResize is for vertical resize.
// Usually displayed as a vertical bar with arrows pointing east and west.
CursorColResize
// CursorRowResize is for horizontal resize.
// Usually displayed as a horizontal bar with arrows pointing north and south.
CursorRowResize
// CursorGrab is for content that can be grabbed (dragged to be moved).
// Usually displayed as an open hand.
CursorGrab
// CursorGrabbing is for content that is being grabbed (dragged to be moved).
// Usually displayed as a closed hand.
CursorGrabbing
// CursorNotAllowed is shown when the request action cannot be carried out.
// Usually displayed as a circle with a line through.
CursorNotAllowed
// CursorWait is shown when the program is busy and user cannot interact.
// Usually displayed as a hourglass or the system equivalent.
CursorWait
// CursorProgress is shown when the program is busy, but the user can still interact.
// Usually displayed as a default cursor with a hourglass.
CursorProgress
// CursorNorthWestResize is for top-left corner resizing.
// Usually displayed as an arrow towards north-west.
CursorNorthWestResize
// CursorNorthEastResize is for top-right corner resizing.
// Usually displayed as an arrow towards north-east.
CursorNorthEastResize
// CursorSouthWestResize is for bottom-left corner resizing.
// Usually displayed as an arrow towards south-west.
CursorSouthWestResize
// CursorSouthEastResize is for bottom-right corner resizing.
// Usually displayed as an arrow towards south-east.
CursorSouthEastResize
// CursorNorthSouth is for top-bottom resizing.
// Usually displayed as a bi-directional arrow towards north-south.
CursorNorthSouthResize
// CursorEastWestResize is for left-right resizing.
// Usually displayed as a bi-directional arrow towards east-west.
CursorEastWestResize
// CursorWestResize is for left resizing.
// Usually displayed as an arrow towards west.
CursorWestResize
// CursorEastResize is for right resizing.
// Usually displayed as an arrow towards east.
CursorEastResize
// CursorNorthResize is for top resizing.
// Usually displayed as an arrow towards north.
CursorNorthResize
// CursorSouthResize is for bottom resizing.
// Usually displayed as an arrow towards south.
CursorSouthResize
// CursorNorthEastSouthWestResize is for top-right to bottom-left diagonal resizing.
// Usually displayed as a double ended arrow on the corresponding diagonal.
CursorNorthEastSouthWestResize
// CursorNorthWestSouthEastResize is for top-left to bottom-right diagonal resizing.
// Usually displayed as a double ended arrow on the corresponding diagonal.
CursorNorthWestSouthEastResize
)
const (
// A Cancel event is generated when the current gesture is
// interrupted by other handlers or the system.
Cancel Kind = 1 << iota
// Press of a pointer.
Press
// Release of a pointer.
Release
// Move of a pointer.
Move
// Drag of a pointer.
Drag
// Pointer enters an area watching for pointer input
Enter
// Pointer leaves an area watching for pointer input
Leave
// Scroll of a pointer.
Scroll
)
const (
// Mouse generated event.
Mouse Source = iota
// Touch generated event.
Touch
)
const (
// Shared priority is for handlers that
// are part of a matching set larger than 1.
Shared Priority = iota
// Grabbed is used for matching sets of size 1.
Grabbed
)
const (
// ButtonPrimary is the primary button, usually the left button for a
// right-handed user.
ButtonPrimary Buttons = 1 << iota
// ButtonSecondary is the secondary button, usually the right button for a
// right-handed user.
ButtonSecondary
// ButtonTertiary is the tertiary button, usually the middle button.
ButtonTertiary
// ButtonQuaternary is the fourth button, usually used for browser
// navigation (backward)
ButtonQuaternary
// ButtonQuinary is the fifth button, usually used for browser
// navigation (forward)
ButtonQuinary
)
func (s ScrollRange) Union(s2 ScrollRange) ScrollRange {
return ScrollRange{
Min: min(s.Min, s2.Min),
Max: max(s.Max, s2.Max),
}
}
// Push the current pass mode to the pass stack and set the pass mode.
func (p PassOp) Push(o *op.Ops) PassStack {
id, mid := ops.PushOp(&o.Internal, ops.PassStack)
data := ops.Write(&o.Internal, ops.TypePassLen)
data[0] = byte(ops.TypePass)
return PassStack{ops: &o.Internal, id: id, macroID: mid}
}
func (p PassStack) Pop() {
ops.PopOp(p.ops, ops.PassStack, p.id, p.macroID)
data := ops.Write(p.ops, ops.TypePopPassLen)
data[0] = byte(ops.TypePopPass)
}
func (op Cursor) Add(o *op.Ops) {
data := ops.Write(&o.Internal, ops.TypeCursorLen)
data[0] = byte(ops.TypeCursor)
data[1] = byte(op)
}
func (t Kind) String() string {
if t == Cancel {
return "Cancel"
}
var buf strings.Builder
for tt := Kind(1); tt > 0; tt <<= 1 {
if t&tt > 0 {
if buf.Len() > 0 {
buf.WriteByte('|')
}
buf.WriteString((t & tt).string())
}
}
return buf.String()
}
func (t Kind) string() string {
switch t {
case Press:
return "Press"
case Release:
return "Release"
case Cancel:
return "Cancel"
case Move:
return "Move"
case Drag:
return "Drag"
case Enter:
return "Enter"
case Leave:
return "Leave"
case Scroll:
return "Scroll"
default:
panic("unknown Type")
}
}
func (p Priority) String() string {
switch p {
case Shared:
return "Shared"
case Grabbed:
return "Grabbed"
default:
panic("unknown priority")
}
}
func (s Source) String() string {
switch s {
case Mouse:
return "Mouse"
case Touch:
return "Touch"
default:
panic("unknown source")
}
}
// Contain reports whether the set b contains
// all of the buttons.
func (b Buttons) Contain(buttons Buttons) bool {
return b&buttons == buttons
}
func (b Buttons) String() string {
var strs []string
if b.Contain(ButtonPrimary) {
strs = append(strs, "ButtonPrimary")
}
if b.Contain(ButtonSecondary) {
strs = append(strs, "ButtonSecondary")
}
if b.Contain(ButtonTertiary) {
strs = append(strs, "ButtonTertiary")
}
if b.Contain(ButtonQuaternary) {
strs = append(strs, "ButtonQuaternary")
}
if b.Contain(ButtonQuinary) {
strs = append(strs, "ButtonQuinary")
}
return strings.Join(strs, "|")
}
func (c Cursor) String() string {
switch c {
case CursorDefault:
return "Default"
case CursorNone:
return "None"
case CursorText:
return "Text"
case CursorVerticalText:
return "VerticalText"
case CursorPointer:
return "Pointer"
case CursorCrosshair:
return "Crosshair"
case CursorAllScroll:
return "AllScroll"
case CursorColResize:
return "ColResize"
case CursorRowResize:
return "RowResize"
case CursorGrab:
return "Grab"
case CursorGrabbing:
return "Grabbing"
case CursorNotAllowed:
return "NotAllowed"
case CursorWait:
return "Wait"
case CursorProgress:
return "Progress"
case CursorNorthWestResize:
return "NorthWestResize"
case CursorNorthEastResize:
return "NorthEastResize"
case CursorSouthWestResize:
return "SouthWestResize"
case CursorSouthEastResize:
return "SouthEastResize"
case CursorNorthSouthResize:
return "NorthSouthResize"
case CursorEastWestResize:
return "EastWestResize"
case CursorWestResize:
return "WestResize"
case CursorEastResize:
return "EastResize"
case CursorNorthResize:
return "NorthResize"
case CursorSouthResize:
return "SouthResize"
case CursorNorthEastSouthWestResize:
return "NorthEastSouthWestResize"
case CursorNorthWestSouthEastResize:
return "NorthWestSouthEastResize"
default:
panic("unknown Type")
}
}
func (Event) ImplementsEvent() {}
func (GrabCmd) ImplementsCommand() {}
func (Filter) ImplementsFilter() {}
+91
View File
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: Unlicense OR MIT
// Package semantic provides operations for semantic descriptions of a user
// interface, to facilitate presentation and interaction in external software
// such as screen readers.
//
// Semantic descriptions are organized in a tree, with clip operations as
// nodes. Operations in this package are associated with the current semantic
// node, that is the most recent pushed clip operation.
package semantic
import (
"gioui.org/internal/ops"
"gioui.org/op"
)
// LabelOp provides the content of a textual component.
type LabelOp string
// DescriptionOp describes a component.
type DescriptionOp string
// ClassOp provides the component class.
type ClassOp int
const (
Unknown ClassOp = iota
Button
CheckBox
Editor
RadioButton
Switch
)
// SelectedOp describes the selected state for components that have
// boolean state.
type SelectedOp bool
// EnabledOp describes the enabled state.
type EnabledOp bool
func (l LabelOp) Add(o *op.Ops) {
data := ops.Write1String(&o.Internal, ops.TypeSemanticLabelLen, string(l))
data[0] = byte(ops.TypeSemanticLabel)
}
func (d DescriptionOp) Add(o *op.Ops) {
data := ops.Write1String(&o.Internal, ops.TypeSemanticDescLen, string(d))
data[0] = byte(ops.TypeSemanticDesc)
}
func (c ClassOp) Add(o *op.Ops) {
data := ops.Write(&o.Internal, ops.TypeSemanticClassLen)
data[0] = byte(ops.TypeSemanticClass)
data[1] = byte(c)
}
func (s SelectedOp) Add(o *op.Ops) {
data := ops.Write(&o.Internal, ops.TypeSemanticSelectedLen)
data[0] = byte(ops.TypeSemanticSelected)
if s {
data[1] = 1
}
}
func (e EnabledOp) Add(o *op.Ops) {
data := ops.Write(&o.Internal, ops.TypeSemanticEnabledLen)
data[0] = byte(ops.TypeSemanticEnabled)
if e {
data[1] = 1
}
}
func (c ClassOp) String() string {
switch c {
case Unknown:
return "Unknown"
case Button:
return "Button"
case CheckBox:
return "CheckBox"
case Editor:
return "Editor"
case RadioButton:
return "RadioButton"
case Switch:
return "Switch"
default:
panic("invalid ClassOp")
}
}
+77
View File
@@ -0,0 +1,77 @@
package system
import (
"strings"
"gioui.org/internal/ops"
"gioui.org/op"
)
// ActionAreaOp makes the current clip area available for
// system gestures.
//
// Note: only ActionMove is supported.
type ActionInputOp Action
// Action is a set of window decoration actions.
type Action uint
const (
// ActionMinimize minimizes a window.
ActionMinimize Action = 1 << iota
// ActionMaximize maximizes a window.
ActionMaximize
// ActionUnmaximize restores a maximized window.
ActionUnmaximize
// ActionFullscreen makes a window fullscreen.
ActionFullscreen
// ActionRaise requests that the platform bring this window to the top of all open windows.
// Some platforms do not allow this except under certain circumstances, such as when
// a window from the same application already has focus. If the platform does not
// support it, this method will do nothing.
ActionRaise
// ActionCenter centers the window on the screen.
// It is ignored in Fullscreen mode and on Wayland.
ActionCenter
// ActionClose closes a window.
// Only applicable on macOS, Windows, X11 and Wayland.
ActionClose
// ActionMove moves a window directed by the user.
ActionMove
)
func (op ActionInputOp) Add(o *op.Ops) {
data := ops.Write(&o.Internal, ops.TypeActionInputLen)
data[0] = byte(ops.TypeActionInput)
data[1] = byte(op)
}
func (a Action) String() string {
var buf strings.Builder
for b := Action(1); a != 0; b <<= 1 {
if a&b != 0 {
if buf.Len() > 0 {
buf.WriteByte('|')
}
buf.WriteString(b.string())
a &^= b
}
}
return buf.String()
}
func (a Action) string() string {
switch a {
case ActionMinimize:
return "ActionMinimize"
case ActionMaximize:
return "ActionMaximize"
case ActionUnmaximize:
return "ActionUnmaximize"
case ActionClose:
return "ActionClose"
case ActionMove:
return "ActionMove"
}
return ""
}
+68
View File
@@ -0,0 +1,68 @@
package system
// Locale provides language information for the current system.
type Locale struct {
// Language is the BCP-47 tag for the primary language of the system.
Language string
// Direction indicates the primary direction of text and layout
// flow for the system.
Direction TextDirection
}
const (
axisShift = iota
progressionShift
)
// TextDirection defines a direction for text flow.
type TextDirection byte
const (
// LTR is left-to-right text.
LTR TextDirection = TextDirection(Horizontal<<axisShift) | TextDirection(FromOrigin<<progressionShift)
// RTL is right-to-left text.
RTL TextDirection = TextDirection(Horizontal<<axisShift) | TextDirection(TowardOrigin<<progressionShift)
)
// Axis returns the axis of the text layout.
func (d TextDirection) Axis() TextAxis {
return TextAxis((d & (1 << axisShift)) >> axisShift)
}
// Progression returns the way that the text flows relative to the origin.
func (d TextDirection) Progression() TextProgression {
return TextProgression((d & (1 << progressionShift)) >> progressionShift)
}
func (d TextDirection) String() string {
switch d {
case RTL:
return "RTL"
default:
return "LTR"
}
}
// TextAxis defines the layout axis of text.
type TextAxis byte
const (
// Horizontal indicates text that flows along the X axis.
Horizontal TextAxis = iota
// Vertical indicates text that flows along the Y axis.
Vertical
)
// TextProgression indicates how text flows along an axis relative to the
// origin. For these purposes, the origin is defined as the upper-left
// corner of coordinate space.
type TextProgression byte
const (
// FromOrigin indicates text that flows along its axis away from the
// origin (upper left corner).
FromOrigin TextProgression = iota
// TowardOrigin indicates text that flows along its axis towards the
// origin (upper left corner).
TowardOrigin
)
+95
View File
@@ -0,0 +1,95 @@
// Package transfer contains operations and events for brokering data transfers.
//
// The transfer protocol is as follows:
//
// - Data sources use [SourceFilter] to receive an [InitiateEvent] when a drag
// is initiated, and an [RequestEvent] for each initiation of a data transfer.
// Sources respond to requests with [OfferCmd].
// - Data targets use [TargetFilter] to receive an [DataEvent] for receiving data.
// The target must close the data event after use.
//
// When a user initiates a pointer-guided drag and drop transfer, the
// source as well as all potential targets receive an InitiateEvent.
// Potential targets are targets with at least one MIME type in common
// with the source. When a drag gesture completes, a CancelEvent is sent
// to the source and all potential targets.
//
// Note that the RequestEvent is sent to the source upon drop.
package transfer
import (
"io"
"gioui.org/io/event"
)
// OfferCmd is used by data sources as a response to a RequestEvent.
type OfferCmd struct {
Tag event.Tag
// Type is the MIME type of Data.
// It must be the Type from the corresponding RequestEvent.
Type string
// Data contains the offered data. It is closed when the
// transfer is complete or cancelled.
// Data must be kept valid until closed, and it may be used from
// a goroutine separate from the one processing the frame.
Data io.ReadCloser
}
func (OfferCmd) ImplementsCommand() {}
// SourceFilter filters for any [RequestEvent] that match a MIME type
// as well as [InitiateEvent] and [CancelEvent].
// Use multiple filters to offer multiple types.
type SourceFilter struct {
// Target is a tag included in a previous event.Op.
Target event.Tag
// Type is the MIME type supported by this source.
Type string
}
// TargetFilter filters for any [DataEvent] whose type matches a MIME type
// as well as [CancelEvent]. Use multiple filters to accept multiple types.
type TargetFilter struct {
// Target is a tag included in a previous event.Op.
Target event.Tag
// Type is the MIME type accepted by this target.
Type string
}
// RequestEvent requests data from a data source. The source must
// respond with an OfferCmd.
type RequestEvent struct {
// Type is the first matched type between the source and the target.
Type string
}
func (RequestEvent) ImplementsEvent() {}
// InitiateEvent is sent to a data source when a drag-and-drop
// transfer gesture is initiated.
//
// Potential data targets also receive the event.
type InitiateEvent struct{}
func (InitiateEvent) ImplementsEvent() {}
// CancelEvent is sent to data sources and targets to cancel the
// effects of an InitiateEvent.
type CancelEvent struct{}
func (CancelEvent) ImplementsEvent() {}
// DataEvent is sent to the target receiving the transfer.
type DataEvent struct {
// Type is the MIME type of Data.
Type string
// Open returns the transfer data. It is only valid to call Open in the frame
// the DataEvent is received. The caller must close the return value after use.
Open func() io.ReadCloser
}
func (DataEvent) ImplementsEvent() {}
func (SourceFilter) ImplementsFilter() {}
func (TargetFilter) ImplementsFilter() {}