diff --git a/app/window.go b/app/window.go index 977ab6a1..9adba8df 100644 --- a/app/window.go +++ b/app/window.go @@ -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) { diff --git a/io/input/router.go b/io/input/router.go index b72d97cf..7fe13c80 100644 --- a/io/input/router.go +++ b/io/input/router.go @@ -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 { diff --git a/io/key/key.go b/io/key/key.go index d1819ba9..d16c5bb3 100644 --- a/io/key/key.go +++ b/io/key/key.go @@ -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 @@ -243,11 +246,12 @@ func (h InputHintOp) Add(o *op.Ops) { data[1] = byte(h.Hint) } -func (EditEvent) ImplementsEvent() {} -func (Event) ImplementsEvent() {} -func (FocusEvent) ImplementsEvent() {} -func (SnippetEvent) ImplementsEvent() {} -func (SelectionEvent) ImplementsEvent() {} +func (EditEvent) ImplementsEvent() {} +func (Event) ImplementsEvent() {} +func (FocusEvent) ImplementsEvent() {} +func (CompositionEvent) ImplementsEvent() {} +func (SnippetEvent) ImplementsEvent() {} +func (SelectionEvent) ImplementsEvent() {} func (FocusCmd) ImplementsCommand() {} func (SoftKeyboardCmd) ImplementsCommand() {} diff --git a/widget/editor.go b/widget/editor.go index bf477121..6e5da092 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -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" ) @@ -107,8 +108,9 @@ type imeState struct { rng key.Range caret key.Caret } - snippet key.Snippet - start, end int + snippet key.Snippet + composition key.Range + start, end int } type maskReader struct { @@ -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) {