Skip to content

Commit

Permalink
Refactor ComposeScene (#908)
Browse files Browse the repository at this point in the history
## Proposed Changes

It's the evolution of #891

Compose scene:
- Deprecate the existing API and introduce an internal interface instead
- Remove key listeners from `setContent`
- `constraints` -> `size`
- Add `lastKnownPointerPosition` to public interface 
- `CombinedComposeScene` as a compatible version of scene
- Remove recently added `semanticsOwner`
- `ComposeScene.Pointer` -> `ComposeScenePointer`

Other changes:
- Rename `SkiaBasedOwner` to `RootNodeOwner`. Also, it uses aggregation
instead of implementing a few interfaces.
- `SkikoComposeUiTest` overrides `PlatformContext.RootForTestListener`
now. It solves TODOs about `WindowInfo` in tests.
- `SkiaRootForTest` is renamed to `PlatformRootForTest`
- `onRootCreatedCallback`/`onRootDisposedCallback` is now part of
`PlatformContext`
- `PlatformRootForTest` exposes real `visibleBounds` instead of just
window size information.
- `PlatformRootForTest` exposes send pointer input methods instead of
`scene` reference.
- `InternalComposeUiApi` is opted in inside the same module.
- `Platform` -> `PlatfromContext`
- `dialogScrimBlendMode` -> `isWindowTransparent`
- `AccessibilityController`: Remove interface from `skikoMain` source
set
- `AccessibilityController`: `syncLoop` triggered right from
desktop-only part
- Replaced `Platform.accessibilityController(SemanticsOwner)` to
`semanticsOwnerListener`
- Fixes an invalid test case: out of screen Popup cannot receive input
now
- Removed unnecessary expect/actual `createSkiaLayer`
- Moves "Deprecated" `DefaultViewConfiguration` to test where there is a
single usage of it.

## API Changes

See `compose/ui/ui/api/desktop/ui.api` file

## Testing

Test: run existing tests.
  • Loading branch information
MatkovIvan committed Nov 24, 2023
1 parent 280891a commit 602e66d
Show file tree
Hide file tree
Showing 72 changed files with 3,826 additions and 2,096 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
Expand Down Expand Up @@ -1216,7 +1217,7 @@ class ScrollbarTest {
}


@OptIn(InternalTestApi::class)
@OptIn(InternalTestApi::class, InternalComposeUiApi::class)
private fun ComposeTestRule.performMouseScroll(x: Int, y: Int, delta: Float) {
(this as DesktopComposeTestRule).scene.sendPointerEvent(
PointerEventType.Scroll,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
Expand All @@ -49,6 +50,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@OptIn(InternalComposeUiApi::class)
@ExperimentalTestApi
@RunWith(JUnit4::class)
class DesktopScrollableTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.ui.ComposeScene
import androidx.compose.ui.ImageComposeScene
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerButton
Expand All @@ -43,7 +44,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest

@ExperimentalCoroutinesApi
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, InternalComposeUiApi::class)
class OnClickTest {

private fun testClick(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ import androidx.compose.foundation.PointerMatcher
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.ui.ImageComposeScene
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.input.pointer.PointerButtons
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.platform.DefaultViewConfiguration
import androidx.compose.ui.platform.PlatformContext
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.use
Expand All @@ -38,7 +40,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest

@Suppress("DEPRECATION")
@OptIn(ExperimentalFoundationApi::class)
class DragGestureTest {

Expand Down Expand Up @@ -462,3 +463,9 @@ class DragGestureTest {
}
}
}

@OptIn(InternalComposeUiApi::class)
private class DefaultViewConfiguration(private val density: Density) : ViewConfiguration by PlatformContext.Empty.viewConfiguration {
override val touchSlop: Float
get() = with(density) { PlatformContext.Empty.viewConfiguration.touchSlop.dp.toPx() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
package androidx.compose.ui.test.junit4

import androidx.compose.runtime.Composable
import androidx.compose.ui.ComposeScene
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.scene.ComposeScene
import androidx.compose.ui.test.DesktopComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.IdlingResource
Expand Down Expand Up @@ -57,6 +58,7 @@ class DesktopComposeTestRule private constructor(
effectContext: CoroutineContext = EmptyCoroutineContext
) : this(DesktopComposeUiTest(effectContext = effectContext))

@InternalComposeUiApi
var scene: ComposeScene
get() = composeTest.scene
set(value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,20 @@
package androidx.compose.ui.test

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.ComposeScene
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asComposeCanvas
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.platform.InfiniteAnimationPolicy
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.platform.PlatformContext
import androidx.compose.ui.platform.WindowInfo
import androidx.compose.ui.scene.MultiLayerComposeScene
import androidx.compose.ui.scene.ComposeScene
import androidx.compose.ui.scene.ComposeSceneContext
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.junit4.ComposeRootRegistry
import androidx.compose.ui.test.junit4.MainTestClockImpl
Expand All @@ -41,7 +43,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.PlatformTextInputService
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import kotlin.coroutines.CoroutineContext
Expand Down Expand Up @@ -90,7 +91,7 @@ private const val IDLING_RESOURCES_CHECK_INTERVAL_MS = 20L
* `LaunchedEffect`s and `rememberCoroutineScope` will be derived from this context.
*/
@ExperimentalTestApi
@OptIn(ExperimentalCoroutinesApi::class, InternalTestApi::class)
@OptIn(ExperimentalCoroutinesApi::class, InternalTestApi::class, InternalComposeUiApi::class)
class SkikoComposeUiTest(
width: Int = 1024,
height: Int = 768,
Expand All @@ -111,31 +112,6 @@ class SkikoComposeUiTest(
var onImeActionPerformed: (ImeAction) -> Unit,
)

private val textInputService = object : PlatformTextInputService {
var session: Session? = null

override fun startInput(
value: TextFieldValue,
imeOptions: ImeOptions,
onEditCommand: (List<EditCommand>) -> Unit,
onImeActionPerformed: (ImeAction) -> Unit
) {
session = Session(
imeOptions = imeOptions,
onEditCommand = onEditCommand,
onImeActionPerformed = onImeActionPerformed
)
}

override fun stopInput() {
session = null
}

override fun showSoftwareKeyboard() = Unit
override fun hideSoftwareKeyboard() = Unit
override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) = Unit
}

private val composeRootRegistry = ComposeRootRegistry()

private val coroutineDispatcher = UnconfinedTestDispatcher()
Expand All @@ -157,7 +133,9 @@ class SkikoComposeUiTest(
private val coroutineContext =
coroutineDispatcher + uncaughtExceptionHandler + infiniteAnimationPolicy
private val surface = Surface.makeRasterN32Premul(width, height)
private val size = IntSize(width, height)

@InternalComposeUiApi
lateinit var scene: ComposeScene
internal set

Expand Down Expand Up @@ -188,7 +166,7 @@ class SkikoComposeUiTest(

private fun render(timeMillis: Long) {
scene.render(
surface.canvas,
surface.canvas.asComposeCanvas(),
timeMillis * NanoSecondsPerMilliSecond
)
}
Expand All @@ -200,14 +178,13 @@ class SkikoComposeUiTest(
}
}

private fun createUi() = ComposeScene(
textInputService = textInputService,
private fun createUi() = MultiLayerComposeScene(
density = density,
coroutineContext = coroutineContext,
composeSceneContext = TestComposeSceneContext(),
invalidate = { }
).apply {
constraints = Constraints(maxWidth = surface.width, maxHeight = surface.height)
// TODO: Initialize WindowInfo once public way is available
).also {
it.size = size
}

private fun shouldPumpTime(): Boolean {
Expand Down Expand Up @@ -303,22 +280,11 @@ class SkikoComposeUiTest(

@OptIn(ExperimentalComposeUiApi::class)
override fun setContent(composable: @Composable () -> Unit) {
// TODO: Remove override once it's properly initialized
val overriddenComposable: @Composable () -> Unit = {
val windowInfo = object : WindowInfo by LocalWindowInfo.current {
override val containerSize: IntSize
get() = IntSize(surface.width, surface.height)
}
CompositionLocalProvider(
LocalWindowInfo provides windowInfo,
content = composable
)
}
if (isOnUiThread()) {
scene.setContent(content = overriddenComposable)
scene.setContent(content = composable)
} else {
runOnUiThread {
scene.setContent(content = overriddenComposable)
scene.setContent(content = composable)
}

// Only wait for idleness if not on the UI thread. If we are on the UI thread, the
Expand Down Expand Up @@ -375,6 +341,54 @@ class SkikoComposeUiTest(
override fun captureToImage(semanticsNode: SemanticsNode): ImageBitmap =
this@SkikoComposeUiTest.captureToImage(semanticsNode)
}

private inner class TestWindowInfo : WindowInfo {
override val isWindowFocused: Boolean
get() = true

@ExperimentalComposeUiApi
override val containerSize: IntSize
get() = size
}

private inner class TestTextInputService : PlatformTextInputService {
var session: Session? = null

override fun startInput(
value: TextFieldValue,
imeOptions: ImeOptions,
onEditCommand: (List<EditCommand>) -> Unit,
onImeActionPerformed: (ImeAction) -> Unit
) {
session = Session(
imeOptions = imeOptions,
onEditCommand = onEditCommand,
onImeActionPerformed = onImeActionPerformed
)
}

override fun stopInput() {
session = null
}

override fun showSoftwareKeyboard() = Unit
override fun hideSoftwareKeyboard() = Unit
override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) = Unit
}

private inner class TestContext : PlatformContext by PlatformContext.Empty {
override val windowInfo: WindowInfo =
TestWindowInfo()
override val textInputService: PlatformTextInputService =
TestTextInputService()

override val rootForTestListener: PlatformContext.RootForTestListener?
get() = composeRootRegistry
}

private inner class TestComposeSceneContext : ComposeSceneContext {
override val platformContext = TestContext()
}
}

@ExperimentalTestApi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,65 +17,63 @@
package androidx.compose.ui.test.junit4

import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.platform.SkiaRootForTest
import androidx.compose.ui.platform.PlatformContext
import androidx.compose.ui.platform.PlatformRootForTest

/**
* Registry where all views implementing [SkiaRootForTest] should be registered while they
* Registry where all views implementing [PlatformRootForTest] should be registered while they
* are attached to the window. This registry is used by the testing library to query the roots'
* state.
*/
@OptIn(InternalComposeUiApi::class)
internal class ComposeRootRegistry {
internal class ComposeRootRegistry : PlatformContext.RootForTestListener {
private val lock = Any()
private val roots = mutableSetOf<SkiaRootForTest>()
private val roots = mutableSetOf<PlatformRootForTest>()

/**
* Returns if the registry is setup to receive registrations from [SkiaRootForTest]s
* Returns if the registry is setup to receive registrations from [PlatformRootForTest]s
*/
val isSetUp: Boolean
get() = SkiaRootForTest.onRootCreatedCallback == ::onRootCreated
private var isTracking = false

/**
* Sets up this registry to be notified of any [SkiaRootForTest] created
* Sets up this registry to be notified of any [PlatformRootForTest] created
*/
private fun setupRegistry() {
SkiaRootForTest.onRootCreatedCallback = ::onRootCreated
SkiaRootForTest.onRootDisposedCallback = ::onRootDisposed
isTracking = true
}

/**
* Cleans up the changes made by [setupRegistry]. Call this after your test has run.
*/
private fun tearDownRegistry() {
// Stop accepting new roots
SkiaRootForTest.onRootCreatedCallback = null
SkiaRootForTest.onRootDisposedCallback = null
isTracking = false
synchronized(lock) {
// Clear all references
roots.clear()
}
}

private fun onRootCreated(root: SkiaRootForTest) {
override fun onRootForTestCreated(root: PlatformRootForTest) {
synchronized(lock) {
if (isSetUp) {
if (isTracking) {
roots.add(root)
}
}
}

private fun onRootDisposed(root: SkiaRootForTest) {
override fun onRootForTestDisposed(root: PlatformRootForTest) {
synchronized(lock) {
if (isSetUp) {
if (isTracking) {
roots.remove(root)
}
}
}

/**
* Returns a copy of the set of all registered [SkiaRootForTest]s that can be interacted with.
* Returns a copy of the set of all registered [PlatformRootForTest]s that can be interacted with.
*/
fun getComposeRoots(): Set<SkiaRootForTest> {
fun getComposeRoots(): Set<PlatformRootForTest> {
return synchronized(lock) { roots.toSet() }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.nativeKeyCode
import androidx.compose.ui.input.key.nativeKeyLocation
import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.platform.SkiaRootForTest
import java.awt.Component
import java.awt.event.InputEvent

Expand Down

0 comments on commit 602e66d

Please sign in to comment.