mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
widget,io,app: draw IME composition underline
Track IME composition range updates as key.CompositionEvent and route them to the focused editor. Draw a thin underline under the current composition range using the editor text material. Keep the composition range in imeState so it is reset with the rest of the editor input method state, and normalize it in input.Router before forwarding CompositionEvent. Signed-off-by: qiannian <qianniancn@gmail.com>
This commit is contained in:
@@ -439,6 +439,7 @@ func (c *callbacks) EditorState() editorState {
|
|||||||
|
|
||||||
func (c *callbacks) SetComposingRegion(r key.Range) {
|
func (c *callbacks) SetComposingRegion(r key.Range) {
|
||||||
c.w.imeState.compose = r
|
c.w.imeState.compose = r
|
||||||
|
c.w.driver.ProcessEvent(key.CompositionEvent(r))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *callbacks) EditorInsert(text string) {
|
func (c *callbacks) EditorInsert(text string) {
|
||||||
|
|||||||
+8
-1
@@ -419,7 +419,7 @@ func (f *filter) Merge(f2 filter) {
|
|||||||
|
|
||||||
func (f *filter) Matches(e event.Event) bool {
|
func (f *filter) Matches(e event.Event) bool {
|
||||||
switch e.(type) {
|
switch e.(type) {
|
||||||
case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent:
|
case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent, key.CompositionEvent:
|
||||||
return f.focusable
|
return f.focusable
|
||||||
default:
|
default:
|
||||||
return f.pointer.Matches(e)
|
return f.pointer.Matches(e)
|
||||||
@@ -463,6 +463,13 @@ func (q *Router) processEvent(e event.Event, system bool) {
|
|||||||
evts = append(evts, taggedEvent{tag: f, event: e})
|
evts = append(evts, taggedEvent{tag: f, event: e})
|
||||||
}
|
}
|
||||||
q.changeState(e, state, evts)
|
q.changeState(e, state, evts)
|
||||||
|
case key.CompositionEvent:
|
||||||
|
e = key.CompositionEvent(rangeNorm(key.Range(e)))
|
||||||
|
var evts []taggedEvent
|
||||||
|
if f := state.focus; f != nil {
|
||||||
|
evts = append(evts, taggedEvent{tag: f, event: e})
|
||||||
|
}
|
||||||
|
q.changeState(e, state, evts)
|
||||||
case key.EditEvent, key.FocusEvent, key.SelectionEvent:
|
case key.EditEvent, key.FocusEvent, key.SelectionEvent:
|
||||||
var evts []taggedEvent
|
var evts []taggedEvent
|
||||||
if f := state.focus; f != nil {
|
if f := state.focus; f != nil {
|
||||||
|
|||||||
+9
-5
@@ -77,6 +77,9 @@ type Caret struct {
|
|||||||
// SelectionEvent is generated when an input method changes the selection.
|
// SelectionEvent is generated when an input method changes the selection.
|
||||||
type SelectionEvent Range
|
type SelectionEvent Range
|
||||||
|
|
||||||
|
// CompositionEvent is generated when an input method changes the composing range.
|
||||||
|
type CompositionEvent Range
|
||||||
|
|
||||||
// SnippetEvent is generated when the snippet range is updated by an
|
// SnippetEvent is generated when the snippet range is updated by an
|
||||||
// input method.
|
// input method.
|
||||||
type SnippetEvent Range
|
type SnippetEvent Range
|
||||||
@@ -243,11 +246,12 @@ func (h InputHintOp) Add(o *op.Ops) {
|
|||||||
data[1] = byte(h.Hint)
|
data[1] = byte(h.Hint)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (EditEvent) ImplementsEvent() {}
|
func (EditEvent) ImplementsEvent() {}
|
||||||
func (Event) ImplementsEvent() {}
|
func (Event) ImplementsEvent() {}
|
||||||
func (FocusEvent) ImplementsEvent() {}
|
func (FocusEvent) ImplementsEvent() {}
|
||||||
func (SnippetEvent) ImplementsEvent() {}
|
func (CompositionEvent) ImplementsEvent() {}
|
||||||
func (SelectionEvent) ImplementsEvent() {}
|
func (SnippetEvent) ImplementsEvent() {}
|
||||||
|
func (SelectionEvent) ImplementsEvent() {}
|
||||||
|
|
||||||
func (FocusCmd) ImplementsCommand() {}
|
func (FocusCmd) ImplementsCommand() {}
|
||||||
func (SoftKeyboardCmd) ImplementsCommand() {}
|
func (SoftKeyboardCmd) ImplementsCommand() {}
|
||||||
|
|||||||
+30
-2
@@ -25,6 +25,7 @@ import (
|
|||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
"gioui.org/op/clip"
|
"gioui.org/op/clip"
|
||||||
|
"gioui.org/op/paint"
|
||||||
"gioui.org/text"
|
"gioui.org/text"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
)
|
)
|
||||||
@@ -107,8 +108,9 @@ type imeState struct {
|
|||||||
rng key.Range
|
rng key.Range
|
||||||
caret key.Caret
|
caret key.Caret
|
||||||
}
|
}
|
||||||
snippet key.Snippet
|
snippet key.Snippet
|
||||||
start, end int
|
composition key.Range
|
||||||
|
start, end int
|
||||||
}
|
}
|
||||||
|
|
||||||
type maskReader struct {
|
type maskReader struct {
|
||||||
@@ -398,9 +400,12 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
|
|||||||
case key.FocusEvent:
|
case key.FocusEvent:
|
||||||
// Reset IME state.
|
// Reset IME state.
|
||||||
e.ime.imeState = imeState{}
|
e.ime.imeState = imeState{}
|
||||||
|
e.ime.composition = key.Range{Start: -1, End: -1}
|
||||||
if ke.Focus && !e.ReadOnly {
|
if ke.Focus && !e.ReadOnly {
|
||||||
gtx.Execute(key.SoftKeyboardCmd{Show: true})
|
gtx.Execute(key.SoftKeyboardCmd{Show: true})
|
||||||
}
|
}
|
||||||
|
case key.CompositionEvent:
|
||||||
|
e.ime.composition = key.Range(ke)
|
||||||
case key.Event:
|
case key.Event:
|
||||||
if !gtx.Focused(e) || ke.State != key.Press {
|
if !gtx.Focused(e) || ke.State != key.Press {
|
||||||
break
|
break
|
||||||
@@ -735,6 +740,7 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
|
|||||||
if e.Len() > 0 {
|
if e.Len() > 0 {
|
||||||
e.paintSelection(gtx, selectMaterial)
|
e.paintSelection(gtx, selectMaterial)
|
||||||
e.paintText(gtx, textMaterial)
|
e.paintText(gtx, textMaterial)
|
||||||
|
e.paintComposition(gtx, textMaterial)
|
||||||
}
|
}
|
||||||
if gtx.Enabled() {
|
if gtx.Enabled() {
|
||||||
e.paintCaret(gtx, textMaterial)
|
e.paintCaret(gtx, textMaterial)
|
||||||
@@ -759,6 +765,28 @@ func (e *Editor) paintText(gtx layout.Context, material op.CallOp) {
|
|||||||
e.text.PaintText(gtx, material)
|
e.text.PaintText(gtx, material)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Editor) paintComposition(gtx layout.Context, material op.CallOp) {
|
||||||
|
e.initBuffer()
|
||||||
|
r := e.ime.composition
|
||||||
|
if r.Start == -1 || r.Start == r.End {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.text.regions = e.text.Regions(r.Start, r.End, e.text.regions)
|
||||||
|
thickness := max(gtx.Dp(unit.Dp(1)), 1)
|
||||||
|
for _, region := range e.text.regions {
|
||||||
|
y := region.Bounds.Max.Y - max(region.Baseline/3, thickness)
|
||||||
|
underline := image.Rect(region.Bounds.Min.X, y, region.Bounds.Max.X, y+thickness)
|
||||||
|
underline = underline.Intersect(image.Rectangle{Max: e.text.viewSize})
|
||||||
|
if underline.Empty() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stack := clip.Rect(underline).Push(gtx.Ops)
|
||||||
|
material.Add(gtx.Ops)
|
||||||
|
paint.PaintOp{}.Add(gtx.Ops)
|
||||||
|
stack.Pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// paintCaret paints the text glyphs using the provided material to set the fill material
|
// paintCaret paints the text glyphs using the provided material to set the fill material
|
||||||
// of the caret rectangle.
|
// of the caret rectangle.
|
||||||
func (e *Editor) paintCaret(gtx layout.Context, material op.CallOp) {
|
func (e *Editor) paintCaret(gtx layout.Context, material op.CallOp) {
|
||||||
|
|||||||
Reference in New Issue
Block a user