feat: 离屏渲染
This commit is contained in:
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,55 +1,24 @@
|
|||||||
package net.airgame.compose.ui.client
|
package net.airgame.compose.ui.client
|
||||||
|
|
||||||
import com.mojang.blaze3d.platform.TextureUtil
|
import net.airgame.compose.ui.client.screen.ComposeScreen
|
||||||
import com.mojang.blaze3d.systems.RenderSystem
|
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import net.fabricmc.api.ClientModInitializer
|
import net.fabricmc.api.ClientModInitializer
|
||||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
|
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
|
||||||
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper
|
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper
|
||||||
import net.minecraft.client.option.KeyBinding
|
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 net.minecraft.client.util.InputUtil
|
||||||
import org.jetbrains.skiko.MainUIDispatcher
|
|
||||||
|
|
||||||
class ComposeUIMod : ClientModInitializer {
|
class ComposeUIMod : ClientModInitializer {
|
||||||
companion object {
|
private val bind = KeyBinding("测试", InputUtil.GLFW_KEY_G, "测试")
|
||||||
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() {
|
override fun onInitializeClient() {
|
||||||
KeyBindingHelper.registerKeyBinding(bind)
|
KeyBindingHelper.registerKeyBinding(bind)
|
||||||
ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { client ->
|
ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { client ->
|
||||||
while (bind.wasPressed()) {
|
while (bind.wasPressed()) {
|
||||||
val screen = ComposeScreen {
|
val screen = ComposeScreen {
|
||||||
|
CenterUI {
|
||||||
TestUI()
|
TestUI()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
client.setScreen(screen)
|
client.setScreen(screen)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@@ -1,33 +1,194 @@
|
|||||||
package net.airgame.compose.ui.client
|
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.desktop.ui.tooling.preview.Preview
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun TestUI() {
|
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(
|
Column(
|
||||||
Modifier.fillMaxSize(),
|
modifier = Modifier.size(800.dp, 600.dp).background(Color(0x80808080)),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
verticalArrangement = Arrangement.spacedBy(3.dp)
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
) {
|
||||||
Button(onClick = {
|
Surface(
|
||||||
show = !show
|
modifier = Modifier.fillMaxWidth(),
|
||||||
println("clicked")
|
color = Color.Gray
|
||||||
}) {
|
) {
|
||||||
Text("点我切换")
|
Text(
|
||||||
|
modifier = Modifier.padding(5.dp),
|
||||||
|
fontSize = 24.sp,
|
||||||
|
text = "邮箱列表"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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("一键清空")
|
||||||
}
|
}
|
||||||
AnimatedVisibility(show) {
|
|
||||||
Text("Hello Compose Minecraft!")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
)
|
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user