diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 739de1f0..5dfbba5e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,6 +39,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.gitmodules b/.gitmodules index 37e5cd06..a8ec31b5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "ferricia"] path = ferricia url = https://github.com/bitsusei/TerraModulus-Ferricia-Engine +[submodule "vector-math"] + path = vector-math + url = https://github.com/bitsusei/kotlin-vector-math diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 131e44d7..2e4da65a 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,7 @@ - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index ba9deec3..6e08a227 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -5,5 +5,6 @@ + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 8541b9db..bec2e46f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,21 +5,63 @@ plugins { kotlin("jvm") version "2.3.21" kotlin("plugin.serialization") version "2.1.20" id("org.jetbrains.kotlinx.atomicfu") version "0.27.0" + id("net.terramodulus.plugins.cargo") apply false +// id("fr.stardustenterprises.rust.wrapper") version "3.2.4" apply false application } -allprojects { - apply(plugin = "org.jetbrains.kotlin.jvm") +version = "0.0.1" - version = "0.0.1" +repositories { + mavenCentral() +} + +project(":ferricia") { + // Candidates: fr.stardustenterprises.rust.wrapper + apply(plugin = "net.terramodulus.plugins.cargo") - repositories { - mavenCentral() + if (providers.gradleProperty("release").isPresent) configure { + release = true // use `-Prelease=true` + } + configure { + outputFile = release.map { + projectDir.resolve("target/${if (it) "release" else "debug"}/${System.mapLibraryName("ferricia")}") + } + } + // somehow, .cargo extension is unusable + tasks.register("buildClient") { + args = listOf("-F", "client") + println(outputFile.get()) + } + tasks.register("buildServer") { + args = listOf("-F", "server") + } + configurations { + create("client") { + isCanBeConsumed = true + isCanBeResolved = false + } + create("server") { + isCanBeConsumed = true + isCanBeResolved = false + } + } + artifacts { + add("client", tasks.named("buildClient")) + add("server", tasks.named("buildServer")) } } configure(listOf(project(":kernel"), project(":internal"))) { configure(listOf(project("common"), project("client"), project("server"))) { + apply(plugin = "org.jetbrains.kotlin.jvm") + + version = rootProject.version + + repositories { + mavenCentral() + } + sourceSets.main { kotlin.srcDir("kotlin") resources.srcDir("resources") @@ -61,6 +103,23 @@ project(":kernel") { } } +project(":kernel:client") { + dependencies { + implementation(project(":ferricia", "client")) + } +} +project(":kernel:server") { + dependencies { + implementation(project(":ferricia", "server")) + } +} + +configure(listOf(project(":internal:common"), project(":kernel:common"))) { + dependencies { + api("com.cout970:kotlin-vector-math:0.1.0") + } +} + project(":kernel:common") { dependencies { api("org.jetbrains:annotations:26.1.0") @@ -86,10 +145,12 @@ project(":kernel:common") { project(":kernel:client").dependencies { implementation("net.sf.jopt-simple:jopt-simple:5.0.4") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0") } project(":kernel:server").dependencies { implementation("net.sf.jopt-simple:jopt-simple:5.0.4") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0") } project(":internal:common").dependencies { @@ -112,28 +173,6 @@ configure(listOf(project(":kernel:server"), project(":kernel:client"))) { } } -/** Build Ferricia Engine with Cargo */ -tasks.register("cargoBuildClient") { - onlyIf { - !gradle.taskGraph.hasTask(":kernel:server:jar") - } - workingDir = rootProject.file("ferricia") - commandLine("cargo", "build") - if (project.hasProperty("release")) args("--release") // use `-Prelease=true` - args("-F", "client") -} -tasks.register("cargoBuildServer") { - onlyIf { - !gradle.taskGraph.hasTask(":kernel:client:jar") - } - workingDir = rootProject.file("ferricia") - commandLine("cargo", "build") - if (project.hasProperty("release")) args("--release") // use `-Prelease=true` - args("-F", "server") -} -project(":kernel:client").tasks.named("jar") { dependsOn(tasks.named("cargoBuildClient")) } -project(":kernel:server").tasks.named("jar") { dependsOn(tasks.named("cargoBuildServer")) } - tasks.register("buildClient") { group = "build" description = "Build client" @@ -151,17 +190,13 @@ tasks.named("run") { tasks.register("runClient") { group = "application" description = "Run client" - dependsOn("cargoBuildClient") dependsOn(":kernel:client:run") } -project(":kernel:client").tasks.named("run").get().mustRunAfter(tasks.named("cargoBuildClient")) tasks.register("runServer") { group = "application" description = "Run server" - dependsOn("cargoBuildServer") dependsOn(":kernel:server:run") } -project(":kernel:server").tasks.named("run").get().mustRunAfter(tasks.named("cargoBuildServer")) configure(listOf(project(":kernel:server"), project(":kernel:client"))) { distributions { @@ -170,15 +205,12 @@ configure(listOf(project(":kernel:server"), project(":kernel:client"))) { duplicatesStrategy = DuplicatesStrategy.EXCLUDE into("lib") { val dir = if (project.hasProperty("release")) "release" else "debug" + from("$rootDir/ferricia/target/$dir/${System.mapLibraryName("ferricia")}") if (OperatingSystem.current().isWindows) from( - "$rootDir/ferricia/target/$dir/ferricia.dll", "$rootDir/ferricia/target/$dir/oded.dll", "$rootDir/ferricia/target/$dir/OpenAL32.dll", "$rootDir/ferricia/target/$dir/SDL3.dll", - ) else { // suppose UNIX - // other libs should be installed on user's end directly - from("$rootDir/ferricia/target/$dir/libferricia.so") - } + ) // for UNIX, other libs should have been installed on user's end directly } } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..fb5da883 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() + gradlePluginPortal() +} + +gradlePlugin { + plugins { + register("cargoPlugin") { + id = "net.terramodulus.plugins.cargo" + implementationClass = "CargoPlugin" + } + } +} diff --git a/buildSrc/src/main/kotlin/CargoExtension.kt b/buildSrc/src/main/kotlin/CargoExtension.kt new file mode 100644 index 00000000..520ea2ae --- /dev/null +++ b/buildSrc/src/main/kotlin/CargoExtension.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property + +interface CargoExtension { + val release: Property + val outputFile: RegularFileProperty +} diff --git a/buildSrc/src/main/kotlin/CargoPlugin.kt b/buildSrc/src/main/kotlin/CargoPlugin.kt new file mode 100644 index 00000000..843b7d61 --- /dev/null +++ b/buildSrc/src/main/kotlin/CargoPlugin.kt @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +import org.gradle.api.Plugin +import org.gradle.api.Project + +class CargoPlugin : Plugin { + override fun apply(target: Project) { + val extension = target.extensions.create("cargoConfig", CargoExtension::class.java) + + extension.release.convention(false) + + target.tasks.withType(CargoTask::class.java).configureEach { + release.convention(extension.release) + outputFile.convention(extension.outputFile) + inputs.dir(project.projectDir) + } + } +} diff --git a/buildSrc/src/main/kotlin/CargoTask.kt b/buildSrc/src/main/kotlin/CargoTask.kt new file mode 100644 index 00000000..1ee19e2a --- /dev/null +++ b/buildSrc/src/main/kotlin/CargoTask.kt @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import javax.inject.Inject + +abstract class CargoTask @Inject constructor(private val execOperations: ExecOperations, objectFactory: ObjectFactory) : DefaultTask() { +// @get:Input +// abstract val environment: Property> + + @get:Input + @get:Optional + abstract val release: Property + + @get:Input + @get:Optional + val args: ListProperty = objectFactory.listProperty(String::class.java) + + @get:OutputFile + abstract val outputFile: RegularFileProperty + + init { + release.convention(false) + args.convention(args.empty()) + } + + @TaskAction + fun build() { + execOperations.exec { + executable = "cargo" + args = listOf("build") + this@CargoTask.args.get() + if (release.get()) args = args + "--release" + workingDir = project.projectDir + } + } +} diff --git a/ferricia b/ferricia index 66bbf397..074ccd87 160000 --- a/ferricia +++ b/ferricia @@ -1 +1 @@ -Subproject commit 66bbf397fc38663b797a63f31493149602223434 +Subproject commit 074ccd87f11b9a8b139bb8fc83b61e735a3f6e2b diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b95..1b33c55b 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a793..aaaabb3c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f3b75f3b..23d15a93 100644 --- a/gradlew +++ b/gradlew @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -205,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a218..db3a6ac2 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/settings.gradle.kts b/settings.gradle.kts index a487543e..e2221f71 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,3 +16,11 @@ rootProject.children.forEach { it.projectDir = File(settingsDir, "src/${it.name}") include("${it.name}:common", "${it.name}:client", "${it.name}:server") } + +include("ferricia") // this is Rust + +includeBuild("vector-math") { + dependencySubstitution { + substitute(module("com.cout970:kotlin-vector-math")).using(project(":")) + } +} diff --git a/src/internal/client/kotlin/net/terramodulus/engine/AsdIntData.kt b/src/internal/client/kotlin/net/terramodulus/engine/AsdIntData.kt new file mode 100644 index 00000000..401d7344 --- /dev/null +++ b/src/internal/client/kotlin/net/terramodulus/engine/AsdIntData.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.engine + +/** + * ASD Intermediate Data + */ +class AsdIntData { +} diff --git a/src/internal/client/kotlin/net/terramodulus/engine/BaseAsdProcessor.kt b/src/internal/client/kotlin/net/terramodulus/engine/BaseAsdProcessor.kt new file mode 100644 index 00000000..a63aabda --- /dev/null +++ b/src/internal/client/kotlin/net/terramodulus/engine/BaseAsdProcessor.kt @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.engine + +import java.io.InputStream + +/** + * Since for most parts, there is no definite metadata of lengths for all data, + * it is necessary to provide at least processing fragmentation from implementation. + */ +interface BaseAsdProcessor { + /** + * @param data ByteArrayInputStream backed by direct byte buffer + */ + fun process(data: InputStream) +} diff --git a/src/internal/client/kotlin/net/terramodulus/engine/Camera3D.kt b/src/internal/client/kotlin/net/terramodulus/engine/Camera3D.kt index 343458f3..ff6cd069 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Camera3D.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Camera3D.kt @@ -5,8 +5,6 @@ package net.terramodulus.engine -import net.terramodulus.engine.ferricia.Gwr.drawGwrObj -import net.terramodulus.engine.ferricia.Gwr.geoShaders import net.terramodulus.engine.ferricia.Gwr.newCamera import net.terramodulus.engine.ferricia.Gwr.refreshCameraPos import net.terramodulus.engine.ferricia.Gwr.setCameraZoomLevel @@ -16,7 +14,7 @@ import kotlin.properties.Delegates class Camera3D internal constructor(private val canvas: Canvas, pos: FloatArray) : Closeable { internal val handle = newCamera(canvas.handle, pos) - fun loadGeoShaders(vsh: String, fsh: String) = geoShaders(vsh, fsh) + fun loadGeoShaders(vsh: String, fsh: String) = canvas.load3DGeoShaders(vsh, fsh) fun refreshPos(pos: FloatArray) = refreshCameraPos(handle, pos) @@ -25,7 +23,7 @@ class Camera3D internal constructor(private val canvas: Canvas, pos: FloatArray) } fun renderGwrGeo(drawable: WorldObjDrawable, programHandle: ULong) = - drawGwrObj(canvas.handle, handle, drawable.handle, programHandle) + canvas.drawGwrObj(this, drawable, programHandle) override fun close() { canvas.camera3D = null diff --git a/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt b/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt index a838815b..8808235e 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt @@ -5,6 +5,10 @@ package net.terramodulus.engine +import net.terramodulus.engine.ferricia.Gwr +import net.terramodulus.engine.ferricia.Gwr.drawGwrObj +import net.terramodulus.engine.ferricia.Gwr.newMeshGeomCube +import net.terramodulus.engine.ferricia.Gwr.newMeshGeomSphere import net.terramodulus.engine.ferricia.Mui import net.terramodulus.engine.ferricia.Mui.clearCanvas import net.terramodulus.engine.ferricia.Mui.drawGuiGeo @@ -14,6 +18,9 @@ import net.terramodulus.engine.ferricia.Mui.geoShaders import net.terramodulus.engine.ferricia.Mui.getGLVersion import net.terramodulus.engine.ferricia.Mui.initCanvasHandle import net.terramodulus.engine.ferricia.Mui.loadImageToCanvas +import net.terramodulus.engine.ferricia.Mui.newSimpleLineGeom +import net.terramodulus.engine.ferricia.Mui.newSimpleRectGeom +import net.terramodulus.engine.ferricia.Mui.newSpriteMesh import net.terramodulus.engine.ferricia.Mui.setCanvasClearColor import net.terramodulus.engine.ferricia.Mui.texShaders import java.io.Closeable @@ -23,16 +30,17 @@ import java.io.Closeable * * This manages GL viewport in the SDL window and rendering in the viewport. */ +@OptIn(ExperimentalUnsignedTypes::class) class Canvas internal constructor(private val windowHandle: ULong) : Closeable { internal val handle = initCanvasHandle(windowHandle) val glVersion = getGLVersion(windowHandle) internal var camera3D: Camera3D? = null; - fun clear() = clearCanvas() + fun clear() = clearCanvas(windowHandle) - fun setClearColor(r: Float, g: Float, b: Float, a: Float) = setCanvasClearColor(r, g, b, a) + fun setClearColor(r: Float, g: Float, b: Float, a: Float) = setCanvasClearColor(windowHandle, r, g, b, a) - fun resizeGLViewport() = if (camera3D == null) { + internal fun resizeGLViewport() = if (camera3D == null) { Mui.resizeGLViewport(windowHandle, handle) } else { Mui.resizeGLViewportCamera(windowHandle, handle, camera3D!!.handle) @@ -45,9 +53,24 @@ class Canvas internal constructor(private val windowHandle: ULong) : Closeable { fun loadImage(data: ByteArray) = loadImageToCanvas(handle, data) - fun loadGeoShaders(vsh: String, fsh: String) = geoShaders(vsh, fsh) + fun loadGeoShaders(vsh: String, fsh: String) = geoShaders(windowHandle, vsh, fsh) - fun loadTexShaders(vsh: String, fsh: String) = texShaders(vsh, fsh) + fun load3DGeoShaders(vsh: String, fsh: String) = Gwr.geoShaders(windowHandle, vsh, fsh) + + fun loadTexShaders(vsh: String, fsh: String) = texShaders(windowHandle, vsh, fsh) + + internal fun newSimpleLineGeom(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) = + newSimpleLineGeom(windowHandle, intArrayOf(x0, y0, x1, y1, r, g, b, a)) + + internal fun newSimpleRectGeom(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) = + newSimpleRectGeom(windowHandle, intArrayOf(x0, y0, x1, y1, r, g, b, a)) + + internal fun newSpriteMesh(x0: Int, y0: Int, x1: Int, y1: Int) = + newSpriteMesh(windowHandle, intArrayOf(x0, y0, x1, y1)) + + internal fun newMeshGeomCube(width: Float) = newMeshGeomCube(windowHandle, width) + + internal fun newMeshGeomSphere(radius: Float) = newMeshGeomSphere(windowHandle, radius) fun renderGuiGeo(drawable: GeomDrawable, programHandle: ULong) = drawGuiGeo(handle, drawable.handle, programHandle) @@ -55,6 +78,9 @@ class Canvas internal constructor(private val windowHandle: ULong) : Closeable { fun renderGuiTex(drawable: MeshDrawable, programHandle: ULong, textureHandle: UInt) = drawGuiTex(handle, drawable.handle, programHandle, textureHandle) + internal fun drawGwrObj(camera3D: Camera3D, drawable: WorldObjDrawable, programHandle: ULong) = + drawGwrObj(windowHandle, handle, camera3D.handle, drawable.handle, programHandle) + override fun close() { dropCanvasHandle(handle) } diff --git a/src/internal/client/kotlin/net/terramodulus/engine/Containers.kt b/src/internal/client/kotlin/net/terramodulus/engine/Containers.kt index 69913a49..03d6cacf 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Containers.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Containers.kt @@ -5,10 +5,6 @@ package net.terramodulus.engine -data class Rgba(val r: Int, val g: Int, val b: Int, val a: Int) { - fun toArray() = intArrayOf(r, g, b, a) -} +import com.cout970.math.vec4.Vec4i -data class Vec3F(val x: Float, val y: Float, val z: Float) { - fun toArray() = floatArrayOf(x, y, z) -} +fun Vec4i.toArray() = intArrayOf(x, y, z, w) diff --git a/src/internal/client/kotlin/net/terramodulus/engine/GeomDrawable.kt b/src/internal/client/kotlin/net/terramodulus/engine/GeomDrawable.kt index 3d8ccd53..68e663bd 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/GeomDrawable.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/GeomDrawable.kt @@ -11,8 +11,8 @@ import net.terramodulus.engine.ferricia.Mui.newSimpleRectGeom sealed class GeomDrawable(handle: ULong) : Drawable(handle) { } -class SimpleLineGeom(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : - GeomDrawable(newSimpleLineGeom(intArrayOf(x0, y0, x1, y1, r, g, b, a))) +class SimpleLineGeom(canvas: Canvas, x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : + GeomDrawable(canvas.newSimpleLineGeom(x0, y0, x1, y1, r, g, b, a)) -class SimpleRectGeom(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : - GeomDrawable(newSimpleRectGeom(intArrayOf(x0, y0, x1, y1, r, g, b, a))) +class SimpleRectGeom(canvas: Canvas, x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : + GeomDrawable(canvas.newSimpleRectGeom(x0, y0, x1, y1, r, g, b, a)) diff --git a/src/internal/client/kotlin/net/terramodulus/engine/MeshDrawable.kt b/src/internal/client/kotlin/net/terramodulus/engine/MeshDrawable.kt index b9357795..836ba323 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/MeshDrawable.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/MeshDrawable.kt @@ -10,6 +10,7 @@ import net.terramodulus.engine.ferricia.Mui.newSpriteMesh sealed class MeshDrawable(handle: ULong) : Drawable(handle) { } -class SpriteMesh(x0: Int, y0: Int, x1: Int, y1: Int) : MeshDrawable(newSpriteMesh(intArrayOf(x0, y0, x1, y1))) { +class SpriteMesh(canvas: Canvas, x0: Int, y0: Int, x1: Int, y1: Int) : + MeshDrawable(canvas.newSpriteMesh(x0, y0, x1, y1)) { } diff --git a/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt b/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt index 556db221..3b9752c7 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt @@ -5,29 +5,48 @@ package net.terramodulus.engine -import net.terramodulus.engine.ferricia.Mui.modelFullScaling -import net.terramodulus.engine.ferricia.Mui.modelSmartScaling +import com.cout970.math.vec2.ImmVec2d +import com.cout970.math.vec2.MutVec2d +import com.cout970.math.vec2.Vec2d +import net.terramodulus.engine.ferricia.Mui.modelGeneralTransform +import net.terramodulus.engine.ferricia.Mui.updateGeneralTransform +import java.io.Closeable +import kotlin.properties.Delegates @OptIn(ExperimentalUnsignedTypes::class) -sealed class ModelTransform(handles: ULongArray) { +sealed class ModelTransform(handles: ULongArray) : Closeable { internal val handle: ULong = handles[0] internal val wideHandle: ULong = handles[1] + + override fun close() { + TODO("Not yet implemented") + } } @OptIn(ExperimentalUnsignedTypes::class) -class SmartScaling private constructor(vararg args: Int) : - ModelTransform(modelSmartScaling(args)) { - - companion object { - fun none(w: Int, h: Int) = SmartScaling(w, h, 0) - - fun x(w: Int, h: Int, ww: Int, hh: Int) = SmartScaling(w, h, 1, ww, hh) - - fun y(w: Int, h: Int, ww: Int, hh: Int) = SmartScaling(w, h, 2, ww, hh) +class GeneralTransform(sx: Double, sy: Double, angle: Double, px: Double, py: Double) : + ModelTransform(modelGeneralTransform(doubleArrayOf(sx, sy, angle, px, py))) { + constructor() : this(1.0, 1.0, 0.0, 0.0, 0.0) + + var scale: Vec2d = ImmVec2d(sx, sy) + private set + var angle: Double = angle + private set + var pos: Vec2d = ImmVec2d(px, py) + private set + + interface Op { + var scale: Vec2d + var angle: Double + var pos: Vec2d + } - fun both(w: Int, h: Int, ww: Int, hh: Int) = SmartScaling(w, h, 3, ww, hh) + fun update(operation: Op.() -> Unit) { + operation(object : Op { + override var scale: Vec2d by this@GeneralTransform::scale + override var angle: Double by this@GeneralTransform::angle + override var pos: Vec2d by this@GeneralTransform::pos + }) + updateGeneralTransform(handle, doubleArrayOf(scale.x, scale.y, angle, pos.x, pos.y)) } } - -@OptIn(ExperimentalUnsignedTypes::class) -class FullScaling(w: Int, h: Int) : ModelTransform(modelFullScaling(intArrayOf(w, h))) diff --git a/src/internal/client/kotlin/net/terramodulus/engine/Window.kt b/src/internal/client/kotlin/net/terramodulus/engine/Window.kt index ef3c5e54..f21f4dc0 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Window.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Window.kt @@ -18,11 +18,30 @@ import java.io.Closeable /** * Manages the SDL window instance and the underlying GL context. */ -class Window : Closeable { +class Window( + width: UInt, + height: UInt, +) : Closeable { + var width = width + private set + var height = height + private set private val sdlHandle = initSdlHandle() - private val windowHandle = initWindowHandle(sdlHandle) + private val windowHandle = initWindowHandle(sdlHandle) // TODO pass dimensions val canvas = Canvas(windowHandle) + private val listeners = HashSet<(UInt, UInt) -> Unit>() + + fun addListener(listener: (UInt, UInt) -> Unit) = listeners.add(listener) + fun removeListener(listener: (UInt, UInt) -> Unit) = listeners.remove(listener) + + fun sizeChanged(width: UInt, height: UInt) { + this.width = width + this.height = height + canvas.resizeGLViewport() + listeners.forEach { it(width, height) } + } + fun show() = showWindow(windowHandle) fun swap() = swapWindow(windowHandle) diff --git a/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt b/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt index 55fe2423..0e29ea6f 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt @@ -5,38 +5,47 @@ package net.terramodulus.engine -import net.terramodulus.engine.ferricia.Gwr.newMeshGeomCube -import net.terramodulus.engine.ferricia.Gwr.newMeshGeomSphere +import com.cout970.math.quaternion.Quatd +import com.cout970.math.vec3.Vec3d +import com.cout970.math.vec4.Vec4i +import net.terramodulus.engine.ferricia.Gwr.newDrawableWorldObj import net.terramodulus.engine.ferricia.Gwr.updateWorldObjModel -sealed class WorldObjDrawable(internal val handle: ULong, private var pos: Vec3D, private var scale: Vec3D, private var rot: Quat) { +class WorldObjDrawable(geom: WorldObjGeom, rgba: Vec4i, private var pos: Vec3d, private var scale: Vec3d, private var rot: Quatd) { + internal val handle = newDrawableWorldObj(geom.wideHandle, rgba.toArray()) fun updateModel(px: Double, py: Double, pz: Double, sx: Double, sy: Double, sz: Double, w: Double, i: Double, j: Double, k: Double) = updateWorldObjModel(handle, doubleArrayOf(px, py, pz, w, i, j, k, sx, sy, sz)) - fun updateModel(pos: Vec3D, scale: Vec3D, rot: Quat) = - updateModel(pos.x, pos.y, pos.z, scale.x, scale.y, scale.z, rot.w, rot.i, rot.j, rot.k) + fun updateModel(pos: Vec3d, scale: Vec3d, rot: Quatd) = + updateModel(pos.x, pos.y, pos.z, scale.x, scale.y, scale.z, rot.w, rot.x, rot.y, rot.z) init { updateModel(pos, scale, rot) } - fun setPos(value: Vec3D) { + fun setPos(value: Vec3d) { pos = value updateModel(pos, scale, rot) } - fun setScale(value: Vec3D) { + fun setScale(value: Vec3d) { scale = value updateModel(pos, scale, rot) } - fun setRot(value: Quat) { + fun setRot(value: Quatd) { rot = value updateModel(pos, scale, rot) } } -class SimpleMesh3dGeomCube(width: Float, rgba: Rgba, pos: Vec3D, scale: Vec3D, rot: Quat) : - WorldObjDrawable(newMeshGeomCube(width, rgba.toArray()), pos, scale, rot) +@OptIn(ExperimentalUnsignedTypes::class) +sealed class WorldObjGeom(handles: ULongArray) { + protected val handle = handles[0] + internal val wideHandle = handles[1] +} + +@OptIn(ExperimentalUnsignedTypes::class) +class SimpleMesh3dGeomCube(canvas: Canvas, width: Float) : WorldObjGeom(canvas.newMeshGeomCube(width)) -class SimpleMesh3dGeomSphere(radius: Float, rgba: Rgba, pos: Vec3D, scale: Vec3D, rot: Quat) : - WorldObjDrawable(newMeshGeomSphere(radius, rgba.toArray()), pos, scale, rot) +@OptIn(ExperimentalUnsignedTypes::class) +class SimpleMesh3dGeomSphere(canvas: Canvas, radius: Float) : WorldObjGeom(canvas.newMeshGeomSphere(radius)) diff --git a/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Gwr.kt b/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Gwr.kt index dc7c8d36..1261a316 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Gwr.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Gwr.kt @@ -5,14 +5,16 @@ package net.terramodulus.engine.ferricia +@OptIn(ExperimentalUnsignedTypes::class) internal object Gwr { /** + * @param windowHandle window handle pointer * @param vsh source code of vector shader * @param fsh source code of fragment shader * @return GWR Geo Shader Program handle pointer */ @JvmName("geoShaders") - external fun geoShaders(vsh: String, fsh: String): ULong + external fun geoShaders(windowHandle: ULong, vsh: String, fsh: String): ULong /** * @param canvasHandle Canvas handle pointer @@ -37,20 +39,28 @@ internal object Gwr { external fun setCameraZoomLevel(cameraHandle: ULong, data: Float) /** + * @param windowHandle window handle pointer * @param width cube's width, in `(0,2]` - * @param data `[r, g, b, a]` - * @return DrawableWorldObj handle pointer + * @return SimpleMesh3dGeom handle pointer and Render3dPrimitive (wide) handle pointer */ @JvmName("newMeshGeomCube") - external fun newMeshGeomCube(width: Float, data: IntArray): ULong + external fun newMeshGeomCube(windowHandle: ULong, width: Float): ULongArray /** + * @param windowHandle window handle pointer * @param width cube's radius, in `(0,1]` + * @return SimpleMesh3dGeom handle pointer and Render3dPrimitive (wide) handle pointer + */ + @JvmName("newMeshGeomSphere") + external fun newMeshGeomSphere(windowHandle: ULong, width: Float): ULongArray + + /** + * @param handle Render3dPrimitive (wide) handle pointer * @param data `[r, g, b, a]` * @return DrawableWorldObj handle pointer */ - @JvmName("newMeshGeomSphere") - external fun newMeshGeomSphere(width: Float, data: IntArray): ULong + @JvmName("newDrawableWorldObj") + external fun newDrawableWorldObj(handle: ULong, data: IntArray): ULong /** * @param objHandle DrawableWorldObj handle pointer @@ -60,11 +70,12 @@ internal object Gwr { external fun updateWorldObjModel(objHandle: ULong, data: DoubleArray) /** + * @param windowHandle window handle pointer * @param canvasHandle Canvas handle pointer * @param cameraHandle Camera3D handle pointer * @param objHandle DrawableWorldObj handle pointer * @param programHandle GWR Shader Program handle pointer */ @JvmName("drawGwrObj") - external fun drawGwrObj(canvasHandle: ULong, cameraHandle: ULong, objHandle: ULong, programHandle: ULong) + external fun drawGwrObj(windowHandle: ULong, canvasHandle: ULong, cameraHandle: ULong, objHandle: ULong, programHandle: ULong) } diff --git a/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt b/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt index 6ffa4fa0..532b2cf7 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt @@ -7,6 +7,7 @@ package net.terramodulus.engine.ferricia import net.terramodulus.engine.MuiEvent +@OptIn(ExperimentalUnsignedTypes::class) internal object Mui { /** * @return SDL handle pointer @@ -91,48 +92,59 @@ internal object Mui { @JvmName("loadImageToCanvas") external fun loadImageToCanvas(canvasHandle: ULong, data: ByteArray): UInt + /** + * @param windowHandle window handle pointer + */ @JvmName("clearCanvas") - external fun clearCanvas() + external fun clearCanvas(windowHandle: ULong) + /** + * @param windowHandle window handle pointer + */ @JvmName("setCanvasClearColor") - external fun setCanvasClearColor(r: Float, g: Float, b: Float, a: Float) + external fun setCanvasClearColor(windowHandle: ULong, r: Float, g: Float, b: Float, a: Float) /** + * @param windowHandle window handle pointer * @param vsh source code of vector shader * @param fsh source code of fragment shader * @return Geo Shader Program handle pointer */ @JvmName("geoShaders") - external fun geoShaders(vsh: String, fsh: String): ULong + external fun geoShaders(windowHandle: ULong, vsh: String, fsh: String): ULong /** + * @param windowHandle window handle pointer * @param vsh source code of vector shader * @param fsh source code of fragment shader * @return Tex Shader Program handle pointer */ @JvmName("texShaders") - external fun texShaders(vsh: String, fsh: String): ULong + external fun texShaders(windowHandle: ULong, vsh: String, fsh: String): ULong /** + * @param windowHandle window handle pointer * @param data `[x0, y0, x1, y1, r, g, b, a]` * @return SimpleLineGeom as DrawableSet handle pointer */ @JvmName("newSimpleLineGeom") - external fun newSimpleLineGeom(data: IntArray): ULong + external fun newSimpleLineGeom(windowHandle: ULong, data: IntArray): ULong /** + * @param windowHandle window handle pointer * @param data `[x0, y0, x1, y1, r, g, b, a]` * @return SimpleRectGeom as DrawableSet handle pointer */ @JvmName("newSimpleRectGeom") - external fun newSimpleRectGeom(data: IntArray): ULong + external fun newSimpleRectGeom(windowHandle: ULong, data: IntArray): ULong /** + * @param windowHandle window handle pointer * @param data `[x0, y0, x1, y1]` * @return SpriteMesh as DrawableSet handle pointer */ @JvmName("newSpriteMesh") - external fun newSpriteMesh(data: IntArray): ULong + external fun newSpriteMesh(windowHandle: ULong, data: IntArray): ULong /** * @param handle DrawableSet handle pointer @@ -142,29 +154,22 @@ internal object Mui { external fun setGeomPos(handle: ULong, data: FloatArray) /** - * @param data `[w, h, param, w, h]` - * @return SmartScaling handle pointers - */ - @JvmName("modelSmartScaling") - external fun modelSmartScaling(data: IntArray): ULongArray - - /** - * @param data `[w, h]` - * @return FullScaling handle pointers + * @param data `[sx, sy, angle, px, py]`; scaling, rotation, position + * @return GeneralTransform handle pointer and PrimModelTransform (wide) handle pointer */ - @JvmName("modelFullScaling") - external fun modelFullScaling(data: IntArray): ULongArray + @JvmName("modelGeneralTransform") + external fun modelGeneralTransform(data: DoubleArray): ULongArray /** - * @param data `[x, y]` - * @return SimpleTranslation handle pointers + * @param handle GeneralTransform thin pointer + * @param data `[sx, sy, angle, px, py]`; scaling, rotation, position */ - @JvmName("modelSimpleTranslation") - external fun modelSimpleTranslation(data: FloatArray): ULongArray + @JvmName("updateGeneralTransform") + external fun updateGeneralTransform(handle: ULong, data: DoubleArray) /** * @param data alpha - * @return AlphaFilter handle pointers + * @return AlphaFilter handle pointer and PrimColorFilter (wide) handle pointer */ @JvmName("filterAlphaFilter") external fun filterAlphaFilter(data: Float): ULongArray diff --git a/src/internal/common/kotlin/net/terramodulus/engine/Containers.kt b/src/internal/common/kotlin/net/terramodulus/engine/Containers.kt deleted file mode 100644 index d15aea07..00000000 --- a/src/internal/common/kotlin/net/terramodulus/engine/Containers.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.engine - -data class Quat(val w: Double, val i: Double, val j: Double, val k: Double) { - fun toArray() = doubleArrayOf(w, i, j, k) -} - -data class Vec3D(val x: Double, val y: Double, val z: Double) { - companion object { - val ZERO = Vec3D(0.0, 0.0, 0.0) - - /** - * @param array array containing 3 double values - */ - fun fromArray(array: DoubleArray) = Vec3D(array[0], array[1], array[2]) - } - - fun toArray() = doubleArrayOf(x, y, z) -} diff --git a/src/internal/common/kotlin/net/terramodulus/engine/PhyBody.kt b/src/internal/common/kotlin/net/terramodulus/engine/PhyBody.kt index b2cca8ff..6ce9bed6 100644 --- a/src/internal/common/kotlin/net/terramodulus/engine/PhyBody.kt +++ b/src/internal/common/kotlin/net/terramodulus/engine/PhyBody.kt @@ -5,6 +5,9 @@ package net.terramodulus.engine +import com.cout970.math.vec3.Vec3d +import net.terramodulus.engine.common.ImmVec3dFromArray +import net.terramodulus.engine.common.toArray import net.terramodulus.engine.ferricia.Physics.addPhyBodyForce import net.terramodulus.engine.ferricia.Physics.addPhyBodyGeom import net.terramodulus.engine.ferricia.Physics.getPhyBodyLinearVel @@ -22,12 +25,12 @@ class PhyBody internal constructor(worldHandle: ULong, mass: Mass) { class SphereTotal(mass: Double, radius: Double) : Mass(newMassSphereTotal(mass, radius)) } - var pos - get() = Vec3D.fromArray(getPhyBodyPos(handle)) + var pos: Vec3d + get() = ImmVec3dFromArray(getPhyBodyPos(handle)) set(value) = setPhyBodyPos(handle, value.toArray()) - var linearVel - get() = Vec3D.fromArray(getPhyBodyLinearVel(handle)) + var linearVel: Vec3d + get() = ImmVec3dFromArray(getPhyBodyLinearVel(handle)) set(value) = setPhyBodyLinearVel(handle, value.toArray()) var gravityMode: Boolean by Delegates.observable(true) { _, _, newValue -> @@ -36,5 +39,5 @@ class PhyBody internal constructor(worldHandle: ULong, mass: Mass) { fun addGeom(geom: PhyGeom) = addPhyBodyGeom(handle, geom.handle) - fun addForce(force: Vec3D) = addPhyBodyForce(handle, force.toArray()) + fun addForce(force: Vec3d) = addPhyBodyForce(handle, force.toArray()) } diff --git a/src/internal/common/kotlin/net/terramodulus/engine/PhyWorld.kt b/src/internal/common/kotlin/net/terramodulus/engine/PhyWorld.kt index b985ab0a..ef74ddbe 100644 --- a/src/internal/common/kotlin/net/terramodulus/engine/PhyWorld.kt +++ b/src/internal/common/kotlin/net/terramodulus/engine/PhyWorld.kt @@ -5,6 +5,9 @@ package net.terramodulus.engine +import com.cout970.math.vec3.Vec3d +import net.terramodulus.engine.common.ZeroImmVec3d +import net.terramodulus.engine.common.toArray import net.terramodulus.engine.ferricia.Physics.newPhyCollisionManager import net.terramodulus.engine.ferricia.Physics.newPhyWorld import net.terramodulus.engine.ferricia.Physics.omitPhyCollisionManagerSpace @@ -18,7 +21,7 @@ class PhyWorld internal constructor(envHandle: ULong) { private val handle = newPhyWorld(envHandle) private val cmHandle = newPhyCollisionManager() - var gravity: Vec3D by Delegates.observable(Vec3D.ZERO) { _, _, newValue -> + var gravity: Vec3d by Delegates.observable(ZeroImmVec3d) { _, _, newValue -> setPhyWorldGravity(handle, newValue.toArray()) } diff --git a/src/internal/common/kotlin/net/terramodulus/engine/common/Containers.kt b/src/internal/common/kotlin/net/terramodulus/engine/common/Containers.kt new file mode 100644 index 00000000..e1a9a08e --- /dev/null +++ b/src/internal/common/kotlin/net/terramodulus/engine/common/Containers.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.engine.common + +import com.cout970.math.vec3.ImmVec3d +import com.cout970.math.vec3.ImmVec3f +import com.cout970.math.vec3.Vec3d + +/** + * @throws ArrayIndexOutOfBoundsException if [array]'s size < 3 + */ +fun ImmVec3dFromArray(array: DoubleArray) = ImmVec3d(array[0], array[1], array[2]) + +fun Vec3d.toArray() = doubleArrayOf(x, y, z) + +val ZeroImmVec3d = ImmVec3d(0.0) +val ZeroImmVec3f = ImmVec3f(0F) diff --git a/src/kernel/client/kotlin/net/terramodulus/core/Main.kt b/src/kernel/client/kotlin/net/terramodulus/core/Main.kt index f27c43ad..44747f35 100644 --- a/src/kernel/client/kotlin/net/terramodulus/core/Main.kt +++ b/src/kernel/client/kotlin/net/terramodulus/core/Main.kt @@ -15,7 +15,6 @@ import net.terramodulus.common.core.ApplicationArgumentParsingError import net.terramodulus.common.core.ApplicationInitializationFault import net.terramodulus.common.core.run import net.terramodulus.common.core.setupInit -import net.terramodulus.mui.GuiManager import net.terramodulus.util.exception.CodeLogicFault import net.terramodulus.util.exception.triggerGlobalCrash import java.awt.Dimension diff --git a/src/kernel/client/kotlin/net/terramodulus/core/TerraModulus.kt b/src/kernel/client/kotlin/net/terramodulus/core/TerraModulus.kt index 29846b28..cc1efb8c 100644 --- a/src/kernel/client/kotlin/net/terramodulus/core/TerraModulus.kt +++ b/src/kernel/client/kotlin/net/terramodulus/core/TerraModulus.kt @@ -6,11 +6,12 @@ package net.terramodulus.core import net.terramodulus.common.core.AbstractTerraModulus -import net.terramodulus.mui.GuiManager +import net.terramodulus.mui.MuiManager +import net.terramodulus.mui.gui.GuiManager import net.terramodulus.void.World class TerraModulus internal constructor() : AbstractTerraModulus() { - private val guiManager = GuiManager(this) + private val muiManager = MuiManager(this) internal var world: World? = null override var tps: Int @@ -18,10 +19,9 @@ class TerraModulus internal constructor() : AbstractTerraModulus() { set(value) {} override fun run() { - guiManager.showWindow() + muiManager.showWindow() while (true) { - guiManager.updateCanvas() -// guiManager.updateScreens() + muiManager.update() Thread.sleep(1) } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/GuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt similarity index 87% rename from src/kernel/client/kotlin/net/terramodulus/mui/GuiManager.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt index 60ab391b..a7b7ba86 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/GuiManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ @@ -8,37 +8,32 @@ package net.terramodulus.mui import net.terramodulus.core.TerraModulus import net.terramodulus.engine.MuiEvent import net.terramodulus.engine.Window -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.ScreenManager -import net.terramodulus.mui.input.InputSystem +import net.terramodulus.mui.aui.AuiManager +import net.terramodulus.mui.gui.GuiManager +import net.terramodulus.mui.hui.HuiManager +import net.terramodulus.mui.kui.InputSystem +import net.terramodulus.mui.kui.KuiManager import net.terramodulus.util.logging.logger import java.io.Closeable private val logger = logger {} +private const val WIDTH = 800u +private const val HEIGHT = 480u -/** - * Graphical User Interface (GUI) Manager - */ -internal class GuiManager internal constructor(core: TerraModulus) : Closeable { - private val window = Window() - val renderSystem = RenderSystem(core, window.canvas) - val inputSystem = InputSystem() - val screenManager = ScreenManager(renderSystem.handle) +internal class MuiManager internal constructor(core: TerraModulus) : Closeable { + private val window = Window(WIDTH, HEIGHT) // SDL Window + internal val auiManager = AuiManager() + internal val huiManager = HuiManager() + internal val kuiManager = KuiManager() + internal val guiManager = GuiManager(window, core) internal fun showWindow() = window.show() -// /** -// * Screen updating, targeting as the same as *maximum FPS*, -// * but the numbers of ticks are not supposed to be compensated when missed, -// * so it is up to the callers to compensate missed activities. -// */ -// internal fun updateScreens() {} - /** - * Canvas updating, per frame, maximally the *maximum FPS*. + * SDL events updating, per frame, maximally the *maximum FPS*. * This includes input ticking and canvas rendering. */ - internal fun updateCanvas() { + internal fun update() { val keyEvents = ArrayList() window.pollEvents().forEach { event -> when (event) { @@ -226,7 +221,7 @@ internal class GuiManager internal constructor(core: TerraModulus) : Closeable { } is MuiEvent.WindowPixelSizeChanged -> { logger.debug { "Window pixel size changed to ${event.width}x${event.height}." } - window.canvas.resizeGLViewport() + window.sizeChanged(event.width, event.height) logger.debug { "Window viewport resized." } } is MuiEvent.WindowResized -> { @@ -240,14 +235,12 @@ internal class GuiManager internal constructor(core: TerraModulus) : Closeable { } } } - inputSystem.update(keyEvents) - screenManager.update(renderSystem, inputSystem) - window.canvas.clear() - screenManager.render(renderSystem) - window.swap() + kuiManager.update(keyEvents) + guiManager.updateScreens(this) + guiManager.updateCanvas() } override fun close() { - window.close() + TODO("Not yet implemented") } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/audio/AudioSystem.kt b/src/kernel/client/kotlin/net/terramodulus/mui/aui/AudioSystem.kt similarity index 82% rename from src/kernel/client/kotlin/net/terramodulus/mui/audio/AudioSystem.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/aui/AudioSystem.kt index 3f8bc02b..26f330fa 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/audio/AudioSystem.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/aui/AudioSystem.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.audio +package net.terramodulus.mui.aui class AudioSystem internal constructor() { } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/AuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/aui/AuiManager.kt similarity index 57% rename from src/kernel/client/kotlin/net/terramodulus/mui/AuiManager.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/aui/AuiManager.kt index 7d58bc1f..32ab8450 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/AuiManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/aui/AuiManager.kt @@ -1,11 +1,9 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui - -import net.terramodulus.mui.audio.AudioSystem +package net.terramodulus.mui.aui /** * Audio User Interface (AUI) Manager diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ColorFilter.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ColorFilter.kt deleted file mode 100644 index d3a3ed42..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ColorFilter.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gfx - -typealias ColorFilter = net.terramodulus.engine.ColorFilter - -typealias AlphaFilter = net.terramodulus.engine.AlphaFilter diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/GuiSprite.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/GuiSprite.kt deleted file mode 100644 index d7d7a944..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/GuiSprite.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gfx - -import net.terramodulus.engine.SpriteMesh - -class GuiSprite(private val rect: RectangleI, private val texture: UInt) { - private val mesh = SpriteMesh(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height) - - fun add(model: ModelTransform) = mesh.add(model) - - fun add(filter: ColorFilter) = mesh.add(filter) - - fun render(renderSystem: RenderSystem) = renderSystem.renderGuiTex(mesh, texture) -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ManagedRect.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ManagedRect.kt deleted file mode 100644 index 2362b7ae..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ManagedRect.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gfx - -import kotlin.properties.Delegates.observable - -class ManagedRect(rect: RectangleF) { - var rect: RectangleF by observable(rect) { _, _, newValue -> observers.forEach { it(newValue) } } - - private val observers = LinkedHashSet<(RectangleF) -> Unit>() - - fun observe(observer: (RectangleF) -> Unit) { - observers.add(observer) - } - - fun unobserve(observer: (RectangleF) -> Unit) { - observers.remove(observer) - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ModelTransform.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ModelTransform.kt deleted file mode 100644 index 8dcdb20c..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/ModelTransform.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gfx - -typealias ModelTransform = net.terramodulus.engine.ModelTransform - -typealias SmartScaling = net.terramodulus.engine.SmartScaling - -typealias FullScaling = net.terramodulus.engine.FullScaling - -fun FullScaling(rect: Dimension2I) = FullScaling(rect.width, rect.height) diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Rectangle.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Rectangle.kt deleted file mode 100644 index ecfeca3f..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Rectangle.kt +++ /dev/null @@ -1,130 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gfx - -/** - * Rectangle in a coordinate system with (0, 0) on the bottom left. - * The anchor of the rectangle is the bottom-left corner. - */ -data class RectangleI( - val x: Int, - val y: Int, - val width: Int, - val height: Int -) { - companion object { - fun withPoints(x0: Int, y0: Int, x1: Int, y1: Int): RectangleI { - val minX: Int; - val maxX: Int; - if (x0 < x1) { - minX = x0; - maxX = x1; - } else { - maxX = x0; - minX = x1; - } - val minY: Int; - val maxY: Int; - if (y0 < y1) { - minY = y0; - maxY = y1; - } else { - maxY = y0; - minY = y1; - } - return RectangleI(minX, minY, maxX - minX, maxY - minY) - } - } - - val size get() = Dimension2I(width, height) - - fun anchor(pos: Anchor5) = when (pos) { - Anchor5.TopLeft -> Vector2I(x, y + width) - Anchor5.TopRight -> Vector2I(x + width, y + height) - Anchor5.BottomLeft -> Vector2I(x, y) - Anchor5.BottomRight -> Vector2I(x + width, y) - Anchor5.Center -> Vector2I(x + width / 2, y + height / 2) - } - - fun translateBy(pos: Vector2I) = RectangleI(x + pos.x, y + pos.y, width, height) - - fun translateBy(x: Int, y: Int) = RectangleI(this.x + x, this.y + y, width, height) - - fun translateByY(y: Int) = RectangleI(x, this.y + y, width, height) - - fun translateByX(x: Int) = RectangleI(this.x + x, y, width, height) - - fun translateToY(y: Int) = RectangleI(x, y, width, height) - - fun translateToX(x: Int) = RectangleI(x, y, width, height) - - fun translateTo(pos: Vector2I) = RectangleI(pos.x, pos.y, width, height) - - fun translateTo(x: Int, y: Int) = RectangleI(x, y, width, height) - - fun toFloat() = RectangleF(x.toFloat(), y.toFloat(), width.toFloat(), height.toFloat()) -} - -/** - * Rectangle in a coordinate system with (0, 0) on the bottom left. - * The anchor of the rectangle is the bottom-left corner. - */ -data class RectangleF( - val x: Float, - val y: Float, - val width: Float, - val height: Float -) { - companion object { - fun withPoints(x0: Float, y0: Float, x1: Float, y1: Float): RectangleF { - val minX: Float; - val maxX: Float; - if (x0 < x1) { - minX = x0; - maxX = x1; - } else { - maxX = x0; - minX = x1; - } - val minY: Float; - val maxY: Float; - if (y0 < y1) { - minY = y0; - maxY = y1; - } else { - maxY = y0; - minY = y1; - } - return RectangleF(minX, maxX, minY, maxY) - } - } - - val size get() = Dimension2F(width, height) - - fun anchor(pos: Anchor5) = when (pos) { - Anchor5.TopLeft -> Vector2F(x, y + width) - Anchor5.TopRight -> Vector2F(x + width, y + height) - Anchor5.BottomLeft -> Vector2F(x, y) - Anchor5.BottomRight -> Vector2F(x + width, y) - Anchor5.Center -> Vector2F(x + width / 2, y + height / 2) - } - - fun translateBy(pos: Vector2F) = RectangleF(x + pos.x, y + pos.y, width, height) - - fun translateBy(x: Float, y: Float) = RectangleF(this.x + x, this.y + y, width, height) - - fun translateByY(y: Float) = RectangleF(x, this.y + y, width, height) - - fun translateByX(x: Float) = RectangleF(this.x + x, y, width, height) - - fun translateToY(y: Float) = RectangleF(x, y, width, height) - - fun translateToX(x: Float) = RectangleF(x, y, width, height) - - fun translateTo(pos: Vector2F) = RectangleF(pos.x, pos.y, width, height) - - fun translateTo(x: Float, y: Float) = RectangleF(x, y, width, height) -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Vector.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Vector.kt deleted file mode 100644 index a92d68db..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Vector.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gfx - -data class Vector2I(val x: Int, val y: Int) { - companion object { - val ZERO = Vector2I(0, 0) - } - - operator fun plus(other: Vector2I) = Vector2I(x + other.x, y + other.y) -} - -data class Vector2D(val x: Double, val y: Double) { - companion object { - val ZERO = Vector2D(.0, .0) - } - - operator fun plus(other: Vector2D) = Vector2D(x + other.x, y + other.y) -} - -data class Vector2F(val x: Float, val y: Float) { - companion object { - val ZERO = Vector2F(0F, 0F) - } - - operator fun plus(other: Vector2F) = Vector2F(x + other.x, y + other.y) -} - -data class Vector3I(val x: Int, val y: Int, val z: Int) { - companion object { - val ZERO = Vector3I(0, 0, 0) - } - - operator fun plus(other: Vector3I) = Vector3I(x + other.x, y + other.y, z + other.z) -} - -data class Vector3D(val x: Double, val y: Double, val z: Double) { - companion object { - val ZERO = Vector3D(.0, .0, .0) - } - - operator fun plus(other: Vector3D) = Vector3D(x + other.x, y + other.y, z + other.z) - - operator fun times(factor: Int) = Vector3D(x * factor, y * factor, z * factor) - operator fun times(factor: Float) = Vector3D(x * factor, y * factor, z * factor) - operator fun times(factor: Double) = Vector3D(x * factor, y * factor, z * factor) -} - -data class Vector3F(val x: Float, val y: Float, val z: Float) { - companion object { - val ZERO = Vector3F(0F, 0F, 0F) - } - - operator fun plus(other: Vector3F) = Vector3F(x + other.x, y + other.y, z + other.z) -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/AbstractPanel.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/AbstractPanel.kt deleted file mode 100644 index 810e7259..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/AbstractPanel.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms - -abstract class AbstractPanel : Component(), Container { -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Container.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/Container.kt deleted file mode 100644 index de7f2fba..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Container.kt +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms - -import net.terramodulus.mui.gfx.ManagedRect - -sealed interface Container { - val rect: ManagedRect -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Menu.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/Menu.kt deleted file mode 100644 index ada1df80..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Menu.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms - -import net.terramodulus.mui.gms.event.MenuEvent -import java.util.ArrayDeque - -abstract class Menu : Container { - private val listeners = HashMap, LinkedHashSet<(MenuEvent) -> Unit>>() - private val components = LinkedHashSet() - private val componentQueue = ArrayDeque() - val handle: Handle = HandleImpl() - - private sealed interface ComponentOperation { - class Add(val component: () -> Component) : ComponentOperation - - class Remove(val component: Component) : ComponentOperation - } - - /** - * It is strongly suggested only using this function during initialization. - */ - protected fun addComponent(component: Component) { - components.add(component) - } - - /** - * It is strongly suggested only using this function during initialization. - */ - protected fun removeComponent(component: Component) { - components.remove(component) - } - - fun addListener(e: Class, l: (T) -> Unit) { - @Suppress("UNCHECKED_CAST") - listeners.computeIfAbsent(e) { LinkedHashSet() }.add(l as (MenuEvent) -> Unit) - } - - fun removeListener(e: Class, l: (T) -> Unit) { - listeners[e]?.remove(l) - } - - internal fun dispatchEvent(event: MenuEvent) { - listeners[event.javaClass]?.forEach { it(event) } - } - - sealed interface Handle { - fun addComponent(component: () -> Component) - - fun removeComponent(component: Component) - } - - private inner class HandleImpl : Handle { - override fun addComponent(component: () -> Component) { - componentQueue.add(ComponentOperation.Add(component)) - } - - override fun removeComponent(component: Component) { - componentQueue.add(ComponentOperation.Remove(component)) - } - } - - abstract fun render() -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Screen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/Screen.kt deleted file mode 100644 index 60715ce5..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Screen.kt +++ /dev/null @@ -1,139 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms - -import net.terramodulus.mui.gfx.ManagedRect -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.event.ScreenEvent -import net.terramodulus.mui.input.InputSystem -import java.util.ArrayDeque - -abstract class Screen : Container { - private val listeners = HashMap, LinkedHashSet<(ScreenEvent) -> Unit>>() - private val menus = LinkedHashSet() - private val components = ArrayList() - private val componentQueue = ArrayDeque() - private val menuQueue = ArrayDeque() - override val rect: ManagedRect - get() = TODO("Not yet implemented") - val handle: Handle = HandleImpl() - - private sealed interface ComponentOperation { - fun apply(components: ArrayList) - - class Add(val component: () -> Component) : ComponentOperation { - override fun apply(components: ArrayList) { - components.add(component()) - } - } - - class Remove(val component: Component) : ComponentOperation { - override fun apply(components: ArrayList) { - components.remove(component) - } - } - } - - private sealed interface MenuOperation { - fun apply(menus: LinkedHashSet) - - class Add(val menu: () -> Menu) : MenuOperation { - override fun apply(menus: LinkedHashSet) { - menus.add(menu()) - } - } - - class Remove(val menu: Menu) : MenuOperation { - override fun apply(menus: LinkedHashSet) { - menus.remove(menu) - } - } - } - - /** - * It is strongly suggested only using this function during initialization. - */ - protected fun addComponent(component: Component) { - components.add(component) - } - - /** - * It is strongly suggested only using this function during initialization. - */ - protected fun removeComponent(component: Component) { - components.remove(component) - } - - /** - * It is strongly suggested only using this function during initialization. - */ - protected fun addMenu(menu: Menu) { - menus.add(menu) - } - - /** - * It is strongly suggested only using this function during initialization. - */ - protected fun removeMenu(menu: Menu) { - menus.remove(menu) - } - - fun addListener(e: Class, l: (T) -> Unit) { - @Suppress("UNCHECKED_CAST") - listeners.computeIfAbsent(e) { LinkedHashSet() }.add(l as (ScreenEvent) -> Unit) - } - - fun removeListener(e: Class, l: (T) -> Unit) { - listeners[e]?.remove(l) - } - - internal fun dispatchEvent(event: ScreenEvent) { - listeners[event.javaClass]?.forEach { it(event) } - } - - sealed interface Handle { - fun addComponent(component: () -> Component) - - fun removeComponent(component: Component) - - fun addMenu(menu: () -> Menu) - - fun removeMenu(menu: Menu) - } - - private inner class HandleImpl : Handle { - override fun addComponent(component: () -> Component) { - componentQueue.add(ComponentOperation.Add(component)) - } - - override fun removeComponent(component: Component) { - componentQueue.add(ComponentOperation.Remove(component)) - } - - override fun addMenu(menu: () -> Menu) { - menuQueue.add(MenuOperation.Add(menu)) - } - - override fun removeMenu(menu: Menu) { - menuQueue.add(MenuOperation.Remove(menu)) - } - } - - internal open fun update(renderSystem: RenderSystem, screenManager: ScreenManager, inputSystem: InputSystem) {}; - - internal fun render(renderSystem: RenderSystem, screenManager: ScreenManager) { - componentQueue.forEach { it.apply(components) } - componentQueue.clear() - menuQueue.forEach { it.apply(menus) } - menuQueue.clear() - components.forEach { it.render(renderSystem) } - } - - /** - * Cleans up and closes any used resource here. - */ - abstract fun exit() -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/ScreenManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/ScreenManager.kt deleted file mode 100644 index f618d3ee..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/ScreenManager.kt +++ /dev/null @@ -1,156 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms - -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.impl.LaunchingScreen -import net.terramodulus.mui.input.InputSystem - -class ScreenManager internal constructor(private val renderSystemHandle: RenderSystem.Handle) { - /** - * FILO screen stack; the top-most screen instance is in the last. - */ - private val screens = ArrayDeque() - private val screenQueue = ArrayDeque() - val handle: Handle = HandleImpl() - - init { - screens.add(LaunchingScreen(renderSystemHandle)) - } - - private sealed interface ScreenOperation { - fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) - - /** - * Exits `n` times - * - * @throws IllegalArgumentException when `n` < 1 - * @throws IllegalStateException when `n` >= [screens] size during operation - */ - class Exit(val n: Int) : ScreenOperation { - init { - require(n < 0) { "`n` < 1" } - } - - override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { - if (n >= screens.size) { - throw IllegalStateException("`n` >= screens.size") - } - - for (i in 1..n) { - screens.removeLast().exit() - } - } - } - - /** - * Opens the `screen` - */ - class Open(val screen: (RenderSystem.Handle) -> Screen) : ScreenOperation { - override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { - screens.addLast(screen(handle)) - } - } - - /** - * Opens the `screen` before the `target` screen - */ - class OpenBefore(val screen: (RenderSystem.Handle) -> Screen, val target: Screen) : ScreenOperation { - override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { - screens.add(screens.lastIndexOf(target), screen(handle)) - } - } - - /** - * Exits until reaching the `screen` then remains on the `screen` - */ - class ExitTo(val screen: Screen) : ScreenOperation { - override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { - val it = screens.asReversed().listIterator() - while (it.hasNext()) { - val e = it.next() - if (e == screen) { - break - } else { - it.remove() - e.exit() - } - } - } - } - - /** - * Clears [screens] then opens the `screen` - */ - class Reset(val screen: (RenderSystem.Handle) -> Screen) : ScreenOperation { - override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { - screens.asReversed().forEach { it.exit() } - screens.clear() - screens.add(screen(handle)) - } - } - } - - sealed interface Handle { - /** - * @see ScreenOperation.Exit - */ - fun exit(n: Int) - - /** - * @see ScreenOperation.Open - */ - fun open(screen: (RenderSystem.Handle) -> Screen) - - /** - * It is not recommended to use this in general scenarios. - * @see ScreenOperation.OpenBefore - */ - fun openBefore(screen: (RenderSystem.Handle) -> Screen, target: Screen) - - /** - * @see ScreenOperation.ExitTo - */ - fun exitTo(screen: Screen) - - /** - * @see ScreenOperation.Reset - */ - fun reset(screen: (RenderSystem.Handle) -> Screen) - } - - private inner class HandleImpl : Handle { - override fun exit(n: Int) { - screenQueue.add(ScreenOperation.Exit(n)) - } - - override fun open(screen: (RenderSystem.Handle) -> Screen) { - screenQueue.add(ScreenOperation.Open(screen)) - } - - override fun openBefore(screen: (RenderSystem.Handle) -> Screen, target: Screen) { - screenQueue.add(ScreenOperation.OpenBefore(screen, target)) - } - - override fun exitTo(screen: Screen) { - screenQueue.add(ScreenOperation.ExitTo(screen)) - } - - override fun reset(screen: (RenderSystem.Handle) -> Screen) { - screenQueue.add(ScreenOperation.Reset(screen)) - } - } - - internal fun update(renderSystem: RenderSystem, inputSystem: InputSystem) { - screens.forEach { it.update(renderSystem, this, inputSystem) } - } - - internal fun render(renderSystem: RenderSystem) { - screenQueue.forEach { it.apply(renderSystemHandle, screens) } - screenQueue.clear() - screens.forEach { it.render(renderSystem, this) } - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/ComponentEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/ComponentEvent.kt deleted file mode 100644 index 7383b352..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/ComponentEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.event - -sealed interface ComponentEvent { -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/MenuEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/MenuEvent.kt deleted file mode 100644 index 2cdbc0b5..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/MenuEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.event - -sealed interface MenuEvent { -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/ScreenEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/ScreenEvent.kt deleted file mode 100644 index ae203cae..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/ScreenEvent.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.event - -sealed interface ScreenEvent { - data object Open : ScreenEvent - data object Close : ScreenEvent -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/BlankComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/BlankComponent.kt deleted file mode 100644 index 129f679b..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/BlankComponent.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.Component - -/** - * This can act as a placeholder [Component] in a [Layout][terramodulus.mui.gms.Layout]. - */ -class BlankComponent : Component() { - override fun render(renderSystem: RenderSystem) {} -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/FlexibleBoxLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/FlexibleBoxLayout.kt deleted file mode 100644 index 421b08d2..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/FlexibleBoxLayout.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.RectangleF -import net.terramodulus.mui.gms.Component -import net.terramodulus.mui.gms.Container -import net.terramodulus.mui.gms.Layout - -class FlexibleBoxLayout(container: Container) : Layout(container) { - override val components: Iterable - get() = TODO("Not yet implemented") - - override fun layout(rect: RectangleF) { - TODO("Not yet implemented") - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GeomComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GeomComponent.kt deleted file mode 100644 index a1ed2188..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GeomComponent.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.GuiGeometry -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.Component - -class GeomComponent(val geom: GuiGeometry) : Component() { - override fun render(renderSystem: RenderSystem) { - geom.render(renderSystem) - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GraphicsComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GraphicsComponent.kt deleted file mode 100644 index 2d8aa37f..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GraphicsComponent.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.GuiSprite -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.AbstractPanel -import net.terramodulus.mui.gms.Component - -sealed interface GraphicsComponent - -class SpriteComponent(val sprite: GuiSprite) : Component(), GraphicsComponent { - override fun render(renderSystem: RenderSystem) { - sprite.render(renderSystem) - } -} - -@Suppress("CanSealedSubClassBeObject") -class CanvasComponent : AbstractPanel(), GraphicsComponent { - override fun render(renderSystem: RenderSystem) { - TODO("Not yet implemented") - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/LaunchingScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/LaunchingScreen.kt deleted file mode 100644 index 3442eef2..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/LaunchingScreen.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.AlphaFilter -import net.terramodulus.mui.gfx.Dimension2I -import net.terramodulus.mui.gfx.FullScaling -import net.terramodulus.mui.gfx.GuiRect -import net.terramodulus.mui.gfx.GuiSprite -import net.terramodulus.mui.gfx.RectangleI -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gfx.SmartScaling -import net.terramodulus.mui.gms.Screen -import net.terramodulus.mui.gms.ScreenManager -import net.terramodulus.mui.input.InputSystem - -private val REF_SIZE = Dimension2I(800, 480) - -private val BG_COLOR = floatArrayOf(.145F, .776F, .768F) - -private const val ANI_DURATION = .75F // in second - -private const val PAUSE_DURATION = 2 // in second - -internal class LaunchingScreen(renderSystemHandle: RenderSystem.Handle) : Screen() { - private var stage = 0 - private var last = System.currentTimeMillis() // timestamp in milliseconds - private var alphaFilter = AlphaFilter(0F) - - init { - GeomComponent(GuiRect(0, 0, 800, 480, 37, 198, 196, 255)).apply { - geom.add(alphaFilter) - geom.add(FullScaling(REF_SIZE)) - addComponent(this) - } - SpriteComponent(GuiSprite( - RectangleI(0, 0, 512, 128), - renderSystemHandle.loadTexture("/studio_logo.png"), - )).apply { - sprite.add(alphaFilter) - sprite.add(SmartScaling.both(REF_SIZE.width, REF_SIZE.height, 512, 128)) - addComponent(this) - } - } - - override fun update(renderSystem: RenderSystem, screenManager: ScreenManager, inputSystem: InputSystem) { - val current = System.currentTimeMillis() - val elapsed = (current - last) / 1000F // elapsed time for this stage - when (stage) { - 0 -> if (elapsed >= ANI_DURATION) { - stage = 1 - last = current - alphaFilter.alpha = 1F - } else { - alphaFilter.alpha = elapsed / ANI_DURATION - } - - 1 -> if (elapsed >= PAUSE_DURATION) { - stage = 2 - last = current - } - - 2 -> if (elapsed >= ANI_DURATION) { - stage = 3 - last = current - alphaFilter.alpha = 0F - } else { - alphaFilter.alpha = 1 - elapsed / ANI_DURATION - } - - 3 -> screenManager.handle.reset(::ResourceLoadingScreen) - } - } - - override fun exit() {} -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/PositioningComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/PositioningComponent.kt deleted file mode 100644 index 729b19be..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/PositioningComponent.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.Component - -class PositioningComponent : Component() { - override fun render(renderSystem: RenderSystem) { - TODO("Not yet implemented") - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/ResourceLoadingScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/ResourceLoadingScreen.kt deleted file mode 100644 index e62d091b..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/ResourceLoadingScreen.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.AlphaFilter -import net.terramodulus.mui.gfx.Dimension2I -import net.terramodulus.mui.gfx.FullScaling -import net.terramodulus.mui.gfx.GuiRect -import net.terramodulus.mui.gfx.GuiSprite -import net.terramodulus.mui.gfx.RectangleI -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gfx.SmartScaling -import net.terramodulus.mui.gfx.Vector3F -import net.terramodulus.mui.gms.Screen -import net.terramodulus.mui.gms.ScreenManager -import net.terramodulus.mui.input.InputSystem -import kotlin.math.max -import kotlin.math.min -import kotlin.properties.Delegates - -private val REF_SIZE = Dimension2I(800, 480) - -private val CONTENT_SIZE = Dimension2I(400, 200) - -private val BG_COLOR = floatArrayOf(.145F, .776F, 0.768F) - -private const val ANI_DURATION = 1F // in second -private const val PAUSE_DURATION = 2F // in second - -class ResourceLoadingScreen(renderSystemHandle: RenderSystem.Handle) : Screen() { - private var stage = 0 - private var last = System.currentTimeMillis() // timestamp in milliseconds - private var alphaFilter = AlphaFilter(0F) - private val progressBar = ProgressBar() - - init { - GeomComponent(GuiRect(0, 0, 800, 480, 0, 255, 213, 255)).apply { - geom.add(alphaFilter) - geom.add(FullScaling(REF_SIZE)) - addComponent(this) - } - val smartScaling = SmartScaling.both(REF_SIZE.width, REF_SIZE.height, CONTENT_SIZE.width, CONTENT_SIZE.height) - SpriteComponent(GuiSprite(RectangleI(0, 100, 400, 100), renderSystemHandle.loadTexture("/game_logo.png"))).apply { - sprite.add(alphaFilter) - sprite.add(smartScaling) - addComponent(this) - } - GeomComponent(GuiRect(0, 0, 400, 40, 240, 240, 240, 255)).apply { - geom.add(alphaFilter) - geom.add(smartScaling) - addComponent(this) - } - GeomComponent(GuiRect(5, 5, 395, 35, 0, 255, 213, 255)).apply { - geom.add(alphaFilter) - geom.add(smartScaling) - addComponent(this) - } - GeomComponent(progressBar.rect).apply { - geom.add(alphaFilter) - geom.add(smartScaling) - addComponent(this) - } - } - - private class ProgressBar { - val rectDim = RectangleI.withPoints(7, 7, 393, 33) - val length = rectDim.width - var progress: Float by Delegates.observable(0f) { _, _, _ -> - rect.setPos(7, 7, rectDim.x + (progress * length).toInt(), 33) - } - val rect = GuiRect(7, 7, 7, 33, 240, 240, 240, 255) - } - - override fun update(renderSystem: RenderSystem, screenManager: ScreenManager, inputSystem: InputSystem) { - val current = System.currentTimeMillis() - val elapsed = (current - last) / 1000F // elapsed time in second at this stage - when (stage) { - 0 -> if (elapsed >= ANI_DURATION) { - stage = 1 - last = current - alphaFilter.alpha = 1F - } else { - alphaFilter.alpha = elapsed / ANI_DURATION - } - - 1 -> { - // TODO when there is something to load, stay at this stage until ready - progressBar.progress = min(elapsed / PAUSE_DURATION, 1F) - if (progressBar.progress >= 1F) { - stage = 2 - last = current - } - } - - 2 -> if (elapsed >= ANI_DURATION) { - stage = 3 - last = current - alphaFilter.alpha = 0F - } else { - alphaFilter.alpha = 1 - elapsed / ANI_DURATION - } - -// 3 -> screenManager.handle.openBefore(::TitleScreen, this) - 3 -> screenManager.handle.reset(renderSystem.newGameplayScreen(Vector3F.ZERO)) - } - } - - override fun exit() { - - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/SequenceLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/SequenceLayout.kt deleted file mode 100644 index 9d80c12c..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/SequenceLayout.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.RectangleF -import net.terramodulus.mui.gms.Component -import net.terramodulus.mui.gms.Container -import net.terramodulus.mui.gms.Layout - -/** - * Common implementation that is either [ColumnLayout] or [RowLayout]. - * - * This is an optimized special version of [FlexibleBoxLayout] without any expected - * multiple *sequences* of components in a single layout. - */ -sealed class SequenceLayout(container: Container, elements: ElementList) : - Layout.ElementGroup(container, elements) { - class Element { - companion object { - fun default() = Element() - } - } - - override fun add(component: Component) = elements.add(component, Element.default()) - - override fun addBefore(target: Component, component: Component) = - elements.addBefore(target, component, Element.default()) - - override fun addAfter(target: Component, component: Component) = - elements.addAfter(target, component, Element.default()) - - override fun replace(target: Component, component: Component) = - elements.replace(target, component, Element.default()) -} - -/** - * **Column** case of [SequenceLayout]. - */ -class ColumnLayout private constructor(container: Container, elements: ElementList) : - SequenceLayout(container, elements) { - companion object { - fun withComponents(vararg components: Component) = { it: Container -> - ColumnLayout(it, ElementList.withComponentsDefault(Element::default, *components)) - } - - fun withComponents(components: Collection) = { it: Container -> - ColumnLayout(it, ElementList.withComponentsDefault(Element::default, components)) - } - - fun withElements(vararg elements: Pair) = { it: Container -> - ColumnLayout(it, ElementList.withElements(*elements)) - } - - fun withElements(elements: Map) = { it: Container -> - ColumnLayout(it, ElementList.withElements(elements)) - } - } - - override fun layout(rect: RectangleF) { - elements.forEach { TODO() } - } -} - -/** - * **Row** case of [SequenceLayout]. - */ -class RowLayout private constructor(container: Container, elements: ElementList) : - SequenceLayout(container, elements) { - companion object { - fun withComponents(vararg components: Component) = { it: Container -> - RowLayout(it, ElementList.withComponentsDefault(Element::default, *components)) - } - - fun withComponents(components: Collection) = { it: Container -> - RowLayout(it, ElementList.withComponentsDefault(Element::default, components)) - } - - fun withElements(vararg elements: Pair) = { it: Container -> - RowLayout(it, ElementList.withElements(*elements)) - } - - fun withElements(elements: Map) = { it: Container -> - RowLayout(it, ElementList.withElements(elements)) - } - } - - override fun layout(rect: RectangleF) { - elements.forEach { TODO() } - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/TitleScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/TitleScreen.kt deleted file mode 100644 index a13d8e02..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/TitleScreen.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gms.impl - -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.Screen - -class TitleScreen(renderSystemHandle: RenderSystem.Handle) : Screen() { - override fun exit() {} -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt new file mode 100644 index 00000000..651c764b --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +import net.terramodulus.core.TerraModulus +import net.terramodulus.engine.Window +import net.terramodulus.mui.MuiManager +import net.terramodulus.mui.gui.agim.LayoutManager +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.asd.AsdManager +import net.terramodulus.util.logging.logger +import java.io.Closeable + +private val logger = logger {} + +/** + * Graphical User Interface (GUI) Manager + */ +internal class GuiManager internal constructor(private val window: Window, core: TerraModulus) : Closeable { + val renderSystem = RenderSystem(core, window.canvas) + val asdManager = AsdManager() + val screenManager = ScreenManager(window, renderSystem.handle, asdManager.AgimHandle()) + val layoutManager = LayoutManager(screenManager) + + private var proceeded = false + + /** + * Screen updating, targeting as the same as *maximum FPS*, + * but the numbers of ticks are not supposed to be compensated when missed, + * so it is up to the callers to compensate missed activities. + */ + internal fun updateScreens(muiManager: MuiManager) { + screenManager.update(muiManager) + if (!proceeded) { + asdManager.process() + proceeded = true + } + layoutManager.tick() + } + + /** + * Canvas updating, per frame, maximally the *maximum FPS*. + * This includes input ticking and canvas rendering. + */ + internal fun updateCanvas() { + window.canvas.clear() + screenManager.render(renderSystem) + window.swap() + } + + override fun close() { + window.close() + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AbstractPane.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AbstractPane.kt new file mode 100644 index 00000000..28a4b261 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AbstractPane.kt @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.asd.AsdProcessor +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleF + +abstract class AbstractPane(asdHandle: AsdHandle) : Component(asdHandle), Container { + protected inner class ComponentAsdHandleImpl : AsdHandle.Container() { + override lateinit var rect: RectangleD + override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdHandle.registerAsdProcessor(processor) + } + + final override fun update(muiIoI: ScreenManager.MuiIoI) { + super.update(muiIoI) + layout.update() + layout.components.forEach { it.update(muiIoI) } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoProperty.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoProperty.kt new file mode 100644 index 00000000..b1939d3a --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoProperty.kt @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.util.TypedMap + +/** + * Instances of this class must be immutable, as simple values; + * may be data classes, data objects, enums. + */ +abstract class AgimoProperty + +fun T.getPropertyKey() = AgimoPropertyMap.Key(javaClass) + +class AgimoPropertyMap { + private val properties = TypedMap>() + private val observers = HashMap, + LinkedHashSet<(AgimoProperty?, AgimoProperty?) -> Unit>>() + + class Key(c: Class) : TypedMap.Key(c) + + @Suppress("UNCHECKED_CAST") + fun getProperty(key: Key) = properties[key] as T? + + fun putProperty(key: Key, value: T) { + @Suppress("UNCHECKED_CAST") + val old = properties.put(key, value) as T? + triggerPropertyObservers(key, old, value) + } + + @Suppress("UNCHECKED_CAST") + fun removeProperty(key: Key): T? { + val old = properties.remove(key) as T? + triggerPropertyObservers(key, old, null) + return old + } + + fun containsProperty(key: Key) = properties.containsKey(key) + + /** + * @see java.util.Map.compute + */ + inline fun computeProperty(key: Key, remappingFunction: (Key, T?) -> T?) { + val v = remappingFunction(key, getProperty(key)) + if (v === null) removeProperty(key) else putProperty(key, v) + } + + /** + * @see java.util.Map.computeIfAbsent + */ + inline fun computePropertyIfAbsent(key: Key, mappingFunction: (Key) -> T?) { + if (!containsProperty(key)) { + val v = mappingFunction(key) + if (v !== null) putProperty(key, v) + } + } + + /** + * @see java.util.Map.computeIfPresent + */ + inline fun computePropertyIfPresent(key: Key, remappingFunction: (Key, T) -> T?) { + val v = getProperty(key) + if (v !== null) { + val v = remappingFunction(key, v) + if (v !== null) putProperty(key, v) + } + } + + /** + * @see java.util.Map.merge + */ + inline fun mergeProperty(key: Key, value: T, remappingFunction: (T, T) -> T?) { + val v = getProperty(key) + if (v !== null) { + val v = remappingFunction(v, value) + if (v !== null) putProperty(key, v) + } else putProperty(key, value) + } + + fun observeProperty(key: Key, l: (T?, T?) -> Unit) { + @Suppress("UNCHECKED_CAST") + observers.computeIfAbsent(key) { LinkedHashSet() }.add(l as (AgimoProperty?, AgimoProperty?) -> Unit) + } + + fun unobserveProperty(key: Key, l: (T?, T?) -> Unit) { + observers[key]?.remove(l) + } + + private fun triggerPropertyObservers(key: Key, old: T?, new: T?) { + observers[key]?.forEach { it(old, new) } + } + + @Suppress("UNCHECKED_CAST") + fun asMap(): Map, AgimoProperty> = properties as Map, AgimoProperty> + +} + +@Suppress("UNCHECKED_CAST") +fun Map, AgimoProperty>.getProperty(key: AgimoPropertyMap.Key) = + this[key] as T? diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoTreeVisitor.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoTreeVisitor.kt new file mode 100644 index 00000000..d6232bdc --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoTreeVisitor.kt @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import java.util.LinkedList + +internal abstract class AgimoTreeVisitor { + internal fun interface ScreenTreeVisitor { + fun visit(): Sequence + } + + internal fun interface MenuTreeVisitor { + fun visit(): Sequence + } + + class RootNode(screens: Sequence, menus: Sequence) { + val screens = ScreenTree(screens) + val menus = MenuTree(menus) + } + + sealed class ContainerNode(val layout: Layout) { + val elements = LinkedList(layout.components.map { + if (it is AbstractPane) PaneNode(it) else SimpleComponentNode(it) + }.toList()) + } + + sealed interface ComponentNode { + val component: Component + } + + class SimpleComponentNode(override val component: Component) : ComponentNode + + class PaneNode(override val component: AbstractPane) : ContainerNode(component.layout), ComponentNode + + class ScreenTree(screens: Sequence) { + val list = LinkedList(screens.map { ScreenNode(it) }.toList()) + } + + class ScreenNode(val screen: Screen) : ContainerNode(screen.layout) { + val menus = MenuTree(screen.visit().visit()) + } + + class MenuTree(menus: Sequence) { + val list = LinkedList(menus.map { MenuNode(it) }.toList()) + } + + class MenuNode(val menu: Menu) : ContainerNode(menu.layout) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Alignment.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Alignment.kt new file mode 100644 index 00000000..04e024cd --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Alignment.kt @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import com.cout970.math.vec2.Vec2d +import com.cout970.math.vec2.minus +import com.cout970.math.vec2.plus +import net.terramodulus.mui.gui.gfx.Anchor5 +import net.terramodulus.mui.gui.gfx.Dimension2D +import net.terramodulus.mui.gui.gfx.RectangleD + +const val ALIGN_START = 0.0 +const val ALIGN_CENTER = 0.5 +const val ALIGN_END = 1.0 + +/** + * `anchor` is relative position of positioning anchor from the anchor of the rectangle/dimension.; + * should be within the bounds of the rectangle/dimension. + */ +class AnchorAlignmentHelper { + data class Subject(val rect: RectangleD, val anchor: Vec2d) { + fun alignTarget(target: Target): RectangleD { + // This should be the anchor of the rectangle of target + val anchor = rect.anchor(Anchor5.BottomLeft) + anchor - target.anchor + return RectangleD(anchor.x, anchor.y, target.dim.width, target.dim.height) + } + } + + data class Target(val dim: Dimension2D, val anchor: Vec2d) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Component.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Component.kt similarity index 50% rename from src/kernel/client/kotlin/net/terramodulus/mui/gms/Component.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Component.kt index c67121d8..f4058b7c 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Component.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Component.kt @@ -1,29 +1,28 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gms +package net.terramodulus.mui.gui.agim -import net.terramodulus.mui.gfx.ManagedRect -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gms.event.ComponentEvent -import net.terramodulus.mui.input.InputSystem +import net.terramodulus.mui.gui.agim.event.ComponentEvent +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.RenderSystem /** - * [Component] can only be contained by only one [Container]. + * [Component] can only be contained by only one [Container][net.terramodulus.mui.gui.agim.Container] at once. * * It is an undefined behavior when the `Component` is contained repeatedly * or in different containers simultaneously. */ -abstract class Component { +abstract class Component(open val asdHandle: AsdHandle) { private val listeners = HashMap, LinkedHashSet<(ComponentEvent) -> Unit>>() - /** - * This should only be modified by [Layout] managers. - */ - open lateinit var rect: ManagedRect - internal set +// /** +// * Caveat: This should only be modified by [Layout][net.terramodulus.mui.gui.agim.Layout] managers. +// */ +// open lateinit var layoutHandle: LayoutHandle +// internal set abstract fun render(renderSystem: RenderSystem) @@ -40,7 +39,7 @@ abstract class Component { listeners[event.javaClass]?.forEach { it(event) } } - fun update(inputSystem: InputSystem) { - TODO() + internal open fun update(muiIoI: ScreenManager.MuiIoI) { + dispatchEvent(ComponentEvent.Update(muiIoI)) } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Container.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Container.kt new file mode 100644 index 00000000..a80e16d8 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Container.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.mui.gui.asd.AsdHandle + +/** + * **AGIM Container**, direct subclasses are explicitly defined. + */ +sealed interface Container { + val asdHandle: AsdHandle//.Container + + val layout: Layout +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Layout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt similarity index 72% rename from src/kernel/client/kotlin/net/terramodulus/mui/gms/Layout.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt index 53e52ff9..d504356f 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/Layout.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt @@ -1,13 +1,13 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gms +package net.terramodulus.mui.gui.agim -import net.terramodulus.mui.gfx.RectangleF -import net.terramodulus.mui.gms.impl.SequenceLayout.Element -import kotlin.reflect.KProperty +import net.terramodulus.mui.gui.gfx.RenderSystem +import java.io.Closeable +import java.util.ArrayDeque /** * [Layout] is always mutable. @@ -15,40 +15,73 @@ import kotlin.reflect.KProperty * **Layout** is defined only when all its managed components all belong to the container * associated with this layout manager *exclusively*. */ -abstract class Layout(private val container: Container) { +abstract class Layout(protected val container: Container) : Closeable { companion object { - const val ALIGN_START = 0F; - const val ALIGN_CENTER = .5F; - const val ALIGN_END = 1F; + const val ALIGN_START = 0F + const val ALIGN_CENTER = .5F + const val ALIGN_END = 1F } - abstract val components: Iterable + abstract val components: Sequence - private val containerObserver = ::layout.apply(container.rect::observe) + private val layoutOperations = ArrayDeque() + + fun interface Operation { + fun Layout.operate() + } /** - * Updates the layout output using the current layout configurations - * by invoking [layout] internally. + * Query [Operation] on the [Layout] structures, potentially changing any components and configurations. * - * It is recommended to invoke this when this layout is being initialized - * or any layout configuration has been changed. + * It is required to use this when this layout is being initialized + * or any layout configuration is being changed. + */ + fun operate(operation: Operation) { + layoutOperations.add(operation) + } + + /** + * Updates the layout output using pending operations added via [operate], + * and the resultant layout configurations by invoking [layOut] internally if any operation exists. */ - fun update() = layout(container.rect.rect) + fun update() { + val nonEmpty = layoutOperations.isNotEmpty() + while (layoutOperations.isNotEmpty()) { + with(layoutOperations.removeFirst()) { this@Layout.operate() } + } + if (nonEmpty) updated = true + } + + /** + * Whether this [Layout] has been updated (as in [update]) at this moment. + * This should only be updated by [update] and [LayoutManager]. + */ + internal var updated = false /** * Lays out the managed [components] by this [Layout] manager. * - * Only the `rect`s of the managed `components` should be (re)assigned; - * no other state-changing operations should be done beside this. - * @param rect the rectangle of the container at this moment + * In most cases, only the `layoutHandle`s of the managed `components` should be (re)assigned. + * Otherwise, no other state-changing operations should be done beside this. + */ + protected abstract fun layOut(handle: LayoutHandle): Sequence + + internal fun layOutInternal(handle: LayoutHandle) = layOut(handle) + + /** + * Renders this [Layout] with underlying managed [components]. */ - protected abstract fun layout(rect: RectangleF) + internal fun render(renderSystem: RenderSystem) = components.forEach { it.render(renderSystem) } /** * Must be invoked when this [Layout] is no longer in use. */ - fun clear() { - container.rect.unobserve(containerObserver) + fun clear() { // Not sure whether there is the necessity to separate this from [close]. +// container.asdHandle.unobserveRect(containerObserver) + } + + override fun close() { + clear() } /** @@ -93,9 +126,9 @@ abstract class Layout(private val container: Container) { abstract class ElementGroup protected constructor( container: Container, - protected val elements: ElementList, + protected open val elements: ElementList, ) : Group(container) { - final override val components = elements.componentsView + final override val components = elements.componentsView.asSequence() override fun contains(component: Component): Boolean = elements.contains(component) @@ -144,39 +177,18 @@ abstract class Layout(private val container: Container) { elements.replace(target, component, element) } - /** - * @param components must not be empty - */ - protected class ComponentIterable(private vararg val components: KProperty) : Iterable { - override fun iterator(): Iterator = object : Iterator { - private var index = 0 - - private fun untilNotNull(): Boolean { - do { - if (components[index].getter.call() != null) - return true - else - index++ - } while (index < components.size) - return false - } - - override fun hasNext(): Boolean = untilNotNull() + protected fun componentsNullableSequence(vararg components: () -> Component?) = + sequenceOf(*components).mapNotNull { it() } - override fun next(): Component = if (untilNotNull()) { - components[index].getter.call()!! - } else { - throw NoSuchElementException() - } - } - } + protected fun componentsSequence(vararg components: () -> Component) = + sequenceOf(*components).map { it() } /** * Internal list of the layout elements. */ protected class ElementList private constructor( - private val components: MutableList, // order is defined here - private val elementMap: MutableMap, // elements are mapped here + private val components: MutableList, // order is defined here + private val elementMap: MutableMap, // elements are mapped here ) : Iterable> { companion object { fun withComponentsDefault(default: () -> E, vararg components: Component): ElementList { diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutComputation.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutComputation.kt new file mode 100644 index 00000000..08c1a778 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutComputation.kt @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.mui.gui.asd.AsdHandle + +class LayoutComputationUnit @PublishedApi internal constructor( + val computation: LayoutHandle.() -> Map, + val dependencies: Map>>, + val results: Map>>, +) { + companion object { + inline operator fun invoke( + dependencies: MutableMap>>.() -> Unit, + results: MutableMap>>.() -> Unit, + noinline computation: LayoutHandle.() -> Map, + ) = LayoutComputationUnit(computation, + mutableMapOf>>().apply(dependencies), + mutableMapOf>>().apply(results), + ) + } +} + +class LayoutComputationGroup @PublishedApi internal constructor( + val conditions: () -> Set, +// val conditions: LayoutHandle.() -> Set, + val dependencies: MutableMap>>, +) { + companion object { + inline operator fun invoke( + dependencies: MutableMap>>.() -> Unit, + noinline conditions: () -> Set, +// noinline conditions: LayoutHandle.() -> Set, + ) = LayoutComputationGroup(conditions, + mutableMapOf>>().apply(dependencies), + ) + } +} + +// class LayoutComputationDependency(val key: AgimoPropertyMap.Key<*>) { // Dependencies saved in a Set (immutable) +// } + +// class LayoutComputationResult() { // Results saved in a Map (immutable) +// } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutHandle.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutHandle.kt new file mode 100644 index 00000000..334e8e21 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutHandle.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.mui.gui.asd.AsdHandle + +abstract class LayoutHandle internal constructor() { + abstract fun getUnit(handle: AsdHandle): Unit + + abstract class Unit internal constructor() { + abstract fun getProperty(key: AgimoPropertyMap.Key): T? + abstract fun containsProperty(key: AgimoPropertyMap.Key): Boolean + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt new file mode 100644 index 00000000..e4e24840 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt @@ -0,0 +1,393 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import net.terramodulus.mui.gui.agim.impl.BoundsProperty +import net.terramodulus.mui.gui.agim.impl.RectangleProperty +import net.terramodulus.mui.gui.asd.AsdHandle +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.mutableSetOf + +internal class LayoutManager(screenManager: ScreenManager) { + private val agimoTree = AgimoTree(screenManager) + + // TODO maybe later think of a way to dynamically update nodes partially + private class AgimoTree(screenManager: ScreenManager) : AgimoTreeVisitor() { + private val screenVisitor = screenManager.visitScreens() + private val menuVisitor = screenManager.visitMenus() + var root = RootNode(sequenceOf(), sequenceOf()) + + private fun update() { + root = RootNode(screenVisitor.visit(), menuVisitor.visit()) + } + + private fun listContainers(): Map { + val containers = mutableMapOf() + fun pushElements(node: ContainerNode) { + node.elements.forEach { + if (it is PaneNode) { + containers[it] = it.component.asdHandle + pushElements(it) + } + } + } + fun pushMenu(node: MenuNode) { + containers[node] = node.menu.asdHandle + pushElements(node) + } + fun pushScreen(node: ScreenNode) { + containers[node] = node.screen.asdHandle + node.menus.list.forEach(::pushMenu) + pushElements(node) + } + root.screens.list.forEach(::pushScreen) + root.menus.list.forEach(::pushMenu) + return containers + } + + fun discoverLayoutChanges(): LayoutChanges { + val old = listContainers() + update() + val new = listContainers() + return LayoutChanges( + old.filter { !new.containsKey(it.key) }.entries.associate { it.value to it.key.layout }, + new.filter { !old.containsKey(it.key) }.entries.associate { it.value to it.key.layout }, + new.filter { old.containsKey(it.key) && it.key.layout.updated } + .entries.associate { it.value to it.key.layout }, + ) + } + + class LayoutChanges( + val removed: Map, + val added: Map, + val updated: Map, + ) + } + + private data class LayoutNode( + val layout: Layout, + val containerHandle: AsdHandle, + val groups: Sequence, +// var units: Set, + ) + + private typealias InstancedPropertyMap = MutableMap, V>> + + private operator fun InstancedPropertyMap.get(handle: AsdHandle, key: AgimoPropertyMap.Key<*>) = + this[handle]?.get(key) + + private inline fun , E> InstancedPropertyMap + .push(handle: AsdHandle, key: AgimoPropertyMap.Key<*>, value: E, crossinline ifAbsent: () -> V) = + computeIfAbsent(handle) { mutableMapOf() }.computeIfAbsent(key) { ifAbsent() }.add(value) + + private fun InstancedPropertyMap.put(handle: AsdHandle, key: AgimoPropertyMap.Key<*>, value: V) = + computeIfAbsent(handle) { mutableMapOf() }.put(key, value) + +// private fun , E> InstancedPropertyMap +// .push(handle: AsdHandle, key: AgimoPropertyMap.Key<*>, value: E) = +// push(handle, key, value) { mutableSetOf() as MutableCollection } + + private val states = LayoutStates() + + private inner class LayoutStates { + private val layouts = mutableMapOf() + private val groupDeps: InstancedPropertyMap> = mutableMapOf() + private val unitDeps: InstancedPropertyMap> = mutableMapOf() +// private val unitResults: InstancedPropertyMap> = mutableMapOf() + private val units = mutableMapOf>() + private val unitGroups = mutableMapOf() + private val results = mutableMapOf>() + private val depResults = mutableMapOf, AgimoProperty>>() + private val depResultUnits: InstancedPropertyMap = mutableMapOf() + private val affectedProps = mutableMapOf>>() +// private val changedUnits = mutableSetOf() + + fun getLayout(asdHandle: AsdHandle) = layouts[asdHandle] + + fun addLayout(asdHandle: AsdHandle, layout: LayoutNode) { + if (layouts.containsKey(asdHandle)) throw IllegalStateException() + layouts[asdHandle] = layout + layout.groups.forEach { group -> + group.dependencies.forEach { (handle, keys) -> + keys.forEach { + groupDeps.push(handle, it, group) { mutableSetOf() } + } + } + } + } + + private fun addAffectedProp(handle: AsdHandle, key: AgimoPropertyMap.Key<*>) { + affectedProps.computeIfAbsent(handle) { mutableSetOf() }.add(key) + } + + fun compute() { + // TODO Currently groups have no dependencies, so not sure how this should be handled + // However, instancing Groups does not depend on states of Layouts, + // but computations of Units from Groups depend on states of Layouts. + + val unitsToCompute = mutableSetOf() + // Recompute for Units where needed + run { + val unitsToRecompute = mutableSetOf() + affectedProps.forEach { (handle, keys) -> + keys.forEach { + unitDeps[handle, it]?.apply { unitsToRecompute.addAll(this) } + } + } + affectedProps.clear() + val groupsToCompute = mutableSetOf() +// unitsToRecompute.forEach { +// val group = unitGroups[it]!! +// if (groupsToCompute.add(group)) removeGroupUnits(group) +// } + layouts.values.forEach { layout -> + layout.groups.forEach { + if (!units.containsKey(it)) groupsToCompute.add(it) + } + } + groupsToCompute.forEach { group -> + // Ignore dependencies and LayoutHandle used for Group.conditions at the moment + group.conditions().apply { + units[group] = this + }.forEach { unit -> + unitsToCompute.add(unit) + unitGroups[unit] = group + unit.dependencies.forEach { (handle, keys) -> + keys.forEach { + unitDeps.push(handle, it, unit) { mutableSetOf() } + } + } +// unit.results.forEach { (handle, keys) -> +// keys.forEach { +// unitResults.push(handle, it, unit) { mutableSetOf() } +// } +// } + } + } + } + + // Compute Units while respecting their dependencies + units.values.asSequence().flatten().forEach { if (!results.containsKey(it)) unitsToCompute.add(it) } + run { + var added = true + val unitsToRecompute = mutableSetOf() + unitsToRecompute.addAll(unitsToCompute) + while (added) { + unitsToRecompute.forEach { unit -> + unit.results.forEach { (handle, keys) -> + keys.forEach { addAffectedProp(handle, it) } + } + } + unitsToRecompute.clear() + affectedProps.forEach { (handle, keys) -> + keys.forEach { + unitDeps[handle, it]?.apply { unitsToRecompute.addAll(this) } + } + } + added = unitsToCompute.addAll(unitsToRecompute) + affectedProps.clear() + } + } +// val parents = mutableMapOf() +// val children = mutableMapOf>() +// val roots = mutableSetOf() + // one coroutine put Units to the forest and send ?? (do we really need this?) + // one coroutine checks for computability of Units (by dependencies) + // - compute result dependencies that are required by awaiting Units at the moment, + // only when those result dependencies are with uncomputed Units + // - Note: if any case some Units are dependencies but computed already, already fulfilled + // - send fulfilled computable Units to another coroutine + // one coroutine receives computable Units then compute Units sequentially in its scope + // with dependencies computed and combined as Handle, + // then notify another coroutine for dependencies by computed results + runBlocking { + val channelToCompute = Channel(UNLIMITED) + val channelComputed = Channel(UNLIMITED) + launch { + val unitResults: InstancedPropertyMap> = mutableMapOf() + unitsToCompute.forEach { unit -> + unit.results.forEach { (handle, keys) -> + keys.forEach { + unitResults.push(handle, it, unit) { mutableSetOf() } + } + } + } + val unitDeps = mutableMapOf>() + val unitChildren = mutableMapOf>() + unitsToCompute.forEach { unit -> + unit.dependencies.forEach { (handle, keys) -> + keys.forEach { key -> + val deps = unitResults[handle, key] + if (deps !== null) { + deps.forEach { + unitDeps.computeIfAbsent(unit) { mutableSetOf() }.add(it) + unitChildren.computeIfAbsent(it) { mutableSetOf() }.add(unit) + } + } + } + } + } + // First compute Units without any dependency needed to be computed + unitsToCompute.iterator().apply { + while (hasNext()) { + val unit = next() + if (unitDeps[unit] === null) { + channelToCompute.send(unit) + remove() + } + } + } + for (unit in channelComputed) { + unitChildren.remove(unit)?.forEach { + val deps = requireNotNull(unitDeps[it]) + assert(deps.remove(unit)) + if (deps.isEmpty()) { + channelToCompute.send(it) + unitDeps.remove(it) + unitsToCompute.remove(it) + } + } + if (unitsToCompute.isEmpty()) { + channelToCompute.close() + assert(unitChildren.isEmpty()) + assert(unitDeps.isEmpty()) + } + } + } + launch { + val layoutHandle = object : LayoutHandle() { + override fun getUnit(handle: AsdHandle) = object : Unit() { + override fun getProperty(key: AgimoPropertyMap.Key) = + depResults[handle]?.getProperty(key) ?: handle.properties.getProperty(key) + + override fun containsProperty(key: AgimoPropertyMap.Key) = + depResults[handle]?.containsKey(key) == true || handle.properties.containsProperty(key) + } + } + for (unit in channelToCompute) { + val result = unit.computation(layoutHandle) + results[unit] = result + result.forEach { (handle, map) -> + map.asMap().forEach { (key, property) -> + // At the moment, if two Units compute to the same Property, a conflict occurs; + // maybe later if needed, implement precedence or priority or explicit overriding + // for each Property result from different Units + depResultUnits.put(handle, key, unit)?.let { + throw IllegalStateException() + } + depResults.computeIfAbsent(handle) { mutableMapOf() }[key] = property + } + } + channelComputed.send(unit) + } + channelComputed.close() + } + } + + layouts.values.forEach { node -> + node.layout.components.forEach { + val dep = requireNotNull(depResults[it.asdHandle]) + val prev = try { + it.asdHandle::rect.get() + } catch (_: UninitializedPropertyAccessException) { + null + } + it.asdHandle.rect = dep.getProperty(RectangleProperty.KEY)?.value + ?: requireNotNull(dep.getProperty(BoundsProperty.KEY)).value + if (prev != it.asdHandle.rect) + it.asdHandle.triggerRectObservers() + } + } + } + + fun validateCyclicGraphs() { + run { // Groups + data class GroupNode( + val deps: Set>>, + val children: Set>>, + ) + data class PropNode( + val deps: Set, + val children: Set, + ) + val visited = mutableSetOf() + val visiting = mutableSetOf() + layouts.values.map { +// GroupNode( +// it.group.dependencies.flatMap { (handle, keys) -> keys.map { it -> handle to it } }.toSet(), +// it.units.flatMap { it -> +// it.results.flatMap { (handle, keys) -> keys.map { it -> handle to it } } +// }.toSet(), +// ) + } + + } + run { // Units + + } + } + + fun removeLayout(asdHandle: AsdHandle) { + val layout = layouts.remove(asdHandle) + if (layout === null) throw IllegalStateException() + layout.groups.forEach { group -> + group.dependencies.forEach { (handle, keys) -> + keys.forEach { groupDeps[handle, it]?.remove(group) } + } + removeGroupUnits(group) + } + } + + private fun removeGroupUnits(group: LayoutComputationGroup) { + units.remove(group)?.forEach { unit -> + results.remove(unit)!!.forEach { (handle, map) -> // Then this must have been computed, so non-null + map.asMap().keys.forEach { + if (depResultUnits[handle, it] === unit) { + assert(depResults[handle]!!.remove(it) !== null) + assert(depResultUnits[handle]!!.remove(it) !== null) + } + } + } + unitGroups.remove(unit) + unit.dependencies.forEach { (handle, keys) -> + keys.forEach { unitDeps[handle, it]?.remove(unit) } + } + unit.results.forEach { (handle, keys) -> + keys.forEach { addAffectedProp(handle, it) } + } + } + } + } + + internal fun tick() { + val changes = agimoTree.discoverLayoutChanges() + (changes.removed.keys.asSequence() + changes.updated.keys.asSequence()).forEach { + states.removeLayout(it) + } + val layoutHandle = object : LayoutHandle() { + override fun getUnit(handle: AsdHandle) = object : Unit() { + override fun getProperty(key: AgimoPropertyMap.Key) = + handle.properties.getProperty(key) + + override fun containsProperty(key: AgimoPropertyMap.Key) = + handle.properties.containsProperty(key) + } + } + (changes.updated.asSequence() + changes.added.asSequence()).forEach { (handle, layout) -> + states.addLayout(handle, LayoutManager.LayoutNode( + layout, + handle, + layout.layOutInternal(layoutHandle) + )) + } + states.compute() + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Menu.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Menu.kt new file mode 100644 index 00000000..70adafe8 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Menu.kt @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.mui.gui.agim.event.MenuEvent +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.asd.AsdProcessor +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleF +import net.terramodulus.mui.gui.gfx.RenderSystem +import java.io.Closeable + +abstract class Menu( + managerHandle: MenuManager.Handle, + final override val asdHandle: AsdHandle.Container, +) : Container, Closeable { + private val listeners = HashMap, LinkedHashSet<(MenuEvent) -> Unit>>() + val handle: Handle = HandleImpl(managerHandle) + + fun addListener(e: Class, l: (T) -> Unit) { + @Suppress("UNCHECKED_CAST") + listeners.computeIfAbsent(e) { LinkedHashSet() }.add(l as (MenuEvent) -> Unit) + } + + fun removeListener(e: Class, l: (T) -> Unit) { + listeners[e]?.remove(l) + } + + internal fun dispatchEvent(event: MenuEvent) { + listeners[event.javaClass]?.forEach { it(event) } + } + + sealed interface Handle { + fun addMenu(menu: (MenuManager.Handle, AsdHandle) -> Menu) + + fun removeMenu(menu: Menu) + } + + private inner class HandleImpl(private val managerHandle: MenuManager.Handle) : Handle { + override fun addMenu(menu: (MenuManager.Handle, AsdHandle) -> Menu) = managerHandle.addMenu(menu) + + override fun removeMenu(menu: Menu) = managerHandle.removeMenu(menu) + } + + protected inner class ComponentAsdHandleImpl : AsdHandle.Container() { + override lateinit var rect: RectangleD + override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdHandle.registerAsdProcessor(processor) + } + + internal fun render(renderSystem: RenderSystem) { + layout.render(renderSystem) + } + + internal fun update(muiIoI: ScreenManager.MuiIoI) { + dispatchEvent(MenuEvent.Update(muiIoI)) + layout.update() + layout.components.forEach { it.update(muiIoI) } + } + + /** + * Cleans up and closes any used resources in this session. + */ + final override fun close() { + dispatchEvent(MenuEvent.Close) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/MenuManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/MenuManager.kt new file mode 100644 index 00000000..c537aa76 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/MenuManager.kt @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.asd.AsdProcessor +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleF +import net.terramodulus.mui.gui.gfx.RenderSystem +import java.util.ArrayDeque + +class MenuManager internal constructor(private val asdHandle: (AsdProcessor<*>) -> Unit) { + private val menus = LinkedHashSet() + private val menuQueue = ArrayDeque() + val handle: Handle = HandleImpl() + + private sealed interface MenuOperation { + fun apply(menus: LinkedHashSet) + + class Add(val menu: () -> Menu) : MenuOperation { + override fun apply(menus: LinkedHashSet) { + menus.add(menu()) + } + } + + class Remove(val menu: Menu) : MenuOperation { + override fun apply(menus: LinkedHashSet) { + menus.remove(menu) + } + } + } + + sealed interface Handle { + fun addMenu(menu: (Handle, AsdHandle) -> Menu) + + fun removeMenu(menu: Menu) + } + + private inner class HandleImpl : Handle { + override fun addMenu(menu: (Handle, AsdHandle) -> Menu) { + menuQueue.add(MenuOperation.Add { menu(handle, MenuAsdHandleImpl()) }) + } + + override fun removeMenu(menu: Menu) { + menuQueue.add(MenuOperation.Remove(menu)) + } + } + + private inner class MenuAsdHandleImpl : AsdHandle.Menu() { + override lateinit var rect: RectangleD + override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdHandle(processor) + } + + internal fun update(muiIoI: ScreenManager.MuiIoI) { + menuQueue.forEach { it.apply(menus) } + menuQueue.clear() + } + + internal fun render(renderSystem: RenderSystem, screenManager: ScreenManager) { + menus.forEach { it.render(renderSystem) } + } + + internal fun visit() = AgimoTreeVisitor.MenuTreeVisitor { + menus.asSequence() + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Screen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Screen.kt new file mode 100644 index 00000000..363b541c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Screen.kt @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.mui.gui.agim.event.ScreenEvent +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.asd.AsdProcessor +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleF +import net.terramodulus.mui.gui.gfx.RenderSystem +import java.io.Closeable + +abstract class Screen( + managerHandle: ScreenManager.Handle, + final override val asdHandle: AsdHandle.Container +) : Container, Closeable { + private val listeners = HashMap, LinkedHashSet<(ScreenEvent) -> Unit>>() + private val menuManager = MenuManager(asdHandle::registerAsdProcessor) + val handle: Handle = HandleImpl(managerHandle) + +// init { +// // TODO there should be an entry for background, maybe it sets background for entire render background? +// asdHandle.registerAsdProcessor() +// } + + fun addListener(e: Class, l: (T) -> Unit) { + @Suppress("UNCHECKED_CAST") + listeners.computeIfAbsent(e) { LinkedHashSet() }.add(l as (ScreenEvent) -> Unit) + } + + fun removeListener(e: Class, l: (T) -> Unit) { + listeners[e]?.remove(l) + } + + internal fun dispatchEvent(event: ScreenEvent) { + listeners[event.javaClass]?.forEach { it(event) } + } + + sealed interface Handle { + fun addMenu(menu: (MenuManager.Handle, AsdHandle) -> Menu) + + fun removeMenu(menu: Menu) + + fun addTopMenu(menu: (MenuManager.Handle, AsdHandle) -> Menu) + + fun removeTopMenu(menu: Menu) + } + + private inner class HandleImpl(private val managerHandle: ScreenManager.Handle) : Handle { + override fun addMenu(menu: (MenuManager.Handle, AsdHandle) -> Menu) = menuManager.handle.addMenu(menu) + + override fun removeMenu(menu: Menu) = menuManager.handle.removeMenu(menu) + + override fun addTopMenu(menu: (MenuManager.Handle, AsdHandle) -> Menu) = managerHandle.addMenu(menu) + + override fun removeTopMenu(menu: Menu) = managerHandle.removeMenu(menu) + } + + protected inner class ComponentAsdHandleImpl : AsdHandle.Container() { + override lateinit var rect: RectangleD + override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdHandle.registerAsdProcessor(processor) + } + + internal fun update(muiIoI: ScreenManager.MuiIoI) { + dispatchEvent(ScreenEvent.Update(muiIoI)) + layout.update() + layout.components.forEach { it.update(muiIoI) } + } + + internal fun render(renderSystem: RenderSystem, screenManager: ScreenManager) { + layout.render(renderSystem) + menuManager.render(renderSystem, screenManager) + } + + /** + * Cleans up and closes any used resources in this session. + */ + final override fun close() { + dispatchEvent(ScreenEvent.Close) + } + + internal fun visit() = menuManager.visit() +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt new file mode 100644 index 00000000..cef87b73 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt @@ -0,0 +1,259 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +import net.terramodulus.engine.Window +import net.terramodulus.mui.MuiManager +import net.terramodulus.mui.gui.agim.impl.BoundsProperty +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.agim.impl.LaunchingScreen +import net.terramodulus.mui.gui.agim.impl.RectangleProperty +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.asd.AsdManager +import net.terramodulus.mui.gui.asd.AsdProcessor +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleF +import net.terramodulus.mui.kui.InputSystem + +class ScreenManager internal constructor( + window: Window, + private val renderSystemHandle: RenderSystem.Handle, + private val asdManagerHandle: AsdManager.AgimHandle, +) { + /** + * FILO screen stack; the top-most screen instance is in the last. + */ + private val screens = ArrayDeque() + private val screenOpQueue = ArrayDeque() + private val menuManager = MenuManager(asdManagerHandle::registerProcessor) + + private val asdHandles = HashSet() + private var viewportRect = RectangleD(0.0, 0.0, window.width.toDouble(), window.height.toDouble()) + + init { + window.addListener { w, h -> + viewportRect = RectangleD(0.0, 0.0, w.toDouble(), h.toDouble()) + asdHandles.forEach { it.triggerRectObservers() } // already referring viewportRect + } + } + + // TODO Basically need to hide those rectangles in LayoutManager instead +// inner class LayoutHandle.Screen : ManagedRect(), Closeable { +// override var value: RectangleF by Delegates.observable(viewportRect.value) { _, _, newValue -> +// observers.forEach { it(newValue) } +// } +// private set +// +// } + + val handle: Handle = HandleImpl() + + init { + screens.add(LaunchingScreen(handle, ScreenAsdHandleImpl(), renderSystemHandle)) + } + + private sealed interface ScreenOperation { + fun apply(handle: RenderSystem.Handle, screens: ArrayDeque, asdHandles: HashSet) + + /** + * Exits `n` times + * + * @throws IllegalArgumentException when `n` < 1 + * @throws IllegalStateException when `n` >= [screens] size during operation + */ + class Exit(val n: Int) : ScreenOperation { + init { + require(n > 0) { "`n` < 1" } + } + + override fun apply( + handle: RenderSystem.Handle, + screens: ArrayDeque, + asdHandles: HashSet + ) { + if (n >= screens.size) { + throw IllegalStateException("`n` >= screens.size") + } + + for (i in 1..n) { + screens.removeLast().apply { + close() + asdHandles.remove(asdHandle) + } + } + } + } + + /** + * Opens the `screen` + */ + class Open(val screen: (RenderSystem.Handle) -> Screen) : ScreenOperation { + override fun apply( + handle: RenderSystem.Handle, + screens: ArrayDeque, + asdHandles: HashSet + ) { + screens.addLast(screen(handle)) + } + } + + /** + * Opens the `screen` before the `target` screen + */ + class OpenBefore(val target: Screen, val screen: (RenderSystem.Handle) -> Screen) : ScreenOperation { + override fun apply( + handle: RenderSystem.Handle, + screens: ArrayDeque, + asdHandles: HashSet + ) { + screens.add(screens.lastIndexOf(target), screen(handle)) + } + } + + /** + * Exits until reaching the `screen` then remains on the `screen` + */ + class ExitTo(val screen: Screen) : ScreenOperation { + override fun apply( + handle: RenderSystem.Handle, + screens: ArrayDeque, + asdHandles: HashSet + ) { + val it = screens.asReversed().listIterator() + while (it.hasNext()) { + val e = it.next() + if (e == screen) { + break + } else { + it.remove() + e.close() + asdHandles.remove(e.asdHandle) + } + } + } + } + + /** + * Clears [screens] then opens the `screen` + */ + class Reset(val screen: (RenderSystem.Handle) -> Screen) : ScreenOperation { + override fun apply( + handle: RenderSystem.Handle, + screens: ArrayDeque, + asdHandles: HashSet + ) { + screens.asReversed().forEach { it.close() } + screens.clear() + asdHandles.clear() + screens.add(screen(handle)) + } + } + } + + sealed interface Handle { + /** + * @see ScreenOperation.Exit + */ + fun exit(n: Int) + + /** + * @see ScreenOperation.Open + */ + fun open(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) + + /** + * It is not recommended to use this in general scenarios. + * @see ScreenOperation.OpenBefore + */ + fun openBefore(target: Screen, screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) + + /** + * @see ScreenOperation.ExitTo + */ + fun exitTo(screen: Screen) + + /** + * @see ScreenOperation.Reset + */ + fun reset(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) + + fun addMenu(menu: (MenuManager.Handle, AsdHandle) -> Menu) + + fun removeMenu(menu: Menu) + } + + private inner class HandleImpl : Handle { + override fun exit(n: Int) { + screenOpQueue.add(ScreenOperation.Exit(n)) + } + + override fun open(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) { + screenOpQueue.add(ScreenOperation.Open { screen(handle, ScreenAsdHandleImpl(), it) }) + } + + override fun openBefore(target: Screen, screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) { + screenOpQueue.add(ScreenOperation.OpenBefore(target) { screen(handle, ScreenAsdHandleImpl(), it) }) + } + + override fun exitTo(screen: Screen) { + screenOpQueue.add(ScreenOperation.ExitTo(screen)) + } + + override fun reset(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) { + screenOpQueue.add(ScreenOperation.Reset { screen(handle, ScreenAsdHandleImpl(), it) }) + } + + override fun addMenu(menu: (MenuManager.Handle, AsdHandle) -> Menu) = menuManager.handle.addMenu(menu) + + override fun removeMenu(menu: Menu) = menuManager.handle.removeMenu(menu) + } + + private inner class ScreenAsdHandleImpl : AsdHandle.Screen() { + init { + asdHandles.add(this) + properties.putProperty(BoundsProperty.KEY, BoundsProperty(viewportRect)) + properties.putProperty(RectangleProperty.KEY, RectangleProperty(viewportRect)) + observeRect { + properties.putProperty(BoundsProperty.KEY, BoundsProperty(viewportRect)) + properties.putProperty(RectangleProperty.KEY, RectangleProperty(viewportRect)) + } + } + + override var rect get() = viewportRect + set(value) = throw UnsupportedOperationException() // should never be invoked + + override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdManagerHandle.registerProcessor(processor) + } + + /** + * MUI Interoperability Interface + */ + class MuiIoI internal constructor( + val renderSystem: RenderSystem, + val screenManager: ScreenManager, + val inputSystem: InputSystem, + ) + + internal fun update(muiManager: MuiManager) { + repeat(screenOpQueue.size) { + screenOpQueue.removeFirst().apply(renderSystemHandle, screens, asdHandles) + } + val ioi = MuiIoI(muiManager.guiManager.renderSystem, this, muiManager.kuiManager.inputSystem) + menuManager.update(ioi) + screens.forEach { it.update(ioi) } + } + + internal fun render(renderSystem: RenderSystem) { + menuManager.render(renderSystem, this) + screens.forEach { it.render(renderSystem, this) } + } + + internal fun visitScreens() = AgimoTreeVisitor.ScreenTreeVisitor { + screens.asSequence() + } + + internal fun visitMenus() = menuManager.visit() +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ComponentEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ComponentEvent.kt new file mode 100644 index 00000000..bd5ddd73 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ComponentEvent.kt @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.event + +import net.terramodulus.mui.gui.agim.ScreenManager + +sealed interface ComponentEvent { + data class Update(val muiIoI: ScreenManager.MuiIoI) : ComponentEvent + data class Key(val generic: GenericEvent.Key) : ComponentEvent +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvent.kt new file mode 100644 index 00000000..7316c3ee --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvent.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.event + +sealed class GenericEvent { + var bubble = true + + data object Key : GenericEvent() +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/MenuEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/MenuEvent.kt new file mode 100644 index 00000000..0e22e339 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/MenuEvent.kt @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.event + +import net.terramodulus.mui.gui.agim.ScreenManager + +sealed interface MenuEvent { + data class Update(val muiIoI: ScreenManager.MuiIoI) : MenuEvent + data object Close : MenuEvent +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ScreenEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ScreenEvent.kt new file mode 100644 index 00000000..bf79e7cd --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ScreenEvent.kt @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.event + +import net.terramodulus.mui.gui.agim.ScreenManager + +sealed interface ScreenEvent { + data class Update(val muiIoI: ScreenManager.MuiIoI) : ScreenEvent + data object Close : ScreenEvent +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BlankComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BlankComponent.kt new file mode 100644 index 00000000..76ad6f7c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BlankComponent.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Dimension2F + +/** + * This can act as a placeholder [Component] in a [Layout][net.terramodulus.mui.gui.agim.Layout]. + */ +class BlankComponent(asdHandle: AsdHandle) : Component(asdHandle) { + override fun render(renderSystem: RenderSystem) {} +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BoundsProperty.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BoundsProperty.kt new file mode 100644 index 00000000..716d31ec --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BoundsProperty.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.AgimoProperty +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.mui.gui.gfx.RectangleD + +data class BoundsProperty(val value: RectangleD) : AgimoProperty() { + companion object { + val KEY = AgimoPropertyMap.Key(BoundsProperty::class.java) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CompositeLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CompositeLayout.kt new file mode 100644 index 00000000..105f5e12 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CompositeLayout.kt @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.Container +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.agim.LayoutComputationGroup +import net.terramodulus.mui.gui.agim.LayoutHandle +import net.terramodulus.mui.gui.asd.AsdHandle + +class CompositeLayout(container: Container) : Layout(container) { + private val layouts = ArrayDeque() + + override val components = layouts.asSequence().flatMap { it.components } + + fun update(operation: ArrayDeque.() -> Unit) { + operate { + operation(layouts) + layouts.forEach { it.update() } + } + } + + override fun layOut(handle: LayoutHandle) = layouts.flatMap { it.layOutInternal(handle) }.asSequence() +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/FlexibleBoxLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/FlexibleBoxLayout.kt new file mode 100644 index 00000000..1758d165 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/FlexibleBoxLayout.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.Container +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.agim.LayoutComputationGroup +import net.terramodulus.mui.gui.agim.LayoutHandle +import net.terramodulus.mui.gui.asd.AsdHandle + +class FlexibleBoxLayout(container: Container) : Layout(container) { + override val components = TODO("Not yet implemented") + + override fun layOut(handle: LayoutHandle): Sequence { + TODO("Not yet implemented") + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GameplayScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt similarity index 72% rename from src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GameplayScreen.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt index e704c474..6542716f 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GameplayScreen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt @@ -3,39 +3,49 @@ * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gms.impl +package net.terramodulus.mui.gui.agim.impl +import com.cout970.math.quaternion.ImmQuatd +import com.cout970.math.vec3.ImmVec3d +import com.cout970.math.vec3.Vec3d +import com.cout970.math.vec3.Vec3f +import com.cout970.math.vec3.div +import com.cout970.math.vec3.dot +import com.cout970.math.vec3.normalized +import com.cout970.math.vec3.plus +import com.cout970.math.vec3.times +import com.cout970.math.vec3.toImmVec3f +import com.cout970.math.vec4.ImmVec4i import net.terramodulus.core.TerraModulus import net.terramodulus.core.getResourceAsString import net.terramodulus.engine.Camera3D import net.terramodulus.engine.PhyBody import net.terramodulus.engine.PhyGeom -import net.terramodulus.engine.Quat -import net.terramodulus.engine.Rgba import net.terramodulus.engine.SimpleMesh3dGeomCube import net.terramodulus.engine.SimpleMesh3dGeomSphere -import net.terramodulus.engine.Vec3D -import net.terramodulus.engine.Vec3F import net.terramodulus.engine.WorldObjDrawable -import net.terramodulus.mui.gfx.Direction6C -import net.terramodulus.mui.gfx.RenderSystem -import net.terramodulus.mui.gfx.Vector3D -import net.terramodulus.mui.gms.Component -import net.terramodulus.mui.gms.Screen -import net.terramodulus.mui.gms.ScreenManager -import net.terramodulus.mui.input.InputSystem +import net.terramodulus.engine.common.ZeroImmVec3d +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.agim.Screen +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.agim.event.ScreenEvent +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.asd.AsdProcessor +import net.terramodulus.mui.gui.gfx.Direction6C +import net.terramodulus.mui.gui.gfx.RectangleF +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.kui.InputSystem import net.terramodulus.util.logging.logger import net.terramodulus.void.World import kotlin.math.PI -import kotlin.math.sqrt import kotlin.random.Random -private val WHITE = Rgba(255, 255, 255, 255) -private val RED = Rgba(255, 0, 0, 255) -private val GREEN = Rgba(0, 255, 0, 255) -private val BLUE = Rgba(0, 0, 255, 255) -private val STD_SCALE = Vec3D(.5, .5, .5) -private val IDENT_ROT = Quat(1.0, .0, .0, .0) +private val WHITE = ImmVec4i(255, 255, 255, 255) +private val RED = ImmVec4i(255, 0, 0, 255) +private val GREEN = ImmVec4i(0, 255, 0, 255) +private val BLUE = ImmVec4i(0, 0, 255, 255) +private val STD_SCALE = ImmVec3d(.5, .5, .5) +private val IDENT_ROT = ImmQuatd(1.0, .0, .0, .0) private const val MASS = 1.0 private const val MAX_SPEED = PI * PI // reachable by autonomous movement private const val MAX_ACC = PI * PI // without other forces, reaching MAX_SPEED in one second @@ -49,18 +59,37 @@ private const val MAX_ZOOM = 4 private val logger = logger {} -internal class GameplayScreen(private val core: TerraModulus, private val camera: Camera3D, renderSystemHandle: RenderSystem.Handle) : Screen() { +internal class GameplayScreen( + private val core: TerraModulus, + private val camera: Camera3D, + renderSystemHandle: RenderSystem.Handle, + managerHandle: ScreenManager.Handle, + asdHandle: AsdHandle.Container, +) : Screen(managerHandle, asdHandle) { private val geoShaders = camera.loadGeoShaders( getResourceAsString("/gwr_geo.vsh"), getResourceAsString("/gwr_geo.fsh"), ) + private val canvasHandle = renderSystemHandle.canvasHandle + private lateinit var player: PlayerVoidGeom + override val layout = + SingletonLayout(this, BlankComponent(ComponentAsdHandleImpl()), SingletonLayout.Config.Absolute.Full) init { renderSystemHandle.setBackgroundColor(0F, 0F, 0F, 0F) - core.world = World(Ymir()) - addComponent(GameplayRenderer()) + managerHandle.open { p1: ScreenManager.Handle, p2: AsdHandle.Container, p3: RenderSystem.Handle -> + WorldInitScreen(p1, p2, p3).apply { + core.world = World(Ymir(), progressBar) + addListener(ScreenEvent.Close::class.java) { + this@GameplayScreen.layout.update(GameplayRenderer()) + this@GameplayScreen.addListener(ScreenEvent.Update::class.java) { + update0(it.muiIoI) + } + } + } + } // val progressBarEdge = GeomComponent(GuiLine(0, 100, 100, 100, 255, 255, 255, 255)) // addComponent(progressBarEdge) // val progressBarCtnVal = GuiLine(0, 101, 0, 101, 255, 255, 0, 255) @@ -82,15 +111,12 @@ internal class GameplayScreen(private val core: TerraModulus, private val camera } private inner class Ymir : World.Ymir { + private val cubeGeom = SimpleMesh3dGeomCube(canvasHandle.canvas, 2F) + private val sphereGeom = SimpleMesh3dGeomSphere(canvasHandle.canvas, 1F) + override fun wrapCube(phyGeom: PhyGeom, x: Double, y: Double, z: Double): VoidGeom = EnvVoidGeom(phyGeom, - SimpleMesh3dGeomCube( - 2F, - randomColor(), - Vec3D(x, y, z), - STD_SCALE, - IDENT_ROT, - ), - Vec3D(x, y, z) + WorldObjDrawable(cubeGeom, randomColor(), ImmVec3d(x, y, z), STD_SCALE, IDENT_ROT,), + ImmVec3d(x, y, z), ) private fun randomColor() = when (Random.nextInt(3)) { @@ -102,7 +128,7 @@ internal class GameplayScreen(private val core: TerraModulus, private val camera override fun wrapChar(phyBody: PhyBody): VoidGeom { player = PlayerVoidGeom(phyBody, - SimpleMesh3dGeomSphere(1F, WHITE, Vec3D(0.0, 1.0, 0.0), STD_SCALE, IDENT_ROT) + WorldObjDrawable(sphereGeom, WHITE, ImmVec3d(0.0, 1.0, 0.0), STD_SCALE, IDENT_ROT) ) return player } @@ -114,21 +140,21 @@ internal class GameplayScreen(private val core: TerraModulus, private val camera } } - private inner class EnvVoidGeom(override val phyGeom: PhyGeom, drawable: WorldObjDrawable, override val pos: Vec3D) : + private inner class EnvVoidGeom(override val phyGeom: PhyGeom, drawable: WorldObjDrawable, override val pos: Vec3d) : VoidGeom(drawable), World.EnvVoidGeom private inner class PlayerVoidGeom(override val phyBody: PhyBody, drawable: WorldObjDrawable) : VoidGeom(drawable), World.PlayerVoidGeom { - fun move(dir: Vector3D) { - if (dir == Vector3D.ZERO) return // avoid math errors and computations - val dir = Vec3D(dir.x, dir.y, dir.z).normalize() + fun move(dir: Vec3d) { + if (dir == ZeroImmVec3d) return // avoid math errors and computations + val dir = ImmVec3d(dir.x, dir.y, dir.z).normalized() val curVel = phyBody.linearVel // Let d be the unit vector of autonomous movement target direction, // v_c be the current velocity of body, // v_p be the scalar projection of v_c on d. // v_p = v_c * d, may be negative // Autonomous acceleration is made only if v_p < MAX_SPEED. - val projVel = curVel * dir + val projVel = curVel dot dir if (projVel < MAX_SPEED) { // Let v_d be the delta velocity in direction of d, // a_d be the delta acceleration to be made. @@ -142,30 +168,14 @@ internal class GameplayScreen(private val core: TerraModulus, private val camera override fun render() { drawable.setPos(phyBody.pos) - camera.refreshPos(phyBody.pos.toVec3F().toArray()) + camera.refreshPos(phyBody.pos.toImmVec3f().toArray()) super.render() } - override var pos: Vec3D by phyBody::pos + override var pos: Vec3d by phyBody::pos } - private fun Vec3D.normalize(): Vec3D { - val mag = mag() - return Vec3D(x / mag, y / mag, z / mag) - } - - private operator fun Vec3D.times(d: Double) = Vec3D(x * d, y * d, z * d) - private operator fun Vec3D.div(d: Double) = Vec3D(x / d, y / d, z / d) - private operator fun Vec3D.minus(other: Vec3D) = Vec3D(x - other.x, y - other.y, z - other.z) - // dot product - private operator fun Vec3D.times(other: Vec3D) = x * other.x + y * other.y + z * other.z - - // dot product with itself - private fun Vec3D.squared() = x * x + y * y + z * z - // magnitude or length - private fun Vec3D.mag() = sqrt(squared()) - - private fun Vec3D.toVec3F() = Vec3F(x.toFloat(), y.toFloat(), z.toFloat()) + private fun Vec3f.toArray() = floatArrayOf(x, y, z) private fun Direction6C.toKey() = when (this) { Direction6C.North -> InputSystem.Keys.W @@ -177,17 +187,18 @@ internal class GameplayScreen(private val core: TerraModulus, private val camera } private fun Direction6C.toVector() = when (this) { - Direction6C.North -> Vector3D(.0, .0, -1.0) - Direction6C.South -> Vector3D(.0, .0, 1.0) - Direction6C.West -> Vector3D(-1.0, .0, .0) - Direction6C.East -> Vector3D(1.0, .0, .0) - Direction6C.Up -> Vector3D(.0, 1.0, .0) - Direction6C.Down -> Vector3D(.0, -1.0, .0) + Direction6C.North -> ImmVec3d(.0, .0, -1.0) + Direction6C.South -> ImmVec3d(.0, .0, 1.0) + Direction6C.West -> ImmVec3d(-1.0, .0, .0) + Direction6C.East -> ImmVec3d(1.0, .0, .0) + Direction6C.Up -> ImmVec3d(.0, 1.0, .0) + Direction6C.Down -> ImmVec3d(.0, -1.0, .0) } - private fun Vec3D.display() = "[$x, $y, $z]" + private fun Vec3d.display() = "[$x, $y, $z]" - override fun update(renderSystem: RenderSystem, screenManager: ScreenManager, inputSystem: InputSystem) { + private fun update0(muiIoI: ScreenManager.MuiIoI) { + val inputSystem = muiIoI.inputSystem // Those keys are not related to GUI, so they are fine to be here. if (inputSystem.condition { Q.justDown() }) { // Query position of sphere @@ -292,7 +303,7 @@ internal class GameplayScreen(private val core: TerraModulus, private val camera } if (inputSystem.condition { N.justDown() }) { // Reset velocity of sphere to zero - player.phyBody.linearVel = Vec3D.ZERO + player.phyBody.linearVel = ZeroImmVec3d logger.info { "Reset velocity to zero" } } // This is problematic and difficult to be resolved. @@ -320,12 +331,12 @@ internal class GameplayScreen(private val core: TerraModulus, private val camera } } - val dirs = ArrayList() + val dirs = ArrayList() Direction6C.entries.forEach { if (inputSystem.condition { it.toKey().down() }) dirs.add(it.toVector()) } - player.move(dirs.fold(Vector3D.ZERO, Vector3D::plus)) + player.move(dirs.fold(ZeroImmVec3d, Vec3d::plus)) } - private inner class GameplayRenderer : Component() { + private inner class GameplayRenderer : Component(ComponentAsdHandleImpl()) { override fun render(renderSystem: RenderSystem) { if (core.world != null) core.world!!.objects.values.sortedWith( compareBy { it.pos.y }.thenBy { it.pos.z } @@ -334,6 +345,4 @@ internal class GameplayScreen(private val core: TerraModulus, private val camera } internal fun renderGwrGeo(drawable: WorldObjDrawable) = camera.renderGwrGeo(drawable, geoShaders) - - override fun exit() {} } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GeomComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GeomComponent.kt new file mode 100644 index 00000000..01044fa5 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GeomComponent.kt @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.GeneralTransform +import net.terramodulus.mui.gui.gfx.GuiGeometry +import net.terramodulus.mui.gui.gfx.RectStParams +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleI +import net.terramodulus.mui.gui.gfx.RenderSystem + +class GeomComponent( + val geom: GuiGeometry, + private val bounds: RectangleD, + asdHandle: AsdHandle, +) : Component(asdHandle) { + private val transform = GeneralTransform().apply { geom.add(this) } + + init { + val dim = IntrinsicDimensionsProperty(bounds.width.toUInt(), bounds.height.toUInt()) + asdHandle.properties.putProperty(IntrinsicDimensionsProperty.KEY, dim) + asdHandle.properties.putProperty(IntrinsicRatioProperty.KEY, dim.computeRatio()) + asdHandle.observeRect { + RectStParams.fromRects(bounds, asdHandle.rect).applyToGeneralTransform(transform) + } + } + + override fun render(renderSystem: RenderSystem) { + geom.render(renderSystem) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GraphicsComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GraphicsComponent.kt new file mode 100644 index 00000000..bd189193 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GraphicsComponent.kt @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.AbstractPane +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.ColorFilter +import net.terramodulus.mui.gui.gfx.GeneralTransform +import net.terramodulus.mui.gui.gfx.GuiGeometry +import net.terramodulus.mui.gui.gfx.GuiSprite +import net.terramodulus.mui.gui.gfx.ModelTransform +import net.terramodulus.mui.gui.gfx.RectStParams +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RenderSystem + +sealed interface GraphicsComponent + +class SpriteComponent(val sprite: GuiSprite, asdHandle: AsdHandle) : Component(asdHandle), GraphicsComponent { + private val transform = GeneralTransform().apply { sprite.add(this) } + + init { + val dim = IntrinsicDimensionsProperty(sprite.rect.width.toUInt(), sprite.rect.height.toUInt()) + asdHandle.properties.putProperty(IntrinsicDimensionsProperty.KEY, dim) + asdHandle.properties.putProperty(IntrinsicRatioProperty.KEY, dim.computeRatio()) + asdHandle.observeRect { + RectStParams.fromRects(sprite.rect.toDouble(), asdHandle.rect).applyToGeneralTransform(transform) + } + } + + override fun render(renderSystem: RenderSystem) { + sprite.render(renderSystem) + } +} + +class DrawablesComponent( + val drawables: Sequence, + private val bounds: RectangleD, + asdHandle: AsdHandle, +) : Component(asdHandle) { + private val transform = GeneralTransform().apply { addTransform(this) } + + fun addTransform(transform: ModelTransform) = drawables.forEach { drawable -> + when (drawable) { + is Drawable.Geom -> drawable.geom.add(transform) + is Drawable.Sprite -> drawable.sprite.add(transform) + } + } + + fun addFilter(filter: ColorFilter) = drawables.forEach { drawable -> + when (drawable) { + is Drawable.Geom -> drawable.geom.add(filter) + is Drawable.Sprite -> drawable.sprite.add(filter) + } + } + + sealed interface Drawable { + companion object { + operator fun invoke(geom: GuiGeometry) = Geom(geom) + operator fun invoke(sprite: GuiSprite) = Sprite(sprite) + } + + class Geom(val geom: GuiGeometry) : Drawable + class Sprite(val sprite: GuiSprite) : Drawable + } + + init { + val dim = IntrinsicDimensionsProperty(bounds.width.toUInt(), bounds.height.toUInt()) + asdHandle.properties.putProperty(IntrinsicDimensionsProperty.KEY, dim) + asdHandle.properties.putProperty(IntrinsicRatioProperty.KEY, dim.computeRatio()) + asdHandle.observeRect { + RectStParams.fromRects(bounds, asdHandle.rect).applyToGeneralTransform(transform) + } + } + + override fun render(renderSystem: RenderSystem) { + drawables.forEach { drawable -> + when (drawable) { + is Drawable.Geom -> drawable.geom.render(renderSystem) + is Drawable.Sprite -> drawable.sprite.render(renderSystem) + } + } + } +} + +@Suppress("CanSealedSubClassBeObject") +class CanvasComponent(override val layout: Layout, asdHandle: AsdHandle) : AbstractPane(asdHandle), GraphicsComponent { + override fun render(renderSystem: RenderSystem) { + TODO("Not yet implemented") + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/IntrinsicProperties.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/IntrinsicProperties.kt new file mode 100644 index 00000000..c7948845 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/IntrinsicProperties.kt @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.AgimoProperty +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.util.gcd + +/** + * @constructor Simple constructor without any preprocessing + */ +data class IntrinsicRatioProperty(val width: UInt, val height: UInt) : AgimoProperty() { + companion object { + val KEY = AgimoPropertyMap.Key(IntrinsicRatioProperty::class.java) + fun compute(width: UInt, height: UInt): IntrinsicRatioProperty { + val gcd = gcd(width, height) + return IntrinsicRatioProperty(width / gcd, height / gcd) + } + } +} + +class IntrinsicDimensionsProperty(val width: UInt, val height: UInt) : AgimoProperty() { + companion object { + val KEY = AgimoPropertyMap.Key(IntrinsicDimensionsProperty::class.java) + } + fun computeRatio() = IntrinsicRatioProperty.compute(width, height) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/LaunchingScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/LaunchingScreen.kt new file mode 100644 index 00000000..a0ea674b --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/LaunchingScreen.kt @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.Screen +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.agim.event.ScreenEvent +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.AlphaFilter +import net.terramodulus.mui.gui.gfx.GuiRect +import net.terramodulus.mui.gui.gfx.GuiSprite +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleI +import net.terramodulus.mui.gui.gfx.RenderSystem + +private val BG_COLOR = floatArrayOf(.145F, .776F, .768F) + +private const val ANI_DURATION = .75F // in second + +private const val PAUSE_DURATION = 2 // in second + +internal class LaunchingScreen( + managerHandle: ScreenManager.Handle, + asdHandle: AsdHandle.Container, + renderSystemHandle: RenderSystem.Handle, +) : Screen(managerHandle, asdHandle) { + private var stage = 0 + private var last = System.currentTimeMillis() // timestamp in milliseconds + private var alphaFilter = AlphaFilter(0F) + override val layout = CompositeLayout(this) + + init { + layout.update { + add(SingletonLayout(this@LaunchingScreen, GeomComponent(GuiRect( + renderSystemHandle.canvasHandle, 0, 0, 1, 1, 37, 198, 196, 255 + ), RectangleD(0.0, 0.0, 1.0, 1.0), ComponentAsdHandleImpl()).apply { + geom.add(alphaFilter) + }, SingletonLayout.Config.Absolute.Full)) + add(SingletonLayout(this@LaunchingScreen, SpriteComponent(GuiSprite( + renderSystemHandle.canvasHandle, + RectangleI(0, 0, 512, 128), + renderSystemHandle.loadTexture("/studio_logo.png"), + ), ComponentAsdHandleImpl()).apply { + sprite.add(alphaFilter) + }, SingletonLayout.Config.Aligned( + SingletonLayout.Config.ObjectFit.Contain, + SingletonLayout.Config.AlignmentConfig.DEFAULT, + ))) + } + + addListener(ScreenEvent.Update::class.java) { + val current = System.currentTimeMillis() + val elapsed = (current - last) / 1000F // elapsed time for this stage + when (stage) { + 0 -> if (elapsed >= ANI_DURATION) { + stage = 1 + last = current + alphaFilter.alpha = 1F + } else { + alphaFilter.alpha = elapsed / ANI_DURATION + } + + 1 -> if (elapsed >= PAUSE_DURATION) { + stage = 2 + last = current + } + + 2 -> if (elapsed >= ANI_DURATION) { + stage = 3 + last = current + alphaFilter.alpha = 0F + } else { + alphaFilter.alpha = 1 - elapsed / ANI_DURATION + } + + 3 -> it.muiIoI.screenManager.handle.reset(::ResourceLoadingScreen) + } + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/RectangleProperty.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/RectangleProperty.kt new file mode 100644 index 00000000..1e17b41c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/RectangleProperty.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.AgimoProperty +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.mui.gui.gfx.RectangleD + +data class RectangleProperty(val value: RectangleD) : AgimoProperty() { + companion object { + val KEY = AgimoPropertyMap.Key(RectangleProperty::class.java) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ResourceLoadingScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ResourceLoadingScreen.kt new file mode 100644 index 00000000..b5ee0888 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ResourceLoadingScreen.kt @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.engine.common.ZeroImmVec3f +import net.terramodulus.mui.gui.agim.Screen +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.agim.event.ScreenEvent +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.AlphaFilter +import net.terramodulus.mui.gui.gfx.Dimension2I +import net.terramodulus.mui.gui.gfx.GuiRect +import net.terramodulus.mui.gui.gfx.GuiSprite +import net.terramodulus.mui.gui.gfx.Rectangle +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleI +import net.terramodulus.mui.gui.gfx.RenderSystem +import kotlin.math.min +import kotlin.properties.Delegates + +private val BG_COLOR = floatArrayOf(.145F, .776F, 0.768F) + +private const val ANI_DURATION = 1F // in second +private const val PAUSE_DURATION = 2F // in second + +class ResourceLoadingScreen( + managerHandle: ScreenManager.Handle, + asdHandle: AsdHandle.Container, + renderSystemHandle: RenderSystem.Handle, +) : Screen(managerHandle, asdHandle) { + private var stage = 0 + private var last = System.currentTimeMillis() // timestamp in milliseconds + private var alphaFilter = AlphaFilter(0F) + private val progressBar = ProgressBar(renderSystemHandle) + override val layout = CompositeLayout(this) + + init { + layout.update { + add(SingletonLayout(this@ResourceLoadingScreen, GeomComponent(GuiRect( + renderSystemHandle.canvasHandle, 0, 0, 1, 1, 0, 255, 213, 255 + ), RectangleD(0.0, 0.0, 1.0, 1.0), ComponentAsdHandleImpl()).apply { + geom.add(alphaFilter) + }, SingletonLayout.Config.Absolute.Full)) + add(SingletonLayout(this@ResourceLoadingScreen, SimplePane(ComponentAsdHandleImpl()) { + SingletonLayout(this, DrawablesComponent(sequenceOf( + DrawablesComponent.Drawable( + GuiSprite( + renderSystemHandle.canvasHandle, + RectangleI(0, 100, 400, 100), + renderSystemHandle.loadTexture("/game_logo.png") + ) + ), + DrawablesComponent.Drawable( + GuiRect(renderSystemHandle.canvasHandle, 0, 0, 400, 40, 240, 240, 240, 255) + ), + DrawablesComponent.Drawable( + GuiRect(renderSystemHandle.canvasHandle, 5, 5, 395, 35, 0, 255, 213, 255) + ), + DrawablesComponent.Drawable(progressBar.rect), + ), RectangleD(0.0, 0.0, 400.0, 200.0), ComponentAsdHandleImpl()).apply { + addFilter(alphaFilter) + }, SingletonLayout.Config.Aligned( + SingletonLayout.Config.ObjectFit.Contain, + SingletonLayout.Config.AlignmentConfig.DEFAULT, + )) + }, SingletonLayout.Config.Aligned( + SingletonLayout.Config.Relative.Simple(0.5), + SingletonLayout.Config.AlignmentConfig.DEFAULT, + ))) + } + + addListener(ScreenEvent.Update::class.java) { + val current = System.currentTimeMillis() + val elapsed = (current - last) / 1000F // elapsed time in second at this stage + when (stage) { + 0 -> if (elapsed >= ANI_DURATION) { + stage = 1 + last = current + alphaFilter.alpha = 1F + } else { + alphaFilter.alpha = elapsed / ANI_DURATION + } + + 1 -> { + // TODO when there is something to load, stay at this stage until ready + progressBar.progress = min(elapsed / PAUSE_DURATION, 1F) + if (progressBar.progress >= 1F) { + stage = 2 + last = current + } + } + + 2 -> if (elapsed >= ANI_DURATION) { + stage = 3 + last = current + alphaFilter.alpha = 0F + } else { + alphaFilter.alpha = 1 - elapsed / ANI_DURATION + } + +// 3 -> screenManager.handle.openBefore(::TitleScreen, this) + 3 -> it.muiIoI.screenManager.handle.reset(it.muiIoI.renderSystem.newGameplayScreen(ZeroImmVec3f)) + } + } + } + + private class ProgressBar(renderSystemHandle: RenderSystem.Handle) { + val rectDim = Rectangle.withPoints(7, 7, 393, 33) + val length = rectDim.width + var progress: Float by Delegates.observable(0f) { _, _, _ -> + rect.setPos(7, 7, rectDim.x + (progress * length).toInt(), 33) + } + val rect = GuiRect(renderSystemHandle.canvasHandle, 7, 7, 7, 33, 240, 240, 240, 255) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScrollPane.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScrollPane.kt new file mode 100644 index 00000000..73587106 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScrollPane.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.AbstractPane +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.RenderSystem + +class ScrollPane(asdHandle: AsdHandle) : AbstractPane(asdHandle) { + override fun render(renderSystem: RenderSystem) { + TODO("Not yet implemented") + } + + override val layout: Layout + get() = TODO("Not yet implemented") +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SequenceLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SequenceLayout.kt new file mode 100644 index 00000000..6d726bad --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SequenceLayout.kt @@ -0,0 +1,204 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import com.cout970.math.vec2.MutVec2d +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.agim.Container +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.agim.LayoutComputationGroup +import net.terramodulus.mui.gui.agim.LayoutComputationUnit +import net.terramodulus.mui.gui.agim.LayoutHandle +import net.terramodulus.mui.gui.agim.getProperty +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Anchor5 +import net.terramodulus.mui.gui.gfx.Direction2 +import net.terramodulus.mui.gui.gfx.RectangleD +import kotlin.math.max + +/** + * Common implementation that is either [ColumnLayout] or [RowLayout]. + * + * This is an optimized special version of [FlexibleBoxLayout] without any expected + * multiple *sequences* of components in a single layout. + */ +sealed class SequenceLayout( + container: Container, + override val elements: ElementList, + protected var config: Config, +) : Layout.ElementGroup(container, elements) { + class Element { + companion object { + fun default() = Element() + } + } + + /** + * [padding] is the paddings from the four edges. + * [gap] is the gaps only in between elements. + */ + class Config(val direction: Direction2, val gap: Double = 0.0, val padding: Double = 0.0) + + interface ConfigEnv { + var config: Config + } + + fun update(operation: ConfigEnv.() -> Unit) { + operate { operation(object : ConfigEnv { + override var config: Config by this@SequenceLayout::config + }) } + } + + override fun add(component: Component) = elements.add(component, Element.default()) + + override fun addBefore(target: Component, component: Component) = + elements.addBefore(target, component, Element.default()) + + override fun addAfter(target: Component, component: Component) = + elements.addAfter(target, component, Element.default()) + + override fun replace(target: Component, component: Component) = + elements.replace(target, component, Element.default()) +} + +/** + * **Column** case of [SequenceLayout]. + */ +class ColumnLayout private constructor(container: Container, elements: ElementList, config: Config) : + SequenceLayout(container, elements, config) { + companion object { + fun withComponents(vararg components: Component, config: Config) = { it: Container -> + ColumnLayout(it, ElementList.withComponentsDefault(Element::default, *components), config) + } + + fun withComponents(components: Collection, config: Config) = { it: Container -> + ColumnLayout(it, ElementList.withComponentsDefault(Element::default, components), config) + } + + fun withElements(vararg elements: Pair, config: Config) = { it: Container -> + ColumnLayout(it, ElementList.withElements(*elements), config) + } + + fun withElements(elements: Map, config: Config) = { it: Container -> + ColumnLayout(it, ElementList.withElements(elements), config) + } + } + + override fun layOut(handle: LayoutHandle) = sequenceOf(LayoutComputationGroup({}, { + // If all elements are separated into respective Units, race conditions may occur. + setOf(LayoutComputationUnit({ + put(container.asdHandle, setOf(BoundsProperty.KEY)) + elements.forEach { put(it.first.asdHandle, setOf(IntrinsicDimensionsProperty.KEY)) } + }, { + elements.forEach { put(it.first.asdHandle, setOf(BoundsProperty.KEY)) } + put(container.asdHandle, setOf(RectangleProperty.KEY)) + }, { + val containerRect = getUnit(container.asdHandle).getProperty(BoundsProperty.KEY)!!.value + val anchor = MutVec2d(containerRect.x + config.padding, when (config.direction) { + Direction2.Positive -> containerRect.y + config.padding + Direction2.Negative -> containerRect.y + containerRect.height - config.padding + }) + var width = 0.0 + var height = 0.0 + val map = mutableMapOf() + elements.forEach { + val dim = getUnit(it.first.asdHandle).getProperty(IntrinsicDimensionsProperty.KEY)!! + width = max(width, dim.width.toDouble()) + height += dim.height.toDouble() + config.gap + } + height = max(height - config.gap, 0.0) + elements.forEach { + val dim = getUnit(it.first.asdHandle).getProperty(IntrinsicDimensionsProperty.KEY)!! + if (config.direction == Direction2.Negative) anchor.y -= dim.height.toDouble() + map[it.first.asdHandle] = AgimoPropertyMap().apply { + putProperty(BoundsProperty.KEY, BoundsProperty( + RectangleD(anchor.x, anchor.y, width, dim.height.toDouble()) + )) + } + when (config.direction) { + Direction2.Positive -> anchor.y += dim.height.toDouble() + config.gap + Direction2.Negative -> anchor.y -= config.gap + } + } + map[container.asdHandle] = AgimoPropertyMap().apply { + putProperty(RectangleProperty.KEY, RectangleProperty(RectangleD( + containerRect.x, containerRect.y, width + config.padding * 2, height + config.padding * 2, + ))) + } + map + })) + })) +} + +/** + * **Row** case of [SequenceLayout]. + */ +class RowLayout private constructor(container: Container, elements: ElementList, config: Config) : + SequenceLayout(container, elements, config) { + companion object { + fun withComponents(vararg components: Component, config: Config) = { it: Container -> + RowLayout(it, ElementList.withComponentsDefault(Element::default, *components), config) + } + + fun withComponents(components: Collection, config: Config) = { it: Container -> + RowLayout(it, ElementList.withComponentsDefault(Element::default, components), config) + } + + fun withElements(vararg elements: Pair, config: Config) = { it: Container -> + RowLayout(it, ElementList.withElements(*elements), config) + } + + fun withElements(elements: Map, config: Config) = { it: Container -> + RowLayout(it, ElementList.withElements(elements), config) + } + } + + override fun layOut(handle: LayoutHandle) = sequenceOf(LayoutComputationGroup({}, { + // If all elements are separated into respective Units, race conditions may occur. + setOf(LayoutComputationUnit({ + put(container.asdHandle, setOf(BoundsProperty.KEY)) + elements.forEach { put(it.first.asdHandle, setOf(IntrinsicDimensionsProperty.KEY)) } + }, { + elements.forEach { put(it.first.asdHandle, setOf(BoundsProperty.KEY)) } + put(container.asdHandle, setOf(RectangleProperty.KEY)) + }, { + val containerRect = getUnit(container.asdHandle).getProperty(BoundsProperty.KEY)!!.value + val anchor = MutVec2d(when (config.direction) { + Direction2.Positive -> containerRect.x + config.padding + Direction2.Negative -> containerRect.x + containerRect.width - config.padding + }, containerRect.y + config.gap) + var width = 0.0 + var height = 0.0 + val map = mutableMapOf() + elements.forEach { + val dim = getUnit(it.first.asdHandle).getProperty(IntrinsicDimensionsProperty.KEY)!! + width += dim.width.toDouble() + config.gap + height = max(height, dim.height.toDouble()) + } + width = max(width - config.gap, 0.0) + elements.forEach { + val dim = getUnit(it.first.asdHandle).getProperty(IntrinsicDimensionsProperty.KEY)!! + if (config.direction == Direction2.Negative) anchor.x -= dim.width.toDouble() + map[it.first.asdHandle] = AgimoPropertyMap().apply { + putProperty(BoundsProperty.KEY, BoundsProperty( + RectangleD(anchor.x, anchor.y, dim.width.toDouble(), height) + )) + } + when (config.direction) { + Direction2.Positive -> anchor.x += dim.width.toDouble() + config.gap + Direction2.Negative -> anchor.x -= config.gap + } + } + map[container.asdHandle] = AgimoPropertyMap().apply { + putProperty(RectangleProperty.KEY, RectangleProperty(RectangleD( + containerRect.x, containerRect.y, width + config.padding * 2, height + config.padding * 2, + ))) + } + map + })) + })) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SimplePane.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SimplePane.kt new file mode 100644 index 00000000..6dfa52e0 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SimplePane.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.AbstractPane +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.RenderSystem + +class SimplePane(asdHandle: AsdHandle, layout: SimplePane.() -> Layout) : AbstractPane(asdHandle) { + override var layout = layout(this) + override fun render(renderSystem: RenderSystem) { + layout.render(renderSystem) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SingletonLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SingletonLayout.kt new file mode 100644 index 00000000..d9fb3bfe --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SingletonLayout.kt @@ -0,0 +1,267 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import com.cout970.math.vec2.ImmVec2d +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.mui.gui.agim.AnchorAlignmentHelper +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.agim.Container +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.agim.LayoutComputationGroup +import net.terramodulus.mui.gui.agim.LayoutComputationUnit +import net.terramodulus.mui.gui.agim.LayoutHandle +import net.terramodulus.mui.gui.agim.getProperty +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Dimension2D +import net.terramodulus.mui.gui.gfx.InsetsD +import net.terramodulus.mui.gui.gfx.RectangleD +import kotlin.math.max +import kotlin.math.min + +class SingletonLayout(container: Container, component: Component, private var config: Config) : Layout(container) { + override val components = componentsSequence(::component) + var component = component + private set + + sealed class Config private constructor() { + abstract fun layOut(layout: SingletonLayout): Set + + sealed class Absolute private constructor() : Config() { + override fun layOut(layout: SingletonLayout): Set = + setOf(LayoutComputationUnit({ + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) + }, { + put(layout.component.asdHandle, setOf(BoundsProperty.KEY)) + }, { + mapOf(layout.component.asdHandle to AgimoPropertyMap().apply { + val prop = getUnit(layout.container.asdHandle) + putProperty(BoundsProperty.KEY, BoundsProperty(compute( + prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value) + )) + }) + })) + + abstract fun compute(container: RectangleD): RectangleD + + data object Full : Absolute() { + override fun compute(container: RectangleD) = container + } + + data class Insets(var insets: InsetsD) : Absolute() { + override fun compute(container: RectangleD) = container - insets + } + } + + data class Aligned(val config: Relative, val alignment: AlignmentConfig) : Config() { + private operator fun RectangleD.times(other: AlignmentConfig) = + ImmVec2d(width * other.x, height * other.y) + private operator fun Dimension2D.times(other: AlignmentConfig) = + ImmVec2d(width * other.x, height * other.y) + + override fun layOut(layout: SingletonLayout): Set = + setOf(LayoutComputationUnit(config.dependencies(layout), { + put(layout.component.asdHandle, setOf(BoundsProperty.KEY)) + }, { + mapOf(layout.component.asdHandle to AgimoPropertyMap().apply { + val prop = getUnit(layout.container.asdHandle) + val rect = prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value + val target = config.compute(layout, this@LayoutComputationUnit) + putProperty(BoundsProperty.KEY, BoundsProperty( + AnchorAlignmentHelper.Subject(rect.toDouble(), rect * alignment) + .alignTarget(AnchorAlignmentHelper.Target(target, target * alignment)) + )) + }) + })) + } + + sealed class Relative { + // Must include Container Bounds & Rectangle + abstract fun dependencies(layout: SingletonLayout): + MutableMap>>.() -> Unit + + abstract fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D + + data class Simple(val scale: Double) : Relative() { + override fun dependencies(layout: SingletonLayout): + MutableMap>>.() -> Unit = { + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) + } + + override fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D { + val prop = handle.getUnit(layout.container.asdHandle) + val rect = prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value + return Dimension2D(rect.width * scale, rect.height * scale) + } + } + + data class Both(val scaleX: Double, val scaleY: Double) : Relative() { + override fun dependencies(layout: SingletonLayout): + MutableMap>>.() -> Unit = { + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) + } + + override fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D { + val prop = handle.getUnit(layout.container.asdHandle) + val rect = prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value + return Dimension2D(rect.width * scaleX, rect.height * scaleY) + } + } + } + + sealed class ObjectFit private constructor() : Relative() { + override fun dependencies(layout: SingletonLayout): + MutableMap>>.() -> Unit = { + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) + put(layout.component.asdHandle, setOf(IntrinsicRatioProperty.KEY)) + } + + override fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D { + val prop = handle.getUnit(layout.container.asdHandle) + return compute( + prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value, + handle.getUnit(layout.component.asdHandle).getProperty(IntrinsicRatioProperty.KEY)!! + ) + } + + abstract fun compute(container: RectangleD, component: IntrinsicRatioProperty): Dimension2D + + data object Contain : ObjectFit() { + override fun compute(container: RectangleD, component: IntrinsicRatioProperty): Dimension2D { + val w = container.width / component.width.toDouble() + val h = container.height / component.height.toDouble() + val scale = min(w, h) + return Dimension2D( + component.width.toDouble() * scale, + component.height.toDouble() * scale, + ) + } + } + + data object Cover : ObjectFit() { + override fun compute(container: RectangleD, component: IntrinsicRatioProperty): Dimension2D { + val w = container.width / component.width.toDouble() + val h = container.height / component.height.toDouble() + val scale = max(w, h) + return Dimension2D( + component.width.toDouble() * scale, + component.height.toDouble() * scale, + ) + } + } + } + + sealed class Scaled private constructor() : Relative() { + override fun dependencies(layout: SingletonLayout): + MutableMap>>.() -> Unit = { + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) + put(layout.component.asdHandle, setOf(IntrinsicDimensionsProperty.KEY)) + } + + override fun compute(layout: SingletonLayout, handle: LayoutHandle) = compute( + handle.getUnit(layout.component.asdHandle).getProperty(IntrinsicDimensionsProperty.KEY)!! + ) + + abstract fun compute(dim: IntrinsicDimensionsProperty): Dimension2D + + /** + * Scale both dimensions by the same scaling + * @param scale `> 0` + */ + data class Scale(val scale: Double) : Scaled() { + override fun compute(dim: IntrinsicDimensionsProperty) = + Dimension2D(dim.width.toDouble() * scale, dim.height.toDouble() * scale) + } + + class Compute private constructor(private val x: Value, private val y: Value) : Scaled() { + private object MathEnvImpl : MathEnv + + constructor(x: MathEnv.() -> Value, y: MathEnv.() -> Value) : this(x(MathEnvImpl), y(MathEnvImpl)) + + sealed interface Value { + fun compute(dim: IntrinsicDimensionsProperty): Double + + operator fun plus(that: Value) = Operator.Plus(this, that) + operator fun minus(that: Value) = Operator.Minus(this, that) + operator fun times(that: Value) = Operator.Times(this, that) + operator fun div(that: Value) = Operator.Div(this, that) + } + + sealed class Operator private constructor() : Value { + data class Plus(val a: Value, val b: Value) : Operator() { + override fun compute(dim: IntrinsicDimensionsProperty) = a.compute(dim) + b.compute(dim) + } + data class Minus(val a: Value, val b: Value) : Operator() { + override fun compute(dim: IntrinsicDimensionsProperty) = a.compute(dim) - b.compute(dim) + } + data class Times(val a: Value, val b: Value) : Operator() { + override fun compute(dim: IntrinsicDimensionsProperty) = a.compute(dim) * b.compute(dim) + } + data class Div(val a: Value, val b: Value) : Operator() { + override fun compute(dim: IntrinsicDimensionsProperty) = a.compute(dim) / b.compute(dim) + } + } + + sealed class Param private constructor() : Value { + data class Num(val value: Double) : Param() { + override fun compute(dim: IntrinsicDimensionsProperty) = value + } + data object DimX : Param() { + override fun compute(dim: IntrinsicDimensionsProperty) = dim.width.toDouble() + } + data object DimY : Param() { + override fun compute(dim: IntrinsicDimensionsProperty) = dim.height.toDouble() + } + } + + sealed interface MathEnv { + val dimX: Param get() = Param.DimX + val dimY: Param get() = Param.DimY + fun num(value: Double) = Param.Num(value) + } + + override fun compute(dim: IntrinsicDimensionsProperty) = Dimension2D(x.compute(dim), y.compute(dim)) + } + } + + /** + * Range within `[0,1]` + */ + data class AlignmentConfig(val x: Double, val y: Double) { + companion object { + val DEFAULT = AlignmentConfig(0.5, 0.5) + fun withX(x: Double) = AlignmentConfig(x, 0.5) + fun withY(y: Double) = AlignmentConfig(y, 0.5) + } + } + } + + fun update(component: Component) { + operate { + this@SingletonLayout.component = component + } + } + + interface ConfigEnv { + var config: Config + } + + fun update(operation: ConfigEnv.() -> Unit) { + operate { + operation(object : ConfigEnv { + override var config: Config by this@SingletonLayout::config + }) + } + } + + override fun layOut(handle: LayoutHandle) = + sequenceOf(LayoutComputationGroup({}, { config.layOut(this@SingletonLayout) })) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SizedPane.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SizedPane.kt new file mode 100644 index 00000000..8c731449 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SizedPane.kt @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.AbstractPane +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.agim.LayoutComputationGroup +import net.terramodulus.mui.gui.agim.LayoutComputationUnit +import net.terramodulus.mui.gui.agim.LayoutHandle +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.RenderSystem + +/** + * Provide intrinsic information for this container and its [component]. + */ +class SizedPane(asdHandle: AsdHandle, component: Component, private var config: Config) : AbstractPane(asdHandle) { + private val _layout = SizedLayout(component) + override val layout: Layout get() = _layout + + private inner class SizedLayout(var component: Component) : Layout(this@SizedPane) { + override val components = componentsSequence(::component) + + override fun layOut(handle: LayoutHandle) = sequenceOf(LayoutComputationGroup({}, { + setOf(LayoutComputationUnit({}, { + put(component.asdHandle, setOf(IntrinsicRatioProperty.KEY, IntrinsicDimensionsProperty.KEY)) + put(container.asdHandle, setOf(IntrinsicRatioProperty.KEY, IntrinsicDimensionsProperty.KEY)) + }, { + val dim = IntrinsicDimensionsProperty(config.width, config.height) + val ratio = dim.computeRatio() + mapOf( + component.asdHandle to AgimoPropertyMap().apply { + putProperty(IntrinsicRatioProperty.KEY, ratio) + putProperty(IntrinsicDimensionsProperty.KEY, dim) + }, + container.asdHandle to AgimoPropertyMap().apply { + putProperty(IntrinsicRatioProperty.KEY, ratio) + putProperty(IntrinsicDimensionsProperty.KEY, dim) + } + ) + }), LayoutComputationUnit({ // Forwarding Rect to component + put(container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) + }, { + put(component.asdHandle, setOf(BoundsProperty.KEY)) + }, { + mapOf(component.asdHandle to AgimoPropertyMap().apply { + val prop = getUnit(container.asdHandle) + putProperty(BoundsProperty.KEY, BoundsProperty( + prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value) + ) + }) + })) + })) + } + + data class Config(val width: UInt, val height: UInt) + + fun update(component: Component) { + _layout.operate { + _layout.component = component + } + } + + interface ConfigEnv { + var config: Config + } + + fun update(operation: ConfigEnv.() -> Unit) { + _layout.operate { + operation(object : ConfigEnv { + override var config: Config by this@SizedPane::config + }) + } + } + + override fun render(renderSystem: RenderSystem) { + layout.render(renderSystem) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SliderComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SliderComponent.kt new file mode 100644 index 00000000..bc867d03 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SliderComponent.kt @@ -0,0 +1,97 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import com.cout970.math.vec4.Vec4i +import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.ColorFilter +import net.terramodulus.mui.gui.gfx.Direction4A +import net.terramodulus.mui.gui.gfx.Direction4AD +import net.terramodulus.mui.gui.gfx.GeneralTransform +import net.terramodulus.mui.gui.gfx.GuiRect +import net.terramodulus.mui.gui.gfx.RectStParams +import net.terramodulus.mui.gui.gfx.Rectangle +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleI +import net.terramodulus.mui.gui.gfx.RenderSystem +import kotlin.math.roundToInt +import kotlin.properties.Delegates + +class SliderComponent( + val dir: Direction4A, + canvasHandle: RenderSystem.CanvasHandle, + asdHandle: AsdHandle, + config: Config, +) : Component(asdHandle) { + // Though this could be made into a Pane, it would be better to optimize this into a simple Component + // to reduce complexities and avoid unnecessary layout computations. + companion object { + // Kind of like resolution, since parameters of Engine geometries only accept integers at the moment. + private val BOUNDS = RectangleI(0, 0, 10000, 10000) + } + + private val bgTransform = GeneralTransform() + private val fgTransform = GeneralTransform() + private val background = GuiRect(canvasHandle, + BOUNDS.x, BOUNDS.y, BOUNDS.width, BOUNDS.height, + config.bgColor.x, config.bgColor.y, config.bgColor.z, config.bgColor.w, + ).apply { add(bgTransform) } + private val foreground = GuiRect(canvasHandle, + BOUNDS.x, BOUNDS.y, BOUNDS.width, BOUNDS.height, + config.fgColor.x, config.fgColor.y, config.fgColor.z, config.bgColor.w, + ).apply { add(fgTransform) } + var fraction: Double by Delegates.observable(0.0) { _, _, value -> + try { + updateForeground(asdHandle.rect, value) + } catch (_: UninitializedPropertyAccessException) {} + } + + class Config(val bgColor: Vec4i, val fgColor: Vec4i) + + init { + asdHandle.observeRect { + RectStParams.fromRects(BOUNDS.toDouble(), asdHandle.rect).applyToGeneralTransform(bgTransform) + updateForeground(asdHandle.rect, fraction) + } + } + + fun addFilter(filter: ColorFilter) { + background.add(filter) + foreground.add(filter) + } + + private fun updateForeground(rect: RectangleD, fraction: Double) { + // Using division by fraction is a quick hack to apply scaling along the axis while using the same BOUNDS. + RectStParams.fromRects(when (dir) { + Direction4A.XPos -> // left to right + Rectangle.withDirection( + BOUNDS.x, BOUNDS.y, (BOUNDS.width / fraction).roundToInt(), BOUNDS.height, + Direction4AD.QuadOne, + ) + Direction4A.XNeg -> // right to left + Rectangle.withDirection( + BOUNDS.width, BOUNDS.y, (BOUNDS.width / fraction).roundToInt(), BOUNDS.height, + Direction4AD.QuadTwo, + ) + Direction4A.YPos -> // bottom to top + Rectangle.withDirection( + BOUNDS.x, BOUNDS.y, BOUNDS.width, (BOUNDS.height / fraction).roundToInt(), + Direction4AD.QuadOne, + ) + Direction4A.YNeg -> // top to bottom + Rectangle.withDirection( + BOUNDS.x, BOUNDS.height, BOUNDS.width, (BOUNDS.height / fraction).roundToInt(), + Direction4AD.QuadFour, + ) + }.toDouble(), rect).applyToGeneralTransform(fgTransform) + } + + override fun render(renderSystem: RenderSystem) { + background.render(renderSystem) + foreground.render(renderSystem) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TitleScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TitleScreen.kt new file mode 100644 index 00000000..a93109a9 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TitleScreen.kt @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.agim.Screen +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.asd.AsdHandle + +class TitleScreen( + managerHandle: ScreenManager.Handle, + asdHandle: AsdHandle.Container, + renderSystemHandle: RenderSystem.Handle, +) : Screen(managerHandle, asdHandle) { + override val layout = + SingletonLayout(this, BlankComponent(ComponentAsdHandleImpl()), SingletonLayout.Config.Absolute.Full) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/WorldInitScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/WorldInitScreen.kt new file mode 100644 index 00000000..a36c277c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/WorldInitScreen.kt @@ -0,0 +1,158 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.impl + +import com.cout970.math.vec4.ImmVec4i +import net.terramodulus.mui.gui.agim.Screen +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.agim.event.ScreenEvent +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.AlphaFilter +import net.terramodulus.mui.gui.gfx.Direction4A +import net.terramodulus.mui.gui.gfx.GuiRect +import net.terramodulus.mui.gui.gfx.GuiSprite +import net.terramodulus.mui.gui.gfx.InsetsD +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleI +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.void.World + +private const val ANI_DURATION = 1F // in second + +class WorldInitScreen internal constructor( + managerHandle: ScreenManager.Handle, + asdHandle: AsdHandle.Container, + renderSystemHandle: RenderSystem.Handle, +) : Screen(managerHandle, asdHandle) { + private var stage = 0 + private var last = System.currentTimeMillis() // timestamp in milliseconds + private var alphaFilter = AlphaFilter(0F) +// private val progressBar = ProgressBar(renderSystemHandle) + private val progressBarComponent = SliderComponent( + Direction4A.XPos, + renderSystemHandle.canvasHandle, + ComponentAsdHandleImpl(), + // Not sure why background has to be non-transparent for this to render correctly + SliderComponent.Config(ImmVec4i(56, 255, 252, 255), ImmVec4i(59, 12, 120, 255)), + ).apply { + addFilter(alphaFilter) + } + override val layout = CompositeLayout(this) + private val progressBarImpl = ProgressBarImpl() + internal val progressBar: World.ProgressBar = progressBarImpl + + init { + layout.update { + add(SingletonLayout(this@WorldInitScreen, GeomComponent(GuiRect( + renderSystemHandle.canvasHandle, 0, 0, 1, 1, 56, 255, 252, 255 + ), RectangleD(0.0, 0.0, 1.0, 1.0), ComponentAsdHandleImpl()).apply { + geom.add(alphaFilter) + }, SingletonLayout.Config.Absolute.Full)) + add(SingletonLayout(this@WorldInitScreen, SimplePane(ComponentAsdHandleImpl()) { + SingletonLayout(this, DrawablesComponent(sequenceOf( + DrawablesComponent.Drawable( + GuiSprite( + renderSystemHandle.canvasHandle, + RectangleI(0, 100, 400, 100), + renderSystemHandle.loadTexture("/game_logo.png") + ) + ), + DrawablesComponent.Drawable( + GuiRect(renderSystemHandle.canvasHandle, 0, 0, 400, 40, 59, 12, 120, 255) + ), + DrawablesComponent.Drawable( + GuiRect(renderSystemHandle.canvasHandle, 5, 5, 395, 35, 56, 255, 252, 255) + ), +// DrawablesComponent.Drawable(progressBar.rect), + ), RectangleD(0.0, 0.0, 400.0, 200.0), ComponentAsdHandleImpl()).apply { + addFilter(alphaFilter) + }, SingletonLayout.Config.Aligned( + SingletonLayout.Config.ObjectFit.Contain, + SingletonLayout.Config.AlignmentConfig.DEFAULT, + )) + }, SingletonLayout.Config.Aligned( + SingletonLayout.Config.Relative.Simple(0.5), + SingletonLayout.Config.AlignmentConfig.DEFAULT, + ))) + add(SingletonLayout(this@WorldInitScreen, SimplePane(ComponentAsdHandleImpl()) { + SingletonLayout(this, SizedPane(ComponentAsdHandleImpl(), SimplePane(ComponentAsdHandleImpl()) { + SingletonLayout(this, progressBarComponent, + SingletonLayout.Config.Absolute.Insets(InsetsD(7.0, 167.0, 7.0, 7.0)) + ) + }, + SizedPane.Config(400u, 200u)), SingletonLayout.Config.Aligned( + SingletonLayout.Config.ObjectFit.Contain, + SingletonLayout.Config.AlignmentConfig.DEFAULT, + ) + ) + }, SingletonLayout.Config.Aligned( + SingletonLayout.Config.Relative.Simple(0.5), + SingletonLayout.Config.AlignmentConfig.DEFAULT, + ))) + } + + addListener(ScreenEvent.Update::class.java) { + val current = System.currentTimeMillis() + val elapsed = (current - last) / 1000F // elapsed time in second at this stage + when (stage) { + 0 -> if (elapsed >= ANI_DURATION) { + stage = 1 + last = current + alphaFilter.alpha = 1F + progressBarImpl.ready = true + } else { + alphaFilter.alpha = elapsed / ANI_DURATION + } + + 1 -> { + if (progressBarComponent.fraction >= 1) { + progressBarComponent.fraction = 1.0 + stage = 2 + last = current + } + } + + 2 -> if (elapsed >= ANI_DURATION) { + stage = 3 + last = current + alphaFilter.alpha = 0F + } else { + alphaFilter.alpha = 1 - elapsed / ANI_DURATION + } + +// 3 -> screenManager.handle.openBefore(::TitleScreen, this) + 3 -> it.muiIoI.screenManager.handle.exit(1) + } + } + } + + private inner class ProgressBarImpl : World.ProgressBar { + var ready = false + set(value) { + field = value + readyListener?.invoke() + } + var readyListener: (() -> Unit)? = null + + override fun addReadyListener(listener: () -> Unit) { + readyListener = listener + if (ready) listener() + } + + override fun setProgress(progress: Double) { + progressBarComponent.fraction = progress + } + } + +// internal class ProgressBar(renderSystemHandle: RenderSystem.Handle) { +// val rectDim = Rectangle.withPoints(7, 7, 393, 33) +// val length = rectDim.width +// var progress: Float by Delegates.observable(0f) { _, _, _ -> +// rect.setPos(7, 7, rectDim.x + (progress * length).toInt(), 33) +// } +// val rect = GuiRect(renderSystemHandle.canvasHandle, 7, 7, 7, 33, 240, 240, 240, 255) +// } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/package-info.java b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/package-info.java new file mode 100644 index 00000000..5723fbd2 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/package-info.java @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +/** + *

