mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
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:
+59
-5
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user