From 4b515e49036bc0f8b80a2d90598a935c8391b034 Mon Sep 17 00:00:00 2001 From: MiniDay <372403923@qq.com> Date: Tue, 14 Jan 2025 09:03:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=A6=BB=E5=B1=8F=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../compose/ui/client/ComposeScreen.kt | 154 ------------ .../airgame/compose/ui/client/ComposeUIMod.kt | 41 +--- .../net/airgame/compose/ui/client/TestUI.kt | 197 ++++++++++++++-- .../compose/ui/client/screen/ComposeScreen.kt | 223 ++++++++++++++++++ 4 files changed, 407 insertions(+), 208 deletions(-) delete mode 100644 src/client/kotlin/net/airgame/compose/ui/client/ComposeScreen.kt create mode 100644 src/client/kotlin/net/airgame/compose/ui/client/screen/ComposeScreen.kt diff --git a/src/client/kotlin/net/airgame/compose/ui/client/ComposeScreen.kt b/src/client/kotlin/net/airgame/compose/ui/client/ComposeScreen.kt deleted file mode 100644 index 784c848..0000000 --- a/src/client/kotlin/net/airgame/compose/ui/client/ComposeScreen.kt +++ /dev/null @@ -1,154 +0,0 @@ -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 index 50e5f0c..61d3ef5 100644 --- a/src/client/kotlin/net/airgame/compose/ui/client/ComposeUIMod.kt +++ b/src/client/kotlin/net/airgame/compose/ui/client/ComposeUIMod.kt @@ -1,54 +1,23 @@ 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.airgame.compose.ui.client.screen.ComposeScreen 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, "测试") + private 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() + CenterUI { + TestUI() + } } client.setScreen(screen) } diff --git a/src/client/kotlin/net/airgame/compose/ui/client/TestUI.kt b/src/client/kotlin/net/airgame/compose/ui/client/TestUI.kt index b14a6c1..daeb8d8 100644 --- a/src/client/kotlin/net/airgame/compose/ui/client/TestUI.kt +++ b/src/client/kotlin/net/airgame/compose/ui/client/TestUI.kt @@ -1,33 +1,194 @@ package net.airgame.compose.ui.client -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize 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.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun CenterUI(content: @Composable () -> Unit) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + content() + } +} @Composable @Preview fun TestUI() { - var show by remember { mutableStateOf(true) } + val mails = mutableStateListOf( + Mail( + "新手启航大礼包来袭", "2025-01-14", """ + 尊敬的玩家,您好! + 当您踏入《奇幻异界》这片神秘大陆的那一刻,一场精彩绝伦的奇幻之旅便已开启。为了助您在异界中快速成长,我们精心准备了这份新手启航大礼包,愿它成为您冒险之路上的助力。 + 礼包内容如下: + 金币×1000:可用于在游戏内购买各类道具、装备,助您在前期迅速提升实力。 + 初级经验药水×5:使用后可获得大量经验,加速角色升级,让您更快解锁新技能和玩法。 + 新手护符×1:佩戴后可提升角色全属性5%,在您探索未知世界时,为您保驾护航。 + 神秘传送石×3:可随机传送至游戏内的任意一个已解锁的城镇或地图,让您在冒险途中更加便捷地穿梭。 + 新手任务指南×1:详细介绍了游戏内的新手任务流程及隐藏任务线索,助您轻松获取丰厚奖励。 + 礼包已自动发放至您的游戏账户,您可在背包中查收。同时,我们还为新手玩家准备了专属的冒险手册,在游戏内点击“帮助”按钮即可查看,其中包含了丰富的游戏玩法介绍、操作技巧以及常见问题解答,助您快速熟悉游戏。 + 在您冒险的过程中,若遇到任何问题或需要帮助,欢迎随时联系我们的客服团队,我们将竭诚为您服务。此外,别忘了关注我们的官方社区和论坛,那里有更多精彩内容和福利活动等着您! + 愿您在《奇幻异界》的冒险之路上,收获无尽的快乐与惊喜! + 《奇幻异界》运营团队 + """.trimIndent() + ), + Mail( + "周年庆典盛典开启,海量福利来袭", "2025-01-14", """ + 亲爱的勇士,您好! + 时光荏苒,不知不觉间,《荣耀之战》已经陪伴大家度过了一个精彩的年头。在过去的一年里,我们一同经历了无数惊心动魄的战斗,共同见证了无数荣耀时刻。在此,我们衷心感谢每一位勇士的支持与陪伴! + 为了庆祝这一特别的时刻,我们精心筹备了周年庆典盛典,海量福利活动现已全面开启! + 活动一:登录送豪礼 + 活动期间,每日登录游戏即可领取丰厚奖励,累计登录7天,更有机会获得限定坐骑“荣耀战马”! + 第1天:金币×2000、体力药水×2 + 第2天:钻石×50、初级强化石×10 + 第3天:神秘宝箱×1、中级经验药水×1 + 第4天:金币×3000、高级宝石×1 + 第5天:钻石×100、稀有宠物碎片×5 + 第6天:超级强化石×5、高级经验药水×2 + 第7天:限定坐骑“荣耀战马”(7天体验)×1 + 活动二:消费返利大狂欢 + 活动期间,累计消费钻石达到一定额度,即可获得高额返利,最高返利可达200%!此外,还有稀有装备、珍贵道具等你来拿! + 消费钻石100 - 499:返利20%,额外赠送初级宝石×5 + 消费钻石500 - 999:返利30%,额外赠送中级强化石×10 + 消费钻石1000 - 2999:返利50%,额外赠送稀有装备宝箱×1 + 消费钻石3000及以上:返利200%,额外赠送传说级武器×1 + 活动三:周年庆典专属副本 + 全新周年庆典专属副本“荣耀之塔”现已开放!副本内机关重重,怪物强大,但宝藏也异常丰厚。成功通关副本,可获得海量金币、钻石、稀有材料以及周年庆典专属称号“荣耀先锋”! + 活动四:限时抽奖,大奖等你拿 + 活动期间,每日完成指定任务即可获得抽奖机会,奖池内包含超级坐骑“梦幻飞龙”、绝版时装“荣耀战神”、海量钻石等超值大奖,中奖率高达100%! + 以上活动详情可在游戏内“活动中心”查看,更多惊喜福利等你来发现! + 在庆典期间,我们的客服团队将全天候在线,为您解决任何游戏问题。同时,我们也诚挚邀请您参与我们的玩家见面会,届时将有机会与开发团队面对面交流,分享您的游戏心得与建议。 + 再次感谢您对《荣耀之战》的支持与热爱,让我们携手共进,开启新的荣耀篇章! + 《荣耀之战》运营团队 + """.trimIndent() + ), + Mail( + "公会招募:加入“荣耀之光”,共创辉煌", "2025-01-14", """ + 我是《星际争霸》“荣耀之光”公会的会长。在星际战场上,我们一直关注着您的表现。您出色的指挥能力、卓越的战斗技巧以及对游戏的热情,让我们坚信您是我们公会的不二人选。 + “荣耀之光”公会成立于游戏开服之初,至今已有数年的历史。我们公会成员来自五湖四海,大家因为共同的爱好和目标而聚集在一起。在这里,我们不仅有激烈的星际战斗,还有温馨的社交氛围。公会成员之间相互帮助、共同成长,一起为了公会的荣耀而努力奋斗。 + 加入“荣耀之光”公会,您将获得以下专属福利: + 每日公会任务奖励:完成公会任务可获得大量金币、钻石以及公会贡献值。公会贡献值可用于在公会商店兑换稀有装备、珍贵材料以及专属道具。 + 公会仓库资源共享:公会仓库内存放着大量的装备、材料以及道具,成员可免费借用或兑换。当您在游戏中遇到装备或材料短缺的问题时,公会仓库将为您提供强大的支持。 + 专属公会副本:我们拥有独特的公会副本,副本内的怪物难度适中,但宝藏丰厚。公会成员可组队挑战副本,获取专属装备和材料,提升自身实力。 + 公会战荣誉奖励:每周举办的公会战是展示公会实力的舞台。在公会战中,成员们将并肩作战,为公会的荣誉而战。根据公会战的排名,成员可获得丰厚的奖励,包括稀有装备、大量钻石以及专属称号。 + 线下聚会活动:为了增进公会成员之间的友谊,我们定期举办线下聚会活动。在聚会上,大家可以面对面交流游戏心得、分享生活趣事,还能一起参与各种有趣的互动游戏,增进彼此之间的感情。 + 我们相信,您的加入将为“荣耀之光”公会注入新的活力,让我们一起携手共创辉煌!如果您对加入公会感兴趣,请在游戏内回复此邮件,或直接联系公会成员,我们将竭诚为您服务。 + “荣耀之光”公会会长 + """.trimIndent() + ), + Mail( + "神秘信件", "2025-01-14", """ + 吾乃隐居于幽暗森林深处的古老精灵族长。数日前,吾族遗失了一件至关重要的神器——生命之源。据吾族先知预言,唯有心怀正义且实力超群之人,方能找回神器,拯救吾族于危难之中。吾族族人已暗中观察你多时,你便是那命中注定之人!请前往幽暗森林之心,寻找线索,开启一段奇幻冒险。吾族将永远铭记你的恩情! + 古老精灵族长 + """.trimIndent() + ), + Mail( + "国王的密令", "2025-01-14", """ + 朕闻得边境之地,魔族余孽蠢蠢欲动,欲图谋不轨。朕命你即刻前往边境,查探魔族动向,必要时可调遣附近驻军,将其剿灭。此乃关乎王国安危之重任,朕盼你凯旋而归,朕定有重赏! + 国王陛下 + """.trimIndent() + ), + Mail( + "稀有装备出售", "2025-01-14", """ + 各位玩家,您好! + 本人因游戏风格转变,现有一件稀有装备——“雷霆之怒”双手锤,欲出售。该装备属性卓越,攻击力+120,暴击率+5%,附带独特技能:雷霆一击,可对目标造成额外50%伤害,并有几率使其麻痹2秒。装备等级要求30级,售价金币×2000,诚心出售,有意者请私信联系,非诚勿扰! + """.trimIndent() + ), + Mail( + "稀有装备出售", "2025-01-14", """ + 亲爱的玩家们: + 本人急需大量高级魔力水晶,用于打造强力魔法装备。现高价收购,每颗魔力水晶金币×50。本人信誉良好,交易迅速,可直接在游戏内交易,也可线下交易(需双方协商)。若有大量存货者,可适当加价。望各位玩家不吝赐教,助我一臂之力! + """.trimIndent() + ), + ) Column( - Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + modifier = Modifier.size(800.dp, 600.dp).background(Color(0x80808080)), + verticalArrangement = Arrangement.spacedBy(3.dp) ) { - Button(onClick = { - show = !show - println("clicked") - }) { - Text("点我切换") + Surface( + modifier = Modifier.fillMaxWidth(), + color = Color.Gray + ) { + Text( + modifier = Modifier.padding(5.dp), + fontSize = 24.sp, + text = "邮箱列表" + ) } - AnimatedVisibility(show) { - Text("Hello Compose Minecraft!") + LazyColumn( + modifier = Modifier.weight(1f).padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(mails) { + MainContent(it) + } + } + Row( + modifier = Modifier.fillMaxWidth().background(Color.Gray), + horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally), + ) { + Button(onClick = {}) { + Text("全部领取") + } + Button(onClick = {}) { + Text("一键清空") + } } } -} \ No newline at end of file +} + +@Composable +fun MainContent(mail: Mail) { + var isExpand by remember { mutableStateOf(false) } + Card(elevation = 5.dp) { + Column( + modifier = Modifier.padding(5.dp), + verticalArrangement = Arrangement.spacedBy(3.dp) + ) { + Row { + Text( + text = mail.title, + color = MaterialTheme.colors.primary, + style = MaterialTheme.typography.subtitle1 + ) + Spacer(Modifier.weight(1f)) + Text( + text = mail.sendTime, + color = MaterialTheme.colors.secondary, + style = MaterialTheme.typography.subtitle1 + ) + } + Spacer(Modifier.fillMaxWidth().height(1.dp).background(color = Color.Black)) + Surface( + shape = MaterialTheme.shapes.medium, + elevation = 1.dp, + ) { + Text( + modifier = Modifier.clickable { isExpand = !isExpand }.animateContentSize(), + style = MaterialTheme.typography.body1, + text = mail.content, + maxLines = if (isExpand) Int.MAX_VALUE else 3 + ) + } + } + } +} + +class Mail( + val title: String, + val sendTime: String, + val content: String +) \ No newline at end of file diff --git a/src/client/kotlin/net/airgame/compose/ui/client/screen/ComposeScreen.kt b/src/client/kotlin/net/airgame/compose/ui/client/screen/ComposeScreen.kt new file mode 100644 index 0000000..be91513 --- /dev/null +++ b/src/client/kotlin/net/airgame/compose/ui/client/screen/ComposeScreen.kt @@ -0,0 +1,223 @@ +package net.airgame.compose.ui.client.screen + +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.MultiLayerComposeScene +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize +import com.mojang.blaze3d.platform.GlStateManager +import com.mojang.blaze3d.platform.TextureUtil +import com.mojang.blaze3d.systems.RenderSystem +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Runnable +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +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.text.Text +import org.jetbrains.skia.* +import org.jetbrains.skiko.FrameDispatcher +import org.lwjgl.glfw.GLFW.* +import org.lwjgl.opengl.GL30.* +import org.lwjgl.system.MemoryUtil.NULL +import kotlin.coroutines.CoroutineContext + +@OptIn(InternalComposeUiApi::class) +class ComposeScreen( + val composeContent: @Composable (ComposeScreen) -> Unit +) : Screen(Text.literal("FrameRenderComposeScreen")) { + companion object { + private val frameBufferID by lazy { GlStateManager.glGenFramebuffers() } + private val textureID by lazy { TextureUtil.generateTextureId() } + private val composeLayer: RenderLayer by lazy { + RenderLayer.of( + "cpu_render_compose", + VertexFormats.POSITION_TEXTURE, + VertexFormat.DrawMode.QUADS, + 786432, + RenderLayer.MultiPhaseParameters.builder() + .texture(RenderPhase.TextureBase({ + RenderSystem.setShaderTexture(0, textureID) + }, {})) + .program(RenderPhase.POSITION_TEXTURE_PROGRAM) + .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY) + .depthTest(RenderPhase.LEQUAL_DEPTH_TEST) + .build(false) + ) + } + private val windowID by lazy { + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE) + val oldWindow = glfwGetCurrentContext() + glfwCreateWindow(1, 1, "", NULL, oldWindow) + } + } + + private val windowWidth: Int get() = MinecraftClient.getInstance().window.width + private val windowHeight: Int get() = MinecraftClient.getInstance().window.height + + private lateinit var directContext: DirectContext + private lateinit var surface: Surface + private lateinit var canvas: Canvas + private lateinit var scene: ComposeScene + + private val tasks = mutableListOf() + private val tasksCopy = mutableListOf() + + private val coroutineDispatcher = object : CoroutineDispatcher() { + override fun dispatch(context: CoroutineContext, block: Runnable) { + synchronized(tasks) { + tasks.add(block) + } + } + } + private val frameDispatcher = FrameDispatcher(coroutineDispatcher) { + RenderSystem.recordRenderCall { + if (surface.isClosed) { + return@recordRenderCall + } + val oldWindowID = glfwGetCurrentContext() + glfwMakeContextCurrent(windowID) + + surface.canvas.clear(Color.TRANSPARENT) + scene.render(canvas, System.nanoTime()) + directContext.flush() + + glfwMakeContextCurrent(oldWindowID) + } + } + + override fun init() { + val oldFrameBuffer = glGetInteger(GL_FRAMEBUFFER_BINDING) + val oldTexture = glGetInteger(GL_TEXTURE_BINDING_2D) + + glBindFramebuffer(GL_FRAMEBUFFER, frameBufferID) + glBindTexture(GL_TEXTURE_2D, textureID) + glTexImage2D( + GL_TEXTURE_2D, 0, GL_RGBA, windowWidth, windowHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL + ) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureID, 0) + + glBindFramebuffer(GL_FRAMEBUFFER, oldFrameBuffer) + glBindTexture(GL_TEXTURE_2D, oldTexture) + + directContext = DirectContext.makeGL() + val renderTarget = BackendRenderTarget.makeGL( + windowWidth, windowHeight, 0, 8, frameBufferID, GL_RGBA8 + ) + surface = Surface.makeFromBackendRenderTarget( + directContext, renderTarget, SurfaceOrigin.TOP_LEFT, SurfaceColorFormat.RGBA_8888, ColorSpace.sRGB + )!! + canvas = surface.canvas.asComposeCanvas() + scene = MultiLayerComposeScene( + density = Density(MinecraftClient.getInstance().options.guiScale.value.toFloat() / 2), + size = IntSize(windowWidth, windowHeight), + coroutineContext = coroutineDispatcher, + invalidate = { frameDispatcher.scheduleFrame() } + ) + scene.setContent { + composeContent(this@ComposeScreen) + } + super.init() + } + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + synchronized(tasks) { + tasksCopy.clear() + tasksCopy.addAll(tasks) + tasks.clear() + } + for (runnable in tasksCopy) { + runnable.run() + } + context.draw { + val vertexConsumer = it.getBuffer(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) + } + } + + 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.density = Density(client.options.guiScale.value.toFloat() / 2) + scene.size = IntSize(windowWidth, windowHeight) + } + + override fun mouseMoved(mouseX: Double, mouseY: Double) { + val x = MinecraftClient.getInstance().mouse.x.toFloat() + val y = MinecraftClient.getInstance().mouse.y.toFloat() + coroutineDispatcher.dispatch(coroutineDispatcher) { + scene.sendPointerEvent( + PointerEventType.Move, + position = Offset(x, y) + ) + } + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + coroutineDispatcher.dispatch(coroutineDispatcher) { + val x = MinecraftClient.getInstance().mouse.x.toFloat() + val y = MinecraftClient.getInstance().mouse.y.toFloat() + scene.sendPointerEvent( + PointerEventType.Press, + button = PointerButton.Primary, + position = Offset(x, y) + ) + } + return true + } + + override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + coroutineDispatcher.dispatch(coroutineDispatcher) { + val x = MinecraftClient.getInstance().mouse.x.toFloat() + val y = MinecraftClient.getInstance().mouse.y.toFloat() + scene.sendPointerEvent( + PointerEventType.Release, + button = PointerButton.Primary, + position = Offset(x, y) + ) + } + return true + } + + override fun mouseScrolled( + mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double + ): Boolean { + val x = MinecraftClient.getInstance().mouse.x.toFloat() + val y = MinecraftClient.getInstance().mouse.y.toFloat() + println("mouseScrolled: $x $y $horizontalAmount $verticalAmount") + coroutineDispatcher.dispatch(coroutineDispatcher) { + scene.sendPointerEvent( + PointerEventType.Scroll, + button = PointerButton.Tertiary, + position = Offset(x, y), + scrollDelta = Offset(horizontalAmount.toFloat(), -verticalAmount.toFloat()) + ) + } + return true + } + + override fun close() { + MinecraftClient.getInstance().setScreen(null) + surface.close() + scene.close() + directContext.close() + } +} \ No newline at end of file