From ac74c1f68c2cbadfe88624b857448cf26cc7684e Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Sun, 9 Mar 2025 17:55:13 +0100 Subject: [PATCH 01/14] build(graphql): add wasmJS target --- alchemist-graphql/build.gradle.kts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/alchemist-graphql/build.gradle.kts b/alchemist-graphql/build.gradle.kts index 571bd0d5ea..0f5fb20947 100644 --- a/alchemist-graphql/build.gradle.kts +++ b/alchemist-graphql/build.gradle.kts @@ -10,8 +10,11 @@ import Libs.alchemist import Libs.incarnation import Util.allVerificationTasks +import Util.devServer +import Util.webCommonConfiguration import com.apollographql.apollo3.gradle.internal.ApolloGenerateSourcesTask import com.expediagroup.graphql.plugin.gradle.tasks.AbstractGenerateClientTask +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { id("kotlin-multiplatform-convention") @@ -22,6 +25,13 @@ plugins { } kotlin { + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + webCommonConfiguration() + devServer() + } + sourceSets { val commonMain by getting { dependencies { From 76443c734846c590f7bde954e51d0ad1b6f1f002 Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Sun, 9 Mar 2025 17:57:17 +0100 Subject: [PATCH 02/14] feat(composeui): scaffold first interactions with graphql --- alchemist-composeui/build.gradle.kts | 12 ++++ .../unibo/alchemist/boundary/composeui/App.kt | 36 ++++++----- .../viewmodels/SimulationStatusViewModel.kt | 62 +++++++++++++++++++ .../boundary/composeui/ComposeMonitor.kt | 26 +++++--- gradle/libs.versions.toml | 2 + 5 files changed, 117 insertions(+), 21 deletions(-) create mode 100644 alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt diff --git a/alchemist-composeui/build.gradle.kts b/alchemist-composeui/build.gradle.kts index 911215f8e8..1bba46306e 100644 --- a/alchemist-composeui/build.gradle.kts +++ b/alchemist-composeui/build.gradle.kts @@ -1,3 +1,4 @@ +import Libs.alchemist import Util.devServer import Util.webCommonConfiguration import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl @@ -35,6 +36,17 @@ kotlin { implementation(compose.foundation) implementation(compose.material) implementation(compose.components.resources) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.apollo.runtime) + api(alchemist("graphql")) + } + } + + val jvmMain by getting { + dependencies { + implementation(compose.desktop.currentOs) + implementation(libs.kotlin.coroutines.swing) } } } diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt index 53ccbf338b..31b7cf5c6f 100644 --- a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt @@ -9,19 +9,21 @@ package it.unibo.alchemist.boundary.composeui -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import it.unibo.alchemist.boundary.composeui.viewmodels.SimulationStatusViewModel /** * Application entry point, this will be rendered the same in all the platforms. @@ -29,17 +31,23 @@ import androidx.compose.ui.Modifier @Composable fun app() { MaterialTheme { - var showContent by remember { mutableStateOf(false) } Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Button(onClick = { showContent = !showContent }) { - Text("Click me!") - } - AnimatedVisibility(showContent) { - val greeting = remember { getPlatform() } - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Text("Compose: $greeting") - } - } + simulationStatus() + } + } +} + +@Composable +fun simulationStatus(viewModel: SimulationStatusViewModel = viewModel { SimulationStatusViewModel() }) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + Text("Simulation status: ${uiState.status}") + Text("Simulation time: ${uiState.time}") + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + Button(onClick = { viewModel.pause() }) { + Text("Pause") + } + Button(onClick = { viewModel.play() }) { + Text("Play") } } } diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt new file mode 100644 index 0000000000..8d5beff6aa --- /dev/null +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010-2025, Danilo Pianini and contributors + * listed, for each module, in the respective subproject's build.gradle.kts file. + * + * This file is part of Alchemist, and is distributed under the terms of the + * GNU General Public License, with a linking exception, + * as described in the file LICENSE in the Alchemist distribution's top directory. + */ + +package it.unibo.alchemist.boundary.composeui.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import it.unibo.alchemist.boundary.graphql.client.GraphQLClientFactory +import it.unibo.alchemist.boundary.graphql.client.NodesSubscription +import it.unibo.alchemist.boundary.graphql.client.PauseSimulationMutation +import it.unibo.alchemist.boundary.graphql.client.PlaySimulationMutation +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +data class SimulationState(val status: String = "Loading", val time: Double = 0.0) + +class SimulationStatusViewModel : ViewModel() { + private val _uiState = MutableStateFlow(SimulationState()) + val uiState = _uiState.asStateFlow() + + // TODO: parameterize the host and port and separate client in different file + private val client = GraphQLClientFactory.subscriptionClient( + "127.0.0.1", + 3000, + ) + + fun pause() { + viewModelScope.launch { + client.mutation(PauseSimulationMutation()).execute() + } + } + + fun play() { + viewModelScope.launch { + client.mutation(PlaySimulationMutation()).execute() + } + } + + init { + viewModelScope.launch { + client.subscription(NodesSubscription()) + .toFlow() + .collect { res -> + _uiState.update { + val data = res.dataOrThrow() + SimulationState( + status = data.simulation.status, + time = data.simulation.time, + ) + } + } + } + } +} diff --git a/alchemist-composeui/src/jvmMain/kotlin/it/unibo/alchemist/boundary/composeui/ComposeMonitor.kt b/alchemist-composeui/src/jvmMain/kotlin/it/unibo/alchemist/boundary/composeui/ComposeMonitor.kt index 77f47406b5..05ddfff0d0 100644 --- a/alchemist-composeui/src/jvmMain/kotlin/it/unibo/alchemist/boundary/composeui/ComposeMonitor.kt +++ b/alchemist-composeui/src/jvmMain/kotlin/it/unibo/alchemist/boundary/composeui/ComposeMonitor.kt @@ -10,21 +10,33 @@ package it.unibo.alchemist.boundary.composeui import androidx.compose.ui.window.Window -import androidx.compose.ui.window.application +import androidx.compose.ui.window.awaitApplication import it.unibo.alchemist.boundary.OutputMonitor import it.unibo.alchemist.model.Environment +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlin.concurrent.thread /** * Monitor extension that uses JVM Compose UI to display the simulation. */ class ComposeMonitor : OutputMonitor { override fun initialized(environment: Environment) { - application { - Window( - onCloseRequest = { }, - title = "Alchemist", - ) { - app() + thread( + name = "ComposeMonitor", + isDaemon = true, + ) { + runBlocking { + launch { + awaitApplication { + Window( + onCloseRequest = ::exitApplication, + title = "Alchemist", + ) { + app() + } + } + } } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 46a92d512d..0c63e3e35f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ scalacache = "0.28.0" [libraries] androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } antlr4 = { module = "org.antlr:antlr4", version.ref = "antlr4" } antlr4-runtime = { module = "org.antlr:antlr4-runtime", version.ref = "antlr4" } apache-commons-cli = "commons-cli:commons-cli:1.9.0" @@ -73,6 +74,7 @@ kotest-framework-engine = { module = "io.kotest:kotest-framework-engine", versio kotest-runner = { module = "io.kotest:kotest-runner-junit5-jvm", version.ref = "kotest" } kotlin-cli = "org.jetbrains.kotlinx:kotlinx-cli:0.3.6" kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlin-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } kotlin-jvm-plugin = { module = "org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin", version.ref = "kotlin" } kotlin-multiplatform-plugin = { module = "org.jetbrains.kotlin.multiplatform:org.jetbrains.kotlin.multiplatform.gradle.plugin", version.ref = "kotlin" } From dd69fbfe2bfea1d628880ca9f94c375d4d62f1dd Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Tue, 22 Apr 2025 15:29:57 +0200 Subject: [PATCH 03/14] feat: wip subscription management --- .../viewmodels/SimulationStatusViewModel.kt | 27 +++++++++++-------- .../graphql/SimulationStatus.graphql | 1 + 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt index 8d5beff6aa..527d5ca242 100644 --- a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt @@ -12,9 +12,10 @@ package it.unibo.alchemist.boundary.composeui.viewmodels import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import it.unibo.alchemist.boundary.graphql.client.GraphQLClientFactory -import it.unibo.alchemist.boundary.graphql.client.NodesSubscription import it.unibo.alchemist.boundary.graphql.client.PauseSimulationMutation import it.unibo.alchemist.boundary.graphql.client.PlaySimulationMutation +import it.unibo.alchemist.boundary.graphql.client.SimulationStatusQuery +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -46,17 +47,21 @@ class SimulationStatusViewModel : ViewModel() { init { viewModelScope.launch { - client.subscription(NodesSubscription()) - .toFlow() - .collect { res -> - _uiState.update { - val data = res.dataOrThrow() - SimulationState( - status = data.simulation.status, - time = data.simulation.time, - ) + while (true) { + client.query(SimulationStatusQuery()) + .toFlow() + .collect { response -> + response.data?.let { data -> + _uiState.update { + SimulationState( + status = data.simulation.status, + time = data.simulation.time, + ) + } + } } - } + delay(50) + } } } } diff --git a/alchemist-graphql/src/commonMain/resources/graphql/SimulationStatus.graphql b/alchemist-graphql/src/commonMain/resources/graphql/SimulationStatus.graphql index 8751757338..e1ac2aff66 100644 --- a/alchemist-graphql/src/commonMain/resources/graphql/SimulationStatus.graphql +++ b/alchemist-graphql/src/commonMain/resources/graphql/SimulationStatus.graphql @@ -1,5 +1,6 @@ query SimulationStatus { simulation { + time status } } \ No newline at end of file From 27a4173657f643fe73f70647db9c5d1679e79c54 Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Thu, 24 Apr 2025 11:12:54 +0200 Subject: [PATCH 04/14] feat: inital material3 ui, added canvas --- alchemist-composeui/build.gradle.kts | 2 +- .../unibo/alchemist/boundary/composeui/App.kt | 94 +++++++++++++++---- .../viewmodels/SimulationStatusViewModel.kt | 23 +++-- .../graphql/SimulationStatus.graphql | 1 - 4 files changed, 93 insertions(+), 27 deletions(-) diff --git a/alchemist-composeui/build.gradle.kts b/alchemist-composeui/build.gradle.kts index 1bba46306e..26bb63f3b4 100644 --- a/alchemist-composeui/build.gradle.kts +++ b/alchemist-composeui/build.gradle.kts @@ -34,7 +34,7 @@ kotlin { implementation(compose.runtime) implementation(compose.ui) implementation(compose.foundation) - implementation(compose.material) + implementation(compose.material3) implementation(compose.components.resources) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt index 31b7cf5c6f..b9a3dc44ac 100644 --- a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt @@ -9,45 +9,101 @@ package it.unibo.alchemist.boundary.composeui +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.Button -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import it.unibo.alchemist.boundary.composeui.viewmodels.SimulationStatus import it.unibo.alchemist.boundary.composeui.viewmodels.SimulationStatusViewModel /** * Application entry point, this will be rendered the same in all the platforms. */ @Composable -fun app() { - MaterialTheme { - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - simulationStatus() +fun app(viewModel: SimulationStatusViewModel = viewModel { SimulationStatusViewModel() }) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + Scaffold( + topBar = { topBar(uiState) }, + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding).padding(horizontal = 8.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + controlButton(uiState, viewModel::play, viewModel::pause) + OutlinedCard( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + border = BorderStroke(1.dp, Color.Black), + ) { + Canvas(modifier = Modifier.fillMaxSize()) { + } + } } } } +/** + * Top bar put on top of the application. + */ +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun simulationStatus(viewModel: SimulationStatusViewModel = viewModel { SimulationStatusViewModel() }) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - Text("Simulation status: ${uiState.status}") - Text("Simulation time: ${uiState.time}") - Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { - Button(onClick = { viewModel.pause() }) { - Text("Pause") +fun topBar(status: SimulationStatus) { + TopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text( + "Simulation: $status", + ) + }, + ) +} + +/** + * Button to control the simulation. + */ +@Composable +fun controlButton(status: SimulationStatus, resume: () -> Unit, pause: () -> Unit) { + if (status == SimulationStatus.Running) { + Button(onClick = { pause() }) { + Text("Pause", modifier = Modifier.padding(8.dp)) } - Button(onClick = { viewModel.play() }) { - Text("Play") + } else { + Button(onClick = { resume() }) { + Text("Resume", modifier = Modifier.padding(8.dp)) } } } + +/** + * Node graphical representation. + */ +@Composable +fun node(drawScope: DrawScope, id: Int) { + val x = (drawScope.size.width / 2) + (id * 10) + val y = (drawScope.size.height / 2) + (id * 10) + drawScope.drawCircle(Color.White, radius = 10f, center = Offset(x, y)) +} diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt index 527d5ca242..4141a2ff58 100644 --- a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt @@ -21,10 +21,16 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -data class SimulationState(val status: String = "Loading", val time: Double = 0.0) +enum class SimulationStatus { + Init, + Ready, + Paused, + Running, + Terminated, +} class SimulationStatusViewModel : ViewModel() { - private val _uiState = MutableStateFlow(SimulationState()) + private val _uiState = MutableStateFlow(SimulationStatus.Init) val uiState = _uiState.asStateFlow() // TODO: parameterize the host and port and separate client in different file @@ -53,10 +59,15 @@ class SimulationStatusViewModel : ViewModel() { .collect { response -> response.data?.let { data -> _uiState.update { - SimulationState( - status = data.simulation.status, - time = data.simulation.time, - ) + // True correlation can be achieved only moving + // alchemist-api Status enum class to commonMain + when (data.simulation.status) { + "READY" -> SimulationStatus.Ready + "PAUSED" -> SimulationStatus.Paused + "RUNNING" -> SimulationStatus.Running + "TERMINATED" -> SimulationStatus.Terminated + else -> SimulationStatus.Init + } } } } diff --git a/alchemist-graphql/src/commonMain/resources/graphql/SimulationStatus.graphql b/alchemist-graphql/src/commonMain/resources/graphql/SimulationStatus.graphql index e1ac2aff66..8751757338 100644 --- a/alchemist-graphql/src/commonMain/resources/graphql/SimulationStatus.graphql +++ b/alchemist-graphql/src/commonMain/resources/graphql/SimulationStatus.graphql @@ -1,6 +1,5 @@ query SimulationStatus { simulation { - time status } } \ No newline at end of file From 63f2e7dc040dd74f8f673fd9d97eb9d48a33c098 Mon Sep 17 00:00:00 2001 From: AngeloFilaseta Date: Thu, 24 Apr 2025 11:14:30 +0200 Subject: [PATCH 05/14] feat(graphql-surrogate): add step to graphql simulation surrogate --- .../graphql/schema/model/surrogates/SimulationSurrogate.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/alchemist-graphql-surrogates/src/main/kotlin/it/unibo/alchemist/boundary/graphql/schema/model/surrogates/SimulationSurrogate.kt b/alchemist-graphql-surrogates/src/main/kotlin/it/unibo/alchemist/boundary/graphql/schema/model/surrogates/SimulationSurrogate.kt index f4c747f2bf..e245666cdb 100644 --- a/alchemist-graphql-surrogates/src/main/kotlin/it/unibo/alchemist/boundary/graphql/schema/model/surrogates/SimulationSurrogate.kt +++ b/alchemist-graphql-surrogates/src/main/kotlin/it/unibo/alchemist/boundary/graphql/schema/model/surrogates/SimulationSurrogate.kt @@ -34,6 +34,12 @@ data class SimulationSurrogate>(@GraphQLIgnore override v @GraphQLDescription("The time of the simulation") fun time(): Double = origin.time.toDouble() + /** + * The step at which the simulation is. + */ + @GraphQLDescription("The step at which the simulation is") + fun step(): String = origin.step.toString() + /** * The environment of the simulation. */ From 382bc81ad12c0316d7775b70aeb41d37a965b092 Mon Sep 17 00:00:00 2001 From: "Danilo Pianini [bot]" Date: Thu, 24 Apr 2025 09:21:06 +0000 Subject: [PATCH 06/14] chore(build): actualize the `yarn.lock` file --- kotlin-js-store/yarn.lock | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 3fc6f0615f..8e85a47f6b 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -59,6 +59,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@js-joda/core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" + integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.5" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" From b4473deaae2630f3574d763dffdf074118a16188 Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Thu, 24 Apr 2025 16:10:17 +0200 Subject: [PATCH 07/14] feat: added alert in case of error --- .../unibo/alchemist/boundary/composeui/App.kt | 42 ++++++++++++------ .../viewmodels/SimulationStatusViewModel.kt | 44 +++++++++++++++++-- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt index b9a3dc44ac..10fdbffbb5 100644 --- a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt @@ -10,11 +10,11 @@ package it.unibo.alchemist.boundary.composeui import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -27,12 +27,11 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.apollographql.apollo3.api.Error import it.unibo.alchemist.boundary.composeui.viewmodels.SimulationStatus import it.unibo.alchemist.boundary.composeui.viewmodels.SimulationStatusViewModel @@ -41,22 +40,27 @@ import it.unibo.alchemist.boundary.composeui.viewmodels.SimulationStatusViewMode */ @Composable fun app(viewModel: SimulationStatusViewModel = viewModel { SimulationStatusViewModel() }) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val simulationStatus by viewModel.simulationStatus.collectAsStateWithLifecycle() + val time by viewModel.time.collectAsStateWithLifecycle() + val errors by viewModel.errors.collectAsStateWithLifecycle() Scaffold( - topBar = { topBar(uiState) }, + topBar = { topBar(simulationStatus) }, ) { innerPadding -> Column( modifier = Modifier.padding(innerPadding).padding(horizontal = 8.dp, vertical = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - controlButton(uiState, viewModel::play, viewModel::pause) + controlButton(simulationStatus, viewModel::play, viewModel::pause) OutlinedCard( + modifier = Modifier.fillMaxSize(), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface, ), border = BorderStroke(1.dp, Color.Black), ) { - Canvas(modifier = Modifier.fillMaxSize()) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("Time: $time") + errorDialog(viewModel::monitor, errors) } } } @@ -98,12 +102,22 @@ fun controlButton(status: SimulationStatus, resume: () -> Unit, pause: () -> Uni } } -/** - * Node graphical representation. - */ @Composable -fun node(drawScope: DrawScope, id: Int) { - val x = (drawScope.size.width / 2) + (id * 10) - val y = (drawScope.size.height / 2) + (id * 10) - drawScope.drawCircle(Color.White, radius = 10f, center = Offset(x, y)) +fun errorDialog(dismiss: () -> Unit, errors: List?) { + if (!errors.isNullOrEmpty()) { + AlertDialog( + onDismissRequest = dismiss, + title = { Text("Error") }, + text = { + for (error in errors) { + Text(error.message) + } + }, + confirmButton = { + Button(onClick = dismiss) { + Text("OK") + } + }, + ) + } } diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt index 4141a2ff58..42af9fd557 100644 --- a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt @@ -11,7 +11,9 @@ package it.unibo.alchemist.boundary.composeui.viewmodels import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.apollographql.apollo3.api.Error import it.unibo.alchemist.boundary.graphql.client.GraphQLClientFactory +import it.unibo.alchemist.boundary.graphql.client.NodesSubscription import it.unibo.alchemist.boundary.graphql.client.PauseSimulationMutation import it.unibo.alchemist.boundary.graphql.client.PlaySimulationMutation import it.unibo.alchemist.boundary.graphql.client.SimulationStatusQuery @@ -29,9 +31,19 @@ enum class SimulationStatus { Terminated, } +var i = 0 +var j = 0 + class SimulationStatusViewModel : ViewModel() { - private val _uiState = MutableStateFlow(SimulationStatus.Init) - val uiState = _uiState.asStateFlow() + private val _simulationStatus = MutableStateFlow(SimulationStatus.Init) + val simulationStatus = _simulationStatus.asStateFlow() + + private val _time = MutableStateFlow(0.0) + val time = _time.asStateFlow() + private var tempTime = 0.0 + + private val _errors = MutableStateFlow>(emptyList()) + val errors = _errors.asStateFlow() // TODO: parameterize the host and port and separate client in different file private val client = GraphQLClientFactory.subscriptionClient( @@ -51,14 +63,40 @@ class SimulationStatusViewModel : ViewModel() { } } + fun monitor() { + _errors.value = emptyList() + viewModelScope.launch { + client.subscription(NodesSubscription()) + .toFlow() + .collect { response -> + if (response.hasErrors()) { + response.errors?.let { errors -> + _errors.update { errors } + } + } + i++ + println("data received $i") + if (i > 99) { + i = 0 + j++ + println("fetching data $j") + response.data?.let { data -> + _time.value = data.simulation.time + } + } + } + } + } + init { + monitor() viewModelScope.launch { while (true) { client.query(SimulationStatusQuery()) .toFlow() .collect { response -> response.data?.let { data -> - _uiState.update { + _simulationStatus.update { // True correlation can be achieved only moving // alchemist-api Status enum class to commonMain when (data.simulation.status) { From a303301e0c67be10b9c6580a4382dd9c987b8840 Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Sun, 4 May 2025 18:00:51 +0200 Subject: [PATCH 08/14] feat: added simulation and node subscription --- .../unibo/alchemist/boundary/composeui/App.kt | 21 +++--- .../boundary/composeui/NodeDrawer.kt | 27 +++++++ .../composeui/viewmodels/NodeViewModel.kt | 71 ++++++++++++++++++ ...tusViewModel.kt => SimulationViewModel.kt} | 72 +++++++------------ .../resources/graphql/NodeInfo.graphql | 17 +++++ .../graphql/NodesSubscription.graphql | 18 ----- .../graphql/SimulationSubscription.graphql | 16 +++++ 7 files changed, 170 insertions(+), 72 deletions(-) create mode 100644 alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/NodeDrawer.kt create mode 100644 alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/NodeViewModel.kt rename alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/{SimulationStatusViewModel.kt => SimulationViewModel.kt} (52%) create mode 100644 alchemist-graphql/src/commonMain/resources/graphql/NodeInfo.graphql delete mode 100644 alchemist-graphql/src/commonMain/resources/graphql/NodesSubscription.graphql create mode 100644 alchemist-graphql/src/commonMain/resources/graphql/SimulationSubscription.graphql diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt index 10fdbffbb5..f47206b08d 100644 --- a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt @@ -33,24 +33,24 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.apollographql.apollo3.api.Error import it.unibo.alchemist.boundary.composeui.viewmodels.SimulationStatus -import it.unibo.alchemist.boundary.composeui.viewmodels.SimulationStatusViewModel +import it.unibo.alchemist.boundary.composeui.viewmodels.SimulationViewModel /** * Application entry point, this will be rendered the same in all the platforms. */ @Composable -fun app(viewModel: SimulationStatusViewModel = viewModel { SimulationStatusViewModel() }) { - val simulationStatus by viewModel.simulationStatus.collectAsStateWithLifecycle() - val time by viewModel.time.collectAsStateWithLifecycle() +fun app(viewModel: SimulationViewModel = viewModel { SimulationViewModel() }) { + val status by viewModel.status.collectAsStateWithLifecycle() val errors by viewModel.errors.collectAsStateWithLifecycle() + val nodes by viewModel.nodes.collectAsStateWithLifecycle() Scaffold( - topBar = { topBar(simulationStatus) }, + topBar = { topBar(status) }, ) { innerPadding -> Column( modifier = Modifier.padding(innerPadding).padding(horizontal = 8.dp, vertical = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - controlButton(simulationStatus, viewModel::play, viewModel::pause) + controlButton(status, viewModel::play, viewModel::pause) OutlinedCard( modifier = Modifier.fillMaxSize(), colors = CardDefaults.cardColors( @@ -59,8 +59,10 @@ fun app(viewModel: SimulationStatusViewModel = viewModel { SimulationStatusViewM border = BorderStroke(1.dp, Color.Black), ) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - Text("Time: $time") - errorDialog(viewModel::monitor, errors) + errorDialog(viewModel::fetch, errors) + for (node in nodes) { + nodeDrawer(node.id) + } } } } @@ -102,6 +104,9 @@ fun controlButton(status: SimulationStatus, resume: () -> Unit, pause: () -> Uni } } +/** + * Display the error dialog, currently used to circumvent the null issue we're facing when subscribing to simulation. + */ @Composable fun errorDialog(dismiss: () -> Unit, errors: List?) { if (!errors.isNullOrEmpty()) { diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/NodeDrawer.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/NodeDrawer.kt new file mode 100644 index 0000000000..8f83e78148 --- /dev/null +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/NodeDrawer.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2010-2025, Danilo Pianini and contributors + * listed, for each module, in the respective subproject's build.gradle.kts file. + * + * This file is part of Alchemist, and is distributed under the terms of the + * GNU General Public License, with a linking exception, + * as described in the file LICENSE in the Alchemist distribution's top directory. + */ + +package it.unibo.alchemist.boundary.composeui + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import it.unibo.alchemist.boundary.composeui.viewmodels.NodeViewModel + +/** + * Display the information of a node, subscribing to its own channel for data. + */ +@Composable +fun nodeDrawer(nodeId: Int) { + val nodeModel: NodeViewModel = viewModel(key = "node-$nodeId") { NodeViewModel(nodeId) } + val nodeInfo by nodeModel.nodeInfo.collectAsStateWithLifecycle() + Text("Node $nodeId: $nodeInfo") +} diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/NodeViewModel.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/NodeViewModel.kt new file mode 100644 index 0000000000..50614adaee --- /dev/null +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/NodeViewModel.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010-2025, Danilo Pianini and contributors + * listed, for each module, in the respective subproject's build.gradle.kts file. + * + * This file is part of Alchemist, and is distributed under the terms of the + * GNU General Public License, with a linking exception, + * as described in the file LICENSE in the Alchemist distribution's top directory. + */ + +package it.unibo.alchemist.boundary.composeui.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.apollographql.apollo3.api.Error +import it.unibo.alchemist.boundary.graphql.client.GraphQLClientFactory +import it.unibo.alchemist.boundary.graphql.client.NodeInfoSubscription +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +data class Molecule(val name: String) + +data class MoleculeConcentration(val concentration: String, val molecule: Molecule) + +data class NodeInfo( + val id: Int, + val moleculeCount: Int, + val properties: List, + val contents: List, +) + +class NodeViewModel(private val nodeId: Int) : ViewModel() { + private val _nodeInfo = MutableStateFlow(null) + val nodeInfo = _nodeInfo.asStateFlow() + + private val _errors = MutableStateFlow>(emptyList()) + val errors = _errors.asStateFlow() + + // TODO: parameterize the host and port and separate client in different file + private val client = GraphQLClientFactory.subscriptionClient( + "127.0.0.1", + 3000, + ) + + private fun load() { + _errors.value = emptyList() + viewModelScope.launch { + client.subscription(NodeInfoSubscription(nodeId)) + .toFlow() + .collect { response -> + response.data?.let { data -> + _nodeInfo.value = NodeInfo( + data.environment.nodeById.id, + data.environment.nodeById.moleculeCount, + data.environment.nodeById.properties, + data.environment.nodeById.contents.entries.map { + MoleculeConcentration( + it.concentration, + Molecule(it.molecule.name), + ) + }, + ) + } + } + } + } + + init { + load() + } +} diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationViewModel.kt similarity index 52% rename from alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt rename to alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationViewModel.kt index 42af9fd557..0c1d58fd66 100644 --- a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationStatusViewModel.kt +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationViewModel.kt @@ -13,11 +13,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.apollographql.apollo3.api.Error import it.unibo.alchemist.boundary.graphql.client.GraphQLClientFactory -import it.unibo.alchemist.boundary.graphql.client.NodesSubscription import it.unibo.alchemist.boundary.graphql.client.PauseSimulationMutation import it.unibo.alchemist.boundary.graphql.client.PlaySimulationMutation -import it.unibo.alchemist.boundary.graphql.client.SimulationStatusQuery -import kotlinx.coroutines.delay +import it.unibo.alchemist.boundary.graphql.client.SimulationSubscription import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -31,18 +29,15 @@ enum class SimulationStatus { Terminated, } -var i = 0 -var j = 0 - -class SimulationStatusViewModel : ViewModel() { - private val _simulationStatus = MutableStateFlow(SimulationStatus.Init) - val simulationStatus = _simulationStatus.asStateFlow() - - private val _time = MutableStateFlow(0.0) - val time = _time.asStateFlow() - private var tempTime = 0.0 +data class Node(val id: Int, val coordinates: List) +class SimulationViewModel : ViewModel() { + private val _nodes = MutableStateFlow>(emptyList()) + private val _status = MutableStateFlow(SimulationStatus.Init) private val _errors = MutableStateFlow>(emptyList()) + + val nodes = _nodes.asStateFlow() + val status = _status.asStateFlow() val errors = _errors.asStateFlow() // TODO: parameterize the host and port and separate client in different file @@ -63,10 +58,10 @@ class SimulationStatusViewModel : ViewModel() { } } - fun monitor() { + fun fetch() { _errors.value = emptyList() viewModelScope.launch { - client.subscription(NodesSubscription()) + client.subscription(SimulationSubscription()) .toFlow() .collect { response -> if (response.hasErrors()) { @@ -74,14 +69,21 @@ class SimulationStatusViewModel : ViewModel() { _errors.update { errors } } } - i++ - println("data received $i") - if (i > 99) { - i = 0 - j++ - println("fetching data $j") - response.data?.let { data -> - _time.value = data.simulation.time + response.data?.let { data -> + _status.update { + when (data.simulation.status) { + "READY" -> SimulationStatus.Ready + "PAUSED" -> SimulationStatus.Paused + "RUNNING" -> SimulationStatus.Running + "TERMINATED" -> SimulationStatus.Terminated + else -> SimulationStatus.Init + } + } + _nodes.value = data.simulation.environment.nodeToPos.entries.map { + Node( + id = it.id, + coordinates = it.position.coordinates, + ) } } } @@ -89,28 +91,6 @@ class SimulationStatusViewModel : ViewModel() { } init { - monitor() - viewModelScope.launch { - while (true) { - client.query(SimulationStatusQuery()) - .toFlow() - .collect { response -> - response.data?.let { data -> - _simulationStatus.update { - // True correlation can be achieved only moving - // alchemist-api Status enum class to commonMain - when (data.simulation.status) { - "READY" -> SimulationStatus.Ready - "PAUSED" -> SimulationStatus.Paused - "RUNNING" -> SimulationStatus.Running - "TERMINATED" -> SimulationStatus.Terminated - else -> SimulationStatus.Init - } - } - } - } - delay(50) - } - } + fetch() } } diff --git a/alchemist-graphql/src/commonMain/resources/graphql/NodeInfo.graphql b/alchemist-graphql/src/commonMain/resources/graphql/NodeInfo.graphql new file mode 100644 index 0000000000..a0f0e1302c --- /dev/null +++ b/alchemist-graphql/src/commonMain/resources/graphql/NodeInfo.graphql @@ -0,0 +1,17 @@ +subscription NodeInfo($id: Int!) { + environment { + nodeById(id: $id) { + id + moleculeCount + properties + contents { + entries { + concentration + molecule { + name + } + } + } + } + } +} \ No newline at end of file diff --git a/alchemist-graphql/src/commonMain/resources/graphql/NodesSubscription.graphql b/alchemist-graphql/src/commonMain/resources/graphql/NodesSubscription.graphql deleted file mode 100644 index 4a4f29dfa2..0000000000 --- a/alchemist-graphql/src/commonMain/resources/graphql/NodesSubscription.graphql +++ /dev/null @@ -1,18 +0,0 @@ -subscription NodesSubscription { - simulation { - status - time - environment { - nodes { - contents { - entries { - molecule { - name - } - concentration - } - } - } - } - } -} diff --git a/alchemist-graphql/src/commonMain/resources/graphql/SimulationSubscription.graphql b/alchemist-graphql/src/commonMain/resources/graphql/SimulationSubscription.graphql new file mode 100644 index 0000000000..00f49426ec --- /dev/null +++ b/alchemist-graphql/src/commonMain/resources/graphql/SimulationSubscription.graphql @@ -0,0 +1,16 @@ +subscription SimulationSubscription { + simulation { + status + environment { + nodeToPos { + entries { + id + position { + coordinates + dimensions + } + } + } + } + } +} \ No newline at end of file From b86809f5149264ece45e34dba62567cd3a50827f Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Sun, 11 May 2025 22:19:19 +0200 Subject: [PATCH 09/14] feat: vertical scrolling --- .../kotlin/it/unibo/alchemist/boundary/composeui/App.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt index f47206b08d..a5705bd7c2 100644 --- a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt @@ -14,6 +14,8 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CardDefaults @@ -58,7 +60,12 @@ fun app(viewModel: SimulationViewModel = viewModel { SimulationViewModel() }) { ), border = BorderStroke(1.dp, Color.Black), ) { - Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.verticalScroll( + rememberScrollState(), + ), + ) { errorDialog(viewModel::fetch, errors) for (node in nodes) { nodeDrawer(node.id) From a62762e855ab6638fcae758426effc2f1b656816 Mon Sep 17 00:00:00 2001 From: "Danilo Pianini [bot]" Date: Sun, 11 May 2025 20:23:55 +0000 Subject: [PATCH 10/14] chore(build): update the javadoc.io cache --- javadoc-io.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/javadoc-io.json b/javadoc-io.json index 1f875dc79e..c3339027b7 100644 --- a/javadoc-io.json +++ b/javadoc-io.json @@ -258,6 +258,15 @@ "org.jetbrains.compose.material/material/1.8.1": { "first": "https://javadoc.io/doc/org.jetbrains.compose.material/material/1.8.1" }, + "org.jetbrains.androidx.lifecycle/lifecycle-runtime-compose/2.8.4": { + "first": "https://javadoc.io/doc/org.jetbrains.androidx.lifecycle/lifecycle-runtime-compose/2.8.4" + }, + "org.jetbrains.androidx.lifecycle/lifecycle-viewmodel-compose/2.8.4": { + "first": "https://javadoc.io/doc/org.jetbrains.androidx.lifecycle/lifecycle-viewmodel-compose/2.8.4" + }, + "org.jetbrains.compose.desktop/desktop-jvm-linux-x64/1.8.0": { + "first": "https://javadoc.io/doc/org.jetbrains.compose.desktop/desktop-jvm-linux-x64/1.8.0" + }, "org.jetbrains.compose.runtime/runtime/1.8.1": { "first": "https://javadoc.io/doc/org.jetbrains.compose.runtime/runtime/1.8.1" }, @@ -330,6 +339,9 @@ "org.jetbrains.kotlinx/kotlinx-coroutines-core/1.10.2": { "first": "https://javadoc.io/doc/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.10.2" }, + "org.jetbrains.kotlinx/kotlinx-coroutines-swing/1.10.2": { + "first": "https://javadoc.io/doc/org.jetbrains.kotlinx/kotlinx-coroutines-swing/1.10.2" + }, "org.jetbrains.kotlinx/kotlinx-coroutines-test/1.10.2": { "first": "https://javadoc.io/doc/org.jetbrains.kotlinx/kotlinx-coroutines-test/1.10.2" }, From 36ffb657071011fac3a34a09fc06c6270f0ad9aa Mon Sep 17 00:00:00 2001 From: "Danilo Pianini [bot]" Date: Tue, 20 May 2025 13:42:03 +0000 Subject: [PATCH 11/14] chore(build): update the javadoc.io cache --- javadoc-io.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/javadoc-io.json b/javadoc-io.json index c3339027b7..4ddc9410be 100644 --- a/javadoc-io.json +++ b/javadoc-io.json @@ -258,11 +258,11 @@ "org.jetbrains.compose.material/material/1.8.1": { "first": "https://javadoc.io/doc/org.jetbrains.compose.material/material/1.8.1" }, - "org.jetbrains.androidx.lifecycle/lifecycle-runtime-compose/2.8.4": { - "first": "https://javadoc.io/doc/org.jetbrains.androidx.lifecycle/lifecycle-runtime-compose/2.8.4" + "org.jetbrains.androidx.lifecycle/lifecycle-runtime-compose/2.9.0": { + "first": "https://javadoc.io/doc/org.jetbrains.androidx.lifecycle/lifecycle-runtime-compose/2.9.0" }, - "org.jetbrains.androidx.lifecycle/lifecycle-viewmodel-compose/2.8.4": { - "first": "https://javadoc.io/doc/org.jetbrains.androidx.lifecycle/lifecycle-viewmodel-compose/2.8.4" + "org.jetbrains.androidx.lifecycle/lifecycle-viewmodel-compose/2.9.0": { + "first": "https://javadoc.io/doc/org.jetbrains.androidx.lifecycle/lifecycle-viewmodel-compose/2.9.0" }, "org.jetbrains.compose.desktop/desktop-jvm-linux-x64/1.8.0": { "first": "https://javadoc.io/doc/org.jetbrains.compose.desktop/desktop-jvm-linux-x64/1.8.0" From 014467123be1af1ae2c8355a6ae9b162a9e1c563 Mon Sep 17 00:00:00 2001 From: "Danilo Pianini [bot]" Date: Tue, 20 May 2025 14:49:10 +0000 Subject: [PATCH 12/14] chore(build): update the javadoc.io cache --- javadoc-io.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/javadoc-io.json b/javadoc-io.json index 4ddc9410be..bdd4898408 100644 --- a/javadoc-io.json +++ b/javadoc-io.json @@ -264,12 +264,15 @@ "org.jetbrains.androidx.lifecycle/lifecycle-viewmodel-compose/2.9.0": { "first": "https://javadoc.io/doc/org.jetbrains.androidx.lifecycle/lifecycle-viewmodel-compose/2.9.0" }, - "org.jetbrains.compose.desktop/desktop-jvm-linux-x64/1.8.0": { - "first": "https://javadoc.io/doc/org.jetbrains.compose.desktop/desktop-jvm-linux-x64/1.8.0" - }, "org.jetbrains.compose.runtime/runtime/1.8.1": { "first": "https://javadoc.io/doc/org.jetbrains.compose.runtime/runtime/1.8.1" }, + "org.jetbrains.compose.desktop/desktop-jvm-linux-x64/1.8.1": { + "first": "https://javadoc.io/doc/org.jetbrains.compose.desktop/desktop-jvm-linux-x64/1.8.1" + }, + "org.jetbrains.compose.material3/material3/1.8.1": { + "first": "https://javadoc.io/doc/org.jetbrains.compose.material3/material3/1.8.1" + }, "org.jetbrains.compose.ui/ui/1.8.1": { "first": "https://javadoc.io/doc/org.jetbrains.compose.ui/ui/1.8.1" }, From 5e6e6f6c6185098d09023c4135724d729516c05b Mon Sep 17 00:00:00 2001 From: "Danilo Pianini [bot]" Date: Mon, 26 May 2025 19:44:03 +0000 Subject: [PATCH 13/14] chore(build): update the javadoc.io cache --- javadoc-io.json | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/javadoc-io.json b/javadoc-io.json index bdd4898408..c630afc0b5 100644 --- a/javadoc-io.json +++ b/javadoc-io.json @@ -249,30 +249,27 @@ "first": "https://javadoc.io/doc/org.graphstream/gs-core/2.0", "second": "https://javadoc.io/doc/org.graphstream/gs-core/2.0/element-list" }, - "org.jetbrains.compose.components/components-resources/1.8.1": { - "first": "https://javadoc.io/doc/org.jetbrains.compose.components/components-resources/1.8.1" - }, - "org.jetbrains.compose.foundation/foundation/1.8.1": { - "first": "https://javadoc.io/doc/org.jetbrains.compose.foundation/foundation/1.8.1" - }, - "org.jetbrains.compose.material/material/1.8.1": { - "first": "https://javadoc.io/doc/org.jetbrains.compose.material/material/1.8.1" - }, "org.jetbrains.androidx.lifecycle/lifecycle-runtime-compose/2.9.0": { "first": "https://javadoc.io/doc/org.jetbrains.androidx.lifecycle/lifecycle-runtime-compose/2.9.0" }, "org.jetbrains.androidx.lifecycle/lifecycle-viewmodel-compose/2.9.0": { "first": "https://javadoc.io/doc/org.jetbrains.androidx.lifecycle/lifecycle-viewmodel-compose/2.9.0" }, - "org.jetbrains.compose.runtime/runtime/1.8.1": { - "first": "https://javadoc.io/doc/org.jetbrains.compose.runtime/runtime/1.8.1" + "org.jetbrains.compose.components/components-resources/1.8.1": { + "first": "https://javadoc.io/doc/org.jetbrains.compose.components/components-resources/1.8.1" }, "org.jetbrains.compose.desktop/desktop-jvm-linux-x64/1.8.1": { "first": "https://javadoc.io/doc/org.jetbrains.compose.desktop/desktop-jvm-linux-x64/1.8.1" }, + "org.jetbrains.compose.foundation/foundation/1.8.1": { + "first": "https://javadoc.io/doc/org.jetbrains.compose.foundation/foundation/1.8.1" + }, "org.jetbrains.compose.material3/material3/1.8.1": { "first": "https://javadoc.io/doc/org.jetbrains.compose.material3/material3/1.8.1" }, + "org.jetbrains.compose.runtime/runtime/1.8.1": { + "first": "https://javadoc.io/doc/org.jetbrains.compose.runtime/runtime/1.8.1" + }, "org.jetbrains.compose.ui/ui/1.8.1": { "first": "https://javadoc.io/doc/org.jetbrains.compose.ui/ui/1.8.1" }, From 08586aa8f1f425d78be87a30b093a3537f962269 Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Sun, 1 Jun 2025 10:30:37 +0200 Subject: [PATCH 14/14] fix: styling fix and review --- alchemist-composeui/build.gradle.kts | 2 +- .../unibo/alchemist/boundary/composeui/App.kt | 48 +++++++++---------- .../boundary/composeui/NodeDrawer.kt | 2 +- .../alchemist/boundary/composeui/Main.kt | 2 +- .../boundary/composeui/ComposeMonitor.kt | 7 +-- .../alchemist/boundary/composeui/Main.kt | 2 +- 6 files changed, 29 insertions(+), 34 deletions(-) diff --git a/alchemist-composeui/build.gradle.kts b/alchemist-composeui/build.gradle.kts index 26bb63f3b4..f671732845 100644 --- a/alchemist-composeui/build.gradle.kts +++ b/alchemist-composeui/build.gradle.kts @@ -31,6 +31,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { + api(alchemist("graphql")) implementation(compose.runtime) implementation(compose.ui) implementation(compose.foundation) @@ -39,7 +40,6 @@ kotlin { implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.apollo.runtime) - api(alchemist("graphql")) } } diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt index a5705bd7c2..c3f15d20bc 100644 --- a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt @@ -41,18 +41,18 @@ import it.unibo.alchemist.boundary.composeui.viewmodels.SimulationViewModel * Application entry point, this will be rendered the same in all the platforms. */ @Composable -fun app(viewModel: SimulationViewModel = viewModel { SimulationViewModel() }) { +fun App(viewModel: SimulationViewModel = viewModel { SimulationViewModel() }) { val status by viewModel.status.collectAsStateWithLifecycle() val errors by viewModel.errors.collectAsStateWithLifecycle() val nodes by viewModel.nodes.collectAsStateWithLifecycle() Scaffold( - topBar = { topBar(status) }, + topBar = { TopBar(status) }, ) { innerPadding -> Column( modifier = Modifier.padding(innerPadding).padding(horizontal = 8.dp, vertical = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - controlButton(status, viewModel::play, viewModel::pause) + ControlButton(status, viewModel::play, viewModel::pause) OutlinedCard( modifier = Modifier.fillMaxSize(), colors = CardDefaults.cardColors( @@ -66,9 +66,11 @@ fun app(viewModel: SimulationViewModel = viewModel { SimulationViewModel() }) { rememberScrollState(), ), ) { - errorDialog(viewModel::fetch, errors) + if (errors.isNotEmpty()) { + ErrorDialog(viewModel::fetch, errors) + } for (node in nodes) { - nodeDrawer(node.id) + NodeDrawer(node.id) } } } @@ -81,7 +83,7 @@ fun app(viewModel: SimulationViewModel = viewModel { SimulationViewModel() }) { */ @OptIn(ExperimentalMaterial3Api::class) @Composable -fun topBar(status: SimulationStatus) { +fun TopBar(status: SimulationStatus) { TopAppBar( colors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = MaterialTheme.colorScheme.primaryContainer, @@ -99,7 +101,7 @@ fun topBar(status: SimulationStatus) { * Button to control the simulation. */ @Composable -fun controlButton(status: SimulationStatus, resume: () -> Unit, pause: () -> Unit) { +fun ControlButton(status: SimulationStatus, resume: () -> Unit, pause: () -> Unit) { if (status == SimulationStatus.Running) { Button(onClick = { pause() }) { Text("Pause", modifier = Modifier.padding(8.dp)) @@ -115,21 +117,19 @@ fun controlButton(status: SimulationStatus, resume: () -> Unit, pause: () -> Uni * Display the error dialog, currently used to circumvent the null issue we're facing when subscribing to simulation. */ @Composable -fun errorDialog(dismiss: () -> Unit, errors: List?) { - if (!errors.isNullOrEmpty()) { - AlertDialog( - onDismissRequest = dismiss, - title = { Text("Error") }, - text = { - for (error in errors) { - Text(error.message) - } - }, - confirmButton = { - Button(onClick = dismiss) { - Text("OK") - } - }, - ) - } +fun ErrorDialog(dismiss: () -> Unit, errors: List) { + AlertDialog( + onDismissRequest = dismiss, + title = { Text("Error") }, + text = { + for (error in errors) { + Text(error.message) + } + }, + confirmButton = { + Button(onClick = dismiss) { + Text("OK") + } + }, + ) } diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/NodeDrawer.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/NodeDrawer.kt index 8f83e78148..3ba4e301dc 100644 --- a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/NodeDrawer.kt +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/NodeDrawer.kt @@ -20,7 +20,7 @@ import it.unibo.alchemist.boundary.composeui.viewmodels.NodeViewModel * Display the information of a node, subscribing to its own channel for data. */ @Composable -fun nodeDrawer(nodeId: Int) { +fun NodeDrawer(nodeId: Int) { val nodeModel: NodeViewModel = viewModel(key = "node-$nodeId") { NodeViewModel(nodeId) } val nodeInfo by nodeModel.nodeInfo.collectAsStateWithLifecycle() Text("Node $nodeId: $nodeInfo") diff --git a/alchemist-composeui/src/jsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt b/alchemist-composeui/src/jsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt index 9811caadc1..b8ef9b2cab 100644 --- a/alchemist-composeui/src/jsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt +++ b/alchemist-composeui/src/jsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt @@ -21,7 +21,7 @@ import org.jetbrains.skiko.wasm.onWasmReady fun main() { onWasmReady { ComposeViewport(checkNotNull(document.body)) { - app() + App() } } } diff --git a/alchemist-composeui/src/jvmMain/kotlin/it/unibo/alchemist/boundary/composeui/ComposeMonitor.kt b/alchemist-composeui/src/jvmMain/kotlin/it/unibo/alchemist/boundary/composeui/ComposeMonitor.kt index 05ddfff0d0..ae3df386ab 100644 --- a/alchemist-composeui/src/jvmMain/kotlin/it/unibo/alchemist/boundary/composeui/ComposeMonitor.kt +++ b/alchemist-composeui/src/jvmMain/kotlin/it/unibo/alchemist/boundary/composeui/ComposeMonitor.kt @@ -29,12 +29,7 @@ class ComposeMonitor : OutputMonitor { runBlocking { launch { awaitApplication { - Window( - onCloseRequest = ::exitApplication, - title = "Alchemist", - ) { - app() - } + Window(onCloseRequest = ::exitApplication, title = "Alchemist") { App() } } } } diff --git a/alchemist-composeui/src/wasmJsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt b/alchemist-composeui/src/wasmJsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt index 76a83dea20..c8b1253dd4 100644 --- a/alchemist-composeui/src/wasmJsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt +++ b/alchemist-composeui/src/wasmJsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt @@ -19,6 +19,6 @@ import kotlinx.browser.document @OptIn(ExperimentalComposeUiApi::class) fun main() { ComposeViewport(checkNotNull(document.body)) { - app() + App() } }