Add minimum touch target size for Semantics and Pointer Input

Bug: 189106692

Minimum touch target size has been added to ViewConfiguration
and used in pointer input and accessibility onClick() to
ensure that clickable containers are accessible.

When a touch is not a direct hit, the nearest target that
hits in the minimum touch target region is considered hit.
This will affect hit testing performance to some extent, but
shouldn't be an order of magnitude different.

RelNote: "Added minimum touch target size to ViewConfiguration
for use in semantics and pointer input to ensure accessiblity."

Test: new tests

Change-Id: Ie861ca1fcdbfcc9455352fc3a459d5734d5d57cc
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
index 35f18d6..4527c60 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
@@ -554,7 +554,7 @@
                     } else {
                         painterId.value = R.drawable.ic_vector_square_asset_test
                     }
-                },
+                }.size(50.dp),
                 contentScale = ContentScale.FillBounds
             )
         }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
index d553329..834b038 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
@@ -412,7 +412,10 @@
             this.role = role
         }
         // b/156468846:  add long click semantics and double click if needed
-        onClick(action = { onClick(); true }, label = onClickLabel)
+        onClick(
+            action = { onClick(); true },
+            label = onClickLabel
+        )
         if (onLongClick != null) {
             onLongClick(action = { onLongClick(); true }, label = onLongClickLabel)
         }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index a577d7c2..e0614a5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -764,7 +764,9 @@
                     }
 
                     if (
-                        event.changes.fastAny { it.consumed.downChange || it.isOutOfBounds(size) }
+                        event.changes.fastAny {
+                            it.consumed.downChange || it.isOutOfBounds(size, extendedTouchPadding)
+                        }
                     ) {
                         finished = true // Canceled
                     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
index eef7db7..0e65d0a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
@@ -334,7 +334,9 @@
                 change.consumeDownChange()
                 channel.trySend(Up(change.position, change.uptimeMillis))
             } else if (
-                event.changes.fastAny { it.consumed.downChange || it.isOutOfBounds(size) }
+                event.changes.fastAny {
+                    it.consumed.downChange || it.isOutOfBounds(size, extendedTouchPadding)
+                }
             ) {
                 channel.trySend(Cancel)
             } else {
@@ -415,7 +417,10 @@
             return event.changes[0]
         }
 
