Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Call SkiaLayerComponent.onComposeInvalidation() only from the event dispatch thread #1288

Merged
merged 1 commit into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntRect
import java.awt.Component
import java.awt.EventQueue
import java.awt.Rectangle
import javax.swing.JComponent
import kotlin.math.ceil
Expand Down Expand Up @@ -88,3 +89,16 @@ internal fun JComponent.setTransparent(transparent: Boolean) {
isOpaque = false
}
}

/**
* Calls [block] synchronously on the event dispatching thread. If the calling thread is already the
* event dispatch thread, simply executes [block]; otherwise schedules [block] to run on event
* dispatching thread and waits for it to return.
*/
internal fun runOnEDTThread(block: () -> Unit) {
if (EventQueue.isDispatchThread()) {
block()
} else {
EventQueue.invokeAndWait(block)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.compose.ui.awt.AwtEventListener
import androidx.compose.ui.awt.AwtEventListeners
import androidx.compose.ui.awt.OnlyValidPrimaryMouseButtonFilter
import androidx.compose.ui.awt.SwingInteropContainer
import androidx.compose.ui.awt.runOnEDTThread
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.geometry.Offset
Expand Down Expand Up @@ -492,9 +493,11 @@ internal class ComposeSceneMediator(
}
}

fun onComposeInvalidation() = catchExceptions {
if (isDisposed) return
skiaLayerComponent.onComposeInvalidation()
fun onComposeInvalidation() = runOnEDTThread {
catchExceptions {
if (isDisposed) return@catchExceptions
skiaLayerComponent.onComposeInvalidation()
}
}

fun onChangeComponentPosition() = catchExceptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.neverEqualPolicy
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusRequester
Expand Down Expand Up @@ -79,11 +80,17 @@ import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performKeyPress
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.runApplicationTest
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.Assert.assertFalse
import org.junit.Ignore
import org.junit.Rule
Expand Down Expand Up @@ -638,6 +645,40 @@ class ComposeSceneTest {
}
}

@Test
fun stateChangeFromNonUiThreadDoesntCrash() = runApplicationTest {
// https://github.com/JetBrains/compose-multiplatform/issues/4546
var value by mutableStateOf(0)
val done = CompletableDeferred<Unit>()
var exceptionThrown: Throwable? = null

launchTestApplication {
Window(onCloseRequest = {}) {
Canvas(Modifier.size(100.dp)) {
@Suppress("UNUSED_EXPRESSION")
value
}
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
try {
for (i in 1..50) {
value = i
Snapshot.sendApplyNotifications()
delay(1)
}
} catch (e: Throwable) {
exceptionThrown = e
}
done.complete(Unit)
}
}
}
}

done.await()
assertNull(exceptionThrown, "Exception thrown setting snapshot state from non-UI thread")
}

private class TestException : RuntimeException()

@ExperimentalComposeUiApi
Expand Down