diff --git a/build.gradle.kts b/build.gradle.kts index da46dc8b3..6e257573d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,7 @@ plugins { id("kool.androidlib-conventions") apply false id("kool.lib-conventions") apply false id("kool.publish-conventions") apply false + alias(libs.plugins.compose.compiler) apply false } allprojects { diff --git a/buildSrc/src/main/kotlin/kool.lib-conventions.gradle.kts b/buildSrc/src/main/kotlin/kool.lib-conventions.gradle.kts index da1a3e0b2..6ee85b01f 100644 --- a/buildSrc/src/main/kotlin/kool.lib-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/kool.lib-conventions.gradle.kts @@ -38,6 +38,7 @@ kotlin { optIn("kotlin.contracts.ExperimentalContracts") optIn("kotlin.io.encoding.ExperimentalEncodingApi") optIn("kotlin.ExperimentalStdlibApi") + optIn("de.fabmax.kool.InternalKoolAPI") } } sourceSets { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 740c3dedc..86d75910f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,7 @@ [versions] agp = "8.12.3" +compose = "1.9.3" +compose-mini = "0.0.1" kotlin = "2.3.0" kotlin-coroutines = "1.10.2" kotlin-serialization = "1.9.0" @@ -51,8 +53,11 @@ physx-wasm = { group = "npm", name = "physx-js-webidl", version.ref = "physx-was box2d-jni = { group = "de.fabmax.box2d-jni", name = "box2d-jni", version.ref = "box2d-jni" } box2d-android = { group = "de.fabmax.box2d-jni", name = "box2d-jni-android", version.ref = "box2d-jni" } box2d-wasm = { group = "npm", name = "kool-box2d-wasm", version.ref = "box2d-wasm" } - -# wgpu backend +compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose" } +compose-mini-runtime = { group = "me.dvyy.compose.mini", name = "runtime", version.ref = "compose-mini"} +compose-mini-modifier = { group = "me.dvyy.compose.mini", name = "modifier", version.ref = "compose-mini" } +compose-mini-modifier-composed = { group = "me.dvyy.compose.mini", name = "modifier-composed", version.ref = "compose-mini"} +# wgpu backcend wgpu4k = { module = "io.ygdrasil:wgpu4k", version.ref = "wgpu4k" } webgpu-descriptors = { module = "io.ygdrasil:webgpu-ktypes-descriptors", version.ref = "webgpu-ktypes" } rococoa = { module = "io.ygdrasil:rococoa", version.ref = "rococoa" } @@ -71,6 +76,9 @@ plugindep-maven-publish = { group = "com.vanniktech", name = "gradle-maven-publi [plugins] webidl = { id = "de.fabmax.webidl-util", version = "0.10.5" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose" } + [bundles] lwjgl = ["lwjgl-core", "lwjgl-glfw", "lwjgl-jemalloc", "lwjgl-opengl", "lwjgl-vulkan", "lwjgl-vma", "lwjgl-shaderc", "lwjgl-nfd", "lwjgl-stb", "lwjgl-jawt", "lwjgl-msdfgen"] diff --git a/kool-core/build.gradle.kts b/kool-core/build.gradle.kts index ee49d6907..e64446a7b 100644 --- a/kool-core/build.gradle.kts +++ b/kool-core/build.gradle.kts @@ -13,6 +13,7 @@ kotlin { api(libs.kotlin.coroutines) implementation(libs.kotlin.serialization.json) implementation(libs.kotlin.atomicfu) + implementation(libs.compose.runtime) } commonTest.dependencies { implementation(libs.kotlin.test) diff --git a/kool-core/src/androidMain/kotlin/de/fabmax/kool/util/Time.android.kt b/kool-core/src/androidMain/kotlin/de/fabmax/kool/util/Time.android.kt deleted file mode 100644 index a2ae29c48..000000000 --- a/kool-core/src/androidMain/kotlin/de/fabmax/kool/util/Time.android.kt +++ /dev/null @@ -1,7 +0,0 @@ -package de.fabmax.kool.util - -internal actual fun SystemClock(): SystemClock = SystemClockImpl - -private object SystemClockImpl : SystemClock { - override fun now(): Double = System.nanoTime() / 1e9 -} \ No newline at end of file diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/Annotations.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/Annotations.kt new file mode 100644 index 000000000..1af1a94c8 --- /dev/null +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/Annotations.kt @@ -0,0 +1,15 @@ +package de.fabmax.kool + +@RequiresOptIn( + level = RequiresOptIn.Level.ERROR, + message = "This is internal Kool API that may lead to unexpected behavior or change without warning." +) +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER, +) +@Retention(AnnotationRetention.BINARY) +annotation class InternalKoolAPI diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/KoolContext.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/KoolContext.kt index df382585e..88b5e3627 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/KoolContext.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/KoolContext.kt @@ -1,5 +1,6 @@ package de.fabmax.kool +import androidx.compose.runtime.snapshots.Snapshot import de.fabmax.kool.input.Input import de.fabmax.kool.pipeline.ComputePass import de.fabmax.kool.pipeline.GpuPass @@ -11,6 +12,7 @@ import de.fabmax.kool.scene.Scene import de.fabmax.kool.util.BufferedList import de.fabmax.kool.util.KoolDispatchers import de.fabmax.kool.util.Time +import kotlinx.atomicfu.atomic /** * @author fabmax @@ -25,6 +27,11 @@ abstract class KoolContext { private var prevFrameTime = Time.precisionTime + private val applyScheduled = atomic(false) + private val snapshotHandle = Snapshot.registerGlobalWriteObserver { + applyScheduled.compareAndSet(expect = false, update = true) + } + val onRender = BufferedList<(KoolContext) -> Unit>() val onShutdown = BufferedList<(KoolContext) -> Unit>() @@ -40,6 +47,10 @@ abstract class KoolContext { .also { brdf -> onShutdown += { brdf.release() } } } + init { + onShutdown += { snapshotHandle.dispose() } + } + abstract fun openUrl(url: String, sameWindow: Boolean = true) abstract fun run() @@ -73,7 +84,14 @@ abstract class KoolContext { Input.poll(this) + // Apply any mutable state changes from user input + if (applyScheduled.compareAndSet(expect = true, update = false)) { + Snapshot.sendApplyNotifications() + } + KoolDispatchers.Frontend.executeDispatchedTasks() + Time.composeFrameClock.sendFrame(Time.nanoTime) // Let recomposer update UI nodes + onRender.update() for (i in onRender.indices) { onRender[i](this) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/Dimension.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/Dimension.kt index 2f32b92f5..eeeafdff3 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/Dimension.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/Dimension.kt @@ -48,4 +48,6 @@ value class Dp(val value: Float): Dimension, Comparable { return fromPx(round(pxf)) } } -} \ No newline at end of file +} + +val Number.dp: Dp get() = Dp(this.toFloat()) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/ScrollPane.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/ScrollPane.kt index 245f7a91a..6c9d03de4 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/ScrollPane.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/ScrollPane.kt @@ -181,7 +181,7 @@ open class ScrollPaneNode(parent: UiNode?, surface: UiSurface) : UiNode(parent, var currentScrollY = state.yScrollDp.use() var desiredScrollX = state.xScrollDpDesired.use() var desiredScrollY = state.yScrollDpDesired.use() - + val parent = parent if (parent != null) { state.viewWidthDp.set(parent.widthPx / UiScale.measuredScale) state.viewHeightDp.set(parent.heightPx / UiScale.measuredScale) @@ -221,4 +221,4 @@ open class ScrollPaneNode(parent: UiNode?, surface: UiSurface) : UiNode(parent, companion object { val factory: (UiNode, UiSurface) -> ScrollPaneNode = { parent, surface -> ScrollPaneNode(parent, surface) } } -} \ No newline at end of file +} diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/UiModifier.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/UiModifier.kt index d40df8887..0013eacb2 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/UiModifier.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/UiModifier.kt @@ -46,6 +46,7 @@ open class UiModifier(val surface: UiSurface) { val onDragStart: MutableList<(PointerEvent) -> Unit> by listProperty() val onDrag: MutableList<(PointerEvent) -> Unit> by listProperty() val onDragEnd: MutableList<(PointerEvent) -> Unit> by listProperty() + val onRender: MutableList Unit> by listProperty() protected fun property(defaultVal: T): PropertyHolder { val holder = PropertyHolder { defaultVal } @@ -309,4 +310,4 @@ fun T.dragListener(draggable: Draggable): T { onDrag(draggable::onDrag) onDragEnd(draggable::onDragEnd) return this -} \ No newline at end of file +} diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/UiNode.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/UiNode.kt index 44cb8caa6..141e92532 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/UiNode.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/UiNode.kt @@ -1,5 +1,6 @@ package de.fabmax.kool.modules.ui2 +import de.fabmax.kool.InternalKoolAPI import de.fabmax.kool.KoolContext import de.fabmax.kool.math.MutableVec2f import de.fabmax.kool.math.MutableVec4f @@ -12,15 +13,23 @@ import kotlin.math.max import kotlin.math.min import kotlin.reflect.KClass -abstract class UiNode(val parent: UiNode?, override val surface: UiSurface) : UiScope { +abstract class UiNode(parent: UiNode?, override val surface: UiSurface) : UiScope { + @set:InternalKoolAPI + var parent: UiNode? = parent + override val uiNode: UiNode get() = this var nodeIndex = 0 private set protected val oldChildren = mutableListOf() - protected val mutChildren = mutableListOf() + + @InternalKoolAPI + val mutChildren = mutableListOf() + + @OptIn(InternalKoolAPI::class) val children: List get() = mutChildren + val weakMemory = WeakMemory() private var scopeName: String? = null @@ -131,7 +140,7 @@ abstract class UiNode(val parent: UiNode?, override val surface: UiSurface) : Ui this.topPx = minY this.rightPx = maxX this.bottomPx = maxY - + val parent = parent if (parent != null) { clipBoundsPx.x = max(parent.clipLeftPx, minX) clipBoundsPx.y = max(parent.clipTopPx, minY) @@ -205,6 +214,7 @@ abstract class UiNode(val parent: UiNode?, override val surface: UiSurface) : Ui open fun render(ctx: KoolContext) { modifier.background?.renderUi(this) modifier.border?.renderUi(this) + modifier.onRender.forEach { render -> render(this) } } open fun measureContentSize(ctx: KoolContext) { @@ -412,4 +422,4 @@ abstract class UiNode(val parent: UiNode?, override val surface: UiSurface) : Ui companion object { val NO_CLIP = Vec4f(-1e9f, -1e9f, 1e9f, 1e9f) } -} \ No newline at end of file +} diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/UiSurface.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/UiSurface.kt index dcd75d1e1..bb835780f 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/UiSurface.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/UiSurface.kt @@ -16,11 +16,17 @@ import de.fabmax.kool.scene.geometry.MeshBuilder import de.fabmax.kool.scene.geometry.Usage import de.fabmax.kool.util.* +/** + * @property clearViewportOnUpdateUi + * Whether the viewport node should have defaults applied when updating UI. + * Set to false if managing UI nodes externally (ex. via Compose.) + */ open class UiSurface( val parentScene: Scene, colors: Colors = Colors.darkColors(), sizes: Sizes = Sizes.medium, - name: String = "uiSurface" + name: String = "uiSurface", + val clearViewportOnUpdateUi: Boolean = true, ) : Node(name) { constructor( @@ -155,7 +161,7 @@ open class UiSurface( perfPrep = pt.takeMs().also { pt.reset() } viewport.setBounds(0f, 0f, viewportWidth.use(this), viewportHeight.use(this)) - viewport.applyDefaults() + if (clearViewportOnUpdateUi) viewport.applyDefaults() composeContent() perfCompose = pt.takeMs().also { pt.reset() } @@ -758,4 +764,4 @@ open class UiSurface( } } } -} \ No newline at end of file +} diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/util/Time.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/util/Time.kt index 27463ab69..e28f4a026 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/util/Time.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/util/Time.kt @@ -1,13 +1,15 @@ package de.fabmax.kool.util +import androidx.compose.runtime.BroadcastFrameClock +import de.fabmax.kool.InternalKoolAPI import de.fabmax.kool.util.Time.frameCount import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlin.time.Clock object Time { - private val systemClock = SystemClock() /** * Time since previous frame in seconds. */ @@ -44,7 +46,25 @@ object Time { * measure time intervals. Precision depends on platform, on JVM it should be around nanoseconds, on JS it should * be around 0.1 milliseconds. */ - val precisionTime: Double get() = systemClock.now() + val precisionTime: Double get() = nanoTime / 1_000_000_000.0 + + /** + * Current system clock time in nanoseconds. + */ + val nanoTime: Long get() { + val now = Clock.System.now() + return now.epochSeconds * 1_000_000_000L + now.nanosecondsOfSecond + } + + /** + * Frame clock from Jetpack compose runtime. + * Frames are emitted after polling user input and dispatching any queued frontend scope tasks. + * + * Any blocks waiting for a new frame will run immediately when the frame is emitted, then switch + * to their parent context to return the result. + */ + @InternalKoolAPI + val composeFrameClock = BroadcastFrameClock() internal fun update(dt: Double) { gameTime += dt @@ -54,10 +74,4 @@ object Time { for (i in frameTimes.indices) { sum += frameTimes[i] } fps = (frameTimes.size / sum) * 0.1 + fps * 0.9 } -} - -internal expect fun SystemClock(): SystemClock - -internal interface SystemClock { - fun now(): Double } \ No newline at end of file diff --git a/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Time.desktop.kt b/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Time.desktop.kt deleted file mode 100644 index a2ae29c48..000000000 --- a/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Time.desktop.kt +++ /dev/null @@ -1,7 +0,0 @@ -package de.fabmax.kool.util - -internal actual fun SystemClock(): SystemClock = SystemClockImpl - -private object SystemClockImpl : SystemClock { - override fun now(): Double = System.nanoTime() / 1e9 -} \ No newline at end of file diff --git a/kool-core/src/webMain/kotlin/de/fabmax/kool/util/Time.web.kt b/kool-core/src/webMain/kotlin/de/fabmax/kool/util/Time.web.kt deleted file mode 100644 index c6e609053..000000000 --- a/kool-core/src/webMain/kotlin/de/fabmax/kool/util/Time.web.kt +++ /dev/null @@ -1,14 +0,0 @@ -package de.fabmax.kool.util - -import kotlin.time.Clock -import kotlin.time.ExperimentalTime - -internal actual fun SystemClock(): SystemClock = SystemClockImpl - -private object SystemClockImpl : SystemClock { - @OptIn(ExperimentalTime::class) - override fun now(): Double { - val now = Clock.System.now() - return now.toEpochMilliseconds() / 1000 + now.nanosecondsOfSecond / 1e9 - } -} \ No newline at end of file