-        if (event.changes.fastAny { it.consumed.downChange || it.isOutOfBounds(size) }) {
+        if (event.changes.fastAny {
+            it.consumed.downChange || it.isOutOfBounds(size, extendedTouchPadding)
+        }
+        ) {
             return null // Canceled
         }
 
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
index acba3a7..32eb24d 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
@@ -142,7 +142,7 @@
         val event = awaitPointerEvent()
         val change = event.changes[0]
         if (change.changedToUp()) {
-            return if (change.isOutOfBounds(size)) {
+            return if (change.isOutOfBounds(size, extendedTouchPadding)) {
                 null
             } else {
                 event
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchTest.kt
index 3bbb7dd..2ed6eac 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchTest.kt
@@ -316,11 +316,19 @@
         rule.onNodeWithTag("2").assertIsOff()
     }
 
-    private fun materialSizesTestForValue(checked: Boolean) {
+    private fun materialSizesTestForValue(checked: Boolean) = with(rule.density) {
+        // The padding should be 2 DP, but we round to pixels when determining layout
+        val paddingInPixels = 2.dp.roundToPx()
+
+        // Convert back to DP so that we have an exact DP value to work with. We don't
+        // want to multiply the error by two (one for each padding), so we get the exact
+        // padding based on the expected pixels consumed by the padding.
+        val paddingInDp = paddingInPixels.toDp()
+
         rule.setMaterialContentForSizeAssertions {
             Switch(checked = checked, onCheckedChange = {}, enabled = false)
         }
-            .assertWidthIsEqualTo(34.dp + 2.dp * 2)
-            .assertHeightIsEqualTo(20.dp + 2.dp * 2)
+            .assertWidthIsEqualTo(34.dp + paddingInDp * 2)
+            .assertHeightIsEqualTo(20.dp + paddingInDp * 2)
     }
 }
diff --git a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/TestViewConfiguration.kt b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/TestViewConfiguration.kt
index 64f01f6..b7f339bd 100644
--- a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/TestViewConfiguration.kt
+++ b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/TestViewConfiguration.kt
@@ -20,6 +20,7 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.unit.DpSize
 
 /**
  * A [ViewConfiguration] that can be used for testing. The default values are representative for
@@ -35,7 +36,8 @@
     override val longPressTimeoutMillis: Long = 500L,
     override val doubleTapTimeoutMillis: Long = 300L,
     override val doubleTapMinTimeMillis: Long = 40L,
-    override val touchSlop: Float = 18f
+    override val touchSlop: Float = 18f,
+    override val minimumTouchTargetSize: DpSize = DpSize.Zero
 ) : ViewConfiguration
 
 @Composable
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 63ab22d..e81ee11 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -1287,9 +1287,11 @@
   @kotlin.coroutines.RestrictsSuspension public interface AwaitPointerEventScope extends androidx.compose.ui.unit.Density {
     method public suspend Object? awaitPointerEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerEvent> p);
     method public androidx.compose.ui.input.pointer.PointerEvent getCurrentEvent();
+    method public default long getExtendedTouchPadding();
     method public long getSize();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
     property public abstract androidx.compose.ui.input.pointer.PointerEvent currentEvent;
+    property public default long extendedTouchPadding;
     property public abstract long size;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
   }
@@ -1332,7 +1334,8 @@
     method public static void consumeAllChanges(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static void consumeDownChange(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static void consumePositionChange(androidx.compose.ui.input.pointer.PointerInputChange);
-    method public static boolean isOutOfBounds(androidx.compose.ui.input.pointer.PointerInputChange, long size);
+    method @Deprecated public static boolean isOutOfBounds(androidx.compose.ui.input.pointer.PointerInputChange, long size);
+    method public static boolean isOutOfBounds(androidx.compose.ui.input.pointer.PointerInputChange, long size, long extendedTouchPadding);
     method public static long positionChange(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static boolean positionChangeConsumed(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static long positionChangeIgnoreConsumed(androidx.compose.ui.input.pointer.PointerInputChange);
@@ -1415,8 +1418,10 @@
 
   public interface PointerInputScope extends androidx.compose.ui.unit.Density {
     method public suspend <R> Object? awaitPointerEventScope(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.AwaitPointerEventScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R> p);
+    method public default long getExtendedTouchPadding();
     method public long getSize();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
+    property public default long extendedTouchPadding;
     property public abstract long size;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
   }
@@ -2065,10 +2070,12 @@
     method public long getDoubleTapMinTimeMillis();
     method public long getDoubleTapTimeoutMillis();
     method public long getLongPressTimeoutMillis();
+    method public default long getMinimumTouchTargetSize();
     method public float getTouchSlop();
     property public abstract long doubleTapMinTimeMillis;
     property public abstract long doubleTapTimeoutMillis;
     property public abstract long longPressTimeoutMillis;
+    property public default long minimumTouchTargetSize;
     property public abstract float touchSlop;
   }
 
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index cd35a7b7..cf26b1a 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -1451,9 +1451,11 @@
   @kotlin.coroutines.RestrictsSuspension public interface AwaitPointerEventScope extends androidx.compose.ui.unit.Density {
     method public suspend Object? awaitPointerEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerEvent> p);
     method public androidx.compose.ui.input.pointer.PointerEvent getCurrentEvent();
+    method public default long getExtendedTouchPadding();
     method public long getSize();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
     property public abstract androidx.compose.ui.input.pointer.PointerEvent currentEvent;
+    property public default long extendedTouchPadding;
     property public abstract long size;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
   }
@@ -1496,7 +1498,8 @@
     method public static void consumeAllChanges(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static void consumeDownChange(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static void consumePositionChange(androidx.compose.ui.input.pointer.PointerInputChange);
-    method public static boolean isOutOfBounds(androidx.compose.ui.input.pointer.PointerInputChange, long size);
+    method @Deprecated public static boolean isOutOfBounds(androidx.compose.ui.input.pointer.PointerInputChange, long size);
+    method public static boolean isOutOfBounds(androidx.compose.ui.input.pointer.PointerInputChange, long size, long extendedTouchPadding);
     method public static long positionChange(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static boolean positionChangeConsumed(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static long positionChangeIgnoreConsumed(androidx.compose.ui.input.pointer.PointerInputChange);
@@ -1579,8 +1582,10 @@
 
   public interface PointerInputScope extends androidx.compose.ui.unit.Density {
     method public suspend <R> Object? awaitPointerEventScope(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.AwaitPointerEventScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R> p);
+    method public default long getExtendedTouchPadding();
     method public long getSize();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
+    property public default long extendedTouchPadding;
     property public abstract long size;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
   }
@@ -2268,10 +2273,12 @@
     method public long getDoubleTapMinTimeMillis();
     method public long getDoubleTapTimeoutMillis();
     method public long getLongPressTimeoutMillis();
+    method public default long getMinimumTouchTargetSize();
     method public float getTouchSlop();
     property public abstract long doubleTapMinTimeMillis;
     property public abstract long doubleTapTimeoutMillis;
     property public abstract long longPressTimeoutMillis;
+    property public default long minimumTouchTargetSize;
     property public abstract float touchSlop;
   }
 
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 6dc2c81..0dc2a0e 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -1287,9 +1287,11 @@
   @kotlin.coroutines.RestrictsSuspension public interface AwaitPointerEventScope extends androidx.compose.ui.unit.Density {
     method public suspend Object? awaitPointerEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerEvent> p);
     method public androidx.compose.ui.input.pointer.PointerEvent getCurrentEvent();
+    method public default long getExtendedTouchPadding();
     method public long getSize();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
     property public abstract androidx.compose.ui.input.pointer.PointerEvent currentEvent;
+    property public default long extendedTouchPadding;
     property public abstract long size;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
   }
@@ -1332,7 +1334,8 @@
     method public static void consumeAllChanges(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static void consumeDownChange(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static void consumePositionChange(androidx.compose.ui.input.pointer.PointerInputChange);
-    method public static boolean isOutOfBounds(androidx.compose.ui.input.pointer.PointerInputChange, long size);
+    method @Deprecated public static boolean isOutOfBounds(androidx.compose.ui.input.pointer.PointerInputChange, long size);
+    method public static boolean isOutOfBounds(androidx.compose.ui.input.pointer.PointerInputChange, long size, long extendedTouchPadding);
     method public static long positionChange(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static boolean positionChangeConsumed(androidx.compose.ui.input.pointer.PointerInputChange);
     method public static long positionChangeIgnoreConsumed(androidx.compose.ui.input.pointer.PointerInputChange);
@@ -1415,8 +1418,10 @@
 
   public interface PointerInputScope extends androidx.compose.ui.unit.Density {
     method public suspend <R> Object? awaitPointerEventScope(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.AwaitPointerEventScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R> p);
+    method public default long getExtendedTouchPadding();
     method public long getSize();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
+    property public default long extendedTouchPadding;
     property public abstract long size;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
   }
@@ -1797,14 +1802,17 @@
     method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
     method public androidx.compose.ui.layout.MeasurePolicy getMeasurePolicy();
     method public androidx.compose.ui.Modifier getModifier();
+    method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
     method public void setDensity(androidx.compose.ui.unit.Density density);
     method public void setLayoutDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection);
     method public void setMeasurePolicy(androidx.compose.ui.layout.MeasurePolicy measurePolicy);
     method public void setModifier(androidx.compose.ui.Modifier modifier);
+    method public void setViewConfiguration(androidx.compose.ui.platform.ViewConfiguration viewConfiguration);
     property public abstract androidx.compose.ui.unit.Density density;
     property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
     property public abstract androidx.compose.ui.layout.MeasurePolicy measurePolicy;
     property public abstract androidx.compose.ui.Modifier modifier;
+    property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
     field public static final androidx.compose.ui.node.ComposeUiNode.Companion Companion;
   }
 
@@ -1814,11 +1822,13 @@
     method public kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.unit.LayoutDirection,kotlin.Unit> getSetLayoutDirection();
     method public kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.layout.MeasurePolicy,kotlin.Unit> getSetMeasurePolicy();
     method public kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.Modifier,kotlin.Unit> getSetModifier();
+    method public kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.platform.ViewConfiguration,kotlin.Unit> getSetViewConfiguration();
     property public final kotlin.jvm.functions.Function0<androidx.compose.ui.node.ComposeUiNode> Constructor;
     property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.unit.Density,kotlin.Unit> SetDensity;
     property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.unit.LayoutDirection,kotlin.Unit> SetLayoutDirection;
     property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.layout.MeasurePolicy,kotlin.Unit> SetMeasurePolicy;
     property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.Modifier,kotlin.Unit> SetModifier;
+    property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.ComposeUiNode,androidx.compose.ui.platform.ViewConfiguration,kotlin.Unit> SetViewConfiguration;
   }
 
   public final class LayoutNodeKt {
@@ -2095,10 +2105,12 @@
     method public long getDoubleTapMinTimeMillis();
     method public long getDoubleTapTimeoutMillis();
     method public long getLongPressTimeoutMillis();
+    method public default long getMinimumTouchTargetSize();
     method public float getTouchSlop();
     property public abstract long doubleTapMinTimeMillis;
     property public abstract long doubleTapTimeoutMillis;
     property public abstract long longPressTimeoutMillis;
+    property public default long minimumTouchTargetSize;
     property public abstract float touchSlop;
   }
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index fed8579..7d4b955 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -118,6 +118,7 @@
 import androidx.compose.ui.text.input.PasswordVisualTransformation
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.compose.ui.window.Dialog
 import androidx.core.view.ViewCompat
@@ -2276,8 +2277,8 @@
     }
 
     @Test
-    fun testReportedBounds_clickableNode_includesPadding() {
-        val size = 100
+    fun testReportedBounds_clickableNode_includesPadding(): Unit = with(rule.density) {
+        val size = 100.dp.roundToPx()
         container.setContent {
             with(LocalDensity.current) {
                 Column {
@@ -2308,8 +2309,8 @@
     }
 
     @Test
-    fun testReportedBounds_clickableNode_excludesPadding() {
-        val size = 100
+    fun testReportedBounds_clickableNode_excludesPadding(): Unit = with(rule.density) {
+        val size = 100.dp.roundToPx()
         val density = Density(2f)
         container.setContent {
             CompositionLocalProvider(LocalDensity provides density) {
@@ -2371,8 +2372,8 @@
     }
 
     @Test
-    fun testReportedBounds_withTwoClickable_outermostWins() {
-        val size = 100
+    fun testReportedBounds_withTwoClickable_outermostWins(): Unit = with(rule.density) {
+        val size = 100.dp.roundToPx()
         container.setContent {
             with(LocalDensity.current) {
                 Column {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
index e47b044..360b423 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
@@ -454,17 +454,16 @@
     }
 
     @Test
-    fun testPainterFixedDimensionUnchanged() {
+    fun testPainterFixedDimensionUnchanged(): Unit = with(rule.density) {
         val painterWidth = 1000f
         val painterHeight = 375f
-        val density = rule.density.density
-        val composableWidth = 500f
-        val composableHeight = 800f
+        val composableWidth = 250f
+        val composableHeight = 400f
         // Because the constraints are tight here, do not attempt to resize the composable
         // based on the intrinsic dimensions of the Painter
         testPainterScaleMatchesSize(
-            Modifier.requiredWidth((composableWidth / density).dp)
-                .requiredHeight((composableHeight / density).dp),
+            Modifier.requiredWidth(composableWidth.toDp())
+                .requiredHeight(composableHeight.toDp()),
             ContentScale.Fit,
             Size(painterWidth, painterHeight),
             composableWidth,
@@ -517,12 +516,10 @@
         painterSize: Size,
         composableWidthPx: Float,
         composableHeightPx: Float
-    ) {
-        var composableWidth = 0f
-        var composableHeight = 0f
+    ) = with(rule.density) {
+        val composableWidth = composableWidthPx.toDp()
+        val composableHeight = composableHeightPx.toDp()
         rule.setContent {
-            composableWidth = composableWidthPx / LocalDensity.current.density
-            composableHeight = composableHeightPx / LocalDensity.current.density
             // Because the painter is told to fit inside the constraints, the width should
             // match that of the provided fixed width and the height should match that of the
             // composable as no scaling is being done
@@ -541,24 +538,24 @@
         }
 
         rule.onRoot()
-            .assertWidthIsEqualTo(composableWidth.dp)
-            .assertHeightIsEqualTo(composableHeight.dp)
+            .assertWidthIsEqualTo(composableWidth)
+            .assertHeightIsEqualTo(composableHeight)
     }
 
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Test
-    fun testBitmapPainterScalesContent() {
+    fun testBitmapPainterScalesContent(): Unit = with(rule.density) {
         // BitmapPainter should handle scaling its content image up to fill the
         // corresponding content bounds. Because the composable is twice the
         // height of the image and we are providing ContentScale.FillHeight
         // the BitmapPainter should draw the image with twice its original
         // height and width centered within the bounds of the composable
-        val boxWidth = 600
-        val boxHeight = 400
-        val srcImage = ImageBitmap(100, 200)
+        val boxWidth = 300
+        val boxHeight = 200
+        val srcImage = ImageBitmap(50, 100)
         val canvas = Canvas(srcImage)
         val paint = Paint().apply { this.color = Color.Red }
-        canvas.drawRect(0f, 0f, 400f, 200f, paint)
+        canvas.drawRect(0f, 0f, 200f, 100f, paint)
 
         val testTag = "testTag"
 
@@ -567,8 +564,8 @@
                 modifier = Modifier
                     .testTag(testTag)
                     .background(color = Color.Gray)
-                    .requiredWidth((boxWidth / LocalDensity.current.density).dp)
-                    .requiredHeight((boxHeight / LocalDensity.current.density).dp)
+                    .requiredWidth(boxWidth.toDp())
+                    .requiredHeight(boxHeight.toDp())
                     .paint(BitmapPainter(srcImage), contentScale = ContentScale.FillHeight)
             )
         }
@@ -623,24 +620,24 @@
 
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Test
-    fun testVectorPainterScalesContent() {
+    fun testVectorPainterScalesContent(): Unit = with(rule.density) {
         // VectorPainter should handle scaling its content vector up to fill the
         // corresponding content bounds. Because the composable is twice the
         // height of the vector and we are providing ContentScale.FillHeight
         // the VectorPainter should draw the vector with twice its original
         // height and width centered within the bounds of the composable
-        val boxWidth = 600
-        val boxHeight = 400
+        val boxWidth = 300
+        val boxHeight = 200
 
-        val vectorWidth = 100
-        val vectorHeight = 200
+        val vectorWidth = 50
+        val vectorHeight = 100
         rule.setContent {
-            val vectorWidthDp = (vectorWidth / LocalDensity.current.density).dp
-            val vectorHeightDp = (vectorHeight / LocalDensity.current.density).dp
+            val vectorWidthDp = vectorWidth.toDp()
+            val vectorHeightDp = vectorHeight.toDp()
             Box(
                 modifier = Modifier.background(color = Color.Gray)
-                    .requiredWidth((boxWidth / LocalDensity.current.density).dp)
-                    .requiredHeight((boxHeight / LocalDensity.current.density).dp)
+                    .requiredWidth(boxWidth.toDp())
+                    .requiredHeight(boxHeight.toDp())
                     .paint(
                         rememberVectorPainter(
                             defaultWidth = vectorWidthDp,
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
index 383b749..95448cb 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
@@ -95,11 +95,11 @@
     @Test
     fun testVectorAlignment() {
         rule.setContent {
-            VectorTint(minimumSize = 500, alignment = Alignment.BottomEnd)
+            VectorTint(minimumSize = 450, alignment = Alignment.BottomEnd)
         }
 
-        takeScreenShot(500).apply {
-            assertEquals(getPixel(480, 480), Color.Cyan.toArgb())
+        takeScreenShot(450).apply {
+            assertEquals(getPixel(430, 430), Color.Cyan.toArgb())
         }
     }
 
@@ -191,8 +191,8 @@
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Test
     fun testImageVectorChangeOnStateChange() {
-        val defaultWidth = 24.dp
-        val defaultHeight = 24.dp
+        val defaultWidth = 48.dp
+        val defaultHeight = 48.dp
         val viewportWidth = 24f
         val viewportHeight = 24f
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index d74228e..5bb9d6a 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -3191,4 +3191,4 @@
     override fun get(alignmentLine: AlignmentLine): Int {
         TODO("not implemented")
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/RestrictedSizeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/RestrictedSizeTest.kt
new file mode 100644
index 0000000..c3836cc
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/RestrictedSizeTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2021 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.ui.input.pointer
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.click
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performGesture
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class RestrictedSizeTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    private val tag = "tag"
+
+    @Test
+    fun pointerPositionAtMeasuredSize(): Unit = with(rule.density) {
+        var point = Offset.Zero
+
+        rule.setContent {
+            Box(Modifier.fillMaxSize()) {
+                Box(Modifier.requiredSize(50.dp).testTag(tag)) {
+                    Box(
+                        Modifier.requiredSize(80.dp).pointerInput(Unit) {
+                            awaitPointerEventScope {
+                                val event = awaitPointerEvent()
+                                point = event.changes[0].position
+                            }
+                        }
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(tag)
+            .performGesture {
+                click(Offset.Zero)
+            }
+
+        assertThat(point.x).isEqualTo(15.dp.roundToPx().toFloat())
+        assertThat(point.y).isEqualTo(15.dp.roundToPx().toFloat())
+    }
+
+    @Test
+    fun pointerOutOfLayoutBounds(): Unit = with(rule.density) {
+        var point = Offset.Zero
+        var isOutOfBounds = true
+
+        rule.setContent {
+            Box(Modifier.fillMaxSize()) {
+                Box(Modifier.requiredSize(50.dp).testTag(tag)) {
+                    Box(
+                        Modifier.requiredSize(80.dp).pointerInput(Unit) {
+                            awaitPointerEventScope {
+                                val event = awaitPointerEvent()
+                                point = event.changes[0].position
+                                isOutOfBounds =
+                                    event.changes[0].isOutOfBounds(size, extendedTouchPadding)
+                            }
+                        }
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(tag)
+            .performGesture {
+                click(Offset(-5f, -2f))
+            }
+
+        assertThat(point.x).isEqualTo(15.dp.roundToPx().toFloat() - 5f)
+        assertThat(point.y).isEqualTo(15.dp.roundToPx().toFloat() - 2f)
+        assertThat(isOutOfBounds).isFalse()
+    }
+
+    @Test
+    fun semanticsSizeTooSmall(): Unit = with(rule.density) {
+        rule.setContent {
+            Box(Modifier.fillMaxSize()) {
+                Box(Modifier.requiredSize(50.dp)) {
+                    Box(
+                        Modifier.requiredSize(80.dp).testTag(tag)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(tag)
+            .assertWidthIsEqualTo(80.dp)
+            .assertHeightIsEqualTo(80.dp)
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 988de49..11bf968 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -189,6 +189,7 @@
             .then(semanticsModifier)
             .then(_focusManager.modifier)
             .then(keyInputModifier)
+        it.density = density
     }
 
     override val rootForTest: RootForTest = this
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 1110f69..49c7984 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -65,6 +65,7 @@
 import androidx.compose.ui.fastJoinToString
 import androidx.compose.ui.focus.requestFocus
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.node.HitTestResult
 import androidx.compose.ui.platform.accessibility.hasCollectionInfo
 import androidx.compose.ui.platform.accessibility.setCollectionInfo
 import androidx.compose.ui.platform.accessibility.setCollectionItemInfo
@@ -1439,7 +1440,7 @@
     internal fun hitTestSemanticsAt(x: Float, y: Float): Int {
         view.measureAndLayout()
 
-        val hitSemanticsWrappers: MutableList<SemanticsWrapper> = mutableListOf()
+        val hitSemanticsWrappers = HitTestResult<SemanticsWrapper>()
         view.root.hitTestSemantics(
             pointerPosition = Offset(x, y),
             hitSemanticsWrappers = hitSemanticsWrappers
@@ -2536,4 +2537,4 @@
         }
     }
     return null
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
index 01ed2cd..2ea5b6e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
@@ -21,6 +21,7 @@
 import androidx.compose.runtime.Immutable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.input.pointer.PointerEventPass.Final
 import androidx.compose.ui.input.pointer.PointerEventPass.Initial
 import androidx.compose.ui.input.pointer.PointerEventPass.Main
@@ -498,6 +499,10 @@
  * `(0, 0, size.width, size.height)` or `false` if the current pointer is up or it is inside the
  * given bounds.
  */
+@Deprecated(
+    message = "Use isOutOfBounds() that supports minimum touch target",
+    replaceWith = ReplaceWith("this.isOutOfBounds(size, extendedTouchPadding)")
+)
 fun PointerInputChange.isOutOfBounds(size: IntSize): Boolean {
     val position = position
     val x = position.x
@@ -506,3 +511,25 @@
     val height = size.height
     return x < 0f || x > width || y < 0f || y > height
 }
+
+/**
+ * Returns `true` if the pointer has moved outside of the pointer region. For Touch
+ * events, this is (-extendedTouchPadding.width, -extendedTouchPadding.height,
+ * size.width + extendedTouchPadding.width, size.height + extendedTouchPadding.height) and
+ * for other events, this is `(0, 0, size.width, size.height)`. Returns`false` if the
+ * current pointer is up or it is inside the pointer region.
+ */
+fun PointerInputChange.isOutOfBounds(size: IntSize, extendedTouchPadding: Size): Boolean {
+    if (type != PointerType.Touch) {
+        @Suppress("DEPRECATION")
+        return isOutOfBounds(size)
+    }
+    val position = position
+    val x = position.x
+    val y = position.y
+    val minX = -extendedTouchPadding.width
+    val maxX = size.width + extendedTouchPadding.width
+    val minY = -extendedTouchPadding.height
+    val maxY = size.height + extendedTouchPadding.height
+    return x < minX || x > maxX || y < minY || y > maxY
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
index 1f97dcc..7c21cbf 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
@@ -17,6 +17,7 @@
 package androidx.compose.ui.input.pointer
 
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.node.HitTestResult
 import androidx.compose.ui.node.InternalCoreApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.util.fastForEach
@@ -34,7 +35,7 @@
 
     private val hitPathTracker = HitPathTracker(root.coordinates)
     private val pointerInputChangeEventProducer = PointerInputChangeEventProducer()
-    private val hitResult: MutableList<PointerInputFilter> = mutableListOf()
+    private val hitResult = HitTestResult<PointerInputFilter>()
 
     /**
      * Receives [PointerInputEvent]s and process them through the tree rooted on [root].
@@ -58,10 +59,8 @@
         // Add new hit paths to the tracker due to down events.
         internalPointerEvent.changes.values.forEach { pointerInputChange ->
             if (pointerInputChange.changedToDownIgnoreConsumed()) {
-                root.hitTest(
-                    pointerInputChange.position,
-                    hitResult
-                )
+                val isTouchEvent = pointerInputChange.type == PointerType.Touch
+                root.hitTest(pointerInputChange.position, hitResult, isTouchEvent)
                 if (hitResult.isNotEmpty()) {
                     hitPathTracker.addHitPath(pointerInputChange.id, hitResult)
                     hitResult.clear()
@@ -204,4 +203,4 @@
     val val1 = if (dispatchedToAPointerInputModifier) 1 else 0
     val val2 = if (anyMovementConsumed) (1 shl 1) else 0
     return ProcessResult(val1 or val2)
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
index eb6c8a8..a736a9c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
@@ -22,6 +22,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.fastMapNotNull
+import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.ViewConfiguration
@@ -38,6 +39,7 @@
 import kotlin.coroutines.RestrictsSuspension
 import kotlin.coroutines.createCoroutine
 import kotlin.coroutines.resume
+import kotlin.math.max
 
 /**
  * Receiver scope for awaiting pointer events in a call to [PointerInputScope.awaitPointerEventScope].
@@ -56,6 +58,13 @@
      */
     val size: IntSize
 
+    /*
+     * The additional space applied to each side of the layout area. This can be
+     * non-[zero][Size.Zero] when `minimumTouchTargetSize` is set in [pointerInput].
+     */
+    val extendedTouchPadding: Size
+        get() = Size.Zero
+
     /**
      * The [PointerEvent] from the most recent touch event.
      */
@@ -100,6 +109,13 @@
     val size: IntSize
 
     /**
+     * The additional space applied to each side of the layout area when the layout is smaller
+     * than [ViewConfiguration.minimumTouchTargetSize].
+     */
+    val extendedTouchPadding: Size
+        get() = Size.Zero
+
+    /**
      * The [ViewConfiguration] used to tune gesture detectors.
      */
     val viewConfiguration: ViewConfiguration
@@ -289,6 +305,15 @@
      */
     private var boundsSize: IntSize = IntSize.Zero
 
+    override val extendedTouchPadding: Size
+        get() {
+            val minimumTouchTargetSize = viewConfiguration.minimumTouchTargetSize.toSize()
+            val size = size
+            val horizontal = max(0f, minimumTouchTargetSize.width - size.width) / 2f
+            val vertical = max(0f, minimumTouchTargetSize.height - size.height) / 2f
+            return Size(horizontal, vertical)
+        }
+
     /**
      * Snapshot the current [pointerHandlers] and run [block] on each one.
      * May not be called reentrant or concurrent with itself.
@@ -424,6 +449,8 @@
             get() = [email protected]
         override val viewConfiguration: ViewConfiguration
             get() = [email protected]
+        override val extendedTouchPadding: Size
+            get() = [email protected]
 
         fun offerPointerEvent(event: PointerEvent, pass: PointerEventPass) {
             if (pass == awaitPass) {
@@ -458,4 +485,4 @@
             pointerAwaiter = continuation
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
index b15b091..3a50384 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.node.MeasureBlocks
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.simpleIdentityToString
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
@@ -71,12 +72,14 @@
 ) {
     val density = LocalDensity.current
     val layoutDirection = LocalLayoutDirection.current
+    val viewConfiguration = LocalViewConfiguration.current
     ReusableComposeNode<ComposeUiNode, Applier<Any>>(
         factory = ComposeUiNode.Constructor,
         update = {
             set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
             set(density, ComposeUiNode.SetDensity)
             set(layoutDirection, ComposeUiNode.SetLayoutDirection)
+            set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
         },
         skippableUpdate = materializerOf(modifier),
         content = content
@@ -191,6 +194,7 @@
     val materialized = currentComposer.materialize(modifier)
     val density = LocalDensity.current
     val layoutDirection = LocalLayoutDirection.current
+    val viewConfiguration = LocalViewConfiguration.current
 
     ReusableComposeNode<LayoutNode, Applier<Any>>(
         factory = LayoutNode.Constructor,
@@ -199,6 +203,7 @@
             set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
             set(density, ComposeUiNode.SetDensity)
             set(layoutDirection, ComposeUiNode.SetLayoutDirection)
+            set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
             @Suppress("DEPRECATION")
             init { this.canMultiMeasure = true }
         },
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 59b148f..b031665 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -32,6 +32,7 @@
 import androidx.compose.ui.node.LayoutNode.LayoutState
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.createSubcomposition
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.LayoutDirection
@@ -103,6 +104,7 @@
     val materialized = currentComposer.materialize(modifier)
     val density = LocalDensity.current
     val layoutDirection = LocalLayoutDirection.current
+    val viewConfiguration = LocalViewConfiguration.current
     ComposeNode<LayoutNode, Applier<Any>>(
         factory = LayoutNode.Constructor,
         update = {
@@ -111,6 +113,7 @@
             set(measurePolicy, state.setMeasurePolicy)
             set(density, ComposeUiNode.SetDensity)
             set(layoutDirection, ComposeUiNode.SetLayoutDirection)
+            set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
         }
     )
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ComposeUiNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ComposeUiNode.kt
index 57f6b75..f4f88ef 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ComposeUiNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ComposeUiNode.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.platform.ViewConfiguration
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.LayoutDirection
 
@@ -30,6 +31,7 @@
     var layoutDirection: LayoutDirection
     var density: Density
     var modifier: Modifier
+    var viewConfiguration: ViewConfiguration
 
     /**
      * Object of pre-allocated lambdas used to make use with ComposeNode allocation-less.
@@ -42,5 +44,7 @@
             { this.measurePolicy = it }
         val SetLayoutDirection: ComposeUiNode.(LayoutDirection) -> Unit =
             { this.layoutDirection = it }
+        val SetViewConfiguration: ComposeUiNode.(ViewConfiguration) -> Unit =
+            { this.viewConfiguration = it }
     }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
index 5b510e3..37b9109 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.GraphicsLayerScope
 import androidx.compose.ui.input.pointer.PointerInputFilter
@@ -70,17 +71,18 @@
 
     override fun hitTest(
         pointerPosition: Offset,
-        hitPointerInputFilters: MutableList<PointerInputFilter>
+        hitTestResult: HitTestResult<PointerInputFilter>,
+        isTouchEvent: Boolean
     ) {
         if (withinLayerBounds(pointerPosition)) {
             val positionInWrapped = wrapped.fromParentPosition(pointerPosition)
-            wrapped.hitTest(positionInWrapped, hitPointerInputFilters)
+            wrapped.hitTest(positionInWrapped, hitTestResult, isTouchEvent)
         }
     }
 
     override fun hitTestSemantics(
         pointerPosition: Offset,
-        hitSemanticsWrappers: MutableList<SemanticsWrapper>
+        hitSemanticsWrappers: HitTestResult<SemanticsWrapper>
     ) {
         if (withinLayerBounds(pointerPosition)) {
             val positionInWrapped = wrapped.fromParentPosition(pointerPosition)
@@ -165,4 +167,76 @@
     override fun maxIntrinsicHeight(width: Int) = wrapped.maxIntrinsicHeight(width)
 
     override val parentData: Any? get() = wrapped.parentData
+
+    private fun offsetFromEdge(pointerPosition: Offset): Offset {
+        val x = pointerPosition.x
+        val horizontal = maxOf(0f, if (x < 0) -x else x - measuredWidth)
+        val y = pointerPosition.y
+        val vertical = maxOf(0f, if (y < 0) -y else y - measuredHeight)
+
+        return Offset(horizontal, vertical)
+    }
+
+    /**
+     * Returns the additional amount on the horizontal and vertical dimensions that
+     * this extends beyond [width] and [height] on all sides. This takes into account
+     * [minimumTouchTargetSize] and [measuredSize] vs. [width] and [height].
+     */
+    protected fun calculateMinimumTouchTargetPadding(minimumTouchTargetSize: Size): Size {
+        val widthDiff = minimumTouchTargetSize.width - measuredWidth.toFloat()
+        val heightDiff = minimumTouchTargetSize.height - measuredHeight.toFloat()
+        return Size(maxOf(0f, widthDiff / 2f), maxOf(0f, heightDiff / 2f))
+    }
+
+    /**
+     * Does a hit test, adding [content] as a [HitTestResult.hit] or
+     * [HitTestResult.hitInMinimumTouchTarget] depending on whether or not it hit
+     * or hit in the [minimumTouchTargetSize] area. The newly-created [HitTestResult] is returned
+     * if there was a hit or `null` is returned if it missed.
+     */
+    protected fun <T> hitTestInMinimumTouchTarget(
+        pointerPosition: Offset,
+        hitTestResult: HitTestResult<T>,
+        content: T,
+        block: () -> Unit
+    ) {
+        if (!withinLayerBounds(pointerPosition)) {
+            return
+        }
+        if (isPointerInBounds(pointerPosition)) {
+            hitTestResult.hit(content, block)
+        } else {
+            val offsetFromEdge = offsetFromEdge(pointerPosition)
+            val distanceFromEdge = maxOf(offsetFromEdge.x, offsetFromEdge.y)
+            val minimumTouchTargetSize = minimumTouchTargetSize
+
+            if (offsetFromEdge.x >= minimumTouchTargetSize.width / 2f ||
+                offsetFromEdge.y >= minimumTouchTargetSize.height / 2f ||
+                !hitTestResult.isHitInMinimumTouchTargetBetter(distanceFromEdge)
+            ) {
+                return // complete miss or the other hit was better
+            }
+
+            if (isHitInMinimumTouchTarget(offsetFromEdge, minimumTouchTargetSize)) {
+                // This was definitely closer than any other target and hit this
+                hitTestResult.hitInMinimumTouchTarget(content, distanceFromEdge, block)
+            } else {
+                // We have to consider anything that may be within the minimum touch target
+                // in case a child is within the minimum touch target. For example, a
+                // switch may have a thumb to one side. The switch's width may preclude
+                // it from receiving minimum touch target special treatment, but the thumb
+                // may be small enough to receive a minimum touch target outside the bounds
+                // of the switch.
+                hitTestResult.speculativeHit(content, distanceFromEdge, block)
+            }
+        }
+    }
+
+    private fun isHitInMinimumTouchTarget(
+        offsetFromEdge: Offset,
+        minimumTouchTargetSize: Size
+    ): Boolean {
+        val touchPadding = calculateMinimumTouchTargetPadding(minimumTouchTargetSize)
+        return offsetFromEdge.x < touchPadding.width && offsetFromEdge.y < touchPadding.height
+    }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/HitTestResult.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/HitTestResult.kt
new file mode 100644
index 0000000..49a8886
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/HitTestResult.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2021 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.ui.node
+
+/**
+ * This tracks the hit test results to allow for minimum touch target and single-pass hit testing.
+ * If there is a hit at the minimum touch target, searching for a hit within the layout bounds
+ * can still continue, but the near miss is still tracked.
+ *
+ * The List<T> interface should only be used after hit testing has completed.
+ *
+ * @see LayoutNode.hitTest
+ * @see LayoutNodeWrapper.hitTest
+ * @see PointerInputDelegatingWrapper.hitTest
+ */
+internal class HitTestResult<T> : List<T> {
+    private var values = arrayOfNulls<Any>(16)
+    private var distancesFromEdge = FloatArray(16)
+    private var hitDepth = -1
+
+    override var size: Int = 0
+        private set
+
+    /**
+     * `true` when there has been a direct hit within touch bounds ([hit] called) or
+     * `false` otherwise.
+     */
+    val isHit: Boolean get() =
+        hitDepth < lastIndex && values[hitDepth + 1] != null && distancesFromEdge[hitDepth + 1] < 0f
+
+    private fun resizeToHitDepth() {
+        for (i in (hitDepth + 1)..lastIndex) {
+            values[i] = null
+        }
+        size = hitDepth + 1
+    }
+
+    /**
+     * Returns `true` if [distanceFromEdge] is less than the previous value passed in
+     * [hitInMinimumTouchTarget] or [speculativeHit].
+     */
+    fun isHitInMinimumTouchTargetBetter(distanceFromEdge: Float): Boolean {
+        return hitDepth == lastIndex ||
+            values[hitDepth + 1] == null ||
+            distanceFromEdge < distancesFromEdge[hitDepth + 1]
+    }
+
+    /**
+     * Records [node] as a hit, adding it to the [HitTestResult] or replacing the existing one.
+     * Runs [childHitTest] to do further hit testing for children.
+     */
+    fun hit(node: T, childHitTest: () -> Unit) {
+        hitInMinimumTouchTarget(node, -1f, childHitTest)
+    }
+
+    /**
+     * Records [node] as a hit with [distanceFromEdge] distance, replacing any existing record.
+     * Runs [childHitTest] to do further hit testing for children.
+     */
+    fun hitInMinimumTouchTarget(node: T, distanceFromEdge: Float, childHitTest: () -> Unit) {
+        hitDepth++
+        ensureContainerSize()
+        values[hitDepth] = node
+        distancesFromEdge[hitDepth] = distanceFromEdge
+        resizeToHitDepth()
+        childHitTest()
+        hitDepth--
+    }
+
+    /**
+     * Temporarily records [node] as a hit with [distanceFromEdge] distance and calls
+     * [childHitTest] to record hits for children. If no children have hits, then
+     * the hit is discarded. If a child had a hit, then [node] replaces an existing
+     * hit.
+     */
+    fun speculativeHit(node: T, distanceFromEdge: Float, childHitTest: () -> Unit) {
+        if (hitDepth == lastIndex) {
+            // Speculation is easy. We don't have to do any array shuffling.
+            hitInMinimumTouchTarget(node, distanceFromEdge, childHitTest)
+            if (hitDepth + 1 == lastIndex) {
+                // Discard the hit because there were no child hits.
+                resizeToHitDepth()
+            }
+            return
+        }
+
+        // We have to tack the speculation to the end of the array
+        val previousHitDepth = hitDepth
+        hitDepth = lastIndex
+
+        hitInMinimumTouchTarget(node, distanceFromEdge, childHitTest)
+        if (hitDepth + 1 < lastIndex) {
+            // This was a successful hit, so we should move this to the previous hit depth
+            val fromIndex = hitDepth + 1
+            val toIndex = previousHitDepth + 1
+            values.copyInto(
+                destination = values,
+                destinationOffset = toIndex,
+                startIndex = fromIndex,
+                endIndex = size
+            )
+            distancesFromEdge.copyInto(
+                destination = distancesFromEdge,
+                destinationOffset = toIndex,
+                startIndex = fromIndex,
+                endIndex = size
+            )
+
+            // Discard the remainder of the hits
+            hitDepth = previousHitDepth + size - hitDepth - 1
+        }
+        resizeToHitDepth()
+        hitDepth = previousHitDepth
+    }
+
+    private fun ensureContainerSize() {
+        if (hitDepth >= values.size) {
+            val newSize = values.size + 16
+            values = values.copyOf(newSize)
+            distancesFromEdge = distancesFromEdge.copyOf(newSize)
+        }
+    }
+
+    override fun contains(element: T): Boolean = indexOf(element) != -1
+
+    override fun containsAll(elements: Collection<T>): Boolean {
+        elements.forEach {
+            if (!contains(it)) {
+                return false
+            }
+        }
+        return true
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    override fun get(index: Int): T = values[index] as T
+
+    override fun indexOf(element: T): Int {
+        for (i in 0..lastIndex) {
+            if (values[i] == element) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    override fun isEmpty(): Boolean = size == 0
+
+    override fun iterator(): Iterator<T> = HitTestResultIterator()
+
+    override fun lastIndexOf(element: T): Int {
+        for (i in lastIndex downTo 0) {
+            if (values[i] == element) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    override fun listIterator(): ListIterator<T> = HitTestResultIterator()
+
+    override fun listIterator(index: Int): ListIterator<T> = HitTestResultIterator(index)
+
+    override fun subList(fromIndex: Int, toIndex: Int): List<T> =
+        SubList(fromIndex, toIndex)
+
+    /**
+     * Clears all entries to make an empty list.
+     */
+    fun clear() {
+        hitDepth = -1
+        resizeToHitDepth()
+    }
+
+    private inner class HitTestResultIterator(
+        var index: Int = 0,
+        val minIndex: Int = 0,
+        val maxIndex: Int = size
+    ) : ListIterator<T> {
+        override fun hasNext(): Boolean = index < maxIndex
+
+        override fun hasPrevious(): Boolean = index > minIndex
+
+        @Suppress("UNCHECKED_CAST")
+        override fun next(): T = values[index++] as T
+
+        override fun nextIndex(): Int = index - minIndex
+
+        @Suppress("UNCHECKED_CAST")
+        override fun previous(): T = values[--index] as T
+
+        override fun previousIndex(): Int = index - minIndex - 1
+    }
+
+    private inner class SubList(
+        val minIndex: Int,
+        val maxIndex: Int
+    ) : List<T> {
+        override val size: Int
+            get() = maxIndex - minIndex
+
+        override fun contains(element: T): Boolean = indexOf(element) != -1
+
+        override fun containsAll(elements: Collection<T>): Boolean {
+            elements.forEach {
+                if (!contains(it)) {
+                    return false
+                }
+            }
+            return true
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        override fun get(index: Int): T = values[index + minIndex] as T
+
+        override fun indexOf(element: T): Int {
+            for (i in minIndex..maxIndex) {
+                if (values[i] == element) {
+                    return i - minIndex
+                }
+            }
+            return -1
+        }
+
+        override fun isEmpty(): Boolean = size == 0
+
+        override fun iterator(): Iterator<T> = HitTestResultIterator(minIndex, minIndex, maxIndex)
+
+        override fun lastIndexOf(element: T): Int {
+            for (i in maxIndex downTo minIndex) {
+                if (values[i] == element) {
+                    return i - minIndex
+                }
+            }
+            return -1
+        }
+
+        override fun listIterator(): ListIterator<T> =
+            HitTestResultIterator(minIndex, minIndex, maxIndex)
+
+        override fun listIterator(index: Int): ListIterator<T> =
+            HitTestResultIterator(minIndex + index, minIndex, maxIndex)
+
+        override fun subList(fromIndex: Int, toIndex: Int): List<T> =
+            SubList(minIndex + fromIndex, minIndex + toIndex)
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
index a92a309..5a277e4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
@@ -140,31 +140,32 @@
 
     override fun hitTest(
         pointerPosition: Offset,
-        hitPointerInputFilters: MutableList<PointerInputFilter>
+        hitTestResult: HitTestResult<PointerInputFilter>,
+        isTouchEvent: Boolean
     ) {
-        hitTestSubtree(pointerPosition, hitPointerInputFilters, LayoutNode::hitTest)
+        hitTestSubtree(pointerPosition, hitTestResult, isTouchEvent, LayoutNode::hitTest)
     }
 
     override fun hitTestSemantics(
         pointerPosition: Offset,
-        hitSemanticsWrappers: MutableList<SemanticsWrapper>
+        hitSemanticsWrappers: HitTestResult<SemanticsWrapper>
     ) {
-        hitTestSubtree(pointerPosition, hitSemanticsWrappers, LayoutNode::hitTestSemantics)
+        hitTestSubtree(pointerPosition, hitSemanticsWrappers, true, LayoutNode::hitTestSemantics)
     }
 
     private inline fun <T> hitTestSubtree(
         pointerPosition: Offset,
-        hitResult: MutableList<T>,
-        nodeHitTest: LayoutNode.(Offset, MutableList<T>) -> Unit
+        hitTestResult: HitTestResult<T>,
+        isTouchEvent: Boolean,
+        nodeHitTest: LayoutNode.(Offset, HitTestResult<T>, Boolean) -> Unit
     ) {
         if (withinLayerBounds(pointerPosition)) {
-            val originalSize = hitResult.size
             // Any because as soon as true is returned, we know we have found a hit path and we must
             // not add hit results on different paths so we should not even go looking.
             layoutNode.zSortedChildren.reversedAny { child ->
                 if (child.isPlaced) {
-                    child.nodeHitTest(pointerPosition, hitResult)
-                    hitResult.size > originalSize
+                    child.nodeHitTest(pointerPosition, hitTestResult, isTouchEvent)
+                    hitTestResult.isHit
                 } else {
                     false
                 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index b553e6a2..9e3a1fd 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -55,6 +55,7 @@
 import androidx.compose.ui.node.LayoutNode.LayoutState.NeedsRelayout
 import androidx.compose.ui.node.LayoutNode.LayoutState.NeedsRemeasure
 import androidx.compose.ui.node.LayoutNode.LayoutState.Ready
+import androidx.compose.ui.platform.ViewConfiguration
 import androidx.compose.ui.platform.nativeClass
 import androidx.compose.ui.platform.simpleIdentityToString
 import androidx.compose.ui.semantics.SemanticsModifier
@@ -62,6 +63,7 @@
 import androidx.compose.ui.semantics.outerSemantics
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.LayoutDirection
 
 /**
@@ -513,6 +515,8 @@
             }
         }
 
+    override var viewConfiguration: ViewConfiguration = DummyViewConfiguration
+
     private fun onDensityOrLayoutDirectionChanged() {
         // measure/layout modifiers on the node
         requestRemeasure()
@@ -825,28 +829,32 @@
      * all [PointerInputModifier]s on all descendant [LayoutNode]s.
      *
      * If [pointerPosition] is within the bounds of any tested
-     * [PointerInputModifier]s, the [PointerInputModifier] is added to [hitPointerInputFilters]
+     * [PointerInputModifier]s, the [PointerInputModifier] is added to [hitTestResult]
      * and true is returned.
      *
      * @param pointerPosition The tested pointer position, which is relative to
      * the LayoutNode.
-     * @param hitPointerInputFilters The collection that the hit [PointerInputFilter]s will be
+     * @param hitTestResult The collection that the hit [PointerInputFilter]s will be
      * added to if hit.
      */
     internal fun hitTest(
         pointerPosition: Offset,
-        hitPointerInputFilters: MutableList<PointerInputFilter>
+        hitTestResult: HitTestResult<PointerInputFilter>,
+        isTouchEvent: Boolean = false
     ) {
         val positionInWrapped = outerLayoutNodeWrapper.fromParentPosition(pointerPosition)
         outerLayoutNodeWrapper.hitTest(
             positionInWrapped,
-            hitPointerInputFilters
+            hitTestResult,
+            isTouchEvent
         )
     }
 
+    @Suppress("UNUSED_PARAMETER")
     internal fun hitTestSemantics(
         pointerPosition: Offset,
-        hitSemanticsWrappers: MutableList<SemanticsWrapper>
+        hitSemanticsWrappers: HitTestResult<SemanticsWrapper>,
+        isTouchEvent: Boolean = true
     ) {
         val positionInWrapped = outerLayoutNodeWrapper.fromParentPosition(pointerPosition)
         outerLayoutNodeWrapper.hitTestSemantics(
@@ -1335,6 +1343,23 @@
          * Pre-allocated constructor to be used with ComposeNode
          */
         internal val Constructor: () -> LayoutNode = { LayoutNode() }
+
+        /**
+         * All of these values are only used in tests. The real ViewConfiguration should
+         * be set in Layout()
+         */
+        internal val DummyViewConfiguration = object : ViewConfiguration {
+            override val longPressTimeoutMillis: Long
+                get() = 400L
+            override val doubleTapTimeoutMillis: Long
+                get() = 300L
+            override val doubleTapMinTimeMillis: Long
+                get() = 40L
+            override val touchSlop: Float
+                get() = 16f
+            override val minimumTouchTargetSize: DpSize
+                get() = DpSize.Zero
+        }
     }
 
     /**
@@ -1406,4 +1431,4 @@
         wrapper.isChained = true
     }
     return this
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
index bb72662..c44a5a4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
@@ -24,6 +24,7 @@
 import androidx.compose.ui.geometry.MutableRect
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.geometry.isFinite
 import androidx.compose.ui.geometry.toRect
 import androidx.compose.ui.graphics.Canvas
@@ -175,7 +176,7 @@
     var isShallowPlacing = false
 
     private var _rectCache: MutableRect? = null
-    private val rectCache: MutableRect
+    protected val rectCache: MutableRect
         get() = _rectCache ?: MutableRect(0f, 0f, 0f, 0f).also {
             _rectCache = it
         }
@@ -352,25 +353,28 @@
     override val isValid: Boolean
         get() = layer != null
 
+    protected val minimumTouchTargetSize: Size
+        get() = with(layerDensity) { layoutNode.viewConfiguration.minimumTouchTargetSize.toSize() }
+
     /**
      * Executes a hit test on any appropriate type associated with this [LayoutNodeWrapper].
      *
-     * Override appropriately to either add a [PointerInputFilter] to [hitPointerInputFilters] or
+     * Override appropriately to either add a [HitTestResult] to [hitTestResult] or
      * to pass the execution on.
      *
      * @param pointerPosition The tested pointer position, which is relative to
      * the [LayoutNodeWrapper].
-     * @param hitPointerInputFilters The collection that the hit [PointerInputFilter]s will be
-     * added to if hit.
+     * @param hitTestResult The parent [HitTestResult] that any hit should be added to.
      */
     abstract fun hitTest(
         pointerPosition: Offset,
-        hitPointerInputFilters: MutableList<PointerInputFilter>
+        hitTestResult: HitTestResult<PointerInputFilter>,
+        isTouchEvent: Boolean
     )
 
     abstract fun hitTestSemantics(
         pointerPosition: Offset,
-        hitSemanticsWrappers: MutableList<SemanticsWrapper>
+        hitSemanticsWrappers: HitTestResult<SemanticsWrapper>
     )
 
     override fun windowToLocal(relativeToWindow: Offset): Offset {
@@ -547,7 +551,7 @@
      * Modifies bounds to be in the parent LayoutNodeWrapper's coordinates, including clipping,
      * if [clipBounds] is true.
      */
-    private fun rectInParent(bounds: MutableRect, clipBounds: Boolean) {
+    internal fun rectInParent(bounds: MutableRect, clipBounds: Boolean) {
         val layer = layer
         if (layer != null) {
             if (isClipping && clipBounds) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/PointerInputDelegatingWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/PointerInputDelegatingWrapper.kt
index cae95de..274124e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/PointerInputDelegatingWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/PointerInputDelegatingWrapper.kt
@@ -38,16 +38,34 @@
 
     override fun hitTest(
         pointerPosition: Offset,
-        hitPointerInputFilters: MutableList<PointerInputFilter>
+        hitTestResult: HitTestResult<PointerInputFilter>,
+        isTouchEvent: Boolean
     ) {
-        if (isPointerInBounds(pointerPosition) && withinLayerBounds(pointerPosition)) {
-            // If the pointer is in bounds, we hit the pointer input filter, so add it!
-            hitPointerInputFilters.add(modifier.pointerInputFilter)
-
-            // Also, keep looking to see if we also might hit any children.
-            // This avoids checking layer bounds twice as when we call super.hitTest()
-            val positionInWrapped = wrapped.fromParentPosition(pointerPosition)
-            wrapped.hitTest(positionInWrapped, hitPointerInputFilters)
+        if (!isTouchEvent) {
+            if (isPointerInBounds(pointerPosition) && withinLayerBounds(pointerPosition)) {
+                hitTestResult.hit(modifier.pointerInputFilter) {
+                    hitTestChild(pointerPosition, hitTestResult, isTouchEvent)
+                }
+            }
+        } else {
+            hitTestInMinimumTouchTarget(
+                pointerPosition,
+                hitTestResult,
+                modifier.pointerInputFilter
+            ) {
+                hitTestChild(pointerPosition, hitTestResult, isTouchEvent)
+            }
         }
     }
+
+    private fun hitTestChild(
+        pointerPosition: Offset,
+        hitTestResult: HitTestResult<PointerInputFilter>,
+        isTouchEvent: Boolean
+    ) {
+        // Also, keep looking to see if we also might hit any children.
+        // This avoids checking layer bounds twice as when we call super.hitTest()
+        val positionInWrapped = wrapped.fromParentPosition(pointerPosition)
+        wrapped.hitTest(positionInWrapped, hitTestResult, isTouchEvent)
+    }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/ViewConfiguration.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/ViewConfiguration.kt
index 9a3e115..c6f26fd 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/ViewConfiguration.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/ViewConfiguration.kt
@@ -16,6 +16,9 @@
 
 package androidx.compose.ui.platform
 
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+
 /**
  * Contains methods to standard constants used in the UI for timeouts, sizes, and distances.
  */
@@ -41,4 +44,12 @@
      * Distance in pixels a touch can wander before we think the user is scrolling.
      */
     val touchSlop: Float
-}
+
+    /**
+     * The minimum touch target size. If layout has reduced the pointer input bounds below this,
+     * the touch target will be expanded evenly around the layout to ensure that it is at least
+     * this big.
+     */
+    val minimumTouchTargetSize: DpSize
+        get() = DpSize(48.dp, 48.dp)
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index 41b1f8b..ae27a54 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -19,16 +19,15 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.layout.AlignmentLine
-import androidx.compose.ui.layout.boundsInRoot
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.layout.LayoutInfo
-import androidx.compose.ui.layout.boundsInWindow
 import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.LayoutNodeWrapper
 import androidx.compose.ui.node.RootForTest
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastForEach
+import kotlin.math.roundToInt
 
 /**
  * A list of key/value pairs associated with a layout node or its subtree.
@@ -84,7 +83,10 @@
     /**
      * The size of the bounding box for this node, with no clipping applied
      */
-    val size: IntSize get() = findWrapperToGetBounds().size
+    val size: IntSize get() {
+        val size = findWrapperToGetBounds().semanticsSize
+        return IntSize(size.width.roundToInt(), size.height.roundToInt())
+    }
 
     /**
      * The bounding box for this node relative to the root of this Compose hierarchy, with
@@ -94,7 +96,7 @@
     val boundsInRoot: Rect
         get() {
             if (!layoutNode.isAttached) return Rect.Zero
-            return this.findWrapperToGetBounds().boundsInRoot()
+            return findWrapperToGetBounds().semanticsBoundsInRoot()
         }
 
     /**
@@ -104,7 +106,7 @@
     val positionInRoot: Offset
         get() {
             if (!layoutNode.isAttached) return Offset.Zero
-            return findWrapperToGetBounds().positionInRoot()
+            return findWrapperToGetBounds().semanticsPositionInRoot()
         }
 
     /**
@@ -114,7 +116,7 @@
     val boundsInWindow: Rect
         get() {
             if (!layoutNode.isAttached) return Rect.Zero
-            return findWrapperToGetBounds().boundsInWindow()
+            return findWrapperToGetBounds().semanticsBoundsInWindow()
         }
 
     /**
@@ -123,7 +125,7 @@
     val positionInWindow: Offset
         get() {
             if (!layoutNode.isAttached) return Offset.Zero
-            return findWrapperToGetBounds().positionInWindow()
+            return findWrapperToGetBounds().semanticsPositionInWindow()
         }
 
     /**
@@ -319,7 +321,7 @@
      * of use cases it means that accessibility bounds will be equal to the clickable area.
      * Otherwise the outermost semantics will be used to report bounds, size and position.
      */
-    private fun findWrapperToGetBounds(): LayoutNodeWrapper {
+    private fun findWrapperToGetBounds(): SemanticsWrapper {
         return if (unmergedConfig.isMergingSemanticsOfDescendants) {
             layoutNode.outerMergingSemantics ?: outerSemanticsNodeWrapper
         } else {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
index 0798b76..d24f050 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
@@ -824,8 +824,7 @@
  *
  * The presence of this property indicates that the element is toggleable.
  */
-var SemanticsPropertyReceiver.toggleableState
-by SemanticsProperties.ToggleableState
+var SemanticsPropertyReceiver.toggleableState by SemanticsProperties.ToggleableState
 
 /**
  * The node is marked as a password.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsWrapper.kt
index af6c6a6..40218de 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsWrapper.kt
@@ -16,21 +16,47 @@
 
 package androidx.compose.ui.semantics
 
+import androidx.compose.ui.geometry.MutableRect
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.geometry.toRect
+import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.layout.findRoot
+import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.node.DelegatingLayoutNodeWrapper
+import androidx.compose.ui.node.HitTestResult
 import androidx.compose.ui.node.LayoutNodeWrapper
+import androidx.compose.ui.node.requireOwner
+import androidx.compose.ui.unit.toSize
 
 internal class SemanticsWrapper(
     wrapped: LayoutNodeWrapper,
     semanticsModifier: SemanticsModifier
 ) : DelegatingLayoutNodeWrapper<SemanticsModifier>(wrapped, semanticsModifier) {
+    val semanticsSize: Size
+        get() {
+            val measuredSize = measuredSize
+            if (!useMinimumTouchTarget) {
+                return measuredSize.toSize()
+            }
+            val minTouchTargetSize = minimumTouchTargetSize
+            val width = maxOf(measuredSize.width.toFloat(), minTouchTargetSize.width)
+            val height = maxOf(measuredSize.height.toFloat(), minTouchTargetSize.height)
+            return Size(width, height)
+        }
+
+    private val useMinimumTouchTarget: Boolean
+        get() = modifier.semanticsConfiguration.getOrNull(SemanticsActions.OnClick) != null
+
     fun collapsedSemanticsConfiguration(): SemanticsConfiguration {
         val nextSemantics = wrapped.nearestSemantics { true }
         if (nextSemantics == null || modifier.semanticsConfiguration.isClearingSemantics) {
             return modifier.semanticsConfiguration
         }
 
-        var config = modifier.semanticsConfiguration.copy()
+        val config = modifier.semanticsConfiguration.copy()
         config.collapsePeer(nextSemantics.collapsedSemanticsConfiguration())
         return config
     }
@@ -51,13 +77,86 @@
 
     override fun hitTestSemantics(
         pointerPosition: Offset,
-        hitSemanticsWrappers: MutableList<SemanticsWrapper>
+        hitSemanticsWrappers: HitTestResult<SemanticsWrapper>
     ) {
-        if (isPointerInBounds(pointerPosition) && withinLayerBounds(pointerPosition)) {
-            hitSemanticsWrappers.add(this)
-
+        hitTestInMinimumTouchTarget(
+            pointerPosition,
+            hitSemanticsWrappers,
+            this
+        ) {
+            // Also, keep looking to see if we also might hit any children.
+            // This avoids checking layer bounds twice as when we call super.hitTest()
             val positionInWrapped = wrapped.fromParentPosition(pointerPosition)
             wrapped.hitTestSemantics(positionInWrapped, hitSemanticsWrappers)
         }
     }
+
+    fun semanticsPositionInRoot(): Offset {
+        if (!useMinimumTouchTarget) {
+            return positionInRoot()
+        }
+        check(isAttached) { ExpectAttachedLayoutCoordinates }
+        val root = findRoot()
+
+        val padding = calculateMinimumTouchTargetPadding(minimumTouchTargetSize)
+        val left = -padding.width
+        val top = -padding.height
+
+        return root.localPositionOf(this, Offset(left, top))
+    }
+
+    fun semanticsPositionInWindow(): Offset {
+        val positionInRoot = semanticsPositionInRoot()
+        return layoutNode.requireOwner().calculatePositionInWindow(positionInRoot)
+    }
+
+    fun semanticsBoundsInRoot(): Rect {
+        if (!useMinimumTouchTarget) {
+            return boundsInRoot()
+        }
+        return calculateBoundsInRoot().toRect()
+    }
+
+    fun semanticsBoundsInWindow(): Rect {
+        if (!useMinimumTouchTarget) {
+            return boundsInWindow()
+        }
+        val bounds = calculateBoundsInRoot()
+
+        val root = findRoot()
+        val topLeft = root.localToWindow(Offset(bounds.left, bounds.top))
+        val topRight = root.localToWindow(Offset(bounds.right, bounds.top))
+        val bottomRight = root.localToWindow(Offset(bounds.right, bounds.bottom))
+        val bottomLeft = root.localToWindow(Offset(bounds.left, bounds.bottom))
+        val left = minOf(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x)
+        val top = minOf(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y)
+        val right = maxOf(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x)
+        val bottom = maxOf(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y)
+
+        return Rect(left, top, right, bottom)
+    }
+
+    private fun calculateBoundsInRoot(): MutableRect {
+        check(isAttached) { ExpectAttachedLayoutCoordinates }
+        val root = findRoot()
+
+        val bounds = rectCache
+        val padding = calculateMinimumTouchTargetPadding(minimumTouchTargetSize)
+        bounds.left = -padding.width
+        bounds.top = -padding.height
+        bounds.right = measuredWidth + padding.width
+        bounds.bottom = measuredHeight + padding.height
+
+        var wrapper: LayoutNodeWrapper = this
+        while (wrapper !== root) {
+            wrapper.rectInParent(bounds, true)
+            if (bounds.isEmpty) {
+                bounds.set(0f, 0f, 0f, 0f)
+                return bounds
+            }
+
+            wrapper = wrapper.wrappedBy!!
+        }
+        return bounds
+    }
 }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt
index 24552d8..99f8808 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt
@@ -55,6 +55,7 @@
 import androidx.compose.ui.input.pointer.TestPointerInputEventData
 import androidx.compose.ui.layout.RootMeasurePolicy
 import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.node.HitTestResult
 import androidx.compose.ui.node.InternalCoreApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.LayoutNodeDrawScope
@@ -336,7 +337,7 @@
     internal fun onMouseScroll(position: Offset, event: MouseScrollEvent) {
         measureAndLayout()
 
-        val inputFilters = mutableListOf<PointerInputFilter>()
+        val inputFilters = HitTestResult<PointerInputFilter>()
         root.hitTest(position, inputFilters)
 
         for (
@@ -351,7 +352,7 @@
     }
 
     private var oldMoveFilters = listOf<PointerMoveEventFilter>()
-    private var newMoveFilters = mutableListOf<PointerInputFilter>()
+    private val newMoveFilters = HitTestResult<PointerInputFilter>()
 
     internal fun onPointerMove(position: Offset) {
         // TODO: do we actually need that?
@@ -391,7 +392,7 @@
         }
 
         oldMoveFilters = newMoveFilters.filterIsInstance<PointerMoveEventFilter>()
-        newMoveFilters = mutableListOf()
+        newMoveFilters.clear()
     }
 
     internal fun onPointerEnter(position: Offset) {
@@ -410,7 +411,7 @@
             }
         }
         oldMoveFilters = newMoveFilters.filterIsInstance<PointerMoveEventFilter>()
-        newMoveFilters = mutableListOf()
+        newMoveFilters.clear()
     }
 
     internal fun onPointerExit() {
@@ -421,6 +422,6 @@
             }
         }
         oldMoveFilters = listOf()
-        newMoveFilters = mutableListOf()
+        newMoveFilters.clear()
     }
 }
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/HitTestResultTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/HitTestResultTest.kt
new file mode 100644
index 0000000..33dbbbe
--- /dev/null
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/HitTestResultTest.kt
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2021 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.ui.node
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+import com.google.common.truth.Truth.assertThat
+
+@RunWith(JUnit4::class)
+class HitTestResultTest {
+    @Test
+    fun testHit() {
+        val hitTestResult = HitTestResult<String>()
+        hitTestResult.hit("Hello") {
+            hitTestResult.hit("World") {
+                assertThat(hitTestResult.isHit).isFalse()
+            }
+            assertThat(hitTestResult.isHit).isTrue()
+        }
+        assertThat(hitTestResult.isHit).isTrue()
+        assertThat(hitTestResult.isHitInMinimumTouchTargetBetter(0f)).isFalse()
+
+        assertThat(hitTestResult).hasSize(2)
+        assertThat(hitTestResult[0]).isEqualTo("Hello")
+        assertThat(hitTestResult[1]).isEqualTo("World")
+
+        hitTestResult.hit("Baz") {}
+        assertThat(hitTestResult.isHit).isTrue()
+        assertThat(hitTestResult).hasSize(1)
+        assertThat(hitTestResult[0]).isEqualTo("Baz")
+    }
+
+    @Test
+    fun testHitInMinimumTouchTarget() {
+        val hitTestResult = HitTestResult<String>()
+        hitTestResult.hitInMinimumTouchTarget("Hello", 1f) {
+            hitTestResult.hitInMinimumTouchTarget("World", 2f) { }
+            assertThat(hitTestResult.isHit).isFalse()
+            assertThat(hitTestResult.isHitInMinimumTouchTargetBetter(1.5f)).isTrue()
+            assertThat(hitTestResult.isHitInMinimumTouchTargetBetter(2.5f)).isFalse()
+        }
+        assertThat(hitTestResult.isHit).isFalse()
+        assertThat(hitTestResult.isHitInMinimumTouchTargetBetter(0.5f)).isTrue()
+        assertThat(hitTestResult.isHitInMinimumTouchTargetBetter(1.5f)).isFalse()
+
+        assertThat(hitTestResult).hasSize(2)
+        assertThat(hitTestResult[0]).isEqualTo("Hello")
+        assertThat(hitTestResult[1]).isEqualTo("World")
+
+        hitTestResult.hitInMinimumTouchTarget("Baz", 0.5f) { }
+        assertThat(hitTestResult.isHit).isFalse()
+        assertThat(hitTestResult).hasSize(1)
+        assertThat(hitTestResult[0]).isEqualTo("Baz")
+    }
+
+    @Test
+    fun testEasySpeculativeHit() {
+        val hitTestResult = HitTestResult<String>()
+        hitTestResult.speculativeHit("Hello", 1f) {
+        }
+        assertThat(hitTestResult).hasSize(0)
+
+        hitTestResult.speculativeHit("Hello", 1f) {
+            hitTestResult.hitInMinimumTouchTarget("World", 2f) {}
+        }
+
+        assertThat(hitTestResult.isHit).isFalse()
+        assertThat(hitTestResult.isHitInMinimumTouchTargetBetter(0.5f)).isTrue()
+        assertThat(hitTestResult.isHitInMinimumTouchTargetBetter(1.5f)).isFalse()
+
+        assertThat(hitTestResult).hasSize(2)
+        assertThat(hitTestResult[0]).isEqualTo("Hello")
+        assertThat(hitTestResult[1]).isEqualTo("World")
+    }
+
+    @Test
+    fun testSpeculativeHitWithMove() {
+        val hitTestResult = HitTestResult<String>()
+        hitTestResult.hitInMinimumTouchTarget("Foo", 1.5f) { }
+
+        hitTestResult.speculativeHit("Hello", 1f) {
+        }
+
+        assertThat(hitTestResult).hasSize(1)
+        assertThat(hitTestResult[0]).isEqualTo("Foo")
+
+        hitTestResult.speculativeHit("Hello", 1f) {
+            hitTestResult.hitInMinimumTouchTarget("World", 2f) {}
+        }
+
+        assertThat(hitTestResult.isHit).isFalse()
+        assertThat(hitTestResult.isHitInMinimumTouchTargetBetter(0.5f)).isTrue()
+        assertThat(hitTestResult.isHitInMinimumTouchTargetBetter(1.25f)).isFalse()
+
+        assertThat(hitTestResult).hasSize(2)
+        assertThat(hitTestResult[0]).isEqualTo("Hello")
+        assertThat(hitTestResult[1]).isEqualTo("World")
+    }
+
+    @Test
+    fun testClear() {
+        val hitTestResult = fillHitTestResult()
+        assertThat(hitTestResult).hasSize(5)
+        hitTestResult.clear()
+        assertThat(hitTestResult).hasSize(0)
+    }
+
+    @Test
+    fun testContains() {
+        val hitTestResult = fillHitTestResult()
+        assertThat(hitTestResult.contains("Hello")).isTrue()
+        assertThat(hitTestResult.contains("World")).isTrue()
+        assertThat(hitTestResult.contains("this")).isTrue()
+        assertThat(hitTestResult.contains("is")).isTrue()
+        assertThat(hitTestResult.contains("great")).isTrue()
+        assertThat(hitTestResult.contains("foo")).isFalse()
+    }
+
+    @Test
+    fun testContainsAll() {
+        val hitTestResult = fillHitTestResult()
+        assertThat(hitTestResult.containsAll(listOf("Hello", "great", "this"))).isTrue()
+        assertThat(hitTestResult.containsAll(listOf("Hello", "great", "foo", "this"))).isFalse()
+    }
+
+    @Test
+    fun testGet() {
+        val hitTestResult = fillHitTestResult()
+        assertThat(hitTestResult[0]).isEqualTo("Hello")
+        assertThat(hitTestResult[1]).isEqualTo("World")
+        assertThat(hitTestResult[2]).isEqualTo("this")
+        assertThat(hitTestResult[3]).isEqualTo("is")
+        assertThat(hitTestResult[4]).isEqualTo("great")
+    }
+
+    @Test
+    fun testIndexOf() {
+        val hitTestResult = fillHitTestResult("World")
+        assertThat(hitTestResult.indexOf("Hello")).isEqualTo(0)
+        assertThat(hitTestResult.indexOf("World")).isEqualTo(1)
+        assertThat(hitTestResult.indexOf("this")).isEqualTo(2)
+        assertThat(hitTestResult.indexOf("is")).isEqualTo(3)
+        assertThat(hitTestResult.indexOf("great")).isEqualTo(4)
+        assertThat(hitTestResult.indexOf("foo")).isEqualTo(-1)
+    }
+
+    @Test
+    fun testIsEmpty() {
+        val hitTestResult = fillHitTestResult()
+        assertThat(hitTestResult.isEmpty()).isFalse()
+        hitTestResult.clear()
+        assertThat(hitTestResult.isEmpty()).isTrue()
+        assertThat(HitTestResult<String>().isEmpty()).isTrue()
+    }
+
+    @Test
+    fun testIterator() {
+        val hitTestResult = fillHitTestResult()
+        assertThat(hitTestResult.toList()).isEqualTo(
+            listOf("Hello", "World", "this", "is", "great")
+        )
+    }
+
+    @Test
+    fun testLastIndexOf() {
+        val hitTestResult = fillHitTestResult("World")
+        assertThat(hitTestResult.lastIndexOf("Hello")).isEqualTo(0)
+        assertThat(hitTestResult.lastIndexOf("World")).isEqualTo(5)
+        assertThat(hitTestResult.lastIndexOf("this")).isEqualTo(2)
+        assertThat(hitTestResult.lastIndexOf("is")).isEqualTo(3)
+        assertThat(hitTestResult.lastIndexOf("great")).isEqualTo(4)
+        assertThat(hitTestResult.lastIndexOf("foo")).isEqualTo(-1)
+    }
+
+    @Test
+    fun testListIterator() {
+        val hitTestResult = fillHitTestResult()
+        val iterator = hitTestResult.listIterator()
+
+        val values = listOf("Hello", "World", "this", "is", "great")
+
+        values.forEachIndexed { index, value ->
+            assertThat(iterator.nextIndex()).isEqualTo(index)
+            if (index > 0) {
+                assertThat(iterator.previousIndex()).isEqualTo(index - 1)
+            }
+            assertThat(iterator.hasNext()).isTrue()
+            val hasPrevious = (index != 0)
+            assertThat(iterator.hasPrevious()).isEqualTo(hasPrevious)
+            assertThat(iterator.next()).isEqualTo(value)
+        }
+
+        for (index in values.lastIndex downTo 0) {
+            val value = values[index]
+            assertThat(iterator.previous()).isEqualTo(value)
+        }
+    }
+
+    @Test
+    fun testListIteratorWithStart() {
+        val hitTestResult = fillHitTestResult()
+        val iterator = hitTestResult.listIterator(2)
+
+        val values = listOf("Hello", "World", "this", "is", "great")
+
+        for (index in 2..values.lastIndex) {
+            assertThat(iterator.nextIndex()).isEqualTo(index)
+            if (index > 0) {
+                assertThat(iterator.previousIndex()).isEqualTo(index - 1)
+            }
+            assertThat(iterator.hasNext()).isTrue()
+            val hasPrevious = (index != 0)
+            assertThat(iterator.hasPrevious()).isEqualTo(hasPrevious)
+            assertThat(iterator.next()).isEqualTo(values[index])
+        }
+
+        for (index in values.lastIndex downTo 0) {
+            val value = values[index]
+            assertThat(iterator.previous()).isEqualTo(value)
+        }
+    }
+
+    @Test
+    fun testSubList() {
+        val hitTestResult = fillHitTestResult()
+        val subList = hitTestResult.subList(2, 4)
+        assertThat(subList).hasSize(2)
+
+        assertThat(subList.toList()).isEqualTo(listOf("this", "is"))
+        assertThat(subList.contains("this")).isTrue()
+        assertThat(subList.contains("foo")).isFalse()
+        assertThat(subList.containsAll(listOf("this", "is"))).isTrue()
+        assertThat(subList.containsAll(listOf("is", "this"))).isTrue()
+        assertThat(subList.containsAll(listOf("foo", "this"))).isFalse()
+        assertThat(subList[0]).isEqualTo("this")
+        assertThat(subList[1]).isEqualTo("is")
+        assertThat(subList.indexOf("is")).isEqualTo(1)
+        assertThat(subList.isEmpty()).isFalse()
+        assertThat(hitTestResult.subList(4, 4).isEmpty()).isTrue()
+        assertThat(subList.subList(0, 2).toList()).isEqualTo(subList.toList())
+        assertThat(subList.subList(0, 1)[0]).isEqualTo("this")
+
+        val listIterator1 = subList.listIterator()
+        assertThat(listIterator1.hasNext()).isTrue()
+        assertThat(listIterator1.hasPrevious()).isFalse()
+        assertThat(listIterator1.nextIndex()).isEqualTo(0)
+        assertThat(listIterator1.next()).isEqualTo("this")
+        assertThat(listIterator1.hasNext()).isTrue()
+        assertThat(listIterator1.hasPrevious()).isTrue()
+        assertThat(listIterator1.nextIndex()).isEqualTo(1)
+        assertThat(listIterator1.next()).isEqualTo("is")
+        assertThat(listIterator1.hasNext()).isFalse()
+        assertThat(listIterator1.hasPrevious()).isTrue()
+        assertThat(listIterator1.previousIndex()).isEqualTo(1)
+        assertThat(listIterator1.previous()).isEqualTo("is")
+
+        val listIterator2 = subList.listIterator(1)
+        assertThat(listIterator2.hasPrevious()).isTrue()
+        assertThat(listIterator2.hasNext()).isTrue()
+        assertThat(listIterator2.previousIndex()).isEqualTo(0)
+        assertThat(listIterator2.nextIndex()).isEqualTo(1)
+        assertThat(listIterator2.previous()).isEqualTo("this")
+    }
+
+    private fun fillHitTestResult(last: String? = null): HitTestResult<String> {
+        val hitTestResult = HitTestResult<String>()
+        hitTestResult.hit("Hello") {
+            hitTestResult.hit("World") {
+                hitTestResult.hit("this") {
+                    hitTestResult.hit("is") {
+                        hitTestResult.hit("great") {
+                            last?.let {
+                                hitTestResult.hit(it) {}
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return hitTestResult
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 6518162..f476cf4 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -15,6 +15,7 @@
  */
 package androidx.compose.ui.node
 
+import androidx.compose.testutils.TestViewConfiguration
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.autofill.Autofill
@@ -48,13 +49,18 @@
 import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.platform.ViewConfiguration
 import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsModifier
+import androidx.compose.ui.semantics.SemanticsWrapper
 import androidx.compose.ui.text.font.Font
 import androidx.compose.ui.text.input.TextInputService
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.toOffset
 import androidx.compose.ui.zIndex
 import com.google.common.truth.Truth.assertThat
@@ -813,6 +819,353 @@
     }
 
     @Test
+    fun hitTest_pointerInMinimumTouchTarget_pointerInputFilterHit() {
+        val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
+        val layoutNode =
+            LayoutNode(
+                0, 0, 1, 1,
+                PointerInputModifierImpl(pointerInputFilter),
+                DpSize(48.dp, 48.dp)
+            ).apply {
+                attach(MockOwner())
+            }
+        val hit = mutableListOf<PointerInputFilter>()
+
+        layoutNode.hitTest(Offset(-3f, 3f), hit, true)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputFilter))
+    }
+
+    @Test
+    fun hitTest_pointerInMinimumTouchTarget_pointerInputFilterHit_nestedNodes() {
+        val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
+        val outerNode = LayoutNode(0, 0, 1, 1).apply { attach(MockOwner()) }
+        val layoutNode = LayoutNode(
+            0, 0, 1, 1,
+            PointerInputModifierImpl(pointerInputFilter),
+            DpSize(48.dp, 48.dp)
+        )
+        outerNode.add(layoutNode)
+        layoutNode.onNodePlaced()
+        val hit = mutableListOf<PointerInputFilter>()
+
+        outerNode.hitTest(Offset(-3f, 3f), hit, true)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputFilter))
+    }
+
+    @Test
+    fun hitTest_pointerInMinimumTouchTarget_closestHit() {
+        val pointerInputFilter1: PointerInputFilter = mockPointerInputFilter()
+        val layoutNode1 = LayoutNode(
+            0, 0, 5, 5,
+            PointerInputModifierImpl(pointerInputFilter1),
+            DpSize(48.dp, 48.dp)
+        )
+
+        val pointerInputFilter2: PointerInputFilter = mockPointerInputFilter()
+        val layoutNode2 = LayoutNode(
+            6, 6, 11, 11,
+            PointerInputModifierImpl(pointerInputFilter2),
+            DpSize(48.dp, 48.dp)
+        )
+        val outerNode = LayoutNode(0, 0, 11, 11).apply { attach(MockOwner()) }
+        outerNode.add(layoutNode1)
+        outerNode.add(layoutNode2)
+        layoutNode1.onNodePlaced()
+        layoutNode2.onNodePlaced()
+
+        val hit = mutableListOf<PointerInputFilter>()
+
+        // Hit closer to layoutNode1
+        outerNode.hitTest(Offset(5.1f, 5.5f), hit, true)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputFilter1))
+
+        hit.clear()
+
+        // Hit closer to layoutNode2
+        outerNode.hitTest(Offset(5.9f, 5.5f), hit, true)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputFilter2))
+
+        hit.clear()
+
+        // Hit closer to layoutNode1
+        outerNode.hitTest(Offset(5.5f, 5.1f), hit, true)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputFilter1))
+
+        hit.clear()
+
+        // Hit closer to layoutNode2
+        outerNode.hitTest(Offset(5.5f, 5.9f), hit, true)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputFilter2))
+
+        hit.clear()
+
+        // Hit inside layoutNode1
+        outerNode.hitTest(Offset(4.9f, 4.9f), hit, true)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputFilter1))
+
+        hit.clear()
+
+        // Hit inside layoutNode2
+        outerNode.hitTest(Offset(6.1f, 6.1f), hit, true)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputFilter2))
+    }
+
+    /**
+     * When a child is in the minimum touch target area, but the parent is big enough to not
+     * worry about minimum touch target, the child should still be able to be hit outside the
+     * parent's bounds.
+     */
+    @Test
+    fun hitTest_pointerInMinimumTouchTarget_inChild_closestHit() {
+        test_pointerInMinimumTouchTarget_inChild_closestHit { outerNode, nodeWithChild, soloNode ->
+            outerNode.add(nodeWithChild)
+            outerNode.add(soloNode)
+        }
+    }
+
+    /**
+     * When a child is in the minimum touch target area, but the parent is big enough to not
+     * worry about minimum touch target, the child should still be able to be hit outside the
+     * parent's bounds. This is different from
+     * [hitTest_pointerInMinimumTouchTarget_inChild_closestHit] because the node with the nested
+     * child is after the other node.
+     */
+    @Test
+    fun hitTest_pointerInMinimumTouchTarget_inChildOver_closestHit() {
+        test_pointerInMinimumTouchTarget_inChild_closestHit { outerNode, nodeWithChild, soloNode ->
+            outerNode.add(soloNode)
+            outerNode.add(nodeWithChild)
+        }
+    }
+
+    private fun test_pointerInMinimumTouchTarget_inChild_closestHit(
+        block: (outerNode: LayoutNode, nodeWithChild: LayoutNode, soloNode: LayoutNode) -> Unit
+    ) {
+        val pointerInputFilter1: PointerInputFilter = mockPointerInputFilter()
+        val layoutNode1 = LayoutNode(
+            5, 5, 10, 10,
+            PointerInputModifierImpl(pointerInputFilter1),
+            DpSize(48.dp, 48.dp)
+        )
+
+        val pointerInputFilter2: PointerInputFilter = mockPointerInputFilter()
+        val layoutNode2 = LayoutNode(
+            0, 0, 10, 10,
+            PointerInputModifierImpl(pointerInputFilter2),
+            DpSize(48.dp, 48.dp)
+        )
+        layoutNode2.add(layoutNode1)
+
+        val pointerInputFilter3: PointerInputFilter = mockPointerInputFilter()
+        val layoutNode3 = LayoutNode(
+            12, 12, 17, 17,
+            PointerInputModifierImpl(pointerInputFilter3),
+            DpSize(48.dp, 48.dp)
+        )
+
+        val outerNode = LayoutNode(0, 0, 20, 20).apply { attach(MockOwner()) }
+        block(outerNode, layoutNode2, layoutNode3)
+        layoutNode1.onNodePlaced()
+        layoutNode2.onNodePlaced()
+        layoutNode3.onNodePlaced()
+
+        val hit = mutableListOf<PointerInputFilter>()
+
+        // Hit outside of layoutNode2, but near layoutNode1
+        outerNode.hitTest(Offset(10.1f, 10.1f), hit, true)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputFilter2, pointerInputFilter1))
+
+        hit.clear()
+
+        // Hit closer to layoutNode3
+        outerNode.hitTest(Offset(11.9f, 11.9f), hit, true)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputFilter3))
+    }
+
+    @Test
+    fun hitTest_pointerInMinimumTouchTarget_closestHitWithOverlap() {
+        val pointerInputFilter1: PointerInputFilter = mockPointerInputFilter()
+        val layoutNode1 = LayoutNode(
+            0, 0, 5, 5, PointerInputModifierImpl(pointerInputFilter1),
+            DpSize(48.dp, 48.dp)
+        )
+
+        val pointerInputFilter2: PointerInputFilter = mockPointerInputFilter()
+        val layoutNode2 = LayoutNode(
+            4, 4, 9, 9,
+            PointerInputModifierImpl(pointerInputFilter2),
+            DpSize(48.dp, 48.dp)
+        )
+        val outerNode = LayoutNode(0, 0, 9, 9).apply { attach(MockOwner()) }
+        outerNode.add(layoutNode1)
+        outerNode.add(layoutNode2)
+        layoutNode1.onNodePlaced()
+        layoutNode2.onNodePlaced()
+
+        val hit = mutableListOf<PointerInputFilter>()
+
+        // Hit layoutNode1
+        outerNode.hitTest(Offset(3.95f, 3.95f), hit, true)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputFilter1))
+
+        hit.clear()
+
+        // Hit layoutNode2
+        outerNode.hitTest(Offset(4.05f, 4.05f), hit, true)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputFilter2))
+    }
+
+    @Test
+    fun hitTestSemantics_pointerInMinimumTouchTarget_pointerInputFilterHit() {
+        val semanticsConfiguration = SemanticsConfiguration()
+        val semanticsModifier = object : SemanticsModifier {
+            override val id: Int = 1
+            override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
+        }
+        val layoutNode =
+            LayoutNode(
+                0, 0, 1, 1,
+                semanticsModifier,
+                DpSize(48.dp, 48.dp)
+            ).apply {
+                attach(MockOwner())
+            }
+        val hit = HitTestResult<SemanticsWrapper>()
+
+        layoutNode.hitTestSemantics(Offset(-3f, 3f), hit)
+
+        assertThat(hit).hasSize(1)
+        assertThat(hit[0].modifier).isEqualTo(semanticsModifier)
+    }
+
+    @Test
+    fun hitTestSemantics_pointerInMinimumTouchTarget_pointerInputFilterHit_nestedNodes() {
+        val semanticsConfiguration = SemanticsConfiguration()
+        val semanticsModifier = object : SemanticsModifier {
+            override val id: Int = 1
+            override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
+        }
+        val outerNode = LayoutNode(0, 0, 1, 1).apply { attach(MockOwner()) }
+        val layoutNode = LayoutNode(0, 0, 1, 1, semanticsModifier, DpSize(48.dp, 48.dp))
+        outerNode.add(layoutNode)
+        layoutNode.onNodePlaced()
+        val hit = HitTestResult<SemanticsWrapper>()
+
+        layoutNode.hitTestSemantics(Offset(-3f, 3f), hit)
+
+        assertThat(hit).hasSize(1)
+        assertThat(hit[0].modifier).isEqualTo(semanticsModifier)
+    }
+
+    @Test
+    fun hitTestSemantics_pointerInMinimumTouchTarget_closestHit() {
+        val semanticsConfiguration = SemanticsConfiguration()
+        val semanticsModifier1 = object : SemanticsModifier {
+            override val id: Int = 1
+            override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
+        }
+        val semanticsModifier2 = object : SemanticsModifier {
+            override val id: Int = 1
+            override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
+        }
+        val layoutNode1 = LayoutNode(0, 0, 5, 5, semanticsModifier1, DpSize(48.dp, 48.dp))
+        val layoutNode2 = LayoutNode(6, 6, 11, 11, semanticsModifier2, DpSize(48.dp, 48.dp))
+        val outerNode = LayoutNode(0, 0, 11, 11).apply { attach(MockOwner()) }
+        outerNode.add(layoutNode1)
+        outerNode.add(layoutNode2)
+        layoutNode1.onNodePlaced()
+        layoutNode2.onNodePlaced()
+
+        // Hit closer to layoutNode1
+        val hit1 = HitTestResult<SemanticsWrapper>()
+        outerNode.hitTestSemantics(Offset(5.1f, 5.5f), hit1, true)
+
+        assertThat(hit1).hasSize(1)
+        assertThat(hit1[0].modifier).isEqualTo(semanticsModifier1)
+
+        // Hit closer to layoutNode2
+        val hit2 = HitTestResult<SemanticsWrapper>()
+        outerNode.hitTestSemantics(Offset(5.9f, 5.5f), hit2, true)
+
+        assertThat(hit2).hasSize(1)
+        assertThat(hit2[0].modifier).isEqualTo(semanticsModifier2)
+
+        // Hit closer to layoutNode1
+        val hit3 = HitTestResult<SemanticsWrapper>()
+        outerNode.hitTestSemantics(Offset(5.5f, 5.1f), hit3, true)
+
+        assertThat(hit3).hasSize(1)
+        assertThat(hit3[0].modifier).isEqualTo(semanticsModifier1)
+
+        // Hit closer to layoutNode2
+        val hit4 = HitTestResult<SemanticsWrapper>()
+        outerNode.hitTestSemantics(Offset(5.5f, 5.9f), hit4, true)
+
+        assertThat(hit4).hasSize(1)
+        assertThat(hit4[0].modifier).isEqualTo(semanticsModifier2)
+
+        // Hit inside layoutNode1
+        val hit5 = HitTestResult<SemanticsWrapper>()
+        outerNode.hitTestSemantics(Offset(4.9f, 4.9f), hit5, true)
+
+        assertThat(hit5).hasSize(1)
+        assertThat(hit5[0].modifier).isEqualTo(semanticsModifier1)
+
+        // Hit inside layoutNode2
+        val hit6 = HitTestResult<SemanticsWrapper>()
+        outerNode.hitTestSemantics(Offset(6.1f, 6.1f), hit6, true)
+
+        assertThat(hit6).hasSize(1)
+        assertThat(hit6[0].modifier).isEqualTo(semanticsModifier2)
+    }
+
+    @Test
+    fun hitTestSemantics_pointerInMinimumTouchTarget_closestHitWithOverlap() {
+        val semanticsConfiguration = SemanticsConfiguration()
+        val semanticsModifier1 = object : SemanticsModifier {
+            override val id: Int = 1
+            override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
+        }
+        val semanticsModifier2 = object : SemanticsModifier {
+            override val id: Int = 1
+            override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
+        }
+        val layoutNode1 = LayoutNode(0, 0, 5, 5, semanticsModifier1, DpSize(48.dp, 48.dp))
+        val layoutNode2 = LayoutNode(4, 4, 9, 9, semanticsModifier2, DpSize(48.dp, 48.dp))
+        val outerNode = LayoutNode(0, 0, 11, 11).apply { attach(MockOwner()) }
+        outerNode.add(layoutNode1)
+        outerNode.add(layoutNode2)
+        layoutNode1.onNodePlaced()
+        layoutNode2.onNodePlaced()
+
+        // Hit layoutNode1
+        val hit1 = HitTestResult<SemanticsWrapper>()
+        outerNode.hitTestSemantics(Offset(3.95f, 3.95f), hit1, true)
+
+        assertThat(hit1).hasSize(1)
+        assertThat(hit1[0].modifier).isEqualTo(semanticsModifier1)
+
+        // Hit layoutNode2
+        val hit2 = HitTestResult<SemanticsWrapper>()
+        outerNode.hitTestSemantics(Offset(4.05f, 4.05f), hit2, true)
+
+        assertThat(hit2).hasSize(1)
+        assertThat(hit2[0].modifier).isEqualTo(semanticsModifier2)
+    }
+
+    @Test
     fun hitTest_pointerOutOfBounds_nothingHit() {
         val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
         val layoutNode =
@@ -840,6 +1193,34 @@
     }
 
     @Test
