feat: 离屏渲染

This commit is contained in:
2025-01-14 09:03:54 +08:00
parent 340392bcf9
commit 4b515e4903
4 changed files with 407 additions and 208 deletions

View File

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

View File

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

View File

@@ -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("一键清空")
}
}
}
}
@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
)

View File

@@ -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<Runnable>()
private val tasksCopy = mutableListOf<Runnable>()
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()
}
}