Skip to content

Commit

Permalink
Magnifier for iOS 17+ (#1000)
Browse files Browse the repository at this point in the history
## Proposed Changes

- Text field and selection container magnifier for iOS 17+

<img
src="https://github.com/JetBrains/compose-multiplatform-core/assets/63979218/f7fd9432-066f-4cc3-ad96-5029bfe75848"
height = 300>

## Testing

Test: try magnifier in all types of text fields

## Issues Fixed

Partly: JetBrains/compose-multiplatform#3692
  • Loading branch information
alexzhirkevich committed Apr 5, 2024
1 parent ae0fd0d commit 5a698ba
Show file tree
Hide file tree
Showing 8 changed files with 722 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.util.fastAny
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -84,6 +85,12 @@ internal suspend fun PointerInputScope.detectDownAndDragGesturesWithObserver(
}
launch(start = CoroutineStart.UNDISPATCHED) {
detectDragGesturesWithObserver(observer)
}.invokeOnCompletion {
// Otherwise observer won't be notified if
// composable was disposed before the drag cancellation
if (it is CancellationException){
observer.onCancel()
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,13 @@ internal class TextFieldSelectionManager(
* when the dragging is stopped.
*/
var draggingHandle: Handle? by mutableStateOf(null)
private set
internal set

/**
* The current position of a drag, in decoration box coordinates.
*/
var currentDragPosition: Offset? by mutableStateOf(null)
private set
internal set

/**
* The previous offset of a drag, before selection adjustments.
Expand Down Expand Up @@ -450,7 +450,10 @@ internal class TextFieldSelectionManager(
updateFloatingToolbar(show = true)
}

override fun onCancel() {}
override fun onCancel() {
draggingHandle = null
currentDragPosition = null
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 The Android Open Source Project
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ internal fun Modifier.cupertinoTextFieldPointer(
): Modifier = if (enabled) {
// TODO switch to ".updateSelectionTouchMode { state.isInTouchMode = it }" as in defaultTextFieldPointer
if (isInTouchMode) {
val longPressHandlerModifier = getLongPressHandlerModifier(state, offsetMapping)
val longPressHandlerModifier = getLongPressHandlerModifier(state, offsetMapping, manager)
val tapHandlerModifier = getTapHandlerModifier(
interactionSource,
state,
Expand Down Expand Up @@ -110,7 +110,7 @@ private fun getTapHandlerModifier(
if (currentState.handleState != HandleState.Selection) {
currentState.layoutResult?.let { layoutResult ->
// TODO: Research native behavior with any text transformations (which adds symbols like with using NSNumberFormatter)
if (manager.visualTransformation != VisualTransformation.None) {
if (currentManager.visualTransformation != VisualTransformation.None) {
TextFieldDelegate.setCursorOffset(
touchPointOffset,
layoutResult,
Expand All @@ -126,7 +126,7 @@ private fun getTapHandlerModifier(
offsetMapping = currentOffsetMapping,
showContextMenu = {
// it shouldn't be selection, but this is a way to call context menu in BasicTextField
manager.enterSelectionMode(true)
currentManager.enterSelectionMode(true)
},
onValueChange = currentState.onValueChange
)
Expand Down Expand Up @@ -175,10 +175,12 @@ private fun getTapHandlerModifier(
@Composable
private fun getLongPressHandlerModifier(
state: TextFieldState,
offsetMapping: OffsetMapping
offsetMapping: OffsetMapping,
manager: TextFieldSelectionManager,
): Modifier {
val currentState by rememberUpdatedState(state)
val currentOffsetMapping by rememberUpdatedState(offsetMapping)
val currentManager by rememberUpdatedState(manager)

return Modifier.pointerInput(Unit) {
val longTapActionsObserver =
Expand All @@ -187,6 +189,9 @@ private fun getLongPressHandlerModifier(
var dragBeginOffset = Offset.Zero

override fun onStart(startPoint: Offset) {
currentManager.draggingHandle = Handle.SelectionEnd
currentManager.currentDragPosition = startPoint

currentState.layoutResult?.let { layoutResult ->
TextFieldDelegate.setCursorOffset(
startPoint,
Expand All @@ -204,6 +209,7 @@ private fun getLongPressHandlerModifier(
dragTotalDistance += delta
currentState.layoutResult?.let { layoutResult ->
val currentDragPosition = dragBeginOffset + dragTotalDistance
currentManager.currentDragPosition = currentDragPosition
TextFieldDelegate.setCursorOffset(
currentDragPosition,
layoutResult,
Expand All @@ -219,9 +225,15 @@ private fun getLongPressHandlerModifier(

override fun onUp() {}

override fun onStop() {}
override fun onStop() {
currentManager.draggingHandle = null
currentManager.currentDragPosition = null
}

override fun onCancel() {}
override fun onCancel() {
currentManager.draggingHandle = null
currentManager.currentDragPosition = null
}
}

detectDragGesturesAfterLongPress(
Expand Down Expand Up @@ -339,4 +351,4 @@ private fun createTextFieldValue(
annotatedString = annotatedString,
selection = selection
)
}
}
Loading

0 comments on commit 5a698ba

Please sign in to comment.