forked from joejulian/gio
c3f2abebca
The empty key.Filter.Name now means matching every key name. This is a replacement for the previous special case where the top-level key.InputOp handler would get all unmatched events. Add special case for system events such as focus switch shortcuts. Signed-off-by: Elias Naur <mail@eliasnaur.com>
365 lines
8.1 KiB
Go
365 lines
8.1 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package input
|
|
|
|
import (
|
|
"image"
|
|
"sort"
|
|
|
|
"gioui.org/f32"
|
|
"gioui.org/io/event"
|
|
"gioui.org/io/key"
|
|
)
|
|
|
|
// EditorState represents the state of an editor needed by input handlers.
|
|
type EditorState struct {
|
|
Selection struct {
|
|
Transform f32.Affine2D
|
|
key.Range
|
|
key.Caret
|
|
}
|
|
Snippet key.Snippet
|
|
}
|
|
|
|
type TextInputState uint8
|
|
|
|
type keyQueue struct {
|
|
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{}
|
|
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) {
|
|
for _, f2 := range *k {
|
|
if f == f2 {
|
|
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")
|
|
}
|
|
}
|