From ac8718917ebd68412fd5e30490cdfdd2384970a6 Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Mon, 5 Dec 2022 18:49:35 +0100 Subject: [PATCH] Fix text input popup position (#339) * PlatformTextInputService. Undeprecate notifyFocusedRect Fixes https://github.com/JetBrains/compose-jb/issues/2040 Fixes https://github.com/JetBrains/compose-jb/issues/2493 notifyFocusedRect was deprecated in https://android-review.googlesource.com/c/platform/frameworks/support/+/1959647 On Android, before deprecation this method was used to scroll to the focused text input. This functionality was extracted to the text field itself, but we had another functionality on the other platforms. On Desktop we show text input popup near text input (for some languages): https://github.com/JetBrains/compose-jb/issues/2040#issuecomment-1333429514 I think that this method is the right choice to implement this functionality, but I am not completely sure. Here my thoughts about alternatives: 1. Use BringIntoViewRequester. Not sure that it is possible, because its purpose - to show the view to the user, not use the passed information to determine the text popup position 2. Get information about focused area from another source that is not related to text input. For example, we can inject FocusManager, and retrieve the focused rect from it. Probably not a good idea, because the rect of arbitrary focused node isn't the same thing as the rect of focused input area. * Keep @Deprecated until we don't merget this into AOSP --- .../compose/foundation/text/CoreTextField.kt | 36 +++++++++++-- .../foundation/text/TextFieldDelegate.kt | 54 +++++++++++++++++++ .../compose/ui/text/input/TextInputService.kt | 23 +++++++- 3 files changed, 109 insertions(+), 4 deletions(-) diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt index 9122b415ea582..98fc0dc5d8310 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt @@ -282,7 +282,8 @@ internal fun CoreTextField( textInputService, state, value, - imeOptions + imeOptions, + offsetMapping ) // The focusable modifier itself will request the entire focusable be brought into view @@ -381,6 +382,19 @@ internal fun CoreTextField( state.showCursorHandle = manager.isSelectionHandleInVisibleBound(isStartHandle = true) } + state.layoutResult?.let { layoutResult -> + state.inputSession?.let { inputSession -> + TextFieldDelegate.notifyFocusedRect( + value, + state.textDelegate, + layoutResult.value, + it, + inputSession, + state.hasFocus, + offsetMapping + ) + } + } } state.layoutResult?.innerTextFieldCoordinates = it } @@ -819,11 +833,13 @@ private fun tapToFocus( } } +@OptIn(InternalFoundationTextApi::class) private fun notifyTextInputServiceOnFocusChange( textInputService: TextInputService, state: TextFieldState, value: TextFieldValue, - imeOptions: ImeOptions + imeOptions: ImeOptions, + offsetMapping: OffsetMapping ) { if (state.hasFocus) { state.inputSession = TextFieldDelegate.onFocus( @@ -833,7 +849,21 @@ private fun notifyTextInputServiceOnFocusChange( imeOptions, state.onValueChange, state.onImeActionPerformed - ) + ).also { newSession -> + state.layoutCoordinates?.let { coords -> + state.layoutResult?.let { layoutResult -> + TextFieldDelegate.notifyFocusedRect( + value, + state.textDelegate, + layoutResult.value, + coords, + newSession, + state.hasFocus, + offsetMapping + ) + } + } + } } else { onBlur(state) } diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt index 2d2b8da782e30..d8c004c4be24d 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt @@ -17,8 +17,11 @@ package androidx.compose.foundation.text import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.Paragraph import androidx.compose.ui.text.SpanStyle @@ -130,6 +133,57 @@ internal class TextFieldDelegate { TextPainter.paint(canvas, textLayoutResult) } + /** + * Notify system that focused input area. + * + * System is typically scrolled up not to be covered by keyboard. + * + * @param value The editor model + * @param textDelegate The text delegate + * @param layoutCoordinates The layout coordinates + * @param textInputSession The current input session. + * @param hasFocus True if focus is gained. + * @param offsetMapping The mapper from/to editing buffer to/from visible text. + */ + @JvmStatic + internal fun notifyFocusedRect( + value: TextFieldValue, + textDelegate: TextDelegate, + textLayoutResult: TextLayoutResult, + layoutCoordinates: LayoutCoordinates, + textInputSession: TextInputSession, + hasFocus: Boolean, + offsetMapping: OffsetMapping + ) { + if (!hasFocus) { + return + } + val focusOffsetInTransformed = offsetMapping.originalToTransformed(value.selection.max) + val bbox = when { + focusOffsetInTransformed < textLayoutResult.layoutInput.text.length -> { + textLayoutResult.getBoundingBox(focusOffsetInTransformed) + } + focusOffsetInTransformed != 0 -> { + textLayoutResult.getBoundingBox(focusOffsetInTransformed - 1) + } + else -> { // empty text. + val defaultSize = computeSizeForDefaultText( + textDelegate.style, + textDelegate.density, + textDelegate.fontFamilyResolver + ) + Rect(0f, 0f, 1.0f, defaultSize.height.toFloat()) + } + } + val globalLT = layoutCoordinates.localToRoot(Offset(bbox.left, bbox.top)) + + // TODO remove `Deprecated`, if it is removed in AOSP repository + @Suppress("DEPRECATION") + textInputSession.notifyFocusedRect( + Rect(Offset(globalLT.x, globalLT.y), Size(bbox.width, bbox.height)) + ) + } + /** * Called when edit operations are passed from TextInputService * diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt index 79cbf377f7e55..e137e1edb883b 100644 --- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt +++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt @@ -147,7 +147,20 @@ class TextInputSession( } } - @Suppress("DeprecatedCallableAddReplaceWith", "DEPRECATION") + /** + * Notify the focused rectangle to the system. + * + * The system can ignore this information or use it to show additional functionality near this rectangle. + * + * For example, desktop systems show a popup near the focused input area (for some languages). + * + * If the session is not open, no action will be performed. + * + * @param rect the rectangle that describes the boundaries on the screen that requires focus + * @return false if this session expired and no action was performed + */ + // TODO remove `Deprecated`, if it is removed in AOSP repository + @Suppress("DEPRECATION") @Deprecated("This method should not be called, used BringIntoViewRequester instead.") fun notifyFocusedRect(rect: Rect): Boolean = ensureOpenSession { platformTextInputService.notifyFocusedRect(rect) @@ -255,6 +268,14 @@ interface PlatformTextInputService { */ fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) + /** + * Notify the focused rectangle to the system. + * + * The system can ignore this information or use it to show additional functionality near this rectangle. + * + * For example, desktop systems show a popup near the focused input area (for some languages). + */ + // TODO remove `Deprecated`, if it is removed in AOSP repository @Deprecated("This method should not be called, used BringIntoViewRequester instead.") fun notifyFocusedRect(rect: Rect) { }