Skip to content

Commit 7dfaf94

Browse files
authored
Adopt compose multitap textfield gestures (#2868)
Fixes: [CMP-9145](https://youtrack.jetbrains.com/issue/CMP-9145) Adopt: Enable triple-tap support to select Paragraph ## Release Notes N/A
1 parent 1d5979f commit 7dfaf94

5 files changed

Lines changed: 437 additions & 317 deletions

File tree

compose/foundation/foundation/src/iosMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.ios.kt

Lines changed: 43 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
package androidx.compose.foundation.text.input.internal.selection
1818

19-
import androidx.compose.foundation.gestures.awaitEachGesture
20-
import androidx.compose.foundation.gestures.awaitPress
2119
import androidx.compose.foundation.gestures.detectTapAndPress
2220
import androidx.compose.foundation.interaction.MutableInteractionSource
2321
import androidx.compose.foundation.interaction.PressInteraction
@@ -42,22 +40,16 @@ import androidx.compose.foundation.text.input.internal.selection.TextFieldSelect
4240
import androidx.compose.foundation.text.input.internal.selection.TextToolbarState.Cursor
4341
import androidx.compose.foundation.text.input.internal.selection.TextToolbarState.None
4442
import androidx.compose.foundation.text.input.internal.selection.TextToolbarState.Selection
45-
import androidx.compose.foundation.text.selection.ClicksCounter
4643
import androidx.compose.foundation.text.selection.MouseSelectionObserver
4744
import androidx.compose.foundation.text.selection.SelectionAdjustment
48-
import androidx.compose.foundation.text.selection.isMouseOrTouchPad
49-
import androidx.compose.foundation.text.selection.mouseSelection
50-
import androidx.compose.foundation.text.selection.touchSelectionFirstPress
45+
import androidx.compose.foundation.text.selection.awaitSelectionGestures
5146
import androidx.compose.ui.Modifier
5247
import androidx.compose.ui.geometry.Offset
5348
import androidx.compose.ui.geometry.isSpecified
5449
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
55-
import androidx.compose.ui.input.pointer.PointerInputChange
5650
import androidx.compose.ui.input.pointer.PointerInputScope
57-
import androidx.compose.ui.input.pointer.isPrimaryPressed
5851
import androidx.compose.ui.platform.Clipboard
5952
import androidx.compose.ui.text.TextRange
60-
import androidx.compose.ui.util.fastAll
6153
import kotlinx.coroutines.CoroutineScope
6254
import kotlinx.coroutines.CoroutineStart
6355
import kotlinx.coroutines.coroutineScope
@@ -212,100 +204,74 @@ internal actual suspend fun TextFieldSelectionState.textFieldSelectionGestures(
212204
) {
213205
val selectionState = this
214206
val uiKitTextDragObserver = UIKitTextFieldTextDragObserver(selectionState)
215-
val clicksCounter = ClicksCounter(pointerInputScope.viewConfiguration)
216-
pointerInputScope.awaitEachGesture {
217-
while (true) {
218-
val downEvent = awaitPress({true})
219-
clicksCounter.update(downEvent.changes[0])
220-
val isPrecise = downEvent.isMouseOrTouchPad()
221-
if (
222-
isPrecise &&
223-
downEvent.buttons.isPrimaryPressed &&
224-
downEvent.changes.fastAll { !it.isConsumed }
225-
) {
226-
// Use default BTF2 logic for mouse
227-
mouseSelection(mouseSelectionObserver, clicksCounter, downEvent)
228-
} else if (!isPrecise) {
229-
when (clicksCounter.clicks) {
230-
1 -> {
231-
// The default BTF2 logic, except
232-
// moving text cursor without selection requires custom TextDragObserver
233-
touchSelectionFirstPress(
234-
observer = uiKitTextDragObserver,
235-
downEvent = downEvent
236-
)
237-
}
238-
239-
2 -> {
240-
doRepeatingTapSelection(
241-
downEvent.changes.first(),
242-
selectionState,
243-
SelectionAdjustment.Word
244-
)
245-
}
207+
pointerInputScope.awaitSelectionGestures(
208+
mouseSelectionObserver = mouseSelectionObserver,
209+
textDragObserver = uiKitTextDragObserver,
210+
)
211+
}
246212

247-
else -> {
248-
val downChange = downEvent.changes.first()
249-
clearSelection(
250-
downChange,
251-
selectionState
252-
) // Previous selection must be cleared, otherwise this closure won't get third (and further) click
253-
doRepeatingTapSelection(
254-
downChange,
255-
selectionState,
256-
SelectionAdjustment.Paragraph
257-
)
258-
}
259-
}
260-
}
261-
}
213+
private fun TextFieldSelectionState.moveCaretByLongPress(
214+
touchPointOffset: Offset,
215+
) {
216+
hapticFeedBack?.performHapticFeedback(HapticFeedbackType.LongPress)
217+
// Long Press at the blank area, the cursor should show up at the end of the line.
218+
if (!textLayoutState.isPositionOnText(touchPointOffset)) {
219+
val offset = textLayoutState.getOffsetForPosition(touchPointOffset)
220+
textFieldState.placeCursorBeforeCharAt(offset)
221+
} else {
222+
if (textFieldState.visualText.isEmpty()) return
223+
val coercedOffset =
224+
textLayoutState.coercedInVisibleBoundsOfInputText(touchPointOffset)
225+
placeCursorAtNearestOffset(
226+
textLayoutState.fromDecorationToTextLayout(coercedOffset)
227+
)
262228
}
229+
showCursorHandle = true
230+
updateHandleDragging(Handle.Cursor, touchPointOffset)
263231
}
264232

265-
private fun doRepeatingTapSelection(
266-
touchChange: PointerInputChange,
267-
selectionState: TextFieldSelectionState,
233+
private fun TextFieldSelectionState.doRepeatingTapSelection(
234+
touchPointOffset: Offset,
268235
selectionAdjustment: SelectionAdjustment
269236
) {
270-
val selectionOffset = selectionState.textLayoutState.getOffsetForPosition(
271-
position = touchChange.position
237+
clearSelection(touchPointOffset) // otherwise it won't be changed by triple tap
238+
239+
val selectionOffset = textLayoutState.getOffsetForPosition(
240+
position = touchPointOffset
272241
)
273-
touchChange.consume()
274242

275-
val newSelection = selectionState.updateSelection(
276-
selectionState.textFieldState.visualText,
243+
val newSelection = updateSelection(
244+
textFieldState.visualText,
277245
selectionOffset,
278246
selectionOffset,
279247
isStartHandle = false,
280248
adjustment = selectionAdjustment,
281249
hapticFeedbackType = HapticFeedbackType.TextHandleMove,
282250
)
283251

284-
selectionState.textFieldState.selectCharsIn(newSelection)
285-
selectionState.updateTextToolbarState(Selection)
252+
textFieldState.selectCharsIn(newSelection)
253+
updateTextToolbarState(Selection)
286254
}
287255

288-
private fun clearSelection(
289-
touchChange: PointerInputChange,
290-
selectionState: TextFieldSelectionState
256+
private fun TextFieldSelectionState.clearSelection(
257+
touchPointOffset: Offset,
291258
) {
292-
val selectionOffset = selectionState.textLayoutState.getOffsetForPosition(
293-
position = touchChange.position
259+
val selectionOffset = textLayoutState.getOffsetForPosition(
260+
position = touchPointOffset
294261
)
295-
val clearedSelection = selectionState.updateSelection(
296-
TextFieldCharSequence(selectionState.textFieldState.visualText, TextRange.Zero),
262+
val clearedSelection = updateSelection(
263+
TextFieldCharSequence(textFieldState.visualText, TextRange.Zero),
297264
selectionOffset,
298265
selectionOffset,
299266
isStartHandle = false,
300267
adjustment = SelectionAdjustment.None,
301268
hapticFeedbackType = HapticFeedbackType.TextHandleMove,
302269
)
303-
selectionState.textFieldState.selectCharsIn(clearedSelection)
270+
textFieldState.selectCharsIn(clearedSelection)
304271
}
305272

306273
private class UIKitTextFieldTextDragObserver(
307274
private val textFieldSelectionState: TextFieldSelectionState,
308-
private val requestFocus: () -> Unit = {}
309275
) : TextDragObserver {
310276
private var dragBeginPosition: Offset = Offset.Unspecified
311277
private var dragTotalDistance: Offset = Offset.Zero
@@ -317,7 +283,6 @@ private class UIKitTextFieldTextDragObserver(
317283
dragBeginPosition = Offset.Unspecified
318284
dragTotalDistance = Offset.Zero
319285
textFieldSelectionState.directDragGestureInitiator = InputType.None
320-
requestFocus()
321286
textFieldSelectionState.clearHandleDragging()
322287
}
323288
}
@@ -338,21 +303,11 @@ private class UIKitTextFieldTextDragObserver(
338303
dragBeginPosition = startPoint
339304
dragTotalDistance = Offset.Zero
340305

341-
textFieldSelectionState.hapticFeedBack?.performHapticFeedback(HapticFeedbackType.LongPress)
342-
// Long Press at the blank area, the cursor should show up at the end of the line.
343-
if (!textFieldSelectionState.textLayoutState.isPositionOnText(startPoint)) {
344-
val offset = textFieldSelectionState.textLayoutState.getOffsetForPosition(startPoint)
345-
textFieldSelectionState.textFieldState.placeCursorBeforeCharAt(offset)
306+
if (selectionAdjustment != SelectionAdjustment.None) {
307+
textFieldSelectionState.doRepeatingTapSelection(startPoint, selectionAdjustment)
346308
} else {
347-
if (textFieldSelectionState.textFieldState.visualText.isEmpty()) return
348-
val coercedOffset =
349-
textFieldSelectionState.textLayoutState.coercedInVisibleBoundsOfInputText(startPoint)
350-
textFieldSelectionState.placeCursorAtNearestOffset(
351-
textFieldSelectionState.textLayoutState.fromDecorationToTextLayout(coercedOffset)
352-
)
309+
textFieldSelectionState.moveCaretByLongPress(startPoint)
353310
}
354-
textFieldSelectionState.showCursorHandle = true
355-
textFieldSelectionState.updateHandleDragging(Handle.Cursor, startPoint)
356311
}
357312

358313
override fun onDrag(delta: Offset) {

0 commit comments

Comments
 (0)