Summary
Under two-thumb multi-part composition, a previous gesture's stroke geometry can leak into the next fresh swipe, fusing a deleted word's swipe into the new one. It is most visible when swiping a fresh word at the start of a text box: the new word comes out merged with previously-deleted gesture geometry instead of being recognized on its own.
Root cause
WordComposer.mExtendBatchInputBase (the multi-part merged-trail base consumed by WordComposer.setBatchInputPointers) is only cleared by a normally-completing gesture — onUpdateTailBatchInputCompleted, which is the lone routine clear and sits after two empty-recognition early-returns. An abnormal gesture end leaves mExtendBatchInputBaseSet armed:
onCancelBatchInput never clears it,
- the empty-top-word early-returns in
onUpdateTailBatchInputCompleted skip the clear,
WordComposer.reset() doesn't touch it, so no deletion path clears it either.
Once leaked, the flag survives every backspace / selection-delete / commit. A later fresh gesture (no composing word ⇒ onStartBatchInput's extend branch never runs, so it neither sets nor clears the base) hits the merge guard in setBatchInputPointers and prepends the ghost trail. Start-of-text-box maximizes visibility because there is no composing word there to legitimately re-set the base.
This is the same class of bug as the mLiveStroke handling, which is already dropped on every word-end path; mExtendBatchInputBase simply never got the same treatment.
Steps to reproduce
- Enable two-thumb typing (manual spacing or combining grace).
- Swipe a word, then trigger an abnormal gesture end (cancel a follow-up gesture, or hit an empty recognition).
- Delete everything.
- Swipe a fresh word at the start of the text box.
Expected: the fresh word is recognized on its own.
Actual: it sometimes fuses with the previously-deleted gesture's geometry.
Proposed fix
Clear mExtendBatchInputBase at the same word-end sites where mLiveStroke is already dropped (handleBackspaceEvent, resetComposingState, commitChosenWord, the composing-desync recovery) plus the two gesture-lifecycle origins (onStartBatchInput top, onCancelBatchInput). This avoids touching the hot WordComposer.reset() path. PR to follow.
Summary
Under two-thumb multi-part composition, a previous gesture's stroke geometry can leak into the next fresh swipe, fusing a deleted word's swipe into the new one. It is most visible when swiping a fresh word at the start of a text box: the new word comes out merged with previously-deleted gesture geometry instead of being recognized on its own.
Root cause
WordComposer.mExtendBatchInputBase(the multi-part merged-trail base consumed byWordComposer.setBatchInputPointers) is only cleared by a normally-completing gesture —onUpdateTailBatchInputCompleted, which is the lone routine clear and sits after two empty-recognition early-returns. An abnormal gesture end leavesmExtendBatchInputBaseSetarmed:onCancelBatchInputnever clears it,onUpdateTailBatchInputCompletedskip the clear,WordComposer.reset()doesn't touch it, so no deletion path clears it either.Once leaked, the flag survives every backspace / selection-delete / commit. A later fresh gesture (no composing word ⇒
onStartBatchInput's extend branch never runs, so it neither sets nor clears the base) hits the merge guard insetBatchInputPointersand prepends the ghost trail. Start-of-text-box maximizes visibility because there is no composing word there to legitimately re-set the base.This is the same class of bug as the
mLiveStrokehandling, which is already dropped on every word-end path;mExtendBatchInputBasesimply never got the same treatment.Steps to reproduce
Expected: the fresh word is recognized on its own.
Actual: it sometimes fuses with the previously-deleted gesture's geometry.
Proposed fix
Clear
mExtendBatchInputBaseat the same word-end sites wheremLiveStrokeis already dropped (handleBackspaceEvent,resetComposingState,commitChosenWord, the composing-desync recovery) plus the two gesture-lifecycle origins (onStartBatchInputtop,onCancelBatchInput). This avoids touching the hotWordComposer.reset()path. PR to follow.