diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/CompactControlsVisualRenderingTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/CompactControlsVisualRenderingTest.kt index 720fc450..b3c5fff7 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/CompactControlsVisualRenderingTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/CompactControlsVisualRenderingTest.kt @@ -55,45 +55,47 @@ class CompactControlsVisualRenderingTest(@param:TempDir private val tempDir: Pat } val filters = StationFilterState() - val scene = ImageComposeScene(width = 1366, height = 380, density = Density(scale)) { - Kst4ContestTheme(darkMode = darkMode, baseFontSizeSp = 12f) { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background, - ) { - Column(Modifier.padding(4.dp)) { - TimelineView( - state = TimelineState(), - onCandidateClicked = {}, - modifier = Modifier.fillMaxWidth(), - ) - Spacer(Modifier.height(8.dp)) - StationFilterBar( - filters = filters, - activeBands = EnumSet.of(Band.B_144, Band.B_432, Band.B_1296), - reachability = ReachabilityState( - EnumSet.of(Band.B_144, Band.B_432, Band.B_1296), - ), - onChanged = {}, - ) - Spacer(Modifier.height(12.dp)) - ChatInputRow( - state = chatInputState, - prefs = preferences, - ownQrgMain = ObservedValue(preferences.getMYQRGFirstCat()), - ownQrgSecond = ObservedValue(preferences.getMYQRGSecondCat()), - antennaQtf = ObservedValue(preferences.getActualQTF()), - antennaQtf2 = ObservedValue(preferences.getActualQTF2()), - so2rEnabled = false, - ) + return onEventThread { + val scene = ImageComposeScene(width = 1366, height = 380, density = Density(scale)) { + Kst4ContestTheme(darkMode = darkMode, baseFontSizeSp = 12f) { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + Column(Modifier.padding(4.dp)) { + TimelineView( + state = TimelineState(), + onCandidateClicked = {}, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(8.dp)) + StationFilterBar( + filters = filters, + activeBands = EnumSet.of(Band.B_144, Band.B_432, Band.B_1296), + reachability = ReachabilityState( + EnumSet.of(Band.B_144, Band.B_432, Band.B_1296), + ), + onChanged = {}, + ) + Spacer(Modifier.height(12.dp)) + ChatInputRow( + state = chatInputState, + prefs = preferences, + ownQrgMain = ObservedValue(preferences.getMYQRGFirstCat()), + ownQrgSecond = ObservedValue(preferences.getMYQRGSecondCat()), + antennaQtf = ObservedValue(preferences.getActualQTF()), + antennaQtf2 = ObservedValue(preferences.getActualQTF2()), + so2rEnabled = false, + ) + } } } } - } - return try { - scene.render().encodeToData()!!.bytes - } finally { - scene.close() + try { + scene.render().encodeToData()!!.bytes + } finally { + scene.close() + } } } diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/OnEventThread.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/OnEventThread.kt new file mode 100644 index 00000000..4e5d682a --- /dev/null +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/OnEventThread.kt @@ -0,0 +1,37 @@ +package kst4contest.view.compose + +import java.awt.EventQueue +import java.lang.reflect.InvocationTargetException + +/* + * An ImageComposeScene must live on the AWT event thread, the thread the application renders + * on. Compose Multiplatform 1.8.2 sends the global snapshot notifications there + * (GlobalSnapshotManager), and doing so takes two locks in the opposite order from a render + * on any other thread: + * + * - the render holds the scene's FlushCoroutineDispatcher runLock while a task it runs + * (here LazyListState.scrollToItem) measures and wants the SnapshotStateObserver's + * observedScopeMapsLock; + * - the event thread holds observedScopeMapsLock in drainChanges while a derived state it + * re-reads wakes a snapshotFlow collector of that scene, whose dispatch wants runLock. + * + * Both waited forever in CI runs 40252077018, 40252089738 and 40299943329; the thread dump of + * the last one shows both stacks. On one thread the two monitors are simply re-entered, which + * is why the application never hangs this way. + */ + +/** + * Runs [block] on the AWT event thread, waits for it and returns its result. Whatever + * [block] throws is rethrown here unwrapped, so a failed assertion still reads as one. + */ +internal fun onEventThread(block: () -> T): T { + if (EventQueue.isDispatchThread()) return block() + + var result: Result? = null + try { + EventQueue.invokeAndWait { result = runCatching(block) } + } catch (wrapped: InvocationTargetException) { + throw wrapped.cause ?: wrapped + } + return result!!.getOrThrow() +} diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/SelectedStationMessageUiVisualRenderingTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/SelectedStationMessageUiVisualRenderingTest.kt index dd312fed..1cd7eb62 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/SelectedStationMessageUiVisualRenderingTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/SelectedStationMessageUiVisualRenderingTest.kt @@ -84,37 +84,39 @@ class SelectedStationMessageUiVisualRenderingTest { replaceRows(listOf(message(member("DL1ABC", 2), member("DL2XYZ", 2), "pse sked"))) } - val scene = ImageComposeScene(width = 1366, height = windowHeight, density = Density(scale)) { - Kst4ContestTheme(darkMode = darkMode, baseFontSizeSp = 12f) { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background, - ) { - Row(Modifier.fillMaxSize().padding(6.dp)) { - MessageTabs( - publicMessages = publicMessages, - clusterMessages = clusterTable(), - qsoOfTheOther = qsoMessages, - modifier = Modifier.weight(1f), - ) - SelectedStationPanel( - state = selectedState, - messages = selectedMessages, - activeBands = EnumSet.of(Band.B_144, Band.B_432, Band.B_1296), - openInBrowser = Consumer {}, - onShowOnMap = {}, - onShowPathInAirScout = {}, - modifier = Modifier.width(panelWidth).padding(start = 6.dp), - ) + return onEventThread { + val scene = ImageComposeScene(width = 1366, height = windowHeight, density = Density(scale)) { + Kst4ContestTheme(darkMode = darkMode, baseFontSizeSp = 12f) { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + Row(Modifier.fillMaxSize().padding(6.dp)) { + MessageTabs( + publicMessages = publicMessages, + clusterMessages = clusterTable(), + qsoOfTheOther = qsoMessages, + modifier = Modifier.weight(1f), + ) + SelectedStationPanel( + state = selectedState, + messages = selectedMessages, + activeBands = EnumSet.of(Band.B_144, Band.B_432, Band.B_1296), + openInBrowser = Consumer {}, + onShowOnMap = {}, + onShowPathInAirScout = {}, + modifier = Modifier.width(panelWidth).padding(start = 6.dp), + ) + } } } } - } - return try { - scene.render().encodeToData()!!.bytes - } finally { - scene.close() + try { + scene.render().encodeToData()!!.bytes + } finally { + scene.close() + } } } diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/StationWorkspaceVisualRenderingTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/StationWorkspaceVisualRenderingTest.kt index b4a3547e..971674ce 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/StationWorkspaceVisualRenderingTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/StationWorkspaceVisualRenderingTest.kt @@ -87,62 +87,64 @@ class StationWorkspaceVisualRenderingTest { RowKeys.byReference(), ) - val scene = ImageComposeScene( - width = 1366, - height = scenario.height, - density = Density(scale), - ) { - Kst4ContestTheme(darkMode = darkMode, baseFontSizeSp = 12f) { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background, - ) { - Box(Modifier.fillMaxSize()) { - Column( - Modifier - .width(scenario.width.dp) - .fillMaxHeight() - .align(Alignment.TopEnd) - .padding(4.dp), + return onEventThread { + val scene = ImageComposeScene( + width = 1366, + height = scenario.height, + density = Density(scale), + ) { + Kst4ContestTheme(darkMode = darkMode, baseFontSizeSp = 12f) { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, ) { - StationFilterBar( - filters = StationFilterState(), - activeBands = EnumSet.of(Band.B_144, Band.B_432), - reachability = ReachabilityState(EnumSet.of(Band.B_144, Band.B_432)), - onChanged = {}, - ) - DataTableView( - state = stations, - modifier = Modifier.weight(0.45f), - emphasizeCell = { member, column -> - column.id == "callsign" && newJoined.isRecent(member) - }, - ) - TopPriorityBar( - state = priority, - newJoined = newJoined, - onShowMore = {}, - ) - SelectedStationPanel( - state = selected, - messages = selectedMessages, - activeBands = EnumSet.of(Band.B_144, Band.B_432), - contentSplitter = SplitterState(2, doubleArrayOf(0.48)) {}, - openInBrowser = Consumer {}, - onShowOnMap = {}, - onShowPathInAirScout = {}, - modifier = Modifier.weight(0.55f), - ) - } + Box(Modifier.fillMaxSize()) { + Column( + Modifier + .width(scenario.width.dp) + .fillMaxHeight() + .align(Alignment.TopEnd) + .padding(4.dp), + ) { + StationFilterBar( + filters = StationFilterState(), + activeBands = EnumSet.of(Band.B_144, Band.B_432), + reachability = ReachabilityState(EnumSet.of(Band.B_144, Band.B_432)), + onChanged = {}, + ) + DataTableView( + state = stations, + modifier = Modifier.weight(0.45f), + emphasizeCell = { member, column -> + column.id == "callsign" && newJoined.isRecent(member) + }, + ) + TopPriorityBar( + state = priority, + newJoined = newJoined, + onShowMore = {}, + ) + SelectedStationPanel( + state = selected, + messages = selectedMessages, + activeBands = EnumSet.of(Band.B_144, Band.B_432), + contentSplitter = SplitterState(2, doubleArrayOf(0.48)) {}, + openInBrowser = Consumer {}, + onShowOnMap = {}, + onShowPathInAirScout = {}, + modifier = Modifier.weight(0.55f), + ) + } + } } } } - } - return try { - scene.render().encodeToData()!!.bytes - } finally { - scene.close() + try { + scene.render().encodeToData()!!.bytes + } finally { + scene.close() + } } } diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/TopPriorityCandidatesWindowTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/TopPriorityCandidatesWindowTest.kt index 9ff879a8..43442e6f 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/TopPriorityCandidatesWindowTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/TopPriorityCandidatesWindowTest.kt @@ -32,17 +32,19 @@ class TopPriorityCandidatesWindowTest { /** Runs [block] against a rendered list of [twoCandidates] and reports what it picked. */ private fun picking(block: (ImageComposeScene) -> Unit): String? { var picked: String? = null - val scene = ImageComposeScene(width = 360, height = 500, density = Density(1f)) { - TopPriorityCandidatesWindow.CandidateListForTest(twoCandidates) { - picked = it.displayCallSign + onEventThread { + val scene = ImageComposeScene(width = 360, height = 500, density = Density(1f)) { + TopPriorityCandidatesWindow.CandidateListForTest(twoCandidates) { + picked = it.displayCallSign + } + } + try { + scene.render() + block(scene) + scene.render() + } finally { + scene.close() } - } - try { - scene.render() - block(scene) - scene.render() - } finally { - scene.close() } return picked } @@ -78,13 +80,15 @@ class TopPriorityCandidatesWindowTest { @Test fun `the list renders with candidates and without`() { listOf(emptyList(), twoCandidates).forEach { ranking -> - val scene = ImageComposeScene(width = 360, height = 500, density = Density(1f)) { - TopPriorityCandidatesWindow.CandidateListForTest(ranking) { } - } - try { - scene.render() - } finally { - scene.close() + onEventThread { + val scene = ImageComposeScene(width = 360, height = 500, density = Density(1f)) { + TopPriorityCandidatesWindow.CandidateListForTest(ranking) { } + } + try { + scene.render() + } finally { + scene.close() + } } } } diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/map/ComposeStationMapDrawingTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/map/ComposeStationMapDrawingTest.kt index 544d8fc7..00321c41 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/map/ComposeStationMapDrawingTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/map/ComposeStationMapDrawingTest.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.ImageComposeScene import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Density +import kst4contest.view.compose.onEventThread import kst4contest.view.map.MapCallsignRawSnapshot import kst4contest.view.map.MapViewport import kst4contest.view.map.TileFetcher @@ -71,27 +72,29 @@ class ComposeStationMapDrawingTest { workedGrossFields: Set = emptySet(), darkMode: Boolean = true, ): BufferedImage { - val scene = ImageComposeScene(canvasWidth, canvasHeight, Density(1f)) { - ComposeStationMap( - markers = markers, - tileFetcher = offlineTileFetcher, - mapState = MapState(zoom = zoom, centerLon = 10.0, centerLat = 51.0), - groupingEnabled = false, - selectedMarkerCallsign = null, - ownLocator6 = ownLocator6, - antennaAzimuthDeg = 90.0, - antennaBeamWidthDeg = 0.0, - maxQrbKm = 0.0, - onMarkerClicked = null, - workedGrossFields = workedGrossFields, - darkMode = darkMode, - modifier = Modifier.fillMaxSize(), - ) - } - try { - return ImageIO.read(ByteArrayInputStream(scene.render().encodeToData()!!.bytes)) - } finally { - scene.close() + return onEventThread { + val scene = ImageComposeScene(canvasWidth, canvasHeight, Density(1f)) { + ComposeStationMap( + markers = markers, + tileFetcher = offlineTileFetcher, + mapState = MapState(zoom = zoom, centerLon = 10.0, centerLat = 51.0), + groupingEnabled = false, + selectedMarkerCallsign = null, + ownLocator6 = ownLocator6, + antennaAzimuthDeg = 90.0, + antennaBeamWidthDeg = 0.0, + maxQrbKm = 0.0, + onMarkerClicked = null, + workedGrossFields = workedGrossFields, + darkMode = darkMode, + modifier = Modifier.fillMaxSize(), + ) + } + try { + ImageIO.read(ByteArrayInputStream(scene.render().encodeToData()!!.bytes)) + } finally { + scene.close() + } } } diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/map/ComposeStationMapLayoutTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/map/ComposeStationMapLayoutTest.kt index e4adeaa0..a9d7f75f 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/map/ComposeStationMapLayoutTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/map/ComposeStationMapLayoutTest.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.ImageComposeScene import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Density +import kst4contest.view.compose.onEventThread import kst4contest.view.map.MapCallsignRawSnapshot import kst4contest.view.map.TileFetcher import org.junit.jupiter.api.Test @@ -33,30 +34,32 @@ class ComposeStationMapLayoutTest { ) private fun renderAt(widthPx: Int, heightPx: Int, zoom: Float) { - val scene = ImageComposeScene( - width = widthPx, - height = heightPx, - density = Density(1f), - ) { - ComposeStationMap( - markers = stations, - tileFetcher = TileFetcher(), - mapState = MapState(zoom = zoom, centerLon = 10.0, centerLat = 51.0), - groupingEnabled = true, - selectedMarkerCallsign = "F5JMI", - ownLocator6 = "JO50JP", - antennaAzimuthDeg = 229.0, - antennaBeamWidthDeg = 60.0, - maxQrbKm = 800.0, - onMarkerClicked = null, - darkMode = true, - modifier = Modifier.fillMaxSize(), - ) - } - try { - scene.render() - } finally { - scene.close() + onEventThread { + val scene = ImageComposeScene( + width = widthPx, + height = heightPx, + density = Density(1f), + ) { + ComposeStationMap( + markers = stations, + tileFetcher = TileFetcher(), + mapState = MapState(zoom = zoom, centerLon = 10.0, centerLat = 51.0), + groupingEnabled = true, + selectedMarkerCallsign = "F5JMI", + ownLocator6 = "JO50JP", + antennaAzimuthDeg = 229.0, + antennaBeamWidthDeg = 60.0, + maxQrbKm = 800.0, + onMarkerClicked = null, + darkMode = true, + modifier = Modifier.fillMaxSize(), + ) + } + try { + scene.render() + } finally { + scene.close() + } } } diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/map/PathAnalysisDetailsLayoutTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/map/PathAnalysisDetailsLayoutTest.kt index 77e8b77e..461bdd8e 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/map/PathAnalysisDetailsLayoutTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/map/PathAnalysisDetailsLayoutTest.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.ui.ImageComposeScene import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Density +import kst4contest.view.compose.onEventThread import kst4contest.view.map.PathAnalysisResult import org.junit.jupiter.api.Test @@ -25,21 +26,23 @@ class PathAnalysisDetailsLayoutTest { private val narrowWidthPx = 120 private fun renderAt(widthPx: Int, result: PathAnalysisResult?) { - val scene = ImageComposeScene( - width = widthPx, - height = 600, - density = Density(1f), - ) { - PathAnalysisDetails( - result = result, - darkMode = true, - modifier = Modifier.fillMaxWidth(), - ) - } - try { - scene.render() - } finally { - scene.close() + onEventThread { + val scene = ImageComposeScene( + width = widthPx, + height = 600, + density = Density(1f), + ) { + PathAnalysisDetails( + result = result, + darkMode = true, + modifier = Modifier.fillMaxWidth(), + ) + } + try { + scene.render() + } finally { + scene.close() + } } } diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/map/PathAnalysisDetailsWidthTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/map/PathAnalysisDetailsWidthTest.kt index 3de304f9..d03b7f9f 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/map/PathAnalysisDetailsWidthTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/map/PathAnalysisDetailsWidthTest.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.ui.ImageComposeScene import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Density +import kst4contest.view.compose.onEventThread import kst4contest.view.map.PathAnalysisResult import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @@ -31,18 +32,20 @@ class PathAnalysisDetailsWidthTest { private val assessmentBackground = 0x33383E private fun render(paneWidthPx: Int, heightPx: Int = 600): BufferedImage { - val scene = ImageComposeScene(width = paneWidthPx, height = heightPx, density = Density(1f)) { - PathAnalysisDetails( - result = PathAnalysisResult.loading("JO50JP", "JN24JB", "F5JMI"), - darkMode = true, - modifier = Modifier.fillMaxWidth(), - ) - } - try { - val png = scene.render().encodeToData()!!.bytes - return ImageIO.read(ByteArrayInputStream(png)) - } finally { - scene.close() + return onEventThread { + val scene = ImageComposeScene(width = paneWidthPx, height = heightPx, density = Density(1f)) { + PathAnalysisDetails( + result = PathAnalysisResult.loading("JO50JP", "JN24JB", "F5JMI"), + darkMode = true, + modifier = Modifier.fillMaxWidth(), + ) + } + try { + val png = scene.render().encodeToData()!!.bytes + ImageIO.read(ByteArrayInputStream(png)) + } finally { + scene.close() + } } } diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/map/PathProfileChartLayoutTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/map/PathProfileChartLayoutTest.kt index 3884b9b6..7e20cb29 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/map/PathProfileChartLayoutTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/map/PathProfileChartLayoutTest.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.ImageComposeScene import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Density +import kst4contest.view.compose.onEventThread import org.junit.jupiter.api.Test /** @@ -18,17 +19,19 @@ import org.junit.jupiter.api.Test class PathProfileChartLayoutTest { private fun renderAt(widthPx: Int, heightPx: Int) { - val scene = ImageComposeScene( - width = widthPx, - height = heightPx, - density = Density(1f), - ) { - PathProfileChart(modifier = Modifier.fillMaxSize(), darkMode = true) - } - try { - scene.render() - } finally { - scene.close() + onEventThread { + val scene = ImageComposeScene( + width = widthPx, + height = heightPx, + density = Density(1f), + ) { + PathProfileChart(modifier = Modifier.fillMaxSize(), darkMode = true) + } + try { + scene.render() + } finally { + scene.close() + } } } diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/map/StationMapBodyLayoutTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/map/StationMapBodyLayoutTest.kt index 3e44f7fd..ac43f073 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/map/StationMapBodyLayoutTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/map/StationMapBodyLayoutTest.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.unit.dp import kst4contest.view.compose.SplitterOrientation import kst4contest.view.compose.SplitterPane import kst4contest.view.compose.SplitterState +import kst4contest.view.compose.onEventThread import kst4contest.view.map.MapCallsignRawSnapshot import kst4contest.view.map.PathAnalysisResult import kst4contest.view.map.TileFetcher @@ -88,13 +89,15 @@ class StationMapBodyLayoutTest { } private fun renderAt(widthPx: Int, heightPx: Int, pathAnalysisVisible: Boolean = true) { - val scene = ImageComposeScene(width = widthPx, height = heightPx, density = Density(1f)) { - Body(pathAnalysisVisible) - } - try { - scene.render() - } finally { - scene.close() + onEventThread { + val scene = ImageComposeScene(width = widthPx, height = heightPx, density = Density(1f)) { + Body(pathAnalysisVisible) + } + try { + scene.render() + } finally { + scene.close() + } } }