Skip to content

Commit

Permalink
Remove TestComposeWindow in favor to ImageComposeScene
Browse files Browse the repository at this point in the history
This CL introduces ImageComposeScene - simplified version of ComposeScene, with drawing only into Image.

It is almost the same as the old TestComposeWindow, but with a different class and method names. TestComposeWindow was deprecated before Compose 1.0, so it is safe to remove.

See examples of usage in ImageComposeSceneTest.

Also, we introduce:
- renderComposeScene - one-liner function to draw Composable content into an image
- ImageComposeScene.use - similar to AutoClosable.use to disposing created resources. We don't inherit AutoCloseable, because we want to move ImageComposeScene into skikoMain. For now, common code doesn't have Closeable, but in the future it can (https://github.com/Kotlin/kotlinx-io/blob/c5bba0114e33c2afd8e4b1ab214aad94ee416d00/core/commonMain/src/kotlinx/io/Closeable.common.kt#L7), so I renamed `dispose` method to `close.

ImageComposeScene still has some flaws, which will be fixed in the future (https://github.com/JetBrains/compose-jb/search?q=ImageComposeScene&type=issues)

Test: ./gradlew jvmTest desktopTest -Pandroidx.compose.multiplatformEnabled=true
Change-Id: I48d2769fd2eaff53e391371aea9ed37964b6afc3
  • Loading branch information
igordmn authored and Oleksandr Karpovich committed Oct 26, 2022
1 parent bd6f01e commit 7713197
Show file tree
Hide file tree
Showing 16 changed files with 690 additions and 595 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import androidx.compose.material.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.TestComposeWindow
import androidx.compose.ui.test.InternalTestApi
import androidx.compose.ui.renderComposeScene
import androidx.compose.ui.test.junit4.DesktopScreenshotTestRule
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
Expand Down Expand Up @@ -79,9 +79,7 @@ class ParagraphTest {
@Ignore
@Test
fun paragraphBasics() {
val window = TestComposeWindow(width = 1024, height = 768)

window.setContent {
val snapshot = renderComposeScene(width = 1024, height = 768) {
ProvideTextStyle(TextStyle(fontFamily = fontFamily)) {
Column(Modifier.fillMaxSize().background(Color.White), Arrangement.SpaceEvenly) {
Text(
Expand Down Expand Up @@ -121,6 +119,6 @@ class ParagraphTest {
}
}
}
screenshotRule.snap(window.surface)
screenshotRule.write(snapshot)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.InternalTestApi
import androidx.compose.ui.test.TouchInjectionScope
Expand Down Expand Up @@ -455,7 +455,8 @@ class ScrollbarTest {
(this as DesktopComposeTestRule).scene.sendPointerEvent(
PointerEventType.Scroll,
Offset(x.toFloat(), y.toFloat()),
scrollDelta = Offset(x = 0f, y = delta)
scrollDelta = Offset(x = 0f, y = delta),
nativeEvent = awtWheelEvent()
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.foundation

import java.awt.Component
import java.awt.event.MouseWheelEvent

private val EventComponent = object : Component() {}

internal fun awtWheelEvent(isScrollByPages: Boolean = false) = MouseWheelEvent(
EventComponent,
MouseWheelEvent.MOUSE_WHEEL,
0,
0,
0,
0,
0,
false,
if (isScrollByPages) {
MouseWheelEvent.WHEEL_BLOCK_SCROLL
} else {
MouseWheelEvent.WHEEL_UNIT_SCROLL
},
1,
0
)
Original file line number Diff line number Diff line change
Expand Up @@ -14,55 +14,47 @@
* limitations under the License.
*/

@file:Suppress("DEPRECATION") // https://github.com/JetBrains/compose-jb/issues/1514

package androidx.compose.foundation.gestures

import androidx.compose.foundation.awtWheelEvent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.ImageComposeScene
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.mouse.MouseScrollEvent
import androidx.compose.ui.input.mouse.MouseScrollOrientation
import androidx.compose.ui.input.mouse.MouseScrollUnit
import androidx.compose.ui.platform.TestComposeWindow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.use
import com.google.common.truth.Truth.assertThat
import org.junit.Ignore
import kotlin.math.sqrt
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import kotlin.math.sqrt

// TODO(demin): convert to ComposeScene instead of TestComposeWindow,
// after that we won't need `window.render`
@OptIn(ExperimentalComposeUiApi::class)
@RunWith(JUnit4::class)
@Ignore // TODO(b/217238066) remove after migration to ImageComposeScene (it will be upstreamed from Compose MPP 1.0.0)
class DesktopScrollableTest {
private val density = 2f

private fun window() = TestComposeWindow(
width = 100,
height = 100,
density = Density(density)
)

private fun scrollLineLinux(bounds: Dp) = sqrt(bounds.value * density)
private fun scrollLineWindows(bounds: Dp) = bounds.value * density / 20f
private fun scrollLineMacOs() = density * 10f
private fun scrollPage(bounds: Dp) = bounds.value * density

@Test
fun `linux, scroll vertical`() {
val window = window()
fun `linux, scroll vertical`() = ImageComposeScene(
width = 100,
height = 100,
density = Density(density)
).use { scene ->
val context = TestColumn()

window.setContent {
scene.setContent {
CompositionLocalProvider(
LocalScrollConfig provides LinuxGnomeConfig
) {
Expand All @@ -77,31 +69,34 @@ class DesktopScrollableTest {
}
}

window.onMouseScroll(
x = 0,
y = 0,
event = MouseScrollEvent(MouseScrollUnit.Line(3f), MouseScrollOrientation.Vertical)
scene.sendPointerEvent(
eventType = PointerEventType.Scroll,
position = Offset.Zero,
scrollDelta = Offset(0f, 3f),
nativeEvent = awtWheelEvent(),
)
window.render()

assertThat(context.offset).isWithin(0.1f).of(-3 * scrollLineLinux(20.dp))

window.onMouseScroll(
x = 0,
y = 0,
event = MouseScrollEvent(MouseScrollUnit.Line(3f), MouseScrollOrientation.Vertical)
scene.sendPointerEvent(
eventType = PointerEventType.Scroll,
position = Offset.Zero,
scrollDelta = Offset(0f, 3f),
nativeEvent = awtWheelEvent(),
)
window.render()

assertThat(context.offset).isWithin(0.1f).of(-6 * scrollLineLinux(20.dp))
}

@Test
fun `windows, scroll vertical`() {
val window = window()
fun `windows, scroll vertical`() = ImageComposeScene(
width = 100,
height = 100,
density = Density(density)
).use { scene ->
val context = TestColumn()

window.setContent {
scene.setContent {
CompositionLocalProvider(
LocalScrollConfig provides WindowsWinUIConfig
) {
Expand All @@ -116,31 +111,34 @@ class DesktopScrollableTest {
}
}

window.onMouseScroll(
x = 0,
y = 0,
event = MouseScrollEvent(MouseScrollUnit.Line(-2f), MouseScrollOrientation.Vertical)
scene.sendPointerEvent(
eventType = PointerEventType.Scroll,
position = Offset.Zero,
scrollDelta = Offset(0f, -2f),
nativeEvent = awtWheelEvent(),
)
window.render()

assertThat(context.offset).isWithin(0.1f).of(2 * scrollLineWindows(20.dp))

window.onMouseScroll(
x = 0,
y = 0,
event = MouseScrollEvent(MouseScrollUnit.Line(4f), MouseScrollOrientation.Vertical)
scene.sendPointerEvent(
eventType = PointerEventType.Scroll,
position = Offset.Zero,
scrollDelta = Offset(0f, 4f),
nativeEvent = awtWheelEvent(),
)
window.render()

assertThat(context.offset).isWithin(0.1f).of(-2 * scrollLineWindows(20.dp))
}

@Test
fun `windows, scroll one page vertical`() {
val window = window()
fun `windows, scroll one page vertical`() = ImageComposeScene(
width = 100,
height = 100,
density = Density(density)
).use { scene ->
val context = TestColumn()

window.setContent {
scene.setContent {
CompositionLocalProvider(
LocalScrollConfig provides WindowsWinUIConfig
) {
Expand All @@ -155,22 +153,25 @@ class DesktopScrollableTest {
}
}

window.onMouseScroll(
x = 0,
y = 0,
event = MouseScrollEvent(MouseScrollUnit.Page(1f), MouseScrollOrientation.Vertical)
scene.sendPointerEvent(
eventType = PointerEventType.Scroll,
position = Offset.Zero,
scrollDelta = Offset(0f, 1f),
nativeEvent = awtWheelEvent(isScrollByPages = true),
)
window.render()

assertThat(context.offset).isWithin(0.1f).of(-scrollPage(20.dp))
}

@Test
fun `macOS, scroll vertical`() {
val window = window()
fun `macOS, scroll vertical`() = ImageComposeScene(
width = 100,
height = 100,
density = Density(density)
).use { scene ->
val context = TestColumn()

window.setContent {
scene.setContent {
CompositionLocalProvider(
LocalScrollConfig provides MacOSCocoaConfig
) {
Expand All @@ -185,22 +186,25 @@ class DesktopScrollableTest {
}
}

window.onMouseScroll(
x = 0,
y = 0,
event = MouseScrollEvent(MouseScrollUnit.Line(-5f), MouseScrollOrientation.Vertical)
scene.sendPointerEvent(
eventType = PointerEventType.Scroll,
position = Offset.Zero,
scrollDelta = Offset(0f, -5.5f),
nativeEvent = awtWheelEvent(),
)
window.render()

assertThat(context.offset).isWithin(0.1f).of(5f * scrollLineMacOs())
assertThat(context.offset).isWithin(0.1f).of(5.5f * scrollLineMacOs())
}

@Test
fun `scroll with different orientation`() {
val window = window()
fun `scroll with different orientation`() = ImageComposeScene(
width = 100,
height = 100,
density = Density(density)
).use { scene ->
val column = TestColumn()

window.setContent {
scene.setContent {
CompositionLocalProvider(
LocalScrollConfig provides LinuxGnomeConfig
) {
Expand All @@ -215,12 +219,12 @@ class DesktopScrollableTest {
}
}

window.onMouseScroll(
x = 0,
y = 0,
event = MouseScrollEvent(MouseScrollUnit.Line(3f), MouseScrollOrientation.Horizontal)
scene.sendPointerEvent(
eventType = PointerEventType.Scroll,
position = Offset.Zero,
scrollDelta = Offset(3f, 0f),
nativeEvent = awtWheelEvent(),
)
window.render()

assertThat(column.offset).isEqualTo(0f)
}
Expand All @@ -237,4 +241,4 @@ class DesktopScrollableTest {
return delta
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

package androidx.compose.desktop.ui.tooling.preview.runtime

import androidx.compose.runtime.Composable
import androidx.compose.runtime.currentComposer
import androidx.compose.ui.renderComposeScene
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.platform.TestComposeWindow
import androidx.compose.ui.unit.Density
Expand Down Expand Up @@ -51,8 +51,7 @@ internal class NonInteractivePreviewFacade {
val className = fqName.substringBeforeLast(".")
val methodName = fqName.substringAfterLast(".")
val density = scale?.let { Density(it.toFloat()) } ?: Density(1f)
val window = TestComposeWindow(width = width, height = height, density = density)
window.setContent @Composable {
return renderComposeScene(width = width, height = height, density = density) {
// We need to delay the reflection instantiation of the class until we are in the
// composable to ensure all the right initialization has happened and the Composable
// class loads correctly.
Expand All @@ -61,8 +60,7 @@ internal class NonInteractivePreviewFacade {
methodName,
currentComposer
)
}
return window.surface.makeImageSnapshot().encodeToData()!!.bytes
}.encodeToData()!!.bytes
}
}
}
Loading

0 comments on commit 7713197

Please sign in to comment.