Merge "Fix GestureScope position calculations" into androidx-master-dev
diff --git a/compose/desktop/desktop/build.gradle b/compose/desktop/desktop/build.gradle
index f002278..aa33496 100644
--- a/compose/desktop/desktop/build.gradle
+++ b/compose/desktop/desktop/build.gradle
@@ -49,9 +49,6 @@
             api(SKIKO)
 
             implementation(KOTLIN_COROUTINES_SWING)
-
-            // TODO: move to jvmTest
-            implementation(JUNIT)
         }
 
         jvmTest {
@@ -59,9 +56,15 @@
             resources.srcDirs += "src/jvmTest/res"
             dependencies {
                 implementation(SKIKO_CURRENT_OS)
+                implementation project(":ui:ui-test")
+                implementation(JUNIT)
                 implementation(TRUTH)
             }
         }
+
+        test.dependencies {
+            implementation project(":ui:ui-test")
+        }
     }
 }
 
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/InputsTest.kt b/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/InputsTest.kt
new file mode 100644
index 0000000..c6e27ea
--- /dev/null
+++ b/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/InputsTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 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.ui.desktop
+
+import androidx.compose.material.Slider
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.ui.test.assertValueEquals
+import androidx.ui.test.onNodeWithTag
+import androidx.ui.test.runOnIdle
+
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import androidx.ui.test.createComposeRule
+
+@RunWith(JUnit4::class)
+class InputsTest {
+    private val tag = "slider"
+
+    @get:Rule
+    val composeTestRule = createComposeRule(disableTransitions = true)
+
+    @Test
+    fun sliderPosition_valueCoercion() {
+        val state = mutableStateOf(0f)
+        composeTestRule.setContent {
+            Slider(
+                modifier = Modifier.testTag(tag),
+                value = state.value,
+                onValueChange = { state.value = it },
+                valueRange = 0f..1f
+            )
+        }
+        runOnIdle {
+            state.value = 2f
+        }
+        onNodeWithTag(tag).assertValueEquals("100 percent")
+        runOnIdle {
+            state.value = -123145f
+        }
+        onNodeWithTag(tag).assertValueEquals("0 percent")
+    }
+}
\ No newline at end of file
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/ParagraphTest.kt b/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/ParagraphTest.kt
index 4ce0fc1..d64aa8a 100644
--- a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/ParagraphTest.kt
+++ b/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/ParagraphTest.kt
@@ -14,10 +14,8 @@
 * limitations under the License.
 */
 
-package androidx.compose.desktop
+package androidx.ui.desktop
 
-import androidx.compose.desktop.test.DesktopScreenshotTestRule
-import androidx.compose.desktop.test.TestSkiaWindow
 import androidx.compose.foundation.Text
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
@@ -37,6 +35,8 @@
 import androidx.compose.ui.text.platform.font
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import androidx.ui.test.DesktopScreenshotTestRule
+import androidx.ui.test.TestSkiaWindow
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/graphics/DesktopGraphicsTest.kt b/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/graphics/DesktopGraphicsTest.kt
index 1d73970..d1deb20 100644
--- a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/graphics/DesktopGraphicsTest.kt
+++ b/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/graphics/DesktopGraphicsTest.kt
@@ -16,8 +16,7 @@
 
 package androidx.compose.ui.graphics
 
-import androidx.compose.desktop.initCompose
-import androidx.compose.desktop.test.DesktopScreenshotTestRule
+import androidx.ui.test.DesktopScreenshotTestRule
 import org.jetbrains.skija.Surface
 import org.junit.After
 import org.junit.Rule
@@ -47,10 +46,4 @@
     fun teardown() {
         _surface?.close()
     }
-
-    private companion object {
-        init {
-            initCompose()
-        }
-    }
 }
