feat: 初版提交

This commit is contained in:
2025-01-13 23:11:52 +08:00
commit 340392bcf9
15 changed files with 442 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.gradle
.idea
.kotlin
build
run

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# ComposeUI
将 Jetpack Compose 集成到 Minecraft 中

80
build.gradle.kts Normal file
View File

@@ -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<MavenPublication>("mavenJava") {
artifactId = project.property("archives_base_name") as String
from(components["java"])
}
}
repositories {
}
}

18
gradle.properties Normal file
View File

@@ -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

View File

@@ -0,0 +1 @@
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip

17
settings.gradle.kts Normal file
View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
})
}
}

View File

@@ -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
}
}

View File

@@ -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!")
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,11 @@
{
"required": true,
"minVersion": "0.8",
"package": "net.airgame.compose.ui.mixin.client",
"compatibilityLevel": "JAVA_21",
"client": [
],
"injectors": {
"defaultRequire": 1
}
}

View File

@@ -0,0 +1,9 @@
package net.airgame.compose.ui
import net.fabricmc.api.ModInitializer
class ComposeUI : ModInitializer {
override fun onInitialize() {
}
}

View File

@@ -0,0 +1,11 @@
{
"required": true,
"minVersion": "0.8",
"package": "net.airgame.compose.ui.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
],
"injectors": {
"defaultRequire": 1
}
}

View File

@@ -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}"
}
}