forked from joejulian/gio
io/router,app: add support for directional focus moves
Implement support for up/down/right/left directional focus moves and map Android directional pad keys to focus moves. Fixes: https://todo.sr.ht/~eliasnaur/gio/195 References: https://github.com/tailscale/tailscale/issues/1611 Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+12
-8
@@ -897,14 +897,6 @@ func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) {
|
||||
func convertKeyCode(code C.jint) (string, bool) {
|
||||
var n string
|
||||
switch code {
|
||||
case C.AKEYCODE_DPAD_UP:
|
||||
n = key.NameUpArrow
|
||||
case C.AKEYCODE_DPAD_DOWN:
|
||||
n = key.NameDownArrow
|
||||
case C.AKEYCODE_DPAD_LEFT:
|
||||
n = key.NameLeftArrow
|
||||
case C.AKEYCODE_DPAD_RIGHT:
|
||||
n = key.NameRightArrow
|
||||
case C.AKEYCODE_FORWARD_DEL:
|
||||
n = key.NameDeleteForward
|
||||
case C.AKEYCODE_DEL:
|
||||
@@ -930,6 +922,18 @@ func convertKeyCode(code C.jint) (string, bool) {
|
||||
//export Java_org_gioui_GioView_onKeyEvent
|
||||
func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.jlong, keyCode, r C.jint, pressed C.jboolean, t C.jlong) {
|
||||
w := views[handle]
|
||||
if pressed == C.JNI_TRUE {
|
||||
switch keyCode {
|
||||
case C.AKEYCODE_DPAD_UP:
|
||||
w.callbacks.MoveFocus(router.FocusUp)
|
||||
case C.AKEYCODE_DPAD_DOWN:
|
||||
w.callbacks.MoveFocus(router.FocusDown)
|
||||
case C.AKEYCODE_DPAD_LEFT:
|
||||
w.callbacks.MoveFocus(router.FocusLeft)
|
||||
case C.AKEYCODE_DPAD_RIGHT:
|
||||
w.callbacks.MoveFocus(router.FocusRight)
|
||||
}
|
||||
}
|
||||
if n, ok := convertKeyCode(keyCode); ok {
|
||||
state := key.Release
|
||||
if pressed == C.JNI_TRUE {
|
||||
|
||||
@@ -504,6 +504,12 @@ func (c *callbacks) SetEditorSnippet(r key.Range) {
|
||||
c.Event(key.SnippetEvent(r))
|
||||
}
|
||||
|
||||
func (c *callbacks) MoveFocus(dir router.FocusDirection) {
|
||||
c.w.queue.q.MoveFocus(dir)
|
||||
c.w.setNextFrame(time.Time{})
|
||||
c.w.updateAnimation(c.d)
|
||||
}
|
||||
|
||||
func (e *editorState) Replace(r key.Range, text string) {
|
||||
if r.Start > r.End {
|
||||
r.Start, r.End = r.End, r.Start
|
||||
|
||||
+128
-7
@@ -3,6 +3,9 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
@@ -23,6 +26,7 @@ type TextInputState uint8
|
||||
type keyQueue struct {
|
||||
focus event.Tag
|
||||
order []event.Tag
|
||||
dirOrder []dirFocusEntry
|
||||
handlers map[event.Tag]*keyHandler
|
||||
state TextInputState
|
||||
hint key.InputHint
|
||||
@@ -32,10 +36,11 @@ type keyQueue struct {
|
||||
type keyHandler struct {
|
||||
// visible will be true if the InputOp is present
|
||||
// in the current frame.
|
||||
visible bool
|
||||
new bool
|
||||
hint key.InputHint
|
||||
order int
|
||||
visible bool
|
||||
new bool
|
||||
hint key.InputHint
|
||||
order int
|
||||
dirOrder int
|
||||
}
|
||||
|
||||
// keyCollector tracks state required to update a keyQueue
|
||||
@@ -46,12 +51,27 @@ type keyCollector struct {
|
||||
changed bool
|
||||
}
|
||||
|
||||
type dirFocusEntry struct {
|
||||
tag event.Tag
|
||||
row int
|
||||
bounds f32.Rectangle
|
||||
}
|
||||
|
||||
const (
|
||||
TextInputKeep TextInputState = iota
|
||||
TextInputClose
|
||||
TextInputOpen
|
||||
)
|
||||
|
||||
type FocusDirection int
|
||||
|
||||
const (
|
||||
FocusRight FocusDirection = iota
|
||||
FocusLeft
|
||||
FocusUp
|
||||
FocusDown
|
||||
)
|
||||
|
||||
// InputState returns the last text input state as
|
||||
// determined in Frame.
|
||||
func (q *keyQueue) InputState() TextInputState {
|
||||
@@ -83,6 +103,7 @@ func (q *keyQueue) Reset() {
|
||||
h.order = -1
|
||||
}
|
||||
q.order = q.order[:0]
|
||||
q.dirOrder = q.dirOrder[:0]
|
||||
}
|
||||
|
||||
func (q *keyQueue) Frame(events *handlerEvents, collector keyCollector) {
|
||||
@@ -103,6 +124,105 @@ func (q *keyQueue) Frame(events *handlerEvents, collector keyCollector) {
|
||||
if changed {
|
||||
q.setFocus(focus, events)
|
||||
}
|
||||
q.updateFocusLayout()
|
||||
}
|
||||
|
||||
// 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() {
|
||||
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) * .5
|
||||
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 {
|
||||
q.handlers[o.tag].dirOrder = i
|
||||
}
|
||||
}
|
||||
|
||||
func (q *keyQueue) MoveFocus(dir FocusDirection, events *handlerEvents) {
|
||||
order := 0
|
||||
if q.focus != nil {
|
||||
order = q.handlers[q.focus].dirOrder
|
||||
}
|
||||
focus := q.dirOrder[order]
|
||||
switch dir {
|
||||
case FocusRight, FocusLeft:
|
||||
next := order
|
||||
if q.focus != nil {
|
||||
next = order + 1
|
||||
if dir == FocusLeft {
|
||||
next = order - 1
|
||||
}
|
||||
}
|
||||
if 0 <= next && next < len(q.dirOrder) {
|
||||
newFocus := q.dirOrder[next]
|
||||
if newFocus.row == focus.row {
|
||||
q.setFocus(newFocus.tag, events)
|
||||
}
|
||||
}
|
||||
case FocusUp, FocusDown:
|
||||
delta := +1
|
||||
if dir == FocusUp {
|
||||
delta = -1
|
||||
}
|
||||
nextRow := 0
|
||||
if q.focus != nil {
|
||||
nextRow = focus.row + delta
|
||||
}
|
||||
var closest event.Tag
|
||||
dist := float32(math.Inf(+1))
|
||||
center := (focus.bounds.Min.X + focus.bounds.Max.X) * .5
|
||||
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) * .5
|
||||
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 {
|
||||
q.setFocus(closest, events)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (q *keyQueue) Push(e event.Event, events *handlerEvents) {
|
||||
@@ -168,7 +288,7 @@ func (k *keyCollector) softKeyboard(show bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func (k *keyCollector) handlerFor(tag event.Tag) *keyHandler {
|
||||
func (k *keyCollector) handlerFor(tag event.Tag, bounds f32.Rectangle) *keyHandler {
|
||||
h, ok := k.q.handlers[tag]
|
||||
if !ok {
|
||||
h = &keyHandler{new: true, order: -1}
|
||||
@@ -177,12 +297,13 @@ func (k *keyCollector) handlerFor(tag event.Tag) *keyHandler {
|
||||
if h.order == -1 {
|
||||
h.order = len(k.q.order)
|
||||
k.q.order = append(k.q.order, tag)
|
||||
k.q.dirOrder = append(k.q.dirOrder, dirFocusEntry{tag: tag, bounds: bounds})
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (k *keyCollector) inputOp(op key.InputOp) {
|
||||
h := k.handlerFor(op.Tag)
|
||||
func (k *keyCollector) inputOp(op key.InputOp, bounds f32.Rectangle) {
|
||||
h := k.handlerFor(op.Tag, bounds)
|
||||
h.visible = true
|
||||
h.hint = op.Hint
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"image"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
)
|
||||
|
||||
func TestKeyWakeup(t *testing.T) {
|
||||
@@ -246,6 +248,41 @@ func TestTabFocus(t *testing.T) {
|
||||
assertFocus(t, r, &handlers[2])
|
||||
}
|
||||
|
||||
func TestDirectionalFocus(t *testing.T) {
|
||||
ops := new(op.Ops)
|
||||
r := new(Router)
|
||||
handlers := []image.Rectangle{
|
||||
image.Rect(10, 10, 50, 50),
|
||||
image.Rect(50, 20, 100, 80),
|
||||
image.Rect(20, 26, 60, 80),
|
||||
image.Rect(10, 60, 50, 100),
|
||||
}
|
||||
|
||||
for i, bounds := range handlers {
|
||||
cl := clip.Rect(bounds).Push(ops)
|
||||
key.InputOp{Tag: &handlers[i]}.Add(ops)
|
||||
cl.Pop()
|
||||
}
|
||||
r.Frame(ops)
|
||||
|
||||
r.MoveFocus(FocusLeft)
|
||||
assertFocus(t, r, &handlers[0])
|
||||
r.MoveFocus(FocusLeft)
|
||||
assertFocus(t, r, &handlers[0])
|
||||
r.MoveFocus(FocusRight)
|
||||
assertFocus(t, r, &handlers[1])
|
||||
r.MoveFocus(FocusRight)
|
||||
assertFocus(t, r, &handlers[1])
|
||||
r.MoveFocus(FocusDown)
|
||||
assertFocus(t, r, &handlers[2])
|
||||
r.MoveFocus(FocusDown)
|
||||
assertFocus(t, r, &handlers[2])
|
||||
r.MoveFocus(FocusLeft)
|
||||
assertFocus(t, r, &handlers[3])
|
||||
r.MoveFocus(FocusUp)
|
||||
assertFocus(t, r, &handlers[0])
|
||||
}
|
||||
|
||||
func assertKeyEvent(t *testing.T, events []event.Event, expected bool, expectedInputs ...event.Event) {
|
||||
t.Helper()
|
||||
var evtFocus int
|
||||
|
||||
+16
-4
@@ -222,6 +222,14 @@ func (c *pointerCollector) currentArea() int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func (c *pointerCollector) currentAreaBounds() f32.Rectangle {
|
||||
a := c.currentArea()
|
||||
if a == -1 {
|
||||
panic("no root area")
|
||||
}
|
||||
return c.q.areas[a].bounds()
|
||||
}
|
||||
|
||||
func (c *pointerCollector) addHitNode(n hitNode) {
|
||||
n.next = c.state.nodePlusOne - 1
|
||||
c.q.hitTree = append(c.q.hitTree, n)
|
||||
@@ -382,10 +390,7 @@ func (q *pointerQueue) appendSemanticChildren(nodes []SemanticNode, areaIdx int)
|
||||
nodes = append(nodes, SemanticNode{
|
||||
ID: semID,
|
||||
Desc: SemanticDesc{
|
||||
Bounds: f32.Rectangle{
|
||||
Min: a.trans.Transform(a.area.rect.Min),
|
||||
Max: a.trans.Transform(a.area.rect.Max),
|
||||
},
|
||||
Bounds: a.bounds(),
|
||||
Label: cnt.label,
|
||||
Description: cnt.desc,
|
||||
Class: cnt.class,
|
||||
@@ -869,6 +874,13 @@ func (op *areaOp) Hit(pos f32.Point) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *areaNode) bounds() f32.Rectangle {
|
||||
return f32.Rectangle{
|
||||
Min: a.trans.Transform(a.area.rect.Min),
|
||||
Max: a.trans.Transform(a.area.rect.Max),
|
||||
}
|
||||
}
|
||||
|
||||
func setScrollEvent(scroll float32, min, max int) (left, scrolled float32) {
|
||||
if v := float32(max); scroll > v {
|
||||
return scroll - v, v
|
||||
|
||||
+6
-1
@@ -148,6 +148,10 @@ func (q *Router) Queue(events ...event.Event) bool {
|
||||
return q.handlers.HadEvents()
|
||||
}
|
||||
|
||||
func (q *Router) MoveFocus(dir FocusDirection) {
|
||||
q.key.queue.MoveFocus(dir, &q.handlers)
|
||||
}
|
||||
|
||||
// TextInputState returns the input state from the most recent
|
||||
// call to Frame.
|
||||
func (q *Router) TextInputState() TextInputState {
|
||||
@@ -316,7 +320,8 @@ func (q *Router) collect() {
|
||||
Tag: encOp.Refs[0].(event.Tag),
|
||||
Hint: key.InputHint(encOp.Data[1]),
|
||||
}
|
||||
kc.inputOp(op)
|
||||
b := pc.currentAreaBounds()
|
||||
kc.inputOp(op, b)
|
||||
case ops.TypeSnippet:
|
||||
op := key.SnippetOp{
|
||||
Tag: encOp.Refs[0].(event.Tag),
|
||||
|
||||
Reference in New Issue
Block a user