\ No newline at end of file
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/graphics/DesktopPaintTest.kt b/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/graphics/DesktopPaintTest.kt
index a7b97be..fff340a 100644
--- a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/graphics/DesktopPaintTest.kt
+++ b/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/graphics/DesktopPaintTest.kt
@@ -70,7 +70,7 @@
         canvas.drawRect(left = 0f, top = 0f, right = 16f, bottom = 16f, paint = redPaint)
 
         canvas.drawImage(
-            image = imageFromResource("androidx.compose.desktop/test.png"),
+            image = imageFromResource("androidx/compose/desktop/test.png"),
             topLeftOffset = Offset(2f, 4f),
             paint = Paint().apply {
                 colorFilter = ColorFilter(Color.Blue, BlendMode.Plus)
@@ -83,7 +83,7 @@
     @Test
     fun filterQuality() {
         canvas.drawImageRect(
-            image = imageFromResource("androidx.compose.desktop/test.png"),
+            image = imageFromResource("androidx/compose/desktop/test.png"),
             srcOffset = IntOffset(0, 2),
             srcSize = IntSize(2, 4),
             dstOffset = IntOffset(0, 4),
@@ -91,7 +91,7 @@
             paint = redPaint
         )
         canvas.drawImageRect(
-            image = imageFromResource("androidx.compose.desktop/test.png"),
+            image = imageFromResource("androidx/compose/desktop/test.png"),
             srcOffset = IntOffset(0, 2),
             srcSize = IntSize(2, 4),
             dstOffset = IntOffset(4, 4),
@@ -101,7 +101,7 @@
             }
         )
         canvas.drawImageRect(
-            image = imageFromResource("androidx.compose.desktop/test.png"),
+            image = imageFromResource("androidx/compose/desktop/test.png"),
             srcOffset = IntOffset(0, 2),
             srcSize = IntSize(2, 4),
             dstOffset = IntOffset(8, 4),
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/graphics/canvas/DesktopCanvasTest.kt b/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/graphics/canvas/DesktopCanvasTest.kt
index 40ec7ffd..789ee32 100644
--- a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/graphics/canvas/DesktopCanvasTest.kt
+++ b/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/graphics/canvas/DesktopCanvasTest.kt
@@ -92,12 +92,12 @@
     @Test
     fun drawImage() {
         canvas.drawImage(
-            image = imageFromResource("androidx.compose.desktop/test.png"),
+            image = imageFromResource("androidx/compose/desktop/test.png"),
             topLeftOffset = Offset(2f, 4f),
             paint = redPaint
         )
         canvas.drawImage(
-            image = imageFromResource("androidx.compose.desktop/test.png"),
+            image = imageFromResource("androidx/compose/desktop/test.png"),
             topLeftOffset = Offset(-2f, 0f),
             paint = redPaint
         )
@@ -108,7 +108,7 @@
     @Test
     fun drawImageRect() {
         canvas.drawImageRect(
-            image = imageFromResource("androidx.compose.desktop/test.png"),
+            image = imageFromResource("androidx/compose/desktop/test.png"),
             srcOffset = IntOffset(0, 2),
             srcSize = IntSize(2, 4),
             dstOffset = IntOffset(0, 4),
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/platform/DrawLayerTest.kt b/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/platform/DrawLayerTest.kt
index f91c0c2..24debbf 100644
--- a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/platform/DrawLayerTest.kt
+++ b/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/platform/DrawLayerTest.kt
@@ -16,8 +16,6 @@
 
 package androidx.compose.ui.platform
 
-import androidx.compose.desktop.test.DesktopScreenshotTestRule
-import androidx.compose.desktop.test.TestSkiaWindow
 import androidx.compose.foundation.Box
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -26,6 +24,8 @@
 import androidx.compose.ui.drawLayer
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
+import androidx.ui.test.DesktopScreenshotTestRule
+import androidx.ui.test.TestSkiaWindow
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ConstraintLayoutTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ConstraintLayoutTest.kt
index dad4601..e60a42d 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ConstraintLayoutTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ConstraintLayoutTest.kt
@@ -71,7 +71,7 @@
                         height = Dimension.wrapContent
                     }
                     // Try to be large to make wrap content impossible.
-                    .preferredWidth((composeTestRule.displayMetrics.widthPixels).toDp())
+                    .preferredWidth((composeTestRule.displaySize.width).toDp())
                     // This could be any (width in height out child) e.g. text
                     .aspectRatio(2f)
                     .onPositioned { coordinates ->
@@ -93,12 +93,12 @@
         runOnIdle {
             // The aspect ratio could not wrap and it is wrap suggested, so it respects constraints.
             assertEquals(
-                (composeTestRule.displayMetrics.widthPixels / 2),
+                (composeTestRule.displaySize.width / 2),
                 aspectRatioBoxSize.value!!.width
             )
             // Aspect ratio is preserved.
             assertEquals(
-                (composeTestRule.displayMetrics.widthPixels / 2 / 2),
+                (composeTestRule.displaySize.width / 2 / 2),
                 aspectRatioBoxSize.value!!.height
             )
             // Divider has fixed width 1.dp in constraint set.
@@ -129,7 +129,7 @@
                         height = Dimension.preferredWrapContent
                     }
                     // Try to be large to make wrap content impossible.
-                    .preferredWidth((composeTestRule.displayMetrics.widthPixels).toDp())
+                    .preferredWidth((composeTestRule.displaySize.width).toDp())
                     // This could be any (width in height out child) e.g. text
                     .aspectRatio(2f)
                     .onPositioned { coordinates ->
@@ -151,12 +151,12 @@
         runOnIdle {
             // The aspect ratio could not wrap and it is wrap suggested, so it respects constraints.
             assertEquals(
-                (composeTestRule.displayMetrics.widthPixels / 2),
+                (composeTestRule.displaySize.width / 2),
                 aspectRatioBoxSize.value!!.width
             )
             // Aspect ratio is preserved.
             assertEquals(
-                (composeTestRule.displayMetrics.widthPixels / 2 / 2),
+                (composeTestRule.displaySize.width / 2 / 2),
                 aspectRatioBoxSize.value!!.height
             )
             // Divider has fixed width 1.dp in constraint set.
@@ -187,7 +187,7 @@
                         height = Dimension.wrapContent
                     }
                     // Try to be large to make wrap content impossible.
-                    .preferredWidth((composeTestRule.displayMetrics.widthPixels).toDp())
+                    .preferredWidth((composeTestRule.displaySize.width).toDp())
                     // This could be any (width in height out child) e.g. text
                     .aspectRatio(2f)
                     .onPositioned { coordinates ->
@@ -210,12 +210,12 @@
         runOnIdle {
             // The aspect ratio could not wrap and it is wrap suggested, so it respects constraints.
             assertEquals(
-                (composeTestRule.displayMetrics.widthPixels / 2),
+                (composeTestRule.displaySize.width / 2),
                 aspectRatioBoxSize.value!!.width
             )
             // Aspect ratio is preserved.
             assertEquals(
-                (composeTestRule.displayMetrics.widthPixels / 2 / 2),
+                (composeTestRule.displaySize.width / 2 / 2),
                 aspectRatioBoxSize.value!!.height
             )
             // Divider has fixed width 1.dp in constraint set.
@@ -245,7 +245,7 @@
                         height = Dimension.wrapContent
                     }
                     // Try to be large to make wrap content impossible.
-                    .preferredWidth((composeTestRule.displayMetrics.widthPixels).toDp())
+                    .preferredWidth((composeTestRule.displaySize.width).toDp())
                     // This could be any (width in height out child) e.g. text
                     .aspectRatio(2f)
                     .onPositioned { coordinates ->
@@ -268,12 +268,12 @@
         runOnIdle {
             // The aspect ratio could not wrap and it is wrap suggested, so it respects constraints.
             assertEquals(
-                (composeTestRule.displayMetrics.widthPixels / 2),
+                (composeTestRule.displaySize.width / 2),
                 aspectRatioBoxSize.value!!.width
             )
             // Aspect ratio is preserved.
             assertEquals(
-                (composeTestRule.displayMetrics.widthPixels / 2 / 2),
+                (composeTestRule.displaySize.width / 2 / 2),
                 aspectRatioBoxSize.value!!.height
             )
             // Divider has fixed width 1.dp in constraint set.
@@ -395,8 +395,8 @@
             }
         }
 
-        val displayWidth = composeTestRule.displayMetrics.widthPixels
-        val displayHeight = composeTestRule.displayMetrics.heightPixels
+        val displayWidth = composeTestRule.displaySize.width
+        val displayHeight = composeTestRule.displaySize.height
 
         runOnIdle {
             assertEquals(
@@ -463,8 +463,8 @@
             }
         }
 
-        val displayWidth = composeTestRule.displayMetrics.widthPixels
-        val displayHeight = composeTestRule.displayMetrics.heightPixels
+        val displayWidth = composeTestRule.displaySize.width
+        val displayHeight = composeTestRule.displaySize.height
 
         runOnIdle {
             assertEquals(
@@ -536,8 +536,8 @@
             }
         }
 
-        val displayWidth = composeTestRule.displayMetrics.widthPixels
-        val displayHeight = composeTestRule.displayMetrics.heightPixels
+        val displayWidth = composeTestRule.displaySize.width
+        val displayHeight = composeTestRule.displaySize.height
 
         runOnIdle {
             assertEquals(
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index 364462c..da3d897 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -81,6 +81,7 @@
         }
 
         desktopTest.dependencies {
+            implementation project(':ui:ui-test')
             implementation(TRUTH)
             implementation(JUNIT)
             implementation(SKIKO_CURRENT_OS)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ZoomableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ZoomableTest.kt
index 829927f..3a7e4dc 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ZoomableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ZoomableTest.kt
@@ -27,8 +27,8 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.toSize
 import androidx.test.filters.SmallTest
-import androidx.ui.test.AnimationClockTestRule
 import androidx.ui.test.center
+import androidx.ui.test.createAnimationClockRule
 import androidx.ui.test.createComposeRule
 import androidx.ui.test.onNodeWithTag
 import androidx.ui.test.performGesture
@@ -53,7 +53,7 @@
     val composeTestRule = createComposeRule()
 
     @get:Rule
-    val clockRule = AnimationClockTestRule()
+    val clockRule = createAnimationClockRule()
 
     @Test
     fun zoomable_zoomIn() {
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt
index d801956..1fd124c 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt
@@ -210,9 +210,9 @@
             assertThat(textPosition.value!!.y).isEqualTo(
                 ((listItemHeight.toIntPx() - textSize.value!!.height) / 2f).roundToInt().toFloat()
             )
-            val dm = composeTestRule.displayMetrics
+            val ds = composeTestRule.displaySize
             assertThat(trailingPosition.value!!.x).isEqualTo(
-                dm.widthPixels - trailingSize.value!!.width -
+                ds.width - trailingSize.value!!.width -
                         expectedRightPadding.toIntPx().toFloat()
             )
             assertThat(trailingPosition.value!!.y).isEqualTo(
@@ -310,9 +310,9 @@
                 expectedTextBaseline.toIntPx().toFloat() +
                         expectedSecondaryTextBaselineOffset.toIntPx().toFloat()
             )
-            val dm = composeTestRule.displayMetrics
+            val ds = composeTestRule.displaySize
             assertThat(trailingPosition.value!!.x).isEqualTo(
-                dm.widthPixels - trailingSize.value!!.width -
+                ds.width - trailingSize.value!!.width -
                         expectedRightPadding.toIntPx().toFloat()
             )
             assertThat(trailingBaseline.value!!).isEqualTo(
@@ -453,9 +453,9 @@
             assertThat(iconPosition.value!!.y).isEqualTo(
                 expectedIconTopPadding.toIntPx().toFloat()
             )
-            val dm = composeTestRule.displayMetrics
+            val ds = composeTestRule.displaySize
             assertThat(trailingPosition.value!!.x).isEqualTo(
-                dm.widthPixels - trailingSize.value!!.width -
+                ds.width - trailingSize.value!!.width -
                         expectedRightPadding.toIntPx().toFloat()
             )
             assertThat(trailingPosition.value!!.y).isEqualTo(
@@ -529,9 +529,9 @@
             assertThat(iconPosition.value!!.y).isEqualTo(
                 expectedIconTopPadding.toIntPx().toFloat()
             )
-            val dm = composeTestRule.displayMetrics
+            val ds = composeTestRule.displaySize
             assertThat(trailingPosition.value!!.x).isEqualTo(
-                dm.widthPixels - trailingSize.value!!.width.toFloat() -
+                ds.width - trailingSize.value!!.width.toFloat() -
                         expectedRightPadding.toIntPx().toFloat()
             )
             assertThat(trailingPosition.value!!.y).isEqualTo(
@@ -643,9 +643,9 @@
             assertThat(iconPosition.value!!.y).isEqualTo(
                 expectedIconTopPadding.toIntPx().toFloat()
             )
-            val dm = composeTestRule.displayMetrics
+            val ds = composeTestRule.displaySize
             assertThat(trailingPosition.value!!.x).isEqualTo(
-                dm.widthPixels - trailingSize.value!!.width -
+                ds.width - trailingSize.value!!.width -
                         expectedRightPadding.toIntPx().toFloat()
             )
             assertThat(trailingBaseline.value!!).isEqualTo(
diff --git a/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/DesktopUiDispatcher.kt b/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/DesktopUiDispatcher.kt
index 04d74ea..e1922fd 100644
--- a/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/DesktopUiDispatcher.kt
+++ b/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/DesktopUiDispatcher.kt
@@ -43,7 +43,7 @@
 
     private fun scheduleIfNeeded() {
         synchronized(lock) {
-            if (!scheduled && (callbacks.isNotEmpty())) {
+            if (!scheduled && hasPendingChanges()) {
                 invokeLater { tick() }
                 scheduled = true
             }
@@ -70,13 +70,20 @@
         }
     }
 
-    private fun tick() {
-        scheduled = false
+    fun hasPendingChanges() = callbacks.isNotEmpty()
+
+    fun runAllCallbacks() {
         val now = System.nanoTime()
         runCallbacks(now, callbacks)
         scheduleIfNeeded()
     }
 
+    private fun tick() {
+        scheduled = false
+        runAllCallbacks()
+        scheduleIfNeeded()
+    }
+
     fun scheduleCallback(action: Action) {
         synchronized(lock) {
             callbacks.add(action)
@@ -125,4 +132,4 @@
             Dispatcher + Dispatcher.frameClock
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/test-utils/src/androidAndroidTest/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunnerTest.kt b/compose/test-utils/src/androidAndroidTest/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunnerTest.kt
index ce794b8..c0e7f9a 100644
--- a/compose/test-utils/src/androidAndroidTest/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunnerTest.kt
+++ b/compose/test-utils/src/androidAndroidTest/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunnerTest.kt
@@ -24,7 +24,7 @@
 import androidx.compose.runtime.onCommit
 import androidx.compose.runtime.remember
 import androidx.test.filters.SmallTest
-import androidx.ui.test.android.AndroidComposeTestRule
+import androidx.ui.test.AndroidComposeTestRule
 import androidx.ui.test.android.createAndroidComposeRule
 import org.junit.Rule
 import org.junit.Test
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/TestRuleExtensions.kt b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/TestRuleExtensions.kt
index 81d7ced..73aa388 100644
--- a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/TestRuleExtensions.kt
+++ b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/TestRuleExtensions.kt
@@ -17,7 +17,7 @@
 package androidx.compose.testutils
 
 import androidx.activity.ComponentActivity
-import androidx.ui.test.android.AndroidComposeTestRule
+import androidx.ui.test.AndroidComposeTestRule
 
 /**
  * Takes the given test case and prepares it for execution-controlled test via
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 5c28f5b..532afc4 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -1557,6 +1557,20 @@
     method public androidx.compose.ui.unit.Uptime? getUptime();
   }
 
+  public final class PointerInputEvent {
+    method public android.view.MotionEvent getMotionEvent();
+    method public java.util.List<androidx.compose.ui.input.pointer.PointerInputEventData> getPointers();
+    method public long getUptime();
+  }
+
+  public final class PointerInputEventData {
+    method public long component1();
+    method public androidx.compose.ui.input.pointer.PointerInputData component2();
+    method public androidx.compose.ui.input.pointer.PointerInputEventData copy-pdufZyI(long id, androidx.compose.ui.input.pointer.PointerInputData pointerInputData);
+    method public long getId();
+    method public androidx.compose.ui.input.pointer.PointerInputData getPointerInputData();
+  }
+
   public final class PointerInputEventProcessorKt {
   }
 
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 5c28f5b..532afc4 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -1557,6 +1557,20 @@
     method public androidx.compose.ui.unit.Uptime? getUptime();
   }
 
+  public final class PointerInputEvent {
+    method public android.view.MotionEvent getMotionEvent();
+    method public java.util.List<androidx.compose.ui.input.pointer.PointerInputEventData> getPointers();
+    method public long getUptime();
+  }
+
+  public final class PointerInputEventData {
+    method public long component1();
+    method public androidx.compose.ui.input.pointer.PointerInputData component2();
+    method public androidx.compose.ui.input.pointer.PointerInputEventData copy-pdufZyI(long id, androidx.compose.ui.input.pointer.PointerInputData pointerInputData);
+    method public long getId();
+    method public androidx.compose.ui.input.pointer.PointerInputData getPointerInputData();
+  }
+
   public final class PointerInputEventProcessorKt {
   }
 
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 55c3aae..375bb15 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -1604,6 +1604,20 @@
     method public androidx.compose.ui.unit.Uptime? getUptime();
   }
 
+  public final class PointerInputEvent {
+    method public android.view.MotionEvent getMotionEvent();
+    method public java.util.List<androidx.compose.ui.input.pointer.PointerInputEventData> getPointers();
+    method public long getUptime();
+  }
+
+  public final class PointerInputEventData {
+    method public long component1();
+    method public androidx.compose.ui.input.pointer.PointerInputData component2();
+    method public androidx.compose.ui.input.pointer.PointerInputEventData copy-pdufZyI(long id, androidx.compose.ui.input.pointer.PointerInputData pointerInputData);
+    method public long getId();
+    method public androidx.compose.ui.input.pointer.PointerInputData getPointerInputData();
+  }
+
   public final class PointerInputEventProcessorKt {
   }
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogUiTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogUiTest.kt
index 31ee176..cdfdcb3 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogUiTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogUiTest.kt
@@ -115,7 +115,7 @@
 
         // Click outside the dialog to dismiss it
         val outsideX = 0
-        val outsideY = composeTestRule.displayMetrics.heightPixels / 2
+        val outsideY = composeTestRule.displaySize.height / 2
         UiDevice.getInstance(getInstrumentation()).click(outsideX, outsideY)
 
         onNodeWithText(defaultText).assertDoesNotExist()
@@ -137,7 +137,7 @@
 
         // Click outside the dialog to try to dismiss it
         val outsideX = 0
-        val outsideY = composeTestRule.displayMetrics.heightPixels / 2
+        val outsideY = composeTestRule.displaySize.height / 2
         UiDevice.getInstance(getInstrumentation()).click(outsideX, outsideY)
 
         // The Dialog should still be visible
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEvent.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEvent.kt
index 89e7ab6..aeb3bb5 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEvent.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEvent.kt
@@ -19,7 +19,7 @@
 import android.view.MotionEvent
 import androidx.compose.ui.unit.Uptime
 
-internal actual class PointerInputEvent(
+actual class PointerInputEvent(
     actual val uptime: Uptime,
     actual val pointers: List<PointerInputEventData>,
     val motionEvent: MotionEvent
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
index 590b7f03..a2c7e8d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.input.pointer
 
+import androidx.compose.ui.node.InternalCoreApi
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.plus
 import androidx.compose.ui.util.annotation.VisibleForTesting
@@ -25,6 +26,7 @@
  * Organizes pointers and the [PointerInputFilter]s that they hit into a hierarchy such that
  * [PointerInputChange]s can be dispatched to the [PointerInputFilter]s in a hierarchical fashion.
  */
+@OptIn(InternalCoreApi::class)
 internal class HitPathTracker {
 
     @VisibleForTesting
@@ -263,6 +265,7 @@
  * pointer or [PointerInputFilter] information.
  */
 @VisibleForTesting
+@OptIn(InternalCoreApi::class)
 internal open class NodeParent {
     val children: MutableSet<Node> = mutableSetOf()
 
@@ -384,6 +387,7 @@
  * hit it (tracked as [PointerId]s).
  */
 @VisibleForTesting
+@OptIn(InternalCoreApi::class)
 internal class Node(val pointerInputFilter: PointerInputFilter) : NodeParent() {
 
     val pointerIds: MutableSet<PointerId> = mutableSetOf()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt
index cf227e8..05f0cc2 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.input.pointer
 
+import androidx.compose.ui.node.InternalCoreApi
 import androidx.compose.ui.unit.Uptime
 
 /**
@@ -24,7 +25,8 @@
  *
  * All pointer locations are relative to the device screen.
  */
-internal expect class PointerInputEvent {
+@InternalCoreApi
+expect class PointerInputEvent {
     val uptime: Uptime
     val pointers: List<PointerInputEventData>
 }
@@ -34,7 +36,7 @@
  *
  * All pointer locations are relative to the device screen.
  */
-internal data class PointerInputEventData(
+data class PointerInputEventData(
     val id: PointerId,
     val pointerInputData: PointerInputData
 )
@@ -46,6 +48,7 @@
  * it is efficient to split the changes between those that are relevant to the sub tree and those
  * that are not.
  */
+@OptIn(InternalCoreApi::class)
 internal expect class InternalPointerEvent(
     changes: MutableMap<PointerId, PointerInputChange>,
     pointerInputEvent: PointerInputEvent
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 c8bb788..056468f 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
@@ -26,6 +26,7 @@
 import androidx.compose.ui.input.pointer.PointerEventPass.Initial
 import androidx.compose.ui.input.pointer.PointerEventPass.Main
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.node.InternalCoreApi
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Uptime
@@ -126,7 +127,7 @@
 /**
  * Describes a pointer input change event that has occurred at a particular point in time.
  */
-expect class PointerEvent internal constructor(
+expect class PointerEvent @OptIn(InternalCoreApi::class) internal constructor(
     changes: List<PointerInputChange>,
     internalPointerEvent: InternalPointerEvent?
 ) {
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 dc9da8b..954a2ae 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,13 +17,14 @@
 package androidx.compose.ui.input.pointer
 
 import androidx.compose.ui.node.ExperimentalLayoutNodeApi
+import androidx.compose.ui.node.InternalCoreApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.util.fastForEach
 
 /**
  * The core element that receives [PointerInputEvent]s and process them in Compose UI.
  */
-@OptIn(ExperimentalLayoutNodeApi::class)
+@OptIn(ExperimentalLayoutNodeApi::class, InternalCoreApi::class)
 internal class PointerInputEventProcessor(val root: LayoutNode) {
 
     private val hitPathTracker = HitPathTracker()
@@ -101,6 +102,7 @@
 /**
  * Produces [InternalPointerEvent]s by tracking changes between [PointerInputEvent]s
  */
+@OptIn(InternalCoreApi::class)
 private class PointerInputChangeEventProducer {
     private val previousPointerInputData: MutableMap<PointerId, PointerInputData> = mutableMapOf()
 
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/TestTag.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/TestTag.kt
similarity index 100%
rename from compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/TestTag.kt
rename to compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/TestTag.kt
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEvent.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEvent.kt
index d078dc4..31733df 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEvent.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEvent.kt
@@ -18,7 +18,7 @@
 
 import androidx.compose.ui.unit.Uptime
 
-internal actual class PointerInputEvent(
+actual class PointerInputEvent(
     actual val uptime: Uptime,
     actual val pointers: List<PointerInputEventData>
 )
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
index 4974193..bcfd308 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
@@ -218,7 +218,7 @@
         root.draw(DesktopCanvas(canvas))
     }
 
-    internal fun processPointerInput(event: PointerInputEvent) {
+    fun processPointerInput(event: PointerInputEvent) {
         measureAndLayout()
         pointerInputEventProcessor.process(event)
     }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt
index c96aa083..6c0e93a 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt
@@ -38,7 +38,7 @@
     component: Component,
     private val redraw: () -> Unit
 ) {
-    private val list = LinkedHashSet<DesktopOwner>()
+    val list = LinkedHashSet<DesktopOwner>()
 
     // Optimization: we don't need more than one redrawing per tick
     private var redrawingScheduled = false
@@ -135,4 +135,4 @@
             redrawingScheduled = true
         }
     }
-}
\ No newline at end of file
+}
diff --git a/ui/ui-test-font/src/main/AndroidManifest.xml b/ui/ui-test-font/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..2751a04
--- /dev/null
+++ b/ui/ui-test-font/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.compose.ui.text.font.test">
+</manifest>
diff --git a/ui/ui-test/api/current.txt b/ui/ui-test/api/current.txt
index 1ddc9ad..9302bcf 100644
--- a/ui/ui-test/api/current.txt
+++ b/ui/ui-test/api/current.txt
@@ -9,10 +9,11 @@
     method public static void performSemanticsAction(androidx.ui.test.SemanticsNodeInteraction, androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> key);
   }
 
-  public final class AndroidAssertionsKt {
+  public final class AndroidAnimationClockTestRuleKt {
+    method public static androidx.ui.test.AnimationClockTestRule createAnimationClockRule();
   }
 
-  public final class AndroidBaseInputDispatcherKt {
+  public final class AndroidAssertionsKt {
   }
 
   public final class AndroidBitmapHelpersKt {
@@ -26,6 +27,31 @@
     method public static boolean contains-ej0GBII(androidx.compose.ui.graphics.Path, long offset);
   }
 
+  public final class AndroidComposeTestRule<T extends androidx.activity.ComponentActivity> implements androidx.ui.test.ComposeTestRule {
+    ctor public AndroidComposeTestRule(androidx.test.ext.junit.rules.ActivityScenarioRule<T> activityRule, boolean disableTransitions, boolean disableBlinkingCursor);
+    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
+    method public androidx.test.ext.junit.rules.ActivityScenarioRule<T> getActivityRule();
+    method public androidx.ui.test.AnimationClockTestRule getClockTestRule();
+    method public androidx.compose.ui.unit.Density getDensity();
+    method public long getDisplaySize();
+    method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+    property public androidx.ui.test.AnimationClockTestRule clockTestRule;
+    property public androidx.compose.ui.unit.Density density;
+    property public long displaySize;
+  }
+
+  public final class AndroidComposeTestRule.AndroidComposeStatement extends org.junit.runners.model.Statement {
+    ctor public AndroidComposeTestRule.AndroidComposeStatement(org.junit.runners.model.Statement base);
+    method public void evaluate();
+  }
+
+  public final class AndroidComposeTestRuleKt {
+    method public static androidx.ui.test.ComposeTestRule createComposeRule(boolean disableTransitions, boolean disableBlinkingCursor);
+  }
+
+  public final class AndroidInputDispatcherKt {
+  }
+
   public final class AndroidOutputKt {
   }
 
@@ -35,16 +61,14 @@
   public final class AndroidSynchronizationKt {
   }
 
-  public final class AnimationClockTestRule implements org.junit.rules.TestRule {
-    ctor public AnimationClockTestRule();
-    method public void advanceClock(long milliseconds);
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
+  public interface AnimationClockTestRule extends org.junit.rules.TestRule {
+    method public default void advanceClock(long milliseconds);
     method public androidx.ui.test.TestAnimationClock getClock();
     method public boolean isPaused();
     method public void pauseClock();
-    method public void resumeClock();
-    property public final androidx.ui.test.TestAnimationClock clock;
-    property public final boolean isPaused;
+    method public default void resumeClock();
+    property public abstract androidx.ui.test.TestAnimationClock clock;
+    property public abstract boolean isPaused;
   }
 
   public final class AnimationClocksKt {
@@ -94,15 +118,11 @@
   public interface ComposeTestRule extends org.junit.rules.TestRule {
     method public androidx.ui.test.AnimationClockTestRule getClockTestRule();
     method public androidx.compose.ui.unit.Density getDensity();
-    method public android.util.DisplayMetrics getDisplayMetrics();
+    method public long getDisplaySize();
     method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
     property public abstract androidx.ui.test.AnimationClockTestRule clockTestRule;
     property public abstract androidx.compose.ui.unit.Density density;
-    property public abstract android.util.DisplayMetrics displayMetrics;
-  }
-
-  public final class ComposeTestRuleKt {
-    method public static androidx.ui.test.ComposeTestRule createComposeRule(boolean disableTransitions = false, boolean disableBlinkingCursor = true);
+    property public abstract long displaySize;
   }
 
   public final class CoroutineBuildersKt {
@@ -337,27 +357,9 @@
 
 package androidx.ui.test.android {
 
-  public final class AndroidComposeTestRule<T extends androidx.activity.ComponentActivity> implements androidx.ui.test.ComposeTestRule {
-    ctor public AndroidComposeTestRule(androidx.test.ext.junit.rules.ActivityScenarioRule<T> activityRule, boolean disableTransitions, boolean disableBlinkingCursor);
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
-    method public androidx.test.ext.junit.rules.ActivityScenarioRule<T> getActivityRule();
-    method public androidx.ui.test.AnimationClockTestRule getClockTestRule();
-    method public androidx.compose.ui.unit.Density getDensity();
-    method public android.util.DisplayMetrics getDisplayMetrics();
-    method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
-    property public androidx.ui.test.AnimationClockTestRule clockTestRule;
-    property public androidx.compose.ui.unit.Density density;
-    property public android.util.DisplayMetrics displayMetrics;
-  }
-
-  public final class AndroidComposeTestRule.AndroidComposeStatement extends org.junit.runners.model.Statement {
-    ctor public AndroidComposeTestRule.AndroidComposeStatement(org.junit.runners.model.Statement base);
-    method public void evaluate();
-  }
-
   public final class AndroidComposeTestRuleKt {
-    method public static <T extends androidx.activity.ComponentActivity> androidx.ui.test.android.AndroidComposeTestRule<T> createAndroidComposeRule(Class<T> activityClass, boolean disableTransitions = false, boolean disableBlinkingCursor = true);
-    method public static inline <reified T extends androidx.activity.ComponentActivity> androidx.ui.test.android.AndroidComposeTestRule<T>! createAndroidComposeRule(boolean disableTransitions = false, boolean disableBlinkingCursor = true);
+    method public static <T extends androidx.activity.ComponentActivity> androidx.ui.test.AndroidComposeTestRule<T> createAndroidComposeRule(Class<T> activityClass, boolean disableTransitions = false, boolean disableBlinkingCursor = true);
+    method public static inline <reified T extends androidx.activity.ComponentActivity> androidx.ui.test.AndroidComposeTestRule<T>! createAndroidComposeRule(boolean disableTransitions = false, boolean disableBlinkingCursor = true);
   }
 
   public final class ComposeIdlingResourceKt {
diff --git a/ui/ui-test/api/public_plus_experimental_current.txt b/ui/ui-test/api/public_plus_experimental_current.txt
index 1ddc9ad..9302bcf 100644
--- a/ui/ui-test/api/public_plus_experimental_current.txt
+++ b/ui/ui-test/api/public_plus_experimental_current.txt
@@ -9,10 +9,11 @@
     method public static void performSemanticsAction(androidx.ui.test.SemanticsNodeInteraction, androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> key);
   }
 
-  public final class AndroidAssertionsKt {
+  public final class AndroidAnimationClockTestRuleKt {
+    method public static androidx.ui.test.AnimationClockTestRule createAnimationClockRule();
   }
 
-  public final class AndroidBaseInputDispatcherKt {
+  public final class AndroidAssertionsKt {
   }
 
   public final class AndroidBitmapHelpersKt {
@@ -26,6 +27,31 @@
     method public static boolean contains-ej0GBII(androidx.compose.ui.graphics.Path, long offset);
   }
 
+  public final class AndroidComposeTestRule<T extends androidx.activity.ComponentActivity> implements androidx.ui.test.ComposeTestRule {
+    ctor public AndroidComposeTestRule(androidx.test.ext.junit.rules.ActivityScenarioRule<T> activityRule, boolean disableTransitions, boolean disableBlinkingCursor);
+    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
+    method public androidx.test.ext.junit.rules.ActivityScenarioRule<T> getActivityRule();
+    method public androidx.ui.test.AnimationClockTestRule getClockTestRule();
+    method public androidx.compose.ui.unit.Density getDensity();
+    method public long getDisplaySize();
+    method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+    property public androidx.ui.test.AnimationClockTestRule clockTestRule;
+    property public androidx.compose.ui.unit.Density density;
+    property public long displaySize;
+  }
+
+  public final class AndroidComposeTestRule.AndroidComposeStatement extends org.junit.runners.model.Statement {
+    ctor public AndroidComposeTestRule.AndroidComposeStatement(org.junit.runners.model.Statement base);
+    method public void evaluate();
+  }
+
+  public final class AndroidComposeTestRuleKt {
+    method public static androidx.ui.test.ComposeTestRule createComposeRule(boolean disableTransitions, boolean disableBlinkingCursor);
+  }
+
+  public final class AndroidInputDispatcherKt {
+  }
+
   public final class AndroidOutputKt {
   }
 
@@ -35,16 +61,14 @@
   public final class AndroidSynchronizationKt {
   }
 
-  public final class AnimationClockTestRule implements org.junit.rules.TestRule {
-    ctor public AnimationClockTestRule();
-    method public void advanceClock(long milliseconds);
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
+  public interface AnimationClockTestRule extends org.junit.rules.TestRule {
+    method public default void advanceClock(long milliseconds);
     method public androidx.ui.test.TestAnimationClock getClock();
     method public boolean isPaused();
     method public void pauseClock();
-    method public void resumeClock();
-    property public final androidx.ui.test.TestAnimationClock clock;
-    property public final boolean isPaused;
+    method public default void resumeClock();
+    property public abstract androidx.ui.test.TestAnimationClock clock;
+    property public abstract boolean isPaused;
   }
 
   public final class AnimationClocksKt {
@@ -94,15 +118,11 @@
   public interface ComposeTestRule extends org.junit.rules.TestRule {
     method public androidx.ui.test.AnimationClockTestRule getClockTestRule();
     method public androidx.compose.ui.unit.Density getDensity();
-    method public android.util.DisplayMetrics getDisplayMetrics();
+    method public long getDisplaySize();
     method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
     property public abstract androidx.ui.test.AnimationClockTestRule clockTestRule;
     property public abstract androidx.compose.ui.unit.Density density;
-    property public abstract android.util.DisplayMetrics displayMetrics;
-  }
-
-  public final class ComposeTestRuleKt {
-    method public static androidx.ui.test.ComposeTestRule createComposeRule(boolean disableTransitions = false, boolean disableBlinkingCursor = true);
+    property public abstract long displaySize;
   }
 
   public final class CoroutineBuildersKt {
@@ -337,27 +357,9 @@
 
 package androidx.ui.test.android {
 
-  public final class AndroidComposeTestRule<T extends androidx.activity.ComponentActivity> implements androidx.ui.test.ComposeTestRule {
-    ctor public AndroidComposeTestRule(androidx.test.ext.junit.rules.ActivityScenarioRule<T> activityRule, boolean disableTransitions, boolean disableBlinkingCursor);
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
-    method public androidx.test.ext.junit.rules.ActivityScenarioRule<T> getActivityRule();
-    method public androidx.ui.test.AnimationClockTestRule getClockTestRule();
-    method public androidx.compose.ui.unit.Density getDensity();
-    method public android.util.DisplayMetrics getDisplayMetrics();
-    method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
-    property public androidx.ui.test.AnimationClockTestRule clockTestRule;
-    property public androidx.compose.ui.unit.Density density;
-    property public android.util.DisplayMetrics displayMetrics;
-  }
-
-  public final class AndroidComposeTestRule.AndroidComposeStatement extends org.junit.runners.model.Statement {
-    ctor public AndroidComposeTestRule.AndroidComposeStatement(org.junit.runners.model.Statement base);
-    method public void evaluate();
-  }
-
   public final class AndroidComposeTestRuleKt {
-    method public static <T extends androidx.activity.ComponentActivity> androidx.ui.test.android.AndroidComposeTestRule<T> createAndroidComposeRule(Class<T> activityClass, boolean disableTransitions = false, boolean disableBlinkingCursor = true);
-    method public static inline <reified T extends androidx.activity.ComponentActivity> androidx.ui.test.android.AndroidComposeTestRule<T>! createAndroidComposeRule(boolean disableTransitions = false, boolean disableBlinkingCursor = true);
+    method public static <T extends androidx.activity.ComponentActivity> androidx.ui.test.AndroidComposeTestRule<T> createAndroidComposeRule(Class<T> activityClass, boolean disableTransitions = false, boolean disableBlinkingCursor = true);
+    method public static inline <reified T extends androidx.activity.ComponentActivity> androidx.ui.test.AndroidComposeTestRule<T>! createAndroidComposeRule(boolean disableTransitions = false, boolean disableBlinkingCursor = true);
   }
 
   public final class ComposeIdlingResourceKt {
diff --git a/ui/ui-test/api/restricted_current.txt b/ui/ui-test/api/restricted_current.txt
index 1ddc9ad..9302bcf 100644
--- a/ui/ui-test/api/restricted_current.txt
+++ b/ui/ui-test/api/restricted_current.txt
@@ -9,10 +9,11 @@
     method public static void performSemanticsAction(androidx.ui.test.SemanticsNodeInteraction, androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> key);
   }
 
-  public final class AndroidAssertionsKt {
+  public final class AndroidAnimationClockTestRuleKt {
+    method public static androidx.ui.test.AnimationClockTestRule createAnimationClockRule();
   }
 
-  public final class AndroidBaseInputDispatcherKt {
+  public final class AndroidAssertionsKt {
   }
 
   public final class AndroidBitmapHelpersKt {
@@ -26,6 +27,31 @@
     method public static boolean contains-ej0GBII(androidx.compose.ui.graphics.Path, long offset);
   }
 
+  public final class AndroidComposeTestRule<T extends androidx.activity.ComponentActivity> implements androidx.ui.test.ComposeTestRule {
+    ctor public AndroidComposeTestRule(androidx.test.ext.junit.rules.ActivityScenarioRule<T> activityRule, boolean disableTransitions, boolean disableBlinkingCursor);
+    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
+    method public androidx.test.ext.junit.rules.ActivityScenarioRule<T> getActivityRule();
+    method public androidx.ui.test.AnimationClockTestRule getClockTestRule();
+    method public androidx.compose.ui.unit.Density getDensity();
+    method public long getDisplaySize();
+    method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+    property public androidx.ui.test.AnimationClockTestRule clockTestRule;
+    property public androidx.compose.ui.unit.Density density;
+    property public long displaySize;
+  }
+
+  public final class AndroidComposeTestRule.AndroidComposeStatement extends org.junit.runners.model.Statement {
+    ctor public AndroidComposeTestRule.AndroidComposeStatement(org.junit.runners.model.Statement base);
+    method public void evaluate();
+  }
+
+  public final class AndroidComposeTestRuleKt {
+    method public static androidx.ui.test.ComposeTestRule createComposeRule(boolean disableTransitions, boolean disableBlinkingCursor);
+  }
+
+  public final class AndroidInputDispatcherKt {
+  }
+
   public final class AndroidOutputKt {
   }
 
@@ -35,16 +61,14 @@
   public final class AndroidSynchronizationKt {
   }
 
-  public final class AnimationClockTestRule implements org.junit.rules.TestRule {
-    ctor public AnimationClockTestRule();
-    method public void advanceClock(long milliseconds);
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
+  public interface AnimationClockTestRule extends org.junit.rules.TestRule {
+    method public default void advanceClock(long milliseconds);
     method public androidx.ui.test.TestAnimationClock getClock();
     method public boolean isPaused();
     method public void pauseClock();
-    method public void resumeClock();
-    property public final androidx.ui.test.TestAnimationClock clock;
-    property public final boolean isPaused;
+    method public default void resumeClock();
+    property public abstract androidx.ui.test.TestAnimationClock clock;
+    property public abstract boolean isPaused;
   }
 
   public final class AnimationClocksKt {
@@ -94,15 +118,11 @@
   public interface ComposeTestRule extends org.junit.rules.TestRule {
     method public androidx.ui.test.AnimationClockTestRule getClockTestRule();
     method public androidx.compose.ui.unit.Density getDensity();
-    method public android.util.DisplayMetrics getDisplayMetrics();
+    method public long getDisplaySize();
     method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
     property public abstract androidx.ui.test.AnimationClockTestRule clockTestRule;
     property public abstract androidx.compose.ui.unit.Density density;
-    property public abstract android.util.DisplayMetrics displayMetrics;
-  }
-
-  public final class ComposeTestRuleKt {
-    method public static androidx.ui.test.ComposeTestRule createComposeRule(boolean disableTransitions = false, boolean disableBlinkingCursor = true);
+    property public abstract long displaySize;
   }
 
   public final class CoroutineBuildersKt {
@@ -337,27 +357,9 @@
 
 package androidx.ui.test.android {
 
-  public final class AndroidComposeTestRule<T extends androidx.activity.ComponentActivity> implements androidx.ui.test.ComposeTestRule {
-    ctor public AndroidComposeTestRule(androidx.test.ext.junit.rules.ActivityScenarioRule<T> activityRule, boolean disableTransitions, boolean disableBlinkingCursor);
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
-    method public androidx.test.ext.junit.rules.ActivityScenarioRule<T> getActivityRule();
-    method public androidx.ui.test.AnimationClockTestRule getClockTestRule();
-    method public androidx.compose.ui.unit.Density getDensity();
-    method public android.util.DisplayMetrics getDisplayMetrics();
-    method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
-    property public androidx.ui.test.AnimationClockTestRule clockTestRule;
-    property public androidx.compose.ui.unit.Density density;
-    property public android.util.DisplayMetrics displayMetrics;
-  }
-
-  public final class AndroidComposeTestRule.AndroidComposeStatement extends org.junit.runners.model.Statement {
-    ctor public AndroidComposeTestRule.AndroidComposeStatement(org.junit.runners.model.Statement base);
-    method public void evaluate();
-  }
-
   public final class AndroidComposeTestRuleKt {
-    method public static <T extends androidx.activity.ComponentActivity> androidx.ui.test.android.AndroidComposeTestRule<T> createAndroidComposeRule(Class<T> activityClass, boolean disableTransitions = false, boolean disableBlinkingCursor = true);
-    method public static inline <reified T extends androidx.activity.ComponentActivity> androidx.ui.test.android.AndroidComposeTestRule<T>! createAndroidComposeRule(boolean disableTransitions = false, boolean disableBlinkingCursor = true);
+    method public static <T extends androidx.activity.ComponentActivity> androidx.ui.test.AndroidComposeTestRule<T> createAndroidComposeRule(Class<T> activityClass, boolean disableTransitions = false, boolean disableBlinkingCursor = true);
+    method public static inline <reified T extends androidx.activity.ComponentActivity> androidx.ui.test.AndroidComposeTestRule<T>! createAndroidComposeRule(boolean disableTransitions = false, boolean disableBlinkingCursor = true);
   }
 
   public final class ComposeIdlingResourceKt {
diff --git a/ui/ui-test/build.gradle b/ui/ui-test/build.gradle
index ab3e2fc..da620d8 100644
--- a/ui/ui-test/build.gradle
+++ b/ui/ui-test/build.gradle
@@ -34,6 +34,8 @@
 
 kotlin {
     android()
+    jvm("desktop")
+
     sourceSets {
         commonMain.dependencies {
             api project(":compose:animation:animation-core")
@@ -53,6 +55,11 @@
             implementation project(":compose:runtime:runtime-saved-instance-state")
         }
 
+        jvmMain.dependencies {
+            implementation "androidx.collection:collection:1.1.0"
+            implementation(JUNIT)
+        }
+
         androidMain.dependencies {
             api(JUNIT)
             api(ANDROIDX_TEST_EXT_JUNIT)
@@ -74,6 +81,14 @@
             implementation project(':compose:material:material')
             implementation project(":compose:ui:ui")
         }
+
+        desktopMain.dependencies {
+            implementation(JUNIT)
+            implementation(TRUTH)
+            implementation(SKIKO)
+        }
+
+        desktopMain.dependsOn jvmMain
     }
 }
 
diff --git a/ui/ui-test/src/androidAndroidTest/AndroidManifest.xml b/ui/ui-test/src/androidAndroidTest/AndroidManifest.xml
index 6ec2ae6..cad929e 100644
--- a/ui/ui-test/src/androidAndroidTest/AndroidManifest.xml
+++ b/ui/ui-test/src/androidAndroidTest/AndroidManifest.xml
@@ -15,7 +15,7 @@
   ~ limitations under the License.
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="androidx.ui.test.test">
+    package="androidx.ui.test">
     <application>
         <activity android:name="androidx.activity.ComponentActivity"
             android:theme="@style/TestTheme"/>
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/gesturescope/SendDoubleClickTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/gesturescope/SendDoubleClickTest.kt
index 1a6ac80..f87ff98 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/gesturescope/SendDoubleClickTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/gesturescope/SendDoubleClickTest.kt
@@ -21,7 +21,7 @@
 import androidx.compose.ui.gesture.doubleTapGestureFilter
 import androidx.compose.ui.geometry.Offset
 import androidx.ui.test.InputDispatcher.Companion.eventPeriod
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
+import androidx.ui.test.android.AndroidInputDispatcher.InputDispatcherTestRule
 import androidx.ui.test.createComposeRule
 import androidx.ui.test.performGesture
 import androidx.ui.test.onNodeWithTag
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/gesturescope/SendSwipeVelocityTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/gesturescope/SendSwipeVelocityTest.kt
index 1109fe4..f7d13e9 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/gesturescope/SendSwipeVelocityTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/gesturescope/SendSwipeVelocityTest.kt
@@ -23,7 +23,7 @@
 import androidx.compose.foundation.layout.Stack
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.wrapContentSize
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
+import androidx.ui.test.android.AndroidInputDispatcher.InputDispatcherTestRule
 import androidx.ui.test.createComposeRule
 import androidx.ui.test.performGesture
 import androidx.ui.test.onNodeWithTag
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/InputDispatcherTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/InputDispatcherTest.kt
index cbad2f5..09d5d38 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/InputDispatcherTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/inputdispatcher/InputDispatcherTest.kt
@@ -17,10 +17,9 @@
 package androidx.ui.test.inputdispatcher
 
 import androidx.compose.ui.geometry.Offset
-import androidx.ui.test.AndroidBaseInputDispatcher
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
-import androidx.ui.test.InputDispatcher
 import androidx.ui.test.android.AndroidInputDispatcher
+import androidx.ui.test.android.AndroidInputDispatcher.InputDispatcherTestRule
+import androidx.ui.test.InputDispatcher
 import androidx.ui.test.util.MotionEventRecorder
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
@@ -75,5 +74,5 @@
 }
 
 internal fun InputDispatcher.verifyNoGestureInProgress() {
-    assertThat((this as AndroidBaseInputDispatcher).isGestureInProgress).isFalse()
+    assertThat((this as AndroidInputDispatcher).isGestureInProgress).isFalse()
 }
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendCancelTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendCancelTest.kt
index 5a466d3..5fab8f1 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendCancelTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendCancelTest.kt
@@ -18,7 +18,7 @@
 
 import androidx.test.filters.MediumTest
 import androidx.compose.ui.geometry.Offset
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
+import androidx.ui.test.android.AndroidInputDispatcher.InputDispatcherTestRule
 import androidx.ui.test.createComposeRule
 import androidx.ui.test.inputdispatcher.verifyNoGestureInProgress
 import androidx.ui.test.partialgesturescope.Common.partialGesture
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendDownTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendDownTest.kt
index ad9375b..7a18c79 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendDownTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendDownTest.kt
@@ -19,7 +19,7 @@
 import android.os.SystemClock.sleep
 import androidx.test.filters.MediumTest
 import androidx.compose.ui.geometry.Offset
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
+import androidx.ui.test.android.AndroidInputDispatcher.InputDispatcherTestRule
 import androidx.ui.test.createComposeRule
 import androidx.ui.test.partialgesturescope.Common.partialGesture
 import androidx.ui.test.runOnIdle
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendMoveByTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendMoveByTest.kt
index 451dca3..a585a81 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendMoveByTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendMoveByTest.kt
@@ -19,7 +19,7 @@
 import android.os.SystemClock.sleep
 import androidx.test.filters.MediumTest
 import androidx.compose.ui.geometry.Offset
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
+import androidx.ui.test.android.AndroidInputDispatcher.InputDispatcherTestRule
 import androidx.ui.test.createComposeRule
 import androidx.ui.test.movePointerBy
 import androidx.ui.test.partialgesturescope.Common.partialGesture
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendMoveTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendMoveTest.kt
index 9dc1097..cdf7083 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendMoveTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendMoveTest.kt
@@ -18,7 +18,7 @@
 
 import androidx.test.filters.MediumTest
 import androidx.compose.ui.geometry.Offset
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
+import androidx.ui.test.android.AndroidInputDispatcher.InputDispatcherTestRule
 import androidx.ui.test.createComposeRule
 import androidx.ui.test.partialgesturescope.Common.partialGesture
 import androidx.ui.test.cancel
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendMoveToTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendMoveToTest.kt
index c47d796..6a60568 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendMoveToTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendMoveToTest.kt
@@ -19,7 +19,7 @@
 import android.os.SystemClock.sleep
 import androidx.test.filters.MediumTest
 import androidx.compose.ui.geometry.Offset
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
+import androidx.ui.test.android.AndroidInputDispatcher.InputDispatcherTestRule
 import androidx.ui.test.createComposeRule
 import androidx.ui.test.movePointerTo
 import androidx.ui.test.partialgesturescope.Common.partialGesture
diff --git a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendUpTest.kt b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendUpTest.kt
index 91c1ba7..02b7cbb 100644
--- a/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendUpTest.kt
+++ b/ui/ui-test/src/androidAndroidTest/kotlin/androidx/ui/test/partialgesturescope/SendUpTest.kt
@@ -19,7 +19,7 @@
 import android.os.SystemClock.sleep
 import androidx.test.filters.MediumTest
 import androidx.compose.ui.geometry.Offset
-import androidx.ui.test.AndroidBaseInputDispatcher.InputDispatcherTestRule
+import androidx.ui.test.android.AndroidInputDispatcher.InputDispatcherTestRule
 import androidx.ui.test.createComposeRule
 import androidx.ui.test.inputdispatcher.verifyNoGestureInProgress
 import androidx.ui.test.partialgesturescope.Common.partialGesture
diff --git a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AnimationClockTestRule.kt b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidAnimationClockTestRule.kt
similarity index 88%
rename from ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AnimationClockTestRule.kt
rename to ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidAnimationClockTestRule.kt
index d5ac7ab..9ab8c78 100644
--- a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AnimationClockTestRule.kt
+++ b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidAnimationClockTestRule.kt
@@ -41,7 +41,7 @@
  * animations. Otherwise, built in steps that make sure the UI is stable when performing actions
  * or assertions will fail to work.
  */
-class AnimationClockTestRule : TestRule {
+internal class AndroidAnimationClockTestRule : AnimationClockTestRule {
 
     /** Backing property for [clock] */
     private val _clock = AndroidTestAnimationClock()
@@ -52,27 +52,27 @@
      * make sure to let it implement [TestAnimationClock] and register it with
      * [registerTestClock].
      */
-    val clock: TestAnimationClock get() = _clock
+    override val clock: TestAnimationClock get() = _clock
 
     /**
      * Convenience property for calling [`clock.isPaused`][TestAnimationClock.isPaused]
      */
-    val isPaused: Boolean get() = clock.isPaused
+    override val isPaused: Boolean get() = clock.isPaused
 
     /**
      * Convenience method for calling [`clock.pauseClock()`][TestAnimationClock.pauseClock]
      */
-    fun pauseClock() = clock.pauseClock()
+    override fun pauseClock() = clock.pauseClock()
 
     /**
      * Convenience method for calling [`clock.resumeClock()`][TestAnimationClock.resumeClock]
      */
-    fun resumeClock() = clock.resumeClock()
+    override fun resumeClock() = clock.resumeClock()
 
     /**
      * Convenience method for calling [`clock.advanceClock()`][TestAnimationClock.advanceClock]
      */
-    fun advanceClock(milliseconds: Long) = clock.advanceClock(milliseconds)
+    override fun advanceClock(milliseconds: Long) = clock.advanceClock(milliseconds)
 
     override fun apply(base: Statement, description: Description?): Statement {
         return AnimationClockStatement(base)
@@ -97,3 +97,6 @@
         }
     }
 }
+
+actual fun createAnimationClockRule(): AnimationClockTestRule =
+    AndroidAnimationClockTestRule()
\ No newline at end of file
diff --git a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidBaseInputDispatcher.kt b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidBaseInputDispatcher.kt
index dea6057..e69de29 100644
--- a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidBaseInputDispatcher.kt
+++ b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidBaseInputDispatcher.kt
@@ -1,601 +0,0 @@
-/*
- * Copyright 2019 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.ui.test
-
-import androidx.collection.SparseArrayCompat
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.lerp
-import androidx.compose.ui.node.Owner
-import androidx.compose.ui.platform.AndroidOwner
-import androidx.compose.ui.unit.Duration
-import androidx.compose.ui.unit.inMilliseconds
-import androidx.compose.ui.unit.milliseconds
-import androidx.ui.test.android.AndroidInputDispatcher
-import androidx.ui.test.android.AndroidOwnerRegistry
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
-import java.util.WeakHashMap
-import kotlin.math.max
-import kotlin.math.roundToInt
-
-/**
- * Interface for dispatching full and partial gestures.
- *
- * Full gestures:
- * * [enqueueClick]
- * * [enqueueSwipe]
- * * [enqueueSwipes]
- *
- * Partial gestures:
- * * [enqueueDown]
- * * [enqueueMove]
- * * [enqueueUp]
- * * [enqueueCancel]
- * * [movePointer]
- * * [getCurrentPosition]
- *
- * Chaining methods:
- * * [enqueueDelay]
- */
-internal abstract class AndroidBaseInputDispatcher : InputDispatcher() {
-    companion object : AndroidOwnerRegistry.OnRegistrationChangedListener {
-        /**
-         * Indicates that [nextDownTime] is not set
-         */
-        private const val DownTimeNotSet = -1L
-
-        /**
-         * Stores the [InputDispatcherState] of each [Owner]. The state will be restored in an
-         * [InputDispatcher] when it is created for an owner that has a state stored.
-         */
-        internal val states = WeakHashMap<Owner, InputDispatcherState>()
-
-        init {
-            AndroidOwnerRegistry.addOnRegistrationChangedListener(this)
-        }
-
-        override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
-            if (!registered) {
-                states.remove(owner)
-            }
-        }
-    }
-
-    override fun saveState(owner: Owner?) {
-        if (owner != null && AndroidOwnerRegistry.getUnfilteredOwners().contains(owner)) {
-            states[owner] = InputDispatcherState(nextDownTime, gestureLateness, partialGesture)
-        }
-    }
-
-    internal var nextDownTime = DownTimeNotSet
-
-    /**
-     * The time difference between enqueuing the first event of the gesture and dispatching it.
-     *
-     * When the first event of a gesture is enqueued, its eventTime is fixed to the current time.
-     * However, there is inevitably some time between enqueuing and dispatching of that event.
-     * This means that event is going to be "late" by [gestureLateness] milliseconds when it is
-     * dispatched. Because the dispatcher wants to align events with the current time, it will
-     * dispatch all events that are late immediately and without delay, until it has reached an
-     * event whose eventTime is in the future (i.e. an event that is "early").
-     *
-     * The [gestureLateness] will be used to offset all events, effectively aligning the first
-     * event with the dispatch time.
-     */
-    internal var gestureLateness: Long? = null
-
-    internal var partialGesture: PartialGesture? = null
-
-    /**
-     * Indicates if a gesture is in progress or not. A gesture is in progress if at least one
-     * finger is (still) touching the screen.
-     */
-    val isGestureInProgress: Boolean
-        get() = partialGesture != null
-
-    abstract override val now: Long
-
-    /**
-     * Generates the downTime of the next gesture with the given [duration]. The gesture's
-     * [duration] is necessary to facilitate chaining of gestures: if another gesture is made
-     * after the next one, it will start exactly [duration] after the start of the next gesture.
-     * Always use this method to determine the downTime of the [down event][enqueueDown] of a
-     * gesture.
-     *
-     * If the duration is unknown when calling this method, use a duration of zero and update
-     * with [moveNextDownTime] when the duration is known, or use [moveNextDownTime]
-     * incrementally if the gesture unfolds gradually.
-     */
-    private fun generateDownTime(duration: Duration): Long {
-        val downTime = if (nextDownTime == DownTimeNotSet) {
-            now
-        } else {
-            nextDownTime
-        }
-        nextDownTime = downTime + duration.inMilliseconds()
-        return downTime
-    }
-
-    /**
-     * Moves the start time of the next gesture ahead by the given [duration]. Does not affect
-     * any event time from the current gesture. Use this when the expected duration passed to
-     * [generateDownTime] has changed.
-     */
-    private fun moveNextDownTime(duration: Duration) {
-        generateDownTime(duration)
-    }
-
-    /**
-     * Increases the eventTime with the given [time]. Also pushes the downTime for the next
-     * chained gesture by the same amount to facilitate chaining.
-     */
-    private fun PartialGesture.increaseEventTime(time: Long = eventPeriod) {
-        moveNextDownTime(time.milliseconds)
-        lastEventTime += time
-    }
-
-    /**
-     * Adds a delay between the end of the last full or current partial gesture of the given
-     * [duration]. Guarantees that the first event time of the next gesture will be exactly
-     * [duration] later then if that gesture would be injected without this delay, provided that
-     * the next gesture is started using the same [InputDispatcher] instance as the one used to
-     * end the last gesture.
-     *
-     * Note: this does not affect the time of the next event for the _current_ partial gesture,
-     * using [enqueueMove], [enqueueUp] and [enqueueCancel], but it will affect the time of the
-     * _next_ gesture (including partial gestures started with [enqueueDown]).
-     *
-     * @param duration The duration of the delay. Must be positive
-     */
-    override fun enqueueDelay(duration: Duration) {
-        require(duration >= Duration.Zero) {
-            "duration of a delay can only be positive, not $duration"
-        }
-        moveNextDownTime(duration)
-    }
-
-    /**
-     * Generates a click event at [position]. There will be 10ms in between the down and the up
-     * event. The generated events are enqueued in this [InputDispatcher] and will be sent when
-     * [sendAllSynchronous] is called at the end of [performGesture].
-     *
-     * @param position The coordinate of the click
-     */
-    override fun enqueueClick(position: Offset) {
-        enqueueDown(0, position)
-        enqueueMove()
-        enqueueUp(0)
-    }
-
-    /**
-     * Generates a swipe gesture from [start] to [end] with the given [duration]. The generated
-     * events are enqueued in this [InputDispatcher] and will be sent when [sendAllSynchronous]
-     * is called at the end of [performGesture].
-     *
-     * @param start The start position of the gesture
-     * @param end The end position of the gesture
-     * @param duration The duration of the gesture
-     */
-    override fun enqueueSwipe(start: Offset, end: Offset, duration: Duration) {
-        val durationFloat = duration.inMilliseconds().toFloat()
-        enqueueSwipe(
-            curve = { lerp(start, end, it / durationFloat) },
-            duration = duration
-        )
-    }
-
-    /**
-     * Generates a swipe gesture from [curve]&#40;0) to [curve]&#40;[duration]), following the
-     * route defined by [curve]. Will force sampling of an event at all times defined in
-     * [keyTimes]. The number of events sampled between the key times is implementation
-     * dependent. The generated events are enqueued in this [InputDispatcher] and will be sent
-     * when [sendAllSynchronous] is called at the end of [performGesture].
-     *
-     * @param curve The function that defines the position of the gesture over time
-     * @param duration The duration of the gesture
-     * @param keyTimes An optional list of timestamps in milliseconds at which a move event must
-     * be sampled
-     */
-    override fun enqueueSwipe(
-        curve: (Long) -> Offset,
-        duration: Duration,
-        keyTimes: List<Long>
-    ) {
-        enqueueSwipes(listOf(curve), duration, keyTimes)
-    }
-
-    /**
-     * Generates [curves].size simultaneous swipe gestures, each swipe going from
-     * [curves]&#91;i&#93;(0) to [curves]&#91;i&#93;([duration]), following the route defined by
-     * [curves]&#91;i&#93;. Will force sampling of an event at all times defined in [keyTimes].
-     * The number of events sampled between the key times is implementation dependent. The
-     * generated events are enqueued in this [InputDispatcher] and will be sent when
-     * [sendAllSynchronous] is called at the end of [performGesture].
-     *
-     * @param curves The functions that define the position of the gesture over time
-     * @param duration The duration of the gestures
-     * @param keyTimes An optional list of timestamps in milliseconds at which a move event must
-     * be sampled
-     */
-    override fun enqueueSwipes(
-        curves: List<(Long) -> Offset>,
-        duration: Duration,
-        keyTimes: List<Long>
-    ) {
-        val startTime = 0L
-        val endTime = duration.inMilliseconds()
-
-        // Validate input
-        require(duration >= 1.milliseconds) {
-            "duration must be at least 1 millisecond, not $duration"
-        }
-        val validRange = startTime..endTime
-        require(keyTimes.all { it in validRange }) {
-            "keyTimes contains timestamps out of range [$startTime..$endTime]: $keyTimes"
-        }
-        require(keyTimes.asSequence().zipWithNext { a, b -> a <= b }.all { it }) {
-            "keyTimes must be sorted: $keyTimes"
-        }
-
-        // Send down events
-        curves.forEachIndexed { i, curve ->
-            enqueueDown(i, curve(startTime))
-        }
-
-        // Send move events between each consecutive pair in [t0, ..keyTimes, tN]
-        var currTime = startTime
-        var key = 0
-        while (currTime < endTime) {
-            // advance key
-            while (key < keyTimes.size && keyTimes[key] <= currTime) {
-                key++
-            }
-            // send events between t and next keyTime
-            val tNext = if (key < keyTimes.size) keyTimes[key] else endTime
-            sendPartialSwipes(curves, currTime, tNext)
-            currTime = tNext
-        }
-
-        // And end with up events
-        repeat(curves.size) {
-            enqueueUp(it)
-        }
-    }
-
-    /**
-     * Generates move events between `f([t0])` and `f([tN])` during the time window `(downTime +
-     * t0, downTime + tN]`, using [fs] to sample the coordinate of each event. The number of
-     * events sent (#numEvents) is such that the time between each event is as close to
-     * [InputDispatcher.eventPeriod] as possible, but at least 1. The first event is sent at time
-     * `downTime + (tN - t0) / #numEvents`, the last event is sent at time tN.
-     *
-     * @param fs The functions that define the coordinates of the respective gestures over time
-     * @param t0 The start time of this segment of the swipe, in milliseconds relative to downTime
-     * @param tN The end time of this segment of the swipe, in milliseconds relative to downTime
-     */
-    private fun sendPartialSwipes(
-        fs: List<(Long) -> Offset>,
-        t0: Long,
-        tN: Long
-    ) {
-        var step = 0
-        // How many steps will we take between t0 and tN? At least 1, and a number that will
-        // bring as as close to eventPeriod as possible
-        val steps = max(1, ((tN - t0) / eventPeriod.toFloat()).roundToInt())
-
-        var tPrev = t0
-        while (step++ < steps) {
-            val progress = step / steps.toFloat()
-            val t = androidx.compose.ui.util.lerp(t0, tN, progress)
-            fs.forEachIndexed { i, f ->
-                movePointer(i, f(t))
-            }
-            enqueueMove(t - tPrev)
-            tPrev = t
-        }
-    }
-
-    /**
-     * During a partial gesture, returns the position of the last touch event of the given
-     * [pointerId]. Returns `null` if no partial gesture is in progress for that [pointerId].
-     *
-     * @param pointerId The id of the pointer for which to return the current position
-     * @return The current position of the pointer with the given [pointerId], or `null` if the
-     * pointer is not currently in use
-     */
-    override fun getCurrentPosition(pointerId: Int): Offset? {
-        return partialGesture?.lastPositions?.get(pointerId)
-    }
-
-    /**
-     * Generates a down event at [position] for the pointer with the given [pointerId], starting
-     * a new partial gesture. A partial gesture can only be started if none was currently ongoing
-     * for that pointer. Pointer ids may be reused during the same gesture. The generated event
-     * is enqueued in this [InputDispatcher] and will be sent when [sendAllSynchronous] is called
-     * at the end of [performGesture].
-     *
-     * It is possible to mix partial gestures with full gestures (e.g. generate a [click]
-     * [enqueueClick] during a partial gesture), as long as you make sure that the default
-     * pointer id (id=0) is free to be used by the full gesture.
-     *
-     * A full gesture starts with a down event at some position (with this method) that indicates
-     * a finger has started touching the screen, followed by zero or more [down][enqueueDown],
-     * [move][enqueueMove] and [up][enqueueUp] events that respectively indicate that another
-     * finger started touching the screen, a finger moved around or a finger was lifted up from
-     * the screen. A gesture is finished when [up][enqueueUp] lifts the last remaining finger
-     * from the screen, or when a single [cancel][enqueueCancel] event is generated.
-     *
-     * Partial gestures don't have to be defined all in the same [performGesture] block, but
-     * keep in mind that while the gesture is not complete, all code you execute in between
-     * blocks that progress the gesture, will be executed while imaginary fingers are actively
-     * touching the screen. All events generated during a single [performGesture] block are sent
-     * together at the end of that block.
-     *
-     * In the context of testing, it is not necessary to complete a gesture with an up or cancel
-     * event, if the test ends before it expects the finger to be lifted from the screen.
-     *
-     * @param pointerId The id of the pointer, can be any number not yet in use by another pointer
-     * @param position The coordinate of the down event
-     *
-     * @see movePointer
-     * @see enqueueMove
-     * @see enqueueUp
-     * @see enqueueCancel
-     */
-    override fun enqueueDown(pointerId: Int, position: Offset) {
-        var gesture = partialGesture
-
-        // Check if this pointer is not already down
-        require(gesture == null || !gesture.lastPositions.containsKey(pointerId)) {
-            "Cannot send DOWN event, a gesture is already in progress for pointer $pointerId"
-        }
-
-        gesture?.flushPointerUpdates()
-
-        // Start a new gesture, or add the pointerId to the existing gesture
-        if (gesture == null) {
-            gesture = PartialGesture(generateDownTime(0.milliseconds), position, pointerId)
-            partialGesture = gesture
-        } else {
-            gesture.lastPositions.put(pointerId, position)
-        }
-
-        // Send the DOWN event
-        gesture.enqueueDown(pointerId)
-    }
-
-    /**
-     * Updates the position of the pointer with the given [pointerId] to the given [position],
-     * but does not generate a move event. Use this to move multiple pointers simultaneously. To
-     * generate the next move event, which will contain the current position of _all_ pointers
-     * (not just the moved ones), call [enqueueMove] without arguments. If you move one or more
-     * pointers and then call [enqueueDown] or [enqueueUp], without calling [enqueueMove] first,
-     * a move event will be generated right before that down or up event. See [enqueueDown] for
-     * more information on how to make complete gestures from partial gestures.
-     *
-     * @param pointerId The id of the pointer to move, as supplied in [enqueueDown]
-     * @param position The position to move the pointer to
-     *
-     * @see enqueueDown
-     * @see enqueueMove
-     * @see enqueueUp
-     * @see enqueueCancel
-     */
-    override fun movePointer(pointerId: Int, position: Offset) {
-        val gesture = partialGesture
-
-        // Check if this pointer is in the gesture
-        check(gesture != null) {
-            "Cannot move pointers, no gesture is in progress"
-        }
-        require(gesture.lastPositions.containsKey(pointerId)) {
-            "Cannot move pointer $pointerId, it is not active in the current gesture"
-        }
-
-        gesture.lastPositions.put(pointerId, position)
-        gesture.hasPointerUpdates = true
-    }
-
-    /**
-     * Generates a move event [delay] milliseconds after the previous injected event of this
-     * gesture, without moving any of the pointers. The default [delay] is [10 milliseconds]
-     * [InputDispatcher.eventPeriod]. Use this to commit all changes in pointer location made
-     * with [movePointer]. The generated event will contain the current position of all pointers.
-     * It is enqueued in this [InputDispatcher] and will be sent when [sendAllSynchronous] is
-     * called at the end of [performGesture]. See [enqueueDown] for more information on how to
-     * make complete gestures from partial gestures.
-     *
-     * @param delay The time in milliseconds between the previously injected event and the move
-     * event. [10 milliseconds][InputDispatcher.eventPeriod] by default.
-     */
-    override fun enqueueMove(delay: Long) {
-        val gesture = checkNotNull(partialGesture) {
-            "Cannot send MOVE event, no gesture is in progress"
-        }
-        require(delay >= 0) {
-            "Cannot send MOVE event with a delay of $delay ms"
-        }
-
-        gesture.increaseEventTime(delay)
-        gesture.enqueueMove()
-        gesture.hasPointerUpdates = false
-    }
-
-    /**
-     * Generates an up event for the given [pointerId] at the current position of that pointer,
-     * [delay] milliseconds after the previous injected event of this gesture. The default
-     * [delay] is 0 milliseconds. The generated event is enqueued in this [InputDispatcher] and
-     * will be sent when [sendAllSynchronous] is called at the end of [performGesture]. See
-     * [enqueueDown] for more information on how to make complete gestures from partial gestures.
-     *
-     * @param pointerId The id of the pointer to lift up, as supplied in [enqueueDown]
-     * @param delay The time in milliseconds between the previously injected event and the move
-     * event. 0 milliseconds by default.
-     *
-     * @see enqueueDown
-     * @see movePointer
-     * @see enqueueMove
-     * @see enqueueCancel
-     */
-    override fun enqueueUp(pointerId: Int, delay: Long) {
-        val gesture = partialGesture
-
-        // Check if this pointer is in the gesture
-        check(gesture != null) {
-            "Cannot send UP event, no gesture is in progress"
-        }
-        require(gesture.lastPositions.containsKey(pointerId)) {
-            "Cannot send UP event for pointer $pointerId, it is not active in the current gesture"
-        }
-        require(delay >= 0) {
-            "Cannot send UP event with a delay of $delay ms"
-        }
-
-        gesture.flushPointerUpdates()
-        gesture.increaseEventTime(delay)
-
-        // First send the UP event
-        gesture.enqueueUp(pointerId)
-
-        // Then remove the pointer, and end the gesture if no pointers are left
-        gesture.lastPositions.remove(pointerId)
-        if (gesture.lastPositions.isEmpty) {
-            partialGesture = null
-        }
-    }
-
-    /**
-     * Generates a cancel event [delay] milliseconds after the previous injected event of this
-     * gesture. The default [delay] is [10 milliseconds][InputDispatcher.eventPeriod]. The
-     * generated event is enqueued in this [InputDispatcher] and will be sent when
-     * [sendAllSynchronous] is called at the end of [performGesture]. See [enqueueDown] for more
-     * information on how to make complete gestures from partial gestures.
-     *
-     * @param delay The time in milliseconds between the previously injected event and the cancel
-     * event. [10 milliseconds][InputDispatcher.eventPeriod] by default.
-     *
-     * @see enqueueDown
-     * @see movePointer
-     * @see enqueueMove
-     * @see enqueueUp
-     */
-    override fun enqueueCancel(delay: Long) {
-        val gesture = checkNotNull(partialGesture) {
-            "Cannot send CANCEL event, no gesture is in progress"
-        }
-        require(delay >= 0) {
-            "Cannot send CANCEL event with a delay of $delay ms"
-        }
-
-        gesture.increaseEventTime(delay)
-        gesture.enqueueCancel()
-        partialGesture = null
-    }
-
-    /**
-     * Generates a MOVE event with all pointer locations, if any of the pointers has been moved by
-     * [movePointer] since the last MOVE event.
-     */
-    private fun PartialGesture.flushPointerUpdates() {
-        if (hasPointerUpdates) {
-            enqueueMove(eventPeriod)
-        }
-    }
-
-    protected abstract fun PartialGesture.enqueueDown(pointerId: Int)
-
-    protected abstract fun PartialGesture.enqueueMove()
-
-    protected abstract fun PartialGesture.enqueueUp(pointerId: Int)
-
-    protected abstract fun PartialGesture.enqueueCancel()
-
-    /**
-     * A test rule that modifies [InputDispatcher]s behavior. Can be used to disable dispatching
-     * of MotionEvents in real time (skips the suspend before injection of an event) or to change
-     * the time between consecutive injected events.
-     *
-     * @param disableDispatchInRealTime If set, controls whether or not events with an eventTime
-     * in the future will be dispatched as soon as possible or at that exact eventTime. If
-     * `false` or not set, will suspend until the eventTime, if `true`, will send the event
-     * immediately without suspending. See also [InputDispatcher.dispatchInRealTime].
-     * @param eventPeriodOverride If set, specifies a different period in milliseconds between
-     * two consecutive injected motion events injected by this [InputDispatcher]. If not
-     * set, the event period of 10 milliseconds is unchanged.
-     *
-     * @see InputDispatcher.eventPeriod
-     */
-    internal class InputDispatcherTestRule(
-        private val disableDispatchInRealTime: Boolean = false,
-        private val eventPeriodOverride: Long? = null
-    ) : TestRule {
-
-        override fun apply(base: Statement, description: Description?): Statement {
-            return ModifyingStatement(base)
-        }
-
-        inner class ModifyingStatement(private val base: Statement) : Statement() {
-            override fun evaluate() {
-                if (disableDispatchInRealTime) {
-                    dispatchInRealTime = false
-                }
-                if (eventPeriodOverride != null) {
-                    eventPeriod = eventPeriodOverride
-                }
-                try {
-                    base.evaluate()
-                } finally {
-                    if (disableDispatchInRealTime) {
-                        dispatchInRealTime = true
-                    }
-                    if (eventPeriodOverride != null) {
-                        eventPeriod = 10L
-                    }
-                }
-            }
-        }
-    }
-}
-
-internal actual class PartialGesture actual constructor(
-    val downTime: Long,
-    startPosition: Offset,
-    pointerId: Int
-) {
-    var lastEventTime: Long = downTime
-    val lastPositions = SparseArrayCompat<Offset>().apply { put(pointerId, startPosition) }
-    var hasPointerUpdates: Boolean = false
-}
-
-internal actual fun InputDispatcher(owner: Owner): InputDispatcher {
-    require(owner is AndroidOwner) {
-        "InputDispatcher currently only supports dispatching to AndroidOwner, not to " +
-                owner::class.java.simpleName
-    }
-    val view = owner.view
-    return AndroidInputDispatcher { view.dispatchTouchEvent(it) }.apply {
-        AndroidBaseInputDispatcher.states.remove(owner)?.also {
-            // TODO(b/157653315): Move restore state to constructor
-            if (it.partialGesture != null) {
-                nextDownTime = it.nextDownTime
-                gestureLateness = it.gestureLateness
-                partialGesture = it.partialGesture
-            }
-        }
-    }
-}
diff --git a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidComposeTestRule.kt b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidComposeTestRule.kt
new file mode 100644
index 0000000..d1b4749
--- /dev/null
+++ b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidComposeTestRule.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2020 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.ui.test
+
+import androidx.activity.ComponentActivity
+import androidx.ui.test.android.AndroidOwnerRegistry
+import androidx.ui.test.android.FirstDrawRegistry
+import androidx.ui.test.android.registerComposeWithEspresso
+import androidx.ui.test.android.unregisterComposeFromEspresso
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Recomposer
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.compose.animation.transitionsEnabled
+import androidx.compose.ui.platform.setContent
+import androidx.compose.foundation.InternalFoundationApi
+import androidx.compose.foundation.blinkingCursorEnabled
+import androidx.compose.ui.text.input.textInputServiceFactory
+import androidx.compose.ui.unit.Density
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import androidx.compose.ui.unit.IntSize
+import androidx.ui.test.android.createAndroidComposeRule
+
+actual fun createComposeRule(
+    disableTransitions: Boolean,
+    disableBlinkingCursor: Boolean
+): ComposeTestRule =
+    createAndroidComposeRule<ComponentActivity>(
+        disableTransitions,
+        disableBlinkingCursor
+    )
+
+/**
+ * Android specific implementation of [ComposeTestRule].
+ */
+class AndroidComposeTestRule<T : ComponentActivity>(
+    // TODO(b/153623653): Remove activityRule from arguments when AndroidComposeTestRule can
+    //  work with any kind of Activity launcher.
+    val activityRule: ActivityScenarioRule<T>,
+    private val disableTransitions: Boolean = false,
+    private val disableBlinkingCursor: Boolean = true
+) : ComposeTestRule {
+
+    private fun getActivity(): T {
+        var activity: T? = null
+        if (activity == null) {
+            activityRule.scenario.onActivity { activity = it }
+            if (activity == null) {
+                throw IllegalStateException("Activity was not set in the ActivityScenarioRule!")
+            }
+        }
+        return activity!!
+    }
+
+    override val clockTestRule: AnimationClockTestRule = AndroidAnimationClockTestRule()
+
+    internal var disposeContentHook: (() -> Unit)? = null
+
+    override val density: Density get() =
+        Density(getActivity().resources.displayMetrics.density)
+
+    override val displaySize by lazy {
+        getActivity().resources.displayMetrics.let {
+            IntSize(it.widthPixels, it.heightPixels)
+        }
+    }
+
+    override fun apply(base: Statement, description: Description?): Statement {
+        val activityTestRuleStatement = activityRule.apply(base, description)
+        val composeTestRuleStatement = AndroidComposeStatement(activityTestRuleStatement)
+        return clockTestRule.apply(composeTestRuleStatement, description)
+    }
+
+    /**
+     * @throws IllegalStateException if called more than once per test.
+     */
+    @SuppressWarnings("SyntheticAccessor")
+    override fun setContent(composable: @Composable () -> Unit) {
+        check(disposeContentHook == null) {
+            "Cannot call setContent twice per test!"
+        }
+
+        lateinit var activity: T
+        activityRule.scenario.onActivity { activity = it }
+
+        runOnUiThread {
+            val composition = activity.setContent(
+                Recomposer.current(),
+                composable
+            )
+            disposeContentHook = {
+                composition.dispose()
+            }
+        }
+
+        if (!isOnUiThread()) {
+            // Only wait for idleness if not on the UI thread. If we are on the UI thread, the
+            // caller clearly wants to keep tight control over execution order, so don't go
+            // executing future tasks on the main thread.
+            waitForIdle()
+        }
+    }
+
+    inner class AndroidComposeStatement(
+        private val base: Statement
+    ) : Statement() {
+        override fun evaluate() {
+            val oldTextInputFactory = @Suppress("DEPRECATION_ERROR")(textInputServiceFactory)
+            beforeEvaluate()
+            try {
+                base.evaluate()
+            } finally {
+                afterEvaluate()
+                @Suppress("DEPRECATION_ERROR")
+                textInputServiceFactory = oldTextInputFactory
+            }
+        }
+
+        @OptIn(InternalFoundationApi::class)
+        private fun beforeEvaluate() {
+            transitionsEnabled = !disableTransitions
+            blinkingCursorEnabled = !disableBlinkingCursor
+            AndroidOwnerRegistry.setupRegistry()
+            FirstDrawRegistry.setupRegistry()
+            registerComposeWithEspresso()
+            @Suppress("DEPRECATION_ERROR")
+            textInputServiceFactory = {
+                TextInputServiceForTests(it)
+            }
+        }
+
+        @OptIn(InternalFoundationApi::class)
+        private fun afterEvaluate() {
+            transitionsEnabled = true
+            blinkingCursorEnabled = true
+            AndroidOwnerRegistry.tearDownRegistry()
+            FirstDrawRegistry.tearDownRegistry()
+            unregisterComposeFromEspresso()
+            // Dispose the content
+            if (disposeContentHook != null) {
+                runOnUiThread {
+                    // NOTE: currently, calling dispose after an exception that happened during
+                    // composition is not a safe call. Compose runtime should fix this, and then
+                    // this call will be okay. At the moment, however, calling this could
+                    // itself produce an exception which will then obscure the original
+                    // exception. To fix this, we will just wrap this call in a try/catch of
+                    // its own
+                    try {
+                        disposeContentHook!!()
+                    } catch (e: Exception) {
+                        // ignore
+                    }
+                    disposeContentHook = null
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidInputDispatcher.kt b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidInputDispatcher.kt
new file mode 100644
index 0000000..f490787
--- /dev/null
+++ b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/AndroidInputDispatcher.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2019 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.ui.test
+
+import androidx.compose.ui.node.Owner
+import androidx.compose.ui.platform.AndroidOwner
+import androidx.ui.test.android.AndroidInputDispatcher
+
+internal actual fun InputDispatcher(owner: Owner): InputDispatcher {
+    require(owner is AndroidOwner) {
+        "InputDispatcher currently only supports dispatching to AndroidOwner, not to " +
+                owner::class.java.simpleName
+    }
+    val view = owner.view
+    return AndroidInputDispatcher { view.dispatchTouchEvent(it) }.apply {
+        BaseInputDispatcher.states.remove(owner)?.also {
+            // TODO(b/157653315): Move restore state to constructor
+            if (it.partialGesture != null) {
+                nextDownTime = it.nextDownTime
+                gestureLateness = it.gestureLateness
+                partialGesture = it.partialGesture
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/android/AndroidComposeTestRule.kt b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/android/AndroidComposeTestRule.kt
index ae0825d..8886e6e 100644
--- a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/android/AndroidComposeTestRule.kt
+++ b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/android/AndroidComposeTestRule.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2020 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.
@@ -16,26 +16,10 @@
 
 package androidx.ui.test.android
 
-import android.util.DisplayMetrics
 import androidx.activity.ComponentActivity
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Recomposer
 import androidx.test.ext.junit.rules.ActivityScenarioRule
-import androidx.compose.animation.transitionsEnabled
-import androidx.compose.ui.platform.setContent
-import androidx.compose.foundation.InternalFoundationApi
-import androidx.compose.foundation.blinkingCursorEnabled
-import androidx.compose.ui.text.input.textInputServiceFactory
-import androidx.ui.test.AnimationClockTestRule
-import androidx.ui.test.ComposeTestRule
-import androidx.ui.test.TextInputServiceForTests
+import androidx.ui.test.AndroidComposeTestRule
 import androidx.ui.test.createComposeRule
-import androidx.ui.test.isOnUiThread
-import androidx.ui.test.runOnUiThread
-import androidx.ui.test.waitForIdle
-import androidx.compose.ui.unit.Density
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
 
 /**
  * Factory method to provide android specific implementation of [createComposeRule], for a given
@@ -82,127 +66,4 @@
     ActivityScenarioRule(activityClass),
     disableTransitions,
     disableBlinkingCursor
-)
-
-/**
- * Android specific implementation of [ComposeTestRule].
- */
-class AndroidComposeTestRule<T : ComponentActivity>(
-    // TODO(b/153623653): Remove activityRule from arguments when AndroidComposeTestRule can
-    //  work with any kind of Activity launcher.
-    val activityRule: ActivityScenarioRule<T>,
-    private val disableTransitions: Boolean = false,
-    private val disableBlinkingCursor: Boolean = true
-) : ComposeTestRule {
-
-    private fun getActivity(): T {
-        var activity: T? = null
-        if (activity == null) {
-            activityRule.scenario.onActivity { activity = it }
-            if (activity == null) {
-                throw IllegalStateException("Activity was not set in the ActivityScenarioRule!")
-            }
-        }
-        return activity!!
-    }
-
-    override val clockTestRule = AnimationClockTestRule()
-
-    internal var disposeContentHook: (() -> Unit)? = null
-
-    override val density: Density get() =
-        Density(getActivity().resources.displayMetrics.density)
-
-    override val displayMetrics: DisplayMetrics get() = getActivity().resources.displayMetrics
-
-    override fun apply(base: Statement, description: Description?): Statement {
-        val activityTestRuleStatement = activityRule.apply(base, description)
-        val composeTestRuleStatement = AndroidComposeStatement(activityTestRuleStatement)
-        return clockTestRule.apply(composeTestRuleStatement, description)
-    }
-
-    /**
-     * @throws IllegalStateException if called more than once per test.
-     */
-    @SuppressWarnings("SyntheticAccessor")
-    override fun setContent(composable: @Composable () -> Unit) {
-        check(disposeContentHook == null) {
-            "Cannot call setContent twice per test!"
-        }
-
-        lateinit var activity: T
-        activityRule.scenario.onActivity { activity = it }
-
-        runOnUiThread {
-            val composition = activity.setContent(
-                Recomposer.current(),
-                composable
-            )
-            disposeContentHook = {
-                composition.dispose()
-            }
-        }
-
-        if (!isOnUiThread()) {
-            // Only wait for idleness if not on the UI thread. If we are on the UI thread, the
-            // caller clearly wants to keep tight control over execution order, so don't go
-            // executing future tasks on the main thread.
-            waitForIdle()
-        }
-    }
-
-    inner class AndroidComposeStatement(
-        private val base: Statement
-    ) : Statement() {
-        override fun evaluate() {
-            val oldTextInputFactory = @Suppress("DEPRECATION_ERROR")(textInputServiceFactory)
-            beforeEvaluate()
-            try {
-                base.evaluate()
-            } finally {
-                afterEvaluate()
-                @Suppress("DEPRECATION_ERROR")
-                textInputServiceFactory = oldTextInputFactory
-            }
-        }
-
-        @OptIn(InternalFoundationApi::class)
-        private fun beforeEvaluate() {
-            transitionsEnabled = !disableTransitions
-            blinkingCursorEnabled = !disableBlinkingCursor
-            AndroidOwnerRegistry.setupRegistry()
-            FirstDrawRegistry.setupRegistry()
-            registerComposeWithEspresso()
-            @Suppress("DEPRECATION_ERROR")
-            textInputServiceFactory = {
-                TextInputServiceForTests(it)
-            }
-        }
-
-        @OptIn(InternalFoundationApi::class)
-        private fun afterEvaluate() {
-            transitionsEnabled = true
-            blinkingCursorEnabled = true
-            AndroidOwnerRegistry.tearDownRegistry()
-            FirstDrawRegistry.tearDownRegistry()
-            unregisterComposeFromEspresso()
-            // Dispose the content
-            if (disposeContentHook != null) {
-                runOnUiThread {
-                    // NOTE: currently, calling dispose after an exception that happened during
-                    // composition is not a safe call. Compose runtime should fix this, and then
-                    // this call will be okay. At the moment, however, calling this could
-                    // itself produce an exception which will then obscure the original
-                    // exception. To fix this, we will just wrap this call in a try/catch of
-                    // its own
-                    try {
-                        disposeContentHook!!()
-                    } catch (e: Exception) {
-                        // ignore
-                    }
-                    disposeContentHook = null
-                }
-            }
-        }
-    }
-}
+)
\ No newline at end of file
diff --git a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/android/AndroidInputDispatcher.kt b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/android/AndroidInputDispatcher.kt
index 6261084..739c8cc 100644
--- a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/android/AndroidInputDispatcher.kt
+++ b/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/android/AndroidInputDispatcher.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2020 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.
@@ -27,17 +27,35 @@
 import android.view.MotionEvent.ACTION_UP
 import androidx.compose.runtime.dispatch.AndroidUiDispatcher
 import androidx.compose.ui.geometry.Offset
-import androidx.ui.test.AndroidBaseInputDispatcher
+import androidx.compose.ui.node.Owner
+import androidx.compose.ui.platform.AndroidOwner
+import androidx.ui.test.BaseInputDispatcher
 import androidx.ui.test.InputDispatcher
+import androidx.ui.test.InputDispatcherState
 import androidx.ui.test.PartialGesture
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
 import kotlin.math.max
 
 internal class AndroidInputDispatcher(
     private val sendEvent: (MotionEvent) -> Unit
-) : AndroidBaseInputDispatcher() {
+) : BaseInputDispatcher() {
+
+    companion object : AndroidOwnerRegistry.OnRegistrationChangedListener {
+        init {
+            AndroidOwnerRegistry.addOnRegistrationChangedListener(this)
+        }
+
+        override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
+            if (!registered) {
+                states.remove(owner)
+            }
+        }
+    }
 
     private val batchLock = Any()
     // Batched events are generated just-in-time, given the "lateness" of the dispatching (see
@@ -48,6 +66,12 @@
 
     override val now: Long get() = SystemClock.uptimeMillis()
 
+    override fun saveState(owner: Owner?) {
+        if (owner != null && AndroidOwnerRegistry.getUnfilteredOwners().contains(owner)) {
+            states[owner] = InputDispatcherState(nextDownTime, gestureLateness, partialGesture)
+        }
+    }
+
     override fun PartialGesture.enqueueDown(pointerId: Int) {
         batchMotionEvent(
             if (lastPositions.size() == 1) ACTION_DOWN else ACTION_POINTER_DOWN,
@@ -184,7 +208,7 @@
      */
     private suspend fun sendAndRecycleEvent(event: MotionEvent) {
         try {
-            if (dispatchInRealTime) {
+            if (InputDispatcher.dispatchInRealTime) {
                 val delayMs = event.eventTime - now
                 if (delayMs > 0) {
                     delay(delayMs)
@@ -195,4 +219,50 @@
             event.recycle()
         }
     }
+
+    /**
+     * A test rule that modifies [InputDispatcher]s behavior. Can be used to disable dispatching
+     * of MotionEvents in real time (skips the suspend before injection of an event) or to change
+     * the time between consecutive injected events.
+     *
+     * @param disableDispatchInRealTime If set, controls whether or not events with an eventTime
+     * in the future will be dispatched as soon as possible or at that exact eventTime. If
+     * `false` or not set, will suspend until the eventTime, if `true`, will send the event
+     * immediately without suspending. See also [InputDispatcher.dispatchInRealTime].
+     * @param eventPeriodOverride If set, specifies a different period in milliseconds between
+     * two consecutive injected motion events injected by this [InputDispatcher]. If not
+     * set, the event period of 10 milliseconds is unchanged.
+     *
+     * @see InputDispatcher.eventPeriod
+     */
+    internal class InputDispatcherTestRule(
+        private val disableDispatchInRealTime: Boolean = false,
+        private val eventPeriodOverride: Long? = null
+    ) : TestRule {
+
+        override fun apply(base: Statement, description: Description?): Statement {
+            return ModifyingStatement(base)
+        }
+
+        inner class ModifyingStatement(private val base: Statement) : Statement() {
+            override fun evaluate() {
+                if (disableDispatchInRealTime) {
+                    InputDispatcher.dispatchInRealTime = false
+                }
+                if (eventPeriodOverride != null) {
+                    InputDispatcher.eventPeriod = eventPeriodOverride
+                }
+                try {
+                    base.evaluate()
+                } finally {
+                    if (disableDispatchInRealTime) {
+                        InputDispatcher.dispatchInRealTime = true
+                    }
+                    if (eventPeriodOverride != null) {
+                        InputDispatcher.eventPeriod = 10L
+                    }
+                }
+            }
+        }
+    }
 }
diff --git a/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/InputDispatcher.kt b/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/InputDispatcher.kt
index 2395aa3..80788b7 100644
--- a/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/InputDispatcher.kt
+++ b/ui/ui-test/src/commonMain/kotlin/androidx/ui/test/InputDispatcher.kt
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package androidx.ui.test
 
 import androidx.compose.ui.geometry.Offset
@@ -22,7 +21,26 @@
 import androidx.compose.ui.unit.inMilliseconds
 import androidx.compose.ui.unit.milliseconds
 
-internal abstract class InputDispatcher {
+/**
+ * Interface for dispatching full and partial gestures.
+ *
+ * Full gestures:
+ * * [enqueueClick]
+ * * [enqueueSwipe]
+ * * [enqueueSwipes]
+ *
+ * Partial gestures:
+ * * [enqueueDown]
+ * * [enqueueMove]
+ * * [enqueueUp]
+ * * [enqueueCancel]
+ * * [movePointer]
+ * * [getCurrentPosition]
+ *
+ * Chaining methods:
+ * * [enqueueDelay]
+ */
+internal interface InputDispatcher {
     companion object {
         /**
          * Whether or not injection of events should be suspended in between events until [now]
@@ -44,72 +62,207 @@
     /**
      * The current time, in the time scale used by gesture events.
      */
-    protected abstract val now: Long
+    val now: Long
 
     /**
      * Sends all enqueued events and blocks while they are dispatched. Will suspend before
      * dispatching an event until [now] is at least that event's timestamp. If an exception is
      * thrown during the process, all events that haven't yet been dispatched will be dropped.
      */
-    internal abstract fun sendAllSynchronous()
+    fun sendAllSynchronous()
 
-    internal abstract fun saveState(owner: Owner?)
-
+    fun saveState(owner: Owner?)
     /**
      * Called when this [InputDispatcher] is about to be discarded, from [GestureScope.dispose].
      */
-    internal abstract fun dispose()
+    fun dispose()
 
-    abstract fun enqueueClick(position: Offset)
+    /**
+     * Generates a click event at [position]. There will be 10ms in between the down and the up
+     * event. The generated events are enqueued in this [InputDispatcher] and will be sent when
+     * [sendAllSynchronous] is called at the end of [performGesture].
+     *
+     * @param position The coordinate of the click
+     */
+    fun enqueueClick(position: Offset)
 
-    abstract fun enqueueSwipe(start: Offset, end: Offset, duration: Duration)
+    /**
+     * Generates a swipe gesture from [start] to [end] with the given [duration]. The generated
+     * events are enqueued in this [InputDispatcher] and will be sent when [sendAllSynchronous]
+     * is called at the end of [performGesture].
+     *
+     * @param start The start position of the gesture
+     * @param end The end position of the gesture
+     * @param duration The duration of the gesture
+     */
+    fun enqueueSwipe(start: Offset, end: Offset, duration: Duration)
 
-    abstract fun enqueueSwipe(
+    /**
+     * Generates a swipe gesture from [curve]&#40;0) to [curve]&#40;[duration]), following the
+     * route defined by [curve]. Will force sampling of an event at all times defined in
+     * [keyTimes]. The number of events sampled between the key times is implementation
+     * dependent. The generated events are enqueued in this [InputDispatcher] and will be sent
+     * when [sendAllSynchronous] is called at the end of [performGesture].
+     *
+     * @param curve The function that defines the position of the gesture over time
+     * @param duration The duration of the gesture
+     * @param keyTimes An optional list of timestamps in milliseconds at which a move event must
+     * be sampled
+     */
+    fun enqueueSwipe(
         curve: (Long) -> Offset,
         duration: Duration,
         keyTimes: List<Long> = emptyList()
     )
 
-    abstract fun enqueueSwipes(
+    /**
+     * Generates [curves].size simultaneous swipe gestures, each swipe going from
+     * [curves]&#91;i&#93;(0) to [curves]&#91;i&#93;([duration]), following the route defined by
+     * [curves]&#91;i&#93;. Will force sampling of an event at all times defined in [keyTimes].
+     * The number of events sampled between the key times is implementation dependent. The
+     * generated events are enqueued in this [InputDispatcher] and will be sent when
+     * [sendAllSynchronous] is called at the end of [performGesture].
+     *
+     * @param curves The functions that define the position of the gesture over time
+     * @param duration The duration of the gestures
+     * @param keyTimes An optional list of timestamps in milliseconds at which a move event must
+     * be sampled
+     */
+    fun enqueueSwipes(
         curves: List<(Long) -> Offset>,
         duration: Duration,
         keyTimes: List<Long> = emptyList()
     )
 
-    abstract fun enqueueDelay(duration: Duration)
+    /**
+     * Adds a delay between the end of the last full or current partial gesture of the given
+     * [duration]. Guarantees that the first event time of the next gesture will be exactly
+     * [duration] later then if that gesture would be injected without this delay, provided that
+     * the next gesture is started using the same [InputDispatcher] instance as the one used to
+     * end the last gesture.
+     *
+     * Note: this does not affect the time of the next event for the _current_ partial gesture,
+     * using [enqueueMove], [enqueueUp] and [enqueueCancel], but it will affect the time of the
+     * _next_ gesture (including partial gestures started with [enqueueDown]).
+     *
+     * @param duration The duration of the delay. Must be positive
+     */
+    fun enqueueDelay(duration: Duration)
 
-    abstract fun enqueueDown(pointerId: Int, position: Offset)
+    /**
+     * Generates a down event at [position] for the pointer with the given [pointerId], starting
+     * a new partial gesture. A partial gesture can only be started if none was currently ongoing
+     * for that pointer. Pointer ids may be reused during the same gesture. The generated event
+     * is enqueued in this [InputDispatcher] and will be sent when [sendAllSynchronous] is called
+     * at the end of [performGesture].
+     *
+     * It is possible to mix partial gestures with full gestures (e.g. generate a [click]
+     * [enqueueClick] during a partial gesture), as long as you make sure that the default
+     * pointer id (id=0) is free to be used by the full gesture.
+     *
+     * A full gesture starts with a down event at some position (with this method) that indicates
+     * a finger has started touching the screen, followed by zero or more [down][enqueueDown],
+     * [move][enqueueMove] and [up][enqueueUp] events that respectively indicate that another
+     * finger started touching the screen, a finger moved around or a finger was lifted up from
+     * the screen. A gesture is finished when [up][enqueueUp] lifts the last remaining finger
+     * from the screen, or when a single [cancel][enqueueCancel] event is generated.
+     *
+     * Partial gestures don't have to be defined all in the same [performGesture] block, but
+     * keep in mind that while the gesture is not complete, all code you execute in between
+     * blocks that progress the gesture, will be executed while imaginary fingers are actively
+     * touching the screen. All events generated during a single [performGesture] block are sent
+     * together at the end of that block.
+     *
+     * In the context of testing, it is not necessary to complete a gesture with an up or cancel
+     * event, if the test ends before it expects the finger to be lifted from the screen.
+     *
+     * @param pointerId The id of the pointer, can be any number not yet in use by another pointer
+     * @param position The coordinate of the down event
+     *
+     * @see movePointer
+     * @see enqueueMove
+     * @see enqueueUp
+     * @see enqueueCancel
+     */
+    fun enqueueDown(pointerId: Int, position: Offset)
 
-    abstract fun enqueueUp(pointerId: Int, delay: Long = 0)
+    /**
+     * Generates an up event for the given [pointerId] at the current position of that pointer,
+     * [delay] milliseconds after the previous injected event of this gesture. The default
+     * [delay] is 0 milliseconds. The generated event is enqueued in this [InputDispatcher] and
+     * will be sent when [sendAllSynchronous] is called at the end of [performGesture]. See
+     * [enqueueDown] for more information on how to make complete gestures from partial gestures.
+     *
+     * @param pointerId The id of the pointer to lift up, as supplied in [enqueueDown]
+     * @param delay The time in milliseconds between the previously injected event and the move
+     * event. 0 milliseconds by default.
+     *
+     * @see enqueueDown
+     * @see movePointer
+     * @see enqueueMove
+     * @see enqueueCancel
+     */
+    fun enqueueUp(pointerId: Int, delay: Long = 0)
 
-    abstract fun enqueueCancel(delay: Long = eventPeriod)
+    /**
+     * Generates a cancel event [delay] milliseconds after the previous injected event of this
+     * gesture. The default [delay] is [10 milliseconds][InputDispatcher.eventPeriod]. The
+     * generated event is enqueued in this [InputDispatcher] and will be sent when
+     * [sendAllSynchronous] is called at the end of [performGesture]. See [enqueueDown] for more
+     * information on how to make complete gestures from partial gestures.
+     *
+     * @param delay The time in milliseconds between the previously injected event and the cancel
+     * event. [10 milliseconds][InputDispatcher.eventPeriod] by default.
+     *
+     * @see enqueueDown
+     * @see movePointer
+     * @see enqueueMove
+     * @see enqueueUp
+     */
+    fun enqueueCancel(delay: Long = eventPeriod)
 
-    abstract fun movePointer(pointerId: Int, position: Offset)
+    /**
+     * Updates the position of the pointer with the given [pointerId] to the given [position],
+     * but does not generate a move event. Use this to move multiple pointers simultaneously. To
+     * generate the next move event, which will contain the current position of _all_ pointers
+     * (not just the moved ones), call [enqueueMove] without arguments. If you move one or more
+     * pointers and then call [enqueueDown] or [enqueueUp], without calling [enqueueMove] first,
+     * a move event will be generated right before that down or up event. See [enqueueDown] for
+     * more information on how to make complete gestures from partial gestures.
+     *
+     * @param pointerId The id of the pointer to move, as supplied in [enqueueDown]
+     * @param position The position to move the pointer to
+     *
+     * @see enqueueDown
+     * @see enqueueMove
+     * @see enqueueUp
+     * @see enqueueCancel
+     */
+    fun movePointer(pointerId: Int, position: Offset)
 
-    abstract fun enqueueMove(delay: Long = eventPeriod)
+    /**
+     * Generates a move event [delay] milliseconds after the previous injected event of this
+     * gesture, without moving any of the pointers. The default [delay] is [10 milliseconds]
+     * [eventPeriod]. Use this to commit all changes in pointer location made
+     * with [movePointer]. The generated event will contain the current position of all pointers.
+     * It is enqueued in this [InputDispatcher] and will be sent when [sendAllSynchronous] is
+     * called at the end of [performGesture]. See [enqueueDown] for more information on how to
+     * make complete gestures from partial gestures.
+     *
+     * @param delay The time in milliseconds between the previously injected event and the move
+     * event. [10 milliseconds][eventPeriod] by default.
+     */
+    fun enqueueMove(delay: Long = eventPeriod)
 
-    abstract fun getCurrentPosition(pointerId: Int): Offset?
+    /**
+     * During a partial gesture, returns the position of the last touch event of the given
+     * [pointerId]. Returns `null` if no partial gesture is in progress for that [pointerId].
+     *
+     * @param pointerId The id of the pointer for which to return the current position
+     * @return The current position of the pointer with the given [pointerId], or `null` if the
+     * pointer is not currently in use
+     */
+    fun getCurrentPosition(pointerId: Int): Offset?
 }
 
-/**
- * The state of an [InputDispatcher], saved when the [GestureScope] is disposed and restored when
- * the [GestureScope] is recreated.
- *
- * @param nextDownTime The downTime of the start of the next gesture, when chaining gestures.
- * This property will only be restored if an incomplete gesture was in progress when the state of
- * the [InputDispatcher] was saved.
- * @param gestureLateness The time difference in milliseconds between enqueuing the first event
- * of the gesture and dispatching it. Depending on the implementation of [InputDispatcher], this
- * may or may not be used.
- * @param partialGesture The state of an incomplete gesture. If no gesture was in progress
- * when the state of the [InputDispatcher] was saved, this will be `null`.
- */
-internal data class InputDispatcherState(
-    val nextDownTime: Long,
-    var gestureLateness: Long?,
-    val partialGesture: PartialGesture?
-)
-
-internal expect class PartialGesture(downTime: Long, startPosition: Offset, pointerId: Int)
-
-internal expect fun InputDispatcher(owner: Owner): InputDispatcher
\ No newline at end of file
+internal expect fun InputDispatcher(owner: Owner): InputDispatcher
diff --git a/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopAnimationClockTestRule.kt b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopAnimationClockTestRule.kt
new file mode 100644
index 0000000..cae407e
--- /dev/null
+++ b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopAnimationClockTestRule.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2020 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.ui.test
+
+import androidx.compose.animation.core.AnimationClockObserver
+import androidx.compose.animation.core.InternalAnimationApi
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+internal class DesktopTestAnimationClock : TestAnimationClock {
+    override val isIdle: Boolean
+        get() = TODO("Not yet implemented")
+
+    override fun pauseClock() {
+        TODO("Not yet implemented")
+    }
+
+    override fun resumeClock() {
+        TODO("Not yet implemented")
+    }
+
+    override val isPaused: Boolean
+        get() = TODO("Not yet implemented")
+
+    override fun advanceClock(milliseconds: Long) {
+        TODO("Not yet implemented")
+    }
+
+    override fun subscribe(observer: AnimationClockObserver) {
+        TODO("Not yet implemented")
+    }
+
+    override fun unsubscribe(observer: AnimationClockObserver) {
+        TODO("Not yet implemented")
+    }
+}
+
+internal class DesktopAnimationClockTestRule : AnimationClockTestRule {
+
+    override val clock: TestAnimationClock get() = DesktopTestAnimationClock()
+
+    /**
+     * Convenience property for calling [`clock.isPaused`][TestAnimationClock.isPaused]
+     */
+    override val isPaused: Boolean get() = clock.isPaused
+
+    /**
+     * Convenience method for calling [`clock.pauseClock()`][TestAnimationClock.pauseClock]
+     */
+    override fun pauseClock() = clock.pauseClock()
+
+    /**
+     * Convenience method for calling [`clock.resumeClock()`][TestAnimationClock.resumeClock]
+     */
+    override fun resumeClock() = clock.resumeClock()
+
+    /**
+     * Convenience method for calling [`clock.advanceClock()`][TestAnimationClock.advanceClock]
+     */
+    override fun advanceClock(milliseconds: Long) = clock.advanceClock(milliseconds)
+
+    override fun apply(base: Statement, description: Description?): Statement {
+        return AnimationClockStatement(base)
+    }
+
+    @OptIn(InternalAnimationApi::class)
+    private inner class AnimationClockStatement(private val base: Statement) : Statement() {
+        override fun evaluate() {
+            base.evaluate()
+        }
+    }
+}
+
+actual fun createAnimationClockRule(): AnimationClockTestRule =
+    DesktopAnimationClockTestRule()
\ No newline at end of file
diff --git a/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopAssertions.kt b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopAssertions.kt
new file mode 100644
index 0000000..9b23a2e
--- /dev/null
+++ b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopAssertions.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2020 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.ui.test
+
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.semantics.SemanticsNode
+
+internal actual fun SemanticsNodeInteraction.checkIsDisplayed(): Boolean {
+    TODO()
+}
+
+internal actual fun SemanticsNode.clippedNodeBoundsInWindow(): Rect {
+    TODO()
+}
+
+internal actual fun SemanticsNode.isInScreenBounds(): Boolean {
+    TODO()
+}
\ No newline at end of file
diff --git a/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopComposeTestRule.kt b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopComposeTestRule.kt
new file mode 100644
index 0000000..023bab3
--- /dev/null
+++ b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopComposeTestRule.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2020 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.ui.test
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.EmbeddingContext
+import androidx.compose.runtime.EmbeddingContextFactory
+import androidx.compose.runtime.ExperimentalComposeApi
+import androidx.compose.runtime.Recomposer
+import androidx.compose.runtime.dispatch.DesktopUiDispatcher
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.platform.DesktopOwner
+import androidx.compose.ui.platform.DesktopOwners
+import androidx.compose.ui.platform.setContent
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import kotlinx.coroutines.Dispatchers
+import org.jetbrains.skija.Surface
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import java.awt.Component
+import java.util.LinkedList
+
+actual fun createComposeRule(
+    disableTransitions: Boolean,
+    disableBlinkingCursor: Boolean
+): ComposeTestRule {
+    return DesktopComposeTestRule(
+        disableTransitions,
+        disableBlinkingCursor
+    )
+}
+
+class DesktopComposeTestRule(
+    private val disableTransitions: Boolean = false,
+    private val disableBlinkingCursor: Boolean = true
+) : ComposeTestRule, EmbeddingContext {
+
+    companion object {
+        init {
+            initCompose()
+        }
+
+        var current: DesktopComposeTestRule? = null
+    }
+
+    var owners: DesktopOwners? = null
+
+    override val clockTestRule: AnimationClockTestRule = DesktopAnimationClockTestRule()
+
+    override val density: Density
+        get() = TODO()
+
+    override val displaySize: IntSize get() = IntSize(1024, 768)
+
+    val executionQueue = LinkedList<() -> Unit>()
+
+    override fun apply(base: Statement, description: Description?): Statement {
+        current = this
+        return object : Statement() {
+            override fun evaluate() {
+                EmbeddingContextFactory = fun() = this@DesktopComposeTestRule
+                base.evaluate()
+                runExecutionQueue()
+            }
+        }
+    }
+
+    private fun runExecutionQueue() {
+        while (executionQueue.isNotEmpty()) {
+            executionQueue.removeFirst()()
+        }
+    }
+
+    @OptIn(ExperimentalComposeApi::class)
+    private fun isIdle() =
+        !DesktopUiDispatcher.Dispatcher.hasPendingChanges() &&
+                !Snapshot.current.hasPendingChanges() &&
+                !Recomposer.current().hasPendingChanges()
+
+    internal fun waitForIdle() {
+        while (!isIdle()) {
+            DesktopUiDispatcher.Dispatcher.runAllCallbacks()
+            runExecutionQueue()
+            Thread.sleep(10)
+        }
+    }
+
+    override fun setContent(composable: @Composable () -> Unit) {
+        val surface = Surface.makeRasterN32Premul(displaySize.width, displaySize.height)
+        val canvas = surface.canvas
+        val component = object : Component() {}
+        val owners = DesktopOwners(component = component, redraw = {}).also {
+            owners = it
+        }
+        val owner = DesktopOwner(owners)
+        owner.setContent(composable)
+        owner.setSize(displaySize.width, displaySize.height)
+        owner.draw(canvas)
+    }
+
+    override fun isMainThread() = true
+    override fun mainThreadCompositionContext() = Dispatchers.Default
+}
\ No newline at end of file
diff --git a/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopInputDispatcher.kt b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopInputDispatcher.kt
new file mode 100644
index 0000000..f181b8b
--- /dev/null
+++ b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopInputDispatcher.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2020 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.ui.test
+
+import androidx.compose.ui.input.pointer.PointerId
+import androidx.compose.ui.input.pointer.PointerInputData
+import androidx.compose.ui.input.pointer.PointerInputEvent
+import androidx.compose.ui.input.pointer.PointerInputEventData
+import androidx.compose.ui.node.Owner
+import androidx.compose.ui.platform.DesktopOwner
+import androidx.compose.ui.unit.Uptime
+
+internal actual fun InputDispatcher(owner: Owner): InputDispatcher {
+    return DesktopInputDispatcher(owner as DesktopOwner).apply {
+        BaseInputDispatcher.states.remove(owner)?.also {
+            // TODO(b/157653315): Move restore state to constructor
+            if (it.partialGesture != null) {
+                nextDownTime = it.nextDownTime
+                partialGesture = it.partialGesture
+            }
+        }
+    }
+}
+
+internal class DesktopInputDispatcher(val owner: DesktopOwner) : BaseInputDispatcher() {
+    companion object {
+        var gesturePointerId = 0L
+    }
+
+    override val now: Long get() = System.nanoTime() / 1_000_000
+
+    override fun saveState(owner: Owner?) {
+        if (owner != null) {
+            states[owner] = InputDispatcherState(nextDownTime, gestureLateness, partialGesture)
+        }
+    }
+
+    private var isMousePressed = false
+
+    private var batchedEvents = mutableListOf<PointerInputEvent>()
+
+    override fun PartialGesture.enqueueDown(pointerId: Int) {
+        isMousePressed = true
+        enqueueEvent(pointerInputEvent(isMousePressed))
+    }
+    override fun PartialGesture.enqueueMove() {
+        enqueueEvent(pointerInputEvent(isMousePressed))
+    }
+
+    override fun PartialGesture.enqueueUp(pointerId: Int) {
+        isMousePressed = false
+        enqueueEvent(pointerInputEvent(isMousePressed))
+        gesturePointerId += 1
+    }
+
+    override fun PartialGesture.enqueueCancel() {
+        println("PartialGesture.sendCancel")
+    }
+
+    private fun enqueueEvent(event: PointerInputEvent) {
+        batchedEvents.add(event)
+    }
+
+    private fun PartialGesture.pointerInputEvent(down: Boolean): PointerInputEvent {
+        val time = Uptime(lastEventTime * 1_000_000)
+        val offset = lastPositions.valueAt(0)
+        val event = PointerInputEvent(
+            time,
+            listOf(
+                PointerInputEventData(
+                    PointerId(gesturePointerId),
+                    PointerInputData(
+                        time,
+                        offset,
+                        down
+                    )
+                )
+            )
+        )
+        return event
+    }
+
+    override fun sendAllSynchronous() {
+        val copy = batchedEvents.toList()
+        batchedEvents.clear()
+        copy.forEach {
+            if (InputDispatcher.dispatchInRealTime) {
+                val delayMs = (it.uptime.nanoseconds / 1_000_000) - now
+                if (delayMs > 0) {
+                    Thread.sleep(delayMs)
+                }
+            }
+            owner.processPointerInput(it)
+        }
+    }
+
+    override fun dispose() {
+        batchedEvents.clear()
+    }
+}
\ No newline at end of file
diff --git a/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopOutput.kt b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopOutput.kt
new file mode 100644
index 0000000..6c3edbb
--- /dev/null
+++ b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopOutput.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2020 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.ui.test
+
+internal actual fun printToLog(tag: String, message: String) {
+    TODO()
+}
diff --git a/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopSemanticsNodeInteractions.kt b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopSemanticsNodeInteractions.kt
new file mode 100644
index 0000000..57bbffc
--- /dev/null
+++ b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopSemanticsNodeInteractions.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020 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.ui.test
+
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.semantics.getAllSemanticsNodes
+
+internal actual fun getAllSemanticsNodes(mergingEnabled: Boolean): List<SemanticsNode> {
+    return DesktopComposeTestRule.current!!.owners!!.list.flatMap {
+        it.semanticsOwner.getAllSemanticsNodes(mergingEnabled) }
+}
\ No newline at end of file
diff --git a/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopSynchronization.kt b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopSynchronization.kt
new file mode 100644
index 0000000..6f2c327
--- /dev/null
+++ b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopSynchronization.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2020 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.ui.test
+
+internal actual fun <T> actualRunOnUiThread(action: () -> T): T {
+    val result = action()
+    actualWaitForIdle()
+    return result
+}
+
+internal actual fun actualWaitForIdle() {
+    DesktopComposeTestRule.current!!.waitForIdle()
+}
\ No newline at end of file
diff --git a/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopTextActions.kt b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopTextActions.kt
new file mode 100644
index 0000000..5461ce6
--- /dev/null
+++ b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/DesktopTextActions.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2020 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.ui.test
+
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.text.input.EditOperation
+import androidx.compose.ui.text.input.ImeAction
+
+internal actual fun SemanticsNodeInteraction.actualPerformImeAction(
+    node: SemanticsNode,
+    actionSpecified: ImeAction
+) {
+    TODO()
+}
+
+internal actual fun SemanticsNodeInteraction.actualSendTextInputCommand(
+    node: SemanticsNode,
+    command: List<EditOperation>
+) {
+    TODO()
+}
diff --git a/compose/desktop/desktop/src/jvmMain/kotlin/androidx/compose/desktop/test/SkijaTest.kt b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/SkijaTest.kt
similarity index 87%
rename from compose/desktop/desktop/src/jvmMain/kotlin/androidx/compose/desktop/test/SkijaTest.kt
rename to ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/SkijaTest.kt
index 746f833..eac5449 100644
--- a/compose/desktop/desktop/src/jvmMain/kotlin/androidx/compose/desktop/test/SkijaTest.kt
+++ b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/SkijaTest.kt
@@ -1,14 +1,30 @@
-package androidx.compose.desktop.test
+/*
+ * Copyright 2020 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.ui.test
 
 import androidx.compose.runtime.EmbeddingContext
 import androidx.compose.runtime.EmbeddingContextFactory
 import kotlinx.coroutines.Dispatchers
 import org.jetbrains.skija.Surface
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
+import org.jetbrains.skiko.Library
 import java.io.File
 import java.security.MessageDigest
+import org.junit.rules.TestRule
+import org.junit.runners.model.Statement
+import org.junit.runner.Description
 import java.util.LinkedList
 
 // TODO: replace with androidx.test.screenshot.proto.ScreenshotResultProto after MPP
@@ -150,11 +166,28 @@
     return ScreenshotTestRule(GoldenConfig(fsGoldenPath, repoGoldenPath, modulePath))
 }
 
+fun initCompose() {
+    ComposeInit
+}
+
+private object ComposeInit {
+    init {
+        Library.load("/", "skiko")
+        System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false")
+    }
+}
+
 class ScreenshotTestRule internal constructor(val config: GoldenConfig) : TestRule,
     EmbeddingContext {
     private lateinit var testIdentifier: String
     private lateinit var album: SkijaTestAlbum
 
+    companion object {
+        init {
+            initCompose()
+        }
+    }
+
     val executionQueue = LinkedList<() -> Unit>()
 
     override fun apply(base: Statement, description: Description?): Statement {
diff --git a/compose/desktop/desktop/src/jvmMain/kotlin/androidx/compose/desktop/test/TestSkiaWindow.kt b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/TestSkiaWindow.kt
similarity index 81%
rename from compose/desktop/desktop/src/jvmMain/kotlin/androidx/compose/desktop/test/TestSkiaWindow.kt
rename to ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/TestSkiaWindow.kt
index fddf599..6e1680b 100644
--- a/compose/desktop/desktop/src/jvmMain/kotlin/androidx/compose/desktop/test/TestSkiaWindow.kt
+++ b/ui/ui-test/src/desktopMain/kotlin/androidx/ui/test/TestSkiaWindow.kt
@@ -13,15 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.compose.desktop.test
 
-import androidx.compose.desktop.initCompose
+package androidx.ui.test
+
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.platform.DesktopOwner
 import androidx.compose.ui.platform.DesktopOwners
 import androidx.compose.ui.platform.setContent
 import org.jetbrains.skija.Canvas
 import org.jetbrains.skija.Surface
+import org.jetbrains.skiko.Library
 import java.awt.Component
 
 // TODO(demin): replace by androidx.compose.ui.test.TestComposeWindow when it will be
@@ -39,7 +40,10 @@
 
     companion object {
         init {
-            initCompose()
+            Library.load("/", "skiko")
+            // Until https://github.com/Kotlin/kotlinx.coroutines/issues/2039 is resolved
+            // we have to set this property manually for coroutines to work.
+            System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false")
         }
     }
 
diff --git a/ui/ui-test/src/jvmMain/kotlin/androidx/ui/test/AnimationClockTestRule.kt b/ui/ui-test/src/jvmMain/kotlin/androidx/ui/test/AnimationClockTestRule.kt
new file mode 100644
index 0000000..1ae658b
--- /dev/null
+++ b/ui/ui-test/src/jvmMain/kotlin/androidx/ui/test/AnimationClockTestRule.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2020 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.ui.test
+
+import org.junit.rules.TestRule
+
+interface AnimationClockTestRule : TestRule {
+    /**
+     * The ambient animation clock that is provided at the root of the composition tree.
+     */
+    val clock: TestAnimationClock
+
+    /**
+     * Convenience property for calling [`clock.isPaused`][TestAnimationClock.isPaused]
+     */
+    val isPaused: Boolean
+
+    /**
+     * Convenience method for calling [`clock.pauseClock()`][TestAnimationClock.pauseClock]
+     */
+    fun pauseClock()
+
+    /**
+     * Convenience method for calling [`clock.resumeClock()`][TestAnimationClock.resumeClock]
+     */
+    fun resumeClock() = clock.resumeClock()
+
+    /**
+     * Convenience method for calling [`clock.advanceClock()`][TestAnimationClock.advanceClock]
+     */
+    fun advanceClock(milliseconds: Long) = clock.advanceClock(milliseconds)
+}
+
+expect fun createAnimationClockRule(): AnimationClockTestRule
\ No newline at end of file
diff --git a/ui/ui-test/src/jvmMain/kotlin/androidx/ui/test/BaseInputDispatcher.kt b/ui/ui-test/src/jvmMain/kotlin/androidx/ui/test/BaseInputDispatcher.kt
new file mode 100644
index 0000000..81a2e69
--- /dev/null
+++ b/ui/ui-test/src/jvmMain/kotlin/androidx/ui/test/BaseInputDispatcher.kt
@@ -0,0 +1,341 @@
+/*
+ * Copyright 2020 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.ui.test
+
+import androidx.collection.SparseArrayCompat
+import java.util.WeakHashMap
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.lerp
+import androidx.compose.ui.node.Owner
+import androidx.compose.ui.unit.Duration
+import androidx.compose.ui.unit.inMilliseconds
+import androidx.compose.ui.unit.milliseconds
+import kotlin.math.max
+import kotlin.math.roundToInt
+
+internal abstract class BaseInputDispatcher : InputDispatcher {
+    companion object {
+        /**
+         * Indicates that [nextDownTime] is not set
+         */
+        private const val DownTimeNotSet = -1L
+
+        /**
+         * Stores the [InputDispatcherState] of each [Owner]. The state will be restored in an
+         * [InputDispatcher] when it is created for an owner that has a state stored.
+         */
+        internal val states = WeakHashMap<Owner, InputDispatcherState>()
+    }
+
+    internal var nextDownTime = DownTimeNotSet
+
+    /**
+     * The time difference between enqueuing the first event of the gesture and dispatching it.
+     *
+     * When the first event of a gesture is enqueued, its eventTime is fixed to the current time.
+     * However, there is inevitably some time between enqueuing and dispatching of that event.
+     * This means that event is going to be "late" by [gestureLateness] milliseconds when it is
+     * dispatched. Because the dispatcher wants to align events with the current time, it will
+     * dispatch all events that are late immediately and without delay, until it has reached an
+     * event whose eventTime is in the future (i.e. an event that is "early").
+     *
+     * The [gestureLateness] will be used to offset all events, effectively aligning the first
+     * event with the dispatch time.
+     */
+    internal var gestureLateness: Long? = null
+
+    internal var partialGesture: PartialGesture? = null
+
+    /**
+     * Indicates if a gesture is in progress or not. A gesture is in progress if at least one
+     * finger is (still) touching the screen.
+     */
+    val isGestureInProgress: Boolean
+        get() = partialGesture != null
+
+    abstract override val now: Long
+
+    /**
+     * Generates the downTime of the next gesture with the given [duration]. The gesture's
+     * [duration] is necessary to facilitate chaining of gestures: if another gesture is made
+     * after the next one, it will start exactly [duration] after the start of the next gesture.
+     * Always use this method to determine the downTime of the [down event][enqueueDown] of a
+     * gesture.
+     *
+     * If the duration is unknown when calling this method, use a duration of zero and update
+     * with [moveNextDownTime] when the duration is known, or use [moveNextDownTime]
+     * incrementally if the gesture unfolds gradually.
+     */
+    private fun generateDownTime(duration: Duration): Long {
+        val downTime = if (nextDownTime == DownTimeNotSet) {
+            now
+        } else {
+            nextDownTime
+        }
+        nextDownTime = downTime + duration.inMilliseconds()
+        return downTime
+    }
+
+    /**
+     * Moves the start time of the next gesture ahead by the given [duration]. Does not affect
+     * any event time from the current gesture. Use this when the expected duration passed to
+     * [generateDownTime] has changed.
+     */
+    private fun moveNextDownTime(duration: Duration) {
+        generateDownTime(duration)
+    }
+
+    /**
+     * Increases the eventTime with the given [time]. Also pushes the downTime for the next
+     * chained gesture by the same amount to facilitate chaining.
+     */
+    private fun PartialGesture.increaseEventTime(time: Long = InputDispatcher.eventPeriod) {
+        moveNextDownTime(time.milliseconds)
+        lastEventTime += time
+    }
+
+    override fun enqueueDelay(duration: Duration) {
+        require(duration >= Duration.Zero) {
+            "duration of a delay can only be positive, not $duration"
+        }
+        moveNextDownTime(duration)
+    }
+
+    override fun enqueueClick(position: Offset) {
+        enqueueDown(0, position)
+        enqueueMove()
+        enqueueUp(0)
+    }
+
+    override fun enqueueSwipe(start: Offset, end: Offset, duration: Duration) {
+        val durationFloat = duration.inMilliseconds().toFloat()
+        enqueueSwipe(
+            curve = { lerp(start, end, it / durationFloat) },
+            duration = duration
+        )
+    }
+
+    override fun enqueueSwipe(
+        curve: (Long) -> Offset,
+        duration: Duration,
+        keyTimes: List<Long>
+    ) {
+        enqueueSwipes(listOf(curve), duration, keyTimes)
+    }
+
+    override fun enqueueSwipes(
+        curves: List<(Long) -> Offset>,
+        duration: Duration,
+        keyTimes: List<Long>
+    ) {
+        val startTime = 0L
+        val endTime = duration.inMilliseconds()
+        // Validate input
+        require(duration >= 1.milliseconds) {
+            "duration must be at least 1 millisecond, not $duration"
+        }
+        val validRange = startTime..endTime
+        require(keyTimes.all { it in validRange }) {
+            "keyTimes contains timestamps out of range [$startTime..$endTime]: $keyTimes"
+        }
+        require(keyTimes.asSequence().zipWithNext { a, b -> a <= b }.all { it }) {
+            "keyTimes must be sorted: $keyTimes"
+        }
+        // Send down events
+        curves.forEachIndexed { i, curve ->
+            enqueueDown(i, curve(startTime))
+        }
+        // Send move events between each consecutive pair in [t0, ..keyTimes, tN]
+        var currTime = startTime
+        var key = 0
+        while (currTime < endTime) {
+            // advance key
+            while (key < keyTimes.size && keyTimes[key] <= currTime) {
+                key++
+            }
+            // send events between t and next keyTime
+            val tNext = if (key < keyTimes.size) keyTimes[key] else endTime
+            sendPartialSwipes(curves, currTime, tNext)
+            currTime = tNext
+        }
+        // And end with up events
+        repeat(curves.size) {
+            enqueueUp(it)
+        }
+    }
+
+    /**
+     * Generates move events between `f([t0])` and `f([tN])` during the time window `(downTime +
+     * t0, downTime + tN]`, using [fs] to sample the coordinate of each event. The number of
+     * events sent (#numEvents) is such that the time between each event is as close to
+     * [InputDispatcher.eventPeriod] as possible, but at least 1. The first event is sent at time
+     * `downTime + (tN - t0) / #numEvents`, the last event is sent at time tN.
+     *
+     * @param fs The functions that define the coordinates of the respective gestures over time
+     * @param t0 The start time of this segment of the swipe, in milliseconds relative to downTime
+     * @param tN The end time of this segment of the swipe, in milliseconds relative to downTime
+     */
+    private fun sendPartialSwipes(
+        fs: List<(Long) -> Offset>,
+        t0: Long,
+        tN: Long
+    ) {
+        var step = 0
+        // How many steps will we take between t0 and tN? At least 1, and a number that will
+        // bring as as close to eventPeriod as possible
+        val steps = max(1, ((tN - t0) / InputDispatcher.eventPeriod.toFloat()).roundToInt())
+        var tPrev = t0
+        while (step++ < steps) {
+            val progress = step / steps.toFloat()
+            val t = androidx.compose.ui.util.lerp(t0, tN, progress)
+            fs.forEachIndexed { i, f ->
+                movePointer(i, f(t))
+            }
+            enqueueMove(t - tPrev)
+            tPrev = t
+        }
+    }
+
+    override fun getCurrentPosition(pointerId: Int): Offset? {
+        return partialGesture?.lastPositions?.get(pointerId)
+    }
+
+    override fun enqueueDown(pointerId: Int, position: Offset) {
+        var gesture = partialGesture
+        // Check if this pointer is not already down
+        require(gesture == null || !gesture.lastPositions.containsKey(pointerId)) {
+            "Cannot send DOWN event, a gesture is already in progress for pointer $pointerId"
+        }
+        gesture?.flushPointerUpdates()
+        // Start a new gesture, or add the pointerId to the existing gesture
+        if (gesture == null) {
+            gesture = PartialGesture(generateDownTime(0.milliseconds), position, pointerId)
+            partialGesture = gesture
+        } else {
+            gesture.lastPositions.put(pointerId, position)
+        }
+        // Send the DOWN event
+        gesture.enqueueDown(pointerId)
+    }
+
+    override fun movePointer(pointerId: Int, position: Offset) {
+        val gesture = partialGesture
+        // Check if this pointer is in the gesture
+        check(gesture != null) {
+            "Cannot move pointers, no gesture is in progress"
+        }
+        require(gesture.lastPositions.containsKey(pointerId)) {
+            "Cannot move pointer $pointerId, it is not active in the current gesture"
+        }
+        gesture.lastPositions.put(pointerId, position)
+        gesture.hasPointerUpdates = true
+    }
+
+    override fun enqueueMove(delay: Long) {
+        val gesture = checkNotNull(partialGesture) {
+            "Cannot send MOVE event, no gesture is in progress"
+        }
+        require(delay >= 0) {
+            "Cannot send MOVE event with a delay of $delay ms"
+        }
+        gesture.increaseEventTime(delay)
+        gesture.enqueueMove()
+        gesture.hasPointerUpdates = false
+    }
+
+    override fun enqueueUp(pointerId: Int, delay: Long) {
+        val gesture = partialGesture
+        // Check if this pointer is in the gesture
+        check(gesture != null) {
+            "Cannot send UP event, no gesture is in progress"
+        }
+        require(gesture.lastPositions.containsKey(pointerId)) {
+            "Cannot send UP event for pointer $pointerId, it is not active in the current gesture"
+        }
+        require(delay >= 0) {
+            "Cannot send UP event with a delay of $delay ms"
+        }
+        gesture.flushPointerUpdates()
+        gesture.increaseEventTime(delay)
+        // First send the UP event
+        gesture.enqueueUp(pointerId)
+        // Then remove the pointer, and end the gesture if no pointers are left
+        gesture.lastPositions.remove(pointerId)
+        if (gesture.lastPositions.isEmpty) {
+            partialGesture = null
+        }
+    }
+
+    override fun enqueueCancel(delay: Long) {
+        val gesture = checkNotNull(partialGesture) {
+            "Cannot send CANCEL event, no gesture is in progress"
+        }
+        require(delay >= 0) {
+            "Cannot send CANCEL event with a delay of $delay ms"
+        }
+        gesture.increaseEventTime(delay)
+        gesture.enqueueCancel()
+        partialGesture = null
+    }
+
+    /**
+     * Generates a MOVE event with all pointer locations, if any of the pointers has been moved by
+     * [movePointer] since the last MOVE event.
+     */
+    private fun PartialGesture.flushPointerUpdates() {
+        if (hasPointerUpdates) {
+            enqueueMove(InputDispatcher.eventPeriod)
+        }
+    }
+
+    protected abstract fun PartialGesture.enqueueDown(pointerId: Int)
+
+    protected abstract fun PartialGesture.enqueueMove()
+
+    protected abstract fun PartialGesture.enqueueUp(pointerId: Int)
+
+    protected abstract fun PartialGesture.enqueueCancel()
+}
+
+/**
+ * The state of an [InputDispatcher], saved when the [GestureScope] is disposed and restored when
+ * the [GestureScope] is recreated.
+ *
+ * @param nextDownTime The downTime of the start of the next gesture, when chaining gestures.
+ * This property will only be restored if an incomplete gesture was in progress when the state of
+ * the [InputDispatcher] was saved.
+ * @param gestureLateness The time difference in milliseconds between enqueuing the first event
+ * of the gesture and dispatching it. Depending on the implementation of [InputDispatcher], this
+ * may or may not be used.
+ * @param partialGesture The state of an incomplete gesture. If no gesture was in progress
+ * when the state of the [InputDispatcher] was saved, this will be `null`.
+ */
+internal data class InputDispatcherState(
+    val nextDownTime: Long,
+    var gestureLateness: Long?,
+    val partialGesture: PartialGesture?
+)
+
+internal class PartialGesture constructor(
+    val downTime: Long,
+    startPosition: Offset,
+    pointerId: Int
+) {
+    var lastEventTime: Long = downTime
+    var hasPointerUpdates: Boolean = false
+    val lastPositions = SparseArrayCompat<Offset>().apply { put(pointerId, startPosition) }
+}
diff --git a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/ComposeTestRule.kt b/ui/ui-test/src/jvmMain/kotlin/androidx/ui/test/ComposeTestRule.kt
similarity index 86%
rename from ui/ui-test/src/androidMain/kotlin/androidx/ui/test/ComposeTestRule.kt
rename to ui/ui-test/src/jvmMain/kotlin/androidx/ui/test/ComposeTestRule.kt
index 03b81d9..9f007b5 100644
--- a/ui/ui-test/src/androidMain/kotlin/androidx/ui/test/ComposeTestRule.kt
+++ b/ui/ui-test/src/jvmMain/kotlin/androidx/ui/test/ComposeTestRule.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2020 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.
@@ -16,11 +16,9 @@
 
 package androidx.ui.test
 
-import android.util.DisplayMetrics
-import androidx.activity.ComponentActivity
 import androidx.compose.runtime.Composable
-import androidx.ui.test.android.createAndroidComposeRule
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
 import org.junit.rules.TestRule
 
 /**
@@ -53,7 +51,7 @@
     fun setContent(composable: @Composable () -> Unit)
 
     // TODO(pavlis): Provide better abstraction for host side reusability
-    val displayMetrics: DisplayMetrics get
+    val displaySize: IntSize get
 }
 
 /**
@@ -66,10 +64,7 @@
  * reference to this activity into the manifest file of the corresponding tests (usually in
  * androidTest/AndroidManifest.xml).
  */
-fun createComposeRule(
+expect fun createComposeRule(
     disableTransitions: Boolean = false,
     disableBlinkingCursor: Boolean = true
-): ComposeTestRule = createAndroidComposeRule<ComponentActivity>(
-    disableTransitions,
-    disableBlinkingCursor
-)
+): ComposeTestRule