app,widget: implement Editor IME support, add Android implementation

Fixes: https://todo.sr.ht/~eliasnaur/gio/116
References: https://todo.sr.ht/~eliasnaur/gio/246
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2022-01-13 13:04:31 +01:00
parent c22138f5f8
commit 58cdb3e1da
16 changed files with 880 additions and 55 deletions
+59 -5
View File
@@ -10,6 +10,7 @@ events.
package key
import (
"encoding/binary"
"fmt"
"strings"
@@ -40,6 +41,39 @@ type FocusOp struct {
Tag event.Tag
}
// SelectionOp updates the selection for an input handler.
type SelectionOp struct {
Tag event.Tag
Range
}
// SnippetOp updates the content snippet for an input handler.
type SnippetOp 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. Offset and Length are in runes.
type Snippet struct {
Range
Text string
}
// SelectionEvent is generated when an input method changes the selection.
type SelectionEvent 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 {
@@ -60,9 +94,11 @@ type Event struct {
State State
}
// An EditEvent is generated when text is input.
// An EditEvent requests an edit by an input method.
type EditEvent struct {
Text string
// Range specifies the range to replace with Text.
Range Range
Text string
}
// InputHint changes the on-screen-keyboard type. That hints the
@@ -179,9 +215,27 @@ func (h FocusOp) Add(o *op.Ops) {
data[0] = byte(ops.TypeKeyFocus)
}
func (EditEvent) ImplementsEvent() {}
func (Event) ImplementsEvent() {}
func (FocusEvent) ImplementsEvent() {}
func (s SnippetOp) Add(o *op.Ops) {
data := ops.Write2(&o.Internal, ops.TypeSnippetLen, s.Tag, &s.Text)
data[0] = byte(ops.TypeSnippet)
bo := binary.LittleEndian
bo.PutUint32(data[1:], uint32(s.Range.Start))
bo.PutUint32(data[5:], uint32(s.Range.End))
}
func (s SelectionOp) Add(o *op.Ops) {
data := ops.Write1(&o.Internal, ops.TypeSelectionLen, s.Tag)
data[0] = byte(ops.TypeSelection)
bo := binary.LittleEndian
bo.PutUint32(data[1:], uint32(s.Start))
bo.PutUint32(data[5:], uint32(s.End))
}
func (EditEvent) ImplementsEvent() {}
func (Event) ImplementsEvent() {}
func (FocusEvent) ImplementsEvent() {}
func (SnippetEvent) ImplementsEvent() {}
func (SelectionEvent) ImplementsEvent() {}
func (e Event) String() string {
return fmt.Sprintf("%v %v %v}", e.Name, e.Modifiers, e.State)
+31 -5
View File
@@ -7,6 +7,12 @@ import (
"gioui.org/io/key"
)
// EditorState represents the state of an editor needed by input handlers.
type EditorState struct {
Selection key.Range
Snippet key.Snippet
}
type TextInputState uint8
type keyQueue struct {
@@ -14,6 +20,7 @@ type keyQueue struct {
handlers map[event.Tag]*keyHandler
state TextInputState
hint key.InputHint
content EditorState
}
type keyHandler struct {
@@ -88,6 +95,7 @@ func (q *keyQueue) Frame(events *handlerEvents, collector keyCollector) {
}
}
if collector.changed && collector.focus != q.focus {
q.content = EditorState{}
if q.focus != nil {
events.Add(q.focus, key.FocusEvent{Focus: false})
}
@@ -119,16 +127,34 @@ func (k *keyCollector) softKeyboard(show bool) {
}
}
func (k *keyCollector) inputOp(op key.InputOp) {
h, ok := k.q.handlers[op.Tag]
if !ok {
h = &keyHandler{new: true}
k.q.handlers[op.Tag] = h
func (k *keyCollector) handlerFor(tag event.Tag) *keyHandler {
h, ok := k.q.handlers[tag]
if ok {
return h
}
h = &keyHandler{new: true}
k.q.handlers[tag] = h
return h
}
func (k *keyCollector) inputOp(op key.InputOp) {
h := k.handlerFor(op.Tag)
h.visible = true
h.hint = op.Hint
}
func (k *keyCollector) selectionOp(op key.SelectionOp) {
if op.Tag == k.q.focus {
k.q.content.Selection = op.Range
}
}
func (k *keyCollector) snippetOp(op key.SnippetOp) {
if op.Tag == k.q.focus {
k.q.content.Snippet = op.Snippet
}
}
func (t TextInputState) String() string {
switch t {
case TextInputKeep:
+29 -2
View File
@@ -138,7 +138,7 @@ func (q *Router) Queue(events ...event.Event) bool {
q.profile = e
case pointer.Event:
q.pointer.queue.Push(e, &q.handlers)
case key.EditEvent, key.Event, key.FocusEvent:
case key.EditEvent, key.Event, key.FocusEvent, key.SnippetEvent, key.SelectionEvent:
q.key.queue.Push(e, &q.handlers)
case clipboard.Event:
q.cqueue.Push(e, &q.handlers)
@@ -188,6 +188,12 @@ func (q *Router) AppendSemantics(nodes []SemanticNode) []SemanticNode {
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.content
}
func (q *Router) collect() {
q.transStack = q.transStack[:0]
pc := &q.pointer.collector
@@ -197,6 +203,7 @@ func (q *Router) collect() {
*kc = keyCollector{q: &q.key.queue}
q.key.queue.Reset()
var t f32.Affine2D
bo := binary.LittleEndian
for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
switch ops.OpType(encOp.Data[0]) {
case ops.TypeInvalidate:
@@ -252,7 +259,6 @@ func (q *Router) collect() {
case ops.TypePopPass:
pc.popPass()
case ops.TypePointerInput:
bo := binary.LittleEndian
op := pointer.InputOp{
Tag: encOp.Refs[0].(event.Tag),
Grab: encOp.Data[1] != 0,
@@ -310,6 +316,27 @@ func (q *Router) collect() {
Hint: key.InputHint(encOp.Data[1]),
}
kc.inputOp(op)
case ops.TypeSnippet:
op := key.SnippetOp{
Tag: encOp.Refs[0].(event.Tag),
Snippet: key.Snippet{
Range: key.Range{
Start: int(int32(bo.Uint32(encOp.Data[1:]))),
End: int(int32(bo.Uint32(encOp.Data[5:]))),
},
Text: *(encOp.Refs[1].(*string)),
},
}
kc.snippetOp(op)
case ops.TypeSelection:
op := key.SelectionOp{
Tag: encOp.Refs[0].(event.Tag),
Range: key.Range{
Start: int(int32(bo.Uint32(encOp.Data[1:]))),
End: int(int32(bo.Uint32(encOp.Data[5:]))),
},
}
kc.selectionOp(op)
// Semantic ops.
case ops.TypeSemanticLabel: