mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
496fc3cc82
If the client asks for the focus to be set to a tag, allow it. There is a check at the end of Router.Frame that clears the focus if the tag turns out to fail the requirements (visible and has asked for FocusEvents). The change simplifies the logic for determining whether a command can be executed immediately. Signed-off-by: Elias Naur <mail@eliasnaur.com>
376 lines
8.3 KiB
Go
376 lines
8.3 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 struct {
|
|
focusable bool
|
|
filters []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.key.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(e event.Event) bool {
|
|
switch e := e.(type) {
|
|
case key.Event:
|
|
for _, f := range k.filters {
|
|
if keyFilterMatch(f, e) {
|
|
return true
|
|
}
|
|
}
|
|
case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent:
|
|
return k.focusable
|
|
}
|
|
return false
|
|
}
|
|
|
|
func keyFilterMatch(f key.Filter, e key.Event) bool {
|
|
if 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 event.Filter) {
|
|
switch f := f.(type) {
|
|
case key.FocusFilter:
|
|
k.focusable = true
|
|
case key.Filter:
|
|
for _, f2 := range k.filters {
|
|
if f == f2 {
|
|
return
|
|
}
|
|
}
|
|
k.filters = append(k.filters, f)
|
|
}
|
|
}
|
|
|
|
func (k *keyFilter) Merge(k2 keyFilter) {
|
|
k.focusable = k.focusable || k2.focusable
|
|
k.filters = append(k.filters, k2.filters...)
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|