commit 340392bcf9a5d8fdacae3b5445de5d7912aaeaab Author: MiniDay <372403923@qq.com> Date: Mon Jan 13 23:11:52 2025 +0800 feat: 初版提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e51cb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.gradle +.idea +.kotlin +build +run \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3ae1a0a --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# ComposeUI + +将 Jetpack Compose 集成到 Minecraft 中 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..af8b9f2 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,80 @@ +plugins { + kotlin("jvm") + id("maven-publish") + id("fabric-loom") version "1.9-SNAPSHOT" + + id("org.jetbrains.compose") + id("org.jetbrains.kotlin.plugin.compose") + + id("com.github.johnrengelman.shadow") version "8+" +} + +version = project.property("mod_version") as String +group = project.property("maven_group") as String + +base { + archivesName = project.property("archives_base_name") as String +} +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + withSourcesJar() +} +kotlin { + jvmToolchain(21) +} +loom { + splitEnvironmentSourceSets() + + mods { + register("compose-ui-mod") { + sourceSet("main") + sourceSet("client") + } + } + +} + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() +} + +dependencies { + // To change the versions see the gradle.properties file + minecraft("com.mojang:minecraft:${project.property("minecraft_version")}") + mappings("net.fabricmc:yarn:${project.property("yarn_mappings")}:v2") + modImplementation("net.fabricmc:fabric-loader:${project.property("loader_version")}") + modImplementation("net.fabricmc:fabric-language-kotlin:${project.property("kotlin_loader_version")}") + + modImplementation("net.fabricmc.fabric-api:fabric-api:${project.property("fabric_version")}") + + implementation(compose.desktop.currentOs) + includeInternal(compose.desktop.currentOs) { + exclude(module = "kotlin-stdlib") + exclude(module = "kotlin-stdlib-jdk7") + exclude(module = "kotlin-stdlib-jdk8") + exclude(module = "annotations") + } +} +tasks { + processResources { + filesMatching("fabric.mod.json") { + expand(project.properties) + } + } + runClient { + args("--username", "MiniDay", "--width", "1280", "--height", "720") + } +} +publishing { + publications { + create("mavenJava") { + artifactId = project.property("archives_base_name") as String + from(components["java"]) + } + } + repositories { + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..59d997e --- /dev/null +++ b/gradle.properties @@ -0,0 +1,18 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx2G +# Fabric Properties +# check these on https://modmuss50.me/fabric.html +minecraft_version=1.21.4 +yarn_mappings=1.21.4+build.4 +loader_version=0.16.9 +kotlin_loader_version=1.13.0+kotlin.2.1.0 +# Mod Properties +mod_version=1.0.0 +maven_group=net.airgame +archives_base_name=compose-ui-mod +# Dependencies +# check this on https://modmuss50.me/fabric.html +fabric_version=0.114.0+1.21.4 + +kotlin.version=2.1.0 +compose.version=1.6.10 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ab6f03b --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..5514ffc --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + maven("https://maven.fabricmc.net/") { + name = "Fabric" + } + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() + mavenCentral() + gradlePluginPortal() + } + + plugins { + kotlin("jvm").version(extra["kotlin.version"] as String) + id("org.jetbrains.compose").version(extra["compose.version"] as String) + id("org.jetbrains.kotlin.plugin.compose").version(extra["kotlin.version"] as String) + } +} diff --git a/src/client/kotlin/net/airgame/compose/ui/client/ComposeScreen.kt b/src/client/kotlin/net/airgame/compose/ui/client/ComposeScreen.kt new file mode 100644 index 0000000..784c848 --- /dev/null +++ b/src/client/kotlin/net/airgame/compose/ui/client/ComposeScreen.kt @@ -0,0 +1,154 @@ +package net.airgame.compose.ui.client + +import androidx.compose.runtime.Composable +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.input.pointer.PointerButton +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.scene.ComposeScene +import androidx.compose.ui.scene.SingleLayerComposeScene +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize +import com.mojang.blaze3d.platform.TextureUtil +import com.mojang.blaze3d.systems.RenderSystem +import kotlinx.coroutines.launch +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.texture.NativeImage +import net.minecraft.text.Text +import org.jetbrains.skia.Color +import org.jetbrains.skia.EncodedImageFormat +import org.jetbrains.skia.Surface +import org.jetbrains.skiko.FrameDispatcher + +@OptIn(InternalComposeUiApi::class) +class ComposeScreen( + val content: @Composable (ComposeScreen) -> Unit +) : Screen(Text.literal("")) { + private val frameDispatcher = FrameDispatcher(ComposeUIMod.singleThreadDispatcher) { + renderFrame() + } + + private val windowWidth: Int get() = MinecraftClient.getInstance().window.width + private val windowHeight: Int get() = MinecraftClient.getInstance().window.height + + private lateinit var surface: Surface + private lateinit var canvas :Canvas + private lateinit var scene: ComposeScene + + override fun init() { + surface = Surface.makeRasterN32Premul(windowWidth, windowHeight) + canvas = surface.canvas.asComposeCanvas() + scene = SingleLayerComposeScene( + coroutineContext = ComposeUIMod.singleThreadDispatcher, + size = IntSize(windowWidth, windowHeight), + density = Density(MinecraftClient.getInstance().options.guiScale.value.toFloat()) + ) { + frameDispatcher.scheduleFrame() + } + ComposeUIMod.coroutineScope.launch { + scene.setContent { + content(this@ComposeScreen) + } + } + } + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + if (surface.isClosed) { + return + } + ComposeUIMod.coroutineScope.launch { + scene.sendPointerEvent( + PointerEventType.Move, + position = Offset( +// mouseX.toFloat(), mouseY.toFloat() + MinecraftClient.getInstance().mouse.x.toFloat(), + MinecraftClient.getInstance().mouse.y.toFloat() + ) + ) + } + context.draw { + val vertexConsumer = it.getBuffer(ComposeUIMod.composeLayer) + val matrix4f = context.matrices.peek().positionMatrix + vertexConsumer.vertex(matrix4f, 0f, 0f, 0f).texture(0f, 0f) + vertexConsumer.vertex(matrix4f, 0f, height.toFloat(), 0f).texture(0f, 1f) + vertexConsumer.vertex(matrix4f, width.toFloat(), height.toFloat(), 0f).texture(1f, 1f) + vertexConsumer.vertex(matrix4f, width.toFloat(), 0f, 0f).texture(1f, 0f) + } + } + + private fun renderFrame() { + if (surface.isClosed) { + return + } + val timer = DebugTimer() + surface.canvas.clear(Color.TRANSPARENT) + timer.log("clear") + scene.render(canvas, System.nanoTime()) + timer.log("render") + surface.makeImageSnapshot().use { image -> + timer.log("makeImageSnapshot") + image.encodeToData(EncodedImageFormat.PNG)?.use { data -> + timer.log("encodeToData") + val nativeImage = NativeImage.read(data.bytes) + timer.log("NativeImage.read") + RenderSystem.recordRenderCall { + timer.log("recordRenderCall") + TextureUtil.prepareImage(ComposeUIMod.textureGlID, 0, nativeImage.width, nativeImage.height) + timer.log("prepareImage") + nativeImage.upload(0, 0, 0, 0, 0, nativeImage.width, nativeImage.height, true) + timer.log("upload") + } + } + } + } + + override fun resize(client: MinecraftClient, width: Int, height: Int) { + super.resize(client, width, height) + val old = surface + surface = Surface.makeRasterN32Premul(windowWidth, windowHeight) + canvas = surface.canvas.asComposeCanvas() + old.close() + scene.size = IntSize(windowWidth, windowHeight) + scene.density = Density(client.options.guiScale.value.toFloat()) + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + ComposeUIMod.coroutineScope.launch { + scene.sendPointerEvent( + PointerEventType.Press, + button = PointerButton.Primary, + position = Offset( +// mouseX.toFloat(), mouseY.toFloat() + MinecraftClient.getInstance().mouse.x.toFloat(), + MinecraftClient.getInstance().mouse.y.toFloat() + ) + ) + } + return true + } + + override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + ComposeUIMod.coroutineScope.launch { + scene.sendPointerEvent( + PointerEventType.Release, + button = PointerButton.Primary, + position = Offset( +// mouseX.toFloat(), mouseY.toFloat() + MinecraftClient.getInstance().mouse.x.toFloat(), + MinecraftClient.getInstance().mouse.y.toFloat() + ) + ) + } + return true + } + + override fun close() { + MinecraftClient.getInstance().setScreen(null) + scene.close() + surface.close() + } +} \ No newline at end of file diff --git a/src/client/kotlin/net/airgame/compose/ui/client/ComposeUIMod.kt b/src/client/kotlin/net/airgame/compose/ui/client/ComposeUIMod.kt new file mode 100644 index 0000000..50e5f0c --- /dev/null +++ b/src/client/kotlin/net/airgame/compose/ui/client/ComposeUIMod.kt @@ -0,0 +1,57 @@ +package net.airgame.compose.ui.client + +import com.mojang.blaze3d.platform.TextureUtil +import com.mojang.blaze3d.systems.RenderSystem +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import net.fabricmc.api.ClientModInitializer +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper +import net.minecraft.client.option.KeyBinding +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.RenderPhase +import net.minecraft.client.render.VertexFormat +import net.minecraft.client.render.VertexFormats +import net.minecraft.client.util.InputUtil +import org.jetbrains.skiko.MainUIDispatcher + +class ComposeUIMod : ClientModInitializer { + companion object { + val singleThreadDispatcher = + MainUIDispatcher.limitedParallelism(1) + CoroutineExceptionHandler { _, throwable -> + throwable.printStackTrace() + } + val coroutineScope = CoroutineScope(singleThreadDispatcher) + val textureGlID: Int by lazy { TextureUtil.generateTextureId() } + val composeLayer: RenderLayer by lazy { + RenderLayer.of( + "compose", + VertexFormats.POSITION_TEXTURE, + VertexFormat.DrawMode.QUADS, + 786432, + RenderLayer.MultiPhaseParameters.builder() + .texture(RenderPhase.TextureBase({ + RenderSystem.setShaderTexture(0, textureGlID) + }, {})) + .program(RenderPhase.POSITION_TEXTURE_PROGRAM) + .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY) + .depthTest(RenderPhase.LEQUAL_DEPTH_TEST) + .build(false) + ) + } + } + + val bind = KeyBinding("测试", InputUtil.GLFW_KEY_G, "测试") + + override fun onInitializeClient() { + KeyBindingHelper.registerKeyBinding(bind) + ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { client -> + while (bind.wasPressed()) { + val screen = ComposeScreen { + TestUI() + } + client.setScreen(screen) + } + }) + } +} diff --git a/src/client/kotlin/net/airgame/compose/ui/client/DebugTimer.kt b/src/client/kotlin/net/airgame/compose/ui/client/DebugTimer.kt new file mode 100644 index 0000000..3329e07 --- /dev/null +++ b/src/client/kotlin/net/airgame/compose/ui/client/DebugTimer.kt @@ -0,0 +1,10 @@ +package net.airgame.compose.ui.client + +class DebugTimer { + var time = System.currentTimeMillis() + fun log(string: String) { + val now = System.currentTimeMillis() + println("$string: ${now - time} ms") + time = now + } +} \ No newline at end of file diff --git a/src/client/kotlin/net/airgame/compose/ui/client/TestUI.kt b/src/client/kotlin/net/airgame/compose/ui/client/TestUI.kt new file mode 100644 index 0000000..b14a6c1 --- /dev/null +++ b/src/client/kotlin/net/airgame/compose/ui/client/TestUI.kt @@ -0,0 +1,33 @@ +package net.airgame.compose.ui.client + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +@Preview +fun TestUI() { + var show by remember { mutableStateOf(true) } + Column( + Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Button(onClick = { + show = !show + println("clicked") + }) { + Text("点我切换") + } + AnimatedVisibility(show) { + Text("Hello Compose Minecraft!") + } + } +} \ No newline at end of file diff --git a/src/client/resources/assets/compose-ui-mod/icon.png b/src/client/resources/assets/compose-ui-mod/icon.png new file mode 100644 index 0000000..8ff89b7 Binary files /dev/null and b/src/client/resources/assets/compose-ui-mod/icon.png differ diff --git a/src/client/resources/compose-ui-mod.client.mixins.json b/src/client/resources/compose-ui-mod.client.mixins.json new file mode 100644 index 0000000..b4067e4 --- /dev/null +++ b/src/client/resources/compose-ui-mod.client.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "net.airgame.compose.ui.mixin.client", + "compatibilityLevel": "JAVA_21", + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/kotlin/net/airgame/compose/ui/ComposeUI.kt b/src/main/kotlin/net/airgame/compose/ui/ComposeUI.kt new file mode 100644 index 0000000..407135f --- /dev/null +++ b/src/main/kotlin/net/airgame/compose/ui/ComposeUI.kt @@ -0,0 +1,9 @@ +package net.airgame.compose.ui + +import net.fabricmc.api.ModInitializer + +class ComposeUI : ModInitializer { + + override fun onInitialize() { + } +} diff --git a/src/main/resources/compose-ui-mod.mixins.json b/src/main/resources/compose-ui-mod.mixins.json new file mode 100644 index 0000000..e91ad17 --- /dev/null +++ b/src/main/resources/compose-ui-mod.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "net.airgame.compose.ui.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..e8cc0cf --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 1, + "id": "compose-ui-mod", + "version": "${version}", + "name": "ComposeUI", + "description": "add jetpack compose to minecraft", + "authors": [], + "contact": {}, + "license": "MIT", + "icon": "assets/compose-ui-mod/icon.png", + "environment": "client", + "entrypoints": { + "client": [ + "net.airgame.compose.ui.client.ComposeUIMod" + ], + "main": [ + "net.airgame.compose.ui.ComposeUI" + ] + }, + "mixins": [ + "compose-ui-mod.mixins.json", + { + "config": "compose-ui-mod.client.mixins.json", + "environment": "client" + } + ], + "depends": { + "fabricloader": ">=${loader_version}", + "fabric-language-kotlin": ">=${kotlin_loader_version}", + "fabric": "*", + "minecraft": "${minecraft_version}" + } +}