From 7650fc6d79604e89f8f98388970ecd2ca40559a8 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:36:23 +0800 Subject: [PATCH 01/28] Add vector-math --- .gitmodules | 3 +++ build.gradle.kts | 6 ++++++ settings.gradle.kts | 2 ++ vector-math | 1 + 4 files changed, 12 insertions(+) create mode 160000 vector-math 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/build.gradle.kts b/build.gradle.kts index 8541b9db..615ceab1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -61,6 +61,12 @@ project(":kernel") { } } +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") diff --git a/settings.gradle.kts b/settings.gradle.kts index a487543e..0f27e6cd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,3 +16,5 @@ rootProject.children.forEach { it.projectDir = File(settingsDir, "src/${it.name}") include("${it.name}:common", "${it.name}:client", "${it.name}:server") } + +includeBuild("vector-math") 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 From de98c65097810b3ae7a30a380375b5b30939dd21 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Wed, 29 Apr 2026 02:41:37 +0800 Subject: [PATCH 02/28] Simplify usages of math vectors Also upgrade Gradle to 8.14.4 --- gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 6 +- gradlew.bat | 4 +- .../net/terramodulus/engine/Containers.kt | 8 +- .../terramodulus/engine/WorldObjDrawable.kt | 19 ++- .../net/terramodulus/engine/Containers.kt | 23 --- .../kotlin/net/terramodulus/engine/PhyBody.kt | 13 +- .../net/terramodulus/engine/PhyWorld.kt | 5 +- .../terramodulus/engine/common/Containers.kt | 20 +++ .../net/terramodulus/mui/gfx/Rectangle.kt | 152 ++++++++++-------- .../net/terramodulus/mui/gfx/RenderSystem.kt | 3 +- .../kotlin/net/terramodulus/mui/gfx/Vector.kt | 52 ------ .../mui/gms/impl/GameplayScreen.kt | 88 +++++----- .../mui/gms/impl/ResourceLoadingScreen.kt | 8 +- .../kotlin/net/terramodulus/void/World.kt | 34 ++-- 16 files changed, 201 insertions(+), 236 deletions(-) delete mode 100644 src/internal/common/kotlin/net/terramodulus/engine/Containers.kt create mode 100644 src/internal/common/kotlin/net/terramodulus/engine/common/Containers.kt diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9530d66f5e68d973ea569d8e19de379189..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch delta 34943 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

Gn4F+B~IivciU9^t0j-Mgao3 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 DDw4oP 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/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/WorldObjDrawable.kt b/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt index 55fe2423..cf854519 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt @@ -5,38 +5,41 @@ package net.terramodulus.engine +import com.cout970.math.quaternion.Quatd +import com.cout970.math.vec3.Vec3d +import com.cout970.math.vec4.Vec4i import net.terramodulus.engine.ferricia.Gwr.newMeshGeomCube import net.terramodulus.engine.ferricia.Gwr.newMeshGeomSphere 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) { +sealed class WorldObjDrawable(internal val handle: ULong, private var pos: Vec3d, private var scale: Vec3d, private var rot: Quatd) { 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) : +class SimpleMesh3dGeomCube(width: Float, rgba: Vec4i, pos: Vec3d, scale: Vec3d, rot: Quatd) : WorldObjDrawable(newMeshGeomCube(width, rgba.toArray()), pos, scale, rot) -class SimpleMesh3dGeomSphere(radius: Float, rgba: Rgba, pos: Vec3D, scale: Vec3D, rot: Quat) : +class SimpleMesh3dGeomSphere(radius: Float, rgba: Vec4i, pos: Vec3d, scale: Vec3d, rot: Quatd) : WorldObjDrawable(newMeshGeomSphere(radius, rgba.toArray()), pos, scale, rot) 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/mui/gfx/Rectangle.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Rectangle.kt index ecfeca3f..93b8081f 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Rectangle.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Rectangle.kt @@ -5,15 +5,17 @@ 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 +import com.cout970.math.vec2.ImmVec2f +import com.cout970.math.vec2.ImmVec2i +import com.cout970.math.vec2.Vec2 +import com.cout970.math.vec2.Vec2f +import com.cout970.math.vec2.Vec2i + +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 { @@ -37,48 +39,7 @@ data class RectangleI( } 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; @@ -102,29 +63,92 @@ data class RectangleF( } } - val size get() = Dimension2F(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.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 -> 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) + 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: Vector2F) = RectangleF(x + pos.x, y + pos.y, width, height) + 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) + + abstract fun toFloat(): RectangleF +} - fun translateBy(x: Float, y: Float) = RectangleF(this.x + x, this.y + y, width, height) +/** + * 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( + 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) - fun translateByY(y: Float) = RectangleF(x, this.y + y, width, height) + override fun vec2(x: Int, y: Int) = ImmVec2i(x, y) - fun translateByX(x: Float) = RectangleF(this.x + x, y, width, height) + override fun Int.plus(other: Int) = this + other - fun translateToY(y: Float) = RectangleF(x, y, width, height) + override fun Int.div(other: Int) = this / other - fun translateToX(x: Float) = RectangleF(x, y, width, height) + override val Vec2i.x: Int by ::x + override val Vec2i.y: Int by ::y + override val size = Dimension2I(width, height) - fun translateTo(pos: Vector2F) = RectangleF(pos.x, pos.y, width, height) + override fun toFloat() = RectangleF(x.toFloat(), y.toFloat(), width.toFloat(), height.toFloat()) +} - fun translateTo(x: Float, y: Float) = RectangleF(x, y, width, height) +/** + * 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( + 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.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 toFloat() = this } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/RenderSystem.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/RenderSystem.kt index 0e86fb93..520c796c 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/RenderSystem.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/RenderSystem.kt @@ -5,6 +5,7 @@ package net.terramodulus.mui.gfx +import com.cout970.math.vec3.Vec3f import net.terramodulus.core.TerraModulus import net.terramodulus.core.getResourceAsBytes import net.terramodulus.core.getResourceAsString @@ -39,7 +40,7 @@ class RenderSystem internal constructor(private val core: TerraModulus, private } } - internal fun newGameplayScreen(pos: Vector3F) = + internal fun newGameplayScreen(pos: Vec3f) = { it: Handle -> GameplayScreen(core, canvas.createCamera(floatArrayOf(pos.x, pos.y, pos.z)), it) } internal fun renderGuiTex(drawable: MeshDrawable, texture: UInt) = canvas.renderGuiTex(drawable, texShaders, texture) diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Vector.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Vector.kt index a92d68db..243e93c5 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Vector.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Vector.kt @@ -4,55 +4,3 @@ */ 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/impl/GameplayScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GameplayScreen.kt index e704c474..8e67bbba 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GameplayScreen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GameplayScreen.kt @@ -5,21 +5,28 @@ package net.terramodulus.mui.gms.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.engine.common.ZeroImmVec3d 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 @@ -27,15 +34,14 @@ import net.terramodulus.mui.input.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 @@ -86,11 +92,11 @@ internal class GameplayScreen(private val core: TerraModulus, private val camera SimpleMesh3dGeomCube( 2F, randomColor(), - Vec3D(x, y, z), + ImmVec3d(x, y, z), STD_SCALE, IDENT_ROT, ), - Vec3D(x, y, z) + ImmVec3d(x, y, z) ) private fun randomColor() = when (Random.nextInt(3)) { @@ -102,7 +108,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) + SimpleMesh3dGeomSphere(1F, WHITE, ImmVec3d(0.0, 1.0, 0.0), STD_SCALE, IDENT_ROT) ) return player } @@ -114,21 +120,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 +148,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,15 +167,15 @@ 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) { // Those keys are not related to GUI, so they are fine to be here. @@ -292,7 +282,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,9 +310,9 @@ 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() { 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 index e62d091b..0fb74f48 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/ResourceLoadingScreen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/ResourceLoadingScreen.kt @@ -5,19 +5,19 @@ package net.terramodulus.mui.gms.impl +import net.terramodulus.engine.common.ZeroImmVec3f 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.Rectangle 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 @@ -66,7 +66,7 @@ class ResourceLoadingScreen(renderSystemHandle: RenderSystem.Handle) : Screen() } private class ProgressBar { - val rectDim = RectangleI.withPoints(7, 7, 393, 33) + 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) @@ -104,7 +104,7 @@ class ResourceLoadingScreen(renderSystemHandle: RenderSystem.Handle) : Screen() } // 3 -> screenManager.handle.openBefore(::TitleScreen, this) - 3 -> screenManager.handle.reset(renderSystem.newGameplayScreen(Vector3F.ZERO)) + 3 -> screenManager.handle.reset(renderSystem.newGameplayScreen(ZeroImmVec3f)) } } diff --git a/src/kernel/common/kotlin/net/terramodulus/void/World.kt b/src/kernel/common/kotlin/net/terramodulus/void/World.kt index f8fbea9a..6c5d7d9f 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 @@ -25,7 +27,7 @@ class World(commander: Ymir) : 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) @@ -47,7 +49,7 @@ class World(commander: Ymir) : Closeable { val floor = world.createGeomPlane(doubleArrayOf(0.0, 1.0, 0.0, -100.0)) 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,7 +58,7 @@ 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 @@ -92,7 +94,7 @@ class World(commander: Ymir) : Closeable { interface VoidGeom { fun render() - val pos: Vec3D + val pos: Vec3d } interface EnvVoidGeom : VoidGeom { @@ -111,12 +113,12 @@ class World(commander: Ymir) : Closeable { val interval = 5.0 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) { @@ -125,10 +127,10 @@ class World(commander: Ymir) : Closeable { 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)) + val origin = ImmVec3d(xx, 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) { @@ -152,8 +154,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)) From 8509452daba76a487e09859ac264d6813dedced7 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Wed, 29 Apr 2026 03:04:20 +0800 Subject: [PATCH 03/28] Project: Solve configuration cache issue Just by not storing those tasks --- build.gradle.kts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 615ceab1..c6ef7aee 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -123,7 +123,8 @@ tasks.register("cargoBuildClient") { onlyIf { !gradle.taskGraph.hasTask(":kernel:server:jar") } - workingDir = rootProject.file("ferricia") + notCompatibleWithConfigurationCache("complicated") + workingDir = layout.projectDirectory.dir("ferricia").asFile commandLine("cargo", "build") if (project.hasProperty("release")) args("--release") // use `-Prelease=true` args("-F", "client") @@ -132,7 +133,8 @@ tasks.register("cargoBuildServer") { onlyIf { !gradle.taskGraph.hasTask(":kernel:client:jar") } - workingDir = rootProject.file("ferricia") + notCompatibleWithConfigurationCache("complicated") + workingDir = layout.projectDirectory.dir("ferricia").asFile commandLine("cargo", "build") if (project.hasProperty("release")) args("--release") // use `-Prelease=true` args("-F", "server") From 4aac1d3253623419fc165f1841888995259181fc Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Wed, 29 Apr 2026 03:19:27 +0800 Subject: [PATCH 04/28] Project: Substitute dep for vector-math --- settings.gradle.kts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 0f27e6cd..bf2d7eb6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,4 +17,8 @@ rootProject.children.forEach { include("${it.name}:common", "${it.name}:client", "${it.name}:server") } -includeBuild("vector-math") +includeBuild("vector-math") { + dependencySubstitution { + substitute(module("com.cout970:kotlin-vector-math")).using(project(":")) + } +} From 177050fd0a9ade610c72765252b2f83f0f0f443f Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Wed, 29 Apr 2026 03:30:54 +0800 Subject: [PATCH 05/28] Update codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 3 +++ 1 file changed, 3 insertions(+) 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 From 2dedf60fde82fc56b18f27456175264dc21574d1 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Fri, 1 May 2026 06:13:32 +0800 Subject: [PATCH 06/28] MUI: WIP: Restruct and impl MUI as per EFP 9 Basic restructure Adding usages of Layout, but not yet implemented --- .../kotlin/net/terramodulus/engine/Canvas.kt | 2 +- .../kotlin/net/terramodulus/engine/Window.kt | 17 +- .../kotlin/net/terramodulus/core/Main.kt | 1 - .../net/terramodulus/core/TerraModulus.kt | 10 +- .../mui/{GuiManager.kt => MuiManager.kt} | 49 ++--- .../mui/{audio => aui}/AudioSystem.kt | 2 +- .../terramodulus/mui/{ => aui}/AuiManager.kt | 6 +- .../net/terramodulus/mui/gfx/ColorFilter.kt | 10 - .../net/terramodulus/mui/gfx/ManagedRect.kt | 22 -- .../terramodulus/mui/gfx/ModelTransform.kt | 14 -- .../kotlin/net/terramodulus/mui/gfx/Vector.kt | 6 - .../net/terramodulus/mui/gms/AbstractPanel.kt | 9 - .../net/terramodulus/mui/gms/Container.kt | 12 -- .../kotlin/net/terramodulus/mui/gms/Menu.kt | 67 ------ .../kotlin/net/terramodulus/mui/gms/Screen.kt | 139 ------------ .../net/terramodulus/mui/gms/ScreenManager.kt | 156 -------------- .../mui/gms/event/ComponentEvent.kt | 9 - .../terramodulus/mui/gms/event/MenuEvent.kt | 9 - .../mui/gms/impl/BlankComponent.kt | 16 -- .../mui/gms/impl/FlexibleBoxLayout.kt | 20 -- .../mui/gms/impl/GeomComponent.kt | 16 -- .../mui/gms/impl/GraphicsComponent.kt | 26 --- .../mui/gms/impl/PositioningComponent.kt | 15 -- .../terramodulus/mui/gms/impl/TitleScreen.kt | 13 -- .../net/terramodulus/mui/gui/GuiManager.kt | 47 +++++ .../terramodulus/mui/gui/agim/AbstractPane.kt | 14 ++ .../mui/{gms => gui/agim}/Component.kt | 20 +- .../terramodulus/mui/gui/agim/Container.kt | 17 ++ .../mui/{gms => gui/agim}/Layout.kt | 94 +++++---- .../net/terramodulus/mui/gui/agim/Menu.kt | 32 +++ .../terramodulus/mui/gui/agim/MenuManager.kt | 56 +++++ .../net/terramodulus/mui/gui/agim/Screen.kt | 70 +++++++ .../mui/gui/agim/ScreenManager.kt | 198 ++++++++++++++++++ .../mui/gui/agim/event/ComponentEvent.kt | 10 + .../mui/gui/agim/event/MenuEvent.kt | 9 + .../{gms => gui/agim}/event/ScreenEvent.kt | 4 +- .../mui/gui/agim/impl/BlankComponent.kt | 16 ++ .../mui/gui/agim/impl/CompositeLayout.kt | 20 ++ .../mui/gui/agim/impl/FlexibleBoxLayout.kt | 18 ++ .../{gms => gui/agim}/impl/GameplayScreen.kt | 24 ++- .../mui/gui/agim/impl/GeomComponent.kt | 16 ++ .../mui/gui/agim/impl/GraphicsComponent.kt | 27 +++ .../{gms => gui/agim}/impl/LaunchingScreen.kt | 32 +-- .../mui/gui/agim/impl/PositioningComponent.kt | 15 ++ .../agim}/impl/ResourceLoadingScreen.kt | 38 ++-- .../{gms => gui/agim}/impl/SequenceLayout.kt | 12 +- .../mui/gui/agim/impl/TitleScreen.kt | 17 ++ .../mui/gui/agim/package-info.java | 11 + .../terramodulus/mui/{ => gui}/gfx/Anchor.kt | 4 +- .../terramodulus/mui/gui/gfx/ColorFilter.kt | 13 ++ .../mui/{ => gui}/gfx/Dimension.kt | 4 +- .../mui/{ => gui}/gfx/Direction.kt | 4 +- .../mui/{ => gui}/gfx/GuiGeometry.kt | 4 +- .../mui/{ => gui}/gfx/GuiSprite.kt | 4 +- .../terramodulus/mui/gui/gfx/ManagedRect.kt | 30 +++ .../mui/gui/gfx/ModelTransform.kt | 17 ++ .../mui/{ => gui}/gfx/Rectangle.kt | 4 +- .../mui/{ => gui}/gfx/RenderSystem.kt | 15 +- .../net/terramodulus/mui/gui/gfx/Vector.kt | 6 + .../net/terramodulus/mui/hui/HuiManager.kt | 9 + .../mui/{input => kui}/InputSystem.kt | 2 +- .../net/terramodulus/mui/kui/KuiManager.kt | 16 ++ 62 files changed, 869 insertions(+), 726 deletions(-) rename src/kernel/client/kotlin/net/terramodulus/mui/{GuiManager.kt => MuiManager.kt} (87%) rename src/kernel/client/kotlin/net/terramodulus/mui/{audio => aui}/AudioSystem.kt (82%) rename src/kernel/client/kotlin/net/terramodulus/mui/{ => aui}/AuiManager.kt (57%) delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gfx/ColorFilter.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gfx/ManagedRect.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gfx/ModelTransform.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gfx/Vector.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gms/AbstractPanel.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gms/Container.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gms/Menu.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gms/Screen.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gms/ScreenManager.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gms/event/ComponentEvent.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gms/event/MenuEvent.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/BlankComponent.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/FlexibleBoxLayout.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GeomComponent.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/GraphicsComponent.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/PositioningComponent.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/TitleScreen.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AbstractPane.kt rename src/kernel/client/kotlin/net/terramodulus/mui/{gms => gui/agim}/Component.kt (57%) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Container.kt rename src/kernel/client/kotlin/net/terramodulus/mui/{gms => gui/agim}/Layout.kt (79%) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Menu.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/MenuManager.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Screen.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ComponentEvent.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/MenuEvent.kt rename src/kernel/client/kotlin/net/terramodulus/mui/{gms => gui/agim}/event/ScreenEvent.kt (56%) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BlankComponent.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CompositeLayout.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/FlexibleBoxLayout.kt rename src/kernel/client/kotlin/net/terramodulus/mui/{gms => gui/agim}/impl/GameplayScreen.kt (95%) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GeomComponent.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GraphicsComponent.kt rename src/kernel/client/kotlin/net/terramodulus/mui/{gms => gui/agim}/impl/LaunchingScreen.kt (65%) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/PositioningComponent.kt rename src/kernel/client/kotlin/net/terramodulus/mui/{gms => gui/agim}/impl/ResourceLoadingScreen.kt (76%) rename src/kernel/client/kotlin/net/terramodulus/mui/{gms => gui/agim}/impl/SequenceLayout.kt (90%) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TitleScreen.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/package-info.java rename src/kernel/client/kotlin/net/terramodulus/mui/{ => gui}/gfx/Anchor.kt (83%) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ColorFilter.kt rename src/kernel/client/kotlin/net/terramodulus/mui/{ => gui}/gfx/Dimension.kt (74%) rename src/kernel/client/kotlin/net/terramodulus/mui/{ => gui}/gfx/Direction.kt (92%) rename src/kernel/client/kotlin/net/terramodulus/mui/{ => gui}/gfx/GuiGeometry.kt (90%) rename src/kernel/client/kotlin/net/terramodulus/mui/{ => gui}/gfx/GuiSprite.kt (80%) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ManagedRect.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ModelTransform.kt rename src/kernel/client/kotlin/net/terramodulus/mui/{ => gui}/gfx/Rectangle.kt (97%) rename src/kernel/client/kotlin/net/terramodulus/mui/{ => gui}/gfx/RenderSystem.kt (81%) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Vector.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/hui/HuiManager.kt rename src/kernel/client/kotlin/net/terramodulus/mui/{input => kui}/InputSystem.kt (99%) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/kui/KuiManager.kt diff --git a/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt b/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt index a838815b..4bdbcf25 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt @@ -32,7 +32,7 @@ class Canvas internal constructor(private val windowHandle: ULong) : Closeable { fun setClearColor(r: Float, g: Float, b: Float, a: Float) = setCanvasClearColor(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) diff --git a/src/internal/client/kotlin/net/terramodulus/engine/Window.kt b/src/internal/client/kotlin/net/terramodulus/engine/Window.kt index ef3c5e54..677819ad 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Window.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Window.kt @@ -18,11 +18,24 @@ 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) + fun sizeChanged(width: UInt, height: UInt) { + this.width = width + this.height = height + canvas.resizeGLViewport() + } + fun show() = showWindow(windowHandle) fun swap() = swapWindow(windowHandle) 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/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/Vector.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Vector.kt deleted file mode 100644 index 243e93c5..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Vector.kt +++ /dev/null @@ -1,6 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gfx 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/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/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/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..0ac450ec --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt @@ -0,0 +1,47 @@ +/* + * 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.gfx.RenderSystem +import net.terramodulus.mui.gui.agim.ScreenManager +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 screenManager = ScreenManager(renderSystem.handle) + + /** + * 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) + } + + /** + * 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..c0d4f14e --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AbstractPane.kt @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim + +abstract class AbstractPane : Component(), Container { + final override fun update(muiIopIf: ScreenManager.MuiIopIf) { + super.update(muiIopIf) + layout.update() + layout.components.forEach { it.update(muiIopIf) } + } +} 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 57% 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..9723bf6c 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,17 +1,17 @@ /* - * 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.gfx.ManagedRect +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.kui.InputSystem /** - * [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. @@ -20,7 +20,7 @@ abstract class Component { private val listeners = HashMap, LinkedHashSet<(ComponentEvent) -> Unit>>() /** - * This should only be modified by [Layout] managers. + * Caveat: This should only be modified by [Layout][net.terramodulus.mui.gui.agim.Layout] managers. */ open lateinit var rect: ManagedRect internal set @@ -40,7 +40,7 @@ abstract class Component { listeners[event.javaClass]?.forEach { it(event) } } - fun update(inputSystem: InputSystem) { - TODO() + internal open fun update(muiIopIf: ScreenManager.MuiIopIf) { + dispatchEvent(ComponentEvent.Update) } } 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..56687c20 --- /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.gfx.ManagedRect + +/** + * **AGIM Container**, direct subclasses are explicitly defined. + */ +sealed interface Container { + val rect: ManagedRect + + 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 79% 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..58c3e16d 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.RectangleF +import java.io.Closeable +import java.util.ArrayDeque /** * [Layout] is always mutable. @@ -15,25 +15,44 @@ 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(private 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 update() = layout(container.rect.rect) + 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() { + val nonEmpty = layoutOperations.isNotEmpty() + while (layoutOperations.isNotEmpty()) { + with(layoutOperations.removeFirst()) { this@Layout.operate() } + } + if (nonEmpty) layout(container.rect.value) + } /** * Lays out the managed [components] by this [Layout] manager. @@ -47,10 +66,14 @@ abstract class Layout(private val container: Container) { /** * Must be invoked when this [Layout] is no longer in use. */ - fun clear() { + fun clear() { // Not sure whether there is the necessity to separate this from [close]. container.rect.unobserve(containerObserver) } + override fun close() { + clear() + } + /** * Should not rely on indices in the [Layout] since they are not meaningful. */ @@ -92,10 +115,10 @@ abstract class Layout(private val container: Container) { } abstract class ElementGroup protected constructor( - container: Container, - protected val elements: ElementList, + container: Container, + protected 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 +167,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 - } + protected fun componentsNullableSequence(vararg components: () -> Component?) = + sequenceOf(*components).mapNotNull { it() } - override fun hasNext(): Boolean = untilNotNull() - - 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/Menu.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Menu.kt new file mode 100644 index 00000000..45bb9e67 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Menu.kt @@ -0,0 +1,32 @@ +/* + * 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 + +abstract class Menu : Container { + private val listeners = HashMap, LinkedHashSet<(MenuEvent) -> Unit>>() + + 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) } + } + + abstract fun render() + + internal fun update(muiIopIf: ScreenManager.MuiIopIf) { + layout.update() + layout.components.forEach { it.update(muiIopIf) } + } +} 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..04ad2c0f --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/MenuManager.kt @@ -0,0 +1,56 @@ +/* + * 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.gfx.RenderSystem +import java.util.ArrayDeque + +class MenuManager internal constructor() { + 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: () -> Menu) + + fun removeMenu(menu: Menu) + } + + private inner class HandleImpl : Handle { + override fun addMenu(menu: () -> Menu) { + menuQueue.add(MenuOperation.Add(menu)) + } + + override fun removeMenu(menu: Menu) { + menuQueue.add(MenuOperation.Remove(menu)) + } + } + + internal fun update(muiIopIf: ScreenManager.MuiIopIf) { + menuQueue.forEach { it.apply(menus) } + menuQueue.clear() + } + + internal fun render(renderSystem: RenderSystem, screenManager: ScreenManager) { + menus.forEach { it.render() } + } +} 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..245a40c5 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Screen.kt @@ -0,0 +1,70 @@ +/* + * 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.gfx.RenderSystem +import java.io.Closeable + +abstract class Screen( + managerHandle: ScreenManager.Handle, + final override val rect: ScreenManager.DelegatedRect +) : Container, Closeable { + private val listeners = HashMap, LinkedHashSet<(ScreenEvent) -> Unit>>() + private val menuManager = MenuManager() + val handle: Handle = HandleImpl(managerHandle) + + 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: () -> Menu) + + fun removeMenu(menu: Menu) + + fun addTopMenu(menu: () -> Menu) + + fun removeTopMenu(menu: Menu) + } + + private inner class HandleImpl(private val managerHandle: ScreenManager.Handle) : Handle { + override fun addMenu(menu: () -> Menu) = menuManager.handle.addMenu(menu) + + override fun removeMenu(menu: Menu) = menuManager.handle.removeMenu(menu) + + override fun addTopMenu(menu: () -> Menu) = managerHandle.addMenu(menu) + + override fun removeTopMenu(menu: Menu) = managerHandle.removeMenu(menu) + } + + internal fun update(muiIopIf: ScreenManager.MuiIopIf) { + layout.update() + layout.components.forEach { it.update(muiIopIf) } + } + + internal fun render(renderSystem: RenderSystem, screenManager: ScreenManager) { + menuManager.render(renderSystem, screenManager) + layout.components.forEach { it.render(renderSystem) } + } + + /** + * Cleans up and closes any used resources in this session. + */ + override fun close() { + listeners[ScreenEvent.Close.javaClass]?.forEach { it(ScreenEvent.Close) } + rect.close() + } +} 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..99e16aa2 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt @@ -0,0 +1,198 @@ +/* + * 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.gfx.RenderSystem +import net.terramodulus.mui.gui.agim.impl.LaunchingScreen +import net.terramodulus.mui.gui.gfx.ManagedRect +import net.terramodulus.mui.gui.gfx.RectangleF +import net.terramodulus.mui.kui.InputSystem +import java.io.Closeable +import kotlin.properties.Delegates + +class ScreenManager internal constructor(window: Window, 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() + private val menuManager = MenuManager() + private val viewportRect = ManagedRect.Normal(RectangleF(0F, 0F, window.width.toFloat(), window.height.toFloat())) + + inner class DelegatedRect : ManagedRect(), Closeable { + override var value: RectangleF by Delegates.observable(viewportRect.value) { _, _, newValue -> + observers.forEach { it(newValue) } + } + private set + + private val listener: (RectangleF) -> Unit = { rect -> value = rect } + + init { + viewportRect.observe(listener) + } + + override fun close() { + viewportRect.unobserve(listener) + } + } + + val handle: Handle = HandleImpl() + + init { + screens.add(LaunchingScreen(renderSystemHandle, handle, DelegatedRect())) + } + + 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().close() + } + } + } + + /** + * 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 target: Screen, val screen: (RenderSystem.Handle) -> 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.close() + } + } + } + } + + /** + * 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.close() } + screens.clear() + screens.add(screen(handle)) + } + } + } + + sealed interface Handle { + /** + * @see ScreenOperation.Exit + */ + fun exit(n: Int) + + /** + * @see ScreenOperation.Open + */ + fun open(screen: (Handle, DelegatedRect, RenderSystem.Handle) -> Screen) + + /** + * It is not recommended to use this in general scenarios. + * @see ScreenOperation.OpenBefore + */ + fun openBefore(target: Screen, screen: (Handle, DelegatedRect, RenderSystem.Handle) -> Screen) + + /** + * @see ScreenOperation.ExitTo + */ + fun exitTo(screen: Screen) + + /** + * @see ScreenOperation.Reset + */ + fun reset(screen: (Handle, DelegatedRect, RenderSystem.Handle) -> Screen) + + fun addMenu(menu: () -> Menu) + + fun removeMenu(menu: Menu) + } + + private inner class HandleImpl : Handle { + override fun exit(n: Int) { + screenQueue.add(ScreenOperation.Exit(n)) + } + + override fun open(screen: (Handle, DelegatedRect, RenderSystem.Handle) -> Screen) { + screenQueue.add(ScreenOperation.Open { screen(handle, DelegatedRect(), it) }) + } + + override fun openBefore(target: Screen, screen: (Handle, DelegatedRect, RenderSystem.Handle) -> Screen) { + screenQueue.add(ScreenOperation.OpenBefore(target) { screen(handle, DelegatedRect(), it) }) + } + + override fun exitTo(screen: Screen) { + screenQueue.add(ScreenOperation.ExitTo(screen)) + } + + override fun reset(screen: (Handle, DelegatedRect, RenderSystem.Handle) -> Screen) { + screenQueue.add(ScreenOperation.Reset { screen(handle, DelegatedRect(), it) }) + } + + override fun addMenu(menu: () -> Menu) = menuManager.handle.addMenu(menu) + + override fun removeMenu(menu: Menu) = menuManager.handle.removeMenu(menu) + } + + /** + * MUI Interoperability Interface + */ + internal class MuiIopIf(val renderSystem: RenderSystem, screenManager: ScreenManager, inputSystem: InputSystem) + + internal fun update(muiManager: MuiManager) { + val iopIf = MuiIopIf(muiManager.guiManager.renderSystem, this, muiManager.kuiManager.inputSystem) + menuManager.update(iopIf) + screens.forEach { it.update(iopIf) } + } + + internal fun render(renderSystem: RenderSystem) { + menuManager.render(renderSystem, this) + screenQueue.forEach { it.apply(renderSystemHandle, screens) } + screenQueue.clear() + screens.forEach { it.render(renderSystem, this) } + } +} 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..8b345598 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ComponentEvent.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.event + +sealed interface ComponentEvent { + data object Update : ComponentEvent +} 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..f0c8bf91 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/MenuEvent.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.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/gui/agim/event/ScreenEvent.kt similarity index 56% rename from src/kernel/client/kotlin/net/terramodulus/mui/gms/event/ScreenEvent.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ScreenEvent.kt index ae203cae..b557e4e2 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/event/ScreenEvent.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/ScreenEvent.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.gms.event +package net.terramodulus.mui.gui.agim.event sealed interface ScreenEvent { data object Open : 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..d8fcc68b --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BlankComponent.kt @@ -0,0 +1,16 @@ +/* + * 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 + +/** + * This can act as a placeholder [Component] in a [Layout][net.terramodulus.mui.gui.agim.Layout]. + */ +class BlankComponent : Component() { + override fun render(renderSystem: RenderSystem) {} +} 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..3ddbde54 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CompositeLayout.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.Container +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.gfx.RectangleF + +class CompositeLayout(container: Container) : Layout(container) { + private val layouts = ArrayDeque() + + override val components = layouts.asSequence().flatMap { it.components } + + override fun layout(rect: RectangleF) { + TODO("Not yet implemented") + } +} 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..ac61beaa --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/FlexibleBoxLayout.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.agim.Container +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.gfx.RectangleF + +class FlexibleBoxLayout(container: Container) : Layout(container) { + override val components = 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/GameplayScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt similarity index 95% 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 8e67bbba..7298f5c1 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,7 +3,7 @@ * 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 @@ -25,12 +25,12 @@ import net.terramodulus.engine.SimpleMesh3dGeomCube import net.terramodulus.engine.SimpleMesh3dGeomSphere import net.terramodulus.engine.WorldObjDrawable import net.terramodulus.engine.common.ZeroImmVec3d -import net.terramodulus.mui.gfx.Direction6C -import net.terramodulus.mui.gfx.RenderSystem -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.mui.gui.agim.Component +import net.terramodulus.mui.gui.agim.Screen +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.gfx.Direction6C +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 @@ -55,7 +55,13 @@ 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, + rect: ScreenManager.DelegatedRect, +) : Screen(managerHandle, rect) { private val geoShaders = camera.loadGeoShaders( getResourceAsString("/gwr_geo.vsh"), getResourceAsString("/gwr_geo.fsh"), @@ -324,6 +330,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..5faaa12c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GeomComponent.kt @@ -0,0 +1,16 @@ +/* + * 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.gfx.GuiGeometry +import net.terramodulus.mui.gui.gfx.RenderSystem + +class GeomComponent(val geom: GuiGeometry) : Component() { + 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..f93867ce --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GraphicsComponent.kt @@ -0,0 +1,27 @@ +/* + * 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.gfx.GuiSprite +import net.terramodulus.mui.gui.gfx.RenderSystem + +sealed interface GraphicsComponent + +class SpriteComponent(val sprite: GuiSprite) : Component(), GraphicsComponent { + override fun render(renderSystem: RenderSystem) { + sprite.render(renderSystem) + } +} + +@Suppress("CanSealedSubClassBeObject") +class CanvasComponent(override val layout: Layout) : AbstractPane(), 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/gui/agim/impl/LaunchingScreen.kt similarity index 65% rename from src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/LaunchingScreen.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/LaunchingScreen.kt index 3442eef2..8c4d13cc 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/LaunchingScreen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/LaunchingScreen.kt @@ -1,21 +1,21 @@ /* - * 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.impl +package net.terramodulus.mui.gui.agim.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 +import net.terramodulus.mui.gui.gfx.AlphaFilter +import net.terramodulus.mui.gui.gfx.Dimension2I +import net.terramodulus.mui.gui.gfx.FullScaling +import net.terramodulus.mui.gui.gfx.GuiRect +import net.terramodulus.mui.gui.gfx.GuiSprite +import net.terramodulus.mui.gui.gfx.RectangleI +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.gfx.SmartScaling +import net.terramodulus.mui.gui.agim.Screen +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.kui.InputSystem private val REF_SIZE = Dimension2I(800, 480) @@ -25,7 +25,11 @@ private const val ANI_DURATION = .75F // in second private const val PAUSE_DURATION = 2 // in second -internal class LaunchingScreen(renderSystemHandle: RenderSystem.Handle) : Screen() { +internal class LaunchingScreen( + renderSystemHandle: RenderSystem.Handle, + managerHandle: ScreenManager.Handle, + rect: ScreenManager.DelegatedRect, +) : Screen(managerHandle, rect) { private var stage = 0 private var last = System.currentTimeMillis() // timestamp in milliseconds private var alphaFilter = AlphaFilter(0F) diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/PositioningComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/PositioningComponent.kt new file mode 100644 index 00000000..200850ef --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/PositioningComponent.kt @@ -0,0 +1,15 @@ +/* + * 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 + +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/gui/agim/impl/ResourceLoadingScreen.kt similarity index 76% rename from src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/ResourceLoadingScreen.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ResourceLoadingScreen.kt index 0fb74f48..33ff6af6 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/ResourceLoadingScreen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ResourceLoadingScreen.kt @@ -1,23 +1,23 @@ /* - * 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.impl +package net.terramodulus.mui.gui.agim.impl import net.terramodulus.engine.common.ZeroImmVec3f -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.Rectangle -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 +import net.terramodulus.mui.gui.agim.Screen +import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.gui.gfx.AlphaFilter +import net.terramodulus.mui.gui.gfx.Dimension2I +import net.terramodulus.mui.gui.gfx.FullScaling +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.RectangleI +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.gfx.SmartScaling +import net.terramodulus.mui.kui.InputSystem import kotlin.math.min import kotlin.properties.Delegates @@ -30,7 +30,11 @@ 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() { +class ResourceLoadingScreen( + renderSystemHandle: RenderSystem.Handle, + managerHandle: ScreenManager.Handle, + rect: ScreenManager.DelegatedRect, +) : Screen(managerHandle, rect) { private var stage = 0 private var last = System.currentTimeMillis() // timestamp in milliseconds private var alphaFilter = AlphaFilter(0F) @@ -107,8 +111,4 @@ class ResourceLoadingScreen(renderSystemHandle: RenderSystem.Handle) : Screen() 3 -> screenManager.handle.reset(renderSystem.newGameplayScreen(ZeroImmVec3f)) } } - - 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/gui/agim/impl/SequenceLayout.kt similarity index 90% rename from src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/SequenceLayout.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SequenceLayout.kt index 9d80c12c..0a00cd55 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gms/impl/SequenceLayout.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SequenceLayout.kt @@ -1,14 +1,14 @@ /* - * 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.impl +package net.terramodulus.mui.gui.agim.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 +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.gfx.RectangleF /** * Common implementation that is either [ColumnLayout] or [RowLayout]. 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..ab0881e0 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TitleScreen.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.impl + +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.agim.Screen +import net.terramodulus.mui.gui.agim.ScreenManager + +class TitleScreen( + renderSystemHandle: RenderSystem.Handle, + managerHandle: ScreenManager.Handle, + rect: ScreenManager.DelegatedRect, +) : Screen(managerHandle, rect) { +} 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/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 74% 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..6d2eb829 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,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 data class Dimension2I(val width: Int, val height: Int) 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 92% 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..a583a702 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, 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 90% 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..950c8236 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 diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/GuiSprite.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiSprite.kt similarity index 80% rename from src/kernel/client/kotlin/net/terramodulus/mui/gfx/GuiSprite.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiSprite.kt index d7d7a944..11ba563d 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/GuiSprite.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiSprite.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.SpriteMesh diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ManagedRect.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ManagedRect.kt new file mode 100644 index 00000000..ea1403a9 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ManagedRect.kt @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.gfx + +import kotlin.properties.Delegates.observable + +/** + * AGIM internally Managed Rectangle + */ +abstract class ManagedRect { + abstract val value: RectangleF + + protected val observers = LinkedHashSet<(RectangleF) -> Unit>() + + class Normal(rect: RectangleF) : ManagedRect() { + override var value: RectangleF by observable(rect) { _, _, newValue -> observers.forEach { it(newValue) } } + internal set + } + + internal fun observe(observer: (RectangleF) -> Unit) { + observers.add(observer) + } + + internal fun unobserve(observer: (RectangleF) -> Unit) { + observers.remove(observer) + } +} 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..1dd00547 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ModelTransform.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.gfx + +import net.terramodulus.engine.ModelTransform +import net.terramodulus.engine.SmartScaling + +typealias ModelTransform = ModelTransform + +typealias SmartScaling = 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/gui/gfx/Rectangle.kt similarity index 97% rename from src/kernel/client/kotlin/net/terramodulus/mui/gfx/Rectangle.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt index 93b8081f..7246eecc 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gfx/Rectangle.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.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 com.cout970.math.vec2.ImmVec2f import com.cout970.math.vec2.ImmVec2i 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 81% 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 520c796c..6091ae4c 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,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 com.cout970.math.vec3.Vec3f import net.terramodulus.core.TerraModulus @@ -12,7 +12,7 @@ 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.impl.GameplayScreen class RenderSystem internal constructor(private val core: TerraModulus, private val canvas: Canvas) { val handle: Handle = HandleImpl() @@ -40,8 +40,13 @@ class RenderSystem internal constructor(private val core: TerraModulus, private } } - internal fun newGameplayScreen(pos: Vec3f) = - { it: Handle -> GameplayScreen(core, canvas.createCamera(floatArrayOf(pos.x, pos.y, pos.z)), it) } + internal fun newGameplayScreen(pos: Vec3f) = { it: Handle -> + GameplayScreen( + core, + canvas.createCamera(floatArrayOf(pos.x, pos.y, pos.z)), + it + ) + } 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) + } +} From 323bf3405078442264fb5838e67f612c432f1ccc Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Sun, 3 May 2026 08:18:20 +0800 Subject: [PATCH 07/28] Project: Make Ferricia build task more elegant With cargo build Testing io.github.arc-blroth.cargo-wrapper, but there is a candidate of fr.stardustenterprises.rust.wrapper For asia.hombre.neorust and org.hiero.gradle.feature.rust, they are too sophisticated to be used in our use case --- build.gradle.kts | 91 ++++++++++++++++++++++++++------------------- settings.gradle.kts | 2 + 2 files changed, 55 insertions(+), 38 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index c6ef7aee..56fcc34f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,21 +5,56 @@ plugins { kotlin("jvm") version "2.3.21" kotlin("plugin.serialization") version "2.1.20" id("org.jetbrains.kotlinx.atomicfu") version "0.27.0" + id("io.github.arc-blroth.cargo-wrapper") version "1.0.0" 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 = "io.github.arc-blroth.cargo-wrapper") - repositories { - mavenCentral() + if (providers.gradleProperty("release").isPresent) configure { + profile = "release" // use `-Prelease=true` + } + // somehow, .cargo extension is unusable + configure { + outputs = mapOf("" to System.mapLibraryName("ferricia")) + } + configurations { + create("client") { + configure { + arguments = listOf("-F", "client") + } + } + create("server") { + configure { + arguments = listOf("-F", "server") + } + } + } + artifacts { + add("client", tasks.named("build")) + add("server", tasks.named("build")) } } 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 +96,17 @@ 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") @@ -118,30 +164,6 @@ configure(listOf(project(":kernel:server"), project(":kernel:client"))) { } } -/** Build Ferricia Engine with Cargo */ -tasks.register("cargoBuildClient") { - onlyIf { - !gradle.taskGraph.hasTask(":kernel:server:jar") - } - notCompatibleWithConfigurationCache("complicated") - workingDir = layout.projectDirectory.dir("ferricia").asFile - 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") - } - notCompatibleWithConfigurationCache("complicated") - workingDir = layout.projectDirectory.dir("ferricia").asFile - 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" @@ -159,17 +181,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 { @@ -178,15 +196,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/settings.gradle.kts b/settings.gradle.kts index bf2d7eb6..e2221f71 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,8 @@ rootProject.children.forEach { 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(":")) From 7126aa6bd822342ca7343baf96a3c12ee5a1f6be Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Thu, 7 May 2026 11:09:51 +0800 Subject: [PATCH 08/28] MUI: Try impl more MUI but progress stuck especially on Layout and styles, requiring reviewing CSS specs --- ferricia | 2 +- .../net/terramodulus/engine/ModelTransform.kt | 19 +++++ .../net/terramodulus/engine/ferricia/Mui.kt | 15 ++++ .../terramodulus/mui/gui/agim/Component.kt | 4 +- .../net/terramodulus/mui/gui/agim/Menu.kt | 16 ++++- .../terramodulus/mui/gui/agim/MenuManager.kt | 2 +- .../net/terramodulus/mui/gui/agim/Screen.kt | 5 +- .../mui/gui/agim/ScreenManager.kt | 8 ++- .../mui/gui/agim/asd/AsdManager.kt | 14 ++++ .../mui/gui/agim/asd/MenuStyles.kt | 9 +++ .../mui/gui/agim/asd/ScreenStyles.kt | 9 +++ .../mui/gui/agim/event/ComponentEvent.kt | 5 +- .../mui/gui/agim/event/GenericEvent.kt | 12 ++++ .../mui/gui/agim/event/MenuEvent.kt | 4 ++ .../mui/gui/agim/event/ScreenEvent.kt | 4 +- .../mui/gui/agim/impl/AbsoluteLayout.kt | 47 ++++++++++++ .../mui/gui/agim/impl/CompositeLayout.kt | 6 +- .../mui/gui/agim/impl/LaunchingScreen.kt | 64 ++++++++--------- .../mui/gui/agim/impl/PositioningComponent.kt | 15 ---- .../gui/agim/impl/ResourceLoadingScreen.kt | 2 +- .../mui/gui/agim/impl/ScrollPane.kt | 19 +++++ .../mui/gui/agim/impl/SliderComponent.kt | 71 +++++++++++++++++++ .../net/terramodulus/mui/gui/gfx/Direction.kt | 7 ++ .../net/terramodulus/mui/gui/gfx/Insets.kt | 35 +++++++++ .../terramodulus/mui/gui/gfx/ManagedRect.kt | 6 ++ .../net/terramodulus/mui/gui/gfx/Rectangle.kt | 31 ++++++++ 26 files changed, 370 insertions(+), 61 deletions(-) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/AsdManager.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/MenuStyles.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/ScreenStyles.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvent.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/AbsoluteLayout.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/PositioningComponent.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScrollPane.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SliderComponent.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Insets.kt diff --git a/ferricia b/ferricia index 66bbf397..45d00517 160000 --- a/ferricia +++ b/ferricia @@ -1 +1 @@ -Subproject commit 66bbf397fc38663b797a63f31493149602223434 +Subproject commit 45d005177d84f14300c25a015148b6480336a31d diff --git a/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt b/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt index 556db221..68a9d2b1 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt @@ -5,8 +5,13 @@ package net.terramodulus.engine +import com.cout970.math.vec2.MutVec2d +import com.cout970.math.vec2.Vec2d import net.terramodulus.engine.ferricia.Mui.modelFullScaling +import net.terramodulus.engine.ferricia.Mui.modelGeneralTransform import net.terramodulus.engine.ferricia.Mui.modelSmartScaling +import net.terramodulus.engine.ferricia.Mui.updateGeneralTransform +import kotlin.properties.Delegates @OptIn(ExperimentalUnsignedTypes::class) sealed class ModelTransform(handles: ULongArray) { @@ -14,6 +19,20 @@ sealed class ModelTransform(handles: ULongArray) { internal val wideHandle: ULong = handles[1] } +@OptIn(ExperimentalUnsignedTypes::class) +class GeneralTransform(sx: Double, sy: Double, angle: Double, px: Double, py: Double) : + ModelTransform(modelGeneralTransform(doubleArrayOf(sx, sy, angle, px, py))) { + var scale: Vec2d by Delegates.observable(MutVec2d(sx, sy)) { _, _, new -> + updateGeneralTransform(handle, doubleArrayOf(new.x, new.y, angle, px, py)) + } + var angle: Double by Delegates.observable(angle) { _, _, new -> + updateGeneralTransform(handle, doubleArrayOf(sx, sy, new, px, py)) + } + var pos: Vec2d by Delegates.observable(MutVec2d(px, py)) { _, _, new -> + updateGeneralTransform(handle, doubleArrayOf(sx, sy, angle, new.x, new.y)) + } +} + @OptIn(ExperimentalUnsignedTypes::class) class SmartScaling private constructor(vararg args: Int) : ModelTransform(modelSmartScaling(args)) { 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..2a3100b6 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 @@ -141,6 +142,20 @@ internal object Mui { @JvmName("setGeomPos") external fun setGeomPos(handle: ULong, data: FloatArray) + /** + * @param data `[sx, sy, angle, px, py]`; scaling, rotation, position + * @return GeneralTransform handle pointers + */ + @JvmName("modelGeneralTransform") + external fun modelGeneralTransform(data: DoubleArray): ULongArray + + /** + * @param handle GeneralTransform thin pointer + * @param data `[sx, sy, angle, px, py]`; scaling, rotation, position + */ + @JvmName("updateGeneralTransform") + external fun updateGeneralTransform(handle: ULong, data: DoubleArray) + /** * @param data `[w, h, param, w, h]` * @return SmartScaling handle pointers diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Component.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Component.kt index 9723bf6c..1da300c2 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Component.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Component.kt @@ -22,7 +22,7 @@ abstract class Component { /** * Caveat: This should only be modified by [Layout][net.terramodulus.mui.gui.agim.Layout] managers. */ - open lateinit var rect: ManagedRect + open lateinit var rect: ManagedRect.Normal internal set abstract fun render(renderSystem: RenderSystem) @@ -41,6 +41,6 @@ abstract class Component { } internal open fun update(muiIopIf: ScreenManager.MuiIopIf) { - dispatchEvent(ComponentEvent.Update) + dispatchEvent(ComponentEvent.Update(muiIopIf)) } } 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 index 45bb9e67..5828d4e2 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Menu.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Menu.kt @@ -6,8 +6,10 @@ package net.terramodulus.mui.gui.agim import net.terramodulus.mui.gui.agim.event.MenuEvent +import net.terramodulus.mui.gui.gfx.RenderSystem +import java.io.Closeable -abstract class Menu : Container { +abstract class Menu : Container, Closeable { private val listeners = HashMap, LinkedHashSet<(MenuEvent) -> Unit>>() fun addListener(e: Class, l: (T) -> Unit) { @@ -23,10 +25,20 @@ abstract class Menu : Container { listeners[event.javaClass]?.forEach { it(event) } } - abstract fun render() + internal fun render(renderSystem: RenderSystem) { + layout.components.forEach { it.render(renderSystem) } + } internal fun update(muiIopIf: ScreenManager.MuiIopIf) { + dispatchEvent(MenuEvent.Update(muiIopIf)) layout.update() layout.components.forEach { it.update(muiIopIf) } } + + /** + * 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 index 04ad2c0f..07c9d0f9 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/MenuManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/MenuManager.kt @@ -51,6 +51,6 @@ class MenuManager internal constructor() { } internal fun render(renderSystem: RenderSystem, screenManager: ScreenManager) { - menus.forEach { it.render() } + menus.forEach { it.render(renderSystem) } } } 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 index 245a40c5..58176308 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Screen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Screen.kt @@ -51,6 +51,7 @@ abstract class Screen( } internal fun update(muiIopIf: ScreenManager.MuiIopIf) { + dispatchEvent(ScreenEvent.Update(muiIopIf)) layout.update() layout.components.forEach { it.update(muiIopIf) } } @@ -63,8 +64,8 @@ abstract class Screen( /** * Cleans up and closes any used resources in this session. */ - override fun close() { - listeners[ScreenEvent.Close.javaClass]?.forEach { it(ScreenEvent.Close) } + final override fun close() { + dispatchEvent(ScreenEvent.Close) rect.close() } } 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 index 99e16aa2..f0d8a50e 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt @@ -44,7 +44,7 @@ class ScreenManager internal constructor(window: Window, private val renderSyste val handle: Handle = HandleImpl() init { - screens.add(LaunchingScreen(renderSystemHandle, handle, DelegatedRect())) + screens.add(LaunchingScreen(handle, DelegatedRect(), renderSystemHandle)) } private sealed interface ScreenOperation { @@ -181,7 +181,11 @@ class ScreenManager internal constructor(window: Window, private val renderSyste /** * MUI Interoperability Interface */ - internal class MuiIopIf(val renderSystem: RenderSystem, screenManager: ScreenManager, inputSystem: InputSystem) + class MuiIopIf internal constructor( + val renderSystem: RenderSystem, + val screenManager: ScreenManager, + val inputSystem: InputSystem, + ) internal fun update(muiManager: MuiManager) { val iopIf = MuiIopIf(muiManager.guiManager.renderSystem, this, muiManager.kuiManager.inputSystem) diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/AsdManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/AsdManager.kt new file mode 100644 index 00000000..9361f451 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/AsdManager.kt @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.asd + +// TODO Should ASD affect choices of Layout? +class AsdManager internal constructor() { + companion object { + // TODO temporary demonstrative testing default + internal fun default(): AsdManager = AsdManager() + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/MenuStyles.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/MenuStyles.kt new file mode 100644 index 00000000..9ee4e43e --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/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.agim.asd + +class MenuStyles { +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/ScreenStyles.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/ScreenStyles.kt new file mode 100644 index 00000000..3c6ab931 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/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.agim.asd + +class ScreenStyles { +} 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 index 8b345598..90b80378 100644 --- 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 @@ -5,6 +5,9 @@ package net.terramodulus.mui.gui.agim.event +import net.terramodulus.mui.gui.agim.ScreenManager + sealed interface ComponentEvent { - data object Update : ComponentEvent + data class Update(val muiIopIf: ScreenManager.MuiIopIf) : 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 index f0c8bf91..31316537 100644 --- 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 @@ -5,5 +5,9 @@ package net.terramodulus.mui.gui.agim.event +import net.terramodulus.mui.gui.agim.ScreenManager + sealed interface MenuEvent { + data class Update(val muiIopIf: ScreenManager.MuiIopIf) : 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 index b557e4e2..d829b5de 100644 --- 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 @@ -5,7 +5,9 @@ package net.terramodulus.mui.gui.agim.event +import net.terramodulus.mui.gui.agim.ScreenManager + sealed interface ScreenEvent { - data object Open : ScreenEvent + data class Update(val muiIopIf: ScreenManager.MuiIopIf) : ScreenEvent data object Close : ScreenEvent } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/AbsoluteLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/AbsoluteLayout.kt new file mode 100644 index 00000000..5189436b --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/AbsoluteLayout.kt @@ -0,0 +1,47 @@ +/* + * 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.agim.Container +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.gfx.InsetsD +import net.terramodulus.mui.gui.gfx.InsetsF +import net.terramodulus.mui.gui.gfx.RectangleF + +class AbsoluteLayout(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(container: RectangleF): RectangleF + + data object Full : Config() { + override fun layout(container: RectangleF) = container + } + + data class Insets(var insets: InsetsF) : Config() { + override fun layout(container: RectangleF) = container - insets + } + } + + fun update(component: Component) { + operate { + this@AbsoluteLayout.component = component + } + } + + fun update(operation: (Config) -> Config) { + operate { + config = operation(config) + } + } + + override fun layout(rect: RectangleF) { + component.rect.value = config.layout(rect) + } +} 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 index 3ddbde54..cccd31a7 100644 --- 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 @@ -14,7 +14,11 @@ class CompositeLayout(container: Container) : Layout(container) { override val components = layouts.asSequence().flatMap { it.components } + fun update(operation: ArrayDeque.() -> Unit) { + operate { operation(layouts) } + } + override fun layout(rect: RectangleF) { - TODO("Not yet implemented") + layouts.forEach { it.update() } } } 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 index 8c4d13cc..4ab1b770 100644 --- 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 @@ -5,6 +5,7 @@ package net.terramodulus.mui.gui.agim.impl +import net.terramodulus.engine.GeneralTransform import net.terramodulus.mui.gui.gfx.AlphaFilter import net.terramodulus.mui.gui.gfx.Dimension2I import net.terramodulus.mui.gui.gfx.FullScaling @@ -15,7 +16,7 @@ import net.terramodulus.mui.gui.gfx.RenderSystem import net.terramodulus.mui.gui.gfx.SmartScaling import net.terramodulus.mui.gui.agim.Screen import net.terramodulus.mui.gui.agim.ScreenManager -import net.terramodulus.mui.kui.InputSystem +import net.terramodulus.mui.gui.agim.event.ScreenEvent private val REF_SIZE = Dimension2I(800, 480) @@ -26,19 +27,20 @@ private const val ANI_DURATION = .75F // in second private const val PAUSE_DURATION = 2 // in second internal class LaunchingScreen( - renderSystemHandle: RenderSystem.Handle, managerHandle: ScreenManager.Handle, rect: ScreenManager.DelegatedRect, + renderSystemHandle: RenderSystem.Handle, ) : Screen(managerHandle, rect) { private var stage = 0 private var last = System.currentTimeMillis() // timestamp in milliseconds private var alphaFilter = AlphaFilter(0F) + override val layout = CompositeLayout(this) init { - GeomComponent(GuiRect(0, 0, 800, 480, 37, 198, 196, 255)).apply { - geom.add(alphaFilter) - geom.add(FullScaling(REF_SIZE)) - addComponent(this) + layout.update { + add(AbsoluteLayout(this@LaunchingScreen, GeomComponent(GuiRect(0, 0, 1, 1, 37, 198, 196, 255)).apply { + geom.add(alphaFilter) + }, AbsoluteLayout.Config.Full)) } SpriteComponent(GuiSprite( RectangleI(0, 0, 512, 128), @@ -48,36 +50,34 @@ internal class LaunchingScreen( 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 - } + 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 - } + 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 - } + 2 -> if (elapsed >= ANI_DURATION) { + stage = 3 + last = current + alphaFilter.alpha = 0F + } else { + alphaFilter.alpha = 1 - elapsed / ANI_DURATION + } - 3 -> screenManager.handle.reset(::ResourceLoadingScreen) + 3 -> it.muiIopIf.screenManager.handle.reset(::ResourceLoadingScreen) + } } } - - override fun exit() {} } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/PositioningComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/PositioningComponent.kt deleted file mode 100644 index 200850ef..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/PositioningComponent.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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 - -class PositioningComponent : Component() { - override fun render(renderSystem: RenderSystem) { - TODO("Not yet implemented") - } -} 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 index 33ff6af6..9b92ccee 100644 --- 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 @@ -31,9 +31,9 @@ private const val ANI_DURATION = 1F // in second private const val PAUSE_DURATION = 2F // in second class ResourceLoadingScreen( - renderSystemHandle: RenderSystem.Handle, managerHandle: ScreenManager.Handle, rect: ScreenManager.DelegatedRect, + renderSystemHandle: RenderSystem.Handle, ) : Screen(managerHandle, rect) { private var stage = 0 private var last = System.currentTimeMillis() // timestamp in milliseconds 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..eb972ac5 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScrollPane.kt @@ -0,0 +1,19 @@ +/* + * 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.gfx.RenderSystem + +class ScrollPane : AbstractPane() { + 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/SliderComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SliderComponent.kt new file mode 100644 index 00000000..1e06a869 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SliderComponent.kt @@ -0,0 +1,71 @@ +/* + * 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.Component +import net.terramodulus.mui.gui.gfx.Anchor5 +import net.terramodulus.mui.gui.gfx.Direction4A +import net.terramodulus.mui.gui.gfx.Direction4AD +import net.terramodulus.mui.gui.gfx.GuiRect +import net.terramodulus.mui.gui.gfx.Rectangle +import net.terramodulus.mui.gui.gfx.RectangleF +import net.terramodulus.mui.gui.gfx.RenderSystem +import kotlin.properties.Delegates + +class SliderComponent(val dir: Direction4A) : Component() { + private var bgInit = false + private var fgInit = false + private lateinit var background: GuiRect + private lateinit var bgComponent: GeomComponent + private lateinit var foreground: GuiRect + private lateinit var fgComponent: GeomComponent + var fraction: Float by Delegates.observable(0F) { _, _, value -> + updateForeground(rect.value, value) + } + + init { + rect.observe { + val anchor = it.anchor(Anchor5.TopRight) + if (!bgInit) { + background = GuiRect(it.x.toInt(), it.y.toInt(), anchor.xi, anchor.yi, 255, 255, 255, 255) + bgComponent = GeomComponent(background) + bgInit = true + } else { + background.setPos(it.x.toInt(), it.y.toInt(), anchor.xi, anchor.yi) + } + updateForeground(it, fraction) + } + } + + private fun updateForeground(bounds: RectangleF, fraction: Float) { + // (bounds.x, bounds.y) is the bottom-left anchor + val topRight = bounds.anchor(Anchor5.TopRight) + val rect = when (dir) { + Direction4A.XPos -> // left to right + Rectangle.withDirection(bounds.x, bounds.y, bounds.width * fraction, bounds.height, Direction4AD.QuadOne) + Direction4A.XNeg -> // right to left + Rectangle.withDirection(topRight.x, bounds.y, bounds.width * fraction, bounds.height, Direction4AD.QuadTwo) + Direction4A.YPos -> // bottom to top + Rectangle.withDirection(bounds.x, bounds.y, bounds.width, bounds.height * fraction, Direction4AD.QuadOne) + Direction4A.YNeg -> // top to bottom + Rectangle.withDirection(bounds.x, topRight.y, bounds.width, bounds.height * fraction, Direction4AD.QuadFour) + } + val anchor = rect.anchor(Anchor5.TopRight) + if (!fgInit) { + foreground = GuiRect(rect.x.toInt(), rect.y.toInt(), anchor.xi, anchor.yi, 122, 122, 122, 255) + fgComponent = GeomComponent(foreground) + fgInit = true + } else { + foreground.setPos(rect.x.toInt(), rect.y.toInt(), anchor.xi, anchor.yi) + } + } + + override fun render(renderSystem: RenderSystem) { + rect.value + bgComponent.render(renderSystem) + fgComponent.render(renderSystem) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt index a583a702..1e3aed54 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt @@ -39,6 +39,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/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/ManagedRect.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ManagedRect.kt index ea1403a9..f80d3f3b 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ManagedRect.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ManagedRect.kt @@ -15,9 +15,15 @@ abstract class ManagedRect { protected val observers = LinkedHashSet<(RectangleF) -> Unit>() + internal abstract fun setValue(rect: RectangleF) + class Normal(rect: RectangleF) : ManagedRect() { override var value: RectangleF by observable(rect) { _, _, newValue -> observers.forEach { it(newValue) } } internal set + + override fun setValue(rect: RectangleF) { + value = rect + } } internal fun observe(observer: (RectangleF) -> Unit) { 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 index 7246eecc..daa4c5fe 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt @@ -61,11 +61,26 @@ sealed class Rectangle, N: Number, V: Vec2, D>( } 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 @@ -96,6 +111,22 @@ sealed class Rectangle, N: Number, V: Vec2, D>( 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 toFloat(): RectangleF } From bd224769890b167be3419e98bab53a55034e5d9d Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:17:29 +0800 Subject: [PATCH 09/28] MUI: Updating structures Mostly refer to EFP 9 --- .idea/kotlinc.xml | 3 +- .idea/vcs.xml | 1 + .../net/terramodulus/engine/AsdIntData.kt | 12 ++ .../terramodulus/engine/BaseAsdProcessor.kt | 19 +++ .../kotlin/net/terramodulus/engine/Window.kt | 6 + .../net/terramodulus/mui/gui/GuiManager.kt | 10 +- .../terramodulus/mui/gui/agim/AbstractPane.kt | 17 ++- .../mui/gui/agim/AgimoTreeVisitor.kt | 51 +++++++ .../terramodulus/mui/gui/agim/Component.kt | 19 ++- .../terramodulus/mui/gui/agim/Container.kt | 4 +- .../net/terramodulus/mui/gui/agim/Layout.kt | 24 +-- .../net/terramodulus/mui/gui/agim/Menu.kt | 34 ++++- .../terramodulus/mui/gui/agim/MenuManager.kt | 22 ++- .../net/terramodulus/mui/gui/agim/Screen.kt | 36 +++-- .../mui/gui/agim/ScreenManager.kt | 139 ++++++++++++------ .../mui/gui/agim/asd/AsdManager.kt | 14 -- .../mui/gui/agim/event/ComponentEvent.kt | 2 +- .../mui/gui/agim/event/MenuEvent.kt | 2 +- .../mui/gui/agim/event/ScreenEvent.kt | 2 +- .../mui/gui/agim/impl/AbsoluteLayout.kt | 14 +- .../mui/gui/agim/impl/BlankComponent.kt | 4 +- .../mui/gui/agim/impl/CompositeLayout.kt | 4 +- .../mui/gui/agim/impl/FlexibleBoxLayout.kt | 4 +- .../mui/gui/agim/impl/GameplayScreen.kt | 21 ++- .../mui/gui/agim/impl/GeomComponent.kt | 3 +- .../mui/gui/agim/impl/GraphicsComponent.kt | 5 +- .../mui/gui/agim/impl/LaunchingScreen.kt | 28 ++-- .../net/terramodulus/mui/gui/asd/AsdHandle.kt | 49 ++++++ .../net/terramodulus/mui/gui/asd/AsdIr.kt | 12 ++ .../terramodulus/mui/gui/asd/AsdManager.kt | 41 ++++++ .../terramodulus/mui/gui/asd/AsdProcessor.kt | 24 +++ .../terramodulus/mui/gui/asd/LayoutManager.kt | 10 ++ .../mui/gui/asd/LayoutViewport.kt | 39 +++++ .../mui/gui/{agim => }/asd/MenuStyles.kt | 2 +- .../mui/gui/{agim => }/asd/ScreenStyles.kt | 2 +- .../terramodulus/mui/gui/gfx/ManagedRect.kt | 1 + .../net/terramodulus/mui/gui/gfx/Rectangle.kt | 4 +- 37 files changed, 535 insertions(+), 149 deletions(-) create mode 100644 src/internal/client/kotlin/net/terramodulus/engine/AsdIntData.kt create mode 100644 src/internal/client/kotlin/net/terramodulus/engine/BaseAsdProcessor.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoTreeVisitor.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/AsdManager.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdHandle.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdIr.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdManager.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdProcessor.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/LayoutManager.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/LayoutViewport.kt rename src/kernel/client/kotlin/net/terramodulus/mui/gui/{agim => }/asd/MenuStyles.kt (76%) rename src/kernel/client/kotlin/net/terramodulus/mui/gui/{agim => }/asd/ScreenStyles.kt (77%) 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/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/Window.kt b/src/internal/client/kotlin/net/terramodulus/engine/Window.kt index 677819ad..f21f4dc0 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Window.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Window.kt @@ -30,10 +30,16 @@ class Window( 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) diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt index 0ac450ec..a523fa27 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt @@ -10,6 +10,7 @@ import net.terramodulus.engine.Window import net.terramodulus.mui.MuiManager 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 @@ -20,7 +21,10 @@ private val logger = logger {} */ internal class GuiManager internal constructor(private val window: Window, core: TerraModulus) : Closeable { val renderSystem = RenderSystem(core, window.canvas) - val screenManager = ScreenManager(renderSystem.handle) + val asdManager = AsdManager() + val screenManager = ScreenManager(window, renderSystem.handle, asdManager.AgimHandle()) + + private var proceeded = false /** * Screen updating, targeting as the same as *maximum FPS*, @@ -29,6 +33,10 @@ internal class GuiManager internal constructor(private val window: Window, core: */ internal fun updateScreens(muiManager: MuiManager) { screenManager.update(muiManager) + if (!proceeded) { + asdManager.process() + proceeded = true + } } /** 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 index c0d4f14e..c6f23675 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AbstractPane.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AbstractPane.kt @@ -5,10 +5,19 @@ package net.terramodulus.mui.gui.agim -abstract class AbstractPane : Component(), Container { - final override fun update(muiIopIf: ScreenManager.MuiIopIf) { - super.update(muiIopIf) +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.asd.AsdProcessor +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: RectangleF + 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(muiIopIf) } + layout.components.forEach { it.update(muiIoI) } } } 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..d22cfb66 --- /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

+ } + + protected class RootNode(screens: Sequence, menus: Sequence) { + val screens = ScreenTree(screens) + val menus = MenuTree(menus) + } + + protected abstract class ContainerNode(val layout: Layout) { + val elements = LinkedList(layout.components.map { + if (it is AbstractPane) PaneNode(it) else SimpleComponentNode(it) + }.toList()) + } + + protected sealed interface ComponentNode { + val component: Component + } + + protected class SimpleComponentNode(override val component: Component) : ComponentNode + + protected class PaneNode(override val component: AbstractPane) : ContainerNode(component.layout), ComponentNode + + protected class ScreenTree(screens: Sequence) { + val list = LinkedList(screens.map { ScreenNode(it) }.toList()) + } + + protected class ScreenNode(val screen: Screen) : ContainerNode(screen.layout) { + val menus = MenuTree(screen.visit().visit()) + } + + protected class MenuTree(menus: Sequence) { + val list = LinkedList(menus.map { MenuNode(it) }.toList()) + } + + protected class MenuNode(val menu: Menu) : ContainerNode(menu.layout) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Component.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Component.kt index 1da300c2..f4058b7c 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Component.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Component.kt @@ -6,9 +6,8 @@ package net.terramodulus.mui.gui.agim import net.terramodulus.mui.gui.agim.event.ComponentEvent -import net.terramodulus.mui.gui.gfx.ManagedRect +import net.terramodulus.mui.gui.asd.AsdHandle import net.terramodulus.mui.gui.gfx.RenderSystem -import net.terramodulus.mui.kui.InputSystem /** * [Component] can only be contained by only one [Container][net.terramodulus.mui.gui.agim.Container] at once. @@ -16,14 +15,14 @@ import net.terramodulus.mui.kui.InputSystem * 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>>() - /** - * Caveat: This should only be modified by [Layout][net.terramodulus.mui.gui.agim.Layout] managers. - */ - open lateinit var rect: ManagedRect.Normal - 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) } } - internal open fun update(muiIopIf: ScreenManager.MuiIopIf) { - dispatchEvent(ComponentEvent.Update(muiIopIf)) + 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 index 56687c20..a80e16d8 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Container.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Container.kt @@ -5,13 +5,13 @@ package net.terramodulus.mui.gui.agim -import net.terramodulus.mui.gui.gfx.ManagedRect +import net.terramodulus.mui.gui.asd.AsdHandle /** * **AGIM Container**, direct subclasses are explicitly defined. */ sealed interface Container { - val rect: ManagedRect + val asdHandle: AsdHandle//.Container val layout: Layout } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt index 58c3e16d..431c58dc 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt @@ -5,7 +5,8 @@ package net.terramodulus.mui.gui.agim -import net.terramodulus.mui.gui.gfx.RectangleF +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.RenderSystem import java.io.Closeable import java.util.ArrayDeque @@ -24,7 +25,7 @@ abstract class Layout(private val container: Container) : Closeable { abstract val components: Sequence - private val containerObserver = ::layout.apply(container.rect::observe) + private val containerObserver = { layOut(container.asdHandle) }.apply(container.asdHandle::observe) private val layoutOperations = ArrayDeque() @@ -44,30 +45,35 @@ abstract class Layout(private val container: Container) : Closeable { /** * Updates the layout output using pending operations added via [operate], - * and the resultant layout configurations by invoking [layout] internally if any operation exists. + * and the resultant layout configurations by invoking [layOut] internally if any operation exists. */ fun update() { val nonEmpty = layoutOperations.isNotEmpty() while (layoutOperations.isNotEmpty()) { with(layoutOperations.removeFirst()) { this@Layout.operate() } } - if (nonEmpty) layout(container.rect.value) + if (nonEmpty) layOut(container.asdHandle) } /** * 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. + * @param handle the `LayoutHandle` of the Container */ - protected abstract fun layout(rect: RectangleF) + protected abstract fun layOut(handle: AsdHandle) + + /** + * Renders this [Layout] with underlying managed [components]. + */ + internal fun render(renderSystem: RenderSystem) = components.forEach { it.render(renderSystem) } /** * Must be invoked when this [Layout] is no longer in use. */ fun clear() { // Not sure whether there is the necessity to separate this from [close]. - container.rect.unobserve(containerObserver) + container.asdHandle.unobserve(containerObserver) } override fun close() { 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 index 5828d4e2..009d36bf 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Menu.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Menu.kt @@ -6,11 +6,18 @@ 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.RectangleF import net.terramodulus.mui.gui.gfx.RenderSystem import java.io.Closeable -abstract class Menu : Container, 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") @@ -25,14 +32,31 @@ abstract class Menu : Container, Closeable { 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: RectangleF + override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdHandle.registerAsdProcessor(processor) + } + internal fun render(renderSystem: RenderSystem) { - layout.components.forEach { it.render(renderSystem) } + layout.render(renderSystem) } - internal fun update(muiIopIf: ScreenManager.MuiIopIf) { - dispatchEvent(MenuEvent.Update(muiIopIf)) + internal fun update(muiIoI: ScreenManager.MuiIoI) { + dispatchEvent(MenuEvent.Update(muiIoI)) layout.update() - layout.components.forEach { it.update(muiIopIf) } + layout.components.forEach { it.update(muiIoI) } } /** 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 index 07c9d0f9..70870f85 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/MenuManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/MenuManager.kt @@ -5,10 +5,13 @@ 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.RectangleF import net.terramodulus.mui.gui.gfx.RenderSystem import java.util.ArrayDeque -class MenuManager internal constructor() { +class MenuManager internal constructor(private val asdHandle: (AsdProcessor<*>) -> Unit) { private val menus = LinkedHashSet() private val menuQueue = ArrayDeque() val handle: Handle = HandleImpl() @@ -30,14 +33,14 @@ class MenuManager internal constructor() { } sealed interface Handle { - fun addMenu(menu: () -> Menu) + fun addMenu(menu: (Handle, AsdHandle) -> Menu) fun removeMenu(menu: Menu) } private inner class HandleImpl : Handle { - override fun addMenu(menu: () -> Menu) { - menuQueue.add(MenuOperation.Add(menu)) + override fun addMenu(menu: (Handle, AsdHandle) -> Menu) { + menuQueue.add(MenuOperation.Add { menu(handle, MenuAsdHandleImpl()) }) } override fun removeMenu(menu: Menu) { @@ -45,7 +48,12 @@ class MenuManager internal constructor() { } } - internal fun update(muiIopIf: ScreenManager.MuiIopIf) { + private inner class MenuAsdHandleImpl : AsdHandle.Menu() { + override lateinit var rect: RectangleF + override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdHandle(processor) + } + + internal fun update(muiIoI: ScreenManager.MuiIoI) { menuQueue.forEach { it.apply(menus) } menuQueue.clear() } @@ -53,4 +61,8 @@ class MenuManager internal constructor() { 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 index 58176308..441b618f 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Screen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Screen.kt @@ -6,17 +6,25 @@ 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.RectangleF import net.terramodulus.mui.gui.gfx.RenderSystem import java.io.Closeable abstract class Screen( managerHandle: ScreenManager.Handle, - final override val rect: ScreenManager.DelegatedRect + final override val asdHandle: AsdHandle.Container ) : Container, Closeable { private val listeners = HashMap, LinkedHashSet<(ScreenEvent) -> Unit>>() - private val menuManager = MenuManager() + 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) @@ -31,34 +39,39 @@ abstract class Screen( } sealed interface Handle { - fun addMenu(menu: () -> Menu) + fun addMenu(menu: (MenuManager.Handle, AsdHandle) -> Menu) fun removeMenu(menu: Menu) - fun addTopMenu(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: () -> Menu) = menuManager.handle.addMenu(menu) + 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: () -> Menu) = managerHandle.addMenu(menu) + override fun addTopMenu(menu: (MenuManager.Handle, AsdHandle) -> Menu) = managerHandle.addMenu(menu) override fun removeTopMenu(menu: Menu) = managerHandle.removeMenu(menu) } - internal fun update(muiIopIf: ScreenManager.MuiIopIf) { - dispatchEvent(ScreenEvent.Update(muiIopIf)) + protected inner class ComponentAsdHandleImpl : AsdHandle.Container() { + override lateinit var rect: RectangleF + 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(muiIopIf) } + layout.components.forEach { it.update(muiIoI) } } internal fun render(renderSystem: RenderSystem, screenManager: ScreenManager) { + layout.render(renderSystem) menuManager.render(renderSystem, screenManager) - layout.components.forEach { it.render(renderSystem) } } /** @@ -66,6 +79,7 @@ abstract class Screen( */ final override fun close() { dispatchEvent(ScreenEvent.Close) - rect.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 index f0d8a50e..520ae322 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt @@ -9,46 +9,51 @@ import net.terramodulus.engine.Window import net.terramodulus.mui.MuiManager import net.terramodulus.mui.gui.gfx.RenderSystem import net.terramodulus.mui.gui.agim.impl.LaunchingScreen -import net.terramodulus.mui.gui.gfx.ManagedRect +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.RectangleF import net.terramodulus.mui.kui.InputSystem -import java.io.Closeable -import kotlin.properties.Delegates -class ScreenManager internal constructor(window: Window, private val renderSystemHandle: RenderSystem.Handle) { +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 screenQueue = ArrayDeque() - private val menuManager = MenuManager() - private val viewportRect = ManagedRect.Normal(RectangleF(0F, 0F, window.width.toFloat(), window.height.toFloat())) + private val screenOpQueue = ArrayDeque() + private val menuManager = MenuManager(asdManagerHandle::registerProcessor) - inner class DelegatedRect : ManagedRect(), Closeable { - override var value: RectangleF by Delegates.observable(viewportRect.value) { _, _, newValue -> - observers.forEach { it(newValue) } - } - private set - - private val listener: (RectangleF) -> Unit = { rect -> value = rect } - - init { - viewportRect.observe(listener) - } + private val asdHandles = HashSet() + private var viewportRect = RectangleF(0F, 0F, window.width.toFloat(), window.height.toFloat()) - override fun close() { - viewportRect.unobserve(listener) + init { + window.addListener { w, h -> + viewportRect = RectangleF(0F, 0F, w.toFloat(), h.toFloat()) + asdHandles.forEach { it.triggerObservers() } // 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, DelegatedRect(), renderSystemHandle)) + screens.add(LaunchingScreen(handle, ScreenAsdHandleImpl(), renderSystemHandle)) } private sealed interface ScreenOperation { - fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) + fun apply(handle: RenderSystem.Handle, screens: ArrayDeque, asdHandles: HashSet) /** * Exits `n` times @@ -61,13 +66,20 @@ class ScreenManager internal constructor(window: Window, private val renderSyste require(n < 0) { "`n` < 1" } } - override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { + 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().close() + screens.removeLast().apply { + close() + asdHandles.remove(asdHandle) + } } } } @@ -76,7 +88,11 @@ class ScreenManager internal constructor(window: Window, private val renderSyste * Opens the `screen` */ class Open(val screen: (RenderSystem.Handle) -> Screen) : ScreenOperation { - override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { + override fun apply( + handle: RenderSystem.Handle, + screens: ArrayDeque, + asdHandles: HashSet + ) { screens.addLast(screen(handle)) } } @@ -85,7 +101,11 @@ class ScreenManager internal constructor(window: Window, private val renderSyste * 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) { + override fun apply( + handle: RenderSystem.Handle, + screens: ArrayDeque, + asdHandles: HashSet + ) { screens.add(screens.lastIndexOf(target), screen(handle)) } } @@ -94,7 +114,11 @@ class ScreenManager internal constructor(window: Window, private val renderSyste * Exits until reaching the `screen` then remains on the `screen` */ class ExitTo(val screen: Screen) : ScreenOperation { - override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { + override fun apply( + handle: RenderSystem.Handle, + screens: ArrayDeque, + asdHandles: HashSet + ) { val it = screens.asReversed().listIterator() while (it.hasNext()) { val e = it.next() @@ -103,6 +127,7 @@ class ScreenManager internal constructor(window: Window, private val renderSyste } else { it.remove() e.close() + asdHandles.remove(e.asdHandle) } } } @@ -112,9 +137,14 @@ class ScreenManager internal constructor(window: Window, private val renderSyste * Clears [screens] then opens the `screen` */ class Reset(val screen: (RenderSystem.Handle) -> Screen) : ScreenOperation { - override fun apply(handle: RenderSystem.Handle, screens: ArrayDeque) { + override fun apply( + handle: RenderSystem.Handle, + screens: ArrayDeque, + asdHandles: HashSet + ) { screens.asReversed().forEach { it.close() } screens.clear() + asdHandles.clear() screens.add(screen(handle)) } } @@ -129,13 +159,13 @@ class ScreenManager internal constructor(window: Window, private val renderSyste /** * @see ScreenOperation.Open */ - fun open(screen: (Handle, DelegatedRect, RenderSystem.Handle) -> Screen) + 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, DelegatedRect, RenderSystem.Handle) -> Screen) + fun openBefore(target: Screen, screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) /** * @see ScreenOperation.ExitTo @@ -145,58 +175,73 @@ class ScreenManager internal constructor(window: Window, private val renderSyste /** * @see ScreenOperation.Reset */ - fun reset(screen: (Handle, DelegatedRect, RenderSystem.Handle) -> Screen) + fun reset(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) - fun addMenu(menu: () -> Menu) + fun addMenu(menu: (MenuManager.Handle, AsdHandle) -> Menu) fun removeMenu(menu: Menu) } private inner class HandleImpl : Handle { override fun exit(n: Int) { - screenQueue.add(ScreenOperation.Exit(n)) + screenOpQueue.add(ScreenOperation.Exit(n)) } - override fun open(screen: (Handle, DelegatedRect, RenderSystem.Handle) -> Screen) { - screenQueue.add(ScreenOperation.Open { screen(handle, DelegatedRect(), it) }) + 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, DelegatedRect, RenderSystem.Handle) -> Screen) { - screenQueue.add(ScreenOperation.OpenBefore(target) { screen(handle, DelegatedRect(), 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) { - screenQueue.add(ScreenOperation.ExitTo(screen)) + screenOpQueue.add(ScreenOperation.ExitTo(screen)) } - override fun reset(screen: (Handle, DelegatedRect, RenderSystem.Handle) -> Screen) { - screenQueue.add(ScreenOperation.Reset { screen(handle, DelegatedRect(), it) }) + override fun reset(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) { + screenOpQueue.add(ScreenOperation.Reset { screen(handle, ScreenAsdHandleImpl(), it) }) } - override fun addMenu(menu: () -> Menu) = menuManager.handle.addMenu(menu) + 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) + } + + 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 MuiIopIf internal constructor( + class MuiIoI internal constructor( val renderSystem: RenderSystem, val screenManager: ScreenManager, val inputSystem: InputSystem, ) internal fun update(muiManager: MuiManager) { - val iopIf = MuiIopIf(muiManager.guiManager.renderSystem, this, muiManager.kuiManager.inputSystem) - menuManager.update(iopIf) - screens.forEach { it.update(iopIf) } + screenOpQueue.forEach { it.apply(renderSystemHandle, screens, asdHandles) } + screenOpQueue.clear() + 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) - screenQueue.forEach { it.apply(renderSystemHandle, screens) } - screenQueue.clear() screens.forEach { it.render(renderSystem, this) } } + + internal fun visit() = AgimoTreeVisitor.ScreenTreeVisitor { + screens.asSequence() + } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/AsdManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/AsdManager.kt deleted file mode 100644 index 9361f451..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/AsdManager.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gui.agim.asd - -// TODO Should ASD affect choices of Layout? -class AsdManager internal constructor() { - companion object { - // TODO temporary demonstrative testing default - internal fun default(): AsdManager = AsdManager() - } -} 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 index 90b80378..bd5ddd73 100644 --- 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 @@ -8,6 +8,6 @@ package net.terramodulus.mui.gui.agim.event import net.terramodulus.mui.gui.agim.ScreenManager sealed interface ComponentEvent { - data class Update(val muiIopIf: ScreenManager.MuiIopIf) : 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/MenuEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/MenuEvent.kt index 31316537..0e22e339 100644 --- 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 @@ -8,6 +8,6 @@ package net.terramodulus.mui.gui.agim.event import net.terramodulus.mui.gui.agim.ScreenManager sealed interface MenuEvent { - data class Update(val muiIopIf: ScreenManager.MuiIopIf) : 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 index d829b5de..bf79e7cd 100644 --- 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 @@ -8,6 +8,6 @@ package net.terramodulus.mui.gui.agim.event import net.terramodulus.mui.gui.agim.ScreenManager sealed interface ScreenEvent { - data class Update(val muiIopIf: ScreenManager.MuiIopIf) : 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/AbsoluteLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/AbsoluteLayout.kt index 5189436b..920bdda1 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/AbsoluteLayout.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/AbsoluteLayout.kt @@ -8,9 +8,10 @@ package net.terramodulus.mui.gui.agim.impl 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.gfx.InsetsD +import net.terramodulus.mui.gui.asd.AsdHandle import net.terramodulus.mui.gui.gfx.InsetsF import net.terramodulus.mui.gui.gfx.RectangleF +import net.terramodulus.mui.gui.gfx.RenderSystem class AbsoluteLayout(container: Container, component: Component, private var config: Config) : Layout(container) { override val components = componentsSequence(::component) @@ -18,14 +19,14 @@ class AbsoluteLayout(container: Container, component: Component, private var con private set sealed class Config private constructor() { - abstract fun layout(container: RectangleF): RectangleF + abstract fun layOut(container: RectangleF): RectangleF data object Full : Config() { - override fun layout(container: RectangleF) = container + override fun layOut(container: RectangleF) = container } data class Insets(var insets: InsetsF) : Config() { - override fun layout(container: RectangleF) = container - insets + override fun layOut(container: RectangleF) = container - insets } } @@ -41,7 +42,8 @@ class AbsoluteLayout(container: Container, component: Component, private var con } } - override fun layout(rect: RectangleF) { - component.rect.value = config.layout(rect) + override fun layOut(handle: AsdHandle) { + component.asdHandle.rect = config.layOut(handle.rect) + component.asdHandle.triggerObservers() } } 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 index d8fcc68b..88a6d0f3 100644 --- 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 @@ -7,10 +7,12 @@ 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 : Component() { +class BlankComponent(asdHandle: AsdHandle, dim: Dimension2F) : Component(asdHandle) { override fun render(renderSystem: RenderSystem) {} } 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 index cccd31a7..ff26b95b 100644 --- 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 @@ -7,7 +7,7 @@ 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.gfx.RectangleF +import net.terramodulus.mui.gui.asd.AsdHandle class CompositeLayout(container: Container) : Layout(container) { private val layouts = ArrayDeque() @@ -18,7 +18,7 @@ class CompositeLayout(container: Container) : Layout(container) { operate { operation(layouts) } } - override fun layout(rect: RectangleF) { + override fun layOut(handle: AsdHandle) { layouts.forEach { it.update() } } } 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 index ac61beaa..e981ca09 100644 --- 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 @@ -7,12 +7,12 @@ 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.gfx.RectangleF +import net.terramodulus.mui.gui.asd.AsdHandle class FlexibleBoxLayout(container: Container) : Layout(container) { override val components = TODO("Not yet implemented") - override fun layout(rect: RectangleF) { + override fun layOut(handle: AsdHandle) { TODO("Not yet implemented") } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt index 7298f5c1..2a12dbfe 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt @@ -28,7 +28,11 @@ 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 @@ -60,19 +64,23 @@ internal class GameplayScreen( private val camera: Camera3D, renderSystemHandle: RenderSystem.Handle, managerHandle: ScreenManager.Handle, - rect: ScreenManager.DelegatedRect, -) : Screen(managerHandle, rect) { + asdHandle: AsdHandle.Container, +) : Screen(managerHandle, asdHandle) { private val geoShaders = camera.loadGeoShaders( getResourceAsString("/gwr_geo.vsh"), getResourceAsString("/gwr_geo.fsh"), ) private lateinit var player: PlayerVoidGeom + override val layout: AbsoluteLayout init { renderSystemHandle.setBackgroundColor(0F, 0F, 0F, 0F) core.world = World(Ymir()) - addComponent(GameplayRenderer()) + layout = AbsoluteLayout(this, GameplayRenderer(), AbsoluteLayout.Config.Full) + 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) @@ -183,7 +191,8 @@ internal class GameplayScreen( 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 @@ -320,8 +329,8 @@ internal class GameplayScreen( Direction6C.entries.forEach { if (inputSystem.condition { it.toKey().down() }) dirs.add(it.toVector()) } 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 } 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 index 5faaa12c..865c06a2 100644 --- 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 @@ -6,10 +6,11 @@ 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.GuiGeometry import net.terramodulus.mui.gui.gfx.RenderSystem -class GeomComponent(val geom: GuiGeometry) : Component() { +class GeomComponent(val geom: GuiGeometry, asdHandle: AsdHandle) : Component(asdHandle) { 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 index f93867ce..d2d8077f 100644 --- 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 @@ -8,19 +8,20 @@ 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.GuiSprite import net.terramodulus.mui.gui.gfx.RenderSystem sealed interface GraphicsComponent -class SpriteComponent(val sprite: GuiSprite) : Component(), GraphicsComponent { +class SpriteComponent(val sprite: GuiSprite, asdHandle: AsdHandle) : Component(asdHandle), GraphicsComponent { override fun render(renderSystem: RenderSystem) { sprite.render(renderSystem) } } @Suppress("CanSealedSubClassBeObject") -class CanvasComponent(override val layout: Layout) : AbstractPane(), GraphicsComponent { +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/LaunchingScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/LaunchingScreen.kt index 4ab1b770..dfe83d18 100644 --- 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 @@ -5,10 +5,8 @@ package net.terramodulus.mui.gui.agim.impl -import net.terramodulus.engine.GeneralTransform import net.terramodulus.mui.gui.gfx.AlphaFilter import net.terramodulus.mui.gui.gfx.Dimension2I -import net.terramodulus.mui.gui.gfx.FullScaling import net.terramodulus.mui.gui.gfx.GuiRect import net.terramodulus.mui.gui.gfx.GuiSprite import net.terramodulus.mui.gui.gfx.RectangleI @@ -17,6 +15,7 @@ import net.terramodulus.mui.gui.gfx.SmartScaling 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 private val REF_SIZE = Dimension2I(800, 480) @@ -28,9 +27,9 @@ private const val PAUSE_DURATION = 2 // in second internal class LaunchingScreen( managerHandle: ScreenManager.Handle, - rect: ScreenManager.DelegatedRect, + asdHandle: AsdHandle.Container, renderSystemHandle: RenderSystem.Handle, -) : Screen(managerHandle, rect) { +) : Screen(managerHandle, asdHandle) { private var stage = 0 private var last = System.currentTimeMillis() // timestamp in milliseconds private var alphaFilter = AlphaFilter(0F) @@ -38,17 +37,20 @@ internal class LaunchingScreen( init { layout.update { - add(AbsoluteLayout(this@LaunchingScreen, GeomComponent(GuiRect(0, 0, 1, 1, 37, 198, 196, 255)).apply { + add(AbsoluteLayout(this@LaunchingScreen, GeomComponent(GuiRect( + 0, 0, 1, 1, 37, 198, 196, 255 + ), ComponentAsdHandleImpl()).apply { geom.add(alphaFilter) }, AbsoluteLayout.Config.Full)) } - 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) + layout.update { + add(AbsoluteLayout(this@LaunchingScreen, SpriteComponent(GuiSprite( + RectangleI(0, 0, 512, 128), + renderSystemHandle.loadTexture("/studio_logo.png"), + ), ComponentAsdHandleImpl()).apply { + sprite.add(alphaFilter) + sprite.add(SmartScaling.both(REF_SIZE.width, REF_SIZE.height, 512, 128)) + }, AbsoluteLayout.Config.Full)) } addListener(ScreenEvent.Update::class.java) { @@ -76,7 +78,7 @@ internal class LaunchingScreen( alphaFilter.alpha = 1 - elapsed / ANI_DURATION } - 3 -> it.muiIopIf.screenManager.handle.reset(::ResourceLoadingScreen) + 3 -> it.muiIoI.screenManager.handle.reset(::ResourceLoadingScreen) } } } 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..4551f541 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdHandle.kt @@ -0,0 +1,49 @@ +/* + * 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.gfx.RectangleF + +/** + * 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, [triggerObservers] must be invoked. + */ + abstract var rect: RectangleF + internal set + + protected val observers = LinkedHashSet<() -> Unit>() + + internal fun observe(observer: () -> Unit) { + observers.add(observer) + } + + internal fun unobserve(observer: () -> Unit) { + observers.remove(observer) + } + + internal fun triggerObservers() { + observers.forEach { it() } + } + + /** + * 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/LayoutManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/LayoutManager.kt new file mode 100644 index 00000000..7a39def6 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/LayoutManager.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.asd + +internal class LayoutManager { + val viewport = LayoutViewport() +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/LayoutViewport.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/LayoutViewport.kt new file mode 100644 index 00000000..d9e25012 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/LayoutViewport.kt @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.asd + +import java.util.LinkedList + +// TODO Do we really need this? +// Maybe somehow this could become/transform into a visitor helper? +class LayoutViewport { + private val treeRoot = RootNode() + + private open class Node { + /** + * Should be read-only outside this class scope; + * modifications and state management should be managed within this class scope + */ + val children = LinkedList() + + open class Child(val parent: Node) : Node() + } + + private class RootNode : Node() { + val screens = ScreenTree(this) + val menus = MenuTree(this) + } + + private open class TopContainerNode(parent: Node) : Node.Child(parent) { + + } + + private class ScreenTree(parent: Node) : Node.Child(parent) { + val menus = MenuTree(this) + } + + private class MenuTree(parent: Node) : Node.Child(parent) {} +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/MenuStyles.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/MenuStyles.kt similarity index 76% rename from src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/MenuStyles.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/MenuStyles.kt index 9ee4e43e..0022ce5d 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/MenuStyles.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/MenuStyles.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gui.agim.asd +package net.terramodulus.mui.gui.asd class MenuStyles { } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/ScreenStyles.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/ScreenStyles.kt similarity index 77% rename from src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/ScreenStyles.kt rename to src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/ScreenStyles.kt index 3c6ab931..76571c6e 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/asd/ScreenStyles.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/ScreenStyles.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-3.0-only */ -package net.terramodulus.mui.gui.agim.asd +package net.terramodulus.mui.gui.asd class ScreenStyles { } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ManagedRect.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ManagedRect.kt index f80d3f3b..f661b451 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ManagedRect.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ManagedRect.kt @@ -15,6 +15,7 @@ abstract class ManagedRect { protected val observers = LinkedHashSet<(RectangleF) -> Unit>() + // What is this? internal abstract fun setValue(rect: RectangleF) class Normal(rect: RectangleF) : ManagedRect() { 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 index daa4c5fe..2a009c07 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt @@ -145,7 +145,7 @@ data class RectangleI( 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 @@ -175,7 +175,7 @@ data class RectangleF( 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 From 84fb0c33175120b9150aad0a5234e595f0ce0067 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Sun, 16 Aug 2026 13:06:00 +0800 Subject: [PATCH 10/28] MUI: Updating structures --- .../net/terramodulus/engine/ModelTransform.kt | 20 ---- .../net/terramodulus/engine/ferricia/Mui.kt | 21 ---- .../mui/gui/agim/AgimoProperty.kt | 12 ++ .../terramodulus/mui/gui/agim/Alignment.kt | 33 ++++++ .../net/terramodulus/mui/gui/agim/Layout.kt | 4 +- .../mui/gui/agim/ScreenManager.kt | 2 +- .../mui/gui/agim/impl/AbsoluteLayout.kt | 3 +- .../mui/gui/agim/impl/IntrinsicProperties.kt | 30 +++++ .../mui/gui/agim/impl/ObjectFitLayout.kt | 104 +++++++++++++++++ .../mui/gui/agim/impl/ScaledLayout.kt | 108 ++++++++++++++++++ .../mui/gui/agim/impl/SequenceLayout.kt | 5 +- .../net/terramodulus/mui/gui/asd/AsdHandle.kt | 105 +++++++++++++++-- .../net/terramodulus/mui/gui/gfx/Dimension.kt | 2 + .../net/terramodulus/mui/gui/gfx/Rectangle.kt | 49 ++++++-- .../kotlin/net/terramodulus/util/Math.kt | 22 ++++ 15 files changed, 456 insertions(+), 64 deletions(-) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoProperty.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Alignment.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/IntrinsicProperties.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ObjectFitLayout.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScaledLayout.kt create mode 100644 src/kernel/common/kotlin/net/terramodulus/util/Math.kt diff --git a/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt b/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt index 68a9d2b1..f4b8ffe5 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt @@ -7,9 +7,7 @@ package net.terramodulus.engine import com.cout970.math.vec2.MutVec2d import com.cout970.math.vec2.Vec2d -import net.terramodulus.engine.ferricia.Mui.modelFullScaling import net.terramodulus.engine.ferricia.Mui.modelGeneralTransform -import net.terramodulus.engine.ferricia.Mui.modelSmartScaling import net.terramodulus.engine.ferricia.Mui.updateGeneralTransform import kotlin.properties.Delegates @@ -32,21 +30,3 @@ class GeneralTransform(sx: Double, sy: Double, angle: Double, px: Double, py: Do updateGeneralTransform(handle, doubleArrayOf(sx, sy, angle, new.x, new.y)) } } - -@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) - - fun both(w: Int, h: Int, ww: Int, hh: Int) = SmartScaling(w, h, 3, ww, hh) - } -} - -@OptIn(ExperimentalUnsignedTypes::class) -class FullScaling(w: Int, h: Int) : ModelTransform(modelFullScaling(intArrayOf(w, h))) 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 2a3100b6..f42a5dc0 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt @@ -156,27 +156,6 @@ internal object Mui { @JvmName("updateGeneralTransform") external fun updateGeneralTransform(handle: ULong, data: DoubleArray) - /** - * @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 - */ - @JvmName("modelFullScaling") - external fun modelFullScaling(data: IntArray): ULongArray - - /** - * @param data `[x, y]` - * @return SimpleTranslation handle pointers - */ - @JvmName("modelSimpleTranslation") - external fun modelSimpleTranslation(data: FloatArray): ULongArray - /** * @param data alpha * @return AlphaFilter handle pointers 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..22766af0 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoProperty.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 + +/** + * Instances of this class must be immutable, as simple values; + * may be data classes, data objects, enums. + */ +abstract class AgimoProperty 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/gui/agim/Layout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt index 431c58dc..2b19b2aa 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt @@ -25,7 +25,7 @@ abstract class Layout(private val container: Container) : Closeable { abstract val components: Sequence - private val containerObserver = { layOut(container.asdHandle) }.apply(container.asdHandle::observe) + private val containerObserver = { layOut(container.asdHandle) }.apply(container.asdHandle::observeRect) private val layoutOperations = ArrayDeque() @@ -73,7 +73,7 @@ abstract class Layout(private val container: Container) : Closeable { * Must be invoked when this [Layout] is no longer in use. */ fun clear() { // Not sure whether there is the necessity to separate this from [close]. - container.asdHandle.unobserve(containerObserver) + container.asdHandle.unobserveRect(containerObserver) } override fun close() { 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 index 520ae322..7c218b78 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt @@ -33,7 +33,7 @@ class ScreenManager internal constructor( init { window.addListener { w, h -> viewportRect = RectangleF(0F, 0F, w.toFloat(), h.toFloat()) - asdHandles.forEach { it.triggerObservers() } // already referring viewportRect + asdHandles.forEach { it.triggerRectObservers() } // already referring viewportRect } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/AbsoluteLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/AbsoluteLayout.kt index 920bdda1..b80d95ba 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/AbsoluteLayout.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/AbsoluteLayout.kt @@ -11,7 +11,6 @@ import net.terramodulus.mui.gui.agim.Layout import net.terramodulus.mui.gui.asd.AsdHandle import net.terramodulus.mui.gui.gfx.InsetsF import net.terramodulus.mui.gui.gfx.RectangleF -import net.terramodulus.mui.gui.gfx.RenderSystem class AbsoluteLayout(container: Container, component: Component, private var config: Config) : Layout(container) { override val components = componentsSequence(::component) @@ -44,6 +43,6 @@ class AbsoluteLayout(container: Container, component: Component, private var con override fun layOut(handle: AsdHandle) { component.asdHandle.rect = config.layOut(handle.rect) - component.asdHandle.triggerObservers() + component.asdHandle.triggerRectObservers() } } 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..abd0e86d --- /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.asd.AsdHandle +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 = AsdHandle.PropertyKey(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 = AsdHandle.PropertyKey(IntrinsicDimensionsProperty::class.java) + } + fun computeRatio() = IntrinsicRatioProperty.compute(width, height) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ObjectFitLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ObjectFitLayout.kt new file mode 100644 index 00000000..ef64f4ba --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ObjectFitLayout.kt @@ -0,0 +1,104 @@ +/* + * 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.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.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Dimension2D +import net.terramodulus.mui.gui.gfx.RectangleF +import kotlin.math.max +import kotlin.math.min + +/** + * Only applicable to AGIMO elements with intrinsic ratios. + */ +class ObjectFitLayout( + container: Container, + component: Component, + private var mode: ModeConfig, + private var alignment: AlignmentConfig, +) : Layout(container) { + override val components = componentsSequence(::component) + var component = component + private set + + // TODO should Fill be implemented? + enum class ModeConfig { + Contain { + override fun compute(container: RectangleF, component: IntrinsicRatioProperty): Dimension2D { + val w = container.width.toDouble() / component.width.toDouble() + val h = container.height.toDouble() / component.height.toDouble() + val scale = min(w, h) + return Dimension2D( + component.width.toDouble() * scale, + component.height.toDouble() * scale, + ) + } + }, + Cover { + override fun compute(container: RectangleF, component: IntrinsicRatioProperty): Dimension2D { + val w = container.width.toDouble() / component.width.toDouble() + val h = container.height.toDouble() / component.height.toDouble() + val scale = max(w, h) + return Dimension2D( + component.width.toDouble() * scale, + component.height.toDouble() * scale, + ) + } + }, + ; + + abstract fun compute(container: RectangleF, component: IntrinsicRatioProperty): Dimension2D + } + + /** + * 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@ObjectFitLayout.component = component + } + } + + interface Config { + var mode: ModeConfig + var alignment: AlignmentConfig + } + + fun update(operation: Config.() -> Unit) { + operate { + operation(object : Config { + override var mode: ModeConfig by this@ObjectFitLayout::mode + override var alignment: AlignmentConfig by this@ObjectFitLayout::alignment + }) + } + } + + private operator fun RectangleF.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(handle: AsdHandle) { + val ratio = component.asdHandle.getProperty(IntrinsicRatioProperty.KEY) + if (ratio === null) + throw IllegalStateException("component has no intrinsic ratio") + val target = mode.compute(handle.rect, ratio) + component.asdHandle.rect = AnchorAlignmentHelper.Subject(handle.rect.toDouble(), handle.rect * alignment) + .alignTarget(AnchorAlignmentHelper.Target(target, target * alignment)).toFloat() + component.asdHandle.triggerRectObservers() + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScaledLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScaledLayout.kt new file mode 100644 index 00000000..bf01443e --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScaledLayout.kt @@ -0,0 +1,108 @@ +/* + * 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.Component +import net.terramodulus.mui.gui.agim.Container +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Dimension2D + +/** + * Scaling intrinsic dimensions of AGIMO element for intrinsic dimensions for this object. + */ +class ScaledLayout( + container: Container, + component: Component, + private var config: Config, +) : Layout(container) { + override val components = componentsSequence(::component) + var component = component + private set + + sealed class Config { + abstract fun compute(dim: IntrinsicDimensionsProperty): Dimension2D + + /** + * Scale both dimensions by the same scaling + * @param scale `> 0` + */ + data class Scale(val scale: Double) : Config() { + 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) : Config() { + 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)) + } + } + + fun update(component: Component) { + operate { + this@ScaledLayout.component = component + } + } + + fun update(operation: (Config) -> Config) { + operate { + config = operation(config) + } + } + + override fun layOut(handle: AsdHandle) { + val dim = component.asdHandle.getProperty(IntrinsicDimensionsProperty.KEY) + if (dim === null) + throw IllegalStateException("component has no intrinsic dimensions") + config.compute(dim) + TODO("Then?") + } +} 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 index 0a00cd55..1c22a726 100644 --- 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 @@ -8,6 +8,7 @@ package net.terramodulus.mui.gui.agim.impl 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.asd.AsdHandle import net.terramodulus.mui.gui.gfx.RectangleF /** @@ -59,7 +60,7 @@ class ColumnLayout private constructor(container: Container, elements: ElementLi } } - override fun layout(rect: RectangleF) { + override fun layOut(handle: AsdHandle) { elements.forEach { TODO() } } } @@ -87,7 +88,7 @@ class RowLayout private constructor(container: Container, elements: ElementList< } } - override fun layout(rect: RectangleF) { + override fun layOut(handle: AsdHandle) { elements.forEach { TODO() } } } 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 index 4551f541..adfe77f2 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdHandle.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdHandle.kt @@ -5,7 +5,10 @@ package net.terramodulus.mui.gui.asd +import net.terramodulus.mui.gui.agim.AgimoProperty 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, @@ -14,23 +17,109 @@ import net.terramodulus.mui.gui.gfx.RectangleF abstract class AsdHandle internal constructor() { /** * Caveat: Must only be modified by [Layout][net.terramodulus.mui.gui.agim.Layout]. - * When modified, [triggerObservers] must be invoked. + * When modified, [triggerRectObservers] must be invoked. */ abstract var rect: RectangleF internal set - protected val observers = LinkedHashSet<() -> Unit>() + protected val rectObservers = LinkedHashSet<() -> Unit>() - internal fun observe(observer: () -> Unit) { - observers.add(observer) + internal fun observeRect(observer: () -> Unit) { + rectObservers.add(observer) } - internal fun unobserve(observer: () -> Unit) { - observers.remove(observer) + internal fun unobserveRect(observer: () -> Unit) { + rectObservers.remove(observer) } - internal fun triggerObservers() { - observers.forEach { it() } + internal fun triggerRectObservers() { + rectObservers.forEach { it() } + } + + protected val properties = TypedAnchorMap() + protected val propertyObservers = HashMap, + LinkedHashSet<(AgimoProperty?, AgimoProperty?) -> Unit>>() + + class PropertyKey(c: Class) : TypedAnchorMap.Key(c) { + override fun hashCode() = type.hashCode() + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is PropertyKey<*>) return false + return type == other.type + } + } + + fun T.getAsdPropertyKey() = PropertyKey(javaClass) + + @Suppress("UNCHECKED_CAST") + fun getProperty(key: PropertyKey) = properties.getTyped(key) as T? + + fun putProperty(key: PropertyKey, value: T) { + @Suppress("UNCHECKED_CAST") + val old = properties.putTyped(key, value) as T? + triggerPropertyObservers(key, old, value) + } + + @Suppress("UNCHECKED_CAST") + fun removeProperty(key: PropertyKey): T? { + val old = properties.remove(key) as T? + triggerPropertyObservers(key, old, null) + return old + } + + fun containsProperty(key: PropertyKey) = properties.containsKey(key) + + /** + * @see java.util.Map.compute + */ + inline fun computeProperty(key: PropertyKey, remappingFunction: (PropertyKey, 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: PropertyKey, mappingFunction: (PropertyKey) -> T?) { + if (!containsProperty(key)) { + val v = mappingFunction(key) + if (v !== null) putProperty(key, v) + } + } + + /** + * @see java.util.Map.computeIfPresent + */ + inline fun computePropertyIfPresent(key: PropertyKey, remappingFunction: (PropertyKey, 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: PropertyKey, 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: PropertyKey, l: (T?, T?) -> Unit) { + @Suppress("UNCHECKED_CAST") + propertyObservers.computeIfAbsent(key) { LinkedHashSet() }.add(l as (AgimoProperty?, AgimoProperty?) -> Unit) + } + + fun unobserveProperty(key: PropertyKey, l: (T?, T?) -> Unit) { + propertyObservers[key]?.remove(l) + } + + protected fun triggerPropertyObservers(key: PropertyKey, old: T?, new: T?) { + propertyObservers[key]?.forEach { it(old, new) } } /** diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Dimension.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Dimension.kt index 6d2eb829..ede9a02c 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Dimension.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Dimension.kt @@ -9,6 +9,8 @@ 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/gui/gfx/Rectangle.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt index 2a009c07..6ba2783c 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt @@ -5,12 +5,18 @@ 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, @@ -127,13 +133,11 @@ sealed class Rectangle, N: Number, V: Vec2, D>( height - other.bottom - other.top, ) + abstract fun toInt(): RectangleI abstract fun toFloat(): RectangleF + abstract fun toDouble(): RectangleD } -/** - * 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( override val x: Int, override val y: Int, @@ -152,13 +156,11 @@ data class RectangleI( 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()) } -/** - * 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( override val x: Float, override val y: Float, @@ -181,5 +183,36 @@ data class RectangleF( 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/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) +} From 5c4c705fef91b3eb203147b3acbf885062b54a81 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:54:08 +0800 Subject: [PATCH 11/28] MUI: Restructuring AGIMO/Layout & ASD --- .../net/terramodulus/mui/gui/GuiManager.kt | 2 + .../mui/gui/agim/AgimoProperty.kt | 86 ++++++++++ .../mui/gui/agim/AgimoTreeVisitor.kt | 18 +-- .../net/terramodulus/mui/gui/agim/Layout.kt | 11 +- .../mui/gui/agim/LayoutComputation.kt | 45 ++++++ .../terramodulus/mui/gui/agim/LayoutHandle.kt | 16 ++ .../mui/gui/agim/LayoutManager.kt | 153 ++++++++++++++++++ .../mui/gui/agim/ScreenManager.kt | 4 +- .../mui/gui/agim/impl/ScrollPane.kt | 3 +- .../mui/gui/agim/impl/SimplePane.kt | 17 ++ .../mui/gui/agim/impl/SingletonLayout.kt | 108 +++++++++++++ .../net/terramodulus/mui/gui/asd/AsdHandle.kt | 86 +--------- .../terramodulus/mui/gui/asd/LayoutManager.kt | 10 -- .../mui/gui/asd/LayoutViewport.kt | 39 ----- .../kotlin/net/terramodulus/util/TypedMap.kt | 41 +++++ 15 files changed, 493 insertions(+), 146 deletions(-) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutComputation.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutHandle.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SimplePane.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SingletonLayout.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/LayoutManager.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/LayoutViewport.kt create mode 100644 src/kernel/common/kotlin/net/terramodulus/util/TypedMap.kt diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt index a523fa27..d6ca96da 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt @@ -8,6 +8,7 @@ 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 @@ -23,6 +24,7 @@ internal class GuiManager internal constructor(private val window: Window, core: val renderSystem = RenderSystem(core, window.canvas) val asdManager = AsdManager() val screenManager = ScreenManager(window, renderSystem.handle, asdManager.AgimHandle()) + val layoutManager = LayoutManager() private var proceeded = false 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 index 22766af0..107b6360 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoProperty.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoProperty.kt @@ -5,8 +5,94 @@ 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> +} 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 index d22cfb66..d6232bdc 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoTreeVisitor.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoTreeVisitor.kt @@ -16,36 +16,36 @@ internal abstract class AgimoTreeVisitor { fun visit(): Sequence } - protected class RootNode(screens: Sequence, menus: Sequence) { + class RootNode(screens: Sequence, menus: Sequence) { val screens = ScreenTree(screens) val menus = MenuTree(menus) } - protected abstract class ContainerNode(val layout: Layout) { + sealed class ContainerNode(val layout: Layout) { val elements = LinkedList(layout.components.map { if (it is AbstractPane) PaneNode(it) else SimpleComponentNode(it) }.toList()) } - protected sealed interface ComponentNode { + sealed interface ComponentNode { val component: Component } - protected class SimpleComponentNode(override val component: Component) : ComponentNode + class SimpleComponentNode(override val component: Component) : ComponentNode - protected class PaneNode(override val component: AbstractPane) : ContainerNode(component.layout), ComponentNode + class PaneNode(override val component: AbstractPane) : ContainerNode(component.layout), ComponentNode - protected class ScreenTree(screens: Sequence) { + class ScreenTree(screens: Sequence) { val list = LinkedList(screens.map { ScreenNode(it) }.toList()) } - protected class ScreenNode(val screen: Screen) : ContainerNode(screen.layout) { + class ScreenNode(val screen: Screen) : ContainerNode(screen.layout) { val menus = MenuTree(screen.visit().visit()) } - protected class MenuTree(menus: Sequence) { + class MenuTree(menus: Sequence) { val list = LinkedList(menus.map { MenuNode(it) }.toList()) } - protected class MenuNode(val menu: Menu) : ContainerNode(menu.layout) + class MenuNode(val menu: Menu) : ContainerNode(menu.layout) } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt index 2b19b2aa..bfe498cc 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt @@ -52,17 +52,22 @@ abstract class Layout(private val container: Container) : Closeable { while (layoutOperations.isNotEmpty()) { with(layoutOperations.removeFirst()) { this@Layout.operate() } } - if (nonEmpty) layOut(container.asdHandle) + 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. * * 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. - * @param handle the `LayoutHandle` of the Container */ - protected abstract fun layOut(handle: AsdHandle) + protected abstract fun layOut(handle: LayoutHandle): LayoutComputationGroup /** * Renders this [Layout] with underlying managed [components]. 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..09b2f3f6 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutComputation.kt @@ -0,0 +1,45 @@ +/* + * 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: Map>>.() -> Unit, + results: Map>>.() -> Unit, + noinline computation: LayoutHandle.() -> Map, + ) = LayoutComputationUnit(computation, + mutableMapOf>>().apply(dependencies), + mutableMapOf>>().apply(results), + ) + } +} + +class LayoutComputationGroup @PublishedApi internal constructor( + val conditions: LayoutHandle.() -> Set, + val dependencies: Map>>, +) { + companion object { + inline operator fun invoke( + dependencies: Map>>.() -> Unit, + 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..61f4f26c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutHandle.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 + +import net.terramodulus.mui.gui.asd.AsdHandle + +sealed interface LayoutHandle { + fun getUnit(handle: AsdHandle): Unit + + sealed interface Unit { + val properties: Map, AgimoProperty> + } +} 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..eb001d00 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt @@ -0,0 +1,153 @@ +/* + * 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 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 { 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, + var group: LayoutComputationGroup, + 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 , E> InstancedPropertyMap +// .push(handle: AsdHandle, key: AgimoPropertyMap.Key<*>, value: E) = +// push(handle, key, value) { mutableSetOf() as MutableCollection } + + private inner class LayoutStates { + private val layouts = mutableMapOf() + private val groupDeps: InstancedPropertyMap> = mutableMapOf() + private val unitDeps: InstancedPropertyMap> = mutableMapOf() + private val groups = mutableMapOf>() + private val results = 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 + } + + private fun addAffectedProp(handle: AsdHandle, key: AgimoPropertyMap.Key<*>) { + affectedProps.computeIfAbsent(handle) { mutableSetOf() }.add(key) + } + + 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() + groups.remove(layout.group) + layout.group.dependencies.forEach { (handle, keys) -> + keys.forEach { groupDeps[handle, it]?.remove(layout.group) } + } + layout.units.forEach { unit -> + unit.dependencies.forEach { (handle, keys) -> + keys.forEach { unitDeps[handle, it]?.remove(unit) } + } + changedUnits.add(unit) + } + } + } + + internal fun tick() { + val changes = agimoTree.discoverLayoutChanges() + + } +} 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 index 7c218b78..829703cd 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt @@ -241,7 +241,9 @@ class ScreenManager internal constructor( screens.forEach { it.render(renderSystem, this) } } - internal fun visit() = AgimoTreeVisitor.ScreenTreeVisitor { + internal fun visitScreens() = AgimoTreeVisitor.ScreenTreeVisitor { screens.asSequence() } + + internal fun visitMenus() = menuManager.visit() } 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 index eb972ac5..73587106 100644 --- 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 @@ -7,9 +7,10 @@ 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 : AbstractPane() { +class ScrollPane(asdHandle: AsdHandle) : AbstractPane(asdHandle) { override fun render(renderSystem: RenderSystem) { TODO("Not yet implemented") } 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..4137b804 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SimplePane.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.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, override var layout: Layout) : AbstractPane(asdHandle) { + 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..4b7e5bfd --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SingletonLayout.kt @@ -0,0 +1,108 @@ +/* + * 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.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.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Dimension2D +import net.terramodulus.mui.gui.gfx.InsetsF +import net.terramodulus.mui.gui.gfx.RectangleF +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(container: RectangleF): RectangleF + + sealed class Absolute private constructor() : Config() { + data object Full : Config() { + override fun layOut(container: RectangleF) = container + } + + data class Insets(var insets: InsetsF) : Config() { + override fun layOut(container: RectangleF) = container - insets + } + } + + enum class ObjectFit { + Contain { + override fun compute(container: RectangleF, component: IntrinsicRatioProperty): Dimension2D { + val w = container.width.toDouble() / component.width.toDouble() + val h = container.height.toDouble() / component.height.toDouble() + val scale = min(w, h) + return Dimension2D( + component.width.toDouble() * scale, + component.height.toDouble() * scale, + ) + } + }, + Cover { + override fun compute(container: RectangleF, component: IntrinsicRatioProperty): Dimension2D { + val w = container.width.toDouble() / component.width.toDouble() + val h = container.height.toDouble() / component.height.toDouble() + val scale = max(w, h) + return Dimension2D( + component.width.toDouble() * scale, + component.height.toDouble() * scale, + ) + } + }, + ; + + abstract fun compute(container: RectangleF, component: IntrinsicRatioProperty): Dimension2D + + private operator fun RectangleF.times(other: ObjectFitLayout.AlignmentConfig) = ImmVec2d(width * other.x, height * other.y) + private operator fun Dimension2D.times(other: ObjectFitLayout.AlignmentConfig) = ImmVec2d(width * other.x, height * other.y) + + override fun layOut(handle: AsdHandle) { + val ratio = component.asdHandle.getProperty(IntrinsicRatioProperty.KEY) + if (ratio === null) + throw IllegalStateException("component has no intrinsic ratio") + val target = mode.compute(handle.rect, ratio) + component.asdHandle.rect = AnchorAlignmentHelper.Subject(handle.rect.toDouble(), handle.rect * alignment) + .alignTarget(AnchorAlignmentHelper.Target(target, target * alignment)).toFloat() + component.asdHandle.triggerRectObservers() + } + } + + /** + * 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 + } + } + + fun update(operation: (Config) -> Config) { + operate { + config = operation(config) + } + } + + override fun layOut(handle: AsdHandle) { + component.asdHandle.rect = config.layOut(handle.rect) + component.asdHandle.triggerRectObservers() + } +} 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 index adfe77f2..cadc2284 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdHandle.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdHandle.kt @@ -6,6 +6,7 @@ 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.RectangleF import net.terramodulus.util.TypedAnchorMap import java.util.function.BiFunction @@ -36,91 +37,10 @@ abstract class AsdHandle internal constructor() { rectObservers.forEach { it() } } - protected val properties = TypedAnchorMap() - protected val propertyObservers = HashMap, - LinkedHashSet<(AgimoProperty?, AgimoProperty?) -> Unit>>() - - class PropertyKey(c: Class) : TypedAnchorMap.Key(c) { - override fun hashCode() = type.hashCode() - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is PropertyKey<*>) return false - return type == other.type - } - } - - fun T.getAsdPropertyKey() = PropertyKey(javaClass) - - @Suppress("UNCHECKED_CAST") - fun getProperty(key: PropertyKey) = properties.getTyped(key) as T? - - fun putProperty(key: PropertyKey, value: T) { - @Suppress("UNCHECKED_CAST") - val old = properties.putTyped(key, value) as T? - triggerPropertyObservers(key, old, value) - } - - @Suppress("UNCHECKED_CAST") - fun removeProperty(key: PropertyKey): T? { - val old = properties.remove(key) as T? - triggerPropertyObservers(key, old, null) - return old - } - - fun containsProperty(key: PropertyKey) = properties.containsKey(key) - /** - * @see java.util.Map.compute + * Permanent AGIMO Properties */ - inline fun computeProperty(key: PropertyKey, remappingFunction: (PropertyKey, 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: PropertyKey, mappingFunction: (PropertyKey) -> T?) { - if (!containsProperty(key)) { - val v = mappingFunction(key) - if (v !== null) putProperty(key, v) - } - } - - /** - * @see java.util.Map.computeIfPresent - */ - inline fun computePropertyIfPresent(key: PropertyKey, remappingFunction: (PropertyKey, 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: PropertyKey, 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: PropertyKey, l: (T?, T?) -> Unit) { - @Suppress("UNCHECKED_CAST") - propertyObservers.computeIfAbsent(key) { LinkedHashSet() }.add(l as (AgimoProperty?, AgimoProperty?) -> Unit) - } - - fun unobserveProperty(key: PropertyKey, l: (T?, T?) -> Unit) { - propertyObservers[key]?.remove(l) - } - - protected fun triggerPropertyObservers(key: PropertyKey, old: T?, new: T?) { - propertyObservers[key]?.forEach { it(old, new) } - } + val properties = AgimoPropertyMap() /** * Registers ASD Processors from AGIMOs to [AsdManager]. diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/LayoutManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/LayoutManager.kt deleted file mode 100644 index 7a39def6..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/LayoutManager.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gui.asd - -internal class LayoutManager { - val viewport = LayoutViewport() -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/LayoutViewport.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/LayoutViewport.kt deleted file mode 100644 index d9e25012..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/LayoutViewport.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gui.asd - -import java.util.LinkedList - -// TODO Do we really need this? -// Maybe somehow this could become/transform into a visitor helper? -class LayoutViewport { - private val treeRoot = RootNode() - - private open class Node { - /** - * Should be read-only outside this class scope; - * modifications and state management should be managed within this class scope - */ - val children = LinkedList() - - open class Child(val parent: Node) : Node() - } - - private class RootNode : Node() { - val screens = ScreenTree(this) - val menus = MenuTree(this) - } - - private open class TopContainerNode(parent: Node) : Node.Child(parent) { - - } - - private class ScreenTree(parent: Node) : Node.Child(parent) { - val menus = MenuTree(this) - } - - private class MenuTree(parent: Node) : Node.Child(parent) {} -} 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 + } + } +} From b0ceba16844e0f56422ebeb0ee8ad07d49755060 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:59:37 +0800 Subject: [PATCH 12/28] MUI: Restructuring AGIMO/Layout & ASD --- .../mui/gui/agim/AgimoProperty.kt | 5 + .../net/terramodulus/mui/gui/agim/Layout.kt | 15 +- .../mui/gui/agim/LayoutComputation.kt | 8 +- .../mui/gui/agim/impl/AbsoluteLayout.kt | 48 ---- .../mui/gui/agim/impl/CompositeLayout.kt | 11 +- .../mui/gui/agim/impl/FlexibleBoxLayout.kt | 4 +- .../mui/gui/agim/impl/GameplayScreen.kt | 6 +- .../mui/gui/agim/impl/IntrinsicProperties.kt | 6 +- .../mui/gui/agim/impl/LaunchingScreen.kt | 24 +- .../mui/gui/agim/impl/ObjectFitLayout.kt | 104 -------- .../mui/gui/agim/impl/RectangleProperty.kt | 16 ++ .../mui/gui/agim/impl/ScaledLayout.kt | 108 -------- .../mui/gui/agim/impl/SequenceLayout.kt | 75 ++++-- .../mui/gui/agim/impl/SingletonLayout.kt | 233 +++++++++++++++--- .../net/terramodulus/mui/gui/gfx/Direction.kt | 4 + .../mui/gui/gfx/ModelTransform.kt | 7 - 16 files changed, 313 insertions(+), 361 deletions(-) delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/AbsoluteLayout.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ObjectFitLayout.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/RectangleProperty.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScaledLayout.kt 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 index 107b6360..b1939d3a 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoProperty.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AgimoProperty.kt @@ -95,4 +95,9 @@ class AgimoPropertyMap { @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/Layout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt index bfe498cc..d504356f 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Layout.kt @@ -5,7 +5,6 @@ package net.terramodulus.mui.gui.agim -import net.terramodulus.mui.gui.asd.AsdHandle import net.terramodulus.mui.gui.gfx.RenderSystem import java.io.Closeable import java.util.ArrayDeque @@ -16,7 +15,7 @@ import java.util.ArrayDeque * **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) : Closeable { +abstract class Layout(protected val container: Container) : Closeable { companion object { const val ALIGN_START = 0F const val ALIGN_CENTER = .5F @@ -25,8 +24,6 @@ abstract class Layout(private val container: Container) : Closeable { abstract val components: Sequence - private val containerObserver = { layOut(container.asdHandle) }.apply(container.asdHandle::observeRect) - private val layoutOperations = ArrayDeque() fun interface Operation { @@ -67,7 +64,9 @@ abstract class Layout(private val container: Container) : Closeable { * 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): LayoutComputationGroup + protected abstract fun layOut(handle: LayoutHandle): Sequence + + internal fun layOutInternal(handle: LayoutHandle) = layOut(handle) /** * Renders this [Layout] with underlying managed [components]. @@ -78,7 +77,7 @@ abstract class Layout(private val container: Container) : Closeable { * Must be invoked when this [Layout] is no longer in use. */ fun clear() { // Not sure whether there is the necessity to separate this from [close]. - container.asdHandle.unobserveRect(containerObserver) +// container.asdHandle.unobserveRect(containerObserver) } override fun close() { @@ -126,8 +125,8 @@ abstract class Layout(private val container: Container) : Closeable { } abstract class ElementGroup protected constructor( - container: Container, - protected val elements: ElementList, + container: Container, + protected open val elements: ElementList, ) : Group(container) { final override val components = elements.componentsView.asSequence() 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 index 09b2f3f6..d57d985a 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutComputation.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutComputation.kt @@ -14,8 +14,8 @@ class LayoutComputationUnit @PublishedApi internal constructor( ) { companion object { inline operator fun invoke( - dependencies: Map>>.() -> Unit, - results: Map>>.() -> Unit, + dependencies: MutableMap>>.() -> Unit, + results: MutableMap>>.() -> Unit, noinline computation: LayoutHandle.() -> Map, ) = LayoutComputationUnit(computation, mutableMapOf>>().apply(dependencies), @@ -26,11 +26,11 @@ class LayoutComputationUnit @PublishedApi internal constructor( class LayoutComputationGroup @PublishedApi internal constructor( val conditions: LayoutHandle.() -> Set, - val dependencies: Map>>, + val dependencies: MutableMap>>, ) { companion object { inline operator fun invoke( - dependencies: Map>>.() -> Unit, + dependencies: MutableMap>>.() -> Unit, noinline conditions: LayoutHandle.() -> Set, ) = LayoutComputationGroup(conditions, mutableMapOf>>().apply(dependencies), diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/AbsoluteLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/AbsoluteLayout.kt deleted file mode 100644 index b80d95ba..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/AbsoluteLayout.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.agim.Container -import net.terramodulus.mui.gui.agim.Layout -import net.terramodulus.mui.gui.asd.AsdHandle -import net.terramodulus.mui.gui.gfx.InsetsF -import net.terramodulus.mui.gui.gfx.RectangleF - -class AbsoluteLayout(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(container: RectangleF): RectangleF - - data object Full : Config() { - override fun layOut(container: RectangleF) = container - } - - data class Insets(var insets: InsetsF) : Config() { - override fun layOut(container: RectangleF) = container - insets - } - } - - fun update(component: Component) { - operate { - this@AbsoluteLayout.component = component - } - } - - fun update(operation: (Config) -> Config) { - operate { - config = operation(config) - } - } - - override fun layOut(handle: AsdHandle) { - component.asdHandle.rect = config.layOut(handle.rect) - component.asdHandle.triggerRectObservers() - } -} 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 index ff26b95b..105f5e12 100644 --- 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 @@ -7,6 +7,8 @@ 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) { @@ -15,10 +17,11 @@ class CompositeLayout(container: Container) : Layout(container) { override val components = layouts.asSequence().flatMap { it.components } fun update(operation: ArrayDeque.() -> Unit) { - operate { operation(layouts) } + operate { + operation(layouts) + layouts.forEach { it.update() } + } } - override fun layOut(handle: AsdHandle) { - 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 index e981ca09..1758d165 100644 --- 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 @@ -7,12 +7,14 @@ 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: AsdHandle) { + override fun layOut(handle: LayoutHandle): Sequence { TODO("Not yet implemented") } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt index 2a12dbfe..6dd069a1 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt @@ -72,12 +72,12 @@ internal class GameplayScreen( ) private lateinit var player: PlayerVoidGeom - override val layout: AbsoluteLayout + override val layout: SingletonLayout init { renderSystemHandle.setBackgroundColor(0F, 0F, 0F, 0F) core.world = World(Ymir()) - layout = AbsoluteLayout(this, GameplayRenderer(), AbsoluteLayout.Config.Full) + layout = SingletonLayout(this, GameplayRenderer(), SingletonLayout.Config.Absolute.Full) addListener(ScreenEvent.Update::class.java) { update0(it.muiIoI) } @@ -329,7 +329,7 @@ internal class GameplayScreen( Direction6C.entries.forEach { if (inputSystem.condition { it.toKey().down() }) dirs.add(it.toVector()) } player.move(dirs.fold(ZeroImmVec3d, Vec3d::plus)) } - + private inner class GameplayRenderer : Component(ComponentAsdHandleImpl()) { override fun render(renderSystem: RenderSystem) { if (core.world != null) core.world!!.objects.values.sortedWith( 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 index abd0e86d..c7948845 100644 --- 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 @@ -6,7 +6,7 @@ package net.terramodulus.mui.gui.agim.impl import net.terramodulus.mui.gui.agim.AgimoProperty -import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.agim.AgimoPropertyMap import net.terramodulus.util.gcd /** @@ -14,7 +14,7 @@ import net.terramodulus.util.gcd */ data class IntrinsicRatioProperty(val width: UInt, val height: UInt) : AgimoProperty() { companion object { - val KEY = AsdHandle.PropertyKey(IntrinsicRatioProperty::class.java) + 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) @@ -24,7 +24,7 @@ data class IntrinsicRatioProperty(val width: UInt, val height: UInt) : AgimoProp class IntrinsicDimensionsProperty(val width: UInt, val height: UInt) : AgimoProperty() { companion object { - val KEY = AsdHandle.PropertyKey(IntrinsicDimensionsProperty::class.java) + 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 index dfe83d18..9488e503 100644 --- 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 @@ -5,19 +5,15 @@ 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.Dimension2I import net.terramodulus.mui.gui.gfx.GuiRect import net.terramodulus.mui.gui.gfx.GuiSprite import net.terramodulus.mui.gui.gfx.RectangleI import net.terramodulus.mui.gui.gfx.RenderSystem -import net.terramodulus.mui.gui.gfx.SmartScaling -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 - -private val REF_SIZE = Dimension2I(800, 480) private val BG_COLOR = floatArrayOf(.145F, .776F, .768F) @@ -37,20 +33,22 @@ internal class LaunchingScreen( init { layout.update { - add(AbsoluteLayout(this@LaunchingScreen, GeomComponent(GuiRect( + add(SingletonLayout(this@LaunchingScreen, GeomComponent(GuiRect( 0, 0, 1, 1, 37, 198, 196, 255 ), ComponentAsdHandleImpl()).apply { geom.add(alphaFilter) - }, AbsoluteLayout.Config.Full)) + }, SingletonLayout.Config.Absolute.Full)) } layout.update { - add(AbsoluteLayout(this@LaunchingScreen, SpriteComponent(GuiSprite( + add(SingletonLayout(this@LaunchingScreen, SpriteComponent(GuiSprite( RectangleI(0, 0, 512, 128), renderSystemHandle.loadTexture("/studio_logo.png"), ), ComponentAsdHandleImpl()).apply { sprite.add(alphaFilter) - sprite.add(SmartScaling.both(REF_SIZE.width, REF_SIZE.height, 512, 128)) - }, AbsoluteLayout.Config.Full)) + }, SingletonLayout.Config.Aligned( + SingletonLayout.Config.ObjectFit.Contain, + SingletonLayout.Config.AlignmentConfig.DEFAULT, + ))) } addListener(ScreenEvent.Update::class.java) { diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ObjectFitLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ObjectFitLayout.kt deleted file mode 100644 index ef64f4ba..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ObjectFitLayout.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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.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.asd.AsdHandle -import net.terramodulus.mui.gui.gfx.Dimension2D -import net.terramodulus.mui.gui.gfx.RectangleF -import kotlin.math.max -import kotlin.math.min - -/** - * Only applicable to AGIMO elements with intrinsic ratios. - */ -class ObjectFitLayout( - container: Container, - component: Component, - private var mode: ModeConfig, - private var alignment: AlignmentConfig, -) : Layout(container) { - override val components = componentsSequence(::component) - var component = component - private set - - // TODO should Fill be implemented? - enum class ModeConfig { - Contain { - override fun compute(container: RectangleF, component: IntrinsicRatioProperty): Dimension2D { - val w = container.width.toDouble() / component.width.toDouble() - val h = container.height.toDouble() / component.height.toDouble() - val scale = min(w, h) - return Dimension2D( - component.width.toDouble() * scale, - component.height.toDouble() * scale, - ) - } - }, - Cover { - override fun compute(container: RectangleF, component: IntrinsicRatioProperty): Dimension2D { - val w = container.width.toDouble() / component.width.toDouble() - val h = container.height.toDouble() / component.height.toDouble() - val scale = max(w, h) - return Dimension2D( - component.width.toDouble() * scale, - component.height.toDouble() * scale, - ) - } - }, - ; - - abstract fun compute(container: RectangleF, component: IntrinsicRatioProperty): Dimension2D - } - - /** - * 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@ObjectFitLayout.component = component - } - } - - interface Config { - var mode: ModeConfig - var alignment: AlignmentConfig - } - - fun update(operation: Config.() -> Unit) { - operate { - operation(object : Config { - override var mode: ModeConfig by this@ObjectFitLayout::mode - override var alignment: AlignmentConfig by this@ObjectFitLayout::alignment - }) - } - } - - private operator fun RectangleF.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(handle: AsdHandle) { - val ratio = component.asdHandle.getProperty(IntrinsicRatioProperty.KEY) - if (ratio === null) - throw IllegalStateException("component has no intrinsic ratio") - val target = mode.compute(handle.rect, ratio) - component.asdHandle.rect = AnchorAlignmentHelper.Subject(handle.rect.toDouble(), handle.rect * alignment) - .alignTarget(AnchorAlignmentHelper.Target(target, target * alignment)).toFloat() - component.asdHandle.triggerRectObservers() - } -} 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/ScaledLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScaledLayout.kt deleted file mode 100644 index bf01443e..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScaledLayout.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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.Component -import net.terramodulus.mui.gui.agim.Container -import net.terramodulus.mui.gui.agim.Layout -import net.terramodulus.mui.gui.asd.AsdHandle -import net.terramodulus.mui.gui.gfx.Dimension2D - -/** - * Scaling intrinsic dimensions of AGIMO element for intrinsic dimensions for this object. - */ -class ScaledLayout( - container: Container, - component: Component, - private var config: Config, -) : Layout(container) { - override val components = componentsSequence(::component) - var component = component - private set - - sealed class Config { - abstract fun compute(dim: IntrinsicDimensionsProperty): Dimension2D - - /** - * Scale both dimensions by the same scaling - * @param scale `> 0` - */ - data class Scale(val scale: Double) : Config() { - 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) : Config() { - 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)) - } - } - - fun update(component: Component) { - operate { - this@ScaledLayout.component = component - } - } - - fun update(operation: (Config) -> Config) { - operate { - config = operation(config) - } - } - - override fun layOut(handle: AsdHandle) { - val dim = component.asdHandle.getProperty(IntrinsicDimensionsProperty.KEY) - if (dim === null) - throw IllegalStateException("component has no intrinsic dimensions") - config.compute(dim) - TODO("Then?") - } -} 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 index 1c22a726..a9b8c233 100644 --- 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 @@ -8,8 +8,13 @@ package net.terramodulus.mui.gui.agim.impl 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.RectangleF +import net.terramodulus.mui.gui.gfx.Anchor5 +import net.terramodulus.mui.gui.gfx.Direction2 /** * Common implementation that is either [ColumnLayout] or [RowLayout]. @@ -17,14 +22,33 @@ import net.terramodulus.mui.gui.gfx.RectangleF * 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) { +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) = @@ -40,36 +64,51 @@ sealed class SequenceLayout(container: Container, elements: ElementList /** * **Column** case of [SequenceLayout]. */ -class ColumnLayout private constructor(container: Container, elements: ElementList) : - SequenceLayout(container, elements) { +class ColumnLayout private constructor(container: Container, elements: ElementList, config: Config) : + SequenceLayout(container, elements, config) { companion object { - fun withComponents(vararg components: Component) = { it: Container -> - ColumnLayout(it, ElementList.withComponentsDefault(Element::default, *components)) + fun withComponents(vararg components: Component, config: Config) = { it: Container -> + ColumnLayout(it, ElementList.withComponentsDefault(Element::default, *components), config) } - fun withComponents(components: Collection) = { it: Container -> - ColumnLayout(it, ElementList.withComponentsDefault(Element::default, components)) + fun withComponents(components: Collection, config: Config) = { it: Container -> + ColumnLayout(it, ElementList.withComponentsDefault(Element::default, components), config) } - fun withElements(vararg elements: Pair) = { it: Container -> - ColumnLayout(it, ElementList.withElements(*elements)) + fun withElements(vararg elements: Pair, config: Config) = { it: Container -> + ColumnLayout(it, ElementList.withElements(*elements), config) } - fun withElements(elements: Map) = { it: Container -> - ColumnLayout(it, ElementList.withElements(elements)) + fun withElements(elements: Map, config: Config) = { it: Container -> + ColumnLayout(it, ElementList.withElements(elements), config) } } - override fun layOut(handle: AsdHandle) { - elements.forEach { TODO() } - } + override fun layOut(handle: LayoutHandle) = sequenceOf(LayoutComputationGroup({ + put(container.asdHandle, setOf(RectangleProperty.KEY)) + }, { + val containerRect = getUnit(container.asdHandle).properties.getProperty(RectangleProperty.KEY)!!.value + var anchor = containerRect.anchor(when (config.direction) { + Direction2.Positive -> Anchor5.BottomLeft + Direction2.Negative -> Anchor5.TopLeft + }) + elements.map { + LayoutComputationUnit({ + put(container.asdHandle, setOf(IntrinsicDimensionsProperty.KEY)) + }, { + + }, { + + }) + }.toSet() + })) } /** * **Row** case of [SequenceLayout]. */ -class RowLayout private constructor(container: Container, elements: ElementList) : - SequenceLayout(container, elements) { +class RowLayout private constructor(container: Container, elements: ElementList, config: Config) : + SequenceLayout(container, elements, config) { companion object { fun withComponents(vararg components: Component) = { it: Container -> RowLayout(it, ElementList.withComponentsDefault(Element::default, *components)) 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 index 4b7e5bfd..e04ec0c5 100644 --- 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 @@ -6,14 +6,19 @@ 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.InsetsF -import net.terramodulus.mui.gui.gfx.RectangleF +import net.terramodulus.mui.gui.gfx.InsetsD +import net.terramodulus.mui.gui.gfx.RectangleD import kotlin.math.max import kotlin.math.min @@ -23,56 +28,201 @@ class SingletonLayout(container: Container, component: Component, private var co private set sealed class Config private constructor() { - abstract fun layOut(container: RectangleF): RectangleF + abstract fun layOut(layout: SingletonLayout): Set sealed class Absolute private constructor() : Config() { - data object Full : Config() { - override fun layOut(container: RectangleF) = container + override fun layOut(layout: SingletonLayout): Set = + setOf(LayoutComputationUnit({ + put(layout.container.asdHandle, setOf(RectangleProperty.KEY)) + }, { + put(layout.component.asdHandle, setOf(RectangleProperty.KEY)) + }, { + mapOf(layout.component.asdHandle to AgimoPropertyMap().apply { + putProperty(RectangleProperty.KEY, RectangleProperty(layOut( + getUnit(layout.container.asdHandle).properties.getProperty(RectangleProperty.KEY)!!.value) + )) + }) + })) + + abstract fun layOut(container: RectangleD): RectangleD + + data object Full : Absolute() { + override fun layOut(container: RectangleD) = container + } + + data class Insets(var insets: InsetsD) : Absolute() { + override fun layOut(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(RectangleProperty.KEY)) + }, { + mapOf(layout.component.asdHandle to AgimoPropertyMap().apply { + val rect = getUnit(layout.container.asdHandle).properties + .getProperty(RectangleProperty.KEY)!!.value + val target = config.compute(layout, this@LayoutComputationUnit) + putProperty(RectangleProperty.KEY, RectangleProperty( + AnchorAlignmentHelper.Subject(rect.toDouble(), rect * alignment) + .alignTarget(AnchorAlignmentHelper.Target(target, target * alignment)) + )) + }) + })) + } + + sealed class Relative { + // Must include Container 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)) + } + + override fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D { + val rect = handle.getUnit(layout.container.asdHandle).properties + .getProperty(RectangleProperty.KEY)!!.value + return Dimension2D(rect.width * scale, rect.height * scale) + } } - data class Insets(var insets: InsetsF) : Config() { - override fun layOut(container: RectangleF) = container - insets + data class Both(val scaleX: Double, val scaleY: Double) : Relative() { + override fun dependencies(layout: SingletonLayout): + MutableMap>>.() -> Unit = { + put(layout.container.asdHandle, setOf(RectangleProperty.KEY)) + } + + override fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D { + val rect = handle.getUnit(layout.container.asdHandle).properties + .getProperty(RectangleProperty.KEY)!!.value + return Dimension2D(rect.width * scaleX, rect.height * scaleY) + } } } - enum class ObjectFit { - Contain { - override fun compute(container: RectangleF, component: IntrinsicRatioProperty): Dimension2D { - val w = container.width.toDouble() / component.width.toDouble() - val h = container.height.toDouble() / component.height.toDouble() + sealed class ObjectFit private constructor() : Relative() { + override fun dependencies(layout: SingletonLayout): + MutableMap>>.() -> Unit = { + put(layout.container.asdHandle, setOf(RectangleProperty.KEY)) + put(layout.component.asdHandle, setOf(IntrinsicRatioProperty.KEY)) + } + + override fun compute(layout: SingletonLayout, handle: LayoutHandle) = compute( + handle.getUnit(layout.container.asdHandle).properties + .getProperty(RectangleProperty.KEY)!!.value, + handle.getUnit(layout.component.asdHandle).properties + .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, ) } - }, - Cover { - override fun compute(container: RectangleF, component: IntrinsicRatioProperty): Dimension2D { - val w = container.width.toDouble() / component.width.toDouble() - val h = container.height.toDouble() / component.height.toDouble() + } + + 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, ) } - }, - ; - - abstract fun compute(container: RectangleF, component: IntrinsicRatioProperty): Dimension2D - - private operator fun RectangleF.times(other: ObjectFitLayout.AlignmentConfig) = ImmVec2d(width * other.x, height * other.y) - private operator fun Dimension2D.times(other: ObjectFitLayout.AlignmentConfig) = ImmVec2d(width * other.x, height * other.y) - - override fun layOut(handle: AsdHandle) { - val ratio = component.asdHandle.getProperty(IntrinsicRatioProperty.KEY) - if (ratio === null) - throw IllegalStateException("component has no intrinsic ratio") - val target = mode.compute(handle.rect, ratio) - component.asdHandle.rect = AnchorAlignmentHelper.Subject(handle.rect.toDouble(), handle.rect * alignment) - .alignTarget(AnchorAlignmentHelper.Target(target, target * alignment)).toFloat() - component.asdHandle.triggerRectObservers() + } + } + + sealed class Scaled private constructor() : Relative() { + override fun dependencies(layout: SingletonLayout): + MutableMap>>.() -> Unit = { + put(layout.container.asdHandle, setOf(RectangleProperty.KEY)) + put(layout.component.asdHandle, setOf(IntrinsicDimensionsProperty.KEY)) + } + + override fun compute(layout: SingletonLayout, handle: LayoutHandle) = compute( + handle.getUnit(layout.component.asdHandle).properties + .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)) } } @@ -86,7 +236,6 @@ class SingletonLayout(container: Container, component: Component, private var co fun withY(y: Double) = AlignmentConfig(y, 0.5) } } - } fun update(component: Component) { @@ -95,14 +244,18 @@ class SingletonLayout(container: Container, component: Component, private var co } } - fun update(operation: (Config) -> Config) { + interface ConfigEnv { + var config: Config + } + + fun update(operation: ConfigEnv.() -> Unit) { operate { - config = operation(config) + operation(object : ConfigEnv { + override var config: Config by this@SingletonLayout::config + }) } } - override fun layOut(handle: AsdHandle) { - component.asdHandle.rect = config.layOut(handle.rect) - component.asdHandle.triggerRectObservers() - } + override fun layOut(handle: LayoutHandle) = + sequenceOf(LayoutComputationGroup({}, { config.layOut(this@SingletonLayout) })) } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt index 1e3aed54..b8d78bd6 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt @@ -11,6 +11,10 @@ package net.terramodulus.mui.gui.gfx * Those should be used with care since they are not already interconvertible. */ +enum class Direction2 { + Positive, Negative; +} + /** * Set of 4 compass directions. */ 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 index 1dd00547..cc2c7ce5 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ModelTransform.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ModelTransform.kt @@ -6,12 +6,5 @@ package net.terramodulus.mui.gui.gfx import net.terramodulus.engine.ModelTransform -import net.terramodulus.engine.SmartScaling typealias ModelTransform = ModelTransform - -typealias SmartScaling = SmartScaling - -typealias FullScaling = net.terramodulus.engine.FullScaling - -fun FullScaling(rect: Dimension2I) = FullScaling(rect.width, rect.height) From 1a07204c0641bbb9c33f3a730a3363766108d6b1 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:26:19 +0800 Subject: [PATCH 13/28] MUI: Restructuring AGIMO/Layout & ASD --- .../mui/gui/agim/LayoutManager.kt | 17 +++ .../mui/gui/agim/impl/BoundsProperty.kt | 16 +++ .../mui/gui/agim/impl/GeomComponent.kt | 14 +- .../mui/gui/agim/impl/LaunchingScreen.kt | 21 +-- .../gui/agim/impl/ResourceLoadingScreen.kt | 9 +- .../mui/gui/agim/impl/SequenceLayout.kt | 125 ++++++++++++++---- .../mui/gui/agim/impl/SingletonLayout.kt | 60 +++++---- .../mui/gui/agim/impl/SizedPane.kt | 72 ++++++++++ .../terramodulus/mui/gui/gfx/GuiGeometry.kt | 2 +- 9 files changed, 270 insertions(+), 66 deletions(-) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BoundsProperty.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SizedPane.kt 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 index eb001d00..8e406a2e 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt @@ -97,12 +97,29 @@ internal class LayoutManager(screenManager: ScreenManager) { fun addLayout(asdHandle: AsdHandle, layout: LayoutNode) { if (layouts.containsKey(asdHandle)) throw IllegalStateException() layouts[asdHandle] = layout + layout.group.dependencies.forEach { (handle, keys) -> + keys.forEach { + groupDeps.push(handle, it, layout.group) { mutableSetOf() } + } + } + layout.units.forEach { unit -> + unit.dependencies.forEach { (handle, keys) -> + keys.forEach { + unitDeps.push(handle, it, unit) { mutableSetOf() } + } + } + } + groups[layout.group] = layout.units } private fun addAffectedProp(handle: AsdHandle, key: AgimoPropertyMap.Key<*>) { affectedProps.computeIfAbsent(handle) { mutableSetOf() }.add(key) } + private fun compute() { + + } + fun validateCyclicGraphs() { run { // Groups data class GroupNode( 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/GeomComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GeomComponent.kt index 865c06a2..82d6d7eb 100644 --- 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 @@ -8,9 +8,21 @@ 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.GuiGeometry +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, asdHandle: AsdHandle) : Component(asdHandle) { +class GeomComponent( + val geom: GuiGeometry, + private val bounds: (RectangleD, GuiGeometry) -> Unit, + asdHandle: AsdHandle, +) : Component(asdHandle) { + init { + asdHandle.observeRect { + bounds(asdHandle.rect.toDouble(), geom) + } + } + override fun render(renderSystem: RenderSystem) { geom.render(renderSystem) } 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 index 9488e503..16c5f539 100644 --- 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 @@ -35,17 +35,20 @@ internal class LaunchingScreen( layout.update { add(SingletonLayout(this@LaunchingScreen, GeomComponent(GuiRect( 0, 0, 1, 1, 37, 198, 196, 255 - ), ComponentAsdHandleImpl()).apply { + ), { rect, geom -> + (geom as GuiRect).setPos(rect.x.toInt(), rect.y.toInt(), rect.width.toInt(), rect.height.toInt()) + }, ComponentAsdHandleImpl()).apply { geom.add(alphaFilter) }, SingletonLayout.Config.Absolute.Full)) - } - layout.update { - add(SingletonLayout(this@LaunchingScreen, SpriteComponent(GuiSprite( - RectangleI(0, 0, 512, 128), - renderSystemHandle.loadTexture("/studio_logo.png"), - ), ComponentAsdHandleImpl()).apply { - sprite.add(alphaFilter) - }, SingletonLayout.Config.Aligned( + add(SingletonLayout(this@LaunchingScreen, SizedPane(ComponentAsdHandleImpl(), + SpriteComponent(GuiSprite( + RectangleI(0, 0, 512, 128), + renderSystemHandle.loadTexture("/studio_logo.png"), + ), ComponentAsdHandleImpl()).apply { + sprite.add(alphaFilter) + }, + SizedPane.Config(512u, 128u) + ), SingletonLayout.Config.Aligned( SingletonLayout.Config.ObjectFit.Contain, SingletonLayout.Config.AlignmentConfig.DEFAULT, ))) 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 index 9b92ccee..afc5f57e 100644 --- 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 @@ -8,6 +8,7 @@ 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.asd.AsdHandle import net.terramodulus.mui.gui.gfx.AlphaFilter import net.terramodulus.mui.gui.gfx.Dimension2I import net.terramodulus.mui.gui.gfx.FullScaling @@ -32,15 +33,19 @@ private const val PAUSE_DURATION = 2F // in second class ResourceLoadingScreen( managerHandle: ScreenManager.Handle, - rect: ScreenManager.DelegatedRect, + asdHandle: AsdHandle.Container, renderSystemHandle: RenderSystem.Handle, -) : Screen(managerHandle, rect) { +) : Screen(managerHandle, asdHandle) { private var stage = 0 private var last = System.currentTimeMillis() // timestamp in milliseconds private var alphaFilter = AlphaFilter(0F) private val progressBar = ProgressBar() + override val layout = CompositeLayout(this) init { + layout.update { + add(SingletonLayout()) + } GeomComponent(GuiRect(0, 0, 800, 480, 0, 255, 213, 255)).apply { geom.add(alphaFilter) geom.add(FullScaling(REF_SIZE)) 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 index a9b8c233..c1dbe299 100644 --- 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 @@ -5,6 +5,8 @@ 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 @@ -15,6 +17,8 @@ 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]. @@ -84,23 +88,49 @@ class ColumnLayout private constructor(container: Container, elements: ElementLi } } - override fun layOut(handle: LayoutHandle) = sequenceOf(LayoutComputationGroup({ - put(container.asdHandle, setOf(RectangleProperty.KEY)) - }, { - val containerRect = getUnit(container.asdHandle).properties.getProperty(RectangleProperty.KEY)!!.value - var anchor = containerRect.anchor(when (config.direction) { - Direction2.Positive -> Anchor5.BottomLeft - Direction2.Negative -> Anchor5.TopLeft - }) - elements.map { - LayoutComputationUnit({ - put(container.asdHandle, setOf(IntrinsicDimensionsProperty.KEY)) - }, { - - }, { - + 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).properties.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 }) - }.toSet() + var width = 0.0 + var height = 0.0 + val map = mutableMapOf() + elements.forEach { + val dim = getUnit(it.first.asdHandle).properties.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).properties.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 + })) })) } @@ -110,24 +140,65 @@ class ColumnLayout private constructor(container: Container, elements: ElementLi class RowLayout private constructor(container: Container, elements: ElementList, config: Config) : SequenceLayout(container, elements, config) { companion object { - fun withComponents(vararg components: Component) = { it: Container -> - RowLayout(it, ElementList.withComponentsDefault(Element::default, *components)) + fun withComponents(vararg components: Component, config: Config) = { it: Container -> + RowLayout(it, ElementList.withComponentsDefault(Element::default, *components), config) } - fun withComponents(components: Collection) = { it: Container -> - RowLayout(it, ElementList.withComponentsDefault(Element::default, components)) + fun withComponents(components: Collection, config: Config) = { it: Container -> + RowLayout(it, ElementList.withComponentsDefault(Element::default, components), config) } - fun withElements(vararg elements: Pair) = { it: Container -> - RowLayout(it, ElementList.withElements(*elements)) + fun withElements(vararg elements: Pair, config: Config) = { it: Container -> + RowLayout(it, ElementList.withElements(*elements), config) } - fun withElements(elements: Map) = { it: Container -> - RowLayout(it, ElementList.withElements(elements)) + fun withElements(elements: Map, config: Config) = { it: Container -> + RowLayout(it, ElementList.withElements(elements), config) } } - override fun layOut(handle: AsdHandle) { - elements.forEach { TODO() } - } + 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).properties.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).properties.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).properties.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/SingletonLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SingletonLayout.kt index e04ec0c5..6f121606 100644 --- 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 @@ -33,25 +33,27 @@ class SingletonLayout(container: Container, component: Component, private var co sealed class Absolute private constructor() : Config() { override fun layOut(layout: SingletonLayout): Set = setOf(LayoutComputationUnit({ - put(layout.container.asdHandle, setOf(RectangleProperty.KEY)) + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) }, { - put(layout.component.asdHandle, setOf(RectangleProperty.KEY)) + put(layout.component.asdHandle, setOf(BoundsProperty.KEY)) }, { mapOf(layout.component.asdHandle to AgimoPropertyMap().apply { - putProperty(RectangleProperty.KEY, RectangleProperty(layOut( - getUnit(layout.container.asdHandle).properties.getProperty(RectangleProperty.KEY)!!.value) + val prop = getUnit(layout.container.asdHandle).properties + putProperty(BoundsProperty.KEY, BoundsProperty(compute( + prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value) )) }) })) - abstract fun layOut(container: RectangleD): RectangleD + abstract fun compute(container: RectangleD): RectangleD data object Full : Absolute() { - override fun layOut(container: RectangleD) = container + override fun compute(container: RectangleD) = container } data class Insets(var insets: InsetsD) : Absolute() { - override fun layOut(container: RectangleD) = container - insets + override fun compute(container: RectangleD) = container - insets } } @@ -63,13 +65,14 @@ class SingletonLayout(container: Container, component: Component, private var co override fun layOut(layout: SingletonLayout): Set = setOf(LayoutComputationUnit(config.dependencies(layout), { - put(layout.component.asdHandle, setOf(RectangleProperty.KEY)) + put(layout.component.asdHandle, setOf(BoundsProperty.KEY)) }, { mapOf(layout.component.asdHandle to AgimoPropertyMap().apply { - val rect = getUnit(layout.container.asdHandle).properties - .getProperty(RectangleProperty.KEY)!!.value + val prop = getUnit(layout.container.asdHandle).properties + val rect = prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value val target = config.compute(layout, this@LayoutComputationUnit) - putProperty(RectangleProperty.KEY, RectangleProperty( + putProperty(BoundsProperty.KEY, BoundsProperty( AnchorAlignmentHelper.Subject(rect.toDouble(), rect * alignment) .alignTarget(AnchorAlignmentHelper.Target(target, target * alignment)) )) @@ -78,7 +81,7 @@ class SingletonLayout(container: Container, component: Component, private var co } sealed class Relative { - // Must include Container Rectangle + // Must include Container Bounds & Rectangle abstract fun dependencies(layout: SingletonLayout): MutableMap>>.() -> Unit @@ -87,12 +90,13 @@ class SingletonLayout(container: Container, component: Component, private var co data class Simple(val scale: Double) : Relative() { override fun dependencies(layout: SingletonLayout): MutableMap>>.() -> Unit = { - put(layout.container.asdHandle, setOf(RectangleProperty.KEY)) + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) } override fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D { - val rect = handle.getUnit(layout.container.asdHandle).properties - .getProperty(RectangleProperty.KEY)!!.value + val prop = handle.getUnit(layout.container.asdHandle).properties + val rect = prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value return Dimension2D(rect.width * scale, rect.height * scale) } } @@ -100,12 +104,13 @@ class SingletonLayout(container: Container, component: Component, private var co data class Both(val scaleX: Double, val scaleY: Double) : Relative() { override fun dependencies(layout: SingletonLayout): MutableMap>>.() -> Unit = { - put(layout.container.asdHandle, setOf(RectangleProperty.KEY)) + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) } override fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D { - val rect = handle.getUnit(layout.container.asdHandle).properties - .getProperty(RectangleProperty.KEY)!!.value + val prop = handle.getUnit(layout.container.asdHandle).properties + val rect = prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value return Dimension2D(rect.width * scaleX, rect.height * scaleY) } } @@ -114,16 +119,19 @@ class SingletonLayout(container: Container, component: Component, private var co sealed class ObjectFit private constructor() : Relative() { override fun dependencies(layout: SingletonLayout): MutableMap>>.() -> Unit = { - put(layout.container.asdHandle, setOf(RectangleProperty.KEY)) + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) put(layout.component.asdHandle, setOf(IntrinsicRatioProperty.KEY)) } - override fun compute(layout: SingletonLayout, handle: LayoutHandle) = compute( - handle.getUnit(layout.container.asdHandle).properties - .getProperty(RectangleProperty.KEY)!!.value, - handle.getUnit(layout.component.asdHandle).properties - .getProperty(IntrinsicRatioProperty.KEY)!! - ) + override fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D { + val prop = handle.getUnit(layout.container.asdHandle).properties + return compute( + prop.getProperty(RectangleProperty.KEY)?.value + ?: prop.getProperty(BoundsProperty.KEY)!!.value, + handle.getUnit(layout.component.asdHandle).properties + .getProperty(IntrinsicRatioProperty.KEY)!! + ) + } abstract fun compute(container: RectangleD, component: IntrinsicRatioProperty): Dimension2D @@ -155,7 +163,7 @@ class SingletonLayout(container: Container, component: Component, private var co sealed class Scaled private constructor() : Relative() { override fun dependencies(layout: SingletonLayout): MutableMap>>.() -> Unit = { - put(layout.container.asdHandle, setOf(RectangleProperty.KEY)) + put(layout.container.asdHandle, setOf(RectangleProperty.KEY, BoundsProperty.KEY)) put(layout.component.asdHandle, setOf(IntrinsicDimensionsProperty.KEY)) } 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..51a66c8d --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SizedPane.kt @@ -0,0 +1,72 @@ +/* + * 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) + } + ) + })) + })) + } + + 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/gfx/GuiGeometry.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiGeometry.kt index 950c8236..0858682e 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiGeometry.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiGeometry.kt @@ -14,7 +14,7 @@ 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) } From 81c78113c7b3c96ee1213fc0007998f9574d7002 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Sat, 22 Aug 2026 15:16:51 +0800 Subject: [PATCH 14/28] MUI: Restructuring AGIMO/Layout & ASD --- build.gradle.kts | 2 + .../net/terramodulus/engine/ModelTransform.kt | 36 +++- .../terramodulus/mui/gui/agim/AbstractPane.kt | 3 +- .../mui/gui/agim/LayoutComputation.kt | 6 +- .../mui/gui/agim/LayoutManager.kt | 175 +++++++++++++++--- .../net/terramodulus/mui/gui/agim/Menu.kt | 3 +- .../terramodulus/mui/gui/agim/MenuManager.kt | 3 +- .../net/terramodulus/mui/gui/agim/Screen.kt | 3 +- .../mui/gui/agim/ScreenManager.kt | 5 +- .../mui/gui/agim/impl/GeomComponent.kt | 11 +- .../mui/gui/agim/impl/GraphicsComponent.kt | 13 ++ .../mui/gui/agim/impl/LaunchingScreen.kt | 12 +- .../net/terramodulus/mui/gui/asd/AsdHandle.kt | 3 +- .../net/terramodulus/mui/gui/gfx/GuiSprite.kt | 2 +- .../mui/gui/gfx/ModelTransform.kt | 31 ++++ 15 files changed, 255 insertions(+), 53 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 56fcc34f..adcac650 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -138,10 +138,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 { diff --git a/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt b/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt index f4b8ffe5..3b9752c7 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ModelTransform.kt @@ -5,28 +5,48 @@ package net.terramodulus.engine +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 GeneralTransform(sx: Double, sy: Double, angle: Double, px: Double, py: Double) : ModelTransform(modelGeneralTransform(doubleArrayOf(sx, sy, angle, px, py))) { - var scale: Vec2d by Delegates.observable(MutVec2d(sx, sy)) { _, _, new -> - updateGeneralTransform(handle, doubleArrayOf(new.x, new.y, angle, px, py)) - } - var angle: Double by Delegates.observable(angle) { _, _, new -> - updateGeneralTransform(handle, doubleArrayOf(sx, sy, new, 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 } - var pos: Vec2d by Delegates.observable(MutVec2d(px, py)) { _, _, new -> - updateGeneralTransform(handle, doubleArrayOf(sx, sy, angle, new.x, new.y)) + + 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)) } } 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 index c6f23675..28a4b261 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AbstractPane.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/AbstractPane.kt @@ -7,11 +7,12 @@ 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: RectangleF + override lateinit var rect: RectangleD override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdHandle.registerAsdProcessor(processor) } 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 index d57d985a..08c1a778 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutComputation.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutComputation.kt @@ -25,13 +25,15 @@ class LayoutComputationUnit @PublishedApi internal constructor( } class LayoutComputationGroup @PublishedApi internal constructor( - val conditions: LayoutHandle.() -> Set, + val conditions: () -> Set, +// val conditions: LayoutHandle.() -> Set, val dependencies: MutableMap>>, ) { companion object { inline operator fun invoke( dependencies: MutableMap>>.() -> Unit, - noinline conditions: LayoutHandle.() -> Set, + noinline conditions: () -> Set, +// noinline conditions: LayoutHandle.() -> Set, ) = LayoutComputationGroup(conditions, mutableMapOf>>().apply(dependencies), ) 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 index 8e406a2e..cf0bcd8c 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt @@ -5,7 +5,13 @@ 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.asd.AsdHandle +import kotlin.collections.component1 +import kotlin.collections.component2 import kotlin.collections.mutableSetOf internal class LayoutManager(screenManager: ScreenManager) { @@ -66,8 +72,8 @@ internal class LayoutManager(screenManager: ScreenManager) { private data class LayoutNode( val layout: Layout, val containerHandle: AsdHandle, - var group: LayoutComputationGroup, - var units: Set, + val groups: Sequence, +// var units: Set, ) private typealias InstancedPropertyMap = MutableMap, V>> @@ -87,29 +93,25 @@ internal class LayoutManager(screenManager: ScreenManager) { private val layouts = mutableMapOf() private val groupDeps: InstancedPropertyMap> = mutableMapOf() private val unitDeps: InstancedPropertyMap> = mutableMapOf() - private val groups = mutableMapOf>() + private val unitResults: InstancedPropertyMap> = mutableMapOf() + private val units = mutableMapOf>() + private val unitGroups = mutableMapOf() private val results = mutableMapOf>() private val affectedProps = mutableMapOf>>() - private val changedUnits = mutableSetOf() +// 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.group.dependencies.forEach { (handle, keys) -> - keys.forEach { - groupDeps.push(handle, it, layout.group) { mutableSetOf() } - } - } - layout.units.forEach { unit -> - unit.dependencies.forEach { (handle, keys) -> + layout.groups.forEach { group -> + group.dependencies.forEach { (handle, keys) -> keys.forEach { - unitDeps.push(handle, it, unit) { mutableSetOf() } + groupDeps.push(handle, it, group) { mutableSetOf() } } } } - groups[layout.group] = layout.units } private fun addAffectedProp(handle: AsdHandle, key: AgimoPropertyMap.Key<*>) { @@ -117,7 +119,126 @@ internal class LayoutManager(screenManager: ScreenManager) { } private 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)) removeGroup(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) } +// 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 { + for (unit in channelToCompute) { + + } + channelComputed.close() + } + } } fun validateCyclicGraphs() { @@ -133,12 +254,12 @@ internal class LayoutManager(screenManager: ScreenManager) { 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(), - ) +// 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(), +// ) } } @@ -150,15 +271,19 @@ internal class LayoutManager(screenManager: ScreenManager) { fun removeLayout(asdHandle: AsdHandle) { val layout = layouts.remove(asdHandle) if (layout === null) throw IllegalStateException() - groups.remove(layout.group) - layout.group.dependencies.forEach { (handle, keys) -> - keys.forEach { groupDeps[handle, it]?.remove(layout.group) } + layout.groups.forEach { removeGroup(it) } + } + + private fun removeGroup(group: LayoutComputationGroup) { + group.dependencies.forEach { (handle, keys) -> + keys.forEach { groupDeps[handle, it]?.remove(group) } } - layout.units.forEach { unit -> + units.remove(group)?.forEach { unit -> + results.remove(unit) + unitGroups.remove(unit) unit.dependencies.forEach { (handle, keys) -> keys.forEach { unitDeps[handle, it]?.remove(unit) } } - changedUnits.add(unit) } } } 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 index 009d36bf..70adafe8 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Menu.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Menu.kt @@ -8,6 +8,7 @@ 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 @@ -45,7 +46,7 @@ abstract class Menu( } protected inner class ComponentAsdHandleImpl : AsdHandle.Container() { - override lateinit var rect: RectangleF + override lateinit var rect: RectangleD override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdHandle.registerAsdProcessor(processor) } 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 index 70870f85..c537aa76 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/MenuManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/MenuManager.kt @@ -7,6 +7,7 @@ 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 @@ -49,7 +50,7 @@ class MenuManager internal constructor(private val asdHandle: (AsdProcessor<*>) } private inner class MenuAsdHandleImpl : AsdHandle.Menu() { - override lateinit var rect: RectangleF + override lateinit var rect: RectangleD override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdHandle(processor) } 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 index 441b618f..363b541c 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Screen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/Screen.kt @@ -8,6 +8,7 @@ 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 @@ -59,7 +60,7 @@ abstract class Screen( } protected inner class ComponentAsdHandleImpl : AsdHandle.Container() { - override lateinit var rect: RectangleF + override lateinit var rect: RectangleD override fun registerAsdProcessor(processor: AsdProcessor<*>) = asdHandle.registerAsdProcessor(processor) } 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 index 829703cd..50bea273 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt @@ -12,6 +12,7 @@ import net.terramodulus.mui.gui.agim.impl.LaunchingScreen 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 @@ -28,11 +29,11 @@ class ScreenManager internal constructor( private val menuManager = MenuManager(asdManagerHandle::registerProcessor) private val asdHandles = HashSet() - private var viewportRect = RectangleF(0F, 0F, window.width.toFloat(), window.height.toFloat()) + private var viewportRect = RectangleD(0.0, 0.0, window.width.toDouble(), window.height.toDouble()) init { window.addListener { w, h -> - viewportRect = RectangleF(0F, 0F, w.toFloat(), h.toFloat()) + viewportRect = RectangleD(0.0, 0.0, w.toDouble(), h.toDouble()) asdHandles.forEach { it.triggerRectObservers() } // already referring viewportRect } } 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 index 82d6d7eb..01044fa5 100644 --- 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 @@ -7,19 +7,26 @@ 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, GuiGeometry) -> Unit, + 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 { - bounds(asdHandle.rect.toDouble(), geom) + RectStParams.fromRects(bounds, asdHandle.rect).applyToGeneralTransform(transform) } } 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 index d2d8077f..727c889b 100644 --- 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 @@ -9,12 +9,25 @@ 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.GeneralTransform import net.terramodulus.mui.gui.gfx.GuiSprite +import net.terramodulus.mui.gui.gfx.RectStParams 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(asdHandle.rect.width.toUInt(), asdHandle.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) } 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 index 16c5f539..1addb7d2 100644 --- 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 @@ -12,6 +12,7 @@ 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 @@ -35,20 +36,15 @@ internal class LaunchingScreen( layout.update { add(SingletonLayout(this@LaunchingScreen, GeomComponent(GuiRect( 0, 0, 1, 1, 37, 198, 196, 255 - ), { rect, geom -> - (geom as GuiRect).setPos(rect.x.toInt(), rect.y.toInt(), rect.width.toInt(), rect.height.toInt()) - }, ComponentAsdHandleImpl()).apply { + ), RectangleD(0.0, 0.0, 1.0, 1.0), ComponentAsdHandleImpl()).apply { geom.add(alphaFilter) }, SingletonLayout.Config.Absolute.Full)) - add(SingletonLayout(this@LaunchingScreen, SizedPane(ComponentAsdHandleImpl(), - SpriteComponent(GuiSprite( + add(SingletonLayout(this@LaunchingScreen, SpriteComponent(GuiSprite( RectangleI(0, 0, 512, 128), renderSystemHandle.loadTexture("/studio_logo.png"), ), ComponentAsdHandleImpl()).apply { sprite.add(alphaFilter) - }, - SizedPane.Config(512u, 128u) - ), SingletonLayout.Config.Aligned( + }, SingletonLayout.Config.Aligned( SingletonLayout.Config.ObjectFit.Contain, SingletonLayout.Config.AlignmentConfig.DEFAULT, ))) 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 index cadc2284..660d2c0f 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdHandle.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/asd/AsdHandle.kt @@ -7,6 +7,7 @@ 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 @@ -20,7 +21,7 @@ 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: RectangleF + abstract var rect: RectangleD internal set protected val rectObservers = LinkedHashSet<() -> Unit>() 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 index 11ba563d..5227856e 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiSprite.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiSprite.kt @@ -7,7 +7,7 @@ package net.terramodulus.mui.gui.gfx import net.terramodulus.engine.SpriteMesh -class GuiSprite(private val rect: RectangleI, private val texture: UInt) { +class GuiSprite(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) 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 index cc2c7ce5..c561ac4c 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ModelTransform.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ModelTransform.kt @@ -5,6 +5,37 @@ 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) + } + } +} From 8198dbe1267b0c492776d2942bc5511d14621edc Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Sun, 23 Aug 2026 17:31:58 +0800 Subject: [PATCH 15/28] MUI: Restructuring AGIMO/Layout & ASD --- ferricia | 2 +- .../net/terramodulus/engine/Camera3D.kt | 6 +- .../kotlin/net/terramodulus/engine/Canvas.kt | 36 ++++- .../net/terramodulus/engine/GeomDrawable.kt | 8 +- .../net/terramodulus/engine/MeshDrawable.kt | 3 +- .../terramodulus/engine/WorldObjDrawable.kt | 8 +- .../net/terramodulus/engine/ferricia/Gwr.kt | 12 +- .../net/terramodulus/engine/ferricia/Mui.kt | 25 +++- .../net/terramodulus/mui/gui/GuiManager.kt | 3 +- .../terramodulus/mui/gui/agim/LayoutHandle.kt | 9 +- .../mui/gui/agim/LayoutManager.kt | 129 +++++++++++++++--- .../mui/gui/agim/ScreenManager.kt | 8 ++ .../mui/gui/agim/impl/BlankComponent.kt | 2 +- .../mui/gui/agim/impl/GameplayScreen.kt | 5 +- .../mui/gui/agim/impl/GraphicsComponent.kt | 54 ++++++++ .../mui/gui/agim/impl/LaunchingScreen.kt | 13 +- .../gui/agim/impl/ResourceLoadingScreen.kt | 124 ++++++++--------- .../mui/gui/agim/impl/SequenceLayout.kt | 12 +- .../mui/gui/agim/impl/SingletonLayout.kt | 16 +-- .../mui/gui/agim/impl/SliderComponent.kt | 99 ++++++++------ .../mui/gui/agim/impl/TitleScreen.kt | 10 +- .../terramodulus/mui/gui/gfx/GuiGeometry.kt | 8 +- .../net/terramodulus/mui/gui/gfx/GuiSprite.kt | 4 +- .../terramodulus/mui/gui/gfx/ManagedRect.kt | 37 ----- .../terramodulus/mui/gui/gfx/RenderSystem.kt | 21 ++- 25 files changed, 419 insertions(+), 235 deletions(-) delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ManagedRect.kt diff --git a/ferricia b/ferricia index 45d00517..0798e3c6 160000 --- a/ferricia +++ b/ferricia @@ -1 +1 @@ -Subproject commit 45d005177d84f14300c25a015148b6480336a31d +Subproject commit 0798e3c6760e5a30fc91cb7ec0a60169975eeaa7 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 4bdbcf25..6354415e 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt @@ -5,6 +5,11 @@ package net.terramodulus.engine +import com.cout970.math.vec4.Vec4i +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 +19,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 @@ -28,9 +36,9 @@ class Canvas internal constructor(private val windowHandle: ULong) : Closeable { 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) internal fun resizeGLViewport() = if (camera3D == null) { Mui.resizeGLViewport(windowHandle, handle) @@ -45,9 +53,26 @@ 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, rgba: Vec4i) = + newMeshGeomCube(windowHandle, width, rgba.toArray()) + + internal fun newMeshGeomSphere(radius: Float, rgba: Vec4i) = + newMeshGeomSphere(windowHandle, radius, rgba.toArray()) fun renderGuiGeo(drawable: GeomDrawable, programHandle: ULong) = drawGuiGeo(handle, drawable.handle, programHandle) @@ -55,6 +80,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/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/WorldObjDrawable.kt b/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt index cf854519..96158285 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt @@ -38,8 +38,8 @@ sealed class WorldObjDrawable(internal val handle: ULong, private var pos: Vec3d } } -class SimpleMesh3dGeomCube(width: Float, rgba: Vec4i, pos: Vec3d, scale: Vec3d, rot: Quatd) : - WorldObjDrawable(newMeshGeomCube(width, rgba.toArray()), pos, scale, rot) +class SimpleMesh3dGeomCube(canvas: Canvas, width: Float, rgba: Vec4i, pos: Vec3d, scale: Vec3d, rot: Quatd) : + WorldObjDrawable(canvas.newMeshGeomCube(width, rgba), pos, scale, rot) -class SimpleMesh3dGeomSphere(radius: Float, rgba: Vec4i, pos: Vec3d, scale: Vec3d, rot: Quatd) : - WorldObjDrawable(newMeshGeomSphere(radius, rgba.toArray()), pos, scale, rot) +class SimpleMesh3dGeomSphere(canvas: Canvas, radius: Float, rgba: Vec4i, pos: Vec3d, scale: Vec3d, rot: Quatd) : + WorldObjDrawable(canvas.newMeshGeomSphere(radius, rgba), pos, scale, rot) 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..0dd90336 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Gwr.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Gwr.kt @@ -7,12 +7,13 @@ package net.terramodulus.engine.ferricia 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 +38,22 @@ 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 */ @JvmName("newMeshGeomCube") - external fun newMeshGeomCube(width: Float, data: IntArray): ULong + external fun newMeshGeomCube(windowHandle: ULong, width: Float, data: IntArray): ULong /** + * @param windowHandle window handle pointer * @param width cube's radius, in `(0,1]` * @param data `[r, g, b, a]` * @return DrawableWorldObj handle pointer */ @JvmName("newMeshGeomSphere") - external fun newMeshGeomSphere(width: Float, data: IntArray): ULong + external fun newMeshGeomSphere(windowHandle: ULong, width: Float, data: IntArray): ULong /** * @param objHandle DrawableWorldObj handle pointer @@ -60,11 +63,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 f42a5dc0..7d1a95c3 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt @@ -92,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 diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt index d6ca96da..651c764b 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt @@ -24,7 +24,7 @@ internal class GuiManager internal constructor(private val window: Window, core: val renderSystem = RenderSystem(core, window.canvas) val asdManager = AsdManager() val screenManager = ScreenManager(window, renderSystem.handle, asdManager.AgimHandle()) - val layoutManager = LayoutManager() + val layoutManager = LayoutManager(screenManager) private var proceeded = false @@ -39,6 +39,7 @@ internal class GuiManager internal constructor(private val window: Window, core: asdManager.process() proceeded = true } + layoutManager.tick() } /** 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 index 61f4f26c..334e8e21 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutHandle.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutHandle.kt @@ -7,10 +7,11 @@ package net.terramodulus.mui.gui.agim import net.terramodulus.mui.gui.asd.AsdHandle -sealed interface LayoutHandle { - fun getUnit(handle: AsdHandle): Unit +abstract class LayoutHandle internal constructor() { + abstract fun getUnit(handle: AsdHandle): Unit - sealed interface Unit { - val properties: Map, AgimoProperty> + 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 index cf0bcd8c..ec640fd0 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt @@ -9,6 +9,8 @@ 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 @@ -85,18 +87,25 @@ internal class LayoutManager(screenManager: ScreenManager) { .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 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() @@ -118,7 +127,7 @@ internal class LayoutManager(screenManager: ScreenManager) { affectedProps.computeIfAbsent(handle) { mutableSetOf() }.add(key) } - private fun compute() { + 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. @@ -134,10 +143,10 @@ internal class LayoutManager(screenManager: ScreenManager) { } affectedProps.clear() val groupsToCompute = mutableSetOf() - unitsToRecompute.forEach { - val group = unitGroups[it]!! - if (groupsToCompute.add(group)) removeGroup(group) - } +// 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) @@ -155,17 +164,37 @@ internal class LayoutManager(screenManager: ScreenManager) { unitDeps.push(handle, it, unit) { mutableSetOf() } } } - unit.results.forEach { (handle, keys) -> - keys.forEach { - unitResults.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() @@ -233,12 +262,45 @@ internal class LayoutManager(screenManager: ScreenManager) { } } launch { - for (unit in channelToCompute) { + 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 = it.asdHandle.rect + it.asdHandle.rect = dep.getProperty(RectangleProperty.KEY)?.value + ?: requireNotNull(dep.getProperty(BoundsProperty.KEY)).value + if (prev != it.asdHandle.rect) + it.asdHandle.triggerRectObservers() + } + } } fun validateCyclicGraphs() { @@ -271,25 +333,56 @@ internal class LayoutManager(screenManager: ScreenManager) { fun removeLayout(asdHandle: AsdHandle) { val layout = layouts.remove(asdHandle) if (layout === null) throw IllegalStateException() - layout.groups.forEach { removeGroup(it) } + layout.groups.forEach { group -> + group.dependencies.forEach { (handle, keys) -> + keys.forEach { groupDeps[handle, it]?.remove(group) } + } + removeGroupUnits(group) + } } - private fun removeGroup(group: LayoutComputationGroup) { - group.dependencies.forEach { (handle, keys) -> - keys.forEach { groupDeps[handle, it]?.remove(group) } - } + private fun removeGroupUnits(group: LayoutComputationGroup) { units.remove(group)?.forEach { unit -> - results.remove(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/ScreenManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt index 50bea273..c2999ad4 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt @@ -7,8 +7,10 @@ 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 @@ -212,6 +214,12 @@ class ScreenManager internal constructor( 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 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 index 88a6d0f3..76ad6f7c 100644 --- 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 @@ -13,6 +13,6 @@ 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, dim: Dimension2F) : Component(asdHandle) { +class BlankComponent(asdHandle: AsdHandle) : Component(asdHandle) { override fun render(renderSystem: RenderSystem) {} } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt index 6dd069a1..1b13e3fa 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt @@ -71,6 +71,8 @@ internal class GameplayScreen( getResourceAsString("/gwr_geo.fsh"), ) + private val canvasHandle = renderSystemHandle.canvasHandle + private lateinit var player: PlayerVoidGeom override val layout: SingletonLayout @@ -104,6 +106,7 @@ internal class GameplayScreen( private inner class Ymir : World.Ymir { override fun wrapCube(phyGeom: PhyGeom, x: Double, y: Double, z: Double): VoidGeom = EnvVoidGeom(phyGeom, SimpleMesh3dGeomCube( + canvasHandle.canvas, 2F, randomColor(), ImmVec3d(x, y, z), @@ -122,7 +125,7 @@ internal class GameplayScreen( override fun wrapChar(phyBody: PhyBody): VoidGeom { player = PlayerVoidGeom(phyBody, - SimpleMesh3dGeomSphere(1F, WHITE, ImmVec3d(0.0, 1.0, 0.0), STD_SCALE, IDENT_ROT) + SimpleMesh3dGeomSphere(canvasHandle.canvas, 1F, WHITE, ImmVec3d(0.0, 1.0, 0.0), STD_SCALE, IDENT_ROT) ) return player } 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 index 727c889b..23ffb7f8 100644 --- 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 @@ -9,9 +9,13 @@ 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 @@ -33,6 +37,56 @@ class SpriteComponent(val sprite: GuiSprite, asdHandle: AsdHandle) : Component(a } } +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) { 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 index 1addb7d2..a0ea674b 100644 --- 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 @@ -35,16 +35,17 @@ internal class LaunchingScreen( init { layout.update { add(SingletonLayout(this@LaunchingScreen, GeomComponent(GuiRect( - 0, 0, 1, 1, 37, 198, 196, 255 + 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( - RectangleI(0, 0, 512, 128), - renderSystemHandle.loadTexture("/studio_logo.png"), - ), ComponentAsdHandleImpl()).apply { - sprite.add(alphaFilter) - }, SingletonLayout.Config.Aligned( + 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, ))) 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 index afc5f57e..8c649d0c 100644 --- 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 @@ -8,22 +8,19 @@ 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.FullScaling 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 net.terramodulus.mui.gui.gfx.SmartScaling -import net.terramodulus.mui.kui.InputSystem 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) @@ -39,81 +36,74 @@ class ResourceLoadingScreen( private var stage = 0 private var last = System.currentTimeMillis() // timestamp in milliseconds private var alphaFilter = AlphaFilter(0F) - private val progressBar = ProgressBar() + private val progressBar = ProgressBar(renderSystemHandle) override val layout = CompositeLayout(this) init { layout.update { - add(SingletonLayout()) - } - 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) + 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, 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, + ))) } - 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 = 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(7, 7, 7, 33, 240, 240, 240, 255) - } + 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 + } - 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 + } + } - 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 + 2 -> if (elapsed >= ANI_DURATION) { + stage = 3 last = current + alphaFilter.alpha = 0F + } else { + alphaFilter.alpha = 1 - elapsed / ANI_DURATION } - } - 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)) } + } + } -// 3 -> screenManager.handle.openBefore(::TitleScreen, this) - 3 -> screenManager.handle.reset(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/SequenceLayout.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SequenceLayout.kt index c1dbe299..6d726bad 100644 --- 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 @@ -97,7 +97,7 @@ class ColumnLayout private constructor(container: Container, elements: ElementLi elements.forEach { put(it.first.asdHandle, setOf(BoundsProperty.KEY)) } put(container.asdHandle, setOf(RectangleProperty.KEY)) }, { - val containerRect = getUnit(container.asdHandle).properties.getProperty(BoundsProperty.KEY)!!.value + 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 @@ -106,13 +106,13 @@ class ColumnLayout private constructor(container: Container, elements: ElementLi var height = 0.0 val map = mutableMapOf() elements.forEach { - val dim = getUnit(it.first.asdHandle).properties.getProperty(IntrinsicDimensionsProperty.KEY)!! + 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).properties.getProperty(IntrinsicDimensionsProperty.KEY)!! + 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( @@ -166,7 +166,7 @@ class RowLayout private constructor(container: Container, elements: ElementList< elements.forEach { put(it.first.asdHandle, setOf(BoundsProperty.KEY)) } put(container.asdHandle, setOf(RectangleProperty.KEY)) }, { - val containerRect = getUnit(container.asdHandle).properties.getProperty(BoundsProperty.KEY)!!.value + 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 @@ -175,13 +175,13 @@ class RowLayout private constructor(container: Container, elements: ElementList< var height = 0.0 val map = mutableMapOf() elements.forEach { - val dim = getUnit(it.first.asdHandle).properties.getProperty(IntrinsicDimensionsProperty.KEY)!! + 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).properties.getProperty(IntrinsicDimensionsProperty.KEY)!! + 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( 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 index 6f121606..d9fb3bfe 100644 --- 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 @@ -38,7 +38,7 @@ class SingletonLayout(container: Container, component: Component, private var co put(layout.component.asdHandle, setOf(BoundsProperty.KEY)) }, { mapOf(layout.component.asdHandle to AgimoPropertyMap().apply { - val prop = getUnit(layout.container.asdHandle).properties + val prop = getUnit(layout.container.asdHandle) putProperty(BoundsProperty.KEY, BoundsProperty(compute( prop.getProperty(RectangleProperty.KEY)?.value ?: prop.getProperty(BoundsProperty.KEY)!!.value) @@ -68,7 +68,7 @@ class SingletonLayout(container: Container, component: Component, private var co put(layout.component.asdHandle, setOf(BoundsProperty.KEY)) }, { mapOf(layout.component.asdHandle to AgimoPropertyMap().apply { - val prop = getUnit(layout.container.asdHandle).properties + 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) @@ -94,7 +94,7 @@ class SingletonLayout(container: Container, component: Component, private var co } override fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D { - val prop = handle.getUnit(layout.container.asdHandle).properties + 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) @@ -108,7 +108,7 @@ class SingletonLayout(container: Container, component: Component, private var co } override fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D { - val prop = handle.getUnit(layout.container.asdHandle).properties + 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) @@ -124,12 +124,11 @@ class SingletonLayout(container: Container, component: Component, private var co } override fun compute(layout: SingletonLayout, handle: LayoutHandle): Dimension2D { - val prop = handle.getUnit(layout.container.asdHandle).properties + val prop = handle.getUnit(layout.container.asdHandle) return compute( prop.getProperty(RectangleProperty.KEY)?.value ?: prop.getProperty(BoundsProperty.KEY)!!.value, - handle.getUnit(layout.component.asdHandle).properties - .getProperty(IntrinsicRatioProperty.KEY)!! + handle.getUnit(layout.component.asdHandle).getProperty(IntrinsicRatioProperty.KEY)!! ) } @@ -168,8 +167,7 @@ class SingletonLayout(container: Container, component: Component, private var co } override fun compute(layout: SingletonLayout, handle: LayoutHandle) = compute( - handle.getUnit(layout.component.asdHandle).properties - .getProperty(IntrinsicDimensionsProperty.KEY)!! + handle.getUnit(layout.component.asdHandle).getProperty(IntrinsicDimensionsProperty.KEY)!! ) abstract fun compute(dim: IntrinsicDimensionsProperty): Dimension2D 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 index 1e06a869..38cf2dc2 100644 --- 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 @@ -5,67 +5,86 @@ package net.terramodulus.mui.gui.agim.impl +import com.cout970.math.vec3.Vec3i import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.asd.AsdHandle import net.terramodulus.mui.gui.gfx.Anchor5 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.RectangleF +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) : Component() { - private var bgInit = false - private var fgInit = false - private lateinit var background: GuiRect - private lateinit var bgComponent: GeomComponent - private lateinit var foreground: GuiRect - private lateinit var fgComponent: GeomComponent - var fraction: Float by Delegates.observable(0F) { _, _, value -> - updateForeground(rect.value, value) +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, 255, + ).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, 255, + ).apply { add(fgTransform) } + var fraction: Double by Delegates.observable(0.0) { _, _, value -> + updateForeground(asdHandle.rect, value) + } + + class Config(val bgColor: Vec3i, val fgColor: Vec3i) + init { - rect.observe { - val anchor = it.anchor(Anchor5.TopRight) - if (!bgInit) { - background = GuiRect(it.x.toInt(), it.y.toInt(), anchor.xi, anchor.yi, 255, 255, 255, 255) - bgComponent = GeomComponent(background) - bgInit = true - } else { - background.setPos(it.x.toInt(), it.y.toInt(), anchor.xi, anchor.yi) - } - updateForeground(it, fraction) + asdHandle.observeRect { + RectStParams.fromRects(BOUNDS.toDouble(), asdHandle.rect).applyToGeneralTransform(bgTransform) + updateForeground(asdHandle.rect, fraction) } } - private fun updateForeground(bounds: RectangleF, fraction: Float) { - // (bounds.x, bounds.y) is the bottom-left anchor - val topRight = bounds.anchor(Anchor5.TopRight) - val rect = when (dir) { + 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, bounds.height, Direction4AD.QuadOne) + Rectangle.withDirection( + BOUNDS.x, BOUNDS.y, (BOUNDS.width / fraction).roundToInt(), BOUNDS.height, + Direction4AD.QuadOne, + ) Direction4A.XNeg -> // right to left - Rectangle.withDirection(topRight.x, bounds.y, bounds.width * fraction, bounds.height, Direction4AD.QuadTwo) + 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, Direction4AD.QuadOne) + Rectangle.withDirection( + BOUNDS.x, BOUNDS.y, BOUNDS.width, (BOUNDS.height / fraction).roundToInt(), + Direction4AD.QuadOne, + ) Direction4A.YNeg -> // top to bottom - Rectangle.withDirection(bounds.x, topRight.y, bounds.width, bounds.height * fraction, Direction4AD.QuadFour) - } - val anchor = rect.anchor(Anchor5.TopRight) - if (!fgInit) { - foreground = GuiRect(rect.x.toInt(), rect.y.toInt(), anchor.xi, anchor.yi, 122, 122, 122, 255) - fgComponent = GeomComponent(foreground) - fgInit = true - } else { - foreground.setPos(rect.x.toInt(), rect.y.toInt(), anchor.xi, anchor.yi) - } + Rectangle.withDirection( + BOUNDS.x, BOUNDS.height, BOUNDS.width, (BOUNDS.height / fraction).roundToInt(), + Direction4AD.QuadFour, + ) + }.toDouble(), rect).applyToGeneralTransform(fgTransform) } override fun render(renderSystem: RenderSystem) { - rect.value - bgComponent.render(renderSystem) - fgComponent.render(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 index ab0881e0..a93109a9 100644 --- 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 @@ -5,13 +5,17 @@ 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( - renderSystemHandle: RenderSystem.Handle, managerHandle: ScreenManager.Handle, - rect: ScreenManager.DelegatedRect, -) : Screen(managerHandle, rect) { + 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/gfx/GuiGeometry.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiGeometry.kt index 0858682e..6515e0ff 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiGeometry.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiGeometry.kt @@ -19,14 +19,14 @@ sealed class GuiGeometry(protected val geom: GeomDrawable) { 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 index 5227856e..67f869ad 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiSprite.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/GuiSprite.kt @@ -7,8 +7,8 @@ package net.terramodulus.mui.gui.gfx import net.terramodulus.engine.SpriteMesh -class GuiSprite(val rect: RectangleI, private val texture: UInt) { - private val mesh = SpriteMesh(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height) +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) diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ManagedRect.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ManagedRect.kt deleted file mode 100644 index f661b451..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ManagedRect.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025-2026 TerraModulus Team and Contributors - * SPDX-License-Identifier: LGPL-3.0-only - */ - -package net.terramodulus.mui.gui.gfx - -import kotlin.properties.Delegates.observable - -/** - * AGIM internally Managed Rectangle - */ -abstract class ManagedRect { - abstract val value: RectangleF - - protected val observers = LinkedHashSet<(RectangleF) -> Unit>() - - // What is this? - internal abstract fun setValue(rect: RectangleF) - - class Normal(rect: RectangleF) : ManagedRect() { - override var value: RectangleF by observable(rect) { _, _, newValue -> observers.forEach { it(newValue) } } - internal set - - override fun setValue(rect: RectangleF) { - value = rect - } - } - - internal fun observe(observer: (RectangleF) -> Unit) { - observers.add(observer) - } - - internal fun unobserve(observer: (RectangleF) -> Unit) { - observers.remove(observer) - } -} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/RenderSystem.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/RenderSystem.kt index 6091ae4c..d27de899 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/RenderSystem.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/RenderSystem.kt @@ -12,7 +12,9 @@ import net.terramodulus.core.getResourceAsString import net.terramodulus.engine.Canvas import net.terramodulus.engine.GeomDrawable import net.terramodulus.engine.MeshDrawable +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() @@ -27,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) { @@ -40,13 +50,10 @@ class RenderSystem internal constructor(private val core: TerraModulus, private } } - internal fun newGameplayScreen(pos: Vec3f) = { 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) From 4a63571b586ed97c8d786191de44da33ebb7c57c Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Mon, 24 Aug 2026 04:30:21 +0800 Subject: [PATCH 16/28] Project: Fix cargo build errors Somehow the previously used plugin generates issues (args like `-F` not passed), so a custom plugin is used instead --- build.gradle.kts | 35 +++++++++------- buildSrc/build.gradle.kts | 22 ++++++++++ buildSrc/src/main/kotlin/CargoExtension.kt | 12 ++++++ buildSrc/src/main/kotlin/CargoPlugin.kt | 20 +++++++++ buildSrc/src/main/kotlin/CargoTask.kt | 47 ++++++++++++++++++++++ ferricia | 2 +- 6 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/src/main/kotlin/CargoExtension.kt create mode 100644 buildSrc/src/main/kotlin/CargoPlugin.kt create mode 100644 buildSrc/src/main/kotlin/CargoTask.kt diff --git a/build.gradle.kts b/build.gradle.kts index adcac650..bec2e46f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { kotlin("jvm") version "2.3.21" kotlin("plugin.serialization") version "2.1.20" id("org.jetbrains.kotlinx.atomicfu") version "0.27.0" - id("io.github.arc-blroth.cargo-wrapper") version "1.0.0" apply false + id("net.terramodulus.plugins.cargo") apply false // id("fr.stardustenterprises.rust.wrapper") version "3.2.4" apply false application } @@ -18,30 +18,37 @@ repositories { project(":ferricia") { // Candidates: fr.stardustenterprises.rust.wrapper - apply(plugin = "io.github.arc-blroth.cargo-wrapper") + apply(plugin = "net.terramodulus.plugins.cargo") - if (providers.gradleProperty("release").isPresent) configure { - profile = "release" // use `-Prelease=true` + 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 - configure { - outputs = mapOf("" to System.mapLibraryName("ferricia")) + tasks.register("buildClient") { + args = listOf("-F", "client") + println(outputFile.get()) + } + tasks.register("buildServer") { + args = listOf("-F", "server") } configurations { create("client") { - configure { - arguments = listOf("-F", "client") - } + isCanBeConsumed = true + isCanBeResolved = false } create("server") { - configure { - arguments = listOf("-F", "server") - } + isCanBeConsumed = true + isCanBeResolved = false } } artifacts { - add("client", tasks.named("build")) - add("server", tasks.named("build")) + add("client", tasks.named("buildClient")) + add("server", tasks.named("buildServer")) } } 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..6a38f002 --- /dev/null +++ b/buildSrc/src/main/kotlin/CargoPlugin.kt @@ -0,0 +1,20 @@ +/* + * 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) + } + } +} 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 0798e3c6..73420e7d 160000 --- a/ferricia +++ b/ferricia @@ -1 +1 @@ -Subproject commit 0798e3c6760e5a30fc91cb7ec0a60169975eeaa7 +Subproject commit 73420e7d699a220be39edecea1d5fc4c4105018b From fea61576921d7483d7604abb07bc0e3a7532e70e Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:34:52 +0800 Subject: [PATCH 17/28] Fix errors --- buildSrc/src/main/kotlin/CargoPlugin.kt | 1 + ferricia | 2 +- .../net/terramodulus/mui/gui/agim/LayoutManager.kt | 9 +++++++-- .../terramodulus/mui/gui/agim/impl/GraphicsComponent.kt | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/buildSrc/src/main/kotlin/CargoPlugin.kt b/buildSrc/src/main/kotlin/CargoPlugin.kt index 6a38f002..843b7d61 100644 --- a/buildSrc/src/main/kotlin/CargoPlugin.kt +++ b/buildSrc/src/main/kotlin/CargoPlugin.kt @@ -15,6 +15,7 @@ class CargoPlugin : Plugin { target.tasks.withType(CargoTask::class.java).configureEach { release.convention(extension.release) outputFile.convention(extension.outputFile) + inputs.dir(project.projectDir) } } } diff --git a/ferricia b/ferricia index 73420e7d..b7dd53f4 160000 --- a/ferricia +++ b/ferricia @@ -1 +1 @@ -Subproject commit 73420e7d699a220be39edecea1d5fc4c4105018b +Subproject commit b7dd53f4c313650ace7d716441d01a624103d402 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 index ec640fd0..e4e24840 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt @@ -60,7 +60,8 @@ internal class LayoutManager(screenManager: ScreenManager) { 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 { it.key.layout.updated }.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 }, ) } @@ -294,7 +295,11 @@ internal class LayoutManager(screenManager: ScreenManager) { layouts.values.forEach { node -> node.layout.components.forEach { val dep = requireNotNull(depResults[it.asdHandle]) - val prev = it.asdHandle.rect + 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) 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 index 23ffb7f8..bd189193 100644 --- 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 @@ -24,7 +24,7 @@ class SpriteComponent(val sprite: GuiSprite, asdHandle: AsdHandle) : Component(a private val transform = GeneralTransform().apply { sprite.add(this) } init { - val dim = IntrinsicDimensionsProperty(asdHandle.rect.width.toUInt(), asdHandle.rect.height.toUInt()) + 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 { From 0b2644a198149396e89c717718c0e31f95481660 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Tue, 25 Aug 2026 11:46:07 +0800 Subject: [PATCH 18/28] MUI: Optimize world object creations Also add world init screen and more object sets in world --- ferricia | 2 +- .../kotlin/net/terramodulus/engine/Canvas.kt | 8 +- .../terramodulus/engine/WorldObjDrawable.kt | 20 ++- .../net/terramodulus/engine/ferricia/Gwr.kt | 17 +- .../net/terramodulus/engine/ferricia/Mui.kt | 4 +- .../mui/gui/agim/ScreenManager.kt | 7 +- .../mui/gui/agim/impl/GameplayScreen.kt | 33 ++-- .../gui/agim/impl/ResourceLoadingScreen.kt | 37 ++-- .../mui/gui/agim/impl/SimplePane.kt | 3 +- .../mui/gui/agim/impl/SizedPane.kt | 12 ++ .../mui/gui/agim/impl/SliderComponent.kt | 19 ++- .../mui/gui/agim/impl/WorldInitScreen.kt | 158 ++++++++++++++++++ .../kotlin/net/terramodulus/void/World.kt | 108 +++++++----- 13 files changed, 326 insertions(+), 102 deletions(-) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/WorldInitScreen.kt diff --git a/ferricia b/ferricia index b7dd53f4..074ccd87 160000 --- a/ferricia +++ b/ferricia @@ -1 +1 @@ -Subproject commit b7dd53f4c313650ace7d716441d01a624103d402 +Subproject commit 074ccd87f11b9a8b139bb8fc83b61e735a3f6e2b diff --git a/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt b/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt index 6354415e..8808235e 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt @@ -5,7 +5,6 @@ package net.terramodulus.engine -import com.cout970.math.vec4.Vec4i import net.terramodulus.engine.ferricia.Gwr import net.terramodulus.engine.ferricia.Gwr.drawGwrObj import net.terramodulus.engine.ferricia.Gwr.newMeshGeomCube @@ -31,6 +30,7 @@ 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) @@ -68,11 +68,9 @@ class Canvas internal constructor(private val windowHandle: ULong) : Closeable { internal fun newSpriteMesh(x0: Int, y0: Int, x1: Int, y1: Int) = newSpriteMesh(windowHandle, intArrayOf(x0, y0, x1, y1)) - internal fun newMeshGeomCube(width: Float, rgba: Vec4i) = - newMeshGeomCube(windowHandle, width, rgba.toArray()) + internal fun newMeshGeomCube(width: Float) = newMeshGeomCube(windowHandle, width) - internal fun newMeshGeomSphere(radius: Float, rgba: Vec4i) = - newMeshGeomSphere(windowHandle, radius, rgba.toArray()) + internal fun newMeshGeomSphere(radius: Float) = newMeshGeomSphere(windowHandle, radius) fun renderGuiGeo(drawable: GeomDrawable, programHandle: ULong) = drawGuiGeo(handle, drawable.handle, programHandle) diff --git a/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt b/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt index 96158285..0e29ea6f 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/WorldObjDrawable.kt @@ -8,11 +8,11 @@ package net.terramodulus.engine import com.cout970.math.quaternion.Quatd import com.cout970.math.vec3.Vec3d import com.cout970.math.vec4.Vec4i -import net.terramodulus.engine.ferricia.Gwr.newMeshGeomCube -import net.terramodulus.engine.ferricia.Gwr.newMeshGeomSphere +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: Quatd) { +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: Quatd) = @@ -38,8 +38,14 @@ sealed class WorldObjDrawable(internal val handle: ULong, private var pos: Vec3d } } -class SimpleMesh3dGeomCube(canvas: Canvas, width: Float, rgba: Vec4i, pos: Vec3d, scale: Vec3d, rot: Quatd) : - WorldObjDrawable(canvas.newMeshGeomCube(width, rgba), 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(canvas: Canvas, radius: Float, rgba: Vec4i, pos: Vec3d, scale: Vec3d, rot: Quatd) : - WorldObjDrawable(canvas.newMeshGeomSphere(radius, rgba), 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 0dd90336..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,6 +5,7 @@ package net.terramodulus.engine.ferricia +@OptIn(ExperimentalUnsignedTypes::class) internal object Gwr { /** * @param windowHandle window handle pointer @@ -40,20 +41,26 @@ internal object Gwr { /** * @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(windowHandle: ULong, 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(windowHandle: ULong, width: Float, data: IntArray): ULong + @JvmName("newDrawableWorldObj") + external fun newDrawableWorldObj(handle: ULong, data: IntArray): ULong /** * @param objHandle DrawableWorldObj handle pointer 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 7d1a95c3..532b2cf7 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt @@ -155,7 +155,7 @@ internal object Mui { /** * @param data `[sx, sy, angle, px, py]`; scaling, rotation, position - * @return GeneralTransform handle pointers + * @return GeneralTransform handle pointer and PrimModelTransform (wide) handle pointer */ @JvmName("modelGeneralTransform") external fun modelGeneralTransform(data: DoubleArray): ULongArray @@ -169,7 +169,7 @@ internal object Mui { /** * @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/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt index c2999ad4..cef87b73 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt @@ -66,7 +66,7 @@ class ScreenManager internal constructor( */ class Exit(val n: Int) : ScreenOperation { init { - require(n < 0) { "`n` < 1" } + require(n > 0) { "`n` < 1" } } override fun apply( @@ -238,8 +238,9 @@ class ScreenManager internal constructor( ) internal fun update(muiManager: MuiManager) { - screenOpQueue.forEach { it.apply(renderSystemHandle, screens, asdHandles) } - screenOpQueue.clear() + 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) } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt index 1b13e3fa..6542716f 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt @@ -74,14 +74,21 @@ internal class GameplayScreen( private val canvasHandle = renderSystemHandle.canvasHandle private lateinit var player: PlayerVoidGeom - override val layout: SingletonLayout + override val layout = + SingletonLayout(this, BlankComponent(ComponentAsdHandleImpl()), SingletonLayout.Config.Absolute.Full) init { renderSystemHandle.setBackgroundColor(0F, 0F, 0F, 0F) - core.world = World(Ymir()) - layout = SingletonLayout(this, GameplayRenderer(), SingletonLayout.Config.Absolute.Full) - addListener(ScreenEvent.Update::class.java) { - update0(it.muiIoI) + 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) @@ -104,16 +111,12 @@ internal class GameplayScreen( } 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( - canvasHandle.canvas, - 2F, - randomColor(), - ImmVec3d(x, y, z), - STD_SCALE, - IDENT_ROT, - ), - ImmVec3d(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)) { @@ -125,7 +128,7 @@ internal class GameplayScreen( override fun wrapChar(phyBody: PhyBody): VoidGeom { player = PlayerVoidGeom(phyBody, - SimpleMesh3dGeomSphere(canvasHandle.canvas, 1F, WHITE, ImmVec3d(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 } 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 index 8c649d0c..b5ee0888 100644 --- 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 @@ -21,8 +21,6 @@ import net.terramodulus.mui.gui.gfx.RenderSystem import kotlin.math.min import kotlin.properties.Delegates -private val CONTENT_SIZE = Dimension2I(400, 200) - private val BG_COLOR = floatArrayOf(.145F, .776F, 0.768F) private const val ANI_DURATION = 1F // in second @@ -46,19 +44,30 @@ class ResourceLoadingScreen( ), RectangleD(0.0, 0.0, 1.0, 1.0), ComponentAsdHandleImpl()).apply { geom.add(alphaFilter) }, SingletonLayout.Config.Absolute.Full)) - add(SingletonLayout(this@ResourceLoadingScreen, 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) + 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.ObjectFit.Contain, + SingletonLayout.Config.Relative.Simple(0.5), SingletonLayout.Config.AlignmentConfig.DEFAULT, ))) } 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 index 4137b804..6dfa52e0 100644 --- 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 @@ -10,7 +10,8 @@ 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, override var layout: Layout) : AbstractPane(asdHandle) { +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/SizedPane.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/SizedPane.kt index 51a66c8d..8c731449 100644 --- 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 @@ -42,6 +42,18 @@ class SizedPane(asdHandle: AsdHandle, component: Component, private var config: 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) + ) + }) })) })) } 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 index 38cf2dc2..bc867d03 100644 --- 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 @@ -5,10 +5,10 @@ package net.terramodulus.mui.gui.agim.impl -import com.cout970.math.vec3.Vec3i +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.Anchor5 +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 @@ -38,17 +38,19 @@ class SliderComponent( 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, 255, + 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, 255, + config.fgColor.x, config.fgColor.y, config.fgColor.z, config.bgColor.w, ).apply { add(fgTransform) } var fraction: Double by Delegates.observable(0.0) { _, _, value -> - updateForeground(asdHandle.rect, value) + try { + updateForeground(asdHandle.rect, value) + } catch (_: UninitializedPropertyAccessException) {} } - class Config(val bgColor: Vec3i, val fgColor: Vec3i) + class Config(val bgColor: Vec4i, val fgColor: Vec4i) init { asdHandle.observeRect { @@ -57,6 +59,11 @@ class SliderComponent( } } + 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) { 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/common/kotlin/net/terramodulus/void/World.kt b/src/kernel/common/kotlin/net/terramodulus/void/World.kt index 6c5d7d9f..1d3d1935 100644 --- a/src/kernel/common/kotlin/net/terramodulus/void/World.kt +++ b/src/kernel/common/kotlin/net/terramodulus/void/World.kt @@ -23,7 +23,7 @@ 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() @@ -48,6 +48,12 @@ 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 = ImmVec3d(0.0, -9.81, 0.0) floor.setBits(1u, 1u.inv()) @@ -61,26 +67,36 @@ class World(commander: Ymir) : Closeable { 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 { @@ -106,11 +122,13 @@ 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( ImmVec3d(1.0, 0.0, 0.0), @@ -120,32 +138,36 @@ class World(commander: Ymir) : Closeable { 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 = ImmVec3d(xx, 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 (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)) + } } } } From 653f170e696f802352b77118cf4e1b24415fc968 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:06:12 +0800 Subject: [PATCH 19/28] MUI: Add basic text rendering --- ferricia | 2 +- .../kotlin/net/terramodulus/engine/Canvas.kt | 8 ++ .../net/terramodulus/engine/Containers.kt | 3 + .../net/terramodulus/engine/FontManager.kt | 18 ++++ .../net/terramodulus/engine/GlyphManager.kt | 12 +++ .../net/terramodulus/engine/TextRenderer.kt | 21 +++++ .../engine/TextRenderingContext.kt | 41 +++++++++ .../net/terramodulus/engine/ferricia/Mui.kt | 87 +++++++++++++++++++ .../net/terramodulus/mui/gui/GuiManager.kt | 2 + .../mui/gui/agim/impl/TextDisplayComponent.kt | 36 ++++++++ .../mui/gui/agim/impl/WorldInitScreen.kt | 20 +++++ .../terramodulus/mui/gui/gfx/RenderSystem.kt | 23 +++++ .../terramodulus/mui/gui/gfx/TextContext.kt | 60 +++++++++++++ src/kernel/client/resources/gms_txt.fsh | 33 +++++++ .../terramodulus/util/LateInitObservable.kt | 27 ++++++ .../kotlin/net/terramodulus/void/World.kt | 3 +- 16 files changed, 394 insertions(+), 2 deletions(-) create mode 100644 src/internal/client/kotlin/net/terramodulus/engine/FontManager.kt create mode 100644 src/internal/client/kotlin/net/terramodulus/engine/GlyphManager.kt create mode 100644 src/internal/client/kotlin/net/terramodulus/engine/TextRenderer.kt create mode 100644 src/internal/client/kotlin/net/terramodulus/engine/TextRenderingContext.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TextDisplayComponent.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/TextContext.kt create mode 100644 src/kernel/client/resources/gms_txt.fsh create mode 100644 src/kernel/common/kotlin/net/terramodulus/util/LateInitObservable.kt diff --git a/ferricia b/ferricia index 074ccd87..0294283b 160000 --- a/ferricia +++ b/ferricia @@ -1 +1 @@ -Subproject commit 074ccd87f11b9a8b139bb8fc83b61e735a3f6e2b +Subproject commit 0294283b87afbf05c66baee35b629fc60fa77288 diff --git a/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt b/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt index 8808235e..a5a765df 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Canvas.kt @@ -21,6 +21,7 @@ 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.newTxtProgram import net.terramodulus.engine.ferricia.Mui.setCanvasClearColor import net.terramodulus.engine.ferricia.Mui.texShaders import java.io.Closeable @@ -51,6 +52,8 @@ class Canvas internal constructor(private val windowHandle: ULong) : Closeable { return camera3D!! } + fun newGlyphManager(manager: FontManager) = manager.newGlyphManager(windowHandle) + fun loadImage(data: ByteArray) = loadImageToCanvas(handle, data) fun loadGeoShaders(vsh: String, fsh: String) = geoShaders(windowHandle, vsh, fsh) @@ -59,6 +62,11 @@ class Canvas internal constructor(private val windowHandle: ULong) : Closeable { fun loadTexShaders(vsh: String, fsh: String) = texShaders(windowHandle, vsh, fsh) + fun loadTxtShaders(vsh: String, fsh: String) = newTxtProgram(windowHandle, vsh, fsh) + + fun newTextRenderer(geoProgramHandle: ULong, txtProgramHandle: ULong) = + TextRenderer(windowHandle, geoProgramHandle, txtProgramHandle) + 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)) diff --git a/src/internal/client/kotlin/net/terramodulus/engine/Containers.kt b/src/internal/client/kotlin/net/terramodulus/engine/Containers.kt index 03d6cacf..c08aeab7 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Containers.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Containers.kt @@ -5,6 +5,9 @@ package net.terramodulus.engine +import com.cout970.math.vec2.Vec2f import com.cout970.math.vec4.Vec4i fun Vec4i.toArray() = intArrayOf(x, y, z, w) + +fun Vec2f.toArray() = floatArrayOf(x, y) diff --git a/src/internal/client/kotlin/net/terramodulus/engine/FontManager.kt b/src/internal/client/kotlin/net/terramodulus/engine/FontManager.kt new file mode 100644 index 00000000..a51a5d3d --- /dev/null +++ b/src/internal/client/kotlin/net/terramodulus/engine/FontManager.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.engine + +import com.cout970.math.vec4.Vec4i +import net.terramodulus.engine.ferricia.Mui.newFontManager + +class FontManager { + internal val handle = newFontManager() + + internal fun newGlyphManager(windowHandle: ULong) = GlyphManager(handle, windowHandle) + + fun newTextRenderingManager(fontSize: Float, lineHeight: Float, color: Vec4i) = + TextRenderingContext(handle, fontSize, lineHeight, color) +} diff --git a/src/internal/client/kotlin/net/terramodulus/engine/GlyphManager.kt b/src/internal/client/kotlin/net/terramodulus/engine/GlyphManager.kt new file mode 100644 index 00000000..76383f24 --- /dev/null +++ b/src/internal/client/kotlin/net/terramodulus/engine/GlyphManager.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.engine + +import net.terramodulus.engine.ferricia.Mui.newGlyphManager + +class GlyphManager internal constructor(managerHandle: ULong, windowHandle: ULong) { + internal val handle = newGlyphManager(managerHandle, windowHandle) +} diff --git a/src/internal/client/kotlin/net/terramodulus/engine/TextRenderer.kt b/src/internal/client/kotlin/net/terramodulus/engine/TextRenderer.kt new file mode 100644 index 00000000..be3e7ccb --- /dev/null +++ b/src/internal/client/kotlin/net/terramodulus/engine/TextRenderer.kt @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.engine + +import com.cout970.math.vec2.Vec2f +import net.terramodulus.engine.ferricia.Mui.newTextRenderer + +class TextRenderer internal constructor(windowHandle: ULong, geoProgramHandle: ULong, txtProgramHandle: ULong) { + private val handle = newTextRenderer(windowHandle, geoProgramHandle, txtProgramHandle) + + fun renderText( + ctx: TextRenderingContext, + canvas: Canvas, + glyphManager: GlyphManager, + fontManager: FontManager, + pos: Vec2f, + ) = ctx.render(canvas.handle, glyphManager.handle, handle, fontManager.handle, pos) +} diff --git a/src/internal/client/kotlin/net/terramodulus/engine/TextRenderingContext.kt b/src/internal/client/kotlin/net/terramodulus/engine/TextRenderingContext.kt new file mode 100644 index 00000000..17e6b370 --- /dev/null +++ b/src/internal/client/kotlin/net/terramodulus/engine/TextRenderingContext.kt @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.engine + +import com.cout970.math.vec2.Vec2f +import com.cout970.math.vec4.Vec4i +import net.terramodulus.engine.ferricia.Mui.newTextRenderingContext +import net.terramodulus.engine.ferricia.Mui.renderText +import net.terramodulus.engine.ferricia.Mui.setTextRenderingContextColor +import net.terramodulus.engine.ferricia.Mui.setTextRenderingContextMetrics +import net.terramodulus.engine.ferricia.Mui.setTextRenderingContextSize +import net.terramodulus.engine.ferricia.Mui.setTextRenderingContextText + +class TextRenderingContext internal constructor( + managerHandle: ULong, + fontSize: Float, + lineHeight: Float, + color: Vec4i, +) { + private val handle = newTextRenderingContext(managerHandle, floatArrayOf(fontSize, lineHeight), color.toArray()) + + fun setColor(color: Vec4i) = setTextRenderingContextColor(handle, color.toArray()) + + fun setMetrics(fontSize: Float, lineHeight: Float,) = + setTextRenderingContextMetrics(handle, floatArrayOf(fontSize, lineHeight)) + + fun setSize(width: Float, height: Float) = setTextRenderingContextSize(handle, floatArrayOf(width, height)) + + fun setText(text: String) = setTextRenderingContextText(handle, text) + + internal fun render( + canvasHandle: ULong, + glyphMgrHandle: ULong, + rendererHandle: ULong, + fontMgrHandle: ULong, + pos: Vec2f, + ) = renderText(canvasHandle, glyphMgrHandle, rendererHandle, fontMgrHandle, handle, pos.toArray()) +} 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 532b2cf7..d1c01040 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt @@ -224,4 +224,91 @@ internal object Mui { */ @JvmName("drawGuiTex") external fun drawGuiTex(canvasHandle: ULong, drawableHandle: ULong, programHandle: ULong, textureHandle: UInt) + + /** + * @return FontManager handle pointer + */ + @JvmName("newFontManager") + external fun newFontManager(): ULong + + /** + * @param managerHandle FontManager handle pointer + * @param windowHandle Window handle pointer + * @return GlyphManager handle pointer + */ + @JvmName("newGlyphManager") + external fun newGlyphManager(managerHandle: ULong, windowHandle: ULong): ULong + + /** + * @param windowHandle Window handle pointer + * @param vsh Window handle pointer + * @param fsh Window handle pointer + * @return TxtProgram handle pointer + */ + @JvmName("newTxtProgram") + external fun newTxtProgram(windowHandle: ULong, vsh: String, fsh: String): ULong + + /** + * @param windowHandle Window handle pointer + * @param geoProgramHandle GeoProgram handle pointer + * @param txtProgramHandle TxtProgram handle pointer + * @return TextRenderer handle pointer + */ + @JvmName("newTextRenderer") + external fun newTextRenderer(windowHandle: ULong, geoProgramHandle: ULong, txtProgramHandle: ULong): ULong + + /** + * @param managerHandle FontManager handle pointer + * @param data1 Font size and line height in pixels + * @param data2 `[r, g, b, a]` in [0,255] + * @return TextRenderingContext handle pointer + */ + @JvmName("newTextRenderingContext") + external fun newTextRenderingContext(managerHandle: ULong, data1: FloatArray, data2: IntArray): ULong + + /** + * @param ctxHandle TextRenderingContext handle pointer + * @param data `[r, g, b, a]` in [0,255] + */ + @JvmName("setTextRenderingContextColor") + external fun setTextRenderingContextColor(ctxHandle: ULong, data: IntArray) + + /** + * @param ctxHandle TextRenderingContext handle pointer + * @param data Font size and line height in pixels + */ + @JvmName("setTextRenderingContextMetrics") + external fun setTextRenderingContextMetrics(ctxHandle: ULong, data: FloatArray) + + /** + * @param ctxHandle TextRenderingContext handle pointer + * @param data Width and height in pixels + */ + @JvmName("setTextRenderingContextSize") + external fun setTextRenderingContextSize(ctxHandle: ULong, data: FloatArray) + + /** + * @param ctxHandle TextRenderingContext handle pointer + * @param text Contents of entire text widget + */ + @JvmName("setTextRenderingContextText") + external fun setTextRenderingContextText(ctxHandle: ULong, text: String) + + /** + * @param canvasHandle Canvas handle pointer + * @param glyphMgrHandle GlyphManager handle pointer + * @param rendererHandle TextRenderer handle pointer + * @param fontMgrHandle FontManager handle pointer + * @param ctxHandle TextRenderingContext handle pointer + * @param data `[x, y]` Position + */ + @JvmName("renderText") + external fun renderText( + canvasHandle: ULong, + glyphMgrHandle: ULong, + rendererHandle: ULong, + fontMgrHandle: ULong, + ctxHandle: ULong, + data: FloatArray, + ) } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt index 651c764b..6cd95835 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt @@ -6,6 +6,8 @@ package net.terramodulus.mui.gui import net.terramodulus.core.TerraModulus +import net.terramodulus.engine.FontManager +import net.terramodulus.engine.GlyphManager import net.terramodulus.engine.Window import net.terramodulus.mui.MuiManager import net.terramodulus.mui.gui.agim.LayoutManager diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TextDisplayComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TextDisplayComponent.kt new file mode 100644 index 00000000..3aae112f --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TextDisplayComponent.kt @@ -0,0 +1,36 @@ +/* + * 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.Component +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.gui.gfx.TextContext +import net.terramodulus.util.lateInitObservable + +class TextDisplayComponent( + asdHandle: AsdHandle, + renderSystemHandle: RenderSystem.Handle, + config: TextContext.Config, +) : Component(asdHandle) { + private val context = TextContext(renderSystemHandle, config) + var text: String by lateInitObservable { _, _, new -> + context.setText(new) + } + + init { + asdHandle.observeRect { + context.update(asdHandle.rect) + println(asdHandle.rect) + } + } + + fun update(operation: TextContext.ConfigEnv.() -> Unit) = context.update(operation) + + override fun render(renderSystem: RenderSystem) { + context.render() + } +} 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 index a36c277c..e6578ab6 100644 --- 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 @@ -18,6 +18,7 @@ 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.mui.gui.gfx.TextContext import net.terramodulus.void.World private const val ANI_DURATION = 1F // in second @@ -92,6 +93,25 @@ class WorldInitScreen internal constructor( SingletonLayout.Config.Relative.Simple(0.5), SingletonLayout.Config.AlignmentConfig.DEFAULT, ))) + add(SingletonLayout(this@WorldInitScreen, SimplePane(ComponentAsdHandleImpl()) { + SingletonLayout(this, SizedPane(ComponentAsdHandleImpl(), SimplePane(ComponentAsdHandleImpl()) { + SingletonLayout(this, TextDisplayComponent( + ComponentAsdHandleImpl(), + renderSystemHandle, + TextContext.Config(16.0F, 16.0F, ImmVec4i(255)), + ).apply { + text = "Demo Test 123 ABC" + }, SingletonLayout.Config.Absolute.Insets(InsetsD(7.0, 149.0, 7.0, 35.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) { diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/RenderSystem.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/RenderSystem.kt index d27de899..a03ae08f 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/RenderSystem.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/RenderSystem.kt @@ -5,19 +5,26 @@ package net.terramodulus.mui.gui.gfx +import com.cout970.math.vec2.Vec2f import com.cout970.math.vec3.Vec3f +import com.cout970.math.vec4.Vec4i import net.terramodulus.core.TerraModulus import net.terramodulus.core.getResourceAsBytes import net.terramodulus.core.getResourceAsString import net.terramodulus.engine.Canvas +import net.terramodulus.engine.FontManager import net.terramodulus.engine.GeomDrawable import net.terramodulus.engine.MeshDrawable +import net.terramodulus.engine.TextRenderingContext 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() + // In production, initialization of fonts shall be deferred to session of resource loading. + private val fontManager = FontManager() + private val glyphManager = canvas.newGlyphManager(fontManager) private val texShaders = canvas.loadTexShaders( getResourceAsString("/gms_tex.vsh"), getResourceAsString("/gms_tex.fsh") @@ -26,6 +33,11 @@ class RenderSystem internal constructor(private val core: TerraModulus, private getResourceAsString("/gms_geo.vsh"), getResourceAsString("/gms_geo.fsh") ) + private val txtShaders = canvas.loadTxtShaders( + getResourceAsString("/gms_tex.vsh"), + getResourceAsString("/gms_txt.fsh") + ) + private val textRenderer = canvas.newTextRenderer(geoShaders, txtShaders) val targetFps = 1000; sealed interface Handle { @@ -34,6 +46,10 @@ class RenderSystem internal constructor(private val core: TerraModulus, private fun loadTexture(path: String): UInt fun setBackgroundColor(red: Float, green: Float, blue: Float, alpha: Float) + + fun renderText(ctx: TextRenderingContext, pos: Vec2f) + + fun newTextRenderingContext(fontSize: Float, lineHeight: Float, color: Vec4i): TextRenderingContext } inner class CanvasHandle internal constructor() { @@ -48,6 +64,13 @@ class RenderSystem internal constructor(private val core: TerraModulus, private override fun setBackgroundColor(red: Float, green: Float, blue: Float, alpha: Float) { canvas.setClearColor(red, green, blue, alpha) } + + override fun renderText(ctx: TextRenderingContext, pos: Vec2f) { + textRenderer.renderText(ctx, canvas, glyphManager, fontManager, pos) + } + + override fun newTextRenderingContext(fontSize: Float, lineHeight: Float, color: Vec4i) = + fontManager.newTextRenderingManager(fontSize, lineHeight, color) } internal fun newGameplayScreen(pos: Vec3f) = diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/TextContext.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/TextContext.kt new file mode 100644 index 00000000..bc1fe9a8 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/TextContext.kt @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 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.Vec2d +import com.cout970.math.vec2.toImmVec2f +import com.cout970.math.vec4.Vec4i +import kotlin.properties.Delegates + +class TextContext(private val renderSystemHandle: RenderSystem.Handle, private var config: Config) { + private val context = renderSystemHandle.newTextRenderingContext(config.fontSize, config.lineHeight, config.color) + private lateinit var size: Dimension2D + private lateinit var pos: Vec2d + + data class Config(val fontSize: Float, val lineHeight: Float, val color: Vec4i) + + interface ConfigEnv { + var fontSize: Float + var lineHeight: Float + var color: Vec4i + } + + fun update(operation: ConfigEnv.() -> Unit) { + object : ConfigEnv { + var fontSizeChanged = false + override var fontSize: Float by Delegates.observable(config.fontSize) { _, _, _ -> + fontSizeChanged = true + } + var lineHeightChanged = false + override var lineHeight: Float by Delegates.observable(config.lineHeight) { _, _, _ -> + lineHeightChanged = true + } + var colorChanged = false + override var color: Vec4i by Delegates.observable(config.color) { _, _, _ -> + colorChanged = true + } + }.apply(operation).apply { + if (fontSizeChanged || lineHeightChanged) context.setMetrics(fontSize, lineHeight) + if (colorChanged) context.setColor(color) + config = Config(fontSize, lineHeight, color) + } + } + + fun update(rect: RectangleD) { + val prevSize = try { size } catch (_: UninitializedPropertyAccessException) { null } + val prevPos = try { pos } catch (_: UninitializedPropertyAccessException) { null } + if (prevSize == null || prevSize.width != rect.width || prevSize.height != rect.height) + size = Dimension2D(rect.width, rect.height) + if (prevPos == null || prevPos.x != rect.x || prevPos.y != rect.y) + pos = ImmVec2d(rect.x, rect.y) + } + + fun setText(text: String) = context.setText(text) + + fun render() = renderSystemHandle.renderText(context, pos.toImmVec2f()) +} diff --git a/src/kernel/client/resources/gms_txt.fsh b/src/kernel/client/resources/gms_txt.fsh new file mode 100644 index 00000000..3ea08399 --- /dev/null +++ b/src/kernel/client/resources/gms_txt.fsh @@ -0,0 +1,33 @@ +#version 110 + +varying vec2 texCoord; + +uniform sampler2D msdfTex; // The generated MSDF atlas texture +uniform vec4 textColor; // Desired text color + +// Helper function to extract the median of the RGB channels +float median(vec3 rgb) { + return max(min(rgb.r, rgb.g), min(max(rgb.r, rgb.g), rgb.b)); +} + +float smoothstep_110(float edge0, float edge1, float x) { + float t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); + return t * t * (3.0 - 2.0 * t); +} + +void main() { + // 1. Sample the multi-channel signed distance field texture + vec3 msdfSample = texture2D(msdfTex, texCoord).rgb; + + // 2. Extract the pseudo-distance by taking the median + float sd = median(msdfSample); + + // 3. Screen-space derivatives ensure perfect antialiasing at any scale + float pixelWindow = fwidth(sd); + float opacity = smoothstep_110(0.5 - pixelWindow, 0.5 + pixelWindow, sd); + + // 4. Discard invisible pixels or mix color based on opacity + if (opacity < 0.001) discard; + + gl_FragColor = vec4(textColor.rgb, textColor.a * opacity); +} diff --git a/src/kernel/common/kotlin/net/terramodulus/util/LateInitObservable.kt b/src/kernel/common/kotlin/net/terramodulus/util/LateInitObservable.kt new file mode 100644 index 00000000..9f8dd70d --- /dev/null +++ b/src/kernel/common/kotlin/net/terramodulus/util/LateInitObservable.kt @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.util + +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +class LateInitObservable( + private val onChange: (prop: KProperty<*>, old: T?, new: T) -> Unit +) : ReadWriteProperty { + private var value: T? = null + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return value ?: throw IllegalStateException("Property ${property.name} is not initialized.") + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + val oldValue = this.value + this.value = value + onChange(property, oldValue, value) + } +} + +fun lateInitObservable(onChange: (prop: KProperty<*>, old: T?, new: T) -> Unit) = LateInitObservable(onChange) diff --git a/src/kernel/common/kotlin/net/terramodulus/void/World.kt b/src/kernel/common/kotlin/net/terramodulus/void/World.kt index 1d3d1935..5b0d11e9 100644 --- a/src/kernel/common/kotlin/net/terramodulus/void/World.kt +++ b/src/kernel/common/kotlin/net/terramodulus/void/World.kt @@ -125,7 +125,8 @@ class World(commander: Ymir, progressBar: ProgressBar) : Closeable { private fun randomCubes(commander: Ymir, progressBar: ProgressBar): ArrayList { val list = ArrayList() var i = 0 - val radius = 12 +// val radius = 12 + val radius = 5 val total = radius * radius * 2 * 2 * 7 val intervalHor = 5.0 val intervalVert = 8 From 11e0350ca127c02f75e66f9a6d9a20b9674a0834 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Fri, 4 Sep 2026 00:39:14 +0800 Subject: [PATCH 20/28] Render: Tweak text rendering --- .idea/jarRepositories.xml | 25 +++++++++++++++ .idea/modules.xml | 10 ------ ferricia | 2 +- .../mui/gui/agim/impl/WorldInitScreen.kt | 2 +- src/kernel/client/resources/gms_txt.fsh | 31 +++++++++---------- 5 files changed, 41 insertions(+), 29 deletions(-) create mode 100644 .idea/jarRepositories.xml delete mode 100644 .idea/modules.xml diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 00000000..e40fc1cc --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index ba62ca49..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/ferricia b/ferricia index 0294283b..ffe0ed1b 160000 --- a/ferricia +++ b/ferricia @@ -1 +1 @@ -Subproject commit 0294283b87afbf05c66baee35b629fc60fa77288 +Subproject commit ffe0ed1b6a4f3c8538847336538a5102b75592a8 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 index e6578ab6..c92f1f24 100644 --- 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 @@ -100,7 +100,7 @@ class WorldInitScreen internal constructor( renderSystemHandle, TextContext.Config(16.0F, 16.0F, ImmVec4i(255)), ).apply { - text = "Demo Test 123 ABC" + text = "Demo Test 123 ABC MONWQK1V" }, SingletonLayout.Config.Absolute.Insets(InsetsD(7.0, 149.0, 7.0, 35.0))) }, SizedPane.Config(400u, 200u)), SingletonLayout.Config.Aligned( diff --git a/src/kernel/client/resources/gms_txt.fsh b/src/kernel/client/resources/gms_txt.fsh index 3ea08399..a3e62a16 100644 --- a/src/kernel/client/resources/gms_txt.fsh +++ b/src/kernel/client/resources/gms_txt.fsh @@ -4,30 +4,27 @@ varying vec2 texCoord; uniform sampler2D msdfTex; // The generated MSDF atlas texture uniform vec4 textColor; // Desired text color +uniform vec2 texSize; -// Helper function to extract the median of the RGB channels -float median(vec3 rgb) { - return max(min(rgb.r, rgb.g), min(max(rgb.r, rgb.g), rgb.b)); +float median(float r, float g, float b) { + return max(min(r, g), min(max(r, g), b)); } -float smoothstep_110(float edge0, float edge1, float x) { - float t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); - return t * t * (3.0 - 2.0 * t); -} +#define DISTANCE_RANGE 3.0 +#define FRACTION_1_64 0.015625 +// Reference: https://github.com/Blatko1/awesome-msdf +// Reference: https://medium.com/@sihaolu/performant-crisp-text-rendering-in-metal-with-multi-channel-signed-distance-field-msdf-9acd634d0052 void main() { - // 1. Sample the multi-channel signed distance field texture - vec3 msdfSample = texture2D(msdfTex, texCoord).rgb; - - // 2. Extract the pseudo-distance by taking the median - float sd = median(msdfSample); + vec3 texel = texture2D(msdfTex, texCoord).rgb; + float dist = median(texel.r, texel.g, texel.b); - // 3. Screen-space derivatives ensure perfect antialiasing at any scale - float pixelWindow = fwidth(sd); - float opacity = smoothstep_110(0.5 - pixelWindow, 0.5 + pixelWindow, sd); + vec2 screenTexSize = 1.0 / fwidth(texCoord); + float screenPxRange = max(0.5 * dot(vec2(DISTANCE_RANGE) / texSize, screenTexSize), 1.0); + float pxDist = screenPxRange * (dist - 0.5); - // 4. Discard invisible pixels or mix color based on opacity - if (opacity < 0.001) discard; + float opacity = clamp(pxDist + 0.5, 0.0, 1.0); + if (opacity < FRACTION_1_64) discard; gl_FragColor = vec4(textColor.rgb, textColor.a * opacity); } From a3b61e258ee569406fcf7e868a461f320d5b5ea9 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:30:34 +0800 Subject: [PATCH 21/28] MUI: Updating structures Improved GUI screens Adding mouse events --- .../kotlin/net/terramodulus/mui/MuiManager.kt | 1 + .../mui/gui/agim/event/BubblingEvent.kt | 21 +++++ .../mui/gui/agim/event/ComponentEvent.kt | 2 +- .../mui/gui/agim/event/GenericEvent.kt | 11 ++- .../mui/gui/agim/event/GenericEvents.kt | 26 ++++++ .../mui/gui/agim/event/MenuEvent.kt | 1 + .../mui/gui/agim/event/ScreenEvent.kt | 7 ++ .../gui/agim/impl/ResourceLoadingScreen.kt | 5 +- .../mui/gui/agim/impl/WorldInitScreen.kt | 15 +++- .../net/terramodulus/mui/kui/InputSystem.kt | 90 ++++++++++--------- 10 files changed, 132 insertions(+), 47 deletions(-) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/BubblingEvent.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvents.kt diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt index a7b7ba86..437d2d0b 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt @@ -137,6 +137,7 @@ internal class MuiManager internal constructor(core: TerraModulus) : Closeable { } is MuiEvent.MouseButtonDown -> { logger.debug { "Mouse (id: ${event.mouseId}) key `${event.key}` down." } + guiManager } is MuiEvent.MouseButtonUp -> { logger.debug { "Mouse (id: ${event.mouseId}) key `${event.key}` up." } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/BubblingEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/BubblingEvent.kt new file mode 100644 index 00000000..494b762b --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/BubblingEvent.kt @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.event + +sealed interface BubblingEvent { + val bubbles: Boolean + + fun stopPropagation() +} + +internal class BubblingEventImpl : BubblingEvent { + override var bubbles: Boolean = true + private set + + override fun stopPropagation() { + bubbles = false + } +} 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 index bd5ddd73..d8fc1f09 100644 --- 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 @@ -9,5 +9,5 @@ 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 + data class Generic(val generic: T) : 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 index 7316c3ee..bf70fc1a 100644 --- 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 @@ -5,8 +5,13 @@ package net.terramodulus.mui.gui.agim.event -sealed class GenericEvent { - var bubble = true +// Must not be used in kernel +abstract class GenericEvent : BubblingEvent by BubblingEventImpl() { + override fun equals(other: Any?): Boolean { + return this === other + } - data object Key : GenericEvent() + override fun hashCode(): Int { + return System.identityHashCode(this) + } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvents.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvents.kt new file mode 100644 index 00000000..0ac2d441 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvents.kt @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui.agim.event + +import com.cout970.math.vec2.Vec2d +import net.terramodulus.mui.kui.InputSystem + +object GenericEvents { + abstract class MouseEnter internal constructor() + abstract class MouseLeave internal constructor() + abstract class MouseMove internal constructor() { + abstract val a: Vec2d + abstract val b: Vec2d + } + abstract class MouseDown internal constructor() { + abstract val pos: Vec2d + abstract val keyId: InputSystem.KeyId + } + abstract class MouseUp internal constructor() { + abstract val pos: Vec2d + abstract val keyId: InputSystem.KeyId + } +} 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 index 0e22e339..de00b7e3 100644 --- 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 @@ -10,4 +10,5 @@ import net.terramodulus.mui.gui.agim.ScreenManager sealed interface MenuEvent { data class Update(val muiIoI: ScreenManager.MuiIoI) : MenuEvent data object Close : MenuEvent + data class Generic(val generic: T) : 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 index bf79e7cd..12022ba1 100644 --- 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 @@ -5,9 +5,16 @@ package net.terramodulus.mui.gui.agim.event +import com.cout970.math.vec2.Vec2d import net.terramodulus.mui.gui.agim.ScreenManager +import net.terramodulus.mui.kui.InputSystem sealed interface ScreenEvent { data class Update(val muiIoI: ScreenManager.MuiIoI) : ScreenEvent + data class MouseDown(override val pos: Vec2d, override val keyId: InputSystem.KeyId) : + ScreenEvent, GenericEvents.MouseDown(), BubblingEvent by BubblingEventImpl() + data class MouseUp(override val pos: Vec2d, override val keyId: InputSystem.KeyId) : + ScreenEvent, GenericEvents.MouseUp(), BubblingEvent by BubblingEventImpl() data object Close : ScreenEvent + data class Generic(val generic: T) : ScreenEvent } 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 index b5ee0888..9852c64a 100644 --- 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 @@ -19,6 +19,7 @@ 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.math.pow import kotlin.properties.Delegates private val BG_COLOR = floatArrayOf(.145F, .776F, 0.768F) @@ -86,7 +87,9 @@ class ResourceLoadingScreen( 1 -> { // TODO when there is something to load, stay at this stage until ready - progressBar.progress = min(elapsed / PAUSE_DURATION, 1F) + val x = elapsed / PAUSE_DURATION + // S-curve animation, but this will not look good if speed is not constant + progressBar.progress = min(1 - (1 - x.pow(3.5F)).pow(12), 1F) if (progressBar.progress >= 1F) { stage = 2 last = current 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 index c92f1f24..f477684f 100644 --- 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 @@ -20,6 +20,7 @@ import net.terramodulus.mui.gui.gfx.RectangleI import net.terramodulus.mui.gui.gfx.RenderSystem import net.terramodulus.mui.gui.gfx.TextContext import net.terramodulus.void.World +import kotlin.math.roundToInt private const val ANI_DURATION = 1F // in second @@ -44,6 +45,7 @@ class WorldInitScreen internal constructor( override val layout = CompositeLayout(this) private val progressBarImpl = ProgressBarImpl() internal val progressBar: World.ProgressBar = progressBarImpl + private var onAlphaChange: () -> Unit = {} init { layout.update { @@ -98,9 +100,14 @@ class WorldInitScreen internal constructor( SingletonLayout(this, TextDisplayComponent( ComponentAsdHandleImpl(), renderSystemHandle, - TextContext.Config(16.0F, 16.0F, ImmVec4i(255)), + TextContext.Config(16.0F, 16.0F, ImmVec4i(255, 255, 255, 0)), ).apply { - text = "Demo Test 123 ABC MONWQK1V" + text = "Initializing Demo World..." + onAlphaChange = { + update { + color = ImmVec4i(255, 255, 255, (alphaFilter.alpha * 255).roundToInt()) + } + } }, SingletonLayout.Config.Absolute.Insets(InsetsD(7.0, 149.0, 7.0, 35.0))) }, SizedPane.Config(400u, 200u)), SingletonLayout.Config.Aligned( @@ -122,9 +129,11 @@ class WorldInitScreen internal constructor( stage = 1 last = current alphaFilter.alpha = 1F + onAlphaChange() progressBarImpl.ready = true } else { alphaFilter.alpha = elapsed / ANI_DURATION + onAlphaChange() } 1 -> { @@ -139,8 +148,10 @@ class WorldInitScreen internal constructor( stage = 3 last = current alphaFilter.alpha = 0F + onAlphaChange() } else { alphaFilter.alpha = 1 - elapsed / ANI_DURATION + onAlphaChange() } // 3 -> screenManager.handle.openBefore(::TitleScreen, this) diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/kui/InputSystem.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/InputSystem.kt index d9f8b3e6..52499a01 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/kui/InputSystem.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/InputSystem.kt @@ -9,8 +9,22 @@ package net.terramodulus.mui.kui class InputSystem internal constructor() { private val keys = HashMap() + sealed interface Keys { + fun down(): KeyPredicate.Down + fun justDown(): KeyPredicate.JustDown + fun justUp(): KeyPredicate.JustUp + fun matches(other: KeyId): Boolean + } + + private class KeysImpl(private val id: KeyId) : Keys { + override fun down() = KeyPredicate.Down(id) + override fun justDown() = KeyPredicate.JustDown(id) + override fun justUp() = KeyPredicate.JustUp(id) + override fun matches(other: KeyId) = id == other + } + // Values refer to ferricia::mui::KeyboardKey - enum class Keys(private val id: KeyId) { + enum class KeyboardKeys(private val id: KeyId) : Keys by KeysImpl(id) { A(KeyId(0u)), B(KeyId(1u)), C(KeyId(2u)), @@ -41,10 +55,6 @@ class InputSystem internal constructor() { Minus(KeyId(41u)), Equals(KeyId(42u)), LShift(KeyId(205u)), - ; - fun down() = KeyPredicate.Down(id) - fun justDown() = KeyPredicate.JustDown(id) - fun justUp() = KeyPredicate.JustUp(id) } sealed class KeyPredicate { @@ -84,40 +94,38 @@ class InputSystem internal constructor() { infix fun or(other: KeyPredicate) = Or(this, other) } - private val keysScope = KeysScope() - // This class may be programmatically generated - inner class KeysScope internal constructor() { - val A = Keys.A - val B = Keys.B - val C = Keys.C - val D = Keys.D - val E = Keys.E - val F = Keys.F - val G = Keys.G - val H = Keys.H - val I = Keys.I - val J = Keys.J - val K = Keys.K - val L = Keys.L - val M = Keys.M - val N = Keys.N - val O = Keys.O - val P = Keys.P - val Q = Keys.Q - val R = Keys.R - val S = Keys.S - val T = Keys.T - val U = Keys.U - val V = Keys.V - val W = Keys.W - val X = Keys.X - val Y = Keys.Y - val Z = Keys.Z - val Space = Keys.Space - val Minus = Keys.Minus - val Equals = Keys.Equals - val LShift = Keys.LShift + object KeysScope { + val A = KeyboardKeys.A + val B = KeyboardKeys.B + val C = KeyboardKeys.C + val D = KeyboardKeys.D + val E = KeyboardKeys.E + val F = KeyboardKeys.F + val G = KeyboardKeys.G + val H = KeyboardKeys.H + val I = KeyboardKeys.I + val J = KeyboardKeys.J + val K = KeyboardKeys.K + val L = KeyboardKeys.L + val M = KeyboardKeys.M + val N = KeyboardKeys.N + val O = KeyboardKeys.O + val P = KeyboardKeys.P + val Q = KeyboardKeys.Q + val R = KeyboardKeys.R + val S = KeyboardKeys.S + val T = KeyboardKeys.T + val U = KeyboardKeys.U + val V = KeyboardKeys.V + val W = KeyboardKeys.W + val X = KeyboardKeys.X + val Y = KeyboardKeys.Y + val Z = KeyboardKeys.Z + val Space = KeyboardKeys.Space + val Minus = KeyboardKeys.Minus + val Equals = KeyboardKeys.Equals + val LShift = KeyboardKeys.LShift } init { @@ -126,7 +134,9 @@ class InputSystem internal constructor() { } @JvmInline - value class KeyId(private val id: UInt) + value class KeyId(private val id: UInt) { + fun matches(other: Keys) = other.matches(this) + } class Key { var down = false @@ -137,7 +147,7 @@ class InputSystem internal constructor() { fun isJustUp() = !down && justChanged } - fun condition(predicate: KeysScope.() -> KeyPredicate) = keysScope.predicate().test(KeyPredicate.Helper(keys)) + fun condition(predicate: KeysScope.() -> KeyPredicate) = KeysScope.predicate().test(KeyPredicate.Helper(keys)) sealed class KeyEvent private constructor(internal open val key: KeyId) { data class Down(override val key: KeyId) : KeyEvent(key) From 8552dcdd3674dfcfa036d0bf9bf2ff8087344f48 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Sat, 5 Sep 2026 12:33:57 +0800 Subject: [PATCH 22/28] MUI: Restructuring KUI/UID --- .../kotlin/net/terramodulus/mui/MuiManager.kt | 13 +- .../terramodulus/mui/gui/InputCtxStates.kt | 12 ++ .../terramodulus/mui/gui/InputGlobalStates.kt | 34 ++++ .../net/terramodulus/mui/gui/InputState.kt | 29 +++ .../terramodulus/mui/gui/MouseCtxStates.kt | 12 ++ .../terramodulus/mui/gui/MouseGlobalStates.kt | 8 + .../net/terramodulus/mui/gui/MouseState.kt | 40 ++++ .../mui/gui/agim/event/BubblingEvent.kt | 12 +- .../mui/gui/agim/event/GenericEvent.kt | 2 +- .../mui/gui/agim/event/GenericEvents.kt | 20 +- .../mui/gui/agim/event/ScreenEvent.kt | 6 - .../mui/gui/agim/impl/GameplayScreen.kt | 41 +++-- .../mui/gui/agim/package-info.java | 11 -- .../net/terramodulus/mui/gui/gfx/Rectangle.kt | 24 +++ .../mui/kui/GamepadInputHandler.kt | 10 + .../net/terramodulus/mui/kui/InputSystem.kt | 172 +++--------------- .../mui/kui/KeyboardInputHandler.kt | 152 ++++++++++++++++ .../net/terramodulus/mui/kui/KuiManager.kt | 14 +- .../terramodulus/mui/kui/MouseInputHandler.kt | 103 +++++++++++ .../terramodulus/mui/kui/VirtualKeyboard.kt | 10 + .../kotlin/net/terramodulus/mui/uid/Device.kt | 13 ++ .../net/terramodulus/mui/uid/GamepadDevice.kt | 10 + .../terramodulus/mui/uid/KeyboardDevice.kt | 27 +++ .../net/terramodulus/mui/uid/MouseDevice.kt | 25 +++ .../net/terramodulus/mui/uid/UidManager.kt | 37 ++++ .../net/terramodulus/util/exception/Core.kt | 4 + 26 files changed, 623 insertions(+), 218 deletions(-) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/InputCtxStates.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/InputState.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseCtxStates.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseGlobalStates.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseState.kt delete mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/package-info.java create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/kui/GamepadInputHandler.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/kui/KeyboardInputHandler.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/kui/MouseInputHandler.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/kui/VirtualKeyboard.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/uid/Device.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/uid/GamepadDevice.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/uid/KeyboardDevice.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/uid/MouseDevice.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/uid/UidManager.kt diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt index 437d2d0b..d08a828e 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt @@ -12,7 +12,9 @@ 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.KeyboardInputHandler import net.terramodulus.mui.kui.KuiManager +import net.terramodulus.mui.uid.UidManager import net.terramodulus.util.logging.logger import java.io.Closeable @@ -22,9 +24,10 @@ private const val HEIGHT = 480u internal class MuiManager internal constructor(core: TerraModulus) : Closeable { private val window = Window(WIDTH, HEIGHT) // SDL Window + internal val uidManager = UidManager() internal val auiManager = AuiManager() internal val huiManager = HuiManager() - internal val kuiManager = KuiManager() + internal val kuiManager = KuiManager(uidManager) internal val guiManager = GuiManager(window, core) internal fun showWindow() = window.show() @@ -34,7 +37,7 @@ internal class MuiManager internal constructor(core: TerraModulus) : Closeable { * This includes input ticking and canvas rendering. */ internal fun update() { - val keyEvents = ArrayList() + val keyEvents = ArrayList() window.pollEvents().forEach { event -> when (event) { is MuiEvent.DisplayAdded -> { @@ -120,11 +123,11 @@ internal class MuiManager internal constructor(core: TerraModulus) : Closeable { } is MuiEvent.KeyboardKeyDown -> { logger.debug { "Keyboard (id: ${event.keyboardId}) key `${event.key}` down." } - keyEvents.add(InputSystem.KeyEvent.Down(InputSystem.KeyId(event.key))) + keyEvents.add(InputSystem.InputEvent.Keyboard(KeyboardInputHandler.KeyEvent.Down(KeyboardInputHandler.KeyId(event.key)))) } is MuiEvent.KeyboardKeyUp -> { logger.debug { "Keyboard (id: ${event.keyboardId}) key `${event.key}` up." } - keyEvents.add(InputSystem.KeyEvent.Up(InputSystem.KeyId(event.key))) + keyEvents.add(InputSystem.InputEvent.Keyboard(KeyboardInputHandler.KeyEvent.Up(KeyboardInputHandler.KeyId(event.key)))) } MuiEvent.KeyboardRemoved -> { logger.debug { "Keyboard removed." } @@ -236,7 +239,7 @@ internal class MuiManager internal constructor(core: TerraModulus) : Closeable { } } } - kuiManager.update(keyEvents) + kuiManager.inputSystem.update(keyEvents.asSequence()) guiManager.updateScreens(this) guiManager.updateCanvas() } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputCtxStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputCtxStates.kt new file mode 100644 index 00000000..6343686b --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputCtxStates.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +import net.terramodulus.mui.gui.asd.AsdHandle + +sealed class InputCtxStates(protected val asdHandle: AsdHandle) { + protected val listeners = mutableSetOf>() +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt new file mode 100644 index 00000000..e1d4f414 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +sealed class InputGlobalStates, K : Any> { + protected val ctxStates = mutableSetOf() + protected val triggers = mutableMapOf>>() + protected val listeners = mutableMapOf, InputState.Listener>() + + internal fun addCtxStates(c: C) { + ctxStates.add(c) + } + + internal fun removeCtxStates(c: C) { + ctxStates.remove(c) + } + + internal fun addListener(listener: InputState.Listener) { + listener.triggers.forEach { + listeners[it] = listener + triggers.computeIfAbsent(it.key) { mutableSetOf() }.add(it) + } + } + + internal fun removeListener(listener: InputState.Listener) { + listener.triggers.forEach { + listeners.remove(it) + triggers[it.key]!!.remove(it) + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputState.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputState.kt new file mode 100644 index 00000000..0a948edc --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputState.kt @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +/** + * It is unlikely that local triggers being unaffected by other factors, + * so this should match any range of inputs while this may also filter for specific triggers. + * + * Likely be implemented as simple data classes. + */ +sealed class InputState { + sealed interface Listener { + val triggers: Set> + + fun act() + } + + sealed interface Trigger { + /** + * Shall be implemented as simple data classes supporting equality. + */ + val key: K + + fun check(state: S): Boolean + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseCtxStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseCtxStates.kt new file mode 100644 index 00000000..6dd7332c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseCtxStates.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +import net.terramodulus.mui.gui.asd.AsdHandle + +class MouseCtxStates(asdHandle: AsdHandle) : InputCtxStates(asdHandle) { + +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseGlobalStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseGlobalStates.kt new file mode 100644 index 00000000..3ce189f0 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseGlobalStates.kt @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +class MouseGlobalStates : InputGlobalStates() diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseState.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseState.kt new file mode 100644 index 00000000..58844be8 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseState.kt @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +import com.cout970.math.vec2.Vec2d +import net.terramodulus.mui.uid.MouseDevice +import kotlin.time.Duration + +sealed class MouseState : InputState() { + sealed class Key private constructor() { + data object Movement : Key() + data class ButtonJustDown(val id: MouseDevice.ButtonId) : Key() + data class ButtonJustUp(val id: MouseDevice.ButtonId) : Key() + data object ButtonKeepDown : Key() + data object WheelYNeg : Key() + data object WheelYPos : Key() + data object WheelXNeg : Key() + data object WheelXPos : Key() + } + + data class Movement(val delX: Double, val delY: Double, val pos: Vec2d) : MouseState() + data class ButtonJustDown(val id: MouseDevice.ButtonId, val pos: Vec2d) : MouseState() + data class ButtonJustUp(val id: MouseDevice.ButtonId, val pos: Vec2d) : MouseState() + data class ButtonKeepDown(val dur: Duration, val pos: Vec2d) : MouseState() + data class WheelYMotion(val delta: Double, val pos: Vec2d) : MouseState() + data class WheelXMotion(val delta: Double, val pos: Vec2d) : MouseState() + + class Listener(override val triggers: Set, private val action: () -> Unit) : + InputState.Listener { + override fun act() = action() + } + + class Trigger(override val key: Key, private val condition: (MouseState) -> Boolean) : + InputState.Trigger { + override fun check(state: MouseState) = condition(state) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/BubblingEvent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/BubblingEvent.kt index 494b762b..7a2f34a4 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/BubblingEvent.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/BubblingEvent.kt @@ -5,17 +5,11 @@ package net.terramodulus.mui.gui.agim.event -sealed interface BubblingEvent { - val bubbles: Boolean - - fun stopPropagation() -} - -internal class BubblingEventImpl : BubblingEvent { - override var bubbles: Boolean = true +sealed class BubblingEvent { + var bubbles: Boolean = true private set - override fun stopPropagation() { + fun stopPropagation() { bubbles = false } } 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 index bf70fc1a..5b3a52ee 100644 --- 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 @@ -6,7 +6,7 @@ package net.terramodulus.mui.gui.agim.event // Must not be used in kernel -abstract class GenericEvent : BubblingEvent by BubblingEventImpl() { +abstract class GenericEvent : BubblingEvent() { override fun equals(other: Any?): Boolean { return this === other } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvents.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvents.kt index 0ac2d441..1505c2ac 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvents.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/event/GenericEvents.kt @@ -5,22 +5,8 @@ package net.terramodulus.mui.gui.agim.event -import com.cout970.math.vec2.Vec2d -import net.terramodulus.mui.kui.InputSystem - object GenericEvents { - abstract class MouseEnter internal constructor() - abstract class MouseLeave internal constructor() - abstract class MouseMove internal constructor() { - abstract val a: Vec2d - abstract val b: Vec2d - } - abstract class MouseDown internal constructor() { - abstract val pos: Vec2d - abstract val keyId: InputSystem.KeyId - } - abstract class MouseUp internal constructor() { - abstract val pos: Vec2d - abstract val keyId: InputSystem.KeyId - } + // TODO not sure whether this is really needed or useful in the future + // This should either provide sealed classes to be extended by (more than one) subclasses of AGIMO Events, + // or provide variants to be used with GenericEvent. } 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 index 12022ba1..2f6e8c7b 100644 --- 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 @@ -5,16 +5,10 @@ package net.terramodulus.mui.gui.agim.event -import com.cout970.math.vec2.Vec2d import net.terramodulus.mui.gui.agim.ScreenManager -import net.terramodulus.mui.kui.InputSystem sealed interface ScreenEvent { data class Update(val muiIoI: ScreenManager.MuiIoI) : ScreenEvent - data class MouseDown(override val pos: Vec2d, override val keyId: InputSystem.KeyId) : - ScreenEvent, GenericEvents.MouseDown(), BubblingEvent by BubblingEventImpl() - data class MouseUp(override val pos: Vec2d, override val keyId: InputSystem.KeyId) : - ScreenEvent, GenericEvents.MouseUp(), BubblingEvent by BubblingEventImpl() data object Close : ScreenEvent data class Generic(val generic: T) : ScreenEvent } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt index 6542716f..a4ecd15f 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt @@ -35,6 +35,7 @@ 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.mui.kui.KeyboardInputHandler import net.terramodulus.util.logging.logger import net.terramodulus.void.World import kotlin.math.PI @@ -178,12 +179,12 @@ internal class GameplayScreen( private fun Vec3f.toArray() = floatArrayOf(x, y, z) private fun Direction6C.toKey() = when (this) { - Direction6C.North -> InputSystem.Keys.W - Direction6C.South -> InputSystem.Keys.S - Direction6C.West -> InputSystem.Keys.A - Direction6C.East -> InputSystem.Keys.D - Direction6C.Up -> InputSystem.Keys.Space - Direction6C.Down -> InputSystem.Keys.LShift + Direction6C.North -> KeyboardInputHandler.Keys.W + Direction6C.South -> KeyboardInputHandler.Keys.S + Direction6C.West -> KeyboardInputHandler.Keys.A + Direction6C.East -> KeyboardInputHandler.Keys.D + Direction6C.Up -> KeyboardInputHandler.Keys.Space + Direction6C.Down -> KeyboardInputHandler.Keys.LShift } private fun Direction6C.toVector() = when (this) { @@ -200,25 +201,25 @@ internal class GameplayScreen( 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() }) { + if (inputSystem.condition { keyboard { Q.justDown } }) { // Query position of sphere logger.info { "Position: ${player.pos.display()}" } } - if (inputSystem.condition { R.justDown() }) { + if (inputSystem.condition { keyboard { R.justDown } }) { // Query velocity of sphere // Note: Acceleration is hard to be queried as force is zeroed after each world step logger.info { "Velocity: ${player.phyBody.linearVel.display()}" } } - if (inputSystem.condition { U.justDown() }) { + if (inputSystem.condition { keyboard { U.justDown } }) { // Query gravity of world and gravity mode of (influence to) sphere logger.info { "Gravity: ${core.world!!.gravity.display()}; influence: ${player.phyBody.gravityMode}" } } - if (inputSystem.condition { I.justDown() }) { + if (inputSystem.condition { keyboard { I.justDown } }) { // Toggle gravity mode of (influence to) sphere player.phyBody.gravityMode = !player.phyBody.gravityMode logger.info { "Gravity influence toggled: ${player.phyBody.gravityMode}" } } - if (inputSystem.condition { O.justDown() }) { + if (inputSystem.condition { keyboard { O.justDown } }) { // Increase world gravity if (-core.world!!.gravity.y < MAX_GRAVITY) { core.world!!.gravity *= 2.0 @@ -235,7 +236,7 @@ internal class GameplayScreen( } } } - if (inputSystem.condition { P.justDown() }) { + if (inputSystem.condition { keyboard { P.justDown } }) { // Decrease world gravity if (-core.world!!.gravity.y > MIN_GRAVITY) { core.world!!.gravity /= 2.0 @@ -252,11 +253,11 @@ internal class GameplayScreen( } } } - if (inputSystem.condition { J.justDown() }) { + if (inputSystem.condition { keyboard { J.justDown } }) { // Query friction states logger.info { "Friction: ${core.world!!.friction}; mode: ${core.world!!.frictionMode}" } } - if (inputSystem.condition { K.justDown() }) { + if (inputSystem.condition { keyboard { K.justDown } }) { // Toggle friction mode core.world!!.frictionMode = World.FrictionMode.entries[ (core.world!!.frictionMode.ordinal + 1) % World.FrictionMode.entries.size @@ -267,7 +268,7 @@ internal class GameplayScreen( } } } - if (inputSystem.condition { L.justDown() }) { + if (inputSystem.condition { keyboard { L.justDown } }) { // Increase friction (for Limited mode) if (core.world!!.friction < MAX_FRICTION) { core.world!!.friction *= 2 @@ -284,7 +285,7 @@ internal class GameplayScreen( } } } - if (inputSystem.condition { M.justDown() }) { + if (inputSystem.condition { keyboard { M.justDown } }) { // Decrease friction (for Limited mode) if (core.world!!.friction > MIN_FRICTION) { core.world!!.friction /= 2 @@ -301,7 +302,7 @@ internal class GameplayScreen( } } } - if (inputSystem.condition { N.justDown() }) { + if (inputSystem.condition { keyboard { N.justDown } }) { // Reset velocity of sphere to zero player.phyBody.linearVel = ZeroImmVec3d logger.info { "Reset velocity to zero" } @@ -312,7 +313,7 @@ internal class GameplayScreen( // player.pos = Vec3D(0.0, 1.0, 0.0) // logger.info { "Reset position to spawn point" } // } - if (inputSystem.condition { Equals.justDown() }) { + if (inputSystem.condition { keyboard { Equals.justDown } }) { // Zoom in camera if (camera.zoomLevel < MAX_ZOOM) { camera.zoomLevel *= 2 @@ -321,7 +322,7 @@ internal class GameplayScreen( logger.info { "Zoom maximized: ${camera.zoomLevel}" } } } - if (inputSystem.condition { Minus.justDown() }) { + if (inputSystem.condition { keyboard { Minus.justDown } }) { // Zoom out camera if (camera.zoomLevel > MIN_ZOOM) { camera.zoomLevel /= 2 @@ -332,7 +333,7 @@ internal class GameplayScreen( } val dirs = ArrayList() - Direction6C.entries.forEach { if (inputSystem.condition { it.toKey().down() }) dirs.add(it.toVector()) } + Direction6C.entries.forEach { if (inputSystem.condition { keyboard { it.toKey().down } }) dirs.add(it.toVector()) } player.move(dirs.fold(ZeroImmVec3d, Vec3d::plus)) } 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 deleted file mode 100644 index 5723fbd2..00000000 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/package-info.java +++ /dev/null @@ -1,11 +0,0 @@ -/* - * 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/gfx/Rectangle.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt index 6ba2783c..3cb54388 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Rectangle.kt @@ -216,3 +216,27 @@ data class RectangleD( override fun toFloat() = RectangleF(x.toFloat(), y.toFloat(), width.toFloat(), height.toFloat()) override fun toDouble() = this } + +@ExposedCopyVisibility +data class RectRange private constructor(val rect: RectangleD, internal val type: Type) { + internal enum class Type { Inclusive, Range, Exclusive } + companion object { + fun inclusive(rect: RectangleD) = RectRange(rect, Type.Inclusive) + fun exclusive(rect: RectangleD) = RectRange(rect, Type.Exclusive) + + /** + * For each axis, inclusive for the lower bound and exclusive for the upper bound. + */ + fun range(rect: RectangleD) = RectRange(rect, Type.Range) + } + + fun contains(pt: Vec2d): Boolean { + val lower = ImmVec2d(rect.x, rect.y) + val upper = ImmVec2d(rect.x + rect.width, rect.y + rect.height) + return when (type) { + Type.Inclusive -> pt.x in lower.x..upper.x && pt.y in lower.y..upper.y + Type.Exclusive -> pt.x > lower.x && pt.x < upper.x && pt.y > lower.y && pt.y < upper.y + Type.Range -> pt.x in lower.x..() - - sealed interface Keys { - fun down(): KeyPredicate.Down - fun justDown(): KeyPredicate.JustDown - fun justUp(): KeyPredicate.JustUp - fun matches(other: KeyId): Boolean - } - - private class KeysImpl(private val id: KeyId) : Keys { - override fun down() = KeyPredicate.Down(id) - override fun justDown() = KeyPredicate.JustDown(id) - override fun justUp() = KeyPredicate.JustUp(id) - override fun matches(other: KeyId) = id == other - } - - // Values refer to ferricia::mui::KeyboardKey - enum class KeyboardKeys(private val id: KeyId) : Keys by KeysImpl(id) { - A(KeyId(0u)), - B(KeyId(1u)), - C(KeyId(2u)), - D(KeyId(3u)), - E(KeyId(4u)), - F(KeyId(5u)), - G(KeyId(6u)), - H(KeyId(7u)), - I(KeyId(8u)), - J(KeyId(9u)), - K(KeyId(10u)), - L(KeyId(11u)), - M(KeyId(12u)), - N(KeyId(13u)), - O(KeyId(14u)), - P(KeyId(15u)), - Q(KeyId(16u)), - R(KeyId(17u)), - S(KeyId(18u)), - T(KeyId(19u)), - U(KeyId(20u)), - V(KeyId(21u)), - W(KeyId(22u)), - X(KeyId(23u)), - Y(KeyId(24u)), - Z(KeyId(25u)), - Space(KeyId(40u)), - Minus(KeyId(41u)), - Equals(KeyId(42u)), - LShift(KeyId(205u)), - } - - sealed class KeyPredicate { - // Automatically asserted helper - internal class Helper(private val keys: Map) { - operator fun get(key: KeyId) = keys[key]!! - } - - internal abstract fun test(keys: Helper): Boolean - - data class Down(val x: KeyId) : KeyPredicate() { - override fun test(keys: Helper) = keys[x].down - } - - data class JustDown(val x: KeyId) : KeyPredicate() { - override fun test(keys: Helper) = keys[x].isJustDown() - } +// TODO Tentative solution, may be rewritten later. +/** + * Simple integrated access interface to various input handlers + */ +class InputSystem internal constructor(private val kuiManager: KuiManager) { + sealed class InputPredicateBase> { + sealed interface Helper - data class JustUp(val x: KeyId) : KeyPredicate() { - override fun test(keys: Helper) = keys[x].isJustUp() - } + internal abstract fun test(helper: H): Boolean - data class And(val x: KeyPredicate, val y: KeyPredicate) : KeyPredicate() { - override fun test(keys: Helper) = x.test(keys) && y.test(keys) + class And>(val x: P, val y: P) : InputPredicateBase() { + override fun test(helper: H) = x.test(helper) && y.test(helper) } - data class Or(val x: KeyPredicate, val y: KeyPredicate) : KeyPredicate() { - override fun test(keys: Helper) = x.test(keys) || y.test(keys) + class Or>(val x: P, val y: P) : InputPredicateBase() { + override fun test(helper: H) = x.test(helper) || y.test(helper) } - data class Not(val x: KeyPredicate) : KeyPredicate() { - override fun test(keys: Helper) = !x.test(keys) + class Not>(val x: P) : InputPredicateBase() { + override fun test(helper: H) = !x.test(helper) } - operator fun not() = Not(this) - infix fun and(other: KeyPredicate) = And(this, other) - infix fun or(other: KeyPredicate) = Or(this, other) + @Suppress("UNCHECKED_CAST") + operator fun not() = Not(this as P) + @Suppress("UNCHECKED_CAST") + infix fun and(other: P) = And(this as P, other) + @Suppress("UNCHECKED_CAST") + infix fun or(other: P) = Or(this as P, other) } - // This class may be programmatically generated - object KeysScope { - val A = KeyboardKeys.A - val B = KeyboardKeys.B - val C = KeyboardKeys.C - val D = KeyboardKeys.D - val E = KeyboardKeys.E - val F = KeyboardKeys.F - val G = KeyboardKeys.G - val H = KeyboardKeys.H - val I = KeyboardKeys.I - val J = KeyboardKeys.J - val K = KeyboardKeys.K - val L = KeyboardKeys.L - val M = KeyboardKeys.M - val N = KeyboardKeys.N - val O = KeyboardKeys.O - val P = KeyboardKeys.P - val Q = KeyboardKeys.Q - val R = KeyboardKeys.R - val S = KeyboardKeys.S - val T = KeyboardKeys.T - val U = KeyboardKeys.U - val V = KeyboardKeys.V - val W = KeyboardKeys.W - val X = KeyboardKeys.X - val Y = KeyboardKeys.Y - val Z = KeyboardKeys.Z - val Space = KeyboardKeys.Space - val Minus = KeyboardKeys.Minus - val Equals = KeyboardKeys.Equals - val LShift = KeyboardKeys.LShift + inner class InputsScope { + fun keyboard(predicate: KeyboardInputHandler.KeysScope.() -> KeyboardInputHandler.KeyPredicate) = + kuiManager.keyboardInputHandler.condition(predicate) } - init { - // Refers to ferricia::mui::KeyboardKey - for (i in 0u..242u) keys[KeyId(i)] = Key() - } + private val inputsScope = InputsScope() - @JvmInline - value class KeyId(private val id: UInt) { - fun matches(other: Keys) = other.matches(this) - } + fun condition(predicate: InputsScope.() -> Boolean) = inputsScope.predicate() - class Key { - var down = false - internal set - internal var justChanged = false - - fun isJustDown() = down && justChanged - fun isJustUp() = !down && justChanged + internal sealed class InputEvent private constructor() { + data class Keyboard(val inner: KeyboardInputHandler.KeyEvent) : InputEvent() } - fun condition(predicate: KeysScope.() -> KeyPredicate) = KeysScope.predicate().test(KeyPredicate.Helper(keys)) - - sealed class KeyEvent private constructor(internal open val key: KeyId) { - data class Down(override val key: KeyId) : KeyEvent(key) - data class Up(override val key: KeyId) : KeyEvent(key) - } - - internal fun update(events: List) { - keys.forEach { (_, key) -> key.justChanged = false } - events.forEach { - // Note: This may not handle the case where a key is just down less than a tick. - keys[it.key]!!.justChanged = true - when (it) { - is KeyEvent.Down -> keys[it.key]!!.down = true - is KeyEvent.Up -> keys[it.key]!!.down = false - } - } + internal fun update(events: Sequence) { + kuiManager.keyboardInputHandler.update(events.filterIsInstance().map { it.inner }) } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/kui/KeyboardInputHandler.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/KeyboardInputHandler.kt new file mode 100644 index 00000000..c7228aba --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/KeyboardInputHandler.kt @@ -0,0 +1,152 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.kui + +import net.terramodulus.mui.uid.KeyboardDevice + +class KeyboardInputHandler internal constructor(keyboardDevice: KeyboardDevice) { + typealias KeyId = KeyboardDevice.KeyId + + sealed class KeyPredicate : InputSystem.InputPredicateBase() { + // Automatically asserted helper + class Helper internal constructor(private val keys: Map) : InputSystem.InputPredicateBase.Helper { + operator fun get(key: KeyId) = keys[key]!! + } + + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + abstract override fun test(keys: Helper): Boolean + + data class Down(val x: KeyId) : KeyPredicate() { + override fun test(keys: Helper) = keys[x].down + } + + data class JustDown(val x: KeyId) : KeyPredicate() { + override fun test(keys: Helper) = keys[x].justDown + } + + data class JustUp(val x: KeyId) : KeyPredicate() { + override fun test(keys: Helper) = keys[x].justUp + } + } + + sealed interface InnerKeys { + val down: KeyPredicate.Down + val justDown: KeyPredicate.JustDown + val justUp: KeyPredicate.JustUp + fun matches(other: KeyId): Boolean // Is this useful? + } + + private class KeysImpl(private val id: KeyId) : InnerKeys { + override val down get() = KeyPredicate.Down(id) + override val justDown get() = KeyPredicate.JustDown(id) + override val justUp get() = KeyPredicate.JustUp(id) + override fun matches(other: KeyId) = id == other + } + + // Values refer to ferricia::mui::KeyboardKey + enum class Keys(private val id: KeyId) : InnerKeys by KeysImpl(id) { + A(KeyId(0u)), + B(KeyId(1u)), + C(KeyId(2u)), + D(KeyId(3u)), + E(KeyId(4u)), + F(KeyId(5u)), + G(KeyId(6u)), + H(KeyId(7u)), + I(KeyId(8u)), + J(KeyId(9u)), + K(KeyId(10u)), + L(KeyId(11u)), + M(KeyId(12u)), + N(KeyId(13u)), + O(KeyId(14u)), + P(KeyId(15u)), + Q(KeyId(16u)), + R(KeyId(17u)), + S(KeyId(18u)), + T(KeyId(19u)), + U(KeyId(20u)), + V(KeyId(21u)), + W(KeyId(22u)), + X(KeyId(23u)), + Y(KeyId(24u)), + Z(KeyId(25u)), + Space(KeyId(40u)), + Minus(KeyId(41u)), + Equals(KeyId(42u)), + LShift(KeyId(205u)), + } + + object KeysScope { + val A = Keys.A + val B = Keys.B + val C = Keys.C + val D = Keys.D + val E = Keys.E + val F = Keys.F + val G = Keys.G + val H = Keys.H + val I = Keys.I + val J = Keys.J + val K = Keys.K + val L = Keys.L + val M = Keys.M + val N = Keys.N + val O = Keys.O + val P = Keys.P + val Q = Keys.Q + val R = Keys.R + val S = Keys.S + val T = Keys.T + val U = Keys.U + val V = Keys.V + val W = Keys.W + val X = Keys.X + val Y = Keys.Y + val Z = Keys.Z + val Space = Keys.Space + val Minus = Keys.Minus + val Equals = Keys.Equals + val LShift = Keys.LShift + } + + private val keys = HashMap() + + init { + keyboardDevice.iterKeys().forEach { (k, v) -> + keys[k] = Key(v) + } + } + + // Large difference if specific keyboards can be specifically handled + class Key internal constructor(internal val raw: KeyboardDevice.Key) { + val down: Boolean get() = raw.down + internal var justChanged = false + + val justDown: Boolean get() = down && justChanged + val justUp: Boolean get() = !down && justChanged + } + + fun condition(predicate: KeysScope.() -> KeyPredicate) = KeysScope.predicate().test(KeyPredicate.Helper(keys)) + + sealed class KeyEvent private constructor(internal open val key: KeyId) { + data class Down(override val key: KeyId) : KeyEvent(key) + data class Up(override val key: KeyId) : KeyEvent(key) + } + + internal fun update(events: Sequence) { + keys.values.forEach { it.justChanged = false } + events.forEach { + // Note: This may not handle the case where a key is just down less than a tick. + // This also assumes that keyboard states are consistent across time frames. + keys[it.key]!!.justChanged = true + when (it) { + is KeyEvent.Down -> keys[it.key]!!.raw.down = true + is KeyEvent.Up -> keys[it.key]!!.raw.down = false + } + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/kui/KuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/KuiManager.kt index 41701cb3..d5ced3f1 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/kui/KuiManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/KuiManager.kt @@ -5,12 +5,14 @@ package net.terramodulus.mui.kui -import net.terramodulus.mui.kui.InputSystem.KeyEvent +import net.terramodulus.mui.uid.UidManager -class KuiManager { - val inputSystem = InputSystem() +class KuiManager internal constructor(uidManager: UidManager) { + val keyboardInputHandler = KeyboardInputHandler(uidManager.keyboardDevice) + val mouseInputHandler = MouseInputHandler(uidManager.mouseDevice) + val inputSystem = InputSystem(this) - internal fun update(events: List) { - inputSystem.update(events) - } +// internal fun update(events: List) { +// inputSystem.update(events) +// } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/kui/MouseInputHandler.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/MouseInputHandler.kt new file mode 100644 index 00000000..975c8f50 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/MouseInputHandler.kt @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.kui + +import net.terramodulus.mui.uid.MouseDevice +import kotlin.sequences.forEach + +class MouseInputHandler(mouseDevice: MouseDevice) { + typealias ButtonId = MouseDevice.ButtonId + + sealed class ButtonPredicate : InputSystem.InputPredicateBase() { + // Automatically asserted helper + class Helper internal constructor(private val buttons: Map) : InputSystem.InputPredicateBase.Helper { + operator fun get(button: ButtonId) = buttons[button]!! + } + + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + abstract override fun test(buttons: Helper): Boolean + + data class Down(val x: ButtonId) : ButtonPredicate() { + override fun test(buttons: Helper) = buttons[x].down + } + + data class JustDown(val x: ButtonId) : ButtonPredicate() { + override fun test(buttons: Helper) = buttons[x].justDown + } + + data class JustUp(val x: ButtonId) : ButtonPredicate() { + override fun test(buttons: Helper) = buttons[x].justUp + } + } + + sealed interface InnerButtons { + val down: ButtonPredicate.Down + val justDown: ButtonPredicate.JustDown + val justUp: ButtonPredicate.JustUp + fun matches(other: ButtonId): Boolean // Is this useful? + } + + private class ButtonsImpl(private val id: ButtonId) : InnerButtons { + override val down get() = ButtonPredicate.Down(id) + override val justDown get() = ButtonPredicate.JustDown(id) + override val justUp get() = ButtonPredicate.JustUp(id) + override fun matches(other: ButtonId) = id == other + } + + // Values refer to ferricia::mui::KeyboardKey + enum class Buttons(private val id: ButtonId) : InnerButtons by ButtonsImpl(id) { + Left(ButtonId(0u)), + Middle(ButtonId(1u)), + Right(ButtonId(2u)), + X1(ButtonId(3u)), + X2(ButtonId(4u)), + } + + object ButtonsScope { + val Left = Buttons.Left + val Middle = Buttons.Middle + val Right = Buttons.Right + val X1 = Buttons.X1 + val X2 = Buttons.X2 + } + + private val buttons = HashMap() + + init { + mouseDevice.iterButtons().forEach { (k, v) -> + buttons[k] = Button(v) + } + } + + // Large difference if specific keyboards can be specifically handled + class Button internal constructor(internal val raw: MouseDevice.Button) { + val down: Boolean get() = raw.down + internal var justChanged = false + + val justDown: Boolean get() = down && justChanged + val justUp: Boolean get() = !down && justChanged + } + + fun condition(predicate: ButtonsScope.() -> ButtonPredicate) = ButtonsScope.predicate().test(ButtonPredicate.Helper(buttons)) + + sealed class KeyEvent private constructor(internal open val key: ButtonId) { + data class Down(override val key: ButtonId) : KeyEvent(key) + data class Up(override val key: ButtonId) : KeyEvent(key) + } + + internal fun update(events: Sequence) { + buttons.values.forEach { it.justChanged = false } + events.forEach { + // Note: This may not handle the case where a key is just down less than a tick. + // This also assumes that keyboard states are consistent across time frames. + buttons[it.key]!!.justChanged = true + when (it) { + is KeyEvent.Down -> buttons[it.key]!!.raw.down = true + is KeyEvent.Up -> buttons[it.key]!!.raw.down = false + } + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/kui/VirtualKeyboard.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/VirtualKeyboard.kt new file mode 100644 index 00000000..aac31b62 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/VirtualKeyboard.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.kui + +class VirtualKeyboard { + // TODO +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/uid/Device.kt b/src/kernel/client/kotlin/net/terramodulus/mui/uid/Device.kt new file mode 100644 index 00000000..6cfd2508 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/uid/Device.kt @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.uid + +sealed interface Device { + val id: Id + + @JvmInline + value class Id(private val value: UInt) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/uid/GamepadDevice.kt b/src/kernel/client/kotlin/net/terramodulus/mui/uid/GamepadDevice.kt new file mode 100644 index 00000000..9f1e0ffa --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/uid/GamepadDevice.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.uid + +class GamepadDevice(override val id: Device.Id) : Device { + // TODO +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/uid/KeyboardDevice.kt b/src/kernel/client/kotlin/net/terramodulus/mui/uid/KeyboardDevice.kt new file mode 100644 index 00000000..10b69670 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/uid/KeyboardDevice.kt @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.uid + +import kotlin.collections.set + +class KeyboardDevice internal constructor(override val id: Device.Id) : Device { + internal class Key internal constructor() { + var down = false + } + + @JvmInline + value class KeyId(private val id: UInt) + + private val keys = HashMap() + + init { + // Refers to ferricia::mui::KeyboardKey + for (i in 0u..242u) keys[KeyId(i)] = Key() + } + + internal fun getKey(key: KeyId) = keys[key] + internal fun iterKeys() = keys.asSequence() +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/uid/MouseDevice.kt b/src/kernel/client/kotlin/net/terramodulus/mui/uid/MouseDevice.kt new file mode 100644 index 00000000..c8fce41c --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/uid/MouseDevice.kt @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.uid + +class MouseDevice(override val id: Device.Id) : Device { + class Button internal constructor() { + var down = false + } + + @JvmInline + value class ButtonId(private val id: UInt) + + private val buttons = HashMap() + + init { + // Refers to ferricia::mui::MouseKey + for (i in 0u..4u) buttons[ButtonId(i)] = Button() + } + + internal fun getKey(key: ButtonId) = buttons[key] + internal fun iterButtons() = buttons.asSequence() +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/uid/UidManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/uid/UidManager.kt new file mode 100644 index 00000000..d0ae87f4 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/uid/UidManager.kt @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.uid + +/** + * User Interface Device (UID) Manager + */ +internal class UidManager { + class Devices internal constructor() { + private val devices = mutableMapOf() + + fun count() = devices.count() + + fun exists(id: Device.Id) = devices.containsKey(id) + + internal fun add(device: D) { + devices[device.id] = device + } + + internal fun remove(deviceId: Device.Id) { + if (devices.remove(deviceId) == null) + throw IllegalStateException("Device [$javaClass] $deviceId does not exist") + } + } + + typealias KeyboardDevices = Devices + typealias MouseDevices = Devices + + // not yet used +// val keyboardDevices = KeyboardDevices() +// val mouseDevices = MouseDevices() + val keyboardDevice = KeyboardDevice(Device.Id(0u)) + val mouseDevice = MouseDevice(Device.Id(0u)) +} diff --git a/src/kernel/common/kotlin/net/terramodulus/util/exception/Core.kt b/src/kernel/common/kotlin/net/terramodulus/util/exception/Core.kt index d822c0b4..4f574518 100644 --- a/src/kernel/common/kotlin/net/terramodulus/util/exception/Core.kt +++ b/src/kernel/common/kotlin/net/terramodulus/util/exception/Core.kt @@ -14,6 +14,10 @@ import kotlin.reflect.KClass private val logger = logger {} +// TODO add functions that can throw errors in debug and throw warnings in production +// This is useful for like when a resource is closed twice, logic error but may likely run fine in production +// May also optionally add fallback lambda to be run in production to suppress such error + @OptIn(ExperimentalContracts::class) inline fun codeAssert(block: () -> R): R { contract { From 4e3d5106eb02fe3b3f949f2b0102332b645e350f Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:02:15 +0800 Subject: [PATCH 23/28] MUI: Implementing inputs to AGIM System --- .../net/terramodulus/mui/gui/GuiManager.kt | 5 +-- .../terramodulus/mui/gui/InputCtxStates.kt | 34 +++++++++++++++- .../terramodulus/mui/gui/InputGlobalStates.kt | 11 +----- .../net/terramodulus/mui/gui/InputState.kt | 2 +- .../terramodulus/mui/gui/InputStatesHandle.kt | 10 +++++ .../terramodulus/mui/gui/MouseCtxStates.kt | 25 +++++++++++- .../terramodulus/mui/gui/MouseGlobalStates.kt | 2 +- .../net/terramodulus/mui/gui/MouseState.kt | 4 +- .../mui/gui/agim/ScreenManager.kt | 39 ++++++++++++++++++- .../mui/gui/agim/event/ComponentEvent.kt | 1 + .../mui/gui/agim/impl/ButtonComponent.kt | 29 ++++++++++++++ .../mui/gui/agim/impl/GameplayScreen.kt | 31 +++++++++++---- .../terramodulus/mui/gui/gfx/RenderSystem.kt | 5 ++- .../terramodulus/mui/kui/MouseInputHandler.kt | 5 ++- 14 files changed, 171 insertions(+), 32 deletions(-) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/InputStatesHandle.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ButtonComponent.kt diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt index 6cd95835..7207701b 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/GuiManager.kt @@ -6,8 +6,6 @@ package net.terramodulus.mui.gui import net.terramodulus.core.TerraModulus -import net.terramodulus.engine.FontManager -import net.terramodulus.engine.GlyphManager import net.terramodulus.engine.Window import net.terramodulus.mui.MuiManager import net.terramodulus.mui.gui.agim.LayoutManager @@ -25,7 +23,8 @@ private val logger = logger {} 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 inputStatesHandle = InputStatesHandle() + val screenManager = ScreenManager(window, renderSystem.handle, asdManager.AgimHandle(), inputStatesHandle) val layoutManager = LayoutManager(screenManager) private var proceeded = false diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputCtxStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputCtxStates.kt index 6343686b..fe8006a0 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputCtxStates.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputCtxStates.kt @@ -6,7 +6,39 @@ package net.terramodulus.mui.gui import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.RectRange +import java.io.Closeable -sealed class InputCtxStates(protected val asdHandle: AsdHandle) { +sealed class InputCtxStates( + protected val globalStates: InputGlobalStates, + protected val asdHandle: AsdHandle, +) : Closeable { protected val listeners = mutableSetOf>() + val ctxRange: CtxRange by lazy { CtxRange() } + + inner class CtxRange : Closeable { + lateinit var rect: RectRange + private set + private val listener = { + rect = RectRange.range(asdHandle.rect) + }.also(asdHandle::observeRect) + + override fun close() { + asdHandle.unobserveRect(listener) + } + } + + fun addListener(listener: InputState.Listener) { + listeners.add(listener) + globalStates.addListener(listener) + } + + fun removeListener(listener: InputState.Listener) { + listeners.remove(listener) + globalStates.removeListener(listener) + } + + override fun close() { + listeners.forEach { globalStates.removeListener(it) } + } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt index e1d4f414..fc0629e0 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt @@ -5,19 +5,10 @@ package net.terramodulus.mui.gui -sealed class InputGlobalStates, K : Any> { - protected val ctxStates = mutableSetOf() +sealed class InputGlobalStates { protected val triggers = mutableMapOf>>() protected val listeners = mutableMapOf, InputState.Listener>() - internal fun addCtxStates(c: C) { - ctxStates.add(c) - } - - internal fun removeCtxStates(c: C) { - ctxStates.remove(c) - } - internal fun addListener(listener: InputState.Listener) { listener.triggers.forEach { listeners[it] = listener diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputState.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputState.kt index 0a948edc..a91c3615 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputState.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputState.kt @@ -15,7 +15,7 @@ sealed class InputState { sealed interface Listener { val triggers: Set> - fun act() + fun act(state: S) } sealed interface Trigger { diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputStatesHandle.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputStatesHandle.kt new file mode 100644 index 00000000..978f4065 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputStatesHandle.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2026 TerraModulus Team and Contributors + * SPDX-License-Identifier: LGPL-3.0-only + */ + +package net.terramodulus.mui.gui + +class InputStatesHandle { + val mouseGlobalStates = MouseGlobalStates() +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseCtxStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseCtxStates.kt index 6dd7332c..c04cc7eb 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseCtxStates.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseCtxStates.kt @@ -6,7 +6,28 @@ package net.terramodulus.mui.gui import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.uid.MouseDevice -class MouseCtxStates(asdHandle: AsdHandle) : InputCtxStates(asdHandle) { - +class MouseCtxStates(globalStates: MouseGlobalStates, asdHandle: AsdHandle) : + InputCtxStates(globalStates, asdHandle) { + inline fun listenRectFullClick(buttonId: MouseDevice.ButtonId, crossinline action: () -> Unit): MouseState.Listener { + var clicked = false + return MouseState.Listener(setOf( + MouseState.Trigger(MouseState.Key.ButtonJustDown(buttonId)) { true }, + MouseState.Trigger(MouseState.Key.ButtonJustUp(buttonId)) { true }, + )) { + when (it) { + is MouseState.ButtonJustDown -> { + assert(it.id == buttonId) + clicked = ctxRange.rect.contains(it.pos) + } + is MouseState.ButtonJustUp -> { + assert(it.id == buttonId) + if (clicked && ctxRange.rect.contains(it.pos)) action() + clicked = false + } + else -> throw UnsupportedOperationException() + } + } + } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseGlobalStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseGlobalStates.kt index 3ce189f0..b8ff005a 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseGlobalStates.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseGlobalStates.kt @@ -5,4 +5,4 @@ package net.terramodulus.mui.gui -class MouseGlobalStates : InputGlobalStates() +class MouseGlobalStates : InputGlobalStates() diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseState.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseState.kt index 58844be8..fd804e17 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseState.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseState.kt @@ -28,9 +28,9 @@ sealed class MouseState : InputState() { data class WheelYMotion(val delta: Double, val pos: Vec2d) : MouseState() data class WheelXMotion(val delta: Double, val pos: Vec2d) : MouseState() - class Listener(override val triggers: Set, private val action: () -> Unit) : + class Listener(override val triggers: Set, private val action: (MouseState) -> Unit) : InputState.Listener { - override fun act() = action() + override fun act(state: MouseState) = action(state) } class Trigger(override val key: Key, private val condition: (MouseState) -> Boolean) : 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 index cef87b73..b1c34879 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/ScreenManager.kt @@ -7,6 +7,7 @@ package net.terramodulus.mui.gui.agim import net.terramodulus.engine.Window import net.terramodulus.mui.MuiManager +import net.terramodulus.mui.gui.InputStatesHandle import net.terramodulus.mui.gui.agim.impl.BoundsProperty import net.terramodulus.mui.gui.gfx.RenderSystem import net.terramodulus.mui.gui.agim.impl.LaunchingScreen @@ -15,13 +16,13 @@ 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, + private val inputStatesHandle: InputStatesHandle, ) { /** * FILO screen stack; the top-most screen instance is in the last. @@ -164,12 +165,26 @@ class ScreenManager internal constructor( */ fun open(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) + /** + * @see ScreenOperation.Open + */ + fun open(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle, InputStatesHandle) -> 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) + /** + * It is not recommended to use this in general scenarios. + * @see ScreenOperation.OpenBefore + */ + fun openBefore( + target: Screen, + screen: (Handle, AsdHandle.Screen, RenderSystem.Handle, InputStatesHandle) -> Screen, + ) + /** * @see ScreenOperation.ExitTo */ @@ -180,6 +195,11 @@ class ScreenManager internal constructor( */ fun reset(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) + /** + * @see ScreenOperation.Reset + */ + fun reset(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle, InputStatesHandle) -> Screen) + fun addMenu(menu: (MenuManager.Handle, AsdHandle) -> Menu) fun removeMenu(menu: Menu) @@ -194,10 +214,23 @@ class ScreenManager internal constructor( screenOpQueue.add(ScreenOperation.Open { screen(handle, ScreenAsdHandleImpl(), it) }) } + override fun open(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle, InputStatesHandle) -> Screen) { + screenOpQueue.add(ScreenOperation.Open { screen(handle, ScreenAsdHandleImpl(), it, inputStatesHandle) }) + } + override fun openBefore(target: Screen, screen: (Handle, AsdHandle.Screen, RenderSystem.Handle) -> Screen) { screenOpQueue.add(ScreenOperation.OpenBefore(target) { screen(handle, ScreenAsdHandleImpl(), it) }) } + override fun openBefore( + target: Screen, + screen: (Handle, AsdHandle.Screen, RenderSystem.Handle, InputStatesHandle) -> Screen + ) { + screenOpQueue.add(ScreenOperation.OpenBefore(target) { + screen(handle, ScreenAsdHandleImpl(), it, inputStatesHandle) + }) + } + override fun exitTo(screen: Screen) { screenOpQueue.add(ScreenOperation.ExitTo(screen)) } @@ -206,6 +239,10 @@ class ScreenManager internal constructor( screenOpQueue.add(ScreenOperation.Reset { screen(handle, ScreenAsdHandleImpl(), it) }) } + override fun reset(screen: (Handle, AsdHandle.Screen, RenderSystem.Handle, InputStatesHandle) -> Screen) { + screenOpQueue.add(ScreenOperation.Reset { screen(handle, ScreenAsdHandleImpl(), it, inputStatesHandle) }) + } + override fun addMenu(menu: (MenuManager.Handle, AsdHandle) -> Menu) = menuManager.handle.addMenu(menu) override fun removeMenu(menu: Menu) = menuManager.handle.removeMenu(menu) 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 index d8fc1f09..f71c5800 100644 --- 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 @@ -8,6 +8,7 @@ package net.terramodulus.mui.gui.agim.event import net.terramodulus.mui.gui.agim.ScreenManager sealed interface ComponentEvent { + // TODO Add Init and Close? data class Update(val muiIoI: ScreenManager.MuiIoI) : ComponentEvent data class Generic(val generic: T) : ComponentEvent } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ButtonComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ButtonComponent.kt new file mode 100644 index 00000000..45947827 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ButtonComponent.kt @@ -0,0 +1,29 @@ +/* + * 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.InputStatesHandle +import net.terramodulus.mui.gui.MouseCtxStates +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 +import net.terramodulus.mui.kui.MouseInputHandler + +class ButtonComponent( + asdHandle: AsdHandle, + inputStatesHandle: InputStatesHandle, + layout: ButtonComponent.() -> Layout, +) : AbstractPane(asdHandle) { + override val layout = layout(this) + private val mouseCtxStates = MouseCtxStates(inputStatesHandle.mouseGlobalStates, asdHandle).apply { + addListener(listenRectFullClick(MouseInputHandler.Buttons.Left.id) { + println("Hello World!") + }) + } + + override fun render(renderSystem: RenderSystem) = layout.render(renderSystem) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt index a4ecd15f..816823c0 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt @@ -25,16 +25,16 @@ import net.terramodulus.engine.SimpleMesh3dGeomCube import net.terramodulus.engine.SimpleMesh3dGeomSphere import net.terramodulus.engine.WorldObjDrawable import net.terramodulus.engine.common.ZeroImmVec3d +import net.terramodulus.mui.gui.InputStatesHandle 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.InsetsD import net.terramodulus.mui.gui.gfx.RenderSystem -import net.terramodulus.mui.kui.InputSystem +import net.terramodulus.mui.gui.gfx.TextContext import net.terramodulus.mui.kui.KeyboardInputHandler import net.terramodulus.util.logging.logger import net.terramodulus.void.World @@ -66,6 +66,7 @@ internal class GameplayScreen( renderSystemHandle: RenderSystem.Handle, managerHandle: ScreenManager.Handle, asdHandle: AsdHandle.Container, + inputStatesHandle: InputStatesHandle, ) : Screen(managerHandle, asdHandle) { private val geoShaders = camera.loadGeoShaders( getResourceAsString("/gwr_geo.vsh"), @@ -75,8 +76,7 @@ internal class GameplayScreen( private val canvasHandle = renderSystemHandle.canvasHandle private lateinit var player: PlayerVoidGeom - override val layout = - SingletonLayout(this, BlankComponent(ComponentAsdHandleImpl()), SingletonLayout.Config.Absolute.Full) + override val layout = CompositeLayout(this) init { renderSystemHandle.setBackgroundColor(0F, 0F, 0F, 0F) @@ -84,7 +84,24 @@ internal class GameplayScreen( WorldInitScreen(p1, p2, p3).apply { core.world = World(Ymir(), progressBar) addListener(ScreenEvent.Close::class.java) { - this@GameplayScreen.layout.update(GameplayRenderer()) + this@GameplayScreen.layout.update { + add(SingletonLayout( + this@GameplayScreen, + GameplayRenderer(), + SingletonLayout.Config.Absolute.Full, + )) + add(SingletonLayout( + this@GameplayScreen, + ButtonComponent(ComponentAsdHandleImpl(), inputStatesHandle) { + SingletonLayout(this, TextDisplayComponent( + ComponentAsdHandleImpl(), + renderSystemHandle, + TextContext.Config(24.0F, 24.0F, ImmVec4i(255)), + ), SingletonLayout.Config.Absolute.Full) + }, + SingletonLayout.Config.Absolute.Insets(InsetsD(20.0, 0.0, 0.0, 300.0)), + )) + } this@GameplayScreen.addListener(ScreenEvent.Update::class.java) { update0(it.muiIoI) } @@ -116,7 +133,7 @@ internal class GameplayScreen( private val sphereGeom = SimpleMesh3dGeomSphere(canvasHandle.canvas, 1F) override fun wrapCube(phyGeom: PhyGeom, x: Double, y: Double, z: Double): VoidGeom = EnvVoidGeom(phyGeom, - WorldObjDrawable(cubeGeom, randomColor(), ImmVec3d(x, y, z), STD_SCALE, IDENT_ROT,), + WorldObjDrawable(cubeGeom, randomColor(), ImmVec3d(x, y, z), STD_SCALE, IDENT_ROT), ImmVec3d(x, y, z), ) diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/RenderSystem.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/RenderSystem.kt index a03ae08f..6da14585 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/RenderSystem.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/RenderSystem.kt @@ -16,6 +16,7 @@ import net.terramodulus.engine.FontManager import net.terramodulus.engine.GeomDrawable import net.terramodulus.engine.MeshDrawable import net.terramodulus.engine.TextRenderingContext +import net.terramodulus.mui.gui.InputStatesHandle import net.terramodulus.mui.gui.agim.ScreenManager import net.terramodulus.mui.gui.agim.impl.GameplayScreen import net.terramodulus.mui.gui.asd.AsdHandle @@ -74,8 +75,8 @@ class RenderSystem internal constructor(private val core: TerraModulus, private } 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) + { mh: ScreenManager.Handle, ah: AsdHandle.Container, it: Handle, ish: InputStatesHandle -> + GameplayScreen(core, canvas.createCamera(floatArrayOf(pos.x, pos.y, pos.z)), it, mh, ah, ish) } internal fun renderGuiTex(drawable: MeshDrawable, texture: UInt) = canvas.renderGuiTex(drawable, texShaders, texture) diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/kui/MouseInputHandler.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/MouseInputHandler.kt index 975c8f50..3f164073 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/kui/MouseInputHandler.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/MouseInputHandler.kt @@ -34,13 +34,14 @@ class MouseInputHandler(mouseDevice: MouseDevice) { } sealed interface InnerButtons { + val id: ButtonId val down: ButtonPredicate.Down val justDown: ButtonPredicate.JustDown val justUp: ButtonPredicate.JustUp fun matches(other: ButtonId): Boolean // Is this useful? } - private class ButtonsImpl(private val id: ButtonId) : InnerButtons { + private class ButtonsImpl(override val id: ButtonId) : InnerButtons { override val down get() = ButtonPredicate.Down(id) override val justDown get() = ButtonPredicate.JustDown(id) override val justUp get() = ButtonPredicate.JustUp(id) @@ -48,7 +49,7 @@ class MouseInputHandler(mouseDevice: MouseDevice) { } // Values refer to ferricia::mui::KeyboardKey - enum class Buttons(private val id: ButtonId) : InnerButtons by ButtonsImpl(id) { + enum class Buttons(override val id: ButtonId) : InnerButtons by ButtonsImpl(id) { Left(ButtonId(0u)), Middle(ButtonId(1u)), Right(ButtonId(2u)), From 3b9bc97b0b90ac66cc6fcb988620597a2985cdac Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:08:22 +0800 Subject: [PATCH 24/28] GUI: Fix deadlock of layout computation --- .../net/terramodulus/mui/gui/agim/LayoutManager.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) 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 index e4e24840..c95398fc 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/LayoutManager.kt @@ -12,9 +12,6 @@ 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) @@ -245,7 +242,7 @@ internal class LayoutManager(screenManager: ScreenManager) { } } } - for (unit in channelComputed) { + if (unitsToCompute.isNotEmpty()) for (unit in channelComputed) { unitChildren.remove(unit)?.forEach { val deps = requireNotNull(unitDeps[it]) assert(deps.remove(unit)) @@ -255,12 +252,11 @@ internal class LayoutManager(screenManager: ScreenManager) { unitsToCompute.remove(it) } } - if (unitsToCompute.isEmpty()) { - channelToCompute.close() - assert(unitChildren.isEmpty()) - assert(unitDeps.isEmpty()) - } + if (unitsToCompute.isEmpty()) break } + channelToCompute.close() + assert(unitChildren.isEmpty()) + assert(unitDeps.isEmpty()) } launch { val layoutHandle = object : LayoutHandle() { From d243579562e78a51339c4469f3228e92a8b27098 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:09:50 +0800 Subject: [PATCH 25/28] MUI: Add basic Button & Checkbox & mouse tracking Still in WIP, but mouse tracking function is ready --- ferricia | 2 +- .../kotlin/net/terramodulus/engine/Window.kt | 4 + .../net/terramodulus/engine/ferricia/Mui.kt | 7 ++ .../kotlin/net/terramodulus/mui/MuiManager.kt | 20 +++- .../terramodulus/mui/gui/InputCtxStates.kt | 6 + .../terramodulus/mui/gui/InputGlobalStates.kt | 4 + .../terramodulus/mui/gui/InputStatesHandle.kt | 7 ++ .../terramodulus/mui/gui/MouseGlobalStates.kt | 32 ++++- .../mui/gui/agim/impl/CheckboxComponent.kt | 63 ++++++++++ .../mui/gui/agim/impl/GameplayScreen.kt | 112 +++++++++++++++--- .../net/terramodulus/mui/kui/InputSystem.kt | 1 + .../terramodulus/mui/kui/MouseInputHandler.kt | 26 ++-- 12 files changed, 248 insertions(+), 36 deletions(-) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CheckboxComponent.kt diff --git a/ferricia b/ferricia index ffe0ed1b..5bbb5d96 160000 --- a/ferricia +++ b/ferricia @@ -1 +1 @@ -Subproject commit ffe0ed1b6a4f3c8538847336538a5102b75592a8 +Subproject commit 5bbb5d96d3ddda0703a610f4d2a30566301f7450 diff --git a/src/internal/client/kotlin/net/terramodulus/engine/Window.kt b/src/internal/client/kotlin/net/terramodulus/engine/Window.kt index f21f4dc0..cfffb58f 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Window.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Window.kt @@ -5,8 +5,10 @@ package net.terramodulus.engine +import com.cout970.math.vec2.ImmVec2f import net.terramodulus.engine.ferricia.Mui.dropSdlHandle import net.terramodulus.engine.ferricia.Mui.dropWindowHandle +import net.terramodulus.engine.ferricia.Mui.getMousePos import net.terramodulus.engine.ferricia.Mui.initSdlHandle import net.terramodulus.engine.ferricia.Mui.initWindowHandle import net.terramodulus.engine.ferricia.Mui.resizeGLViewport @@ -48,6 +50,8 @@ class Window( fun pollEvents() = sdlPoll(sdlHandle) + fun getMousePos() = getMousePos(sdlHandle).let { ImmVec2f(it[0], height.toFloat() - it[1]) } + override fun close() { dropWindowHandle(windowHandle) dropSdlHandle(sdlHandle) 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 d1c01040..542e766d 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Mui.kt @@ -40,6 +40,13 @@ internal object Mui { @JvmName("getGLVersion") external fun getGLVersion(windowHandle: ULong): String + /** + * @param sdlHandle SDL handle pointer + * @return `[x, y]` in window coordinates + */ + @JvmName("getMousePos") + external fun getMousePos(sdlHandle: ULong): FloatArray + /** * @param sdlHandle SDL handle pointer * @return the list of all MUI events in this frame diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt b/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt index d08a828e..5c25276a 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/MuiManager.kt @@ -14,6 +14,7 @@ import net.terramodulus.mui.hui.HuiManager import net.terramodulus.mui.kui.InputSystem import net.terramodulus.mui.kui.KeyboardInputHandler import net.terramodulus.mui.kui.KuiManager +import net.terramodulus.mui.kui.MouseInputHandler import net.terramodulus.mui.uid.UidManager import net.terramodulus.util.logging.logger import java.io.Closeable @@ -37,7 +38,7 @@ internal class MuiManager internal constructor(core: TerraModulus) : Closeable { * This includes input ticking and canvas rendering. */ internal fun update() { - val keyEvents = ArrayList() + val inputEvents = ArrayList() window.pollEvents().forEach { event -> when (event) { is MuiEvent.DisplayAdded -> { @@ -123,11 +124,11 @@ internal class MuiManager internal constructor(core: TerraModulus) : Closeable { } is MuiEvent.KeyboardKeyDown -> { logger.debug { "Keyboard (id: ${event.keyboardId}) key `${event.key}` down." } - keyEvents.add(InputSystem.InputEvent.Keyboard(KeyboardInputHandler.KeyEvent.Down(KeyboardInputHandler.KeyId(event.key)))) + inputEvents.add(InputSystem.InputEvent.Keyboard(KeyboardInputHandler.KeyEvent.Down(KeyboardInputHandler.KeyId(event.key)))) } is MuiEvent.KeyboardKeyUp -> { logger.debug { "Keyboard (id: ${event.keyboardId}) key `${event.key}` up." } - keyEvents.add(InputSystem.InputEvent.Keyboard(KeyboardInputHandler.KeyEvent.Up(KeyboardInputHandler.KeyId(event.key)))) + inputEvents.add(InputSystem.InputEvent.Keyboard(KeyboardInputHandler.KeyEvent.Up(KeyboardInputHandler.KeyId(event.key)))) } MuiEvent.KeyboardRemoved -> { logger.debug { "Keyboard removed." } @@ -140,13 +141,19 @@ internal class MuiManager internal constructor(core: TerraModulus) : Closeable { } is MuiEvent.MouseButtonDown -> { logger.debug { "Mouse (id: ${event.mouseId}) key `${event.key}` down." } - guiManager + inputEvents.add(InputSystem.InputEvent.Mouse(MouseInputHandler.Event.Button.Down(MouseInputHandler.ButtonId( + event.key.toUInt() + )))) } is MuiEvent.MouseButtonUp -> { logger.debug { "Mouse (id: ${event.mouseId}) key `${event.key}` up." } + inputEvents.add(InputSystem.InputEvent.Mouse(MouseInputHandler.Event.Button.Up(MouseInputHandler.ButtonId( + event.key.toUInt() + )))) } is MuiEvent.MouseMotion -> { - logger.debug { "Mouse (id: ${event.mouseId}) motion (${event.x}, ${event.y})." } + // y is inverted as coordinates in y are inversed from window coordinates to rendering coordinates + inputEvents.add(InputSystem.InputEvent.Mouse(MouseInputHandler.Event.Movement(event.x, -event.y))) } MuiEvent.MouseRemoved -> { logger.debug { "Mouse removed." } @@ -239,7 +246,8 @@ internal class MuiManager internal constructor(core: TerraModulus) : Closeable { } } } - kuiManager.inputSystem.update(keyEvents.asSequence()) + guiManager.inputStatesHandle.update(inputEvents.asSequence(), window.getMousePos()) + kuiManager.inputSystem.update(inputEvents.asSequence()) guiManager.updateScreens(this) guiManager.updateCanvas() } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputCtxStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputCtxStates.kt index fe8006a0..722d1b77 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputCtxStates.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputCtxStates.kt @@ -23,6 +23,12 @@ sealed class InputCtxStates( rect = RectRange.range(asdHandle.rect) }.also(asdHandle::observeRect) + init { + try { + rect = RectRange.range(asdHandle.rect) + } catch (_: UninitializedPropertyAccessException) {} + } + override fun close() { asdHandle.unobserveRect(listener) } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt index fc0629e0..37fc79e8 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt @@ -22,4 +22,8 @@ sealed class InputGlobalStates { triggers[it.key]!!.remove(it) } } + + internal fun triggerListeners(key: K, state: S) { + triggers[key]?.forEach { listeners[it]!!.act(state) } + } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputStatesHandle.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputStatesHandle.kt index 978f4065..013fdf52 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputStatesHandle.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputStatesHandle.kt @@ -5,6 +5,13 @@ package net.terramodulus.mui.gui +import com.cout970.math.vec2.Vec2f +import net.terramodulus.mui.kui.InputSystem.InputEvent + class InputStatesHandle { val mouseGlobalStates = MouseGlobalStates() + + internal fun update(events: Sequence, mousePos: Vec2f) { + mouseGlobalStates.update(events.filterIsInstance().map { it.inner }, mousePos) + } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseGlobalStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseGlobalStates.kt index b8ff005a..0903d8a0 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseGlobalStates.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/MouseGlobalStates.kt @@ -5,4 +5,34 @@ package net.terramodulus.mui.gui -class MouseGlobalStates : InputGlobalStates() +import com.cout970.math.vec2.ImmVec2d +import com.cout970.math.vec2.Vec2f +import net.terramodulus.mui.kui.MouseInputHandler + +class MouseGlobalStates : InputGlobalStates() { + internal fun update(events: Sequence, mousePos: Vec2f) { + val mousePos = ImmVec2d(mousePos.xd, mousePos.yd) + events.forEach { + when (it) { + is MouseInputHandler.Event.Button -> { + when (it) { + is MouseInputHandler.Event.Button.Down -> triggerListeners( + MouseState.Key.ButtonJustDown(it.key), + MouseState.ButtonJustDown(it.key, mousePos), + ) + is MouseInputHandler.Event.Button.Up -> triggerListeners( + MouseState.Key.ButtonJustUp(it.key), + MouseState.ButtonJustUp(it.key, mousePos), + ) + } + } + is MouseInputHandler.Event.Movement -> { + triggerListeners( + MouseState.Key.Movement, + MouseState.Movement(it.delX.toDouble(), it.delY.toDouble(), mousePos), + ) + } + } + } + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CheckboxComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CheckboxComponent.kt new file mode 100644 index 00000000..9609db7d --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/CheckboxComponent.kt @@ -0,0 +1,63 @@ +/* + * 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.InputStatesHandle +import net.terramodulus.mui.gui.MouseCtxStates +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.GuiLine +import net.terramodulus.mui.gui.gfx.RectangleD +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.mui.kui.MouseInputHandler + +class CheckboxComponent( + asdHandle: AsdHandle, + inputStatesHandle: InputStatesHandle, + canvasHandle: RenderSystem.CanvasHandle, + callback: (Boolean) -> Unit, +) : AbstractPane(asdHandle) { + private var checked = false + private val uncheckedFace = DrawablesComponent(sequenceOf( + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 0, 0, 50, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 0, 50, 0, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 50, 50, 50, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 50, 0, 50, 50, 255, 255, 255, 255)), + ), RectangleD(0.0, 0.0, 50.0, 50.0), ComponentAsdHandleImpl()) + private val checkedFace = DrawablesComponent(sequenceOf( + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 0, 0, 50, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 0, 50, 0, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 50, 50, 50, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 50, 0, 50, 50, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 10, 20, 20, 10, 255, 255, 255, 255)), + DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 20, 10, 45, 45, 255, 255, 255, 255)), + ), RectangleD(0.0, 0.0, 50.0, 50.0), ComponentAsdHandleImpl()) + override val layout = SingletonLayout(this, uncheckedFace, SingletonLayout.Config.Absolute.Full) +// override val layout = SingletonLayout(this, DrawablesComponent(object : Sequence { +// private val outline = sequenceOf( +// DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 0, 0, 50, 255, 255, 255, 255)), +// DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 0, 50, 0, 255, 255, 255, 255)), +// DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 0, 50, 50, 50, 255, 255, 255, 255)), +// DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 50, 0, 50, 50, 255, 255, 255, 255)), +// ) +// private val mark = sequenceOf( +// DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 10, 20, 20, 10, 255, 255, 255, 255)), +// DrawablesComponent.Drawable.Geom(GuiLine(canvasHandle, 20, 10, 45, 45, 255, 255, 255, 255)), +// ) +// override fun iterator(): Iterator = +// if (checked) { outline + mark } else { outline }.iterator() +// }, RectangleD(0.0, 0.0, 50.0, 50.0), ComponentAsdHandleImpl()), SingletonLayout.Config.Absolute.Full) + private val mouseCtxStates = MouseCtxStates(inputStatesHandle.mouseGlobalStates, asdHandle).apply { + addListener(listenRectFullClick(MouseInputHandler.Buttons.Left.id) { + checked = !checked + layout.update(if (checked) { checkedFace } else { uncheckedFace }) + callback(checked) + }) + } + + override fun render(renderSystem: RenderSystem) = layout.render(renderSystem) +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt index 816823c0..324cfe52 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt @@ -6,6 +6,7 @@ package net.terramodulus.mui.gui.agim.impl import com.cout970.math.quaternion.ImmQuatd +import com.cout970.math.vec2.Vec2d import com.cout970.math.vec3.ImmVec3d import com.cout970.math.vec3.Vec3d import com.cout970.math.vec3.Vec3f @@ -26,12 +27,15 @@ import net.terramodulus.engine.SimpleMesh3dGeomSphere import net.terramodulus.engine.WorldObjDrawable import net.terramodulus.engine.common.ZeroImmVec3d import net.terramodulus.mui.gui.InputStatesHandle +import net.terramodulus.mui.gui.MouseCtxStates +import net.terramodulus.mui.gui.MouseState 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.gfx.Direction6C +import net.terramodulus.mui.gui.gfx.GuiLine import net.terramodulus.mui.gui.gfx.InsetsD import net.terramodulus.mui.gui.gfx.RenderSystem import net.terramodulus.mui.gui.gfx.TextContext @@ -39,7 +43,11 @@ import net.terramodulus.mui.kui.KeyboardInputHandler import net.terramodulus.util.logging.logger import net.terramodulus.void.World import kotlin.math.PI +import kotlin.math.roundToInt +import kotlin.properties.Delegates import kotlin.random.Random +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TimeSource private val WHITE = ImmVec4i(255, 255, 255, 255) private val RED = ImmVec4i(255, 0, 0, 255) @@ -77,6 +85,7 @@ internal class GameplayScreen( private lateinit var player: PlayerVoidGeom override val layout = CompositeLayout(this) + private val mouseDebugTrackingLayer = MouseDebugTrackingLayer(renderSystemHandle.canvasHandle, inputStatesHandle) init { renderSystemHandle.setBackgroundColor(0F, 0F, 0F, 0F) @@ -97,10 +106,35 @@ internal class GameplayScreen( ComponentAsdHandleImpl(), renderSystemHandle, TextContext.Config(24.0F, 24.0F, ImmVec4i(255)), - ), SingletonLayout.Config.Absolute.Full) + ).apply { + text = "Button" + }, SingletonLayout.Config.Absolute.Full) }, SingletonLayout.Config.Absolute.Insets(InsetsD(20.0, 0.0, 0.0, 300.0)), )) + add(SingletonLayout( + this@GameplayScreen, + SimplePane(ComponentAsdHandleImpl()) { + SingletonLayout(this, SizedPane( + ComponentAsdHandleImpl(), + CheckboxComponent( + ComponentAsdHandleImpl(), + inputStatesHandle, + renderSystemHandle.canvasHandle, + ) { mouseDebugTrackingLayer.enabled = it }, + SizedPane.Config(100u, 100u), + ), SingletonLayout.Config.Aligned( + SingletonLayout.Config.ObjectFit.Contain, + SingletonLayout.Config.AlignmentConfig.withX(0.0), + )) + }, + SingletonLayout.Config.Absolute.Insets(InsetsD(20.0, 0.0, 0.0, 350.0)), + )) + add(SingletonLayout( + this@GameplayScreen, + mouseDebugTrackingLayer, + SingletonLayout.Config.Absolute.Full, + )) } this@GameplayScreen.addListener(ScreenEvent.Update::class.java) { update0(it.muiIoI) @@ -108,24 +142,64 @@ internal class GameplayScreen( } } } -// 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) -// val progressBarCtn = GeomComponent(progressBarCtnVal) -// addComponent(progressBarCtn) -// class Tracker : World.ProgressTracker { -// override val progress: AtomicInteger = AtomicInteger(0) -// override val max: AtomicInteger = AtomicInteger(0) -// override fun update() { -// progressBarCtnVal.setPos(0, 101, 100 * progress.get() / max.get(), 101) -// } -// } -// Thread { -// core.world = World(Tracker(), Ymir()) -// removeComponent(progressBarEdge) -// removeComponent(progressBarCtn) -// addComponent(GameplayRenderer()) -// }.start() + } + + private inner class MouseDebugTrackingLayer( + private val handle: RenderSystem.CanvasHandle, + inputStatesHandle: InputStatesHandle, + ) : Component(ComponentAsdHandleImpl()) { + private var prevPos: Vec2d? = null + private val lines = ArrayDeque() + private val timeSource = TimeSource.Monotonic + private val mouseCtxStates = MouseCtxStates(inputStatesHandle.mouseGlobalStates, asdHandle) + private val threshold = 3.seconds + var enabled: Boolean by Delegates.observable(false) { _, _, newValue -> if (!newValue) lines.clear() } + + private inner class Element(val timestamp: TimeSource.Monotonic.ValueTimeMark, val geom: GuiLine) + + init { + asdHandle.observeRect { + lines.clear() + prevPos = null + } + mouseCtxStates.addListener(MouseState.Listener( + setOf(MouseState.Trigger(MouseState.Key.Movement) { true }) + ) { + when (it) { + is MouseState.Movement -> { + if (prevPos != null) { + // Standard tracking aligned with SDL + lines.add(Element( + timeSource.markNow(), + GuiLine(handle, prevPos!!.xi, prevPos!!.yi, it.pos.xi, it.pos.yi, 255, 165, 0, 255), + )) + // Secondary tracking by relative values from event polling + val x = (prevPos!!.xd + it.delX).roundToInt() + val y = (prevPos!!.yd + it.delY).roundToInt() + lines.add(Element( + timeSource.markNow(), + GuiLine(handle, prevPos!!.xi, prevPos!!.yi, x, y, 0, 255, 0, 255), + )) + } + prevPos = it.pos + } + else -> throw AssertionError() + } + }) + } + + override fun render(renderSystem: RenderSystem) { + if (enabled) { + val now = timeSource.markNow() + lines.iterator().apply { + while (hasNext()) { + val it = next() + if (now - it.timestamp > threshold) remove() + else it.geom.render(renderSystem) + } + } + } + } } private inner class Ymir : World.Ymir { diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/kui/InputSystem.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/InputSystem.kt index f8f6c886..ac6d5fa9 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/kui/InputSystem.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/InputSystem.kt @@ -46,6 +46,7 @@ class InputSystem internal constructor(private val kuiManager: KuiManager) { internal sealed class InputEvent private constructor() { data class Keyboard(val inner: KeyboardInputHandler.KeyEvent) : InputEvent() + data class Mouse(val inner: MouseInputHandler.Event) : InputEvent() } internal fun update(events: Sequence) { diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/kui/MouseInputHandler.kt b/src/kernel/client/kotlin/net/terramodulus/mui/kui/MouseInputHandler.kt index 3f164073..29d32556 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/kui/MouseInputHandler.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/kui/MouseInputHandler.kt @@ -84,20 +84,28 @@ class MouseInputHandler(mouseDevice: MouseDevice) { fun condition(predicate: ButtonsScope.() -> ButtonPredicate) = ButtonsScope.predicate().test(ButtonPredicate.Helper(buttons)) - sealed class KeyEvent private constructor(internal open val key: ButtonId) { - data class Down(override val key: ButtonId) : KeyEvent(key) - data class Up(override val key: ButtonId) : KeyEvent(key) + internal sealed class Event private constructor() { + sealed class Button private constructor(open val key: ButtonId) : Event() { + data class Down(override val key: ButtonId) : Button(key) + data class Up(override val key: ButtonId) : Button(key) + } + data class Movement(val delX: Float, val delY: Float) : Event() } - internal fun update(events: Sequence) { + internal fun update(events: Sequence) { buttons.values.forEach { it.justChanged = false } events.forEach { - // Note: This may not handle the case where a key is just down less than a tick. - // This also assumes that keyboard states are consistent across time frames. - buttons[it.key]!!.justChanged = true when (it) { - is KeyEvent.Down -> buttons[it.key]!!.raw.down = true - is KeyEvent.Up -> buttons[it.key]!!.raw.down = false + is Event.Button -> { + // Note: This may not handle the case where a key is just down less than a tick. + // This also assumes that keyboard states are consistent across time frames. + buttons[it.key]!!.justChanged = true + when (it) { + is Event.Button.Down -> buttons[it.key]!!.raw.down = true + is Event.Button.Up -> buttons[it.key]!!.raw.down = false + } + } + is Event.Movement -> {} // TODO } } } From 806ea35e3d5f9f648886c0dca01b51310dd5f523 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Sat, 12 Sep 2026 07:36:29 +0800 Subject: [PATCH 26/28] MUI: Add FPS counter display Failed to use ODE to filter objects It turns out trimesh collisions only detect collisions with triangles but not inner space of objects. Planes should be used, but it would be too complex to implement again. Instead, likely there should be AABBs used in graphics without relying on physics engine. --- .idea/compiler.xml | 20 +++++------ build.gradle.kts | 20 ++++++++--- .../net/terramodulus/engine/Camera3D.kt | 4 +++ .../net/terramodulus/engine/ferricia/Gwr.kt | 7 ++++ .../kotlin/net/terramodulus/engine/PhyBody.kt | 8 ++++- .../net/terramodulus/core/TerraModulus.kt | 19 +++++++--- .../mui/gui/agim/impl/GameplayScreen.kt | 35 +++++++++++++++---- .../mui/gui/agim/impl/TextDisplayComponent.kt | 1 - .../common/core/AbstractTerraModulus.kt | 3 -- .../kotlin/net/terramodulus/void/World.kt | 8 ++++- 10 files changed, 94 insertions(+), 31 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index e254cd93..e3cab1e4 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,16 +1,16 @@ - - - - - - - - - - + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index bec2e46f..5a90eaf0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,7 @@ plugins { id("net.terramodulus.plugins.cargo") apply false // id("fr.stardustenterprises.rust.wrapper") version "3.2.4" apply false application + kotlin("kapt") version "2.3.21" } version = "0.0.1" @@ -121,6 +122,8 @@ configure(listOf(project(":internal:common"), project(":kernel:common"))) { } project(":kernel:common") { + apply(plugin = "org.jetbrains.kotlin.kapt") + dependencies { api("org.jetbrains:annotations:26.1.0") api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") @@ -136,7 +139,7 @@ project(":kernel:common") { implementation("org.apache.logging.log4j:log4j-api:2.24.3") implementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.24.3") implementation(platform("org.apache.logging.log4j:log4j-bom:2.24.3")) - annotationProcessor("org.apache.logging.log4j:log4j-core:2.24.3") + kapt("org.apache.logging.log4j:log4j-core:2.24.3") runtimeOnly("com.lmax:disruptor:4.0.0") api("io.github.oshai:kotlin-logging-jvm:7.0.3") implementation("net.sf.jopt-simple:jopt-simple:5.0.4") @@ -229,9 +232,18 @@ configure(listOf(project(":kernel:server"), project(":kernel:client"))) { } tasks.named("run") { - jvmArgs("-Djava.library.path=${rootProject.file("ferricia/target/${ - if (project.hasProperty("release")) "release" else "debug" - }").path}") + jvmArgs( + "-Djava.library.path=${ + rootProject.file( + "ferricia/target/${ + if (project.hasProperty("release")) "release" else "debug" + }" + ).path + }" + ) args("--screen-size", "800x500") } } +dependencies { + testImplementation(kotlin("test")) +} diff --git a/src/internal/client/kotlin/net/terramodulus/engine/Camera3D.kt b/src/internal/client/kotlin/net/terramodulus/engine/Camera3D.kt index ff6cd069..f5e500b4 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/Camera3D.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/Camera3D.kt @@ -5,6 +5,8 @@ package net.terramodulus.engine +import com.cout970.math.vec2.ImmVec2d +import net.terramodulus.engine.ferricia.Gwr.getCameraSpace import net.terramodulus.engine.ferricia.Gwr.newCamera import net.terramodulus.engine.ferricia.Gwr.refreshCameraPos import net.terramodulus.engine.ferricia.Gwr.setCameraZoomLevel @@ -16,6 +18,8 @@ class Camera3D internal constructor(private val canvas: Canvas, pos: FloatArray) fun loadGeoShaders(vsh: String, fsh: String) = canvas.load3DGeoShaders(vsh, fsh) + fun getSpace() = getCameraSpace(handle).let { ImmVec2d(it[0], it[1]) } + fun refreshPos(pos: FloatArray) = refreshCameraPos(handle, pos) var zoomLevel: Float by Delegates.observable(1F) { _, _, new -> 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 1261a316..c3f55316 100644 --- a/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Gwr.kt +++ b/src/internal/client/kotlin/net/terramodulus/engine/ferricia/Gwr.kt @@ -24,6 +24,13 @@ internal object Gwr { @JvmName("newCamera") external fun newCamera(canvasHandle: ULong, data: FloatArray): ULong + /** + * @param cameraHandle Camera3D handle pointer + * @return `[x, z]` space dimensions + */ + @JvmName("getCameraSpace") + external fun getCameraSpace(cameraHandle: ULong): DoubleArray + /** * @param cameraHandle Camera3D handle pointer * @param data `[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 6ce9bed6..1e626f3d 100644 --- a/src/internal/common/kotlin/net/terramodulus/engine/PhyBody.kt +++ b/src/internal/common/kotlin/net/terramodulus/engine/PhyBody.kt @@ -37,7 +37,13 @@ class PhyBody internal constructor(worldHandle: ULong, mass: Mass) { setPhyBodyGravityMode(handle, newValue) } - fun addGeom(geom: PhyGeom) = addPhyBodyGeom(handle, geom.handle) + private val _geoms = mutableSetOf() + val geoms: Set get() = _geoms + + fun addGeom(geom: PhyGeom) { + addPhyBodyGeom(handle, geom.handle) + _geoms.add(geom) + } fun addForce(force: Vec3d) = addPhyBodyForce(handle, force.toArray()) } diff --git a/src/kernel/client/kotlin/net/terramodulus/core/TerraModulus.kt b/src/kernel/client/kotlin/net/terramodulus/core/TerraModulus.kt index cc1efb8c..53cfb892 100644 --- a/src/kernel/client/kotlin/net/terramodulus/core/TerraModulus.kt +++ b/src/kernel/client/kotlin/net/terramodulus/core/TerraModulus.kt @@ -9,20 +9,31 @@ import net.terramodulus.common.core.AbstractTerraModulus import net.terramodulus.mui.MuiManager import net.terramodulus.mui.gui.GuiManager import net.terramodulus.void.World +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TimeSource class TerraModulus internal constructor() : AbstractTerraModulus() { private val muiManager = MuiManager(this) internal var world: World? = null - override var tps: Int - get() = TODO("Not yet implemented") - set(value) {} + var tps = 0 + private set override fun run() { muiManager.showWindow() + val timeSource = TimeSource.Monotonic + var lastTick = timeSource.markNow() + var ticks = 0 while (true) { muiManager.update() - Thread.sleep(1) + ticks++ + val now = timeSource.markNow() + if (now - lastTick >= 1.seconds) { + lastTick = now + tps = ticks + ticks = 0 + } + Thread.sleep(0) } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt index 324cfe52..7ae8a335 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt @@ -96,7 +96,7 @@ internal class GameplayScreen( this@GameplayScreen.layout.update { add(SingletonLayout( this@GameplayScreen, - GameplayRenderer(), + GameplayRenderer(renderSystemHandle), SingletonLayout.Config.Absolute.Full, )) add(SingletonLayout( @@ -235,8 +235,10 @@ internal class GameplayScreen( 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 { + private inner class PlayerVoidGeom( + override val phyBody: PhyBody, + drawable: WorldObjDrawable, + ) : VoidGeom(drawable), World.PlayerVoidGeom { fun move(dir: Vec3d) { if (dir == ZeroImmVec3d) return // avoid math errors and computations val dir = ImmVec3d(dir.x, dir.y, dir.z).normalized() @@ -428,11 +430,30 @@ internal class GameplayScreen( player.move(dirs.fold(ZeroImmVec3d, Vec3d::plus)) } - private inner class GameplayRenderer : Component(ComponentAsdHandleImpl()) { + private inner class GameplayRenderer(renderSystemHandle: RenderSystem.Handle) : + Component(ComponentAsdHandleImpl()) { + private val tpsText = TextContext(renderSystemHandle, TextContext.Config(16F, 16F, ImmVec4i(255))) + + init { + asdHandle.observeRect { + tpsText.update(asdHandle.rect) + } + } + override fun render(renderSystem: RenderSystem) { - if (core.world != null) core.world!!.objects.values.sortedWith( - compareBy { it.pos.y }.thenBy { it.pos.z } - ).forEach { it.render() } + if (core.world != null) { +// val range = camera.getSpace() * 1.1 // with little tolerance +// val ceil = 3 +// val floor = 10 +// worldStates.pos = player.pos.toMutVec3d().apply { y -= floor - (ceil + floor).toDouble() / 2 } +// worldStates.dims = ImmVec3d(range.x, (ceil + floor).toDouble(), range.y) + core.world!!.objects.values.sortedWith( + compareBy { it.pos.y }.thenBy { it.pos.z } + ).forEach { it.render() } + } + + tpsText.setText("${core.tps} FPS") + tpsText.render() } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TextDisplayComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TextDisplayComponent.kt index 3aae112f..eac524b9 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TextDisplayComponent.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TextDisplayComponent.kt @@ -24,7 +24,6 @@ class TextDisplayComponent( init { asdHandle.observeRect { context.update(asdHandle.rect) - println(asdHandle.rect) } } diff --git a/src/kernel/common/kotlin/net/terramodulus/common/core/AbstractTerraModulus.kt b/src/kernel/common/kotlin/net/terramodulus/common/core/AbstractTerraModulus.kt index b0373bdf..0636a9f2 100644 --- a/src/kernel/common/kotlin/net/terramodulus/common/core/AbstractTerraModulus.kt +++ b/src/kernel/common/kotlin/net/terramodulus/common/core/AbstractTerraModulus.kt @@ -15,8 +15,5 @@ import java.io.Closeable * @constructor Cannot be `internal` because `client` and `server` are other modules. */ abstract class AbstractTerraModulus : Closeable { - abstract var tps: Int - protected set - abstract fun run() } diff --git a/src/kernel/common/kotlin/net/terramodulus/void/World.kt b/src/kernel/common/kotlin/net/terramodulus/void/World.kt index 5b0d11e9..e8c6e20c 100644 --- a/src/kernel/common/kotlin/net/terramodulus/void/World.kt +++ b/src/kernel/common/kotlin/net/terramodulus/void/World.kt @@ -26,6 +26,8 @@ private val logger = logger {} class World(commander: Ymir, progressBar: ProgressBar) : Closeable { private val env = PhyEnv() private val world = env.createWorld() + var timePerTick = Duration.ZERO + private set var gravity: Vec3d by world::gravity var frictionMode: FrictionMode by Delegates.observable(FrictionMode.Infinite) { _, _, new -> @@ -88,6 +90,7 @@ class World(commander: Ymir, progressBar: ProgressBar) : Closeable { val now = timeSource.markNow() // remaining time after elapsed time used to maintain stable interval val rem = interval - (now - lastMark) + timePerTick = (now - lastMark) if (rem > Duration.ZERO) { // sleeps the remaining time only when it is positive Thread.sleep(rem.inWholeMilliseconds) } @@ -96,7 +99,6 @@ class World(commander: Ymir, progressBar: ProgressBar) : Closeable { } }.start() }.start() - } interface Ymir { @@ -111,14 +113,18 @@ class World(commander: Ymir, progressBar: ProgressBar) : Closeable { fun render() val pos: Vec3d + + val phyGeoms: Sequence } interface EnvVoidGeom : VoidGeom { val phyGeom: PhyGeom + override val phyGeoms: Sequence get() = sequenceOf(phyGeom) } interface PlayerVoidGeom : VoidGeom { val phyBody: PhyBody + override val phyGeoms: Sequence get() = phyBody.geoms.asSequence() } // Source: https://en.wikipedia.org/wiki/Maze_generation_algorithm From 66b73a2c34954170c62469ceb55ce88115e81239 Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:43:23 +0800 Subject: [PATCH 27/28] GUI: Add basic track bar + slider + scale bar Mostly lack true implementations and now they are just drafts, but some would at least provide basic functionalities BorderedPane is yet unusable --- .../mui/gui/agim/impl/BorderedPane.kt | 34 ++++ .../mui/gui/agim/impl/GameplayScreen.kt | 2 +- .../mui/gui/agim/impl/GraphicsComponent.kt | 58 +++++- .../mui/gui/agim/impl/ScaledBarComponent.kt | 85 ++++++++ .../mui/gui/agim/impl/SequenceLayout.kt | 26 ++- .../mui/gui/agim/impl/SliderComponent.kt | 178 +++++++++++------ .../mui/gui/agim/impl/TrackBarComponent.kt | 186 ++++++++++++++++++ .../mui/gui/agim/impl/WorldInitScreen.kt | 8 +- .../net/terramodulus/mui/gui/gfx/Direction.kt | 26 ++- .../mui/gui/gfx/ModelTransform.kt | 34 ++++ 10 files changed, 550 insertions(+), 87 deletions(-) create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BorderedPane.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScaledBarComponent.kt create mode 100644 src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TrackBarComponent.kt diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BorderedPane.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BorderedPane.kt new file mode 100644 index 00000000..305b669b --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/BorderedPane.kt @@ -0,0 +1,34 @@ +/* + * 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.Component +import net.terramodulus.mui.gui.agim.Layout +import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.InsetsD +import net.terramodulus.mui.gui.gfx.RenderSystem + +class BorderedPane(canvasHandle: RenderSystem.CanvasHandle, asdHandle: AsdHandle, component: Component, config: Config) : AbstractPane(asdHandle) { + private val _layout = (config.breadth + config.gap).let { + SingletonLayout(this, component, SingletonLayout.Config.Absolute.Insets(InsetsD(it, it, it, it))) + } + override val layout: Layout = CompositeLayout(this).apply { + update { + // TODO incompatible with current implementation of OutlineComponent +// add(SingletonLayout(this, OutlineComponent(), SingletonLayout.Config.Absolute.Full)) + add(_layout) + } + } + + fun update(component: Component) = _layout.update(component) + + class Config(val breadth: Double, val gap: Double) + + override fun render(renderSystem: RenderSystem) { + layout.render(renderSystem) + } +} diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt index 7ae8a335..b783578c 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt @@ -165,7 +165,7 @@ internal class GameplayScreen( mouseCtxStates.addListener(MouseState.Listener( setOf(MouseState.Trigger(MouseState.Key.Movement) { true }) ) { - when (it) { + if (enabled) when (it) { is MouseState.Movement -> { if (prevPos != null) { // Standard tracking aligned with SDL 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 index bd189193..824c76cd 100644 --- 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 @@ -5,18 +5,24 @@ package net.terramodulus.mui.gui.agim.impl +import com.cout970.math.vec4.Vec4i 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.agim.impl.DrawablesComponent.Drawable import net.terramodulus.mui.gui.asd.AsdHandle +import net.terramodulus.mui.gui.gfx.Anchor5 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.GuiRect 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 +import kotlin.math.roundToInt +import kotlin.sequences.forEach sealed interface GraphicsComponent @@ -87,7 +93,57 @@ class DrawablesComponent( } } -@Suppress("CanSealedSubClassBeObject") +/** + * @param bounds Outer bounds of the outline + */ +class OutlineComponent( + canvasHandle: RenderSystem.CanvasHandle, + private val bounds: RectangleD, + breadth: Double, + color: Vec4i, + asdHandle: AsdHandle, +) : Component(asdHandle) { + private val transform = GeneralTransform() + private val geoms: Array + + init { + val tr = bounds.anchor(Anchor5.TopRight) + val bl = bounds.anchor(Anchor5.BottomLeft) + geoms = arrayOf( + GuiRect(canvasHandle, + bl.x.roundToInt(), (tr.y - breadth).roundToInt(), (tr.x - breadth).roundToInt(), tr.y.roundToInt(), + color.x, color.y, color.z, color.w, + ), + GuiRect(canvasHandle, + (tr.x - breadth).roundToInt(), (bl.y + breadth).roundToInt(), tr.x.roundToInt(), tr.y.roundToInt(), + color.x, color.y, color.z, color.w, + ), + GuiRect(canvasHandle, + (bl.x + breadth).roundToInt(), bl.y.roundToInt(), tr.x.roundToInt(), (bl.y + breadth).roundToInt(), + color.x, color.y, color.z, color.w, + ), + GuiRect(canvasHandle, + bl.x.roundToInt(), bl.y.roundToInt(), (bl.x + breadth).roundToInt(), (bl.y - breadth).roundToInt(), + color.x, color.y, color.z, color.w, + ), + ) + geoms.forEach { it.add(transform) } + + 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) + } + } + + fun addFilter(filter: ColorFilter) = geoms.forEach { it.add(filter) } + + override fun render(renderSystem: RenderSystem) { + geoms.forEach { it.render(renderSystem) } + } +} + 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/ScaledBarComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScaledBarComponent.kt new file mode 100644 index 00000000..e2890859 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ScaledBarComponent.kt @@ -0,0 +1,85 @@ +/* + * 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.engine.GeneralTransform +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.GuiRect +import net.terramodulus.mui.gui.gfx.RectStParams +import net.terramodulus.mui.gui.gfx.RectangleI +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.util.LateInitObservable + +/** + * A single scaled bar with the provided dimensions as the bounds of the bar. + */ +open class ScaledBarComponent protected constructor( + canvasHandle: RenderSystem.CanvasHandle, + asdHandle: AsdHandle, + config: Config, +) : Component(asdHandle) { + constructor( + canvasHandle: RenderSystem.CanvasHandle, + asdHandle: AsdHandle, + config: ConstructEnvImpl.() -> Config, + ) : this(canvasHandle, asdHandle, config(ConstructEnvImpl)) + + companion object { + @JvmStatic + protected val BOUNDS = RectangleI(0, 0, 1, 1) + } + + private val scaleTransform = GeneralTransform() + protected val boundsTransform = GeneralTransform() + protected val dir = config.dir + protected val bar = GuiRect(canvasHandle, + BOUNDS.x, BOUNDS.y, BOUNDS.width, BOUNDS.height, + config.color.x, config.color.y, config.color.z, config.color.w, + ).apply { + add(scaleTransform) + add(boundsTransform) + } + + var fraction by LateInitObservable { _, _, new -> update(new) } + + init { + if (config.fraction != null) fraction = config.fraction + asdHandle.observeRect { + RectStParams.fromRects(BOUNDS.toDouble(), asdHandle.rect).applyToGeneralTransform(boundsTransform) + } + } + + fun addFilter(filter: ColorFilter) = bar.add(filter) + + @Suppress("unused") + interface ConstructEnv { + val xPos get() = Direction4A.XPos + val xNeg get() = Direction4A.XNeg + val yPos get() = Direction4A.YPos + val yNeg get() = Direction4A.YNeg + } + + object ConstructEnvImpl : ConstructEnv { + fun config(dir: Direction4A, color: Vec4i, fraction: Double? = null) = + Config(dir, color, fraction) + } + + open class Config( + val dir: Direction4A, + val color: Vec4i, + val fraction: Double? = null + ) + + private fun update(fraction: Double) { + RectStParams.withScale(BOUNDS.toDouble(), dir, fraction).applyToGeneralTransform(scaleTransform) + } + + override fun render(renderSystem: RenderSystem) = bar.render(renderSystem) +} 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 index 6d726bad..041833c5 100644 --- 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 @@ -13,10 +13,8 @@ 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.Direction2S import net.terramodulus.mui.gui.gfx.RectangleD import kotlin.math.max @@ -41,7 +39,7 @@ sealed class SequenceLayout( * [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) + class Config(val direction: Direction2S, val gap: Double = 0.0, val padding: Double = 0.0) interface ConfigEnv { var config: Config @@ -99,8 +97,8 @@ class ColumnLayout private constructor(container: Container, elements: ElementLi }, { 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 + Direction2S.Positive -> containerRect.y + config.padding + Direction2S.Negative -> containerRect.y + containerRect.height - config.padding }) var width = 0.0 var height = 0.0 @@ -113,15 +111,15 @@ class ColumnLayout private constructor(container: Container, elements: ElementLi 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() + if (config.direction == Direction2S.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 + Direction2S.Positive -> anchor.y += dim.height.toDouble() + config.gap + Direction2S.Negative -> anchor.y -= config.gap } } map[container.asdHandle] = AgimoPropertyMap().apply { @@ -168,8 +166,8 @@ class RowLayout private constructor(container: Container, elements: ElementList< }, { 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 + Direction2S.Positive -> containerRect.x + config.padding + Direction2S.Negative -> containerRect.x + containerRect.width - config.padding }, containerRect.y + config.gap) var width = 0.0 var height = 0.0 @@ -182,15 +180,15 @@ class RowLayout private constructor(container: Container, elements: ElementList< 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() + if (config.direction == Direction2S.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 + Direction2S.Positive -> anchor.x += dim.width.toDouble() + config.gap + Direction2S.Negative -> anchor.x -= config.gap } } map[container.asdHandle] = AgimoPropertyMap().apply { 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 index bc867d03..aa7f048b 100644 --- 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 @@ -5,93 +5,141 @@ package net.terramodulus.mui.gui.agim.impl +import com.cout970.math.vec2.Vec2d import com.cout970.math.vec4.Vec4i -import net.terramodulus.mui.gui.agim.Component +import net.terramodulus.mui.gui.InputStatesHandle +import net.terramodulus.mui.gui.MouseCtxStates +import net.terramodulus.mui.gui.MouseState 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 net.terramodulus.mui.kui.MouseInputHandler import kotlin.math.roundToInt -import kotlin.properties.Delegates -class SliderComponent( - val dir: Direction4A, +/** + * Interactive Slider + * + * Caveat: [fraction] must not be externally modified. + */ +class SliderComponent private constructor( canvasHandle: RenderSystem.CanvasHandle, + inputStatesHandle: InputStatesHandle, 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) +) : ScaledBarComponent(canvasHandle, asdHandle, config) { + constructor( + canvasHandle: RenderSystem.CanvasHandle, + inputStatesHandle: InputStatesHandle, + asdHandle: AsdHandle, + config: ConstructEnv.() -> Config, + ) : this(canvasHandle, inputStatesHandle, asdHandle, config(ConstructEnv)) + + sealed class SliderMode { + /** + * Translates `fraction` to a desired value point when necessary. + */ + internal abstract fun translate(fraction: Double): Double? + + internal abstract fun react(fraction: Double) + + // Hard to specify type here, so Any is used instead, but should be careful + internal abstract fun getFraction(value: Any): Double + + /** + * @param length must be positive + */ + class Points(val length: Int, val reaction: (Int) -> Unit) : SliderMode() { + override fun translate(fraction: Double) = + (fraction * length).roundToInt().coerceIn(0, length).toDouble() / length + + override fun react(fraction: Double) = reaction((fraction * length).roundToInt().coerceIn(0, length)) + + /** + * @param value must be [Int] in `0`..[length] + */ + override fun getFraction(value: Any): Double { + require(value is Int && value in 0..length) + return value / length.toDouble() + } + } + + class Ranged(val range: ClosedFloatingPointRange, val reaction: (Double) -> Unit) : SliderMode() { + override fun translate(fraction: Double) = null + + override fun react(fraction: Double) = reaction(fraction * (range.endInclusive - range.start) + range.start) + + /** + * @param value must be [Double] in [range] + */ + override fun getFraction(value: Any): Double { + require(value is Double && value in range) + return (value - range.start) / (range.endInclusive - range.start) + } + } } - private val bgTransform = GeneralTransform() - private val fgTransform = GeneralTransform() + private val sliderMode = config.mode + 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) + ).apply { add(boundsTransform) } - init { - asdHandle.observeRect { - RectStParams.fromRects(BOUNDS.toDouble(), asdHandle.rect).applyToGeneralTransform(bgTransform) - updateForeground(asdHandle.rect, fraction) - } + object ConstructEnv : ScaledBarComponent.ConstructEnv { + fun withPoints(length: Int, init: Int, reaction: (Int) -> Unit) = + SliderMode.Points(length, reaction).let { it to it.getFraction(init) } + fun withRanged(range: ClosedFloatingPointRange, init: Double, reaction: (Double) -> Unit) = + SliderMode.Ranged(range, reaction).let { it to it.getFraction(init) } + fun config(mode: Pair, dir: Direction4A, bgColor: Vec4i, fgColor: Vec4i) = + Config(mode.first, dir, bgColor, fgColor, mode.second) } - fun addFilter(filter: ColorFilter) { - background.add(filter) - foreground.add(filter) - } + class Config(val mode: SliderMode, dir: Direction4A, val bgColor: Vec4i, fgColor: Vec4i, fraction: Double) : + ScaledBarComponent.Config(dir, fgColor, fraction) - 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) + private val mouseCtxStates = MouseCtxStates(inputStatesHandle.mouseGlobalStates, asdHandle).apply { + var reacting = false + addListener(MouseState.Listener( + setOf(MouseState.Trigger(MouseState.Key.ButtonJustDown(MouseInputHandler.Buttons.Left.id)) { true }) + ) { + assert(it is MouseState.ButtonJustDown && MouseInputHandler.Buttons.Left.matches(it.id)) + assert(!reacting) + if (ctxRange.rect.contains((it as MouseState.ButtonJustDown).pos)) reacting = true + }) + addListener(MouseState.Listener(setOf( + MouseState.Trigger(MouseState.Key.Movement) { reacting }, + MouseState.Trigger(MouseState.Key.ButtonJustUp(MouseInputHandler.Buttons.Left.id)) { reacting }, + )) { + assert(reacting) + val pos: Vec2d + when (it) { + is MouseState.Movement -> { + pos = it.pos + } + is MouseState.ButtonJustUp -> { + assert(MouseInputHandler.Buttons.Left.matches(it.id)) + pos = it.pos + reacting = false + } + else -> throw AssertionError() + } + val rect = asdHandle.rect + val fraction = when (dir) { + Direction4A.XPos -> ((pos.x - rect.x) / rect.width).coerceIn(0.0, 1.0) + // optimized from: (rect.x + rect.width - pos.x) / rect.width + Direction4A.XNeg -> ((rect.x - pos.x) / rect.width + 1).coerceIn(0.0, 1.0) + Direction4A.YPos -> ((pos.y - rect.y) / rect.height).coerceIn(0.0, 1.0) + // optimized from: (rect.y + rect.height - pos.y) / rect.height + Direction4A.YNeg -> ((rect.y - pos.y) / rect.height + 1).coerceIn(0.0, 1.0) + } + this@SliderComponent.fraction = sliderMode.translate(fraction) ?: fraction + sliderMode.react(fraction) + }) } override fun render(renderSystem: RenderSystem) { background.render(renderSystem) - foreground.render(renderSystem) + super.render(renderSystem) } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TrackBarComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TrackBarComponent.kt new file mode 100644 index 00000000..06ccfd10 --- /dev/null +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/TrackBarComponent.kt @@ -0,0 +1,186 @@ +/* + * 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.Dimension2D +import net.terramodulus.mui.gui.gfx.Direction4A +import net.terramodulus.mui.gui.gfx.GeneralTransform +import net.terramodulus.mui.gui.gfx.GuiRect +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.RectangleI +import net.terramodulus.mui.gui.gfx.RenderSystem +import net.terramodulus.util.LateInitObservable + +/** + * Track Bar, with marks on the bar. + */ +// TODO Add decoration marks like the handle +class TrackBarComponent( + canvasHandle: RenderSystem.CanvasHandle, + asdHandle: AsdHandle, + config: ConstructEnv.() -> ConstructEnv.Config, +) : Component(asdHandle) { + private sealed class BarMode private constructor() { + companion object { + val BOUNDS = RectangleI(0, 0, 1, 1) + } + + abstract fun addModel(modelTransform: ModelTransform) + + abstract fun updateBar(fraction: Double) + + abstract fun renderBar(renderSystem: RenderSystem) + + class Singleton(canvasHandle: RenderSystem.CanvasHandle, color: Vec4i) : BarMode() { + private val bar = GuiRect(canvasHandle, + BOUNDS.x, BOUNDS.y, BOUNDS.width, BOUNDS.height, + color.x, color.y, color.z, color.w, + ) + + override fun addModel(modelTransform: ModelTransform) = bar.add(modelTransform) + + override fun updateBar(fraction: Double) {} + + override fun renderBar(renderSystem: RenderSystem) = bar.render(renderSystem) + } + + class Sectioned( + canvasHandle: RenderSystem.CanvasHandle, + private val dir: Direction4A, + priColor: Vec4i, + secColor: Vec4i, + ) : BarMode() { + // Primary section refers to the section uncovered by the fraction and the direction. + // Secondary section refers to the section directed by the fraction with the direction. + private val priBar = GuiRect(canvasHandle, + BOUNDS.x, BOUNDS.y, BOUNDS.width, BOUNDS.height, + priColor.x, priColor.y, priColor.z, priColor.w, + ) + private val secBar = GuiRect(canvasHandle, + BOUNDS.x, BOUNDS.y, BOUNDS.width, BOUNDS.height, + secColor.x, secColor.y, secColor.z, secColor.w, + ) + private val priTransform = GeneralTransform().apply { priBar.add(this) } + private val secTransform = GeneralTransform().apply { secBar.add(this) } + + override fun addModel(modelTransform: ModelTransform) { + priBar.add(modelTransform) + secBar.add(modelTransform) + } + + override fun updateBar(fraction: Double) { + RectStParams.withScale(BOUNDS.toDouble(), dir.toOppo(), fraction).applyToGeneralTransform(priTransform) + RectStParams.withScale(BOUNDS.toDouble(), dir, fraction).applyToGeneralTransform(secTransform) + } + + override fun renderBar(renderSystem: RenderSystem) { + priBar.render(renderSystem) + secBar.render(renderSystem) + } + } + } + + private val barMode: BarMode + private val size: Dimension2D + private val bounds: RectangleD // calculated from `size` with origin (0, 0) + private val barRect: RectangleD + private val dir: Direction4A + + private val barTransform = GeneralTransform() + private val mainTransform = GeneralTransform() + + var fraction by LateInitObservable { _, _, new -> update(new) } + + init { + val config = config(ConstructEnv) + barMode = when (config.mode) { + is ConstructEnv.BarMode.Sectioned -> + BarMode.Sectioned(canvasHandle, config.dir, config.mode.priColor, config.mode.secColor) + is ConstructEnv.BarMode.Singleton -> BarMode.Singleton(canvasHandle, config.mode.color) + } + barMode.addModel(barTransform) + barMode.addModel(mainTransform) + size = config.size + bounds = RectangleD(0.0, 0.0, size.width, size.height) + barRect = when (config.dir) { + Direction4A.XPos -> RectangleD( + config.offMin, + (size.height - config.breadth) / 2, + size.width - config.offMin - config.offMax, + config.breadth, + ) + Direction4A.XNeg -> RectangleD( + config.offMax, + (size.height - config.breadth) / 2, + size.width - config.offMin - config.offMax, + config.breadth, + ) + Direction4A.YPos -> RectangleD( + (size.width - config.breadth) / 2, + config.offMin, + config.breadth, + size.height - config.offMin - config.offMax, + ) + Direction4A.YNeg -> RectangleD( + (size.width - config.breadth) / 2, + config.offMax, + config.breadth, + size.height - config.offMin - config.offMax, + ) + } + RectStParams.fromRects(BarMode.BOUNDS.toDouble(), barRect).applyToGeneralTransform(barTransform) + dir = config.dir + if (config.fraction != null) fraction = config.fraction + + val dim = IntrinsicDimensionsProperty(size.width.toUInt(), size.height.toUInt()) + asdHandle.properties.putProperty(IntrinsicDimensionsProperty.KEY, dim) + asdHandle.properties.putProperty(IntrinsicRatioProperty.KEY, dim.computeRatio()) + asdHandle.observeRect { + RectStParams.fromRects(bounds, asdHandle.rect).applyToGeneralTransform(mainTransform) + } + } + + @Suppress("unused") + object ConstructEnv { + val XPos = Direction4A.XPos + val XNeg = Direction4A.XNeg + val YPos = Direction4A.YPos + val YNeg = Direction4A.YNeg + sealed class BarMode { + data class Singleton(val color: Vec4i) : BarMode() + data class Sectioned(val priColor: Vec4i, val secColor: Vec4i) : BarMode() + } + + /** + * Offsets must be non-negative, as insets from bounds. + * Targets of offsets depend on the direction. + * The dimension in the cross axis should be larger or equal than the breadth. + */ + class Config( + val mode: BarMode, + // TODO Currently fixed sized, but later should be dynamic + val size: Dimension2D, + val dir: Direction4A, + val offMin: Double, + val offMax: Double, + val breadth: Double, + val fraction: Double? = null + ) + } + + private fun update(fraction: Double) { + barMode.updateBar(fraction) + } + + override fun render(renderSystem: RenderSystem) { + barMode.renderBar(renderSystem) + } +} 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 index f477684f..19779f7c 100644 --- 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 @@ -11,6 +11,7 @@ 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.Dimension2D import net.terramodulus.mui.gui.gfx.Direction4A import net.terramodulus.mui.gui.gfx.GuiRect import net.terramodulus.mui.gui.gfx.GuiSprite @@ -33,13 +34,10 @@ class WorldInitScreen internal constructor( private var last = System.currentTimeMillis() // timestamp in milliseconds private var alphaFilter = AlphaFilter(0F) // private val progressBar = ProgressBar(renderSystemHandle) - private val progressBarComponent = SliderComponent( - Direction4A.XPos, + private val progressBarComponent = ScaledBarComponent( 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 { + ) { config(xPos, ImmVec4i(59, 12, 120, 255), 0.0) }.apply { addFilter(alphaFilter) } override val layout = CompositeLayout(this) diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt index b8d78bd6..e1205562 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/Direction.kt @@ -11,10 +11,27 @@ package net.terramodulus.mui.gui.gfx * Those should be used with care since they are not already interconvertible. */ -enum class Direction2 { +/** + * Set of 2 (numeric) signed directions + */ +enum class Direction2S { Positive, Negative; } +/** + * Set of 2 diagonal directions + */ +enum class Direction2D { + Horizontal, Vertical; + + companion object { // aliases + val Horiz = Horizontal + val Hor = Horizontal + val Vert = Vertical + val Vrt = Vertical + } +} + /** * Set of 4 compass directions. */ @@ -41,6 +58,13 @@ enum class Direction4H { */ enum class Direction4A { XPos, XNeg, YPos, YNeg; + + fun toOppo() = when (this) { + XPos -> XNeg + XNeg -> XPos + YPos -> YNeg + YNeg -> YPos + } } /** 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 index c561ac4c..590c4280 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ModelTransform.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/gfx/ModelTransform.kt @@ -29,6 +29,40 @@ data class RectStParams(val scaleX: Double, val scaleY: Double, val translateX: val ty = b.y - a.y * sy return RectStParams(sx, sy, tx, ty) } + + fun withScale(bounds: RectangleD, dir: Direction4A, scale: Double): RectStParams { + val sx: Double + val sy: Double + val tx: Double + val ty: Double + when (dir) { + Direction4A.XPos -> { + sx = scale + sy = 1.0 + tx = 0.0 + ty = 0.0 + } + Direction4A.XNeg -> { + sx = scale + sy = 1.0 + tx = bounds.width * (1 - sx) + ty = 0.0 + } + Direction4A.YPos -> { + sx = 1.0 + sy = scale + tx = 0.0 + ty = 0.0 + } + Direction4A.YNeg -> { + sx = 1.0 + sy = scale + tx = 0.0 + ty = bounds.height * (1 - sy) + } + } + return RectStParams(sx, sy, tx, ty) + } } fun applyToGeneralTransform(generalTransform: GeneralTransform) { From 8c2f782affb7bc2adb773692ed56ca243f91a35b Mon Sep 17 00:00:00 2001 From: Ben Forge <74168521+BenCheung0422@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:24:50 +0800 Subject: [PATCH 28/28] MUI: Fix button and certain layouts Also add demo for sliders and sequence layout --- ferricia | 2 +- .../terramodulus/mui/gui/InputGlobalStates.kt | 2 +- .../mui/gui/agim/impl/ButtonComponent.kt | 5 +- .../mui/gui/agim/impl/GameplayScreen.kt | 61 ++++++++++++++++++- .../mui/gui/agim/impl/SequenceLayout.kt | 2 +- .../mui/gui/agim/impl/WorldInitScreen.kt | 2 +- 6 files changed, 64 insertions(+), 10 deletions(-) diff --git a/ferricia b/ferricia index 5bbb5d96..4dc0564f 160000 --- a/ferricia +++ b/ferricia @@ -1 +1 @@ -Subproject commit 5bbb5d96d3ddda0703a610f4d2a30566301f7450 +Subproject commit 4dc0564ff639b179d9e152c68fe7e97b341224b5 diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt index 37fc79e8..88239010 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/InputGlobalStates.kt @@ -24,6 +24,6 @@ sealed class InputGlobalStates { } internal fun triggerListeners(key: K, state: S) { - triggers[key]?.forEach { listeners[it]!!.act(state) } + triggers[key]?.forEach { if (it.check(state)) listeners[it]!!.act(state) } } } diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ButtonComponent.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ButtonComponent.kt index 45947827..25c7764f 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ButtonComponent.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/ButtonComponent.kt @@ -17,12 +17,11 @@ class ButtonComponent( asdHandle: AsdHandle, inputStatesHandle: InputStatesHandle, layout: ButtonComponent.() -> Layout, + action: () -> Unit, ) : AbstractPane(asdHandle) { override val layout = layout(this) private val mouseCtxStates = MouseCtxStates(inputStatesHandle.mouseGlobalStates, asdHandle).apply { - addListener(listenRectFullClick(MouseInputHandler.Buttons.Left.id) { - println("Hello World!") - }) + addListener(listenRectFullClick(MouseInputHandler.Buttons.Left.id) { action() }) } override fun render(renderSystem: RenderSystem) = layout.render(renderSystem) diff --git a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt index b783578c..d9f351fa 100644 --- a/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt +++ b/src/kernel/client/kotlin/net/terramodulus/mui/gui/agim/impl/GameplayScreen.kt @@ -34,6 +34,7 @@ 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.Direction2S import net.terramodulus.mui.gui.gfx.Direction6C import net.terramodulus.mui.gui.gfx.GuiLine import net.terramodulus.mui.gui.gfx.InsetsD @@ -101,17 +102,71 @@ internal class GameplayScreen( )) add(SingletonLayout( this@GameplayScreen, - ButtonComponent(ComponentAsdHandleImpl(), inputStatesHandle) { + ButtonComponent(ComponentAsdHandleImpl(), inputStatesHandle, { SingletonLayout(this, TextDisplayComponent( ComponentAsdHandleImpl(), renderSystemHandle, TextContext.Config(24.0F, 24.0F, ImmVec4i(255)), ).apply { - text = "Button" + text = "Button\nABC" }, SingletonLayout.Config.Absolute.Full) - }, + }) { println("Button reacted.") }, SingletonLayout.Config.Absolute.Insets(InsetsD(20.0, 0.0, 0.0, 300.0)), )) + add(SingletonLayout( + this@GameplayScreen, + SimplePane(ComponentAsdHandleImpl()) { + ColumnLayout.withComponents(listOf( + SizedPane(ComponentAsdHandleImpl(), SimplePane(ComponentAsdHandleImpl()) { + val parent = this + CompositeLayout(this).apply { + val slider: SliderComponent + lateinit var listener: (Double) -> Unit + add(SingletonLayout(parent, SliderComponent( + renderSystemHandle.canvasHandle, + inputStatesHandle, ComponentAsdHandleImpl() + ) { + config(withRanged(-1.0..5.0, 0.0) { listener(it) }, + xPos, ImmVec4i(123, 234, 56, 255), ImmVec4i(50, 50, 250, 255)) + }.apply { slider = this }, SingletonLayout.Config.Absolute.Full)) + add(SingletonLayout(parent, + TextDisplayComponent(ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(25F, 25F, ImmVec4i(255)) + ).apply { + text = "0.0" + listener = { text = "$it" } + }, + SingletonLayout.Config.Absolute.Full, + )) + } + }, SizedPane.Config(100u, 25u)), + SizedPane(ComponentAsdHandleImpl(), SimplePane(ComponentAsdHandleImpl()) { + val parent = this + CompositeLayout(this).apply { + val slider: SliderComponent + lateinit var listener: (Int) -> Unit + add(SingletonLayout(parent, SliderComponent( + renderSystemHandle.canvasHandle, + inputStatesHandle, ComponentAsdHandleImpl() + ) { + config(withPoints(6, 1) { listener(it) }, + xPos, ImmVec4i(203, 234, 56, 255), ImmVec4i(50, 50, 250, 255)) + }.apply { slider = this }, SingletonLayout.Config.Absolute.Full)) + add(SingletonLayout(parent, + TextDisplayComponent(ComponentAsdHandleImpl(), renderSystemHandle, + TextContext.Config(25F, 25F, ImmVec4i(255)) + ).apply { + text = "1" + listener = { text = "$it" } + }, + SingletonLayout.Config.Absolute.Full, + )) + } + }, SizedPane.Config(100u, 25u)), + ), SequenceLayout.Config(Direction2S.Negative))(this) + }, + SingletonLayout.Config.Absolute.Insets(InsetsD(20.0, 180.0, 0.0, 200.0)), + )) add(SingletonLayout( this@GameplayScreen, SimplePane(ComponentAsdHandleImpl()) { 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 index 041833c5..cbc5aa67 100644 --- 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 @@ -26,7 +26,7 @@ import kotlin.math.max */ sealed class SequenceLayout( container: Container, - override val elements: ElementList, + elements: ElementList, protected var config: Config, ) : Layout.ElementGroup(container, elements) { class Element { 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 index 19779f7c..65c0dc4f 100644 --- 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 @@ -106,7 +106,7 @@ class WorldInitScreen internal constructor( color = ImmVec4i(255, 255, 255, (alphaFilter.alpha * 255).roundToInt()) } } - }, SingletonLayout.Config.Absolute.Insets(InsetsD(7.0, 149.0, 7.0, 35.0))) + }, SingletonLayout.Config.Absolute.Insets(InsetsD(7.0, 142.0, 7.0, 42.0))) }, SizedPane.Config(400u, 200u)), SingletonLayout.Config.Aligned( SingletonLayout.Config.ObjectFit.Contain,