+    fun hitTest_pointerOutOfBounds_nothingHit_extendedBounds() {
+        val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
+        val layoutNode =
+            LayoutNode(
+                0, 0, 1, 1,
+                PointerInputModifierImpl(pointerInputFilter),
+                minimumTouchTargetSize = DpSize(4.dp, 8.dp)
+            ).apply {
+                attach(MockOwner())
+            }
+        val hit = mutableListOf<PointerInputFilter>()
+
+        layoutNode.hitTest(Offset(-3f, -5f), hit)
+        layoutNode.hitTest(Offset(0f, -5f), hit)
+        layoutNode.hitTest(Offset(3f, -5f), hit)
+
+        layoutNode.hitTest(Offset(-3f, 0f), hit)
+        // 0, 0 would hit
+        layoutNode.hitTest(Offset(3f, 0f), hit)
+
+        layoutNode.hitTest(Offset(-3f, 5f), hit)
+        layoutNode.hitTest(Offset(0f, 5f), hit)
+        layoutNode.hitTest(Offset(-3f, 5f), hit)
+
+        assertThat(hit).isEmpty()
+    }
+
+    @Test
     fun hitTest_nestedOffsetNodesHits3_allHitInCorrectOrder() {
         hitTest_nestedOffsetNodes_allHitInCorrectOrder(3)
     }
