From 6c2eca16bbdb347d0b85c36358b1bd5232055fe7 Mon Sep 17 00:00:00 2001 From: Vladimir Mazunin Date: Tue, 11 Jun 2024 15:30:57 +0400 Subject: [PATCH] Fix initial cursor position in empty TextField when TextAlignment is set explicitly (#1354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After focusing on the textfield and before entering any text, now blinking cursor will be positioned in accordance with TextAlignment. Fixes https://youtrack.jetbrains.com/issue/COMPOSE-1360/ Fixes https://github.com/JetBrains/compose-multiplatform/issues/2711 Fixes https://github.com/JetBrains/compose-multiplatform/issues/3098 Fixes https://github.com/JetBrains/compose-multiplatform/issues/4611 ## Testing Manual testing. ## Release Notes ### Fixes - Multiple Platforms - Fix initial cursor position in the empty `TextField` with explicitly set `TextAlignment`. ## Google CLA You need to sign the Google Contributor’s License Agreement at https://cla.developers.google.com/. This is needed since we synchronise most of the code with Google’s AOSP repository. Signing this agreement allows us to synchronise code from your Pull Requests as well. --------- Co-authored-by: Ivan Matkov --- .../compose/ui/text/SkiaParagraph.skiko.kt | 13 +++- .../compose/ui/text/SkikoParagraphTest.kt | 62 +++++++++++++++++-- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/SkiaParagraph.skiko.kt b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/SkiaParagraph.skiko.kt index 61b867eb0bca3..12a7f56872a16 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/SkiaParagraph.skiko.kt +++ b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/SkiaParagraph.skiko.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.text.platform.SkiaParagraphIntrinsics import androidx.compose.ui.text.platform.cursorHorizontalPosition import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.text.style.ResolvedTextDirection +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.isUnspecified @@ -256,7 +257,7 @@ internal class SkiaParagraph( val isRtl = paragraphIntrinsics.textDirection == ResolvedTextDirection.Rtl val isLtr = !isRtl return when { - prevBox == null && nextBox == null -> if (isRtl) width else 0f + prevBox == null && nextBox == null -> getAlignedStartingPosition(isRtl) prevBox == null -> nextBox!!.cursorHorizontalPosition(true) nextBox == null -> prevBox.cursorHorizontalPosition() nextBox.direction == prevBox.direction -> nextBox.cursorHorizontalPosition(true) @@ -269,6 +270,16 @@ internal class SkiaParagraph( } } + private fun getAlignedStartingPosition(isRtl: Boolean): Float = + when (layouter.textStyle.textAlign) { + TextAlign.Left -> 0f + TextAlign.Right -> width + TextAlign.Center -> width / 2 + TextAlign.Start -> if (isRtl) width else 0f + TextAlign.End -> if (isRtl) 0f else width + else -> 0f + } + private var _lineMetrics: Array? = null private val lineMetrics: Array get() { diff --git a/compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/SkikoParagraphTest.kt b/compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/SkikoParagraphTest.kt index d7750a99b93a4..ee0c0e97db561 100644 --- a/compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/SkikoParagraphTest.kt +++ b/compose/ui/ui-text/src/skikoTest/kotlin/androidx/compose/ui/text/SkikoParagraphTest.kt @@ -17,6 +17,8 @@ package androidx.compose.ui.text import androidx.compose.ui.text.font.createFontFamilyResolver +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import kotlin.test.Ignore @@ -28,6 +30,7 @@ import kotlin.test.assertFailsWith class SkikoParagraphTest { private val fontFamilyResolver = createFontFamilyResolver() private val defaultDensity = Density(density = 1f) + private val maxWidthConstraint = 1000 @Test fun getWordBoundary_out_of_boundary_too_small() { @@ -315,7 +318,8 @@ class SkikoParagraphTest { fun getWordBoundary_multichar() { // "ab 𐐔𐐯𐑅𐐨𐑉𐐯𐐻 cd" - example of multi-char code units // | (offset=3) | (offset=6) - val text = "ab \uD801\uDC14\uD801\uDC2F\uD801\uDC45\uD801\uDC28\uD801\uDC49\uD801\uDC2F\uD801\uDC3B cd" + val text = + "ab \uD801\uDC14\uD801\uDC2F\uD801\uDC45\uD801\uDC28\uD801\uDC49\uD801\uDC2F\uD801\uDC3B cd" val paragraph = simpleParagraph(text) assertEquals( @@ -324,10 +328,60 @@ class SkikoParagraphTest { ) } - private fun simpleParagraph(text: String) = Paragraph( + @Test + fun getHorizontalPosition_cursor_empty_textfield_ltr_start_alignment() { + val paragraph = simpleParagraph("", TextStyle(textAlign = TextAlign.Start, textDirection = TextDirection.Ltr)) + val cursorHorizontalPosition: Float = paragraph.getHorizontalPosition(0, false) + assertEquals(0f, cursorHorizontalPosition) + } + + @Test + fun getHorizontalPosition_cursor_empty_textfield_ltr_end_alignment() { + val paragraph = simpleParagraph("", TextStyle(textAlign = TextAlign.End, textDirection = TextDirection.Ltr)) + val cursorHorizontalPosition = paragraph.getHorizontalPosition(0, false) + assertEquals(maxWidthConstraint.toFloat(), cursorHorizontalPosition) + } + + @Test + fun getHorizontalPosition_cursor_empty_textfield_center_alignment() { + val paragraph = simpleParagraph("", TextStyle(textAlign = TextAlign.Center)) + val cursorHorizontalPosition = paragraph.getHorizontalPosition(0, false) + assertEquals((maxWidthConstraint / 2).toFloat(), cursorHorizontalPosition) + } + + @Test + fun getHorizontalPosition_cursor_empty_textfield_rtl_start_alignment() { + val paragraph = simpleParagraph("", TextStyle(textAlign = TextAlign.Start, textDirection = TextDirection.Rtl)) + val cursorHorizontalPosition: Float = paragraph.getHorizontalPosition(0, false) + assertEquals(maxWidthConstraint.toFloat(), cursorHorizontalPosition) + } + + @Test + fun getHorizontalPosition_cursor_empty_textfield_rtl_end_alignment() { + val paragraph = simpleParagraph("", TextStyle(textAlign = TextAlign.End, textDirection = TextDirection.Rtl)) + val cursorHorizontalPosition = paragraph.getHorizontalPosition(0, false) + assertEquals(0f, cursorHorizontalPosition) + } + + @Test + fun getHorizontalPosition_cursor_empty_textfield_left_alignment() { + val paragraph = simpleParagraph("", TextStyle(textAlign = TextAlign.Start)) + val cursorHorizontalPosition: Float = paragraph.getHorizontalPosition(0, false) + assertEquals(0f, cursorHorizontalPosition) + } + + @Test + fun getHorizontalPosition_cursor_empty_textfield_right_alignment() { + val paragraph = simpleParagraph("", TextStyle(textAlign = TextAlign.End)) + val cursorHorizontalPosition = paragraph.getHorizontalPosition(0, false) + assertEquals(maxWidthConstraint.toFloat(), cursorHorizontalPosition) + } + + + private fun simpleParagraph(text: String, textStyle: TextStyle = TextStyle()) = Paragraph( text = text, - style = TextStyle(), - constraints = Constraints(maxWidth = 1000), + style = textStyle, + constraints = Constraints(maxWidth = maxWidthConstraint), density = defaultDensity, fontFamilyResolver = fontFamilyResolver )