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) {
|
||||
c.w.imeState.compose = r
|
||||
c.w.driver.ProcessEvent(key.CompositionEvent(r))
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
default:
|
||||
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})
|
||||
}
|
||||
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:
|
||||
var evts []taggedEvent
|
||||
if f := state.focus; f != nil {
|
||||
|
||||
@@ -77,6 +77,9 @@ type Caret struct {
|
||||
// SelectionEvent is generated when an input method changes the selection.
|
||||
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
|
||||
// input method.
|
||||
type SnippetEvent Range
|
||||
@@ -246,6 +249,7 @@ func (h InputHintOp) Add(o *op.Ops) {
|
||||
func (EditEvent) ImplementsEvent() {}
|
||||
func (Event) ImplementsEvent() {}
|
||||
func (FocusEvent) ImplementsEvent() {}
|
||||
func (CompositionEvent) ImplementsEvent() {}
|
||||
func (SnippetEvent) ImplementsEvent() {}
|
||||
func (SelectionEvent) ImplementsEvent() {}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
)
|
||||
@@ -108,6 +109,7 @@ type imeState struct {
|
||||
caret key.Caret
|
||||
}
|
||||
snippet key.Snippet
|
||||
composition key.Range
|
||||
start, end int
|
||||
}
|
||||
|
||||
@@ -398,9 +400,12 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
|
||||
case key.FocusEvent:
|
||||
// Reset IME state.
|
||||
e.ime.imeState = imeState{}
|
||||
e.ime.composition = key.Range{Start: -1, End: -1}
|
||||
if ke.Focus && !e.ReadOnly {
|
||||
gtx.Execute(key.SoftKeyboardCmd{Show: true})
|
||||
}
|
||||
case key.CompositionEvent:
|
||||
e.ime.composition = key.Range(ke)
|
||||
case key.Event:
|
||||
if !gtx.Focused(e) || ke.State != key.Press {
|
||||
break
|
||||
@@ -735,6 +740,7 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
|
||||
if e.Len() > 0 {
|
||||
e.paintSelection(gtx, selectMaterial)
|
||||
e.paintText(gtx, textMaterial)
|
||||
e.paintComposition(gtx, textMaterial)
|
||||
}
|
||||
if gtx.Enabled() {
|
||||
e.paintCaret(gtx, textMaterial)
|
||||
@@ -759,6 +765,28 @@ func (e *Editor) paintText(gtx layout.Context, material op.CallOp) {
|
||||
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
|
||||
// of the caret rectangle.
|
||||
func (e *Editor) paintCaret(gtx layout.Context, material op.CallOp) {
|
||||
|
||||
Reference in New Issue
Block a user