Implement settling logic to pane expansion anchor positions

Test: instrumentation tests
Bug: 327637983
Change-Id: I9ff9777409ef7ad5d18d013df4d7ed704eeb0b07
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt
index 4538a66..da49e44 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt
@@ -486,7 +486,7 @@
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 @Composable
-private fun SampleThreePaneScaffoldWithPaneExpansion(
+internal fun SampleThreePaneScaffoldWithPaneExpansion(
     paneExpansionState: PaneExpansionState,
     paneExpansionDragHandle: (@Composable (PaneExpansionState) -> Unit)? = null,
 ) {
@@ -509,6 +509,6 @@
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 @Composable
-private fun MockDragHandle(state: PaneExpansionState) {
+internal fun MockDragHandle(state: PaneExpansionState) {
     PaneExpansionDragHandle(state, MaterialTheme.colorScheme.outline)
 }
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 f738efb..25b7da3 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,13 +23,19 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
 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.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -164,12 +170,147 @@
         rule.onNodeWithTag("SecondaryPane").assertExists()
         rule.onNodeWithTag("TertiaryPane").assertDoesNotExist()
     }
+
+    @Test
+    fun threePaneScaffold_paneExpansionWithDragHandle_slowDraggingAndSettling() {
+        val mockPaneExpansionState = PaneExpansionState(anchors = MockPaneExpansionAnchors)
+        var mockDraggingPx = 0f
+        var expectedSettledOffsetPx = 0
+        lateinit var scope: CoroutineScope
+
+        rule.setContentWithSimulatedSize(
+            simulatedWidth = 1024.dp,
+            simulatedHeight = 800.dp
+        ) {
+            scope = rememberCoroutineScope()
+            mockDraggingPx = with(LocalDensity.current) { 200.dp.toPx() }
+            expectedSettledOffsetPx = with(LocalDensity.current) {
+                MockPaneExpansionMiddleAnchor.toPx().toInt()
+            }
+            SampleThreePaneScaffoldWithPaneExpansion(mockPaneExpansionState) {
+                MockDragHandle(it)
+            }
+        }
+
+        rule.runOnIdle {
+            mockPaneExpansionState.dispatchRawDelta(mockDraggingPx)
+            scope.launch {
+                mockPaneExpansionState.settleToAnchorIfNeeded(0F)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(mockPaneExpansionState.currentMeasuredDraggingOffset).isEqualTo(
+                expectedSettledOffsetPx
+            )
+        }
+    }
+
+    @Test
+    fun threePaneScaffold_paneExpansionWithDragHandle_fastDraggingAndSettling() {
+        val mockPaneExpansionState = PaneExpansionState(anchors = MockPaneExpansionAnchors)
+        var mockDraggingPx = 0f
+        lateinit var scope: CoroutineScope
+
+        rule.setContentWithSimulatedSize(
+            simulatedWidth = 1024.dp,
+            simulatedHeight = 800.dp
+        ) {
+            scope = rememberCoroutineScope()
+            mockDraggingPx = with(LocalDensity.current) { 200.dp.toPx() }
+            SampleThreePaneScaffoldWithPaneExpansion(mockPaneExpansionState) {
+                MockDragHandle(it)
+            }
+        }
+
+        rule.runOnIdle {
+            mockPaneExpansionState.dispatchRawDelta(mockDraggingPx)
+            scope.launch {
+                mockPaneExpansionState.settleToAnchorIfNeeded(400F)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(mockPaneExpansionState.currentMeasuredDraggingOffset).isEqualTo(
+                mockPaneExpansionState.maxExpansionWidth
+            )
+        }
+    }
+
+    @Test
+    fun threePaneScaffold_paneExpansionWithDragHandle_draggingAndSettlingCloseToLeftEdge() {
+        val mockPaneExpansionState = PaneExpansionState(anchors = MockPaneExpansionAnchors)
+        var mockDraggingDp = 0f
+        lateinit var scope: CoroutineScope
+
+        rule.setContentWithSimulatedSize(
+            simulatedWidth = 1024.dp,
+            simulatedHeight = 800.dp
+        ) {
+            scope = rememberCoroutineScope()
+            mockDraggingDp = with(LocalDensity.current) { -360.dp.toPx() }
+            SampleThreePaneScaffoldWithPaneExpansion(mockPaneExpansionState) {
+                MockDragHandle(it)
+            }
+        }
+
+        rule.runOnIdle {
+            mockPaneExpansionState.dispatchRawDelta(mockDraggingDp)
+            scope.launch {
+                mockPaneExpansionState.settleToAnchorIfNeeded(-200F)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(mockPaneExpansionState.currentMeasuredDraggingOffset).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun threePaneScaffold_paneExpansionWithDragHandle_draggingAndSettlingCloseToRightEdge() {
+        val mockPaneExpansionState = PaneExpansionState(anchors = MockPaneExpansionAnchors)
+        var mockDraggingDp = 0f
+        lateinit var scope: CoroutineScope
+
+        rule.setContentWithSimulatedSize(
+            simulatedWidth = 1024.dp,
+            simulatedHeight = 800.dp
+        ) {
+            scope = rememberCoroutineScope()
+            mockDraggingDp = with(LocalDensity.current) { 640.dp.toPx() }
+            SampleThreePaneScaffoldWithPaneExpansion(mockPaneExpansionState) {
+                MockDragHandle(it)
+            }
+        }
+
+        rule.runOnIdle {
+            mockPaneExpansionState.dispatchRawDelta(mockDraggingDp)
+            scope.launch {
+                mockPaneExpansionState.settleToAnchorIfNeeded(200F)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(mockPaneExpansionState.currentMeasuredDraggingOffset).isEqualTo(
+                mockPaneExpansionState.maxExpansionWidth
+            )
+        }
+    }
 }
 
 private val MockScaffoldDirective = PaneScaffoldDirective.Default
 
 internal const val ThreePaneScaffoldTestTag = "SampleThreePaneScaffold"
 
+private val MockPaneExpansionMiddleAnchor = 400.dp
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private val MockPaneExpansionAnchors = listOf(
+    PaneExpansionAnchor(percentage = 0),
+    PaneExpansionAnchor(startOffset = MockPaneExpansionMiddleAnchor),
+    PaneExpansionAnchor(percentage = 100),
+)
+
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 @Composable
 private fun SampleThreePaneScaffold(scaffoldValue: ThreePaneScaffoldValue) {
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.android.kt b/compose/material3/adaptive/adaptive-layout/src/androidMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.android.kt
new file mode 100644
index 0000000..6377185
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/androidMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.android.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.adaptive.layout
+
+import androidx.compose.foundation.systemGestureExclusion as androidSystemGestureExclusion
+import androidx.compose.ui.Modifier
+
+internal actual fun Modifier.systemGestureExclusion(): Modifier =
+    this.androidSystemGestureExclusion()
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.kt
index 5cf1465..8382ba4 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.kt
@@ -57,4 +57,10 @@
 
 @ExperimentalMaterial3AdaptiveApi
 internal fun Modifier.paneExpansionDragHandle(state: PaneExpansionState): Modifier =
-    this.draggable(state, Orientation.Horizontal)
+    this.draggable(
+        state = state,
+        orientation = Orientation.Horizontal,
+        onDragStopped = { velocity -> state.settleToAnchorIfNeeded(velocity) }
+    ).systemGestureExclusion()
+
+internal expect fun Modifier.systemGestureExclusion(): Modifier
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt
index 8ae6844..10b24e4 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionState.kt
@@ -16,22 +16,35 @@
 
 package androidx.compose.material3.adaptive.layout
 
+import androidx.annotation.IntRange
+import androidx.annotation.VisibleForTesting
+import androidx.collection.IntList
+import androidx.collection.MutableIntList
+import androidx.collection.emptyIntList
+import androidx.compose.animation.core.animate
 import androidx.compose.foundation.MutatePriority
 import androidx.compose.foundation.MutatorMutex
 import androidx.compose.foundation.gestures.DragScope
 import androidx.compose.foundation.gestures.DraggableState
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.isSpecified
+import kotlin.math.abs
 import kotlinx.coroutines.coroutineScope
 
 @ExperimentalMaterial3AdaptiveApi
 @Stable
-internal class PaneExpansionState : DraggableState {
+internal class PaneExpansionState(
+    internal val anchors: List<PaneExpansionAnchor> = emptyList()
+) : DraggableState {
     private var firstPaneWidthState by mutableIntStateOf(UnspecifiedWidth)
     private var firstPanePercentageState by mutableFloatStateOf(Float.NaN)
     private var currentDraggingOffsetState by mutableIntStateOf(UnspecifiedWidth)
@@ -56,29 +69,33 @@
     internal var currentDraggingOffset
         get() = currentDraggingOffsetState
         private set(value) {
+            val coercedValue = value.coerceIn(0, maxExpansionWidth)
             if (value == currentDraggingOffsetState) {
                 return
             }
-            currentDraggingOffsetState = value
+            currentDraggingOffsetState = coercedValue
+            currentMeasuredDraggingOffset = coercedValue
         }
 
-    // Use this field to store the dragging offset decided by measuring instead of dragging to
-    // prevent redundant re-composition.
-    internal var currentMeasuredDraggingOffset = UnspecifiedWidth
-
     internal var isDragging by mutableStateOf(false)
         private set
 
+    internal var isSettling by mutableStateOf(false)
+        private set
+
+    internal val isDraggingOrSettling get() = isDragging || isSettling
+
+    @VisibleForTesting
     internal var maxExpansionWidth = 0
-        set(value) {
-            if (field == value) {
-                return
-            }
-            field = value
-            if (firstPaneWidth != UnspecifiedWidth) {
-                firstPaneWidth = firstPaneWidth
-            }
-        }
+        private set
+
+    // Use this field to store the dragging offset decided by measuring instead of dragging to
+    // prevent redundant re-composition.
+    @VisibleForTesting
+    internal var currentMeasuredDraggingOffset = UnspecifiedWidth
+        private set
+
+    private var anchorPositions: IntList = emptyIntList()
 
     private val dragScope: DragScope = object : DragScope {
         override fun dragBy(pixels: Float): Unit = dispatchRawDelta(pixels)
@@ -95,9 +112,7 @@
         if (currentMeasuredDraggingOffset == UnspecifiedWidth) {
             return
         }
-        currentDraggingOffset =
-            (currentMeasuredDraggingOffset + delta).toInt().coerceIn(0, maxExpansionWidth)
-        currentMeasuredDraggingOffset = currentDraggingOffset
+        currentDraggingOffset = (currentMeasuredDraggingOffset + delta).toInt()
     }
 
     override suspend fun drag(
@@ -109,7 +124,121 @@
         isDragging = false
     }
 
+    internal fun onMeasured(measuredWidth: Int, density: Density) {
+        if (measuredWidth == maxExpansionWidth) {
+            return
+        }
+        maxExpansionWidth = measuredWidth
+        if (firstPaneWidth != UnspecifiedWidth) {
+            firstPaneWidth = firstPaneWidth
+        }
+        anchorPositions = anchors.toPositions(measuredWidth, density)
+    }
+
+    internal fun onExpansionOffsetMeasured(measuredOffset: Int) {
+        currentMeasuredDraggingOffset = measuredOffset
+    }
+
+    internal suspend fun settleToAnchorIfNeeded(velocity: Float) {
+        val currentAnchorPositions = anchorPositions
+        if (currentAnchorPositions.isEmpty()) {
+            return
+        }
+        dragMutex.mutate(MutatePriority.PreventUserInput) {
+            isSettling = true
+            // TODO(conradchen): Use the right animation spec here.
+            animate(
+                currentMeasuredDraggingOffset.toFloat(),
+                currentAnchorPositions.getPositionOfTheClosestAnchor(
+                    currentMeasuredDraggingOffset,
+                    velocity
+                ).toFloat(),
+                velocity,
+            ) { value, _ ->
+                currentDraggingOffset = value.toInt()
+            }
+            isSettling = false
+        }
+    }
+
+    private fun IntList.getPositionOfTheClosestAnchor(
+        currentPosition: Int,
+        velocity: Float
+    ): Int = minBy(
+        when {
+            velocity >= AnchoringVelocityThreshold -> {
+                { anchorPosition: Int ->
+                    val delta = anchorPosition - currentPosition
+                    if (delta < 0) Int.MAX_VALUE else delta
+                }
+            }
+            velocity <= -AnchoringVelocityThreshold -> {
+                { anchorPosition: Int ->
+                    val delta = currentPosition - anchorPosition
+                    if (delta < 0) Int.MAX_VALUE else delta
+                }
+            }
+            else -> {
+                { anchorPosition: Int ->
+                    abs(currentPosition - anchorPosition)
+                }
+            }
+        }
+    )
+
     companion object {
         const val UnspecifiedWidth = -1
+        private const val AnchoringVelocityThreshold = 200F
     }
 }
+
+@ExperimentalMaterial3AdaptiveApi
+@Immutable
+internal class PaneExpansionAnchor private constructor(
+    val percentage: Int,
+    val startOffset: Dp // TODO(conradchen): confirm RTL support
+) {
+    constructor(@IntRange(0, 100) percentage: Int) : this(percentage, Dp.Unspecified)
+
+    constructor(startOffset: Dp) : this(Int.MIN_VALUE, startOffset)
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private fun List<PaneExpansionAnchor>.toPositions(
+    maxExpansionWidth: Int,
+    density: Density
+): IntList {
+    val anchors = MutableIntList(size)
+    @Suppress("ListIterator") // Not necessarily a random-accessible list
+    forEach { anchor ->
+        if (anchor.startOffset.isSpecified) {
+            val position = with(density) { anchor.startOffset.toPx() }.toInt().let {
+                if (it < 0) maxExpansionWidth + it else it
+            }
+            if (position in 0..maxExpansionWidth) {
+                anchors.add(position)
+            }
+        } else {
+            anchors.add(maxExpansionWidth * anchor.percentage / 100)
+        }
+    }
+    anchors.sort()
+    return anchors
+}
+
+private fun <T : Comparable<T>> IntList.minBy(selector: (Int) -> T): Int {
+    if (isEmpty()) {
+        throw NoSuchElementException()
+    }
+    var minElem = this[0]
+    var minValue = selector(minElem)
+    for (i in 1 until size) {
+        val elem = this[i]
+        val value = selector(elem)
+        if (minValue > value) {
+            minElem = elem
+            minValue = value
+        }
+    }
+    return minElem
+}
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 6e92343..096469a 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
@@ -308,7 +308,7 @@
                 constraints.maxHeight
             )
             if (!isLookingAhead) {
-                paneExpansionState.maxExpansionWidth = outerBounds.width
+                paneExpansionState.onMeasured(outerBounds.width, this@measure)
             }
 
             if (!paneExpansionState.isUnspecified() && visiblePanes.size == 2) {
@@ -318,7 +318,7 @@
                     // Respect the user dragging result if there's any
                     val halfSpacerSize = verticalSpacerSize / 2
                     if (paneExpansionState.currentDraggingOffset <= halfSpacerSize) {
-                        val bounds = if (paneExpansionState.isDragging) {
+                        val bounds = if (paneExpansionState.isDraggingOrSettling) {
                             outerBounds.copy(
                                 left = paneExpansionState.currentDraggingOffset * 2 +
                                     outerBounds.left
@@ -333,7 +333,7 @@
                         )
                     } else if (paneExpansionState.currentDraggingOffset >=
                         outerBounds.width - halfSpacerSize) {
-                        val bounds = if (paneExpansionState.isDragging) {
+                        val bounds = if (paneExpansionState.isDraggingOrSettling) {
                             outerBounds.copy(
                                 right = paneExpansionState.currentDraggingOffset * 2 -
                                     outerBounds.right
@@ -497,7 +497,7 @@
 
             if (visiblePanes.size == 2 && dragHandleMeasurables.isNotEmpty()) {
                 val handleOffsetX =
-                    if (!paneExpansionState.isDragging ||
+                    if (!paneExpansionState.isDraggingOrSettling ||
                         paneExpansionState.currentDraggingOffset ==
                         PaneExpansionState.UnspecifiedWidth) {
                         val spacerMiddleOffset = getSpacerMiddleOffsetX(
@@ -505,7 +505,7 @@
                             visiblePanes[1]
                         )
                         if (!isLookingAhead) {
-                            paneExpansionState.currentMeasuredDraggingOffset = spacerMiddleOffset
+                            paneExpansionState.onExpansionOffsetMeasured(spacerMiddleOffset)
                         }
                         spacerMiddleOffset
                     } else {
@@ -519,8 +519,7 @@
                     handleOffsetX
                 )
             } else if (!isLookingAhead) {
-                paneExpansionState.currentMeasuredDraggingOffset =
-                    PaneExpansionState.UnspecifiedWidth
+                paneExpansionState.onExpansionOffsetMeasured(PaneExpansionState.UnspecifiedWidth)
             }
 
             // Place the hidden panes to ensure a proper motion at the AnimatedVisibility,
diff --git a/compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt b/compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt
new file mode 100644
index 0000000..d4a754b
--- /dev/null
+++ b/compose/material3/adaptive/adaptive-layout/src/desktopMain/kotlin/androidx/compose/material3/adaptive/layout/PaneExpansionDragHandle.desktop.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.adaptive.layout
+
+import androidx.compose.ui.Modifier
+
+internal actual fun Modifier.systemGestureExclusion(): Modifier = this