Abstract Graphical Interface Model (AGIM) + *

+ * Defined in EFP 9 (or a dedicated EFP for this chapter). + */ +package net.terramodulus.mui.gui.agim; diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdHandle.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdHandle.kt new file mode 100644 index 00000000..660d2c0f --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdHandle.kt @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.asd + +import net.terramodulus.mui.gui.agim.AgimoProperty +import net.terramodulus.mui.gui.agim.AgimoPropertyMap +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RectangleF +import net.terramodulus.util.TypedAnchorMap +import java.util.function.BiFunction + +/** + * Since rectangles are modified only during layout processing, + * further follow-ups must not be deferred to next tick. + */ +abstract class AsdHandle internal constructor() { + /** + * Caveat: Must only be modified by [Layout][net.terramodulus.mui.gui.agim.Layout]. + * When modified, [triggerRectObservers] must be invoked. + */ + abstract var rect: RectangleD + internal set + + protected val rectObservers = LinkedHashSet<() -> Unit>() + + internal fun observeRect(observer: () -> Unit) { + rectObservers.add(observer) + } + + internal fun unobserveRect(observer: () -> Unit) { + rectObservers.remove(observer) + } + + internal fun triggerRectObservers() { + rectObservers.forEach { it() } + } + + /** + * Permanent AGIMO Properties + */ + val properties = AgimoPropertyMap() + + /** + * Registers ASD Processors from AGIMOs to [AsdManager]. + */ + abstract fun registerAsdProcessor(processor: AsdProcessor<*>) + + // TODO likely those below are useless + abstract class Container : AsdHandle() {} + + abstract class Menu : Container() {} + + abstract class Screen : Container() {} + +// interface Component : LayoutHandle {} // Do we need this? +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdIr.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdIr.kt new file mode 100644 index 00000000..355c613c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdIr.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.asd + +/** + * ASD Intermediate Representation (IR) + */ +class AsdIr { +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdManager.kt new file mode 100644 index 00000000..6d6e75af --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdManager.kt @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.asd + +import net.terramodulus.mui.gui.agim.AgimoTreeVisitor + +// TODO Should ASD affect choices of Layout? +// There are two proposals: +// - Each Container manages a Layout, then the Layout is put into LayoutManager/LayoutViewport +// - Each Container is totally managed by AsdManager except for Facets, but this rather complicated for ASD +internal class AsdManager internal constructor() { + companion object { + // TODO temporary demonstrative testing default + internal fun default(): AsdManager = AsdManager() + } + + private val processors = HashSet>() + + internal fun registerProcessor(processor: AsdProcessor<*>) { + processors.add(processor) + } + + internal inner class AgimHandle { + internal fun registerProcessor(processor: AsdProcessor<*>) = this@AsdManager.registerProcessor(processor) + } + + internal fun process() { + fun process(processor: AsdProcessor) = processor.processDefined(processor.produceDefined()) + processors.forEach { process(it) } + } + + // TODO What should be the use of this + private inner class TreeVisitor(visitorScreens: ScreenTreeVisitor, visitorMenus: MenuTreeVisitor) : AgimoTreeVisitor() { + init { + val root = RootNode(visitorScreens.visit(), visitorMenus.visit()) + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdProcessor.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdProcessor.kt new file mode 100644 index 00000000..4a50dc05 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdProcessor.kt @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.asd + +import net.terramodulus.engine.BaseAsdProcessor + +// TODO in reality, only binary data are consumed, but +// in testing environment, objects are created directly +// - most likely, each AGIMO and Layout register necessary Processor +// to consume input data and configure its managed instance of AGIMO/Layout +abstract class AsdProcessor { + companion object { + fun get(): BaseAsdProcessor = TODO("This processor's only use is to provide InputStream") + } + + // TODO Temporary implementation to configure ASD without binary data but produced configs + abstract fun processDefined(definitions: T) + + // TODO Temporary implementation to produce defined configurations for testing and demo + abstract fun produceDefined(): T +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/MenuStyles.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/MenuStyles.kt new file mode 100644 index 00000000..0022ce5d --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/MenuStyles.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.asd + +class MenuStyles { +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/ScreenStyles.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/ScreenStyles.kt new file mode 100644 index 00000000..76571c6e --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/ScreenStyles.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.asd + +class ScreenStyles { +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Anchor.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Anchor.kt similarity index 83% rename from src/kernel/client/kotlin/net/terramodulus/mui/gfx/Anchor.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Anchor.kt index b000efe7..b3880d64 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Anchor.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Anchor.kt @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gfx +package net.terramodulus.mui.gui.gfx /* * Every set of anchor position directions has different meanings in different context, diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ColorFilter.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ColorFilter.kt new file mode 100644 index 00000000..fa70486f --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ColorFilter.kt @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.gfx + +import net.terramodulus.engine.AlphaFilter +import net.terramodulus.engine.ColorFilter + +typealias ColorFilter = ColorFilter + +typealias AlphaFilter = AlphaFilter diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Dimension.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Dimension.kt similarity index 65% rename from src/kernel/client/kotlin/net/terramodulus/mui/gfx/Dimension.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Dimension.kt index 6825bcb0..ede9a02c 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Dimension.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Dimension.kt @@ -1,14 +1,16 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gfx +package net.terramodulus.mui.gui.gfx data class Dimension2I(val width: Int, val height: Int) data class Dimension2F(val width: Float, val height: Float) +data class Dimension2D(val width: Double, val height: Double) + data class Dimension3I(val width: Int, val height: Int, val length: Int) data class Dimension3F(val width: Float, val height: Float, val length: Float) diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Direction.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt similarity index 81% rename from src/kernel/client/kotlin/net/terramodulus/mui/gfx/Direction.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt index 621776f8..b8d78bd6 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Direction.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gfx +package net.terramodulus.mui.gui.gfx /* * Every set of directions has different meanings in different context, @@ -11,6 +11,10 @@ package net.terramodulus.mui.gfx * Those should be used with care since they are not already interconvertible. */ +enum class Direction2 { + Positive, Negative; +} + /** * Set of 4 compass directions. */ @@ -39,6 +43,13 @@ enum class Direction4A { XPos, XNeg, YPos, YNeg; } +/** + * Set of 4 axial diagonal directions, by Cartesian quadrants. + */ +enum class Direction4AD { + QuadOne, QuadTwo, QuadThree, QuadFour; +} + /** * Set of 8 compass directions. */ diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/GuiGeometry.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiGeometry.kt similarity index 55% rename from src/kernel/client/kotlin/net/terramodulus/mui/gfx/GuiGeometry.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiGeometry.kt index cc6a61b4..6515e0ff 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/GuiGeometry.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiGeometry.kt @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gfx +package net.terramodulus.mui.gui.gfx import net.terramodulus.engine.GeomDrawable import net.terramodulus.engine.SimpleLineGeom @@ -14,19 +14,19 @@ sealed class GuiGeometry(protected val geom: GeomDrawable) { fun add(filter: ColorFilter) = geom.add(filter) - fun setPos(pos: FloatArray) = geom.setPos(pos) + protected fun setPos(pos: FloatArray) = geom.setPos(pos) fun render(renderSystem: RenderSystem) = renderSystem.renderGuiGeo(geom) } -class GuiLine(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : - GuiGeometry(SimpleLineGeom(x0, y0, x1, y1, r, g, b, a)) { +class GuiLine(handle: RenderSystem.CanvasHandle, x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : + GuiGeometry(SimpleLineGeom(handle.canvas, x0, y0, x1, y1, r, g, b, a)) { fun setPos(x0: Int, y0: Int, x1: Int, y1: Int) = setPos(floatArrayOf(x0.toFloat(), y0.toFloat(), x1.toFloat(), y1.toFloat())) } -class GuiRect(x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : - GuiGeometry(SimpleRectGeom(x0, y0, x1, y1, r, g, b, a)) { +class GuiRect(handle: RenderSystem.CanvasHandle, x0: Int, y0: Int, x1: Int, y1: Int, r: Int, g: Int, b: Int, a: Int) : + GuiGeometry(SimpleRectGeom(handle.canvas, x0, y0, x1, y1, r, g, b, a)) { fun setPos(x0: Int, y0: Int, x1: Int, y1: Int) = setPos(floatArrayOf(x0.toFloat(), y0.toFloat(), x1.toFloat(), y1.toFloat())) } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiSprite.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiSprite.kt new file mode 100644 index 00000000..67f869ad --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiSprite.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.gfx + +import net.terramodulus.engine.SpriteMesh + +class GuiSprite(handle: RenderSystem.CanvasHandle, val rect: RectangleI, private val texture: UInt) { + private val mesh = SpriteMesh(handle.canvas, rect.x, rect.y, rect.x + rect.width, rect.y + rect.height) + + fun add(model: ModelTransform) = mesh.add(model) + + fun add(filter: ColorFilter) = mesh.add(filter) + + fun render(renderSystem: RenderSystem) = renderSystem.renderGuiTex(mesh, texture) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Insets.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Insets.kt new file mode 100644 index 00000000..623d7554 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Insets.kt @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.gfx + +abstract class Insets(open val left: N, open val top: N, open val right: N, open val bottom: N) { + +} + +data class InsetsI( + override val left: Int, + override val top: Int, + override val right: Int, + override val bottom: Int, +) : Insets(left, top, right, bottom) + +data class InsetsF( + override val left: Float, + override val top: Float, + override val right: Float, + override val bottom: Float, +) : Insets(left, top, right, bottom) { + operator fun plus(other: InsetsF) = InsetsF(left + other.left, top + other.top, right + other.right, bottom + other.bottom) +} + +data class InsetsD( + override val left: Double, + override val top: Double, + override val right: Double, + override val bottom: Double, +) : Insets(left, top, right, bottom) { + operator fun plus(other: InsetsD) = InsetsD(left + other.left, top + other.top, right + other.right, bottom + other.bottom) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ModelTransform.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ModelTransform.kt new file mode 100644 index 00000000..c561ac4c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ModelTransform.kt @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.gfx + +import com.cout970.math.vec2.ImmVec2d +import net.terramodulus.engine.GeneralTransform +import net.terramodulus.engine.ModelTransform + +typealias ModelTransform = ModelTransform + +typealias GeneralTransform = GeneralTransform + +/** + * Scaling and Translation transform parameters (for SRT Transform) for a rectangle. + */ +data class RectStParams(val scaleX: Double, val scaleY: Double, val translateX: Double, val translateY: Double) { + companion object { + /** + * @param a source rectangle + * @param b target rectangle + */ + fun fromRects(a: RectangleD, b: RectangleD): RectStParams { + val sx = b.width / a.width + val sy = b.height / a.height + val tx = b.x - a.x * sx + val ty = b.y - a.y * sy + return RectStParams(sx, sy, tx, ty) + } + } + + fun applyToGeneralTransform(generalTransform: GeneralTransform) { + generalTransform.update { + scale = ImmVec2d(scaleX, scaleY) + angle = 0.0 + pos = ImmVec2d(translateX, translateY) + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt new file mode 100644 index 00000000..6ba2783c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt @@ -0,0 +1,218 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.gfx + +import com.cout970.math.vec2.ImmVec2d +import com.cout970.math.vec2.ImmVec2f +import com.cout970.math.vec2.ImmVec2i +import com.cout970.math.vec2.Vec2 +import com.cout970.math.vec2.Vec2d +import com.cout970.math.vec2.Vec2f +import com.cout970.math.vec2.Vec2i + +/** + * Rectangle in a coordinate system with (0, 0) on the bottom left. + * The anchor of the rectangle is the bottom-left corner. + */ +sealed class Rectangle, N: Number, V: Vec2, D>( + open val x: N, + open val y: N, + open val width: N, + open val height: N, +) { + companion object { + fun withPoints(x0: Int, y0: Int, x1: Int, y1: Int): RectangleI { + val minX: Int; + val maxX: Int; + if (x0 < x1) { + minX = x0; + maxX = x1; + } else { + maxX = x0; + minX = x1; + } + val minY: Int; + val maxY: Int; + if (y0 < y1) { + minY = y0; + maxY = y1; + } else { + maxY = y0; + minY = y1; + } + return RectangleI(minX, minY, maxX - minX, maxY - minY) + } + + fun withPoints(x0: Float, y0: Float, x1: Float, y1: Float): RectangleF { + val minX: Float; + val maxX: Float; + if (x0 < x1) { + minX = x0; + maxX = x1; + } else { + maxX = x0; + minX = x1; + } + val minY: Float; + val maxY: Float; + if (y0 < y1) { + minY = y0; + maxY = y1; + } else { + maxY = y0; + minY = y1; + } + return RectangleF(minX, maxX, minY, maxY) + } + + fun withDirection(x: Int, y: Int, width: Int, height: Int, dir: Direction4AD) = when (dir) { + Direction4AD.QuadOne -> RectangleI(x, y, width, height) + Direction4AD.QuadTwo -> RectangleI(x - width, y, width, height) + Direction4AD.QuadThree -> RectangleI(x - width, y - height, width, height) + Direction4AD.QuadFour -> RectangleI(x, y - height, width, height) + } + + fun withDirection(x: Float, y: Float, width: Float, height: Float, dir: Direction4AD) = when (dir) { + Direction4AD.QuadOne -> RectangleF(x, y, width, height) + Direction4AD.QuadTwo -> RectangleF(x - width, y, width, height) + Direction4AD.QuadThree -> RectangleF(x - width, y - height, width, height) + Direction4AD.QuadFour -> RectangleF(x, y - height, width, height) + } + } + + protected abstract fun constructor(x: N, y: N, width: N, height: N): T + protected abstract fun vec2(x: N, y: N): V + protected abstract operator fun N.plus(other: N): N + protected abstract operator fun N.minus(other: N): N + protected abstract operator fun N.div(other: Int): N + protected abstract val V.x: N + protected abstract val V.y: N + + abstract val size: D + + fun anchor(pos: Anchor5) = when (pos) { + Anchor5.TopLeft -> vec2(x, y + width) + Anchor5.TopRight -> vec2(x + width, y + height) + Anchor5.BottomLeft -> vec2(x, y) + Anchor5.BottomRight -> vec2(x + width, y) + Anchor5.Center -> vec2(x + width / 2, y + height / 2) + } + + fun translateBy(pos: V) = constructor(x + pos.x, y + pos.y, width, height) + + fun translateBy(x: N, y: N) = constructor(this.x + x, this.y + y, width, height) + + fun translateByY(y: N) = constructor(x, this.y + y, width, height) + + fun translateByX(x: N) = constructor(this.x + x, y, width, height) + + fun translateToY(y: N) = constructor(x, y, width, height) + + fun translateToX(x: N) = constructor(x, y, width, height) + + fun translateTo(pos: V) = constructor(pos.x, pos.y, width, height) + + fun translateTo(x: N, y: N) = constructor(x, y, width, height) + + /** Inflates the [Rectangle] with the [Insets] */ + operator fun plus(other: Insets) = constructor( + x - other.left, + y - other.bottom, + width + other.left + other.right, + height + other.bottom + other.top, + ) + + /** Deflates the [Rectangle] with the [Insets] */ + operator fun minus(other: Insets) = constructor( + x + other.left, + y + other.bottom, + width - other.left - other.right, + height - other.bottom - other.top, + ) + + abstract fun toInt(): RectangleI + abstract fun toFloat(): RectangleF + abstract fun toDouble(): RectangleD +} + +data class RectangleI( + override val x: Int, + override val y: Int, + override val width: Int, + override val height: Int +) : Rectangle(x, y, width, height) { + override fun constructor(x: Int, y: Int, width: Int, height: Int) = RectangleI(x, y, width, height) + + override fun vec2(x: Int, y: Int) = ImmVec2i(x, y) + + override fun Int.plus(other: Int) = this + other + override fun Int.minus(other: Int) = this - other + override fun Int.div(other: Int) = this / other + + override val Vec2i.x: Int by ::x + override val Vec2i.y: Int by ::y + override val size = Dimension2I(width, height) + + override fun toInt() = this + override fun toFloat() = RectangleF(x.toFloat(), y.toFloat(), width.toFloat(), height.toFloat()) + override fun toDouble() = RectangleD(x.toDouble(), y.toDouble(), width.toDouble(), height.toDouble()) +} + +data class RectangleF( + override val x: Float, + override val y: Float, + override val width: Float, + override val height: Float +) : Rectangle(x, y, width, height) { + override fun constructor( + x: Float, + y: Float, + width: Float, + height: Float + ) = RectangleF(x, y, width, height) + + override fun vec2(x: Float, y: Float) = ImmVec2f(x, y) + + override fun Float.plus(other: Float) = this + other + override fun Float.minus(other: Float) = this - other + override fun Float.div(other: Int) = this / other + + override val Vec2f.x: Float by ::x + override val Vec2f.y: Float by ::y + override val size = Dimension2F(width, height) + + override fun toInt() = RectangleI(x.toInt(), y.toInt(), width.toInt(), height.toInt()) + override fun toFloat() = this + override fun toDouble() = RectangleD(x.toDouble(), y.toDouble(), width.toDouble(), height.toDouble()) +} + +data class RectangleD( + override val x: Double, + override val y: Double, + override val width: Double, + override val height: Double +) : Rectangle(x, y, width, height) { + override fun constructor( + x: Double, + y: Double, + width: Double, + height: Double + ) = RectangleD(x, y, width, height) + + override fun vec2(x: Double, y: Double) = ImmVec2d(x, y) + + override fun Double.plus(other: Double) = this + other + override fun Double.minus(other: Double) = this - other + override fun Double.div(other: Int) = this / other + + override val Vec2d.x: Double by ::x + override val Vec2d.y: Double by ::y + override val size = Dimension2D(width, height) + + override fun toInt() = RectangleI(x.toInt(), y.toInt(), width.toInt(), height.toInt()) + override fun toFloat() = RectangleF(x.toFloat(), y.toFloat(), width.toFloat(), height.toFloat()) + override fun toDouble() = this +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/RenderSystem.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/RenderSystem.kt similarity index 65% rename from src/kernel/client/kotlin/net/terramodulus/mui/gfx/RenderSystem.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/RenderSystem.kt index 0e86fb93..d27de899 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/RenderSystem.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/RenderSystem.kt @@ -1,17 +1,20 @@ /* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gfx +package net.terramodulus.mui.gui.gfx +import com.cout970.math.vec3.Vec3f import net.terramodulus.core.TerraModulus import net.terramodulus.core.getResourceAsBytes import net.terramodulus.core.getResourceAsString import net.terramodulus.engine.Canvas import net.terramodulus.engine.GeomDrawable import net.terramodulus.engine.MeshDrawable -import net.terramodulus.mui.gms.impl.GameplayScreen +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.agim.impl.GameplayScreen +import net.terramodulus.mui.gui.asd.AsdHandle class RenderSystem internal constructor(private val core: TerraModulus, private val canvas: Canvas) { val handle: Handle = HandleImpl() @@ -26,12 +29,20 @@ class RenderSystem internal constructor(private val core: TerraModulus, private val targetFps = 1000; sealed interface Handle { + val canvasHandle: CanvasHandle + fun loadTexture(path: String): UInt fun setBackgroundColor(red: Float, green: Float, blue: Float, alpha: Float) } + inner class CanvasHandle internal constructor() { + internal val canvas = this@RenderSystem.canvas + } + private inner class HandleImpl : Handle { + override val canvasHandle = CanvasHandle() + override fun loadTexture(path: String) = canvas.loadImage(getResourceAsBytes(path)) override fun setBackgroundColor(red: Float, green: Float, blue: Float, alpha: Float) { @@ -39,8 +50,10 @@ class RenderSystem internal constructor(private val core: TerraModulus, private } } - internal fun newGameplayScreen(pos: Vector3F) = - { it: Handle -> GameplayScreen(core, canvas.createCamera(floatArrayOf(pos.x, pos.y, pos.z)), it) } + internal fun newGameplayScreen(pos: Vec3f) = + { managerHandle: ScreenManager.Handle, asdHandle: AsdHandle.Container, it: Handle -> + GameplayScreen(core, canvas.createCamera(floatArrayOf(pos.x, pos.y, pos.z)), it, managerHandle, asdHandle) + } internal fun renderGuiTex(drawable: MeshDrawable, texture: UInt) = canvas.renderGuiTex(drawable, texShaders, texture) diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Vector.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Vector.kt new file mode 100644 index 00000000..47f84c19 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Vector.kt @@ -0,0 +1,6 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.gfx diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/hui/HuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/hui/HuiManager.kt new file mode 100644 index 00000000..f3963b9a --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/hui/HuiManager.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.hui + +class HuiManager { +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/input/InputSystem.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/InputSystem.kt similarity index 99% rename from src/kernel/client/kotlin/net/terramodulus/mui/input/InputSystem.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/kui/InputSystem.kt index 7656eed8..d9f8b3e6 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/input/InputSystem.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/InputSystem.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.input +package net.terramodulus.mui.kui // TODO Temporary solution, should be rewritten in next update. class InputSystem internal constructor() { diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/kui/KuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/KuiManager.kt new file mode 100644 index 00000000..41701cb3 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/KuiManager.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.kui + +import net.terramodulus.mui.kui.InputSystem.KeyEvent + +class KuiManager { + val inputSystem = InputSystem() + + internal fun update(events: List) { + inputSystem.update(events) + } +} diff --git a/src/kernel/common/kotlin/net/terramodulus/util/Math.kt b/src/kernel/common/kotlin/net/terramodulus/util/Math.kt new file mode 100644 index 00000000..fb7265b8 --- /dev/null +++ b/src/kernel/common/kotlin/net/terramodulus/util/Math.kt @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.util + +tailrec fun gcd(a: Int, b: Int): Int { + return if (b == 0) a else gcd(b, a % b) +} + +tailrec fun gcd(a: UInt, b: UInt): UInt { + return if (b == 0u) a else gcd(b, a % b) +} + +tailrec fun gcd(a: Long, b: Long): Long { + return if (b == 0L) a else gcd(b, a % b) +} + +tailrec fun gcd(a: ULong, b: ULong): ULong { + return if (b == 0uL) a else gcd(b, a % b) +} diff --git a/src/kernel/common/kotlin/net/terramodulus/util/TypedMap.kt b/src/kernel/common/kotlin/net/terramodulus/util/TypedMap.kt new file mode 100644 index 00000000..28a3e58a --- /dev/null +++ b/src/kernel/common/kotlin/net/terramodulus/util/TypedMap.kt @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.util + +import java.util.function.BiFunction +import java.util.function.Function + +/** + * A map with access by references to unique keys with type of values specified, + * matching the pattern of **Typed Anchor Dynamic Mapping**. + * Localized constrains may be applied by copying this universal structure.\ + * All bulk functions are all unsupported for type safety. + * @param T base class for map values + */ +open class TypedMap> + private constructor(private val map: MutableMap) : MutableMap by map { + constructor() : this(HashMap()) + + open class Simple : TypedMap>() + + /** + * A unique, immutable key that defines and enforces the type for values of a [TypedMap]. + * By hiding public access to the map instance, constrains may be applied by subclassing this. + */ + open class Key(val type: Class) { + companion object { + inline operator fun invoke() = Key(T::class.java) + } + override fun hashCode() = type.hashCode() + override fun equals(other: Any?): Boolean { + if (other === null) return false + if (this === other) return true + if (other !is Key<*>) return false + if (javaClass != other.javaClass) return false + return type == other.type + } + } +} diff --git a/src/kernel/common/kotlin/net/terramodulus/void/World.kt b/src/kernel/common/kotlin/net/terramodulus/void/World.kt index f8fbea9a..1d3d1935 100644 --- a/src/kernel/common/kotlin/net/terramodulus/void/World.kt +++ b/src/kernel/common/kotlin/net/terramodulus/void/World.kt @@ -5,11 +5,13 @@ package net.terramodulus.void +import com.cout970.math.vec3.ImmVec3d +import com.cout970.math.vec3.Vec3d +import com.cout970.math.vec3.plus import net.terramodulus.engine.PhyBody import net.terramodulus.engine.PhyEnv import net.terramodulus.engine.PhyGeom import net.terramodulus.engine.PhyGeomBox -import net.terramodulus.engine.Vec3D import net.terramodulus.util.logging.logger import java.io.Closeable import kotlin.properties.Delegates @@ -21,11 +23,11 @@ import kotlin.time.TimeSource private val logger = logger {} -class World(commander: Ymir) : Closeable { +class World(commander: Ymir, progressBar: ProgressBar) : Closeable { private val env = PhyEnv() private val world = env.createWorld() - var gravity: Vec3D by world::gravity + var gravity: Vec3d by world::gravity var frictionMode: FrictionMode by Delegates.observable(FrictionMode.Infinite) { _, _, new -> when (new) { FrictionMode.Zero -> world.setFriction(0.0) @@ -46,8 +48,14 @@ class World(commander: Ymir) : Closeable { // Floor at y=-100 val floor = world.createGeomPlane(doubleArrayOf(0.0, 1.0, 0.0, -100.0)) + interface ProgressBar { + fun addReadyListener(listener: () -> Unit) + + fun setProgress(progress: Double) + } + init { - gravity = Vec3D(0.0, -9.81, 0.0) + gravity = ImmVec3d(0.0, -9.81, 0.0) floor.setBits(1u, 1u.inv()) world.omitSpace(mainSpace) // Spawn point @@ -56,29 +64,39 @@ class World(commander: Ymir) : Closeable { objects[ObjId.randomUnique(objects)] = commander.wrapChar( world.newBody(PhyBody.Mass.SphereTotal(1.0, .5)).apply { addGeom(createGeomSphere(.5)) - pos = Vec3D(0.0, 1.0, 0.0) + pos = ImmVec3d(0.0, 1.0, 0.0) } ) - // Test Objects - randomCubes(commander).forEach { objects[ObjId.randomUnique(objects)] = it } - // Running in parallel Thread { - val timeSource = TimeSource.Monotonic - val interval = 1.seconds / 20 // 20 Hz - var lastMark = timeSource.markNow() - while(true) { - // uncalculated ticks are not accumulated at this stage, *skipped* instead - tick() - val now = timeSource.markNow() - // remaining time after elapsed time used to maintain stable interval - val rem = interval - (now - lastMark) - if (rem > Duration.ZERO) { // sleeps the remaining time only when it is positive - Thread.sleep(rem.inWholeMilliseconds) + var ready = false // intermediate state to prevent cross-thread processing by listener invocation + while (!ready) { + progressBar.addReadyListener { + ready = true } - // makes sure timing does not include slept time - lastMark = timeSource.markNow() + Thread.sleep(1) } + // Test Objects + randomCubes(commander, progressBar).forEach { objects[ObjId.randomUnique(objects)] = it } + // Running in parallel + Thread { + val timeSource = TimeSource.Monotonic + val interval = 1.seconds / 20 // 20 Hz + var lastMark = timeSource.markNow() + while(true) { + // uncalculated ticks are not accumulated at this stage, *skipped* instead + tick() + val now = timeSource.markNow() + // remaining time after elapsed time used to maintain stable interval + val rem = interval - (now - lastMark) + if (rem > Duration.ZERO) { // sleeps the remaining time only when it is positive + Thread.sleep(rem.inWholeMilliseconds) + } + // makes sure timing does not include slept time + lastMark = timeSource.markNow() + } + }.start() }.start() + } interface Ymir { @@ -92,7 +110,7 @@ class World(commander: Ymir) : Closeable { interface VoidGeom { fun render() - val pos: Vec3D + val pos: Vec3d } interface EnvVoidGeom : VoidGeom { @@ -104,46 +122,52 @@ class World(commander: Ymir) : Closeable { } // Source: https://en.wikipedia.org/wiki/Maze_generation_algorithm - private fun randomCubes(commander: Ymir): ArrayList { + private fun randomCubes(commander: Ymir, progressBar: ProgressBar): ArrayList { val list = ArrayList() var i = 0 - val total = 10 * 10 * 2 * 2 - val interval = 5.0 + val radius = 12 + val total = radius * radius * 2 * 2 * 7 + val intervalHor = 5.0 + val intervalVert = 8 val max = 5 * 5 * 5 // 125 for each set val directions = arrayOf( - Vec3D(1.0, 0.0, 0.0), - Vec3D(-1.0, 0.0, 0.0), - Vec3D(0.0, 1.0, 0.0), - Vec3D(0.0, -1.0, 0.0), - Vec3D(0.0, 0.0, 1.0), - Vec3D(0.0, 0.0, -1.0), + ImmVec3d(1.0, 0.0, 0.0), + ImmVec3d(-1.0, 0.0, 0.0), + ImmVec3d(0.0, 1.0, 0.0), + ImmVec3d(0.0, -1.0, 0.0), + ImmVec3d(0.0, 0.0, 1.0), + ImmVec3d(0.0, 0.0, -1.0), ) - for (x in 1..10) { - for (z in 1..10) { - for (xs in booleanArrayOf(false, true)) { - for (zs in booleanArrayOf(false, true)) { - logger.info { "Generating: ${++i}/$total" } - val xx = (if (xs) x else -x).toDouble() * interval - val zz = (if (zs) z else -z).toDouble() * interval - val origin = Vec3D(xx, Random.nextInt(-3..3).toDouble(), zz) - val visited = mutableSetOf(Vec3D(0.0, 0.0, 0.0)) - val heads = ArrayDeque() - heads.addLast(Vec3D(0.0, 0.0, 0.0)) - while (!heads.isEmpty()) { - val head = heads.removeFirst() - for (d in directions) { - val cur = head + d - if (Random.nextInt(max) > visited.size && cur !in visited) { - visited.add(cur) - if (Random.nextInt(max) > visited.size) { - heads.addLast(cur) + for (x in 1..radius) { + for (y in -3..3) { + for (z in 1..radius) { + for (xs in booleanArrayOf(false, true)) { + for (zs in booleanArrayOf(false, true)) { + // logger.info { "Generating: ${++i}/$total" } + progressBar.setProgress(++i / total.toDouble()) + val xx = (if (xs) x else -x).toDouble() * intervalHor + val zz = (if (zs) z else -z).toDouble() * intervalHor + val yy = (y * intervalVert).toDouble() + val origin = ImmVec3d(xx, yy + Random.nextInt(-3..3).toDouble(), zz) + val visited = mutableSetOf(ImmVec3d(0.0, 0.0, 0.0)) + val heads = ArrayDeque() + heads.addLast(ImmVec3d(0.0, 0.0, 0.0)) + while (!heads.isEmpty()) { + val head = heads.removeFirst() + for (d in directions) { + val cur = head + d + if (Random.nextInt(max) > visited.size && cur !in visited) { + visited.add(cur) + if (Random.nextInt(max) > visited.size) { + heads.addLast(cur) + } } } } - } - for (p in visited) { - val pt = origin + p - list.add(commander.wrapCube(createCube(pt.x, pt.y, pt.z), pt.x, pt.y, pt.z)) + for (p in visited) { + val pt = origin + p + list.add(commander.wrapCube(createCube(pt.x, pt.y, pt.z), pt.x, pt.y, pt.z)) + } } } } @@ -152,8 +176,6 @@ class World(commander: Ymir) : Closeable { return list } - private operator fun Vec3D.plus(other: Vec3D): Vec3D = Vec3D(x + other.x, y + other.y, z + other.z) - private fun createCube(x: Double, y: Double, z: Double): PhyGeomBox { val cube = createGeomBox(1.0, 1.0, 1.0) cube.setPosition(doubleArrayOf(x, y, z)) diff --git a/vector-math b/vector-math new file mode 160000 index 00000000..d25db430 --- /dev/null +++ b/vector-math @@ -0,0 +1 @@ +Subproject commit d25db430d52bb0ce28d485b1d073db1acc31a99a