From 7e5cfc53a85424f9f95b9c95ef1f854aa9def94f Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Tue, 2 Mar 2021 10:58:39 +0000 Subject: [PATCH] Imageviewer. Refactor Application for observable properties (#447) * Refactor Application for observable properties * Style * Add RememberObserver * Comment out problematic update --- .../imageviewer/model/DesktopContentState.kt | 9 +- .../example/imageviewer/utils/Application.kt | 190 +++++++++--------- 2 files changed, 106 insertions(+), 93 deletions(-) diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt index 6f9d66e42f..82cb159e7b 100644 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt @@ -1,6 +1,7 @@ package example.imageviewer.model import androidx.compose.runtime.MutableState +import androidx.compose.runtime.RememberObserver import androidx.compose.runtime.mutableStateOf import example.imageviewer.ResString import example.imageviewer.core.FilterType @@ -16,7 +17,7 @@ import java.util.concurrent.Executors import javax.swing.SwingUtilities.invokeLater -object ContentState { +object ContentState : RememberObserver { private lateinit var repository: ImageRepository private lateinit var uriRepository: String @@ -280,6 +281,12 @@ object ContentState { } } } + + override fun onRemembered() { } + override fun onAbandoned() { } + override fun onForgotten() { + executor.shutdown() + } } private object MainImageWrapper { diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Application.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Application.kt index a3c1236748..760cd5ab30 100644 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Application.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Application.kt @@ -3,79 +3,39 @@ package example.imageviewer.utils import androidx.compose.desktop.AppManager import androidx.compose.desktop.AppWindow import androidx.compose.desktop.WindowEvents -import androidx.compose.runtime.Applier -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Composition -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.ExperimentalComposeApi -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MonotonicFrameClock -import androidx.compose.runtime.Recomposer -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.window.MenuBar -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch +import kotlinx.coroutines.* +import kotlinx.coroutines.swing.Swing import java.awt.image.BufferedImage -import javax.swing.SwingUtilities -import kotlin.system.exitProcess fun Application( content: @Composable ApplicationScope.() -> Unit -) = SwingUtilities.invokeLater { - AppManager.setEvents(onWindowsEmpty = null) - val scope = ApplicationScope(content) - scope.start() -} - -@OptIn(ExperimentalComposeApi::class, ExperimentalCoroutinesApi::class) -class ApplicationScope( - private val content: @Composable ApplicationScope.() -> Unit ) { - private val frameClock = ImmediateFrameClock() - private val context = Dispatchers.Main + frameClock - private val scope = CoroutineScope(context) + GlobalScope.launch(Dispatchers.Swing + ImmediateFrameClock()) { + AppManager.setEvents(onWindowsEmpty = null) - private val recomposer = Recomposer(context) - private val composition = Composition(EmptyApplier(), recomposer) + withRunningRecomposer { recomposer -> + val latch = CompletableDeferred() + val applier = ApplicationApplier { latch.complete(Unit) } - private val windows = mutableSetOf() - private var windowsVersion by mutableStateOf(Any()) + val composition = Composition(applier, recomposer) + try { + val scope = ApplicationScope(recomposer) - fun start() { - scope.launch(start = CoroutineStart.UNDISPATCHED) { - recomposer.runRecomposeAndApplyChanges() - } - composition.setContent { - content() - WindowsMonitor() - } - } + composition.setContent { scope.content() } - @Composable - private fun WindowsMonitor() { - LaunchedEffect(windowsVersion) { - if (windows.isEmpty()) { - dispose() - exitProcess(0) + latch.join() + } finally { + composition.dispose() } } } +} - private fun dispose() { - composition.dispose() - scope.cancel() - } - - // TODO make parameters observable (now if any parameter is changed we don't change the window) +class ApplicationScope internal constructor(private val recomposer: Recomposer) { @Composable fun ComposableWindow( title: String = "JetpackDesktopWindow", @@ -91,20 +51,10 @@ class ApplicationScope( content: @Composable () -> Unit = {} ) { var isOpened by remember { mutableStateOf(true) } - if (isOpened) { - DisposableEffect(Unit) { - lateinit var window: AppWindow - - fun onClose() { - if (isOpened) { - windows.remove(window) - onDismissRequest?.invoke() - windowsVersion = Any() - isOpened = false - } - } - - window = AppWindow( + if (!isOpened) return + ComposeNode( + factory = { + val window = AppWindow( title = title, size = size, location = location, @@ -115,21 +65,24 @@ class ApplicationScope( resizable = resizable, events = events, onDismissRequest = { - onClose() + onDismissRequest?.invoke() + isOpened = false } ) - - windows.add(window) window.show(recomposer, content) - - onDispose { - if (!window.isClosed) { - window.close() - } - onClose() - } + window + }, + update = { + set(title) { setTitle(it) } + set(size) { setSize(it.width, it.height) } + // set(location) { setLocation(it.x, it.y) } + set(icon) { setIcon(it) } + // set(menuBar) { if (it != null) setMenuBar(it) else removeMenuBar() } + // set(resizable) { setResizable(it) } + // set(events) { setEvents(it) } + // set(onDismissRequest) { setDismiss(it) } } - } + ) } } @@ -139,14 +92,67 @@ private class ImmediateFrameClock : MonotonicFrameClock { ) = onFrame(System.nanoTime()) } -@OptIn(ExperimentalComposeApi::class) -private class EmptyApplier : Applier { - override val current: Unit = Unit - override fun down(node: Unit) = Unit - override fun up() = Unit - override fun insertTopDown(index: Int, instance: Unit) = Unit - override fun insertBottomUp(index: Int, instance: Unit) = Unit - override fun remove(index: Int, count: Int) = Unit - override fun move(from: Int, to: Int, count: Int) = Unit - override fun clear() = Unit +private class ApplicationApplier( + private val onWindowsEmpty: () -> Unit +) : Applier { + private val windows = mutableListOf() + + override var current: AppWindow? = null + + override fun insertBottomUp(index: Int, instance: AppWindow?) { + requireNotNull(instance) + check(current == null) { "Windows cannot be nested!" } + windows.add(index, instance) + } + + override fun remove(index: Int, count: Int) { + repeat(count) { + val window = windows.removeAt(index) + if (!window.isClosed) { + window.close() + } + } + } + + override fun move(from: Int, to: Int, count: Int) { + if (from > to) { + var current = to + repeat(count) { + val node = windows.removeAt(from) + windows.add(current, node) + current++ + } + } else { + repeat(count) { + val node = windows.removeAt(from) + windows.add(to - 1, node) + } + } + } + + override fun clear() { + windows.forEach { if (!it.isClosed) it.close() } + windows.clear() + } + + override fun onEndChanges() { + if (windows.isEmpty()) { + onWindowsEmpty() + } + } + + override fun down(node: AppWindow?) { + requireNotNull(node) + check(current == null) { "Windows cannot be nested!" } + current = node + } + + override fun up() { + check(current != null) { "Windows cannot be nested!" } + current = null + } + + override fun insertTopDown(index: Int, instance: AppWindow?) { + // ignored. Building tree bottom-up + } }