diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt index fe57c297f..45ad41008 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt @@ -542,20 +542,22 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp if (stepsY != 0 && deleteSwipeLineBoundaries.isNotEmpty()) { deleteSwipeLineIndex = (deleteSwipeLineIndex - stepsY).coerceIn(0, deleteSwipeLineBoundaries.size - 1) + // A rightward (negative) offset only makes sense inside the line it was made on. + // A vertical step re-anchors the selection at the start of the new line. + if (deleteSwipeLineIndex != oldLineIndex && deleteSwipeCharOffset < 0) deleteSwipeCharOffset = 0 } if (stepsX != 0) { val actual = actualSteps(stepsX) - deleteSwipeCharOffset = (deleteSwipeCharOffset - actual).coerceAtLeast(0) + // Positive offset: selection start is left of the line start (selected more by swiping left). + // Negative offset: selection start is right of the line start (deselected by swiping right + // after a vertical swipe). It can never go past the position where the swipe started. + val minOffset = -(deleteSwipeSelectionAnchor() - deleteSwipeLineBase()).coerceAtLeast(0) + deleteSwipeCharOffset = (deleteSwipeCharOffset - actual).coerceAtLeast(minOffset) } if (deleteSwipeLineIndex != oldLineIndex || deleteSwipeCharOffset != oldCharOffset) { - val lineBaseStart = if (deleteSwipeLineBoundaries.isNotEmpty()) { - deleteSwipeLineBoundaries[deleteSwipeLineIndex] - } else { - deleteSwipeInitialEnd - } - val finalStart = (lineBaseStart - deleteSwipeCharOffset).coerceIn(0, deleteSwipeInitialEnd) + val finalStart = (deleteSwipeLineBase() - deleteSwipeCharOffset).coerceIn(0, deleteSwipeInitialEnd) deleteSwipeStepCount++ if (deleteSwipeStepCount % 2 != 0) { @@ -565,6 +567,16 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp } } + /** Start of the line currently used as base of the delete swipe selection. */ + private fun deleteSwipeLineBase(): Int = + if (deleteSwipeLineBoundaries.isNotEmpty()) deleteSwipeLineBoundaries[deleteSwipeLineIndex] + else deleteSwipeInitialEnd + + /** Selection start when the delete swipe began, i.e. the right-most point the selection start may be moved to. */ + private fun deleteSwipeSelectionAnchor(): Int = + if (deleteSwipeLineBoundaries.isNotEmpty()) deleteSwipeLineBoundaries[0] + else deleteSwipeInitialEnd + private fun onMoveDeletePointerWords2D(stepsX: Int, stepsY: Int) { val oldLineIndex = deleteSwipeLineIndex val oldWordIndex = deleteSwipeWordIndex @@ -572,22 +584,34 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp // Negative stepsY moves UP (select more lines), positive stepsY moves DOWN (deselect lines) if (stepsY != 0 && deleteSwipeLineBoundaries.isNotEmpty()) { deleteSwipeLineIndex = (deleteSwipeLineIndex - stepsY).coerceIn(0, deleteSwipeLineBoundaries.size - 1) + // A rightward (negative) word index only makes sense inside the line it was made on. + if (deleteSwipeLineIndex != oldLineIndex && deleteSwipeWordIndex < 0) deleteSwipeWordIndex = 0 } - // Negative stepsX moves LEFT (select more words), positive stepsX moves RIGHT (deselect words) + // Negative stepsX moves LEFT (select more words), positive stepsX moves RIGHT (deselect words). + // Positive index: words to the left of the line start. Negative index: words to the right of the + // line start that are deselected again (limited to what lies before the swipe's start point). if (stepsX != 0) { - deleteSwipeWordIndex = (deleteSwipeWordIndex - stepsX).coerceAtLeast(0) + val minIndex = -getWordBoundariesForwards( + deleteSwipeInitialText, deleteSwipeSelectionAnchor(), deleteSwipeLineBase() + ).size + deleteSwipeWordIndex = (deleteSwipeWordIndex - stepsX).coerceAtLeast(minIndex) } if (deleteSwipeLineIndex != oldLineIndex || deleteSwipeWordIndex != oldWordIndex) { - val lineBaseStart = if (deleteSwipeLineBoundaries.isNotEmpty()) { - deleteSwipeLineBoundaries[deleteSwipeLineIndex] - } else { - deleteSwipeInitialEnd - } + val lineBaseStart = deleteSwipeLineBase() val finalStart = if (deleteSwipeWordIndex == 0) { lineBaseStart + } else if (deleteSwipeWordIndex < 0) { + val wordsAfterLine = getWordBoundariesForwards( + deleteSwipeInitialText, deleteSwipeSelectionAnchor(), lineBaseStart + ) + if (wordsAfterLine.isNotEmpty()) { + wordsAfterLine[(-deleteSwipeWordIndex).coerceAtMost(wordsAfterLine.size) - 1] + } else { + lineBaseStart + } } else { val wordsBeforeLine = if (deleteSwipeLineIndex == 0) { deleteSwipeWordBoundaries @@ -1049,6 +1073,23 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp return boundaries } + /** + * Word boundaries between [fromOffset] and the end of [text], in ascending order (nearest to + * [fromOffset] first). Used to deselect words by swiping right after a vertical delete swipe. + * [textEndOffset] is the absolute offset of the end of [text]. Boundaries are the same ones + * [getWordBoundariesBackwards] produces, so selecting and deselecting step over identical positions. + */ + internal fun getWordBoundariesForwards(text: String, textEndOffset: Int, fromOffset: Int): List { + if (fromOffset >= textEndOffset) return emptyList() + val textStartOffset = textEndOffset - text.length + val charsBeforeFrom = (fromOffset - textStartOffset).coerceIn(0, text.length) + val segment = text.substring(charsBeforeFrom) + return getWordBoundariesBackwards(segment, textEndOffset) + .filter { it > fromOffset } + .distinct() + .reversed() + } + internal fun getWordBoundariesBackwards(text: String, endOffset: Int): List { val boundaries = mutableListOf() var i = text.length diff --git a/app/src/test/java/helium314/keyboard/keyboard/WordDeleteSwipeTest.kt b/app/src/test/java/helium314/keyboard/keyboard/WordDeleteSwipeTest.kt index 4618cc694..6a7053527 100644 --- a/app/src/test/java/helium314/keyboard/keyboard/WordDeleteSwipeTest.kt +++ b/app/src/test/java/helium314/keyboard/keyboard/WordDeleteSwipeTest.kt @@ -119,4 +119,45 @@ class WordDeleteSwipeTest { assertEquals(" line\n", initialText.substring(wordsBeforeLine[1], lineBaseStart)) assertEquals("\nsecond line\n", initialText.substring(wordsBeforeLine[2], lineBaseStart)) } + + @Test + fun `forward word boundaries are ascending and end at the swipe start`() { + val text = "one two three" + // from the start of the text: positions between words, nearest first, ending at the cursor + assertEquals(listOf(3, 7, 13), KeyboardActionListenerImpl.getWordBoundariesForwards(text, text.length, 0)) + // from the middle of the text only boundaries after that point remain + assertEquals(listOf(7, 13), KeyboardActionListenerImpl.getWordBoundariesForwards(text, text.length, 3)) + } + + @Test + fun `forward word boundaries are empty when there is nothing to deselect`() { + val text = "one two three" + assertEquals(emptyList(), KeyboardActionListenerImpl.getWordBoundariesForwards(text, text.length, text.length)) + assertEquals(emptyList(), KeyboardActionListenerImpl.getWordBoundariesForwards("", 0, 0)) + } + + @Test + fun `forward word boundaries match backward boundaries`() { + // selecting by swiping left and deselecting by swiping right step over the same positions + val text = "first line\nsecond line\nthird line" + val end = text.length // 33 + val lineBase = KeyboardActionListenerImpl.getLineBoundariesBackwards(text, end)[1] // 23 + assertEquals(23, lineBase) + val forward = KeyboardActionListenerImpl.getWordBoundariesForwards(text, end, lineBase) + assertEquals(listOf(28, 33), forward) + assertEquals("third", text.substring(lineBase, forward[0]).trim()) + assertEquals("third line", text.substring(lineBase, forward[1])) + } + + @Test + fun `forward word boundaries respect text offset`() { + // text before the cursor does not start at offset 0 of the field + val text = "alpha beta gamma" + val endOffset = 100 + val startOffset = endOffset - text.length // 84 + assertEquals( + listOf(startOffset + 5, startOffset + 10, endOffset), + KeyboardActionListenerImpl.getWordBoundariesForwards(text, endOffset, startOffset) + ) + } }