Skip to content

Commit

Permalink
Draw the first frame before window is visible
Browse files Browse the repository at this point in the history
Fixes JetBrains/compose-multiplatform#1794

If the first frame is too long to draw (> 500ms), users should make their apps asynchronous and draw UI gradully in multiple frames without blocking the UI thread for too long in single frame
  • Loading branch information
igordmn committed Feb 8, 2022
1 parent 21f72b7 commit 0d21256
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,13 @@ private val iconSize = Size(32f, 32f)
internal fun Window.setIcon(painter: Painter?) {
setIconImage(painter?.toAwtImage(density, layoutDirection, iconSize))
}

internal fun Window.makeDisplayable() {
val oldPreferredSize = preferredSize
preferredSize = size
try {
pack()
} finally {
preferredSize = oldPreferredSize
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package androidx.compose.ui.window

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
import androidx.compose.runtime.currentCompositionLocalContext
import androidx.compose.runtime.getValue
Expand All @@ -30,6 +29,7 @@ import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.ComponentUpdater
import androidx.compose.ui.util.makeDisplayable
import androidx.compose.ui.util.setIcon
import androidx.compose.ui.util.setPositionSafely
import androidx.compose.ui.util.setSizeSafely
Expand Down Expand Up @@ -261,6 +261,11 @@ fun Dialog(
it.compositionLocalContext = compositionLocalContext
it.exceptionHandler = windowExceptionHandlerFactory.exceptionHandler(it)
update(it)

if (!it.isDisplayable) {
it.makeDisplayable()
it.contentPane.paint(it.graphics)
}
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package androidx.compose.ui.window

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
Expand All @@ -33,6 +32,7 @@ import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.ComponentUpdater
import androidx.compose.ui.util.makeDisplayable
import androidx.compose.ui.util.setIcon
import androidx.compose.ui.util.setPositionSafely
import androidx.compose.ui.util.setSizeSafely
Expand Down Expand Up @@ -375,6 +375,11 @@ fun Window(
it.compositionLocalContext = compositionLocalContext
it.exceptionHandler = windowExceptionHandlerFactory.exceptionHandler(it)
update(it)

if (!it.isDisplayable) {
it.makeDisplayable()
it.contentPane.paint(it.graphics)
}
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package androidx.compose.ui.window

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -55,10 +56,7 @@ internal fun runApplicationTest(
withTimeout(30000) {
val exceptionHandler = TestExceptionHandler()
withExceptionHandler(exceptionHandler) {
val testScope = WindowTestScope(this, useDelay, exceptionHandler)
if (testScope.isOpen) {
testScope.body()
}
WindowTestScope(this, useDelay, exceptionHandler).body()
}
exceptionHandler.throwIfCaught()
}
Expand Down Expand Up @@ -142,10 +140,25 @@ internal class WindowTestScope(
var isOpen by mutableStateOf(true)
private val initialRecomposers = Recomposer.runningRecomposers.value

// TODO(demin) replace launchApplication to launchTestApplication in all tests,
// because we don't close the window with simple launchApplication
fun launchTestApplication(
content: @Composable ApplicationScope.() -> Unit
) = launchApplication {
if (isOpen) {
content()
}
}

// TODO(demin) remove when we migrate from launchApplication to launchTestApplication (see TODO above)
fun exitApplication() {
isOpen = false
}

fun exitTestApplication() {
isOpen = false
}

suspend fun awaitIdle() {
if (useDelay) {
delay(500)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@

package androidx.compose.ui.window.dialog

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
Expand Down Expand Up @@ -520,4 +522,35 @@ class DialogTest {

exitApplication()
}


@Test(timeout = 30000)
fun `should draw before dialog is visible`() = runApplicationTest {
var isComposed = false
var isDrawn = false
var isVisibleOnFirstComposition = false
var isVisibleOnFirstDraw = false

launchTestApplication {
Dialog(onCloseRequest = ::exitApplication) {
if (!isComposed) {
isVisibleOnFirstComposition = window.isVisible
isComposed = true
}

Canvas(Modifier.fillMaxSize()) {
if (!isDrawn) {
isVisibleOnFirstDraw = window.isVisible
isDrawn = true
}
}
}
}

awaitIdle()
assertThat(isVisibleOnFirstComposition).isFalse()
assertThat(isVisibleOnFirstDraw).isFalse()

exitTestApplication()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@

package androidx.compose.ui.window.window

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Slider
Expand Down Expand Up @@ -490,4 +492,34 @@ class WindowTest {

exitApplication()
}

@Test(timeout = 30000)
fun `should draw before window is visible`() = runApplicationTest {
var isComposed = false
var isDrawn = false
var isVisibleOnFirstComposition = false
var isVisibleOnFirstDraw = false

launchTestApplication {
Window(onCloseRequest = ::exitApplication) {
if (!isComposed) {
isVisibleOnFirstComposition = window.isVisible
isComposed = true
}

Canvas(Modifier.fillMaxSize()) {
if (!isDrawn) {
isVisibleOnFirstDraw = window.isVisible
isDrawn = true
}
}
}
}

awaitIdle()
assertThat(isVisibleOnFirstComposition).isFalse()
assertThat(isVisibleOnFirstDraw).isFalse()

exitTestApplication()
}
}

0 comments on commit 0d21256

Please sign in to comment.