Fix TabRow inverted indicator for RtL layout

Bug: b/359245765
Test: run TabTest.kt
Change-Id: I73661a6b182ae5a5782ca8d827802a8e1e29237d
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TabTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TabTest.kt
index 57d4a35..f430e5c 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TabTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TabTest.kt
@@ -40,6 +40,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.Role
@@ -64,6 +65,7 @@
 import androidx.compose.ui.test.onParent
 import androidx.compose.ui.test.performClick
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.height
 import androidx.compose.ui.unit.sp
@@ -302,6 +304,62 @@
     }
 
     @Test
+    fun fixedTabRowRt_indicatorPosition_rtl() {
+        val indicatorHeight = 1.dp
+
+        rule.setMaterialContent(lightColorScheme()) {
+            var state by remember { mutableStateOf(0) }
+            val titles = listOf("TAB 1", "TAB 2")
+
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                Box(Modifier.testTag("tabRow")) {
+                    SecondaryTabRow(
+                        selectedTabIndex = state,
+                        indicator = {
+                            Box(
+                                Modifier.tabIndicatorOffset(state)
+                                    .fillMaxWidth()
+                                    .height(indicatorHeight)
+                                    .background(color = Color.Red)
+                                    .testTag("indicator")
+                            )
+                        },
+                    ) {
+                        titles.forEachIndexed { index, title ->
+                            Tab(
+                                selected = state == index,
+                                onClick = { state = index },
+                                text = { Text(title) },
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        val tabRowBounds = rule.onNodeWithTag("tabRow").getUnclippedBoundsInRoot()
+
+        rule
+            .onNodeWithTag("indicator", true)
+            .assertPositionInRootIsEqualTo(
+                expectedLeft = (tabRowBounds.width / 2),
+                expectedTop = tabRowBounds.height - indicatorHeight,
+            )
+
+        // Click the second tab
+        rule.onAllNodes(isSelectable())[1].performClick()
+
+        // Indicator should now be placed in the bottom left of the second tab.
+        // For RTL layout, its x coordinate should be at the start of the TabRow.
+        rule
+            .onNodeWithTag("indicator", true)
+            .assertPositionInRootIsEqualTo(
+                expectedLeft = 0.dp,
+                expectedTop = tabRowBounds.height - indicatorHeight,
+            )
+    }
+
+    @Test
     fun tabRow_indicatorHeight() {
         val indicatorHeight = 1.dp
         val titles = listOf("TAB 1", "TAB 2")
@@ -550,6 +608,69 @@
     }
 
     @Test
+    fun scrollableTabRow_indicatorPosition_rtl() {
+        val indicatorHeight = 1.dp
+        val minimumTabWidth = 90.dp
+
+        rule.setMaterialContent(lightColorScheme()) {
+            var state by remember { mutableStateOf(0) }
+            val titles = listOf("TAB 1", "TAB 2")
+
+            val indicator: @Composable TabIndicatorScope.(index: Int) -> Unit = { index ->
+                Box(
+                    Modifier.tabIndicatorOffset(index)
+                        .fillMaxWidth()
+                        .height(indicatorHeight)
+                        .background(color = Color.Red)
+                        .testTag("indicator")
+                )
+            }
+
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                Box {
+                    SecondaryScrollableTabRow(
+                        modifier = Modifier.testTag("tabRow"),
+                        selectedTabIndex = state,
+                        indicator = { indicator(state) },
+                    ) {
+                        titles.forEachIndexed { index, title ->
+                            Tab(
+                                selected = state == index,
+                                onClick = { state = index },
+                                text = { Text(title) },
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        val tabRowBounds = rule.onNodeWithTag("tabRow").getUnclippedBoundsInRoot()
+        val tabRowPadding = 52.dp
+        // Indicator should be placed in the bottom left of the first tab
+        rule
+            .onNodeWithTag("indicator", true)
+            .assertPositionInRootIsEqualTo(
+                // Tabs in a scrollable tab row are offset 52.dp from each end
+                expectedLeft = tabRowBounds.width - tabRowPadding - minimumTabWidth,
+                expectedTop = tabRowBounds.height - indicatorHeight,
+            )
+
+        // Click the second tab
+        rule.onAllNodes(isSelectable())[1].performClick()
+
+        // Indicator should now be placed in the bottom left of the second tab.
+        // For RTL layout, its x coordinate should be at the start of the TabRow.
+        rule
+            .onNodeWithTag("indicator", true)
+            .assertPositionInRootIsEqualTo(
+                expectedLeft =
+                    tabRowBounds.width - tabRowPadding - minimumTabWidth - minimumTabWidth,
+                expectedTop = tabRowBounds.height - indicatorHeight,
+            )
+    }
+
+    @Test
     fun scrollableTabRow_dividerHeight() {
         rule.setMaterialContent(lightColorScheme()) {
             val titles = listOf("TAB 1", "TAB 2")
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
index 0b191f1..7ed7cc6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
@@ -67,6 +67,7 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.fastFold
 import androidx.compose.ui.util.fastForEach
@@ -888,7 +889,7 @@
     var tabPositionsState: State<List<TabPosition>>,
     var selectedTabIndex: Int,
     var followContentSize: Boolean,
-    var animationSpec: FiniteAnimationSpec<Dp>
+    var animationSpec: FiniteAnimationSpec<Dp>,
 ) : Modifier.Node(), LayoutModifierNode {
 
     private var offsetAnimatable: Animatable<Dp, AnimationVector1D>? = null
@@ -939,7 +940,12 @@
             initialOffset = indicatorOffset
         }
 
-        val offset = offsetAnimatable?.value ?: indicatorOffset
+        val offset =
+            if (layoutDirection == LayoutDirection.Ltr) {
+                offsetAnimatable?.value ?: indicatorOffset
+            } else {
+                -(offsetAnimatable?.value ?: indicatorOffset)
+            }
 
         val width = widthAnimatable?.value ?: currentTabWidth