Fix the issue that pane content states are not saved

The content inside a pane has to be wrapped inside SaveableStateHolder
as they can be removed from the layout hierarchy when navigation inside
a pane scaffold happens. Introduce this as a default behavior of
AnimatedPane.

Test: instrumentation test
Bug: 343845141
Bug: 374064147
Change-Id: I98399f90e102a266b24c502bafa9b19f37e6bc8b
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
index 3677863..5abcd21 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
@@ -23,11 +23,14 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.StateRestorationTester
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.dp
@@ -399,6 +402,62 @@
             }
         }
     }
+
+    @Test
+    fun threePaneScaffold_afterPaneSwitching_paneStatesAreSaved() {
+        val restorationTester = StateRestorationTester(rule)
+        val scaffoldValueSecondaryShown =
+            ThreePaneScaffoldValue(
+                primary = PaneAdaptedValue.Expanded,
+                secondary = PaneAdaptedValue.Expanded,
+                tertiary = PaneAdaptedValue.Hidden
+            )
+        val scaffoldValueSecondaryHidden =
+            ThreePaneScaffoldValue(
+                primary = PaneAdaptedValue.Expanded,
+                secondary = PaneAdaptedValue.Hidden,
+                tertiary = PaneAdaptedValue.Hidden
+            )
+
+        var increment = 0
+        var numberOnSecondaryPane = -1
+        var restorableNumberOnSecondaryPane = -1
+        var testScaffoldValue by mutableStateOf(scaffoldValueSecondaryShown)
+
+        restorationTester.setContent {
+            SampleThreePaneScaffold(
+                scaffoldDirective = MockScaffoldDirective,
+                scaffoldValue = testScaffoldValue,
+                paneOrder = ListDetailPaneScaffoldDefaults.PaneOrder,
+                secondaryContent = {
+                    numberOnSecondaryPane = remember { increment++ }
+                    restorableNumberOnSecondaryPane = rememberSaveable { increment++ }
+                }
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(numberOnSecondaryPane).isEqualTo(0)
+            assertThat(restorableNumberOnSecondaryPane).isEqualTo(1)
+            testScaffoldValue = scaffoldValueSecondaryHidden
+        }
+
+        // wait for the screen switch to apply
+        rule.runOnIdle {
+            numberOnSecondaryPane = -1
+            restorableNumberOnSecondaryPane = -1
+        }
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        // switch back to screen1
+        rule.runOnIdle { testScaffoldValue = scaffoldValueSecondaryShown }
+
+        rule.runOnIdle {
+            assertThat(numberOnSecondaryPane).isEqualTo(2)
+            assertThat(restorableNumberOnSecondaryPane).isEqualTo(1)
+        }
+    }
 }
 
 private val MockScaffoldDirective = PaneScaffoldDirective.Default
@@ -434,6 +493,9 @@
     paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? =
         null,
     paneExpansionState: PaneExpansionState = PaneExpansionState(),
+    primaryContent: (@Composable ThreePaneScaffoldScope.() -> Unit) = {},
+    secondaryContent: (@Composable ThreePaneScaffoldScope.() -> Unit) = {},
+    tertiaryContent: (@Composable ThreePaneScaffoldScope.() -> Unit) = {}
 ) {
     ThreePaneScaffold(
         modifier = Modifier.fillMaxSize().testTag(ThreePaneScaffoldTestTag),
@@ -447,7 +509,9 @@
                 Surface(
                     modifier = Modifier.fillMaxSize(),
                     color = MaterialTheme.colorScheme.secondary
-                ) {}
+                ) {
+                    secondaryContent()
+                }
             }
         },
         tertiaryPane = {
@@ -455,12 +519,16 @@
                 Surface(
                     modifier = Modifier.fillMaxSize(),
                     color = MaterialTheme.colorScheme.tertiary
-                ) {}
+                ) {
+                    tertiaryContent()
+                }
             }
         }
     ) {
         AnimatedPane(modifier = Modifier.testTag(tag = "PrimaryPane")) {
-            Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.primary) {}
+            Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.primary) {
+                primaryContent()
+            }
         }
     }
 }
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
index 49dd66a..3e4c481 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
@@ -151,7 +151,11 @@
                 enter = enterTransition,
                 exit = exitTransition
             ) {
-                AnimatedPaneScope.create(this).content()
+                (scope as ThreePaneScaffoldPaneScopeImpl).saveableStateHolder.SaveableStateProvider(
+                    paneRole.toString()
+                ) {
+                    AnimatedPaneScope.create(this).content()
+                }
             }
         }
     }
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
index 0607f9f..e205ada 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
@@ -20,6 +20,7 @@
 import androidx.compose.animation.core.Transition
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.runtime.saveable.SaveableStateHolder
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.LookaheadScope
 import androidx.compose.ui.layout.Measurable
@@ -139,7 +140,11 @@
     val paneMotion: PaneMotion
 }
 
-internal abstract class PaneScaffoldScopeImpl : PaneScaffoldScope {
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+internal abstract class PaneScaffoldScopeImpl(
+    // TODO(conradchen): Add it to PaneScaffoldScope API in 1.2
+    val saveableStateHolder: SaveableStateHolder
+) : PaneScaffoldScope {
     override fun Modifier.preferredWidth(width: Dp): Modifier {
         require(width == Dp.Unspecified || width > 0.dp) { "invalid width" }
         return this.then(PreferredWidthElement(width))
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
index cc543e20..cb5757f 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
@@ -27,6 +27,7 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Modifier
@@ -194,9 +195,13 @@
                 scaffoldStateTransition = currentTransition
             }
 
+    val stateHolder = rememberSaveableStateHolder()
+
     LookaheadScope {
         val scaffoldScope =
-            remember(currentTransition, this) { ThreePaneScaffoldScopeImpl(transitionScope, this) }
+            remember(currentTransition, this) {
+                ThreePaneScaffoldScopeImpl(transitionScope, this, stateHolder)
+            }
         with(LocalThreePaneScaffoldOverride.current) {
             ThreePaneScaffoldOverrideContext(
                     modifier = modifier,
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScope.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScope.kt
index 0bdd476..90753f6 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScope.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScope.kt
@@ -25,6 +25,7 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.SaveableStateHolder
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.LookaheadScope
@@ -46,12 +47,13 @@
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 internal class ThreePaneScaffoldScopeImpl(
     transitionScope: PaneScaffoldTransitionScope<ThreePaneScaffoldRole, ThreePaneScaffoldValue>,
-    lookaheadScope: LookaheadScope
+    lookaheadScope: LookaheadScope,
+    saveableStateHolder: SaveableStateHolder,
 ) :
     ThreePaneScaffoldScope,
     PaneScaffoldTransitionScope<ThreePaneScaffoldRole, ThreePaneScaffoldValue> by transitionScope,
     LookaheadScope by lookaheadScope,
-    PaneScaffoldScopeImpl() {
+    PaneScaffoldScopeImpl(saveableStateHolder) {
 
     @ExperimentalMaterial3AdaptiveApi
     override fun Modifier.paneExpansionDraggable(
@@ -92,6 +94,8 @@
     scaffoldScope: ThreePaneScaffoldScope,
 ) : ThreePaneScaffoldPaneScope, ThreePaneScaffoldScope by scaffoldScope {
     override var paneMotion: PaneMotion by mutableStateOf(PaneMotion.ExitToLeft)
+    // TODO(conradchen): Remove this when it goes to public API of PaneScaffoldScope
+    val saveableStateHolder = (scaffoldScope as ThreePaneScaffoldScopeImpl).saveableStateHolder
 }
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)