@@ -1898,29 +2279,46 @@
     override val sharedDrawScope = LayoutNodeDrawScope()
 }
 
-private fun LayoutNode(x: Int, y: Int, x2: Int, y2: Int, modifier: Modifier = Modifier) =
-    LayoutNode().apply {
-        this.modifier = modifier
-        measurePolicy = object : LayoutNode.NoIntrinsicsMeasurePolicy("not supported") {
-            override fun MeasureScope.measure(
-                measurables: List<Measurable>,
-                constraints: Constraints
-            ): MeasureResult =
-                layout(x2 - x, y2 - y) {
-                    measurables.forEach { it.measure(constraints).place(0, 0) }
-                }
-        }
-        attach(MockOwner())
-        layoutState = LayoutNode.LayoutState.NeedsRemeasure
-        remeasure(Constraints())
-        var wrapper: LayoutNodeWrapper? = outerLayoutNodeWrapper
-        while (wrapper != null) {
-            wrapper.measureResult = innerLayoutNodeWrapper.measureResult
-            wrapper = (wrapper as? LayoutNodeWrapper)?.wrapped
-        }
-        place(x, y)
-        detach()
+private fun LayoutNode.hitTest(
+    pointerPosition: Offset,
+    hitPointerInputFilters: MutableList<PointerInputFilter>,
+    isTouchEvent: Boolean = false
+) {
+    val hitTestResult = HitTestResult<PointerInputFilter>()
+    hitTest(pointerPosition, hitTestResult, isTouchEvent)
+    hitPointerInputFilters.addAll(hitTestResult)
+}
+
+private fun LayoutNode(
+    x: Int,
+    y: Int,
+    x2: Int,
+    y2: Int,
+    modifier: Modifier = Modifier,
+    minimumTouchTargetSize: DpSize = DpSize.Zero
+) = LayoutNode().apply {
+    this.viewConfiguration = TestViewConfiguration(minimumTouchTargetSize = minimumTouchTargetSize)
+    this.modifier = modifier
+    measurePolicy = object : LayoutNode.NoIntrinsicsMeasurePolicy("not supported") {
+        override fun MeasureScope.measure(
+            measurables: List<Measurable>,
+            constraints: Constraints
+        ): MeasureResult =
+            layout(x2 - x, y2 - y) {
+                measurables.forEach { it.measure(constraints).place(0, 0) }
+            }
     }
+    attach(MockOwner())
+    layoutState = LayoutNode.LayoutState.NeedsRemeasure
+    remeasure(Constraints())
+    var wrapper: LayoutNodeWrapper? = outerLayoutNodeWrapper
+    while (wrapper != null) {
+        wrapper.measureResult = innerLayoutNodeWrapper.measureResult
+        wrapper = (wrapper as? LayoutNodeWrapper)?.wrapped
+    }
+    place(x, y)
+    detach()
+}
 
 private fun mockPointerInputFilter(): PointerInputFilter = object : PointerInputFilter() {
     override fun onPointerEvent(
@@ -1932,4 +2330,4 @@
 
     override fun onCancel() {
     }
-}
\ No newline at end of file
+}