diff --git a/app/src/main/java/helium314/keyboard/latin/WordComposer.java b/app/src/main/java/helium314/keyboard/latin/WordComposer.java index a7e1b8034..097b74747 100644 --- a/app/src/main/java/helium314/keyboard/latin/WordComposer.java +++ b/app/src/main/java/helium314/keyboard/latin/WordComposer.java @@ -208,6 +208,18 @@ public void applyProcessedEvent(final Event event, final boolean keepCursorPosit // shouldn't be overridden by the "typed key" coordinates // (See {@link #setBatchInputWord}). if (!mIsBatchMode) { + // Two-thumb / multi-part: the FIRST tap of a fresh word resets the raw stroke + // buffer. mInputPointers is intentionally NOT cleared by reset() or backspace + // (it must survive setBatchInputWord so a gesture commit can re-feed it), and + // addPointerAt overwrites in place WITHOUT shrinking the length — so a word + // abandoned via backspace leaves a stale tail. Without this, re-typing the word + // and swiping would snapshot that stale buffer as the merged-trail extend base + // and build an ever-longer garbage word. Clearing only at index 0 (a brand-new + // word) drops the stale geometry while leaving mid-word taps (newIndex > 0, incl. + // a tap extending a gesture) and the batch-mode gesture trail untouched. + if (newIndex == 0) { + mInputPointers.reset(); + } // TODO: Set correct pointer id and time mInputPointers.addPointerAt(newIndex, keyX, keyY, 0, 0); } diff --git a/app/src/test/java/helium314/keyboard/latin/WordComposerTest.java b/app/src/test/java/helium314/keyboard/latin/WordComposerTest.java index e9c867895..3f2f25e7f 100644 --- a/app/src/test/java/helium314/keyboard/latin/WordComposerTest.java +++ b/app/src/test/java/helium314/keyboard/latin/WordComposerTest.java @@ -130,6 +130,40 @@ public void testExtendBatchInputBaseMergesAndRetimes() { assertTrue("base must end before the new gesture begins", times[1] < times[2]); } + // #7: the raw stroke buffer (mInputPointers) must not bleed into a fresh word. It is + // intentionally NOT cleared by reset() (so a gesture commit can re-feed it via + // setBatchInputWord), and addPointerAt overwrites in place WITHOUT shrinking the length — so + // after a word is abandoned via backspace, a stale tail survives. The next swipe would then + // snapshot that stale buffer as the merged-trail extend base and build an ever-longer garbage + // word (the "swipes stick around and grow" bug). The first tap of a new word must start clean. + @Test + public void testFreshWordResetsStaleInputPointers() { + final WordComposer wordComposer = new WordComposer(); + + // Geometry left by a prior (gesture) word: a 5-point stroke. + final InputPointers stale = new InputPointers(16); + stale.addPointer(1, 1, 0, 0); + stale.addPointer(2, 2, 0, 10); + stale.addPointer(3, 3, 0, 20); + stale.addPointer(4, 4, 0, 30); + stale.addPointer(5, 5, 0, 40); + wordComposer.setBatchInputPointers(stale); + assertEquals(5, wordComposer.getInputPointers().getPointerSize()); + + // Word abandoned (e.g. backspaced). reset() deliberately does NOT clear mInputPointers, + // so the stale 5-point buffer survives — this is the premise of the bug. + wordComposer.reset(); + assertEquals(5, wordComposer.getInputPointers().getPointerSize()); + + // Start a fresh word with a single tapped letter (index 0). The stale buffer must be + // dropped, leaving exactly one point — not 5 (pre-fix: addAt(0) overwrites in place and + // the length stays 5, which is what the following swipe then merged with). + final int[] codePoints = new int[] { 'a' }; + final int[] coordinates = CoordinateUtils.newCoordinateArray(1, 50, 60); + wordComposer.setComposingWord(codePoints, coordinates); + assertEquals(1, wordComposer.getInputPointers().getPointerSize()); + } + @Test public void testExtendBatchInputBaseEmptyIsNoOp() { final WordComposer wordComposer = new WordComposer();