From f3757f6fbd378ef88e70e925cac6a3c84f298d8e Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Thu, 8 Aug 2024 16:02:05 +0300 Subject: [PATCH 001/114] Global toggle sounds --- .../lambda/gui/impl/clickgui/buttons/ModuleButton.kt | 12 ++++-------- common/src/main/kotlin/com/lambda/module/Module.kt | 10 ++++++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt index 86ec9933c..df80cf068 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt @@ -174,12 +174,11 @@ class ModuleButton( } override fun performClickAction(e: GuiEvent.MouseClick) { - val sound = when (e.button) { + when (e.button) { Mouse.Button.Left -> { module.toggle() - if (module.isEnabled) LambdaSound.MODULE_ON else LambdaSound.MODULE_OFF - } + } Mouse.Button.Right -> { // Don't let user spam val targetHeight = if (isOpen) settingsHeight else 0.0 @@ -189,13 +188,10 @@ class ModuleButton( if (isOpen) settingsLayer.onEvent(GuiEvent.Show()) updateHeight() - if (isOpen) LambdaSound.SETTINGS_OPEN else LambdaSound.SETTINGS_CLOSE + val sound = if (isOpen) LambdaSound.SETTINGS_OPEN else LambdaSound.SETTINGS_CLOSE + playSoundRandomly(sound.event) } - - else -> return } - - playSoundRandomly(sound.event) } override fun equals(other: Any?) = diff --git a/common/src/main/kotlin/com/lambda/module/Module.kt b/common/src/main/kotlin/com/lambda/module/Module.kt index 4cca48a40..61545b980 100644 --- a/common/src/main/kotlin/com/lambda/module/Module.kt +++ b/common/src/main/kotlin/com/lambda/module/Module.kt @@ -18,6 +18,8 @@ import com.lambda.gui.impl.clickgui.LambdaClickGui import com.lambda.gui.impl.clickgui.buttons.ModuleButton import com.lambda.module.modules.client.ClickGui import com.lambda.module.tag.ModuleTag +import com.lambda.sound.LambdaSound +import com.lambda.sound.SoundManager.playSoundRandomly import com.lambda.util.KeyCode import com.lambda.util.Nameable @@ -119,6 +121,14 @@ abstract class Module( || screen is LambdaClickGui) ) toggle() } + + onEnable { + playSoundRandomly(LambdaSound.MODULE_ON.event) + } + + onDisable { + playSoundRandomly(LambdaSound.MODULE_OFF.event) + } } fun enable() { From 65751e3e0d84ae6c7d2c43bb16107c9383187f98 Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Sat, 10 Aug 2024 13:37:37 +0300 Subject: [PATCH 002/114] Tickshift improvements --- .../lambda/mixin/MinecraftClientMixin.java | 10 ++++++++++ .../com/lambda/event/events/TickEvent.kt | 19 +++++++++++++++++-- .../lambda/module/modules/movement/Blink.kt | 18 ++++++++++++------ .../module/modules/movement/TickShift.kt | 11 +++++++---- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java b/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java index 9ffcd5c65..6c92d6558 100644 --- a/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java +++ b/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java @@ -35,6 +35,16 @@ void onTickPost(CallbackInfo ci) { EventFlow.post(new TickEvent.Post()); } + @Inject(method = "render", at = @At("HEAD")) + void onLoopTickPre(CallbackInfo ci) { + EventFlow.post(new TickEvent.GameLoop.Pre()); + } + + @Inject(method = "render", at = @At("RETURN")) + void onLoopTickPost(CallbackInfo ci) { + EventFlow.post(new TickEvent.GameLoop.Post()); + } + @Inject(at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;info(Ljava/lang/String;)V", shift = At.Shift.AFTER, remap = false), method = "stop") private void onShutdown(CallbackInfo ci) { EventFlow.post(new ClientEvent.Shutdown()); diff --git a/common/src/main/kotlin/com/lambda/event/events/TickEvent.kt b/common/src/main/kotlin/com/lambda/event/events/TickEvent.kt index 85f9b1a0a..d47597fdb 100644 --- a/common/src/main/kotlin/com/lambda/event/events/TickEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/TickEvent.kt @@ -18,15 +18,30 @@ import com.lambda.event.events.TickEvent.Pre */ abstract class TickEvent : Event { /** - * A class representing a [TickEvent] that is triggered before each tick of the game loop. + * A class representing a [TickEvent] that is triggered before each tick of the tick loop. */ class Pre : TickEvent() /** - * A class representing a [TickEvent] that is triggered after each tick of the game loop. + * A class representing a [TickEvent] that is triggered after each tick of the tick loop. */ class Post : TickEvent() + /** + * A class representing a [TickEvent] that is triggered on each tick of the game loop. + */ + abstract class GameLoop : TickEvent() { + /** + * A class representing a [TickEvent.Player] that is triggered before each tick of the game loop. + */ + class Pre : TickEvent() + + /** + * A class representing a [TickEvent.Player] that is triggered after each tick of the game loop. + */ + class Post : TickEvent() + } + /** * A class representing a [TickEvent] that is triggered when the player gets ticked. */ diff --git a/common/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt b/common/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt index ddae9757b..702931923 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt @@ -14,10 +14,12 @@ import com.lambda.util.ClientPacket import com.lambda.util.PacketUtils.handlePacketSilently import com.lambda.util.PacketUtils.sendPacketSilently import com.lambda.util.math.ColorUtils.setAlpha +import com.lambda.util.math.VecUtils.minus import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket import net.minecraft.network.packet.s2c.play.EntityVelocityUpdateS2CPacket import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Box +import net.minecraft.util.math.Vec3d import java.util.concurrent.ConcurrentLinkedDeque object Blink : Module( @@ -61,6 +63,16 @@ object Blink : Module( return@listener } + listener { event -> + val packet = event.packet + if (packet !is PlayerMoveC2SPacket) return@listener + + val vec = Vec3d(packet.getX(0.0), packet.getY(0.0), packet.getZ(0.0)) + if (vec == Vec3d.ZERO) return@listener + + lastBox = player.boundingBox.offset(vec - player.pos) + } + listener { event -> if (!isActive || !shiftVelocity) return@listener @@ -81,12 +93,6 @@ object Blink : Module( while (packetPool.isNotEmpty()) { packetPool.poll().let { packet -> connection.sendPacketSilently(packet) - - if (packet is PlayerMoveC2SPacket && packet.changesPosition()) { - lastBox = player.boundingBox - .offset(player.pos.negate()) - .offset(packet.getX(0.0), packet.getY(0.0), packet.getZ(0.0)) - } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/movement/TickShift.kt b/common/src/main/kotlin/com/lambda/module/modules/movement/TickShift.kt index 1f9d92f1c..893674986 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/movement/TickShift.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/movement/TickShift.kt @@ -26,10 +26,13 @@ object TickShift : Module( private val boostAmount by setting("Boost", 3.0, 1.1..20.0, 0.01) private val slowdown by setting("Slowdown", 0.35, 0.01..0.9, 0.01) private val delaySetting by setting("Delay", 0, 0..2000, 10) - private val strict by setting("Strict", true) - private val shiftVelocity by setting("Shift velocity", true) + private val grim by setting("Grim", true) + private val strictSetting by setting("Strict", true) { !grim } + private val shiftVelocity by setting("Shift velocity", true) { grim } private val requiresAura by setting("Requires Aura", false) + private val strict get() = grim || strictSetting + val isActive: Boolean get() { if (requiresAura && (!KillAura.isEnabled || KillAura.target == null)) return false return System.currentTimeMillis() - lastBoost > delaySetting @@ -90,7 +93,7 @@ object TickShift : Module( } listener { event -> - if (!isActive) return@listener + if (!isActive || !grim || event.isCanceled()) return@listener if (event.packet !is CommonPongC2SPacket) return@listener pingPool.add(event.packet) @@ -99,7 +102,7 @@ object TickShift : Module( } listener { event -> - if (!isActive || !shiftVelocity) return@listener + if (!isActive || !grim || !shiftVelocity || event.isCanceled()) return@listener if (event.packet !is EntityVelocityUpdateS2CPacket) return@listener if (event.packet.id != player.id) return@listener From 81a0c6324759ea41d7bb7bebbb313eae668cd26e Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Wed, 21 Aug 2024 19:30:30 -0400 Subject: [PATCH 003/114] various performance tweaks (3x faster load time) --- common/build.gradle.kts | 1 + .../graphics/renderer/gui/font/LambdaEmoji.kt | 2 +- .../renderer/gui/font/glyph/EmojiGlyphs.kt | 31 ++++---- .../renderer/gui/font/glyph/FontGlyphs.kt | 66 +++++++++------- .../lambda/graphics/texture/MipmapTexture.kt | 8 +- .../lambda/graphics/texture/TextureUtils.kt | 30 +++----- .../main/kotlin/com/lambda/http/Request.kt | 23 ++++-- .../module/modules/client/RenderSettings.kt | 7 +- .../kotlin/com/lambda/util/FolderRegister.kt | 75 ++++++++++++++++++- fabric/build.gradle.kts | 1 + forge/build.gradle.kts | 10 +-- neoforge/build.gradle.kts | 1 + 12 files changed, 170 insertions(+), 85 deletions(-) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index c918e1820..07481fb66 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { // Add dependencies on the required Kotlin modules. implementation("org.reflections:reflections:0.10.2") implementation("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") + implementation("com.pngencoder:pngencoder:0.15.0") // Add Kotlin implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt index 87cef41fd..8d8f85f48 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt @@ -17,7 +17,7 @@ enum class LambdaEmoji(private val zipUrl: String) { object Loader : Loadable { override fun load(): String { entries.forEach(LambdaEmoji::loadGlyphs) - return "Loaded ${entries.size} emoji pools" + return "Loaded ${entries.size} emoji sets" } } } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt index d8f92e6e6..0405a0dcf 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt @@ -3,23 +3,22 @@ package com.lambda.graphics.renderer.gui.font.glyph import com.google.common.math.IntMath.pow import com.lambda.Lambda.LOG import com.lambda.graphics.texture.MipmapTexture +import com.lambda.http.Method +import com.lambda.http.request import com.lambda.module.modules.client.RenderSettings -import com.lambda.threading.runGameScheduled -import com.lambda.threading.runIO import com.lambda.util.math.Vec2d import java.awt.Color import java.awt.Graphics2D import java.awt.image.BufferedImage import java.io.File -import java.net.URL import java.util.zip.ZipFile import javax.imageio.ImageIO import kotlin.math.ceil import kotlin.math.log2 import kotlin.math.sqrt import kotlin.system.measureTimeMillis +import kotlin.time.Duration.Companion.days -// TODO: AbstractGlyphs to use for both Font & Emoji glyphs? class EmojiGlyphs(zipUrl: String) { private val emojiMap = mutableMapOf() private lateinit var fontTexture: MipmapTexture @@ -29,9 +28,7 @@ class EmojiGlyphs(zipUrl: String) { init { runCatching { - val time = measureTimeMillis { - downloadAndProcessZip(zipUrl) - } + val time = measureTimeMillis { downloadAndProcessZip(zipUrl) } LOG.info("Loaded ${emojiMap.size} emojis in $time ms") }.onFailure { LOG.error("Failed to load emojis: ${it.message}", it) @@ -40,14 +37,20 @@ class EmojiGlyphs(zipUrl: String) { } private fun downloadAndProcessZip(zipUrl: String) { - val file = File.createTempFile("emoji", ".zip").apply { deleteOnExit() } + val file = request(zipUrl) { + method(Method.GET) + }.maybeDownload("emojis.zip", maxAge = 30.days) - URL(zipUrl).openStream().use { input -> - file.outputStream().use { output -> - input.copyTo(output) - } - } + fontTexture = MipmapTexture(processZip(file)) + } + /** + * Processes the given zip file and loads the emojis into the texture. + * + * @param file The zip file containing the emojis. + * @return The texture containing the emojis. + */ + private fun processZip(file: File): BufferedImage { ZipFile(file).use { zip -> val firstImage = ImageIO.read(zip.getInputStream(zip.entries().nextElement())) val length = zip.size().toDouble() @@ -90,7 +93,7 @@ class EmojiGlyphs(zipUrl: String) { } } - fontTexture = MipmapTexture(image) + return image } fun bind() { diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt index e00cbb284..3c492b26b 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt @@ -1,6 +1,6 @@ package com.lambda.graphics.renderer.gui.font.glyph -import com.lambda.Lambda +import com.lambda.Lambda.LOG import com.lambda.graphics.texture.MipmapTexture import com.lambda.graphics.texture.TextureUtils.getCharImage import com.lambda.module.modules.client.RenderSettings @@ -13,52 +13,60 @@ import java.awt.image.BufferedImage import kotlin.math.max import kotlin.system.measureTimeMillis -class FontGlyphs(font: Font) { +class FontGlyphs( + private val font: Font +) { private val charMap = Int2ObjectOpenHashMap() - private val fontTexture: MipmapTexture + private lateinit var fontTexture: MipmapTexture var fontHeight = 0.0; private set init { - val time = measureTimeMillis { - val image = BufferedImage(TEXTURE_SIZE, TEXTURE_SIZE, BufferedImage.TYPE_INT_ARGB) + runCatching { + val time = measureTimeMillis { processGlyphs() } + LOG.info("Font ${font.fontName} loaded with ${charMap.size} characters in $time ms") + }.onFailure { + LOG.error("Failed to load font glyphs: ${it.message}", it) + fontTexture = MipmapTexture(BufferedImage(1024, 1024, BufferedImage.TYPE_INT_ARGB)) + } + } - val graphics = image.graphics as Graphics2D - graphics.background = Color(0, 0, 0, 0) + private fun processGlyphs() { + val image = BufferedImage(TEXTURE_SIZE, TEXTURE_SIZE, BufferedImage.TYPE_INT_ARGB) - var x = 0 - var y = 0 - var rowHeight = 0 + val graphics = image.graphics as Graphics2D + graphics.background = Color(0, 0, 0, 0) - (Char.MIN_VALUE.. - val charImage = getCharImage(font, char) ?: return@forEach + var x = 0 + var y = 0 + var rowHeight = 0 - rowHeight = max(rowHeight, charImage.height + STEP) + (Char.MIN_VALUE.. + val charImage = getCharImage(font, char) ?: return@forEach - if (x + charImage.width >= TEXTURE_SIZE) { - y += rowHeight - x = 0 - rowHeight = 0 - } + rowHeight = max(rowHeight, charImage.height + STEP) - check(y + charImage.height <= TEXTURE_SIZE) { "Can't load font glyphs. Texture size is too small" } + if (x + charImage.width >= TEXTURE_SIZE) { + y += rowHeight + x = 0 + rowHeight = 0 + } - graphics.drawImage(charImage, x, y, null) + check(y + charImage.height <= TEXTURE_SIZE) { "Can't load font glyphs. Texture size is too small" } - val size = Vec2d(charImage.width, charImage.height) - val uv1 = Vec2d(x, y) * ONE_TEXEL_SIZE - val uv2 = Vec2d(x, y).plus(size) * ONE_TEXEL_SIZE + graphics.drawImage(charImage, x, y, null) - charMap[char.code] = GlyphInfo(size, uv1, uv2) - fontHeight = max(fontHeight, size.y) + val size = Vec2d(charImage.width, charImage.height) + val uv1 = Vec2d(x, y) * ONE_TEXEL_SIZE + val uv2 = Vec2d(x, y).plus(size) * ONE_TEXEL_SIZE - x += charImage.width + STEP - } + charMap[char.code] = GlyphInfo(size, uv1, uv2) + fontHeight = max(fontHeight, size.y) - fontTexture = MipmapTexture(image) + x += charImage.width + STEP } - Lambda.LOG.info("Font ${font.fontName} loaded with ${charMap.size} characters in $time ms") + fontTexture = MipmapTexture(image) } fun bind() { diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt index f52906d64..ea918e92a 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt @@ -36,7 +36,13 @@ class MipmapTexture(image: BufferedImage, levels: Int = 4) : Texture() { } companion object { + /** + * Retrieves an image from the resources folder and generates a mipmap texture. + * + * @param path The path to the image. + * @param levels The number of mipmap levels. + */ fun fromResource(path: String, levels: Int = 4): MipmapTexture = MipmapTexture(ImageIO.read(LambdaResource(path).stream), levels) } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt index 5bb814180..5ed6289cb 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt @@ -1,19 +1,22 @@ package com.lambda.graphics.texture +import com.lambda.module.modules.client.RenderSettings import com.mojang.blaze3d.systems.RenderSystem +import com.pngencoder.PngEncoder import net.minecraft.client.texture.NativeImage import org.lwjgl.BufferUtils -import org.lwjgl.opengl.GL13C.* +import org.lwjgl.opengl.GL45C.* import java.awt.* import java.awt.image.BufferedImage import java.io.ByteArrayOutputStream -import javax.imageio.ImageIO import kotlin.math.roundToInt import kotlin.math.sqrt object TextureUtils { private val metricCache = mutableMapOf() - + private val encoderPreset = PngEncoder() + .withCompressionLevel(RenderSettings.textureCompression) + .withMultiThreadedCompressionEnabled(RenderSettings.threadedCompression) fun bindTexture(id: Int, slot: Int = 0) { RenderSystem.activeTexture(GL_TEXTURE0 + slot) @@ -35,21 +38,6 @@ object TextureUtils { // Array of floats normalized to [0.0, 1.0] -> [R, G, B, A] glTexImage2D(GL_TEXTURE_2D, lod, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, readImage(bufferedImage)) - // I'd also like to use glTexSubImage2D, but we have an issue where the function - // would return an error about an invalid texture format. - // - // It would allow us to upload texture data asynchronously and is more efficient - // from testing we gain approximately 20% runtime performance. - // If someone with advanced OpenGL knowledge could help us out, that would be great. - // (Very unlikely to happen, but I can hope) - // - // I've also read online that glTexStorage2D can be used for the same purpose as - // glTexImage2D with NULL data. - // However, some users may have ancient hardware that does not support this function. - // as it was implemented in OpenGL 4.2 and ES 3.0. - // - // glTexSubImage2D(GL_TEXTURE_2D, lod, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, readImage(bufferedImage)) - setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) } @@ -73,10 +61,10 @@ object TextureUtils { } private fun readImage(bufferedImage: BufferedImage): Long { - val stream = ByteArrayOutputStream() - ImageIO.write(bufferedImage, "png", stream) + val bytes = encoderPreset + .withBufferedImage(bufferedImage) + .toBytes() - val bytes = stream.toByteArray() val buffer = BufferUtils .createByteBuffer(bytes.size) .put(bytes) diff --git a/common/src/main/kotlin/com/lambda/http/Request.kt b/common/src/main/kotlin/com/lambda/http/Request.kt index f975fa0ff..c3002601c 100644 --- a/common/src/main/kotlin/com/lambda/http/Request.kt +++ b/common/src/main/kotlin/com/lambda/http/Request.kt @@ -2,10 +2,12 @@ package com.lambda.http import com.lambda.Lambda import com.lambda.util.FolderRegister.cache +import com.lambda.util.FolderRegister.createFileIfNotExists +import com.lambda.util.FolderRegister.createIfNotExists import java.io.File +import java.io.OutputStream import java.net.HttpURLConnection import java.net.URL -import java.time.Instant import kotlin.time.Duration import kotlin.time.Duration.Companion.days @@ -33,14 +35,21 @@ data class Request( /** * Downloads the resource at the specified path and caches it for future use. * - * @param path The path to the resource. + * @param name The full name of the file to be cached. * @param maxAge The maximum age of the cached resource. Default is 4 days. + * + * @return A pair containing the cached file and a boolean indicating whether the file was downloaded. */ - fun maybeDownload(path: String, maxAge: Duration = 4.days): ByteArray { - val file = File("${cache}/${path.substringAfterLast("/").hashCode()}") + fun maybeDownload( + name: String, + maxAge: Duration = 7.days, + ): File { + val (file, wasCreated) = createFileIfNotExists(name, cache, true) - if (file.exists() && Instant.now().toEpochMilli() - file.lastModified() < maxAge.inWholeMilliseconds) - return file.readBytes() + if (System.currentTimeMillis() - file.lastModified() < maxAge.inWholeMilliseconds + && file.length() > 0 + && !wasCreated) + return file file.writeText("") // Clear the file before writing to it. @@ -64,7 +73,7 @@ data class Request( } } - return file.readBytes() + return file } /** diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt index c2726bd6f..ec29fad21 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt @@ -18,6 +18,10 @@ object RenderSettings : Module( val baselineOffset by setting("Vertical Offset", 0.0, -10.0..10.0, 0.5) { page == Page.Font } private val lodBiasSetting by setting("Smoothing", 0.0, -10.0..10.0, 0.5) { page == Page.Font } + // Texture + val textureCompression by setting("Compression", 1, 1..9, 1, description = "Texture compression level, higher is slower") { page == Page.TEXTURE } + val threadedCompression by setting("Threaded Compression", false, description = "Use multiple threads for texture compression") { page == Page.TEXTURE } + // ESP val uploadsPerTick by setting("Uploads", 16, 1..256, 1, unit = " chunk/tick") { page == Page.ESP } val rebuildsPerTick by setting("Rebuilds", 64, 1..256, 1, unit = " chunk/tick") { page == Page.ESP } @@ -28,6 +32,7 @@ object RenderSettings : Module( private enum class Page { Font, - ESP + TEXTURE, + ESP, } } diff --git a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt index 26af87db9..86e698d57 100644 --- a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt +++ b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt @@ -8,8 +8,11 @@ import com.lambda.util.FolderRegister.mods import com.lambda.util.FolderRegister.packetLogs import com.lambda.util.FolderRegister.replay import com.lambda.util.StringUtils.sanitizeForFilename +import org.apache.commons.codec.digest.DigestUtils import java.io.File +import java.io.InputStream import java.net.InetSocketAddress +import java.security.MessageDigest /** * The [FolderRegister] object is responsible for managing the directory structure of the application. @@ -31,9 +34,7 @@ object FolderRegister { val cache: File = File(lambda, "cache") fun File.createIfNotExists() { - if (!exists()) { - mkdirs() - } + createFileIfNotExists(this.name, this.parentFile) } fun File.listRecursive(predicate: (File) -> Boolean = { true }) = walk().filter(predicate) @@ -48,4 +49,72 @@ object FolderRegister { path.createIfNotExists() return path } + + /** + * Returns a file with the given name in the specified directory, creating it if it does not exist. + * If the directory is not specified, it will try to parse it from the name. + * Otherwise, it will default to the Lambda directory. + * + * @param name The name of the file. + * @param directory The directory in which the file is located. Default is the Lambda directory. + * @param hash Whether to hash the name of the file. + * @return A pair containing the file and a boolean indicating whether the file was created. + */ + fun createFileIfNotExists(name: String, directory: File? = null, hash: Boolean = false): Pair { + var parsedDir: File = directory ?: lambda + + if (directory == null) { + parsedDir = name.substringAfterLast('/').substringBeforeLast('.') + .let { if (it.isEmpty()) lambda else File(it) } + } + + val compiledName = + if (hash) DigestUtils.sha256Hex(name) + else name + + val file = File(parsedDir, compiledName) + val created = !file.exists() + + if (created) { + file.parentFile.mkdirs() + file.createNewFile() + } + + return file to created + } + + + /** + * Returns a file with the given name in the specified directory, creating it if it does not exist. + * If the directory is not specified, it will try to parse it from the name. + * Otherwise, it will default to the Lambda directory. + * + * @param name The name of the file. + * @param directory The directory in which the file is located. Default is the Lambda directory. + * @param compute A lambda function to compute the file contents if it was created. + */ + @JvmName("getFileOrComputeByteArray") + inline fun getFileOrCompute(name: String, directory: File? = null, compute: () -> ByteArray): File { + val (file, wasCreated) = createFileIfNotExists(name, directory) + if (wasCreated) file.outputStream().use { it.write(compute()) } + + return file + } + + /** + * Returns a file with the given name in the specified directory, creating it if it does not exist. + * If the directory is not specified, it will try to parse it from the name. + * Otherwise, it will default to the Lambda directory. + * + * @param name The name of the file. + * @param directory The directory in which the file is located. Default is the Lambda directory. + * @param compute A lambda function to compute the file contents if it was created. + */ + @JvmName("getFileOrComputeInputStream") + inline fun getFileOrCompute(name: String, directory: File? = null, compute: () -> InputStream): File { + val (file, wasCreated) = createFileIfNotExists(name, directory) + if (wasCreated) file.outputStream().use { compute().copyTo(it) } + + return file + } } diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index 2cbb32749..8844cc10f 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { includeLib("org.javassist:javassist:3.28.0-GA") includeLib("dev.babbaj:nether-pathfinder:1.5") includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") + includeLib("com.pngencoder:pngencoder:0.15.0") // Add mods to the mod jar includeMod("net.fabricmc.fabric-api:fabric-api:$fabricApiVersion+$minecraftVersion") diff --git a/forge/build.gradle.kts b/forge/build.gradle.kts index 479861fa3..935fa55ed 100644 --- a/forge/build.gradle.kts +++ b/forge/build.gradle.kts @@ -80,14 +80,8 @@ dependencies { // Add dependencies on the required Kotlin modules. includeLib("org.reflections:reflections:0.10.2") includeLib("org.javassist:javassist:3.28.0-GA") - - // Temporary, only works for production - // See https://github.com/MinecraftForge/MinecraftForge/issues/8878 - includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") { - exclude(group = "org.jetbrains.kotlin") - exclude(group = "org.jetbrains.kotlinx") - exclude(group = "org.slf4j") - } + includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") + includeLib("com.pngencoder:pngencoder:0.15.0") // Add mods to the mod jar includeMod("thedarkcolour:kotlinforforge:$kotlinForgeVersion") diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts index 12c4af40c..9978da288 100644 --- a/neoforge/build.gradle.kts +++ b/neoforge/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { includeLib("org.javassist:javassist:3.28.0-GA") includeLib("dev.babbaj:nether-pathfinder:1.5") includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") + includeLib("com.pngencoder:pngencoder:0.15.0") // Add mods to the mod jar includeMod("thedarkcolour:kotlinforforge-neoforge:$kotlinForgeVersion") From ea972be77204e4f0e97fb2de1f95589b18ee54b3 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Wed, 21 Aug 2024 20:16:07 -0400 Subject: [PATCH 004/114] todo: pixel buffer object --- .../com/lambda/graphics/buffer/PixelBuffer.kt | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt new file mode 100644 index 000000000..26e0e9828 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt @@ -0,0 +1,66 @@ +package com.lambda.graphics.buffer + +import org.lwjgl.opengl.GL45C.* +import java.nio.ByteBuffer + +// NOT TESTED +class PixelBuffer( + width: Int, + height: Int +) { + private val pboIds = IntArray(2) { 0 } + private var index = 0 + + fun upload(data: ByteBuffer, block: () -> Unit) { + // Bind the current PBO for writing + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[index]) + + // Map the buffer and copy data into it + val bufferData = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY) as ByteBuffer + bufferData.put(data) + glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER) + + // Process the data + block() + + // Unbind the buffer + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) + + // Switch to the other PBO + index = (index + 1) % 2 + } + + fun download(): ByteBuffer { + // Bind the current PBO for reading + glBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[index]) + + // Map the buffer and copy data from it + val bufferData = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY) as ByteBuffer + val data = bufferData.slice() + + // Unbind the buffer + glUnmapBuffer(GL_PIXEL_PACK_BUFFER) + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0) + + return data + } + + fun finalize() { + // Delete the PBOs + glDeleteBuffers(pboIds) + } + + init { + // Generate the PBOs + glGenBuffers(pboIds) + + // Fill the buffers with null data to allocate the memory spaces + glBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[0]) + glBufferData(GL_PIXEL_PACK_BUFFER, width * height * 4L, GL_DYNAMIC_READ) + glBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[1]) + glBufferData(GL_PIXEL_PACK_BUFFER, width * height * 4L, GL_DYNAMIC_READ) + + // Unbind the buffer + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0) + } +} From c6328ca34cd436ab7c246578d39b9c575da54c66 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Wed, 21 Aug 2024 20:25:45 -0400 Subject: [PATCH 005/114] pixel n-buffer object --- .../com/lambda/graphics/buffer/PixelBuffer.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt index 26e0e9828..97a53fbaf 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt @@ -6,9 +6,10 @@ import java.nio.ByteBuffer // NOT TESTED class PixelBuffer( width: Int, - height: Int + height: Int, + buffers: Int = 2 ) { - private val pboIds = IntArray(2) { 0 } + private val pboIds = IntArray(buffers) { 0 } private var index = 0 fun upload(data: ByteBuffer, block: () -> Unit) { @@ -27,7 +28,7 @@ class PixelBuffer( glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) // Switch to the other PBO - index = (index + 1) % 2 + index = (index + 1) % pboIds.size } fun download(): ByteBuffer { @@ -55,10 +56,10 @@ class PixelBuffer( glGenBuffers(pboIds) // Fill the buffers with null data to allocate the memory spaces - glBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[0]) - glBufferData(GL_PIXEL_PACK_BUFFER, width * height * 4L, GL_DYNAMIC_READ) - glBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[1]) - glBufferData(GL_PIXEL_PACK_BUFFER, width * height * 4L, GL_DYNAMIC_READ) + repeat(buffers) { + glBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[it]) + glBufferData(GL_PIXEL_PACK_BUFFER, width * height * 4L, GL_DYNAMIC_READ) + } // Unbind the buffer glBindBuffer(GL_PIXEL_PACK_BUFFER, 0) From 7f35a62d8a5d92b16c92f5d721bc714b46014393 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Wed, 21 Aug 2024 20:41:08 -0400 Subject: [PATCH 006/114] handle empty pbos --- .../com/lambda/graphics/buffer/PixelBuffer.kt | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt index 97a53fbaf..44b84a277 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt @@ -1,17 +1,31 @@ package com.lambda.graphics.buffer +import net.minecraft.client.texture.NativeImage import org.lwjgl.opengl.GL45C.* import java.nio.ByteBuffer -// NOT TESTED class PixelBuffer( - width: Int, - height: Int, - buffers: Int = 2 + private val width: Int, + private val height: Int, + private val buffers: Int = 2 ) { private val pboIds = IntArray(buffers) { 0 } private var index = 0 + fun mapTexture(id: Int, buffer: ByteBuffer) { + upload(buffer) { + // Bind the texture + glBindTexture(GL_TEXTURE_2D, id) + + if (buffers > 0) + // Copy the data from the PBO to the texture + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, 0) + else + // Copy the data from the buffer to the texture + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NativeImage.read(buffer).pointer) + } + } + fun upload(data: ByteBuffer, block: () -> Unit) { // Bind the current PBO for writing glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[index]) @@ -28,7 +42,7 @@ class PixelBuffer( glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) // Switch to the other PBO - index = (index + 1) % pboIds.size + if (buffers > 0) index = (index + 1) % buffers } fun download(): ByteBuffer { From 1bba654d9f7f58f959a05d4114d29d43ea93324d Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 23 Aug 2024 19:42:29 -0400 Subject: [PATCH 007/114] test: streaming to pbo --- common/build.gradle.kts | 1 + .../kotlin/com/lambda/graphics/RenderMain.kt | 2 +- .../buffer/{vao/vertex => }/BufferUsage.kt | 8 +- .../com/lambda/graphics/buffer/PixelBuffer.kt | 81 ---------------- .../graphics/buffer/{ => fbo}/FrameBuffer.kt | 4 +- .../lambda/graphics/buffer/pbo/PixelBuffer.kt | 97 +++++++++++++++++++ .../com/lambda/graphics/buffer/vao/VAO.kt | 2 +- .../graphics/renderer/esp/ChunkedESP.kt | 4 +- .../graphics/renderer/esp/global/StaticESP.kt | 4 +- .../renderer/esp/impl/DynamicESPRenderer.kt | 4 +- .../graphics/renderer/esp/impl/ESPRenderer.kt | 2 +- .../renderer/esp/impl/StaticESPRenderer.kt | 4 +- .../com/lambda/graphics/texture/Texture.kt | 6 +- .../com/lambda/graphics/video/JCodecUtils.kt | 74 ++++++++++++++ .../kotlin/com/lambda/graphics/video/Video.kt | 44 +++++++++ .../com/lambda/gui/impl/AbstractClickGui.kt | 8 +- .../kotlin/com/lambda/module/hud/VideoTest.kt | 22 +++++ fabric/build.gradle.kts | 28 ++++++ forge/build.gradle.kts | 1 + neoforge/build.gradle.kts | 1 + 20 files changed, 290 insertions(+), 107 deletions(-) rename common/src/main/kotlin/com/lambda/graphics/buffer/{vao/vertex => }/BufferUsage.kt (60%) delete mode 100644 common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt rename common/src/main/kotlin/com/lambda/graphics/buffer/{ => fbo}/FrameBuffer.kt (99%) create mode 100644 common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt create mode 100644 common/src/main/kotlin/com/lambda/graphics/video/JCodecUtils.kt create mode 100644 common/src/main/kotlin/com/lambda/graphics/video/Video.kt create mode 100644 common/src/main/kotlin/com/lambda/module/hud/VideoTest.kt diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 07481fb66..eb6433c76 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation("org.reflections:reflections:0.10.2") implementation("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") implementation("com.pngencoder:pngencoder:0.15.0") + implementation("org.jcodec:jcodec:0.2.3") // Add Kotlin implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") diff --git a/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt index 998a21872..6eabe8442 100644 --- a/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt +++ b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt @@ -7,7 +7,7 @@ import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listener import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.graphics.animation.AnimationTicker -import com.lambda.graphics.buffer.FrameBuffer +import com.lambda.graphics.buffer.fbo.FrameBuffer import com.lambda.graphics.gl.GlStateUtils.setupGL import com.lambda.graphics.gl.Matrices import com.lambda.graphics.gl.Matrices.resetMatrices diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/BufferUsage.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/BufferUsage.kt similarity index 60% rename from common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/BufferUsage.kt rename to common/src/main/kotlin/com/lambda/graphics/buffer/BufferUsage.kt index 1cd56273d..7300d8e3f 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/BufferUsage.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/BufferUsage.kt @@ -1,10 +1,12 @@ -package com.lambda.graphics.buffer.vao.vertex +package com.lambda.graphics.buffer import com.lambda.graphics.gl.GLObject import org.lwjgl.opengl.GL30C.GL_DYNAMIC_DRAW import org.lwjgl.opengl.GL30C.GL_STATIC_DRAW +import org.lwjgl.opengl.GL30C.GL_STREAM_DRAW enum class BufferUsage(override val gl: Int) : GLObject { STATIC(GL_STATIC_DRAW), - DYNAMIC(GL_DYNAMIC_DRAW) -} \ No newline at end of file + DYNAMIC(GL_DYNAMIC_DRAW), + STREAM(GL_STREAM_DRAW); +} diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt deleted file mode 100644 index 44b84a277..000000000 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/PixelBuffer.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.lambda.graphics.buffer - -import net.minecraft.client.texture.NativeImage -import org.lwjgl.opengl.GL45C.* -import java.nio.ByteBuffer - -class PixelBuffer( - private val width: Int, - private val height: Int, - private val buffers: Int = 2 -) { - private val pboIds = IntArray(buffers) { 0 } - private var index = 0 - - fun mapTexture(id: Int, buffer: ByteBuffer) { - upload(buffer) { - // Bind the texture - glBindTexture(GL_TEXTURE_2D, id) - - if (buffers > 0) - // Copy the data from the PBO to the texture - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, 0) - else - // Copy the data from the buffer to the texture - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NativeImage.read(buffer).pointer) - } - } - - fun upload(data: ByteBuffer, block: () -> Unit) { - // Bind the current PBO for writing - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[index]) - - // Map the buffer and copy data into it - val bufferData = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY) as ByteBuffer - bufferData.put(data) - glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER) - - // Process the data - block() - - // Unbind the buffer - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) - - // Switch to the other PBO - if (buffers > 0) index = (index + 1) % buffers - } - - fun download(): ByteBuffer { - // Bind the current PBO for reading - glBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[index]) - - // Map the buffer and copy data from it - val bufferData = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY) as ByteBuffer - val data = bufferData.slice() - - // Unbind the buffer - glUnmapBuffer(GL_PIXEL_PACK_BUFFER) - glBindBuffer(GL_PIXEL_PACK_BUFFER, 0) - - return data - } - - fun finalize() { - // Delete the PBOs - glDeleteBuffers(pboIds) - } - - init { - // Generate the PBOs - glGenBuffers(pboIds) - - // Fill the buffers with null data to allocate the memory spaces - repeat(buffers) { - glBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[it]) - glBufferData(GL_PIXEL_PACK_BUFFER, width * height * 4L, GL_DYNAMIC_READ) - } - - // Unbind the buffer - glBindBuffer(GL_PIXEL_PACK_BUFFER, 0) - } -} diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/fbo/FrameBuffer.kt similarity index 99% rename from common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt rename to common/src/main/kotlin/com/lambda/graphics/buffer/fbo/FrameBuffer.kt index e16e32891..25b3bd3bc 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/fbo/FrameBuffer.kt @@ -1,4 +1,4 @@ -package com.lambda.graphics.buffer +package com.lambda.graphics.buffer.fbo import com.lambda.Lambda.mc import com.lambda.graphics.RenderMain import com.lambda.graphics.buffer.vao.VAO @@ -124,4 +124,4 @@ class FrameBuffer(private val depth: Boolean = false) { private val vao = VAO(VertexMode.TRIANGLES, VertexAttrib.Group.POS_UV) private var lastFrameBuffer: Int? = null } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt new file mode 100644 index 000000000..0c5971dbe --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt @@ -0,0 +1,97 @@ +package com.lambda.graphics.buffer.pbo + +import com.lambda.graphics.buffer.BufferUsage +import com.lambda.graphics.texture.TextureUtils.setupTexture +import org.lwjgl.opengl.GL45C.* +import java.nio.ByteBuffer +import kotlin.system.measureNanoTime + +class PixelBuffer( + private val width: Int, + private val height: Int, + private val buffers: Int = 2, + private val bufferUsage: BufferUsage = BufferUsage.DYNAMIC, +) { + private val pboIds = IntArray(buffers) + private var writeIdx = 0 // Used to copy pixels from the PBO to the texture + private var uploadIdx = 0 // Used to upload data to the PBO + + fun mapTexture(id: Int, buffer: ByteBuffer) { + val time = measureNanoTime { + upload(buffer) { allocate -> + // Bind the texture + glBindTexture(GL_TEXTURE_2D, id) + + if (allocate) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + + // Allocate the texture memory + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0) + } + else { + // Update the texture + glTextureSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, 0) + } + } + } + + println("PBO => texture $id took $time nanoseconds") + } + + fun upload(data: ByteBuffer, process: (Boolean) -> Unit) { + uploadIdx = (writeIdx + 1) % buffers + + // Bind the next PBO to update pixel values + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[uploadIdx]) + + // Allocate the memory for the buffer + process(true) + + // Map the buffer into the memory + val bufferData = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY) + if (bufferData != null) { + bufferData.put(data) + + // Release the buffer + glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER) + } else { + println("Failed to map the PBO") + } + + // Bind the current PBO for writing + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[writeIdx]) + + // Copy the pixel values from the PBO to the texture + process(false) + + // Unbind the PBO + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) + + println("Uploaded data to PBO $uploadIdx at $bufferData") + + // Swap the indices + writeIdx = uploadIdx + } + + // Called when no references to the object exist + fun finalize() { + // Delete the PBOs + glDeleteBuffers(pboIds) + } + + init { + if (buffers < 1) throw IllegalArgumentException("Buffers must be greater than or equal to 1") + + // Generate the PBOs + glGenBuffers(pboIds) + + // Fill the buffers with null data to allocate the memory spaces + repeat(buffers) { + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[it]) + glBufferData(GL_PIXEL_UNPACK_BUFFER, width * height * 4L, GL_STREAM_DRAW) + } + + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) // Unbind the buffer + } +} diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vao/VAO.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vao/VAO.kt index b6cb584cc..ccec90409 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vao/VAO.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vao/VAO.kt @@ -1,6 +1,6 @@ package com.lambda.graphics.buffer.vao -import com.lambda.graphics.buffer.vao.vertex.BufferUsage +import com.lambda.graphics.buffer.BufferUsage import com.lambda.graphics.buffer.vao.vertex.VertexAttrib import com.lambda.graphics.buffer.vao.vertex.VertexMode import com.lambda.graphics.gl.Matrices diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt index 966fe139d..1d1f8ef2c 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt @@ -5,7 +5,7 @@ import com.lambda.event.events.TickEvent import com.lambda.event.events.WorldEvent import com.lambda.event.listener.SafeListener.Companion.concurrentListener import com.lambda.event.listener.SafeListener.Companion.listener -import com.lambda.graphics.buffer.vao.vertex.BufferUsage +import com.lambda.graphics.buffer.BufferUsage import com.lambda.graphics.renderer.esp.impl.ESPRenderer import com.lambda.graphics.renderer.esp.impl.StaticESPRenderer import com.lambda.module.modules.client.RenderSettings @@ -126,4 +126,4 @@ class ChunkedESP private constructor( } } } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/StaticESP.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/StaticESP.kt index 454d624b9..fa40ed0ca 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/StaticESP.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/StaticESP.kt @@ -4,7 +4,7 @@ import com.lambda.event.EventFlow.post import com.lambda.event.events.RenderEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listener -import com.lambda.graphics.buffer.vao.vertex.BufferUsage +import com.lambda.graphics.buffer.BufferUsage import com.lambda.graphics.renderer.esp.impl.StaticESPRenderer object StaticESP : StaticESPRenderer(BufferUsage.DYNAMIC, false) { @@ -15,4 +15,4 @@ object StaticESP : StaticESPRenderer(BufferUsage.DYNAMIC, false) { upload() } } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt index bf8f9a878..e9383f035 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt @@ -1,5 +1,5 @@ package com.lambda.graphics.renderer.esp.impl -import com.lambda.graphics.buffer.vao.vertex.BufferUsage +import com.lambda.graphics.buffer.BufferUsage -open class DynamicESPRenderer : ESPRenderer(BufferUsage.DYNAMIC, true) \ No newline at end of file +open class DynamicESPRenderer : ESPRenderer(BufferUsage.DYNAMIC, true) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/ESPRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/ESPRenderer.kt index c35a4a2e5..15d0977da 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/ESPRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/ESPRenderer.kt @@ -2,7 +2,7 @@ package com.lambda.graphics.renderer.esp.impl import com.lambda.Lambda.mc import com.lambda.graphics.buffer.vao.VAO -import com.lambda.graphics.buffer.vao.vertex.BufferUsage +import com.lambda.graphics.buffer.BufferUsage import com.lambda.graphics.buffer.vao.vertex.VertexAttrib import com.lambda.graphics.buffer.vao.vertex.VertexMode import com.lambda.graphics.gl.GlStateUtils.withFaceCulling diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt index 26c7c9140..592412dde 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt @@ -1,7 +1,7 @@ package com.lambda.graphics.renderer.esp.impl import com.lambda.graphics.buffer.vao.IRenderContext -import com.lambda.graphics.buffer.vao.vertex.BufferUsage +import com.lambda.graphics.buffer.BufferUsage import java.awt.Color import java.util.concurrent.ConcurrentHashMap @@ -45,4 +45,4 @@ open class StaticESPRenderer( } data class Vertex(val x: Double, val y: Double, val z: Double, val color: Color) -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index 7512b523b..ff5990c87 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -1,12 +1,10 @@ package com.lambda.graphics.texture import com.lambda.graphics.texture.TextureUtils.bindTexture -import com.lambda.threading.mainThread -import com.lambda.threading.runGameScheduled import org.lwjgl.opengl.GL13.glGenTextures -abstract class Texture { - private val id = glGenTextures() +open class Texture { + val id = glGenTextures() fun bind(slot: Int = 0) = bindTexture(id, slot) } diff --git a/common/src/main/kotlin/com/lambda/graphics/video/JCodecUtils.kt b/common/src/main/kotlin/com/lambda/graphics/video/JCodecUtils.kt new file mode 100644 index 000000000..d95392d01 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/video/JCodecUtils.kt @@ -0,0 +1,74 @@ +package com.lambda.graphics.video + +import org.jcodec.api.FrameGrab +import org.jcodec.common.io.NIOUtils +import org.jcodec.common.model.ColorSpace +import org.jcodec.common.model.Picture +import org.jcodec.scale.ColorUtil +import org.jcodec.scale.RgbToBgr +import java.awt.image.BufferedImage +import java.awt.image.DataBufferByte +import java.io.File + +object JCodecUtils { + fun demuxVideo(path: String): FrameGrab = + FrameGrab.createFrameGrab(NIOUtils.readableChannel(File(path))) + + fun toBufferedImage(src: Picture): BufferedImage { + val processedSrc = convertToBGR(src) + val dst = BufferedImage(processedSrc.croppedWidth, processedSrc.croppedHeight, BufferedImage.TYPE_3BYTE_BGR) + + if (processedSrc.crop == null) { + copyImageData(processedSrc, dst) + } else { + copyCroppedImageData(processedSrc, dst) + } + + return dst + } + + private fun convertToBGR(src: Picture): Picture { + if (src.color == ColorSpace.BGR) return src + + val bgr = Picture.createCropped(src.width, src.height, ColorSpace.BGR, src.crop) + + if (src.color == ColorSpace.RGB) { + RgbToBgr().transform(src, bgr) + } else { + val transform = ColorUtil.getTransform(src.color, ColorSpace.RGB) + transform.transform(src, bgr) + RgbToBgr().transform(bgr, bgr) + } + + return bgr + } + + private fun copyImageData(src: Picture, dst: BufferedImage) { + val data = (dst.raster.dataBuffer as DataBufferByte).data + val srcData = src.getPlaneData(0) + + for (i in data.indices) { + data[i] = (srcData[i] + 128).toByte() + } + } + + private fun copyCroppedImageData(src: Picture, dst: BufferedImage) { + val data = (dst.raster.dataBuffer as DataBufferByte).data + val srcData = src.getPlaneData(0) + val dstStride = dst.width * 3 + val srcStride = src.width * 3 + + for (line in 0 until dst.height) { + var dstOffset = line * dstStride + var srcOffset = line * srcStride + + for (x in 0 until dstStride step 3) { + data[dstOffset] = (srcData[srcOffset] + 128).toByte() + data[dstOffset + 1] = (srcData[srcOffset + 1] + 128).toByte() + data[dstOffset + 2] = (srcData[srcOffset + 2] + 128).toByte() + dstOffset += 3 + srcOffset += 3 + } + } + } +} diff --git a/common/src/main/kotlin/com/lambda/graphics/video/Video.kt b/common/src/main/kotlin/com/lambda/graphics/video/Video.kt new file mode 100644 index 000000000..beff72f0e --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/video/Video.kt @@ -0,0 +1,44 @@ +package com.lambda.graphics.video + +import com.lambda.graphics.buffer.BufferUsage +import com.lambda.graphics.buffer.pbo.PixelBuffer +import com.lambda.graphics.texture.Texture +import com.lambda.graphics.video.JCodecUtils.toBufferedImage +import com.pngencoder.PngEncoder +import java.io.File +import java.nio.ByteBuffer +import javax.imageio.ImageIO + + +class Video( + private val input: String, +) { + private val decoderStream = JCodecUtils.demuxVideo(input) + + val width = decoderStream.mediaInfo.dim.width + val height = decoderStream.mediaInfo.dim.height + + private val pbo = PixelBuffer(width, height, buffers = 2, bufferUsage = BufferUsage.STREAM) + val texture = Texture() + + fun upload() { + val picture = decoderStream.nativeFrame + if (picture == null) { + decoderStream.seekToFramePrecise(0) + return + } + + val image = toBufferedImage(picture) + + val bytes = PngEncoder() + .withBufferedImage(image) + .toBytes() + + val buffer = + ByteBuffer.allocateDirect(bytes.size) + .put(bytes) + .flip() + + pbo.mapTexture(texture.id, buffer) + } +} diff --git a/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt b/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt index aca17d474..6170d4cfb 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt @@ -2,7 +2,7 @@ package com.lambda.gui.impl import com.lambda.Lambda.mc import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.graphics.buffer.FrameBuffer +import com.lambda.graphics.buffer.fbo.FrameBuffer import com.lambda.graphics.shader.Shader import com.lambda.gui.AbstractGuiConfigurable import com.lambda.gui.GuiConfigurable @@ -10,17 +10,13 @@ import com.lambda.gui.api.GuiEvent import com.lambda.gui.api.LambdaGui import com.lambda.gui.api.component.WindowComponent import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.gui.impl.clickgui.LambdaClickGui import com.lambda.gui.impl.clickgui.buttons.SettingButton import com.lambda.gui.impl.clickgui.windows.ModuleWindow import com.lambda.gui.impl.clickgui.windows.tag.CustomModuleWindow import com.lambda.gui.impl.clickgui.windows.tag.TagWindow -import com.lambda.gui.impl.hudgui.LambdaHudGui import com.lambda.module.Module import com.lambda.module.modules.client.ClickGui import com.lambda.util.Mouse -import com.mojang.blaze3d.systems.RenderSystem.recordRenderCall -import kotlin.reflect.KMutableProperty import kotlin.reflect.KMutableProperty0 abstract class AbstractClickGui(name: String, owner: Module? = null) : LambdaGui(name, owner) { @@ -128,4 +124,4 @@ abstract class AbstractClickGui(name: String, owner: Module? = null) : LambdaGui if (!isOpen) return closing = true } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/module/hud/VideoTest.kt b/common/src/main/kotlin/com/lambda/module/hud/VideoTest.kt new file mode 100644 index 000000000..cf3a7ac83 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/hud/VideoTest.kt @@ -0,0 +1,22 @@ +package com.lambda.module.hud + +import com.lambda.graphics.renderer.gui.TextureRenderer.drawTexture +import com.lambda.graphics.video.Video +import com.lambda.module.HudModule + +object VideoTest : HudModule( + name = "VideoTest", + defaultTags = setOf(), +) { + override val width = 430.0 + override val height = 548.0 + + private val video = Video("C:\\Users\\Kamigen\\Documents\\weed.mp4") + + init { + onRender { + video.upload() + drawTexture(video.texture, rect) + } + } +} diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index 8844cc10f..d0d60bf8e 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.internal.jvm.Jvm + val modVersion: String by project val minecraftVersion: String by project val fabricLoaderVersion: String by project @@ -68,6 +70,7 @@ dependencies { includeLib("dev.babbaj:nether-pathfinder:1.5") includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") includeLib("com.pngencoder:pngencoder:0.15.0") + includeLib("org.jcodec:jcodec:0.2.3") // Add mods to the mod jar includeMod("net.fabricmc.fabric-api:fabric-api:$fabricApiVersion+$minecraftVersion") @@ -100,4 +103,29 @@ tasks { // They allow you to make field, method, and class access public. injectAccessWidener = true } + + register("run + RenderDoc") { + val javaHome = Jvm.current().javaHome + + commandLine = listOf( + "C:\\Program Files\\RenderDoc\\renderdoccmd.exe", + "capture", + "--opt-api-validation", + "--opt-api-validation-unmute", + "--opt-hook-children", + "--wait-for-exit", + "--working-dir", + ".", + "$javaHome/bin/java.exe", + "-Xmx64m", + "-Xms64m", + //"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005", + "-Dorg.gradle.appname=gradlew", + "-Dorg.gradle.java.home=$javaHome", + "-classpath", + "C:\\Users\\Kamigen\\Desktop\\BetterElytraBot\\NeoLambda\\gradle\\wrapper\\gradle-wrapper.jar", + "org.gradle.wrapper.GradleWrapperMain", + ":fabric:runClient", + ) + } } diff --git a/forge/build.gradle.kts b/forge/build.gradle.kts index 935fa55ed..bf6133cdf 100644 --- a/forge/build.gradle.kts +++ b/forge/build.gradle.kts @@ -82,6 +82,7 @@ dependencies { includeLib("org.javassist:javassist:3.28.0-GA") includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") includeLib("com.pngencoder:pngencoder:0.15.0") + includeLib("org.jcodec:jcodec:0.2.3") // Add mods to the mod jar includeMod("thedarkcolour:kotlinforforge:$kotlinForgeVersion") diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts index 9978da288..4693ae091 100644 --- a/neoforge/build.gradle.kts +++ b/neoforge/build.gradle.kts @@ -69,6 +69,7 @@ dependencies { includeLib("dev.babbaj:nether-pathfinder:1.5") includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") includeLib("com.pngencoder:pngencoder:0.15.0") + includeLib("org.jcodec:jcodec:0.2.3") // Add mods to the mod jar includeMod("thedarkcolour:kotlinforforge-neoforge:$kotlinForgeVersion") From 06aba2d8aa6ea37cc2f37f906ca0136062eb1ff4 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 23 Aug 2024 20:35:03 -0400 Subject: [PATCH 008/114] added upload record time --- .../lambda/graphics/buffer/pbo/PixelBuffer.kt | 88 +++++++++---------- 1 file changed, 41 insertions(+), 47 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt index 0c5971dbe..e10bf7cd5 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt @@ -1,10 +1,8 @@ package com.lambda.graphics.buffer.pbo import com.lambda.graphics.buffer.BufferUsage -import com.lambda.graphics.texture.TextureUtils.setupTexture import org.lwjgl.opengl.GL45C.* import java.nio.ByteBuffer -import kotlin.system.measureNanoTime class PixelBuffer( private val width: Int, @@ -16,62 +14,58 @@ class PixelBuffer( private var writeIdx = 0 // Used to copy pixels from the PBO to the texture private var uploadIdx = 0 // Used to upload data to the PBO - fun mapTexture(id: Int, buffer: ByteBuffer) { - val time = measureNanoTime { - upload(buffer) { allocate -> - // Bind the texture - glBindTexture(GL_TEXTURE_2D, id) - - if (allocate) { - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) - - // Allocate the texture memory - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0) - } - else { - // Update the texture - glTextureSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, 0) - } - } + private val queryId = glGenQueries() // Used to measure the time taken to upload data to the PBO + val uploadTime get() = IntArray(1).also { glGetQueryObjectiv(queryId, GL_QUERY_RESULT, it) }[0] + + fun mapTexture(id: Int, buffer: ByteBuffer) = + upload(buffer) { + // Bind the texture + glBindTexture(GL_TEXTURE_2D, id) + + // Perform the actual data transfer to the GPU + glTextureSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, 0) } - println("PBO => texture $id took $time nanoseconds") - } + fun upload(data: ByteBuffer, process: () -> Unit) = + recordTransfer { + uploadIdx = (writeIdx + 1) % buffers - fun upload(data: ByteBuffer, process: (Boolean) -> Unit) { - uploadIdx = (writeIdx + 1) % buffers + // Bind the next PBO to update pixel values + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[writeIdx]) - // Bind the next PBO to update pixel values - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[uploadIdx]) + // Map the buffer into the memory + val bufferData = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY) + if (bufferData != null) { + bufferData.put(data) - // Allocate the memory for the buffer - process(true) + // Release the buffer + glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER) + } else { + println("Failed to map the PBO") + } - // Map the buffer into the memory - val bufferData = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY) - if (bufferData != null) { - bufferData.put(data) + // Bind the current PBO for writing + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[uploadIdx]) - // Release the buffer - glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER) - } else { - println("Failed to map the PBO") - } + // Copy the pixel values from the PBO to the texture + process() - // Bind the current PBO for writing - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[writeIdx]) + // Unbind the PBO + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) - // Copy the pixel values from the PBO to the texture - process(false) + // Swap the indices + writeIdx = uploadIdx + } - // Unbind the PBO - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) + private fun recordTransfer(block: () -> Unit) { + // Start the timer + glBeginQuery(GL_TIME_ELAPSED, queryId) - println("Uploaded data to PBO $uploadIdx at $bufferData") + // Perform the transfer + block() - // Swap the indices - writeIdx = uploadIdx + // Stop the timer + glEndQuery(GL_TIME_ELAPSED) } // Called when no references to the object exist @@ -89,7 +83,7 @@ class PixelBuffer( // Fill the buffers with null data to allocate the memory spaces repeat(buffers) { glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[it]) - glBufferData(GL_PIXEL_UNPACK_BUFFER, width * height * 4L, GL_STREAM_DRAW) + glBufferData(GL_PIXEL_UNPACK_BUFFER, width * height * 4L, bufferUsage.gl) } glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) // Unbind the buffer From 68b24da1629bf9eb621fec4cd2701bbd44eacd4a Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 23 Aug 2024 20:45:25 -0400 Subject: [PATCH 009/114] added debug gremedy labels --- .../kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt index e10bf7cd5..7f7a8cd3b 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt @@ -2,6 +2,7 @@ package com.lambda.graphics.buffer.pbo import com.lambda.graphics.buffer.BufferUsage import org.lwjgl.opengl.GL45C.* +import org.lwjgl.opengl.GREMEDYStringMarker import java.nio.ByteBuffer class PixelBuffer( @@ -30,6 +31,8 @@ class PixelBuffer( recordTransfer { uploadIdx = (writeIdx + 1) % buffers + GREMEDYStringMarker.glStringMarkerGREMEDY("Data transfer to buffer") + // Bind the next PBO to update pixel values glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[writeIdx]) @@ -47,6 +50,8 @@ class PixelBuffer( // Bind the current PBO for writing glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[uploadIdx]) + GREMEDYStringMarker.glStringMarkerGREMEDY("Data transfer to GPU") + // Copy the pixel values from the PBO to the texture process() From 12e2045b5697e6570ae84af4ed78a368e43f4614 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 23 Aug 2024 21:40:26 -0400 Subject: [PATCH 010/114] better pbo --- .../lambda/graphics/buffer/pbo/PixelBuffer.kt | 73 ++++++++++++++----- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt index 7f7a8cd3b..c743e104f 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt @@ -1,8 +1,9 @@ package com.lambda.graphics.buffer.pbo +import com.lambda.Lambda.LOG import com.lambda.graphics.buffer.BufferUsage +import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL45C.* -import org.lwjgl.opengl.GREMEDYStringMarker import java.nio.ByteBuffer class PixelBuffer( @@ -17,24 +18,51 @@ class PixelBuffer( private val queryId = glGenQueries() // Used to measure the time taken to upload data to the PBO val uploadTime get() = IntArray(1).also { glGetQueryObjectiv(queryId, GL_QUERY_RESULT, it) }[0] + var transferRate = 0L // The transfer rate in bytes per second + private set + + var pboSupported = false + private set fun mapTexture(id: Int, buffer: ByteBuffer) = upload(buffer) { // Bind the texture glBindTexture(GL_TEXTURE_2D, id) - // Perform the actual data transfer to the GPU - glTextureSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, 0) + if (buffers > 0 && pboSupported) { + // Bind the next PBO to update pixel values + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[writeIdx]) + + // Perform the actual data transfer to the GPU + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, 0) + } + else { + // Perform the actual data transfer to the GPU + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer) + } } fun upload(data: ByteBuffer, process: () -> Unit) = recordTransfer { - uploadIdx = (writeIdx + 1) % buffers + if (buffers >= 2) + uploadIdx = (writeIdx + 1) % buffers - GREMEDYStringMarker.glStringMarkerGREMEDY("Data transfer to buffer") + // Copy the pixel values from the PBO to the texture + process() - // Bind the next PBO to update pixel values - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[writeIdx]) + if (!pboSupported) return@recordTransfer + + // Bind the current PBO for writing + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[uploadIdx]) + + // Note that glMapBuffer() causes sync issue. + // If GPU is working with this buffer, glMapBuffer() will wait(stall) + // until GPU to finish its job. To avoid waiting (idle), you can call + // first glBufferData() with NULL pointer before glMapBuffer(). + // If you do that, the previous data in PBO will be discarded and + // glMapBuffer() returns a new allocated pointer immediately + // even if GPU is still working with the previous data. + glBufferData(GL_PIXEL_UNPACK_BUFFER, width * height * 4L, bufferUsage.gl) // Map the buffer into the memory val bufferData = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY) @@ -43,17 +71,7 @@ class PixelBuffer( // Release the buffer glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER) - } else { - println("Failed to map the PBO") - } - - // Bind the current PBO for writing - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[uploadIdx]) - - GREMEDYStringMarker.glStringMarkerGREMEDY("Data transfer to GPU") - - // Copy the pixel values from the PBO to the texture - process() + } else throw IllegalStateException("Failed to map the buffer") // Unbind the PBO glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) @@ -63,6 +81,11 @@ class PixelBuffer( } private fun recordTransfer(block: () -> Unit) { + if (!pboSupported) { + block() + return + } + // Start the timer glBeginQuery(GL_TIME_ELAPSED, queryId) @@ -71,6 +94,12 @@ class PixelBuffer( // Stop the timer glEndQuery(GL_TIME_ELAPSED) + + // Calculate the transfer rate + val time = uploadTime + if (time > 0) { + transferRate = (width * height * 4L * 1_000_000_000) / time + } } // Called when no references to the object exist @@ -80,7 +109,13 @@ class PixelBuffer( } init { - if (buffers < 1) throw IllegalArgumentException("Buffers must be greater than or equal to 1") + // Check if the PBO is supported + GL.getCapabilities().let { pboSupported = it.OpenGL30 || it.GL_ARB_pixel_buffer_object } + + if (!pboSupported && buffers > 0) + LOG.warn("Client tried to utilize PBOs, but they are not supported on the machine, falling back to direct buffer upload") + + if (buffers < 0) throw IllegalArgumentException("Buffers must be greater than or equal to 0") // Generate the PBOs glGenBuffers(pboIds) From 7cbfd1443a92e8134fb10a98c1ca6d8fac63474c Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 23 Aug 2024 21:56:18 -0400 Subject: [PATCH 011/114] better handling --- .../lambda/graphics/buffer/pbo/PixelBuffer.kt | 49 ++++++++++--------- .../kotlin/com/lambda/module/hud/VideoTest.kt | 4 +- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt index c743e104f..89b2bd345 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt @@ -17,14 +17,24 @@ class PixelBuffer( private var uploadIdx = 0 // Used to upload data to the PBO private val queryId = glGenQueries() // Used to measure the time taken to upload data to the PBO - val uploadTime get() = IntArray(1).also { glGetQueryObjectiv(queryId, GL_QUERY_RESULT, it) }[0] - var transferRate = 0L // The transfer rate in bytes per second - private set + private val uploadTime get() = IntArray(1).also { glGetQueryObjectiv(queryId, GL_QUERY_RESULT, it) }[0] + private var transferRate = 0L // The transfer rate in bytes per second - var pboSupported = false - private set + private val pboSupported = GL.getCapabilities().OpenGL30 || GL.getCapabilities().GL_ARB_pixel_buffer_object + + private var initialDataSent: Boolean = false + + fun mapTexture(id: Int, buffer: ByteBuffer) { + if (!initialDataSent) { + glBindTexture(GL_TEXTURE_2D, id) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0) + glBindTexture(GL_TEXTURE_2D, 0) + + initialDataSent = true + + return + } - fun mapTexture(id: Int, buffer: ByteBuffer) = upload(buffer) { // Bind the texture glBindTexture(GL_TEXTURE_2D, id) @@ -34,13 +44,20 @@ class PixelBuffer( glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[writeIdx]) // Perform the actual data transfer to the GPU - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, 0) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0) } else { // Perform the actual data transfer to the GPU - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, buffer) } + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + + // Unbind the texture + glBindTexture(GL_TEXTURE_2D, 0) } + } fun upload(data: ByteBuffer, process: () -> Unit) = recordTransfer { @@ -50,8 +67,6 @@ class PixelBuffer( // Copy the pixel values from the PBO to the texture process() - if (!pboSupported) return@recordTransfer - // Bind the current PBO for writing glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[uploadIdx]) @@ -81,11 +96,6 @@ class PixelBuffer( } private fun recordTransfer(block: () -> Unit) { - if (!pboSupported) { - block() - return - } - // Start the timer glBeginQuery(GL_TIME_ELAPSED, queryId) @@ -97,9 +107,7 @@ class PixelBuffer( // Calculate the transfer rate val time = uploadTime - if (time > 0) { - transferRate = (width * height * 4L * 1_000_000_000) / time - } + if (time > 0) transferRate = (width * height * 4L * 1_000_000_000) / time } // Called when no references to the object exist @@ -109,14 +117,11 @@ class PixelBuffer( } init { - // Check if the PBO is supported - GL.getCapabilities().let { pboSupported = it.OpenGL30 || it.GL_ARB_pixel_buffer_object } + if (buffers < 0) throw IllegalArgumentException("Buffers must be greater than or equal to 0") if (!pboSupported && buffers > 0) LOG.warn("Client tried to utilize PBOs, but they are not supported on the machine, falling back to direct buffer upload") - if (buffers < 0) throw IllegalArgumentException("Buffers must be greater than or equal to 0") - // Generate the PBOs glGenBuffers(pboIds) diff --git a/common/src/main/kotlin/com/lambda/module/hud/VideoTest.kt b/common/src/main/kotlin/com/lambda/module/hud/VideoTest.kt index cf3a7ac83..b0e25a88c 100644 --- a/common/src/main/kotlin/com/lambda/module/hud/VideoTest.kt +++ b/common/src/main/kotlin/com/lambda/module/hud/VideoTest.kt @@ -15,8 +15,8 @@ object VideoTest : HudModule( init { onRender { - video.upload() - drawTexture(video.texture, rect) + //video.upload() + //drawTexture(video.texture, rect) } } } From a130c8c34fa4c4aaff0d086696f0d38141f5da6d Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 23 Aug 2024 21:59:35 -0400 Subject: [PATCH 012/114] removed videos --- common/build.gradle.kts | 1 - .../com/lambda/graphics/video/JCodecUtils.kt | 74 ------------------- .../kotlin/com/lambda/graphics/video/Video.kt | 44 ----------- .../kotlin/com/lambda/module/hud/VideoTest.kt | 22 ------ fabric/build.gradle.kts | 26 ------- forge/build.gradle.kts | 1 - neoforge/build.gradle.kts | 1 - 7 files changed, 169 deletions(-) delete mode 100644 common/src/main/kotlin/com/lambda/graphics/video/JCodecUtils.kt delete mode 100644 common/src/main/kotlin/com/lambda/graphics/video/Video.kt delete mode 100644 common/src/main/kotlin/com/lambda/module/hud/VideoTest.kt diff --git a/common/build.gradle.kts b/common/build.gradle.kts index eb6433c76..07481fb66 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -24,7 +24,6 @@ dependencies { implementation("org.reflections:reflections:0.10.2") implementation("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") implementation("com.pngencoder:pngencoder:0.15.0") - implementation("org.jcodec:jcodec:0.2.3") // Add Kotlin implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") diff --git a/common/src/main/kotlin/com/lambda/graphics/video/JCodecUtils.kt b/common/src/main/kotlin/com/lambda/graphics/video/JCodecUtils.kt deleted file mode 100644 index d95392d01..000000000 --- a/common/src/main/kotlin/com/lambda/graphics/video/JCodecUtils.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.lambda.graphics.video - -import org.jcodec.api.FrameGrab -import org.jcodec.common.io.NIOUtils -import org.jcodec.common.model.ColorSpace -import org.jcodec.common.model.Picture -import org.jcodec.scale.ColorUtil -import org.jcodec.scale.RgbToBgr -import java.awt.image.BufferedImage -import java.awt.image.DataBufferByte -import java.io.File - -object JCodecUtils { - fun demuxVideo(path: String): FrameGrab = - FrameGrab.createFrameGrab(NIOUtils.readableChannel(File(path))) - - fun toBufferedImage(src: Picture): BufferedImage { - val processedSrc = convertToBGR(src) - val dst = BufferedImage(processedSrc.croppedWidth, processedSrc.croppedHeight, BufferedImage.TYPE_3BYTE_BGR) - - if (processedSrc.crop == null) { - copyImageData(processedSrc, dst) - } else { - copyCroppedImageData(processedSrc, dst) - } - - return dst - } - - private fun convertToBGR(src: Picture): Picture { - if (src.color == ColorSpace.BGR) return src - - val bgr = Picture.createCropped(src.width, src.height, ColorSpace.BGR, src.crop) - - if (src.color == ColorSpace.RGB) { - RgbToBgr().transform(src, bgr) - } else { - val transform = ColorUtil.getTransform(src.color, ColorSpace.RGB) - transform.transform(src, bgr) - RgbToBgr().transform(bgr, bgr) - } - - return bgr - } - - private fun copyImageData(src: Picture, dst: BufferedImage) { - val data = (dst.raster.dataBuffer as DataBufferByte).data - val srcData = src.getPlaneData(0) - - for (i in data.indices) { - data[i] = (srcData[i] + 128).toByte() - } - } - - private fun copyCroppedImageData(src: Picture, dst: BufferedImage) { - val data = (dst.raster.dataBuffer as DataBufferByte).data - val srcData = src.getPlaneData(0) - val dstStride = dst.width * 3 - val srcStride = src.width * 3 - - for (line in 0 until dst.height) { - var dstOffset = line * dstStride - var srcOffset = line * srcStride - - for (x in 0 until dstStride step 3) { - data[dstOffset] = (srcData[srcOffset] + 128).toByte() - data[dstOffset + 1] = (srcData[srcOffset + 1] + 128).toByte() - data[dstOffset + 2] = (srcData[srcOffset + 2] + 128).toByte() - dstOffset += 3 - srcOffset += 3 - } - } - } -} diff --git a/common/src/main/kotlin/com/lambda/graphics/video/Video.kt b/common/src/main/kotlin/com/lambda/graphics/video/Video.kt deleted file mode 100644 index beff72f0e..000000000 --- a/common/src/main/kotlin/com/lambda/graphics/video/Video.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.lambda.graphics.video - -import com.lambda.graphics.buffer.BufferUsage -import com.lambda.graphics.buffer.pbo.PixelBuffer -import com.lambda.graphics.texture.Texture -import com.lambda.graphics.video.JCodecUtils.toBufferedImage -import com.pngencoder.PngEncoder -import java.io.File -import java.nio.ByteBuffer -import javax.imageio.ImageIO - - -class Video( - private val input: String, -) { - private val decoderStream = JCodecUtils.demuxVideo(input) - - val width = decoderStream.mediaInfo.dim.width - val height = decoderStream.mediaInfo.dim.height - - private val pbo = PixelBuffer(width, height, buffers = 2, bufferUsage = BufferUsage.STREAM) - val texture = Texture() - - fun upload() { - val picture = decoderStream.nativeFrame - if (picture == null) { - decoderStream.seekToFramePrecise(0) - return - } - - val image = toBufferedImage(picture) - - val bytes = PngEncoder() - .withBufferedImage(image) - .toBytes() - - val buffer = - ByteBuffer.allocateDirect(bytes.size) - .put(bytes) - .flip() - - pbo.mapTexture(texture.id, buffer) - } -} diff --git a/common/src/main/kotlin/com/lambda/module/hud/VideoTest.kt b/common/src/main/kotlin/com/lambda/module/hud/VideoTest.kt deleted file mode 100644 index b0e25a88c..000000000 --- a/common/src/main/kotlin/com/lambda/module/hud/VideoTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.lambda.module.hud - -import com.lambda.graphics.renderer.gui.TextureRenderer.drawTexture -import com.lambda.graphics.video.Video -import com.lambda.module.HudModule - -object VideoTest : HudModule( - name = "VideoTest", - defaultTags = setOf(), -) { - override val width = 430.0 - override val height = 548.0 - - private val video = Video("C:\\Users\\Kamigen\\Documents\\weed.mp4") - - init { - onRender { - //video.upload() - //drawTexture(video.texture, rect) - } - } -} diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index d0d60bf8e..e34c7b297 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -70,7 +70,6 @@ dependencies { includeLib("dev.babbaj:nether-pathfinder:1.5") includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") includeLib("com.pngencoder:pngencoder:0.15.0") - includeLib("org.jcodec:jcodec:0.2.3") // Add mods to the mod jar includeMod("net.fabricmc.fabric-api:fabric-api:$fabricApiVersion+$minecraftVersion") @@ -103,29 +102,4 @@ tasks { // They allow you to make field, method, and class access public. injectAccessWidener = true } - - register("run + RenderDoc") { - val javaHome = Jvm.current().javaHome - - commandLine = listOf( - "C:\\Program Files\\RenderDoc\\renderdoccmd.exe", - "capture", - "--opt-api-validation", - "--opt-api-validation-unmute", - "--opt-hook-children", - "--wait-for-exit", - "--working-dir", - ".", - "$javaHome/bin/java.exe", - "-Xmx64m", - "-Xms64m", - //"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005", - "-Dorg.gradle.appname=gradlew", - "-Dorg.gradle.java.home=$javaHome", - "-classpath", - "C:\\Users\\Kamigen\\Desktop\\BetterElytraBot\\NeoLambda\\gradle\\wrapper\\gradle-wrapper.jar", - "org.gradle.wrapper.GradleWrapperMain", - ":fabric:runClient", - ) - } } diff --git a/forge/build.gradle.kts b/forge/build.gradle.kts index bf6133cdf..935fa55ed 100644 --- a/forge/build.gradle.kts +++ b/forge/build.gradle.kts @@ -82,7 +82,6 @@ dependencies { includeLib("org.javassist:javassist:3.28.0-GA") includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") includeLib("com.pngencoder:pngencoder:0.15.0") - includeLib("org.jcodec:jcodec:0.2.3") // Add mods to the mod jar includeMod("thedarkcolour:kotlinforforge:$kotlinForgeVersion") diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts index 4693ae091..9978da288 100644 --- a/neoforge/build.gradle.kts +++ b/neoforge/build.gradle.kts @@ -69,7 +69,6 @@ dependencies { includeLib("dev.babbaj:nether-pathfinder:1.5") includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") includeLib("com.pngencoder:pngencoder:0.15.0") - includeLib("org.jcodec:jcodec:0.2.3") // Add mods to the mod jar includeMod("thedarkcolour:kotlinforforge-neoforge:$kotlinForgeVersion") From bbd705f4a4d68bf97a4800e2e72fbef8cdb2d7b2 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 23 Aug 2024 22:04:38 -0400 Subject: [PATCH 013/114] added documentation for pbo --- .../lambda/graphics/buffer/pbo/PixelBuffer.kt | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt index 89b2bd345..0dc03a38e 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt @@ -6,6 +6,15 @@ import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL45C.* import java.nio.ByteBuffer +/** + * Represents a Pixel Buffer Object (PBO) that facilitates asynchronous data transfer to the GPU. + * This class manages the creation, usage, and cleanup of PBOs and provides methods to map textures and upload data efficiently. + * + * @property width The width of the texture in pixels. + * @property height The height of the texture in pixels. + * @property buffers The number of PBOs to be used. Default is 2, which allows double buffering. + * @property bufferUsage The usage pattern of the buffer, indicating how the buffer will be used (static, dynamic, etc.). + */ class PixelBuffer( private val width: Int, private val height: Int, @@ -24,6 +33,12 @@ class PixelBuffer( private var initialDataSent: Boolean = false + /** + * Maps the given texture ID to the buffer and performs the necessary operations to upload the texture data. + * + * @param id The texture ID to which the buffer will be mapped. + * @param buffer The [ByteBuffer] containing the pixel data to be uploaded to the texture. + */ fun mapTexture(id: Int, buffer: ByteBuffer) { if (!initialDataSent) { glBindTexture(GL_TEXTURE_2D, id) @@ -59,6 +74,12 @@ class PixelBuffer( } } + /** + * Uploads the given pixel data to the PBO and executes the provided processing function to manage the PBO's data transfer. + * + * @param data The [ByteBuffer] containing the pixel data to be uploaded. + * @param process A lambda function to execute after uploading the data to manage the PBO's data transfer. + */ fun upload(data: ByteBuffer, process: () -> Unit) = recordTransfer { if (buffers >= 2) @@ -95,6 +116,11 @@ class PixelBuffer( writeIdx = uploadIdx } + /** + * Measures and records the time taken to transfer data to the PBO, calculating the transfer rate in bytes per second. + * + * @param block A lambda function representing the block of code where the transfer occurs. + */ private fun recordTransfer(block: () -> Unit) { // Start the timer glBeginQuery(GL_TIME_ELAPSED, queryId) @@ -110,12 +136,19 @@ class PixelBuffer( if (time > 0) transferRate = (width * height * 4L * 1_000_000_000) / time } - // Called when no references to the object exist + /** + * Cleans up resources by deleting the PBOs when the object is no longer in use. + */ fun finalize() { // Delete the PBOs glDeleteBuffers(pboIds) } + /** + * Initializes the PBOs, allocates memory for them, and handles unsupported PBO scenarios. + * + * @throws IllegalArgumentException If the number of buffers is less than 0. + */ init { if (buffers < 0) throw IllegalArgumentException("Buffers must be greater than or equal to 0") From 4b965566af0a3841d873645462615bd9cfb08022 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 23 Aug 2024 22:06:09 -0400 Subject: [PATCH 014/114] moved the fbo down one level --- common/src/main/kotlin/com/lambda/graphics/RenderMain.kt | 2 +- .../kotlin/com/lambda/graphics/buffer/{fbo => }/FrameBuffer.kt | 3 ++- common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) rename common/src/main/kotlin/com/lambda/graphics/buffer/{fbo => }/FrameBuffer.kt (99%) diff --git a/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt index 6eabe8442..998a21872 100644 --- a/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt +++ b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt @@ -7,7 +7,7 @@ import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listener import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.graphics.animation.AnimationTicker -import com.lambda.graphics.buffer.fbo.FrameBuffer +import com.lambda.graphics.buffer.FrameBuffer import com.lambda.graphics.gl.GlStateUtils.setupGL import com.lambda.graphics.gl.Matrices import com.lambda.graphics.gl.Matrices.resetMatrices diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/fbo/FrameBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt similarity index 99% rename from common/src/main/kotlin/com/lambda/graphics/buffer/fbo/FrameBuffer.kt rename to common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt index 25b3bd3bc..db164955a 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/fbo/FrameBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt @@ -1,4 +1,5 @@ -package com.lambda.graphics.buffer.fbo +package com.lambda.graphics.buffer + import com.lambda.Lambda.mc import com.lambda.graphics.RenderMain import com.lambda.graphics.buffer.vao.VAO diff --git a/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt b/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt index 6170d4cfb..d2643785f 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt @@ -2,7 +2,7 @@ package com.lambda.gui.impl import com.lambda.Lambda.mc import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.graphics.buffer.fbo.FrameBuffer +import com.lambda.graphics.buffer.FrameBuffer import com.lambda.graphics.shader.Shader import com.lambda.gui.AbstractGuiConfigurable import com.lambda.gui.GuiConfigurable From 895a069cc203a038c01fafb76cca1cf96a6fe7a5 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 23 Aug 2024 22:13:31 -0400 Subject: [PATCH 015/114] removed loader phase time measure --- common/src/main/kotlin/com/lambda/core/Loader.kt | 9 +-------- .../graphics/renderer/gui/font/glyph/EmojiGlyphs.kt | 5 ++--- .../graphics/renderer/gui/font/glyph/FontGlyphs.kt | 5 ++--- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/core/Loader.kt b/common/src/main/kotlin/com/lambda/core/Loader.kt index 271143d2c..f2bb399f4 100644 --- a/common/src/main/kotlin/com/lambda/core/Loader.kt +++ b/common/src/main/kotlin/com/lambda/core/Loader.kt @@ -45,14 +45,7 @@ object Loader { LOG.info("Initializing ${Lambda.MOD_NAME} ${Lambda.VERSION}") val initTime = measureTimeMillis { - loadables.forEach { loadable -> - val info: String - val phaseTime = measureTimeMillis { - info = loadable.load() - } - - LOG.info("$info in ${phaseTime}ms") - } + loadables.forEach { LOG.info(it.load()) } } LOG.info("${Lambda.MOD_NAME} ${Lambda.VERSION} was successfully initialized (${initTime}ms)") diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt index 0405a0dcf..aa07d686c 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt @@ -16,7 +16,6 @@ import javax.imageio.ImageIO import kotlin.math.ceil import kotlin.math.log2 import kotlin.math.sqrt -import kotlin.system.measureTimeMillis import kotlin.time.Duration.Companion.days class EmojiGlyphs(zipUrl: String) { @@ -28,8 +27,8 @@ class EmojiGlyphs(zipUrl: String) { init { runCatching { - val time = measureTimeMillis { downloadAndProcessZip(zipUrl) } - LOG.info("Loaded ${emojiMap.size} emojis in $time ms") + downloadAndProcessZip(zipUrl) + LOG.info("Loaded ${emojiMap.size} emojis") }.onFailure { LOG.error("Failed to load emojis: ${it.message}", it) fontTexture = MipmapTexture(BufferedImage(1024, 1024, BufferedImage.TYPE_INT_ARGB)) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt index 3c492b26b..3461ec2af 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt @@ -11,7 +11,6 @@ import java.awt.Font import java.awt.Graphics2D import java.awt.image.BufferedImage import kotlin.math.max -import kotlin.system.measureTimeMillis class FontGlyphs( private val font: Font @@ -23,8 +22,8 @@ class FontGlyphs( init { runCatching { - val time = measureTimeMillis { processGlyphs() } - LOG.info("Font ${font.fontName} loaded with ${charMap.size} characters in $time ms") + processGlyphs() + LOG.info("Font ${font.fontName} loaded with ${charMap.size} characters") }.onFailure { LOG.error("Failed to load font glyphs: ${it.message}", it) fontTexture = MipmapTexture(BufferedImage(1024, 1024, BufferedImage.TYPE_INT_ARGB)) From 0644c69868193a7027efc22396722fabddf7d9f6 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 24 Aug 2024 08:46:59 -0400 Subject: [PATCH 016/114] Update build.gradle.kts --- fabric/build.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index e34c7b297..8844cc10f 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -1,5 +1,3 @@ -import org.gradle.internal.jvm.Jvm - val modVersion: String by project val minecraftVersion: String by project val fabricLoaderVersion: String by project From de3bb6e5651ca6b34603a508d3e7fd8c2e585ae4 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 25 Aug 2024 05:02:50 +0200 Subject: [PATCH 017/114] TickEvent KDocs --- .../lambda/mixin/MinecraftClientMixin.java | 4 +- .../com/lambda/event/events/TickEvent.kt | 86 ++++++++++++++----- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java b/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java index 6c92d6558..809a077c4 100644 --- a/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java +++ b/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java @@ -37,12 +37,12 @@ void onTickPost(CallbackInfo ci) { @Inject(method = "render", at = @At("HEAD")) void onLoopTickPre(CallbackInfo ci) { - EventFlow.post(new TickEvent.GameLoop.Pre()); + EventFlow.post(new TickEvent.Render.Pre()); } @Inject(method = "render", at = @At("RETURN")) void onLoopTickPost(CallbackInfo ci) { - EventFlow.post(new TickEvent.GameLoop.Post()); + EventFlow.post(new TickEvent.Render.Post()); } @Inject(at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;info(Ljava/lang/String;)V", shift = At.Shift.AFTER, remap = false), method = "stop") diff --git a/common/src/main/kotlin/com/lambda/event/events/TickEvent.kt b/common/src/main/kotlin/com/lambda/event/events/TickEvent.kt index d47597fdb..003f956d9 100644 --- a/common/src/main/kotlin/com/lambda/event/events/TickEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/TickEvent.kt @@ -1,58 +1,98 @@ package com.lambda.event.events import com.lambda.event.Event -import com.lambda.event.EventFlow -import com.lambda.event.events.TickEvent.Post -import com.lambda.event.events.TickEvent.Pre -/** - * An abstract class representing a [TickEvent] in the [EventFlow]. - * - * A [TickEvent] is a type of [Event] that is triggered at each tick of the game loop. - * It has two subclasses: [Pre] and [Post], which are triggered before and after the tick, respectively. - * - * The [TickEvent] class is designed to be extended by any class that needs to react to ticks. - * - * @see Pre - * @see Post - */ abstract class TickEvent : Event { /** - * A class representing a [TickEvent] that is triggered before each tick of the tick loop. + * Triggered before each iteration of the game loop. + * + * Phases: + * + * 1. **Pre-Tick**: Increments uptime, steps world tick manager, decrement item use cooldown. + * 2. **GUI Update**: Processes delayed messages, updates HUD. + * 3. **Game Mode Update**: Updates targeted entity, ticks tutorial, and interaction managers. + * 4. **Texture Update**: Ticks texture manager. + * 5. **Screen Handling**: Manages screen logic, ticks current screen. + * 6. **Debug HUD Update**: Resets debug HUD chunk. + * 7. **Input Handling**: Handles input events, decrements attack cooldown. + * 8. **World Update**: Ticks game and world renderers, world entities. + * 9. **Music and Sound Update**: Ticks music tracker and sound manager. + * 10. **Tutorial and Social Interactions**: Handles tutorial and social interactions, ticks world. + * 11. **Pending Connection**: Ticks integrated server connection. + * 12. **Keyboard Handling**: Polls for debug crash key presses. + * + * @see net.minecraft.client.MinecraftClient.tick */ class Pre : TickEvent() /** - * A class representing a [TickEvent] that is triggered after each tick of the tick loop. + * Triggered after each iteration of the game loop. + * Targeted at 20 ticks per second. + * + * Phases: + * + * 1. **Pre-Tick**: Increments uptime, steps world tick manager, decrement item use cooldown. + * 2. **GUI Update**: Processes delayed messages, updates HUD. + * 3. **Game Mode Update**: Updates targeted entity, ticks tutorial, and interaction managers. + * 4. **Texture Update**: Ticks texture manager. + * 5. **Screen Handling**: Manages screen logic, ticks current screen. + * 6. **Debug HUD Update**: Resets debug HUD chunk. + * 7. **Input Handling**: Handles input events, decrements attack cooldown. + * 8. **World Update**: Ticks game and world renderers, world entities (such as [TickEvent.Player]). + * 9. **Music and Sound Update**: Ticks music tracker and sound manager. + * 10. **Tutorial and Social Interactions**: Handles tutorial and social interactions, ticks world. + * 11. **Pending Connection**: Ticks integrated server connection. + * 12. **Keyboard Handling**: Polls for debug crash key presses. + * + * @see net.minecraft.client.MinecraftClient.tick */ class Post : TickEvent() /** - * A class representing a [TickEvent] that is triggered on each tick of the game loop. + * Triggered before ([Pre]) and after ([Post]) each render tick. + * + * Phases: + * + * 1. **Pre-Render**: Prepares the window for rendering, checks for window close, handles resource reloads. + * 2. **Task Execution**: Executes pending render tasks. + * 3. **Client Tick**: Ticks the client ([TickEvent.Pre] and [TickEvent.Post]) until tick target was met. + * 4. **Render**: Performs the actual rendering of the game. + * 5. **Post-Render**: Finalizes the rendering process, updates the window. + * + * @see net.minecraft.client.MinecraftClient.render */ - abstract class GameLoop : TickEvent() { + abstract class Render : TickEvent() { /** - * A class representing a [TickEvent.Player] that is triggered before each tick of the game loop. + * Triggered before each render tick ([TickEvent.Render]) of the game loop. */ class Pre : TickEvent() /** - * A class representing a [TickEvent.Player] that is triggered after each tick of the game loop. + * Triggered after each render tick ([TickEvent.Render]) of the game loop. */ class Post : TickEvent() } /** - * A class representing a [TickEvent] that is triggered when the player gets ticked. + * Triggered before ([Pre]) and after ([Post]) each player tick that is run during the game loop [TickEvent.Pre]. + * + * Phases: + * + * 1. **Pre-Tick**: Prepares player state before the tick. + * 2. **Movement**: Handles player movement and input. + * 3. **Action**: Processes player actions like swinging hand. + * 4. **Post-Tick**: Finalizes player state after the tick. + * + * @see net.minecraft.client.network.ClientPlayerEntity.tick */ abstract class Player : TickEvent() { /** - * A class representing a [TickEvent.Player] that is triggered before each player tick. + * Triggered before each player tick ([TickEvent.Player]). */ class Pre : Player() /** - * A class representing a [TickEvent.Player] that is triggered after each player tick. + * Triggered after each player tick ([TickEvent.Player]). */ class Post : Player() } From b40b1aef9d0f1aa12097d3478f5c8e4951a012fc Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 25 Aug 2024 05:16:01 +0200 Subject: [PATCH 018/114] Smol refac --- .../kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt index 0dc03a38e..17e815600 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/pbo/PixelBuffer.kt @@ -26,7 +26,8 @@ class PixelBuffer( private var uploadIdx = 0 // Used to upload data to the PBO private val queryId = glGenQueries() // Used to measure the time taken to upload data to the PBO - private val uploadTime get() = IntArray(1).also { glGetQueryObjectiv(queryId, GL_QUERY_RESULT, it) }[0] + private val uploadTime get() = + IntArray(1).also { glGetQueryObjectiv(queryId, GL_QUERY_RESULT, it) }.first() private var transferRate = 0L // The transfer rate in bytes per second private val pboSupported = GL.getCapabilities().OpenGL30 || GL.getCapabilities().GL_ARB_pixel_buffer_object @@ -60,8 +61,7 @@ class PixelBuffer( // Perform the actual data transfer to the GPU glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0) - } - else { + } else { // Perform the actual data transfer to the GPU glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, buffer) } From dcf244df4eafbf389aafb8917d883a5c1b44dce6 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 25 Aug 2024 05:28:26 +0200 Subject: [PATCH 019/114] Better logging and remove texture settings --- .../com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt | 2 +- .../lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt | 3 ++- .../kotlin/com/lambda/interaction/PlayerPacketManager.kt | 1 - .../com/lambda/module/modules/client/RenderSettings.kt | 5 ----- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt index 8d8f85f48..cfe21c3d5 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt @@ -17,7 +17,7 @@ enum class LambdaEmoji(private val zipUrl: String) { object Loader : Loadable { override fun load(): String { entries.forEach(LambdaEmoji::loadGlyphs) - return "Loaded ${entries.size} emoji sets" + return "Loaded ${entries.size} emoji sets with a total of ${entries.sumOf { it.glyphs.count }} emojis" } } } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt index aa07d686c..6e2dd4e05 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt @@ -25,10 +25,11 @@ class EmojiGlyphs(zipUrl: String) { private lateinit var image: BufferedImage private lateinit var graphics: Graphics2D + val count get() = emojiMap.size + init { runCatching { downloadAndProcessZip(zipUrl) - LOG.info("Loaded ${emojiMap.size} emojis") }.onFailure { LOG.error("Failed to load emojis: ${it.message}", it) fontTexture = MipmapTexture(BufferedImage(1024, 1024, BufferedImage.TYPE_INT_ARGB)) diff --git a/common/src/main/kotlin/com/lambda/interaction/PlayerPacketManager.kt b/common/src/main/kotlin/com/lambda/interaction/PlayerPacketManager.kt index 491ca07bb..1a6e95e6d 100644 --- a/common/src/main/kotlin/com/lambda/interaction/PlayerPacketManager.kt +++ b/common/src/main/kotlin/com/lambda/interaction/PlayerPacketManager.kt @@ -5,7 +5,6 @@ import com.lambda.core.Loadable import com.lambda.event.EventFlow.post import com.lambda.event.EventFlow.postChecked import com.lambda.event.events.PlayerPacketEvent -import com.lambda.interaction.rotation.Rotation.Companion.fixSensitivity import com.lambda.threading.runSafe import com.lambda.util.collections.LimitedOrderedSet import com.lambda.util.math.VecUtils.approximate diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt index ec29fad21..099dbdafb 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt @@ -18,10 +18,6 @@ object RenderSettings : Module( val baselineOffset by setting("Vertical Offset", 0.0, -10.0..10.0, 0.5) { page == Page.Font } private val lodBiasSetting by setting("Smoothing", 0.0, -10.0..10.0, 0.5) { page == Page.Font } - // Texture - val textureCompression by setting("Compression", 1, 1..9, 1, description = "Texture compression level, higher is slower") { page == Page.TEXTURE } - val threadedCompression by setting("Threaded Compression", false, description = "Use multiple threads for texture compression") { page == Page.TEXTURE } - // ESP val uploadsPerTick by setting("Uploads", 16, 1..256, 1, unit = " chunk/tick") { page == Page.ESP } val rebuildsPerTick by setting("Rebuilds", 64, 1..256, 1, unit = " chunk/tick") { page == Page.ESP } @@ -32,7 +28,6 @@ object RenderSettings : Module( private enum class Page { Font, - TEXTURE, ESP, } } From c97a29c0c25f6c3cbe8cde2b4710a57802c32273 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 25 Aug 2024 05:39:07 +0200 Subject: [PATCH 020/114] Introduce constants for texture options --- .../com/lambda/graphics/texture/TextureUtils.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt index 5ed6289cb..d3a6f94e8 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt @@ -1,6 +1,5 @@ package com.lambda.graphics.texture -import com.lambda.module.modules.client.RenderSettings import com.mojang.blaze3d.systems.RenderSystem import com.pngencoder.PngEncoder import net.minecraft.client.texture.NativeImage @@ -8,15 +7,17 @@ import org.lwjgl.BufferUtils import org.lwjgl.opengl.GL45C.* import java.awt.* import java.awt.image.BufferedImage -import java.io.ByteArrayOutputStream import kotlin.math.roundToInt import kotlin.math.sqrt object TextureUtils { + private const val COMPRESSION_LEVEL = 1 + private const val THREADED_COMPRESSION = false + private val metricCache = mutableMapOf() private val encoderPreset = PngEncoder() - .withCompressionLevel(RenderSettings.textureCompression) - .withMultiThreadedCompressionEnabled(RenderSettings.threadedCompression) + .withCompressionLevel(COMPRESSION_LEVEL) + .withMultiThreadedCompressionEnabled(THREADED_COMPRESSION) fun bindTexture(id: Int, slot: Int = 0) { RenderSystem.activeTexture(GL_TEXTURE0 + slot) @@ -77,7 +78,7 @@ object TextureUtils { if (!font.canDisplay(codePoint)) return null val fontMetrics = metricCache.getOrPut(font) { - val image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB) + val image = BufferedImage(COMPRESSION_LEVEL, COMPRESSION_LEVEL, BufferedImage.TYPE_INT_ARGB) val graphics2D = image.createGraphics() graphics2D.font = font From 8e9c89c45c6d0aecf8ca723289b465f5b02078e8 Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Sat, 14 Sep 2024 22:03:24 +0300 Subject: [PATCH 021/114] Layouts, dsl builders --- .../lambda/module/modules/client/NewCGui.kt | 60 ++++++ .../kotlin/com/lambda/newgui/DslBuilders.kt | 44 ++++ .../kotlin/com/lambda/newgui/LambdaScreen.kt | 111 ++++++++++ .../main/kotlin/com/lambda/newgui/Layout.kt | 202 ++++++++++++++++++ 4 files changed, 417 insertions(+) create mode 100644 common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt create mode 100644 common/src/main/kotlin/com/lambda/newgui/DslBuilders.kt create mode 100644 common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt create mode 100644 common/src/main/kotlin/com/lambda/newgui/Layout.kt diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt new file mode 100644 index 000000000..25c2ea0e0 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt @@ -0,0 +1,60 @@ +package com.lambda.module.modules.client + +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.newgui.LambdaScreen.Companion.toScreen +import com.lambda.newgui.gui +import com.lambda.newgui.layout +import com.lambda.util.math.ColorUtils.setAlpha +import com.lambda.util.math.Rect +import com.lambda.util.math.Vec2d +import java.awt.Color + +object NewCGui : Module( + name = "NewCGui", + description = "ggs", + defaultTags = setOf(ModuleTag.CLIENT) +) { + private val clickGuiLayout = + gui { + layout { + rect { Rect(Vec2d.ONE * 10.0, Vec2d.ONE * 200.0) } + + onRender { + filled.build(rect, color = Color.WHITE.setAlpha(0.5)) + } + + layout(true) { + rect { Rect(Vec2d.ONE * 10.0, Vec2d.ONE * 200.0) } + + onRender { + filled.build(rect, color = Color.BLACK) + } + + layout(true) { + rect { Rect(Vec2d.ONE * 10.0, Vec2d.ONE * 200.0) } + + onRender { + filled.build(rect, color = Color.WHITE.setAlpha(0.5)) + } + + layout(true) { + rect { Rect(Vec2d.ONE * 10.0, Vec2d.ONE * 200.0) } + + onRender { + filled.build(rect, color = Color.BLACK) + } + } + } + } + } + } + + val CLICK_GUI = clickGuiLayout.toScreen("New Click Gui") + + init { + onEnable { + CLICK_GUI.show() + } + } +} diff --git a/common/src/main/kotlin/com/lambda/newgui/DslBuilders.kt b/common/src/main/kotlin/com/lambda/newgui/DslBuilders.kt new file mode 100644 index 000000000..7c4d29263 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/DslBuilders.kt @@ -0,0 +1,44 @@ +package com.lambda.newgui + +import com.lambda.graphics.RenderMain +import com.lambda.util.math.Rect +import com.lambda.util.math.Vec2d + +@DslMarker +annotation class UIBuilder + +/** + * Creates a "gui" represented by layout component + */ +@UIBuilder +fun gui(block: Layout.() -> Unit) = + Layout(owner = null, useBatching = false, batchChildren = false).apply { + var screenSize = Vec2d.ONE * 10000.0 + + rect { + Rect(Vec2d.ZERO, screenSize) + } + + onRender { + screenSize = RenderMain.screenSize + } + }.apply(block) + + +/** + * Creates empty layout + * + * @param useBatching + * Increases performance by using parent's renderer instead of creating a new one. + * + * @param batchChildren + * Whether allow children to use the renderer of this layout + */ +@UIBuilder +fun Layout.layout( + useBatching: Boolean = false, + batchChildren: Boolean = false, + block: Layout.() -> Unit, +) = Layout(this, useBatching, batchChildren) + .apply(children::add).apply(block) + diff --git a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt new file mode 100644 index 000000000..4effd468e --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt @@ -0,0 +1,111 @@ +package com.lambda.newgui + +import com.lambda.Lambda.mc +import com.lambda.event.Muteable +import com.lambda.event.events.RenderEvent +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.gui.api.GuiEvent +import com.lambda.util.KeyCode +import com.lambda.util.Mouse +import com.lambda.util.Nameable +import com.lambda.util.math.Vec2d +import com.mojang.blaze3d.systems.RenderSystem.recordRenderCall +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +import net.minecraft.text.Text + +class LambdaScreen( + override val name: String, + val layout: Layout +) : Screen(Text.of(name)), Nameable, Muteable { + override val isMuted: Boolean get() = !isOpen + + private var screenSize = Vec2d.ZERO + val isOpen get() = mc.currentScreen == this + + init { + listener { event -> + screenSize = event.screenSize + layout.onEvent(GuiEvent.Render()) + } + + listener { + layout.onEvent(GuiEvent.Tick()) + } + } + + fun show() { + mc.currentScreen?.close() + + recordRenderCall { + mc.setScreen(this) + } + } + + override fun onDisplayed() { + layout.onEvent(GuiEvent.Show()) + } + + override fun removed() { + layout.onEvent(GuiEvent.Hide()) + } + + override fun shouldPause() = false + + override fun render(context: DrawContext?, mouseX: Int, mouseY: Int, delta: Float) { + // Let's remove background tint + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + val translated = KeyCode.virtualMapUS(keyCode, scanCode) + layout.onEvent(GuiEvent.KeyPress(translated)) + + if (keyCode == KeyCode.ESCAPE.keyCode) { + close() + } + + return true + } + + override fun charTyped(chr: Char, modifiers: Int): Boolean { + layout.onEvent(GuiEvent.CharTyped(chr)) + return true + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + layout.onEvent(GuiEvent.MouseClick(Mouse.Button(button), Mouse.Action.Click, rescaleMouse(mouseX, mouseY))) + return true + } + + override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + layout.onEvent(GuiEvent.MouseClick(Mouse.Button(button), Mouse.Action.Release, rescaleMouse(mouseX, mouseY))) + return true + } + + override fun mouseMoved(mouseX: Double, mouseY: Double) { + layout.onEvent(GuiEvent.MouseMove(rescaleMouse(mouseX, mouseY))) + } + + override fun mouseScrolled( + mouseX: Double, + mouseY: Double, + horizontalAmount: Double, + verticalAmount: Double + ): Boolean { + layout.onEvent(GuiEvent.MouseScroll(rescaleMouse(mouseX, mouseY), verticalAmount)) + return true + } + + private fun rescaleMouse(mouseX: Double, mouseY: Double): Vec2d { + val mcMouse = Vec2d(mouseX, mouseY) + val mcWindow = Vec2d(mc.window.scaledWidth, mc.window.scaledHeight) + + val uv = mcMouse / mcWindow + return uv * screenSize + } + + companion object { + fun Layout.toScreen(name: String) = LambdaScreen(name, this) + } +} diff --git a/common/src/main/kotlin/com/lambda/newgui/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/Layout.kt new file mode 100644 index 000000000..91871284d --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/Layout.kt @@ -0,0 +1,202 @@ +package com.lambda.newgui + +import com.lambda.graphics.animation.AnimationTicker +import com.lambda.graphics.gl.Scissor.scissor +import com.lambda.gui.api.GuiEvent +import com.lambda.gui.api.RenderLayer +import com.lambda.util.KeyCode +import com.lambda.util.Mouse +import com.lambda.util.math.Rect +import com.lambda.util.math.Vec2d + +/** + * Represents a component for creating complex ui structures + */ +open class Layout( + private val owner: Layout?, + useBatching: Boolean, + private val batchChildren: Boolean +) { + // Rectangle of the component + open val rect: Rect get() = rectUpdate() + (owner?.rect?.leftTop ?: Vec2d.ZERO) + + // Structure + val children = mutableListOf() + private var selectedChild: Layout? = null + + // Inputs + private var lastMouse = Vec2d.ZERO + private val isHovered: Boolean get() = lastMouse in rect && (owner?.isHovered ?: true) + + // Graphics + val animation = AnimationTicker() + val renderer: RenderLayer + private var owningRenderer = false + + init { + val parentRenderer = owner?.renderer + val parentAcceptsBatching = owner?.batchChildren ?: false + + renderer = if (!useBatching || !parentAcceptsBatching || parentRenderer == null) { + owningRenderer = true + RenderLayer() + } else { + parentRenderer + } + } + + // Actions + private var showAction = {} + private var hideAction = {} + private var tickAction = {} + private var renderAction: RenderLayer.() -> Unit = {} + private var keyPressAction: (key: KeyCode) -> Unit = {} + private var charTypedAction: (char: Char) -> Unit = {} + private var mouseClickAction: (button: Mouse.Button, action: Mouse.Action) -> Unit = { _, _ -> } + private var mouseMoveAction: (mouse: Vec2d) -> Unit = {} + private var mouseScrollAction: (delta: Double) -> Unit = {} + private var rectUpdate = { Rect.ZERO } + + /** + * Sets the action to be performed when the element gets shown. + * + * @param action The action to be performed. + */ + fun onShow(action: () -> Unit) { + showAction = action + } + + /** + * Sets the action to be performed when the element gets hidden. + * + * @param action The action to be performed. + */ + fun onHide(action: () -> Unit) { + hideAction = action + } + + /** + * Sets the action to be performed on each tick. + * + * @param action The action to be performed. + */ + fun onTick(action: () -> Unit) { + tickAction = action + } + + /** + * Sets the action to be performed on each frame. + * + * @param action The action to be performed. + */ + fun onRender(action: RenderLayer.() -> Unit) { + renderAction = action + } + + /** + * Sets the action to be performed when a key gets pressed. + * + * @param action The action to be performed. + */ + fun onKeyPress(action: (key: KeyCode) -> Unit) { + keyPressAction = action + } + + /** + * Sets the action to be performed when user types a char. + * + * @param action The action to be performed. + */ + fun onCharTyped(action: (char: Char) -> Unit) { + charTypedAction = action + } + + /** + * Sets the action to be performed when mouse button gets clicked. + * + * @param action The action to be performed. + */ + fun onMouseClick(action: (button: Mouse.Button, action: Mouse.Action) -> Unit) { + mouseClickAction = action + } + + /** + * Sets the action to be performed when mouse moves. + * + * @param action The action to be performed. + */ + fun onMouseMove(action: (mouse: Vec2d) -> Unit) { + mouseMoveAction = action + } + + /** + * Sets the action to be performed on mouse scroll. + * + * @param action The action to be performed. + */ + fun onMouseScroll(action: (delta: Double) -> Unit) { + mouseScrollAction = action + } + + /** + * Sets the rectangle of this component. + */ + fun rect(block: () -> Rect) { + rectUpdate = block + } + + fun onEvent(e: GuiEvent) { + // Select an element that's on foreground + selectedChild = if (lastMouse in rect) children.lastOrNull { + lastMouse in it.rect + } else null + + // Update children + children.forEach { child -> + if (e is GuiEvent.Render) return@forEach + + if (e is GuiEvent.MouseClick) { + val newAction = if (child.isHovered) e.action else Mouse.Action.Release + val newEvent = GuiEvent.MouseClick(e.button, newAction, e.mouse) + child.onEvent(newEvent) + return@forEach + } + + child.onEvent(e) + } + + when (e) { + is GuiEvent.Show -> { lastMouse = Vec2d.ONE * -1000.0; showAction() } + is GuiEvent.Hide -> { hideAction() } + is GuiEvent.Tick -> { animation.tick(); tickAction() } + is GuiEvent.KeyPress -> { keyPressAction(e.key) } + is GuiEvent.CharTyped -> { charTypedAction(e.char) } + is GuiEvent.MouseMove -> { lastMouse = e.mouse; mouseMoveAction(e.mouse) } + is GuiEvent.MouseScroll -> { lastMouse = e.mouse; mouseScrollAction(e.delta) } + is GuiEvent.MouseClick -> { + lastMouse = e.mouse + val action = if (selectedChild == null) e.action else Mouse.Action.Release + mouseClickAction(e.button, action) + } + is GuiEvent.Render -> { + val (pre, post) = children.partition { !it.owningRenderer } + + // Add drawables from this layout + renderAction(renderer) + + // Add children's drawables over + pre.forEach { it.onEvent(e) } + + scissor(rect) { + // Perform a drawcall + if (owningRenderer) { + renderer.render() + } + + // Draw children with custom renderers + post.forEach { it.onEvent(e) } + } + } + } + } +} \ No newline at end of file From 1cf9550171187827cea2ad308531827eb0d02a8c Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Sun, 15 Sep 2024 19:38:49 +0300 Subject: [PATCH 022/114] Simple window, text fields --- .../gui/CustomModuleWindowSerializer.kt | 6 +- .../serializer/gui/TagWindowSerializer.kt | 6 +- .../gui/api/component/core/DockingRect.kt | 14 +- .../kotlin/com/lambda/module/HudModule.kt | 2 + .../lambda/module/modules/client/NewCGui.kt | 47 +++--- .../kotlin/com/lambda/newgui/DslBuilders.kt | 44 ------ .../kotlin/com/lambda/newgui/LambdaScreen.kt | 25 +++ .../main/kotlin/com/lambda/newgui/Layout.kt | 147 +++++++++++------- .../com/lambda/newgui/component/Alignment.kt | 13 ++ .../lambda/newgui/component/core/TextField.kt | 60 +++++++ .../lambda/newgui/component/window/Window.kt | 98 ++++++++++++ 11 files changed, 318 insertions(+), 144 deletions(-) delete mode 100644 common/src/main/kotlin/com/lambda/newgui/DslBuilders.kt create mode 100644 common/src/main/kotlin/com/lambda/newgui/component/Alignment.kt create mode 100644 common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt create mode 100644 common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt diff --git a/common/src/main/kotlin/com/lambda/config/serializer/gui/CustomModuleWindowSerializer.kt b/common/src/main/kotlin/com/lambda/config/serializer/gui/CustomModuleWindowSerializer.kt index cf2c315f9..83ea40128 100644 --- a/common/src/main/kotlin/com/lambda/config/serializer/gui/CustomModuleWindowSerializer.kt +++ b/common/src/main/kotlin/com/lambda/config/serializer/gui/CustomModuleWindowSerializer.kt @@ -5,6 +5,8 @@ import com.lambda.gui.api.component.core.DockingRect import com.lambda.gui.impl.clickgui.LambdaClickGui import com.lambda.gui.impl.clickgui.windows.tag.CustomModuleWindow import com.lambda.module.ModuleRegistry +import com.lambda.newgui.component.HAlign +import com.lambda.newgui.component.VAlign import com.lambda.util.math.Vec2d import java.lang.reflect.Type @@ -56,8 +58,8 @@ object CustomModuleWindowSerializer : JsonSerializer, JsonDe it["position"].asJsonArray[0].asDouble, it["position"].asJsonArray[1].asDouble ) - dockingH = DockingRect.HAlign.entries[it["docking"].asJsonArray[0].asInt] - dockingV = DockingRect.VAlign.entries[it["docking"].asJsonArray[1].asInt] + dockingH = HAlign.entries[it["docking"].asJsonArray[0].asInt] + dockingV = VAlign.entries[it["docking"].asJsonArray[1].asInt] } } ?: throw JsonParseException("Invalid window data") } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/config/serializer/gui/TagWindowSerializer.kt b/common/src/main/kotlin/com/lambda/config/serializer/gui/TagWindowSerializer.kt index 107c81694..24b4892d8 100644 --- a/common/src/main/kotlin/com/lambda/config/serializer/gui/TagWindowSerializer.kt +++ b/common/src/main/kotlin/com/lambda/config/serializer/gui/TagWindowSerializer.kt @@ -6,6 +6,8 @@ import com.lambda.gui.impl.clickgui.LambdaClickGui import com.lambda.gui.impl.clickgui.windows.tag.TagWindow import com.lambda.gui.impl.hudgui.LambdaHudGui import com.lambda.module.tag.ModuleTag +import com.lambda.newgui.component.HAlign +import com.lambda.newgui.component.VAlign import com.lambda.util.math.Vec2d import java.lang.reflect.Type @@ -53,8 +55,8 @@ object TagWindowSerializer : JsonSerializer, JsonDeserializer Unit) = - Layout(owner = null, useBatching = false, batchChildren = false).apply { - var screenSize = Vec2d.ONE * 10000.0 - - rect { - Rect(Vec2d.ZERO, screenSize) - } - - onRender { - screenSize = RenderMain.screenSize - } - }.apply(block) - - -/** - * Creates empty layout - * - * @param useBatching - * Increases performance by using parent's renderer instead of creating a new one. - * - * @param batchChildren - * Whether allow children to use the renderer of this layout - */ -@UIBuilder -fun Layout.layout( - useBatching: Boolean = false, - batchChildren: Boolean = false, - block: Layout.() -> Unit, -) = Layout(this, useBatching, batchChildren) - .apply(children::add).apply(block) - diff --git a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt index 4effd468e..37e3a2d8e 100644 --- a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt +++ b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt @@ -5,16 +5,21 @@ import com.lambda.event.Muteable import com.lambda.event.events.RenderEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.graphics.RenderMain import com.lambda.gui.api.GuiEvent import com.lambda.util.KeyCode import com.lambda.util.Mouse import com.lambda.util.Nameable +import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d import com.mojang.blaze3d.systems.RenderSystem.recordRenderCall import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.screen.Screen import net.minecraft.text.Text +/** + * Represents a "tunnel" between the [Layout] system and minecraft's [Screen] + */ class LambdaScreen( override val name: String, val layout: Layout @@ -106,6 +111,26 @@ class LambdaScreen( } companion object { + /** + * Creates gui layout + */ + @UIBuilder + fun gui(block: Layout.() -> Unit) = + Layout(owner = null, useBatching = false, batchChildren = true).apply { + var screenSize = Vec2d.ONE * 10000.0 + + rect { + Rect(Vec2d.ZERO, screenSize) + } + + onRender { + screenSize = RenderMain.screenSize + } + }.apply(block) + + /** + * Converts this [Layout] to a minecraft-typed [Screen] represented by the [LambdaScreen] class + */ fun Layout.toScreen(name: String) = LambdaScreen(name, this) } } diff --git a/common/src/main/kotlin/com/lambda/newgui/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/Layout.kt index 91871284d..0da31c889 100644 --- a/common/src/main/kotlin/com/lambda/newgui/Layout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/Layout.kt @@ -10,7 +10,24 @@ import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d /** - * Represents a component for creating complex ui structures + * Represents a component for creating complex ui structures. + * + * @param useBatching Increases performance by using parent's renderer instead of creating a new one. + * + * @param batchChildren Whether allow children to use the renderer of this layout. + * + * Warning: use batching if you know what you're doing. + * Batched elements are always drawn first: + * ```kotlin + * // 1st + * layout(useBatching = true) {} + * + * // 3rd + * layout {} + * + * // 2nd + * layout(useBatching = true) {} + * ``` */ open class Layout( private val owner: Layout?, @@ -25,36 +42,37 @@ open class Layout( private var selectedChild: Layout? = null // Inputs - private var lastMouse = Vec2d.ZERO - private val isHovered: Boolean get() = lastMouse in rect && (owner?.isHovered ?: true) + protected var mousePosition = Vec2d.ZERO + private val isHovered: Boolean get() = mousePosition in rect && (owner?.isHovered ?: true) // Graphics val animation = AnimationTicker() - val renderer: RenderLayer - private var owningRenderer = false - - init { - val parentRenderer = owner?.renderer - val parentAcceptsBatching = owner?.batchChildren ?: false + val renderer: RenderLayer = run { + owner?.let { owner -> + if (!useBatching || !owner.batchChildren) { + return@let null + } - renderer = if (!useBatching || !parentAcceptsBatching || parentRenderer == null) { + owner.renderer + } ?: run { owningRenderer = true RenderLayer() - } else { - parentRenderer } } + private var owningRenderer = false + protected open val passInteractions = false + // Actions - private var showAction = {} - private var hideAction = {} - private var tickAction = {} - private var renderAction: RenderLayer.() -> Unit = {} - private var keyPressAction: (key: KeyCode) -> Unit = {} - private var charTypedAction: (char: Char) -> Unit = {} - private var mouseClickAction: (button: Mouse.Button, action: Mouse.Action) -> Unit = { _, _ -> } - private var mouseMoveAction: (mouse: Vec2d) -> Unit = {} - private var mouseScrollAction: (delta: Double) -> Unit = {} + private var showActions = mutableListOf<() -> Unit>() + private var hideActions = mutableListOf<() -> Unit>() + private var tickActions = mutableListOf<() -> Unit>() + private var renderActions = mutableListOf Unit>() + private var keyPressActions = mutableListOf<(key: KeyCode) -> Unit>() + private var charTypedActions = mutableListOf<(char: Char) -> Unit>() + private var mouseClickActions = mutableListOf<(button: Mouse.Button, action: Mouse.Action) -> Unit>() + private var mouseMoveActions = mutableListOf<(mouse: Vec2d) -> Unit>() + private var mouseScrollActions = mutableListOf<(delta: Double) -> Unit>() private var rectUpdate = { Rect.ZERO } /** @@ -63,7 +81,7 @@ open class Layout( * @param action The action to be performed. */ fun onShow(action: () -> Unit) { - showAction = action + showActions += action } /** @@ -72,7 +90,7 @@ open class Layout( * @param action The action to be performed. */ fun onHide(action: () -> Unit) { - hideAction = action + hideActions += action } /** @@ -81,7 +99,7 @@ open class Layout( * @param action The action to be performed. */ fun onTick(action: () -> Unit) { - tickAction = action + tickActions += action } /** @@ -90,7 +108,7 @@ open class Layout( * @param action The action to be performed. */ fun onRender(action: RenderLayer.() -> Unit) { - renderAction = action + renderActions += action } /** @@ -99,7 +117,7 @@ open class Layout( * @param action The action to be performed. */ fun onKeyPress(action: (key: KeyCode) -> Unit) { - keyPressAction = action + keyPressActions += action } /** @@ -108,7 +126,7 @@ open class Layout( * @param action The action to be performed. */ fun onCharTyped(action: (char: Char) -> Unit) { - charTypedAction = action + charTypedActions += action } /** @@ -117,7 +135,7 @@ open class Layout( * @param action The action to be performed. */ fun onMouseClick(action: (button: Mouse.Button, action: Mouse.Action) -> Unit) { - mouseClickAction = action + mouseClickActions += action } /** @@ -126,7 +144,7 @@ open class Layout( * @param action The action to be performed. */ fun onMouseMove(action: (mouse: Vec2d) -> Unit) { - mouseMoveAction = action + mouseMoveActions += action } /** @@ -135,7 +153,7 @@ open class Layout( * @param action The action to be performed. */ fun onMouseScroll(action: (delta: Double) -> Unit) { - mouseScrollAction = action + mouseScrollActions += action } /** @@ -147,8 +165,8 @@ open class Layout( fun onEvent(e: GuiEvent) { // Select an element that's on foreground - selectedChild = if (lastMouse in rect) children.lastOrNull { - lastMouse in it.rect + selectedChild = if (mousePosition in rect) children.lastOrNull { + !it.passInteractions && mousePosition in it.rect } else null // Update children @@ -156,7 +174,7 @@ open class Layout( if (e is GuiEvent.Render) return@forEach if (e is GuiEvent.MouseClick) { - val newAction = if (child.isHovered) e.action else Mouse.Action.Release + val newAction = if (child == selectedChild || (child.passInteractions)) e.action else Mouse.Action.Release val newEvent = GuiEvent.MouseClick(e.button, newAction, e.mouse) child.onEvent(newEvent) return@forEach @@ -166,37 +184,52 @@ open class Layout( } when (e) { - is GuiEvent.Show -> { lastMouse = Vec2d.ONE * -1000.0; showAction() } - is GuiEvent.Hide -> { hideAction() } - is GuiEvent.Tick -> { animation.tick(); tickAction() } - is GuiEvent.KeyPress -> { keyPressAction(e.key) } - is GuiEvent.CharTyped -> { charTypedAction(e.char) } - is GuiEvent.MouseMove -> { lastMouse = e.mouse; mouseMoveAction(e.mouse) } - is GuiEvent.MouseScroll -> { lastMouse = e.mouse; mouseScrollAction(e.delta) } + is GuiEvent.Show -> { mousePosition = Vec2d.ONE * -1000.0; showActions.forEach { it() } } + is GuiEvent.Hide -> { hideActions.forEach { it() } } + is GuiEvent.Tick -> { animation.tick(); tickActions.forEach { it() } } + is GuiEvent.KeyPress -> { keyPressActions.forEach { it(e.key) } } + is GuiEvent.CharTyped -> { charTypedActions.forEach { it((e.char)) } } + is GuiEvent.MouseMove -> { mousePosition = e.mouse; mouseMoveActions.forEach { it(e.mouse) } } + is GuiEvent.MouseScroll -> { mousePosition = e.mouse; mouseScrollActions.forEach { it(e.delta) } } is GuiEvent.MouseClick -> { - lastMouse = e.mouse - val action = if (selectedChild == null) e.action else Mouse.Action.Release - mouseClickAction(e.button, action) + mousePosition = e.mouse + val action = if (isHovered) e.action else Mouse.Action.Release + mouseClickActions.forEach { it(e.button, action) } } - is GuiEvent.Render -> { + is GuiEvent.Render -> scissor(rect) { val (pre, post) = children.partition { !it.owningRenderer } - // Add drawables from this layout - renderAction(renderer) - - // Add children's drawables over pre.forEach { it.onEvent(e) } + renderActions.forEach { it(renderer) } - scissor(rect) { - // Perform a drawcall - if (owningRenderer) { - renderer.render() - } - - // Draw children with custom renderers - post.forEach { it.onEvent(e) } + if (owningRenderer) { + scissor(rect, renderer::render) } + + post.forEach { it.onEvent(e) } } } } -} \ No newline at end of file + + companion object { + /** + * Creates an empty [Layout] + * + * @param useBatching Increases performance by using parent's renderer instead of creating a new one. + * + * @param batchChildren Whether allow children to use the renderer of this layout + * + * Check [Layout] description for more info about batching + */ + @UIBuilder + fun Layout.layout( + useBatching: Boolean = false, + batchChildren: Boolean = false, + block: Layout.() -> Unit, + ) = Layout(this, useBatching, batchChildren) + .apply(children::add).apply(block) + } +} + +@DslMarker +annotation class UIBuilder \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/Alignment.kt b/common/src/main/kotlin/com/lambda/newgui/component/Alignment.kt new file mode 100644 index 000000000..662df2150 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/component/Alignment.kt @@ -0,0 +1,13 @@ +package com.lambda.newgui.component + +enum class HAlign(val multiplier: Double, val offset: Double) { + LEFT(0.0, -1.0), + CENTER(0.5, 0.0), + RIGHT(1.0, 1.0) +} + +enum class VAlign(val multiplier: Double, val offset: Double) { + TOP(0.0, -1.0), + CENTER(0.5, 0.0), + BOTTOM(1.0, 1.0) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt new file mode 100644 index 000000000..8d3e36c78 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt @@ -0,0 +1,60 @@ +package com.lambda.newgui.component.core + +import com.lambda.newgui.Layout +import com.lambda.newgui.UIBuilder +import com.lambda.newgui.component.HAlign +import com.lambda.util.math.MathUtils.lerp +import com.lambda.util.math.Rect +import com.lambda.util.math.Vec2d +import java.awt.Color + +class TextField( + owner: Layout, + initialText: String, + initialColor: Color = Color.WHITE, + initialScale: Double = 1.0, + initialShadow: Boolean = true, + initialAlignment: HAlign = HAlign.LEFT, + initialOffset: Double = 0.0, +) : Layout(owner, true, true) { + var text = initialText + var color = initialColor + var scale = initialScale + var shadow = initialShadow + + var alignment = initialAlignment + var offset = initialOffset + + // Let user interact through the text + override val passInteractions = true + + init { + rect { + // Completely fill parent component by default + Rect(Vec2d.ZERO, owner.rect.size) + } + + onRender { + val x = lerp( + rect.left, + rect.right - font.getWidth(text, scale), + alignment.multiplier + ) - offset * alignment.offset + + font.build(text, Vec2d(x, rect.center.y), color, scale, shadow) + } + } + + companion object { + @UIBuilder + fun Layout.textField( + text: String, + color: Color = Color.WHITE, + scale: Double = 1.0, + shadow: Boolean = true, + alignment: HAlign = HAlign.LEFT, + offset: Double = 0.0, + block: TextField.() -> Unit = {} + ) = TextField(this, text, color, scale, shadow, alignment, offset).apply(children::add).apply(block) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt new file mode 100644 index 000000000..61f5bca9d --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt @@ -0,0 +1,98 @@ +package com.lambda.newgui.component.window + +import com.lambda.newgui.Layout +import com.lambda.newgui.UIBuilder +import com.lambda.newgui.component.core.TextField.Companion.textField +import com.lambda.newgui.component.window.Window.ContentSpace.Companion.contentSpace +import com.lambda.newgui.component.window.Window.TitleBar.Companion.titleBar +import com.lambda.util.Mouse +import com.lambda.util.math.ColorUtils.setAlpha +import com.lambda.util.math.Rect +import com.lambda.util.math.Vec2d +import java.awt.Color + +class Window( + owner: Layout, + initialPosition: Vec2d, + initialSize: Vec2d, + initialTitle: String +) : Layout(owner, false, true) { + var position = initialPosition + var size = initialSize + + val titleBar = titleBar(initialTitle) + val content = contentSpace { + Rect(titleBar.rect.leftBottom, rect.rightBottom) - position + } + + init { + rect { + Rect.basedOn(position, size) + } + + onRender { + filled.build(rect, 2.0, Color.BLACK.setAlpha(0.2)) + } + } + + class TitleBar( + owner: Window, + initialTitle: String + ) : Layout(owner, true, true) { + val textField = textField(initialTitle) + + private var dragOffset: Vec2d? = null + + init { + rect { + Rect(Vec2d.ZERO, Vec2d(owner.rect.size.x, renderer.font.getHeight() * 1.25)) + } + + onShow { + dragOffset = null + } + + onMouseClick { button: Mouse.Button, action: Mouse.Action -> + dragOffset = if (button == Mouse.Button.Left && action == Mouse.Action.Click) { + mousePosition - owner.position + } else null + } + + onMouseMove { mouse -> + dragOffset?.let { drag -> + owner.position = mouse - drag + } + } + } + + companion object { + @UIBuilder + fun Window.titleBar( + text: String, + ) = TitleBar(this, text).apply(children::add) + } + } + + open class ContentSpace(owner: Layout, rectBlock: () -> Rect) : Layout(owner, false, true) { + init { + rect(rectBlock) + } + + companion object { + @UIBuilder + fun Layout.contentSpace( + rect: () -> Rect, + ) = ContentSpace(this, rect).apply(children::add) + } + } + + companion object { + @UIBuilder + fun Layout.window( + position: Vec2d, + size: Vec2d = Vec2d(100.0, 300.0), + title: String = "Untitled", + block: Window.() -> Unit + ) = Window(this, position, size, title).apply(children::add).apply(block) + } +} \ No newline at end of file From fc91a2dcfda8ec40d07dd2d3e86f688752dc2b72 Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Sun, 15 Sep 2024 19:52:34 +0300 Subject: [PATCH 023/114] Interaction passthrough changes --- common/src/main/kotlin/com/lambda/newgui/Layout.kt | 10 ++++++---- .../com/lambda/newgui/component/core/TextField.kt | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/newgui/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/Layout.kt index 0da31c889..35572eb6b 100644 --- a/common/src/main/kotlin/com/lambda/newgui/Layout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/Layout.kt @@ -61,7 +61,7 @@ open class Layout( } private var owningRenderer = false - protected open val passInteractions = false + protected open val interactionPassthrough = false // Actions private var showActions = mutableListOf<() -> Unit>() @@ -165,8 +165,8 @@ open class Layout( fun onEvent(e: GuiEvent) { // Select an element that's on foreground - selectedChild = if (mousePosition in rect) children.lastOrNull { - !it.passInteractions && mousePosition in it.rect + selectedChild = if (isHovered) children.lastOrNull { + !it.interactionPassthrough && mousePosition in it.rect } else null // Update children @@ -174,7 +174,9 @@ open class Layout( if (e is GuiEvent.Render) return@forEach if (e is GuiEvent.MouseClick) { - val newAction = if (child == selectedChild || (child.passInteractions)) e.action else Mouse.Action.Release + val hovered = child == selectedChild || (child.isHovered && child.interactionPassthrough) + val newAction = if (hovered) e.action else Mouse.Action.Release + val newEvent = GuiEvent.MouseClick(e.button, newAction, e.mouse) child.onEvent(newEvent) return@forEach diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt index 8d3e36c78..f6c84ed0b 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt @@ -26,7 +26,7 @@ class TextField( var offset = initialOffset // Let user interact through the text - override val passInteractions = true + override val interactionPassthrough = true init { rect { From 52483d39be7713762565d3d21ff58abb3a2e5347 Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Sun, 22 Sep 2024 18:28:26 +0300 Subject: [PATCH 024/114] UI Alignment --- .../lambda/module/modules/client/NewCGui.kt | 24 +--- .../kotlin/com/lambda/newgui/LambdaScreen.kt | 4 +- .../newgui/component/core/IListEntry.kt | 5 + .../lambda/newgui/component/core/TextField.kt | 37 +++--- .../lambda/newgui/component/core/UIBuilder.kt | 4 + .../newgui/{ => component/layout}/Layout.kt | 110 +++++++++++++++--- .../newgui/component/layout/ListLayout.kt | 61 ++++++++++ .../lambda/newgui/component/window/Window.kt | 96 +++++++++------ 8 files changed, 250 insertions(+), 91 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/newgui/component/core/IListEntry.kt create mode 100644 common/src/main/kotlin/com/lambda/newgui/component/core/UIBuilder.kt rename common/src/main/kotlin/com/lambda/newgui/{ => component/layout}/Layout.kt (69%) create mode 100644 common/src/main/kotlin/com/lambda/newgui/component/layout/ListLayout.kt diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt index e7b4aeaa5..5370af1e4 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt @@ -1,15 +1,12 @@ package com.lambda.module.modules.client -import com.lambda.Lambda.mc import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.newgui.LambdaScreen.Companion.gui import com.lambda.newgui.LambdaScreen.Companion.toScreen -import com.lambda.newgui.component.HAlign -import com.lambda.newgui.component.core.TextField.Companion.textField +import com.lambda.newgui.component.window.Window import com.lambda.newgui.component.window.Window.Companion.window import com.lambda.util.math.Vec2d -import java.awt.Color object NewCGui : Module( name = "NewCGui", @@ -17,28 +14,13 @@ object NewCGui : Module( defaultTags = setOf(ModuleTag.CLIENT) ) { val titleBarHeight by setting("Title Bar Height", 4.0, 0.0..10.0, 0.1) + val padding by setting("Padding", 2.0, 0.0..6.0, 0.1) private val clickGuiLayout = gui { window(position = Vec2d.ONE * 20.0, title = "Test window") { - titleBar.textField.apply { - text = "Overriding the title" + content.window(Vec2d.ONE * 5.0, Vec2d.ONE * 60.0) { - // Making it align the left corner and have 3px offset from the left side - alignment = HAlign.LEFT - offset = 3.0 - } - - textField("Text field over the window") - - content.textField("Text field inside of the content region") { - alignment = HAlign.CENTER - scale = 0.5 - - onTick { - // Dynamically updating states - color = if (mc.player?.isDead == true) Color.RED else Color.GREEN - } } } } diff --git a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt index 37e3a2d8e..7281f0b4a 100644 --- a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt +++ b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt @@ -7,6 +7,8 @@ import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listener import com.lambda.graphics.RenderMain import com.lambda.gui.api.GuiEvent +import com.lambda.newgui.component.layout.Layout +import com.lambda.newgui.component.core.UIBuilder import com.lambda.util.KeyCode import com.lambda.util.Mouse import com.lambda.util.Nameable @@ -119,7 +121,7 @@ class LambdaScreen( Layout(owner = null, useBatching = false, batchChildren = true).apply { var screenSize = Vec2d.ONE * 10000.0 - rect { + rectUpdate { Rect(Vec2d.ZERO, screenSize) } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/IListEntry.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/IListEntry.kt new file mode 100644 index 000000000..be9216326 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/IListEntry.kt @@ -0,0 +1,5 @@ +package com.lambda.newgui.component.core + +interface IListEntry { + var heightOffset: Double +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt index f6c84ed0b..0ae239c45 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt @@ -1,10 +1,8 @@ package com.lambda.newgui.component.core -import com.lambda.newgui.Layout -import com.lambda.newgui.UIBuilder -import com.lambda.newgui.component.HAlign +import com.lambda.newgui.component.VAlign +import com.lambda.newgui.component.layout.Layout import com.lambda.util.math.MathUtils.lerp -import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d import java.awt.Color @@ -14,7 +12,6 @@ class TextField( initialColor: Color = Color.WHITE, initialScale: Double = 1.0, initialShadow: Boolean = true, - initialAlignment: HAlign = HAlign.LEFT, initialOffset: Double = 0.0, ) : Layout(owner, true, true) { var text = initialText @@ -22,26 +19,35 @@ class TextField( var scale = initialScale var shadow = initialShadow - var alignment = initialAlignment var offset = initialOffset // Let user interact through the text override val interactionPassthrough = true init { - rect { - // Completely fill parent component by default - Rect(Vec2d.ZERO, owner.rect.size) - } + verticalAlignment = VAlign.CENTER + rectUpdate(owner::rect) onRender { + val w = font.getWidth(text, scale) + val h = font.getHeight(scale) + val x = lerp( rect.left, - rect.right - font.getWidth(text, scale), - alignment.multiplier - ) - offset * alignment.offset + rect.right - w, + horizontalAlignment.multiplier + ) - offset * horizontalAlignment.offset + + val y = when { + verticalAlignment == VAlign.CENTER || rect.size.y <= h -> rect.center.y + else -> lerp( + rect.top + h * 0.5, + rect.bottom - h * 0.5, + verticalAlignment.multiplier + ) + } - font.build(text, Vec2d(x, rect.center.y), color, scale, shadow) + font.build(text, Vec2d(x, y), color, scale, shadow) } } @@ -52,9 +58,8 @@ class TextField( color: Color = Color.WHITE, scale: Double = 1.0, shadow: Boolean = true, - alignment: HAlign = HAlign.LEFT, offset: Double = 0.0, block: TextField.() -> Unit = {} - ) = TextField(this, text, color, scale, shadow, alignment, offset).apply(children::add).apply(block) + ) = TextField(this, text, color, scale, shadow, offset).apply(children::add).apply(block) } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/UIBuilder.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/UIBuilder.kt new file mode 100644 index 000000000..fdca7fe38 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/UIBuilder.kt @@ -0,0 +1,4 @@ +package com.lambda.newgui.component.core + +@DslMarker +annotation class UIBuilder \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt similarity index 69% rename from common/src/main/kotlin/com/lambda/newgui/Layout.kt rename to common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt index 35572eb6b..d91ff1d77 100644 --- a/common/src/main/kotlin/com/lambda/newgui/Layout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt @@ -1,11 +1,16 @@ -package com.lambda.newgui +package com.lambda.newgui.component.layout +import com.lambda.graphics.RenderMain import com.lambda.graphics.animation.AnimationTicker import com.lambda.graphics.gl.Scissor.scissor import com.lambda.gui.api.GuiEvent import com.lambda.gui.api.RenderLayer +import com.lambda.newgui.component.HAlign +import com.lambda.newgui.component.VAlign +import com.lambda.newgui.component.core.UIBuilder import com.lambda.util.KeyCode import com.lambda.util.Mouse +import com.lambda.util.math.MathUtils.coerceIn import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d @@ -30,12 +35,65 @@ import com.lambda.util.math.Vec2d * ``` */ open class Layout( - private val owner: Layout?, + val owner: Layout?, useBatching: Boolean, - private val batchChildren: Boolean + private val batchChildren: Boolean, ) { - // Rectangle of the component - open val rect: Rect get() = rectUpdate() + (owner?.rect?.leftTop ?: Vec2d.ZERO) + /** + * The rectangle of this component + */ + val rect get() = Rect.basedOn(position, size) + + /** + * The size of this component + */ + open var size = Vec2d.ZERO + + /** + * Horizontal alignment + */ + var horizontalAlignment = HAlign.LEFT; set(to) { + val from = field + field = to + + val delta = to.multiplier - from.multiplier + relativePos += Vec2d.RIGHT * delta * (size.x - ownerRect.size.x) + } + + /** + * Vertical alignment + */ + var verticalAlignment = VAlign.TOP; set(to) { + val from = field + field = to + + val delta = to.multiplier - from.multiplier + relativePos += Vec2d.BOTTOM * delta * (size.y - ownerRect.size.y) + } + + /** + * Relative position of the component + */ + var relativePos = Vec2d.ZERO + + /** + * Absolute(drawn) position of the component + */ + var position: Vec2d + get() = ownerRect.leftTop + relativeToAbs(relativePos).let { + if (!clampPosition) it + else it.coerceIn( + 0.0, ownerRect.size.x - size.x, + 0.0, ownerRect.size.y - size.y + ) + }; set(value) { relativePos = absToRelative(value - ownerRect.leftTop) } + + // Rect-related properties + private var screenSize = Vec2d.ZERO + private val ownerRect get() = owner?.rect ?: Rect(Vec2d.ZERO, screenSize) + private val dockingOffset get() = (ownerRect.size - size) * Vec2d(horizontalAlignment.multiplier, verticalAlignment.multiplier) + private fun relativeToAbs(posIn: Vec2d) = posIn + dockingOffset + private fun absToRelative(posIn: Vec2d) = posIn - dockingOffset // Structure val children = mutableListOf() @@ -61,7 +119,9 @@ open class Layout( } private var owningRenderer = false + protected open val interactionPassthrough = false + protected open val clampPosition = false // Actions private var showActions = mutableListOf<() -> Unit>() @@ -73,7 +133,7 @@ open class Layout( private var mouseClickActions = mutableListOf<(button: Mouse.Button, action: Mouse.Action) -> Unit>() private var mouseMoveActions = mutableListOf<(mouse: Vec2d) -> Unit>() private var mouseScrollActions = mutableListOf<(delta: Double) -> Unit>() - private var rectUpdate = { Rect.ZERO } + private var rectUpdate: (() -> Rect)? = null /** * Sets the action to be performed when the element gets shown. @@ -157,13 +217,22 @@ open class Layout( } /** - * Sets the rectangle of this component. + * Sets the rect of the element */ - fun rect(block: () -> Rect) { + fun rectUpdate(block: () -> Rect) { rectUpdate = block } fun onEvent(e: GuiEvent) { + if (e is GuiEvent.Render) { + screenSize = RenderMain.screenSize + + rectUpdate?.invoke()?.let { + position = it.leftTop + size = it.size + } + } + // Select an element that's on foreground selectedChild = if (isHovered) children.lastOrNull { !it.interactionPassthrough && mousePosition in it.rect @@ -192,23 +261,31 @@ open class Layout( is GuiEvent.KeyPress -> { keyPressActions.forEach { it(e.key) } } is GuiEvent.CharTyped -> { charTypedActions.forEach { it((e.char)) } } is GuiEvent.MouseMove -> { mousePosition = e.mouse; mouseMoveActions.forEach { it(e.mouse) } } - is GuiEvent.MouseScroll -> { mousePosition = e.mouse; mouseScrollActions.forEach { it(e.delta) } } + is GuiEvent.MouseScroll -> { + mousePosition = e.mouse + + if (isHovered) { + mouseScrollActions.forEach { it(e.delta) } + } + } is GuiEvent.MouseClick -> { mousePosition = e.mouse val action = if (isHovered) e.action else Mouse.Action.Release mouseClickActions.forEach { it(e.button, action) } } - is GuiEvent.Render -> scissor(rect) { + is GuiEvent.Render -> { val (pre, post) = children.partition { !it.owningRenderer } pre.forEach { it.onEvent(e) } renderActions.forEach { it(renderer) } if (owningRenderer) { - scissor(rect, renderer::render) + renderer.render() } - post.forEach { it.onEvent(e) } + scissor(rect) { // ToDo: merge to ListLayout + post.forEach { it.onEvent(e) } + } } } } @@ -217,21 +294,20 @@ open class Layout( /** * Creates an empty [Layout] * - * @param useBatching Increases performance by using parent's renderer instead of creating a new one. + * @param useBatching Whether to use parent's renderer * * @param batchChildren Whether allow children to use the renderer of this layout * + * @param block Actions to perform within this component + * * Check [Layout] description for more info about batching */ @UIBuilder fun Layout.layout( useBatching: Boolean = false, batchChildren: Boolean = false, - block: Layout.() -> Unit, + block: Layout.() -> Unit = {}, ) = Layout(this, useBatching, batchChildren) .apply(children::add).apply(block) } } - -@DslMarker -annotation class UIBuilder \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/ListLayout.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/ListLayout.kt new file mode 100644 index 000000000..03126785d --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/ListLayout.kt @@ -0,0 +1,61 @@ +package com.lambda.newgui.component.layout + +import com.lambda.newgui.component.core.IListEntry +import com.lambda.newgui.component.core.UIBuilder + +class ListLayout( + owner: Layout, + private val scrollable: Boolean +) : Layout(owner, false, true) { + private var scrollOffset: Double = 0.0 + private var rubberbandRequest = 0.0 + private var rubberbandDelta = 0.0 + + init { + onShow { + scrollOffset = 0.0 + } + + onTick { + rubberbandDelta += rubberbandRequest + rubberbandRequest = 0.0 + + rubberbandDelta *= 0.5 + if (rubberbandDelta < 0.05) rubberbandDelta = 0.0 + + var y = scrollOffset + rubberbandDelta + + children.forEach { child -> + if (child !is IListEntry) return@forEach + + child.heightOffset = y + y += child.rect.size.y + 2 + } + } + + onMouseScroll { delta -> + if (!scrollable) return@onMouseScroll + + scrollOffset += delta * 10.0 + + val prevOffset = scrollOffset + val range = -children.sumOf { it.rect.size.y } + rect.size.y + scrollOffset = scrollOffset.coerceAtLeast(range).coerceAtMost(0.0) + + rubberbandRequest += prevOffset - scrollOffset + } + } + + companion object { + /** + * Creates an empty [ListLayout] + * + * @param block Actions to perform within this component + */ + @UIBuilder + fun Layout.listLayout( + scrollable: Boolean = true, + block: ListLayout.() -> Unit + ) = ListLayout(this, scrollable).apply(children::add).apply(block) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt index 61f5bca9d..9306dd026 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt @@ -1,51 +1,73 @@ package com.lambda.newgui.component.window -import com.lambda.newgui.Layout -import com.lambda.newgui.UIBuilder +import com.lambda.module.modules.client.NewCGui +import com.lambda.newgui.component.layout.Layout +import com.lambda.newgui.component.core.UIBuilder +import com.lambda.newgui.component.HAlign import com.lambda.newgui.component.core.TextField.Companion.textField -import com.lambda.newgui.component.window.Window.ContentSpace.Companion.contentSpace +import com.lambda.newgui.component.layout.ListLayout.Companion.listLayout import com.lambda.newgui.component.window.Window.TitleBar.Companion.titleBar import com.lambda.util.Mouse -import com.lambda.util.math.ColorUtils.setAlpha import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d import java.awt.Color +/** + * Represents a window component + * + * Contains titlebar and content layout + */ class Window( owner: Layout, - initialPosition: Vec2d, - initialSize: Vec2d, initialTitle: String ) : Layout(owner, false, true) { - var position = initialPosition - var size = initialSize - val titleBar = titleBar(initialTitle) - val content = contentSpace { - Rect(titleBar.rect.leftBottom, rect.rightBottom) - position + val content = listLayout { + rectUpdate { + Rect( + titleBar.rect.leftBottom + NewCGui.padding, + this@Window.rect.rightBottom - NewCGui.padding + ) + } } - init { - rect { - Rect.basedOn(position, size) - } + override val clampPosition = true + init { onRender { - filled.build(rect, 2.0, Color.BLACK.setAlpha(0.2)) + filled.build( + rect, + 2.0, + Color(50, 50, 50), + shade = true + ) + + outline.build( + rect, + 2.0, + 1.0, + Color.WHITE, + true + ) } } + /** + * Represents a titlebar component + */ class TitleBar( owner: Window, - initialTitle: String + title: String ) : Layout(owner, true, true) { - val textField = textField(initialTitle) + val textField = textField(title) { + horizontalAlignment = HAlign.CENTER + } private var dragOffset: Vec2d? = null init { - rect { - Rect(Vec2d.ZERO, Vec2d(owner.rect.size.x, renderer.font.getHeight() * 1.25)) + rectUpdate { + Rect(owner.rect.leftTop, owner.rect.rightTop + Vec2d(0.0, renderer.font.getHeight() * 1.5)) } onShow { @@ -73,26 +95,28 @@ class Window( } } - open class ContentSpace(owner: Layout, rectBlock: () -> Rect) : Layout(owner, false, true) { - init { - rect(rectBlock) - } - - companion object { - @UIBuilder - fun Layout.contentSpace( - rect: () -> Rect, - ) = ContentSpace(this, rect).apply(children::add) - } - } - companion object { + /** + * Creates new empty [Window] + * + * @param position The initial position of the window + * + * @param size The initial size of the window + * + * @param title The title of the window + * + * @param block Actions to perform within this component + */ @UIBuilder fun Layout.window( - position: Vec2d, + position: Vec2d = Vec2d.ZERO, size: Vec2d = Vec2d(100.0, 300.0), title: String = "Untitled", - block: Window.() -> Unit - ) = Window(this, position, size, title).apply(children::add).apply(block) + block: Window.() -> Unit = {} + ) = Window(this, title).apply(children::add).apply { + this.position = position + this.size = size + block(this) + } } } \ No newline at end of file From 48a180f9c8e4d2ab336f795833b5425920afa98e Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Sun, 29 Sep 2024 16:44:41 +0300 Subject: [PATCH 025/114] Window content layout --- .../lambda/module/modules/client/NewCGui.kt | 6 +- .../newgui/component/core/IListEntry.kt | 5 - .../lambda/newgui/component/core/TextField.kt | 4 +- .../lambda/newgui/component/layout/Layout.kt | 25 +++-- .../component/layout/LayoutProperties.kt | 18 ++++ .../newgui/component/layout/ListLayout.kt | 61 ----------- .../newgui/component/window/TitleBar.kt | 56 ++++++++++ .../lambda/newgui/component/window/Window.kt | 101 ++++++------------ .../newgui/component/window/WindowContent.kt | 84 +++++++++++++++ 9 files changed, 213 insertions(+), 147 deletions(-) delete mode 100644 common/src/main/kotlin/com/lambda/newgui/component/core/IListEntry.kt create mode 100644 common/src/main/kotlin/com/lambda/newgui/component/layout/LayoutProperties.kt delete mode 100644 common/src/main/kotlin/com/lambda/newgui/component/layout/ListLayout.kt create mode 100644 common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt create mode 100644 common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt index 5370af1e4..be5e3b020 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt @@ -4,7 +4,6 @@ import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.newgui.LambdaScreen.Companion.gui import com.lambda.newgui.LambdaScreen.Companion.toScreen -import com.lambda.newgui.component.window.Window import com.lambda.newgui.component.window.Window.Companion.window import com.lambda.util.math.Vec2d @@ -15,12 +14,15 @@ object NewCGui : Module( ) { val titleBarHeight by setting("Title Bar Height", 4.0, 0.0..10.0, 0.1) val padding by setting("Padding", 2.0, 0.0..6.0, 0.1) + val listStep by setting("List Step", 2.0, 0.0..6.0, 0.1) private val clickGuiLayout = gui { window(position = Vec2d.ONE * 20.0, title = "Test window") { - content.window(Vec2d.ONE * 5.0, Vec2d.ONE * 60.0) { + repeat(6) { + window(Vec2d.ONE * 5.0, Vec2d.ONE * 60.0) { + } } } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/IListEntry.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/IListEntry.kt deleted file mode 100644 index be9216326..000000000 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/IListEntry.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.lambda.newgui.component.core - -interface IListEntry { - var heightOffset: Double -} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt index 0ae239c45..08f1e6f29 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt @@ -21,10 +21,8 @@ class TextField( var offset = initialOffset - // Let user interact through the text - override val interactionPassthrough = true - init { + properties.interactionPassthrough = true verticalAlignment = VAlign.CENTER rectUpdate(owner::rect) diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt index d91ff1d77..9dc6d2451 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt @@ -47,7 +47,7 @@ open class Layout( /** * The size of this component */ - open var size = Vec2d.ZERO + var size = Vec2d.ZERO /** * Horizontal alignment @@ -81,7 +81,7 @@ open class Layout( */ var position: Vec2d get() = ownerRect.leftTop + relativeToAbs(relativePos).let { - if (!clampPosition) it + if (!properties.clampPosition) it else it.coerceIn( 0.0, ownerRect.size.x - size.x, 0.0, ownerRect.size.y - size.y @@ -95,6 +95,11 @@ open class Layout( private fun relativeToAbs(posIn: Vec2d) = posIn + dockingOffset private fun absToRelative(posIn: Vec2d) = posIn - dockingOffset + /** + * Configurable properties of the component + */ + val properties = LayoutProperties() + // Structure val children = mutableListOf() private var selectedChild: Layout? = null @@ -104,7 +109,6 @@ open class Layout( private val isHovered: Boolean get() = mousePosition in rect && (owner?.isHovered ?: true) // Graphics - val animation = AnimationTicker() val renderer: RenderLayer = run { owner?.let { owner -> if (!useBatching || !owner.batchChildren) { @@ -120,9 +124,6 @@ open class Layout( private var owningRenderer = false - protected open val interactionPassthrough = false - protected open val clampPosition = false - // Actions private var showActions = mutableListOf<() -> Unit>() private var hideActions = mutableListOf<() -> Unit>() @@ -235,7 +236,7 @@ open class Layout( // Select an element that's on foreground selectedChild = if (isHovered) children.lastOrNull { - !it.interactionPassthrough && mousePosition in it.rect + !it.properties.interactionPassthrough && mousePosition in it.rect } else null // Update children @@ -243,7 +244,7 @@ open class Layout( if (e is GuiEvent.Render) return@forEach if (e is GuiEvent.MouseClick) { - val hovered = child == selectedChild || (child.isHovered && child.interactionPassthrough) + val hovered = child == selectedChild || (child.isHovered && child.properties.interactionPassthrough) val newAction = if (hovered) e.action else Mouse.Action.Release val newEvent = GuiEvent.MouseClick(e.button, newAction, e.mouse) @@ -257,7 +258,7 @@ open class Layout( when (e) { is GuiEvent.Show -> { mousePosition = Vec2d.ONE * -1000.0; showActions.forEach { it() } } is GuiEvent.Hide -> { hideActions.forEach { it() } } - is GuiEvent.Tick -> { animation.tick(); tickActions.forEach { it() } } + is GuiEvent.Tick -> { tickActions.forEach { it() } } is GuiEvent.KeyPress -> { keyPressActions.forEach { it(e.key) } } is GuiEvent.CharTyped -> { charTypedActions.forEach { it((e.char)) } } is GuiEvent.MouseMove -> { mousePosition = e.mouse; mouseMoveActions.forEach { it(e.mouse) } } @@ -283,9 +284,13 @@ open class Layout( renderer.render() } - scissor(rect) { // ToDo: merge to ListLayout + val postAction = { post.forEach { it.onEvent(e) } } + + if (properties.scissorChildren) { + scissor(rect, postAction) + } else postAction() } } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/LayoutProperties.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/LayoutProperties.kt new file mode 100644 index 000000000..0e1b51b8c --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/LayoutProperties.kt @@ -0,0 +1,18 @@ +package com.lambda.newgui.component.layout + +class LayoutProperties { + /** + * If true, interactions pass through to elements beneath this one. + */ + var interactionPassthrough = false + + /** + * If true, this element's rectangle is clamped within parent's bounds. + */ + var clampPosition = false + + /** + * If true, children using their own render layer are clipped within this rect. + */ + var scissorChildren = false +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/ListLayout.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/ListLayout.kt deleted file mode 100644 index 03126785d..000000000 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/ListLayout.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.lambda.newgui.component.layout - -import com.lambda.newgui.component.core.IListEntry -import com.lambda.newgui.component.core.UIBuilder - -class ListLayout( - owner: Layout, - private val scrollable: Boolean -) : Layout(owner, false, true) { - private var scrollOffset: Double = 0.0 - private var rubberbandRequest = 0.0 - private var rubberbandDelta = 0.0 - - init { - onShow { - scrollOffset = 0.0 - } - - onTick { - rubberbandDelta += rubberbandRequest - rubberbandRequest = 0.0 - - rubberbandDelta *= 0.5 - if (rubberbandDelta < 0.05) rubberbandDelta = 0.0 - - var y = scrollOffset + rubberbandDelta - - children.forEach { child -> - if (child !is IListEntry) return@forEach - - child.heightOffset = y - y += child.rect.size.y + 2 - } - } - - onMouseScroll { delta -> - if (!scrollable) return@onMouseScroll - - scrollOffset += delta * 10.0 - - val prevOffset = scrollOffset - val range = -children.sumOf { it.rect.size.y } + rect.size.y - scrollOffset = scrollOffset.coerceAtLeast(range).coerceAtMost(0.0) - - rubberbandRequest += prevOffset - scrollOffset - } - } - - companion object { - /** - * Creates an empty [ListLayout] - * - * @param block Actions to perform within this component - */ - @UIBuilder - fun Layout.listLayout( - scrollable: Boolean = true, - block: ListLayout.() -> Unit - ) = ListLayout(this, scrollable).apply(children::add).apply(block) - } -} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt new file mode 100644 index 000000000..7e996e99d --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt @@ -0,0 +1,56 @@ +package com.lambda.newgui.component.window + +import com.lambda.newgui.component.HAlign +import com.lambda.newgui.component.core.TextField.Companion.textField +import com.lambda.newgui.component.core.UIBuilder +import com.lambda.newgui.component.layout.Layout +import com.lambda.util.Mouse +import com.lambda.util.math.Rect +import com.lambda.util.math.Vec2d + +/** + * Represents a titlebar component + */ +class TitleBar( + owner: Window, + title: String, + drag: Boolean +) : Layout(owner, true, true) { + val textField = textField(title) { + horizontalAlignment = HAlign.CENTER + } + + private var dragOffset: Vec2d? = null + + init { + rectUpdate { + Rect(owner.rect.leftTop, owner.rect.rightTop + Vec2d(0.0, renderer.font.getHeight() * 1.5)) + } + + if (drag) { + onShow { + dragOffset = null + } + + onMouseClick { button: Mouse.Button, action: Mouse.Action -> + dragOffset = if (button == Mouse.Button.Left && action == Mouse.Action.Click) { + mousePosition - owner.position + } else null + } + + onMouseMove { mouse -> + dragOffset?.let { drag -> + owner.position = mouse - drag + } + } + } + } + + companion object { + @UIBuilder + fun Window.titleBar( + text: String, + drag: Boolean + ) = TitleBar(this, text, drag).apply(children::add) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt index 9306dd026..0248d156a 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt @@ -1,13 +1,11 @@ package com.lambda.newgui.component.window import com.lambda.module.modules.client.NewCGui +import com.lambda.newgui.component.VAlign import com.lambda.newgui.component.layout.Layout import com.lambda.newgui.component.core.UIBuilder -import com.lambda.newgui.component.HAlign -import com.lambda.newgui.component.core.TextField.Companion.textField -import com.lambda.newgui.component.layout.ListLayout.Companion.listLayout -import com.lambda.newgui.component.window.Window.TitleBar.Companion.titleBar -import com.lambda.util.Mouse +import com.lambda.newgui.component.window.TitleBar.Companion.titleBar +import com.lambda.newgui.component.window.WindowContent.Companion.windowContent import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d import java.awt.Color @@ -15,25 +13,32 @@ import java.awt.Color /** * Represents a window component * - * Contains titlebar and content layout + * Consists of titlebar and content layout */ -class Window( +open class Window( owner: Layout, - initialTitle: String + initialTitle: String, + draggable: Boolean, + scrollable: Boolean ) : Layout(owner, false, true) { - val titleBar = titleBar(initialTitle) - val content = listLayout { - rectUpdate { - Rect( - titleBar.rect.leftBottom + NewCGui.padding, - this@Window.rect.rightBottom - NewCGui.padding - ) - } - } - - override val clampPosition = true + val titleBar = titleBar(initialTitle, draggable) + val content = windowContent(scrollable) init { + // Clamp the window only within the screen bounds + properties.clampPosition = owner.owner == null + + onShow { + content.properties.scissorChildren = true + + content.rectUpdate { + Rect( + titleBar.rect.leftBottom + NewCGui.padding, + this@Window.rect.rightBottom - NewCGui.padding + ) + } + } + onRender { filled.build( rect, @@ -52,49 +57,6 @@ class Window( } } - /** - * Represents a titlebar component - */ - class TitleBar( - owner: Window, - title: String - ) : Layout(owner, true, true) { - val textField = textField(title) { - horizontalAlignment = HAlign.CENTER - } - - private var dragOffset: Vec2d? = null - - init { - rectUpdate { - Rect(owner.rect.leftTop, owner.rect.rightTop + Vec2d(0.0, renderer.font.getHeight() * 1.5)) - } - - onShow { - dragOffset = null - } - - onMouseClick { button: Mouse.Button, action: Mouse.Action -> - dragOffset = if (button == Mouse.Button.Left && action == Mouse.Action.Click) { - mousePosition - owner.position - } else null - } - - onMouseMove { mouse -> - dragOffset?.let { drag -> - owner.position = mouse - drag - } - } - } - - companion object { - @UIBuilder - fun Window.titleBar( - text: String, - ) = TitleBar(this, text).apply(children::add) - } - } - companion object { /** * Creates new empty [Window] @@ -105,18 +67,25 @@ class Window( * * @param title The title of the window * - * @param block Actions to perform within this component + * @param draggable Whether to allow user to drag the window + * + * @param scrollable Whether to allow user to scroll the elements + * Note: applies to elements with [VAlign.TOP] only + * + * @param block Actions to perform within content space of the window */ @UIBuilder fun Layout.window( position: Vec2d = Vec2d.ZERO, size: Vec2d = Vec2d(100.0, 300.0), title: String = "Untitled", - block: Window.() -> Unit = {} - ) = Window(this, title).apply(children::add).apply { + draggable: Boolean = true, + scrollable: Boolean = true, + block: WindowContent.() -> Unit = {} + ) = Window(this, title, draggable, scrollable).apply(children::add).apply { this.position = position this.size = size - block(this) + block(this.content) } } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt new file mode 100644 index 000000000..b15164a38 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt @@ -0,0 +1,84 @@ +package com.lambda.newgui.component.window + +import com.lambda.graphics.animation.Animation.Companion.exp +import com.lambda.graphics.animation.AnimationTicker +import com.lambda.module.modules.client.NewCGui +import com.lambda.newgui.component.VAlign +import com.lambda.newgui.component.core.UIBuilder +import com.lambda.newgui.component.layout.Layout +import com.lambda.util.math.Vec2d +import kotlin.math.abs + +class WindowContent( + owner: Window, + scrollable: Boolean +) : Layout(owner, false, true) { + private val animation = AnimationTicker() + + private var dwheel = 0.0 + private var scrollOffset = 0.0 + private var rubberbandDelta = 0.0 + private var renderScrollOffset by animation.exp({ scrollOffset + rubberbandDelta }, 0.7) + + private val scrollableChildren get() = children.filter { it.verticalAlignment == VAlign.TOP } + + init { + onShow { + scrollOffset = 0.0 + rubberbandDelta = 0.0 + dwheel = 0.0 + renderScrollOffset = 0.0 + + reorderChildren() + } + + onTick { + scrollOffset += dwheel + dwheel = 0.0 + + val c = scrollableChildren + var childHeight = c.sumOf { it.rect.size.y + NewCGui.listStep } + if (c.isNotEmpty()) childHeight -= NewCGui.listStep + + val range = rect.size.y - childHeight + + val prevOffset = scrollOffset + scrollOffset = scrollOffset.coerceAtLeast(range).coerceAtMost(0.0) + + rubberbandDelta += prevOffset - scrollOffset + rubberbandDelta *= 0.5 + if (abs(rubberbandDelta) < 0.05) rubberbandDelta = 0.0 + + animation.tick() + } + + onRender { + reorderChildren() + } + + onMouseScroll { delta -> + if (!scrollable) return@onMouseScroll + dwheel += delta * 10.0 + } + } + + private fun reorderChildren() { + var offset = renderScrollOffset + + scrollableChildren.forEach { child -> + child.position = Vec2d(child.position.x, position.y + offset) + offset += child.rect.size.y + NewCGui.listStep + } + } + + companion object { + /** + * Creates an empty [WindowContent] component + * + * @param scrollable Whether to scroll + */ + @UIBuilder + fun Window.windowContent(scrollable: Boolean) = + WindowContent(this, scrollable).apply(children::add) + } +} \ No newline at end of file From e93d9ca0aceafeb007a69db2803efdfd70bd6187 Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Sun, 29 Sep 2024 19:47:03 +0300 Subject: [PATCH 026/114] Window resizing --- .../gui/api/component/core/DockingRect.kt | 2 +- .../lambda/module/modules/client/NewCGui.kt | 2 +- .../lambda/newgui/component/core/TextField.kt | 8 +- .../lambda/newgui/component/layout/Layout.kt | 29 ++++-- .../lambda/newgui/component/window/Window.kt | 89 ++++++++++++++++--- .../newgui/component/window/WindowContent.kt | 2 +- 6 files changed, 108 insertions(+), 24 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/core/DockingRect.kt b/common/src/main/kotlin/com/lambda/gui/api/component/core/DockingRect.kt index c58e9b7b6..e844dd9c6 100644 --- a/common/src/main/kotlin/com/lambda/gui/api/component/core/DockingRect.kt +++ b/common/src/main/kotlin/com/lambda/gui/api/component/core/DockingRect.kt @@ -3,10 +3,10 @@ package com.lambda.gui.api.component.core import com.lambda.module.modules.client.ClickGui import com.lambda.newgui.component.HAlign import com.lambda.newgui.component.VAlign -import com.lambda.util.math.MathUtils.coerceIn import com.lambda.util.math.MathUtils.roundToStep import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d +import com.lambda.util.math.coerceIn abstract class DockingRect { abstract var relativePos: Vec2d diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt index be5e3b020..cfedda385 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt @@ -13,7 +13,7 @@ object NewCGui : Module( defaultTags = setOf(ModuleTag.CLIENT) ) { val titleBarHeight by setting("Title Bar Height", 4.0, 0.0..10.0, 0.1) - val padding by setting("Padding", 2.0, 0.0..6.0, 0.1) + val padding by setting("Padding", 2.0, 1.0..6.0, 0.1) val listStep by setting("List Step", 2.0, 0.0..6.0, 0.1) private val clickGuiLayout = diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt index 08f1e6f29..a513c201f 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt @@ -2,8 +2,8 @@ package com.lambda.newgui.component.core import com.lambda.newgui.component.VAlign import com.lambda.newgui.component.layout.Layout -import com.lambda.util.math.MathUtils.lerp import com.lambda.util.math.Vec2d +import com.lambda.util.math.lerp import java.awt.Color class TextField( @@ -31,17 +31,17 @@ class TextField( val h = font.getHeight(scale) val x = lerp( + horizontalAlignment.multiplier, rect.left, rect.right - w, - horizontalAlignment.multiplier ) - offset * horizontalAlignment.offset val y = when { verticalAlignment == VAlign.CENTER || rect.size.y <= h -> rect.center.y else -> lerp( + verticalAlignment.multiplier, rect.top + h * 0.5, - rect.bottom - h * 0.5, - verticalAlignment.multiplier + rect.bottom - h * 0.5 ) } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt index 9dc6d2451..6ddb22b4b 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt @@ -10,9 +10,9 @@ import com.lambda.newgui.component.VAlign import com.lambda.newgui.component.core.UIBuilder import com.lambda.util.KeyCode import com.lambda.util.Mouse -import com.lambda.util.math.MathUtils.coerceIn import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d +import com.lambda.util.math.coerceIn /** * Represents a component for creating complex ui structures. @@ -102,7 +102,7 @@ open class Layout( // Structure val children = mutableListOf() - private var selectedChild: Layout? = null + protected var selectedChild: Layout? = null // Inputs protected var mousePosition = Vec2d.ZERO @@ -297,15 +297,15 @@ open class Layout( companion object { /** - * Creates an empty [Layout] + * Creates an empty [Layout]. * - * @param useBatching Whether to use parent's renderer + * @param useBatching Whether to use parent's renderer. * - * @param batchChildren Whether allow children to use the renderer of this layout + * @param batchChildren Whether allow children to use the renderer of this layout. * - * @param block Actions to perform within this component + * @param block Actions to perform within this component. * - * Check [Layout] description for more info about batching + * Check [Layout] description for more info about batching. */ @UIBuilder fun Layout.layout( @@ -314,5 +314,20 @@ open class Layout( block: Layout.() -> Unit = {}, ) = Layout(this, useBatching, batchChildren) .apply(children::add).apply(block) + + /** + * Creates new [AnimationTicker]. + * + * Use it to create and manage animations. + * + * It's ok to have multiple tickers per component if you need to tick different animations at different timings. + * + * @param register Whether to tick this [AnimationTicker]. + * Otherwise, you will have to tick it manually + */ + @UIBuilder + fun Layout.animationTicker(register: Boolean = true) = AnimationTicker().apply { + if (register) onTick(this::tick) + } } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt index 0248d156a..e74b74751 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt @@ -6,8 +6,10 @@ import com.lambda.newgui.component.layout.Layout import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.window.TitleBar.Companion.titleBar import com.lambda.newgui.component.window.WindowContent.Companion.windowContent +import com.lambda.util.Mouse import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d +import com.lambda.util.math.coerceIn import java.awt.Color /** @@ -18,25 +20,46 @@ import java.awt.Color open class Window( owner: Layout, initialTitle: String, + initialPosition: Vec2d, + initialSize: Vec2d, draggable: Boolean, - scrollable: Boolean + scrollable: Boolean, + private val minimizable: Boolean, + private val resizable: Boolean ) : Layout(owner, false, true) { val titleBar = titleBar(initialTitle, draggable) val content = windowContent(scrollable) + private val animation = animationTicker() + + // Minimizing + var minimized = true + + // Resizing + private var resizeX: Double? = null + private var resizeY: Double? = null + init { + position = initialPosition + size = initialSize + // Clamp the window only within the screen bounds properties.clampPosition = owner.owner == null onShow { - content.properties.scissorChildren = true + with(content) { + properties.scissorChildren = true - content.rectUpdate { - Rect( - titleBar.rect.leftBottom + NewCGui.padding, - this@Window.rect.rightBottom - NewCGui.padding - ) + rectUpdate { + Rect( + titleBar.rect.leftBottom + NewCGui.padding, + this@Window.rect.rightBottom - NewCGui.padding + ) + } } + + resizeX = null + resizeY = null } onRender { @@ -55,6 +78,42 @@ open class Window( true ) } + + onMouseClick { button: Mouse.Button, action: Mouse.Action -> + resizeX = null + resizeY = null + + if (!resizable) return@onMouseClick + if (selectedChild != null) return@onMouseClick + if (button != Mouse.Button.Left || action != Mouse.Action.Click) return@onMouseClick + + val resizeXHovered = mousePosition in Rect( + titleBar.rect.rightTop - Vec2d(RESIZE_RANGE, 0.0), + rect.rightBottom + ) + + val resizeYHovered = mousePosition in Rect( + rect.leftBottom - Vec2d(0.0, RESIZE_RANGE), + rect.rightBottom + ) + + if (resizeXHovered) resizeX = mousePosition.x - size.x + if (resizeYHovered) resizeY = mousePosition.y - size.y + } + + onMouseMove { + if (resizeX == null && resizeY == null) return@onMouseMove + + val x = resizeX?.let { rx -> + mousePosition.x - rx + } ?: size.x + + val y = resizeY?.let { ry -> + mousePosition.y - ry + } ?: size.y + + size = Vec2d(x, y).coerceIn(10.0, 1000.0, titleBar.size.y, 1000.0) + } } companion object { @@ -72,6 +131,10 @@ open class Window( * @param scrollable Whether to allow user to scroll the elements * Note: applies to elements with [VAlign.TOP] only * + * @param minimizable Whether to allow user to minimize the window + * + * @param resizable Whether to allow user to resize the window + * * @param block Actions to perform within content space of the window */ @UIBuilder @@ -81,11 +144,17 @@ open class Window( title: String = "Untitled", draggable: Boolean = true, scrollable: Boolean = true, + minimizable: Boolean = true, + resizable: Boolean = true, block: WindowContent.() -> Unit = {} - ) = Window(this, title, draggable, scrollable).apply(children::add).apply { - this.position = position - this.size = size + ) = Window( + this, title, + position, size, + draggable, scrollable, minimizable, resizable + ).apply(children::add).apply { block(this.content) } + + private const val RESIZE_RANGE = 5.0 } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt index b15164a38..e81115a64 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt @@ -13,7 +13,7 @@ class WindowContent( owner: Window, scrollable: Boolean ) : Layout(owner, false, true) { - private val animation = AnimationTicker() + private val animation = animationTicker(false) private var dwheel = 0.0 private var scrollOffset = 0.0 From 82c931e5dd0eaff7ab4ac51c7e8d8e51851e2d64 Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Mon, 30 Sep 2024 00:44:28 +0300 Subject: [PATCH 027/114] Cursor system --- .../lambda/newgui/component/core/TextField.kt | 14 ++-- .../lambda/newgui/component/layout/Layout.kt | 14 +++- .../lambda/newgui/component/window/Window.kt | 70 +++++++++++++------ .../src/main/kotlin/com/lambda/util/Mouse.kt | 49 +++++++++++-- 4 files changed, 114 insertions(+), 33 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt index a513c201f..8905541bd 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt @@ -21,27 +21,27 @@ class TextField( var offset = initialOffset + val textWidth get() = renderer.font.getWidth(text, scale) + val textHeight get() = renderer.font.getHeight(scale) + init { properties.interactionPassthrough = true verticalAlignment = VAlign.CENTER rectUpdate(owner::rect) onRender { - val w = font.getWidth(text, scale) - val h = font.getHeight(scale) - val x = lerp( horizontalAlignment.multiplier, rect.left, - rect.right - w, + rect.right - textWidth, ) - offset * horizontalAlignment.offset val y = when { - verticalAlignment == VAlign.CENTER || rect.size.y <= h -> rect.center.y + verticalAlignment == VAlign.CENTER || rect.size.y <= textHeight -> rect.center.y else -> lerp( verticalAlignment.multiplier, - rect.top + h * 0.5, - rect.bottom - h * 0.5 + rect.top + textHeight * 0.5, + rect.bottom - textHeight * 0.5 ) } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt index 6ddb22b4b..aeeb7e149 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt @@ -106,7 +106,7 @@ open class Layout( // Inputs protected var mousePosition = Vec2d.ZERO - private val isHovered: Boolean get() = mousePosition in rect && (owner?.isHovered ?: true) + protected val isHovered: Boolean get() = mousePosition in rect && (owner?.isHovered ?: true) // Graphics val renderer: RenderLayer = run { @@ -329,5 +329,17 @@ open class Layout( fun Layout.animationTicker(register: Boolean = true) = AnimationTicker().apply { if (register) onTick(this::tick) } + + /** + * Creates new [Mouse.CursorController]. + * + * Use it to set the mouse cursor type for various conditions: hovering, resizing, typing etc... + */ + @UIBuilder + @Suppress("UNUSED_EXPRESSION") + fun Layout.cursorController(): Mouse.CursorController { + this // hack ide to let me make that ui-related only + return Mouse.CursorController() + } } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt index e74b74751..a3ae269fd 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt @@ -31,6 +31,7 @@ open class Window( val content = windowContent(scrollable) private val animation = animationTicker() + private val cursorController = cursorController() // Minimizing var minimized = true @@ -38,6 +39,8 @@ open class Window( // Resizing private var resizeX: Double? = null private var resizeY: Double? = null + private var resizeXHovered = false + private var resizeYHovered = false init { position = initialPosition @@ -60,6 +63,12 @@ open class Window( resizeX = null resizeY = null + resizeXHovered = false + resizeYHovered = false + } + + onHide { + cursorController.reset() } onRender { @@ -79,40 +88,61 @@ open class Window( ) } + onTick { + val rxh = resizeXHovered || resizeX != null + val ryh = resizeYHovered || resizeY != null + + val cursor = when { + rxh && ryh -> Mouse.Cursor.ResizeHV + rxh -> Mouse.Cursor.ResizeH + ryh -> Mouse.Cursor.ResizeV + else -> Mouse.Cursor.Arrow + } + + cursorController.setCursor(cursor) + } + onMouseClick { button: Mouse.Button, action: Mouse.Action -> resizeX = null resizeY = null - if (!resizable) return@onMouseClick - if (selectedChild != null) return@onMouseClick if (button != Mouse.Button.Left || action != Mouse.Action.Click) return@onMouseClick - val resizeXHovered = mousePosition in Rect( - titleBar.rect.rightTop - Vec2d(RESIZE_RANGE, 0.0), - rect.rightBottom - ) - - val resizeYHovered = mousePosition in Rect( - rect.leftBottom - Vec2d(0.0, RESIZE_RANGE), - rect.rightBottom - ) - if (resizeXHovered) resizeX = mousePosition.x - size.x if (resizeYHovered) resizeY = mousePosition.y - size.y } onMouseMove { - if (resizeX == null && resizeY == null) return@onMouseMove + resizeXHovered = false + resizeYHovered = false + + if (!resizable) return@onMouseMove + + // Hover state update + if (selectedChild == null && isHovered) { + resizeXHovered = mousePosition in Rect( + titleBar.rect.rightTop - Vec2d(RESIZE_RANGE, 0.0), + rect.rightBottom + ) + + resizeYHovered = mousePosition in Rect( + rect.leftBottom - Vec2d(0.0, RESIZE_RANGE), + rect.rightBottom + ) + } - val x = resizeX?.let { rx -> - mousePosition.x - rx - } ?: size.x + // Resize + if (resizeX != null || resizeY != null) { + val x = resizeX?.let { rx -> + mousePosition.x - rx + } ?: size.x - val y = resizeY?.let { ry -> - mousePosition.y - ry - } ?: size.y + val y = resizeY?.let { ry -> + mousePosition.y - ry + } ?: size.y - size = Vec2d(x, y).coerceIn(10.0, 1000.0, titleBar.size.y, 1000.0) + size = Vec2d(x, y).coerceIn(80.0, 1000.0, titleBar.size.y + RESIZE_RANGE, 1000.0) + } } } diff --git a/common/src/main/kotlin/com/lambda/util/Mouse.kt b/common/src/main/kotlin/com/lambda/util/Mouse.kt index 01b5c940e..aa01d8888 100644 --- a/common/src/main/kotlin/com/lambda/util/Mouse.kt +++ b/common/src/main/kotlin/com/lambda/util/Mouse.kt @@ -1,21 +1,60 @@ package com.lambda.util -import org.lwjgl.glfw.GLFW +import com.lambda.Lambda.mc +import com.mojang.blaze3d.systems.RenderSystem +import org.lwjgl.glfw.GLFW.* class Mouse { @JvmInline value class Button(val key: Int) { companion object { - val Left = Button(GLFW.GLFW_MOUSE_BUTTON_LEFT) - val Right = Button(GLFW.GLFW_MOUSE_BUTTON_RIGHT) - val Middle = Button(GLFW.GLFW_MOUSE_BUTTON_MIDDLE) + val Left = Button(GLFW_MOUSE_BUTTON_LEFT) + val Right = Button(GLFW_MOUSE_BUTTON_RIGHT) + val Middle = Button(GLFW_MOUSE_BUTTON_MIDDLE) } - val isMainButton get() = key == GLFW.GLFW_MOUSE_BUTTON_LEFT || key == GLFW.GLFW_MOUSE_BUTTON_RIGHT + val isMainButton get() = key == GLFW_MOUSE_BUTTON_LEFT || key == GLFW_MOUSE_BUTTON_RIGHT } enum class Action { Click, Release } + + enum class Cursor(private val getCursorPointer: () -> Long) { + Arrow(::arrow), + Pointer(::pointer), + ResizeH(::resizeH), ResizeV(::resizeV), ResizeHV(::resizeHV); + + fun set() { + if (lastCursor == this) return + lastCursor = this + + RenderSystem.assertOnRenderThread() + glfwSetCursor(mc.window.handle, getCursorPointer()) + } + } + + class CursorController { + private var lastSetCursor: Cursor? = null + + fun setCursor(cursor: Cursor) { + // We're doing this to let other controllers be able to set the cursor when this one doesn't change + if (lastSetCursor == cursor) return + + cursor.set() + lastSetCursor = cursor + } + + fun reset() = setCursor(Cursor.Arrow) + } + + companion object { + private val arrow by lazy { glfwCreateStandardCursor(GLFW_ARROW_CURSOR) } + private val pointer by lazy { glfwCreateStandardCursor(GLFW_POINTING_HAND_CURSOR) } + private val resizeH by lazy { glfwCreateStandardCursor(GLFW_RESIZE_EW_CURSOR) } + private val resizeV by lazy { glfwCreateStandardCursor(GLFW_RESIZE_NS_CURSOR) } + private val resizeHV by lazy { glfwCreateStandardCursor(GLFW_RESIZE_NWSE_CURSOR) } + var lastCursor = Cursor.Arrow + } } \ No newline at end of file From 09fcdc64d1e96c812703b517b0a8e44646784f23 Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Mon, 30 Sep 2024 04:36:37 +0300 Subject: [PATCH 028/114] Window minimization, api changes --- .../kotlin/com/lambda/newgui/LambdaScreen.kt | 8 +- .../lambda/newgui/component/core/TextField.kt | 4 +- .../lambda/newgui/component/layout/Layout.kt | 94 +++++++++++-------- .../newgui/component/window/TitleBar.kt | 5 - .../lambda/newgui/component/window/Window.kt | 66 +++++++++---- .../newgui/component/window/WindowContent.kt | 3 + .../main/kotlin/com/lambda/util/math/Rect.kt | 1 - 7 files changed, 113 insertions(+), 68 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt index 7281f0b4a..be75ac082 100644 --- a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt +++ b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt @@ -119,14 +119,8 @@ class LambdaScreen( @UIBuilder fun gui(block: Layout.() -> Unit) = Layout(owner = null, useBatching = false, batchChildren = true).apply { - var screenSize = Vec2d.ONE * 10000.0 - - rectUpdate { - Rect(Vec2d.ZERO, screenSize) - } - onRender { - screenSize = RenderMain.screenSize + size = RenderMain.screenSize } }.apply(block) diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt index 8905541bd..8a0b37866 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt @@ -27,9 +27,11 @@ class TextField( init { properties.interactionPassthrough = true verticalAlignment = VAlign.CENTER - rectUpdate(owner::rect) onRender { + position = owner.position + size = owner.size + val x = lerp( horizontalAlignment.multiplier, rect.left, diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt index aeeb7e149..1d3999afa 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt @@ -42,10 +42,36 @@ open class Layout( /** * The rectangle of this component */ - val rect get() = Rect.basedOn(position, size) + var rect + get() = Rect.basedOn(actualPosition, actualSize) + set(value) { position = value.leftTop; size = value.size } + + private val actualPosition get() = positionOverride() + private val actualSize get() = sizeOverride() + + /** + * Relative position of the component + */ + var relativePos = Vec2d.ZERO + + /** + * The position of the component + * + * Note: actual position could be overridden using [overridePosition], to get actual position [rect].leftTop instead + */ + var position: Vec2d + get() = ownerRect.leftTop + relativeToAbs(relativePos).let { + if (!properties.clampPosition) it + else it.coerceIn( + 0.0, ownerRect.size.x - actualSize.x, + 0.0, ownerRect.size.y - actualSize.y + ) + }; set(value) { relativePos = absToRelative(value - ownerRect.leftTop) } /** * The size of this component + * + * Note: actual size could be overridden using [overridePosition], to get actual size [rect].size instead */ var size = Vec2d.ZERO @@ -57,7 +83,7 @@ open class Layout( field = to val delta = to.multiplier - from.multiplier - relativePos += Vec2d.RIGHT * delta * (size.x - ownerRect.size.x) + relativePos += Vec2d.RIGHT * delta * (actualSize.x - ownerRect.size.x) } /** @@ -68,30 +94,13 @@ open class Layout( field = to val delta = to.multiplier - from.multiplier - relativePos += Vec2d.BOTTOM * delta * (size.y - ownerRect.size.y) + relativePos += Vec2d.BOTTOM * delta * (actualSize.y - ownerRect.size.y) } - /** - * Relative position of the component - */ - var relativePos = Vec2d.ZERO - - /** - * Absolute(drawn) position of the component - */ - var position: Vec2d - get() = ownerRect.leftTop + relativeToAbs(relativePos).let { - if (!properties.clampPosition) it - else it.coerceIn( - 0.0, ownerRect.size.x - size.x, - 0.0, ownerRect.size.y - size.y - ) - }; set(value) { relativePos = absToRelative(value - ownerRect.leftTop) } - // Rect-related properties private var screenSize = Vec2d.ZERO private val ownerRect get() = owner?.rect ?: Rect(Vec2d.ZERO, screenSize) - private val dockingOffset get() = (ownerRect.size - size) * Vec2d(horizontalAlignment.multiplier, verticalAlignment.multiplier) + private val dockingOffset get() = (ownerRect.size - actualSize) * Vec2d(horizontalAlignment.multiplier, verticalAlignment.multiplier) private fun relativeToAbs(posIn: Vec2d) = posIn + dockingOffset private fun absToRelative(posIn: Vec2d) = posIn - dockingOffset @@ -134,7 +143,8 @@ open class Layout( private var mouseClickActions = mutableListOf<(button: Mouse.Button, action: Mouse.Action) -> Unit>() private var mouseMoveActions = mutableListOf<(mouse: Vec2d) -> Unit>() private var mouseScrollActions = mutableListOf<(delta: Double) -> Unit>() - private var rectUpdate: (() -> Rect)? = null + private var positionOverride: (() -> Vec2d) = { position } + private var sizeOverride: (() -> Vec2d) = { size } /** * Sets the action to be performed when the element gets shown. @@ -218,20 +228,22 @@ open class Layout( } /** - * Sets the rect of the element + * Overrides the drawn position of the component */ - fun rectUpdate(block: () -> Rect) { - rectUpdate = block + fun overridePosition(transform: () -> Vec2d) { + positionOverride = transform + } + + /** + * Overrides the drawn size of the component + */ + fun overrideSize(transform: () -> Vec2d) { + sizeOverride = transform } fun onEvent(e: GuiEvent) { if (e is GuiEvent.Render) { screenSize = RenderMain.screenSize - - rectUpdate?.invoke()?.let { - position = it.leftTop - size = it.size - } } // Select an element that's on foreground @@ -275,22 +287,28 @@ open class Layout( mouseClickActions.forEach { it(e.button, action) } } is GuiEvent.Render -> { - val (pre, post) = children.partition { !it.owningRenderer } + val drawChildren = rect.size.let { it.x > 0.1 && it.y > 0.1 } + val partition by lazy { children.partition { !it.owningRenderer } } - pre.forEach { it.onEvent(e) } renderActions.forEach { it(renderer) } + if (drawChildren) { + partition.first.forEach { it.onEvent(e) } + } + if (owningRenderer) { renderer.render() } - val postAction = { - post.forEach { it.onEvent(e) } - } + if (drawChildren) { + val postAction = { + partition.second.forEach { it.onEvent(e) } + } - if (properties.scissorChildren) { - scissor(rect, postAction) - } else postAction() + if (properties.scissorChildren) { + scissor(rect, postAction) + } else postAction() + } } } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt index 7e996e99d..7b9fc866b 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt @@ -5,7 +5,6 @@ import com.lambda.newgui.component.core.TextField.Companion.textField import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.layout.Layout import com.lambda.util.Mouse -import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d /** @@ -23,10 +22,6 @@ class TitleBar( private var dragOffset: Vec2d? = null init { - rectUpdate { - Rect(owner.rect.leftTop, owner.rect.rightTop + Vec2d(0.0, renderer.font.getHeight() * 1.5)) - } - if (drag) { onShow { dragOffset = null diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt index a3ae269fd..925f78ad8 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt @@ -1,5 +1,6 @@ package com.lambda.newgui.component.window +import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.module.modules.client.NewCGui import com.lambda.newgui.component.VAlign import com.lambda.newgui.component.layout.Layout @@ -33,8 +34,23 @@ open class Window( private val animation = animationTicker() private val cursorController = cursorController() + init { + position = initialPosition + size = initialSize + } + + // Position + /*private val renderX by animation.exp(position::x, 0.8) + private val renderY by animation.exp(position::y, 0.8) + private val renderPosition get() = Vec2d(renderX, renderY)*/ + + // Size + private val renderWidth by animation.exp({ size.x }, 0.8) + private val renderHeight by animation.exp(::targetHeight, 0.8) + private val targetHeight get() = (if (minimized) 0.0 else size.y).coerceAtLeast(titleBar.size.y) + // Minimizing - var minimized = true + var minimized = false // Resizing private var resizeX: Double? = null @@ -43,24 +59,39 @@ open class Window( private var resizeYHovered = false init { - position = initialPosition - size = initialSize - // Clamp the window only within the screen bounds properties.clampPosition = owner.owner == null - onShow { - with(content) { - properties.scissorChildren = true - - rectUpdate { - Rect( - titleBar.rect.leftBottom + NewCGui.padding, - this@Window.rect.rightBottom - NewCGui.padding - ) - } + overrideSize { + Vec2d(renderWidth, renderHeight) + } + + with(titleBar) { + onRender { + val heightVec = Vec2d(0.0, textField.textHeight * 1.5) + rect = Rect(this@Window.rect.leftTop, this@Window.rect.rightTop + heightVec) + } + + onMouseClick { button, action -> + if (!minimizable) return@onMouseClick + if (button != Mouse.Button.Right || action != Mouse.Action.Click) return@onMouseClick + + minimized = !minimized + } + } + + with(content) { + properties.scissorChildren = true + + onRender { + rect = Rect( + titleBar.rect.leftBottom + NewCGui.padding, + this@Window.rect.rightBottom - NewCGui.padding + ) } + } + onShow { resizeX = null resizeY = null resizeXHovered = false @@ -116,7 +147,7 @@ open class Window( resizeXHovered = false resizeYHovered = false - if (!resizable) return@onMouseMove + if (!resizable || minimized) return@onMouseMove // Hover state update if (selectedChild == null && isHovered) { @@ -141,7 +172,10 @@ open class Window( mousePosition.y - ry } ?: size.y - size = Vec2d(x, y).coerceIn(80.0, 1000.0, titleBar.size.y + RESIZE_RANGE, 1000.0) + size = Vec2d(x, y).coerceIn( + 80.0, 1000.0, + titleBar.size.y + RESIZE_RANGE, 1000.0 + ) } } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt index e81115a64..63f9f4736 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt @@ -63,6 +63,9 @@ class WindowContent( } private fun reorderChildren() { + // Skip for closed windows + if (size.y < 0.1) return + var offset = renderScrollOffset scrollableChildren.forEach { child -> diff --git a/common/src/main/kotlin/com/lambda/util/math/Rect.kt b/common/src/main/kotlin/com/lambda/util/math/Rect.kt index 8488fb076..3583ff164 100644 --- a/common/src/main/kotlin/com/lambda/util/math/Rect.kt +++ b/common/src/main/kotlin/com/lambda/util/math/Rect.kt @@ -1,6 +1,5 @@ package com.lambda.util.math -import com.lambda.util.math.lerp import kotlin.math.max import kotlin.math.min From 14caf939d9aca76bc937142f490835cd5b35e3a3 Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Mon, 30 Sep 2024 10:06:50 +0300 Subject: [PATCH 029/114] Corrected scissor usage --- .../lambda/newgui/component/layout/Layout.kt | 33 ++++++++++--------- .../component/layout/LayoutProperties.kt | 4 +-- .../lambda/newgui/component/window/Window.kt | 10 +++++- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt index 1d3999afa..405dbf632 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt @@ -287,28 +287,31 @@ open class Layout( mouseClickActions.forEach { it(e.button, action) } } is GuiEvent.Render -> { - val drawChildren = rect.size.let { it.x > 0.1 && it.y > 0.1 } - val partition by lazy { children.partition { !it.owningRenderer } } + val drawAction = { + val drawChildren = rect.size.let { it.x > 0.1 && it.y > 0.1 } - renderActions.forEach { it(renderer) } + // ToDo: clipping filter to increase performance + // filter { it.rect in this.rect } + val partition by lazy { children.partition { !it.owningRenderer } } - if (drawChildren) { - partition.first.forEach { it.onEvent(e) } - } + renderActions.forEach { it(renderer) } - if (owningRenderer) { - renderer.render() - } + if (drawChildren) { + partition.first.forEach { it.onEvent(e) } + } - if (drawChildren) { - val postAction = { - partition.second.forEach { it.onEvent(e) } + if (owningRenderer) { + renderer.render() } - if (properties.scissorChildren) { - scissor(rect, postAction) - } else postAction() + if (drawChildren) { + partition.second.forEach { it.onEvent(e) } + } } + + if (properties.scissor) { + scissor(rect, drawAction) + } else drawAction() } } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/LayoutProperties.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/LayoutProperties.kt index 0e1b51b8c..cd64ed2f7 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/LayoutProperties.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/LayoutProperties.kt @@ -12,7 +12,7 @@ class LayoutProperties { var clampPosition = false /** - * If true, children using their own render layer are clipped within this rect. + * If true, anything drawn onto this render layer are clipped within this rect. */ - var scissorChildren = false + var scissor = false } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt index 925f78ad8..6b0707f02 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt @@ -40,6 +40,7 @@ open class Window( } // Position + // ToDo find a way to animate this only when dragging /*private val renderX by animation.exp(position::x, 0.8) private val renderY by animation.exp(position::y, 0.8) private val renderPosition get() = Vec2d(renderX, renderY)*/ @@ -68,11 +69,13 @@ open class Window( with(titleBar) { onRender { + // Update title bar position val heightVec = Vec2d(0.0, textField.textHeight * 1.5) rect = Rect(this@Window.rect.leftTop, this@Window.rect.rightTop + heightVec) } onMouseClick { button, action -> + // Toggle minimizing state when right-clicking title bar if (!minimizable) return@onMouseClick if (button != Mouse.Button.Right || action != Mouse.Action.Click) return@onMouseClick @@ -81,9 +84,10 @@ open class Window( } with(content) { - properties.scissorChildren = true + properties.scissor = true onRender { + // Update content position rect = Rect( titleBar.rect.leftBottom + NewCGui.padding, this@Window.rect.rightBottom - NewCGui.padding @@ -103,6 +107,7 @@ open class Window( } onRender { + // Render window background filled.build( rect, 2.0, @@ -110,6 +115,7 @@ open class Window( shade = true ) + // Render outline outline.build( rect, 2.0, @@ -120,6 +126,7 @@ open class Window( } onTick { + // Update cursor val rxh = resizeXHovered || resizeX != null val ryh = resizeYHovered || resizeY != null @@ -134,6 +141,7 @@ open class Window( } onMouseClick { button: Mouse.Button, action: Mouse.Action -> + // Update resize dragging offsets resizeX = null resizeY = null From 9eba76ac24942a21a6230029ba2c010a658c2baf Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Mon, 30 Sep 2024 11:06:30 +0300 Subject: [PATCH 030/114] Titlebar changes, shadow under it --- .../com/lambda/module/modules/client/NewCGui.kt | 2 +- .../lambda/newgui/component/core/TextField.kt | 14 ++++++++++++++ .../lambda/newgui/component/layout/Layout.kt | 4 ++-- .../lambda/newgui/component/window/Window.kt | 5 ++--- .../newgui/component/window/WindowContent.kt | 17 ++++++++++++++--- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt index cfedda385..c8c6023ec 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt @@ -12,7 +12,7 @@ object NewCGui : Module( description = "ggs", defaultTags = setOf(ModuleTag.CLIENT) ) { - val titleBarHeight by setting("Title Bar Height", 4.0, 0.0..10.0, 0.1) + val titleBarHeight by setting("Title Bar Height", 16.0, 0.0..25.0, 0.1) val padding by setting("Padding", 2.0, 1.0..6.0, 0.1) val listStep by setting("List Step", 2.0, 0.0..6.0, 0.1) diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt index 8a0b37866..045c35f5c 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt @@ -1,5 +1,6 @@ package com.lambda.newgui.component.core +import com.lambda.newgui.component.HAlign import com.lambda.newgui.component.VAlign import com.lambda.newgui.component.layout.Layout import com.lambda.util.math.Vec2d @@ -52,6 +53,19 @@ class TextField( } companion object { + /** + * Creates a [TextField] component + * + * @param text String to draw + * + * @param color Color of the font + * + * @param scale Scale of the font + * + * @param shadow Whether the font should drop a shadow + * + * @param offset Offset from the corner(specified by [horizontalAlignment]) of the text (ignored for [HAlign.CENTER]) + */ @UIBuilder fun Layout.textField( text: String, diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt index 405dbf632..5f1f6f72d 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt @@ -57,7 +57,7 @@ open class Layout( /** * The position of the component * - * Note: actual position could be overridden using [overridePosition], to get actual position [rect].leftTop instead + * Note: actual position could be overridden using [overridePosition], to get actual position use [rect].leftTop instead */ var position: Vec2d get() = ownerRect.leftTop + relativeToAbs(relativePos).let { @@ -71,7 +71,7 @@ open class Layout( /** * The size of this component * - * Note: actual size could be overridden using [overridePosition], to get actual size [rect].size instead + * Note: actual size could be overridden using [overrideSize], to get actual size use [rect].size instead */ var size = Vec2d.ZERO diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt index 6b0707f02..7272c7897 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt @@ -70,8 +70,7 @@ open class Window( with(titleBar) { onRender { // Update title bar position - val heightVec = Vec2d(0.0, textField.textHeight * 1.5) - rect = Rect(this@Window.rect.leftTop, this@Window.rect.rightTop + heightVec) + rect = Rect(this@Window.rect.leftTop, this@Window.rect.rightTop + Vec2d.BOTTOM * NewCGui.titleBarHeight) } onMouseClick { button, action -> @@ -89,7 +88,7 @@ open class Window( onRender { // Update content position rect = Rect( - titleBar.rect.leftBottom + NewCGui.padding, + titleBar.rect.leftBottom + Vec2d.RIGHT * NewCGui.padding, this@Window.rect.rightBottom - NewCGui.padding ) } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt index 63f9f4736..50fee948c 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt @@ -1,12 +1,14 @@ package com.lambda.newgui.component.window import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.graphics.animation.AnimationTicker import com.lambda.module.modules.client.NewCGui import com.lambda.newgui.component.VAlign import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.layout.Layout +import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d +import com.lambda.util.math.setAlpha +import java.awt.Color import kotlin.math.abs class WindowContent( @@ -53,6 +55,15 @@ class WindowContent( } onRender { + // Shadow + val topColor = Color.BLACK.setAlpha(0.2) + val bottomColor = Color.BLACK.setAlpha(0.0) + filled.build( + Rect(rect.leftTop, rect.rightTop + Vec2d.BOTTOM * 10.0), 0.0, + topColor, topColor, + bottomColor, bottomColor + ) + reorderChildren() } @@ -66,7 +77,7 @@ class WindowContent( // Skip for closed windows if (size.y < 0.1) return - var offset = renderScrollOffset + var offset = renderScrollOffset + NewCGui.padding scrollableChildren.forEach { child -> child.position = Vec2d(child.position.x, position.y + offset) @@ -78,7 +89,7 @@ class WindowContent( /** * Creates an empty [WindowContent] component * - * @param scrollable Whether to scroll + * @param scrollable Whether to let user scroll this layout */ @UIBuilder fun Window.windowContent(scrollable: Boolean) = From dbbe522e2c0d9964f904526ea02e9f03fb4d8e45 Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Tue, 1 Oct 2024 15:03:14 +0300 Subject: [PATCH 031/114] Windows design --- .../com/lambda/config/groups/Targeting.kt | 2 +- .../buffer/vao/vertex/VertexAttrib.kt | 2 +- .../renderer/gui/rect/FilledRectRenderer.kt | 47 +++++++++++++++-- .../kotlin/com/lambda/gui/api/RenderLayer.kt | 12 +++++ .../lambda/module/modules/client/NewCGui.kt | 14 ++++- .../lambda/newgui/component/core/TextField.kt | 29 +++++------ .../lambda/newgui/component/layout/Layout.kt | 7 +-- .../lambda/newgui/component/window/Window.kt | 51 +++++++++++-------- .../newgui/component/window/WindowContent.kt | 11 +--- .../fragment/renderer/rect_filled.frag | 32 +++++++++++- .../shaders/vertex/renderer/rect_filled.vert | 13 +++-- 11 files changed, 157 insertions(+), 63 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/config/groups/Targeting.kt b/common/src/main/kotlin/com/lambda/config/groups/Targeting.kt index d6f2bd1fe..bc9089f3d 100644 --- a/common/src/main/kotlin/com/lambda/config/groups/Targeting.kt +++ b/common/src/main/kotlin/com/lambda/config/groups/Targeting.kt @@ -143,7 +143,7 @@ abstract class Targeting( return@runSafe entitySearch(targetingRange) { predicate(it) - }.minBy { + }.minByOrNull { priority.factor(this, it) } } diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/VertexAttrib.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/VertexAttrib.kt index 399416eac..c6832ee5a 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/VertexAttrib.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vao/vertex/VertexAttrib.kt @@ -17,7 +17,7 @@ enum class VertexAttrib(val componentCount: Int, componentSize: Int, val normali // GUI FONT(Vec3, Vec2, Color), // pos, uv, color - RECT_FILLED(Vec2, Vec2, Vec2, Float, Float, Color), // pos, uv, size, roundRadius, shade, color + RECT_FILLED(Vec2, Vec2, Vec2, Vec2, Vec2, Float, Color), // pos, uv, size, roundL, roundR, shade, color RECT_OUTLINE(Vec2, Float, Float, Color), // pos, alpha, shade, color // WORLD diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt index c19f128b1..114fd507a 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt @@ -25,6 +25,39 @@ class FilledRectRenderer : AbstractRectRenderer( rightBottom: Color = Color.WHITE, leftBottom: Color = Color.WHITE, shade: Boolean = false, + ) = build( + rect, + roundRadius, roundRadius, roundRadius, roundRadius, + leftTop, rightTop, rightBottom, leftBottom, + shade + ) + + fun build( + rect: Rect, + leftTopRadius: Double = 0.0, + rightTopRadius: Double = 0.0, + rightBottomRadius: Double = 0.0, + leftBottomRadius: Double = 0.0, + color: Color = Color.WHITE, + shade: Boolean = false, + ) = build( + rect, + leftTopRadius, rightTopRadius, rightBottomRadius, leftBottomRadius, + color, color, color, color, + shade + ) + + fun build( + rect: Rect, + leftTopRadius: Double = 0.0, + rightTopRadius: Double = 0.0, + rightBottomRadius: Double = 0.0, + leftBottomRadius: Double = 0.0, + leftTop: Color = Color.WHITE, + rightTop: Color = Color.WHITE, + rightBottom: Color = Color.WHITE, + leftBottom: Color = Color.WHITE, + shade: Boolean = false, ) = vao.use { val pos1 = rect.leftTop val pos2 = rect.rightBottom @@ -36,12 +69,16 @@ class FilledRectRenderer : AbstractRectRenderer( rightBottom.alpha < MIN_ALPHA && leftBottom.alpha < MIN_ALPHA ) return@use + if (size.x < MIN_SIZE || size.y < MIN_SIZE) return@use val halfSize = size * 0.5 val maxRadius = min(halfSize.x, halfSize.y) - val round = roundRadius.coerceAtMost(maxRadius).coerceAtLeast(0.0) + val ltr = leftTopRadius.coerceAtMost(maxRadius).coerceAtLeast(0.0) + val lbr = leftBottomRadius.coerceAtMost(maxRadius).coerceAtLeast(0.0) + val rbr = rightBottomRadius.coerceAtMost(maxRadius).coerceAtLeast(0.0) + val rtr = rightTopRadius.coerceAtMost(maxRadius).coerceAtLeast(0.0) val p1 = pos1 - 0.25 val p2 = pos2 + 0.25 @@ -50,10 +87,10 @@ class FilledRectRenderer : AbstractRectRenderer( grow(4) putQuad( - vec2m(p1.x, p1.y).vec2(0.0, 0.0).vec2(size.x, size.y).float(round).float(s).color(leftTop).end(), - vec2m(p1.x, p2.y).vec2(0.0, 1.0).vec2(size.x, size.y).float(round).float(s).color(leftBottom).end(), - vec2m(p2.x, p2.y).vec2(1.0, 1.0).vec2(size.x, size.y).float(round).float(s).color(rightBottom).end(), - vec2m(p2.x, p1.y).vec2(1.0, 0.0).vec2(size.x, size.y).float(round).float(s).color(rightTop).end() + vec2m(p1.x, p1.y).vec2(0.0, 0.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).color(leftTop).end(), + vec2m(p1.x, p2.y).vec2(0.0, 1.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).color(leftBottom).end(), + vec2m(p2.x, p2.y).vec2(1.0, 1.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).color(rightBottom).end(), + vec2m(p2.x, p1.y).vec2(1.0, 0.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).color(rightTop).end() ) } diff --git a/common/src/main/kotlin/com/lambda/gui/api/RenderLayer.kt b/common/src/main/kotlin/com/lambda/gui/api/RenderLayer.kt index 811f422b8..1d5619c4e 100644 --- a/common/src/main/kotlin/com/lambda/gui/api/RenderLayer.kt +++ b/common/src/main/kotlin/com/lambda/gui/api/RenderLayer.kt @@ -10,6 +10,7 @@ import com.lambda.threading.mainThread class RenderLayer { val filled by mainThread(::FilledRectRenderer) val outline by mainThread(::OutlineRectRenderer) + val font by mainThread { FontRenderer( LambdaFont.FiraSansRegular, @@ -17,9 +18,20 @@ class RenderLayer { ) } + private val boldFont0 = lazy { + FontRenderer( + LambdaFont.FiraSansBold, + LambdaEmoji.Twemoji, + ) + } + + val boldFont by boldFont0 + fun render() { filled.render() outline.render() font.render() + + if (boldFont0.isInitialized()) boldFont0.value.render() } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt index c8c6023ec..9490080b5 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt @@ -6,16 +6,28 @@ import com.lambda.newgui.LambdaScreen.Companion.gui import com.lambda.newgui.LambdaScreen.Companion.toScreen import com.lambda.newgui.component.window.Window.Companion.window import com.lambda.util.math.Vec2d +import com.lambda.util.math.setAlpha +import java.awt.Color object NewCGui : Module( name = "NewCGui", description = "ggs", defaultTags = setOf(ModuleTag.CLIENT) ) { - val titleBarHeight by setting("Title Bar Height", 16.0, 0.0..25.0, 0.1) + val titleBarHeight by setting("Title Bar Height", 18.0, 0.0..25.0, 0.1) val padding by setting("Padding", 2.0, 1.0..6.0, 0.1) val listStep by setting("List Step", 2.0, 0.0..6.0, 0.1) + val roundRadius by setting("Round Radius", 2.0, 0.0..10.0, 0.1) + + val backgroundColor by setting("Background Color", Color.WHITE.setAlpha(0.4)) + val backgroundShade by setting("Background Shade", true) + + val outline by setting("Outline", true) + val outlineWidth by setting("Outline Width", 10.0, 1.0..10.0, 0.1) { outline } + val outlineColor by setting("Outline Color", Color.WHITE.setAlpha(0.6)) { outline } + val outlineShade by setting("Outline Shade", true) { outline } + private val clickGuiLayout = gui { window(position = Vec2d.ONE * 20.0, title = "Test window") { diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt index 045c35f5c..84fd9b2ac 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt @@ -9,21 +9,17 @@ import java.awt.Color class TextField( owner: Layout, - initialText: String, - initialColor: Color = Color.WHITE, - initialScale: Double = 1.0, - initialShadow: Boolean = true, - initialOffset: Double = 0.0, + var text: String, + var color: Color, + var scale: Double, + var bold: Boolean, + var shadow: Boolean, + var offset: Double, ) : Layout(owner, true, true) { - var text = initialText - var color = initialColor - var scale = initialScale - var shadow = initialShadow + val textWidth get() = fr.getWidth(text, scale) + val textHeight get() = fr.getHeight(scale) - var offset = initialOffset - - val textWidth get() = renderer.font.getWidth(text, scale) - val textHeight get() = renderer.font.getHeight(scale) + private val fr get() = if (bold) renderer.boldFont else renderer.font init { properties.interactionPassthrough = true @@ -48,7 +44,7 @@ class TextField( ) } - font.build(text, Vec2d(x, y), color, scale, shadow) + fr.build(text, Vec2d(x, y), color, scale, shadow) } } @@ -62,6 +58,8 @@ class TextField( * * @param scale Scale of the font * + * @param bold Whether to use the bold variant of the font + * * @param shadow Whether the font should drop a shadow * * @param offset Offset from the corner(specified by [horizontalAlignment]) of the text (ignored for [HAlign.CENTER]) @@ -71,9 +69,10 @@ class TextField( text: String, color: Color = Color.WHITE, scale: Double = 1.0, + bold: Boolean = false, shadow: Boolean = true, offset: Double = 0.0, block: TextField.() -> Unit = {} - ) = TextField(this, text, color, scale, shadow, offset).apply(children::add).apply(block) + ) = TextField(this, text, color, scale, bold, shadow, offset).apply(children::add).apply(block) } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt index 5f1f6f72d..df95673b7 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt @@ -290,9 +290,10 @@ open class Layout( val drawAction = { val drawChildren = rect.size.let { it.x > 0.1 && it.y > 0.1 } - // ToDo: clipping filter to increase performance - // filter { it.rect in this.rect } - val partition by lazy { children.partition { !it.owningRenderer } } + val partition by lazy { + children.filter { properties.scissor || it.rect in this.rect } + .partition { !it.owningRenderer } + } renderActions.forEach { it(renderer) } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt index 7272c7897..b6be411d6 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt @@ -11,7 +11,6 @@ import com.lambda.util.Mouse import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d import com.lambda.util.math.coerceIn -import java.awt.Color /** * Represents a window component @@ -26,7 +25,8 @@ open class Window( draggable: Boolean, scrollable: Boolean, private val minimizable: Boolean, - private val resizable: Boolean + private val resizable: Boolean, + clean: Boolean ) : Layout(owner, false, true) { val titleBar = titleBar(initialTitle, draggable) val content = windowContent(scrollable) @@ -68,6 +68,11 @@ open class Window( } with(titleBar) { + textField.apply { + bold = true + shadow = false + } + onRender { // Update title bar position rect = Rect(this@Window.rect.leftTop, this@Window.rect.rightTop + Vec2d.BOTTOM * NewCGui.titleBarHeight) @@ -105,23 +110,25 @@ open class Window( cursorController.reset() } - onRender { - // Render window background - filled.build( - rect, - 2.0, - Color(50, 50, 50), - shade = true - ) - - // Render outline - outline.build( - rect, - 2.0, - 1.0, - Color.WHITE, - true - ) + if (!clean) { + onRender { + // Render window background + filled.build(rect, NewCGui.roundRadius, NewCGui.backgroundColor, NewCGui.backgroundShade) + + // Render window outline + if (NewCGui.outline) { + outline.build(rect, NewCGui.roundRadius, NewCGui.outlineWidth, NewCGui.outlineColor, NewCGui.outlineShade) + } + + // Shadow + /*val topColor = Color.BLACK.setAlpha(0.15) + val bottomColor = Color.BLACK.setAlpha(0.0) + filled.build( + Rect(titleBar.rect.leftBottom, titleBar.rect.rightBottom + Vec2d.BOTTOM * 7.0), 0.0, + topColor, topColor, + bottomColor, bottomColor + )*/ + } } onTick { @@ -206,6 +213,8 @@ open class Window( * * @param resizable Whether to allow user to resize the window * + * @param clean Whether to skip the background rendering + * * @param block Actions to perform within content space of the window */ @UIBuilder @@ -217,11 +226,13 @@ open class Window( scrollable: Boolean = true, minimizable: Boolean = true, resizable: Boolean = true, + clean: Boolean = false, block: WindowContent.() -> Unit = {} ) = Window( this, title, position, size, - draggable, scrollable, minimizable, resizable + draggable, scrollable, minimizable, resizable, + clean ).apply(children::add).apply { block(this.content) } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt index 50fee948c..1ce566aed 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt @@ -26,9 +26,9 @@ class WindowContent( init { onShow { + dwheel = 0.0 scrollOffset = 0.0 rubberbandDelta = 0.0 - dwheel = 0.0 renderScrollOffset = 0.0 reorderChildren() @@ -55,15 +55,6 @@ class WindowContent( } onRender { - // Shadow - val topColor = Color.BLACK.setAlpha(0.2) - val bottomColor = Color.BLACK.setAlpha(0.0) - filled.build( - Rect(rect.leftTop, rect.rightTop + Vec2d.BOTTOM * 10.0), 0.0, - topColor, topColor, - bottomColor, bottomColor - ) - reorderChildren() } diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag index 306a00247..9e0750d46 100644 --- a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag +++ b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag @@ -9,7 +9,8 @@ in vec2 v_Position; in vec2 v_TexCoord; in vec4 v_Color; in vec2 v_Size; -in float v_RoundRadius; +in vec2 v_RoundRadiusL; +in vec2 v_RoundRadiusR; in float v_Shade; out vec4 color; @@ -33,10 +34,37 @@ vec4 shade() { return mix(u_Color1, u_Color2, p) * v_Color; } +float getRoundRadius() { + bool xcmp = v_TexCoord.x > 0.5; + bool ycmp = v_TexCoord.y > 0.5; + + float r = 0.0; + + if (xcmp) { + if (ycmp) { + // Right bottom + r = v_RoundRadiusR.y; + } else { + // Right top + r = v_RoundRadiusR.x; + } + } else { + if (ycmp) { + // Left bottom + r = v_RoundRadiusL.y; + } else { + // Left top + r = v_RoundRadiusL.x; + } + } + + return r; +} + vec4 round() { vec2 halfSize = v_Size * 0.5; - float radius = max(v_RoundRadius, SMOOTHING); + float radius = max(getRoundRadius(), SMOOTHING); vec2 smoothVec = vec2(SMOOTHING); vec2 coord = mix(-smoothVec, v_Size + smoothVec, v_TexCoord); diff --git a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert index e52b0bda8..4acbe1f18 100644 --- a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert +++ b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert @@ -3,9 +3,10 @@ layout (location = 0) in vec4 pos; layout (location = 1) in vec2 uv; layout (location = 2) in vec2 size; -layout (location = 3) in float round; -layout (location = 4) in float shade; -layout (location = 5) in vec4 color; +layout (location = 3) in vec2 roundL; +layout (location = 4) in vec2 roundR; +layout (location = 5) in float shade; +layout (location = 6) in vec4 color; uniform mat4 u_ProjModel; @@ -13,7 +14,8 @@ out vec2 v_Position; out vec2 v_TexCoord; out vec4 v_Color; out vec2 v_Size; -out float v_RoundRadius; +out vec2 v_RoundRadiusL; +out vec2 v_RoundRadiusR; out float v_Shade; void main() { @@ -24,6 +26,7 @@ void main() { v_Color = color; v_Size = size; - v_RoundRadius = round; + v_RoundRadiusL = roundL; + v_RoundRadiusR = roundR; v_Shade = shade; } \ No newline at end of file From b476148643e64cc735b0d60fcebeea46956f1c51 Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Wed, 2 Oct 2024 02:09:18 +0300 Subject: [PATCH 032/114] Massive gc troll fix, rect/outline/font layouts, simple module buttons --- .../lambda/graphics/animation/Animation.kt | 6 +- .../lambda/module/modules/client/NewCGui.kt | 29 ++- .../newgui/component/core/FilledRect.kt | 74 ++++++++ .../newgui/component/core/OutlineRect.kt | 63 +++++++ .../lambda/newgui/component/core/TextField.kt | 66 +++---- .../lambda/newgui/component/layout/Layout.kt | 166 +++++++++++------- .../newgui/component/window/TitleBar.kt | 38 ++-- .../lambda/newgui/component/window/Window.kt | 158 +++++++++-------- .../newgui/component/window/WindowContent.kt | 39 ++-- .../newgui/impl/clickgui/ModuleLayout.kt | 90 ++++++++++ .../src/main/kotlin/com/lambda/util/Mouse.kt | 2 +- .../main/kotlin/com/lambda/util/math/Rect.kt | 8 +- 12 files changed, 511 insertions(+), 228 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt create mode 100644 common/src/main/kotlin/com/lambda/newgui/component/core/OutlineRect.kt create mode 100644 common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt diff --git a/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt b/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt index dbbd88edf..b08f4bc8f 100644 --- a/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt +++ b/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt @@ -10,11 +10,11 @@ class Animation(initialValue: Double, val update: (Double) -> Double) { private var prevValue = initialValue private var currValue = initialValue - operator fun getValue(thisRef: Any?, property: KProperty<*>) = - lerp(mc.partialTicks, prevValue, currValue) - + operator fun getValue(thisRef: Any?, property: KProperty<*>) = value() operator fun setValue(thisRef: Any?, property: KProperty<*>, valueIn: Double) = setValue(valueIn) + fun value(): Double = lerp(mc.partialTicks, prevValue, currValue) + fun setValue(valueIn: Double) { prevValue = valueIn currValue = valueIn diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt index 9490080b5..4d6cd09b1 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt @@ -1,10 +1,13 @@ package com.lambda.module.modules.client import com.lambda.module.Module +import com.lambda.module.ModuleRegistry import com.lambda.module.tag.ModuleTag import com.lambda.newgui.LambdaScreen.Companion.gui import com.lambda.newgui.LambdaScreen.Companion.toScreen +import com.lambda.newgui.component.window.Window import com.lambda.newgui.component.window.Window.Companion.window +import com.lambda.newgui.impl.clickgui.ModuleLayout.Companion.moduleLayout import com.lambda.util.math.Vec2d import com.lambda.util.math.setAlpha import java.awt.Color @@ -17,10 +20,12 @@ object NewCGui : Module( val titleBarHeight by setting("Title Bar Height", 18.0, 0.0..25.0, 0.1) val padding by setting("Padding", 2.0, 1.0..6.0, 0.1) val listStep by setting("List Step", 2.0, 0.0..6.0, 0.1) + val autoResize by setting("Auto Resize", false) val roundRadius by setting("Round Radius", 2.0, 0.0..10.0, 0.1) - val backgroundColor by setting("Background Color", Color.WHITE.setAlpha(0.4)) + val titleBackgroundColor by setting("Title Background Color", Color.WHITE.setAlpha(0.4)) + val backgroundColor by setting("Background Color", Color.WHITE.setAlpha(0.2)) val backgroundShade by setting("Background Shade", true) val outline by setting("Outline", true) @@ -28,22 +33,28 @@ object NewCGui : Module( val outlineColor by setting("Outline Color", Color.WHITE.setAlpha(0.6)) { outline } val outlineShade by setting("Outline Shade", true) { outline } - private val clickGuiLayout = + private val SCREEN by lazy { gui { - window(position = Vec2d.ONE * 20.0, title = "Test window") { - repeat(6) { - window(Vec2d.ONE * 5.0, Vec2d.ONE * 60.0) { + val tags = ModuleTag.defaults + val modules = ModuleRegistry.modules + tags.forEachIndexed { i, tag -> + val windowPosition = Vec2d.ONE * 20.0 + Vec2d.RIGHT * ((115.0 * i) + (i + 1) * 4) + + window(position = windowPosition, title = tag.name, autoResize = Window.AutoResize.ByConfig) { + val tagModules = modules.filter { it.defaultTags.first() == tag } + + tagModules.forEach { module -> + moduleLayout(module) } } } - } - - val CLICK_GUI = clickGuiLayout.toScreen("New Click Gui") + }.toScreen("New Click Gui") + } init { onEnable { - CLICK_GUI.show() + SCREEN.show() } } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt new file mode 100644 index 000000000..ebb1901d5 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt @@ -0,0 +1,74 @@ +package com.lambda.newgui.component.core + +import com.lambda.newgui.component.layout.Layout +import java.awt.Color + +class FilledRect( + owner: Layout +) : Layout(owner, true, true) { + var rectangle = owner.rect + + var leftTopRadius = 0.0 + var rightTopRadius = 0.0 + var rightBottomRadius = 0.0 + var leftBottomRadius = 0.0 + + var leftTopColor: Color = Color.WHITE + var rightTopColor: Color = Color.WHITE + var rightBottomColor: Color = Color.WHITE + var leftBottomColor: Color = Color.WHITE + + var shade = false + + private val updateActions = mutableListOf Unit>() + + fun onUpdate(block: FilledRect.() -> Unit) { + updateActions += block + } + + init { + onRender { + updateActions.forEach { action -> + action(this@FilledRect) + } + + filled.build( + rectangle, + leftTopRadius, + rightTopRadius, + rightBottomRadius, + leftBottomRadius, + leftTopColor, + rightTopColor, + rightBottomColor, + leftBottomColor, + shade + ) + } + } + + fun setRadius(radius: Double) { + leftTopRadius = radius + rightTopRadius = radius + rightBottomRadius = radius + leftBottomRadius = radius + } + + fun setColor(color: Color) { + leftTopColor = color + rightTopColor = color + rightBottomColor = color + leftBottomColor = color + } + + companion object { + /** + * Creates a [FilledRect] component - layout-based rect representation + */ + @UIBuilder + fun Layout.rect(block: FilledRect.() -> Unit = {}) = + FilledRect(this).apply(children::add).apply { + updateActions += block + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/OutlineRect.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/OutlineRect.kt new file mode 100644 index 000000000..e2f32b826 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/OutlineRect.kt @@ -0,0 +1,63 @@ +package com.lambda.newgui.component.core + +import com.lambda.newgui.component.layout.Layout +import java.awt.Color + +class OutlineRect( + owner: Layout +) : Layout(owner, true, true) { + var rectangle = owner.rect + + var roundRadius = 0.0 + var glowRadius = 0.0 + + var leftTopColor: Color = Color.WHITE + var rightTopColor: Color = Color.WHITE + var rightBottomColor: Color = Color.WHITE + var leftBottomColor: Color = Color.WHITE + + var shade = false + + private val updateActions = mutableListOf Unit>() + + fun onUpdate(block: OutlineRect.() -> Unit) { + updateActions += block + } + + init { + onRender { + updateActions.forEach { action -> + action(this@OutlineRect) + } + + outline.build( + rectangle, + roundRadius, + glowRadius, + leftTopColor, + rightTopColor, + rightBottomColor, + leftBottomColor, + shade + ) + } + } + + fun setColor(color: Color) { + leftTopColor = color + rightTopColor = color + rightBottomColor = color + leftBottomColor = color + } + + companion object { + /** + * Creates a [OutlineRect] component - layout-based rect representation + */ + @UIBuilder + fun Layout.outline(block: OutlineRect.() -> Unit = {}) = + OutlineRect(this).apply(children::add).apply { + updateActions += block + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt index 84fd9b2ac..6fd5a8b83 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt @@ -1,78 +1,52 @@ package com.lambda.newgui.component.core -import com.lambda.newgui.component.HAlign -import com.lambda.newgui.component.VAlign import com.lambda.newgui.component.layout.Layout import com.lambda.util.math.Vec2d -import com.lambda.util.math.lerp import java.awt.Color class TextField( owner: Layout, - var text: String, - var color: Color, - var scale: Double, - var bold: Boolean, - var shadow: Boolean, - var offset: Double, ) : Layout(owner, true, true) { + var text = "" + var color = Color.WHITE + var scale = 1.0 + var bold = false + var shadow = true + val textWidth get() = fr.getWidth(text, scale) val textHeight get() = fr.getHeight(scale) private val fr get() = if (bold) renderer.boldFont else renderer.font + private val updateActions = mutableListOf Unit>() + + fun onUpdate(block: TextField.() -> Unit) { + updateActions += block + } + init { properties.interactionPassthrough = true - verticalAlignment = VAlign.CENTER onRender { - position = owner.position - size = owner.size - - val x = lerp( - horizontalAlignment.multiplier, - rect.left, - rect.right - textWidth, - ) - offset * horizontalAlignment.offset - - val y = when { - verticalAlignment == VAlign.CENTER || rect.size.y <= textHeight -> rect.center.y - else -> lerp( - verticalAlignment.multiplier, - rect.top + textHeight * 0.5, - rect.bottom - textHeight * 0.5 - ) + updateActions.forEach { action -> + action(this@TextField) } - fr.build(text, Vec2d(x, y), color, scale, shadow) + width = textWidth + height = textHeight + + val renderPos = Vec2d(renderPositionX, renderPositionY + renderHeight * 0.5) + fr.build(text, renderPos, color, scale, shadow) } } companion object { /** * Creates a [TextField] component - * - * @param text String to draw - * - * @param color Color of the font - * - * @param scale Scale of the font - * - * @param bold Whether to use the bold variant of the font - * - * @param shadow Whether the font should drop a shadow - * - * @param offset Offset from the corner(specified by [horizontalAlignment]) of the text (ignored for [HAlign.CENTER]) */ @UIBuilder fun Layout.textField( - text: String, - color: Color = Color.WHITE, - scale: Double = 1.0, - bold: Boolean = false, - shadow: Boolean = true, - offset: Double = 0.0, block: TextField.() -> Unit = {} - ) = TextField(this, text, color, scale, bold, shadow, offset).apply(children::add).apply(block) + ) = TextField(this).apply(children::add).apply(block) } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt index df95673b7..0e26a4aef 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt @@ -12,7 +12,6 @@ import com.lambda.util.KeyCode import com.lambda.util.Mouse import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d -import com.lambda.util.math.coerceIn /** * Represents a component for creating complex ui structures. @@ -39,70 +38,80 @@ open class Layout( useBatching: Boolean, private val batchChildren: Boolean, ) { - /** - * The rectangle of this component - */ - var rect - get() = Rect.basedOn(actualPosition, actualSize) - set(value) { position = value.leftTop; size = value.size } - - private val actualPosition get() = positionOverride() - private val actualSize get() = sizeOverride() - - /** - * Relative position of the component - */ - var relativePos = Vec2d.ZERO + val rect get() = Rect.basedOn(renderPosition, renderSize) - /** - * The position of the component - * - * Note: actual position could be overridden using [overridePosition], to get actual position use [rect].leftTop instead - */ var position: Vec2d - get() = ownerRect.leftTop + relativeToAbs(relativePos).let { - if (!properties.clampPosition) it - else it.coerceIn( - 0.0, ownerRect.size.x - actualSize.x, - 0.0, ownerRect.size.y - actualSize.y - ) - }; set(value) { relativePos = absToRelative(value - ownerRect.leftTop) } - - /** - * The size of this component - * - * Note: actual size could be overridden using [overrideSize], to get actual size use [rect].size instead - */ - var size = Vec2d.ZERO + get() = Vec2d(positionX, positionY) + set(value) { positionX = value.x; positionY = value.y } + + var positionX: Double + get() = ownerX + (relativePosX + dockingOffsetX).let { + if (!properties.clampPosition) return@let it + it.coerceAtMost(ownerWidth - renderWidth).coerceAtLeast(0.0) + }; set(value) { relativePosX = value - ownerX - dockingOffsetX } + + var positionY: Double + get() = ownerY + (relativePosY + dockingOffsetY).let { + if (!properties.clampPosition) return@let it + it.coerceAtMost(ownerHeight - renderHeight).coerceAtLeast(0.0) + }; set(value) { relativePosY = value - ownerY - dockingOffsetY } + + var size: Vec2d + get() = Vec2d(width, height) + set(value) { width = value.x; height = value.y } + + val leftTop get() = position + val rightTop get() = Vec2d(renderPositionX + renderWidth, renderPositionY) + val rightBottom get() = Vec2d(renderPositionX + renderWidth, renderPositionY + renderHeight) + val leftBottom get() = Vec2d(renderPositionX, renderPositionY + renderHeight) + + var width = 0.0 + var height = 0.0 + + val renderPosition get() = Vec2d(renderPositionX, renderPositionY) + val renderPositionX get() = positionXTransform() + val renderPositionY get() = positionYTransform() + private var positionXTransform = { positionX } + private var positionYTransform = { positionY } + + val renderSize get() = Vec2d(renderWidth, renderHeight) + val renderWidth get() = widthTransform() + val renderHeight get() = heightTransform() + private var widthTransform = { width } + private var heightTransform = { height } + + private var relativePosX = 0.0 + private var relativePosY = 0.0 - /** - * Horizontal alignment - */ var horizontalAlignment = HAlign.LEFT; set(to) { val from = field field = to val delta = to.multiplier - from.multiplier - relativePos += Vec2d.RIGHT * delta * (actualSize.x - ownerRect.size.x) + relativePosX += delta * (renderWidth - ownerWidth) } - /** - * Vertical alignment - */ var verticalAlignment = VAlign.TOP; set(to) { val from = field field = to val delta = to.multiplier - from.multiplier - relativePos += Vec2d.BOTTOM * delta * (actualSize.y - ownerRect.size.y) + relativePosY += delta * (renderHeight - ownerHeight) } - // Rect-related properties private var screenSize = Vec2d.ZERO - private val ownerRect get() = owner?.rect ?: Rect(Vec2d.ZERO, screenSize) - private val dockingOffset get() = (ownerRect.size - actualSize) * Vec2d(horizontalAlignment.multiplier, verticalAlignment.multiplier) - private fun relativeToAbs(posIn: Vec2d) = posIn + dockingOffset - private fun absToRelative(posIn: Vec2d) = posIn - dockingOffset + + private var ownerX = 0.0 + private var ownerY = 0.0 + + private var ownerWidth = 0.0 + private var ownerHeight = 0.0 + + private val dockingOffsetX get() = if (horizontalAlignment == HAlign.LEFT) 0.0 + else (ownerWidth - renderWidth) * horizontalAlignment.multiplier + + private val dockingOffsetY get() = if (verticalAlignment == VAlign.TOP) 0.0 + else (ownerHeight - renderHeight) * verticalAlignment.multiplier /** * Configurable properties of the component @@ -115,7 +124,7 @@ open class Layout( // Inputs protected var mousePosition = Vec2d.ZERO - protected val isHovered: Boolean get() = mousePosition in rect && (owner?.isHovered ?: true) + var isHovered = false; get() = field && (owner?.isHovered ?: true) // Graphics val renderer: RenderLayer = run { @@ -143,8 +152,6 @@ open class Layout( private var mouseClickActions = mutableListOf<(button: Mouse.Button, action: Mouse.Action) -> Unit>() private var mouseMoveActions = mutableListOf<(mouse: Vec2d) -> Unit>() private var mouseScrollActions = mutableListOf<(delta: Double) -> Unit>() - private var positionOverride: (() -> Vec2d) = { position } - private var sizeOverride: (() -> Vec2d) = { size } /** * Sets the action to be performed when the element gets shown. @@ -228,22 +235,62 @@ open class Layout( } /** - * Overrides the drawn position of the component + * Force overrides drawn x position of the layout + */ + fun overrideX(transform: () -> Double) { + positionXTransform = transform + } + + /** + * Force overrides drawn y position of the layout + */ + fun overrideY(transform: () -> Double) { + positionYTransform = transform + } + + /** + * Force overrides drawn position of the layout + */ + fun overridePosition(x: () -> Double, y: () -> Double) { + positionXTransform = x + positionYTransform = y + } + + /** + * Force overrides drawn width of the layout + */ + fun overrideWidth(transform: () -> Double) { + widthTransform = transform + } + + /** + * Force overrides drawn height of the layout */ - fun overridePosition(transform: () -> Vec2d) { - positionOverride = transform + fun overrideHeight(transform: () -> Double) { + heightTransform = transform } /** - * Overrides the drawn size of the component + * Force overrides drawn size of the layout */ - fun overrideSize(transform: () -> Vec2d) { - sizeOverride = transform + fun overrideSize(width: () -> Double, height: () -> Double) { + widthTransform = width + heightTransform = height } fun onEvent(e: GuiEvent) { if (e is GuiEvent.Render) { screenSize = RenderMain.screenSize + + ownerX = owner?.renderPositionX ?: ownerX + ownerY = owner?.renderPositionY ?: ownerY + + ownerWidth = owner?.renderWidth ?: screenSize.x + ownerHeight = owner?.renderHeight ?: screenSize.y + + val xh = (mousePosition.x - renderPositionX) in 0.0..renderWidth + val yh = (mousePosition.y - renderPositionY) in 0.0..renderHeight + isHovered = xh && yh } // Select an element that's on foreground @@ -288,11 +335,10 @@ open class Layout( } is GuiEvent.Render -> { val drawAction = { - val drawChildren = rect.size.let { it.x > 0.1 && it.y > 0.1 } + val drawChildren = renderWidth > 0.1 && renderHeight > 0.1 val partition by lazy { - children.filter { properties.scissor || it.rect in this.rect } - .partition { !it.owningRenderer } + children.partition { !it.owningRenderer } } renderActions.forEach { it(renderer) } @@ -360,7 +406,7 @@ open class Layout( @UIBuilder @Suppress("UNUSED_EXPRESSION") fun Layout.cursorController(): Mouse.CursorController { - this // hack ide to let me make that ui-related only + this // hack ide to let me make this ui-related only return Mouse.CursorController() } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt index 7b9fc866b..5400d9b56 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt @@ -1,6 +1,8 @@ package com.lambda.newgui.component.window +import com.lambda.module.modules.client.NewCGui import com.lambda.newgui.component.HAlign +import com.lambda.newgui.component.VAlign import com.lambda.newgui.component.core.TextField.Companion.textField import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.layout.Layout @@ -15,28 +17,36 @@ class TitleBar( title: String, drag: Boolean ) : Layout(owner, true, true) { - val textField = textField(title) { + val textField = textField { + text = title + bold = true + horizontalAlignment = HAlign.CENTER + verticalAlignment = VAlign.CENTER + + onUpdate { + val tb = this@TitleBar + positionX = tb.renderPositionX + tb.renderWidth * 0.5 - textWidth * 0.5 + positionY = tb.renderPositionY + tb.renderHeight * 0.5 - textHeight * 0.5 + } } private var dragOffset: Vec2d? = null init { - if (drag) { - onShow { - dragOffset = null - } + onShow { + dragOffset = null + } - onMouseClick { button: Mouse.Button, action: Mouse.Action -> - dragOffset = if (button == Mouse.Button.Left && action == Mouse.Action.Click) { - mousePosition - owner.position - } else null - } + onMouseClick { button: Mouse.Button, action: Mouse.Action -> + dragOffset = if (drag && button == Mouse.Button.Left && action == Mouse.Action.Click) { + mousePosition - owner.position + } else null + } - onMouseMove { mouse -> - dragOffset?.let { drag -> - owner.position = mouse - drag - } + onMouseMove { mouse -> + dragOffset?.let { drag -> + owner.position = mouse - drag } } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt index b6be411d6..998686430 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt @@ -3,14 +3,17 @@ package com.lambda.newgui.component.window import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.module.modules.client.NewCGui import com.lambda.newgui.component.VAlign +import com.lambda.newgui.component.core.FilledRect.Companion.rect +import com.lambda.newgui.component.core.OutlineRect.Companion.outline import com.lambda.newgui.component.layout.Layout import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.window.TitleBar.Companion.titleBar import com.lambda.newgui.component.window.WindowContent.Companion.windowContent import com.lambda.util.Mouse +import com.lambda.util.math.MathUtils.toInt import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d -import com.lambda.util.math.coerceIn +import com.lambda.util.math.lerp /** * Represents a window component @@ -26,17 +29,46 @@ open class Window( scrollable: Boolean, private val minimizable: Boolean, private val resizable: Boolean, - clean: Boolean -) : Layout(owner, false, true) { + val autoResize: AutoResize, + useBatching: Boolean, +) : Layout(owner, useBatching, true) { + private val animation = animationTicker() + private val cursorController = cursorController() + val titleBar = titleBar(initialTitle, draggable) val content = windowContent(scrollable) - private val animation = animationTicker() - private val cursorController = cursorController() + protected val titleBarRect = rect { + rectangle = titleBar.rect + setColor(NewCGui.titleBackgroundColor) - init { - position = initialPosition - size = initialSize + val radius = NewCGui.roundRadius + leftTopRadius = radius + rightTopRadius = radius + leftBottomRadius = radius * (1 - minimizeAnimation) + rightBottomRadius = radius * (1 - minimizeAnimation) + + shade = NewCGui.backgroundShade + } + + protected val contentRect = rect { + rectangle = Rect(titleBar.leftBottom, this@Window.rightBottom) + setColor(NewCGui.backgroundColor) + + leftBottomRadius = NewCGui.roundRadius + rightBottomRadius = NewCGui.roundRadius + + shade = NewCGui.backgroundShade + } + + protected val outlineRect = outline { + rectangle = this@Window.rect + setColor(NewCGui.outlineColor) + + roundRadius = NewCGui.roundRadius + glowRadius = NewCGui.outlineWidth * NewCGui.outline.toInt().toDouble() + + shade = NewCGui.outlineShade } // Position @@ -45,13 +77,9 @@ open class Window( private val renderY by animation.exp(position::y, 0.8) private val renderPosition get() = Vec2d(renderX, renderY)*/ - // Size - private val renderWidth by animation.exp({ size.x }, 0.8) - private val renderHeight by animation.exp(::targetHeight, 0.8) - private val targetHeight get() = (if (minimized) 0.0 else size.y).coerceAtLeast(titleBar.size.y) - // Minimizing var minimized = false + private val minimizeAnimation by animation.exp(0.0, 1.0, 0.8) { !minimized } // Resizing private var resizeX: Double? = null @@ -60,23 +88,25 @@ open class Window( private var resizeYHovered = false init { - // Clamp the window only within the screen bounds - properties.clampPosition = owner.owner == null + position = initialPosition + size = initialSize + + overrideWidth(animation.exp(::width, 0.8)::value) - overrideSize { - Vec2d(renderWidth, renderHeight) + overrideHeight { + val rawHeight = if (!autoResize.enabled) height + else titleBar.renderHeight + content.getContentHeight() + lerp(minimizeAnimation, titleBar.renderHeight, rawHeight) } - with(titleBar) { - textField.apply { - bold = true - shadow = false - } + properties.clampPosition = owner.owner == null + content.properties.scissor = true - onRender { - // Update title bar position - rect = Rect(this@Window.rect.leftTop, this@Window.rect.rightTop + Vec2d.BOTTOM * NewCGui.titleBarHeight) - } + with(titleBar) { + overrideSize( + this@Window::renderWidth, + NewCGui::titleBarHeight + ) onMouseClick { button, action -> // Toggle minimizing state when right-clicking title bar @@ -87,18 +117,6 @@ open class Window( } } - with(content) { - properties.scissor = true - - onRender { - // Update content position - rect = Rect( - titleBar.rect.leftBottom + Vec2d.RIGHT * NewCGui.padding, - this@Window.rect.rightBottom - NewCGui.padding - ) - } - } - onShow { resizeX = null resizeY = null @@ -110,27 +128,6 @@ open class Window( cursorController.reset() } - if (!clean) { - onRender { - // Render window background - filled.build(rect, NewCGui.roundRadius, NewCGui.backgroundColor, NewCGui.backgroundShade) - - // Render window outline - if (NewCGui.outline) { - outline.build(rect, NewCGui.roundRadius, NewCGui.outlineWidth, NewCGui.outlineColor, NewCGui.outlineShade) - } - - // Shadow - /*val topColor = Color.BLACK.setAlpha(0.15) - val bottomColor = Color.BLACK.setAlpha(0.0) - filled.build( - Rect(titleBar.rect.leftBottom, titleBar.rect.rightBottom + Vec2d.BOTTOM * 7.0), 0.0, - topColor, topColor, - bottomColor, bottomColor - )*/ - } - } - onTick { // Update cursor val rxh = resizeXHovered || resizeX != null @@ -153,8 +150,8 @@ open class Window( if (button != Mouse.Button.Left || action != Mouse.Action.Click) return@onMouseClick - if (resizeXHovered) resizeX = mousePosition.x - size.x - if (resizeYHovered) resizeY = mousePosition.y - size.y + if (resizeXHovered) resizeX = mousePosition.x - width + if (resizeYHovered) resizeY = mousePosition.y - height } onMouseMove { @@ -164,15 +161,15 @@ open class Window( if (!resizable || minimized) return@onMouseMove // Hover state update - if (selectedChild == null && isHovered) { + if (selectedChild != titleBar && isHovered) { resizeXHovered = mousePosition in Rect( - titleBar.rect.rightTop - Vec2d(RESIZE_RANGE, 0.0), - rect.rightBottom + rightTop - Vec2d(RESIZE_RANGE, 0.0), + rightBottom ) resizeYHovered = mousePosition in Rect( - rect.leftBottom - Vec2d(0.0, RESIZE_RANGE), - rect.rightBottom + leftBottom - Vec2d(0.0, RESIZE_RANGE), + rightBottom ) } @@ -180,20 +177,27 @@ open class Window( if (resizeX != null || resizeY != null) { val x = resizeX?.let { rx -> mousePosition.x - rx - } ?: size.x + } ?: width val y = resizeY?.let { ry -> + if (autoResize.enabled) return@let null mousePosition.y - ry - } ?: size.y + } ?: height - size = Vec2d(x, y).coerceIn( - 80.0, 1000.0, - titleBar.size.y + RESIZE_RANGE, 1000.0 - ) + width = x.coerceIn(80.0, 1000.0) + height = y.coerceIn(titleBar.renderHeight + RESIZE_RANGE, 1000.0) } } } + enum class AutoResize(private val isEnabled: () -> Boolean) { + Disabled({ false }), + ByConfig({ NewCGui.autoResize }), + ForceEnabled({ true }); + + val enabled get() = isEnabled() + } + companion object { /** * Creates new empty [Window] @@ -213,26 +217,28 @@ open class Window( * * @param resizable Whether to allow user to resize the window * - * @param clean Whether to skip the background rendering + * @param autoResize Indicates if this window could be automatically resized based on content height * * @param block Actions to perform within content space of the window */ @UIBuilder fun Layout.window( position: Vec2d = Vec2d.ZERO, - size: Vec2d = Vec2d(100.0, 300.0), + size: Vec2d = Vec2d(115.0, 300.0), title: String = "Untitled", draggable: Boolean = true, scrollable: Boolean = true, minimizable: Boolean = true, resizable: Boolean = true, - clean: Boolean = false, + autoResize: AutoResize = AutoResize.Disabled, + useBatching: Boolean = false, block: WindowContent.() -> Unit = {} ) = Window( this, title, position, size, draggable, scrollable, minimizable, resizable, - clean + autoResize, + useBatching ).apply(children::add).apply { block(this.content) } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt index 1ce566aed..8f7c3e18d 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt @@ -5,10 +5,6 @@ import com.lambda.module.modules.client.NewCGui import com.lambda.newgui.component.VAlign import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.layout.Layout -import com.lambda.util.math.Rect -import com.lambda.util.math.Vec2d -import com.lambda.util.math.setAlpha -import java.awt.Color import kotlin.math.abs class WindowContent( @@ -25,6 +21,11 @@ class WindowContent( private val scrollableChildren get() = children.filter { it.verticalAlignment == VAlign.TOP } init { + overrideX { owner.titleBar.renderPositionX } + overrideY { owner.titleBar.let { it.renderPositionY + it.renderHeight } } + overrideWidth { owner.renderWidth } + overrideHeight { owner.renderHeight - owner.titleBar.renderHeight } + onShow { dwheel = 0.0 scrollOffset = 0.0 @@ -35,17 +36,15 @@ class WindowContent( } onTick { - scrollOffset += dwheel - dwheel = 0.0 + scrollOffset = if (!owner.autoResize.enabled) { + scrollOffset + dwheel + } else 0.0 - val c = scrollableChildren - var childHeight = c.sumOf { it.rect.size.y + NewCGui.listStep } - if (c.isNotEmpty()) childHeight -= NewCGui.listStep - - val range = rect.size.y - childHeight + dwheel = 0.0 val prevOffset = scrollOffset - scrollOffset = scrollOffset.coerceAtLeast(range).coerceAtMost(0.0) + val maxScroll = renderHeight - getContentHeight() - NewCGui.padding * 2 + scrollOffset = scrollOffset.coerceAtLeast(maxScroll).coerceAtMost(0.0) rubberbandDelta += prevOffset - scrollOffset rubberbandDelta *= 0.5 @@ -66,16 +65,26 @@ class WindowContent( private fun reorderChildren() { // Skip for closed windows - if (size.y < 0.1) return + if (renderHeight < 0.1) return var offset = renderScrollOffset + NewCGui.padding scrollableChildren.forEach { child -> - child.position = Vec2d(child.position.x, position.y + offset) - offset += child.rect.size.y + NewCGui.listStep + child.positionY = renderPositionY + offset + offset += child.renderHeight + NewCGui.listStep } } + fun getContentHeight(): Double { + val c = scrollableChildren + + val components = c.sumOf(Layout::renderHeight) + val step = NewCGui.listStep * (c.size - 1).coerceAtLeast(0) + val padding = NewCGui.padding * 2 + + return components + step + padding + } + companion object { /** * Creates an empty [WindowContent] component diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt new file mode 100644 index 000000000..d73c0af2f --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt @@ -0,0 +1,90 @@ +package com.lambda.newgui.impl.clickgui + +import com.lambda.graphics.animation.Animation.Companion.exp +import com.lambda.module.Module +import com.lambda.module.modules.client.NewCGui +import com.lambda.newgui.component.HAlign +import com.lambda.newgui.component.VAlign +import com.lambda.newgui.component.core.UIBuilder +import com.lambda.newgui.component.layout.Layout +import com.lambda.newgui.component.window.Window +import com.lambda.util.Mouse +import com.lambda.util.math.Vec2d +import com.lambda.util.math.lerp +import com.lambda.util.math.setAlpha +import java.awt.Color + +class ModuleLayout( + owner: Layout, + module: Module +) : Window( + owner, + module.name, + Vec2d.ZERO, Vec2d.ZERO, + false, false, true, false, + AutoResize.Disabled, // ToDo: should be ForceEnabled, temporarily using this mode to set the height manually + true +) { + private val animation = animationTicker() + private val cursorController = cursorController() + + private var enableAnimation by animation.exp(0.0, 1.0, 0.6, module::isEnabled) + + init { + minimized = true + height = 100.0 + + overrideX { owner.renderPositionX + NewCGui.padding } + overrideWidth { owner.renderWidth - NewCGui.padding * 2 } + + with(titleBar) { + with(textField) { + bold = false + horizontalAlignment = HAlign.LEFT + + onUpdate { + positionX = titleBar.renderPositionX + (titleBar.renderHeight - textHeight) * 0.5 + } + } + + onMouseClick { button, action -> + if (button == Mouse.Button.Left && action == Mouse.Action.Click) { + module.toggle() + } + } + } + + onShow { + enableAnimation = 0.0 + } + + titleBarRect.onUpdate { + setColor(lerp(enableAnimation, DISABLED_COLOR, NewCGui.titleBackgroundColor)) + } + + contentRect.onUpdate { + setColor(lerp(enableAnimation, DISABLED_COLOR, NewCGui.backgroundColor)) + } + + outlineRect.onUpdate { + setColor(lerp(enableAnimation, DISABLED_COLOR, NewCGui.outlineColor)) + } + + onTick { + val cursor = if (titleBar.isHovered) Mouse.Cursor.Pointer else Mouse.Cursor.Arrow + cursorController.setCursor(cursor) + } + + } + + companion object { + /** + * Creates a [ModuleLayout] - visual representation of the [Module] + */ + @UIBuilder + fun Layout.moduleLayout(module: Module) = + ModuleLayout(this, module).apply(children::add) + + private val DISABLED_COLOR = Color.BLACK.setAlpha(0.1) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/Mouse.kt b/common/src/main/kotlin/com/lambda/util/Mouse.kt index aa01d8888..5ebe3e294 100644 --- a/common/src/main/kotlin/com/lambda/util/Mouse.kt +++ b/common/src/main/kotlin/com/lambda/util/Mouse.kt @@ -40,7 +40,7 @@ class Mouse { fun setCursor(cursor: Cursor) { // We're doing this to let other controllers be able to set the cursor when this one doesn't change - if (lastSetCursor == cursor) return + if (lastSetCursor == cursor && cursor == Cursor.Arrow) return cursor.set() lastSetCursor = cursor diff --git a/common/src/main/kotlin/com/lambda/util/math/Rect.kt b/common/src/main/kotlin/com/lambda/util/math/Rect.kt index 3583ff164..c04ad8a33 100644 --- a/common/src/main/kotlin/com/lambda/util/math/Rect.kt +++ b/common/src/main/kotlin/com/lambda/util/math/Rect.kt @@ -4,10 +4,10 @@ import kotlin.math.max import kotlin.math.min data class Rect(private val pos1: Vec2d, private val pos2: Vec2d) { - val left = pos1.x - val top = pos1.y - val right = pos2.x - val bottom = pos2.y + val left get() = pos1.x + val top get() = pos1.y + val right get() = pos2.x + val bottom get() = pos2.y val leftTop get() = Vec2d(left, top) val rightTop get() = Vec2d(right, top) From 9ba5a7a4d9d48e1d27f37cf01acadb44b5be8ecc Mon Sep 17 00:00:00 2001 From: Constructor Date: Wed, 2 Oct 2024 06:10:22 +0200 Subject: [PATCH 033/114] Nullsafety on tag sets --- .../com/lambda/module/modules/client/NewCGui.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt index 4d6cd09b1..19f8b32d8 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt @@ -41,10 +41,14 @@ object NewCGui : Module( tags.forEachIndexed { i, tag -> val windowPosition = Vec2d.ONE * 20.0 + Vec2d.RIGHT * ((115.0 * i) + (i + 1) * 4) - window(position = windowPosition, title = tag.name, autoResize = Window.AutoResize.ByConfig) { - val tagModules = modules.filter { it.defaultTags.first() == tag } - - tagModules.forEach { module -> + window( + position = windowPosition, + title = tag.name, + autoResize = Window.AutoResize.ByConfig + ) { + modules.filter { + it.defaultTags.firstOrNull() == tag + }.forEach { module -> moduleLayout(module) } } From a516060bef03e67b91e571a93b7f3392a2f45432 Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Tue, 8 Oct 2024 00:05:05 +0300 Subject: [PATCH 034/114] many misc things --- .../src/main/kotlin/com/lambda/core/Loader.kt | 2 + .../module/modules/client/GuiSettings.kt | 4 +- .../lambda/module/modules/client/NewCGui.kt | 31 +++--- .../kotlin/com/lambda/newgui/GuiManager.kt | 36 +++++++ .../kotlin/com/lambda/newgui/LambdaScreen.kt | 20 +--- .../kotlin/com/lambda/newgui/ScreenLayout.kt | 22 +++++ .../lambda/newgui/component/layout/Layout.kt | 2 +- .../newgui/component/window/TitleBar.kt | 5 + .../lambda/newgui/component/window/Window.kt | 99 +++++++++++-------- .../newgui/component/window/WindowContent.kt | 7 +- .../newgui/impl/clickgui/ModuleLayout.kt | 37 ++++--- .../newgui/impl/clickgui/ModuleWindow.kt | 38 +++++++ .../kotlin/com/lambda/util/math/Linear.kt | 6 +- 13 files changed, 210 insertions(+), 99 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/newgui/GuiManager.kt create mode 100644 common/src/main/kotlin/com/lambda/newgui/ScreenLayout.kt create mode 100644 common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleWindow.kt diff --git a/common/src/main/kotlin/com/lambda/core/Loader.kt b/common/src/main/kotlin/com/lambda/core/Loader.kt index f2bb399f4..ee20fcf62 100644 --- a/common/src/main/kotlin/com/lambda/core/Loader.kt +++ b/common/src/main/kotlin/com/lambda/core/Loader.kt @@ -12,6 +12,7 @@ import com.lambda.interaction.PlayerPacketManager import com.lambda.interaction.RotationManager import com.lambda.interaction.material.ContainerManager import com.lambda.module.ModuleRegistry +import com.lambda.newgui.GuiManager import com.lambda.sound.SoundRegistry import com.lambda.util.Communication.ascii import kotlin.system.measureTimeMillis @@ -25,6 +26,7 @@ object Loader { get() = "${(System.currentTimeMillis() - started).toDuration(DurationUnit.MILLISECONDS)}" private val loadables = listOf( + GuiManager, ModuleRegistry, CommandRegistry, RotationManager, diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt index 9102b8db6..2c3d8365b 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt @@ -30,8 +30,8 @@ object GuiSettings : Module( val backgroundColor by setting("Background Color", Color(50, 50, 50, 150), visibility = { page == Page.Colors }) val shade by setting("Shade", true, visibility = { page == Page.Colors }) val shadeBackground by setting("Shade Background", true, visibility = { page == Page.Colors }) - val colorWidth by setting("Shade Width", 400.0, 10.0..1000.0, 10.0, visibility = { page == Page.Colors }) - val colorHeight by setting("Shade Height", 400.0, 10.0..1000.0, 10.0, visibility = { page == Page.Colors }) + val colorWidth by setting("Shade Width", 200.0, 10.0..1000.0, 10.0, visibility = { page == Page.Colors }) + val colorHeight by setting("Shade Height", 200.0, 10.0..1000.0, 10.0, visibility = { page == Page.Colors }) val colorSpeed by setting("Color Speed", 1.0, 0.1..10.0, 0.1, visibility = { page == Page.Colors }) val mainColor: Color get() = if (shade) Color.WHITE else primaryColor diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt index 4d6cd09b1..02fe08f5e 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt @@ -3,11 +3,9 @@ package com.lambda.module.modules.client import com.lambda.module.Module import com.lambda.module.ModuleRegistry import com.lambda.module.tag.ModuleTag -import com.lambda.newgui.LambdaScreen.Companion.gui -import com.lambda.newgui.LambdaScreen.Companion.toScreen -import com.lambda.newgui.component.window.Window -import com.lambda.newgui.component.window.Window.Companion.window +import com.lambda.newgui.ScreenLayout.Companion.gui import com.lambda.newgui.impl.clickgui.ModuleLayout.Companion.moduleLayout +import com.lambda.newgui.impl.clickgui.ModuleWindow.Companion.moduleWindow import com.lambda.util.math.Vec2d import com.lambda.util.math.setAlpha import java.awt.Color @@ -25,7 +23,7 @@ object NewCGui : Module( val roundRadius by setting("Round Radius", 2.0, 0.0..10.0, 0.1) val titleBackgroundColor by setting("Title Background Color", Color.WHITE.setAlpha(0.4)) - val backgroundColor by setting("Background Color", Color.WHITE.setAlpha(0.2)) + val backgroundColor by setting("Background Color", Color.WHITE.setAlpha(0.25)) val backgroundShade by setting("Background Shade", true) val outline by setting("Outline", true) @@ -33,28 +31,27 @@ object NewCGui : Module( val outlineColor by setting("Outline Color", Color.WHITE.setAlpha(0.6)) { outline } val outlineShade by setting("Outline Shade", true) { outline } - private val SCREEN by lazy { - gui { - val tags = ModuleTag.defaults - val modules = ModuleRegistry.modules + private val SCREEN get() = gui("New Click Gui") { + val tags = ModuleTag.defaults + val modules = ModuleRegistry.modules - tags.forEachIndexed { i, tag -> - val windowPosition = Vec2d.ONE * 20.0 + Vec2d.RIGHT * ((115.0 * i) + (i + 1) * 4) + tags.forEachIndexed { i, tag -> + val windowPosition = Vec2d.ONE * 20.0 + Vec2d.RIGHT * ((115.0 * i) + (i + 1) * 4) - window(position = windowPosition, title = tag.name, autoResize = Window.AutoResize.ByConfig) { - val tagModules = modules.filter { it.defaultTags.first() == tag } + moduleWindow(tag, windowPosition) { + val tagModules = modules.filter { it.defaultTags.first() == tag } - tagModules.forEach { module -> - moduleLayout(module) - } + tagModules.forEach { module -> + moduleLayout(module) } } - }.toScreen("New Click Gui") + } } init { onEnable { SCREEN.show() + toggle() } } } diff --git a/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt b/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt new file mode 100644 index 000000000..e9b41234c --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt @@ -0,0 +1,36 @@ +package com.lambda.newgui + +import com.lambda.core.Loadable +import com.lambda.module.Module +import com.lambda.newgui.component.core.UIBuilder +import com.lambda.newgui.component.layout.Layout +import com.lambda.newgui.impl.clickgui.ModuleLayout.Companion.moduleLayout + +object GuiManager : Loadable { + val typeMap = mutableMapOf, (owner: Layout, converted: Any) -> Layout>() + + private inline fun typeAdapter(noinline block: (Layout, T) -> Layout) { + typeMap[T::class.java] = { owner, converted -> block(owner, converted as T) } + } + + override fun load(): String { + // Example, not meant to be used + typeAdapter { owner, ref -> + owner.moduleLayout(ref) + } + + return super.load() + } + + /** + * Attempts to convert the given [reference] to the [Layout] + * + * Or throws [IllegalStateException] if there's no registered ui adapter for the type of the [reference] + */ + @UIBuilder + inline fun Layout.layoutOf(reference: T, block: Layout.() -> Unit = {}): Layout { + val clazz = T::class.java + val adapter = typeMap[clazz] ?: throw IllegalArgumentException("Unable to convert ${clazz.simpleName} to a layout") + return adapter(this, reference).apply(children::add).apply(block) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt index be75ac082..6bc60c4b5 100644 --- a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt +++ b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt @@ -24,7 +24,7 @@ import net.minecraft.text.Text */ class LambdaScreen( override val name: String, - val layout: Layout + val layout: ScreenLayout ) : Screen(Text.of(name)), Nameable, Muteable { override val isMuted: Boolean get() = !isOpen @@ -111,22 +111,4 @@ class LambdaScreen( val uv = mcMouse / mcWindow return uv * screenSize } - - companion object { - /** - * Creates gui layout - */ - @UIBuilder - fun gui(block: Layout.() -> Unit) = - Layout(owner = null, useBatching = false, batchChildren = true).apply { - onRender { - size = RenderMain.screenSize - } - }.apply(block) - - /** - * Converts this [Layout] to a minecraft-typed [Screen] represented by the [LambdaScreen] class - */ - fun Layout.toScreen(name: String) = LambdaScreen(name, this) - } } diff --git a/common/src/main/kotlin/com/lambda/newgui/ScreenLayout.kt b/common/src/main/kotlin/com/lambda/newgui/ScreenLayout.kt new file mode 100644 index 000000000..7bd8c1e4d --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/ScreenLayout.kt @@ -0,0 +1,22 @@ +package com.lambda.newgui + +import com.lambda.graphics.RenderMain +import com.lambda.newgui.component.core.UIBuilder +import com.lambda.newgui.component.layout.Layout + +class ScreenLayout : Layout(owner = null, useBatching = false, batchChildren = true) { + init { + onRender { + size = RenderMain.screenSize + } + } + + companion object { + /** + * Creates gui layout + */ + @UIBuilder + fun gui(name: String, block: ScreenLayout.() -> Unit) = + LambdaScreen(name, ScreenLayout().apply(block)) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt index 0e26a4aef..7d0096176 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt @@ -120,7 +120,7 @@ open class Layout( // Structure val children = mutableListOf() - protected var selectedChild: Layout? = null + var selectedChild: Layout? = null // Inputs protected var mousePosition = Vec2d.ZERO diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt index 5400d9b56..d688be9c0 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt @@ -34,6 +34,11 @@ class TitleBar( private var dragOffset: Vec2d? = null init { + overrideSize( + owner::renderWidth, + NewCGui::titleBarHeight + ) + onShow { dragOffset = null } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt index 998686430..8bbd81307 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt @@ -2,7 +2,7 @@ package com.lambda.newgui.component.window import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.module.modules.client.NewCGui -import com.lambda.newgui.component.VAlign +import com.lambda.newgui.ScreenLayout import com.lambda.newgui.component.core.FilledRect.Companion.rect import com.lambda.newgui.component.core.OutlineRect.Companion.outline import com.lambda.newgui.component.layout.Layout @@ -22,15 +22,15 @@ import com.lambda.util.math.lerp */ open class Window( owner: Layout, - initialTitle: String, - initialPosition: Vec2d, - initialSize: Vec2d, - draggable: Boolean, - scrollable: Boolean, - private val minimizable: Boolean, - private val resizable: Boolean, - val autoResize: AutoResize, - useBatching: Boolean, + initialTitle: String = "Untitled", + initialPosition: Vec2d = Vec2d.ZERO, + initialSize: Vec2d = Vec2d(115.0, 300.0), + draggable: Boolean = true, + scrollable: Boolean = true, + private val minimizing: Minimizing = Minimizing.Relative, + private val resizable: Boolean = true, + val autoResize: AutoResize = AutoResize.Disabled, + useBatching: Boolean = false, ) : Layout(owner, useBatching, true) { private val animation = animationTicker() private val cursorController = cursorController() @@ -45,8 +45,10 @@ open class Window( val radius = NewCGui.roundRadius leftTopRadius = radius rightTopRadius = radius - leftBottomRadius = radius * (1 - minimizeAnimation) - rightBottomRadius = radius * (1 - minimizeAnimation) + + val bottomRadius = lerp(content.renderHeight, radius, 0.0) + leftBottomRadius = bottomRadius + rightBottomRadius = bottomRadius shade = NewCGui.backgroundShade } @@ -79,7 +81,14 @@ open class Window( // Minimizing var minimized = false - private val minimizeAnimation by animation.exp(0.0, 1.0, 0.8) { !minimized } + private var heightAnimation by animation.exp( + min = { 0.0 }, + max = { if (minimizing == Minimizing.Relative) targetHeight else 1.0 }, + speed = 0.8, + flag = { !minimized } + ) + + private val targetHeight get() = if (!autoResize.enabled) height - titleBar.renderHeight else content.getContentHeight() // Resizing private var resizeX: Double? = null @@ -94,23 +103,20 @@ open class Window( overrideWidth(animation.exp(::width, 0.8)::value) overrideHeight { - val rawHeight = if (!autoResize.enabled) height - else titleBar.renderHeight + content.getContentHeight() - lerp(minimizeAnimation, titleBar.renderHeight, rawHeight) + titleBar.renderHeight + when (minimizing) { + Minimizing.Disabled -> targetHeight + Minimizing.Relative -> heightAnimation + Minimizing.Absolute -> heightAnimation * targetHeight + } } - properties.clampPosition = owner.owner == null + properties.clampPosition = owner is ScreenLayout content.properties.scissor = true with(titleBar) { - overrideSize( - this@Window::renderWidth, - NewCGui::titleBarHeight - ) - onMouseClick { button, action -> // Toggle minimizing state when right-clicking title bar - if (!minimizable) return@onMouseClick + if (minimizing == Minimizing.Disabled) return@onMouseClick if (button != Mouse.Button.Right || action != Mouse.Action.Click) return@onMouseClick minimized = !minimized @@ -122,6 +128,11 @@ open class Window( resizeY = null resizeXHovered = false resizeYHovered = false + heightAnimation = when { + minimized -> 0.0 + minimizing == Minimizing.Relative -> targetHeight + else -> 1.0 + } } onHide { @@ -150,8 +161,8 @@ open class Window( if (button != Mouse.Button.Left || action != Mouse.Action.Click) return@onMouseClick - if (resizeXHovered) resizeX = mousePosition.x - width - if (resizeYHovered) resizeY = mousePosition.y - height + if (resizeXHovered) resizeX = mousePosition.x - renderWidth + if (resizeYHovered) resizeY = mousePosition.y - renderHeight } onMouseMove { @@ -161,13 +172,13 @@ open class Window( if (!resizable || minimized) return@onMouseMove // Hover state update - if (selectedChild != titleBar && isHovered) { + if (selectedChild != titleBar && content.selectedChild == null && isHovered) { resizeXHovered = mousePosition in Rect( rightTop - Vec2d(RESIZE_RANGE, 0.0), rightBottom ) - resizeYHovered = mousePosition in Rect( + resizeYHovered = !autoResize.enabled && mousePosition in Rect( leftBottom - Vec2d(0.0, RESIZE_RANGE), rightBottom ) @@ -175,17 +186,13 @@ open class Window( // Resize if (resizeX != null || resizeY != null) { - val x = resizeX?.let { rx -> - mousePosition.x - rx - } ?: width - - val y = resizeY?.let { ry -> - if (autoResize.enabled) return@let null - mousePosition.y - ry - } ?: height + resizeX?.let { rx -> + width = (mousePosition.x - rx).coerceIn(80.0, 1000.0) + } - width = x.coerceIn(80.0, 1000.0) - height = y.coerceIn(titleBar.renderHeight + RESIZE_RANGE, 1000.0) + resizeY?.let { ry -> + height = (mousePosition.y - ry).coerceIn(titleBar.renderHeight + RESIZE_RANGE, 1000.0) + } } } } @@ -198,6 +205,17 @@ open class Window( val enabled get() = isEnabled() } + /** + * [Disabled] -> No ability to minimize the window + * [Relative] -> Animation follows the height of the component ( animation(0.0, height) ) + * [Absolute] -> Animation does not depend on the height ( animation(0.0, 1.0) * height ) + */ + enum class Minimizing { + Disabled, + Relative, + Absolute; + } + companion object { /** * Creates new empty [Window] @@ -211,9 +229,8 @@ open class Window( * @param draggable Whether to allow user to drag the window * * @param scrollable Whether to allow user to scroll the elements - * Note: applies to elements with [VAlign.TOP] only * - * @param minimizable Whether to allow user to minimize the window + * @param minimizing The [Minimizing] mode. * * @param resizable Whether to allow user to resize the window * @@ -228,7 +245,7 @@ open class Window( title: String = "Untitled", draggable: Boolean = true, scrollable: Boolean = true, - minimizable: Boolean = true, + minimizing: Minimizing = Minimizing.Relative, resizable: Boolean = true, autoResize: AutoResize = AutoResize.Disabled, useBatching: Boolean = false, @@ -236,7 +253,7 @@ open class Window( ) = Window( this, title, position, size, - draggable, scrollable, minimizable, resizable, + draggable, scrollable, minimizing, resizable, autoResize, useBatching ).apply(children::add).apply { diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt index 8f7c3e18d..4debd3ac3 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt @@ -2,7 +2,6 @@ package com.lambda.newgui.component.window import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.module.modules.client.NewCGui -import com.lambda.newgui.component.VAlign import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.layout.Layout import kotlin.math.abs @@ -18,8 +17,6 @@ class WindowContent( private var rubberbandDelta = 0.0 private var renderScrollOffset by animation.exp({ scrollOffset + rubberbandDelta }, 0.7) - private val scrollableChildren get() = children.filter { it.verticalAlignment == VAlign.TOP } - init { overrideX { owner.titleBar.renderPositionX } overrideY { owner.titleBar.let { it.renderPositionY + it.renderHeight } } @@ -69,14 +66,14 @@ class WindowContent( var offset = renderScrollOffset + NewCGui.padding - scrollableChildren.forEach { child -> + children.forEach { child -> child.positionY = renderPositionY + offset offset += child.renderHeight + NewCGui.listStep } } fun getContentHeight(): Double { - val c = scrollableChildren + val c = children val components = c.sumOf(Layout::renderHeight) val step = NewCGui.listStep * (c.size - 1).coerceAtLeast(0) diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt index d73c0af2f..bb5bf295f 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt @@ -4,15 +4,14 @@ import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.module.Module import com.lambda.module.modules.client.NewCGui import com.lambda.newgui.component.HAlign -import com.lambda.newgui.component.VAlign +import com.lambda.newgui.component.core.FilledRect import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.layout.Layout import com.lambda.newgui.component.window.Window import com.lambda.util.Mouse import com.lambda.util.math.Vec2d import com.lambda.util.math.lerp -import com.lambda.util.math.setAlpha -import java.awt.Color +import com.lambda.util.math.multAlpha class ModuleLayout( owner: Layout, @@ -21,7 +20,7 @@ class ModuleLayout( owner, module.name, Vec2d.ZERO, Vec2d.ZERO, - false, false, true, false, + false, false, Minimizing.Relative, false, AutoResize.Disabled, // ToDo: should be ForceEnabled, temporarily using this mode to set the height manually true ) { @@ -30,6 +29,9 @@ class ModuleLayout( private var enableAnimation by animation.exp(0.0, 1.0, 0.6, module::isEnabled) + // Could be true only if owner is ModuleWindow + var isLast = false + init { minimized = true height = 100.0 @@ -58,23 +60,38 @@ class ModuleLayout( enableAnimation = 0.0 } + onHide { + cursorController.reset() + } + titleBarRect.onUpdate { - setColor(lerp(enableAnimation, DISABLED_COLOR, NewCGui.titleBackgroundColor)) + setColor(lerp(enableAnimation, NewCGui.titleBackgroundColor.multAlpha(0.15), NewCGui.titleBackgroundColor)) + correctRadius() } contentRect.onUpdate { - setColor(lerp(enableAnimation, DISABLED_COLOR, NewCGui.backgroundColor)) + setColor(lerp(enableAnimation, NewCGui.backgroundColor.multAlpha(0.15), NewCGui.backgroundColor)) + correctRadius() } - outlineRect.onUpdate { - setColor(lerp(enableAnimation, DISABLED_COLOR, NewCGui.outlineColor)) - } + children.remove(outlineRect) onTick { val cursor = if (titleBar.isHovered) Mouse.Cursor.Pointer else Mouse.Cursor.Arrow cursorController.setCursor(cursor) } + } + + private fun FilledRect.correctRadius() { + if (!isLast) { + setRadius(0.0) + return + } + leftTopRadius = 0.0 + rightTopRadius = 0.0 + leftBottomRadius -= NewCGui.padding + rightBottomRadius -= NewCGui.padding } companion object { @@ -84,7 +101,5 @@ class ModuleLayout( @UIBuilder fun Layout.moduleLayout(module: Module) = ModuleLayout(this, module).apply(children::add) - - private val DISABLED_COLOR = Color.BLACK.setAlpha(0.1) } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleWindow.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleWindow.kt new file mode 100644 index 000000000..f005ce4a2 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleWindow.kt @@ -0,0 +1,38 @@ +package com.lambda.newgui.impl.clickgui + +import com.lambda.module.tag.ModuleTag +import com.lambda.newgui.component.core.UIBuilder +import com.lambda.newgui.component.layout.Layout +import com.lambda.newgui.component.window.Window +import com.lambda.newgui.component.window.WindowContent +import com.lambda.util.math.Vec2d + +class ModuleWindow( + owner: Layout, + val tag: ModuleTag, // todo: tag system + initialPosition: Vec2d +) : Window(owner, tag.name, initialPosition, minimizing = Minimizing.Absolute, autoResize = AutoResize.ByConfig) { + init { + onTick { + val modules = content.children.filterIsInstance() + + modules.forEachIndexed { i, it -> + it.isLast = modules.lastIndex == i + } + } + } + + companion object { + /** + * Creates a [ModuleWindow] + */ + @UIBuilder + fun Layout.moduleWindow( + tag: ModuleTag, + position: Vec2d = Vec2d.ZERO, + block: WindowContent.() -> Unit = {} + ) = ModuleWindow(this, tag, position).apply(children::add).apply { + block(this.content) + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/math/Linear.kt b/common/src/main/kotlin/com/lambda/util/math/Linear.kt index 500e840eb..09408c6a9 100644 --- a/common/src/main/kotlin/com/lambda/util/math/Linear.kt +++ b/common/src/main/kotlin/com/lambda/util/math/Linear.kt @@ -113,9 +113,9 @@ fun lerp(value: Double, start: Vec2d, end: Vec2d) = */ fun lerp(value: Double, start: Vec3d, end: Vec3d) = Vec3d( - lerp(start.x, end.x, value), - lerp(start.y, end.y, value), - lerp(start.z, end.z, value), + lerp(value, start.x, end.x), + lerp(value, start.y, end.y), + lerp(value, start.z, end.z), ) /** From 50a66c8a2b0b760cfb3d2b4c24a7ac6e8b7d138e Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Tue, 8 Oct 2024 00:06:40 +0300 Subject: [PATCH 035/114] null safety --- .../main/kotlin/com/lambda/module/modules/client/NewCGui.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt index 02fe08f5e..c4a8a2b66 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt @@ -39,9 +39,9 @@ object NewCGui : Module( val windowPosition = Vec2d.ONE * 20.0 + Vec2d.RIGHT * ((115.0 * i) + (i + 1) * 4) moduleWindow(tag, windowPosition) { - val tagModules = modules.filter { it.defaultTags.first() == tag } - - tagModules.forEach { module -> + modules.filter { + it.defaultTags.firstOrNull() == tag + }.forEach { module -> moduleLayout(module) } } From 9ba36c9958d4f757489a49328022b3dde0cad911 Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Tue, 8 Oct 2024 00:06:40 +0300 Subject: [PATCH 036/114] Re-added tag null safety --- .../main/kotlin/com/lambda/module/modules/client/NewCGui.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt index 02fe08f5e..c4a8a2b66 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt @@ -39,9 +39,9 @@ object NewCGui : Module( val windowPosition = Vec2d.ONE * 20.0 + Vec2d.RIGHT * ((115.0 * i) + (i + 1) * 4) moduleWindow(tag, windowPosition) { - val tagModules = modules.filter { it.defaultTags.first() == tag } - - tagModules.forEach { module -> + modules.filter { + it.defaultTags.firstOrNull() == tag + }.forEach { module -> moduleLayout(module) } } From 393c5d6f84261c3f57b63d9f2f52681abc06c328 Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Sat, 19 Oct 2024 14:58:38 +0300 Subject: [PATCH 037/114] misc changes --- .../lambda/module/modules/client/NewCGui.kt | 21 ++++++++++--- .../newgui/component/core/FilledRect.kt | 3 +- .../lambda/newgui/component/core/TextField.kt | 5 +-- .../newgui/component/window/TitleBar.kt | 10 ++---- .../lambda/newgui/component/window/Window.kt | 7 +++-- .../newgui/component/window/WindowContent.kt | 31 ++++++++++--------- .../newgui/impl/clickgui/ModuleLayout.kt | 9 +++--- 7 files changed, 47 insertions(+), 39 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt index c4a8a2b66..aa0c53321 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt @@ -4,8 +4,10 @@ import com.lambda.module.Module import com.lambda.module.ModuleRegistry import com.lambda.module.tag.ModuleTag import com.lambda.newgui.ScreenLayout.Companion.gui +import com.lambda.newgui.component.core.FilledRect.Companion.rect import com.lambda.newgui.impl.clickgui.ModuleLayout.Companion.moduleLayout import com.lambda.newgui.impl.clickgui.ModuleWindow.Companion.moduleWindow +import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d import com.lambda.util.math.setAlpha import java.awt.Color @@ -22,6 +24,8 @@ object NewCGui : Module( val roundRadius by setting("Round Radius", 2.0, 0.0..10.0, 0.1) + val backgroundTint by setting("Background Tint", Color.BLACK.setAlpha(0.4)) + val titleBackgroundColor by setting("Title Background Color", Color.WHITE.setAlpha(0.4)) val backgroundColor by setting("Background Color", Color.WHITE.setAlpha(0.25)) val backgroundShade by setting("Background Shade", true) @@ -31,20 +35,29 @@ object NewCGui : Module( val outlineColor by setting("Outline Color", Color.WHITE.setAlpha(0.6)) { outline } val outlineShade by setting("Outline Shade", true) { outline } + val moduleEnabledColor by setting("Module Enabled Color", Color.WHITE.setAlpha(0.25)) + val moduleDisabledColor by setting("Module Disabled Color", Color.WHITE.setAlpha(0.05)) + private val SCREEN get() = gui("New Click Gui") { + rect { + rectangle = Rect(Vec2d.ZERO, this.size) + setColor(backgroundTint) + } + val tags = ModuleTag.defaults val modules = ModuleRegistry.modules - tags.forEachIndexed { i, tag -> - val windowPosition = Vec2d.ONE * 20.0 + Vec2d.RIGHT * ((115.0 * i) + (i + 1) * 4) + var x = 20.0 + val y = x - moduleWindow(tag, windowPosition) { + tags.forEachIndexed { i, tag -> + x += moduleWindow(tag, Vec2d(x, y)) { modules.filter { it.defaultTags.firstOrNull() == tag }.forEach { module -> moduleLayout(module) } - } + }.width + 3 } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt index ebb1901d5..4997102b4 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt @@ -1,12 +1,13 @@ package com.lambda.newgui.component.core import com.lambda.newgui.component.layout.Layout +import com.lambda.util.math.Rect import java.awt.Color class FilledRect( owner: Layout ) : Layout(owner, true, true) { - var rectangle = owner.rect + var rectangle = Rect.ZERO var leftTopRadius = 0.0 var rightTopRadius = 0.0 diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt index 6fd5a8b83..15b2b815f 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt @@ -32,10 +32,7 @@ class TextField( action(this@TextField) } - width = textWidth - height = textHeight - - val renderPos = Vec2d(renderPositionX, renderPositionY + renderHeight * 0.5) + val renderPos = Vec2d(renderPositionX, renderPositionY + textHeight * 0.5) fr.build(text, renderPos, color, scale, shadow) } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt index d688be9c0..5e18e9cf4 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt @@ -21,14 +21,10 @@ class TitleBar( text = title bold = true - horizontalAlignment = HAlign.CENTER - verticalAlignment = VAlign.CENTER + val tb = this@TitleBar - onUpdate { - val tb = this@TitleBar - positionX = tb.renderPositionX + tb.renderWidth * 0.5 - textWidth * 0.5 - positionY = tb.renderPositionY + tb.renderHeight * 0.5 - textHeight * 0.5 - } + overrideX { tb.renderPositionX + tb.renderWidth * 0.5 - textWidth * 0.5 } + overrideY { tb.renderPositionY + tb.renderHeight * 0.5 - textHeight * 0.5 } } private var dragOffset: Vec2d? = null diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt index 8bbd81307..c27d6b29f 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt @@ -24,7 +24,7 @@ open class Window( owner: Layout, initialTitle: String = "Untitled", initialPosition: Vec2d = Vec2d.ZERO, - initialSize: Vec2d = Vec2d(115.0, 300.0), + initialSize: Vec2d = Vec2d(120.0, 300.0), draggable: Boolean = true, scrollable: Boolean = true, private val minimizing: Minimizing = Minimizing.Relative, @@ -228,7 +228,8 @@ open class Window( * * @param draggable Whether to allow user to drag the window * - * @param scrollable Whether to allow user to scroll the elements + * @param scrollable Whether to let user scroll the content + * This will also make your elements be vertically ordered * * @param minimizing The [Minimizing] mode. * @@ -241,7 +242,7 @@ open class Window( @UIBuilder fun Layout.window( position: Vec2d = Vec2d.ZERO, - size: Vec2d = Vec2d(115.0, 300.0), + size: Vec2d = Vec2d(120.0, 300.0), title: String = "Untitled", draggable: Boolean = true, scrollable: Boolean = true, diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt index 4debd3ac3..aec835107 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt @@ -8,7 +8,7 @@ import kotlin.math.abs class WindowContent( owner: Window, - scrollable: Boolean + private val scrollable: Boolean ) : Layout(owner, false, true) { private val animation = animationTicker(false) @@ -48,9 +48,7 @@ class WindowContent( if (abs(rubberbandDelta) < 0.05) rubberbandDelta = 0.0 animation.tick() - } - onRender { reorderChildren() } @@ -61,22 +59,24 @@ class WindowContent( } private fun reorderChildren() { - // Skip for closed windows - if (renderHeight < 0.1) return - - var offset = renderScrollOffset + NewCGui.padding - - children.forEach { child -> - child.positionY = renderPositionY + offset - offset += child.renderHeight + NewCGui.listStep + if (!scrollable) return + + children.forEachIndexed { i, child -> + val prev by lazy { children[i - 1] } + + child.overrideY { + if (i == 0) { + renderPositionY + renderScrollOffset + NewCGui.padding + } else { + prev.renderPositionY + prev.renderHeight + NewCGui.listStep + } + } } } fun getContentHeight(): Double { - val c = children - - val components = c.sumOf(Layout::renderHeight) - val step = NewCGui.listStep * (c.size - 1).coerceAtLeast(0) + val components = children.sumOf(Layout::renderHeight) + val step = NewCGui.listStep * (children.size - 1).coerceAtLeast(0) val padding = NewCGui.padding * 2 return components + step + padding @@ -87,6 +87,7 @@ class WindowContent( * Creates an empty [WindowContent] component * * @param scrollable Whether to let user scroll this layout + * This will also make your elements be vertically ordered */ @UIBuilder fun Window.windowContent(scrollable: Boolean) = diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt index bb5bf295f..7243f1283 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt @@ -42,10 +42,9 @@ class ModuleLayout( with(titleBar) { with(textField) { bold = false - horizontalAlignment = HAlign.LEFT - onUpdate { - positionX = titleBar.renderPositionX + (titleBar.renderHeight - textHeight) * 0.5 + overrideX { + titleBar.renderPositionX + (titleBar.renderHeight - textHeight) * 0.5 } } @@ -65,12 +64,12 @@ class ModuleLayout( } titleBarRect.onUpdate { - setColor(lerp(enableAnimation, NewCGui.titleBackgroundColor.multAlpha(0.15), NewCGui.titleBackgroundColor)) + setColor(lerp(enableAnimation, NewCGui.moduleDisabledColor, NewCGui.moduleEnabledColor)) correctRadius() } contentRect.onUpdate { - setColor(lerp(enableAnimation, NewCGui.backgroundColor.multAlpha(0.15), NewCGui.backgroundColor)) + setColor(lerp(enableAnimation, NewCGui.moduleDisabledColor, NewCGui.moduleEnabledColor)) correctRadius() } From 0fdcd761da8900809db3ede3deb83ab41eac26df Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Tue, 5 Nov 2024 08:06:38 +0300 Subject: [PATCH 038/114] added text alignment, simple boolean button --- .../kotlin/com/lambda/graphics/gl/Matrices.kt | 8 +-- .../lambda/module/modules/client/NewCGui.kt | 12 ++-- .../kotlin/com/lambda/newgui/GuiManager.kt | 24 ++++---- .../lambda/newgui/component/core/TextField.kt | 15 ++++- .../lambda/newgui/component/layout/Layout.kt | 55 ++++++++++++------- .../newgui/component/window/TitleBar.kt | 7 ++- .../lambda/newgui/component/window/Window.kt | 4 +- .../newgui/component/window/WindowContent.kt | 7 ++- .../newgui/impl/clickgui/ModuleLayout.kt | 22 ++++++-- .../impl/clickgui/settings/BooleanButton.kt | 31 +++++++++++ 10 files changed, 128 insertions(+), 57 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt b/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt index 0cf5748c2..c59d42aba 100644 --- a/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt +++ b/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt @@ -10,19 +10,19 @@ object Matrices { var vertexTransformer: Matrix4d? = null - fun translate(x: Double, y: Double, z: Double) { + fun translate(x: Double, y: Double, z: Double = 0.0) { translate(x.toFloat(), y.toFloat(), z.toFloat()) } - fun translate(x: Float, y: Float, z: Float) { + fun translate(x: Float, y: Float, z: Float = 0f) { stack.last().translate(x, y, z) } - fun scale(x: Double, y: Double, z: Double) { + fun scale(x: Double, y: Double, z: Double = 1.0) { stack.last().scale(x.toFloat(), y.toFloat(), z.toFloat()) } - fun scale(x: Float, y: Float, z: Float) { + fun scale(x: Float, y: Float, z: Float = 1f) { stack.last().scale(x, y, z) } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt index aa0c53321..49cc4baed 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt @@ -5,9 +5,9 @@ import com.lambda.module.ModuleRegistry import com.lambda.module.tag.ModuleTag import com.lambda.newgui.ScreenLayout.Companion.gui import com.lambda.newgui.component.core.FilledRect.Companion.rect +import com.lambda.newgui.component.layout.Layout.Companion.layout import com.lambda.newgui.impl.clickgui.ModuleLayout.Companion.moduleLayout import com.lambda.newgui.impl.clickgui.ModuleWindow.Companion.moduleWindow -import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d import com.lambda.util.math.setAlpha import java.awt.Color @@ -17,7 +17,8 @@ object NewCGui : Module( description = "ggs", defaultTags = setOf(ModuleTag.CLIENT) ) { - val titleBarHeight by setting("Title Bar Height", 18.0, 0.0..25.0, 0.1) + val titleBarHeight by setting("Title Bar Height", 18.0, 10.0..25.0, 0.1) + val settingsHeight by setting("Settings Height", 16.0, 10.0..25.0, 0.1) val padding by setting("Padding", 2.0, 1.0..6.0, 0.1) val listStep by setting("List Step", 2.0, 0.0..6.0, 0.1) val autoResize by setting("Auto Resize", false) @@ -34,15 +35,14 @@ object NewCGui : Module( val outlineWidth by setting("Outline Width", 10.0, 1.0..10.0, 0.1) { outline } val outlineColor by setting("Outline Color", Color.WHITE.setAlpha(0.6)) { outline } val outlineShade by setting("Outline Shade", true) { outline } + val fontScale by setting("Font Scale", 1.0, 0.5..2.0, 0.1) + val fontOffset by setting("Font Offset", 2.0, 0.0..5.0, 0.1) val moduleEnabledColor by setting("Module Enabled Color", Color.WHITE.setAlpha(0.25)) val moduleDisabledColor by setting("Module Disabled Color", Color.WHITE.setAlpha(0.05)) private val SCREEN get() = gui("New Click Gui") { - rect { - rectangle = Rect(Vec2d.ZERO, this.size) - setColor(backgroundTint) - } + val tags = ModuleTag.defaults val modules = ModuleRegistry.modules diff --git a/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt b/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt index e9b41234c..615899a1f 100644 --- a/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt +++ b/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt @@ -1,36 +1,34 @@ package com.lambda.newgui +import com.lambda.config.settings.comparable.BooleanSetting import com.lambda.core.Loadable -import com.lambda.module.Module import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.layout.Layout -import com.lambda.newgui.impl.clickgui.ModuleLayout.Companion.moduleLayout +import com.lambda.newgui.impl.clickgui.settings.BooleanButton.Companion.booleanSetting +import java.lang.reflect.Type +import kotlin.reflect.KClass object GuiManager : Loadable { - val typeMap = mutableMapOf, (owner: Layout, converted: Any) -> Layout>() + val typeMap = mutableMapOf Layout>() - private inline fun typeAdapter(noinline block: (Layout, T) -> Layout) { + private inline fun typeAdapter(noinline block: (Layout, T) -> Layout) { typeMap[T::class.java] = { owner, converted -> block(owner, converted as T) } } override fun load(): String { - // Example, not meant to be used - typeAdapter { owner, ref -> - owner.moduleLayout(ref) + typeAdapter { owner, ref -> + owner.booleanSetting(ref) } - return super.load() + return "Loaded ${typeMap.size} gui type adapters." } /** * Attempts to convert the given [reference] to the [Layout] - * - * Or throws [IllegalStateException] if there's no registered ui adapter for the type of the [reference] */ @UIBuilder - inline fun Layout.layoutOf(reference: T, block: Layout.() -> Unit = {}): Layout { - val clazz = T::class.java - val adapter = typeMap[clazz] ?: throw IllegalArgumentException("Unable to convert ${clazz.simpleName} to a layout") + inline fun Layout.layoutOf(reference: T, block: Layout.() -> Unit = {}): Layout? { + val adapter = typeMap[T::class.java] ?: return null return adapter(this, reference).apply(children::add).apply(block) } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt index 15b2b815f..0d0f76a84 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt @@ -1,7 +1,10 @@ package com.lambda.newgui.component.core +import com.lambda.newgui.component.HAlign +import com.lambda.newgui.component.VAlign import com.lambda.newgui.component.layout.Layout import com.lambda.util.math.Vec2d +import com.lambda.util.math.lerp import java.awt.Color class TextField( @@ -16,8 +19,12 @@ class TextField( val textWidth get() = fr.getWidth(text, scale) val textHeight get() = fr.getHeight(scale) - private val fr get() = if (bold) renderer.boldFont else renderer.font + var textHAlignment = HAlign.LEFT + var textVAlignment = VAlign.CENTER + var offsetX = 0.0 + var offsetY = 0.0 + private val fr get() = if (bold) renderer.boldFont else renderer.font private val updateActions = mutableListOf Unit>() fun onUpdate(block: TextField.() -> Unit) { @@ -26,13 +33,17 @@ class TextField( init { properties.interactionPassthrough = true + fillParent() onRender { updateActions.forEach { action -> action(this@TextField) } - val renderPos = Vec2d(renderPositionX, renderPositionY + textHeight * 0.5) + val rx = renderPositionX + lerp(textHAlignment.multiplier, offsetX, renderWidth - textWidth - offsetX) + val ry = renderPositionY + lerp(textVAlignment.multiplier, offsetY, renderHeight - textHeight - offsetY) + val renderPos = Vec2d(rx, ry + textHeight * 0.5) + fr.build(text, renderPos, color, scale, shadow) } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt index 7d0096176..b43759f3e 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt @@ -278,6 +278,21 @@ open class Layout( heightTransform = height } + /** + * Makes this layout expand up to parents rect + */ + fun fillParent( + overrideX: () -> Double = { owner?.renderPositionX ?: ownerX }, + overrideY: () -> Double = { owner?.renderPositionY ?: ownerY }, + overrideWidth: () -> Double = { owner?.renderWidth?: ownerWidth }, + overrideHeight: () -> Double = { owner?.renderHeight?: ownerHeight } + ) { + overrideX(overrideX) + overrideY(overrideY) + overrideWidth(overrideWidth) + overrideHeight(overrideHeight) + } + fun onEvent(e: GuiEvent) { if (e is GuiEvent.Render) { screenSize = RenderMain.screenSize @@ -334,32 +349,32 @@ open class Layout( mouseClickActions.forEach { it(e.button, action) } } is GuiEvent.Render -> { - val drawAction = { - val drawChildren = renderWidth > 0.1 && renderHeight > 0.1 + if (properties.scissor) { + scissor(rect) { render(e) } + } else render(e) + } + } + } - val partition by lazy { - children.partition { !it.owningRenderer } - } + protected open fun render(e: GuiEvent) { + val drawChildren = renderWidth > 0.1 && renderHeight > 0.1 - renderActions.forEach { it(renderer) } + val partition by lazy { + children.partition { !it.owningRenderer } + } - if (drawChildren) { - partition.first.forEach { it.onEvent(e) } - } + renderActions.forEach { it(renderer) } - if (owningRenderer) { - renderer.render() - } + if (drawChildren) { + partition.first.forEach { it.onEvent(e) } + } - if (drawChildren) { - partition.second.forEach { it.onEvent(e) } - } - } + if (owningRenderer) { + renderer.render() + } - if (properties.scissor) { - scissor(rect, drawAction) - } else drawAction() - } + if (drawChildren) { + partition.second.forEach { it.onEvent(e) } } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt index 5e18e9cf4..3b661cc7b 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt @@ -21,10 +21,11 @@ class TitleBar( text = title bold = true - val tb = this@TitleBar + textHAlignment = HAlign.CENTER - overrideX { tb.renderPositionX + tb.renderWidth * 0.5 - textWidth * 0.5 } - overrideY { tb.renderPositionY + tb.renderHeight * 0.5 - textHeight * 0.5 } + onUpdate { + scale = NewCGui.fontScale + } } private var dragOffset: Vec2d? = null diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt index c27d6b29f..8a39b01c6 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt @@ -100,9 +100,7 @@ open class Window( position = initialPosition size = initialSize - overrideWidth(animation.exp(::width, 0.8)::value) - - overrideHeight { + overrideSize(animation.exp(::width, 0.8)::value) { titleBar.renderHeight + when (minimizing) { Minimizing.Disabled -> targetHeight Minimizing.Relative -> heightAnimation diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt index aec835107..bb2aa7696 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt @@ -1,6 +1,7 @@ package com.lambda.newgui.component.window import com.lambda.graphics.animation.Animation.Companion.exp +import com.lambda.gui.api.GuiEvent import com.lambda.module.modules.client.NewCGui import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.layout.Layout @@ -15,7 +16,10 @@ class WindowContent( private var dwheel = 0.0 private var scrollOffset = 0.0 private var rubberbandDelta = 0.0 + private var renderScrollOffset by animation.exp({ scrollOffset + rubberbandDelta }, 0.7) + private val scaleAnimation by animation.exp(1.0, 0.9, 0.7, ::scrolling) + private var scrolling = false init { overrideX { owner.titleBar.renderPositionX } @@ -37,10 +41,11 @@ class WindowContent( scrollOffset + dwheel } else 0.0 + scrolling = dwheel != 0.0 dwheel = 0.0 val prevOffset = scrollOffset - val maxScroll = renderHeight - getContentHeight() - NewCGui.padding * 2 + val maxScroll = renderHeight - getContentHeight() - NewCGui.padding scrollOffset = scrollOffset.coerceAtLeast(maxScroll).coerceAtMost(0.0) rubberbandDelta += prevOffset - scrollOffset diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt index 7243f1283..9ae7324a9 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt @@ -1,5 +1,6 @@ package com.lambda.newgui.impl.clickgui +import com.lambda.config.settings.comparable.BooleanSetting import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.module.Module import com.lambda.module.modules.client.NewCGui @@ -8,10 +9,10 @@ import com.lambda.newgui.component.core.FilledRect import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.layout.Layout import com.lambda.newgui.component.window.Window +import com.lambda.newgui.impl.clickgui.settings.BooleanButton.Companion.booleanSetting import com.lambda.util.Mouse import com.lambda.util.math.Vec2d import com.lambda.util.math.lerp -import com.lambda.util.math.multAlpha class ModuleLayout( owner: Layout, @@ -20,8 +21,8 @@ class ModuleLayout( owner, module.name, Vec2d.ZERO, Vec2d.ZERO, - false, false, Minimizing.Relative, false, - AutoResize.Disabled, // ToDo: should be ForceEnabled, temporarily using this mode to set the height manually + false, true, Minimizing.Relative, false, + AutoResize.ForceEnabled, true ) { private val animation = animationTicker() @@ -42,9 +43,10 @@ class ModuleLayout( with(titleBar) { with(textField) { bold = false + textHAlignment = HAlign.LEFT - overrideX { - titleBar.renderPositionX + (titleBar.renderHeight - textHeight) * 0.5 + onUpdate { + offsetX = NewCGui.fontOffset } } @@ -79,6 +81,16 @@ class ModuleLayout( val cursor = if (titleBar.isHovered) Mouse.Cursor.Pointer else Mouse.Cursor.Arrow cursorController.setCursor(cursor) } + + content.apply { + module.settings.forEach { setting -> + //layoutOf(setting) doesn't work + + when (setting) { + is BooleanSetting -> booleanSetting(setting) + } + } + } } private fun FilledRect.correctRadius() { diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt new file mode 100644 index 000000000..aec3ce9bc --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt @@ -0,0 +1,31 @@ +package com.lambda.newgui.impl.clickgui.settings + +import com.lambda.config.settings.comparable.BooleanSetting +import com.lambda.module.modules.client.NewCGui +import com.lambda.newgui.component.core.TextField.Companion.textField +import com.lambda.newgui.component.core.UIBuilder +import com.lambda.newgui.component.layout.Layout + +class BooleanButton( + owner: Layout, + val setting: BooleanSetting +) : Layout(owner, true, true) { + init { + overrideSize(owner::renderWidth, NewCGui::settingsHeight) + + textField { + text = setting.name + scale = NewCGui.fontScale * 0.95 + offsetX = NewCGui.fontOffset + } + } + + companion object { + /** + * Creates a [BooleanButton] - visual representation of the [BooleanSetting] + */ + @UIBuilder + fun Layout.booleanSetting(setting: BooleanSetting) = + BooleanButton(this, setting).apply(children::add) + } +} \ No newline at end of file From ed41aae9f500ac21d5af8fc2e93cf54b3ce6f916 Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Wed, 6 Nov 2024 20:54:16 +0300 Subject: [PATCH 039/114] Abstract setting layout --- .../newgui/component/window/TitleBar.kt | 1 + .../newgui/impl/clickgui/SettingLayout.kt | 44 +++++++++++++++++++ .../impl/clickgui/settings/BooleanButton.kt | 18 ++++---- 3 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt index 3b661cc7b..40cfdf21f 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt @@ -24,6 +24,7 @@ class TitleBar( textHAlignment = HAlign.CENTER onUpdate { + offsetX = NewCGui.fontOffset scale = NewCGui.fontScale } } diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt new file mode 100644 index 000000000..fcecb4194 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt @@ -0,0 +1,44 @@ +package com.lambda.newgui.impl.clickgui + +import com.lambda.config.AbstractSetting +import com.lambda.module.modules.client.NewCGui +import com.lambda.newgui.component.HAlign +import com.lambda.newgui.component.layout.Layout +import com.lambda.newgui.component.window.Window +import com.lambda.util.math.Vec2d + +/** + * A base class for setting layouts. + */ +abstract class SettingLayout > ( + owner: Layout, + setting: T, + expandable: Boolean = false +) : Window( // going to use window to easily implement expandable settings (such as color picker) + owner, + setting.name, + Vec2d.ZERO, Vec2d.ZERO, + false, false, + if (expandable) Minimizing.Relative else Minimizing.Disabled, + false, + AutoResize.ForceEnabled, + true +) { + init { + overrideSize(owner::renderWidth, NewCGui::settingsHeight) + minimized = true + + with(titleBar.textField) { + text = setting.name + bold = false + textHAlignment = HAlign.LEFT + + onUpdate { + scale = NewCGui.fontScale * 0.92 + } + } + + children.removeAll(listOf(titleBarRect, contentRect, outlineRect)) + if (!expandable) children.remove(content) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt index aec3ce9bc..5ab87946a 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt @@ -1,22 +1,20 @@ package com.lambda.newgui.impl.clickgui.settings import com.lambda.config.settings.comparable.BooleanSetting -import com.lambda.module.modules.client.NewCGui -import com.lambda.newgui.component.core.TextField.Companion.textField import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.layout.Layout +import com.lambda.newgui.impl.clickgui.SettingLayout +import com.lambda.util.Mouse class BooleanButton( owner: Layout, - val setting: BooleanSetting -) : Layout(owner, true, true) { + setting: BooleanSetting +) : SettingLayout(owner, setting) { init { - overrideSize(owner::renderWidth, NewCGui::settingsHeight) - - textField { - text = setting.name - scale = NewCGui.fontScale * 0.95 - offsetX = NewCGui.fontOffset + titleBar.onMouseClick { button, action -> + if (button == Mouse.Button.Left && action == Mouse.Action.Click) { + setting.value = !setting.value + } } } From 2f333c387bdac4dbe6db4d5f094713f63de02edf Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:23:34 -0500 Subject: [PATCH 040/114] fix: wrong refied type input --- .../main/kotlin/com/lambda/newgui/GuiManager.kt | 16 ++++++++-------- .../lambda/newgui/impl/clickgui/ModuleLayout.kt | 11 +++-------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt b/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt index 615899a1f..95a659b1c 100644 --- a/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt +++ b/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt @@ -5,14 +5,13 @@ import com.lambda.core.Loadable import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.layout.Layout import com.lambda.newgui.impl.clickgui.settings.BooleanButton.Companion.booleanSetting -import java.lang.reflect.Type import kotlin.reflect.KClass object GuiManager : Loadable { - val typeMap = mutableMapOf Layout>() + val typeMap = mutableMapOf, (owner: Layout, converted: Any) -> Layout>() private inline fun typeAdapter(noinline block: (Layout, T) -> Layout) { - typeMap[T::class.java] = { owner, converted -> block(owner, converted as T) } + typeMap[T::class] = { owner, converted -> block(owner, converted as T) } } override fun load(): String { @@ -27,8 +26,9 @@ object GuiManager : Loadable { * Attempts to convert the given [reference] to the [Layout] */ @UIBuilder - inline fun Layout.layoutOf(reference: T, block: Layout.() -> Unit = {}): Layout? { - val adapter = typeMap[T::class.java] ?: return null - return adapter(this, reference).apply(children::add).apply(block) - } -} \ No newline at end of file + inline fun Layout.layoutOf( + reference: Any, + block: Layout.() -> Unit = {} + ): Layout? = + typeMap[reference::class]?.invoke(this, reference)?.apply(block) +} diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt index 9ae7324a9..aa875520c 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt @@ -4,6 +4,7 @@ import com.lambda.config.settings.comparable.BooleanSetting import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.module.Module import com.lambda.module.modules.client.NewCGui +import com.lambda.newgui.GuiManager.layoutOf import com.lambda.newgui.component.HAlign import com.lambda.newgui.component.core.FilledRect import com.lambda.newgui.component.core.UIBuilder @@ -83,13 +84,7 @@ class ModuleLayout( } content.apply { - module.settings.forEach { setting -> - //layoutOf(setting) doesn't work - - when (setting) { - is BooleanSetting -> booleanSetting(setting) - } - } + module.settings.forEach { setting -> layoutOf(setting) } } } @@ -113,4 +108,4 @@ class ModuleLayout( fun Layout.moduleLayout(module: Module) = ModuleLayout(this, module).apply(children::add) } -} \ No newline at end of file +} From 10cbcb6dd16e93172c0320bd02b1a638713b5f74 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Wed, 6 Nov 2024 17:13:40 -0500 Subject: [PATCH 041/114] copyright --- .../renderer/gui/font/FontRenderer.kt | 3 +++ .../renderer/gui/rect/FilledRectRenderer.kt | 21 +++++++++++++++++-- .../kotlin/com/lambda/newgui/ScreenLayout.kt | 19 ++++++++++++++++- .../newgui/component/window/WindowContent.kt | 19 ++++++++++++++++- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index 68072de6c..5e17dd2a9 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -21,9 +21,12 @@ import com.lambda.graphics.buffer.VertexPipeline import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib import com.lambda.graphics.buffer.vertex.attributes.VertexMode import com.lambda.graphics.renderer.gui.font.glyph.GlyphInfo +import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer import com.lambda.graphics.shader.Shader +import com.lambda.gui.api.component.core.DockingRect import com.lambda.module.modules.client.LambdaMoji import com.lambda.module.modules.client.RenderSettings +import com.lambda.util.Mouse import com.lambda.util.math.Vec2d import com.lambda.util.math.a import com.lambda.util.math.setAlpha diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt index 90a09903b..692f365c4 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt @@ -1,6 +1,23 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.graphics.renderer.gui.rect -import com.lambda.graphics.buffer.vao.vertex.VertexAttrib +import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib import com.lambda.graphics.shader.Shader import com.lambda.util.math.MathUtils.toInt import com.lambda.util.math.Rect @@ -58,7 +75,7 @@ class FilledRectRenderer : AbstractRectRenderer( rightBottom: Color = Color.WHITE, leftBottom: Color = Color.WHITE, shade: Boolean = false, - ) = vao.use { + ) = pipeline.use { val pos1 = rect.leftTop val pos2 = rect.rightBottom diff --git a/common/src/main/kotlin/com/lambda/newgui/ScreenLayout.kt b/common/src/main/kotlin/com/lambda/newgui/ScreenLayout.kt index 7bd8c1e4d..8b9cda097 100644 --- a/common/src/main/kotlin/com/lambda/newgui/ScreenLayout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/ScreenLayout.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui import com.lambda.graphics.RenderMain @@ -19,4 +36,4 @@ class ScreenLayout : Layout(owner = null, useBatching = false, batchChildren = t fun gui(name: String, block: ScreenLayout.() -> Unit) = LambdaScreen(name, ScreenLayout().apply(block)) } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt index bb2aa7696..9b628af5c 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui.component.window import com.lambda.graphics.animation.Animation.Companion.exp @@ -98,4 +115,4 @@ class WindowContent( fun Window.windowContent(scrollable: Boolean) = WindowContent(this, scrollable).apply(children::add) } -} \ No newline at end of file +} From bdd7453e381820adf60e34f3250b81e8e4ab0f44 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Wed, 6 Nov 2024 17:13:52 -0500 Subject: [PATCH 042/114] copyright --- .../gui/api/component/core/DockingRect.kt | 17 +++++++++++++++++ .../lambda/module/modules/client/NewCGui.kt | 17 +++++++++++++++++ .../kotlin/com/lambda/newgui/GuiManager.kt | 17 +++++++++++++++++ .../kotlin/com/lambda/newgui/LambdaScreen.kt | 17 +++++++++++++++++ .../com/lambda/newgui/component/Alignment.kt | 19 ++++++++++++++++++- .../newgui/component/core/FilledRect.kt | 19 ++++++++++++++++++- .../newgui/component/core/OutlineRect.kt | 19 ++++++++++++++++++- .../lambda/newgui/component/core/TextField.kt | 19 ++++++++++++++++++- .../lambda/newgui/component/core/UIBuilder.kt | 19 ++++++++++++++++++- .../lambda/newgui/component/layout/Layout.kt | 17 +++++++++++++++++ .../component/layout/LayoutProperties.kt | 19 ++++++++++++++++++- .../newgui/component/window/TitleBar.kt | 19 ++++++++++++++++++- .../lambda/newgui/component/window/Window.kt | 19 ++++++++++++++++++- .../newgui/impl/clickgui/ModuleLayout.kt | 17 +++++++++++++++++ .../newgui/impl/clickgui/ModuleWindow.kt | 19 ++++++++++++++++++- .../newgui/impl/clickgui/SettingLayout.kt | 19 ++++++++++++++++++- .../impl/clickgui/settings/BooleanButton.kt | 19 ++++++++++++++++++- .../src/main/kotlin/com/lambda/util/Mouse.kt | 17 +++++++++++++++++ 18 files changed, 317 insertions(+), 11 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/core/DockingRect.kt b/common/src/main/kotlin/com/lambda/gui/api/component/core/DockingRect.kt index 001c41457..1bfee1d3e 100644 --- a/common/src/main/kotlin/com/lambda/gui/api/component/core/DockingRect.kt +++ b/common/src/main/kotlin/com/lambda/gui/api/component/core/DockingRect.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.gui.api.component.core import com.lambda.module.modules.client.ClickGui diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt index 49cc4baed..1ead0a81b 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.module.modules.client import com.lambda.module.Module diff --git a/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt b/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt index 95a659b1c..cc30df35f 100644 --- a/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt +++ b/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui import com.lambda.config.settings.comparable.BooleanSetting diff --git a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt index 6bc60c4b5..6581d3987 100644 --- a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt +++ b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui import com.lambda.Lambda.mc diff --git a/common/src/main/kotlin/com/lambda/newgui/component/Alignment.kt b/common/src/main/kotlin/com/lambda/newgui/component/Alignment.kt index 662df2150..584be44db 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/Alignment.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/Alignment.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui.component enum class HAlign(val multiplier: Double, val offset: Double) { @@ -10,4 +27,4 @@ enum class VAlign(val multiplier: Double, val offset: Double) { TOP(0.0, -1.0), CENTER(0.5, 0.0), BOTTOM(1.0, 1.0) -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt index 4997102b4..f69a560c6 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui.component.core import com.lambda.newgui.component.layout.Layout @@ -72,4 +89,4 @@ class FilledRect( updateActions += block } } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/OutlineRect.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/OutlineRect.kt index e2f32b826..b7bf01550 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/OutlineRect.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/OutlineRect.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui.component.core import com.lambda.newgui.component.layout.Layout @@ -60,4 +77,4 @@ class OutlineRect( updateActions += block } } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt index 0d0f76a84..cec2cf86a 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui.component.core import com.lambda.newgui.component.HAlign @@ -57,4 +74,4 @@ class TextField( block: TextField.() -> Unit = {} ) = TextField(this).apply(children::add).apply(block) } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/UIBuilder.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/UIBuilder.kt index fdca7fe38..f9f1bdfc0 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/UIBuilder.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/UIBuilder.kt @@ -1,4 +1,21 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui.component.core @DslMarker -annotation class UIBuilder \ No newline at end of file +annotation class UIBuilder diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt index b43759f3e..610634bf7 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui.component.layout import com.lambda.graphics.RenderMain diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/LayoutProperties.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/LayoutProperties.kt index cd64ed2f7..ff3ed3299 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/LayoutProperties.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/LayoutProperties.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui.component.layout class LayoutProperties { @@ -15,4 +32,4 @@ class LayoutProperties { * If true, anything drawn onto this render layer are clipped within this rect. */ var scissor = false -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt index 40cfdf21f..38864d9e3 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui.component.window import com.lambda.module.modules.client.NewCGui @@ -61,4 +78,4 @@ class TitleBar( drag: Boolean ) = TitleBar(this, text, drag).apply(children::add) } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt index 8a39b01c6..7ccadad62 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui.component.window import com.lambda.graphics.animation.Animation.Companion.exp @@ -261,4 +278,4 @@ open class Window( private const val RESIZE_RANGE = 5.0 } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt index aa875520c..f0fe2f511 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui.impl.clickgui import com.lambda.config.settings.comparable.BooleanSetting diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleWindow.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleWindow.kt index f005ce4a2..49a64bc04 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleWindow.kt +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleWindow.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui.impl.clickgui import com.lambda.module.tag.ModuleTag @@ -35,4 +52,4 @@ class ModuleWindow( block(this.content) } } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt index fcecb4194..fd69d550a 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui.impl.clickgui import com.lambda.config.AbstractSetting @@ -41,4 +58,4 @@ abstract class SettingLayout > ( children.removeAll(listOf(titleBarRect, contentRect, outlineRect)) if (!expandable) children.remove(content) } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt index 5ab87946a..6a4db1ad7 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.newgui.impl.clickgui.settings import com.lambda.config.settings.comparable.BooleanSetting @@ -26,4 +43,4 @@ class BooleanButton( fun Layout.booleanSetting(setting: BooleanSetting) = BooleanButton(this, setting).apply(children::add) } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/util/Mouse.kt b/common/src/main/kotlin/com/lambda/util/Mouse.kt index 6c5db4527..d45de54ca 100644 --- a/common/src/main/kotlin/com/lambda/util/Mouse.kt +++ b/common/src/main/kotlin/com/lambda/util/Mouse.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package com.lambda.util import com.lambda.Lambda.mc From 779becaf350e9dd8e04163e1c5a5a592fd868b7d Mon Sep 17 00:00:00 2001 From: Blade-gl Date: Tue, 12 Nov 2024 21:49:14 +0300 Subject: [PATCH 043/114] Boolean button --- .../lambda/module/modules/client/NewCGui.kt | 10 +++- .../newgui/component/core/FilledRect.kt | 24 ++++++++-- .../newgui/component/core/OutlineRect.kt | 6 +-- .../lambda/newgui/component/layout/Layout.kt | 8 ++-- .../lambda/newgui/component/window/Window.kt | 48 ++++++++++--------- .../newgui/impl/clickgui/ModuleLayout.kt | 31 ++++++++---- .../newgui/impl/clickgui/SettingLayout.kt | 10 +++- .../impl/clickgui/settings/BooleanButton.kt | 44 +++++++++++++++-- 8 files changed, 133 insertions(+), 48 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt index 1ead0a81b..fd49fbf4b 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt @@ -35,7 +35,8 @@ object NewCGui : Module( defaultTags = setOf(ModuleTag.CLIENT) ) { val titleBarHeight by setting("Title Bar Height", 18.0, 10.0..25.0, 0.1) - val settingsHeight by setting("Settings Height", 16.0, 10.0..25.0, 0.1) + val moduleHeight by setting("Module Height", 18.0, 10.0..25.0, 0.1) + val settingsHeight by setting("Settings Height", 14.0, 10.0..25.0, 0.1) val padding by setting("Padding", 2.0, 1.0..6.0, 0.1) val listStep by setting("List Step", 2.0, 0.0..6.0, 0.1) val autoResize by setting("Auto Resize", false) @@ -59,7 +60,12 @@ object NewCGui : Module( val moduleDisabledColor by setting("Module Disabled Color", Color.WHITE.setAlpha(0.05)) private val SCREEN get() = gui("New Click Gui") { - + rect { + onUpdate { + rectangle = owner!!.rect + setColor(backgroundTint) + } + } val tags = ModuleTag.defaults val modules = ModuleRegistry.modules diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt index f69a560c6..da752a316 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt @@ -45,11 +45,17 @@ class FilledRect( } init { + properties.interactionPassthrough = true + onRender { updateActions.forEach { action -> action(this@FilledRect) } + // make it pressable + position = rectangle.leftTop + size = rectangle.size + filled.build( rectangle, leftTopRadius, @@ -79,14 +85,26 @@ class FilledRect( leftBottomColor = color } + fun setColorH(colorL: Color, colorR: Color) { + leftTopColor = colorL + rightTopColor = colorR + rightBottomColor = colorR + leftBottomColor = colorL + } + + fun setColorV(colorT: Color, colorB: Color) { + leftTopColor = colorT + rightTopColor = colorT + rightBottomColor = colorB + leftBottomColor = colorB + } + companion object { /** * Creates a [FilledRect] component - layout-based rect representation */ @UIBuilder fun Layout.rect(block: FilledRect.() -> Unit = {}) = - FilledRect(this).apply(children::add).apply { - updateActions += block - } + FilledRect(this).apply(children::add).apply(block) } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/OutlineRect.kt b/common/src/main/kotlin/com/lambda/newgui/component/core/OutlineRect.kt index b7bf01550..9e3bed197 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/OutlineRect.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/core/OutlineRect.kt @@ -42,6 +42,8 @@ class OutlineRect( } init { + properties.interactionPassthrough = true + onRender { updateActions.forEach { action -> action(this@OutlineRect) @@ -73,8 +75,6 @@ class OutlineRect( */ @UIBuilder fun Layout.outline(block: OutlineRect.() -> Unit = {}) = - OutlineRect(this).apply(children::add).apply { - updateActions += block - } + OutlineRect(this).apply(children::add).apply(block) } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt index 610634bf7..e028a3306 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt @@ -77,7 +77,7 @@ open class Layout( get() = Vec2d(width, height) set(value) { width = value.x; height = value.y } - val leftTop get() = position + val leftTop get() = renderPosition val rightTop get() = Vec2d(renderPositionX + renderWidth, renderPositionY) val rightBottom get() = Vec2d(renderPositionX + renderWidth, renderPositionY + renderHeight) val leftBottom get() = Vec2d(renderPositionX, renderPositionY + renderHeight) @@ -436,10 +436,10 @@ open class Layout( * Use it to set the mouse cursor type for various conditions: hovering, resizing, typing etc... */ @UIBuilder - @Suppress("UNUSED_EXPRESSION") fun Layout.cursorController(): Mouse.CursorController { - this // hack ide to let me make this ui-related only - return Mouse.CursorController() + val con = Mouse.CursorController() + onHide { con.reset() } + return con } } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt index 7ccadad62..c4b1de354 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt @@ -56,38 +56,44 @@ open class Window( val content = windowContent(scrollable) protected val titleBarRect = rect { - rectangle = titleBar.rect - setColor(NewCGui.titleBackgroundColor) + onUpdate { + rectangle = titleBar.rect + setColor(NewCGui.titleBackgroundColor) - val radius = NewCGui.roundRadius - leftTopRadius = radius - rightTopRadius = radius + val radius = NewCGui.roundRadius + leftTopRadius = radius + rightTopRadius = radius - val bottomRadius = lerp(content.renderHeight, radius, 0.0) - leftBottomRadius = bottomRadius - rightBottomRadius = bottomRadius + val bottomRadius = lerp(content.renderHeight, radius, 0.0) + leftBottomRadius = bottomRadius + rightBottomRadius = bottomRadius - shade = NewCGui.backgroundShade + shade = NewCGui.backgroundShade + } } protected val contentRect = rect { - rectangle = Rect(titleBar.leftBottom, this@Window.rightBottom) - setColor(NewCGui.backgroundColor) + onUpdate { + rectangle = Rect(titleBar.leftBottom, this@Window.rightBottom) + setColor(NewCGui.backgroundColor) - leftBottomRadius = NewCGui.roundRadius - rightBottomRadius = NewCGui.roundRadius + leftBottomRadius = NewCGui.roundRadius + rightBottomRadius = NewCGui.roundRadius - shade = NewCGui.backgroundShade + shade = NewCGui.backgroundShade + } } protected val outlineRect = outline { - rectangle = this@Window.rect - setColor(NewCGui.outlineColor) + onUpdate { + rectangle = this@Window.rect + setColor(NewCGui.outlineColor) - roundRadius = NewCGui.roundRadius - glowRadius = NewCGui.outlineWidth * NewCGui.outline.toInt().toDouble() + roundRadius = NewCGui.roundRadius + glowRadius = NewCGui.outlineWidth * NewCGui.outline.toInt().toDouble() - shade = NewCGui.outlineShade + shade = NewCGui.outlineShade + } } // Position @@ -150,10 +156,6 @@ open class Window( } } - onHide { - cursorController.reset() - } - onTick { // Update cursor val rxh = resizeXHovered || resizeX != null diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt index f0fe2f511..0d9c4ce5a 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt @@ -17,20 +17,19 @@ package com.lambda.newgui.impl.clickgui -import com.lambda.config.settings.comparable.BooleanSetting import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.module.Module import com.lambda.module.modules.client.NewCGui import com.lambda.newgui.GuiManager.layoutOf import com.lambda.newgui.component.HAlign import com.lambda.newgui.component.core.FilledRect +import com.lambda.newgui.component.core.FilledRect.Companion.rect import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.layout.Layout import com.lambda.newgui.component.window.Window -import com.lambda.newgui.impl.clickgui.settings.BooleanButton.Companion.booleanSetting import com.lambda.util.Mouse -import com.lambda.util.math.Vec2d -import com.lambda.util.math.lerp +import com.lambda.util.math.* +import java.awt.Color class ModuleLayout( owner: Layout, @@ -47,6 +46,7 @@ class ModuleLayout( private val cursorController = cursorController() private var enableAnimation by animation.exp(0.0, 1.0, 0.6, module::isEnabled) + private var openAnimation by animation.exp(1.0, 0.0, 0.6, ::minimized) // Could be true only if owner is ModuleWindow var isLast = false @@ -68,6 +68,8 @@ class ModuleLayout( } } + overrideHeight(NewCGui::moduleHeight) + onMouseClick { button, action -> if (button == Mouse.Button.Left && action == Mouse.Action.Click) { module.toggle() @@ -75,12 +77,25 @@ class ModuleLayout( } } - onShow { - enableAnimation = 0.0 + rect { // Separator + onUpdate { + val vec = Vec2d( + lerp(openAnimation, titleBar.renderWidth * 0.5, NewCGui.fontOffset * 0.5), + -0.25 + ) + + rectangle = Rect( + pos1 = titleBar.leftBottom + vec, + pos2 = titleBar.rightBottom - vec + ) + + setColor(lerp(enableAnimation, Color.WHITE, Color.BLACK).setAlpha(0.2 * openAnimation)) + shade = NewCGui.outlineShade + } } - onHide { - cursorController.reset() + onShow { + enableAnimation = 0.0 } titleBarRect.onUpdate { diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt index fd69d550a..737a0dc2e 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt @@ -29,7 +29,7 @@ import com.lambda.util.math.Vec2d */ abstract class SettingLayout > ( owner: Layout, - setting: T, + val setting: T, expandable: Boolean = false ) : Window( // going to use window to easily implement expandable settings (such as color picker) owner, @@ -41,8 +41,14 @@ abstract class SettingLayout > ( AutoResize.ForceEnabled, true ) { + protected val animation = animationTicker() + protected val cursorController = cursorController() + + var settingValue by setting + init { - overrideSize(owner::renderWidth, NewCGui::settingsHeight) + overrideWidth(owner::renderWidth) + titleBar.overrideHeight(NewCGui::settingsHeight) minimized = true with(titleBar.textField) { diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt index 6a4db1ad7..d38185d5c 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt @@ -18,19 +18,57 @@ package com.lambda.newgui.impl.clickgui.settings import com.lambda.config.settings.comparable.BooleanSetting +import com.lambda.graphics.animation.Animation.Companion.exp +import com.lambda.module.modules.client.NewCGui +import com.lambda.newgui.component.core.FilledRect.Companion.rect import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.layout.Layout import com.lambda.newgui.impl.clickgui.SettingLayout import com.lambda.util.Mouse +import com.lambda.util.math.Rect +import com.lambda.util.math.Vec2d +import com.lambda.util.math.lerp +import com.lambda.util.math.setAlpha +import java.awt.Color class BooleanButton( owner: Layout, setting: BooleanSetting ) : SettingLayout(owner, setting) { + private var activeAnimation by animation.exp(0.0, 1.0, 0.6, ::settingValue) + init { - titleBar.onMouseClick { button, action -> - if (button == Mouse.Button.Left && action == Mouse.Action.Click) { - setting.value = !setting.value + val checkBox = rect { // Checkbox + val shrink = 2.0 + setRadius(100.0) + + onUpdate { + val rb = this@BooleanButton.rightBottom + val h = this@BooleanButton.renderHeight + + rectangle = Rect(rb - Vec2d(h * 1.65, h), rb) + .shrink(shrink) + Vec2d.LEFT * (NewCGui.fontOffset - shrink) + + setColor(Color.BLACK.setAlpha(0.25)) + shade = NewCGui.backgroundShade + } + + onMouseClick { button, action -> + if (button == Mouse.Button.Left && action == Mouse.Action.Click) { + setting.value = !setting.value + } + } + } + + rect { // Knob + setRadius(100.0) + + onUpdate { + val knobStart = Rect.basedOn(checkBox.leftTop, Vec2d.ONE * checkBox.renderHeight) + val knobEnd = Rect(checkBox.rightBottom - checkBox.renderHeight, checkBox.rightBottom) + rectangle = lerp(activeAnimation, knobStart, knobEnd).shrink(1.0) + shade = NewCGui.backgroundShade + setColor(Color.WHITE.setAlpha(0.25)) } } } From cd9818c48ae0f2c9674f49f4981ac1bb0ab53370 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 15 Nov 2024 18:06:02 -0500 Subject: [PATCH 044/114] feat: lambdamoji --- .../mixin/render/ChatInputSuggestorMixin.java | 73 +++++- .../lambda/mixin/render/ChatScreenMixin.java | 50 ---- .../mixin/render/TextRendererMixin.java | 127 ++++++++++ .../kotlin/com/lambda/graphics/gl/Matrices.kt | 134 +++++++--- .../renderer/gui/font/FontRenderer.kt | 235 ++++++++++-------- .../graphics/renderer/gui/font/LambdaEmoji.kt | 12 + .../renderer/gui/font/glyph/EmojiGlyphs.kt | 3 +- .../kotlin/com/lambda/gui/api/RenderLayer.kt | 19 +- .../gui/impl/clickgui/buttons/ModuleButton.kt | 3 +- .../module/modules/client/LambdaMoji.kt | 30 +-- .../module/modules/client/RenderSettings.kt | 4 + .../main/resources/lambda.mixins.common.json | 5 +- 12 files changed, 473 insertions(+), 222 deletions(-) create mode 100644 common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java diff --git a/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java b/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java index 33784a841..c723a4d5f 100644 --- a/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java +++ b/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java @@ -17,26 +17,46 @@ package com.lambda.mixin.render; +import com.google.common.base.Strings; import com.lambda.command.CommandManager; +import com.lambda.module.modules.client.LambdaMoji; +import com.lambda.module.modules.client.RenderSettings; import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; import net.minecraft.client.gui.screen.ChatInputSuggestor; import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.client.network.ClientPlayNetworkHandler; import net.minecraft.command.CommandSource; +import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyVariable; import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; @Mixin(ChatInputSuggestor.class) -public class ChatInputSuggestorMixin { +public abstract class ChatInputSuggestorMixin { @Shadow @Final TextFieldWidget textField; + @Shadow + private @Nullable CompletableFuture pendingSuggestions; + + @Shadow + public abstract void show(boolean narrateFirstSuggestion); + @ModifyVariable(method = "refresh", at = @At(value = "STORE"), index = 3) private boolean refreshModify(boolean showCompletions) { return CommandManager.INSTANCE.isCommand(textField.getText()); @@ -46,4 +66,55 @@ private boolean refreshModify(boolean showCompletions) { private CommandDispatcher refreshRedirect(ClientPlayNetworkHandler instance) { return CommandManager.INSTANCE.currentDispatcher(textField.getText()); } + + @Inject(method = "refresh", at = @At("TAIL")) + private void refreshEmojiSuggestion(CallbackInfo ci) { + if (!LambdaMoji.INSTANCE.isEnabled() || + !LambdaMoji.INSTANCE.getSuggestions()) return; + + String typing = textField.getText(); + + // Don't suggest emojis in commands + if (CommandManager.INSTANCE.isCommand(typing) || + CommandManager.INSTANCE.isLambdaCommand(typing)) return; + + int cursor = textField.getCursor(); + String textToCursor = typing.substring(0, cursor); + if (textToCursor.isEmpty()) return; + + // Most right index at the left of the regex expression + int start = neoLambda$getLastColon(textToCursor); + if (start == -1) return; + + String emojiString = typing.substring(start + 1); + + Stream results = RenderSettings.INSTANCE.getEmojiFont().glyphs.getKeys() + .stream() + .filter(s -> s.startsWith(emojiString)) + .map(s -> s + ":"); + + pendingSuggestions = CommandSource.suggestMatching(results, new SuggestionsBuilder(textToCursor, start + 1)); + pendingSuggestions.thenRun(() -> { + if (!pendingSuggestions.isDone()) return; + + show(false); + }); + } + + @Unique + private static final Pattern COLON_PATTERN = Pattern.compile("(:[a-zA-Z0-9_]+)"); + + @Unique + private int neoLambda$getLastColon(String input) { + if (Strings.isNullOrEmpty(input)) return -1; + + int i = -1; + Matcher matcher = COLON_PATTERN.matcher(input); + + while (matcher.find()) { + i = matcher.start(); + } + + return i; + } } diff --git a/common/src/main/java/com/lambda/mixin/render/ChatScreenMixin.java b/common/src/main/java/com/lambda/mixin/render/ChatScreenMixin.java index 6fa80f568..cc6b0edb8 100644 --- a/common/src/main/java/com/lambda/mixin/render/ChatScreenMixin.java +++ b/common/src/main/java/com/lambda/mixin/render/ChatScreenMixin.java @@ -17,65 +17,15 @@ package com.lambda.mixin.render; -import com.lambda.Lambda; import com.lambda.command.CommandManager; -import com.lambda.graphics.renderer.gui.font.FontRenderer; -import com.lambda.graphics.renderer.gui.font.LambdaEmoji; -import com.lambda.graphics.renderer.gui.font.glyph.GlyphInfo; -import com.lambda.module.modules.client.LambdaMoji; -import com.lambda.util.math.Vec2d; -import kotlin.Pair; -import kotlin.ranges.IntRange; import net.minecraft.client.gui.screen.ChatScreen; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - @Mixin(ChatScreen.class) public abstract class ChatScreenMixin { - @ModifyArg(method = "sendMessage", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayNetworkHandler;sendChatMessage(Ljava/lang/String;)V"), index = 0) - private String modifyChatText(String chatText) { - if (LambdaMoji.INSTANCE.isDisabled()) return chatText; - - List> emojis = FontRenderer.Companion.parseEmojis(chatText, LambdaEmoji.Twemoji); - Collections.reverse(emojis); - - List pushEmojis = new ArrayList<>(); - List pushPositions = new ArrayList<>(); - - for (Pair emoji : emojis) { - String emojiString = chatText.substring(emoji.getSecond().getStart() + 1, emoji.getSecond().getEndInclusive()); - if (LambdaEmoji.Twemoji.get(emojiString) == null) - continue; - - // Because the width of a char is bigger than an emoji - // we can simply replace the matches string by a space - // and render it after the text - chatText = chatText.substring(0, emoji.getSecond().getStart()) + " " + chatText.substring(emoji.getSecond().getEndInclusive() + 1); - - // We cannot retain the position in the future, but we can - // assume that every time you send a message the height of - // the position will change by the height of the glyph - // The positions are from the top left corner of the screen - int x = Lambda.getMc().textRenderer.getWidth(chatText.substring(0, emoji.getSecond().getStart())); - int y = Lambda.getMc().textRenderer.fontHeight; - - pushEmojis.add(String.format(":%s:", emojiString)); - pushPositions.add(new Vec2d(x, y)); - } - - // Not optimal because it has to parse the emoji again but who cares - LambdaMoji.INSTANCE.add(pushEmojis, pushPositions); - - return chatText; - } - @Inject(method = "sendMessage", at = @At("HEAD"), cancellable = true) void sendMessageInject(String chatText, boolean addToHistory, CallbackInfoReturnable cir) { if (!CommandManager.INSTANCE.isLambdaCommand(chatText)) return; diff --git a/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java b/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java new file mode 100644 index 000000000..6dcc5b6d6 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java @@ -0,0 +1,127 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.mixin.render; + +import com.lambda.Lambda; +import com.lambda.graphics.renderer.gui.font.FontRenderer; +import com.lambda.graphics.renderer.gui.font.LambdaEmoji; +import com.lambda.module.modules.client.LambdaMoji; +import com.lambda.util.math.Vec2d; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.text.OrderedText; +import net.minecraft.text.Text; +import org.joml.Matrix4f; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +import java.util.List; + +@Mixin(TextRenderer.class) +public abstract class TextRendererMixin { + @Shadow + protected abstract int drawInternal(String text, float x, float y, int color, boolean shadow, Matrix4f matrix, VertexConsumerProvider vertexConsumers, TextRenderer.TextLayerType layerType, int backgroundColor, int light, boolean mirror); + + @Shadow + protected abstract int drawInternal(OrderedText text, float x, float y, int color, boolean shadow, Matrix4f matrix, VertexConsumerProvider vertexConsumerProvider, TextRenderer.TextLayerType layerType, int backgroundColor, int light); + + /** + * @author Edouard127 + * @reason xx + */ + @Overwrite + public int draw( + String text, + float x, + float y, + int color, + boolean shadow, + Matrix4f matrix, + VertexConsumerProvider vertexConsumers, + TextRenderer.TextLayerType layerType, + int backgroundColor, + int light, + boolean rightToLeft + ) { + if (LambdaMoji.INSTANCE.isDisabled()) return this.drawInternal(text, x, y, color, shadow, matrix, vertexConsumers, layerType, backgroundColor, light, rightToLeft); + + return this.drawInternal(neoLambda$parseEmojisAndRender(text, x, y), x, y, color, shadow, matrix, vertexConsumers, layerType, backgroundColor, light, rightToLeft); + } + + /** + * @author Edouard127 + * @reason xx + */ + @Overwrite + public int draw( + OrderedText text, + float x, + float y, + int color, + boolean shadow, + Matrix4f matrix, + VertexConsumerProvider vertexConsumers, + TextRenderer.TextLayerType layerType, + int backgroundColor, + int light + ) { + if (LambdaMoji.INSTANCE.isDisabled()) return this.drawInternal(text, x, y, color, shadow, matrix, vertexConsumers, layerType, backgroundColor, light); + + StringBuilder builder = new StringBuilder(); + text.accept((index, style, c) -> { + builder.appendCodePoint(c); + return true; + }); + + return this.drawInternal( + Text.literal(neoLambda$parseEmojisAndRender(builder.toString(), x, y)).asOrderedText(), + x, y, color, shadow, matrix, vertexConsumers, layerType, backgroundColor, light + ); + } + + @Unique + private String neoLambda$parseEmojisAndRender(String raw, float x, float y) { + List emojis = LambdaEmoji.Twemoji.parse(raw); + + for (String emoji : emojis) { + String constructed = ":" + emoji + ":"; + int index = raw.indexOf(constructed); + + if (LambdaEmoji.Twemoji.get(emoji) == null || + index == -1) continue; + + int height = Lambda.getMc().textRenderer.fontHeight; + int width = Lambda.getMc().textRenderer.getWidth(raw.substring(0, index)); + + LambdaMoji.INSTANCE.push(constructed, new Vec2d(x + width, y + (float) height / 2)); + + // Replace the emoji with whitespaces depending on the player's settings + raw = raw.replaceFirst(constructed, neoLambda$getReplacement()); + } + + return raw; + } + + @Unique + private String neoLambda$getReplacement() { + int emojiWidth = (int) (((double) Lambda.getMc().textRenderer.fontHeight / 2 / Lambda.getMc().textRenderer.getWidth(" ")) * LambdaMoji.INSTANCE.getScale()); + return " ".repeat(emojiWidth); + } +} diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt b/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt index 6b8cf2c16..7b10a33b2 100644 --- a/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt +++ b/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt @@ -22,64 +22,139 @@ import net.minecraft.util.math.RotationAxis import net.minecraft.util.math.Vec3d import org.joml.* +/** + * A utility object for managing OpenGL transformation matrices. + * Provides a stack-based approach to matrix operations such as translation, scaling, + * and world projection building, with optional support for vertex transformations. + */ object Matrices { - private val stack = ArrayDeque(1) - + /** + * A stack of 4x4 transformation matrices. + */ + private val stack = ArrayDeque(listOf(Matrix4f())) + + /** + * An optional matrix for applying vertex transformations. + */ var vertexTransformer: Matrix4d? = null + /** + * Executes a block of code within the context of a new matrix. + * The current matrix is pushed onto the stack before the block executes and popped after the block completes. + * + * Push and pop operations are essential for managing hierarchical transformations in OpenGL. + * - `push`: Saves the current matrix state to allow local transformations. + * - `block`: Code that uses the modified matrix (ex: rendering) + * - `pop`: Restores the previous state and effectively reverts any changes. + * + * @param block The block of code to execute within the context of the new matrix. + */ + fun push(block: Matrices.() -> Unit) { + push() + block() + pop() + } + + /** + * Pushes a copy of the current matrix onto the stack. + */ + fun push() { + val entry = stack.last() + stack.addLast(Matrix4f(entry)) + } + + /** + * Removes the top matrix from the stack. + * + * @throws NoSuchElementException If the stack is empty. + */ + fun pop() { + stack.removeLast() + } + + /** + * Translates the current matrix by the given x, y, and z values. + * + * @param x The translation amount along the X axis. + * @param y The translation amount along the Y axis. + * @param z The translation amount along the Z axis. Defaults to `0.0`. + */ fun translate(x: Double, y: Double, z: Double = 0.0) { translate(x.toFloat(), y.toFloat(), z.toFloat()) } + /** + * Translates the current matrix by the given x, y, and z values. + * + * @param x The translation amount along the X axis. + * @param y The translation amount along the Y axis. + * @param z The translation amount along the Z axis. Defaults to `0f`. + */ fun translate(x: Float, y: Float, z: Float = 0f) { stack.last().translate(x, y, z) } + /** + * Scales the current matrix by the given x, y, and z factors. + * + * @param x The scaling factor along the X axis. + * @param y The scaling factor along the Y axis. + * @param z The scaling factor along the Z axis. Defaults to `1.0`. + */ fun scale(x: Double, y: Double, z: Double = 1.0) { stack.last().scale(x.toFloat(), y.toFloat(), z.toFloat()) } + /** + * Scales the current matrix by the given x, y, and z factors. + * + * @param x The scaling factor along the X axis. + * @param y The scaling factor along the Y axis. + * @param z The scaling factor along the Z axis. Defaults to `1f`. + */ fun scale(x: Float, y: Float, z: Float = 1f) { stack.last().scale(x, y, z) } - fun multiply(quaternion: Quaternionf) { - stack.last().rotate(quaternion) - } - - fun multiply(quaternion: Quaternionf, originX: Float, originY: Float, originZ: Float) { - stack.last().rotateAround(quaternion, originX, originY, originZ) - } - - fun push() { - val entry = stack.last() - stack.addLast(Matrix4f(entry)) - } - - fun push(block: Matrices.() -> Unit) { - push() - this.block() - pop() - } - - fun pop() { - stack.removeLast() - } - - fun peek() = stack.last() - + /** + * Retrieves the current matrix from the stack without removing it. + * + * @throws NoSuchElementException if the stack is empty + * @return The top matrix on the stack + */ + fun peek(): Matrix4f = stack.last() + + /** + * Resets the matrix stack with a single initial matrix. + * + * @param entry The matrix to initialize the stack with. + */ fun resetMatrices(entry: Matrix4f) { stack.clear() stack.add(entry) } + /** + * Temporarily sets a vertex transformation matrix for the duration of a block. + * + * @param matrix The transformation matrix to apply to vertices. + * @param block The block of code to execute with the transformation applied. + */ fun withVertexTransform(matrix: Matrix4f, block: () -> Unit) { vertexTransformer = Matrix4d(matrix) block() vertexTransformer = null } - fun buildWorldProjection(pos: Vec3d, scale: Double = 1.0, mode: ProjRotationMode = ProjRotationMode.TO_CAMERA) = + /** + * Builds a world projection matrix for a given position, scale, and rotation mode. + * + * @param pos The position in world coordinates. + * @param scale The scaling factor. Defaults to `1.0`. + * @param mode The rotation mode to apply. Defaults to [ProjRotationMode.TO_CAMERA]. + * @return A [Matrix4f] representing the world projection. + */ + fun buildWorldProjection(pos: Vec3d, scale: Double = 1.0, mode: ProjRotationMode = ProjRotationMode.TO_CAMERA): Matrix4f = Matrix4f().apply { val s = 0.025f * scale.toFloat() @@ -93,6 +168,9 @@ object Matrices { scale(-s, -s, s) } + /** + * Modes for determining the rotation of the world projection. + */ enum class ProjRotationMode { TO_CAMERA, UP diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index 5e17dd2a9..a778f6568 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -21,93 +21,132 @@ import com.lambda.graphics.buffer.VertexPipeline import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib import com.lambda.graphics.buffer.vertex.attributes.VertexMode import com.lambda.graphics.renderer.gui.font.glyph.GlyphInfo -import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer import com.lambda.graphics.shader.Shader -import com.lambda.gui.api.component.core.DockingRect +import com.lambda.module.modules.client.ClickGui import com.lambda.module.modules.client.LambdaMoji import com.lambda.module.modules.client.RenderSettings -import com.lambda.util.Mouse import com.lambda.util.math.Vec2d import com.lambda.util.math.a import com.lambda.util.math.setAlpha import java.awt.Color -class FontRenderer( - private val font: LambdaFont, - private val emojis: LambdaEmoji -) { +/** + * Renders text and emoji glyphs using a shader-based font rendering system. + * This class handles text and emoji rendering, shadow effects, and text scaling. + */ +class FontRenderer { + private val chars = RenderSettings.textFont + private val emojis = RenderSettings.emojiFont + + private val shader = Shader("renderer/font") private val pipeline = VertexPipeline(VertexMode.TRIANGLES, VertexAttrib.Group.FONT) - var scaleMultiplier = 1.0 + val shadowShift get() = RenderSettings.shadowShift * 5.0 + val baselineOffset get() = RenderSettings.baselineOffset * 2.0f - 10f + val gap get() = RenderSettings.gap * 0.5f - 0.8f + val scaleMultiplier: Double get() = ClickGui.settingsFontScale /** - * Builds the vertex array for rendering the text. + * Builds the vertex array for rendering the provided text string at a specified position. + * + * @param text The text to render. + * @param position The position to render the text. + * @param color The color of the text. + * @param scale The scale factor of the text. + * @param shadow Whether to render a shadow for the text. + * @param parseEmoji Whether to parse and render emojis in the text. */ fun build( text: String, - position: Vec2d, + position: Vec2d = Vec2d.ZERO, color: Color = Color.WHITE, scale: Double = 1.0, - shadow: Boolean = true, + shadow: Boolean = RenderSettings.shadow, + parseEmoji: Boolean = LambdaMoji.isEnabled + ) = processText(text, color, scale, shadow, parseEmoji) { char, pos1, pos2, color -> buildGlyph(char, position, pos1, pos2, color) } + + /** + * Renders a single glyph at a given position. + * + * @param glyph The glyph information to render. + * @param origin The position to start from + * @param pos1 The starting position of the glyph. + * @param pos2 The end position of the glyph + * @param color The color of the glyph. + */ + fun buildGlyph( + glyph: GlyphInfo, + origin: Vec2d = Vec2d.ZERO, + pos1: Vec2d = Vec2d.ZERO, + pos2: Vec2d = pos1 + glyph.size, + color: Color = Color.WHITE, ) = pipeline.use { - iterateText(text, scale, shadow, color) { char, pos1, pos2, color -> - grow(4) - putQuad( - vec3m(pos1.x + position.x, pos1.y + position.y, 0.0).vec2(char.uv1.x, char.uv1.y).color(color).end(), - vec3m(pos1.x + position.x, pos2.y + position.y, 0.0).vec2(char.uv1.x, char.uv2.y).color(color).end(), - vec3m(pos2.x + position.x, pos2.y + position.y, 0.0).vec2(char.uv2.x, char.uv2.y).color(color).end(), - vec3m(pos2.x + position.x, pos1.y + position.y, 0.0).vec2(char.uv2.x, char.uv1.y).color(color).end() - ) - } + grow(4) + putQuad( + vec3m(pos1.x + origin.x, pos1.y + origin.y, 0.0).vec2(glyph.uv1.x, glyph.uv1.y).color(color).end(), + vec3m(pos1.x + origin.x, pos2.y + origin.y, 0.0).vec2(glyph.uv1.x, glyph.uv2.y).color(color).end(), + vec3m(pos2.x + origin.x, pos2.y + origin.y, 0.0).vec2(glyph.uv2.x, glyph.uv2.y).color(color).end(), + vec3m(pos2.x + origin.x, pos1.y + origin.y, 0.0).vec2(glyph.uv2.x, glyph.uv1.y).color(color).end() + ) } + /** - * Calculates the width of the given text. + * Calculates the width of the specified text. + * + * @param text The text to measure. + * @param scale The scale factor for the width calculation. + * @param parseEmoji Whether to include emojis in the width calculation. + * @return The width of the text at the specified scale. */ - fun getWidth(text: String, scale: Double = 1.0): Double { + fun getWidth( + text: String, + scale: Double = 1.0, + parseEmoji: Boolean = LambdaMoji.isEnabled, + ): Double { var width = 0.0 - iterateText(text, scale, false) { char, _, _, _ -> width += char.width + gap } + processText(text, scale = scale, parseEmoji = parseEmoji) { char, _, _, _ -> width += char.width } return width * getScaleFactor(scale) } /** - * Calculates the height of the text. + * Calculates the height of the text based on the specified scale. * - * The values are hardcoded - * We do not need to ask the emoji font since the height is smaller + * @param scale The scale factor for the height calculation. + * @return The height of the text at the specified scale. */ - fun getHeight(scale: Double = 1.0) = font.glyphs.fontHeight * getScaleFactor(scale) * 0.7 + fun getHeight(scale: Double = 1.0) = chars.glyphs.fontHeight * getScaleFactor(scale) * 0.7 /** - * Iterates over each character and emoji in the text. + * Iterates over each character and emoji in the text and applies a block operation. * * @param text The text to iterate over. + * @param color The color of the text. * @param scale The scale of the text. * @param shadow Whether to render a shadow. - * @param color The color of the text. - * @param block The block to execute for each character. - * - * @see GlyphInfo + * @param parseEmoji Whether to parse and include emojis. + * @param block The function to apply to each character or emoji glyph. */ - private fun iterateText( + private fun processText( text: String, - scale: Double, - shadow: Boolean, color: Color = Color.WHITE, + scale: Double = 1.0, + shadow: Boolean = RenderSettings.shadow, + parseEmoji: Boolean = LambdaMoji.isEnabled, block: (GlyphInfo, Vec2d, Vec2d, Color) -> Unit ) { val actualScale = getScaleFactor(scale) val scaledGap = gap * actualScale val shadowColor = getShadowColor(color) - val emojiColor = Color.WHITE.setAlpha(color.a) + val emojiColor = color.setAlpha(color.a) var posX = 0.0 val posY = getHeight(scale) * -0.5 + baselineOffset * actualScale - val emojis = parseEmojis(text, emojis) + fun drawGlyph(info: GlyphInfo?, color: Color, offset: Double = 0.0) { + if (info == null) return - fun draw(info: GlyphInfo, color: Color, offset: Double = 0.0) { val scaledSize = info.size * actualScale val pos1 = Vec2d(posX, posY) + offset * actualScale val pos2 = pos1 + scaledSize @@ -116,83 +155,77 @@ class FontRenderer( if (offset == 0.0) posX += scaledSize.x + scaledGap } - var index = 0 - textProcessor@ while (index < text.length) { - var innerLoopContact = false // Instead of using BreakContinueInInlineLambdas, we use this - - if (LambdaMoji.isEnabled) { - // Check if there are emojis to render - emojis.firstOrNull { index in it.second }?.let { emoji -> - if (index == emoji.second.first) draw(emoji.first, emojiColor) - - // Skip the emoji - index = emoji.second.last + 1 - innerLoopContact = true - } - } - - if (innerLoopContact) continue@textProcessor - - // Render chars - val charInfo = font[text[index]] ?: continue@textProcessor - - // Draw a shadow before - if (shadow && RenderSettings.shadow && shadowShift > 0.0) { - draw(charInfo, shadowColor, shadowShift) + val parsed = if (parseEmoji) emojis.parse(text) else mutableListOf() + + fun processTextSection(section: String, hasEmojis: Boolean) { + if (section.isEmpty()) return + if (!parseEmoji || parsed.isEmpty() || !hasEmojis) { + // Draw simple characters if no emojis are present + section + .mapNotNull { chars[it] } + .forEach { charGlyph -> + if (shadow && shadowShift > 0.0) drawGlyph(charGlyph, shadowColor, shadowShift) + drawGlyph(charGlyph, color) + } + } else { + // Only compute the first parsed emoji to avoid duplication + // This is important in order to keep the parsed ranges valid + // If you do not this, you will get out of bounds positions + // due to slicing + val emoji = parsed.removeFirstOrNull() ?: return + + // Iterate the emojis from left to right + val start = section.indexOf(emoji) + val end = start + emoji.length + 1 + + val preEmojiText = section.substring(0, start - 1) + val postEmojiText = section.substring(end) + + // Draw the text without emoji + processTextSection(preEmojiText, hasEmojis = false) + + // Draw the emoji + drawGlyph(emojis[emoji], emojiColor) + + // Process the rest of the text after the emoji + processTextSection(postEmojiText, hasEmojis = true) } - - // Draw actual char over the shadow - draw(charInfo, color) - - index++ } + + // Start processing the full text + processTextSection(text, hasEmojis = parsed.isNotEmpty()) } - private fun getScaleFactor(scale: Double) = scaleMultiplier * scale * 0.12 + /** + * Calculates the scale factor for the text based on the provided scale. + * + * @param scale The base scale factor. + * @return The adjusted scale factor. + */ + fun getScaleFactor(scale: Double): Double = scaleMultiplier * scale * 0.12 - private fun getShadowColor(color: Color): Color { - return Color( - (color.red * RenderSettings.shadowBrightness).toInt(), - (color.green * RenderSettings.shadowBrightness).toInt(), - (color.blue * RenderSettings.shadowBrightness).toInt(), - color.alpha - ) - } + /** + * Calculates the shadow color by adjusting the brightness of the input color. + * + * @param color The original color. + * @return The modified shadow color. + */ + fun getShadowColor(color: Color): Color = Color( + (color.red * RenderSettings.shadowBrightness).toInt(), + (color.green * RenderSettings.shadowBrightness).toInt(), + (color.blue * RenderSettings.shadowBrightness).toInt(), + color.alpha + ) fun render() { shader.use() shader["u_EmojiTexture"] = 1 - font.glyphs.bind() + chars.glyphs.bind() emojis.glyphs.bind() pipeline.upload() pipeline.render() pipeline.clear() } - - companion object { - private val shader = Shader("renderer/font") - - val shadowShift get() = RenderSettings.shadowShift * 5.0 - val baselineOffset get() = RenderSettings.baselineOffset * 2.0f - 10f - val gap get() = RenderSettings.gap * 0.5f - 0.8f - - private val emojiRegex = Regex(":[a-zA-Z0-9_]+:") - - /** - * Parses the emojis in the given text. - * - * @param text The text to parse. - * @return A list of pairs containing the glyph info and the range of the emoji in the text. - */ - fun parseEmojis(text: String, emojis: LambdaEmoji) = - mutableListOf>().apply { - emojiRegex.findAll(text).forEach { match -> - val emojiKey = match.value.substring(1, match.value.length - 1) - val charInfo = emojis[emojiKey] ?: return@forEach - add(charInfo to match.range) - } - } - } } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt index 683d8750d..aac0d1e21 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt @@ -31,6 +31,18 @@ enum class LambdaEmoji(private val zipUrl: String) { glyphs = EmojiGlyphs(zipUrl) } + private val emojiRegex = Regex(":[a-zA-Z0-9_]+:") + + /** + * Parses the emojis in the given text. + * + * @param text The text to parse. + * + * @return A list of parsed strings that does not contain the colons + */ + fun parse(text: String): MutableList = + emojiRegex.findAll(text).map { it.value.drop(1).dropLast(1) }.toMutableList() + object Loader : Loadable { override fun load(): String { entries.forEach(LambdaEmoji::loadGlyphs) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt index bbf113885..8d9ad7dee 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt @@ -43,6 +43,7 @@ class EmojiGlyphs(zipUrl: String) { private lateinit var graphics: Graphics2D val count get() = emojiMap.size + val keys get() = emojiMap.keys init { runCatching { @@ -82,7 +83,7 @@ class EmojiGlyphs(zipUrl: String) { image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) graphics = image.graphics as Graphics2D - graphics.color = Color(0, 0, 0, 0) + graphics.background = Color(0, 0, 0, 0) var x = 0 var y = 0 diff --git a/common/src/main/kotlin/com/lambda/gui/api/RenderLayer.kt b/common/src/main/kotlin/com/lambda/gui/api/RenderLayer.kt index 784981458..254be3eeb 100644 --- a/common/src/main/kotlin/com/lambda/gui/api/RenderLayer.kt +++ b/common/src/main/kotlin/com/lambda/gui/api/RenderLayer.kt @@ -18,8 +18,6 @@ package com.lambda.gui.api import com.lambda.graphics.renderer.gui.font.FontRenderer -import com.lambda.graphics.renderer.gui.font.LambdaEmoji -import com.lambda.graphics.renderer.gui.font.LambdaFont import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer import com.lambda.threading.mainThread @@ -28,19 +26,10 @@ class RenderLayer { val filled by mainThread(::FilledRectRenderer) val outline by mainThread(::OutlineRectRenderer) - val font by mainThread { - FontRenderer( - LambdaFont.FiraSansRegular, - LambdaEmoji.Twemoji, - ) - } - - private val boldFont0 = lazy { - FontRenderer( - LambdaFont.FiraSansBold, - LambdaEmoji.Twemoji, - ) - } + // TODO: CHANGE BOTH OF THESE!!!! + // I do NOT want to see 110 vbos + val font by mainThread { FontRenderer() } + private val boldFont0 = lazy { FontRenderer() } val boldFont by boldFont0 diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt index 206cbfec4..3e95483a9 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt @@ -24,6 +24,7 @@ import com.lambda.config.settings.comparable.EnumSetting import com.lambda.config.settings.complex.KeyBindSetting import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.graphics.gl.Scissor.scissor +import com.lambda.graphics.renderer.gui.font.FontRenderer import com.lambda.gui.api.GuiEvent import com.lambda.gui.api.RenderLayer import com.lambda.gui.api.component.WindowComponent @@ -113,8 +114,6 @@ class ModuleButton( is GuiEvent.Render -> { super.onEvent(e) - settingsRenderer.font.scaleMultiplier = ClickGui.settingsFontScale - // Shadow renderer.filled.apply { val rect = Rect( diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt b/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt index 32f5dd38f..c033b9064 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt @@ -18,7 +18,6 @@ package com.lambda.module.modules.client import com.lambda.event.events.RenderEvent -import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listener import com.lambda.gui.api.RenderLayer import com.lambda.module.Module @@ -31,35 +30,22 @@ object LambdaMoji : Module( defaultTags = setOf(ModuleTag.CLIENT, ModuleTag.RENDER), enabledByDefault = true, ) { - private val scale by setting("Emoji Scale", 1.0, 0.5..2.0, 0.1) + val scale by setting("Emoji Scale", 1.0, 0.5..1.5, 0.1) + val suggestions by setting("Chat Suggestions", true) private val renderer = RenderLayer() - private val renderQueue = hashMapOf, List>() + private val renderQueue = mutableListOf>() init { - listener { - var index = 0 - renderQueue.forEach { (emojis, positions) -> - emojis.forEachIndexed { emojiIndex, emoji -> - val pos = positions[emojiIndex] - - renderer.font.build( - text = emoji, - position = Vec2d(pos.x, pos.y * (index.toDouble() + 1)), - scale = scale, - ) - } - - index++ + listener { + renderQueue.forEach { (text, position) -> + renderer.font.build(text, position, scale = scale) } - } - listener { renderer.render() + renderQueue.clear() } } - fun add(emojis: List, positions: List) { - renderQueue[emojis] = positions - } + fun push(text: String, position: Vec2d) = renderQueue.add(Pair(text, position)) } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt index 18403baf5..7e97530da 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt @@ -17,6 +17,8 @@ package com.lambda.module.modules.client +import com.lambda.graphics.renderer.gui.font.LambdaEmoji +import com.lambda.graphics.renderer.gui.font.LambdaFont import com.lambda.module.Module import com.lambda.module.tag.ModuleTag @@ -28,6 +30,8 @@ object RenderSettings : Module( private val page by setting("Page", Page.Font) // Font + val textFont by setting("Text Font", LambdaFont.FiraSansRegular) + val emojiFont by setting("Emoji Font", LambdaEmoji.Twemoji) val shadow by setting("Shadow", true) { page == Page.Font } val shadowBrightness by setting("Shadow Brightness", 0.35, 0.0..0.5, 0.01) { page == Page.Font && shadow } val shadowShift by setting("Shadow Shift", 1.0, 0.0..2.0, 0.05) { page == Page.Font && shadow } diff --git a/common/src/main/resources/lambda.mixins.common.json b/common/src/main/resources/lambda.mixins.common.json index 5c745c805..46cc90b42 100644 --- a/common/src/main/resources/lambda.mixins.common.json +++ b/common/src/main/resources/lambda.mixins.common.json @@ -11,6 +11,7 @@ "entity.ClientPlayerEntityMixin", "entity.ClientPlayInteractionManagerMixin", "entity.EntityMixin", + "entity.FireworkRocketEntityMixin", "entity.LivingEntityMixin", "entity.PlayerEntityMixin", "input.KeyBindingMixin", @@ -39,12 +40,12 @@ "render.RenderTickCounterMixin", "render.ScreenHandlerMixin", "render.SplashOverlayMixin", + "render.TextRendererMixin", "render.VertexBufferMixin", "render.WorldRendererMixin", "world.BlockCollisionSpliteratorMixin", "world.ClientChunkManagerMixin", - "world.ClientWorldMixin", - "entity.FireworkRocketEntityMixin" + "world.ClientWorldMixin" ], "injectors": { "defaultRequire": 1 From ae7bd1fa2bb84b87939e3b0a30f9c9264f80dd67 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Wed, 27 Nov 2024 14:34:14 -0500 Subject: [PATCH 045/114] fixed mouse utils --- .../kotlin/com/lambda/newgui/LambdaScreen.kt | 4 +- .../src/main/kotlin/com/lambda/util/Mouse.kt | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt index 6581d3987..14d27d727 100644 --- a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt +++ b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt @@ -98,12 +98,12 @@ class LambdaScreen( } override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { - layout.onEvent(GuiEvent.MouseClick(Mouse.Button(button), Mouse.Action.Click, rescaleMouse(mouseX, mouseY))) + layout.onEvent(GuiEvent.MouseClick(Mouse.Button.fromMouseCode(button), Mouse.Action.Click, rescaleMouse(mouseX, mouseY))) return true } override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { - layout.onEvent(GuiEvent.MouseClick(Mouse.Button(button), Mouse.Action.Release, rescaleMouse(mouseX, mouseY))) + layout.onEvent(GuiEvent.MouseClick(Mouse.Button.fromMouseCode(button), Mouse.Action.Release, rescaleMouse(mouseX, mouseY))) return true } diff --git a/common/src/main/kotlin/com/lambda/util/Mouse.kt b/common/src/main/kotlin/com/lambda/util/Mouse.kt index 17f550a51..ea78f1c56 100644 --- a/common/src/main/kotlin/com/lambda/util/Mouse.kt +++ b/common/src/main/kotlin/com/lambda/util/Mouse.kt @@ -17,7 +17,16 @@ package com.lambda.util +import com.lambda.Lambda.mc +import com.mojang.blaze3d.systems.RenderSystem import org.lwjgl.glfw.GLFW +import org.lwjgl.glfw.GLFW.GLFW_ARROW_CURSOR +import org.lwjgl.glfw.GLFW.GLFW_POINTING_HAND_CURSOR +import org.lwjgl.glfw.GLFW.GLFW_RESIZE_EW_CURSOR +import org.lwjgl.glfw.GLFW.GLFW_RESIZE_NS_CURSOR +import org.lwjgl.glfw.GLFW.GLFW_RESIZE_NWSE_CURSOR +import org.lwjgl.glfw.GLFW.glfwCreateStandardCursor +import org.lwjgl.glfw.GLFW.glfwSetCursor import kotlin.jvm.Throws class Mouse { @@ -64,4 +73,41 @@ class Mouse { nameMap[name.lowercase()] ?: throw IllegalArgumentException("Action name '$name' not found in nameMap.") } } + + enum class Cursor(private val getCursorPointer: () -> Long) { + Arrow(::arrow), + Pointer(::pointer), + ResizeH(::resizeH), ResizeV(::resizeV), ResizeHV(::resizeHV); + + fun set() { + if (lastCursor == this) return + lastCursor = this + + RenderSystem.assertOnRenderThread() + glfwSetCursor(mc.window.handle, getCursorPointer()) + } + } + + class CursorController { + private var lastSetCursor: Cursor? = null + + fun setCursor(cursor: Cursor) { + // We're doing this to let other controllers be able to set the cursor when this one doesn't change + if (lastSetCursor == cursor && cursor == Cursor.Arrow) return + + cursor.set() + lastSetCursor = cursor + } + + fun reset() = setCursor(Cursor.Arrow) + } + + companion object { + private val arrow by lazy { glfwCreateStandardCursor(GLFW_ARROW_CURSOR) } + private val pointer by lazy { glfwCreateStandardCursor(GLFW_POINTING_HAND_CURSOR) } + private val resizeH by lazy { glfwCreateStandardCursor(GLFW_RESIZE_EW_CURSOR) } + private val resizeV by lazy { glfwCreateStandardCursor(GLFW_RESIZE_NS_CURSOR) } + private val resizeHV by lazy { glfwCreateStandardCursor(GLFW_RESIZE_NWSE_CURSOR) } + var lastCursor = Cursor.Arrow + } } From 2d0e9b7e8ce9394931836ccd0e5c874a9f56039c Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:19:41 -0500 Subject: [PATCH 046/114] refactor: global texture atlas --- .../mixin/render/ChatInputSuggestorMixin.java | 5 +- .../mixin/render/TextRendererMixin.java | 5 +- .../renderer/gui/font/FontRenderer.kt | 10 +- .../gui/font/{glyph => }/GlyphInfo.kt | 2 +- .../graphics/renderer/gui/font/LambdaAtlas.kt | 245 ++++++++++++++++++ .../graphics/renderer/gui/font/LambdaEmoji.kt | 17 +- .../graphics/renderer/gui/font/LambdaFont.kt | 19 +- .../renderer/gui/font/glyph/EmojiGlyphs.kt | 130 ---------- .../renderer/gui/font/glyph/FontGlyphs.kt | 115 -------- .../lambda/graphics/texture/TextureUtils.kt | 32 --- 10 files changed, 266 insertions(+), 314 deletions(-) rename common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/{glyph => }/GlyphInfo.kt (97%) create mode 100644 common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt delete mode 100644 common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt delete mode 100644 common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt diff --git a/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java b/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java index c723a4d5f..c602e6674 100644 --- a/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java +++ b/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java @@ -19,6 +19,7 @@ import com.google.common.base.Strings; import com.lambda.command.CommandManager; +import com.lambda.graphics.renderer.gui.font.LambdaAtlas; import com.lambda.module.modules.client.LambdaMoji; import com.lambda.module.modules.client.RenderSettings; import com.mojang.brigadier.CommandDispatcher; @@ -88,8 +89,8 @@ private void refreshEmojiSuggestion(CallbackInfo ci) { String emojiString = typing.substring(start + 1); - Stream results = RenderSettings.INSTANCE.getEmojiFont().glyphs.getKeys() - .stream() + Stream results = LambdaAtlas.INSTANCE.getKeys(RenderSettings.INSTANCE.getEmojiFont()) + .keySet().stream() .filter(s -> s.startsWith(emojiString)) .map(s -> s + ":"); diff --git a/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java b/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java index 6dcc5b6d6..bd2a51ddf 100644 --- a/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java +++ b/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java @@ -18,9 +18,10 @@ package com.lambda.mixin.render; import com.lambda.Lambda; -import com.lambda.graphics.renderer.gui.font.FontRenderer; +import com.lambda.graphics.renderer.gui.font.LambdaAtlas; import com.lambda.graphics.renderer.gui.font.LambdaEmoji; import com.lambda.module.modules.client.LambdaMoji; +import com.lambda.module.modules.client.RenderSettings; import com.lambda.util.math.Vec2d; import net.minecraft.client.font.TextRenderer; import net.minecraft.client.render.VertexConsumerProvider; @@ -104,7 +105,7 @@ public int draw( String constructed = ":" + emoji + ":"; int index = raw.indexOf(constructed); - if (LambdaEmoji.Twemoji.get(emoji) == null || + if (LambdaAtlas.INSTANCE.get(RenderSettings.INSTANCE.getEmojiFont(), emoji) == null || index == -1) continue; int height = Lambda.getMc().textRenderer.fontHeight; diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index a778f6568..186d39a10 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -20,7 +20,9 @@ package com.lambda.graphics.renderer.gui.font import com.lambda.graphics.buffer.VertexPipeline import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib import com.lambda.graphics.buffer.vertex.attributes.VertexMode -import com.lambda.graphics.renderer.gui.font.glyph.GlyphInfo +import com.lambda.graphics.renderer.gui.font.LambdaAtlas.bind +import com.lambda.graphics.renderer.gui.font.LambdaAtlas.get +import com.lambda.graphics.renderer.gui.font.LambdaAtlas.height import com.lambda.graphics.shader.Shader import com.lambda.module.modules.client.ClickGui import com.lambda.module.modules.client.LambdaMoji @@ -115,7 +117,7 @@ class FontRenderer { * @param scale The scale factor for the height calculation. * @return The height of the text at the specified scale. */ - fun getHeight(scale: Double = 1.0) = chars.glyphs.fontHeight * getScaleFactor(scale) * 0.7 + fun getHeight(scale: Double = 1.0) = chars.height * getScaleFactor(scale) * 0.7 /** * Iterates over each character and emoji in the text and applies a block operation. @@ -221,8 +223,8 @@ class FontRenderer { shader.use() shader["u_EmojiTexture"] = 1 - chars.glyphs.bind() - emojis.glyphs.bind() + chars.bind() + emojis.bind() pipeline.upload() pipeline.render() diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/GlyphInfo.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/GlyphInfo.kt similarity index 97% rename from common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/GlyphInfo.kt rename to common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/GlyphInfo.kt index 691b656c3..59e67780a 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/GlyphInfo.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/GlyphInfo.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.lambda.graphics.renderer.gui.font.glyph +package com.lambda.graphics.renderer.gui.font import com.lambda.util.math.Vec2d diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt new file mode 100644 index 000000000..ce80f577a --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt @@ -0,0 +1,245 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.renderer.gui.font + +import com.google.common.math.IntMath.pow +import com.lambda.event.events.ConnectionEvent +import com.lambda.event.listener.UnsafeListener.Companion.unsafeListenOnce +import com.lambda.graphics.texture.MipmapTexture +import com.lambda.http.Method +import com.lambda.http.request +import com.lambda.module.modules.client.RenderSettings +import com.lambda.threading.runGameScheduled +import com.lambda.util.LambdaResource +import com.lambda.util.math.Vec2d +import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap +import it.unimi.dsi.fastutil.objects.Object2DoubleArrayMap +import it.unimi.dsi.fastutil.objects.Object2IntArrayMap +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import java.awt.Color +import java.awt.Font +import java.awt.FontMetrics +import java.awt.Graphics2D +import java.awt.RenderingHints +import java.awt.image.BufferedImage +import java.util.function.ToIntFunction +import java.util.zip.ZipFile +import javax.imageio.ImageIO +import kotlin.math.ceil +import kotlin.math.log2 +import kotlin.math.max +import kotlin.math.sqrt +import kotlin.time.Duration.Companion.days + +/** + * The [LambdaAtlas] manages the creation and binding of texture atlases for fonts, emojis and user defined atlases + * It stores glyph information, manages texture uploads, and provides functionality to build texture buffers for fonts and emoji sets + * + * It caches font information and emoji data for efficient rendering and includes mechanisms for uploading and binding texture atlases + */ +object LambdaAtlas { + private val fontMap = Object2ObjectOpenHashMap>() + private val emojiMap = Object2ObjectOpenHashMap>() + private val textureMap = Object2ObjectOpenHashMap() + private val slotReservation = Object2IntArrayMap() + + private val bufferPool = + mutableMapOf() // This array is nuked once the data is dispatched to OpenGL + + private val fontCache = mutableMapOf() + private val metricCache = mutableMapOf() + private val heightCache = Object2DoubleArrayMap() + + operator fun LambdaFont.get(char: Char): GlyphInfo? = fontMap.getValue(this)[char.code] + operator fun LambdaEmoji.get(string: String): GlyphInfo? = emojiMap.getValue(this)[string] + + /** + * Upload additional atlas that can be used with the owner to bind textures to shaders + */ + fun Any.uploadAtlas(data: BufferedImage) = textureMap.set(this, MipmapTexture(data)) + + // Allow binding any valid font definition enums + fun > T.bind() = with(textureMap.getValue(this)) + { + bind(slot = slotReservation.computeIfAbsent(this, ToIntFunction { slotReservation.size })) + setLOD(RenderSettings.lodBias.toFloat()) + } + + val LambdaFont.height: Double + get() = heightCache.getDouble(fontCache[this]) + + val LambdaEmoji.keys + get() = emojiMap.getValue(this) + + /** + * Builds the buffer for an emoji set by reading a ZIP file containing emoji images. + * The images are arranged into a texture atlas, and their UV coordinates are computed for later rendering. + * + * @throws IllegalStateException If the texture size is too small to fit the emojis. + */ + fun LambdaEmoji.buildBuffer() { + val file = request(url) { + method(Method.GET) + }.maybeDownload("emojis.zip", maxAge = 30.days) + + var image: BufferedImage + + ZipFile(file).use { zip -> + val firstImage = ImageIO.read(zip.getInputStream(zip.entries().nextElement())) + val length = zip.size().toDouble() + + val textureDimensionLength: (Int) -> Int = { dimLength -> + pow(2, ceil(log2((dimLength + 2) * sqrt(length))).toInt()) + } + + val width = textureDimensionLength(firstImage.width) + val height = textureDimensionLength(firstImage.height) + val texelSize = Vec2d.ONE / Vec2d(width, height) + + image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) + val graphics = image.graphics as Graphics2D + graphics.color = Color(0, 0, 0, 0) + + var x = 0 + var y = 0 + + val constructed = Object2ObjectOpenHashMap() + for (entry in zip.entries()) { + val name = entry.name.substringAfterLast("/").substringBeforeLast(".") + val emoji = ImageIO.read(zip.getInputStream(entry)) + + if (x + emoji.width >= image.width) { + y += emoji.height + 2 + x = 0 + } + + check(y + emoji.height < image.height) { "Can't load emoji glyphs. Texture size is too small" } + + graphics.drawImage(emoji, x, y, null) + + val size = Vec2d(emoji.width, emoji.height) + val uv1 = Vec2d(x, y) * texelSize + val uv2 = Vec2d(x, y).plus(size) * texelSize + + constructed[name] = GlyphInfo(size, -uv1, -uv2) + + x += emoji.width + 2 + } + + emojiMap[this] = constructed + } + + bufferPool[this] = image + } + + fun LambdaFont.buildBuffer( + characters: Int = 2048 // How many characters from that font should be used for the generation + ) { + val font = fontCache.computeIfAbsent(this) { + val resource = LambdaResource("fonts/$fontName.ttf") + + Font.createFont(Font.TRUETYPE_FONT, resource.stream).deriveFont(64.0f) + } + + val textureSize = characters * 2 + val oneTexelSize = 1.0 / textureSize + + val image = BufferedImage(textureSize, textureSize, BufferedImage.TYPE_INT_ARGB) + + val graphics = image.graphics as Graphics2D + graphics.background = Color(0, 0, 0, 0) + + var x = 0 + var y = 0 + var rowHeight = 0 + + val constructed = Int2ObjectArrayMap() + (Char.MIN_VALUE.. + val charImage = getCharImage(font, char) ?: return@forEach + + rowHeight = max(rowHeight, charImage.height + 2) + + if (x + charImage.width >= textureSize) { + y += rowHeight + x = 0 + rowHeight = 0 + } + + check(y + charImage.height <= textureSize) { "Can't load font glyphs. Texture size is too small" } + + graphics.drawImage(charImage, x, y, null) + + val size = Vec2d(charImage.width, charImage.height) + val uv1 = Vec2d(x, y) * oneTexelSize + val uv2 = Vec2d(x, y).plus(size) * oneTexelSize + + constructed[char.code] = GlyphInfo(size, uv1, uv2) + heightCache[font] = max(heightCache.getDouble(font), size.y) // No compare set unfortunately + + x += charImage.width + 2 + } + + fontMap[this] = constructed + bufferPool[this] = image + } + + init { + // TODO: Change this when we've refactored the loadables + unsafeListenOnce { + runGameScheduled { + bufferPool.forEach { (owner, image) -> + textureMap[owner] = MipmapTexture(image) + } + + bufferPool.clear() + } + + true + } + } + + private fun getCharImage(font: Font, codePoint: Char): BufferedImage? { + if (!font.canDisplay(codePoint)) return null + + val fontMetrics = metricCache.getOrPut(font) { + val image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB) + val graphics2D = image.createGraphics() + + graphics2D.font = font + graphics2D.dispose() + + image.graphics.getFontMetrics(font) + } + + val charWidth = if (fontMetrics.charWidth(codePoint) > 0) fontMetrics.charWidth(codePoint) else 8 + val charHeight = if (fontMetrics.height > 0) fontMetrics.height else font.size + + val charImage = BufferedImage(charWidth, charHeight, BufferedImage.TYPE_INT_ARGB) + val graphics2D = charImage.createGraphics() + + graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_DEFAULT) + + graphics2D.font = font + graphics2D.color = Color.WHITE + graphics2D.drawString(codePoint.toString(), 0, fontMetrics.ascent) + graphics2D.dispose() + + return charImage + } +} diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt index aac0d1e21..7f669789d 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt @@ -18,19 +18,11 @@ package com.lambda.graphics.renderer.gui.font import com.lambda.core.Loadable -import com.lambda.graphics.renderer.gui.font.glyph.EmojiGlyphs +import com.lambda.graphics.renderer.gui.font.LambdaAtlas.buildBuffer -enum class LambdaEmoji(private val zipUrl: String) { +enum class LambdaEmoji(val url: String) { Twemoji("https://github.com/Edouard127/emoji-generator/releases/latest/download/emojis.zip"); - lateinit var glyphs: EmojiGlyphs - - operator fun get(emoji: String) = glyphs.emojiFromString(emoji) - - fun loadGlyphs() { - glyphs = EmojiGlyphs(zipUrl) - } - private val emojiRegex = Regex(":[a-zA-Z0-9_]+:") /** @@ -45,8 +37,9 @@ enum class LambdaEmoji(private val zipUrl: String) { object Loader : Loadable { override fun load(): String { - entries.forEach(LambdaEmoji::loadGlyphs) - return "Loaded ${entries.size} emoji sets with a total of ${entries.sumOf { it.glyphs.count }} emojis" + entries.forEach { it.buildBuffer() } + + return "Loaded ${entries.size} emoji sets" } } } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaFont.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaFont.kt index 59eab9ffc..ed76cb5dd 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaFont.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaFont.kt @@ -18,28 +18,15 @@ package com.lambda.graphics.renderer.gui.font import com.lambda.core.Loadable -import com.lambda.graphics.renderer.gui.font.glyph.FontGlyphs -import com.lambda.util.LambdaResource -import java.awt.Font +import com.lambda.graphics.renderer.gui.font.LambdaAtlas.buildBuffer -enum class LambdaFont(private val fontName: String) { +enum class LambdaFont(val fontName: String) { FiraSansRegular("FiraSans-Regular"), FiraSansBold("FiraSans-Bold"); - lateinit var glyphs: FontGlyphs - - operator fun get(char: Char) = glyphs.getChar(char) - - fun loadGlyphs() { - val resource = LambdaResource("fonts/$fontName.ttf") - val stream = resource.stream ?: throw IllegalStateException("Failed to locate font $fontName") - val font = Font.createFont(Font.TRUETYPE_FONT, stream).deriveFont(64.0f) - glyphs = FontGlyphs(font) - } - object Loader : Loadable { override fun load(): String { - entries.forEach(LambdaFont::loadGlyphs) + entries.forEach { it.buildBuffer() } return "Loaded ${entries.size} fonts" } } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt deleted file mode 100644 index 8d9ad7dee..000000000 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.renderer.gui.font.glyph - -import com.google.common.math.IntMath.pow -import com.lambda.Lambda.LOG -import com.lambda.graphics.texture.MipmapTexture -import com.lambda.http.Method -import com.lambda.http.request -import com.lambda.module.modules.client.RenderSettings -import com.lambda.util.math.Vec2d -import java.awt.Color -import java.awt.Graphics2D -import java.awt.image.BufferedImage -import java.io.File -import java.util.zip.ZipFile -import javax.imageio.ImageIO -import kotlin.math.ceil -import kotlin.math.log2 -import kotlin.math.sqrt -import kotlin.time.Duration.Companion.days - -class EmojiGlyphs(zipUrl: String) { - private val emojiMap = mutableMapOf() - private lateinit var fontTexture: MipmapTexture - - private lateinit var image: BufferedImage - private lateinit var graphics: Graphics2D - - val count get() = emojiMap.size - val keys get() = emojiMap.keys - - init { - runCatching { - downloadAndProcessZip(zipUrl) - }.onFailure { - LOG.error("Failed to load emojis: ${it.message}", it) - fontTexture = MipmapTexture(BufferedImage(1024, 1024, BufferedImage.TYPE_INT_ARGB)) - } - } - - private fun downloadAndProcessZip(zipUrl: String) { - val file = request(zipUrl) { - method(Method.GET) - }.maybeDownload("emojis.zip", maxAge = 30.days) - - fontTexture = MipmapTexture(processZip(file)) - } - - /** - * Processes the given zip file and loads the emojis into the texture. - * - * @param file The zip file containing the emojis. - * @return The texture containing the emojis. - */ - private fun processZip(file: File): BufferedImage { - ZipFile(file).use { zip -> - val firstImage = ImageIO.read(zip.getInputStream(zip.entries().nextElement())) - val length = zip.size().toDouble() - - val textureDimensionLength: (Int) -> Int = { dimLength -> - pow(2, ceil(log2((dimLength + STEP) * sqrt(length))).toInt()) - } - - val width = textureDimensionLength(firstImage.width) - val height = textureDimensionLength(firstImage.height) - val texelSize = Vec2d.ONE / Vec2d(width, height) - - image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) - graphics = image.graphics as Graphics2D - graphics.background = Color(0, 0, 0, 0) - - var x = 0 - var y = 0 - - for (entry in zip.entries()) { - val name = entry.name.substringAfterLast("/").substringBeforeLast(".") - val emoji = ImageIO.read(zip.getInputStream(entry)) - - if (x + emoji.width >= image.width) { - y += emoji.height + STEP - x = 0 - } - - check(y + emoji.height < image.height) { "Can't load emoji glyphs. Texture size is too small" } - - graphics.drawImage(emoji, x, y, null) - - val size = Vec2d(emoji.width, emoji.height) - val uv1 = Vec2d(x, y) * texelSize - val uv2 = Vec2d(x, y).plus(size) * texelSize - - emojiMap[name] = GlyphInfo(size, -uv1, -uv2) - - x += emoji.width + STEP - } - } - - return image - } - - fun bind() { - with(fontTexture) { - bind(GL_TEXTURE_SLOT) - setLOD(RenderSettings.lodBias.toFloat()) - } - } - - fun emojiFromString(emoji: String) = emojiMap[emoji] - - companion object { - private const val STEP = 2 - private const val GL_TEXTURE_SLOT = 1 - } -} diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt deleted file mode 100644 index 79ad1e40b..000000000 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.renderer.gui.font.glyph - -import com.lambda.Lambda.LOG -import com.lambda.graphics.texture.MipmapTexture -import com.lambda.graphics.texture.TextureUtils.getCharImage -import com.lambda.module.modules.client.RenderSettings -import com.lambda.util.math.Vec2d -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap -import java.awt.Color -import java.awt.Font -import java.awt.Graphics2D -import java.awt.image.BufferedImage -import kotlin.math.max - -class FontGlyphs( - private val font: Font -) { - private val charMap = Int2ObjectOpenHashMap() - private lateinit var fontTexture: MipmapTexture - - var fontHeight = 0.0; private set - - init { - runCatching { - processGlyphs() - LOG.info("Font ${font.fontName} loaded with ${charMap.size} characters") - }.onFailure { - LOG.error("Failed to load font glyphs: ${it.message}", it) - fontTexture = MipmapTexture(BufferedImage(1024, 1024, BufferedImage.TYPE_INT_ARGB)) - } - } - - private fun processGlyphs() { - val image = BufferedImage(TEXTURE_SIZE, TEXTURE_SIZE, BufferedImage.TYPE_INT_ARGB) - - val graphics = image.graphics as Graphics2D - graphics.background = Color(0, 0, 0, 0) - - var x = 0 - var y = 0 - var rowHeight = 0 - - (Char.MIN_VALUE.. - val charImage = getCharImage(font, char) ?: return@forEach - - rowHeight = max(rowHeight, charImage.height + STEP) - - if (x + charImage.width >= TEXTURE_SIZE) { - y += rowHeight - x = 0 - rowHeight = 0 - } - - check(y + charImage.height <= TEXTURE_SIZE) { "Can't load font glyphs. Texture size is too small" } - - graphics.drawImage(charImage, x, y, null) - - val size = Vec2d(charImage.width, charImage.height) - val uv1 = Vec2d(x, y) * ONE_TEXEL_SIZE - val uv2 = Vec2d(x, y).plus(size) * ONE_TEXEL_SIZE - - charMap[char.code] = GlyphInfo(size, uv1, uv2) - fontHeight = max(fontHeight, size.y) - - x += charImage.width + STEP - } - - fontTexture = MipmapTexture(image) - } - - fun bind() { - with(fontTexture) { - bind(GL_TEXTURE_SLOT) - setLOD(RenderSettings.lodBias.toFloat()) - } - } - - fun getChar(char: Char): GlyphInfo? = - charMap[char.code] - - companion object { - // The allocated texture slot - private const val GL_TEXTURE_SLOT = 0 - - // The space between glyphs is necessary to prevent artifacts from appearing when the font texture is blurred - private const val STEP = 2 - - // Since most Lambda users probably have bad pc, the default size is 2048, which includes latin, cyrillic, greek and arabic - // and in the future we could grow the textures when needed - private const val CHAR_AMOUNT = 2048 - - // The size of the texture in pixels - private const val TEXTURE_SIZE = CHAR_AMOUNT * 2 - - // The size of one texel in UV coordinates - private const val ONE_TEXEL_SIZE = 1.0 / TEXTURE_SIZE - } -} diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt index 2fe308fbc..3d5995389 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt @@ -32,8 +32,6 @@ object TextureUtils { private const val COMPRESSION_LEVEL = 1 private const val THREADED_COMPRESSION = false - private val metricCache = mutableMapOf() - val encoderPreset = PngEncoder() .withCompressionLevel(COMPRESSION_LEVEL) .withMultiThreadedCompressionEnabled(THREADED_COMPRESSION) @@ -92,36 +90,6 @@ object TextureUtils { format: NativeImage.Format = NativeImage.Format.RGBA, ) = NativeImage.read(format, image).pointer - fun getCharImage(font: Font, codePoint: Char): BufferedImage? { - if (!font.canDisplay(codePoint)) return null - - val fontMetrics = metricCache.getOrPut(font) { - val image = BufferedImage(COMPRESSION_LEVEL, COMPRESSION_LEVEL, BufferedImage.TYPE_INT_ARGB) - val graphics2D = image.createGraphics() - - graphics2D.font = font - graphics2D.dispose() - - image.graphics.getFontMetrics(font) - } - - val charWidth = if (fontMetrics.charWidth(codePoint) > 0) fontMetrics.charWidth(codePoint) else 8 - val charHeight = if (fontMetrics.height > 0) fontMetrics.height else font.size - - val charImage = BufferedImage(charWidth, charHeight, BufferedImage.TYPE_INT_ARGB) - val graphics2D = charImage.createGraphics() - - graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) - graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_DEFAULT) - - graphics2D.font = font - graphics2D.color = Color.WHITE - graphics2D.drawString(codePoint.toString(), 0, fontMetrics.ascent) - graphics2D.dispose() - - return charImage - } - fun BufferedImage.rescale(targetWidth: Int, targetHeight: Int): BufferedImage { val type = if (transparency == Transparency.OPAQUE) BufferedImage.TYPE_INT_RGB From 378283e6e1389c2e10bed431285c6b475af8e340 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:23:52 -0500 Subject: [PATCH 047/114] example --- .../graphics/renderer/gui/font/LambdaAtlas.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt index ce80f577a..19591a1f0 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt @@ -51,6 +51,23 @@ import kotlin.time.Duration.Companion.days * It stores glyph information, manages texture uploads, and provides functionality to build texture buffers for fonts and emoji sets * * It caches font information and emoji data for efficient rendering and includes mechanisms for uploading and binding texture atlases + * + * It's also possible to upload custom atlases and bind them with no hassle + * ```kt + * enum class ExampleFont { + * CoolFont("Cool-Font"); + * } + * + * fun loadFont(...) = BufferedImage + * + * ExampleFont.CoolFont.uploadAtlas(loadFont(...)) // The extension keeps a reference to the font owner + * + * ... + * + * onRender { + * ExampleFont.CoolFont.bind(slot = x) + * } + * ``` */ object LambdaAtlas { private val fontMap = Object2ObjectOpenHashMap>() From ca5066f1af108e145c180d172b8af6950f5f8807 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 7 Dec 2024 12:51:25 -0500 Subject: [PATCH 048/114] fixed wrong example --- .../kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt index 19591a1f0..0a3d20e41 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt @@ -65,7 +65,7 @@ import kotlin.time.Duration.Companion.days * ... * * onRender { - * ExampleFont.CoolFont.bind(slot = x) + * ExampleFont.CoolFont.bind() * } * ``` */ From f15eff532de9bed0953da0de28fde505c950eb19 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:38:36 -0500 Subject: [PATCH 049/114] better texture handlers and fixed slots --- .../renderer/gui/font/FontRenderer.kt | 4 +- .../graphics/renderer/gui/font/LambdaAtlas.kt | 51 ++++++--------- .../lambda/graphics/texture/MipmapTexture.kt | 65 ------------------- .../com/lambda/graphics/texture/Texture.kt | 52 ++++++++++++++- .../lambda/graphics/texture/TextureHandler.kt | 46 +++++++++++++ .../lambda/graphics/texture/TextureUtils.kt | 54 --------------- .../kotlin/com/lambda/module/hud/Watermark.kt | 6 +- .../module/modules/client/RenderSettings.kt | 12 +++- .../kotlin/com/lambda/util/LambdaResource.kt | 5 ++ 9 files changed, 137 insertions(+), 158 deletions(-) delete mode 100644 common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt create mode 100644 common/src/main/kotlin/com/lambda/graphics/texture/TextureHandler.kt diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index 186d39a10..9bc7b49f3 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -23,6 +23,7 @@ import com.lambda.graphics.buffer.vertex.attributes.VertexMode import com.lambda.graphics.renderer.gui.font.LambdaAtlas.bind import com.lambda.graphics.renderer.gui.font.LambdaAtlas.get import com.lambda.graphics.renderer.gui.font.LambdaAtlas.height +import com.lambda.graphics.renderer.gui.font.LambdaAtlas.slot import com.lambda.graphics.shader.Shader import com.lambda.module.modules.client.ClickGui import com.lambda.module.modules.client.LambdaMoji @@ -221,7 +222,8 @@ class FontRenderer { fun render() { shader.use() - shader["u_EmojiTexture"] = 1 + shader["u_FontTexture"] = chars.slot + shader["u_EmojiTexture"] = emojis.slot chars.bind() emojis.bind() diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt index 0a3d20e41..c3e81e061 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt @@ -18,12 +18,11 @@ package com.lambda.graphics.renderer.gui.font import com.google.common.math.IntMath.pow -import com.lambda.event.events.ConnectionEvent -import com.lambda.event.listener.UnsafeListener.Companion.unsafeListenOnce -import com.lambda.graphics.texture.MipmapTexture +import com.lambda.core.Loadable +import com.lambda.graphics.texture.TextureHandler.texture +import com.lambda.graphics.texture.TextureHandler.upload import com.lambda.http.Method import com.lambda.http.request -import com.lambda.module.modules.client.RenderSettings import com.lambda.threading.runGameScheduled import com.lambda.util.LambdaResource import com.lambda.util.math.Vec2d @@ -60,7 +59,8 @@ import kotlin.time.Duration.Companion.days * * fun loadFont(...) = BufferedImage * - * ExampleFont.CoolFont.uploadAtlas(loadFont(...)) // The extension keeps a reference to the font owner + * // Function extension from [TexturePipeline] + * ExampleFont.CoolFont.upload(loadFont(...)) // The extension keeps a reference to the font owner * * ... * @@ -69,11 +69,10 @@ import kotlin.time.Duration.Companion.days * } * ``` */ -object LambdaAtlas { +object LambdaAtlas : Loadable { private val fontMap = Object2ObjectOpenHashMap>() private val emojiMap = Object2ObjectOpenHashMap>() - private val textureMap = Object2ObjectOpenHashMap() - private val slotReservation = Object2IntArrayMap() + private val slotReservation = Object2IntArrayMap() // Will cause undefined behavior if someone is trying to allocate more than 32 slots, unlikely private val bufferPool = mutableMapOf() // This array is nuked once the data is dispatched to OpenGL @@ -85,20 +84,15 @@ object LambdaAtlas { operator fun LambdaFont.get(char: Char): GlyphInfo? = fontMap.getValue(this)[char.code] operator fun LambdaEmoji.get(string: String): GlyphInfo? = emojiMap.getValue(this)[string] - /** - * Upload additional atlas that can be used with the owner to bind textures to shaders - */ - fun Any.uploadAtlas(data: BufferedImage) = textureMap.set(this, MipmapTexture(data)) - // Allow binding any valid font definition enums - fun > T.bind() = with(textureMap.getValue(this)) - { - bind(slot = slotReservation.computeIfAbsent(this, ToIntFunction { slotReservation.size })) - setLOD(RenderSettings.lodBias.toFloat()) - } + fun > T.bind() = + this@bind.texture.bind(slot = slotReservation.computeIfAbsent(this@bind, ToIntFunction { slotReservation.size })) + + val > T.slot: Int + get() = slotReservation.getInt(this@slot) val LambdaFont.height: Double - get() = heightCache.getDouble(fontCache[this]) + get() = heightCache.getDouble(fontCache[this@height]) val LambdaEmoji.keys get() = emojiMap.getValue(this) @@ -215,19 +209,16 @@ object LambdaAtlas { bufferPool[this] = image } - init { - // TODO: Change this when we've refactored the loadables - unsafeListenOnce { - runGameScheduled { - bufferPool.forEach { (owner, image) -> - textureMap[owner] = MipmapTexture(image) - } - - bufferPool.clear() - } + // TODO: Change this when we've refactored the loadables + override fun load(): String { + val str = "Loaded ${bufferPool.size} fonts" // avoid race condition - true + runGameScheduled { + bufferPool.forEach { (owner, image) -> owner.upload(image) } + bufferPool.clear() } + + return str } private fun getCharImage(font: Font, codePoint: Char): BufferedImage? { diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt deleted file mode 100644 index 9d9109942..000000000 --- a/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.texture - -import com.lambda.graphics.texture.TextureUtils.rescale -import com.lambda.graphics.texture.TextureUtils.setupLOD -import com.lambda.graphics.texture.TextureUtils.upload -import com.lambda.util.LambdaResource -import org.lwjgl.opengl.GL14.* -import java.awt.image.BufferedImage -import javax.imageio.ImageIO - -class MipmapTexture(image: BufferedImage, levels: Int = 4) : Texture() { - private var lastLod: Float? = null - - init { - bind() - setupLOD(levels) - - // Upload base image - upload(image, 0) - - // Upload downscaled ones - for (level in 1..levels) { - val newWidth = image.width shr level - val newHeight = image.height shr level - val scaled = image.rescale(newWidth, newHeight) - - upload(scaled, level) - } - } - - fun setLOD(targetLod: Float) { - if (lastLod == targetLod) return - lastLod = targetLod - - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, targetLod) - } - - companion object { - /** - * Retrieves an image from the resources folder and generates a mipmap texture. - * - * @param path The path to the image. - * @param levels The number of mipmap levels. - */ - fun fromResource(path: String, levels: Int = 4): MipmapTexture = - MipmapTexture(ImageIO.read(LambdaResource(path).stream), levels) - } -} diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index 52dc5b722..cea3def3a 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -18,10 +18,58 @@ package com.lambda.graphics.texture import com.lambda.graphics.texture.TextureUtils.bindTexture +import com.lambda.graphics.texture.TextureUtils.readImage +import com.lambda.graphics.texture.TextureUtils.setupTexture +import com.lambda.module.modules.client.RenderSettings import org.lwjgl.opengl.GL45C.* +import java.awt.image.BufferedImage -open class Texture { +open class Texture( + private val image: BufferedImage?, + private val levels: Int = 4, +) { val id = glGenTextures() - fun bind(slot: Int = 0) = bindTexture(id, slot) + open fun init() = image + ?.let { + bind() + upload(it) + bind(0) + } + + open fun bind(slot: Int = 0) { + bindTexture(id, slot) + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, RenderSettings.lodBias) + } + + open fun upload(image: BufferedImage, offset: Int = 0) { + // Store level_base +1 through `level` images and generate + // mipmaps from them + setupLOD(levels = levels) + + val width = image.width + val height = image.height + + // Set this mipmap to 0 to define the original texture + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, readImage(image)) + glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack + + setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) + } + + private fun setupLOD(levels: Int) { + // When you call glTextureStorage, you're specifying the total number of levels, including level 0 + // This is a 0-based index system, which means that the maximum mipmap level is n-1 + // + // TLDR: This will not work correctly with immutable texture storage + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_LOD, 0) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LOD, levels) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, levels) + } + + init { + init() // The overridden method will run and not the base one due to kotlin's order of execution ;) + } } diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureHandler.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureHandler.kt new file mode 100644 index 000000000..6fabcba5c --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureHandler.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.texture + +import com.lambda.util.LambdaResource +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import java.awt.image.BufferedImage + +object TextureHandler { + private val textureMap = Object2ObjectOpenHashMap() + + /** + * Returns the texture owned by a specific object + */ + val Any.texture: Texture + get() = textureMap.getValue(this@texture) + + /** + * Generate mipmap texture from data and associate it with its owner + */ + fun Any.upload(data: BufferedImage, mipmaps: Int = 1) = + Texture(data, levels = mipmaps).also { textureMap[this@upload] = it } + + /** + * Generate mipmap texture from data and associate it with its owner + * + * @param path Lambda resource path containing the image data + */ + fun Any.upload(path: String, mipmaps: Int = 1) = + Texture(LambdaResource.readImage(path), levels = mipmaps).also { textureMap[this@upload] = it } +} diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt index 3d5995389..2e84e7154 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt @@ -22,11 +22,8 @@ import com.pngencoder.PngEncoder import net.minecraft.client.texture.NativeImage import org.lwjgl.BufferUtils import org.lwjgl.opengl.GL45C.* -import java.awt.* import java.awt.image.BufferedImage import java.nio.ByteBuffer -import kotlin.math.roundToInt -import kotlin.math.sqrt object TextureUtils { private const val COMPRESSION_LEVEL = 1 @@ -41,22 +38,6 @@ object TextureUtils { RenderSystem.bindTexture(id) } - fun upload(bufferedImage: BufferedImage, lod: Int) { - val width = bufferedImage.width - val height = bufferedImage.height - - glTexImage2D(GL_TEXTURE_2D, lod, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, readImage(bufferedImage)) - - setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) - } - - fun setupLOD(levels: Int) { - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_LOD, 0) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LOD, levels) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, levels) - } - fun setupTexture(minFilter: Int, magFilter: Int) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter) @@ -89,39 +70,4 @@ object TextureUtils { image: ByteBuffer, format: NativeImage.Format = NativeImage.Format.RGBA, ) = NativeImage.read(format, image).pointer - - fun BufferedImage.rescale(targetWidth: Int, targetHeight: Int): BufferedImage { - val type = if (transparency == Transparency.OPAQUE) - BufferedImage.TYPE_INT_RGB - else BufferedImage.TYPE_INT_ARGB - - var image = this - - var width = image.width - var height = image.height - - val divisorX = sqrt((width / targetWidth).toDouble()) - val divisorY = sqrt((height / targetHeight).toDouble()) - - do { - if (width > targetWidth) { - width = (width / divisorX).roundToInt().coerceAtLeast(targetWidth) - } - - if (height > targetHeight) { - height = (height / divisorY).roundToInt().coerceAtLeast(targetHeight) - } - - val tempImage = BufferedImage(width, height, type) - val graphics2D = tempImage.createGraphics() - - graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR) - graphics2D.drawImage(image, 0, 0, width, height, null) - graphics2D.dispose() - - image = tempImage - } while (width != targetWidth || height != targetHeight) - - return image - } } diff --git a/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt b/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt index 640d5caaf..f7b878cad 100644 --- a/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt +++ b/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt @@ -19,7 +19,7 @@ package com.lambda.module.hud import com.lambda.graphics.renderer.gui.TextureRenderer.drawTexture import com.lambda.graphics.renderer.gui.TextureRenderer.drawTextureShaded -import com.lambda.graphics.texture.MipmapTexture +import com.lambda.graphics.texture.TextureHandler.upload import com.lambda.module.HudModule import com.lambda.module.tag.ModuleTag @@ -32,8 +32,8 @@ object Watermark : HudModule( override val width = 50.0 override val height = 50.0 - private val normalTexture = MipmapTexture.fromResource("textures/lambda.png") - private val monoTexture = MipmapTexture.fromResource("textures/lambda_mono.png") + private val normalTexture = upload("textures/lambda.png") + private val monoTexture = upload("textures/lambda_mono.png") init { onRender { diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt index 7e97530da..5d0c465d6 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt @@ -37,7 +37,15 @@ object RenderSettings : Module( val shadowShift by setting("Shadow Shift", 1.0, 0.0..2.0, 0.05) { page == Page.Font && shadow } val gap by setting("Gap", 1.5, -10.0..10.0, 0.5) { page == Page.Font } val baselineOffset by setting("Vertical Offset", 0.0, -10.0..10.0, 0.5) { page == Page.Font } - private val lodBiasSetting by setting("Smoothing", 0.0, -10.0..10.0, 0.5) { page == Page.Font } + + // This value actually depends on the parameters of the texture... + // The specified value is added to the shader-supplied bias value (if any) + // and subsequently clamped into the implementation-defined range + // [-biasmax, biasmax], where biasmax is the value of the implementation + // defined constant GL_MAX_TEXTURE_LOD_BIAS. The initial value is 0.0. + // + // At least we're sure that the smoothing we see is the same for everyone + val lodBias by setting("Smoothing", -2.0f, -15.0f..15.0f, 0.1f) { page == Page.Font } // ESP val uploadsPerTick by setting("Uploads", 16, 1..256, 1, unit = " chunk/tick") { page == Page.ESP } @@ -45,8 +53,6 @@ object RenderSettings : Module( val updateFrequency by setting("Update Frequency", 2, 1..10, 1, "Frequency of block updates", unit = " ticks") { page == Page.ESP } val outlineWidth by setting("Outline Width", 1.0, 0.1..5.0, 0.1, "Width of block outlines", unit = "px") { page == Page.ESP } - val lodBias get() = lodBiasSetting * 0.25f - 0.75f - private enum class Page { Font, ESP, diff --git a/common/src/main/kotlin/com/lambda/util/LambdaResource.kt b/common/src/main/kotlin/com/lambda/util/LambdaResource.kt index 22dae8ec1..d660dba78 100644 --- a/common/src/main/kotlin/com/lambda/util/LambdaResource.kt +++ b/common/src/main/kotlin/com/lambda/util/LambdaResource.kt @@ -18,8 +18,13 @@ package com.lambda.util import java.io.InputStream +import javax.imageio.ImageIO class LambdaResource(val path: String) { val stream: InputStream? get() = javaClass.getResourceAsStream("/assets/lambda/$path") + + companion object { + fun readImage(path: String) = ImageIO.read(LambdaResource(path).stream) + } } From 7fd8b99d3bb8ea3ee9286d910d85782ca59b5c3e Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:39:00 -0500 Subject: [PATCH 050/114] renamed to TextureOwner --- .../com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt | 4 ++-- .../graphics/texture/{TextureHandler.kt => TextureOwner.kt} | 2 +- common/src/main/kotlin/com/lambda/module/hud/Watermark.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename common/src/main/kotlin/com/lambda/graphics/texture/{TextureHandler.kt => TextureOwner.kt} (98%) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt index c3e81e061..de4273c30 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt @@ -19,8 +19,8 @@ package com.lambda.graphics.renderer.gui.font import com.google.common.math.IntMath.pow import com.lambda.core.Loadable -import com.lambda.graphics.texture.TextureHandler.texture -import com.lambda.graphics.texture.TextureHandler.upload +import com.lambda.graphics.texture.TextureOwner.texture +import com.lambda.graphics.texture.TextureOwner.upload import com.lambda.http.Method import com.lambda.http.request import com.lambda.threading.runGameScheduled diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureHandler.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt similarity index 98% rename from common/src/main/kotlin/com/lambda/graphics/texture/TextureHandler.kt rename to common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt index 6fabcba5c..b66645e55 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/TextureHandler.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt @@ -21,7 +21,7 @@ import com.lambda.util.LambdaResource import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import java.awt.image.BufferedImage -object TextureHandler { +object TextureOwner { private val textureMap = Object2ObjectOpenHashMap() /** diff --git a/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt b/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt index f7b878cad..794738eb0 100644 --- a/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt +++ b/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt @@ -19,7 +19,7 @@ package com.lambda.module.hud import com.lambda.graphics.renderer.gui.TextureRenderer.drawTexture import com.lambda.graphics.renderer.gui.TextureRenderer.drawTextureShaded -import com.lambda.graphics.texture.TextureHandler.upload +import com.lambda.graphics.texture.TextureOwner.upload import com.lambda.module.HudModule import com.lambda.module.tag.ModuleTag From d227203713e08c60d297b454fc1714ef31e37429 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 14 Dec 2024 12:59:06 -0500 Subject: [PATCH 051/114] fix: order of execution --- .../kotlin/com/lambda/graphics/texture/Texture.kt | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index cea3def3a..5c27fd323 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -25,18 +25,11 @@ import org.lwjgl.opengl.GL45C.* import java.awt.image.BufferedImage open class Texture( - private val image: BufferedImage?, + image: BufferedImage?, private val levels: Int = 4, ) { val id = glGenTextures() - open fun init() = image - ?.let { - bind() - upload(it) - bind(0) - } - open fun bind(slot: Int = 0) { bindTexture(id, slot) glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, RenderSettings.lodBias) @@ -70,6 +63,10 @@ open class Texture( } init { - init() // The overridden method will run and not the base one due to kotlin's order of execution ;) + image?.let { + bind() + upload(it) + bind(0) + } } } From a030ec76c2fb483fc4aa2c99c53ad655ecca147a Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 14 Dec 2024 14:04:04 -0500 Subject: [PATCH 052/114] removed useless bind --- common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index 5c27fd323..01167ea1b 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -66,7 +66,6 @@ open class Texture( image?.let { bind() upload(it) - bind(0) } } } From e645ef25a47d297626e35968cb60f02c57bbb796 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 14 Dec 2024 14:58:38 -0500 Subject: [PATCH 053/114] ref: typealias LambdaResource --- .../graphics/renderer/gui/font/LambdaAtlas.kt | 6 ++---- .../kotlin/com/lambda/graphics/shader/Shader.kt | 4 ++-- .../com/lambda/graphics/shader/ShaderUtils.kt | 3 ++- .../com/lambda/graphics/texture/TextureOwner.kt | 3 ++- .../kotlin/com/lambda/util/LambdaResource.kt | 16 +++++++++------- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt index de4273c30..bb1949a4d 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt @@ -24,8 +24,8 @@ import com.lambda.graphics.texture.TextureOwner.upload import com.lambda.http.Method import com.lambda.http.request import com.lambda.threading.runGameScheduled -import com.lambda.util.LambdaResource import com.lambda.util.math.Vec2d +import com.lambda.util.stream import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap import it.unimi.dsi.fastutil.objects.Object2DoubleArrayMap import it.unimi.dsi.fastutil.objects.Object2IntArrayMap @@ -162,9 +162,7 @@ object LambdaAtlas : Loadable { characters: Int = 2048 // How many characters from that font should be used for the generation ) { val font = fontCache.computeIfAbsent(this) { - val resource = LambdaResource("fonts/$fontName.ttf") - - Font.createFont(Font.TRUETYPE_FONT, resource.stream).deriveFont(64.0f) + Font.createFont(Font.TRUETYPE_FONT, "fonts/$fontName.ttf".stream).deriveFont(64.0f) } val textureSize = characters * 2 diff --git a/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt b/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt index 87ec7f5b1..3550ed5f0 100644 --- a/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt +++ b/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt @@ -34,8 +34,8 @@ class Shader(fragmentPath: String, vertexPath: String) { private val uniformCache: Object2IntMap = Object2IntOpenHashMap() private val id = createShaderProgram( - loadShader(ShaderType.VERTEX_SHADER, LambdaResource("shaders/vertex/$vertexPath.vert")), - loadShader(ShaderType.FRAGMENT_SHADER, LambdaResource("shaders/fragment/$fragmentPath.frag")) + loadShader(ShaderType.VERTEX_SHADER, "shaders/vertex/$vertexPath.vert"), + loadShader(ShaderType.FRAGMENT_SHADER, "shaders/fragment/$fragmentPath.frag") ) constructor(path: String) : this(path, path) diff --git a/common/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt b/common/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt index 9e229ac46..5e74da477 100644 --- a/common/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt +++ b/common/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt @@ -19,6 +19,7 @@ package com.lambda.graphics.shader import com.google.common.collect.ImmutableList import com.lambda.util.LambdaResource +import com.lambda.util.stream import com.mojang.blaze3d.platform.GlStateManager import org.apache.commons.io.IOUtils import org.joml.Matrix4f @@ -42,7 +43,7 @@ object ShaderUtils { error?.let { err -> val builder = StringBuilder() .append("Failed to compile ${type.name} shader").appendLine() - .append("Path: ${resource.path}").appendLine() + .append("Path: $resource").appendLine() .append("Compiler output:").appendLine() .append(err) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt index b66645e55..46a1bf5c6 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt @@ -18,6 +18,7 @@ package com.lambda.graphics.texture import com.lambda.util.LambdaResource +import com.lambda.util.readImage import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import java.awt.image.BufferedImage @@ -42,5 +43,5 @@ object TextureOwner { * @param path Lambda resource path containing the image data */ fun Any.upload(path: String, mipmaps: Int = 1) = - Texture(LambdaResource.readImage(path), levels = mipmaps).also { textureMap[this@upload] = it } + Texture(path.readImage(), levels = mipmaps).also { textureMap[this@upload] = it } } diff --git a/common/src/main/kotlin/com/lambda/util/LambdaResource.kt b/common/src/main/kotlin/com/lambda/util/LambdaResource.kt index d660dba78..16e6fbce3 100644 --- a/common/src/main/kotlin/com/lambda/util/LambdaResource.kt +++ b/common/src/main/kotlin/com/lambda/util/LambdaResource.kt @@ -17,14 +17,16 @@ package com.lambda.util +import com.lambda.Lambda +import java.awt.image.BufferedImage +import java.io.FileNotFoundException import java.io.InputStream import javax.imageio.ImageIO -class LambdaResource(val path: String) { - val stream: InputStream? - get() = javaClass.getResourceAsStream("/assets/lambda/$path") +typealias LambdaResource = String - companion object { - fun readImage(path: String) = ImageIO.read(LambdaResource(path).stream) - } -} +val LambdaResource.stream: InputStream + get() = Lambda::class.java.getResourceAsStream("/assets/lambda/$this") + ?: throw FileNotFoundException("File \"/assets/lambda/$this\" not found") + +fun LambdaResource.readImage(): BufferedImage = ImageIO.read(this.stream) From 31b24f13550650ef19b877f72d22faae6da8d22f Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 14 Dec 2024 22:01:28 -0500 Subject: [PATCH 054/114] feat: gif renderer --- .../graphics/buffer/pixel/PixelBuffer.kt | 50 ++------- .../kotlin/com/lambda/graphics/gl/Memory.kt | 16 --- .../graphics/texture/AnimatedTexture.kt | 100 ++++++++++++++++++ .../lambda/graphics/texture/TextureOwner.kt | 7 +- .../kotlin/com/lambda/module/hud/GifTest.kt | 39 +++++++ .../kotlin/com/lambda/module/hud/Watermark.kt | 2 + .../main/resources/assets/lambda/chika.gif | Bin 0 -> 452195 bytes 7 files changed, 156 insertions(+), 58 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt create mode 100644 common/src/main/kotlin/com/lambda/module/hud/GifTest.kt create mode 100644 common/src/main/resources/assets/lambda/chika.gif diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt index ef8f7495d..0838e7bf4 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt @@ -18,8 +18,6 @@ package com.lambda.graphics.buffer.pixel import com.lambda.graphics.buffer.IBuffer -import com.lambda.graphics.gl.padding -import com.lambda.graphics.gl.putTo import com.lambda.graphics.texture.Texture import org.lwjgl.opengl.GL45C.* import java.nio.ByteBuffer @@ -46,82 +44,52 @@ class PixelBuffer( private val texture: Texture, private val format: Int, ) : IBuffer { - override val buffers: Int = 2 + override val buffers: Int = 1 override val usage: Int = GL_STATIC_DRAW override val target: Int = GL_PIXEL_UNPACK_BUFFER - override val access: Int = GL_MAP_WRITE_BIT or GL_MAP_COHERENT_BIT + override val access: Int = GL_MAP_WRITE_BIT override var index = 0 override val bufferIds = IntArray(buffers).apply { glGenBuffers(this) } - private val channels = channelMapping[format] ?: throw IllegalArgumentException("Image format unsupported") - private val internalFormat = reverseChannelMapping[channels] ?: throw IllegalArgumentException("Image internal format unsupported") + private val channels = channelMapping[format] ?: throw IllegalArgumentException("Invalid image format, expected OpenGL format, got $format instead") + private val internalFormat = reverseChannelMapping[channels] ?: throw IllegalArgumentException("Invalid internal image format, expected channels count, got $channels instead") private val size = width * height * channels * 1L override fun upload( data: ByteBuffer, offset: Long, ): Throwable? { - // Bind PBO to unpack the data into the texture bind() - - // Bind the texture and PBO glBindTexture(GL_TEXTURE_2D, texture.id) // Copy pixels from PBO to texture object // Use offset instead of pointer glTexSubImage2D( GL_TEXTURE_2D, // Target - 0, // Mipmap level - 0, 0, // x and y offset + 0, // Mipmap level + 0, 0, // x and y offset width, height, // width and height of the texture (set to your size) format, // Format (depends on your data) GL_UNSIGNED_BYTE, // Type (depends on your data) - 0, // PBO offset (for asynchronous transfer) + 0, // PBO offset (for asynchronous transfer) ) - // Unbind the texture - glBindTexture(GL_TEXTURE_2D, 0) - - // Swap the buffer - swap() - - // Bind PBO to update pixel source - bind() - - // Map the buffer into the client's memory - val error = map(offset, size, data::putTo) + val error = update(data, offset) - // Unbind bind(0) return error } init { - // Bind the texture glBindTexture(GL_TEXTURE_2D, texture.id) - // Calculate memory padding in the case we are using tightly - // packed data in order to save memory and satisfy the computer's - // architecture memory alignment - // https://en.wikipedia.org/wiki/Data_structure_alignment - // In this case we calculate the padding and subtract this to 4 - // in order to tell the padding size - glPixelStorei(GL_UNPACK_ALIGNMENT, 4 - padding(channels)) - // Allocate texture storage - // TODO: Might want to figure out the data type based on the input glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, 0) - - // Set the texture parameters glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) - // Unbind the texture - glBindTexture(GL_TEXTURE_2D, 0) - - // Fill the storage with null - storage(size) + allocate(size) } companion object { diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/Memory.kt b/common/src/main/kotlin/com/lambda/graphics/gl/Memory.kt index 8b1ec9708..c99fe6d68 100644 --- a/common/src/main/kotlin/com/lambda/graphics/gl/Memory.kt +++ b/common/src/main/kotlin/com/lambda/graphics/gl/Memory.kt @@ -113,20 +113,4 @@ val Long.kibibyte get() = this * 1024 val Long.mebibyte get() = this * 1024 * 1024 val Long.gibibyte get() = this * 1024 * 1024 * 1024 -/** - * Returns memory alignment for each CPU architecture - */ -fun alignment(): Int { - return when (System.getProperty("os.arch")?.lowercase()) { - "x86", "x86_64" -> 4 // 32-bit or 64-bit x86 - "arm", "armv7l", "aarch64" -> 4 // ARM architectures - else -> 8 // Default to 8 bytes alignment for other architectures - } -} - -/** - * Returns how many bytes will be added to reach memory alignment - */ -fun padding(size: Int): Int = size % alignment() / 8 - fun ByteBuffer.putTo(dst: ByteBuffer) { dst.put(this) } diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt new file mode 100644 index 000000000..f46ec695c --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.texture + +import com.lambda.graphics.buffer.pixel.PixelBuffer +import com.lambda.util.Communication.logError +import com.lambda.util.LambdaResource +import com.lambda.util.stream +import org.lwjgl.BufferUtils +import org.lwjgl.opengl.GL11.GL_RGBA +import org.lwjgl.stb.STBImage +import java.nio.ByteBuffer +import kotlin.properties.Delegates + + +class AnimatedTexture( + private val path: LambdaResource, +) : Texture(null) { + lateinit var frameDurations: IntArray + var width by Delegates.notNull() + var height by Delegates.notNull() + var channels by Delegates.notNull() + var frames by Delegates.notNull() + + val blockSize: Int + get() = width * height * channels + + lateinit var gif: ByteBuffer // Do NOT free this pointer + var pbo: PixelBuffer + + var currentFrame = 0 + var lastUpload = 0L + + override fun bind(slot: Int) { + update() + super.bind(slot) + } + + fun update() { + if (System.currentTimeMillis() - lastUpload >= frameDurations[currentFrame]) { + val slice = gif + .position(blockSize * currentFrame) + .limit(blockSize * (currentFrame + 1)) + + pbo.upload(slice, offset = 0) + ?.let { err -> logError("Error uploading to PBO", err) } + + gif.clear() + + currentFrame = (currentFrame+1) % frames + lastUpload = System.currentTimeMillis() + } + } + + private fun readGif() { + val bytes = path.stream.readAllBytes() + val buffer = ByteBuffer.allocateDirect(bytes.size) + + buffer.put(bytes) + buffer.flip() + + val pDelays = BufferUtils.createPointerBuffer(1) + val pWidth = BufferUtils.createIntBuffer(1) + val pHeight = BufferUtils.createIntBuffer(1) + val pLayers = BufferUtils.createIntBuffer(1) + val pChannels = BufferUtils.createIntBuffer(1) + + // The buffer contains packed frames that can be extracted as follows: + // limit = width * height * channels * [frame number] + gif = STBImage.stbi_load_gif_from_memory(buffer, pDelays, pWidth, pHeight, pLayers, pChannels, 4)!! + + width = pWidth.get() + height = pHeight.get() + frames = pLayers.get() + channels = pChannels.get() + + frameDurations = IntArray(frames) + pDelays.getIntBuffer(frames).get(frameDurations) + } + + init { + readGif() + pbo = PixelBuffer(width, height, this@AnimatedTexture, format = GL_RGBA) + } +} diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt index 46a1bf5c6..e0b50fdd0 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt @@ -17,7 +17,6 @@ package com.lambda.graphics.texture -import com.lambda.util.LambdaResource import com.lambda.util.readImage import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import java.awt.image.BufferedImage @@ -44,4 +43,10 @@ object TextureOwner { */ fun Any.upload(path: String, mipmaps: Int = 1) = Texture(path.readImage(), levels = mipmaps).also { textureMap[this@upload] = it } + + /** + * Loads a gif and associate it with its owner + */ + fun Any.uploadGif(path: String) = + AnimatedTexture(path).also { textureMap[this@uploadGif] = it } } diff --git a/common/src/main/kotlin/com/lambda/module/hud/GifTest.kt b/common/src/main/kotlin/com/lambda/module/hud/GifTest.kt new file mode 100644 index 000000000..b4c8fe833 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/hud/GifTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.module.hud + +import com.lambda.graphics.renderer.gui.TextureRenderer.drawTexture +import com.lambda.graphics.texture.TextureOwner.uploadGif +import com.lambda.module.HudModule +import com.lambda.module.tag.ModuleTag + +object GifTest : HudModule( + name = "GifTest", + defaultTags = setOf(ModuleTag.CLIENT), +) { + val test = uploadGif("chika.gif") + + override val width = 100.0 + override val height = 100.0 + + init { + onRender { + drawTexture(test, rect) + } + } +} diff --git a/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt b/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt index 794738eb0..8d61d3ddd 100644 --- a/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt +++ b/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt @@ -20,8 +20,10 @@ package com.lambda.module.hud import com.lambda.graphics.renderer.gui.TextureRenderer.drawTexture import com.lambda.graphics.renderer.gui.TextureRenderer.drawTextureShaded import com.lambda.graphics.texture.TextureOwner.upload +import com.lambda.graphics.texture.TextureOwner.uploadGif import com.lambda.module.HudModule import com.lambda.module.tag.ModuleTag +import com.lambda.util.math.Vec2d object Watermark : HudModule( name = "Watermark", diff --git a/common/src/main/resources/assets/lambda/chika.gif b/common/src/main/resources/assets/lambda/chika.gif new file mode 100644 index 0000000000000000000000000000000000000000..2def218eca631e2483daa5033ff3fd5a9204b1a3 GIT binary patch literal 452195 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 z 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⋙=SyqB*9T$p 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� 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%0ANjYwGd#Hp?;#mc>0g!3TqM`mDS_p#b0r=06;2( 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
    WRt<64Sj(Vb&f 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 Date: Sun, 15 Dec 2024 17:08:42 -0500 Subject: [PATCH 055/114] ref: common buffer class Also fixed the memory mapping caused by using un-allocated buffers --- .../com/lambda/graphics/buffer/IBuffer.kt | 83 +++++++++++++------ .../graphics/buffer/pixel/PixelBuffer.kt | 34 ++++---- .../graphics/buffer/vertex/ElementBuffer.kt | 7 +- .../graphics/buffer/vertex/VertexArray.kt | 11 +-- .../graphics/buffer/vertex/VertexBuffer.kt | 5 +- .../graphics/texture/AnimatedTexture.kt | 2 +- 6 files changed, 85 insertions(+), 57 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/IBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/IBuffer.kt index 9aaeb7a00..535dee7fb 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/IBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/IBuffer.kt @@ -24,7 +24,7 @@ import org.lwjgl.opengl.GL30C.* import org.lwjgl.opengl.GL44.glBufferStorage import java.nio.ByteBuffer -interface IBuffer { +abstract class IBuffer( /** * Specifies how many buffer must be used * @@ -40,8 +40,13 @@ interface IBuffer { * * Triple buffering helps maintain smoother frame rates, but if your app runs faster than the monitor's refresh rate, it offers little benefit as you eventually still wait for vblank synchronization. */ - val buffers: Int + val buffers: Int = 1, + /** + * Edge case to handle vertex arrays + */ + val isVertexArray: Boolean = false, +) { /** * Specifies how the buffers are used * @@ -57,7 +62,7 @@ interface IBuffer { * | GL_DYNAMIC_READ | Data is modified repeatedly and used many times for reading. | * | GL_DYNAMIC_COPY | Data is modified repeatedly and used many times for copying. | */ - val usage: Int + abstract val usage: Int /** * Specifies the target to which the buffer object is bound which must be one @@ -80,7 +85,7 @@ interface IBuffer { * | GL_TRANSFORM_FEEDBACK_BUFFER | Transform feedback buffer | * | GL_UNIFORM_BUFFER | Uniform block storage | */ - val target: Int + abstract val target: Int /** * Specifies a combination of access flags indicating the desired @@ -97,22 +102,22 @@ interface IBuffer { * | GL_MAP_FLUSH_EXPLICIT_BIT | Requires explicit flushing of modified sub-ranges. | Only with GL_MAP_WRITE_BIT. Data may be undefined if skipped. | * | GL_MAP_UNSYNCHRONIZED_BIT | Skips synchronization before mapping. | May cause data corruption if regions overlap. | */ - val access: Int + abstract val access: Int /** * Index of the current buffer */ - var index: Int + var index: Int = 0; private set /** * List of all the buffers */ - val bufferIds: IntArray + private val bufferIds = IntArray(buffers) /** * Binds the buffer id to the [target] */ - fun bind(id: Int) = glBindBuffer(target, id) + open fun bind(id: Int) = glBindBuffer(target, id) /** * Binds current the buffer [index] to the [target] @@ -130,7 +135,7 @@ interface IBuffer { * Update the current buffer without re-allocating * Alternative to [map] */ - fun update( + open fun update( data: ByteBuffer, offset: Long, ): Throwable? { @@ -151,15 +156,19 @@ interface IBuffer { * * @param data The data to put in the new allocated buffer */ - fun allocate(data: ByteBuffer): Throwable? { + open fun allocate(data: ByteBuffer): Throwable? { if (!bufferValid(target, access)) return IllegalArgumentException("Target is not valid. Refer to the table in the documentation") if (!bufferUsageValid(usage)) return IllegalArgumentException("Buffer usage is invalid") - bind() - glBufferData(target, data, usage) + repeat(buffers) { + bind() + glBufferData(target, data, usage) + swap() + } + bind(0) return null @@ -171,15 +180,19 @@ interface IBuffer { * * @param size The size of the new buffer */ - fun allocate(size: Long): Throwable? { + open fun allocate(size: Long): Throwable? { if (!bufferValid(target, access)) return IllegalArgumentException("Target is not valid. Refer to the table in the documentation") if (!bufferUsageValid(usage)) return IllegalArgumentException("Buffer usage is invalid") - bind() - glBufferData(target, size.coerceAtLeast(0), usage) + repeat(buffers) { + bind() + glBufferData(target, size.coerceAtLeast(0), usage) + swap() + } + bind(0) return null @@ -190,15 +203,19 @@ interface IBuffer { * This function cannot be called twice for the same buffer * This function handles the buffer binding */ - fun storage(data: ByteBuffer): Throwable? { + open fun storage(data: ByteBuffer): Throwable? { if (!bufferValid(target, access)) return IllegalArgumentException("Target is not valid. Refer to the table in the documentation") if (!bufferUsageValid(usage)) return IllegalArgumentException("Buffer usage is invalid") - bind() - glBufferStorage(target, data, usage) + repeat(buffers) { + bind() + glBufferStorage(target, data, usage) + swap() + } + bind(0) return null @@ -211,15 +228,19 @@ interface IBuffer { * * @param size The size of the storage buffer */ - fun storage(size: Long): Throwable? { + open fun storage(size: Long): Throwable? { if (!bufferValid(target, access)) return IllegalArgumentException("Target is not valid. Refer to the table in the documentation") if (!bufferUsageValid(usage)) return IllegalArgumentException("Buffer usage is invalid") - bind() - glBufferStorage(target, size.coerceAtLeast(0), usage) + repeat(buffers) { + bind() + glBufferStorage(target, size.coerceAtLeast(0), usage) + swap() + } + bind(0) return null @@ -228,14 +249,14 @@ interface IBuffer { /** * Maps all or part of a buffer object's data store into the client's address space * - * @param offset Specifies the starting offset within the buffer of the range to be mapped. * @param size Specifies the length of the range to be mapped. + * @param offset Specifies the starting offset within the buffer of the range to be mapped. * @param block Lambda scope with the mapped buffer passed in * @return Error encountered during the mapping process */ - fun map( - offset: Long, + open fun map( size: Long, + offset: Long, block: (ByteBuffer) -> Unit ): Throwable? { if ( @@ -251,7 +272,7 @@ interface IBuffer { if ( offset + size > glGetBufferParameteri(target, GL_BUFFER_SIZE) - ) return IllegalArgumentException("Out of bound mapping: $offset + $size > ${glGetBufferParameteri(target, GL_BUFFER_SIZE)}") + ) return IllegalArgumentException("Out of bound (is the buffer initialized?) $size + $offset > ${glGetBufferParameteri(target, GL_BUFFER_SIZE)}") if ( glGetBufferParameteri(target, GL_BUFFER_MAPPED) @@ -292,7 +313,7 @@ interface IBuffer { * @param offset The starting offset within the buffer of the range to be mapped * @return Error encountered during the mapping process */ - fun upload(data: ByteArray, offset: Long): Throwable? = + open fun upload(data: ByteArray, offset: Long): Throwable? = upload(ByteBuffer.wrap(data), offset) /** @@ -302,5 +323,13 @@ interface IBuffer { * @param offset The starting offset within the buffer of the range to be mapped * @return Error encountered during the mapping process */ - fun upload(data: ByteBuffer, offset: Long): Throwable? + abstract fun upload(data: ByteBuffer, offset: Long): Throwable? + + init { + // Special edge case for vertex arrays + check(buffers > 0) { "Cannot generate less than one buffer" } + + if (isVertexArray) glGenVertexArrays(bufferIds) // If there are more than 1 buffer you should expect undefined behavior, this is not the way to do it + else glGenBuffers(bufferIds) + } } diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt index 0838e7bf4..ef3b065a8 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt @@ -18,38 +18,39 @@ package com.lambda.graphics.buffer.pixel import com.lambda.graphics.buffer.IBuffer +import com.lambda.graphics.gl.putTo import com.lambda.graphics.texture.Texture +import com.lambda.util.math.MathUtils.toInt import org.lwjgl.opengl.GL45C.* import java.nio.ByteBuffer /** * Represents a Pixel Buffer Object (PBO) that facilitates asynchronous data transfer to the GPU. - * This class manages the creation, usage, and cleanup of PBOs and provides methods to upload (map) data efficiently. * - * **Process**: * Every function that performs a pixel transfer operation can use buffer objects instead of client memory. * Functions that perform an upload operation, a pixel unpack, will use the buffer object bound to the target GL_PIXEL_UNPACK_BUFFER. * If a buffer is bound, then the pointer value that those functions take is not a pointer, but an offset from the beginning of that buffer. * - * @property width The width of the texture - * @property height The height of the texture - * @property texture The [Texture] instance - * @property format The image format that will be uploaded + * @property width The width of the texture + * @property height The height of the texture + * @property format The image format that will be uploaded + * @property texture The [Texture] instance to use + * @property asynchronous Whether to use 2 buffers or not + * @property bufferMapping Whether to map a block in memory to upload or not * - * @see Pixel Buffer Object + * @see Reference */ class PixelBuffer( private val width: Int, private val height: Int, - private val texture: Texture, private val format: Int, -) : IBuffer { - override val buffers: Int = 1 + private val texture: Texture, + private val asynchronous: Boolean = false, + private val bufferMapping: Boolean = false, +) : IBuffer(buffers = asynchronous.toInt() + 1) { override val usage: Int = GL_STATIC_DRAW override val target: Int = GL_PIXEL_UNPACK_BUFFER override val access: Int = GL_MAP_WRITE_BIT - override var index = 0 - override val bufferIds = IntArray(buffers).apply { glGenBuffers(this) } private val channels = channelMapping[format] ?: throw IllegalArgumentException("Invalid image format, expected OpenGL format, got $format instead") private val internalFormat = reverseChannelMapping[channels] ?: throw IllegalArgumentException("Invalid internal image format, expected channels count, got $channels instead") @@ -74,7 +75,12 @@ class PixelBuffer( 0, // PBO offset (for asynchronous transfer) ) - val error = update(data, offset) + swap() + bind() + + val error = + if (bufferMapping) map(size, offset, data::putTo) + else update(data, offset) bind(0) @@ -89,7 +95,7 @@ class PixelBuffer( glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) - allocate(size) + storage(size) } companion object { diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt index 20cc67c34..7669ca2fa 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt @@ -23,13 +23,12 @@ import com.lambda.graphics.gl.kibibyte import org.lwjgl.opengl.GL30C.* import java.nio.ByteBuffer -class ElementBuffer(mode: VertexMode) : IBuffer { - override val buffers: Int = 1 +class ElementBuffer(mode: VertexMode) : + IBuffer(buffers = 1) +{ override val usage: Int = GL_DYNAMIC_DRAW override val target: Int = GL_ELEMENT_ARRAY_BUFFER override val access: Int = GL_MAP_WRITE_BIT - override var index = 0 - override val bufferIds = intArrayOf(glGenBuffers()) override fun upload( data: ByteBuffer, diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt index 1043f98c9..7eacbb43e 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt @@ -22,24 +22,21 @@ import net.minecraft.client.render.BufferRenderer import org.lwjgl.opengl.GL30C.* import java.nio.ByteBuffer -class VertexArray : IBuffer { - override val buffers: Int = 1 +class VertexArray : IBuffer(isVertexArray = true) { override val usage: Int = -1 override val target: Int = -1 override val access: Int = -1 - override var index = 0 - override val bufferIds = intArrayOf(glGenVertexArrays()) override fun map( - offset: Long, size: Long, + offset: Long, block: (ByteBuffer) -> Unit - ): Throwable? = throw UnsupportedOperationException("Cannot map a vertex array object to memory") + ): Throwable = throw UnsupportedOperationException("Cannot map a vertex array object to memory") override fun upload( data: ByteBuffer, offset: Long, - ): Throwable? = throw UnsupportedOperationException("Data cannot be uploaded to a vertex array object") + ): Throwable = throw UnsupportedOperationException("Data cannot be uploaded to a vertex array object") override fun allocate(size: Long) = throw UnsupportedOperationException("Cannot grow a vertex array object") diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt index 91977535d..b9e1606f7 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt @@ -28,13 +28,10 @@ import java.nio.ByteBuffer class VertexBuffer( mode: VertexMode, attributes: VertexAttrib.Group, -) : IBuffer { - override val buffers: Int = 1 +) : IBuffer(buffers = 1) { override val usage: Int = GL_DYNAMIC_DRAW override val target: Int = GL_ARRAY_BUFFER override val access: Int = GL_MAP_WRITE_BIT - override var index = 0 - override val bufferIds = intArrayOf(glGenBuffers()) override fun upload( data: ByteBuffer, diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt index f46ec695c..0c0c80972 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt @@ -95,6 +95,6 @@ class AnimatedTexture( init { readGif() - pbo = PixelBuffer(width, height, this@AnimatedTexture, format = GL_RGBA) + pbo = PixelBuffer(width, height, format = GL_RGBA, this@AnimatedTexture) } } From bc30ebc5a3e0db8f84fa567bf39f51602ffd308e Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sun, 15 Dec 2024 17:09:03 -0500 Subject: [PATCH 056/114] fix: immutable storage GL_DYNAMIC_STORAGE_BIT bit --- .../src/main/kotlin/com/lambda/graphics/buffer/IBuffer.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/IBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/IBuffer.kt index 535dee7fb..d11bbf83b 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/IBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/IBuffer.kt @@ -20,8 +20,7 @@ package com.lambda.graphics.buffer import com.lambda.graphics.gl.bufferBound import com.lambda.graphics.gl.bufferUsageValid import com.lambda.graphics.gl.bufferValid -import org.lwjgl.opengl.GL30C.* -import org.lwjgl.opengl.GL44.glBufferStorage +import org.lwjgl.opengl.GL44.* import java.nio.ByteBuffer abstract class IBuffer( @@ -212,7 +211,7 @@ abstract class IBuffer( repeat(buffers) { bind() - glBufferStorage(target, data, usage) + glBufferStorage(target, data, access or GL_DYNAMIC_STORAGE_BIT) swap() } @@ -237,7 +236,7 @@ abstract class IBuffer( repeat(buffers) { bind() - glBufferStorage(target, size.coerceAtLeast(0), usage) + glBufferStorage(target, size.coerceAtLeast(0), access or GL_DYNAMIC_STORAGE_BIT) swap() } From d24db097169680bdd6cc89e8c6641bcbe8d83485 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sun, 15 Dec 2024 17:10:10 -0500 Subject: [PATCH 057/114] IBuffer -> Buffer --- .../com/lambda/graphics/buffer/{IBuffer.kt => Buffer.kt} | 2 +- .../kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt | 4 ++-- .../com/lambda/graphics/buffer/vertex/ElementBuffer.kt | 4 ++-- .../kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt | 4 ++-- .../kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt | 5 ++--- 5 files changed, 9 insertions(+), 10 deletions(-) rename common/src/main/kotlin/com/lambda/graphics/buffer/{IBuffer.kt => Buffer.kt} (99%) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/IBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt similarity index 99% rename from common/src/main/kotlin/com/lambda/graphics/buffer/IBuffer.kt rename to common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt index d11bbf83b..8e63087fd 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/IBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt @@ -23,7 +23,7 @@ import com.lambda.graphics.gl.bufferValid import org.lwjgl.opengl.GL44.* import java.nio.ByteBuffer -abstract class IBuffer( +abstract class Buffer( /** * Specifies how many buffer must be used * diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt index ef3b065a8..6a86de42e 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt @@ -17,7 +17,7 @@ package com.lambda.graphics.buffer.pixel -import com.lambda.graphics.buffer.IBuffer +import com.lambda.graphics.buffer.Buffer import com.lambda.graphics.gl.putTo import com.lambda.graphics.texture.Texture import com.lambda.util.math.MathUtils.toInt @@ -47,7 +47,7 @@ class PixelBuffer( private val texture: Texture, private val asynchronous: Boolean = false, private val bufferMapping: Boolean = false, -) : IBuffer(buffers = asynchronous.toInt() + 1) { +) : Buffer(buffers = asynchronous.toInt() + 1) { override val usage: Int = GL_STATIC_DRAW override val target: Int = GL_PIXEL_UNPACK_BUFFER override val access: Int = GL_MAP_WRITE_BIT diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt index 7669ca2fa..700ff8a8f 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt @@ -17,14 +17,14 @@ package com.lambda.graphics.buffer.vertex -import com.lambda.graphics.buffer.IBuffer +import com.lambda.graphics.buffer.Buffer import com.lambda.graphics.buffer.vertex.attributes.VertexMode import com.lambda.graphics.gl.kibibyte import org.lwjgl.opengl.GL30C.* import java.nio.ByteBuffer class ElementBuffer(mode: VertexMode) : - IBuffer(buffers = 1) + Buffer(buffers = 1) { override val usage: Int = GL_DYNAMIC_DRAW override val target: Int = GL_ELEMENT_ARRAY_BUFFER diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt index 7eacbb43e..5362c6ad4 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt @@ -17,12 +17,12 @@ package com.lambda.graphics.buffer.vertex -import com.lambda.graphics.buffer.IBuffer +import com.lambda.graphics.buffer.Buffer import net.minecraft.client.render.BufferRenderer import org.lwjgl.opengl.GL30C.* import java.nio.ByteBuffer -class VertexArray : IBuffer(isVertexArray = true) { +class VertexArray : Buffer(isVertexArray = true) { override val usage: Int = -1 override val target: Int = -1 override val access: Int = -1 diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt index b9e1606f7..1d9ff8185 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt @@ -17,18 +17,17 @@ package com.lambda.graphics.buffer.vertex -import com.lambda.graphics.buffer.IBuffer +import com.lambda.graphics.buffer.Buffer import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib import com.lambda.graphics.buffer.vertex.attributes.VertexMode import com.lambda.graphics.gl.kibibyte -import com.lambda.graphics.gl.putTo import org.lwjgl.opengl.GL30C.* import java.nio.ByteBuffer class VertexBuffer( mode: VertexMode, attributes: VertexAttrib.Group, -) : IBuffer(buffers = 1) { +) : Buffer(buffers = 1) { override val usage: Int = GL_DYNAMIC_DRAW override val target: Int = GL_ARRAY_BUFFER override val access: Int = GL_MAP_WRITE_BIT From 625ad4d28f3b6e08a962e5e59edfc11b0f7a6062 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:11:30 -0500 Subject: [PATCH 058/114] Update LambdaMoji.kt --- .../kotlin/com/lambda/module/modules/client/LambdaMoji.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt b/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt index c033b9064..26b4e7a03 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt @@ -18,7 +18,7 @@ package com.lambda.module.modules.client import com.lambda.event.events.RenderEvent -import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.gui.api.RenderLayer import com.lambda.module.Module import com.lambda.module.tag.ModuleTag @@ -37,7 +37,7 @@ object LambdaMoji : Module( private val renderQueue = mutableListOf>() init { - listener { + listen { renderQueue.forEach { (text, position) -> renderer.font.build(text, position, scale = scale) } From 519b96d014fc8efb03cdc4056b2a89daae69a4fe Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:13:21 -0500 Subject: [PATCH 059/114] Update LambdaScreen.kt --- common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt index 14d27d727..1c91a2af6 100644 --- a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt +++ b/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt @@ -21,15 +21,12 @@ import com.lambda.Lambda.mc import com.lambda.event.Muteable import com.lambda.event.events.RenderEvent import com.lambda.event.events.TickEvent -import com.lambda.event.listener.SafeListener.Companion.listener -import com.lambda.graphics.RenderMain +import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.gui.api.GuiEvent import com.lambda.newgui.component.layout.Layout -import com.lambda.newgui.component.core.UIBuilder import com.lambda.util.KeyCode import com.lambda.util.Mouse import com.lambda.util.Nameable -import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d import com.mojang.blaze3d.systems.RenderSystem.recordRenderCall import net.minecraft.client.gui.DrawContext @@ -49,12 +46,12 @@ class LambdaScreen( val isOpen get() = mc.currentScreen == this init { - listener { event -> + listen { event -> screenSize = event.screenSize layout.onEvent(GuiEvent.Render()) } - listener { + listen { layout.onEvent(GuiEvent.Tick()) } } From 0f8e2e4fc3e5d652b8c9b72a68e8d5ad4dc9247e Mon Sep 17 00:00:00 2001 From: blade Date: Fri, 20 Dec 2024 21:01:11 +0300 Subject: [PATCH 060/114] RenderPipeline(raw) --- .../com/lambda/event/events/RenderEvent.kt | 6 +- .../kotlin/com/lambda/graphics/RenderMain.kt | 4 -- .../DynamicESP.kt => RenderPipeline.kt} | 29 ++++++-- .../graphics/renderer/esp/global/StaticESP.kt | 34 ---------- .../construction/result/Drawable.kt | 4 +- .../lambda/module/modules/player/FastBreak.kt | 5 +- .../module/modules/player/PacketMine.kt | 4 +- .../newgui/component/window/TitleBar.kt | 1 - .../newgui/component/window/WindowContent.kt | 66 +++++++++++-------- .../newgui/impl/clickgui/ModuleLayout.kt | 45 +++++++++++-- .../newgui/impl/clickgui/SettingLayout.kt | 16 ++++- .../impl/clickgui/settings/BooleanButton.kt | 10 ++- 12 files changed, 134 insertions(+), 90 deletions(-) rename common/src/main/kotlin/com/lambda/graphics/{renderer/esp/global/DynamicESP.kt => RenderPipeline.kt} (59%) delete mode 100644 common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/StaticESP.kt diff --git a/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt b/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt index 6c895d02d..f6bed9186 100644 --- a/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt @@ -21,7 +21,7 @@ import com.lambda.Lambda.mc import com.lambda.event.Event import com.lambda.event.callback.Cancellable import com.lambda.event.callback.ICancellable -import com.lambda.graphics.renderer.esp.global.DynamicESP +import com.lambda.graphics.RenderPipeline import com.lambda.graphics.renderer.esp.global.StaticESP import com.lambda.util.math.Vec2d @@ -29,11 +29,11 @@ sealed class RenderEvent { class World : Event class StaticESP : Event { - val renderer = StaticESP + val renderer = RenderPipeline.STATIC_ESP } class DynamicESP : Event { - val renderer = DynamicESP + val renderer = RenderPipeline.DYNAMIC_ESP } sealed class GUI(val scale: Double) : Event { diff --git a/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt index aa37c6214..d5a564ed1 100644 --- a/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt +++ b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt @@ -28,8 +28,6 @@ import com.lambda.graphics.buffer.FrameBuffer import com.lambda.graphics.gl.GlStateUtils.setupGL import com.lambda.graphics.gl.Matrices import com.lambda.graphics.gl.Matrices.resetMatrices -import com.lambda.graphics.renderer.esp.global.StaticESP -import com.lambda.graphics.renderer.esp.global.DynamicESP import com.lambda.graphics.shader.Shader import com.lambda.gui.impl.hudgui.LambdaHudGui import com.lambda.module.modules.client.ClickGui @@ -80,8 +78,6 @@ object RenderMain { setupGL { RenderEvent.World().post() - StaticESP.render() - DynamicESP.render() } } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/DynamicESP.kt b/common/src/main/kotlin/com/lambda/graphics/RenderPipeline.kt similarity index 59% rename from common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/DynamicESP.kt rename to common/src/main/kotlin/com/lambda/graphics/RenderPipeline.kt index 2a0e837ad..6c43fe88a 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/DynamicESP.kt +++ b/common/src/main/kotlin/com/lambda/graphics/RenderPipeline.kt @@ -15,20 +15,39 @@ * along with this program. If not, see . */ -package com.lambda.graphics.renderer.esp.global +package com.lambda.graphics +import com.lambda.core.Loadable import com.lambda.event.EventFlow.post import com.lambda.event.events.RenderEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listener import com.lambda.graphics.renderer.esp.impl.DynamicESPRenderer +import com.lambda.graphics.renderer.esp.impl.StaticESPRenderer + +object RenderPipeline : Loadable { + // Updates once a tick, stays fixed, uses less memory + val STATIC_ESP = StaticESPRenderer() + + // Updates once a tick, interpolates within frames + val DYNAMIC_ESP = DynamicESPRenderer() -object DynamicESP : DynamicESPRenderer() { init { + // Ticked 3d renderers update listener { - clear() + STATIC_ESP.clear() + RenderEvent.StaticESP().post() + STATIC_ESP.upload() + + DYNAMIC_ESP.clear() RenderEvent.DynamicESP().post() - upload() + DYNAMIC_ESP.upload() + } + + // 3d renderers drawcall + listener { + STATIC_ESP.render() + DYNAMIC_ESP.render() } } -} +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/StaticESP.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/StaticESP.kt deleted file mode 100644 index 8f4ce984b..000000000 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/StaticESP.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.renderer.esp.global - -import com.lambda.event.EventFlow.post -import com.lambda.event.events.RenderEvent -import com.lambda.event.events.TickEvent -import com.lambda.event.listener.SafeListener.Companion.listener -import com.lambda.graphics.renderer.esp.impl.StaticESPRenderer - -object StaticESP : StaticESPRenderer(false) { - init { - listener { - clear() - RenderEvent.StaticESP().post() - upload() - } - } -} diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/result/Drawable.kt b/common/src/main/kotlin/com/lambda/interaction/construction/result/Drawable.kt index 6bec0c214..6742e2ee0 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/result/Drawable.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/result/Drawable.kt @@ -18,9 +18,9 @@ package com.lambda.interaction.construction.result import com.lambda.context.SafeContext +import com.lambda.graphics.RenderPipeline import com.lambda.graphics.renderer.esp.DirectionMask import com.lambda.graphics.renderer.esp.DirectionMask.include -import com.lambda.graphics.renderer.esp.global.StaticESP import com.lambda.graphics.renderer.esp.builders.buildFilled import com.lambda.util.BlockUtils.blockState import net.minecraft.block.BlockState @@ -34,7 +34,7 @@ interface Drawable { fun SafeContext.buildRenderer() fun SafeContext.withBox(box: Box, color: Color, mask: Int = DirectionMask.ALL) { - StaticESP.buildFilled(box, color, mask) + RenderPipeline.STATIC_ESP.buildFilled(box, color, mask) //StaticESP.buildOutline(box, color, mask) } diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt b/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt index 01b906c1d..3b7e985d0 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt @@ -20,10 +20,10 @@ package com.lambda.module.modules.player import com.lambda.context.SafeContext import com.lambda.event.events.* import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.graphics.RenderPipeline import com.lambda.graphics.renderer.esp.DynamicAABB import com.lambda.graphics.renderer.esp.builders.buildFilled import com.lambda.graphics.renderer.esp.builders.buildOutline -import com.lambda.graphics.renderer.esp.global.DynamicESP import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.util.math.lerp @@ -59,8 +59,7 @@ object FastBreak : Module( private val endOutlineColour by setting("End Outline Colour", Color(0f, 1f, 0f, 0.3f), "The colour used to render the end outline of the box", visibility = { page == Page.Render && renderMode.isEnabled() && renderSetting != RenderSetting.Fill && outlineColourMode == ColourMode.Dynamic }) private val outlineWidth by setting("Outline Width", 1f, 0f..3f, 0.1f, "the thickness of the outline", visibility = { page == Page.Render && renderMode.isEnabled() && renderSetting != RenderSetting.Fill }) - - private val renderer = DynamicESP + private val renderer = RenderPipeline.DYNAMIC_ESP private var boxSet = emptySet() private enum class Page { diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt b/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt index d7ef60656..467b82f77 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt @@ -21,10 +21,10 @@ import com.lambda.Lambda.mc import com.lambda.context.SafeContext import com.lambda.event.events.* import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.graphics.RenderPipeline import com.lambda.graphics.renderer.esp.DynamicAABB import com.lambda.graphics.renderer.esp.builders.buildFilled import com.lambda.graphics.renderer.esp.builders.buildOutline -import com.lambda.graphics.renderer.esp.global.DynamicESP import com.lambda.interaction.RotationManager import com.lambda.interaction.rotation.RotationContext import com.lambda.interaction.visibilty.VisibilityChecker.findRotation @@ -246,7 +246,7 @@ object PacketMine : Module( this == Primary } - val renderer = DynamicESP + val renderer = RenderPipeline.DYNAMIC_ESP private var currentMiningBlock = Array(2) { null } private var lastNonEmptyState: BlockState? = null private val blockQueue = ArrayDeque() diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt index 38864d9e3..ee79c2e8e 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt @@ -19,7 +19,6 @@ package com.lambda.newgui.component.window import com.lambda.module.modules.client.NewCGui import com.lambda.newgui.component.HAlign -import com.lambda.newgui.component.VAlign import com.lambda.newgui.component.core.TextField.Companion.textField import com.lambda.newgui.component.core.UIBuilder import com.lambda.newgui.component.layout.Layout diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt index 9b628af5c..91f1bc46c 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt @@ -34,10 +34,43 @@ class WindowContent( private var scrollOffset = 0.0 private var rubberbandDelta = 0.0 - private var renderScrollOffset by animation.exp({ scrollOffset + rubberbandDelta }, 0.7) - private val scaleAnimation by animation.exp(1.0, 0.9, 0.7, ::scrolling) + var renderScrollOffset by animation.exp({ scrollOffset + rubberbandDelta }, 0.7) private var scrolling = false + private var contentHeight = { + NewCGui.padding * 2 + + children.sumOf(Layout::renderHeight) + + NewCGui.listStep * (children.size - 1).coerceAtLeast(0) + } + + private var reorder = block@ { + children.forEachIndexed { i, child -> + val prev by lazy { children[i - 1] } + + child.overrideY { + if (i == 0) { + renderPositionY + renderScrollOffset + NewCGui.padding + } else { + prev.renderPositionY + prev.renderHeight + NewCGui.listStep + } + } + } + } + + /** + * Overrides the summary height of the content + */ + fun overrideContentHeight(block: () -> Double) { + contentHeight = block + } + + /** + * Overrides the action performed on ordering update + */ + fun reorderChildren(block: () -> Unit) { + reorder = block + } + init { overrideX { owner.titleBar.renderPositionX } overrideY { owner.titleBar.let { it.renderPositionY + it.renderHeight } } @@ -50,7 +83,7 @@ class WindowContent( rubberbandDelta = 0.0 renderScrollOffset = 0.0 - reorderChildren() + if (scrollable) reorder() } onTick { @@ -70,8 +103,6 @@ class WindowContent( if (abs(rubberbandDelta) < 0.05) rubberbandDelta = 0.0 animation.tick() - - reorderChildren() } onMouseScroll { delta -> @@ -80,29 +111,12 @@ class WindowContent( } } - private fun reorderChildren() { - if (!scrollable) return - - children.forEachIndexed { i, child -> - val prev by lazy { children[i - 1] } - - child.overrideY { - if (i == 0) { - renderPositionY + renderScrollOffset + NewCGui.padding - } else { - prev.renderPositionY + prev.renderHeight + NewCGui.listStep - } - } - } + override fun render(e: GuiEvent) { + if (scrollable) reorder() + super.render(e) } - fun getContentHeight(): Double { - val components = children.sumOf(Layout::renderHeight) - val step = NewCGui.listStep * (children.size - 1).coerceAtLeast(0) - val padding = NewCGui.padding * 2 - - return components + step + padding - } + fun getContentHeight() = contentHeight() companion object { /** diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt index 0d9c4ce5a..d4094ea9e 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt @@ -77,6 +77,37 @@ class ModuleLayout( } } + content.overrideContentHeight { + val settings = content.children + .filterIsInstance>() + + val components = settings.sumOf { + (it.renderHeight + NewCGui.listStep) * it.visibilityAnimation + } - NewCGui.listStep + + val padding = NewCGui.padding * 2 + components + if (settings.isNotEmpty()) padding else 0.0 + } + + content.reorderChildren { + val settings = content.children + .filterIsInstance>() + + var y = 0.0 + + settings.forEach { + if (it.visible) { + it.heightOffset = y + } + + y += (it.renderHeight + NewCGui.listStep) * it.visibilityAnimation + + it.overrideY { + content.renderPositionY + content.renderScrollOffset + NewCGui.padding + it.heightOffset + } + } + } + rect { // Separator onUpdate { val vec = Vec2d( @@ -94,10 +125,6 @@ class ModuleLayout( } } - onShow { - enableAnimation = 0.0 - } - titleBarRect.onUpdate { setColor(lerp(enableAnimation, NewCGui.moduleDisabledColor, NewCGui.moduleEnabledColor)) correctRadius() @@ -110,18 +137,22 @@ class ModuleLayout( children.remove(outlineRect) + onShow { + enableAnimation = 0.0 + } + onTick { val cursor = if (titleBar.isHovered) Mouse.Cursor.Pointer else Mouse.Cursor.Arrow cursorController.setCursor(cursor) } - content.apply { - module.settings.forEach { setting -> layoutOf(setting) } + module.settings.forEach { setting -> + content.layoutOf(setting) } } private fun FilledRect.correctRadius() { - if (!isLast) { + if (!isLast || !NewCGui.autoResize) { setRadius(0.0) return } diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt index 737a0dc2e..2ff1a9f1a 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt @@ -18,11 +18,15 @@ package com.lambda.newgui.impl.clickgui import com.lambda.config.AbstractSetting +import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.module.modules.client.NewCGui import com.lambda.newgui.component.HAlign import com.lambda.newgui.component.layout.Layout import com.lambda.newgui.component.window.Window import com.lambda.util.math.Vec2d +import com.lambda.util.math.setAlpha +import com.lambda.util.math.transform +import java.awt.Color /** * A base class for setting layouts. @@ -44,12 +48,21 @@ abstract class SettingLayout > ( protected val animation = animationTicker() protected val cursorController = cursorController() + var visibilityAnimation by animation.exp(0.0, 1.0, 0.8, ::visible) + var heightOffset = 0.0 + var settingValue by setting + val visible get() = setting.visibility() init { + minimized = true + overrideWidth(owner::renderWidth) titleBar.overrideHeight(NewCGui::settingsHeight) - minimized = true + + overrideX { + owner.renderPositionX + transform(visibilityAnimation, 0.0, 1.0, -10.0, 0.0) + } with(titleBar.textField) { text = setting.name @@ -58,6 +71,7 @@ abstract class SettingLayout > ( onUpdate { scale = NewCGui.fontScale * 0.92 + color = Color.WHITE.setAlpha(visibilityAnimation) } } diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt index d38185d5c..d1fe16e25 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt +++ b/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt @@ -49,10 +49,16 @@ class BooleanButton( rectangle = Rect(rb - Vec2d(h * 1.65, h), rb) .shrink(shrink) + Vec2d.LEFT * (NewCGui.fontOffset - shrink) - setColor(Color.BLACK.setAlpha(0.25)) + setColor(Color.BLACK.setAlpha(0.25 * visibilityAnimation)) shade = NewCGui.backgroundShade } + onTick { + cursorController.setCursor( + if (isHovered) Mouse.Cursor.Pointer else Mouse.Cursor.Arrow + ) + } + onMouseClick { button, action -> if (button == Mouse.Button.Left && action == Mouse.Action.Click) { setting.value = !setting.value @@ -68,7 +74,7 @@ class BooleanButton( val knobEnd = Rect(checkBox.rightBottom - checkBox.renderHeight, checkBox.rightBottom) rectangle = lerp(activeAnimation, knobStart, knobEnd).shrink(1.0) shade = NewCGui.backgroundShade - setColor(Color.WHITE.setAlpha(0.25)) + setColor(Color.WHITE.setAlpha(0.25 * visibilityAnimation)) } } } From b2841eed3e3b58263400ffae657e386261f5e946 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 21 Dec 2024 13:07:58 -0500 Subject: [PATCH 061/114] refactored animated texture --- .../graphics/texture/AnimatedTexture.kt | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt index 0c0c80972..afc8ea993 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt @@ -25,26 +25,22 @@ import org.lwjgl.BufferUtils import org.lwjgl.opengl.GL11.GL_RGBA import org.lwjgl.stb.STBImage import java.nio.ByteBuffer -import kotlin.properties.Delegates -class AnimatedTexture( - private val path: LambdaResource, -) : Texture(null) { - lateinit var frameDurations: IntArray - var width by Delegates.notNull() - var height by Delegates.notNull() - var channels by Delegates.notNull() - var frames by Delegates.notNull() +class AnimatedTexture(path: LambdaResource) : Texture(null) { + private val pbo: PixelBuffer + private val gif: ByteBuffer // Do NOT free this pointer + private val frameDurations: IntArray + val width: Int + val height: Int + val channels: Int + val frames: Int - val blockSize: Int + private val blockSize: Int get() = width * height * channels - lateinit var gif: ByteBuffer // Do NOT free this pointer - var pbo: PixelBuffer - - var currentFrame = 0 - var lastUpload = 0L + private var currentFrame = 0 + private var lastUpload = 0L override fun bind(slot: Int) { update() @@ -53,6 +49,9 @@ class AnimatedTexture( fun update() { if (System.currentTimeMillis() - lastUpload >= frameDurations[currentFrame]) { + // This is cool because instead of having a buffer for each frame we can + // just move the frame's block on each update + // 0 memory allocation and few cpu cycles val slice = gif .position(blockSize * currentFrame) .limit(blockSize * (currentFrame + 1)) @@ -67,7 +66,7 @@ class AnimatedTexture( } } - private fun readGif() { + init { val bytes = path.stream.readAllBytes() val buffer = ByteBuffer.allocateDirect(bytes.size) @@ -82,19 +81,17 @@ class AnimatedTexture( // The buffer contains packed frames that can be extracted as follows: // limit = width * height * channels * [frame number] - gif = STBImage.stbi_load_gif_from_memory(buffer, pDelays, pWidth, pHeight, pLayers, pChannels, 4)!! + gif = STBImage.stbi_load_gif_from_memory(buffer, pDelays, pWidth, pHeight, pLayers, pChannels, 4) + ?: throw IllegalStateException("There was an unknown error while loading the gif file") width = pWidth.get() height = pHeight.get() frames = pLayers.get() channels = pChannels.get() - frameDurations = IntArray(frames) + pDelays.getIntBuffer(frames).get(frameDurations) - } - init { - readGif() pbo = PixelBuffer(width, height, format = GL_RGBA, this@AnimatedTexture) } } From a2c699358cfb3b0f5c07b945c4f82344384f2902 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 21 Dec 2024 13:08:26 -0500 Subject: [PATCH 062/114] consistency flag and better texture documentation --- .../com/lambda/graphics/texture/Texture.kt | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index 01167ea1b..eca025a5a 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -21,20 +21,44 @@ import com.lambda.graphics.texture.TextureUtils.bindTexture import com.lambda.graphics.texture.TextureUtils.readImage import com.lambda.graphics.texture.TextureUtils.setupTexture import com.lambda.module.modules.client.RenderSettings +import org.lwjgl.opengl.GL11C import org.lwjgl.opengl.GL45C.* import java.awt.image.BufferedImage +/** + * Represents a texture that can be uploaded and bound to the graphics pipeline. + * Supports mipmap generation and LOD (Level of Detail) configuration + * + * @param image Optional initial image to upload to the texture + * @param levels Number of mipmap levels to generate for the texture + * @param forceConsistency Flag to enforce consistency when updating the texture. If true, attempts to update + * the texture after initialization will throw an exception + */ open class Texture( image: BufferedImage?, private val levels: Int = 4, + private val forceConsistency: Boolean = false, ) { + /** + * Indicates whether there is an initial texture or not + */ + var initialized: Boolean = false; private set val id = glGenTextures() + /** + * Binds the texture to a specific slot in the graphics pipeline. + */ open fun bind(slot: Int = 0) { bindTexture(id, slot) glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, RenderSettings.lodBias) } + /** + * Uploads an image to the texture and generates mipmaps for the texture if applicable. + * + * @param image The image to upload to the texture + * @param offset The mipmap level to upload the image to + */ open fun upload(image: BufferedImage, offset: Int = 0) { // Store level_base +1 through `level` images and generate // mipmaps from them @@ -43,13 +67,24 @@ open class Texture( val width = image.width val height = image.height - // Set this mipmap to 0 to define the original texture - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, readImage(image)) + // Set this mipmap to `offset` to define the original texture + glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, readImage(image)) glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) } + open fun update(image: BufferedImage, offset: Int = 0) { + if (forceConsistency && initialized) + throw IllegalStateException("Client tried to update a texture, but the enforce consistency flag was present") + + val width = image.width + val height = image.height + + // Can we rebuild LOD ? + glTexSubImage2D(GL_TEXTURE_2D, offset, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, readImage(image)) + } + private fun setupLOD(levels: Int) { // When you call glTextureStorage, you're specifying the total number of levels, including level 0 // This is a 0-based index system, which means that the maximum mipmap level is n-1 @@ -66,6 +101,8 @@ open class Texture( image?.let { bind() upload(it) + + initialized = true } } } From 46bc2d9d4565eef0417b79b910f66d06df7ed6fe Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 21 Dec 2024 13:09:26 -0500 Subject: [PATCH 063/114] don't force texture consistency in gif --- .../main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt index afc8ea993..f9b8a99c7 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt @@ -27,7 +27,7 @@ import org.lwjgl.stb.STBImage import java.nio.ByteBuffer -class AnimatedTexture(path: LambdaResource) : Texture(null) { +class AnimatedTexture(path: LambdaResource) : Texture(image = null, forceConsistency = false) { private val pbo: PixelBuffer private val gif: ByteBuffer // Do NOT free this pointer private val frameDurations: IntArray From 3884652cf4a53c73302a203b06630dda487dd0aa Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 21 Dec 2024 13:19:42 -0500 Subject: [PATCH 064/114] fixed incomplete merge conflicts --- .../com/lambda/event/events/RenderEvent.kt | 1 - .../com/lambda/graphics/RenderPipeline.kt | 27 ++++++++++++++++--- .../lambda/module/modules/player/FastBreak.kt | 4 +-- .../module/modules/player/PacketMine.kt | 4 +-- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt b/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt index f6bed9186..51b721f2a 100644 --- a/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt @@ -22,7 +22,6 @@ import com.lambda.event.Event import com.lambda.event.callback.Cancellable import com.lambda.event.callback.ICancellable import com.lambda.graphics.RenderPipeline -import com.lambda.graphics.renderer.esp.global.StaticESP import com.lambda.util.math.Vec2d sealed class RenderEvent { diff --git a/common/src/main/kotlin/com/lambda/graphics/RenderPipeline.kt b/common/src/main/kotlin/com/lambda/graphics/RenderPipeline.kt index 32ac305fa..bdf509180 100644 --- a/common/src/main/kotlin/com/lambda/graphics/RenderPipeline.kt +++ b/common/src/main/kotlin/com/lambda/graphics/RenderPipeline.kt @@ -15,20 +15,39 @@ * along with this program. If not, see . */ -package com.lambda.graphics.renderer.esp.global +package com.lambda.graphics +import com.lambda.core.Loadable import com.lambda.event.EventFlow.post import com.lambda.event.events.RenderEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.graphics.renderer.esp.impl.DynamicESPRenderer +import com.lambda.graphics.renderer.esp.impl.StaticESPRenderer + +object RenderPipeline : Loadable { + // Updates once a tick, stays fixed, uses less memory + val STATIC_ESP = StaticESPRenderer() + + // Updates once a tick, interpolates within frames + val DYNAMIC_ESP = DynamicESPRenderer() -object DynamicESP : DynamicESPRenderer() { init { + // Ticked 3d renderers update listen { - clear() + STATIC_ESP.clear() + RenderEvent.StaticESP().post() + STATIC_ESP.upload() + + DYNAMIC_ESP.clear() RenderEvent.DynamicESP().post() - upload() + DYNAMIC_ESP.upload() + } + + // 3d renderers drawcall + listen { + STATIC_ESP.render() + DYNAMIC_ESP.render() } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt b/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt index 7dbbbe400..154dfedad 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt @@ -20,10 +20,10 @@ package com.lambda.module.modules.player import com.lambda.context.SafeContext import com.lambda.event.events.* import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.RenderPipeline import com.lambda.graphics.renderer.esp.DynamicAABB import com.lambda.graphics.renderer.esp.builders.buildFilled import com.lambda.graphics.renderer.esp.builders.buildOutline -import com.lambda.graphics.renderer.esp.global.DynamicESP import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.util.math.lerp @@ -60,7 +60,7 @@ object FastBreak : Module( private val outlineWidth by setting("Outline Width", 1f, 0f..3f, 0.1f, "the thickness of the outline", visibility = { page == Page.Render && renderMode.isEnabled() && renderSetting != RenderSetting.Fill }) - private val renderer = DynamicESP + private val renderer = RenderPipeline.DYNAMIC_ESP private var boxSet = emptySet() private enum class Page { diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt b/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt index 6e2049e11..d949ba614 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt @@ -21,10 +21,10 @@ import com.lambda.Lambda.mc import com.lambda.context.SafeContext import com.lambda.event.events.* import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.RenderPipeline import com.lambda.graphics.renderer.esp.DynamicAABB import com.lambda.graphics.renderer.esp.builders.buildFilled import com.lambda.graphics.renderer.esp.builders.buildOutline -import com.lambda.graphics.renderer.esp.global.DynamicESP import com.lambda.interaction.RotationManager import com.lambda.interaction.rotation.RotationContext import com.lambda.interaction.visibilty.VisibilityChecker.findRotation @@ -246,7 +246,7 @@ object PacketMine : Module( this == Primary } - val renderer = DynamicESP + val renderer = RenderPipeline.DYNAMIC_ESP private var currentMiningBlock = Array(2) { null } private var lastNonEmptyState: BlockState? = null private val blockQueue = ArrayDeque() From f915e46a0281a5b65d94c3238682ae70979e37ea Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 21 Dec 2024 19:11:34 -0500 Subject: [PATCH 065/114] support return lines --- .../renderer/gui/font/FontRenderer.kt | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index 9bc7b49f3..9721d8a51 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -44,10 +44,10 @@ class FontRenderer { private val shader = Shader("renderer/font") private val pipeline = VertexPipeline(VertexMode.TRIANGLES, VertexAttrib.Group.FONT) - val shadowShift get() = RenderSettings.shadowShift * 5.0 - val baselineOffset get() = RenderSettings.baselineOffset * 2.0f - 10f - val gap get() = RenderSettings.gap * 0.5f - 0.8f - val scaleMultiplier: Double get() = ClickGui.settingsFontScale + private val shadowShift get() = RenderSettings.shadowShift * 5.0 + private val baselineOffset get() = RenderSettings.baselineOffset * 2.0f - 10f + private val gap get() = RenderSettings.gap * 0.5f - 0.8f + private val scaleMultiplier: Double get() = ClickGui.settingsFontScale /** * Builds the vertex array for rendering the provided text string at a specified position. @@ -145,7 +145,7 @@ class FontRenderer { val emojiColor = color.setAlpha(color.a) var posX = 0.0 - val posY = getHeight(scale) * -0.5 + baselineOffset * actualScale + var posY = getHeight(scale) * -0.5 + baselineOffset * actualScale fun drawGlyph(info: GlyphInfo?, color: Color, offset: Double = 0.0) { if (info == null) return @@ -164,12 +164,17 @@ class FontRenderer { if (section.isEmpty()) return if (!parseEmoji || parsed.isEmpty() || !hasEmojis) { // Draw simple characters if no emojis are present - section - .mapNotNull { chars[it] } - .forEach { charGlyph -> - if (shadow && shadowShift > 0.0) drawGlyph(charGlyph, shadowColor, shadowShift) - drawGlyph(charGlyph, color) + section.forEach { char -> + // Logic for control characters + when (char) { + '\n', '\r' -> { posX = 0.0; posY += chars.height * actualScale; return@forEach } } + + val glyph = chars[char] ?: return@forEach + + if (shadow && shadowShift > 0.0) drawGlyph(glyph, shadowColor, shadowShift) + drawGlyph(glyph, color) + } } else { // Only compute the first parsed emoji to avoid duplication // This is important in order to keep the parsed ranges valid From 179d03ed67da0dcfef139ce7f19b26a63096555f Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:33:23 -0500 Subject: [PATCH 066/114] make emojis fade with the text box --- .../mixin/render/TextRendererMixin.java | 26 ++++++++++++------- .../com/lambda/graphics/texture/Texture.kt | 3 +-- .../module/modules/client/LambdaMoji.kt | 9 ++++--- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java b/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java index bd2a51ddf..8f5c2bf55 100644 --- a/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java +++ b/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java @@ -33,6 +33,7 @@ import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; +import java.awt.*; import java.util.List; @Mixin(TextRenderer.class) @@ -61,9 +62,9 @@ public int draw( int light, boolean rightToLeft ) { - if (LambdaMoji.INSTANCE.isDisabled()) return this.drawInternal(text, x, y, color, shadow, matrix, vertexConsumers, layerType, backgroundColor, light, rightToLeft); + String parsed = neoLambda$parseEmojisAndRender(text, x, y, color); - return this.drawInternal(neoLambda$parseEmojisAndRender(text, x, y), x, y, color, shadow, matrix, vertexConsumers, layerType, backgroundColor, light, rightToLeft); + return this.drawInternal(parsed, x, y, color, shadow, matrix, vertexConsumers, layerType, backgroundColor, light, rightToLeft); } /** @@ -83,22 +84,21 @@ public int draw( int backgroundColor, int light ) { - if (LambdaMoji.INSTANCE.isDisabled()) return this.drawInternal(text, x, y, color, shadow, matrix, vertexConsumers, layerType, backgroundColor, light); - StringBuilder builder = new StringBuilder(); text.accept((index, style, c) -> { builder.appendCodePoint(c); return true; }); - return this.drawInternal( - Text.literal(neoLambda$parseEmojisAndRender(builder.toString(), x, y)).asOrderedText(), - x, y, color, shadow, matrix, vertexConsumers, layerType, backgroundColor, light - ); + String parsed = neoLambda$parseEmojisAndRender(builder.toString(), x, y, color); + + return this.drawInternal(Text.literal(parsed).asOrderedText(), x, y, color, shadow, matrix, vertexConsumers, layerType, backgroundColor, light); } @Unique - private String neoLambda$parseEmojisAndRender(String raw, float x, float y) { + private String neoLambda$parseEmojisAndRender(String raw, float x, float y, int color) { + if (LambdaMoji.INSTANCE.isDisabled()) return raw; + List emojis = LambdaEmoji.Twemoji.parse(raw); for (String emoji : emojis) { @@ -111,7 +111,13 @@ public int draw( int height = Lambda.getMc().textRenderer.fontHeight; int width = Lambda.getMc().textRenderer.getWidth(raw.substring(0, index)); - LambdaMoji.INSTANCE.push(constructed, new Vec2d(x + width, y + (float) height / 2)); + // Dude I'm sick of working with the shitcode that is minecraft's codebase :sob: + Color trueColor = switch (color) { + case 0x00E0E0E0, 0 -> new Color(255, 255, 255, 255); + default -> new Color(255, 255, 255, (color >> 24 & 0xFF)); + }; + + LambdaMoji.INSTANCE.push(constructed, new Vec2d(x + width, y + (float) height / 2), trueColor); // Replace the emoji with whitespaces depending on the player's settings raw = raw.replaceFirst(constructed, neoLambda$getReplacement()); diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index eca025a5a..91f221f46 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -68,10 +68,9 @@ open class Texture( val height = image.height // Set this mipmap to `offset` to define the original texture + setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, readImage(image)) glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack - - setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) } open fun update(image: BufferedImage, offset: Int = 0) { diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt b/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt index 26b4e7a03..cd7dfdd83 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt @@ -23,6 +23,7 @@ import com.lambda.gui.api.RenderLayer import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.util.math.Vec2d +import java.awt.Color object LambdaMoji : Module( name = "LambdaMoji", @@ -34,12 +35,12 @@ object LambdaMoji : Module( val suggestions by setting("Chat Suggestions", true) private val renderer = RenderLayer() - private val renderQueue = mutableListOf>() + private val renderQueue = mutableListOf>() init { listen { - renderQueue.forEach { (text, position) -> - renderer.font.build(text, position, scale = scale) + renderQueue.forEach { (text, position, color) -> + renderer.font.build(text, position, color, scale = scale) } renderer.render() @@ -47,5 +48,5 @@ object LambdaMoji : Module( } } - fun push(text: String, position: Vec2d) = renderQueue.add(Pair(text, position)) + fun push(text: String, position: Vec2d, color: Color) = renderQueue.add(Triple(text, position, color)) } From e58c0d246ba4f8d9ae4e97f93788ad825db30482 Mon Sep 17 00:00:00 2001 From: blade Date: Mon, 23 Dec 2024 23:23:42 +0300 Subject: [PATCH 067/114] quick fix --- common/src/main/kotlin/com/lambda/Lambda.kt | 8 - .../gui/CustomModuleWindowSerializer.kt | 82 ------- .../serializer/gui/ModuleTagSerializer.kt | 40 ---- .../serializer/gui/TagWindowSerializer.kt | 79 ------- .../{gui/api => event/events}/GuiEvent.kt | 17 +- .../com/lambda/event/events/RenderEvent.kt | 8 +- .../kotlin/com/lambda/graphics/RenderMain.kt | 50 +--- .../buffer/vertex/attributes/VertexAttrib.kt | 45 +++- .../com/lambda/graphics/gl/GlStateUtils.kt | 4 +- .../kotlin/com/lambda/graphics/gl/Scissor.kt | 68 ------ .../graphics/pipeline/ScissorAdapter.kt | 88 +++++++ .../pipeline/UIPipeline.kt} | 34 +-- .../graphics/renderer/esp/ChunkedESP.kt | 5 +- .../renderer/esp/{impl => }/ESPRenderer.kt | 19 +- .../esp/global/DynamicESP.kt} | 27 +-- .../renderer/esp/impl/DynamicESPRenderer.kt | 2 + .../renderer/esp/impl/StaticESPRenderer.kt | 1 + .../renderer/gui/font/FontRenderer.kt | 29 ++- .../graphics/renderer/gui/font/LambdaAtlas.kt | 2 +- .../renderer/gui/rect/FilledRectRenderer.kt | 40 ++-- .../renderer/gui/rect/OutlineRectRenderer.kt | 43 ++-- .../com/lambda/graphics/shader/Shader.kt | 3 +- .../com/lambda/graphics/shader/ShaderUtils.kt | 15 +- .../com/lambda/gui/AbstractGuiConfigurable.kt | 43 ---- .../kotlin/com/lambda/gui/GuiConfigurable.kt | 28 --- .../com/lambda/{newgui => gui}/GuiManager.kt | 8 +- .../com/lambda/gui/HudGuiConfigurable.kt | 25 -- .../lambda/{newgui => gui}/LambdaScreen.kt | 14 +- .../lambda/{newgui => gui}/ScreenLayout.kt | 6 +- .../kotlin/com/lambda/gui/api/LambdaGui.kt | 170 -------------- .../gui/api/component/InteractiveComponent.kt | 59 ----- .../lambda/gui/api/component/ListWindow.kt | 62 ----- .../gui/api/component/WindowComponent.kt | 209 ----------------- .../api/component/button/ButtonComponent.kt | 134 ----------- .../api/component/button/InputBarOverlay.kt | 149 ------------ .../gui/api/component/button/ListButton.kt | 44 ---- .../gui/api/component/core/IComponent.kt | 29 --- .../api/component/core/list/ChildComponent.kt | 25 -- .../gui/api/component/core/list/ChildLayer.kt | 71 ------ .../{newgui => gui}/component/Alignment.kt | 2 +- .../core => component}/DockingRect.kt | 6 +- .../component/core/FilledRect.kt | 7 +- .../component/core/OutlineRect.kt | 7 +- .../component/core/TextField.kt | 18 +- .../component/core/UIBuilder.kt | 2 +- .../component/layout/Layout.kt | 62 ++--- .../component/layout/LayoutProperties.kt | 2 +- .../component/window/TitleBar.kt | 19 +- .../component/window/Window.kt | 42 ++-- .../component/window/WindowContent.kt | 29 ++- .../com/lambda/gui/impl/AbstractClickGui.kt | 145 ------------ .../gui/impl/clickgui/LambdaClickGui.kt | 97 -------- .../impl/clickgui/ModuleLayout.kt | 51 ++-- .../impl/clickgui/ModuleWindow.kt | 10 +- .../impl/clickgui/SettingLayout.kt | 15 +- .../gui/impl/clickgui/buttons/ModuleButton.kt | 220 ------------------ .../impl/clickgui/buttons/SettingButton.kt | 54 ----- .../clickgui/buttons/setting/BindButton.kt | 64 ----- .../clickgui/buttons/setting/BooleanButton.kt | 82 ------- .../clickgui/buttons/setting/EnumSlider.kt | 77 ------ .../clickgui/buttons/setting/NumberSlider.kt | 98 -------- .../impl/clickgui/buttons/setting/Slider.kt | 99 -------- .../clickgui/buttons/setting/StringButton.kt | 61 ----- .../impl/clickgui/settings/BooleanButton.kt | 18 +- .../gui/impl/clickgui/windows/ModuleWindow.kt | 70 ------ .../windows/tag/CustomModuleWindow.kt | 30 --- .../impl/clickgui/windows/tag/TagWindow.kt | 38 --- .../lambda/gui/impl/hudgui/LambdaHudGui.kt | 72 ------ .../construction/result/Drawable.kt | 5 +- .../kotlin/com/lambda/module/HudModule.kt | 27 +-- .../main/kotlin/com/lambda/module/Module.kt | 2 - .../kotlin/com/lambda/module/hud/TaskFlow.kt | 3 +- .../com/lambda/module/hud/TickShiftCharge.kt | 26 ++- .../lambda/module/modules/client/ClickGui.kt | 98 ++++---- .../module/modules/client/GuiSettings.kt | 3 +- .../module/modules/client/LambdaMoji.kt | 6 +- .../lambda/module/modules/client/NewCGui.kt | 93 -------- .../lambda/module/modules/player/FastBreak.kt | 5 +- .../module/modules/player/PacketMine.kt | 5 +- .../lambda/module/modules/render/BlockESP.kt | 1 + .../lambda/module/modules/render/Particles.kt | 2 +- .../shaders/fragment/renderer/font.frag | 18 +- .../fragment/renderer/rect_filled.frag | 7 + .../fragment/renderer/rect_outline.frag | 8 + .../lambda/shaders/vertex/renderer/font.vert | 14 +- .../shaders/vertex/renderer/rect_filled.vert | 12 +- .../shaders/vertex/renderer/rect_outline.vert | 16 +- gradle.properties | 2 +- 88 files changed, 559 insertions(+), 3076 deletions(-) delete mode 100644 common/src/main/kotlin/com/lambda/config/serializer/gui/CustomModuleWindowSerializer.kt delete mode 100644 common/src/main/kotlin/com/lambda/config/serializer/gui/ModuleTagSerializer.kt delete mode 100644 common/src/main/kotlin/com/lambda/config/serializer/gui/TagWindowSerializer.kt rename common/src/main/kotlin/com/lambda/{gui/api => event/events}/GuiEvent.kt (59%) delete mode 100644 common/src/main/kotlin/com/lambda/graphics/gl/Scissor.kt create mode 100644 common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt rename common/src/main/kotlin/com/lambda/{gui/api/RenderLayer.kt => graphics/pipeline/UIPipeline.kt} (61%) rename common/src/main/kotlin/com/lambda/graphics/renderer/esp/{impl => }/ESPRenderer.kt (80%) rename common/src/main/kotlin/com/lambda/graphics/{RenderPipeline.kt => renderer/esp/global/DynamicESP.kt} (59%) delete mode 100644 common/src/main/kotlin/com/lambda/gui/AbstractGuiConfigurable.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/GuiConfigurable.kt rename common/src/main/kotlin/com/lambda/{newgui => gui}/GuiManager.kt (88%) delete mode 100644 common/src/main/kotlin/com/lambda/gui/HudGuiConfigurable.kt rename common/src/main/kotlin/com/lambda/{newgui => gui}/LambdaScreen.kt (93%) rename common/src/main/kotlin/com/lambda/{newgui => gui}/ScreenLayout.kt (90%) delete mode 100644 common/src/main/kotlin/com/lambda/gui/api/LambdaGui.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/api/component/InteractiveComponent.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/api/component/ListWindow.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/api/component/WindowComponent.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/api/component/button/ButtonComponent.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/api/component/button/InputBarOverlay.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/api/component/button/ListButton.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/api/component/core/IComponent.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildComponent.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildLayer.kt rename common/src/main/kotlin/com/lambda/{newgui => gui}/component/Alignment.kt (96%) rename common/src/main/kotlin/com/lambda/gui/{api/component/core => component}/DockingRect.kt (95%) rename common/src/main/kotlin/com/lambda/{newgui => gui}/component/core/FilledRect.kt (94%) rename common/src/main/kotlin/com/lambda/{newgui => gui}/component/core/OutlineRect.kt (91%) rename common/src/main/kotlin/com/lambda/{newgui => gui}/component/core/TextField.kt (80%) rename common/src/main/kotlin/com/lambda/{newgui => gui}/component/core/UIBuilder.kt (94%) rename common/src/main/kotlin/com/lambda/{newgui => gui}/component/layout/Layout.kt (89%) rename common/src/main/kotlin/com/lambda/{newgui => gui}/component/layout/LayoutProperties.kt (96%) rename common/src/main/kotlin/com/lambda/{newgui => gui}/component/window/TitleBar.kt (80%) rename common/src/main/kotlin/com/lambda/{newgui => gui}/component/window/Window.kt (88%) rename common/src/main/kotlin/com/lambda/{newgui => gui}/component/window/WindowContent.kt (85%) delete mode 100644 common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/LambdaClickGui.kt rename common/src/main/kotlin/com/lambda/{newgui => gui}/impl/clickgui/ModuleLayout.kt (73%) rename common/src/main/kotlin/com/lambda/{newgui => gui}/impl/clickgui/ModuleWindow.kt (86%) rename common/src/main/kotlin/com/lambda/{newgui => gui}/impl/clickgui/SettingLayout.kt (86%) delete mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/SettingButton.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/BindButton.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/BooleanButton.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/EnumSlider.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/NumberSlider.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/Slider.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/StringButton.kt rename common/src/main/kotlin/com/lambda/{newgui => gui}/impl/clickgui/settings/BooleanButton.kt (84%) delete mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/ModuleWindow.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/tag/CustomModuleWindow.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/tag/TagWindow.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/impl/hudgui/LambdaHudGui.kt delete mode 100644 common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt diff --git a/common/src/main/kotlin/com/lambda/Lambda.kt b/common/src/main/kotlin/com/lambda/Lambda.kt index 9150d7723..7476fbb4b 100644 --- a/common/src/main/kotlin/com/lambda/Lambda.kt +++ b/common/src/main/kotlin/com/lambda/Lambda.kt @@ -20,12 +20,7 @@ package com.lambda import com.google.gson.Gson import com.google.gson.GsonBuilder import com.lambda.config.serializer.* -import com.lambda.config.serializer.gui.CustomModuleWindowSerializer -import com.lambda.config.serializer.gui.ModuleTagSerializer -import com.lambda.config.serializer.gui.TagWindowSerializer import com.lambda.core.Loader -import com.lambda.gui.impl.clickgui.windows.tag.CustomModuleWindow -import com.lambda.gui.impl.clickgui.windows.tag.TagWindow import com.lambda.module.tag.ModuleTag import com.lambda.util.KeyCode import com.mojang.authlib.GameProfile @@ -52,9 +47,6 @@ object Lambda { val gson: Gson = GsonBuilder() .setPrettyPrinting() - .registerTypeAdapter(ModuleTag::class.java, ModuleTagSerializer) - .registerTypeAdapter(CustomModuleWindow::class.java, CustomModuleWindowSerializer) - .registerTypeAdapter(TagWindow::class.java, TagWindowSerializer) .registerTypeAdapter(KeyCode::class.java, KeyCodeSerializer) .registerTypeAdapter(Color::class.java, ColorSerializer) .registerTypeAdapter(BlockPos::class.java, BlockPosSerializer) diff --git a/common/src/main/kotlin/com/lambda/config/serializer/gui/CustomModuleWindowSerializer.kt b/common/src/main/kotlin/com/lambda/config/serializer/gui/CustomModuleWindowSerializer.kt deleted file mode 100644 index 3c514c7e2..000000000 --- a/common/src/main/kotlin/com/lambda/config/serializer/gui/CustomModuleWindowSerializer.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.config.serializer.gui - -import com.google.gson.* -import com.lambda.gui.api.component.core.DockingRect -import com.lambda.gui.impl.clickgui.LambdaClickGui -import com.lambda.gui.impl.clickgui.windows.tag.CustomModuleWindow -import com.lambda.module.ModuleRegistry -import com.lambda.newgui.component.HAlign -import com.lambda.newgui.component.VAlign -import com.lambda.util.math.Vec2d -import java.lang.reflect.Type - -object CustomModuleWindowSerializer : JsonSerializer, JsonDeserializer { - override fun serialize( - src: CustomModuleWindow?, - typeOfSrc: Type?, - context: JsonSerializationContext?, - ): JsonElement = src?.let { - JsonObject().apply { - addProperty("title", it.title) - add("modules", JsonArray().apply { - it.modules.forEach { - add(it.name) - } - }) - addProperty("width", it.width) - addProperty("height", it.height) - addProperty("isOpen", it.isOpen) - add("position", JsonArray().apply { - add(it.serializedPosition.x) - add(it.serializedPosition.y) - }) - add("docking", JsonArray().apply { - add(it.dockingH.ordinal) - add(it.dockingV.ordinal) - }) - } - } ?: JsonNull.INSTANCE - - override fun deserialize( - json: JsonElement?, - typeOfT: Type?, - context: JsonDeserializationContext?, - ) = json?.asJsonObject?.let { - CustomModuleWindow( - it["title"].asString, - it["modules"].asJsonArray.mapNotNull { name -> - ModuleRegistry.modules.firstOrNull { module -> - module.name == name.asString - } - } as MutableList, - LambdaClickGui - ).apply { - width = it["width"].asDouble - height = it["height"].asDouble - isOpen = it["isOpen"].asBoolean - serializedPosition = Vec2d( - it["position"].asJsonArray[0].asDouble, - it["position"].asJsonArray[1].asDouble - ) - dockingH = HAlign.entries[it["docking"].asJsonArray[0].asInt] - dockingV = VAlign.entries[it["docking"].asJsonArray[1].asInt] - } - } ?: throw JsonParseException("Invalid window data") -} diff --git a/common/src/main/kotlin/com/lambda/config/serializer/gui/ModuleTagSerializer.kt b/common/src/main/kotlin/com/lambda/config/serializer/gui/ModuleTagSerializer.kt deleted file mode 100644 index f7302249e..000000000 --- a/common/src/main/kotlin/com/lambda/config/serializer/gui/ModuleTagSerializer.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.config.serializer.gui - -import com.google.gson.* -import com.lambda.module.tag.ModuleTag -import java.lang.reflect.Type - -object ModuleTagSerializer : JsonSerializer, JsonDeserializer { - override fun serialize( - src: ModuleTag?, - typeOfSrc: Type?, - context: JsonSerializationContext?, - ): JsonElement = - src?.let { - JsonPrimitive(it.name) - } ?: JsonNull.INSTANCE - - override fun deserialize( - json: JsonElement?, - typeOfT: Type?, - context: JsonDeserializationContext?, - ): ModuleTag = - json?.asString?.let { ModuleTag(it) } ?: throw JsonParseException("Invalid module tag format") -} diff --git a/common/src/main/kotlin/com/lambda/config/serializer/gui/TagWindowSerializer.kt b/common/src/main/kotlin/com/lambda/config/serializer/gui/TagWindowSerializer.kt deleted file mode 100644 index e0654591a..000000000 --- a/common/src/main/kotlin/com/lambda/config/serializer/gui/TagWindowSerializer.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.config.serializer.gui - -import com.google.gson.* -import com.lambda.gui.api.component.core.DockingRect -import com.lambda.gui.impl.clickgui.LambdaClickGui -import com.lambda.gui.impl.clickgui.windows.tag.TagWindow -import com.lambda.gui.impl.hudgui.LambdaHudGui -import com.lambda.module.tag.ModuleTag -import com.lambda.newgui.component.HAlign -import com.lambda.newgui.component.VAlign -import com.lambda.util.math.Vec2d -import java.lang.reflect.Type - -object TagWindowSerializer : JsonSerializer, JsonDeserializer { - override fun serialize( - src: TagWindow?, - typeOfSrc: Type?, - context: JsonSerializationContext?, - ): JsonElement = src?.let { - JsonObject().apply { - addProperty("tag", it.tag.name) - addProperty("width", it.width) - addProperty("height", it.height) - addProperty("isOpen", it.isOpen) - add("position", JsonArray().apply { - add(it.serializedPosition.x) - add(it.serializedPosition.y) - }) - add("docking", JsonArray().apply { - add(it.dockingH.ordinal) - add(it.dockingV.ordinal) - }) - addProperty("group", if (it.isHudWindow) "hud" else "main") - } - } ?: JsonNull.INSTANCE - - override fun deserialize( - json: JsonElement?, - typeOfT: Type?, - context: JsonDeserializationContext?, - ) = json?.asJsonObject?.let { - val tag = ModuleTag(it["tag"].asString) - - val gui = when (it["group"].asString) { - "main" -> LambdaClickGui - "hud" -> LambdaHudGui - else -> return@let null - } - - TagWindow(tag, gui).apply { - width = it["width"].asDouble - height = it["height"].asDouble - isOpen = it["isOpen"].asBoolean - serializedPosition = Vec2d( - it["position"].asJsonArray[0].asDouble, - it["position"].asJsonArray[1].asDouble - ) - dockingH = HAlign.entries[it["docking"].asJsonArray[0].asInt] - dockingV = VAlign.entries[it["docking"].asJsonArray[1].asInt] - } - } ?: throw JsonParseException("Invalid window data") -} diff --git a/common/src/main/kotlin/com/lambda/gui/api/GuiEvent.kt b/common/src/main/kotlin/com/lambda/event/events/GuiEvent.kt similarity index 59% rename from common/src/main/kotlin/com/lambda/gui/api/GuiEvent.kt rename to common/src/main/kotlin/com/lambda/event/events/GuiEvent.kt index 6477a2728..adad927e1 100644 --- a/common/src/main/kotlin/com/lambda/gui/api/GuiEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/GuiEvent.kt @@ -15,31 +15,26 @@ * along with this program. If not, see . */ -package com.lambda.gui.api +package com.lambda.event.events import com.lambda.event.Event import com.lambda.util.KeyCode import com.lambda.util.Mouse import com.lambda.util.math.Vec2d -abstract class GuiEvent : Event { - class Show : GuiEvent() - class Hide : GuiEvent() - class Tick : GuiEvent() - class Render : GuiEvent() +sealed class GuiEvent : Event { + data object Show : GuiEvent() + data object Hide : GuiEvent() + data object Tick : GuiEvent() + data object Render : GuiEvent() - @Deprecated("Deprecated key press event", replaceWith = ReplaceWith("com.lambda.event.events.KeyboardEvent.Press")) class KeyPress(val key: KeyCode) : GuiEvent() - @Deprecated("Deprecated char event", replaceWith = ReplaceWith("com.lambda.event.events.KeyboardEvent.Char")) class CharTyped(val char: Char) : GuiEvent() - @Deprecated("Use the new global mouse events", replaceWith = ReplaceWith("com.lambda.event.events.MouseEvent.Click")) class MouseClick(val button: Mouse.Button, val action: Mouse.Action, val mouse: Vec2d) : GuiEvent() - @Deprecated("Use the new global mouse events", replaceWith = ReplaceWith("com.lambda.event.events.MouseEvent.Move")) class MouseMove(val mouse: Vec2d) : GuiEvent() - @Deprecated("Use the new global mouse events", replaceWith = ReplaceWith("com.lambda.event.events.MouseEvent.Scroll")) class MouseScroll(val mouse: Vec2d, val delta: Double) : GuiEvent() } diff --git a/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt b/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt index 51b721f2a..4b4fb35f5 100644 --- a/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt @@ -21,18 +21,20 @@ import com.lambda.Lambda.mc import com.lambda.event.Event import com.lambda.event.callback.Cancellable import com.lambda.event.callback.ICancellable -import com.lambda.graphics.RenderPipeline +import com.lambda.graphics.pipeline.UIPipeline +import com.lambda.graphics.renderer.esp.global.DynamicESP +import com.lambda.graphics.renderer.esp.global.StaticESP import com.lambda.util.math.Vec2d sealed class RenderEvent { class World : Event class StaticESP : Event { - val renderer = RenderPipeline.STATIC_ESP + val renderer = StaticESP } class DynamicESP : Event { - val renderer = RenderPipeline.DYNAMIC_ESP + val renderer = DynamicESP } sealed class GUI(val scale: Double) : Event { diff --git a/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt index 58e08e252..c2411cdd6 100644 --- a/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt +++ b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt @@ -20,54 +20,39 @@ package com.lambda.graphics import com.lambda.Lambda.mc import com.lambda.event.EventFlow.post import com.lambda.event.events.RenderEvent -import com.lambda.event.events.TickEvent -import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.graphics.animation.AnimationTicker import com.lambda.graphics.buffer.FrameBuffer import com.lambda.graphics.gl.GlStateUtils.setupGL import com.lambda.graphics.gl.Matrices import com.lambda.graphics.gl.Matrices.resetMatrices +import com.lambda.graphics.pipeline.UIPipeline import com.lambda.graphics.shader.Shader -import com.lambda.gui.impl.hudgui.LambdaHudGui -import com.lambda.module.modules.client.ClickGui import com.lambda.module.modules.client.GuiSettings import com.lambda.util.math.Vec2d import com.mojang.blaze3d.systems.RenderSystem.getProjectionMatrix import org.joml.Matrix4f object RenderMain { - val projectionMatrix = Matrix4f() - val modelViewMatrix: Matrix4f get() = Matrices.peek() - var screenSize = Vec2d.ZERO - - private val showHud get() = mc.currentScreen == null || LambdaHudGui.isOpen + private val projectionMatrix = Matrix4f() + private val modelViewMatrix get() = Matrices.peek() + val projModel get() = Matrix4f(projectionMatrix).mul(modelViewMatrix) - private val hudAnimation0 = with(AnimationTicker()) { - listen { - tick() - } - - exp(0.0, 1.0, { - if (showHud) ClickGui.closeSpeed else ClickGui.openSpeed - }) { showHud } - } - - private val frameBuffer = FrameBuffer() - private val shader = Shader("post/cgui_animation", "renderer/pos_tex") - private val hudAnimation by hudAnimation0 + var screenSize = Vec2d.ZERO @JvmStatic fun render2D() { resetMatrices(Matrix4f().translate(0f, 0f, -3000f)) setupGL { + UIPipeline.reset() + rescale(1.0) RenderEvent.GUI.Fixed().post() rescale(GuiSettings.scale) - drawHUD() + RenderEvent.GUI.HUD(GuiSettings.scale).post() RenderEvent.GUI.Scaled(GuiSettings.scale).post() + + UIPipeline.render() } } @@ -91,19 +76,4 @@ object RenderMain { screenSize = Vec2d(scaledWidth, scaledHeight) projectionMatrix.setOrtho(0f, scaledWidth.toFloat(), scaledHeight.toFloat(), 0f, 1000f, 21000f) } - - private fun drawHUD() { - if (hudAnimation < 0.001) return - - if (hudAnimation > 0.999) { - RenderEvent.GUI.HUD(GuiSettings.scale).post() - return - } - - frameBuffer.write { - RenderEvent.GUI.HUD(GuiSettings.scale).post() - }.read(shader) { - it["u_Progress"] = hudAnimation - } - } } diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexAttrib.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexAttrib.kt index b7963991d..d8735228c 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexAttrib.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexAttrib.kt @@ -38,15 +38,48 @@ enum class VertexAttrib( POS_UV(Vec2, Vec2), // GUI - FONT(Vec3, Vec2, Color), // pos, uv, color - RECT_FILLED(Vec2, Vec2, Vec2, Vec2, Vec2, Float, Color), // pos, uv, size, roundL, roundR, shade, color - RECT_OUTLINE(Vec2, Float, Float, Color), // pos, alpha, shade, color + FONT( + Vec3, // pos + Vec2, // uv + Vec2, Vec2, // scissor test bounds + Color + ), + + RECT_FILLED( + Vec3, // pos + Vec2, // uv + Vec2, // size + Vec2, Vec2, // roundL, roundR + Float, // shade + Vec2, Vec2, // scissor test bounds + Color + ), + + RECT_OUTLINE( + Vec3, // pos + Vec2, // uv + Float, // alpha + Float, // shade + Vec2, Vec2, // scissor test bounds + Color + ), // WORLD - DYNAMIC_RENDERER(Vec3, Vec3, Color), // prev pos, pos, color - STATIC_RENDERER(Vec3, Color), // pos, color + DYNAMIC_RENDERER( + Vec3, // prev pos + Vec3, // pos + Color + ), + + STATIC_RENDERER( + Vec3, // pos + Color + ), - PARTICLE(Vec3, Vec2, Color); // pos, uv, color + PARTICLE(Vec3, + Vec2, // pos + Color + ); val stride = attributes.sumOf { attribute -> attribute.size } } diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/GlStateUtils.kt b/common/src/main/kotlin/com/lambda/graphics/gl/GlStateUtils.kt index 1b9f15857..ddcefd46e 100644 --- a/common/src/main/kotlin/com/lambda/graphics/gl/GlStateUtils.kt +++ b/common/src/main/kotlin/com/lambda/graphics/gl/GlStateUtils.kt @@ -46,9 +46,11 @@ object GlStateUtils { cull(savedCull) } - fun withDepth(block: () -> Unit) { + fun withDepth(maskWrite: Boolean = false, block: () -> Unit) { depthTest(true) + if (maskWrite) glDepthMask(true) block() + if (maskWrite) glDepthMask(false) depthTest(false) } diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/Scissor.kt b/common/src/main/kotlin/com/lambda/graphics/gl/Scissor.kt deleted file mode 100644 index 24a7be49f..000000000 --- a/common/src/main/kotlin/com/lambda/graphics/gl/Scissor.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.gl - -import com.lambda.Lambda.mc -import com.lambda.module.modules.client.GuiSettings -import com.lambda.util.math.MathUtils.ceilToInt -import com.lambda.util.math.MathUtils.floorToInt -import com.lambda.util.math.Rect -import com.mojang.blaze3d.systems.RenderSystem -import kotlin.math.max - -object Scissor { - private var stack = ArrayDeque() - - fun scissor(rect: Rect, block: () -> Unit) { - // clamp corners so children scissor boxes can't overlap parent - val processed = stack.lastOrNull()?.let(rect::clamp) ?: rect - registerScissor(processed, block) - } - - private fun registerScissor(rect: Rect, block: () -> Unit) { - stack.add(rect) - - scissor(rect) - block() - - stack.removeLast() - scissor(stack.lastOrNull()) - } - - private fun scissor(entry: Rect?) { - if (entry == null) { - RenderSystem.disableScissor() - return - } - - val pos1 = entry.leftTop * GuiSettings.scale - val pos2 = entry.rightBottom * GuiSettings.scale - - val width = max(pos2.x - pos1.x, 1.0) - val height = max(pos2.y - pos1.y, 1.0) - - val y = mc.window.framebufferHeight - pos1.y - height - - RenderSystem.enableScissor( - pos1.x.floorToInt(), - y.floorToInt(), - width.ceilToInt(), - height.ceilToInt() - ) - } -} diff --git a/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt b/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt new file mode 100644 index 000000000..6011a0ab7 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.pipeline + +import com.lambda.graphics.renderer.gui.font.GlyphInfo +import com.lambda.util.math.Rect +import com.lambda.util.math.transform +import kotlin.random.Random + +object ScissorAdapter { + private var stack = ArrayDeque() + private val scissorInstance = ScissorRect() + + fun scissor(rect: Rect, block: () -> Unit) { + // clamp corners so children scissor boxes can't overlap parent + val processed = stack.lastOrNull()?.let(rect::clamp) ?: rect + + // push the stack + stack.add(processed) + + // do render tasks + block() + + // pop the stack + stack.removeLast() + } + + fun scissorTest(x1: Double, y1: Double, x2: Double, y2: Double, glyph: GlyphInfo? = null): ScissorRect { + reset() + + run { + val entry = stack.lastOrNull() ?: return@run + + val width = x2 - x1 + val height = y2 - y1 + + if (width <= 0 || height <= 0) { + nullify() + return@run + } + + val si = scissorInstance + + si.x1 = transform(entry.left, x1, x2, glyph?.u1 ?: 0.0, glyph?.u2 ?: 1.0) + si.y1 = transform(entry.top, y1, y2, glyph?.v1 ?: 0.0, glyph?.v2 ?: 1.0) + si.x2 = transform(entry.right, x1, x2, glyph?.u1 ?: 0.0, glyph?.u2 ?: 1.0) + si.y2 = transform(entry.bottom, y1, y2, glyph?.v1 ?: 0.0, glyph?.v2 ?: 1.0) + } + + return scissorInstance + } + + private fun reset() = scissorInstance.apply { + x1 = 0.0 + y1 = 0.0 + x2 = 1.0 + y2 = 1.0 + } + + private fun nullify() = scissorInstance.apply { + x1 = 0.0 + y1 = 0.0 + x2 = 0.0 + y2 = 0.0 + } + + class ScissorRect { + var x1 = 0.0 + var y1 = 0.0 + var x2 = 1.0 + var y2 = 1.0 + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/api/RenderLayer.kt b/common/src/main/kotlin/com/lambda/graphics/pipeline/UIPipeline.kt similarity index 61% rename from common/src/main/kotlin/com/lambda/gui/api/RenderLayer.kt rename to common/src/main/kotlin/com/lambda/graphics/pipeline/UIPipeline.kt index 254be3eeb..40c9037b8 100644 --- a/common/src/main/kotlin/com/lambda/gui/api/RenderLayer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/pipeline/UIPipeline.kt @@ -15,29 +15,29 @@ * along with this program. If not, see . */ -package com.lambda.gui.api +package com.lambda.graphics.pipeline +import com.lambda.core.Loadable +import com.lambda.graphics.gl.GlStateUtils.withDepth import com.lambda.graphics.renderer.gui.font.FontRenderer import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer -import com.lambda.threading.mainThread -class RenderLayer { - val filled by mainThread(::FilledRectRenderer) - val outline by mainThread(::OutlineRectRenderer) +object UIPipeline : Loadable { + private var uiDepth = 0 + val depth get() = uiDepth * -0.001 - // TODO: CHANGE BOTH OF THESE!!!! - // I do NOT want to see 110 vbos - val font by mainThread { FontRenderer() } - private val boldFont0 = lazy { FontRenderer() } - - val boldFont by boldFont0 + fun objectDrawn() { + uiDepth++ + } - fun render() { - filled.render() - outline.render() - font.render() + fun reset() { + uiDepth = 0 + } - if (boldFont0.isInitialized()) boldFont0.value.render() + fun render() = withDepth(true) { + FilledRectRenderer.render() + OutlineRectRenderer.render() + FontRenderer.render() } -} +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt index 6210c599d..4109ea394 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt @@ -22,7 +22,6 @@ import com.lambda.event.events.TickEvent import com.lambda.event.events.WorldEvent import com.lambda.event.listener.SafeListener.Companion.listenConcurrently import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.graphics.renderer.esp.impl.ESPRenderer import com.lambda.graphics.renderer.esp.impl.StaticESPRenderer import com.lambda.module.modules.client.RenderSettings import com.lambda.threading.awaitMainThread @@ -100,7 +99,7 @@ class ChunkedESP private constructor( } private class EspChunk(val chunk: WorldChunk, val owner: ChunkedESP) { - var renderer: ESPRenderer? = null + var renderer: StaticESPRenderer? = null private val chunkOffsets = listOf(1 to 0, 0 to 1, -1 to 0, 0 to -1) @@ -119,7 +118,7 @@ class ChunkedESP private constructor( } suspend fun rebuild() { - val newRenderer = awaitMainThread { StaticESPRenderer() } + val newRenderer = awaitMainThread { StaticESPRenderer(false) } iterateChunk { x, y, z -> owner.update(newRenderer, chunk.world, x, y, z) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/ESPRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ESPRenderer.kt similarity index 80% rename from common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/ESPRenderer.kt rename to common/src/main/kotlin/com/lambda/graphics/renderer/esp/ESPRenderer.kt index f8eba5060..d25d6bded 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/ESPRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ESPRenderer.kt @@ -15,19 +15,18 @@ * along with this program. If not, see . */ -package com.lambda.graphics.renderer.esp.impl +package com.lambda.graphics.renderer.esp -import com.lambda.Lambda.mc +import com.lambda.Lambda import com.lambda.graphics.buffer.VertexPipeline import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib import com.lambda.graphics.buffer.vertex.attributes.VertexMode -import com.lambda.graphics.gl.GlStateUtils.withFaceCulling -import com.lambda.graphics.gl.GlStateUtils.withLineWidth +import com.lambda.graphics.gl.GlStateUtils import com.lambda.graphics.shader.Shader import com.lambda.module.modules.client.RenderSettings import com.lambda.util.extension.partialTicks -abstract class ESPRenderer(tickedMode: Boolean) { +open class ESPRenderer(tickedMode: Boolean) { val shader: Shader val faces: VertexPipeline val outlines: VertexPipeline @@ -47,11 +46,11 @@ abstract class ESPRenderer(tickedMode: Boolean) { fun render() { shader.use() - shader["u_TickDelta"] = mc.partialTicks - shader["u_CameraPosition"] = mc.gameRenderer.camera.pos + shader["u_TickDelta"] = Lambda.mc.partialTicks + shader["u_CameraPosition"] = Lambda.mc.gameRenderer.camera.pos - withFaceCulling(faces::render) - withLineWidth(RenderSettings.outlineWidth, outlines::render) + GlStateUtils.withFaceCulling(faces::render) + GlStateUtils.withLineWidth(RenderSettings.outlineWidth, outlines::render) } open fun clear() { @@ -70,4 +69,4 @@ abstract class ESPRenderer(tickedMode: Boolean) { "renderer/box_dynamic" ) to VertexAttrib.Group.DYNAMIC_RENDERER } -} +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/RenderPipeline.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/DynamicESP.kt similarity index 59% rename from common/src/main/kotlin/com/lambda/graphics/RenderPipeline.kt rename to common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/DynamicESP.kt index bdf509180..ceea2084f 100644 --- a/common/src/main/kotlin/com/lambda/graphics/RenderPipeline.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/DynamicESP.kt @@ -15,39 +15,20 @@ * along with this program. If not, see . */ -package com.lambda.graphics +package com.lambda.graphics.renderer.esp.global -import com.lambda.core.Loadable import com.lambda.event.EventFlow.post import com.lambda.event.events.RenderEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.graphics.renderer.esp.impl.DynamicESPRenderer -import com.lambda.graphics.renderer.esp.impl.StaticESPRenderer - -object RenderPipeline : Loadable { - // Updates once a tick, stays fixed, uses less memory - val STATIC_ESP = StaticESPRenderer() - - // Updates once a tick, interpolates within frames - val DYNAMIC_ESP = DynamicESPRenderer() +object DynamicESP : DynamicESPRenderer() { init { - // Ticked 3d renderers update listen { - STATIC_ESP.clear() + clear() RenderEvent.StaticESP().post() - STATIC_ESP.upload() - - DYNAMIC_ESP.clear() - RenderEvent.DynamicESP().post() - DYNAMIC_ESP.upload() - } - - // 3d renderers drawcall - listen { - STATIC_ESP.render() - DYNAMIC_ESP.render() + upload() } } } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt index b3590251a..34edd1ed5 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt @@ -17,4 +17,6 @@ package com.lambda.graphics.renderer.esp.impl +import com.lambda.graphics.renderer.esp.ESPRenderer + open class DynamicESPRenderer : ESPRenderer(true) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt index 922883bbd..10fa1349b 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt @@ -18,6 +18,7 @@ package com.lambda.graphics.renderer.esp.impl import com.lambda.graphics.buffer.IRenderContext +import com.lambda.graphics.renderer.esp.ESPRenderer import java.awt.Color import java.util.concurrent.ConcurrentHashMap diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index 9721d8a51..0a7ac1c0d 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -20,6 +20,8 @@ package com.lambda.graphics.renderer.gui.font import com.lambda.graphics.buffer.VertexPipeline import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib import com.lambda.graphics.buffer.vertex.attributes.VertexMode +import com.lambda.graphics.pipeline.ScissorAdapter +import com.lambda.graphics.pipeline.UIPipeline import com.lambda.graphics.renderer.gui.font.LambdaAtlas.bind import com.lambda.graphics.renderer.gui.font.LambdaAtlas.get import com.lambda.graphics.renderer.gui.font.LambdaAtlas.height @@ -37,7 +39,7 @@ import java.awt.Color * Renders text and emoji glyphs using a shader-based font rendering system. * This class handles text and emoji rendering, shadow effects, and text scaling. */ -class FontRenderer { +object FontRenderer { private val chars = RenderSettings.textFont private val emojis = RenderSettings.emojiFont @@ -47,7 +49,7 @@ class FontRenderer { private val shadowShift get() = RenderSettings.shadowShift * 5.0 private val baselineOffset get() = RenderSettings.baselineOffset * 2.0f - 10f private val gap get() = RenderSettings.gap * 0.5f - 0.8f - private val scaleMultiplier: Double get() = ClickGui.settingsFontScale + private val scaleMultiplier: Double get() = 1.0 /** * Builds the vertex array for rendering the provided text string at a specified position. @@ -59,14 +61,16 @@ class FontRenderer { * @param shadow Whether to render a shadow for the text. * @param parseEmoji Whether to parse and render emojis in the text. */ - fun build( + fun drawString( text: String, position: Vec2d = Vec2d.ZERO, color: Color = Color.WHITE, scale: Double = 1.0, shadow: Boolean = RenderSettings.shadow, parseEmoji: Boolean = LambdaMoji.isEnabled - ) = processText(text, color, scale, shadow, parseEmoji) { char, pos1, pos2, color -> buildGlyph(char, position, pos1, pos2, color) } + ) = processText(text, color, scale, shadow, parseEmoji) { char, pos1, pos2, color -> buildGlyph(char, position, pos1, pos2, color) }.also { + UIPipeline.objectDrawn() + } /** * Renders a single glyph at a given position. @@ -85,15 +89,22 @@ class FontRenderer { color: Color = Color.WHITE, ) = pipeline.use { grow(4) + + val x1 = pos1.x + origin.x + val y1 = pos1.y + origin.y + val x2 = pos2.x + origin.x + val y2 = pos2.y + origin.y + + val scissor = ScissorAdapter.scissorTest(x1, y1, x2, y2, glyph) + putQuad( - vec3m(pos1.x + origin.x, pos1.y + origin.y, 0.0).vec2(glyph.uv1.x, glyph.uv1.y).color(color).end(), - vec3m(pos1.x + origin.x, pos2.y + origin.y, 0.0).vec2(glyph.uv1.x, glyph.uv2.y).color(color).end(), - vec3m(pos2.x + origin.x, pos2.y + origin.y, 0.0).vec2(glyph.uv2.x, glyph.uv2.y).color(color).end(), - vec3m(pos2.x + origin.x, pos1.y + origin.y, 0.0).vec2(glyph.uv2.x, glyph.uv1.y).color(color).end() + vec3m(x1, y1, UIPipeline.depth).vec2(glyph.uv1.x, glyph.uv1.y).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(color).end(), + vec3m(x1, y2, UIPipeline.depth).vec2(glyph.uv1.x, glyph.uv2.y).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(color).end(), + vec3m(x2, y2, UIPipeline.depth).vec2(glyph.uv2.x, glyph.uv2.y).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(color).end(), + vec3m(x2, y1, UIPipeline.depth).vec2(glyph.uv2.x, glyph.uv1.y).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(color).end() ) } - /** * Calculates the width of the specified text. * diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt index bb1949a4d..662179595 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt @@ -212,7 +212,7 @@ object LambdaAtlas : Loadable { val str = "Loaded ${bufferPool.size} fonts" // avoid race condition runGameScheduled { - bufferPool.forEach { (owner, image) -> owner.upload(image) } + bufferPool.forEach { (owner, image) -> owner.upload(image, 4) } bufferPool.clear() } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt index 692f365c4..deeabb35c 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt @@ -18,23 +18,28 @@ package com.lambda.graphics.renderer.gui.rect import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib +import com.lambda.graphics.pipeline.ScissorAdapter +import com.lambda.graphics.pipeline.UIPipeline import com.lambda.graphics.shader.Shader import com.lambda.util.math.MathUtils.toInt import com.lambda.util.math.Rect import java.awt.Color import kotlin.math.min -class FilledRectRenderer : AbstractRectRenderer( - VertexAttrib.Group.RECT_FILLED, shader +object FilledRectRenderer : AbstractRectRenderer( + VertexAttrib.Group.RECT_FILLED, Shader("renderer/rect_filled") ) { - fun build( + private const val MIN_SIZE = 0.5 + private const val MIN_ALPHA = 3 + + fun filledRect( rect: Rect, roundRadius: Double = 0.0, color: Color = Color.WHITE, shade: Boolean = false, - ) = build(rect, roundRadius, color, color, color, color, shade) + ) = filledRect(rect, roundRadius, color, color, color, color, shade) - fun build( + fun filledRect( rect: Rect, roundRadius: Double = 0.0, leftTop: Color = Color.WHITE, @@ -42,14 +47,14 @@ class FilledRectRenderer : AbstractRectRenderer( rightBottom: Color = Color.WHITE, leftBottom: Color = Color.WHITE, shade: Boolean = false, - ) = build( + ) = filledRect( rect, roundRadius, roundRadius, roundRadius, roundRadius, leftTop, rightTop, rightBottom, leftBottom, shade ) - fun build( + fun filledRect( rect: Rect, leftTopRadius: Double = 0.0, rightTopRadius: Double = 0.0, @@ -57,14 +62,14 @@ class FilledRectRenderer : AbstractRectRenderer( leftBottomRadius: Double = 0.0, color: Color = Color.WHITE, shade: Boolean = false, - ) = build( + ) = filledRect( rect, leftTopRadius, rightTopRadius, rightBottomRadius, leftBottomRadius, color, color, color, color, shade ) - fun build( + fun filledRect( rect: Rect, leftTopRadius: Double = 0.0, rightTopRadius: Double = 0.0, @@ -103,18 +108,15 @@ class FilledRectRenderer : AbstractRectRenderer( grow(4) + val scissor = ScissorAdapter.scissorTest(p1.x, p1.y, p2.x, p2.y) + putQuad( - vec2m(p1.x, p1.y).vec2(0.0, 0.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).color(leftTop).end(), - vec2m(p1.x, p2.y).vec2(0.0, 1.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).color(leftBottom).end(), - vec2m(p2.x, p2.y).vec2(1.0, 1.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).color(rightBottom).end(), - vec2m(p2.x, p1.y).vec2(1.0, 0.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).color(rightTop).end() + vec3m(p1.x, p1.y, UIPipeline.depth).vec2(0.0, 0.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(leftTop).end(), + vec3m(p1.x, p2.y, UIPipeline.depth).vec2(0.0, 1.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(leftBottom).end(), + vec3m(p2.x, p2.y, UIPipeline.depth).vec2(1.0, 1.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(rightBottom).end(), + vec3m(p2.x, p1.y, UIPipeline.depth).vec2(1.0, 0.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(rightTop).end() ) - } - - companion object { - private const val MIN_SIZE = 0.5 - private const val MIN_ALPHA = 3 - private val shader = Shader("renderer/rect_filled") + UIPipeline.objectDrawn() } } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt index 5f758af13..1a1a9fc80 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt @@ -19,32 +19,35 @@ package com.lambda.graphics.renderer.gui.rect import com.lambda.graphics.buffer.IRenderContext import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib +import com.lambda.graphics.pipeline.ScissorAdapter +import com.lambda.graphics.pipeline.UIPipeline import com.lambda.graphics.shader.Shader import com.lambda.util.math.lerp import com.lambda.util.math.MathUtils.toInt import com.lambda.util.math.MathUtils.toRadian import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d +import com.lambda.util.math.transform import java.awt.Color import kotlin.math.cos import kotlin.math.min import kotlin.math.sin -class OutlineRectRenderer : AbstractRectRenderer( - VertexAttrib.Group.RECT_OUTLINE, shader +object OutlineRectRenderer : AbstractRectRenderer( + VertexAttrib.Group.RECT_OUTLINE, Shader("renderer/rect_outline") ) { - private val quality = 8 - private val verticesCount = quality * 4 + private const val QUALITY = 8 + private const val VERTICES_COUNT = QUALITY * 4 - fun build( + fun outlineRect( rect: Rect, roundRadius: Double = 0.0, glowRadius: Double = 1.0, color: Color = Color.WHITE, shade: Boolean = false, - ) = build(rect, roundRadius, glowRadius, color, color, color, color, shade) + ) = outlineRect(rect, roundRadius, glowRadius, color, color, color, color, shade) - fun build( + fun outlineRect( rect: Rect, roundRadius: Double = 0.0, glowRadius: Double = 1.0, @@ -56,7 +59,9 @@ class OutlineRectRenderer : AbstractRectRenderer( ) = pipeline.use { if (glowRadius < 1) return@use - grow(verticesCount * 3) + grow(VERTICES_COUNT * 3) + + val scissor = ScissorAdapter.scissorTest(rect.left, rect.top, rect.right, rect.bottom) fun IRenderContext.genVertices(size: Double, isGlow: Boolean): MutableList { val r = rect.expand(size) @@ -66,15 +71,25 @@ class OutlineRectRenderer : AbstractRectRenderer( val maxRadius = min(halfSize.x, halfSize.y) - 0.5 val round = (roundRadius + size).coerceAtMost(maxRadius).coerceAtLeast(0.0) - fun MutableList.buildCorners(base: Vec2d, c: Color, angleRange: IntRange) = repeat(quality) { + fun MutableList.buildCorners(base: Vec2d, c: Color, angleRange: IntRange) = repeat(QUALITY) { val min = angleRange.first.toDouble() val max = angleRange.last.toDouble() - val p = it.toDouble() / quality + val p = it.toDouble() / QUALITY val angle = lerp(p, min, max).toRadian() val pos = base + Vec2d(cos(angle), -sin(angle)) * round val s = shade.toInt().toDouble() - add(vec2m(pos.x, pos.y).float(a).float(s).color(c).end()) + + val uvx = transform(pos.x, rect.left, rect.right, 0.0, 1.0) + val uvy = transform(pos.y, rect.top, rect.bottom, 0.0, 1.0) + + add(vec3m(pos.x, pos.y, UIPipeline.depth) + .vec2(uvx, uvy) + .float(a).float(s) + .vec2(scissor.x1, scissor.y1) + .vec2(scissor.x2, scissor.y2) + .color(c).end() + ) } val rt = r.rightTop + Vec2d(-round, round) @@ -94,7 +109,7 @@ class OutlineRectRenderer : AbstractRectRenderer( fun drawStripWith(vertices: MutableList) { var prev = main.last() to vertices.last() - repeat(verticesCount) { + repeat(VERTICES_COUNT) { val new = main[it] to vertices[it] putQuad(new.first, new.second, prev.second, prev.first) prev = new @@ -103,9 +118,7 @@ class OutlineRectRenderer : AbstractRectRenderer( drawStripWith(genVertices(-(glowRadius.coerceAtMost(1.0)), true)) drawStripWith(genVertices(glowRadius, true)) - } - companion object { - private val shader = Shader("renderer/rect_outline") + UIPipeline.objectDrawn() } } diff --git a/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt b/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt index 3550ed5f0..80ad62a13 100644 --- a/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt +++ b/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt @@ -21,7 +21,6 @@ import com.lambda.graphics.RenderMain import com.lambda.graphics.shader.ShaderUtils.createShaderProgram import com.lambda.graphics.shader.ShaderUtils.loadShader import com.lambda.graphics.shader.ShaderUtils.uniformMatrix -import com.lambda.util.LambdaResource import com.lambda.util.math.Vec2d import it.unimi.dsi.fastutil.objects.Object2IntMap import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap @@ -42,7 +41,7 @@ class Shader(fragmentPath: String, vertexPath: String) { fun use() { glUseProgram(id) - set("u_ProjModel", Matrix4f(RenderMain.projectionMatrix).mul(RenderMain.modelViewMatrix)) + set("u_ProjModel", RenderMain.projModel) } private fun loc(name: String) = diff --git a/common/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt b/common/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt index 5e74da477..6cfda3c32 100644 --- a/common/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt +++ b/common/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt @@ -53,10 +53,10 @@ object ShaderUtils { return shader } - fun createShaderProgram(vert: Int, frag: Int): Int { + fun createShaderProgram(vararg shaders: Int): Int { // Create new shader program val program = glCreateProgram() - val error = linkProgram(program, vert, frag) + val error = linkProgram(program, shaders) // Handle error error?.let { err -> @@ -68,8 +68,7 @@ object ShaderUtils { throw RuntimeException(builder.toString()) } - glDeleteShader(vert) - glDeleteShader(frag) + shaders.forEach(::glDeleteShader) return program } @@ -82,9 +81,11 @@ object ShaderUtils { else glGetShaderInfoLog(shader, shaderInfoLogLength) } - private fun linkProgram(program: Int, vertShader: Int, fragShader: Int): String? { - glAttachShader(program, vertShader) - glAttachShader(program, fragShader) + private fun linkProgram(program: Int, shaders: IntArray): String? { + shaders.forEach { + glAttachShader(program, it) + } + glLinkProgram(program) val status = glGetProgrami(program, GL_LINK_STATUS) diff --git a/common/src/main/kotlin/com/lambda/gui/AbstractGuiConfigurable.kt b/common/src/main/kotlin/com/lambda/gui/AbstractGuiConfigurable.kt deleted file mode 100644 index 0b4f69c0f..000000000 --- a/common/src/main/kotlin/com/lambda/gui/AbstractGuiConfigurable.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui - -import com.lambda.config.Configurable -import com.lambda.config.configurations.GuiConfig -import com.lambda.core.Loadable -import com.lambda.gui.impl.AbstractClickGui -import com.lambda.gui.impl.clickgui.windows.tag.TagWindow -import com.lambda.module.tag.ModuleTag -import com.lambda.util.math.Vec2d - -abstract class AbstractGuiConfigurable( - private val ownerGui: AbstractClickGui, - private val tags: Set, - override val name: String -) : Configurable(GuiConfig), Loadable { - var mainWindows by setting("windows", defaultWindows) - - private val defaultWindows - get() = - tags.mapIndexed { index, tag -> - TagWindow(tag, ownerGui).apply { - val step = 5.0 - position = Vec2d((width + step) * index, 0.0) + step - } - } -} diff --git a/common/src/main/kotlin/com/lambda/gui/GuiConfigurable.kt b/common/src/main/kotlin/com/lambda/gui/GuiConfigurable.kt deleted file mode 100644 index 7753183d4..000000000 --- a/common/src/main/kotlin/com/lambda/gui/GuiConfigurable.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui - -import com.lambda.gui.impl.clickgui.LambdaClickGui -import com.lambda.gui.impl.clickgui.windows.tag.CustomModuleWindow -import com.lambda.module.tag.ModuleTag - -object GuiConfigurable : AbstractGuiConfigurable( - LambdaClickGui, ModuleTag.defaults, "gui" -) { - var customWindows by setting("custom windows", listOf()) -} diff --git a/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt b/common/src/main/kotlin/com/lambda/gui/GuiManager.kt similarity index 88% rename from common/src/main/kotlin/com/lambda/newgui/GuiManager.kt rename to common/src/main/kotlin/com/lambda/gui/GuiManager.kt index cc30df35f..fc5feea18 100644 --- a/common/src/main/kotlin/com/lambda/newgui/GuiManager.kt +++ b/common/src/main/kotlin/com/lambda/gui/GuiManager.kt @@ -15,13 +15,13 @@ * along with this program. If not, see . */ -package com.lambda.newgui +package com.lambda.gui import com.lambda.config.settings.comparable.BooleanSetting import com.lambda.core.Loadable -import com.lambda.newgui.component.core.UIBuilder -import com.lambda.newgui.component.layout.Layout -import com.lambda.newgui.impl.clickgui.settings.BooleanButton.Companion.booleanSetting +import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.layout.Layout +import com.lambda.gui.impl.clickgui.settings.BooleanButton.Companion.booleanSetting import kotlin.reflect.KClass object GuiManager : Loadable { diff --git a/common/src/main/kotlin/com/lambda/gui/HudGuiConfigurable.kt b/common/src/main/kotlin/com/lambda/gui/HudGuiConfigurable.kt deleted file mode 100644 index 5db2d6725..000000000 --- a/common/src/main/kotlin/com/lambda/gui/HudGuiConfigurable.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui - -import com.lambda.gui.impl.hudgui.LambdaHudGui -import com.lambda.module.tag.ModuleTag - -object HudGuiConfigurable : AbstractGuiConfigurable( - LambdaHudGui, ModuleTag.hudDefaults, "hudgui" -) diff --git a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt b/common/src/main/kotlin/com/lambda/gui/LambdaScreen.kt similarity index 93% rename from common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt rename to common/src/main/kotlin/com/lambda/gui/LambdaScreen.kt index 1c91a2af6..9f442df39 100644 --- a/common/src/main/kotlin/com/lambda/newgui/LambdaScreen.kt +++ b/common/src/main/kotlin/com/lambda/gui/LambdaScreen.kt @@ -15,15 +15,15 @@ * along with this program. If not, see . */ -package com.lambda.newgui +package com.lambda.gui import com.lambda.Lambda.mc import com.lambda.event.Muteable import com.lambda.event.events.RenderEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.gui.api.GuiEvent -import com.lambda.newgui.component.layout.Layout +import com.lambda.event.events.GuiEvent +import com.lambda.gui.component.layout.Layout import com.lambda.util.KeyCode import com.lambda.util.Mouse import com.lambda.util.Nameable @@ -48,11 +48,11 @@ class LambdaScreen( init { listen { event -> screenSize = event.screenSize - layout.onEvent(GuiEvent.Render()) + layout.onEvent(GuiEvent.Render) } listen { - layout.onEvent(GuiEvent.Tick()) + layout.onEvent(GuiEvent.Tick) } } @@ -65,11 +65,11 @@ class LambdaScreen( } override fun onDisplayed() { - layout.onEvent(GuiEvent.Show()) + layout.onEvent(GuiEvent.Show) } override fun removed() { - layout.onEvent(GuiEvent.Hide()) + layout.onEvent(GuiEvent.Hide) } override fun shouldPause() = false diff --git a/common/src/main/kotlin/com/lambda/newgui/ScreenLayout.kt b/common/src/main/kotlin/com/lambda/gui/ScreenLayout.kt similarity index 90% rename from common/src/main/kotlin/com/lambda/newgui/ScreenLayout.kt rename to common/src/main/kotlin/com/lambda/gui/ScreenLayout.kt index 8b9cda097..2ab5d085b 100644 --- a/common/src/main/kotlin/com/lambda/newgui/ScreenLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/ScreenLayout.kt @@ -15,11 +15,11 @@ * along with this program. If not, see . */ -package com.lambda.newgui +package com.lambda.gui import com.lambda.graphics.RenderMain -import com.lambda.newgui.component.core.UIBuilder -import com.lambda.newgui.component.layout.Layout +import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.layout.Layout class ScreenLayout : Layout(owner = null, useBatching = false, batchChildren = true) { init { diff --git a/common/src/main/kotlin/com/lambda/gui/api/LambdaGui.kt b/common/src/main/kotlin/com/lambda/gui/api/LambdaGui.kt deleted file mode 100644 index e8f71a52d..000000000 --- a/common/src/main/kotlin/com/lambda/gui/api/LambdaGui.kt +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.api - -import com.lambda.Lambda.mc -import com.lambda.event.Muteable -import com.lambda.event.events.RenderEvent -import com.lambda.event.events.TickEvent -import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.graphics.animation.AnimationTicker -import com.lambda.gui.api.component.core.IComponent -import com.lambda.gui.impl.AbstractClickGui -import com.lambda.module.Module -import com.lambda.threading.runSafe -import com.lambda.util.KeyCode -import com.lambda.util.Mouse -import com.lambda.util.Nameable -import com.lambda.util.math.Rect -import com.lambda.util.math.Vec2d -import com.mojang.blaze3d.systems.RenderSystem.recordRenderCall -import net.minecraft.client.gui.DrawContext -import net.minecraft.client.gui.screen.Screen -import net.minecraft.text.Text - -abstract class LambdaGui( - override val name: String, - private val owner: Module? = null -) : Screen(Text.of(name)), IComponent, Nameable, Muteable { - var screenSize = Vec2d.ZERO - override val rect get() = Rect(Vec2d.ZERO, screenSize) - - val isOpen get() = mc.currentScreen == this - override val isMuted: Boolean get() = !isOpen - private var closingAction: (() -> Unit)? = null - - val animation = AnimationTicker() - - init { - listen { event -> - screenSize = event.screenSize - onEvent(GuiEvent.Render()) - } - - listen { - animation.tick() - onEvent(GuiEvent.Tick()) - } - } - - /** - * Shows this gui screen - * - * No safe context required (TODO: let user open clickgui via main menu) - */ - fun show() { - owner?.enable() - if (isOpen) return - - when (val screen = mc.currentScreen) { - is AbstractClickGui -> { - screen.close() - - screen.setCloseTask { - mc.setScreen(this) - } - } - - else -> { - screen?.close() - - recordRenderCall { - mc.setScreen(this) - } - } - } - } - - final override fun onDisplayed() { - onEvent(GuiEvent.Show()) - } - - override fun removed() { - onEvent(GuiEvent.Hide()) - - runSafe { - // quick crashfix (is there any other way to prevent gui being closed twice?) - mc.currentScreen = null - owner?.disable() - mc.currentScreen = this@LambdaGui - - closingAction?.let { - recordRenderCall(it) - closingAction = null - } - } - } - - fun setCloseTask(block: () -> Unit) { - closingAction = block - } - - final override fun render(context: DrawContext?, mouseX: Int, mouseY: Int, delta: Float) { - // Let's remove background tint - } - - final override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - val translated = KeyCode.virtualMapUS(keyCode, scanCode) - onEvent(GuiEvent.KeyPress(translated)) - - if (keyCode == KeyCode.ESCAPE.keyCode) { - close() - } - - return true - } - - final override fun charTyped(chr: Char, modifiers: Int): Boolean { - onEvent(GuiEvent.CharTyped(chr)) - return true - } - - final override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { - onEvent(GuiEvent.MouseClick(Mouse.Button.fromMouseCode(button), Mouse.Action.Click, rescaleMouse(mouseX, mouseY))) - return true - } - - final override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { - onEvent(GuiEvent.MouseClick(Mouse.Button.fromMouseCode(button), Mouse.Action.Release, rescaleMouse(mouseX, mouseY))) - return true - } - - final override fun mouseMoved(mouseX: Double, mouseY: Double) { - onEvent(GuiEvent.MouseMove(rescaleMouse(mouseX, mouseY))) - } - - override fun mouseScrolled( - mouseX: Double, - mouseY: Double, - horizontalAmount: Double, - verticalAmount: Double - ): Boolean { - onEvent(GuiEvent.MouseScroll(rescaleMouse(mouseX, mouseY), verticalAmount)) - return true - } - - final override fun shouldPause() = false - - private fun rescaleMouse(mouseX: Double, mouseY: Double): Vec2d { - val mcMouse = Vec2d(mouseX, mouseY) - val mcWindow = Vec2d(mc.window.scaledWidth, mc.window.scaledHeight) - - val uv = mcMouse / mcWindow - return uv * screenSize - } -} diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/InteractiveComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/InteractiveComponent.kt deleted file mode 100644 index ec4db511e..000000000 --- a/common/src/main/kotlin/com/lambda/gui/api/component/InteractiveComponent.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.api.component - -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.component.core.IComponent -import com.lambda.util.Mouse -import com.lambda.util.math.Vec2d - -abstract class InteractiveComponent : IComponent { - protected open val hovered get() = rect.contains(lastMouse) - protected var activeButton: Mouse.Button? = null - - protected open fun onPress(e: GuiEvent.MouseClick) {} - protected open fun onRelease(e: GuiEvent.MouseClick) {} - - private var lastMouse = Vec2d.ZERO - - override fun onEvent(e: GuiEvent) { - when (e) { - is GuiEvent.Show -> { - lastMouse = Vec2d.ONE * -1000.0 - activeButton = null - } - - is GuiEvent.MouseMove -> { - lastMouse = e.mouse - } - - is GuiEvent.MouseClick -> { - lastMouse = e.mouse - - val prevPressed = activeButton != null - activeButton = - if (hovered && e.button.isMainButton && e.action == Mouse.Action.Click) e.button else null - val pressed = activeButton != null - - if (prevPressed == pressed) return - if (pressed) onPress(e) - else onRelease(e) - } - } - } -} diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/ListWindow.kt b/common/src/main/kotlin/com/lambda/gui/api/component/ListWindow.kt deleted file mode 100644 index 0ab6e22c0..000000000 --- a/common/src/main/kotlin/com/lambda/gui/api/component/ListWindow.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.api.component - -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.component.button.ListButton -import com.lambda.gui.impl.AbstractClickGui -import com.lambda.module.modules.client.ClickGui - -abstract class ListWindow( - owner: AbstractClickGui, -) : WindowComponent(owner) { - private var scrollOffset: Double = 0.0 - private var rubberbandRequest = 0.0 - private var rubberbandDelta = 0.0 - - override fun onEvent(e: GuiEvent) { - when (e) { - is GuiEvent.Tick -> { - rubberbandDelta += rubberbandRequest - rubberbandRequest = 0.0 - - rubberbandDelta *= 0.5 - if (rubberbandDelta < 0.05) rubberbandDelta = 0.0 - - var y = scrollOffset + rubberbandDelta - contentComponents.children.forEach { button -> - button.heightOffset = y - y += button.size.y + button.listStep - } - } - - is GuiEvent.MouseScroll -> { - val delta = e.delta * 10.0 * ClickGui.scrollSpeed - scrollOffset += delta - - val prevOffset = scrollOffset - val range = -contentComponents.children.sumOf { it.size.y + it.listStep } - scrollOffset = scrollOffset.coerceAtLeast(range).coerceAtMost(0.0) - - rubberbandRequest += prevOffset - scrollOffset - } - } - - super.onEvent(e) - } -} diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/WindowComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/WindowComponent.kt deleted file mode 100644 index 50592ee32..000000000 --- a/common/src/main/kotlin/com/lambda/gui/api/component/WindowComponent.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.api.component - -import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.graphics.gl.Scissor.scissor -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.RenderLayer -import com.lambda.gui.api.component.core.DockingRect -import com.lambda.gui.api.component.core.list.ChildComponent -import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.gui.impl.AbstractClickGui -import com.lambda.module.modules.client.ClickGui -import com.lambda.module.modules.client.GuiSettings -import com.lambda.module.modules.client.GuiSettings.primaryColor -import com.lambda.util.Mouse -import com.lambda.util.math.MathUtils.toInt -import com.lambda.util.math.Rect -import com.lambda.util.math.Vec2d -import com.lambda.util.math.lerp -import com.lambda.util.math.multAlpha -import com.lambda.util.math.setAlpha -import java.awt.Color -import kotlin.math.abs - -abstract class WindowComponent( - val gui: AbstractClickGui, -) : ChildComponent(gui.windows) { - abstract val title: String - - abstract var width: Double - abstract var height: Double - - var isOpen = true - override val isActive get() = isOpen - - private var dragOffset: Vec2d? = null - private val padding get() = ClickGui.windowPadding - - private val rectHandler = object : DockingRect() { - override var relativePos = Vec2d.ZERO - override val width get() = this@WindowComponent.width - override val height get() = renderHeightAnimation + titleBarHeight - - override val dockingBase get() = titleBar.center - - override var allowHAlign = ClickGui.allowHAlign - override var allowVAlign = ClickGui.allowVAlign - } - - var serializedPosition by rectHandler::relativePos - var position by rectHandler::position - final override val rect by rectHandler::rect - - var dockingH by rectHandler::dockingH - var dockingV by rectHandler::dockingV - - private val contentRect get() = rect.shrink(padding).moveFirst(Vec2d(0.0, titleBarHeight - padding)) - - private val titleBar: Rect get() = Rect.basedOn(rect.leftTop, rect.size.x, titleBarHeight) - private val titleBarHeight get() = ClickGui.buttonHeight * 1.25 - - private val renderer = RenderLayer() - private val contentRenderer = RenderLayer() - - private val animation = gui.animation - - private val showAnimation by animation.exp(0.0, 1.0, 0.6, ::isOpen) - override val childShowAnimation get() = lerp(gui.childShowAnimation, 0.0, showAnimation) - - private val actualHeight get() = height + padding * 2 * isOpen.toInt() - private var renderHeightAnimation by animation.exp({ 0.0 }, ::actualHeight, 0.6, ::isOpen) - - open val contentComponents = ChildLayer.Drawable>(gui, this, contentRenderer, ::contentRect) - - override fun onEvent(e: GuiEvent) { - super.onEvent(e) - - when (e) { - is GuiEvent.Show -> { - dragOffset = null - renderHeightAnimation = if (isOpen) actualHeight else 0.0 - } - - is GuiEvent.Render -> { - updateRect() - - // TODO: fix blur - // BlurPostProcessor.render(rect, ClickGui.windowBlur, guiAnimation) - - val alpha = (gui.childShowAnimation * 2.0).coerceIn(0.0, 1.0) - - // Background - renderer.filled.build( - rect = rect, - roundRadius = ClickGui.windowRadius, - color = GuiSettings.backgroundColor.multAlpha(alpha), - shade = GuiSettings.shadeBackground - ) - - // Outline - renderer.outline.build( - rect = rect, - roundRadius = ClickGui.windowRadius, - glowRadius = ClickGui.glowRadius, - color = (if (GuiSettings.shadeBackground) Color.WHITE else primaryColor).multAlpha(alpha), - shade = GuiSettings.shadeBackground - ) - - // Title - renderer.font.build( - text = title, - position = titleBar.center - Vec2d(renderer.font.getWidth(title) * 0.5, 0.0), - color = Color.WHITE.setAlpha(gui.childShowAnimation) - ) - - renderer.render() - - scissor(contentRect) { - contentComponents.onEvent(e) - contentRenderer.render() - } - - return - } - - is GuiEvent.MouseMove -> { - val prevPos = position - - dragOffset?.let { - position = e.mouse - it - - if (prevPos != position) { - rectHandler.autoDocking() - } - } - } - - is GuiEvent.MouseClick -> { - dragOffset = null - - if (e.mouse in titleBar && e.action == Mouse.Action.Click) { - when (e.button) { - Mouse.Button.Left -> dragOffset = e.mouse - position - Mouse.Button.Right -> { - // Don't let user spam - val targetHeight = if (isOpen) actualHeight else 0.0 - if (abs(targetHeight - renderHeightAnimation) > 1) return - - isOpen = !isOpen - - if (isOpen) contentComponents.onEvent(GuiEvent.Show()) - } - - else -> {} - } - } - } - } - - contentComponents.onEvent(e) - } - - private fun updateRect() = rectHandler.apply { - screenSize = gui.screenSize - - var updateDocking = false - - if (allowHAlign != ClickGui.allowHAlign) { - allowHAlign = ClickGui.allowHAlign - updateDocking = true - } - - if (allowVAlign != ClickGui.allowVAlign) { - allowVAlign = ClickGui.allowVAlign - updateDocking = true - } - - if (updateDocking) autoDocking() - } - - fun focus() { - // move window into foreground - gui.apply { - scheduleAction { - windows.children.apply { - this@WindowComponent - .apply(::remove) - .apply(::add) - } - } - } - } -} diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/button/ButtonComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/button/ButtonComponent.kt deleted file mode 100644 index 645d4d803..000000000 --- a/common/src/main/kotlin/com/lambda/gui/api/component/button/ButtonComponent.kt +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.api.component.button - -import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.component.core.list.ChildComponent -import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.module.modules.client.ClickGui -import com.lambda.module.modules.client.GuiSettings -import com.lambda.sound.LambdaSound -import com.lambda.sound.SoundManager.playSoundRandomly -import com.lambda.util.Mouse -import com.lambda.util.math.Rect -import com.lambda.util.math.Vec2d -import com.lambda.util.math.lerp -import com.lambda.util.math.multAlpha -import java.awt.Color - -abstract class ButtonComponent( - owner: ChildLayer.Drawable<*, *>, -) : ChildComponent(owner) { - abstract val position: Vec2d - abstract val size: Vec2d - - abstract val text: String - protected open val textColor - get() = lerp(activeAnimation, Color.WHITE, GuiSettings.mainColor).multAlpha( - showAnimation - ) - protected open val centerText = false - - protected abstract var activeAnimation: Double - protected open val roundRadius get() = ClickGui.buttonRadius - - private val actualSize get() = Vec2d(if (size.x == FILL_PARENT) owner.rect.size.x else size.x, size.y) - final override val rect get() = Rect.basedOn(position, actualSize) + owner.rect.leftTop - - protected val renderer = owner.renderer - protected val animation = owner.gui.animation - - private var hoverRectAnimation by animation.exp({ 0.0 }, { 1.0 }, { if (renderHovered) 0.6 else 0.07 }, ::renderHovered) - protected var hoverFontAnimation by animation.exp(0.0, 1.0, 0.5, ::renderHovered) - protected var pressAnimation by animation.exp(0.0, 1.0, 0.5) { activeButton != null } - protected val interactAnimation get() = lerp(pressAnimation, hoverRectAnimation, 1.5) * 0.4 - override val childShowAnimation: Double get() = owner.childShowAnimation - protected open val showAnimation get() = owner.childShowAnimation - - private var lastHoveredTime = 0L - private val renderHovered get() = hovered || System.currentTimeMillis() - lastHoveredTime < 110 - - // Removes button shrinking if there's no space between buttons - protected val shrinkAnimation get() = lerp(ClickGui.buttonStep, 0.0, interactAnimation) - - open fun performClickAction(e: GuiEvent.MouseClick) {} - - override fun onEvent(e: GuiEvent) { - super.onEvent(e) - - when (e) { - is GuiEvent.Show, is GuiEvent.Hide -> reset() - - is GuiEvent.Render -> { - // Active color - renderer.filled.build( - rect = rect.shrink(shrinkAnimation), - roundRadius = roundRadius, - color = GuiSettings.mainColor.multAlpha(activeAnimation * 0.3 * showAnimation), - shade = GuiSettings.shade - ) - - // Hover glint - val hoverRect = Rect.basedOn(rect.leftTop, rect.size.x * hoverRectAnimation, rect.size.y) - renderer.filled.build( - rect = hoverRect.shrink(shrinkAnimation), - roundRadius = roundRadius, - color = GuiSettings.mainColor.multAlpha(interactAnimation * 0.2 * showAnimation), - shade = GuiSettings.shade - ) - - // Text - val textScale = 1.0 - pressAnimation * 0.08 - val textX = ClickGui.windowPadding + interactAnimation + hoverFontAnimation - val textXCentered = rect.size.x * 0.5 - renderer.font.getWidth(text, textScale) * 0.5 - renderer.font.build( - text = text, - position = Vec2d(rect.left + if (!centerText) textX else textXCentered, rect.center.y), - color = textColor, - scale = textScale - ) - } - - is GuiEvent.MouseMove -> { - val time = System.currentTimeMillis() - if (hovered) lastHoveredTime = time - } - } - } - - override fun onPress(e: GuiEvent.MouseClick) { - val pitch = if (e.button == Mouse.Button.Left) 1.0 else 0.9 - playSoundRandomly(LambdaSound.BUTTON_CLICK.event, pitch) - } - - override fun onRelease(e: GuiEvent.MouseClick) { - if (hovered) performClickAction(e) - } - - private fun reset() { - activeAnimation = 0.0 - hoverRectAnimation = 0.0 - pressAnimation = 0.0 - lastHoveredTime = 0L - } - - companion object { - const val FILL_PARENT = -1.0 - } -} diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/button/InputBarOverlay.kt b/common/src/main/kotlin/com/lambda/gui/api/component/button/InputBarOverlay.kt deleted file mode 100644 index c5c7812ec..000000000 --- a/common/src/main/kotlin/com/lambda/gui/api/component/button/InputBarOverlay.kt +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.api.component.button - -import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.RenderLayer -import com.lambda.gui.api.component.core.list.ChildComponent -import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.module.modules.client.ClickGui -import com.lambda.util.KeyCode -import com.lambda.util.math.Rect -import com.lambda.util.math.Vec2d -import com.lambda.util.math.lerp -import com.lambda.util.math.multAlpha -import com.lambda.util.math.setAlpha -import java.awt.Color -import kotlin.math.abs - -abstract class InputBarOverlay( - val renderer: RenderLayer, - owner: ChildLayer.Drawable -) : ChildComponent(owner) { - override val rect: Rect get() = owner.rect - override var isActive = false - - protected abstract val pressAnimation: Double - protected abstract val interactAnimation: Double - protected abstract val hoverFontAnimation: Double - protected abstract val showAnimation: Double - protected open val isKeyBind: Boolean = false - - val activeAnimation by owner.gui.animation.exp(0.0, 1.0, 0.7, ::isActive) - private var typeAnimation by owner.gui.animation.exp({ 0.0 }, 0.2) - - private var targetOffset = 0.0 - private var offset by owner.gui.animation.exp(::targetOffset, 0.4) - - abstract fun getText(): String - open fun setStringValue(string: String) {} - open fun setKeyValue(key: KeyCode) {} - - open fun isCharAllowed(string: String, char: Char): Boolean = true - - private var typed = "" - - override fun onEvent(e: GuiEvent) { - super.onEvent(e) - - when (e) { - is GuiEvent.Show -> { - isActive = false - } - - is GuiEvent.Render -> { - // Value text - renderer.font.apply { - val text = getText() - val scale = lerp(1.0 - activeAnimation, 0.5, 1.0) - val position = - Vec2d(rect.right, rect.center.y) - Vec2d(ClickGui.windowPadding + getWidth(text, scale), 0.0) - val color = Color.WHITE.setAlpha(lerp(showAnimation, 0.0, 1.0 - activeAnimation)) - - build(text, position, color, scale) - } - - val textStartX = rect.left + ClickGui.windowPadding + interactAnimation + hoverFontAnimation - val textColor = Color.WHITE.setAlpha(lerp(showAnimation, 0.0, activeAnimation)) - - // Typing field - renderer.font.apply { - val scale = lerp(activeAnimation, 0.5, 1.0) - pressAnimation * 0.08 - val position = Vec2d(textStartX, rect.center.y) - - targetOffset = getWidth(typed, scale) - build(typed, position, textColor, scale) - } - - // Separator - renderer.filled.apply { - val shrink = lerp(activeAnimation, rect.size.y * 0.5, 2 + abs(typeAnimation)) - - val rect = Rect( - Vec2d(0.0, rect.top + shrink), - Vec2d(1.0, rect.bottom - shrink) - ) + Vec2d(lerp(activeAnimation, rect.right, textStartX + offset + 2), 0.0) - - build(rect, color = textColor.multAlpha(0.8)) - } - } - - is GuiEvent.CharTyped -> { - if (!isActive || !isCharAllowed(typed, e.char) || isKeyBind) return - typed += e.char - typeAnimation = 1.0 - } - - is GuiEvent.KeyPress -> { - if (!isActive) return - - if (isKeyBind) { - val key = when (e.key) { - KeyCode.DELETE, KeyCode.BACKSPACE -> KeyCode.UNBOUND - KeyCode.ESCAPE -> return - else -> e.key - } - - setKeyValue(key) - toggle() - return - } - - when (e.key) { - KeyCode.ENTER -> { - setStringValue(typed) - toggle() - } - - KeyCode.BACKSPACE -> { - typed = typed.dropLast(1) - typeAnimation = -1.0 - } - - else -> {} - } - } - } - } - - fun toggle() { - isActive = !isActive - if (isActive) typed = getText().filter { isCharAllowed("", it) } - } -} diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/button/ListButton.kt b/common/src/main/kotlin/com/lambda/gui/api/component/button/ListButton.kt deleted file mode 100644 index 4880fd119..000000000 --- a/common/src/main/kotlin/com/lambda/gui/api/component/button/ListButton.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.api.component.button - -import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.module.modules.client.ClickGui -import com.lambda.util.math.Vec2d -import com.lambda.util.math.lerp - -abstract class ListButton(owner: ChildLayer.Drawable<*, *>) : ButtonComponent(owner) { - override val position get() = Vec2d(0.0, lerp(owner.childShowAnimation, 0.0, renderHeightOffset)) - override val size get() = Vec2d(FILL_PARENT, ClickGui.buttonHeight) - - open val listStep get() = ClickGui.buttonStep - - var heightOffset = 0.0 - protected var renderHeightAnimation by animation.exp(::heightOffset, 0.8) - protected open val renderHeightOffset get() = renderHeightAnimation - - override fun onEvent(e: GuiEvent) { - if (e is GuiEvent.Show) { - heightOffset = 0.0 - renderHeightAnimation = 0.0 - } - super.onEvent(e) - } -} diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/core/IComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/core/IComponent.kt deleted file mode 100644 index e4b0754e5..000000000 --- a/common/src/main/kotlin/com/lambda/gui/api/component/core/IComponent.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.api.component.core - -import com.lambda.gui.api.GuiEvent -import com.lambda.util.math.Rect - -interface IComponent { - val isActive: Boolean get() = true - val childShowAnimation: Double get() = 1.0 - val rect: Rect - - fun onEvent(e: GuiEvent) -} diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildComponent.kt deleted file mode 100644 index 5dc49b720..000000000 --- a/common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildComponent.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.api.component.core.list - -import com.lambda.gui.api.component.InteractiveComponent - -abstract class ChildComponent(open val owner: ChildLayer<*, *>) : InteractiveComponent() { - open var accessible = false - override val hovered; get() = super.hovered && accessible -} diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildLayer.kt b/common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildLayer.kt deleted file mode 100644 index 737eead08..000000000 --- a/common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildLayer.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.api.component.core.list - -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.LambdaGui -import com.lambda.gui.api.RenderLayer -import com.lambda.gui.api.component.core.IComponent -import com.lambda.util.Mouse -import com.lambda.util.math.Rect - -open class ChildLayer( - val gui: LambdaGui, - val ownerComponent: R, - private val childRect: () -> Rect, - private val childAccessible: (T) -> Boolean = { true }, -) : IComponent { - override val isActive get() = ownerComponent.isActive - override val childShowAnimation get() = ownerComponent.childShowAnimation - override val rect get() = childRect() - - val children = mutableListOf() - - override fun onEvent(e: GuiEvent) { - children.forEach { child -> - when (e) { - is GuiEvent.Tick -> { - val ownerAccessible = (ownerComponent as? ChildComponent)?.accessible ?: true - child.accessible = - childAccessible(child) && child.rect in rect && ownerAccessible && ownerComponent.isActive - } - - is GuiEvent.KeyPress, is GuiEvent.CharTyped, is GuiEvent.MouseScroll -> { - if (!child.accessible) return@forEach - } - - is GuiEvent.MouseClick -> { - val newAction = if (child.accessible) e.action else Mouse.Action.Release - val newEvent = GuiEvent.MouseClick(e.button, newAction, e.mouse) - child.onEvent(newEvent) - return@forEach - } - } - - child.onEvent(e) - } - } - - class Drawable( - gui: LambdaGui, - owner: R, - val renderer: RenderLayer, - contentRect: () -> Rect, - childAccessible: (T) -> Boolean = { true }, - ) : ChildLayer(gui, owner, contentRect, childAccessible) -} diff --git a/common/src/main/kotlin/com/lambda/newgui/component/Alignment.kt b/common/src/main/kotlin/com/lambda/gui/component/Alignment.kt similarity index 96% rename from common/src/main/kotlin/com/lambda/newgui/component/Alignment.kt rename to common/src/main/kotlin/com/lambda/gui/component/Alignment.kt index 584be44db..f2376acce 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/Alignment.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/Alignment.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.lambda.newgui.component +package com.lambda.gui.component enum class HAlign(val multiplier: Double, val offset: Double) { LEFT(0.0, -1.0), diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/core/DockingRect.kt b/common/src/main/kotlin/com/lambda/gui/component/DockingRect.kt similarity index 95% rename from common/src/main/kotlin/com/lambda/gui/api/component/core/DockingRect.kt rename to common/src/main/kotlin/com/lambda/gui/component/DockingRect.kt index 1bfee1d3e..d485f7375 100644 --- a/common/src/main/kotlin/com/lambda/gui/api/component/core/DockingRect.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/DockingRect.kt @@ -15,11 +15,9 @@ * along with this program. If not, see . */ -package com.lambda.gui.api.component.core +package com.lambda.gui.component import com.lambda.module.modules.client.ClickGui -import com.lambda.newgui.component.HAlign -import com.lambda.newgui.component.VAlign import com.lambda.util.math.MathUtils.roundToStep import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d @@ -87,4 +85,4 @@ abstract class DockingRect { } } else VAlign.TOP } -} +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt b/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt similarity index 94% rename from common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt rename to common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt index da752a316..1f5df61e6 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/FilledRect.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt @@ -15,9 +15,10 @@ * along with this program. If not, see . */ -package com.lambda.newgui.component.core +package com.lambda.gui.component.core -import com.lambda.newgui.component.layout.Layout +import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer.filledRect +import com.lambda.gui.component.layout.Layout import com.lambda.util.math.Rect import java.awt.Color @@ -56,7 +57,7 @@ class FilledRect( position = rectangle.leftTop size = rectangle.size - filled.build( + filledRect( rectangle, leftTopRadius, rightTopRadius, diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/OutlineRect.kt b/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt similarity index 91% rename from common/src/main/kotlin/com/lambda/newgui/component/core/OutlineRect.kt rename to common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt index 9e3bed197..d64272116 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/OutlineRect.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt @@ -15,9 +15,10 @@ * along with this program. If not, see . */ -package com.lambda.newgui.component.core +package com.lambda.gui.component.core -import com.lambda.newgui.component.layout.Layout +import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer.outlineRect +import com.lambda.gui.component.layout.Layout import java.awt.Color class OutlineRect( @@ -49,7 +50,7 @@ class OutlineRect( action(this@OutlineRect) } - outline.build( + outlineRect( rectangle, roundRadius, glowRadius, diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt similarity index 80% rename from common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt rename to common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt index cec2cf86a..dbbde6031 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt @@ -15,11 +15,13 @@ * along with this program. If not, see . */ -package com.lambda.newgui.component.core +package com.lambda.gui.component.core -import com.lambda.newgui.component.HAlign -import com.lambda.newgui.component.VAlign -import com.lambda.newgui.component.layout.Layout +import com.lambda.graphics.renderer.gui.font.FontRenderer +import com.lambda.graphics.renderer.gui.font.FontRenderer.drawString +import com.lambda.gui.component.HAlign +import com.lambda.gui.component.VAlign +import com.lambda.gui.component.layout.Layout import com.lambda.util.math.Vec2d import com.lambda.util.math.lerp import java.awt.Color @@ -30,18 +32,16 @@ class TextField( var text = "" var color = Color.WHITE var scale = 1.0 - var bold = false var shadow = true - val textWidth get() = fr.getWidth(text, scale) - val textHeight get() = fr.getHeight(scale) + val textWidth get() = FontRenderer.getWidth(text, scale) + val textHeight get() = FontRenderer.getHeight(scale) var textHAlignment = HAlign.LEFT var textVAlignment = VAlign.CENTER var offsetX = 0.0 var offsetY = 0.0 - private val fr get() = if (bold) renderer.boldFont else renderer.font private val updateActions = mutableListOf Unit>() fun onUpdate(block: TextField.() -> Unit) { @@ -61,7 +61,7 @@ class TextField( val ry = renderPositionY + lerp(textVAlignment.multiplier, offsetY, renderHeight - textHeight - offsetY) val renderPos = Vec2d(rx, ry + textHeight * 0.5) - fr.build(text, renderPos, color, scale, shadow) + drawString(text, renderPos, color, scale, shadow) } } diff --git a/common/src/main/kotlin/com/lambda/newgui/component/core/UIBuilder.kt b/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt similarity index 94% rename from common/src/main/kotlin/com/lambda/newgui/component/core/UIBuilder.kt rename to common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt index f9f1bdfc0..726dafca3 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/core/UIBuilder.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.lambda.newgui.component.core +package com.lambda.gui.component.core @DslMarker annotation class UIBuilder diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt similarity index 89% rename from common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt rename to common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt index e028a3306..77b02db2d 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt @@ -15,16 +15,15 @@ * along with this program. If not, see . */ -package com.lambda.newgui.component.layout +package com.lambda.gui.component.layout import com.lambda.graphics.RenderMain import com.lambda.graphics.animation.AnimationTicker -import com.lambda.graphics.gl.Scissor.scissor -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.RenderLayer -import com.lambda.newgui.component.HAlign -import com.lambda.newgui.component.VAlign -import com.lambda.newgui.component.core.UIBuilder +import com.lambda.graphics.pipeline.ScissorAdapter.scissor +import com.lambda.event.events.GuiEvent +import com.lambda.gui.component.HAlign +import com.lambda.gui.component.VAlign +import com.lambda.gui.component.core.UIBuilder import com.lambda.util.KeyCode import com.lambda.util.Mouse import com.lambda.util.math.Rect @@ -33,10 +32,6 @@ import com.lambda.util.math.Vec2d /** * Represents a component for creating complex ui structures. * - * @param useBatching Increases performance by using parent's renderer instead of creating a new one. - * - * @param batchChildren Whether allow children to use the renderer of this layout. - * * Warning: use batching if you know what you're doing. * Batched elements are always drawn first: * ```kotlin @@ -143,27 +138,13 @@ open class Layout( protected var mousePosition = Vec2d.ZERO var isHovered = false; get() = field && (owner?.isHovered ?: true) - // Graphics - val renderer: RenderLayer = run { - owner?.let { owner -> - if (!useBatching || !owner.batchChildren) { - return@let null - } - - owner.renderer - } ?: run { - owningRenderer = true - RenderLayer() - } - } - private var owningRenderer = false // Actions private var showActions = mutableListOf<() -> Unit>() private var hideActions = mutableListOf<() -> Unit>() private var tickActions = mutableListOf<() -> Unit>() - private var renderActions = mutableListOf Unit>() + private var renderActions = mutableListOf<() -> Unit>() private var keyPressActions = mutableListOf<(key: KeyCode) -> Unit>() private var charTypedActions = mutableListOf<(char: Char) -> Unit>() private var mouseClickActions = mutableListOf<(button: Mouse.Button, action: Mouse.Action) -> Unit>() @@ -202,7 +183,7 @@ open class Layout( * * @param action The action to be performed. */ - fun onRender(action: RenderLayer.() -> Unit) { + fun onRender(action: () -> Unit) { renderActions += action } @@ -366,6 +347,11 @@ open class Layout( mouseClickActions.forEach { it(e.button, action) } } is GuiEvent.Render -> { + val render = { evt: GuiEvent.Render -> + renderActions.forEach { it() } + children.forEach { it.onEvent(evt) } + } + if (properties.scissor) { scissor(rect) { render(e) } } else render(e) @@ -373,28 +359,6 @@ open class Layout( } } - protected open fun render(e: GuiEvent) { - val drawChildren = renderWidth > 0.1 && renderHeight > 0.1 - - val partition by lazy { - children.partition { !it.owningRenderer } - } - - renderActions.forEach { it(renderer) } - - if (drawChildren) { - partition.first.forEach { it.onEvent(e) } - } - - if (owningRenderer) { - renderer.render() - } - - if (drawChildren) { - partition.second.forEach { it.onEvent(e) } - } - } - companion object { /** * Creates an empty [Layout]. diff --git a/common/src/main/kotlin/com/lambda/newgui/component/layout/LayoutProperties.kt b/common/src/main/kotlin/com/lambda/gui/component/layout/LayoutProperties.kt similarity index 96% rename from common/src/main/kotlin/com/lambda/newgui/component/layout/LayoutProperties.kt rename to common/src/main/kotlin/com/lambda/gui/component/layout/LayoutProperties.kt index ff3ed3299..a8c02726c 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/layout/LayoutProperties.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/layout/LayoutProperties.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.lambda.newgui.component.layout +package com.lambda.gui.component.layout class LayoutProperties { /** diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt similarity index 80% rename from common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt rename to common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt index ee79c2e8e..c9c25170c 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/TitleBar.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt @@ -15,13 +15,13 @@ * along with this program. If not, see . */ -package com.lambda.newgui.component.window +package com.lambda.gui.component.window -import com.lambda.module.modules.client.NewCGui -import com.lambda.newgui.component.HAlign -import com.lambda.newgui.component.core.TextField.Companion.textField -import com.lambda.newgui.component.core.UIBuilder -import com.lambda.newgui.component.layout.Layout +import com.lambda.module.modules.client.ClickGui +import com.lambda.gui.component.HAlign +import com.lambda.gui.component.core.TextField.Companion.textField +import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.layout.Layout import com.lambda.util.Mouse import com.lambda.util.math.Vec2d @@ -35,13 +35,12 @@ class TitleBar( ) : Layout(owner, true, true) { val textField = textField { text = title - bold = true textHAlignment = HAlign.CENTER onUpdate { - offsetX = NewCGui.fontOffset - scale = NewCGui.fontScale + offsetX = ClickGui.fontOffset + scale = ClickGui.fontScale } } @@ -50,7 +49,7 @@ class TitleBar( init { overrideSize( owner::renderWidth, - NewCGui::titleBarHeight + ClickGui::titleBarHeight ) onShow { diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt similarity index 88% rename from common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt rename to common/src/main/kotlin/com/lambda/gui/component/window/Window.kt index c4b1de354..0b14fe3d9 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt @@ -15,17 +15,17 @@ * along with this program. If not, see . */ -package com.lambda.newgui.component.window +package com.lambda.gui.component.window import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.module.modules.client.NewCGui -import com.lambda.newgui.ScreenLayout -import com.lambda.newgui.component.core.FilledRect.Companion.rect -import com.lambda.newgui.component.core.OutlineRect.Companion.outline -import com.lambda.newgui.component.layout.Layout -import com.lambda.newgui.component.core.UIBuilder -import com.lambda.newgui.component.window.TitleBar.Companion.titleBar -import com.lambda.newgui.component.window.WindowContent.Companion.windowContent +import com.lambda.module.modules.client.ClickGui +import com.lambda.gui.ScreenLayout +import com.lambda.gui.component.core.FilledRect.Companion.rect +import com.lambda.gui.component.core.OutlineRect.Companion.outline +import com.lambda.gui.component.layout.Layout +import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.window.TitleBar.Companion.titleBar +import com.lambda.gui.component.window.WindowContent.Companion.windowContent import com.lambda.util.Mouse import com.lambda.util.math.MathUtils.toInt import com.lambda.util.math.Rect @@ -58,9 +58,9 @@ open class Window( protected val titleBarRect = rect { onUpdate { rectangle = titleBar.rect - setColor(NewCGui.titleBackgroundColor) + setColor(ClickGui.titleBackgroundColor) - val radius = NewCGui.roundRadius + val radius = ClickGui.roundRadius leftTopRadius = radius rightTopRadius = radius @@ -68,31 +68,31 @@ open class Window( leftBottomRadius = bottomRadius rightBottomRadius = bottomRadius - shade = NewCGui.backgroundShade + shade = ClickGui.backgroundShade } } protected val contentRect = rect { onUpdate { rectangle = Rect(titleBar.leftBottom, this@Window.rightBottom) - setColor(NewCGui.backgroundColor) + setColor(ClickGui.backgroundColor) - leftBottomRadius = NewCGui.roundRadius - rightBottomRadius = NewCGui.roundRadius + leftBottomRadius = ClickGui.roundRadius + rightBottomRadius = ClickGui.roundRadius - shade = NewCGui.backgroundShade + shade = ClickGui.backgroundShade } } protected val outlineRect = outline { onUpdate { rectangle = this@Window.rect - setColor(NewCGui.outlineColor) + setColor(ClickGui.outlineColor) - roundRadius = NewCGui.roundRadius - glowRadius = NewCGui.outlineWidth * NewCGui.outline.toInt().toDouble() + roundRadius = ClickGui.roundRadius + glowRadius = ClickGui.outlineWidth * ClickGui.outline.toInt().toDouble() - shade = NewCGui.outlineShade + shade = ClickGui.outlineShade } } @@ -216,7 +216,7 @@ open class Window( enum class AutoResize(private val isEnabled: () -> Boolean) { Disabled({ false }), - ByConfig({ NewCGui.autoResize }), + ByConfig({ ClickGui.autoResize }), ForceEnabled({ true }); val enabled get() = isEnabled() diff --git a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt similarity index 85% rename from common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt rename to common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt index 91f1bc46c..afdc4a874 100644 --- a/common/src/main/kotlin/com/lambda/newgui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt @@ -15,13 +15,13 @@ * along with this program. If not, see . */ -package com.lambda.newgui.component.window +package com.lambda.gui.component.window import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.gui.api.GuiEvent -import com.lambda.module.modules.client.NewCGui -import com.lambda.newgui.component.core.UIBuilder -import com.lambda.newgui.component.layout.Layout +import com.lambda.event.events.GuiEvent +import com.lambda.module.modules.client.ClickGui +import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.layout.Layout import kotlin.math.abs class WindowContent( @@ -38,9 +38,9 @@ class WindowContent( private var scrolling = false private var contentHeight = { - NewCGui.padding * 2 + + ClickGui.padding * 2 + children.sumOf(Layout::renderHeight) + - NewCGui.listStep * (children.size - 1).coerceAtLeast(0) + ClickGui.listStep * (children.size - 1).coerceAtLeast(0) } private var reorder = block@ { @@ -49,9 +49,9 @@ class WindowContent( child.overrideY { if (i == 0) { - renderPositionY + renderScrollOffset + NewCGui.padding + renderPositionY + renderScrollOffset + ClickGui.padding } else { - prev.renderPositionY + prev.renderHeight + NewCGui.listStep + prev.renderPositionY + prev.renderHeight + ClickGui.listStep } } } @@ -95,7 +95,7 @@ class WindowContent( dwheel = 0.0 val prevOffset = scrollOffset - val maxScroll = renderHeight - getContentHeight() - NewCGui.padding + val maxScroll = renderHeight - getContentHeight() - ClickGui.padding scrollOffset = scrollOffset.coerceAtLeast(maxScroll).coerceAtMost(0.0) rubberbandDelta += prevOffset - scrollOffset @@ -105,17 +105,16 @@ class WindowContent( animation.tick() } + onRender { + if (scrollable) reorder() + } + onMouseScroll { delta -> if (!scrollable) return@onMouseScroll dwheel += delta * 10.0 } } - override fun render(e: GuiEvent) { - if (scrollable) reorder() - super.render(e) - } - fun getContentHeight() = contentHeight() companion object { diff --git a/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt b/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt deleted file mode 100644 index 73e083113..000000000 --- a/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.impl - -import com.lambda.Lambda.mc -import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.graphics.buffer.FrameBuffer -import com.lambda.graphics.shader.Shader -import com.lambda.gui.AbstractGuiConfigurable -import com.lambda.gui.GuiConfigurable -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.LambdaGui -import com.lambda.gui.api.component.WindowComponent -import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.gui.impl.clickgui.buttons.SettingButton -import com.lambda.gui.impl.clickgui.windows.ModuleWindow -import com.lambda.gui.impl.clickgui.windows.tag.CustomModuleWindow -import com.lambda.gui.impl.clickgui.windows.tag.TagWindow -import com.lambda.module.Module -import com.lambda.module.modules.client.ClickGui -import com.lambda.util.Mouse -import kotlin.reflect.KMutableProperty0 - -abstract class AbstractClickGui(name: String, owner: Module? = null) : LambdaGui(name, owner) { - protected var hoveredWindow: WindowComponent<*>? = null - protected var closing = false - - final override var childShowAnimation by animation.exp( - 0.0, 1.0, - { if (closing) ClickGui.closeSpeed else ClickGui.openSpeed } - ) { !closing }; private set - - val windows = ChildLayer, AbstractClickGui>(this, this, ::rect) { child -> - child == hoveredWindow && !closing - } - - private val frameBuffer = FrameBuffer() - private val shader = Shader("post/cgui_animation", "renderer/pos_tex") - - abstract val moduleFilter: (Module) -> Boolean - abstract val configurable: AbstractGuiConfigurable - - private var lastTickedUpdate = 0L - - private val actionPool = ArrayDeque<() -> Unit>() - fun scheduleAction(block: () -> Unit) = actionPool.add(block) - - override fun onEvent(e: GuiEvent) { - while (actionPool.isNotEmpty()) actionPool.removeLast().invoke() - - when (e) { - is GuiEvent.Render -> { - if (childShowAnimation < 0.99) { - frameBuffer.write { - windows.onEvent(e) - }.read(shader) { - it["u_Progress"] = childShowAnimation - } - - return - } - } - - is GuiEvent.Show -> { - hoveredWindow = null - closing = false - childShowAnimation = 0.0 - updateWindows() - } - - is GuiEvent.Tick -> { - val time = System.currentTimeMillis() - if (time - lastTickedUpdate > 1000L) { - lastTickedUpdate = time - updateWindows() - } - - if (closing && childShowAnimation < 0.01) mc.setScreen(null) - } - - is GuiEvent.MouseClick -> { - if (e.action == Mouse.Action.Click) hoveredWindow?.focus() - } - - is GuiEvent.MouseMove -> { - hoveredWindow = windows.children.lastOrNull { child -> - e.mouse in child.rect - } - } - } - - windows.onEvent(e) - } - - fun unfocusSettings() { - windows.children.filterIsInstance().forEach { moduleWindow -> - moduleWindow.contentComponents.children.forEach { moduleButton -> - moduleButton.settingsLayer.children.forEach(SettingButton<*, *>::unfocus) - } - } - } - - private inline fun syncWindows(prop: KMutableProperty0>) = windows.apply { - var configWindows by prop - - // Add windows from config - configWindows.filter { it !in children }.forEach(children::add) - - // Remove outdated/deleted windows - children.removeIf { - it is T && it !in configWindows - } - - // Update config - configWindows = children.filterIsInstance().toMutableList() - } - - private fun updateWindows() { - syncWindows(configurable::mainWindows) - - (configurable as? GuiConfigurable)?.let { - syncWindows(it::customWindows) - } - } - - override fun close() { - if (!isOpen) return - closing = true - } -} diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/LambdaClickGui.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/LambdaClickGui.kt deleted file mode 100644 index 018b31660..000000000 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/LambdaClickGui.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.impl.clickgui - -import com.lambda.gui.GuiConfigurable -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.RenderLayer -import com.lambda.gui.api.component.button.ButtonComponent -import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.gui.impl.AbstractClickGui -import com.lambda.gui.impl.hudgui.LambdaHudGui -import com.lambda.module.HudModule -import com.lambda.module.Module -import com.lambda.module.modules.client.ClickGui -import com.lambda.module.modules.client.GuiSettings -import com.lambda.module.modules.client.GuiSettings.primaryColor -import com.lambda.util.math.Vec2d -import com.lambda.util.math.multAlpha -import java.awt.Color - -object LambdaClickGui : AbstractClickGui("ClickGui", ClickGui) { - override val moduleFilter: (Module) -> Boolean = { - it !is HudModule - } - - override val configurable get() = GuiConfigurable - - private val buttonRenderer = RenderLayer() - private val buttons = ChildLayer.Drawable(this, this, buttonRenderer, ::rect) { - hoveredWindow == null && !closing - } - - override fun onEvent(e: GuiEvent) { - buttons.onEvent(e) - if (e is GuiEvent.Render) buttonRenderer.render() - - super.onEvent(e) - } - - init { - buttons.children.add(object : ButtonComponent(buttons) { - override val position: Vec2d get() = screenSize - size - Vec2d.ONE * 5.0 - override val size = Vec2d(30.0, 15.0) - override val text = "HUD" - override val centerText = true - override val roundRadius = ClickGui.windowRadius - - override var activeAnimation; get() = pressAnimation; set(_) {} - - override fun onEvent(e: GuiEvent) { - super.onEvent(e) - - if (e is GuiEvent.Render) { - val rect = rect.shrink(interactAnimation) - - // Background - renderer.filled.build( - rect = rect, - roundRadius = ClickGui.windowRadius, - color = GuiSettings.backgroundColor.multAlpha(childShowAnimation), - shade = GuiSettings.shadeBackground - ) - - // Outline - renderer.outline.build( - rect = rect, - roundRadius = ClickGui.windowRadius, - glowRadius = ClickGui.glowRadius, - color = (if (GuiSettings.shadeBackground) Color.WHITE else primaryColor).multAlpha( - childShowAnimation - ), - shade = GuiSettings.shadeBackground - ) - } - } - - override fun performClickAction(e: GuiEvent.MouseClick) { - LambdaHudGui.show() - } - }) - } -} diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt similarity index 73% rename from common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt rename to common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt index d4094ea9e..e48ea7238 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt @@ -15,18 +15,18 @@ * along with this program. If not, see . */ -package com.lambda.newgui.impl.clickgui +package com.lambda.gui.impl.clickgui import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.module.Module -import com.lambda.module.modules.client.NewCGui -import com.lambda.newgui.GuiManager.layoutOf -import com.lambda.newgui.component.HAlign -import com.lambda.newgui.component.core.FilledRect -import com.lambda.newgui.component.core.FilledRect.Companion.rect -import com.lambda.newgui.component.core.UIBuilder -import com.lambda.newgui.component.layout.Layout -import com.lambda.newgui.component.window.Window +import com.lambda.module.modules.client.ClickGui +import com.lambda.gui.GuiManager.layoutOf +import com.lambda.gui.component.HAlign +import com.lambda.gui.component.core.FilledRect +import com.lambda.gui.component.core.FilledRect.Companion.rect +import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.layout.Layout +import com.lambda.gui.component.window.Window import com.lambda.util.Mouse import com.lambda.util.math.* import java.awt.Color @@ -55,20 +55,19 @@ class ModuleLayout( minimized = true height = 100.0 - overrideX { owner.renderPositionX + NewCGui.padding } - overrideWidth { owner.renderWidth - NewCGui.padding * 2 } + overrideX { owner.renderPositionX + ClickGui.padding } + overrideWidth { owner.renderWidth - ClickGui.padding * 2 } with(titleBar) { with(textField) { - bold = false textHAlignment = HAlign.LEFT onUpdate { - offsetX = NewCGui.fontOffset + offsetX = ClickGui.fontOffset } } - overrideHeight(NewCGui::moduleHeight) + overrideHeight(ClickGui::moduleHeight) onMouseClick { button, action -> if (button == Mouse.Button.Left && action == Mouse.Action.Click) { @@ -82,10 +81,10 @@ class ModuleLayout( .filterIsInstance>() val components = settings.sumOf { - (it.renderHeight + NewCGui.listStep) * it.visibilityAnimation - } - NewCGui.listStep + (it.renderHeight + ClickGui.listStep) * it.visibilityAnimation + } - ClickGui.listStep - val padding = NewCGui.padding * 2 + val padding = ClickGui.padding * 2 components + if (settings.isNotEmpty()) padding else 0.0 } @@ -100,10 +99,10 @@ class ModuleLayout( it.heightOffset = y } - y += (it.renderHeight + NewCGui.listStep) * it.visibilityAnimation + y += (it.renderHeight + ClickGui.listStep) * it.visibilityAnimation it.overrideY { - content.renderPositionY + content.renderScrollOffset + NewCGui.padding + it.heightOffset + content.renderPositionY + content.renderScrollOffset + ClickGui.padding + it.heightOffset } } } @@ -111,7 +110,7 @@ class ModuleLayout( rect { // Separator onUpdate { val vec = Vec2d( - lerp(openAnimation, titleBar.renderWidth * 0.5, NewCGui.fontOffset * 0.5), + lerp(openAnimation, titleBar.renderWidth * 0.5, ClickGui.fontOffset * 0.5), -0.25 ) @@ -121,17 +120,17 @@ class ModuleLayout( ) setColor(lerp(enableAnimation, Color.WHITE, Color.BLACK).setAlpha(0.2 * openAnimation)) - shade = NewCGui.outlineShade + shade = ClickGui.outlineShade } } titleBarRect.onUpdate { - setColor(lerp(enableAnimation, NewCGui.moduleDisabledColor, NewCGui.moduleEnabledColor)) + setColor(lerp(enableAnimation, ClickGui.moduleDisabledColor, ClickGui.moduleEnabledColor)) correctRadius() } contentRect.onUpdate { - setColor(lerp(enableAnimation, NewCGui.moduleDisabledColor, NewCGui.moduleEnabledColor)) + setColor(lerp(enableAnimation, ClickGui.moduleDisabledColor, ClickGui.moduleEnabledColor)) correctRadius() } @@ -152,15 +151,15 @@ class ModuleLayout( } private fun FilledRect.correctRadius() { - if (!isLast || !NewCGui.autoResize) { + if (!isLast || !ClickGui.autoResize) { setRadius(0.0) return } leftTopRadius = 0.0 rightTopRadius = 0.0 - leftBottomRadius -= NewCGui.padding - rightBottomRadius -= NewCGui.padding + leftBottomRadius -= ClickGui.padding + rightBottomRadius -= ClickGui.padding } companion object { diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleWindow.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt similarity index 86% rename from common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleWindow.kt rename to common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt index 49a64bc04..703910df7 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/ModuleWindow.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt @@ -15,13 +15,13 @@ * along with this program. If not, see . */ -package com.lambda.newgui.impl.clickgui +package com.lambda.gui.impl.clickgui import com.lambda.module.tag.ModuleTag -import com.lambda.newgui.component.core.UIBuilder -import com.lambda.newgui.component.layout.Layout -import com.lambda.newgui.component.window.Window -import com.lambda.newgui.component.window.WindowContent +import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.layout.Layout +import com.lambda.gui.component.window.Window +import com.lambda.gui.component.window.WindowContent import com.lambda.util.math.Vec2d class ModuleWindow( diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt similarity index 86% rename from common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt rename to common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt index 2ff1a9f1a..3ad032536 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/SettingLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt @@ -15,14 +15,14 @@ * along with this program. If not, see . */ -package com.lambda.newgui.impl.clickgui +package com.lambda.gui.impl.clickgui import com.lambda.config.AbstractSetting import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.module.modules.client.NewCGui -import com.lambda.newgui.component.HAlign -import com.lambda.newgui.component.layout.Layout -import com.lambda.newgui.component.window.Window +import com.lambda.module.modules.client.ClickGui +import com.lambda.gui.component.HAlign +import com.lambda.gui.component.layout.Layout +import com.lambda.gui.component.window.Window import com.lambda.util.math.Vec2d import com.lambda.util.math.setAlpha import com.lambda.util.math.transform @@ -58,7 +58,7 @@ abstract class SettingLayout > ( minimized = true overrideWidth(owner::renderWidth) - titleBar.overrideHeight(NewCGui::settingsHeight) + titleBar.overrideHeight(ClickGui::settingsHeight) overrideX { owner.renderPositionX + transform(visibilityAnimation, 0.0, 1.0, -10.0, 0.0) @@ -66,11 +66,10 @@ abstract class SettingLayout > ( with(titleBar.textField) { text = setting.name - bold = false textHAlignment = HAlign.LEFT onUpdate { - scale = NewCGui.fontScale * 0.92 + scale = ClickGui.fontScale * 0.92 color = Color.WHITE.setAlpha(visibilityAnimation) } } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt deleted file mode 100644 index 9d0cb19e7..000000000 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.impl.clickgui.buttons - -import com.lambda.config.settings.NumericSetting -import com.lambda.config.settings.StringSetting -import com.lambda.config.settings.comparable.BooleanSetting -import com.lambda.config.settings.comparable.EnumSetting -import com.lambda.config.settings.complex.KeyBindSetting -import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.graphics.gl.Scissor.scissor -import com.lambda.graphics.renderer.gui.font.FontRenderer -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.RenderLayer -import com.lambda.gui.api.component.WindowComponent -import com.lambda.gui.api.component.button.ListButton -import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.gui.impl.clickgui.buttons.setting.* -import com.lambda.module.Module -import com.lambda.module.modules.client.ClickGui -import com.lambda.module.modules.client.GuiSettings -import com.lambda.sound.LambdaSound -import com.lambda.sound.SoundManager.playSoundRandomly -import com.lambda.util.Mouse -import com.lambda.util.math.MathUtils.toInt -import com.lambda.util.math.Rect -import com.lambda.util.math.Vec2d -import com.lambda.util.math.lerp -import com.lambda.util.math.multAlpha -import com.lambda.util.math.setAlpha -import com.lambda.util.math.transform -import java.awt.Color -import kotlin.math.abs - -class ModuleButton( - val module: Module, - override val owner: ChildLayer.Drawable>, -) : ListButton(owner) { - override val text get() = module.name - private val enabled get() = module.isEnabled - - override var activeAnimation by animation.exp(0.0, 1.0, 0.15, ::enabled) - private val toggleFxDirection by animation.exp(0.0, 1.0, 0.7, ::enabled) - - override val listStep: Double get() = super.listStep + renderHeight - - private var isOpen = false - override val isActive get() = isOpen - - private val openAnimation by animation.exp(0.0, 1.0, 0.7, ::isOpen) - override val childShowAnimation get() = lerp(owner.childShowAnimation, 0.0, openAnimation) - - private var settingsHeight = 0.0 - private var renderHeight by animation.exp(::settingsHeight, 0.6) - private val settingsRect - get() = rect - .moveFirst(Vec2d(0.0, size.y + super.listStep)) - .moveSecond(Vec2d(0.0, renderHeight)) - - private val settingsRenderer = RenderLayer() - val settingsLayer = - ChildLayer.Drawable, ModuleButton>(owner.gui, this, settingsRenderer, ::settingsRect) { - it.visible && abs(settingsHeight - renderHeight) < 3 - } - - init { - // TODO: resort when all settings are implemented - module.settings.mapNotNull { - when (it) { - is BooleanSetting -> BooleanButton(it, settingsLayer) - is NumericSetting<*> -> NumberSlider(it, settingsLayer) - is StringSetting -> StringButton(it, settingsLayer) - is EnumSetting<*> -> EnumSlider(it, settingsLayer) - is KeyBindSetting -> BindButton(it, settingsLayer) - else -> null - } - }.forEach(settingsLayer.children::add) - } - - override fun onEvent(e: GuiEvent) { - when (e) { - is GuiEvent.Show -> { - isOpen = false - updateHeight() - renderHeight = settingsHeight - } - - is GuiEvent.Tick -> { - if (renderHeight < 0.5) return - updateHeight() - - var y = 0.0 - settingsLayer.children.filter(SettingButton<*, *>::visible).forEach { button -> - button.heightOffset = y - y += button.size.y + button.listStep - } - } - - is GuiEvent.Render -> { - super.onEvent(e) - - // Shadow - renderer.filled.apply { - val rect = Rect( - rect.leftTop + Vec2d(0.0, size.y), - rect.rightTop + Vec2d(0.0, size.y + 5.0) - ) - - val progress = transform(renderHeight, 0.0, 10.0, 0.0, 1.0).coerceIn(0.0, 1.0) - val topColor = Color.BLACK.setAlpha(0.2 * progress * showAnimation) - val bottomColor = Color.BLACK.setAlpha(0.0) - - build(rect, 0.0, topColor, topColor, bottomColor, bottomColor) - } - - // Bottom shadow - renderer.filled.apply { - val last = this@ModuleButton.owner.ownerComponent.contentComponents.children.lastOrNull() - val show = this@ModuleButton != last - - val rect = Rect( - settingsRect.leftBottom - Vec2d(0.0, 5.0), - settingsRect.rightBottom - ) - - val progress = transform(renderHeight, 0.0, 10.0, 0.0, 1.0).coerceIn(0.0, 1.0) * show.toInt() - val topColor = Color.BLACK.setAlpha(0.0) - val bottomColor = Color.BLACK.setAlpha(0.2 * progress * showAnimation) - - build(rect, 0.0, topColor, topColor, bottomColor, bottomColor) - } - - // Toggle fx - renderer.filled.apply { - val left = rect - Vec2d(rect.size.x, 0.0) - val right = rect + Vec2d(rect.size.x, 0.0) - - val rect = lerp(activeAnimation, left, right) - .clamp(rect) - .shrink(shrinkAnimation) - - // 0.0 .. 1.0 .. 0.0 animation - val alpha = 1.0 - (abs(activeAnimation - 0.5) * 2.0) - val color = GuiSettings.mainColor.multAlpha(alpha * 0.6 * showAnimation) - - // "Tail" effect - val leftColor = color.multAlpha(1.0 - toggleFxDirection) - val rightColor = color.multAlpha(toggleFxDirection) - - build(rect, roundRadius, leftColor, rightColor, rightColor, leftColor, GuiSettings.shade) - } - - if (renderHeight > 0.5) { - scissor(settingsRect) { - settingsLayer.onEvent(e) - settingsRenderer.render() - } - } - - return - } - } - - super.onEvent(e) - settingsLayer.onEvent(e) - } - - private fun updateHeight() { - settingsHeight = if (isOpen) { - var lastStep = 0.0 - settingsLayer.children - .filter(SettingButton<*, *>::visible) - .sumOf { lastStep = it.listStep; it.size.y + it.listStep } - lastStep + super.listStep - } else 0.0 - } - - override fun performClickAction(e: GuiEvent.MouseClick) { - when (e.button) { - Mouse.Button.Left -> { - module.toggle() - } - - Mouse.Button.Right -> { - // Don't let user spam - val targetHeight = if (isOpen) settingsHeight else 0.0 - if (abs(targetHeight - renderHeight) > 1) return - - isOpen = !isOpen - if (isOpen) settingsLayer.onEvent(GuiEvent.Show()) - updateHeight() - - val sound = if (isOpen) LambdaSound.SETTINGS_OPEN else LambdaSound.SETTINGS_CLOSE - playSoundRandomly(sound.event) - } - - else -> {} - } - } - - override fun equals(other: Any?) = - (other as? ModuleButton)?.module == module - - override fun hashCode() = - module.hashCode() -} diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/SettingButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/SettingButton.kt deleted file mode 100644 index f37978b92..000000000 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/SettingButton.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.impl.clickgui.buttons - -import com.lambda.config.AbstractSetting -import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.component.button.ListButton -import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.util.math.lerp - -abstract class SettingButton>( - val setting: T, - final override val owner: ChildLayer.Drawable, ModuleButton>, -) : ListButton(owner) { - override val text = setting.name - protected var value by setting - - val visible; get() = setting.visibility() - private var prevTickVisible = false - - private var visibilityAnimation by animation.exp(0.0, 1.0, 0.6, ::visible) - override val showAnimation get() = lerp(visibilityAnimation, 0.0, super.showAnimation) - override val renderHeightOffset get() = renderHeightAnimation + lerp(visibilityAnimation, -size.y, 0.0) - override var activeAnimation = 0.0 - - override fun onEvent(e: GuiEvent) { - super.onEvent(e) - - if (e !is GuiEvent.Tick) return - - if (!prevTickVisible && visible) renderHeightAnimation = heightOffset - prevTickVisible = visible - - if (!visible) unfocus() - } - - open fun unfocus() {} -} diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/BindButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/BindButton.kt deleted file mode 100644 index 5a50102f6..000000000 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/BindButton.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.impl.clickgui.buttons.setting - -import com.lambda.config.settings.complex.KeyBindSetting -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.component.button.InputBarOverlay -import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.gui.impl.AbstractClickGui -import com.lambda.gui.impl.clickgui.buttons.ModuleButton -import com.lambda.gui.impl.clickgui.buttons.SettingButton -import com.lambda.util.KeyCode -import com.lambda.util.extension.displayValue -import com.lambda.util.math.multAlpha - -class BindButton( - setting: KeyBindSetting, - owner: ChildLayer.Drawable, ModuleButton>, -) : SettingButton(setting, owner) { - private val layer = ChildLayer.Drawable(owner.gui, this, owner.renderer, ::rect, InputBarOverlay::isActive) - private val inputBar: InputBarOverlay = object : InputBarOverlay(renderer, layer) { - override val pressAnimation get() = this@BindButton.pressAnimation - override val interactAnimation get() = this@BindButton.interactAnimation - override val hoverFontAnimation get() = this@BindButton.hoverFontAnimation - override val showAnimation get() = this@BindButton.showAnimation - override val isKeyBind = true - - override fun getText() = value.displayValue - override fun setKeyValue(key: KeyCode) { - value = key - } - }.apply(layer.children::add) - - override val textColor get() = super.textColor.multAlpha(1.0 - inputBar.activeAnimation) - - override fun onEvent(e: GuiEvent) { - super.onEvent(e) - layer.onEvent(e) - } - - override fun unfocus() { - inputBar.isActive = false - } - - override fun performClickAction(e: GuiEvent.MouseClick) { - if (!inputBar.isActive) (owner.gui as? AbstractClickGui)?.unfocusSettings() - inputBar.toggle() - } -} diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/BooleanButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/BooleanButton.kt deleted file mode 100644 index 86d021aac..000000000 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/BooleanButton.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.impl.clickgui.buttons.setting - -import com.lambda.config.settings.comparable.BooleanSetting -import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.gui.impl.clickgui.buttons.ModuleButton -import com.lambda.gui.impl.clickgui.buttons.SettingButton -import com.lambda.module.modules.client.GuiSettings -import com.lambda.sound.LambdaSound -import com.lambda.sound.SoundManager.playSoundRandomly -import com.lambda.util.Mouse -import com.lambda.util.math.Rect -import com.lambda.util.math.Rect.Companion.inv -import com.lambda.util.math.Vec2d -import com.lambda.util.math.lerp -import com.lambda.util.math.multAlpha - -class BooleanButton( - setting: BooleanSetting, - owner: ChildLayer.Drawable, ModuleButton>, -) : SettingButton(setting, owner) { - private var active by animation.exp(0.0, 1.0, 0.6, ::value) - private val zoomAnimation get() = lerp(showAnimation, 2.0, 0.0) - - private val checkboxRect - get() = Rect(rect.rightTop - Vec2d(rect.size.y * 1.65, 0.0), rect.rightBottom) - .shrink(1.0 + zoomAnimation) - .moveFirst(Vec2d(0.0, 0.5)).moveSecond(Vec2d(0.0, -0.5)) - - private val knobStart get() = Rect.basedOn(checkboxRect.leftTop, Vec2d.ONE * checkboxRect.size.y) - private val knobEnd get() = Rect.basedOn(checkboxRect.rightBottom, Vec2d.ONE * checkboxRect.size.y * -1.0).inv() - private val checkboxKnob get() = lerp(active, knobStart, knobEnd).shrink(1.0 + zoomAnimation + interactAnimation) - - override fun onEvent(e: GuiEvent) { - super.onEvent(e) - - if (e is GuiEvent.Render) { - // Checkbox Background - renderer.filled.build( - rect = checkboxRect, - roundRadius = checkboxRect.size.y, - color = GuiSettings.mainColor.multAlpha(showAnimation * (0.2 + active * 0.2)), - shade = GuiSettings.shade - ) - - // Checkbox Knob - renderer.filled.build( - rect = checkboxKnob, - roundRadius = checkboxKnob.size.y, - color = GuiSettings.backgroundColor.multAlpha(showAnimation), - shade = GuiSettings.shadeBackground - ) - } - } - - override fun performClickAction(e: GuiEvent.MouseClick) { - if (e.button != Mouse.Button.Left) return - value = !value - - val sound = if (value) LambdaSound.BOOLEAN_SETTING_ON else LambdaSound.BOOLEAN_SETTING_OFF - val pitch = if (value) 1.0 else 0.9 - playSoundRandomly(sound.event, pitch) - } -} diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/EnumSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/EnumSlider.kt deleted file mode 100644 index dfdcbfadf..000000000 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/EnumSlider.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.impl.clickgui.buttons.setting - -import com.lambda.config.settings.comparable.EnumSetting -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.gui.impl.clickgui.buttons.ModuleButton -import com.lambda.gui.impl.clickgui.buttons.SettingButton -import com.lambda.module.modules.client.ClickGui -import com.lambda.util.extension.displayValue -import com.lambda.util.math.MathUtils.floorToInt -import com.lambda.util.math.Vec2d -import com.lambda.util.math.lerp -import com.lambda.util.math.setAlpha -import com.lambda.util.math.transform -import java.awt.Color - -class EnumSlider>( - setting: EnumSetting, - owner: ChildLayer.Drawable, ModuleButton>, -) : Slider>(setting, owner) { - private val values = setting.enumValues - private val enumSize = values.size - - override val progress get() = transform(value.ordinal.toDouble(), 0.0, enumSize - 1.0, 0.0, 1.0) - private var valueSetByDrag = false - - override fun onEvent(e: GuiEvent) { - super.onEvent(e) - - if (e is GuiEvent.Render) { - // Enum entry name - renderer.font.apply { - val text = value.displayValue - val progress = 1.0 - activeAnimation - val scale = lerp(progress, 0.5, 1.0) - val width = getWidth(text, scale) - val position = Vec2d(rect.right, rect.center.y) - Vec2d(ClickGui.windowPadding + width, 0.0) - val color = Color.WHITE.setAlpha(lerp(showAnimation, 0.0, progress)) - - build(text, position, color, scale) - } - } - } - - override fun setValueByProgress(progress: Double) { - val entryIndex = (progress * enumSize).floorToInt().coerceIn(0, enumSize - 1) - value = values[entryIndex] - valueSetByDrag = true - } - - override fun onPress(e: GuiEvent.MouseClick) { - valueSetByDrag = false - } - - override fun onRelease(e: GuiEvent.MouseClick) { - if (valueSetByDrag) return - playClickSound() - setting.next() - } -} diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/NumberSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/NumberSlider.kt deleted file mode 100644 index d349743ba..000000000 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/NumberSlider.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.impl.clickgui.buttons.setting - -import com.lambda.config.settings.NumericSetting -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.component.button.InputBarOverlay -import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.gui.impl.AbstractClickGui -import com.lambda.gui.impl.clickgui.buttons.ModuleButton -import com.lambda.gui.impl.clickgui.buttons.SettingButton -import com.lambda.util.Mouse -import com.lambda.util.math.MathUtils.roundToStep -import com.lambda.util.math.MathUtils.typeConvert -import com.lambda.util.math.lerp -import com.lambda.util.math.multAlpha -import com.lambda.util.math.normalize - -class NumberSlider( - setting: NumericSetting, - owner: ChildLayer.Drawable, ModuleButton>, -) : Slider>( - setting, owner -) where N : Number, N : Comparable { - private val doubleRange get() = setting.range.let { it.start.toDouble()..it.endInclusive.toDouble() } - override val progress get() = doubleRange.normalize(value.toDouble()) - - private val layer = ChildLayer.Drawable(owner.gui, this, owner.renderer, ::rect, InputBarOverlay::isActive) - private val inputBar: InputBarOverlay = object : InputBarOverlay(renderer, layer) { - override val pressAnimation get() = this@NumberSlider.pressAnimation - override val interactAnimation get() = this@NumberSlider.interactAnimation - override val hoverFontAnimation get() = this@NumberSlider.hoverFontAnimation - override val showAnimation get() = this@NumberSlider.showAnimation - - override fun isCharAllowed(string: String, char: Char): Boolean { - return when (char) { - '.' -> char !in string - '-' -> string.isEmpty() - else -> char.isDigit() - } - } - - override fun getText() = "$setting".replace(',', '.') // "0,0".toDouble() is null - override fun setStringValue(string: String) { - string.toDoubleOrNull()?.let(::setValue) - } - }.apply(layer.children::add) - - override val textColor get() = super.textColor.multAlpha(1.0 - inputBar.activeAnimation) - - override fun onEvent(e: GuiEvent) { - super.onEvent(e) - layer.onEvent(e) - } - - override fun unfocus() { - inputBar.isActive = false - } - - override fun performClickAction(e: GuiEvent.MouseClick) { - if (e.button != Mouse.Button.Right) return - if (!inputBar.isActive) (owner.gui as? AbstractClickGui)?.unfocusSettings() - inputBar.toggle() - } - - override fun slide() { - if (!inputBar.isActive) super.slide() - } - - override fun setValueByProgress(progress: Double) { - setValue( - lerp( - progress, - setting.range.start.toDouble(), - setting.range.endInclusive.toDouble() - ) - ) - } - - private fun setValue(valueIn: Double) { - value = value.typeConvert(valueIn.roundToStep(setting.step.toDouble())) - } -} diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/Slider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/Slider.kt deleted file mode 100644 index e5bb367c7..000000000 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/Slider.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.impl.clickgui.buttons.setting - -import com.lambda.config.AbstractSetting -import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.gui.impl.clickgui.buttons.ModuleButton -import com.lambda.gui.impl.clickgui.buttons.SettingButton -import com.lambda.module.modules.client.ClickGui -import com.lambda.module.modules.client.GuiSettings -import com.lambda.sound.LambdaSound -import com.lambda.sound.SoundManager.playSound -import com.lambda.util.Mouse -import com.lambda.util.math.Vec2d -import com.lambda.util.math.lerp -import com.lambda.util.math.multAlpha -import com.lambda.util.math.transform - -abstract class Slider>( - setting: T, owner: ChildLayer.Drawable, ModuleButton>, -) : SettingButton(setting, owner) { - protected abstract val progress: Double - - // Force this slider to follow mouse when dragging instead of rounding to the closest setting value - private val progressAnimation by animation.exp({ mouseX?.let(::getProgressByMouse) ?: progress }, 0.6) - private val renderProgress get() = lerp(showAnimation, 0.0, progressAnimation) - - protected abstract fun setValueByProgress(progress: Double) - private var lastPlayedValue = value - private var lastPlayedTiming = 0L - - private var mouseX: Double? = null - get() { - if (activeButton != Mouse.Button.Left) field = null - return field - } - - override fun onEvent(e: GuiEvent) { - super.onEvent(e) - - when (e) { - is GuiEvent.Render -> { - // Slider rect - renderer.filled.build( - rect = rect.moveSecond(Vec2d(-rect.size.x * (1.0 - renderProgress), 0.0)).shrink(shrinkAnimation), - roundRadius = ClickGui.buttonRadius, - color = GuiSettings.mainColor.multAlpha(showAnimation * 0.3), - shade = GuiSettings.shade - ) - - slide() - } - - is GuiEvent.MouseMove -> { - mouseX = e.mouse.x - } - } - } - - override fun onPress(e: GuiEvent.MouseClick) { - super.onPress(e) - mouseX = e.mouse.x - } - - protected open fun slide() = mouseX?.let { mouseX -> - setValueByProgress(getProgressByMouse(mouseX)) - playClickSound() - } - - protected fun playClickSound() { - val time = System.currentTimeMillis() - if (lastPlayedValue == value || time - lastPlayedTiming < 50) return - - lastPlayedValue = value - lastPlayedTiming = time - - playSound(LambdaSound.BUTTON_CLICK.event, lerp(progress, 0.9, 1.2)) - } - - private fun getProgressByMouse(mouseX: Double) = - transform(mouseX, rect.left, rect.right, 0.0, 1.0).coerceIn(0.0, 1.0) -} diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/StringButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/StringButton.kt deleted file mode 100644 index 91ba38c58..000000000 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/StringButton.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.impl.clickgui.buttons.setting - -import com.lambda.config.settings.StringSetting -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.component.button.InputBarOverlay -import com.lambda.gui.api.component.core.list.ChildLayer -import com.lambda.gui.impl.AbstractClickGui -import com.lambda.gui.impl.clickgui.buttons.ModuleButton -import com.lambda.gui.impl.clickgui.buttons.SettingButton -import com.lambda.util.math.multAlpha - -class StringButton( - setting: StringSetting, - owner: ChildLayer.Drawable, ModuleButton>, -) : SettingButton(setting, owner) { - private val layer = ChildLayer.Drawable(owner.gui, this, owner.renderer, ::rect, InputBarOverlay::isActive) - private val inputBar: InputBarOverlay = object : InputBarOverlay(renderer, layer) { - override val pressAnimation get() = this@StringButton.pressAnimation - override val interactAnimation get() = this@StringButton.interactAnimation - override val hoverFontAnimation get() = this@StringButton.hoverFontAnimation - override val showAnimation get() = this@StringButton.showAnimation - - override fun getText() = value - override fun setStringValue(string: String) { - value = string - } - }.apply(layer.children::add) - - override val textColor get() = super.textColor.multAlpha(1.0 - inputBar.activeAnimation) - - override fun onEvent(e: GuiEvent) { - super.onEvent(e) - layer.onEvent(e) - } - - override fun unfocus() { - inputBar.isActive = false - } - - override fun performClickAction(e: GuiEvent.MouseClick) { - if (!inputBar.isActive) (owner.gui as? AbstractClickGui)?.unfocusSettings() - inputBar.toggle() - } -} diff --git a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt similarity index 84% rename from common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt rename to common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt index d1fe16e25..9c2c5c042 100644 --- a/common/src/main/kotlin/com/lambda/newgui/impl/clickgui/settings/BooleanButton.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt @@ -15,15 +15,15 @@ * along with this program. If not, see . */ -package com.lambda.newgui.impl.clickgui.settings +package com.lambda.gui.impl.clickgui.settings import com.lambda.config.settings.comparable.BooleanSetting import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.module.modules.client.NewCGui -import com.lambda.newgui.component.core.FilledRect.Companion.rect -import com.lambda.newgui.component.core.UIBuilder -import com.lambda.newgui.component.layout.Layout -import com.lambda.newgui.impl.clickgui.SettingLayout +import com.lambda.module.modules.client.ClickGui +import com.lambda.gui.component.core.FilledRect.Companion.rect +import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.layout.Layout +import com.lambda.gui.impl.clickgui.SettingLayout import com.lambda.util.Mouse import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d @@ -47,10 +47,10 @@ class BooleanButton( val h = this@BooleanButton.renderHeight rectangle = Rect(rb - Vec2d(h * 1.65, h), rb) - .shrink(shrink) + Vec2d.LEFT * (NewCGui.fontOffset - shrink) + .shrink(shrink) + Vec2d.LEFT * (ClickGui.fontOffset - shrink) setColor(Color.BLACK.setAlpha(0.25 * visibilityAnimation)) - shade = NewCGui.backgroundShade + shade = ClickGui.backgroundShade } onTick { @@ -73,7 +73,7 @@ class BooleanButton( val knobStart = Rect.basedOn(checkBox.leftTop, Vec2d.ONE * checkBox.renderHeight) val knobEnd = Rect(checkBox.rightBottom - checkBox.renderHeight, checkBox.rightBottom) rectangle = lerp(activeAnimation, knobStart, knobEnd).shrink(1.0) - shade = NewCGui.backgroundShade + shade = ClickGui.backgroundShade setColor(Color.WHITE.setAlpha(0.25 * visibilityAnimation)) } } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/ModuleWindow.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/ModuleWindow.kt deleted file mode 100644 index 2aa69c98e..000000000 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/ModuleWindow.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.impl.clickgui.windows - -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.api.component.ListWindow -import com.lambda.gui.impl.AbstractClickGui -import com.lambda.gui.impl.clickgui.buttons.ModuleButton -import com.lambda.module.Module - -abstract class ModuleWindow( - override var title: String, - override var width: Double = 110.0, - override var height: Double = 300.0, - gui: AbstractClickGui, -) : ListWindow(gui) { - private var lastUpdate = 0L - - abstract fun getModuleList(): Collection - - private fun updateModules() { - val time = System.currentTimeMillis() - if (time - lastUpdate < 1000L) return - lastUpdate = time - - contentComponents.apply { - val modules = getModuleList().filter((gui as AbstractClickGui).moduleFilter) - - // Add missing module buttons - modules.filter { module -> - children.all { button -> - button.module != module - } - }.map { ModuleButton(it, contentComponents) } - .forEach(contentComponents.children::add) - - // Remove deleted modules - children.removeIf { - it.module !in modules - } - } - } - - override fun onEvent(e: GuiEvent) { - if (e is GuiEvent.Show || e is GuiEvent.Tick) updateModules() - - if (e is GuiEvent.Tick) { - contentComponents.children.sortBy { - it.module.name - } - } - - super.onEvent(e) - } -} diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/tag/CustomModuleWindow.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/tag/CustomModuleWindow.kt deleted file mode 100644 index d30e7de98..000000000 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/tag/CustomModuleWindow.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.impl.clickgui.windows.tag - -import com.lambda.gui.impl.AbstractClickGui -import com.lambda.gui.impl.clickgui.windows.ModuleWindow -import com.lambda.module.Module - -class CustomModuleWindow( - override var title: String = "Untitled", - val modules: MutableList = mutableListOf(), - gui: AbstractClickGui, -) : ModuleWindow(title, gui = gui) { - override fun getModuleList() = modules -} diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/tag/TagWindow.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/tag/TagWindow.kt deleted file mode 100644 index db45d678e..000000000 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/tag/TagWindow.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.impl.clickgui.windows.tag - -import com.lambda.gui.impl.AbstractClickGui -import com.lambda.gui.impl.clickgui.windows.ModuleWindow -import com.lambda.gui.impl.hudgui.LambdaHudGui -import com.lambda.module.HudModule -import com.lambda.module.Module -import com.lambda.module.ModuleRegistry -import com.lambda.module.tag.ModuleTag - -class TagWindow( - val tag: ModuleTag, - owner: AbstractClickGui, -) : ModuleWindow(tag.name, gui = owner) { - val isHudWindow = gui is LambdaHudGui - private val rawFilter = { m: Module -> m is HudModule } - private val filter get() = if (isHudWindow) rawFilter else { m: Module -> !rawFilter(m) } - - override fun getModuleList() = ModuleRegistry.modules - .filter { it.defaultTags.firstOrNull() == tag && filter(it) } -} diff --git a/common/src/main/kotlin/com/lambda/gui/impl/hudgui/LambdaHudGui.kt b/common/src/main/kotlin/com/lambda/gui/impl/hudgui/LambdaHudGui.kt deleted file mode 100644 index 4cdc026b6..000000000 --- a/common/src/main/kotlin/com/lambda/gui/impl/hudgui/LambdaHudGui.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.impl.hudgui - -import com.lambda.gui.HudGuiConfigurable -import com.lambda.gui.api.GuiEvent -import com.lambda.gui.impl.AbstractClickGui -import com.lambda.gui.impl.clickgui.LambdaClickGui -import com.lambda.module.HudModule -import com.lambda.module.Module -import com.lambda.module.ModuleRegistry -import com.lambda.util.Mouse -import com.lambda.util.math.Vec2d - -object LambdaHudGui : AbstractClickGui("HudGui") { - override val moduleFilter: (Module) -> Boolean = { - it is HudModule - } - - override val configurable get() = HudGuiConfigurable - private val hudModules get() = ModuleRegistry.modules.filterIsInstance() - - private var dragInfo: Pair? = null - - override fun onEvent(e: GuiEvent) { - super.onEvent(e) - - when (e) { - is GuiEvent.Show -> { - dragInfo = null - - setCloseTask { - LambdaClickGui.show() - } - } - - is GuiEvent.MouseMove -> { - if (closing) dragInfo = null - - dragInfo?.let { - it.second.position = e.mouse - it.first - } - } - - is GuiEvent.MouseClick -> { - dragInfo = null - - if (hoveredWindow == null && - e.action == Mouse.Action.Click && - e.button == Mouse.Button.Left - ) hudModules.filter(Module::isEnabled).firstOrNull { e.mouse in it.rect }?.let { - dragInfo = e.mouse - it.position to it - } - } - } - } -} diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/result/Drawable.kt b/common/src/main/kotlin/com/lambda/interaction/construction/result/Drawable.kt index 6742e2ee0..6a799b35e 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/result/Drawable.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/result/Drawable.kt @@ -18,10 +18,11 @@ package com.lambda.interaction.construction.result import com.lambda.context.SafeContext -import com.lambda.graphics.RenderPipeline +import com.lambda.graphics.pipeline.UIPipeline import com.lambda.graphics.renderer.esp.DirectionMask import com.lambda.graphics.renderer.esp.DirectionMask.include import com.lambda.graphics.renderer.esp.builders.buildFilled +import com.lambda.graphics.renderer.esp.global.StaticESP import com.lambda.util.BlockUtils.blockState import net.minecraft.block.BlockState import net.minecraft.util.math.BlockPos @@ -34,7 +35,7 @@ interface Drawable { fun SafeContext.buildRenderer() fun SafeContext.withBox(box: Box, color: Color, mask: Int = DirectionMask.ALL) { - RenderPipeline.STATIC_ESP.buildFilled(box, color, mask) + StaticESP.buildFilled(box, color, mask) //StaticESP.buildOutline(box, color, mask) } diff --git a/common/src/main/kotlin/com/lambda/module/HudModule.kt b/common/src/main/kotlin/com/lambda/module/HudModule.kt index 9532f52c4..820728a6b 100644 --- a/common/src/main/kotlin/com/lambda/module/HudModule.kt +++ b/common/src/main/kotlin/com/lambda/module/HudModule.kt @@ -21,11 +21,10 @@ import com.lambda.event.events.RenderEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.graphics.animation.AnimationTicker -import com.lambda.gui.api.RenderLayer -import com.lambda.gui.api.component.core.DockingRect +import com.lambda.gui.component.DockingRect import com.lambda.module.tag.ModuleTag -import com.lambda.newgui.component.HAlign -import com.lambda.newgui.component.VAlign +import com.lambda.gui.component.HAlign +import com.lambda.gui.component.VAlign import com.lambda.util.KeyCode import com.lambda.util.math.Vec2d @@ -37,8 +36,6 @@ abstract class HudModule( enabledByDefault: Boolean = false, defaultKeybind: KeyCode = KeyCode.UNBOUND, ) : Module(name, description, defaultTags, alwaysListening, enabledByDefault, defaultKeybind) { - private val renderCallables = mutableListOf Unit>() - protected abstract val width: Double protected abstract val height: Double @@ -46,7 +43,7 @@ abstract class HudModule( private var relativePosX by setting("Position X", 0.0, -10000.0..10000.0, 0.1) { false } private var relativePosY by setting("Position Y", 0.0, -10000.0..10000.0, 0.1) { false } override var relativePos - get() = Vec2d(relativePosX, relativePosY); + get() = Vec2d(relativePosX, relativePosY) set(value) { relativePosX = value.x; relativePosY = value.y } @@ -79,22 +76,10 @@ abstract class HudModule( val rect by rectHandler::rect val animation = AnimationTicker() - private val renderer = RenderLayer() - - protected fun onRender(block: RenderLayer.() -> Unit) = - renderCallables.add(block) + protected fun onRender(block: () -> Unit) = + listen { block() } init { - listen { event -> - rectHandler.screenSize = event.screenSize - - renderCallables.forEach { function -> - function.invoke(renderer) - } - - renderer.render() - } - listen { animation.tick() } diff --git a/common/src/main/kotlin/com/lambda/module/Module.kt b/common/src/main/kotlin/com/lambda/module/Module.kt index 030f9f23a..5519ec086 100644 --- a/common/src/main/kotlin/com/lambda/module/Module.kt +++ b/common/src/main/kotlin/com/lambda/module/Module.kt @@ -31,8 +31,6 @@ import com.lambda.event.listener.Listener import com.lambda.event.listener.SafeListener import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.event.listener.UnsafeListener -import com.lambda.gui.impl.clickgui.buttons.ModuleButton -import com.lambda.module.modules.client.ClickGui import com.lambda.module.tag.ModuleTag import com.lambda.sound.LambdaSound import com.lambda.sound.SoundManager.playSoundRandomly diff --git a/common/src/main/kotlin/com/lambda/module/hud/TaskFlow.kt b/common/src/main/kotlin/com/lambda/module/hud/TaskFlow.kt index 62416bf6e..efe6d590c 100644 --- a/common/src/main/kotlin/com/lambda/module/hud/TaskFlow.kt +++ b/common/src/main/kotlin/com/lambda/module/hud/TaskFlow.kt @@ -17,6 +17,7 @@ package com.lambda.module.hud +import com.lambda.graphics.renderer.gui.font.FontRenderer.drawString import com.lambda.module.HudModule import com.lambda.module.tag.ModuleTag import com.lambda.util.math.Vec2d @@ -30,7 +31,7 @@ object TaskFlow : HudModule( init { onRender { - font.build("TaskFlow", Vec2d.ZERO) + drawString("TaskFlow", Vec2d.ZERO) } } } diff --git a/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt b/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt index 57d7f36d0..5eeb05629 100644 --- a/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt +++ b/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt @@ -18,6 +18,8 @@ package com.lambda.module.hud import com.lambda.graphics.animation.Animation.Companion.exp +import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer.filledRect +import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer.outlineRect import com.lambda.module.HudModule import com.lambda.module.modules.client.ClickGui import com.lambda.module.modules.client.GuiSettings @@ -46,28 +48,30 @@ object TickShiftCharge : HudModule( init { onRender { - filled.build( + filledRect( rect = rect, - roundRadius = ClickGui.windowRadius, + roundRadius = ClickGui.roundRadius, color = GuiSettings.backgroundColor, shade = GuiSettings.shadeBackground ) val padding = 1.0 - filled.build( + filledRect( rect = Rect.basedOn(rect.leftTop, rect.size.x * renderProgress, rect.size.y).shrink(padding), - roundRadius = ClickGui.windowRadius - padding, + roundRadius = ClickGui.roundRadius - padding, color = GuiSettings.mainColor.multAlpha(0.3), shade = true ) - outline.build( - rect = rect, - roundRadius = ClickGui.windowRadius, - color = (if (GuiSettings.shadeBackground) Color.WHITE else primaryColor).multAlpha(activeAnimation), - glowRadius = ClickGui.glowRadius * activeAnimation, - shade = true - ) + if (ClickGui.outline) { + outlineRect( + rect = rect, + roundRadius = ClickGui.roundRadius, + color = (if (GuiSettings.shadeBackground) Color.WHITE else primaryColor).multAlpha(activeAnimation), + glowRadius = ClickGui.outlineWidth * activeAnimation, + shade = true + ) + } } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt index 6bfdbfa80..e15269c6e 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt @@ -17,61 +17,77 @@ package com.lambda.module.modules.client -import com.lambda.event.events.ClientEvent -import com.lambda.event.events.KeyboardEvent -import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe -import com.lambda.gui.impl.clickgui.LambdaClickGui -import com.lambda.gui.impl.hudgui.LambdaHudGui import com.lambda.module.Module +import com.lambda.module.ModuleRegistry import com.lambda.module.tag.ModuleTag -import com.lambda.util.KeyCode +import com.lambda.gui.ScreenLayout.Companion.gui +import com.lambda.gui.component.core.FilledRect.Companion.rect +import com.lambda.gui.impl.clickgui.ModuleLayout.Companion.moduleLayout +import com.lambda.gui.impl.clickgui.ModuleWindow.Companion.moduleWindow +import com.lambda.util.math.Vec2d +import com.lambda.util.math.setAlpha +import java.awt.Color object ClickGui : Module( name = "ClickGui", - description = "Sexy", - defaultTags = setOf(ModuleTag.CLIENT), - defaultKeybind = KeyCode.RIGHT_SHIFT + description = "sexy again", + defaultTags = setOf(ModuleTag.CLIENT) ) { - // General - val windowRadius by setting("Window Radius", 2.0, 0.0..10.0, 0.1) - val glowRadius by setting("Glow Radius", 2.0, 0.0..20.0, 0.1) - val buttonRadius by setting("Button Radius", 0.0, 0.0..10.0, 0.1) - val windowPadding by setting("Window Padding", 2.0, 0.0..10.0, 0.1) - val buttonHeight by setting("Button Height", 11.0, 8.0..20.0, 0.1) - val buttonStep by setting("Button Step", 0.0, 0.0..5.0, 0.1) - val settingsFontScale by setting("Settings Font Scale", 0.92, 0.5..1.0, 0.01) + val titleBarHeight by setting("Title Bar Height", 18.0, 10.0..25.0, 0.1) + val moduleHeight by setting("Module Height", 18.0, 10.0..25.0, 0.1) + val settingsHeight by setting("Settings Height", 14.0, 10.0..25.0, 0.1) + val padding by setting("Padding", 2.0, 1.0..6.0, 0.1) + val listStep by setting("List Step", 2.0, 0.0..6.0, 0.1) + val autoResize by setting("Auto Resize", false) - // Animation - val openSpeed by setting("Open Speed", 0.5, 0.1..1.0, 0.01) - val closeSpeed by setting("Close Speed", 0.5, 0.1..1.0, 0.01) - val scrollSpeed by setting("Scroll Speed", 1.0, 0.1..10.0, 0.01) + val roundRadius by setting("Round Radius", 2.0, 0.0..10.0, 0.1) - // Alignment - val allowHAlign by setting("Allow H Docking", false) - val allowVAlign by setting("Allow V Docking", true) - val dockingGridSize by setting("Docking Grid Size", 1.0, 0.0..20.0, 0.5) + val backgroundTint by setting("Background Tint", Color.BLACK.setAlpha(0.4)) - init { - onEnable { - LambdaClickGui.show() - } + val titleBackgroundColor by setting("Title Background Color", Color.WHITE.setAlpha(0.4)) + val backgroundColor by setting("Background Color", Color.WHITE.setAlpha(0.25)) + val backgroundShade by setting("Background Shade", true) - onDisable { - LambdaClickGui.close() - LambdaHudGui.close() + val outline by setting("Outline", true) + val outlineWidth by setting("Outline Width", 10.0, 1.0..10.0, 0.1) { outline } + val outlineColor by setting("Outline Color", Color.WHITE.setAlpha(0.6)) { outline } + val outlineShade by setting("Outline Shade", true) { outline } + val fontScale by setting("Font Scale", 1.0, 0.5..2.0, 0.1) + val fontOffset by setting("Font Offset", 2.0, 0.0..5.0, 0.1) + val dockingGridSize by setting("Docking Grid Size", 1.0, 0.1..10.0, 0.1) + + val moduleEnabledColor by setting("Module Enabled Color", Color.WHITE.setAlpha(0.25)) + val moduleDisabledColor by setting("Module Disabled Color", Color.WHITE.setAlpha(0.05)) + + val SCREEN get() = gui("Click Gui") { + rect { + onUpdate { + rectangle = owner!!.rect + setColor(backgroundTint) + } } - listen(priority = Int.MAX_VALUE) { event -> - if (mc.options.commandKey.isPressed) return@listen - if (keybind == KeyCode.UNBOUND) return@listen - if (event.translated != keybind) return@listen - // ToDo: Exception for ui text input - toggle() + val tags = ModuleTag.defaults + val modules = ModuleRegistry.modules + + var x = 20.0 + val y = x + + tags.forEachIndexed { i, tag -> + x += moduleWindow(tag, Vec2d(x, y)) { + modules.filter { + it.defaultTags.firstOrNull() == tag + }.forEach { module -> + moduleLayout(module) + } + }.width + 3 } + } - listenUnsafe { - disable() + init { + onEnable { + SCREEN.show() + toggle() } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt index a45da7d41..0f000ae86 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt @@ -22,7 +22,6 @@ import com.lambda.event.events.TickEvent import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.graphics.animation.AnimationTicker -import com.lambda.gui.impl.clickgui.LambdaClickGui import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import java.awt.Color @@ -63,7 +62,7 @@ object GuiSettings : Module( private var targetScale = 2.0 get() { - val update = System.currentTimeMillis() - lastChange > 200 || !LambdaClickGui.isOpen + val update = System.currentTimeMillis() - lastChange > 200 || !ClickGui.SCREEN.isOpen if (update) field = scaleSetting / 100.0 * 2.0 return field } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt b/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt index cd7dfdd83..cb77b5752 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt @@ -19,7 +19,7 @@ package com.lambda.module.modules.client import com.lambda.event.events.RenderEvent import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.gui.api.RenderLayer +import com.lambda.graphics.renderer.gui.font.FontRenderer.drawString import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.util.math.Vec2d @@ -34,16 +34,14 @@ object LambdaMoji : Module( val scale by setting("Emoji Scale", 1.0, 0.5..1.5, 0.1) val suggestions by setting("Chat Suggestions", true) - private val renderer = RenderLayer() private val renderQueue = mutableListOf>() init { listen { renderQueue.forEach { (text, position, color) -> - renderer.font.build(text, position, color, scale = scale) + drawString(text, position, color, scale = scale) } - renderer.render() renderQueue.clear() } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt deleted file mode 100644 index fd49fbf4b..000000000 --- a/common/src/main/kotlin/com/lambda/module/modules/client/NewCGui.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.module.modules.client - -import com.lambda.module.Module -import com.lambda.module.ModuleRegistry -import com.lambda.module.tag.ModuleTag -import com.lambda.newgui.ScreenLayout.Companion.gui -import com.lambda.newgui.component.core.FilledRect.Companion.rect -import com.lambda.newgui.component.layout.Layout.Companion.layout -import com.lambda.newgui.impl.clickgui.ModuleLayout.Companion.moduleLayout -import com.lambda.newgui.impl.clickgui.ModuleWindow.Companion.moduleWindow -import com.lambda.util.math.Vec2d -import com.lambda.util.math.setAlpha -import java.awt.Color - -object NewCGui : Module( - name = "NewCGui", - description = "ggs", - defaultTags = setOf(ModuleTag.CLIENT) -) { - val titleBarHeight by setting("Title Bar Height", 18.0, 10.0..25.0, 0.1) - val moduleHeight by setting("Module Height", 18.0, 10.0..25.0, 0.1) - val settingsHeight by setting("Settings Height", 14.0, 10.0..25.0, 0.1) - val padding by setting("Padding", 2.0, 1.0..6.0, 0.1) - val listStep by setting("List Step", 2.0, 0.0..6.0, 0.1) - val autoResize by setting("Auto Resize", false) - - val roundRadius by setting("Round Radius", 2.0, 0.0..10.0, 0.1) - - val backgroundTint by setting("Background Tint", Color.BLACK.setAlpha(0.4)) - - val titleBackgroundColor by setting("Title Background Color", Color.WHITE.setAlpha(0.4)) - val backgroundColor by setting("Background Color", Color.WHITE.setAlpha(0.25)) - val backgroundShade by setting("Background Shade", true) - - val outline by setting("Outline", true) - val outlineWidth by setting("Outline Width", 10.0, 1.0..10.0, 0.1) { outline } - val outlineColor by setting("Outline Color", Color.WHITE.setAlpha(0.6)) { outline } - val outlineShade by setting("Outline Shade", true) { outline } - val fontScale by setting("Font Scale", 1.0, 0.5..2.0, 0.1) - val fontOffset by setting("Font Offset", 2.0, 0.0..5.0, 0.1) - - val moduleEnabledColor by setting("Module Enabled Color", Color.WHITE.setAlpha(0.25)) - val moduleDisabledColor by setting("Module Disabled Color", Color.WHITE.setAlpha(0.05)) - - private val SCREEN get() = gui("New Click Gui") { - rect { - onUpdate { - rectangle = owner!!.rect - setColor(backgroundTint) - } - } - - val tags = ModuleTag.defaults - val modules = ModuleRegistry.modules - - var x = 20.0 - val y = x - - tags.forEachIndexed { i, tag -> - x += moduleWindow(tag, Vec2d(x, y)) { - modules.filter { - it.defaultTags.firstOrNull() == tag - }.forEach { module -> - moduleLayout(module) - } - }.width + 3 - } - } - - init { - onEnable { - SCREEN.show() - toggle() - } - } -} diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt b/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt index 154dfedad..8b3b66138 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt @@ -20,10 +20,11 @@ package com.lambda.module.modules.player import com.lambda.context.SafeContext import com.lambda.event.events.* import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.graphics.RenderPipeline +import com.lambda.graphics.pipeline.UIPipeline import com.lambda.graphics.renderer.esp.DynamicAABB import com.lambda.graphics.renderer.esp.builders.buildFilled import com.lambda.graphics.renderer.esp.builders.buildOutline +import com.lambda.graphics.renderer.esp.global.DynamicESP import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.util.math.lerp @@ -60,7 +61,7 @@ object FastBreak : Module( private val outlineWidth by setting("Outline Width", 1f, 0f..3f, 0.1f, "the thickness of the outline", visibility = { page == Page.Render && renderMode.isEnabled() && renderSetting != RenderSetting.Fill }) - private val renderer = RenderPipeline.DYNAMIC_ESP + private val renderer = DynamicESP private var boxSet = emptySet() private enum class Page { diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt b/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt index d949ba614..d8f7bb0d7 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt @@ -21,10 +21,11 @@ import com.lambda.Lambda.mc import com.lambda.context.SafeContext import com.lambda.event.events.* import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.graphics.RenderPipeline +import com.lambda.graphics.pipeline.UIPipeline import com.lambda.graphics.renderer.esp.DynamicAABB import com.lambda.graphics.renderer.esp.builders.buildFilled import com.lambda.graphics.renderer.esp.builders.buildOutline +import com.lambda.graphics.renderer.esp.global.DynamicESP import com.lambda.interaction.RotationManager import com.lambda.interaction.rotation.RotationContext import com.lambda.interaction.visibilty.VisibilityChecker.findRotation @@ -246,7 +247,7 @@ object PacketMine : Module( this == Primary } - val renderer = RenderPipeline.DYNAMIC_ESP + val renderer = DynamicESP private var currentMiningBlock = Array(2) { null } private var lastNonEmptyState: BlockState? = null private val blockQueue = ArrayDeque() diff --git a/common/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt b/common/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt index 0756948d6..ddbcb1ecc 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt @@ -21,6 +21,7 @@ import com.lambda.Lambda.mc import com.lambda.graphics.renderer.esp.ChunkedESP.Companion.newChunkedESP import com.lambda.graphics.renderer.esp.DirectionMask import com.lambda.graphics.renderer.esp.DirectionMask.buildSideMesh +import com.lambda.graphics.renderer.esp.ESPRenderer import com.lambda.graphics.renderer.esp.builders.buildFilled import com.lambda.graphics.renderer.esp.builders.buildOutline import com.lambda.graphics.renderer.esp.impl.StaticESPRenderer diff --git a/common/src/main/kotlin/com/lambda/module/modules/render/Particles.kt b/common/src/main/kotlin/com/lambda/module/modules/render/Particles.kt index de634fec1..5f45bc1e9 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/render/Particles.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/render/Particles.kt @@ -97,7 +97,7 @@ object Particles : Module( shader["u_CameraPosition"] = mc.gameRenderer.camera.pos pipeline.upload() - withDepth(pipeline::render) + withDepth(false, pipeline::render) pipeline.clear() } } diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/font.frag b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/font.frag index 25cd3d0f7..c5670576c 100644 --- a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/font.frag +++ b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/font.frag @@ -4,18 +4,20 @@ uniform sampler2D u_FontTexture; uniform sampler2D u_EmojiTexture; in vec2 v_TexCoord; +in vec2 v_Scissor1; +in vec2 v_Scissor2; + in vec4 v_Color; out vec4 color; -void main() { - vec4 tex; +bool scissorFailed(vec2 coord) { + return coord.x < v_Scissor1.x || coord.x > v_Scissor2.x || coord.y < v_Scissor1.y || coord.y > v_Scissor2.y; +} - if (v_TexCoord.x > 0.0) { - tex = texture(u_FontTexture, v_TexCoord); - } else { - tex = texture(u_EmojiTexture, -v_TexCoord); - } +void main() { + vec2 coord = v_TexCoord.x > 0.0 ? v_TexCoord : -v_TexCoord; + if (scissorFailed(coord)) discard; - color = tex * v_Color; + color = texture(v_TexCoord.x > 0.0 ? u_FontTexture : u_EmojiTexture, coord) * v_Color; } diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag index 9e0750d46..4e8656d47 100644 --- a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag +++ b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag @@ -7,6 +7,8 @@ uniform vec2 u_Size; in vec2 v_Position; in vec2 v_TexCoord; +in vec2 v_Scissor1; +in vec2 v_Scissor2; in vec4 v_Color; in vec2 v_Size; in vec2 v_RoundRadiusL; @@ -76,6 +78,11 @@ vec4 round() { return vec4(1.0, 1.0, 1.0, clamp(alpha, 0.0, 1.0)); } +bool scissorFailed(vec2 coord) { + return coord.x < v_Scissor1.x || coord.x > v_Scissor2.x || coord.y < v_Scissor1.y || coord.y > v_Scissor2.y; +} + void main() { + if (scissorFailed(v_TexCoord)) discard; color = shade() * round() + noise(); } \ No newline at end of file diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_outline.frag b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_outline.frag index de9501215..529035d28 100644 --- a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_outline.frag +++ b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_outline.frag @@ -6,6 +6,9 @@ uniform vec4 u_Color2; uniform vec2 u_Size; in vec2 v_Position; +in vec2 v_TexCoord; +in vec2 v_Scissor1; +in vec2 v_Scissor2; in float v_Alpha; in vec4 v_Color; in float v_Shade; @@ -26,6 +29,11 @@ vec4 glow() { return vec4(1.0, 1.0, 1.0, newAlpha); } +bool scissorFailed(vec2 coord) { + return coord.x < v_Scissor1.x || coord.x > v_Scissor2.x || coord.y < v_Scissor1.y || coord.y > v_Scissor2.y; +} + void main() { + if (scissorFailed(v_TexCoord)) discard; color = shade() * glow(); } \ No newline at end of file diff --git a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/font.vert b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/font.vert index 14d1bb437..169cab59b 100644 --- a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/font.vert +++ b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/font.vert @@ -2,16 +2,26 @@ layout (location = 0) in vec4 pos; layout (location = 1) in vec2 uv; -layout (location = 2) in vec4 color; +layout (location = 2) in vec2 sc1; +layout (location = 3) in vec2 sc2; +layout (location = 4) in vec4 color; uniform mat4 u_ProjModel; out vec2 v_TexCoord; +out vec2 v_Scissor1; +out vec2 v_Scissor2; + out vec4 v_Color; void main() { - gl_Position = u_ProjModel * pos; + vec4 proj = u_ProjModel * pos; + vec4 div = proj / proj.w; + gl_Position = vec4(div.x, div.y, 0.0, 1.0); v_TexCoord = uv; + v_Scissor1 = sc1; + v_Scissor2 = sc2; + v_Color = color; } \ No newline at end of file diff --git a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert index 4acbe1f18..60664b63e 100644 --- a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert +++ b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert @@ -6,12 +6,16 @@ layout (location = 2) in vec2 size; layout (location = 3) in vec2 roundL; layout (location = 4) in vec2 roundR; layout (location = 5) in float shade; -layout (location = 6) in vec4 color; +layout (location = 6) in vec2 sc1; +layout (location = 7) in vec2 sc2; +layout (location = 8) in vec4 color; uniform mat4 u_ProjModel; out vec2 v_Position; out vec2 v_TexCoord; +out vec2 v_Scissor1; +out vec2 v_Scissor2; out vec4 v_Color; out vec2 v_Size; out vec2 v_RoundRadiusL; @@ -19,10 +23,14 @@ out vec2 v_RoundRadiusR; out float v_Shade; void main() { - gl_Position = u_ProjModel * pos; + vec4 proj = u_ProjModel * pos; + vec4 div = proj / proj.w; + gl_Position = vec4(div.x, div.y, 0.0, 1.0); v_Position = gl_Position.xy * 0.5 + 0.5; v_TexCoord = uv; + v_Scissor1 = sc1; + v_Scissor2 = sc2; v_Color = color; v_Size = size; diff --git a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_outline.vert b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_outline.vert index 03650690f..d6aad3b91 100644 --- a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_outline.vert +++ b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_outline.vert @@ -1,13 +1,19 @@ #version 330 core layout (location = 0) in vec4 pos; -layout (location = 1) in float alpha; -layout (location = 2) in float shade; -layout (location = 3) in vec4 color; +layout (location = 1) in vec2 uv; +layout (location = 2) in float alpha; +layout (location = 3) in float shade; +layout (location = 4) in vec2 sc1; +layout (location = 5) in vec2 sc2; +layout (location = 6) in vec4 color; uniform mat4 u_ProjModel; out vec2 v_Position; +out vec2 v_TexCoord; +out vec2 v_Scissor1; +out vec2 v_Scissor2; out float v_Alpha; out vec4 v_Color; out float v_Shade; @@ -16,6 +22,10 @@ void main() { gl_Position = u_ProjModel * pos; v_Position = gl_Position.xy * 0.5 + 0.5; + v_TexCoord = uv; + v_Scissor1 = sc1; + v_Scissor2 = sc2; + v_Alpha = alpha; v_Color = color; v_Shade = shade; diff --git a/gradle.properties b/gradle.properties index 3c1fa9c67..c7b4a9931 100644 --- a/gradle.properties +++ b/gradle.properties @@ -34,6 +34,6 @@ kotlinForgeVersionMax=4.11.0 kotlin.code.style=official # Gradle https://gradle.org/ -org.gradle.jvmargs=-Xmx8192M \ +org.gradle.jvmargs=-Xmx2048M \ -XX:+HeapDumpOnOutOfMemoryError org.gradle.parallel=true From 60907beb355d879cbd2fa1dc38d41468a8bb66da Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 28 Dec 2024 10:52:56 -0500 Subject: [PATCH 068/114] added checks and dimension properties --- .../graphics/texture/AnimatedTexture.kt | 2 -- .../com/lambda/graphics/texture/Texture.kt | 23 ++++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt index f9b8a99c7..38426fb7e 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt @@ -31,8 +31,6 @@ class AnimatedTexture(path: LambdaResource) : Texture(image = null, forceConsist private val pbo: PixelBuffer private val gif: ByteBuffer // Do NOT free this pointer private val frameDurations: IntArray - val width: Int - val height: Int val channels: Int val frames: Int diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index 91f221f46..bfb30eefc 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -45,6 +45,9 @@ open class Texture( var initialized: Boolean = false; private set val id = glGenTextures() + var width = -1; protected set + var height = -1; protected set + /** * Binds the texture to a specific slot in the graphics pipeline. */ @@ -64,8 +67,9 @@ open class Texture( // mipmaps from them setupLOD(levels = levels) - val width = image.width - val height = image.height + width = image.width + height = image.height + initialized = true // Set this mipmap to `offset` to define the original texture setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) @@ -74,11 +78,16 @@ open class Texture( } open fun update(image: BufferedImage, offset: Int = 0) { - if (forceConsistency && initialized) - throw IllegalStateException("Client tried to update a texture, but the enforce consistency flag was present") + if (!initialized) return upload(image, offset) - val width = image.width - val height = image.height + check(forceConsistency && initialized) { + "Client tried to update a texture, but the enforce consistency flag was present" + } + + check(image.width + image.height > this.width + this.height && initialized) { + "Client tried to update a texture with more data than allowed" + + "Expected ${this.width + this.height} bytes but got ${image.width + image.height}" + } // Can we rebuild LOD ? glTexSubImage2D(GL_TEXTURE_2D, offset, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, readImage(image)) @@ -100,8 +109,6 @@ open class Texture( image?.let { bind() upload(it) - - initialized = true } } } From 071d238b44afe0dcba75da3e596f222c5782875a Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 28 Dec 2024 11:30:16 -0500 Subject: [PATCH 069/114] fixed the checks --- .../kotlin/com/lambda/graphics/texture/Texture.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index bfb30eefc..9c5210855 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -24,25 +24,29 @@ import com.lambda.module.modules.client.RenderSettings import org.lwjgl.opengl.GL11C import org.lwjgl.opengl.GL45C.* import java.awt.image.BufferedImage +import java.lang.IllegalStateException +import java.nio.ByteBuffer /** - * Represents a texture that can be uploaded and bound to the graphics pipeline. + * Represents a texture that can be uploaded and bound to the graphics pipeline * Supports mipmap generation and LOD (Level of Detail) configuration * * @param image Optional initial image to upload to the texture + * @param format The format of the image passed in * @param levels Number of mipmap levels to generate for the texture * @param forceConsistency Flag to enforce consistency when updating the texture. If true, attempts to update * the texture after initialization will throw an exception */ open class Texture( image: BufferedImage?, + val format: Int = GL_RGBA, private val levels: Int = 4, private val forceConsistency: Boolean = false, ) { /** * Indicates whether there is an initial texture or not */ - var initialized: Boolean = false; private set + var initialized: Boolean = false; protected set val id = glGenTextures() var width = -1; protected set @@ -63,6 +67,8 @@ open class Texture( * @param offset The mipmap level to upload the image to */ open fun upload(image: BufferedImage, offset: Int = 0) { + if (forceConsistency && initialized) throw IllegalStateException("Client tried to update a texture, but the enforce consistency flag was present") + // Store level_base +1 through `level` images and generate // mipmaps from them setupLOD(levels = levels) @@ -79,10 +85,7 @@ open class Texture( open fun update(image: BufferedImage, offset: Int = 0) { if (!initialized) return upload(image, offset) - - check(forceConsistency && initialized) { - "Client tried to update a texture, but the enforce consistency flag was present" - } + if (forceConsistency && initialized) throw IllegalStateException("Client tried to update a texture, but the enforce consistency flag was present") check(image.width + image.height > this.width + this.height && initialized) { "Client tried to update a texture with more data than allowed" + From 2abd06a122016c0d5ae0e5b8918af080ef8f386c Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 28 Dec 2024 11:32:28 -0500 Subject: [PATCH 070/114] make the pbo use the texture correctly --- .../graphics/buffer/pixel/PixelBuffer.kt | 31 ++++++------------- .../graphics/texture/AnimatedTexture.kt | 4 +-- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt index 6a86de42e..db4933ee1 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt @@ -22,6 +22,7 @@ import com.lambda.graphics.gl.putTo import com.lambda.graphics.texture.Texture import com.lambda.util.math.MathUtils.toInt import org.lwjgl.opengl.GL45C.* +import java.lang.IllegalStateException import java.nio.ByteBuffer /** @@ -31,9 +32,6 @@ import java.nio.ByteBuffer * Functions that perform an upload operation, a pixel unpack, will use the buffer object bound to the target GL_PIXEL_UNPACK_BUFFER. * If a buffer is bound, then the pointer value that those functions take is not a pointer, but an offset from the beginning of that buffer. * - * @property width The width of the texture - * @property height The height of the texture - * @property format The image format that will be uploaded * @property texture The [Texture] instance to use * @property asynchronous Whether to use 2 buffers or not * @property bufferMapping Whether to map a block in memory to upload or not @@ -41,9 +39,6 @@ import java.nio.ByteBuffer * @see Reference */ class PixelBuffer( - private val width: Int, - private val height: Int, - private val format: Int, private val texture: Texture, private val asynchronous: Boolean = false, private val bufferMapping: Boolean = false, @@ -52,9 +47,8 @@ class PixelBuffer( override val target: Int = GL_PIXEL_UNPACK_BUFFER override val access: Int = GL_MAP_WRITE_BIT - private val channels = channelMapping[format] ?: throw IllegalArgumentException("Invalid image format, expected OpenGL format, got $format instead") - private val internalFormat = reverseChannelMapping[channels] ?: throw IllegalArgumentException("Invalid internal image format, expected channels count, got $channels instead") - private val size = width * height * channels * 1L + private val channels = channelMapping[texture.format] ?: throw IllegalArgumentException("Invalid image format, expected OpenGL format, got ${texture.format} instead") + private val size = texture.width * texture.height * channels * 1L override fun upload( data: ByteBuffer, @@ -69,8 +63,9 @@ class PixelBuffer( GL_TEXTURE_2D, // Target 0, // Mipmap level 0, 0, // x and y offset - width, height, // width and height of the texture (set to your size) - format, // Format (depends on your data) + texture.width, // Width of the texture + texture.height, // Height of the texture + texture.format, // Format of your texture (depends on your data) GL_UNSIGNED_BYTE, // Type (depends on your data) 0, // PBO offset (for asynchronous transfer) ) @@ -88,10 +83,12 @@ class PixelBuffer( } init { + if (!texture.initialized) throw IllegalStateException("Cannot use uninitialized textures for pixel buffers") + glBindTexture(GL_TEXTURE_2D, texture.id) // Allocate texture storage - glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, 0) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture.width, texture.height, 0, texture.format, GL_UNSIGNED_BYTE, 0) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) @@ -113,15 +110,5 @@ class PixelBuffer( GL_RGBA to 4, GL_BGRA to 4, ) - - /** - * Returns an internal format based on how many channels there are - */ - private val reverseChannelMapping = mapOf( - 1 to GL_RED, - 2 to GL_RG, - 3 to GL_RGB, - 4 to GL_RGBA, - ) } } diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt index 38426fb7e..82a740c70 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt @@ -22,7 +22,6 @@ import com.lambda.util.Communication.logError import com.lambda.util.LambdaResource import com.lambda.util.stream import org.lwjgl.BufferUtils -import org.lwjgl.opengl.GL11.GL_RGBA import org.lwjgl.stb.STBImage import java.nio.ByteBuffer @@ -82,6 +81,7 @@ class AnimatedTexture(path: LambdaResource) : Texture(image = null, forceConsist gif = STBImage.stbi_load_gif_from_memory(buffer, pDelays, pWidth, pHeight, pLayers, pChannels, 4) ?: throw IllegalStateException("There was an unknown error while loading the gif file") + initialized = true width = pWidth.get() height = pHeight.get() frames = pLayers.get() @@ -90,6 +90,6 @@ class AnimatedTexture(path: LambdaResource) : Texture(image = null, forceConsist pDelays.getIntBuffer(frames).get(frameDurations) - pbo = PixelBuffer(width, height, format = GL_RGBA, this@AnimatedTexture) + pbo = PixelBuffer(this@AnimatedTexture) } } From 2aa65436820138065c32d00e72eeb641f54817ef Mon Sep 17 00:00:00 2001 From: blade Date: Tue, 31 Dec 2024 00:11:54 +0300 Subject: [PATCH 071/114] font sdf and shit (in progress) --- .../mixin/render/ChatInputSuggestorMixin.java | 2 +- .../mixin/render/TextRendererMixin.java | 4 +- .../kotlin/com/lambda/graphics/RenderMain.kt | 6 +- .../com/lambda/graphics/buffer/FrameBuffer.kt | 154 ------------------ .../lambda/graphics/buffer/IRenderContext.kt | 6 + .../graphics/buffer/frame/CachedFrame.kt | 82 ++++++++++ .../graphics/buffer/frame/FrameBuffer.kt | 121 ++++++++++++++ .../buffer/frame/ScreenFrameBuffer.kt | 76 +++++++++ .../graphics/pipeline/ScissorAdapter.kt | 3 +- .../lambda/graphics/pipeline/UIPipeline.kt | 3 +- .../renderer/gui/font/FontRenderer.kt | 30 ++-- .../renderer/gui/font/{ => core}/GlyphInfo.kt | 4 +- .../gui/font/{ => core}/LambdaAtlas.kt | 57 ++++--- .../gui/font/{ => core}/LambdaEmoji.kt | 16 +- .../gui/font/{ => core}/LambdaFont.kt | 15 +- .../gui/font/sdf/DistanceFieldTexture.kt | 58 +++++++ .../com/lambda/graphics/texture/Texture.kt | 1 - .../module/modules/client/RenderSettings.kt | 13 +- .../lambda/module/modules/debug/RenderTest.kt | 7 + .../lambda/shaders/fragment/font/font.frag | 37 +++++ .../shaders/fragment/renderer/font.frag | 23 --- .../fragment/signed_distance_field.frag | 29 ++++ .../vertex/{renderer => font}/font.vert | 0 23 files changed, 487 insertions(+), 260 deletions(-) delete mode 100644 common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt create mode 100644 common/src/main/kotlin/com/lambda/graphics/buffer/frame/CachedFrame.kt create mode 100644 common/src/main/kotlin/com/lambda/graphics/buffer/frame/FrameBuffer.kt create mode 100644 common/src/main/kotlin/com/lambda/graphics/buffer/frame/ScreenFrameBuffer.kt rename common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/{ => core}/GlyphInfo.kt (97%) rename common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/{ => core}/LambdaAtlas.kt (85%) rename common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/{ => core}/LambdaEmoji.kt (78%) rename common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/{ => core}/LambdaFont.kt (71%) create mode 100644 common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt create mode 100644 common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag delete mode 100644 common/src/main/resources/assets/lambda/shaders/fragment/renderer/font.frag create mode 100644 common/src/main/resources/assets/lambda/shaders/fragment/signed_distance_field.frag rename common/src/main/resources/assets/lambda/shaders/vertex/{renderer => font}/font.vert (100%) diff --git a/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java b/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java index c602e6674..170b025e1 100644 --- a/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java +++ b/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java @@ -19,7 +19,7 @@ import com.google.common.base.Strings; import com.lambda.command.CommandManager; -import com.lambda.graphics.renderer.gui.font.LambdaAtlas; +import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas; import com.lambda.module.modules.client.LambdaMoji; import com.lambda.module.modules.client.RenderSettings; import com.mojang.brigadier.CommandDispatcher; diff --git a/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java b/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java index 8f5c2bf55..3ac455d20 100644 --- a/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java +++ b/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java @@ -18,8 +18,8 @@ package com.lambda.mixin.render; import com.lambda.Lambda; -import com.lambda.graphics.renderer.gui.font.LambdaAtlas; -import com.lambda.graphics.renderer.gui.font.LambdaEmoji; +import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas; +import com.lambda.graphics.renderer.gui.font.core.LambdaEmoji; import com.lambda.module.modules.client.LambdaMoji; import com.lambda.module.modules.client.RenderSettings; import com.lambda.util.math.Vec2d; diff --git a/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt index c2411cdd6..76b616812 100644 --- a/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt +++ b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt @@ -20,20 +20,18 @@ package com.lambda.graphics import com.lambda.Lambda.mc import com.lambda.event.EventFlow.post import com.lambda.event.events.RenderEvent -import com.lambda.graphics.buffer.FrameBuffer import com.lambda.graphics.gl.GlStateUtils.setupGL import com.lambda.graphics.gl.Matrices import com.lambda.graphics.gl.Matrices.resetMatrices import com.lambda.graphics.pipeline.UIPipeline -import com.lambda.graphics.shader.Shader import com.lambda.module.modules.client.GuiSettings import com.lambda.util.math.Vec2d import com.mojang.blaze3d.systems.RenderSystem.getProjectionMatrix import org.joml.Matrix4f object RenderMain { - private val projectionMatrix = Matrix4f() - private val modelViewMatrix get() = Matrices.peek() + val projectionMatrix = Matrix4f() + val modelViewMatrix get() = Matrices.peek() val projModel get() = Matrix4f(projectionMatrix).mul(modelViewMatrix) var screenSize = Vec2d.ZERO diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt deleted file mode 100644 index dce732b33..000000000 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.buffer - -import com.lambda.Lambda.mc -import com.lambda.graphics.RenderMain -import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib -import com.lambda.graphics.buffer.vertex.attributes.VertexMode -import com.lambda.graphics.gl.GlStateUtils.withBlendFunc -import com.lambda.graphics.shader.Shader -import com.lambda.graphics.texture.TextureUtils.bindTexture -import com.lambda.graphics.texture.TextureUtils.setupTexture -import com.lambda.util.math.Vec2d -import org.lwjgl.opengl.GL30.* -import java.nio.IntBuffer - -class FrameBuffer(private val depth: Boolean = false) { - private val fbo = glGenFramebuffers() - - private val colorAttachment = glGenTextures() - private val depthAttachment by lazy(::glGenTextures) - - private val clearMask = if (!depth) GL_COLOR_BUFFER_BIT - else GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT - - private var width = 0 - private var height = 0 - - fun write(block: () -> Unit): FrameBuffer { - val prev = lastFrameBuffer ?: mc.framebuffer.fbo - - glBindFramebuffer(GL_FRAMEBUFFER, fbo) - lastFrameBuffer = fbo - - update() - withBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA, block) - - lastFrameBuffer = prev - glBindFramebuffer(GL_FRAMEBUFFER, prev) - return this - } - - fun read( - shader: Shader, - pos1: Vec2d = Vec2d.ZERO, - pos2: Vec2d = RenderMain.screenSize, - shaderBlock: (Shader) -> Unit = {} - ): FrameBuffer { - bindColorTexture() - - shader.use() - shaderBlock(shader) - - pipeline.use { - grow(4) - - val uv1 = pos1 / RenderMain.screenSize - val uv2 = pos2 / RenderMain.screenSize - - putQuad( - vec2(pos1.x, pos1.y).vec2(uv1.x, 1.0 - uv1.y).end(), - vec2(pos1.x, pos2.y).vec2(uv1.x, 1.0 - uv2.y).end(), - vec2(pos2.x, pos2.y).vec2(uv2.x, 1.0 - uv2.y).end(), - vec2(pos2.x, pos1.y).vec2(uv2.x, 1.0 - uv1.y).end() - ) - } - - withBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA) { - pipeline.upload() - pipeline.render() - pipeline.clear() - } - - return this - } - - fun bindColorTexture(slot: Int = 0): FrameBuffer { - bindTexture(colorAttachment, slot) - return this - } - - fun bindDepthTexture(slot: Int = 0): FrameBuffer { - check(depth) { - "Cannot bind depth texture of a non-depth framebuffer" - } - - bindTexture(depthAttachment, slot) - return this - } - - private fun update() { - val widthIn = mc.window.framebufferWidth - val heightIn = mc.window.framebufferHeight - - if (width != widthIn || height != heightIn) { - width = widthIn - height = heightIn - - setupBufferTexture(colorAttachment) - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, null as IntBuffer?) - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorAttachment, 0) - - if (depth) { - setupBufferTexture(depthAttachment) - glTexImage2D( - GL_TEXTURE_2D, // Target - 0, // LOD Level - GL_DEPTH_COMPONENT32F, // Internal Format - width, // Width - height, // Height - 0, // Border (must be zero) - GL_DEPTH_COMPONENT, // Format - GL_FLOAT, // Type - null as IntBuffer? // Pointer to data - ) - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthAttachment, 0) - } - - glClearColor(0f, 0f, 0f, 0f) - glClearDepth(1.0) - } - - glClear(clearMask) - } - - private fun setupBufferTexture(id: Int) { - bindTexture(id) - setupTexture(GL_NEAREST, GL_NEAREST) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) - } - - companion object { - private val pipeline = VertexPipeline(VertexMode.TRIANGLES, VertexAttrib.Group.POS_UV) - private var lastFrameBuffer: Int? = null - } -} diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/IRenderContext.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/IRenderContext.kt index 3fb28a33b..3bb15197e 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/IRenderContext.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/IRenderContext.kt @@ -38,6 +38,12 @@ interface IRenderContext { fun upload() fun clear() + fun immediateDraw() { + upload() + render() + clear() + } + fun grow(amount: Int) fun use(block: IRenderContext.() -> Unit) { diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/frame/CachedFrame.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/frame/CachedFrame.kt new file mode 100644 index 000000000..9126de77e --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/frame/CachedFrame.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.buffer.frame + +import com.lambda.Lambda.mc +import com.lambda.graphics.RenderMain +import com.lambda.graphics.gl.Matrices +import org.joml.Matrix4f +import org.lwjgl.opengl.GL11C.glViewport + +/** + * A class that handles a cached frame, encapsulating a framebuffer with a specified width and height. + * It provides methods for binding the framebuffer texture and writing to the framebuffer with custom rendering operations. + * + * @param width The width of the framebuffer. + * @param height The height of the framebuffer. + */ +class CachedFrame(val width: Int, val height: Int) { + + // The framebuffer associated with this cached frame + private val frameBuffer = FrameBuffer(width, height) + + /** + * Binds the color texture of the framebuffer to a specified texture slot. + * + * @param slot The texture slot to bind the color texture to. Defaults to slot 0. + */ + fun bind(slot: Int = 0) = frameBuffer.bindColorTexture(slot) + + /** + * Executes custom drawing operations on the framebuffer. + * + * The method temporarily modifies the view and projection matrices, the viewport, + * and then restores them after the block is executed. + * + * @param block A block of code that performs custom drawing operations on the framebuffer. + */ + fun write(block: () -> Unit): CachedFrame { + frameBuffer.write { + // Save the current viewmodel matrix + Matrices.push() + // Set the viewmodel matrix to translate the scene away + Matrices.peek().set(Matrix4f().translate(0f, 0f, -3000f)) + + // Save the previous projection matrix and set a custom orthographic projection + val prevProj = Matrix4f(RenderMain.projectionMatrix) + RenderMain.projectionMatrix.setOrtho(0f, width.toFloat(), height.toFloat(), 0f, 1000f, 21000f) + + // Resize the viewport to match the framebuffer's dimensions + glViewport(0, 0, width, height) + + // Execute the drawing operations defined in the block + block() + + // Restore the previous viewport dimensions + glViewport(0, 0, mc.framebuffer.viewportWidth, mc.framebuffer.viewportHeight) + + // Restore the previous projection matrix + RenderMain.projectionMatrix.set(prevProj) + + // Restore the previous viewmodel matrix + Matrices.pop() + } + + return this + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/frame/FrameBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/frame/FrameBuffer.kt new file mode 100644 index 000000000..d340b524c --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/frame/FrameBuffer.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.buffer.frame + +import com.lambda.Lambda.mc +import com.lambda.graphics.RenderMain.projectionMatrix +import com.lambda.graphics.buffer.VertexPipeline +import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib +import com.lambda.graphics.buffer.vertex.attributes.VertexMode +import com.lambda.graphics.gl.Matrices +import com.lambda.graphics.texture.TextureUtils +import org.joml.Matrix4f +import org.lwjgl.opengl.GL12C.GL_CLAMP_TO_EDGE +import org.lwjgl.opengl.GL30C.* +import java.nio.IntBuffer + +open class FrameBuffer( + protected var width: Int = 1, + protected var height: Int = 1, + private val depth: Boolean = false +) { + private val fbo = glGenFramebuffers() + + private val colorAttachment = glGenTextures() + private val depthAttachment by lazy(::glGenTextures) + + private val clearMask = if (!depth) GL_COLOR_BUFFER_BIT + else GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT + + private var lastWidth = -1 + private var lastHeight = -1 + + open fun write(block: () -> Unit): FrameBuffer { + val prev = lastFrameBuffer ?: mc.framebuffer.fbo + + glBindFramebuffer(GL_FRAMEBUFFER, fbo) + lastFrameBuffer = fbo + + update() + + block() + + lastFrameBuffer = prev + glBindFramebuffer(GL_FRAMEBUFFER, prev) + return this + } + + private fun update() { + if (width == lastWidth && height == lastWidth) { + glClear(clearMask) + return + } + + lastWidth = width + lastHeight = height + + setupBufferTexture(colorAttachment) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, null as IntBuffer?) + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorAttachment, 0) + + if (depth) { + setupBufferTexture(depthAttachment) + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, null as IntBuffer?) + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthAttachment, 0) + } + + glClearColor(0f, 0f, 0f, 0f) + glClearDepth(1.0) + + glClear(clearMask) + + val fboStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER) + + check(fboStatus == GL_FRAMEBUFFER_COMPLETE) { + "Framebuffer not complete: $fboStatus" + } + } + + open fun bindColorTexture(slot: Int = 0): FrameBuffer { + TextureUtils.bindTexture(colorAttachment, slot) + return this + } + + open fun bindDepthTexture(slot: Int = 0): FrameBuffer { + check(depth) { + "Cannot bind depth texture of a non-depth framebuffer" + } + + TextureUtils.bindTexture(depthAttachment, slot) + return this + } + + companion object { + val pipeline = VertexPipeline(VertexMode.TRIANGLES, VertexAttrib.Group.POS_UV) + private var lastFrameBuffer: Int? = null + + private fun setupBufferTexture(id: Int) { + TextureUtils.bindTexture(id) + TextureUtils.setupTexture(GL_LINEAR, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/frame/ScreenFrameBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/frame/ScreenFrameBuffer.kt new file mode 100644 index 000000000..466473a81 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/frame/ScreenFrameBuffer.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.buffer.frame + +import com.lambda.Lambda.mc +import com.lambda.graphics.RenderMain +import com.lambda.graphics.gl.GlStateUtils.withBlendFunc +import com.lambda.graphics.shader.Shader +import com.lambda.util.math.Vec2d +import org.lwjgl.opengl.GL11C.* + +class ScreenFrameBuffer(depth: Boolean = false) : FrameBuffer(depth = depth) { + fun read( + shader: Shader, + pos1: Vec2d = Vec2d.ZERO, + pos2: Vec2d = RenderMain.screenSize, + shaderBlock: (Shader) -> Unit = {} + ): ScreenFrameBuffer { + bindColorTexture() + + shader.use() + shaderBlock(shader) + + pipeline.use { + grow(4) + + val uv1 = pos1 / RenderMain.screenSize + val uv2 = pos2 / RenderMain.screenSize + + putQuad( + vec2(pos1.x, pos1.y).vec2(uv1.x, 1.0 - uv1.y).end(), + vec2(pos1.x, pos2.y).vec2(uv1.x, 1.0 - uv2.y).end(), + vec2(pos2.x, pos2.y).vec2(uv2.x, 1.0 - uv2.y).end(), + vec2(pos2.x, pos1.y).vec2(uv2.x, 1.0 - uv1.y).end() + ) + } + + withBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA) { + pipeline.upload() + pipeline.render() + pipeline.clear() + } + + return this + } + + override fun write(block: () -> Unit): ScreenFrameBuffer { + width = mc.window.framebufferWidth + height = mc.window.framebufferHeight + + return super.write { + withBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA, block) + } as ScreenFrameBuffer + } + + override fun bindColorTexture(slot: Int) = + super.bindColorTexture(slot) as ScreenFrameBuffer + + override fun bindDepthTexture(slot: Int) = + super.bindDepthTexture(slot) as ScreenFrameBuffer +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt b/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt index 6011a0ab7..3bebf7a6b 100644 --- a/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt +++ b/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt @@ -17,10 +17,9 @@ package com.lambda.graphics.pipeline -import com.lambda.graphics.renderer.gui.font.GlyphInfo +import com.lambda.graphics.renderer.gui.font.core.GlyphInfo import com.lambda.util.math.Rect import com.lambda.util.math.transform -import kotlin.random.Random object ScissorAdapter { private var stack = ArrayDeque() diff --git a/common/src/main/kotlin/com/lambda/graphics/pipeline/UIPipeline.kt b/common/src/main/kotlin/com/lambda/graphics/pipeline/UIPipeline.kt index 40c9037b8..230ce841a 100644 --- a/common/src/main/kotlin/com/lambda/graphics/pipeline/UIPipeline.kt +++ b/common/src/main/kotlin/com/lambda/graphics/pipeline/UIPipeline.kt @@ -18,7 +18,6 @@ package com.lambda.graphics.pipeline import com.lambda.core.Loadable -import com.lambda.graphics.gl.GlStateUtils.withDepth import com.lambda.graphics.renderer.gui.font.FontRenderer import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer @@ -35,7 +34,7 @@ object UIPipeline : Loadable { uiDepth = 0 } - fun render() = withDepth(true) { + fun render() { FilledRectRenderer.render() OutlineRectRenderer.render() FontRenderer.render() diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index 0a7ac1c0d..387ea9558 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -22,12 +22,13 @@ import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib import com.lambda.graphics.buffer.vertex.attributes.VertexMode import com.lambda.graphics.pipeline.ScissorAdapter import com.lambda.graphics.pipeline.UIPipeline -import com.lambda.graphics.renderer.gui.font.LambdaAtlas.bind -import com.lambda.graphics.renderer.gui.font.LambdaAtlas.get -import com.lambda.graphics.renderer.gui.font.LambdaAtlas.height -import com.lambda.graphics.renderer.gui.font.LambdaAtlas.slot +import com.lambda.graphics.renderer.gui.font.core.GlyphInfo +import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.get +import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.height +import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.slot +import com.lambda.graphics.renderer.gui.font.sdf.DistanceFieldTexture import com.lambda.graphics.shader.Shader -import com.lambda.module.modules.client.ClickGui +import com.lambda.graphics.texture.TextureOwner.texture import com.lambda.module.modules.client.LambdaMoji import com.lambda.module.modules.client.RenderSettings import com.lambda.util.math.Vec2d @@ -43,13 +44,14 @@ object FontRenderer { private val chars = RenderSettings.textFont private val emojis = RenderSettings.emojiFont - private val shader = Shader("renderer/font") + private val charsSDF = DistanceFieldTexture(chars.texture) + + private val shader = Shader("font/font") private val pipeline = VertexPipeline(VertexMode.TRIANGLES, VertexAttrib.Group.FONT) private val shadowShift get() = RenderSettings.shadowShift * 5.0 private val baselineOffset get() = RenderSettings.baselineOffset * 2.0f - 10f private val gap get() = RenderSettings.gap * 0.5f - 0.8f - private val scaleMultiplier: Double get() = 1.0 /** * Builds the vertex array for rendering the provided text string at a specified position. @@ -221,7 +223,7 @@ object FontRenderer { * @param scale The base scale factor. * @return The adjusted scale factor. */ - fun getScaleFactor(scale: Double): Double = scaleMultiplier * scale * 0.12 + fun getScaleFactor(scale: Double): Double = scale * 8 / chars.height /** * Calculates the shadow color by adjusting the brightness of the input color. @@ -238,14 +240,14 @@ object FontRenderer { fun render() { shader.use() - shader["u_FontTexture"] = chars.slot + shader["u_FontTexture"] = 0 shader["u_EmojiTexture"] = emojis.slot + shader["u_SDFMin"] = 0.3 + shader["u_SDFMax"] = 1.0 - chars.bind() - emojis.bind() + charsSDF.frame.bind() + //emojis.bind() - pipeline.upload() - pipeline.render() - pipeline.clear() + pipeline.immediateDraw() } } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/GlyphInfo.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/GlyphInfo.kt similarity index 97% rename from common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/GlyphInfo.kt rename to common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/GlyphInfo.kt index 59e67780a..e1d9f7f5c 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/GlyphInfo.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/GlyphInfo.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.lambda.graphics.renderer.gui.font +package com.lambda.graphics.renderer.gui.font.core import com.lambda.util.math.Vec2d @@ -60,4 +60,4 @@ data class GlyphInfo( * The V coordinate of the bottom-right corner of the character texture. */ val v2 get() = uv2.y -} +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt similarity index 85% rename from common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt rename to common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt index 662179595..018a8c4b4 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaAtlas.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt @@ -15,9 +15,9 @@ * along with this program. If not, see . */ -package com.lambda.graphics.renderer.gui.font +package com.lambda.graphics.renderer.gui.font.core -import com.google.common.math.IntMath.pow +import com.google.common.math.IntMath import com.lambda.core.Loadable import com.lambda.graphics.texture.TextureOwner.texture import com.lambda.graphics.texture.TextureOwner.upload @@ -30,11 +30,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap import it.unimi.dsi.fastutil.objects.Object2DoubleArrayMap import it.unimi.dsi.fastutil.objects.Object2IntArrayMap import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap -import java.awt.Color -import java.awt.Font -import java.awt.FontMetrics -import java.awt.Graphics2D -import java.awt.RenderingHints +import java.awt.* import java.awt.image.BufferedImage import java.util.function.ToIntFunction import java.util.zip.ZipFile @@ -72,7 +68,8 @@ import kotlin.time.Duration.Companion.days object LambdaAtlas : Loadable { private val fontMap = Object2ObjectOpenHashMap>() private val emojiMap = Object2ObjectOpenHashMap>() - private val slotReservation = Object2IntArrayMap() // Will cause undefined behavior if someone is trying to allocate more than 32 slots, unlikely + private val slotReservation = + Object2IntArrayMap() // Will cause undefined behavior if someone is trying to allocate more than 32 slots, unlikely private val bufferPool = mutableMapOf() // This array is nuked once the data is dispatched to OpenGL @@ -97,6 +94,8 @@ object LambdaAtlas : Loadable { val LambdaEmoji.keys get() = emojiMap.getValue(this) + const val CHAR_SPACE = 8 + /** * Builds the buffer for an emoji set by reading a ZIP file containing emoji images. * The images are arranged into a texture atlas, and their UV coordinates are computed for later rendering. @@ -115,7 +114,7 @@ object LambdaAtlas : Loadable { val length = zip.size().toDouble() val textureDimensionLength: (Int) -> Int = { dimLength -> - pow(2, ceil(log2((dimLength + 2) * sqrt(length))).toInt()) + IntMath.pow(2, ceil(log2((dimLength + CHAR_SPACE) * sqrt(length))).toInt()) } val width = textureDimensionLength(firstImage.width) @@ -126,21 +125,24 @@ object LambdaAtlas : Loadable { val graphics = image.graphics as Graphics2D graphics.color = Color(0, 0, 0, 0) - var x = 0 - var y = 0 + var x = CHAR_SPACE + var y = CHAR_SPACE val constructed = Object2ObjectOpenHashMap() for (entry in zip.entries()) { val name = entry.name.substringAfterLast("/").substringBeforeLast(".") val emoji = ImageIO.read(zip.getInputStream(entry)) - if (x + emoji.width >= image.width) { - y += emoji.height + 2 + val charWidth = emoji.width + CHAR_SPACE + val charHeight = emoji.height + CHAR_SPACE + + if (x + charWidth >= image.width) { + check(y + charHeight < image.height) { "Can't load emoji glyphs. Texture size is too small" } + + y += charHeight x = 0 } - check(y + emoji.height < image.height) { "Can't load emoji glyphs. Texture size is too small" } - graphics.drawImage(emoji, x, y, null) val size = Vec2d(emoji.width, emoji.height) @@ -162,7 +164,7 @@ object LambdaAtlas : Loadable { characters: Int = 2048 // How many characters from that font should be used for the generation ) { val font = fontCache.computeIfAbsent(this) { - Font.createFont(Font.TRUETYPE_FONT, "fonts/$fontName.ttf".stream).deriveFont(64.0f) + Font.createFont(Font.TRUETYPE_FONT, "fonts/$fontName.ttf".stream).deriveFont(128.0f) } val textureSize = characters * 2 @@ -173,24 +175,26 @@ object LambdaAtlas : Loadable { val graphics = image.graphics as Graphics2D graphics.background = Color(0, 0, 0, 0) - var x = 0 - var y = 0 + var x = CHAR_SPACE + var y = CHAR_SPACE var rowHeight = 0 val constructed = Int2ObjectArrayMap() (Char.MIN_VALUE.. val charImage = getCharImage(font, char) ?: return@forEach - rowHeight = max(rowHeight, charImage.height + 2) + rowHeight = max(rowHeight, charImage.height + CHAR_SPACE) + val charWidth = charImage.width + CHAR_SPACE + + if (x + charWidth >= textureSize) { + // Check if possible to step to the next row + check(y + rowHeight <= textureSize) { "Can't load font glyphs. Texture size is too small" } - if (x + charImage.width >= textureSize) { y += rowHeight x = 0 rowHeight = 0 } - check(y + charImage.height <= textureSize) { "Can't load font glyphs. Texture size is too small" } - graphics.drawImage(charImage, x, y, null) val size = Vec2d(charImage.width, charImage.height) @@ -200,7 +204,7 @@ object LambdaAtlas : Loadable { constructed[char.code] = GlyphInfo(size, uv1, uv2) heightCache[font] = max(heightCache.getDouble(font), size.y) // No compare set unfortunately - x += charImage.width + 2 + x += charWidth } fontMap[this] = constructed @@ -209,10 +213,13 @@ object LambdaAtlas : Loadable { // TODO: Change this when we've refactored the loadables override fun load(): String { + LambdaFont.entries.forEach(LambdaFont::load) + LambdaEmoji.entries.forEach(LambdaEmoji::load) + val str = "Loaded ${bufferPool.size} fonts" // avoid race condition runGameScheduled { - bufferPool.forEach { (owner, image) -> owner.upload(image, 4) } + bufferPool.forEach { (owner, image) -> owner.upload(image) } bufferPool.clear() } @@ -248,4 +255,4 @@ object LambdaAtlas : Loadable { return charImage } -} +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaEmoji.kt similarity index 78% rename from common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt rename to common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaEmoji.kt index 7f669789d..1220fa001 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaEmoji.kt @@ -15,10 +15,9 @@ * along with this program. If not, see . */ -package com.lambda.graphics.renderer.gui.font +package com.lambda.graphics.renderer.gui.font.core -import com.lambda.core.Loadable -import com.lambda.graphics.renderer.gui.font.LambdaAtlas.buildBuffer +import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.buildBuffer enum class LambdaEmoji(val url: String) { Twemoji("https://github.com/Edouard127/emoji-generator/releases/latest/download/emojis.zip"); @@ -35,11 +34,8 @@ enum class LambdaEmoji(val url: String) { fun parse(text: String): MutableList = emojiRegex.findAll(text).map { it.value.drop(1).dropLast(1) }.toMutableList() - object Loader : Loadable { - override fun load(): String { - entries.forEach { it.buildBuffer() } - - return "Loaded ${entries.size} emoji sets" - } + fun load(): String { + entries.forEach { it.buildBuffer() } + return "Loaded ${entries.size} emoji sets" } -} +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaFont.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaFont.kt similarity index 71% rename from common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaFont.kt rename to common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaFont.kt index ed76cb5dd..f6d3ed01f 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaFont.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaFont.kt @@ -15,19 +15,16 @@ * along with this program. If not, see . */ -package com.lambda.graphics.renderer.gui.font +package com.lambda.graphics.renderer.gui.font.core -import com.lambda.core.Loadable -import com.lambda.graphics.renderer.gui.font.LambdaAtlas.buildBuffer +import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.buildBuffer enum class LambdaFont(val fontName: String) { FiraSansRegular("FiraSans-Regular"), FiraSansBold("FiraSans-Bold"); - object Loader : Loadable { - override fun load(): String { - entries.forEach { it.buildBuffer() } - return "Loaded ${entries.size} fonts" - } + fun load(): String { + entries.forEach { it.buildBuffer() } + return "Loaded ${entries.size} fonts" } -} +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt new file mode 100644 index 000000000..805888f67 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.renderer.gui.font.sdf + +import com.lambda.graphics.buffer.frame.CachedFrame +import com.lambda.graphics.buffer.frame.FrameBuffer +import com.lambda.graphics.shader.Shader +import com.lambda.graphics.texture.Texture +import com.lambda.util.math.Vec2d + +/** + * A class that represents a distance field texture, which is created by rendering a given texture + * onto a framebuffer with specific shader operations. + * + * The texture is used to create a signed distance field (SDF) for rendering operations. + * + * @param texture The texture to be used for creating the distance field. + */ +class DistanceFieldTexture(texture: Texture) { + val frame = CachedFrame(texture.width, texture.height).write { + FrameBuffer.pipeline.use { + val (pos1, pos2) = Vec2d.ZERO to Vec2d(texture.width, texture.height) + + grow(4) + putQuad( + vec2(pos1.x, pos1.y).vec2(0.0, 1.0).end(), + vec2(pos1.x, pos2.y).vec2(0.0, 0.0).end(), + vec2(pos2.x, pos2.y).vec2(1.0, 0.0).end(), + vec2(pos2.x, pos1.y).vec2(1.0, 1.0).end() + ) + + shader.use() + shader["u_TexelSize"] = Vec2d.ONE / pos2 + texture.bind() + + immediateDraw() + } + } + + companion object { + private val shader = Shader("signed_distance_field", "renderer/pos_tex") + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index 9c5210855..5e920e93a 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -57,7 +57,6 @@ open class Texture( */ open fun bind(slot: Int = 0) { bindTexture(id, slot) - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, RenderSettings.lodBias) } /** diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt index 5d0c465d6..8f19c14a3 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt @@ -17,8 +17,8 @@ package com.lambda.module.modules.client -import com.lambda.graphics.renderer.gui.font.LambdaEmoji -import com.lambda.graphics.renderer.gui.font.LambdaFont +import com.lambda.graphics.renderer.gui.font.core.LambdaEmoji +import com.lambda.graphics.renderer.gui.font.core.LambdaFont import com.lambda.module.Module import com.lambda.module.tag.ModuleTag @@ -38,15 +38,6 @@ object RenderSettings : Module( val gap by setting("Gap", 1.5, -10.0..10.0, 0.5) { page == Page.Font } val baselineOffset by setting("Vertical Offset", 0.0, -10.0..10.0, 0.5) { page == Page.Font } - // This value actually depends on the parameters of the texture... - // The specified value is added to the shader-supplied bias value (if any) - // and subsequently clamped into the implementation-defined range - // [-biasmax, biasmax], where biasmax is the value of the implementation - // defined constant GL_MAX_TEXTURE_LOD_BIAS. The initial value is 0.0. - // - // At least we're sure that the smoothing we see is the same for everyone - val lodBias by setting("Smoothing", -2.0f, -15.0f..15.0f, 0.1f) { page == Page.Font } - // ESP val uploadsPerTick by setting("Uploads", 16, 1..256, 1, unit = " chunk/tick") { page == Page.ESP } val rebuildsPerTick by setting("Rebuilds", 64, 1..256, 1, unit = " chunk/tick") { page == Page.ESP } diff --git a/common/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt b/common/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt index 7a4b168f3..95981d701 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt @@ -19,15 +19,22 @@ package com.lambda.module.modules.debug import com.lambda.event.events.RenderEvent import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.buffer.frame.FrameBuffer import com.lambda.graphics.renderer.esp.DynamicAABB.Companion.dynamicBox import com.lambda.graphics.renderer.esp.builders.build +import com.lambda.graphics.renderer.gui.font.FontRenderer +import com.lambda.graphics.buffer.frame.CachedFrame +import com.lambda.graphics.texture.TextureOwner.texture import com.lambda.module.Module import com.lambda.module.tag.ModuleTag +import com.lambda.util.math.Vec2d import com.lambda.util.math.setAlpha import com.lambda.util.world.entitySearch import net.minecraft.entity.LivingEntity import net.minecraft.util.math.Box +import org.lwjgl.glfw.GLFW.glfwGetTime import java.awt.Color +import kotlin.math.sin object RenderTest : Module( name = "Render:shrimp:Test:canned_food:", diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag b/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag new file mode 100644 index 000000000..2f4631388 --- /dev/null +++ b/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag @@ -0,0 +1,37 @@ +#version 330 core + +uniform sampler2D u_FontTexture; +uniform sampler2D u_EmojiTexture; +uniform float u_SDFMin; +uniform float u_SDFMax; + +in vec2 v_TexCoord; +in vec2 v_Scissor1; +in vec2 v_Scissor2; + +in vec4 v_Color; + +out vec4 color; + +bool scissorFailed(vec2 coord) { + return coord.x < v_Scissor1.x || coord.x > v_Scissor2.x || coord.y < v_Scissor1.y || coord.y > v_Scissor2.y; +} + +void main() { + vec2 coord = v_TexCoord; + + bool isEmoji = coord.x < 0.0; + if (isEmoji) coord = -v_TexCoord; + + if (scissorFailed(coord)) discard; + + if (isEmoji) { + color = texture(u_EmojiTexture, coord) * v_Color; + return; + } + + float sdf = texture(u_FontTexture, coord).r; + float alpha = 1.0 - smoothstep(u_SDFMin, u_SDFMax, 1.0 - sdf); + + color = vec4(1, 1, 1, alpha) * v_Color; +} diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/font.frag b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/font.frag deleted file mode 100644 index c5670576c..000000000 --- a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/font.frag +++ /dev/null @@ -1,23 +0,0 @@ -#version 330 core - -uniform sampler2D u_FontTexture; -uniform sampler2D u_EmojiTexture; - -in vec2 v_TexCoord; -in vec2 v_Scissor1; -in vec2 v_Scissor2; - -in vec4 v_Color; - -out vec4 color; - -bool scissorFailed(vec2 coord) { - return coord.x < v_Scissor1.x || coord.x > v_Scissor2.x || coord.y < v_Scissor1.y || coord.y > v_Scissor2.y; -} - -void main() { - vec2 coord = v_TexCoord.x > 0.0 ? v_TexCoord : -v_TexCoord; - if (scissorFailed(coord)) discard; - - color = texture(v_TexCoord.x > 0.0 ? u_FontTexture : u_EmojiTexture, coord) * v_Color; -} diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/signed_distance_field.frag b/common/src/main/resources/assets/lambda/shaders/fragment/signed_distance_field.frag new file mode 100644 index 000000000..33114a0aa --- /dev/null +++ b/common/src/main/resources/assets/lambda/shaders/fragment/signed_distance_field.frag @@ -0,0 +1,29 @@ +#version 330 core + +uniform sampler2D u_Texture; +uniform vec2 u_TexelSize; + +in vec2 v_TexCoord; +out vec4 color; + +#define SPHREAD 4 + +void main() { + float alpha = 0.0; + float blurWeight = 0.0; + + for (int x = -SPHREAD; x <= SPHREAD; ++x) { + for (int y = -SPHREAD; y <= SPHREAD; ++y) { + vec2 offset = vec2(x, y) * u_TexelSize; + + float color = texture(u_Texture, v_TexCoord + offset).r; + float weight = exp(-color * color); + + alpha += color * weight; + blurWeight += weight; + } + } + + alpha /= blurWeight; + color = vec4(alpha, 1.0, 1.0, 1.0); +} diff --git a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/font.vert b/common/src/main/resources/assets/lambda/shaders/vertex/font/font.vert similarity index 100% rename from common/src/main/resources/assets/lambda/shaders/vertex/renderer/font.vert rename to common/src/main/resources/assets/lambda/shaders/vertex/font/font.vert From 2e3a88364237ec3c9ef83e22436ae8c893e38874 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Mon, 30 Dec 2024 19:03:22 -0500 Subject: [PATCH 072/114] ref: textures --- .../renderer/gui/font/FontRenderer.kt | 11 +-- .../renderer/gui/font/core/LambdaAtlas.kt | 37 +++------ .../gui/font/sdf/DistanceFieldTexture.kt | 17 ++-- .../graphics/texture/AnimatedTexture.kt | 2 +- .../com/lambda/graphics/texture/Texture.kt | 3 +- .../lambda/graphics/texture/TextureOwner.kt | 82 ++++++++++++++++--- 6 files changed, 100 insertions(+), 52 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index 387ea9558..4b35cab54 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -25,10 +25,8 @@ import com.lambda.graphics.pipeline.UIPipeline import com.lambda.graphics.renderer.gui.font.core.GlyphInfo import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.get import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.height -import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.slot -import com.lambda.graphics.renderer.gui.font.sdf.DistanceFieldTexture import com.lambda.graphics.shader.Shader -import com.lambda.graphics.texture.TextureOwner.texture +import com.lambda.graphics.texture.TextureOwner.bind import com.lambda.module.modules.client.LambdaMoji import com.lambda.module.modules.client.RenderSettings import com.lambda.util.math.Vec2d @@ -44,8 +42,6 @@ object FontRenderer { private val chars = RenderSettings.textFont private val emojis = RenderSettings.emojiFont - private val charsSDF = DistanceFieldTexture(chars.texture) - private val shader = Shader("font/font") private val pipeline = VertexPipeline(VertexMode.TRIANGLES, VertexAttrib.Group.FONT) @@ -241,12 +237,11 @@ object FontRenderer { fun render() { shader.use() shader["u_FontTexture"] = 0 - shader["u_EmojiTexture"] = emojis.slot + shader["u_EmojiTexture"] = 1 shader["u_SDFMin"] = 0.3 shader["u_SDFMax"] = 1.0 - charsSDF.frame.bind() - //emojis.bind() + bind(chars, emojis) pipeline.immediateDraw() } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt index 018a8c4b4..d7da86104 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt @@ -19,20 +19,16 @@ package com.lambda.graphics.renderer.gui.font.core import com.google.common.math.IntMath import com.lambda.core.Loadable -import com.lambda.graphics.texture.TextureOwner.texture -import com.lambda.graphics.texture.TextureOwner.upload +import com.lambda.graphics.texture.TextureOwner.uploadField import com.lambda.http.Method import com.lambda.http.request import com.lambda.threading.runGameScheduled import com.lambda.util.math.Vec2d import com.lambda.util.stream -import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap import it.unimi.dsi.fastutil.objects.Object2DoubleArrayMap -import it.unimi.dsi.fastutil.objects.Object2IntArrayMap import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import java.awt.* import java.awt.image.BufferedImage -import java.util.function.ToIntFunction import java.util.zip.ZipFile import javax.imageio.ImageIO import kotlin.math.ceil @@ -66,10 +62,8 @@ import kotlin.time.Duration.Companion.days * ``` */ object LambdaAtlas : Loadable { - private val fontMap = Object2ObjectOpenHashMap>() - private val emojiMap = Object2ObjectOpenHashMap>() - private val slotReservation = - Object2IntArrayMap() // Will cause undefined behavior if someone is trying to allocate more than 32 slots, unlikely + private val fontMap = mutableMapOf>() + private val emojiMap = mutableMapOf>() private val bufferPool = mutableMapOf() // This array is nuked once the data is dispatched to OpenGL @@ -78,16 +72,9 @@ object LambdaAtlas : Loadable { private val metricCache = mutableMapOf() private val heightCache = Object2DoubleArrayMap() - operator fun LambdaFont.get(char: Char): GlyphInfo? = fontMap.getValue(this)[char.code] + operator fun LambdaFont.get(char: Char): GlyphInfo? = fontMap.getValue(this)[char] operator fun LambdaEmoji.get(string: String): GlyphInfo? = emojiMap.getValue(this)[string] - // Allow binding any valid font definition enums - fun > T.bind() = - this@bind.texture.bind(slot = slotReservation.computeIfAbsent(this@bind, ToIntFunction { slotReservation.size })) - - val > T.slot: Int - get() = slotReservation.getInt(this@slot) - val LambdaFont.height: Double get() = heightCache.getDouble(fontCache[this@height]) @@ -154,10 +141,10 @@ object LambdaAtlas : Loadable { x += emoji.width + 2 } - emojiMap[this] = constructed + emojiMap[this@buildBuffer] = constructed } - bufferPool[this] = image + bufferPool[this@buildBuffer] = image } fun LambdaFont.buildBuffer( @@ -179,7 +166,7 @@ object LambdaAtlas : Loadable { var y = CHAR_SPACE var rowHeight = 0 - val constructed = Int2ObjectArrayMap() + val constructed = mutableMapOf() (Char.MIN_VALUE.. val charImage = getCharImage(font, char) ?: return@forEach @@ -201,14 +188,14 @@ object LambdaAtlas : Loadable { val uv1 = Vec2d(x, y) * oneTexelSize val uv2 = Vec2d(x, y).plus(size) * oneTexelSize - constructed[char.code] = GlyphInfo(size, uv1, uv2) + constructed[char] = GlyphInfo(size, uv1, uv2) heightCache[font] = max(heightCache.getDouble(font), size.y) // No compare set unfortunately x += charWidth } - fontMap[this] = constructed - bufferPool[this] = image + fontMap[this@buildBuffer] = constructed + bufferPool[this@buildBuffer] = image } // TODO: Change this when we've refactored the loadables @@ -219,7 +206,7 @@ object LambdaAtlas : Loadable { val str = "Loaded ${bufferPool.size} fonts" // avoid race condition runGameScheduled { - bufferPool.forEach { (owner, image) -> owner.upload(image) } + bufferPool.forEach { (owner, image) -> owner.uploadField(image) } bufferPool.clear() } @@ -255,4 +242,4 @@ object LambdaAtlas : Loadable { return charImage } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt index 805888f67..a80928512 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt @@ -22,6 +22,7 @@ import com.lambda.graphics.buffer.frame.FrameBuffer import com.lambda.graphics.shader.Shader import com.lambda.graphics.texture.Texture import com.lambda.util.math.Vec2d +import java.awt.image.BufferedImage /** * A class that represents a distance field texture, which is created by rendering a given texture @@ -29,12 +30,12 @@ import com.lambda.util.math.Vec2d * * The texture is used to create a signed distance field (SDF) for rendering operations. * - * @param texture The texture to be used for creating the distance field. + * @param image Image data to upload */ -class DistanceFieldTexture(texture: Texture) { - val frame = CachedFrame(texture.width, texture.height).write { +class DistanceFieldTexture(image: BufferedImage) : Texture(image) { + private val frame = CachedFrame(width, height).write { FrameBuffer.pipeline.use { - val (pos1, pos2) = Vec2d.ZERO to Vec2d(texture.width, texture.height) + val (pos1, pos2) = Vec2d.ZERO to Vec2d(width, height) grow(4) putQuad( @@ -46,13 +47,17 @@ class DistanceFieldTexture(texture: Texture) { shader.use() shader["u_TexelSize"] = Vec2d.ONE / pos2 - texture.bind() + super.bind(0) immediateDraw() } } + override fun bind(slot: Int) { + frame.bind(slot) + } + companion object { private val shader = Shader("signed_distance_field", "renderer/pos_tex") } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt index 82a740c70..149b41511 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt @@ -58,7 +58,7 @@ class AnimatedTexture(path: LambdaResource) : Texture(image = null, forceConsist gif.clear() - currentFrame = (currentFrame+1) % frames + currentFrame = (currentFrame + 1) % frames lastUpload = System.currentTimeMillis() } } diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index 5e920e93a..5c1ad77e1 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -109,7 +109,8 @@ open class Texture( init { image?.let { - bind() + // Don't use bind() because if a child class overrides the function we're screwed + bindTexture(id) upload(it) } } diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt index e0b50fdd0..5ace20405 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt @@ -17,36 +17,96 @@ package com.lambda.graphics.texture +import com.lambda.graphics.renderer.gui.font.sdf.DistanceFieldTexture import com.lambda.util.readImage -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import java.awt.image.BufferedImage +/** + * The [TextureOwner] object is responsible for managing textures owned by various objects in the render pipeline + */ object TextureOwner { - private val textureMap = Object2ObjectOpenHashMap() + private val textureMap = HashMap>() /** - * Returns the texture owned by a specific object + * Retrieves the first texture owned by the object */ val Any.texture: Texture - get() = textureMap.getValue(this@texture) + get() = textureMap.getValue(this@texture)[0] + + /** + * Retrieves a specific texture owned by the object by its index + * + * @param index The index of the texture to retrieve + * @return The texture [T] at the given index + */ + @Suppress("unchecked_cast") + fun Any.texture(index: Int) = + textureMap.getValue(this@texture)[index] as T /** - * Generate mipmap texture from data and associate it with its owner + * Binds a list of textures to texture slots, ensuring no more than 32 textures + * are bound at once (to fit within the typical GPU limitations) + * + * @param textures The list of objects that own textures to be bound. + * @throws IllegalArgumentException If more than 32 textures are provided. + */ + fun bind(vararg textures: Any) { + check(textures.size < 33) { "Texture slot overflow, expected to use less than 33 slots, got ${textures.size} slots" } + + textures.forEachIndexed { index, texture -> texture.texture.bind(index) } + } + + /** + * Binds a list of textures to texture slots, ensuring no more than 32 textures + * are bound at once (to fit within the typical GPU limitations) + * + * @param textures The list of textures to be bound + * @throws IllegalArgumentException If more than 32 textures are provided + */ + fun bind(vararg textures: Texture) { + check(textures.size < 33) { "Texture slot overflow, expected to use less than 33 slots, got ${textures.size} slots" } + + textures.forEachIndexed { index, texture -> texture.bind(index) } + } + + /** + * Uploads a texture from image data and associates it with the object, + * optionally generating mipmaps for the texture + * + * @param data The image data as a [BufferedImage] to create the texture + * @param mipmaps The number of mipmaps to generate for the texture (default is 1) + * @return The created texture object */ fun Any.upload(data: BufferedImage, mipmaps: Int = 1) = - Texture(data, levels = mipmaps).also { textureMap[this@upload] = it } + Texture(data, levels = mipmaps).also { textureMap.computeIfAbsent(this@upload) { mutableListOf() }.add(it) } /** - * Generate mipmap texture from data and associate it with its owner + * Uploads a texture from an image file path and associates it with the object, + * optionally generating mipmaps for the texture * - * @param path Lambda resource path containing the image data + * @param path The resource path to the image file + * @param mipmaps The number of mipmaps to generate for the texture (default is 1) + * @return The created texture object */ fun Any.upload(path: String, mipmaps: Int = 1) = - Texture(path.readImage(), levels = mipmaps).also { textureMap[this@upload] = it } + Texture(path.readImage(), levels = mipmaps).also { textureMap.computeIfAbsent(this@upload) { mutableListOf() }.add(it) } /** - * Loads a gif and associate it with its owner + * Uploads a distance field texture from image data and associates it with the object + * Distance field textures are commonly used for rendering fonts. + * + * @param data The image data as a [BufferedImage] to create the distance field texture + * @return The created distance field texture object + */ + fun Any.uploadField(data: BufferedImage) = + DistanceFieldTexture(data).also { textureMap.computeIfAbsent(this@uploadField) { mutableListOf() }.add(it) } + + /** + * Uploads a GIF and associates it with the object as an animated texture + * + * @param path The resource path to the GIF file + * @return The created animated texture object */ fun Any.uploadGif(path: String) = - AnimatedTexture(path).also { textureMap[this@uploadGif] = it } + AnimatedTexture(path).also { textureMap.computeIfAbsent(this@uploadGif) { mutableListOf() }.add(it) } } From 1b856ecd81c6ee5c6fe3980519d6beca2b200527 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Tue, 31 Dec 2024 11:44:41 -0500 Subject: [PATCH 073/114] better mipmap handling --- .../graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt | 2 +- .../src/main/kotlin/com/lambda/graphics/texture/Texture.kt | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt index a80928512..e7dcbdde4 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt @@ -32,7 +32,7 @@ import java.awt.image.BufferedImage * * @param image Image data to upload */ -class DistanceFieldTexture(image: BufferedImage) : Texture(image) { +class DistanceFieldTexture(image: BufferedImage) : Texture(image, levels = 0) { private val frame = CachedFrame(width, height).write { FrameBuffer.pipeline.use { val (pos1, pos2) = Vec2d.ZERO to Vec2d(width, height) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index 5c1ad77e1..d1965334d 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -20,12 +20,9 @@ package com.lambda.graphics.texture import com.lambda.graphics.texture.TextureUtils.bindTexture import com.lambda.graphics.texture.TextureUtils.readImage import com.lambda.graphics.texture.TextureUtils.setupTexture -import com.lambda.module.modules.client.RenderSettings -import org.lwjgl.opengl.GL11C import org.lwjgl.opengl.GL45C.* import java.awt.image.BufferedImage import java.lang.IllegalStateException -import java.nio.ByteBuffer /** * Represents a texture that can be uploaded and bound to the graphics pipeline @@ -77,9 +74,9 @@ open class Texture( initialized = true // Set this mipmap to `offset` to define the original texture - setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, readImage(image)) - glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack + setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) + if (levels > 1) glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack } open fun update(image: BufferedImage, offset: Int = 0) { From 26f72e945013e1535014d49fd5a60f2cd34d1476 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Tue, 31 Dec 2024 11:44:50 -0500 Subject: [PATCH 074/114] pbo doc --- .../kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt index db4933ee1..c8e590125 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt @@ -22,7 +22,6 @@ import com.lambda.graphics.gl.putTo import com.lambda.graphics.texture.Texture import com.lambda.util.math.MathUtils.toInt import org.lwjgl.opengl.GL45C.* -import java.lang.IllegalStateException import java.nio.ByteBuffer /** @@ -85,9 +84,10 @@ class PixelBuffer( init { if (!texture.initialized) throw IllegalStateException("Cannot use uninitialized textures for pixel buffers") + // We can't call the texture's bind method because the animated texture updates the + // data when binding the texture, causing a null pointer exception due to the animated + // texture object not being initialized glBindTexture(GL_TEXTURE_2D, texture.id) - - // Allocate texture storage glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture.width, texture.height, 0, texture.format, GL_UNSIGNED_BYTE, 0) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) From b52727dd336a3639ad21b9cef7afd1153f2c99ef Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Tue, 31 Dec 2024 11:51:49 -0500 Subject: [PATCH 075/114] moved the texture options before the tex call --- common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index d1965334d..9fe0f8464 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -74,8 +74,8 @@ open class Texture( initialized = true // Set this mipmap to `offset` to define the original texture - glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, readImage(image)) setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) + glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, readImage(image)) if (levels > 1) glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack } From 083a62ed5c42a6884ba4254798dbd3ed54ac4b38 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:27:01 -0500 Subject: [PATCH 076/114] removed bytearray upload to buffers --- .../main/kotlin/com/lambda/graphics/buffer/Buffer.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt index 8e63087fd..a292544e6 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt @@ -305,16 +305,6 @@ abstract class Buffer( return null } - /** - * Sets the given data into the client mapped memory and executes the provided processing function to manage data transfer. - * - * @param data Data to set in memory - * @param offset The starting offset within the buffer of the range to be mapped - * @return Error encountered during the mapping process - */ - open fun upload(data: ByteArray, offset: Long): Throwable? = - upload(ByteBuffer.wrap(data), offset) - /** * Sets the given data into the client mapped memory and executes the provided processing function to manage data transfer. * From 752bb58983e51e203c7bd6a1df7131ca35a6fce1 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:28:33 -0500 Subject: [PATCH 077/114] ref: texture api --- .../com/lambda/graphics/texture/Texture.kt | 165 +++++++++++++++--- 1 file changed, 136 insertions(+), 29 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index 9fe0f8464..b95e173c4 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -17,29 +17,69 @@ package com.lambda.graphics.texture +import com.lambda.graphics.renderer.gui.TextureRenderer import com.lambda.graphics.texture.TextureUtils.bindTexture import com.lambda.graphics.texture.TextureUtils.readImage import com.lambda.graphics.texture.TextureUtils.setupTexture +import com.lambda.util.math.Rect.Companion.basedOn +import com.lambda.util.math.Vec2d import org.lwjgl.opengl.GL45C.* import java.awt.image.BufferedImage import java.lang.IllegalStateException +import java.nio.ByteBuffer /** * Represents a texture that can be uploaded and bound to the graphics pipeline * Supports mipmap generation and LOD (Level of Detail) configuration - * - * @param image Optional initial image to upload to the texture - * @param format The format of the image passed in - * @param levels Number of mipmap levels to generate for the texture - * @param forceConsistency Flag to enforce consistency when updating the texture. If true, attempts to update - * the texture after initialization will throw an exception */ -open class Texture( - image: BufferedImage?, - val format: Int = GL_RGBA, - private val levels: Int = 4, - private val forceConsistency: Boolean = false, -) { +open class Texture{ + val format: Int + private val levels: Int + private val forceConsistency: Boolean + + /** + * @param image Optional initial image to upload to the texture + * @param format The format of the image passed in + * @param levels Number of mipmap levels to generate for the texture + * @param forceConsistency Flag to enforce consistency when updating the texture. If true, attempts to update + * the texture after initialization will throw an exception + */ + constructor(image: BufferedImage?, + format: Int = GL_RGBA, + levels: Int = 4, + forceConsistency: Boolean = false) + { + this.format = format + this.levels = levels + this.forceConsistency = forceConsistency + + image?.let { bindTexture(id); upload(it) } + } + + /** + * @param buffer The image buffer + * @param width The width of the image + * @param height The height of the image + * @param format The format of the image passed in + * @param levels Number of mipmap levels to generate for the texture + * @param forceConsistency Flag to enforce consistency when updating the texture. If true, attempts to update + * the texture after initialization will throw an exception + */ + constructor(buffer: ByteBuffer, + width: Int, + height: Int, + format: Int = GL_RGBA, + levels: Int = 4, + forceConsistency: Boolean = false) + { + this.format = format + this.levels = levels + this.forceConsistency = forceConsistency + + bindTexture(id) + upload(buffer, width, height) + } + /** * Indicates whether there is an initial texture or not */ @@ -57,12 +97,20 @@ open class Texture( } /** - * Uploads an image to the texture and generates mipmaps for the texture if applicable. + * Unbinds the currently bound texture + */ + open fun unbind(slot: Int = 0) { + bindTexture(0, slot) + } + + /** + * Uploads an image to the texture and generates mipmaps for the texture if applicable + * This function does not bind the texture * - * @param image The image to upload to the texture - * @param offset The mipmap level to upload the image to + * @param image The image to upload to the texture + * @param offset The mipmap level to upload the image to */ - open fun upload(image: BufferedImage, offset: Int = 0) { + fun upload(image: BufferedImage, offset: Int = 0) { if (forceConsistency && initialized) throw IllegalStateException("Client tried to update a texture, but the enforce consistency flag was present") // Store level_base +1 through `level` images and generate @@ -75,23 +123,90 @@ open class Texture( // Set this mipmap to `offset` to define the original texture setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) - glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, readImage(image)) + glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, format, GL_UNSIGNED_BYTE, readImage(image)) if (levels > 1) glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack } - open fun update(image: BufferedImage, offset: Int = 0) { + /** + * Uploads an image to the texture and generates mipmaps for the texture if applicable + * This function does not bind the texture + * + * @param buffer The image buffer to upload to the texture + * @param width The width of the texture + * @param height The height of the texture + * @param offset The mipmap level to upload the image to + */ + fun upload(buffer: ByteBuffer, width: Int, height: Int, offset: Int = 0) { + if (forceConsistency && initialized) throw IllegalStateException("Client tried to update a texture, but the enforce consistency flag was present") + + // Store level_base +1 through `level` images and generate + // mipmaps from them + setupLOD(levels = levels) + + this.width = width + this.height = height + initialized = true + + // Set this mipmap to `offset` to define the original texture + setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) + glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, format, GL_UNSIGNED_BYTE, buffer) + if (levels > 1) glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack + } + + /** + * Updates the data of a texture + * This function does not bind the texture + * + * @param image The image to upload to the texture + * @param offset The mipmap level to upload the image to + * + * @throws IllegalStateException If the texture has the consistency flag and is already initialized + */ + fun update(image: BufferedImage, offset: Int = 0) { if (!initialized) return upload(image, offset) if (forceConsistency && initialized) throw IllegalStateException("Client tried to update a texture, but the enforce consistency flag was present") - check(image.width + image.height > this.width + this.height && initialized) { + check(image.width + image.height <= this.width + this.height && initialized) { "Client tried to update a texture with more data than allowed" + "Expected ${this.width + this.height} bytes but got ${image.width + image.height}" } - // Can we rebuild LOD ? - glTexSubImage2D(GL_TEXTURE_2D, offset, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, readImage(image)) + glTexSubImage2D(GL_TEXTURE_2D, offset, 0, 0, width, height, format, GL_UNSIGNED_BYTE, readImage(image)) + } + + /** + * Updates the data of a texture + * This function does not bind the texture + * + * @param buffer The image buffer to upload to the texture + * @param width The width of the texture + * @param height The height of the texture + * @param offset The mipmap level to upload the image to + * + * @throws IllegalStateException If the texture has the consistency flag and is already initialized + */ + fun update(buffer: ByteBuffer, width: Int, height: Int, offset: Int = 0) { + if (!initialized) return upload(buffer, width, height, offset) + if (forceConsistency && initialized) throw IllegalStateException("Client tried to update a texture, but the enforce consistency flag was present") + + check(width + height <= this.width + this.height && initialized) { + "Client tried to update a texture with more data than allowed\n" + + "Expected ${this.width + this.height} bytes but got ${width + height}" + } + + glTexSubImage2D(GL_TEXTURE_2D, offset, 0, 0, width, height, format, GL_UNSIGNED_BYTE, buffer) } + /** + * Draws the texture + * This function binds the texture + * + * @param coord The top left coordinate to draw at + * @param scale The width and height multiplier + */ + fun draw(coord: Vec2d, scale: Double = 0.0) = + TextureRenderer.drawTexture(this, basedOn(coord, width * scale, height * scale)) + private fun setupLOD(levels: Int) { // When you call glTextureStorage, you're specifying the total number of levels, including level 0 // This is a 0-based index system, which means that the maximum mipmap level is n-1 @@ -103,12 +218,4 @@ open class Texture( glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, levels) } - - init { - image?.let { - // Don't use bind() because if a child class overrides the function we're screwed - bindTexture(id) - upload(it) - } - } } From 5a1d6c1754c6f1b72206daf87009ec67b56ab1dd Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:28:43 -0500 Subject: [PATCH 078/114] feat: map preview --- .../mixin/render/HandledScreenMixin.java | 47 +++++++++++++++ .../graphics/renderer/gui/TextureRenderer.kt | 1 + .../module/modules/render/MapPreview.kt | 58 +++++++++++++++++++ .../main/kotlin/com/lambda/util/math/Rect.kt | 3 + .../src/main/resources/lambda.accesswidener | 1 + .../main/resources/lambda.mixins.common.json | 1 + 6 files changed, 111 insertions(+) create mode 100644 common/src/main/java/com/lambda/mixin/render/HandledScreenMixin.java create mode 100644 common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt diff --git a/common/src/main/java/com/lambda/mixin/render/HandledScreenMixin.java b/common/src/main/java/com/lambda/mixin/render/HandledScreenMixin.java new file mode 100644 index 000000000..652517702 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/render/HandledScreenMixin.java @@ -0,0 +1,47 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.mixin.render; + +import com.lambda.module.modules.render.MapPreview; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.item.Items; +import net.minecraft.screen.slot.Slot; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import javax.annotation.Nullable; + +@Mixin(HandledScreen.class) +public abstract class HandledScreenMixin { + @Shadow @Nullable public Slot focusedSlot; + + @Inject(method = "drawMouseoverTooltip", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTooltip(Lnet/minecraft/client/font/TextRenderer;Ljava/util/List;Ljava/util/Optional;II)V"), cancellable = true) + void injectTooltip(DrawContext context, int x, int y, CallbackInfo ci) { + if (focusedSlot == null) return; + + var stack = focusedSlot.getStack(); + if (stack.isOf(Items.FILLED_MAP) && MapPreview.INSTANCE.isEnabled()) { + ci.cancel(); // fck off lmao + MapPreview.drawMap(stack, x, y); + } + } +} diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt index 82186f8d1..c040c7152 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt @@ -38,6 +38,7 @@ object TextureRenderer { shader.use() drawInternal(rect) + texture.unbind() } fun drawTextureShaded(texture: Texture, rect: Rect) { diff --git a/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt b/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt new file mode 100644 index 000000000..7e4a90a88 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.module.modules.render + +import com.lambda.graphics.buffer.pixel.PixelBuffer +import com.lambda.graphics.texture.Texture +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.threading.runSafe +import com.lambda.util.math.Vec2d +import net.minecraft.item.FilledMapItem +import net.minecraft.item.ItemStack +import org.lwjgl.BufferUtils +import org.lwjgl.opengl.GL11.GL_RGB +import org.lwjgl.opengl.GL11.GL_RGBA +import org.lwjgl.opengl.GL45C.GL_TEXTURE_2D +import org.lwjgl.opengl.GL45C.glBindTexture + +object MapPreview : Module( + name = "MapPreview", + description = "Preview maps in your inventory", + defaultTags = setOf(ModuleTag.RENDER) +) { + private val scale by setting("Scale", 0.7, 0.1..1.0, 0.05) + + private val buffer = BufferUtils.createByteBuffer(128*128) + private val texture = Texture(buffer, 128, 128, format = GL_RGB, levels = 1) + private val pbo = PixelBuffer(texture) + + @JvmStatic + fun drawMap(stack: ItemStack, x: Int, y: Int) = runSafe { + val state = FilledMapItem.getMapState(stack, world) ?: return@runSafe + buffer.put(state.colors) + buffer.flip() + + val base = Vec2d(x, y) + + texture.bind() + texture.update(buffer, 128, 128) + + texture.draw(base, scale) + } +} diff --git a/common/src/main/kotlin/com/lambda/util/math/Rect.kt b/common/src/main/kotlin/com/lambda/util/math/Rect.kt index 652d2bf16..bfa5790af 100644 --- a/common/src/main/kotlin/com/lambda/util/math/Rect.kt +++ b/common/src/main/kotlin/com/lambda/util/math/Rect.kt @@ -65,6 +65,9 @@ data class Rect(private val pos1: Vec2d, private val pos2: Vec2d) { fun basedOn(base: Vec2d, width: Double, height: Double) = Rect(base, base + Vec2d(width, height)) + fun basedOn(base: Vec2d, width: Int, height: Int) = + Rect(base, base + Vec2d(width, height)) + fun basedOn(base: Vec2d, size: Vec2d) = Rect(base, base + size) diff --git a/common/src/main/resources/lambda.accesswidener b/common/src/main/resources/lambda.accesswidener index ec7219d67..c5dcb4ee0 100644 --- a/common/src/main/resources/lambda.accesswidener +++ b/common/src/main/resources/lambda.accesswidener @@ -66,3 +66,4 @@ accessible field net/minecraft/network/packet/c2s/login/LoginKeyC2SPacket nonce accessible field net/minecraft/world/explosion/Explosion behavior Lnet/minecraft/world/explosion/ExplosionBehavior; accessible field net/minecraft/structure/StructureTemplate blockInfoLists Ljava/util/List; accessible method net/minecraft/item/BlockItem getPlacementState (Lnet/minecraft/item/ItemPlacementContext;)Lnet/minecraft/block/BlockState; +accessible field net/minecraft/client/gui/screen/ingame/HandledScreen focusedSlot Lnet/minecraft/screen/slot/Slot; diff --git a/common/src/main/resources/lambda.mixins.common.json b/common/src/main/resources/lambda.mixins.common.json index 3d1d2cc40..7995eb4a7 100644 --- a/common/src/main/resources/lambda.mixins.common.json +++ b/common/src/main/resources/lambda.mixins.common.json @@ -32,6 +32,7 @@ "render.DebugHudMixin", "render.GameRendererMixin", "render.GlStateManagerMixin", + "render.HandledScreenMixin", "render.InGameHudMixin", "render.InGameOverlayRendererMixin", "render.LightmapTextureManagerMixin", From 9350f0690a97319fe7039e09c903431d18a440f7 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:29:39 -0500 Subject: [PATCH 079/114] removed test pbo call --- .../main/kotlin/com/lambda/module/modules/render/MapPreview.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt b/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt index 7e4a90a88..4a34b9d7b 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt @@ -40,7 +40,6 @@ object MapPreview : Module( private val buffer = BufferUtils.createByteBuffer(128*128) private val texture = Texture(buffer, 128, 128, format = GL_RGB, levels = 1) - private val pbo = PixelBuffer(texture) @JvmStatic fun drawMap(stack: ItemStack, x: Int, y: Int) = runSafe { From 1c68158d966e13c9d3506458ee54011d3bf2b856 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:42:08 -0500 Subject: [PATCH 080/114] map gl format to native format --- .../kotlin/com/lambda/graphics/texture/Texture.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index b95e173c4..da0faf7c4 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -23,6 +23,7 @@ import com.lambda.graphics.texture.TextureUtils.readImage import com.lambda.graphics.texture.TextureUtils.setupTexture import com.lambda.util.math.Rect.Companion.basedOn import com.lambda.util.math.Vec2d +import net.minecraft.client.texture.NativeImage import org.lwjgl.opengl.GL45C.* import java.awt.image.BufferedImage import java.lang.IllegalStateException @@ -123,7 +124,7 @@ open class Texture{ // Set this mipmap to `offset` to define the original texture setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) - glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, format, GL_UNSIGNED_BYTE, readImage(image)) + glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, format, GL_UNSIGNED_BYTE, readImage(image, getNativeFormat(format))) if (levels > 1) glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack } @@ -171,7 +172,7 @@ open class Texture{ "Expected ${this.width + this.height} bytes but got ${image.width + image.height}" } - glTexSubImage2D(GL_TEXTURE_2D, offset, 0, 0, width, height, format, GL_UNSIGNED_BYTE, readImage(image)) + glTexSubImage2D(GL_TEXTURE_2D, offset, 0, 0, width, height, format, GL_UNSIGNED_BYTE, readImage(image, getNativeFormat(format))) } /** @@ -218,4 +219,10 @@ open class Texture{ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, levels) } + + private fun getNativeFormat(gl: Int) = + when (gl) { + GL_RGB -> NativeImage.Format.RGB + else -> NativeImage.Format.RGBA + } } From bbee3b96125fe3d75581b5e959819cd43c3f20df Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:44:26 -0500 Subject: [PATCH 081/114] add luminance mapping --- common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index da0faf7c4..eb4d91303 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -222,6 +222,8 @@ open class Texture{ private fun getNativeFormat(gl: Int) = when (gl) { + GL_RED, GL_GREEN, GL_BLUE -> NativeImage.Format.LUMINANCE + GL_RG -> NativeImage.Format.LUMINANCE_ALPHA GL_RGB -> NativeImage.Format.RGB else -> NativeImage.Format.RGBA } From a5333d7da09affc229a6987dc83644aacba0ded0 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 10 Jan 2025 20:34:24 -0500 Subject: [PATCH 082/114] internal format texture api --- .../kotlin/com/lambda/graphics/texture/Texture.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index eb4d91303..ff82abeef 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -33,7 +33,8 @@ import java.nio.ByteBuffer * Represents a texture that can be uploaded and bound to the graphics pipeline * Supports mipmap generation and LOD (Level of Detail) configuration */ -open class Texture{ +open class Texture { + val internalFormat: Int val format: Int private val levels: Int private val forceConsistency: Boolean @@ -46,10 +47,12 @@ open class Texture{ * the texture after initialization will throw an exception */ constructor(image: BufferedImage?, + internalFormat: Int = GL_RGBA, format: Int = GL_RGBA, levels: Int = 4, forceConsistency: Boolean = false) { + this.internalFormat = internalFormat this.format = format this.levels = levels this.forceConsistency = forceConsistency @@ -69,10 +72,12 @@ open class Texture{ constructor(buffer: ByteBuffer, width: Int, height: Int, + internalFormat: Int = GL_RGBA, format: Int = GL_RGBA, levels: Int = 4, forceConsistency: Boolean = false) { + this.internalFormat = internalFormat this.format = format this.levels = levels this.forceConsistency = forceConsistency @@ -124,7 +129,7 @@ open class Texture{ // Set this mipmap to `offset` to define the original texture setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) - glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, format, GL_UNSIGNED_BYTE, readImage(image, getNativeFormat(format))) + glTexImage2D(GL_TEXTURE_2D, offset, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, readImage(image, getNativeFormat(format))) if (levels > 1) glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack } @@ -150,7 +155,7 @@ open class Texture{ // Set this mipmap to `offset` to define the original texture setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) - glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, format, GL_UNSIGNED_BYTE, buffer) + glTexImage2D(GL_TEXTURE_2D, offset, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, buffer) if (levels > 1) glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack } From 0f6f0a39fe35eaf713158a2d0b4547351360f45b Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 10 Jan 2025 20:34:36 -0500 Subject: [PATCH 083/114] removed handled screen mixin --- .../mixin/render/HandledScreenMixin.java | 47 ------------------- .../main/resources/lambda.mixins.common.json | 2 +- 2 files changed, 1 insertion(+), 48 deletions(-) delete mode 100644 common/src/main/java/com/lambda/mixin/render/HandledScreenMixin.java diff --git a/common/src/main/java/com/lambda/mixin/render/HandledScreenMixin.java b/common/src/main/java/com/lambda/mixin/render/HandledScreenMixin.java deleted file mode 100644 index 652517702..000000000 --- a/common/src/main/java/com/lambda/mixin/render/HandledScreenMixin.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.mixin.render; - -import com.lambda.module.modules.render.MapPreview; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.screen.ingame.HandledScreen; -import net.minecraft.item.Items; -import net.minecraft.screen.slot.Slot; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import javax.annotation.Nullable; - -@Mixin(HandledScreen.class) -public abstract class HandledScreenMixin { - @Shadow @Nullable public Slot focusedSlot; - - @Inject(method = "drawMouseoverTooltip", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTooltip(Lnet/minecraft/client/font/TextRenderer;Ljava/util/List;Ljava/util/Optional;II)V"), cancellable = true) - void injectTooltip(DrawContext context, int x, int y, CallbackInfo ci) { - if (focusedSlot == null) return; - - var stack = focusedSlot.getStack(); - if (stack.isOf(Items.FILLED_MAP) && MapPreview.INSTANCE.isEnabled()) { - ci.cancel(); // fck off lmao - MapPreview.drawMap(stack, x, y); - } - } -} diff --git a/common/src/main/resources/lambda.mixins.common.json b/common/src/main/resources/lambda.mixins.common.json index 7995eb4a7..d34eadf20 100644 --- a/common/src/main/resources/lambda.mixins.common.json +++ b/common/src/main/resources/lambda.mixins.common.json @@ -30,9 +30,9 @@ "render.ChatInputSuggestorMixin", "render.ChatScreenMixin", "render.DebugHudMixin", + "render.DrawContextMixin", "render.GameRendererMixin", "render.GlStateManagerMixin", - "render.HandledScreenMixin", "render.InGameHudMixin", "render.InGameOverlayRendererMixin", "render.LightmapTextureManagerMixin", From 4f4c374a50d01369d8fdf9eed59a1653e6dfabd6 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 10 Jan 2025 20:34:43 -0500 Subject: [PATCH 084/114] feat: map preview --- .../lambda/mixin/render/DrawContextMixin.java | 64 +++++++++++++++++++ .../module/modules/render/MapPreview.kt | 61 ++++++++++++------ 2 files changed, 104 insertions(+), 21 deletions(-) create mode 100644 common/src/main/java/com/lambda/mixin/render/DrawContextMixin.java diff --git a/common/src/main/java/com/lambda/mixin/render/DrawContextMixin.java b/common/src/main/java/com/lambda/mixin/render/DrawContextMixin.java new file mode 100644 index 000000000..d71452858 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/render/DrawContextMixin.java @@ -0,0 +1,64 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.mixin.render; + +import com.lambda.Lambda; +import com.lambda.module.modules.render.MapPreview; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.gui.tooltip.HoveredTooltipPositioner; +import net.minecraft.client.gui.tooltip.TooltipBackgroundRenderer; +import net.minecraft.client.gui.tooltip.TooltipComponent; +import net.minecraft.client.gui.tooltip.TooltipPositioner; +import net.minecraft.client.item.TooltipData; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Mixin(DrawContext.class) +public abstract class DrawContextMixin { + @Shadow protected abstract void drawTooltip(TextRenderer textRenderer, List components, int x, int y, TooltipPositioner positioner); + + @Inject(method = "drawTooltip(Lnet/minecraft/client/font/TextRenderer;Ljava/util/List;Ljava/util/Optional;II)V", at = @At("HEAD"), cancellable = true) + void drawItemTooltip(TextRenderer textRenderer, List text, Optional data, int x, int y, CallbackInfo ci) { + List list = text.stream().map(Text::asOrderedText).map(TooltipComponent::of).collect(Collectors.toList()); + data.ifPresent(datax -> list.add(1, TooltipComponent.of(datax))); + + var screen = (HandledScreen) Lambda.getMc().currentScreen; + if (screen.focusedSlot != null) { + var stack = screen.focusedSlot.getStack(); + if (stack.isOf(Items.FILLED_MAP)) list.add(1, new MapPreview.MapComponent(stack)); + } + + drawTooltip(textRenderer, list, x, y, HoveredTooltipPositioner.INSTANCE); + ci.cancel(); + } +} diff --git a/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt b/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt index 4a34b9d7b..876947654 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt @@ -17,41 +17,60 @@ package com.lambda.module.modules.render -import com.lambda.graphics.buffer.pixel.PixelBuffer -import com.lambda.graphics.texture.Texture +import com.lambda.Lambda.mc import com.lambda.module.Module import com.lambda.module.tag.ModuleTag -import com.lambda.threading.runSafe -import com.lambda.util.math.Vec2d +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.client.font.TextRenderer +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.tooltip.TooltipComponent +import net.minecraft.client.render.MapRenderer import net.minecraft.item.FilledMapItem import net.minecraft.item.ItemStack -import org.lwjgl.BufferUtils -import org.lwjgl.opengl.GL11.GL_RGB -import org.lwjgl.opengl.GL11.GL_RGBA -import org.lwjgl.opengl.GL45C.GL_TEXTURE_2D -import org.lwjgl.opengl.GL45C.glBindTexture +import net.minecraft.item.map.MapState +import net.minecraft.util.Identifier + object MapPreview : Module( name = "MapPreview", description = "Preview maps in your inventory", defaultTags = setOf(ModuleTag.RENDER) ) { - private val scale by setting("Scale", 0.7, 0.1..1.0, 0.05) + private val scale by setting("Scale", 0.7f, 0.1f..1.0f, 0.05f) + + private val background = Identifier("textures/map/map_background.png") + + // The map component is added via the draw context mixin, thanks mojang + class MapComponent(val stack: ItemStack) : TooltipComponent { + val state: MapState? + get() = FilledMapItem.getMapState(stack, mc.world) + + val mapId: Int? + get() = FilledMapItem.getMapId(stack) + + override fun drawItems(fontRenderer: TextRenderer, x: Int, y: Int, context: DrawContext) { + mapId?.let { id -> + // Values taken from net.minecraft.client.render.item.HeldItemRenderer.renderFirstPersonMap + + val matrices = context.matrices - private val buffer = BufferUtils.createByteBuffer(128*128) - private val texture = Texture(buffer, 128, 128, format = GL_RGB, levels = 1) + matrices.push() + matrices.translate(x + 3.0, y + 3.0, 500.0) + matrices.scale(scale, scale, 1f) - @JvmStatic - fun drawMap(stack: ItemStack, x: Int, y: Int) = runSafe { - val state = FilledMapItem.getMapState(stack, world) ?: return@runSafe - buffer.put(state.colors) - buffer.flip() + RenderSystem.enableBlend() + context.drawTexture(background, -7, -7, 0f, 0f, 142, 142, 142, 142) - val base = Vec2d(x, y) + matrices.translate(0.0, 0.0, 1.0) + mc.gameRenderer.mapRenderer.draw(matrices, context.vertexConsumers, id, state, true, 240) + } + } - texture.bind() - texture.update(buffer, 128, 128) + override fun getHeight(): Int { + return if (FilledMapItem.getMapState(stack, mc.world) != null) 100 + else 0 + } - texture.draw(base, scale) + override fun getWidth(textRenderer: TextRenderer) = 72 } } From 662dd8b30b041186ec6a48734e4a41ea1307f6ea Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 10 Jan 2025 20:42:22 -0500 Subject: [PATCH 085/114] fixed map scale --- .../kotlin/com/lambda/module/modules/render/MapPreview.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt b/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt index 876947654..c4a139a9b 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt @@ -24,7 +24,6 @@ import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.client.font.TextRenderer import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.tooltip.TooltipComponent -import net.minecraft.client.render.MapRenderer import net.minecraft.item.FilledMapItem import net.minecraft.item.ItemStack import net.minecraft.item.map.MapState @@ -36,8 +35,6 @@ object MapPreview : Module( description = "Preview maps in your inventory", defaultTags = setOf(ModuleTag.RENDER) ) { - private val scale by setting("Scale", 0.7f, 0.1f..1.0f, 0.05f) - private val background = Identifier("textures/map/map_background.png") // The map component is added via the draw context mixin, thanks mojang @@ -56,7 +53,7 @@ object MapPreview : Module( matrices.push() matrices.translate(x + 3.0, y + 3.0, 500.0) - matrices.scale(scale, scale, 1f) + matrices.scale(0.7f, 0.7f, 1f) RenderSystem.enableBlend() context.drawTexture(background, -7, -7, 0f, 0f, 142, 142, 142, 142) From f354999aaccb72408ee323dfeecc3484ae3eabb3 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 11 Jan 2025 10:56:12 -0500 Subject: [PATCH 086/114] ref: texture class --- .../graphics/renderer/gui/TextureRenderer.kt | 1 - .../graphics/texture/AnimatedTexture.kt | 2 +- .../com/lambda/graphics/texture/Texture.kt | 112 +++++++----------- .../lambda/graphics/texture/TextureUtils.kt | 4 +- .../kotlin/com/lambda/module/hud/Watermark.kt | 2 - 5 files changed, 45 insertions(+), 76 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt index c040c7152..82186f8d1 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt @@ -38,7 +38,6 @@ object TextureRenderer { shader.use() drawInternal(rect) - texture.unbind() } fun drawTextureShaded(texture: Texture, rect: Rect) { diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt index 149b41511..6229c2c17 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt @@ -26,7 +26,7 @@ import org.lwjgl.stb.STBImage import java.nio.ByteBuffer -class AnimatedTexture(path: LambdaResource) : Texture(image = null, forceConsistency = false) { +class AnimatedTexture(path: LambdaResource) : Texture(image = null) { private val pbo: PixelBuffer private val gif: ByteBuffer // Do NOT free this pointer private val frameDurations: IntArray diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index ff82abeef..5ac12882a 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -17,45 +17,34 @@ package com.lambda.graphics.texture -import com.lambda.graphics.renderer.gui.TextureRenderer import com.lambda.graphics.texture.TextureUtils.bindTexture import com.lambda.graphics.texture.TextureUtils.readImage import com.lambda.graphics.texture.TextureUtils.setupTexture -import com.lambda.util.math.Rect.Companion.basedOn -import com.lambda.util.math.Vec2d import net.minecraft.client.texture.NativeImage import org.lwjgl.opengl.GL45C.* import java.awt.image.BufferedImage -import java.lang.IllegalStateException +import java.awt.image.BufferedImage.* import java.nio.ByteBuffer +import kotlin.IllegalStateException /** * Represents a texture that can be uploaded and bound to the graphics pipeline * Supports mipmap generation and LOD (Level of Detail) configuration */ open class Texture { - val internalFormat: Int val format: Int private val levels: Int - private val forceConsistency: Boolean + private val nativeFormat: NativeImage.Format // For mojang native images /** * @param image Optional initial image to upload to the texture - * @param format The format of the image passed in + * @param format The format of the image passed in, if the [image] is null, then you must pass the appropriate format * @param levels Number of mipmap levels to generate for the texture - * @param forceConsistency Flag to enforce consistency when updating the texture. If true, attempts to update - * the texture after initialization will throw an exception */ - constructor(image: BufferedImage?, - internalFormat: Int = GL_RGBA, - format: Int = GL_RGBA, - levels: Int = 4, - forceConsistency: Boolean = false) - { - this.internalFormat = internalFormat - this.format = format + constructor(image: BufferedImage?, format: Int = GL_RGBA, levels: Int = 4) { + this.format = image?.type?.let { bufferedMapping[it] } ?: format this.levels = levels - this.forceConsistency = forceConsistency + this.nativeFormat = nativeMapping.getOrDefault(format, NativeImage.Format.RGBA) image?.let { bindTexture(id); upload(it) } } @@ -64,23 +53,13 @@ open class Texture { * @param buffer The image buffer * @param width The width of the image * @param height The height of the image - * @param format The format of the image passed in + * @param format The format of the image passed in, must be specified * @param levels Number of mipmap levels to generate for the texture - * @param forceConsistency Flag to enforce consistency when updating the texture. If true, attempts to update - * the texture after initialization will throw an exception */ - constructor(buffer: ByteBuffer, - width: Int, - height: Int, - internalFormat: Int = GL_RGBA, - format: Int = GL_RGBA, - levels: Int = 4, - forceConsistency: Boolean = false) - { - this.internalFormat = internalFormat + constructor(buffer: ByteBuffer, width: Int, height: Int, format: Int, levels: Int = 4) { this.format = format this.levels = levels - this.forceConsistency = forceConsistency + this.nativeFormat = nativeMapping.getOrDefault(format, NativeImage.Format.RGBA) bindTexture(id) upload(buffer, width, height) @@ -117,11 +96,9 @@ open class Texture { * @param offset The mipmap level to upload the image to */ fun upload(image: BufferedImage, offset: Int = 0) { - if (forceConsistency && initialized) throw IllegalStateException("Client tried to update a texture, but the enforce consistency flag was present") - // Store level_base +1 through `level` images and generate // mipmaps from them - setupLOD(levels = levels) + setupLOD(levels) width = image.width height = image.height @@ -129,8 +106,8 @@ open class Texture { // Set this mipmap to `offset` to define the original texture setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) - glTexImage2D(GL_TEXTURE_2D, offset, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, readImage(image, getNativeFormat(format))) - if (levels > 1) glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack + glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, format, GL_UNSIGNED_BYTE, readImage(image, nativeFormat)) + if (levels > 0) glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack } /** @@ -143,11 +120,9 @@ open class Texture { * @param offset The mipmap level to upload the image to */ fun upload(buffer: ByteBuffer, width: Int, height: Int, offset: Int = 0) { - if (forceConsistency && initialized) throw IllegalStateException("Client tried to update a texture, but the enforce consistency flag was present") - // Store level_base +1 through `level` images and generate // mipmaps from them - setupLOD(levels = levels) + setupLOD(levels) this.width = width this.height = height @@ -155,8 +130,8 @@ open class Texture { // Set this mipmap to `offset` to define the original texture setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR) - glTexImage2D(GL_TEXTURE_2D, offset, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, buffer) - if (levels > 1) glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack + glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, format, GL_UNSIGNED_BYTE, buffer) + if (levels > 0) glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack } /** @@ -170,14 +145,9 @@ open class Texture { */ fun update(image: BufferedImage, offset: Int = 0) { if (!initialized) return upload(image, offset) - if (forceConsistency && initialized) throw IllegalStateException("Client tried to update a texture, but the enforce consistency flag was present") - - check(image.width + image.height <= this.width + this.height && initialized) { - "Client tried to update a texture with more data than allowed" + - "Expected ${this.width + this.height} bytes but got ${image.width + image.height}" - } - glTexSubImage2D(GL_TEXTURE_2D, offset, 0, 0, width, height, format, GL_UNSIGNED_BYTE, readImage(image, getNativeFormat(format))) + checkDimensions(width, height) + glTexSubImage2D(GL_TEXTURE_2D, offset, 0, 0, width, height, format, GL_UNSIGNED_BYTE, readImage(image, nativeFormat)) } /** @@ -193,26 +163,11 @@ open class Texture { */ fun update(buffer: ByteBuffer, width: Int, height: Int, offset: Int = 0) { if (!initialized) return upload(buffer, width, height, offset) - if (forceConsistency && initialized) throw IllegalStateException("Client tried to update a texture, but the enforce consistency flag was present") - - check(width + height <= this.width + this.height && initialized) { - "Client tried to update a texture with more data than allowed\n" + - "Expected ${this.width + this.height} bytes but got ${width + height}" - } + checkDimensions(width, height) glTexSubImage2D(GL_TEXTURE_2D, offset, 0, 0, width, height, format, GL_UNSIGNED_BYTE, buffer) } - /** - * Draws the texture - * This function binds the texture - * - * @param coord The top left coordinate to draw at - * @param scale The width and height multiplier - */ - fun draw(coord: Vec2d, scale: Double = 0.0) = - TextureRenderer.drawTexture(this, basedOn(coord, width * scale, height * scale)) - private fun setupLOD(levels: Int) { // When you call glTextureStorage, you're specifying the total number of levels, including level 0 // This is a 0-based index system, which means that the maximum mipmap level is n-1 @@ -225,11 +180,28 @@ open class Texture { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, levels) } - private fun getNativeFormat(gl: Int) = - when (gl) { - GL_RED, GL_GREEN, GL_BLUE -> NativeImage.Format.LUMINANCE - GL_RG -> NativeImage.Format.LUMINANCE_ALPHA - GL_RGB -> NativeImage.Format.RGB - else -> NativeImage.Format.RGBA + private fun checkDimensions(width: Int, height: Int) = + check(width + height <= this.width + this.height && initialized) { + "Client tried to update a texture with more data than allowed\n" + + "Expected ${this.width + this.height} bytes but got ${width + height}" } + + companion object { + private val nativeMapping = mapOf( + GL_RED to NativeImage.Format.LUMINANCE, + GL_GREEN to NativeImage.Format.LUMINANCE, + GL_BLUE to NativeImage.Format.LUMINANCE, + GL_RG to NativeImage.Format.LUMINANCE_ALPHA, + GL_RGB to NativeImage.Format.RGB, + GL_RGBA to NativeImage.Format.RGBA, + ) + + private val bufferedMapping = mapOf( + TYPE_BYTE_BINARY to GL_RED, + TYPE_BYTE_GRAY to GL_RG, + TYPE_INT_RGB to GL_RGB, + TYPE_INT_ARGB to GL_RGBA, + TYPE_4BYTE_ABGR to GL_BGRA, + ) + } } diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt index 2e84e7154..fada950a8 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt @@ -52,7 +52,7 @@ object TextureUtils { fun readImage( bufferedImage: BufferedImage, - format: NativeImage.Format = NativeImage.Format.RGBA, + format: NativeImage.Format, ): Long { val bytes = encoderPreset .withBufferedImage(bufferedImage) @@ -68,6 +68,6 @@ object TextureUtils { fun readImage( image: ByteBuffer, - format: NativeImage.Format = NativeImage.Format.RGBA, + format: NativeImage.Format, ) = NativeImage.read(format, image).pointer } diff --git a/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt b/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt index 8d61d3ddd..794738eb0 100644 --- a/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt +++ b/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt @@ -20,10 +20,8 @@ package com.lambda.module.hud import com.lambda.graphics.renderer.gui.TextureRenderer.drawTexture import com.lambda.graphics.renderer.gui.TextureRenderer.drawTextureShaded import com.lambda.graphics.texture.TextureOwner.upload -import com.lambda.graphics.texture.TextureOwner.uploadGif import com.lambda.module.HudModule import com.lambda.module.tag.ModuleTag -import com.lambda.util.math.Vec2d object Watermark : HudModule( name = "Watermark", From 2c7c377bbdade6c73566aa4ff3f37f9dc9a3fdcb Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 11 Jan 2025 15:01:37 -0500 Subject: [PATCH 087/114] centered the map tooltip --- .../main/kotlin/com/lambda/module/modules/render/MapPreview.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt b/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt index c4a139a9b..e299d1ff5 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt @@ -52,7 +52,7 @@ object MapPreview : Module( val matrices = context.matrices matrices.push() - matrices.translate(x + 3.0, y + 3.0, 500.0) + matrices.translate(x + 4.0, y + 4.0, 500.0) matrices.scale(0.7f, 0.7f, 1f) RenderSystem.enableBlend() From a30f575b865c9b8fbc19ac8b16a4029e3be5294d Mon Sep 17 00:00:00 2001 From: Constructor Date: Thu, 16 Jan 2025 23:47:03 +0100 Subject: [PATCH 088/114] Resolve merge conflicts --- .../java/com/lambda/mixin/render/SplashOverlayMixin.java | 4 ++-- .../com/lambda/graphics/renderer/gui/font/FontRenderer.kt | 2 +- common/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt | 2 +- .../main/kotlin/com/lambda/module/modules/render/BlockESP.kt | 5 ++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/common/src/main/java/com/lambda/mixin/render/SplashOverlayMixin.java b/common/src/main/java/com/lambda/mixin/render/SplashOverlayMixin.java index d3a42ba78..3787b762b 100644 --- a/common/src/main/java/com/lambda/mixin/render/SplashOverlayMixin.java +++ b/common/src/main/java/com/lambda/mixin/render/SplashOverlayMixin.java @@ -17,7 +17,7 @@ package com.lambda.mixin.render; -import com.lambda.util.LambdaResource; +import com.lambda.util.LambdaResourceKt; import net.minecraft.client.gui.screen.SplashOverlay; import net.minecraft.client.texture.ResourceTexture; import net.minecraft.resource.DefaultResourcePack; @@ -62,7 +62,7 @@ public LogoTextureMixin(Identifier location) { @Redirect(method = "loadTextureData", at = @At(value = "INVOKE", target = "Lnet/minecraft/resource/DefaultResourcePack;open(Lnet/minecraft/resource/ResourceType;Lnet/minecraft/util/Identifier;)Lnet/minecraft/resource/InputSupplier;")) InputSupplier loadTextureData(DefaultResourcePack instance, ResourceType type, Identifier id) { - return () -> new LambdaResource("textures/lambda_banner.png").getStream(); + return () -> LambdaResourceKt.getStream("textures/lambda_banner.png"); } } } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index 4b35cab54..4169b00e3 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -66,7 +66,7 @@ object FontRenderer { scale: Double = 1.0, shadow: Boolean = RenderSettings.shadow, parseEmoji: Boolean = LambdaMoji.isEnabled - ) = processText(text, color, scale, shadow, parseEmoji) { char, pos1, pos2, color -> buildGlyph(char, position, pos1, pos2, color) }.also { + ) = processText(text, color, scale, shadow, parseEmoji) { char, pos1, pos2, col -> buildGlyph(char, position, pos1, pos2, col) }.also { UIPipeline.objectDrawn() } diff --git a/common/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt b/common/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt index 8241949b7..08ea9fe19 100644 --- a/common/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt +++ b/common/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt @@ -32,7 +32,7 @@ object TaskFlowHUD : HudModule( init { onRender { - drawString("TaskFlow", Vec2d.ZERO) + drawString(TaskFlow.toString(), Vec2d.ZERO) } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt b/common/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt index c347fac40..09a849f2e 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt @@ -21,9 +21,8 @@ import com.lambda.Lambda.mc import com.lambda.graphics.renderer.esp.ChunkedESP.Companion.newChunkedESP import com.lambda.graphics.renderer.esp.DirectionMask import com.lambda.graphics.renderer.esp.DirectionMask.buildSideMesh -import com.lambda.graphics.renderer.esp.ESPRenderer -import com.lambda.graphics.renderer.esp.builders.buildFilled -import com.lambda.graphics.renderer.esp.builders.buildOutline +import com.lambda.graphics.renderer.esp.builders.buildFilledMesh +import com.lambda.graphics.renderer.esp.builders.buildOutlineMesh import com.lambda.graphics.renderer.esp.impl.StaticESPRenderer import com.lambda.module.Module import com.lambda.module.tag.ModuleTag From a0189f7534f53a62e99bd40622e5b5fee559a8fd Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 25 Jan 2025 14:58:16 -0500 Subject: [PATCH 089/114] MapDownloader module --- .../module/modules/player/MapDownloader.kt | 81 +++++++++++++++++++ .../kotlin/com/lambda/util/FolderRegister.kt | 3 +- .../kotlin/com/lambda/util/StringUtils.kt | 30 +++++++ 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 common/src/main/kotlin/com/lambda/module/modules/player/MapDownloader.kt diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/MapDownloader.kt b/common/src/main/kotlin/com/lambda/module/modules/player/MapDownloader.kt new file mode 100644 index 000000000..74a5da185 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/player/MapDownloader.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.module.modules.player + +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.util.FolderRegister +import com.lambda.util.FolderRegister.locationBoundDirectory +import com.lambda.util.StringUtils.hash +import com.lambda.util.player.SlotUtils.combined +import com.lambda.util.world.entitySearch +import net.minecraft.block.MapColor +import net.minecraft.entity.decoration.ItemFrameEntity +import net.minecraft.item.FilledMapItem +import net.minecraft.item.map.MapState +import java.awt.image.BufferedImage +import javax.imageio.ImageIO + +object MapDownloader : Module( + name = "MapDownloader", + description = "Save map data to your computer", + defaultTags = setOf(ModuleTag.PLAYER), +) { + init { + listen { + val mapStates = entitySearch(128.0) + .mapNotNull { FilledMapItem.getMapState(it.heldItemStack, world) } + + player.combined.mapNotNull { FilledMapItem.getMapState(it, world) } + + mapStates.forEach { map -> + val name = map.hash + val image = map.toBufferedImage() + + val file = FolderRegister.maps.toFile().locationBoundDirectory().resolve("$name.png") + if (file.exists()) return@listen + + ImageIO.write(image, "png", file) + } + } + } + + private val MapState.hash: String + get() = colors.hash("SHA-256") + + fun MapState.toBufferedImage(): BufferedImage { + val image = BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB) + + repeat(128) { x -> + repeat(128) { y -> + val index = colors[x + y * 128].toInt() + val color = MapColor.getRenderColor(index) + + val b = (color shr 16) and 0xFF + val g = (color shr 8) and 0xFF + val r = (color shr 0) and 0xFF + + val argb = -0x1000000 or (r shl 16) or (g shl 8) or (b shl 0) + image.setRGB(x, y, argb) + } + } + + return image + } +} diff --git a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt index b436dff5d..28348f60a 100644 --- a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt +++ b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt @@ -48,9 +48,10 @@ object FolderRegister : Loadable { val replay: Path = lambda.resolve("replay") val cache: Path = lambda.resolve("cache") val structure: Path = lambda.resolve("structure") + val maps: Path = lambda.resolve("maps") override fun load(): String { - val folders = listOf(lambda, config, packetLogs, replay, cache, structure) + val folders = listOf(lambda, config, packetLogs, replay, cache, structure, maps) val createdFolders = folders.mapNotNull { if (it.notExists()) { it.createDirectories() diff --git a/common/src/main/kotlin/com/lambda/util/StringUtils.kt b/common/src/main/kotlin/com/lambda/util/StringUtils.kt index ef1ce4fdf..28eaa1894 100644 --- a/common/src/main/kotlin/com/lambda/util/StringUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/StringUtils.kt @@ -17,6 +17,8 @@ package com.lambda.util +import java.security.MessageDigest + object StringUtils { /** * Returns a sanitized file path for both Unix and Linux systems @@ -89,4 +91,32 @@ object StringUtils { return cost[len0 - 1] } + + /** + * See [MessageDigest section](https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#messagedigest-algorithms) of the Java Security Standard Algorithm Names Specification + * + * @receiver The string to hash + * @param algorithm The algorithm instance to use + * + * @return The string representation of the hash + */ + fun String.hash(algorithm: String): String = + MessageDigest + .getInstance(algorithm) + .digest(toByteArray()) + .joinToString(separator = "") { "%02x".format(it) } + + /** + * See [MessageDigest section](https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#messagedigest-algorithms) of the Java Security Standard Algorithm Names Specification + * + * @receiver The byte array to hash + * @param algorithm The algorithm instance to use + * + * @return The string representation of the hash + */ + fun ByteArray.hash(algorithm: String): String = + MessageDigest + .getInstance(algorithm) + .digest(this) + .joinToString(separator = "") { "%02x".format(it) } } From 4bce87523441b3d18b1b0e54a4b47a6d57a533b4 Mon Sep 17 00:00:00 2001 From: blade Date: Wed, 12 Feb 2025 01:28:03 +0300 Subject: [PATCH 090/114] Working immediate mode ui --- .../com/lambda/event/events/GuiEvent.kt | 1 + .../com/lambda/event/events/RenderEvent.kt | 1 - .../kotlin/com/lambda/graphics/RenderMain.kt | 8 +- .../com/lambda/graphics/buffer/Buffer.kt | 7 +- .../lambda/graphics/buffer/VertexPipeline.kt | 5 + .../buffer/vertex/attributes/VertexAttrib.kt | 7 - .../kotlin/com/lambda/graphics/gl/Matrices.kt | 21 +- .../graphics/pipeline/ScissorAdapter.kt | 68 ++--- .../lambda/graphics/pipeline/UIPipeline.kt | 42 --- .../graphics/renderer/esp/ESPRenderer.kt | 5 +- ...RectRenderer.kt => AbstractGUIRenderer.kt} | 36 ++- .../graphics/renderer/gui/TextureRenderer.kt | 19 +- .../renderer/gui/font/FontRenderer.kt | 57 ++-- .../gui/font/sdf/DistanceFieldTexture.kt | 7 +- .../renderer/gui/rect/FilledRectRenderer.kt | 36 ++- .../renderer/gui/rect/OutlineRectRenderer.kt | 24 +- .../com/lambda/graphics/shader/Shader.kt | 16 +- .../kotlin/com/lambda/gui/LambdaScreen.kt | 1 + .../kotlin/com/lambda/gui/ScreenLayout.kt | 4 +- .../lambda/gui/component/core/FilledRect.kt | 36 +-- .../lambda/gui/component/core/OutlineRect.kt | 28 +- .../lambda/gui/component/core/TextField.kt | 33 +-- .../lambda/gui/component/core/UIBuilder.kt | 6 + .../com/lambda/gui/component/layout/Layout.kt | 264 ++++++++++-------- .../lambda/gui/component/window/TitleBar.kt | 24 +- .../com/lambda/gui/component/window/Window.kt | 21 +- .../gui/component/window/WindowContent.kt | 7 +- .../lambda/gui/impl/clickgui/ModuleLayout.kt | 15 +- .../lambda/gui/impl/clickgui/ModuleWindow.kt | 15 +- .../lambda/gui/impl/clickgui/SettingLayout.kt | 5 +- .../impl/clickgui/settings/BooleanButton.kt | 2 +- .../construction/result/Drawable.kt | 1 - .../com/lambda/module/hud/TickShiftCharge.kt | 2 + .../lambda/module/modules/client/ClickGui.kt | 17 +- .../module/modules/client/LambdaMoji.kt | 1 + .../lambda/module/modules/player/FastBreak.kt | 1 - .../module/modules/player/PacketMine.kt | 1 - .../lambda/module/modules/render/Particles.kt | 3 +- .../src/main/kotlin/com/lambda/util/Mouse.kt | 1 + .../lambda/shaders/fragment/font/font.frag | 9 - .../fragment/renderer/rect_filled.frag | 49 ++-- .../fragment/renderer/rect_outline.frag | 26 +- .../lambda/shaders/vertex/font/font.vert | 15 +- .../shaders/vertex/renderer/rect_filled.vert | 25 +- .../shaders/vertex/renderer/rect_outline.vert | 12 +- 45 files changed, 438 insertions(+), 546 deletions(-) delete mode 100644 common/src/main/kotlin/com/lambda/graphics/pipeline/UIPipeline.kt rename common/src/main/kotlin/com/lambda/graphics/renderer/gui/{rect/AbstractRectRenderer.kt => AbstractGUIRenderer.kt} (57%) diff --git a/common/src/main/kotlin/com/lambda/event/events/GuiEvent.kt b/common/src/main/kotlin/com/lambda/event/events/GuiEvent.kt index adad927e1..1fb807083 100644 --- a/common/src/main/kotlin/com/lambda/event/events/GuiEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/GuiEvent.kt @@ -26,6 +26,7 @@ sealed class GuiEvent : Event { data object Show : GuiEvent() data object Hide : GuiEvent() data object Tick : GuiEvent() + data object Update : GuiEvent() data object Render : GuiEvent() class KeyPress(val key: KeyCode) : GuiEvent() diff --git a/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt b/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt index 4b4fb35f5..6c895d02d 100644 --- a/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/RenderEvent.kt @@ -21,7 +21,6 @@ import com.lambda.Lambda.mc import com.lambda.event.Event import com.lambda.event.callback.Cancellable import com.lambda.event.callback.ICancellable -import com.lambda.graphics.pipeline.UIPipeline import com.lambda.graphics.renderer.esp.global.DynamicESP import com.lambda.graphics.renderer.esp.global.StaticESP import com.lambda.util.math.Vec2d diff --git a/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt index 76b616812..e448cd798 100644 --- a/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt +++ b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt @@ -23,7 +23,6 @@ import com.lambda.event.events.RenderEvent import com.lambda.graphics.gl.GlStateUtils.setupGL import com.lambda.graphics.gl.Matrices import com.lambda.graphics.gl.Matrices.resetMatrices -import com.lambda.graphics.pipeline.UIPipeline import com.lambda.module.modules.client.GuiSettings import com.lambda.util.math.Vec2d import com.mojang.blaze3d.systems.RenderSystem.getProjectionMatrix @@ -35,22 +34,19 @@ object RenderMain { val projModel get() = Matrix4f(projectionMatrix).mul(modelViewMatrix) var screenSize = Vec2d.ZERO + var scaleFactor = 1.0 @JvmStatic fun render2D() { resetMatrices(Matrix4f().translate(0f, 0f, -3000f)) setupGL { - UIPipeline.reset() - rescale(1.0) RenderEvent.GUI.Fixed().post() rescale(GuiSettings.scale) RenderEvent.GUI.HUD(GuiSettings.scale).post() RenderEvent.GUI.Scaled(GuiSettings.scale).post() - - UIPipeline.render() } } @@ -72,6 +68,8 @@ object RenderMain { val scaledHeight = height / factor screenSize = Vec2d(scaledWidth, scaledHeight) + scaleFactor = factor + projectionMatrix.setOrtho(0f, scaledWidth.toFloat(), scaledHeight.toFloat(), 0f, 1000f, 21000f) } } diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt index a292544e6..0025bfdff 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt @@ -121,7 +121,12 @@ abstract class Buffer( /** * Binds current the buffer [index] to the [target] */ - fun bind() = bind(bufferIds[index]) + fun bind() = bind(bufferAt(index)) + + /** + * Returns the id of the buffer based on the index + */ + fun bufferAt(index: Int) = bufferIds[index] /** * Swaps the buffer [index] if [buffers] is greater than 1 diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt index ef886c56b..808b690ee 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt @@ -32,6 +32,7 @@ import com.lambda.graphics.gl.Memory.vector2f import com.lambda.graphics.gl.Memory.vector3f import com.lambda.graphics.gl.kibibyte import org.joml.Vector4d +import org.lwjgl.opengl.GL15C import org.lwjgl.opengl.GL20C.* import java.awt.Color @@ -183,6 +184,10 @@ class VertexPipeline( uploadedIndices = 0 } + fun finalize() { + + } + init { // All the buffers have been generated, all we have to // do now it bind them correctly and populate them diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexAttrib.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexAttrib.kt index d8735228c..3ece85693 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexAttrib.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexAttrib.kt @@ -41,17 +41,12 @@ enum class VertexAttrib( FONT( Vec3, // pos Vec2, // uv - Vec2, Vec2, // scissor test bounds Color ), RECT_FILLED( Vec3, // pos Vec2, // uv - Vec2, // size - Vec2, Vec2, // roundL, roundR - Float, // shade - Vec2, Vec2, // scissor test bounds Color ), @@ -59,8 +54,6 @@ enum class VertexAttrib( Vec3, // pos Vec2, // uv Float, // alpha - Float, // shade - Vec2, Vec2, // scissor test bounds Color ), diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt b/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt index 7b10a33b2..71189e7e5 100644 --- a/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt +++ b/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt @@ -38,6 +38,11 @@ object Matrices { */ var vertexTransformer: Matrix4d? = null + /** + * An optional vec3 offset for applying vertex transformations. + */ + var vertexOffset: Vec3d? = null + /** * Executes a block of code within the context of a new matrix. * The current matrix is pushed onto the stack before the block executes and popped after the block completes. @@ -51,7 +56,7 @@ object Matrices { */ fun push(block: Matrices.() -> Unit) { push() - block() + block.invoke(this) pop() } @@ -146,6 +151,20 @@ object Matrices { vertexTransformer = null } + /** + * Temporarily sets a vertex offset vector for the duration of a block. + * + * Use this to avoid precision loss when using matrices while being on huge coordinates. + * + * @param offset The transformation offset to apply to vertices. + * @param block The block of code to execute with the transformation applied. + */ + fun withVertexOffset(offset: Vec3d, block: () -> Unit) { + vertexOffset = offset + block() + vertexOffset = null + } + /** * Builds a world projection matrix for a given position, scale, and rotation mode. * diff --git a/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt b/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt index 3bebf7a6b..be66c63a7 100644 --- a/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt +++ b/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt @@ -17,71 +17,43 @@ package com.lambda.graphics.pipeline -import com.lambda.graphics.renderer.gui.font.core.GlyphInfo +import com.lambda.Lambda.mc +import com.lambda.graphics.RenderMain +import com.lambda.util.math.MathUtils.ceilToInt +import com.lambda.util.math.MathUtils.floorToInt import com.lambda.util.math.Rect -import com.lambda.util.math.transform +import com.mojang.blaze3d.systems.RenderSystem.disableScissor +import com.mojang.blaze3d.systems.RenderSystem.enableScissor object ScissorAdapter { private var stack = ArrayDeque() - private val scissorInstance = ScissorRect() fun scissor(rect: Rect, block: () -> Unit) { - // clamp corners so children scissor boxes can't overlap parent val processed = stack.lastOrNull()?.let(rect::clamp) ?: rect - // push the stack stack.add(processed) + scissorRect(processed) - // do render tasks block() - // pop the stack stack.removeLast() + stack.lastOrNull()?.let { scissorRect(it) } ?: disableScissor() } - fun scissorTest(x1: Double, y1: Double, x2: Double, y2: Double, glyph: GlyphInfo? = null): ScissorRect { - reset() + private fun scissorRect(rect: Rect) { + val pos1 = rect.leftTop * RenderMain.scaleFactor + val pos2 = rect.rightBottom * RenderMain.scaleFactor - run { - val entry = stack.lastOrNull() ?: return@run + val width = pos2.x - pos1.x + val height = pos2.y - pos1.y - val width = x2 - x1 - val height = y2 - y1 + val y = mc.window.framebufferHeight - pos1.y - height - if (width <= 0 || height <= 0) { - nullify() - return@run - } - - val si = scissorInstance - - si.x1 = transform(entry.left, x1, x2, glyph?.u1 ?: 0.0, glyph?.u2 ?: 1.0) - si.y1 = transform(entry.top, y1, y2, glyph?.v1 ?: 0.0, glyph?.v2 ?: 1.0) - si.x2 = transform(entry.right, x1, x2, glyph?.u1 ?: 0.0, glyph?.u2 ?: 1.0) - si.y2 = transform(entry.bottom, y1, y2, glyph?.v1 ?: 0.0, glyph?.v2 ?: 1.0) - } - - return scissorInstance - } - - private fun reset() = scissorInstance.apply { - x1 = 0.0 - y1 = 0.0 - x2 = 1.0 - y2 = 1.0 - } - - private fun nullify() = scissorInstance.apply { - x1 = 0.0 - y1 = 0.0 - x2 = 0.0 - y2 = 0.0 - } - - class ScissorRect { - var x1 = 0.0 - var y1 = 0.0 - var x2 = 1.0 - var y2 = 1.0 + enableScissor( + pos1.x.floorToInt(), + y.floorToInt(), + width.ceilToInt(), + height.ceilToInt() + ) } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/pipeline/UIPipeline.kt b/common/src/main/kotlin/com/lambda/graphics/pipeline/UIPipeline.kt deleted file mode 100644 index 230ce841a..000000000 --- a/common/src/main/kotlin/com/lambda/graphics/pipeline/UIPipeline.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.pipeline - -import com.lambda.core.Loadable -import com.lambda.graphics.renderer.gui.font.FontRenderer -import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer -import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer - -object UIPipeline : Loadable { - private var uiDepth = 0 - val depth get() = uiDepth * -0.001 - - fun objectDrawn() { - uiDepth++ - } - - fun reset() { - uiDepth = 0 - } - - fun render() { - FilledRectRenderer.render() - OutlineRectRenderer.render() - FontRenderer.render() - } -} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ESPRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ESPRenderer.kt index d25d6bded..3087e16c0 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ESPRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ESPRenderer.kt @@ -23,6 +23,7 @@ import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib import com.lambda.graphics.buffer.vertex.attributes.VertexMode import com.lambda.graphics.gl.GlStateUtils import com.lambda.graphics.shader.Shader +import com.lambda.graphics.shader.Shader.Companion.shader import com.lambda.module.modules.client.RenderSettings import com.lambda.util.extension.partialTicks @@ -59,12 +60,12 @@ open class ESPRenderer(tickedMode: Boolean) { } companion object { - private val staticMode = Shader( + private val staticMode = shader( "renderer/pos_color", "renderer/box_static" ) to VertexAttrib.Group.STATIC_RENDERER - private val dynamicMode = Shader( + private val dynamicMode = shader( "renderer/pos_color", "renderer/box_dynamic" ) to VertexAttrib.Group.DYNAMIC_RENDERER diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/AbstractRectRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/AbstractGUIRenderer.kt similarity index 57% rename from common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/AbstractRectRenderer.kt rename to common/src/main/kotlin/com/lambda/graphics/renderer/gui/AbstractGUIRenderer.kt index cc484dc94..3bd9a33c0 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/AbstractRectRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/AbstractGUIRenderer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Lambda + * Copyright 2025 Lambda * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,33 +15,45 @@ * along with this program. If not, see . */ -package com.lambda.graphics.renderer.gui.rect +package com.lambda.graphics.renderer.gui import com.lambda.graphics.RenderMain import com.lambda.graphics.buffer.VertexPipeline import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib import com.lambda.graphics.buffer.vertex.attributes.VertexMode +import com.lambda.graphics.pipeline.ScissorAdapter import com.lambda.graphics.shader.Shader import com.lambda.module.modules.client.GuiSettings +import com.lambda.util.math.MathUtils.toInt +import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d -import org.lwjgl.glfw.GLFW.glfwGetTime +import org.lwjgl.glfw.GLFW -abstract class AbstractRectRenderer( +abstract class AbstractGUIRenderer( attribGroup: VertexAttrib.Group, val shader: Shader ) { - protected val pipeline = VertexPipeline(VertexMode.TRIANGLES, attribGroup) + private val pipeline = VertexPipeline(VertexMode.TRIANGLES, attribGroup) - fun render() { + protected fun render( + shade: Boolean = false, + block: VertexPipeline.() -> Unit + ) { + pipeline.clear() shader.use() - shader["u_Time"] = glfwGetTime() * GuiSettings.colorSpeed * 5.0 - shader["u_Color1"] = GuiSettings.shadeColor1 - shader["u_Color2"] = GuiSettings.shadeColor2 - shader["u_Size"] = RenderMain.screenSize / Vec2d(GuiSettings.colorWidth, GuiSettings.colorHeight) + block(pipeline) + + shader["u_Shade"] = shade.toInt().toDouble() + if (shade) { + shader["u_ShadeTime"] = GLFW.glfwGetTime() * GuiSettings.colorSpeed * 5.0 + shader["u_ShadeColor1"] = GuiSettings.shadeColor1 + shader["u_ShadeColor2"] = GuiSettings.shadeColor2 + + shader["u_ShadeSize"] = RenderMain.screenSize / Vec2d(GuiSettings.colorWidth, GuiSettings.colorHeight) + } pipeline.upload() pipeline.render() - pipeline.clear() } -} +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt index 82186f8d1..b8a736f64 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt @@ -21,7 +21,7 @@ import com.lambda.graphics.RenderMain import com.lambda.graphics.buffer.VertexPipeline import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib import com.lambda.graphics.buffer.vertex.attributes.VertexMode -import com.lambda.graphics.shader.Shader +import com.lambda.graphics.shader.Shader.Companion.shader import com.lambda.graphics.texture.Texture import com.lambda.module.modules.client.GuiSettings import com.lambda.util.math.Rect @@ -30,24 +30,25 @@ import org.lwjgl.glfw.GLFW.glfwGetTime object TextureRenderer { private val pipeline = VertexPipeline(VertexMode.TRIANGLES, VertexAttrib.Group.POS_UV) - private val shader = Shader("renderer/pos_tex") - private val shaderColored = Shader("renderer/pos_tex_shady") + + private val mainShader = shader("renderer/pos_tex") + private val coloredShader = shader("renderer/pos_tex_shady") fun drawTexture(texture: Texture, rect: Rect) { texture.bind() - shader.use() + mainShader.use() drawInternal(rect) } fun drawTextureShaded(texture: Texture, rect: Rect) { texture.bind() - shaderColored.use() + coloredShader.use() - shaderColored["u_Time"] = glfwGetTime() * GuiSettings.colorSpeed * 5.0 - shaderColored["u_Color1"] = GuiSettings.shadeColor1 - shaderColored["u_Color2"] = GuiSettings.shadeColor2 - shaderColored["u_Size"] = RenderMain.screenSize / Vec2d(GuiSettings.colorWidth, GuiSettings.colorHeight) + coloredShader["u_Time"] = glfwGetTime() * GuiSettings.colorSpeed * 5.0 + coloredShader["u_Color1"] = GuiSettings.shadeColor1 + coloredShader["u_Color2"] = GuiSettings.shadeColor2 + coloredShader["u_Size"] = RenderMain.screenSize / Vec2d(GuiSettings.colorWidth, GuiSettings.colorHeight) drawInternal(rect) } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index 4169b00e3..fa2d71886 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -19,13 +19,13 @@ package com.lambda.graphics.renderer.gui.font import com.lambda.graphics.buffer.VertexPipeline import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib -import com.lambda.graphics.buffer.vertex.attributes.VertexMode import com.lambda.graphics.pipeline.ScissorAdapter -import com.lambda.graphics.pipeline.UIPipeline +import com.lambda.graphics.renderer.gui.AbstractGUIRenderer import com.lambda.graphics.renderer.gui.font.core.GlyphInfo import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.get import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.height import com.lambda.graphics.shader.Shader +import com.lambda.graphics.shader.Shader.Companion.shader import com.lambda.graphics.texture.TextureOwner.bind import com.lambda.module.modules.client.LambdaMoji import com.lambda.module.modules.client.RenderSettings @@ -33,17 +33,15 @@ import com.lambda.util.math.Vec2d import com.lambda.util.math.a import com.lambda.util.math.setAlpha import java.awt.Color +import java.awt.Font /** * Renders text and emoji glyphs using a shader-based font rendering system. * This class handles text and emoji rendering, shadow effects, and text scaling. */ -object FontRenderer { - private val chars = RenderSettings.textFont - private val emojis = RenderSettings.emojiFont - - private val shader = Shader("font/font") - private val pipeline = VertexPipeline(VertexMode.TRIANGLES, VertexAttrib.Group.FONT) +object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/font")) { + private val chars get() = RenderSettings.textFont + private val emojis get() = RenderSettings.emojiFont private val shadowShift get() = RenderSettings.shadowShift * 5.0 private val baselineOffset get() = RenderSettings.baselineOffset * 2.0f - 10f @@ -66,8 +64,17 @@ object FontRenderer { scale: Double = 1.0, shadow: Boolean = RenderSettings.shadow, parseEmoji: Boolean = LambdaMoji.isEnabled - ) = processText(text, color, scale, shadow, parseEmoji) { char, pos1, pos2, col -> buildGlyph(char, position, pos1, pos2, col) }.also { - UIPipeline.objectDrawn() + ) = render { + shader["u_FontTexture"] = 0 + shader["u_EmojiTexture"] = 1 + shader["u_SDFMin"] = 0.3 + shader["u_SDFMax"] = 1.0 + + bind(chars, emojis) + + processText(text, color, scale, shadow, parseEmoji) { char, pos1, pos2, col -> + buildGlyph(char, position, pos1, pos2, col) + } } /** @@ -79,27 +86,25 @@ object FontRenderer { * @param pos2 The end position of the glyph * @param color The color of the glyph. */ - fun buildGlyph( + private fun VertexPipeline.buildGlyph( glyph: GlyphInfo, origin: Vec2d = Vec2d.ZERO, pos1: Vec2d = Vec2d.ZERO, pos2: Vec2d = pos1 + glyph.size, color: Color = Color.WHITE, - ) = pipeline.use { - grow(4) - + ) { val x1 = pos1.x + origin.x val y1 = pos1.y + origin.y val x2 = pos2.x + origin.x val y2 = pos2.y + origin.y - val scissor = ScissorAdapter.scissorTest(x1, y1, x2, y2, glyph) + grow(4) putQuad( - vec3m(x1, y1, UIPipeline.depth).vec2(glyph.uv1.x, glyph.uv1.y).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(color).end(), - vec3m(x1, y2, UIPipeline.depth).vec2(glyph.uv1.x, glyph.uv2.y).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(color).end(), - vec3m(x2, y2, UIPipeline.depth).vec2(glyph.uv2.x, glyph.uv2.y).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(color).end(), - vec3m(x2, y1, UIPipeline.depth).vec2(glyph.uv2.x, glyph.uv1.y).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(color).end() + vec3m(x1, y1, 0.0).vec2(glyph.uv1.x, glyph.uv1.y).color(color).end(), + vec3m(x1, y2, 0.0).vec2(glyph.uv1.x, glyph.uv2.y).color(color).end(), + vec3m(x2, y2, 0.0).vec2(glyph.uv2.x, glyph.uv2.y).color(color).end(), + vec3m(x2, y1, 0.0).vec2(glyph.uv2.x, glyph.uv1.y).color(color).end() ) } @@ -219,7 +224,7 @@ object FontRenderer { * @param scale The base scale factor. * @return The adjusted scale factor. */ - fun getScaleFactor(scale: Double): Double = scale * 8 / chars.height + fun getScaleFactor(scale: Double): Double = scale * 9.0 / chars.height /** * Calculates the shadow color by adjusting the brightness of the input color. @@ -233,16 +238,4 @@ object FontRenderer { (color.blue * RenderSettings.shadowBrightness).toInt(), color.alpha ) - - fun render() { - shader.use() - shader["u_FontTexture"] = 0 - shader["u_EmojiTexture"] = 1 - shader["u_SDFMin"] = 0.3 - shader["u_SDFMax"] = 1.0 - - bind(chars, emojis) - - pipeline.immediateDraw() - } } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt index e7dcbdde4..1f8eaf5e0 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt @@ -20,6 +20,7 @@ package com.lambda.graphics.renderer.gui.font.sdf import com.lambda.graphics.buffer.frame.CachedFrame import com.lambda.graphics.buffer.frame.FrameBuffer import com.lambda.graphics.shader.Shader +import com.lambda.graphics.shader.Shader.Companion.shader import com.lambda.graphics.texture.Texture import com.lambda.util.math.Vec2d import java.awt.image.BufferedImage @@ -33,6 +34,8 @@ import java.awt.image.BufferedImage * @param image Image data to upload */ class DistanceFieldTexture(image: BufferedImage) : Texture(image, levels = 0) { + private val shader = shader("signed_distance_field", "renderer/pos_tex") + private val frame = CachedFrame(width, height).write { FrameBuffer.pipeline.use { val (pos1, pos2) = Vec2d.ZERO to Vec2d(width, height) @@ -56,8 +59,4 @@ class DistanceFieldTexture(image: BufferedImage) : Texture(image, levels = 0) { override fun bind(slot: Int) { frame.bind(slot) } - - companion object { - private val shader = Shader("signed_distance_field", "renderer/pos_tex") - } } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt index deeabb35c..a4178b69b 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt @@ -18,16 +18,14 @@ package com.lambda.graphics.renderer.gui.rect import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib -import com.lambda.graphics.pipeline.ScissorAdapter -import com.lambda.graphics.pipeline.UIPipeline -import com.lambda.graphics.shader.Shader -import com.lambda.util.math.MathUtils.toInt +import com.lambda.graphics.renderer.gui.AbstractGUIRenderer +import com.lambda.graphics.shader.Shader.Companion.shader import com.lambda.util.math.Rect import java.awt.Color import kotlin.math.min -object FilledRectRenderer : AbstractRectRenderer( - VertexAttrib.Group.RECT_FILLED, Shader("renderer/rect_filled") +object FilledRectRenderer : AbstractGUIRenderer( + VertexAttrib.Group.RECT_FILLED, shader("renderer/rect_filled") ) { private const val MIN_SIZE = 0.5 private const val MIN_ALPHA = 3 @@ -80,7 +78,7 @@ object FilledRectRenderer : AbstractRectRenderer( rightBottom: Color = Color.WHITE, leftBottom: Color = Color.WHITE, shade: Boolean = false, - ) = pipeline.use { + ) = render(shade) { val pos1 = rect.leftTop val pos2 = rect.rightBottom @@ -90,9 +88,9 @@ object FilledRectRenderer : AbstractRectRenderer( rightTop.alpha < MIN_ALPHA && rightBottom.alpha < MIN_ALPHA && leftBottom.alpha < MIN_ALPHA - ) return@use + ) return@render - if (size.x < MIN_SIZE || size.y < MIN_SIZE) return@use + if (size.x < MIN_SIZE || size.y < MIN_SIZE) return@render val halfSize = size * 0.5 val maxRadius = min(halfSize.x, halfSize.y) @@ -104,19 +102,19 @@ object FilledRectRenderer : AbstractRectRenderer( val p1 = pos1 - 0.25 val p2 = pos2 + 0.25 - val s = shade.toInt().toDouble() - grow(4) - - val scissor = ScissorAdapter.scissorTest(p1.x, p1.y, p2.x, p2.y) + shader["u_Size"] = size + shader["u_RoundLeftTop"] = ltr + shader["u_RoundLeftBottom"] = lbr + shader["u_RoundRightBottom"] = rbr + shader["u_RoundRightTop"] = rtr + grow(4) putQuad( - vec3m(p1.x, p1.y, UIPipeline.depth).vec2(0.0, 0.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(leftTop).end(), - vec3m(p1.x, p2.y, UIPipeline.depth).vec2(0.0, 1.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(leftBottom).end(), - vec3m(p2.x, p2.y, UIPipeline.depth).vec2(1.0, 1.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(rightBottom).end(), - vec3m(p2.x, p1.y, UIPipeline.depth).vec2(1.0, 0.0).vec2(size.x, size.y).vec2(ltr, lbr).vec2(rtr, rbr).float(s).vec2(scissor.x1, scissor.y1).vec2(scissor.x2, scissor.y2).color(rightTop).end() + vec3m(p1.x, p1.y, 0.0).vec2(0.0, 0.0).color(leftTop).end(), + vec3m(p1.x, p2.y, 0.0).vec2(0.0, 1.0).color(leftBottom).end(), + vec3m(p2.x, p2.y, 0.0).vec2(1.0, 1.0).color(rightBottom).end(), + vec3m(p2.x, p1.y, 0.0).vec2(1.0, 0.0).color(rightTop).end() ) - - UIPipeline.objectDrawn() } } diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt index 1a1a9fc80..127cacc98 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt @@ -20,8 +20,9 @@ package com.lambda.graphics.renderer.gui.rect import com.lambda.graphics.buffer.IRenderContext import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib import com.lambda.graphics.pipeline.ScissorAdapter -import com.lambda.graphics.pipeline.UIPipeline +import com.lambda.graphics.renderer.gui.AbstractGUIRenderer import com.lambda.graphics.shader.Shader +import com.lambda.graphics.shader.Shader.Companion.shader import com.lambda.util.math.lerp import com.lambda.util.math.MathUtils.toInt import com.lambda.util.math.MathUtils.toRadian @@ -33,8 +34,8 @@ import kotlin.math.cos import kotlin.math.min import kotlin.math.sin -object OutlineRectRenderer : AbstractRectRenderer( - VertexAttrib.Group.RECT_OUTLINE, Shader("renderer/rect_outline") +object OutlineRectRenderer : AbstractGUIRenderer( + VertexAttrib.Group.RECT_OUTLINE, shader("renderer/rect_outline") ) { private const val QUALITY = 8 private const val VERTICES_COUNT = QUALITY * 4 @@ -56,13 +57,11 @@ object OutlineRectRenderer : AbstractRectRenderer( rightBottom: Color = Color.WHITE, leftBottom: Color = Color.WHITE, shade: Boolean = false, - ) = pipeline.use { - if (glowRadius < 1) return@use + ) = render(shade) { + if (glowRadius < 1) return@render grow(VERTICES_COUNT * 3) - val scissor = ScissorAdapter.scissorTest(rect.left, rect.top, rect.right, rect.bottom) - fun IRenderContext.genVertices(size: Double, isGlow: Boolean): MutableList { val r = rect.expand(size) val a = (!isGlow).toInt().toDouble() @@ -78,18 +77,11 @@ object OutlineRectRenderer : AbstractRectRenderer( val angle = lerp(p, min, max).toRadian() val pos = base + Vec2d(cos(angle), -sin(angle)) * round - val s = shade.toInt().toDouble() val uvx = transform(pos.x, rect.left, rect.right, 0.0, 1.0) val uvy = transform(pos.y, rect.top, rect.bottom, 0.0, 1.0) - add(vec3m(pos.x, pos.y, UIPipeline.depth) - .vec2(uvx, uvy) - .float(a).float(s) - .vec2(scissor.x1, scissor.y1) - .vec2(scissor.x2, scissor.y2) - .color(c).end() - ) + add(vec3m(pos.x, pos.y, 0.0).vec2(uvx, uvy).float(a).color(c).end()) } val rt = r.rightTop + Vec2d(-round, round) @@ -118,7 +110,5 @@ object OutlineRectRenderer : AbstractRectRenderer( drawStripWith(genVertices(-(glowRadius.coerceAtMost(1.0)), true)) drawStripWith(genVertices(glowRadius, true)) - - UIPipeline.objectDrawn() } } diff --git a/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt b/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt index 80ad62a13..c5b718a4d 100644 --- a/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt +++ b/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt @@ -29,7 +29,7 @@ import org.joml.Matrix4f import org.lwjgl.opengl.GL20C.* import java.awt.Color -class Shader(fragmentPath: String, vertexPath: String) { +class Shader private constructor(fragmentPath: String, vertexPath: String) { private val uniformCache: Object2IntMap = Object2IntOpenHashMap() private val id = createShaderProgram( @@ -37,8 +37,6 @@ class Shader(fragmentPath: String, vertexPath: String) { loadShader(ShaderType.FRAGMENT_SHADER, "shaders/fragment/$fragmentPath.frag") ) - constructor(path: String) : this(path, path) - fun use() { glUseProgram(id) set("u_ProjModel", RenderMain.projModel) @@ -79,4 +77,16 @@ class Shader(fragmentPath: String, vertexPath: String) { operator fun set(name: String, mat: Matrix4f) = uniformMatrix(loc(name), mat) + + companion object { + private val shaderCache = hashMapOf, Shader>() + + fun shader(path: String) = + shader(path, path) + + fun shader(fragmentPath: String, vertexPath: String) = + shaderCache.getOrPut(fragmentPath to vertexPath) { + Shader(fragmentPath, vertexPath) + } + } } diff --git a/common/src/main/kotlin/com/lambda/gui/LambdaScreen.kt b/common/src/main/kotlin/com/lambda/gui/LambdaScreen.kt index 9f442df39..cf9a58d83 100644 --- a/common/src/main/kotlin/com/lambda/gui/LambdaScreen.kt +++ b/common/src/main/kotlin/com/lambda/gui/LambdaScreen.kt @@ -48,6 +48,7 @@ class LambdaScreen( init { listen { event -> screenSize = event.screenSize + layout.onEvent(GuiEvent.Update) layout.onEvent(GuiEvent.Render) } diff --git a/common/src/main/kotlin/com/lambda/gui/ScreenLayout.kt b/common/src/main/kotlin/com/lambda/gui/ScreenLayout.kt index 2ab5d085b..07c642ebc 100644 --- a/common/src/main/kotlin/com/lambda/gui/ScreenLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/ScreenLayout.kt @@ -21,9 +21,9 @@ import com.lambda.graphics.RenderMain import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout -class ScreenLayout : Layout(owner = null, useBatching = false, batchChildren = true) { +class ScreenLayout : Layout(owner = null) { init { - onRender { + onUpdate { size = RenderMain.screenSize } } diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt b/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt index 1f5df61e6..bcf8a0266 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt @@ -24,39 +24,31 @@ import java.awt.Color class FilledRect( owner: Layout -) : Layout(owner, true, true) { - var rectangle = Rect.ZERO +) : Layout(owner) { + @UIRenderPr0p3rty var rectangle = Rect.ZERO - var leftTopRadius = 0.0 - var rightTopRadius = 0.0 - var rightBottomRadius = 0.0 - var leftBottomRadius = 0.0 + @UIRenderPr0p3rty var leftTopRadius = 0.0 + @UIRenderPr0p3rty var rightTopRadius = 0.0 + @UIRenderPr0p3rty var rightBottomRadius = 0.0 + @UIRenderPr0p3rty var leftBottomRadius = 0.0 - var leftTopColor: Color = Color.WHITE - var rightTopColor: Color = Color.WHITE - var rightBottomColor: Color = Color.WHITE - var leftBottomColor: Color = Color.WHITE + @UIRenderPr0p3rty var leftTopColor: Color = Color.WHITE + @UIRenderPr0p3rty var rightTopColor: Color = Color.WHITE + @UIRenderPr0p3rty var rightBottomColor: Color = Color.WHITE + @UIRenderPr0p3rty var leftBottomColor: Color = Color.WHITE - var shade = false - - private val updateActions = mutableListOf Unit>() - - fun onUpdate(block: FilledRect.() -> Unit) { - updateActions += block - } + @UIRenderPr0p3rty var shade = false init { properties.interactionPassthrough = true - onRender { - updateActions.forEach { action -> - action(this@FilledRect) - } - + onUpdate { // make it pressable position = rectangle.leftTop size = rectangle.size + } + onRender { filledRect( rectangle, leftTopRadius, diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt b/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt index d64272116..c34124c2a 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt @@ -23,33 +23,23 @@ import java.awt.Color class OutlineRect( owner: Layout -) : Layout(owner, true, true) { - var rectangle = owner.rect +) : Layout(owner) { + @UIRenderPr0p3rty var rectangle = owner.rect - var roundRadius = 0.0 - var glowRadius = 0.0 + @UIRenderPr0p3rty var roundRadius = 0.0 + @UIRenderPr0p3rty var glowRadius = 0.0 - var leftTopColor: Color = Color.WHITE - var rightTopColor: Color = Color.WHITE - var rightBottomColor: Color = Color.WHITE - var leftBottomColor: Color = Color.WHITE + @UIRenderPr0p3rty var leftTopColor: Color = Color.WHITE + @UIRenderPr0p3rty var rightTopColor: Color = Color.WHITE + @UIRenderPr0p3rty var rightBottomColor: Color = Color.WHITE + @UIRenderPr0p3rty var leftBottomColor: Color = Color.WHITE - var shade = false - - private val updateActions = mutableListOf Unit>() - - fun onUpdate(block: OutlineRect.() -> Unit) { - updateActions += block - } + @UIRenderPr0p3rty var shade = false init { properties.interactionPassthrough = true onRender { - updateActions.forEach { action -> - action(this@OutlineRect) - } - outlineRect( rectangle, roundRadius, diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt index dbbde6031..7b14c69e9 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt @@ -28,39 +28,28 @@ import java.awt.Color class TextField( owner: Layout, -) : Layout(owner, true, true) { - var text = "" - var color = Color.WHITE - var scale = 1.0 - var shadow = true +) : Layout(owner) { + @UIRenderPr0p3rty var text = "" + @UIRenderPr0p3rty var color: Color = Color.WHITE + @UIRenderPr0p3rty var scale = 1.0 + @UIRenderPr0p3rty var shadow = true + + @UIRenderPr0p3rty var textHAlignment = HAlign.LEFT + @UIRenderPr0p3rty var textVAlignment = VAlign.CENTER + @UIRenderPr0p3rty var offsetX = 0.0 + @UIRenderPr0p3rty var offsetY = 0.0 val textWidth get() = FontRenderer.getWidth(text, scale) val textHeight get() = FontRenderer.getHeight(scale) - var textHAlignment = HAlign.LEFT - var textVAlignment = VAlign.CENTER - var offsetX = 0.0 - var offsetY = 0.0 - - private val updateActions = mutableListOf Unit>() - - fun onUpdate(block: TextField.() -> Unit) { - updateActions += block - } - init { - properties.interactionPassthrough = true fillParent() + properties.interactionPassthrough = true onRender { - updateActions.forEach { action -> - action(this@TextField) - } - val rx = renderPositionX + lerp(textHAlignment.multiplier, offsetX, renderWidth - textWidth - offsetX) val ry = renderPositionY + lerp(textVAlignment.multiplier, offsetY, renderHeight - textHeight - offsetY) val renderPos = Vec2d(rx, ry + textHeight * 0.5) - drawString(text, renderPos, color, scale, shadow) } } diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt b/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt index 726dafca3..627f45dbf 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt @@ -19,3 +19,9 @@ package com.lambda.gui.component.core @DslMarker annotation class UIBuilder + +@DslMarker +annotation class LayoutBuilder + +@DslMarker +annotation class UIRenderPr0p3rty diff --git a/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt index 77b02db2d..c20f73bfd 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt @@ -19,10 +19,11 @@ package com.lambda.gui.component.layout import com.lambda.graphics.RenderMain import com.lambda.graphics.animation.AnimationTicker -import com.lambda.graphics.pipeline.ScissorAdapter.scissor import com.lambda.event.events.GuiEvent +import com.lambda.graphics.pipeline.ScissorAdapter import com.lambda.gui.component.HAlign import com.lambda.gui.component.VAlign +import com.lambda.gui.component.core.LayoutBuilder import com.lambda.gui.component.core.UIBuilder import com.lambda.util.KeyCode import com.lambda.util.Mouse @@ -31,70 +32,57 @@ import com.lambda.util.math.Vec2d /** * Represents a component for creating complex ui structures. - * - * Warning: use batching if you know what you're doing. - * Batched elements are always drawn first: - * ```kotlin - * // 1st - * layout(useBatching = true) {} - * - * // 3rd - * layout {} - * - * // 2nd - * layout(useBatching = true) {} - * ``` */ open class Layout( - val owner: Layout?, - useBatching: Boolean, - private val batchChildren: Boolean, + val owner: Layout? ) { val rect get() = Rect.basedOn(renderPosition, renderSize) + // ToDo: impl alignmentLayout: Layout, instead of being able to align to the owner only + // Position of the component var position: Vec2d get() = Vec2d(positionX, positionY) set(value) { positionX = value.x; positionY = value.y } - var positionX: Double + private var positionX: Double get() = ownerX + (relativePosX + dockingOffsetX).let { if (!properties.clampPosition) return@let it it.coerceAtMost(ownerWidth - renderWidth).coerceAtLeast(0.0) }; set(value) { relativePosX = value - ownerX - dockingOffsetX } - var positionY: Double + private var positionY: Double get() = ownerY + (relativePosY + dockingOffsetY).let { if (!properties.clampPosition) return@let it it.coerceAtMost(ownerHeight - renderHeight).coerceAtLeast(0.0) }; set(value) { relativePosY = value - ownerY - dockingOffsetY } - var size: Vec2d - get() = Vec2d(width, height) - set(value) { width = value.x; height = value.y } - val leftTop get() = renderPosition val rightTop get() = Vec2d(renderPositionX + renderWidth, renderPositionY) val rightBottom get() = Vec2d(renderPositionX + renderWidth, renderPositionY + renderHeight) val leftBottom get() = Vec2d(renderPositionX, renderPositionY + renderHeight) - var width = 0.0 - var height = 0.0 - val renderPosition get() = Vec2d(renderPositionX, renderPositionY) val renderPositionX get() = positionXTransform() val renderPositionY get() = positionYTransform() private var positionXTransform = { positionX } private var positionYTransform = { positionY } + private var relativePosX = 0.0 + private var relativePosY = 0.0 + + // Size of the component + var size: Vec2d + get() = Vec2d(width, height) + set(value) { width = value.x; height = value.y } val renderSize get() = Vec2d(renderWidth, renderHeight) val renderWidth get() = widthTransform() val renderHeight get() = heightTransform() private var widthTransform = { width } private var heightTransform = { height } + var width = 0.0 + var height = 0.0 - private var relativePosX = 0.0 - private var relativePosY = 0.0 - + // Horizontal alignment var horizontalAlignment = HAlign.LEFT; set(to) { val from = field field = to @@ -103,6 +91,10 @@ open class Layout( relativePosX += delta * (renderWidth - ownerWidth) } + private val dockingOffsetX get() = if (horizontalAlignment == HAlign.LEFT) 0.0 + else (ownerWidth - renderWidth) * horizontalAlignment.multiplier + + // Vertical alignment var verticalAlignment = VAlign.TOP; set(to) { val from = field field = to @@ -111,20 +103,18 @@ open class Layout( relativePosY += delta * (renderHeight - ownerHeight) } + private val dockingOffsetY get() = if (verticalAlignment == VAlign.TOP) 0.0 + else (ownerHeight - renderHeight) * verticalAlignment.multiplier + + // Use screen limits if [owner] is null private var screenSize = Vec2d.ZERO + // Owner params (cached, due to the nullability of [owner]) private var ownerX = 0.0 private var ownerY = 0.0 - private var ownerWidth = 0.0 private var ownerHeight = 0.0 - private val dockingOffsetX get() = if (horizontalAlignment == HAlign.LEFT) 0.0 - else (ownerWidth - renderWidth) * horizontalAlignment.multiplier - - private val dockingOffsetY get() = if (verticalAlignment == VAlign.TOP) 0.0 - else (ownerHeight - renderHeight) * verticalAlignment.multiplier - /** * Configurable properties of the component */ @@ -133,31 +123,42 @@ open class Layout( // Structure val children = mutableListOf() var selectedChild: Layout? = null + protected open val renderChildren: Boolean get() = renderWidth > 0 && renderHeight > 0 // Inputs protected var mousePosition = Vec2d.ZERO var isHovered = false; get() = field && (owner?.isHovered ?: true) - private var owningRenderer = false - // Actions - private var showActions = mutableListOf<() -> Unit>() - private var hideActions = mutableListOf<() -> Unit>() - private var tickActions = mutableListOf<() -> Unit>() - private var renderActions = mutableListOf<() -> Unit>() - private var keyPressActions = mutableListOf<(key: KeyCode) -> Unit>() - private var charTypedActions = mutableListOf<(char: Char) -> Unit>() - private var mouseClickActions = mutableListOf<(button: Mouse.Button, action: Mouse.Action) -> Unit>() - private var mouseMoveActions = mutableListOf<(mouse: Vec2d) -> Unit>() - private var mouseScrollActions = mutableListOf<(delta: Double) -> Unit>() + private var showActions = mutableListOf Unit>() + private var hideActions = mutableListOf Unit>() + private var tickActions = mutableListOf Unit>() + private var updateActions = mutableListOf Unit>() + private var renderActions = mutableListOf Unit>() + private var keyPressActions = mutableListOf Unit>() + private var charTypedActions = mutableListOf Unit>() + private var mouseClickActions = mutableListOf Unit>() + private var mouseMoveActions = mutableListOf Unit>() + private var mouseScrollActions = mutableListOf Unit>() + + /** + * Performs the action on this layout + * + * @param action The action to be performed. + */ + @LayoutBuilder + fun T.use(action: T.() -> Unit) { + action(this).apply { } + } /** * Sets the action to be performed when the element gets shown. * * @param action The action to be performed. */ - fun onShow(action: () -> Unit) { - showActions += action + @LayoutBuilder + fun T.onShow(action: T.() -> Unit) { + showActions += { action() } } /** @@ -165,8 +166,9 @@ open class Layout( * * @param action The action to be performed. */ - fun onHide(action: () -> Unit) { - hideActions += action + @LayoutBuilder + fun T.onHide(action: T.() -> Unit) { + hideActions += { action() } } /** @@ -174,8 +176,19 @@ open class Layout( * * @param action The action to be performed. */ - fun onTick(action: () -> Unit) { - tickActions += action + @LayoutBuilder + fun T.onTick(action: T.() -> Unit) { + tickActions += { action() } + } + + /** + * Sets the update action to be performed before each frame. + * + * @param action The action to be performed. + */ + @LayoutBuilder + fun T.onUpdate(action: T.() -> Unit) { + updateActions += { action() } } /** @@ -183,8 +196,9 @@ open class Layout( * * @param action The action to be performed. */ - fun onRender(action: () -> Unit) { - renderActions += action + @LayoutBuilder + fun T.onRender(action: T.() -> Unit) { + renderActions += { action() } } /** @@ -192,8 +206,9 @@ open class Layout( * * @param action The action to be performed. */ - fun onKeyPress(action: (key: KeyCode) -> Unit) { - keyPressActions += action + @LayoutBuilder + fun T.onKeyPress(action: T.(key: KeyCode) -> Unit) { + keyPressActions += { key -> action(key) } } /** @@ -201,8 +216,9 @@ open class Layout( * * @param action The action to be performed. */ - fun onCharTyped(action: (char: Char) -> Unit) { - charTypedActions += action + @LayoutBuilder + fun T.onCharTyped(action: T.(char: Char) -> Unit) { + charTypedActions += { char -> action(char) } } /** @@ -210,8 +226,9 @@ open class Layout( * * @param action The action to be performed. */ - fun onMouseClick(action: (button: Mouse.Button, action: Mouse.Action) -> Unit) { - mouseClickActions += action + @LayoutBuilder + fun T.onMouseClick(action: T.(button: Mouse.Button, action: Mouse.Action) -> Unit) { + mouseClickActions += { button, mouseAction -> action(button, mouseAction) } } /** @@ -219,8 +236,9 @@ open class Layout( * * @param action The action to be performed. */ - fun onMouseMove(action: (mouse: Vec2d) -> Unit) { - mouseMoveActions += action + @LayoutBuilder + fun T.onMouseMove(action: T.(mouse: Vec2d) -> Unit) { + mouseMoveActions += { mouse -> action(mouse) } } /** @@ -228,13 +246,15 @@ open class Layout( * * @param action The action to be performed. */ - fun onMouseScroll(action: (delta: Double) -> Unit) { - mouseScrollActions += action + @LayoutBuilder + fun T.onMouseScroll(action: T.(delta: Double) -> Unit) { + mouseScrollActions += { delta -> action(delta) } } /** * Force overrides drawn x position of the layout */ + @LayoutBuilder fun overrideX(transform: () -> Double) { positionXTransform = transform } @@ -242,6 +262,7 @@ open class Layout( /** * Force overrides drawn y position of the layout */ + @LayoutBuilder fun overrideY(transform: () -> Double) { positionYTransform = transform } @@ -249,6 +270,7 @@ open class Layout( /** * Force overrides drawn position of the layout */ + @LayoutBuilder fun overridePosition(x: () -> Double, y: () -> Double) { positionXTransform = x positionYTransform = y @@ -257,6 +279,7 @@ open class Layout( /** * Force overrides drawn width of the layout */ + @LayoutBuilder fun overrideWidth(transform: () -> Double) { widthTransform = transform } @@ -264,6 +287,7 @@ open class Layout( /** * Force overrides drawn height of the layout */ + @LayoutBuilder fun overrideHeight(transform: () -> Double) { heightTransform = transform } @@ -271,6 +295,7 @@ open class Layout( /** * Force overrides drawn size of the layout */ + @LayoutBuilder fun overrideSize(width: () -> Double, height: () -> Double) { widthTransform = width heightTransform = height @@ -279,11 +304,12 @@ open class Layout( /** * Makes this layout expand up to parents rect */ + @LayoutBuilder fun fillParent( overrideX: () -> Double = { owner?.renderPositionX ?: ownerX }, overrideY: () -> Double = { owner?.renderPositionY ?: ownerY }, - overrideWidth: () -> Double = { owner?.renderWidth?: ownerWidth }, - overrideHeight: () -> Double = { owner?.renderHeight?: ownerHeight } + overrideWidth: () -> Double = { owner?.renderWidth ?: ownerWidth }, + overrideHeight: () -> Double = { owner?.renderHeight ?: ownerHeight } ) { overrideX(overrideX) overrideY(overrideY) @@ -291,30 +317,72 @@ open class Layout( overrideHeight(overrideHeight) } - fun onEvent(e: GuiEvent) { - if (e is GuiEvent.Render) { + init { + onUpdate { // Update the layout screenSize = RenderMain.screenSize + // Update relative position and bounds ownerX = owner?.renderPositionX ?: ownerX ownerY = owner?.renderPositionY ?: ownerY - ownerWidth = owner?.renderWidth ?: screenSize.x ownerHeight = owner?.renderHeight ?: screenSize.y + // Update hover state (don't mark as hovered if hovered pixel is outside the owner) val xh = (mousePosition.x - renderPositionX) in 0.0..renderWidth val yh = (mousePosition.y - renderPositionY) in 0.0..renderHeight isHovered = xh && yh + + // Select an element that's on foreground + selectedChild = if (isHovered) children.lastOrNull { + !it.properties.interactionPassthrough && mousePosition in it.rect + } else null } + } - // Select an element that's on foreground - selectedChild = if (isHovered) children.lastOrNull { - !it.properties.interactionPassthrough && mousePosition in it.rect - } else null + fun onEvent(e: GuiEvent) { + // Update self + when (e) { + is GuiEvent.Show -> { + mousePosition = Vec2d.ONE * -1000.0 + showActions.forEach { it(this) } + } + is GuiEvent.Hide -> { + hideActions.forEach { it(this) } + } + is GuiEvent.Tick -> { + tickActions.forEach { it(this) } + } + is GuiEvent.KeyPress -> { + keyPressActions.forEach { it(this, e.key) } + } + is GuiEvent.CharTyped -> { + charTypedActions.forEach { it(this, e.char) } + } + is GuiEvent.Update -> { + updateActions.forEach { it(this) } + } + is GuiEvent.Render -> {} + is GuiEvent.MouseMove -> { + mousePosition = e.mouse + mouseMoveActions.forEach { it(this, e.mouse) } + } + is GuiEvent.MouseScroll -> { + mousePosition = e.mouse + + if (isHovered) { + mouseScrollActions.forEach { it(this, e.delta) } + } + } + is GuiEvent.MouseClick -> { + mousePosition = e.mouse + val action = if (isHovered) e.action else Mouse.Action.Release + mouseClickActions.forEach { it(this, e.button, action) } + } + } // Update children children.forEach { child -> if (e is GuiEvent.Render) return@forEach - if (e is GuiEvent.MouseClick) { val hovered = child == selectedChild || (child.isHovered && child.properties.interactionPassthrough) val newAction = if (hovered) e.action else Mouse.Action.Release @@ -327,35 +395,15 @@ open class Layout( child.onEvent(e) } - when (e) { - is GuiEvent.Show -> { mousePosition = Vec2d.ONE * -1000.0; showActions.forEach { it() } } - is GuiEvent.Hide -> { hideActions.forEach { it() } } - is GuiEvent.Tick -> { tickActions.forEach { it() } } - is GuiEvent.KeyPress -> { keyPressActions.forEach { it(e.key) } } - is GuiEvent.CharTyped -> { charTypedActions.forEach { it((e.char)) } } - is GuiEvent.MouseMove -> { mousePosition = e.mouse; mouseMoveActions.forEach { it(e.mouse) } } - is GuiEvent.MouseScroll -> { - mousePosition = e.mouse - - if (isHovered) { - mouseScrollActions.forEach { it(e.delta) } - } - } - is GuiEvent.MouseClick -> { - mousePosition = e.mouse - val action = if (isHovered) e.action else Mouse.Action.Release - mouseClickActions.forEach { it(e.button, action) } + if (e is GuiEvent.Render) { + val block = { + renderActions.forEach { it(this) } + if (renderChildren) children.forEach { it.onEvent(e) } } - is GuiEvent.Render -> { - val render = { evt: GuiEvent.Render -> - renderActions.forEach { it() } - children.forEach { it.onEvent(evt) } - } - if (properties.scissor) { - scissor(rect) { render(e) } - } else render(e) - } + block() + //if (!properties.scissor) block() + //else ScissorAdapter.scissor(rect, block) } } @@ -363,20 +411,14 @@ open class Layout( /** * Creates an empty [Layout]. * - * @param useBatching Whether to use parent's renderer. - * - * @param batchChildren Whether allow children to use the renderer of this layout. - * * @param block Actions to perform within this component. * * Check [Layout] description for more info about batching. */ @UIBuilder fun Layout.layout( - useBatching: Boolean = false, - batchChildren: Boolean = false, block: Layout.() -> Unit = {}, - ) = Layout(this, useBatching, batchChildren) + ) = Layout(this) .apply(children::add).apply(block) /** @@ -391,7 +433,9 @@ open class Layout( */ @UIBuilder fun Layout.animationTicker(register: Boolean = true) = AnimationTicker().apply { - if (register) onTick(this::tick) + if (register) onTick { + this@apply.tick() + } } /** diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt index c9c25170c..f9d0148d4 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt @@ -32,18 +32,7 @@ class TitleBar( owner: Window, title: String, drag: Boolean -) : Layout(owner, true, true) { - val textField = textField { - text = title - - textHAlignment = HAlign.CENTER - - onUpdate { - offsetX = ClickGui.fontOffset - scale = ClickGui.fontScale - } - } - +) : Layout(owner) { private var dragOffset: Vec2d? = null init { @@ -69,6 +58,17 @@ class TitleBar( } } + val textField = textField { + text = title + + textHAlignment = HAlign.CENTER + + onUpdate { + offsetX = ClickGui.fontOffset + scale = ClickGui.fontScale + } + } + companion object { @UIBuilder fun Window.titleBar( diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt index 0b14fe3d9..69b0cd338 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt @@ -46,9 +46,8 @@ open class Window( scrollable: Boolean = true, private val minimizing: Minimizing = Minimizing.Relative, private val resizable: Boolean = true, - val autoResize: AutoResize = AutoResize.Disabled, - useBatching: Boolean = false, -) : Layout(owner, useBatching, true) { + val autoResize: AutoResize = AutoResize.Disabled +) : Layout(owner) { private val animation = animationTicker() private val cursorController = cursorController() @@ -134,14 +133,12 @@ open class Window( properties.clampPosition = owner is ScreenLayout content.properties.scissor = true - with(titleBar) { - onMouseClick { button, action -> - // Toggle minimizing state when right-clicking title bar - if (minimizing == Minimizing.Disabled) return@onMouseClick - if (button != Mouse.Button.Right || action != Mouse.Action.Click) return@onMouseClick + titleBar.onMouseClick { button, action -> + // Toggle minimizing state when right-clicking title bar + if (minimizing == Minimizing.Disabled) return@onMouseClick + if (button != Mouse.Button.Right || action != Mouse.Action.Click) return@onMouseClick - minimized = !minimized - } + minimized = !minimized } onShow { @@ -266,14 +263,12 @@ open class Window( minimizing: Minimizing = Minimizing.Relative, resizable: Boolean = true, autoResize: AutoResize = AutoResize.Disabled, - useBatching: Boolean = false, block: WindowContent.() -> Unit = {} ) = Window( this, title, position, size, draggable, scrollable, minimizing, resizable, - autoResize, - useBatching + autoResize ).apply(children::add).apply { block(this.content) } diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt index afdc4a874..702cc01d8 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt @@ -19,6 +19,7 @@ package com.lambda.gui.component.window import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.event.events.GuiEvent +import com.lambda.gui.component.core.LayoutBuilder import com.lambda.module.modules.client.ClickGui import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout @@ -27,7 +28,7 @@ import kotlin.math.abs class WindowContent( owner: Window, private val scrollable: Boolean -) : Layout(owner, false, true) { +) : Layout(owner) { private val animation = animationTicker(false) private var dwheel = 0.0 @@ -60,6 +61,7 @@ class WindowContent( /** * Overrides the summary height of the content */ + @LayoutBuilder fun overrideContentHeight(block: () -> Double) { contentHeight = block } @@ -67,6 +69,7 @@ class WindowContent( /** * Overrides the action performed on ordering update */ + @LayoutBuilder fun reorderChildren(block: () -> Unit) { reorder = block } @@ -103,9 +106,7 @@ class WindowContent( if (abs(rubberbandDelta) < 0.05) rubberbandDelta = 0.0 animation.tick() - } - onRender { if (scrollable) reorder() } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt index e48ea7238..1d9ea72f3 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt @@ -39,8 +39,7 @@ class ModuleLayout( module.name, Vec2d.ZERO, Vec2d.ZERO, false, true, Minimizing.Relative, false, - AutoResize.ForceEnabled, - true + AutoResize.ForceEnabled ) { private val animation = animationTicker() private val cursorController = cursorController() @@ -54,19 +53,13 @@ class ModuleLayout( init { minimized = true height = 100.0 + openAnimation = 0.0 overrideX { owner.renderPositionX + ClickGui.padding } overrideWidth { owner.renderWidth - ClickGui.padding * 2 } - with(titleBar) { - with(textField) { - textHAlignment = HAlign.LEFT - - onUpdate { - offsetX = ClickGui.fontOffset - } - } - + titleBar.use { + textField.textHAlignment = HAlign.LEFT overrideHeight(ClickGui::moduleHeight) onMouseClick { button, action -> diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt index 703910df7..db2fd5e8c 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt @@ -22,6 +22,8 @@ import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout import com.lambda.gui.component.window.Window import com.lambda.gui.component.window.WindowContent +import com.lambda.gui.impl.clickgui.ModuleLayout.Companion.moduleLayout +import com.lambda.module.ModuleRegistry import com.lambda.util.math.Vec2d class ModuleWindow( @@ -30,13 +32,14 @@ class ModuleWindow( initialPosition: Vec2d ) : Window(owner, tag.name, initialPosition, minimizing = Minimizing.Absolute, autoResize = AutoResize.ByConfig) { init { - onTick { - val modules = content.children.filterIsInstance() - - modules.forEachIndexed { i, it -> - it.isLast = modules.lastIndex == i + ModuleRegistry.modules + .filter { it.defaultTags.firstOrNull() == tag } + .map { module -> content.moduleLayout(module) } + .let { moduleLayouts -> + moduleLayouts.forEachIndexed { i, it -> + it.isLast = moduleLayouts.lastIndex == i + } } - } } companion object { diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt index 3ad032536..a11cc83d5 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt @@ -42,8 +42,7 @@ abstract class SettingLayout > ( false, false, if (expandable) Minimizing.Relative else Minimizing.Disabled, false, - AutoResize.ForceEnabled, - true + AutoResize.ForceEnabled ) { protected val animation = animationTicker() protected val cursorController = cursorController() @@ -64,7 +63,7 @@ abstract class SettingLayout > ( owner.renderPositionX + transform(visibilityAnimation, 0.0, 1.0, -10.0, 0.0) } - with(titleBar.textField) { + titleBar.textField.use { text = setting.name textHAlignment = HAlign.LEFT diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt index 9c2c5c042..aa048667c 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt @@ -71,7 +71,7 @@ class BooleanButton( onUpdate { val knobStart = Rect.basedOn(checkBox.leftTop, Vec2d.ONE * checkBox.renderHeight) - val knobEnd = Rect(checkBox.rightBottom - checkBox.renderHeight, checkBox.rightBottom) + val knobEnd = Rect(checkBox.rightBottom - checkBox.renderHeight, checkBox.rightBottom) rectangle = lerp(activeAnimation, knobStart, knobEnd).shrink(1.0) shade = ClickGui.backgroundShade setColor(Color.WHITE.setAlpha(0.25 * visibilityAnimation)) diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/result/Drawable.kt b/common/src/main/kotlin/com/lambda/interaction/construction/result/Drawable.kt index 6a799b35e..cec234965 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/result/Drawable.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/result/Drawable.kt @@ -18,7 +18,6 @@ package com.lambda.interaction.construction.result import com.lambda.context.SafeContext -import com.lambda.graphics.pipeline.UIPipeline import com.lambda.graphics.renderer.esp.DirectionMask import com.lambda.graphics.renderer.esp.DirectionMask.include import com.lambda.graphics.renderer.esp.builders.buildFilled diff --git a/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt b/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt index 5eeb05629..5b9d3e0b6 100644 --- a/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt +++ b/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt @@ -18,7 +18,9 @@ package com.lambda.module.hud import com.lambda.graphics.animation.Animation.Companion.exp +import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer.filledRect +import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer.outlineRect import com.lambda.module.HudModule import com.lambda.module.modules.client.ClickGui diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt index e15269c6e..7fce88356 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt @@ -18,11 +18,9 @@ package com.lambda.module.modules.client import com.lambda.module.Module -import com.lambda.module.ModuleRegistry import com.lambda.module.tag.ModuleTag import com.lambda.gui.ScreenLayout.Companion.gui import com.lambda.gui.component.core.FilledRect.Companion.rect -import com.lambda.gui.impl.clickgui.ModuleLayout.Companion.moduleLayout import com.lambda.gui.impl.clickgui.ModuleWindow.Companion.moduleWindow import com.lambda.util.math.Vec2d import com.lambda.util.math.setAlpha @@ -67,20 +65,11 @@ object ClickGui : Module( } } - val tags = ModuleTag.defaults - val modules = ModuleRegistry.modules - - var x = 20.0 + var x = 10.0 val y = x - tags.forEachIndexed { i, tag -> - x += moduleWindow(tag, Vec2d(x, y)) { - modules.filter { - it.defaultTags.firstOrNull() == tag - }.forEach { module -> - moduleLayout(module) - } - }.width + 3 + ModuleTag.defaults.forEach { tag -> + x += moduleWindow(tag, Vec2d(x, y)).renderWidth + 5 } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt b/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt index cb77b5752..88d6f1e80 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt @@ -19,6 +19,7 @@ package com.lambda.module.modules.client import com.lambda.event.events.RenderEvent import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.renderer.gui.font.FontRenderer import com.lambda.graphics.renderer.gui.font.FontRenderer.drawString import com.lambda.module.Module import com.lambda.module.tag.ModuleTag diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt b/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt index fcf5a11d0..04fd3e50a 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt @@ -23,7 +23,6 @@ import com.lambda.event.events.PlayerEvent import com.lambda.event.events.RenderEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.graphics.pipeline.UIPipeline import com.lambda.graphics.renderer.esp.DynamicAABB import com.lambda.graphics.renderer.esp.builders.buildFilled import com.lambda.graphics.renderer.esp.builders.buildOutline diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt b/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt index bd894456f..8e32368d9 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt @@ -21,7 +21,6 @@ import com.lambda.Lambda.mc import com.lambda.context.SafeContext import com.lambda.event.events.* import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.graphics.pipeline.UIPipeline import com.lambda.graphics.renderer.esp.DynamicAABB import com.lambda.graphics.renderer.esp.builders.buildFilled import com.lambda.graphics.renderer.esp.builders.buildOutline diff --git a/common/src/main/kotlin/com/lambda/module/modules/render/Particles.kt b/common/src/main/kotlin/com/lambda/module/modules/render/Particles.kt index 66e52ef4b..95a87719a 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/render/Particles.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/render/Particles.kt @@ -33,6 +33,7 @@ import com.lambda.graphics.gl.Matrices import com.lambda.graphics.gl.Matrices.buildWorldProjection import com.lambda.graphics.gl.Matrices.withVertexTransform import com.lambda.graphics.shader.Shader +import com.lambda.graphics.shader.Shader.Companion.shader import com.lambda.interaction.rotation.Rotation import com.lambda.module.Module import com.lambda.module.modules.client.GuiSettings @@ -81,7 +82,7 @@ object Particles : Module( private var particles = mutableListOf() private val pipeline = VertexPipeline(VertexMode.TRIANGLES, VertexAttrib.Group.PARTICLE) - private val shader = Shader("renderer/particle", "renderer/particle") + private val shader = shader("renderer/particle", "renderer/particle") init { listen { diff --git a/common/src/main/kotlin/com/lambda/util/Mouse.kt b/common/src/main/kotlin/com/lambda/util/Mouse.kt index ea78f1c56..a215325a8 100644 --- a/common/src/main/kotlin/com/lambda/util/Mouse.kt +++ b/common/src/main/kotlin/com/lambda/util/Mouse.kt @@ -88,6 +88,7 @@ class Mouse { } } + // ToDo: replace by event class CursorController { private var lastSetCursor: Cursor? = null diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag b/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag index 2f4631388..eb7d163ed 100644 --- a/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag +++ b/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag @@ -6,25 +6,16 @@ uniform float u_SDFMin; uniform float u_SDFMax; in vec2 v_TexCoord; -in vec2 v_Scissor1; -in vec2 v_Scissor2; - in vec4 v_Color; out vec4 color; -bool scissorFailed(vec2 coord) { - return coord.x < v_Scissor1.x || coord.x > v_Scissor2.x || coord.y < v_Scissor1.y || coord.y > v_Scissor2.y; -} - void main() { vec2 coord = v_TexCoord; bool isEmoji = coord.x < 0.0; if (isEmoji) coord = -v_TexCoord; - if (scissorFailed(coord)) discard; - if (isEmoji) { color = texture(u_EmojiTexture, coord) * v_Color; return; diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag index 4e8656d47..e6ab31223 100644 --- a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag +++ b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag @@ -1,19 +1,20 @@ #version 330 core -uniform float u_Time; -uniform vec4 u_Color1; -uniform vec4 u_Color2; uniform vec2 u_Size; +uniform float u_RoundLeftTop; +uniform float u_RoundLeftBottom; +uniform float u_RoundRightBottom; +uniform float u_RoundRightTop; + +uniform float u_Shade; +uniform float u_ShadeTime; +uniform vec4 u_ShadeColor1; +uniform vec4 u_ShadeColor2; +uniform vec2 u_ShadeSize; in vec2 v_Position; in vec2 v_TexCoord; -in vec2 v_Scissor1; -in vec2 v_Scissor2; in vec4 v_Color; -in vec2 v_Size; -in vec2 v_RoundRadiusL; -in vec2 v_RoundRadiusR; -in float v_Shade; out vec4 color; @@ -28,15 +29,16 @@ vec4 noise() { } vec4 shade() { - if (v_Shade != 1.0) return v_Color; + if (u_Shade != 1.0) return v_Color; - vec2 pos = v_Position * u_Size; - float p = sin(pos.x - pos.y - u_Time) * 0.5 + 0.5; + vec2 pos = v_Position * u_ShadeSize; + float p = sin(pos.x - pos.y - u_ShadeTime) * 0.5 + 0.5; - return mix(u_Color1, u_Color2, p) * v_Color; + return mix(u_ShadeColor1, u_ShadeColor2, p) * v_Color; } float getRoundRadius() { + // ToDo: use step bool xcmp = v_TexCoord.x > 0.5; bool ycmp = v_TexCoord.y > 0.5; @@ -44,19 +46,15 @@ float getRoundRadius() { if (xcmp) { if (ycmp) { - // Right bottom - r = v_RoundRadiusR.y; + r = u_RoundRightBottom; } else { - // Right top - r = v_RoundRadiusR.x; + r = u_RoundRightTop; } } else { if (ycmp) { - // Left bottom - r = v_RoundRadiusL.y; + r = u_RoundLeftBottom; } else { - // Left top - r = v_RoundRadiusL.x; + r = u_RoundLeftTop; } } @@ -64,12 +62,12 @@ float getRoundRadius() { } vec4 round() { - vec2 halfSize = v_Size * 0.5; + vec2 halfSize = u_Size * 0.5; float radius = max(getRoundRadius(), SMOOTHING); vec2 smoothVec = vec2(SMOOTHING); - vec2 coord = mix(-smoothVec, v_Size + smoothVec, v_TexCoord); + vec2 coord = mix(-smoothVec, u_Size + smoothVec, v_TexCoord); vec2 center = halfSize - coord; float distance = length(max(abs(center) - halfSize + radius, 0.0)) - radius; @@ -78,11 +76,6 @@ vec4 round() { return vec4(1.0, 1.0, 1.0, clamp(alpha, 0.0, 1.0)); } -bool scissorFailed(vec2 coord) { - return coord.x < v_Scissor1.x || coord.x > v_Scissor2.x || coord.y < v_Scissor1.y || coord.y > v_Scissor2.y; -} - void main() { - if (scissorFailed(v_TexCoord)) discard; color = shade() * round() + noise(); } \ No newline at end of file diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_outline.frag b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_outline.frag index 529035d28..71e8680fe 100644 --- a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_outline.frag +++ b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_outline.frag @@ -1,27 +1,24 @@ #version 330 core -uniform float u_Time; -uniform vec4 u_Color1; -uniform vec4 u_Color2; -uniform vec2 u_Size; +uniform float u_Shade; +uniform float u_ShadeTime; +uniform vec4 u_ShadeColor1; +uniform vec4 u_ShadeColor2; +uniform vec2 u_ShadeSize; in vec2 v_Position; -in vec2 v_TexCoord; -in vec2 v_Scissor1; -in vec2 v_Scissor2; in float v_Alpha; in vec4 v_Color; -in float v_Shade; out vec4 color; vec4 shade() { - if (v_Shade != 1.0) return v_Color; + if (u_Shade != 1.0) return v_Color; - vec2 pos = v_Position * u_Size; - float p = sin(pos.x - pos.y - u_Time) * 0.5 + 0.5; + vec2 pos = v_Position * u_ShadeSize; + float p = sin(pos.x - pos.y - u_ShadeTime) * 0.5 + 0.5; - return mix(u_Color1, u_Color2, p) * v_Color; + return mix(u_ShadeColor1, u_ShadeColor2, p) * v_Color; } vec4 glow() { @@ -29,11 +26,6 @@ vec4 glow() { return vec4(1.0, 1.0, 1.0, newAlpha); } -bool scissorFailed(vec2 coord) { - return coord.x < v_Scissor1.x || coord.x > v_Scissor2.x || coord.y < v_Scissor1.y || coord.y > v_Scissor2.y; -} - void main() { - if (scissorFailed(v_TexCoord)) discard; color = shade() * glow(); } \ No newline at end of file diff --git a/common/src/main/resources/assets/lambda/shaders/vertex/font/font.vert b/common/src/main/resources/assets/lambda/shaders/vertex/font/font.vert index 169cab59b..5f16359df 100644 --- a/common/src/main/resources/assets/lambda/shaders/vertex/font/font.vert +++ b/common/src/main/resources/assets/lambda/shaders/vertex/font/font.vert @@ -2,26 +2,15 @@ layout (location = 0) in vec4 pos; layout (location = 1) in vec2 uv; -layout (location = 2) in vec2 sc1; -layout (location = 3) in vec2 sc2; -layout (location = 4) in vec4 color; +layout (location = 2) in vec4 color; uniform mat4 u_ProjModel; out vec2 v_TexCoord; -out vec2 v_Scissor1; -out vec2 v_Scissor2; - out vec4 v_Color; void main() { - vec4 proj = u_ProjModel * pos; - vec4 div = proj / proj.w; - gl_Position = vec4(div.x, div.y, 0.0, 1.0); - + gl_Position = u_ProjModel * pos; v_TexCoord = uv; - v_Scissor1 = sc1; - v_Scissor2 = sc2; - v_Color = color; } \ No newline at end of file diff --git a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert index 60664b63e..225d9dfd7 100644 --- a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert +++ b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert @@ -2,39 +2,18 @@ layout (location = 0) in vec4 pos; layout (location = 1) in vec2 uv; -layout (location = 2) in vec2 size; -layout (location = 3) in vec2 roundL; -layout (location = 4) in vec2 roundR; -layout (location = 5) in float shade; -layout (location = 6) in vec2 sc1; -layout (location = 7) in vec2 sc2; -layout (location = 8) in vec4 color; +layout (location = 2) in vec4 color; uniform mat4 u_ProjModel; out vec2 v_Position; out vec2 v_TexCoord; -out vec2 v_Scissor1; -out vec2 v_Scissor2; out vec4 v_Color; -out vec2 v_Size; -out vec2 v_RoundRadiusL; -out vec2 v_RoundRadiusR; -out float v_Shade; void main() { - vec4 proj = u_ProjModel * pos; - vec4 div = proj / proj.w; - gl_Position = vec4(div.x, div.y, 0.0, 1.0); + gl_Position = u_ProjModel * pos; v_Position = gl_Position.xy * 0.5 + 0.5; v_TexCoord = uv; - v_Scissor1 = sc1; - v_Scissor2 = sc2; v_Color = color; - - v_Size = size; - v_RoundRadiusL = roundL; - v_RoundRadiusR = roundR; - v_Shade = shade; } \ No newline at end of file diff --git a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_outline.vert b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_outline.vert index d6aad3b91..42e8d4ac0 100644 --- a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_outline.vert +++ b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_outline.vert @@ -3,30 +3,20 @@ layout (location = 0) in vec4 pos; layout (location = 1) in vec2 uv; layout (location = 2) in float alpha; -layout (location = 3) in float shade; -layout (location = 4) in vec2 sc1; -layout (location = 5) in vec2 sc2; -layout (location = 6) in vec4 color; +layout (location = 3) in vec4 color; uniform mat4 u_ProjModel; out vec2 v_Position; out vec2 v_TexCoord; -out vec2 v_Scissor1; -out vec2 v_Scissor2; out float v_Alpha; out vec4 v_Color; -out float v_Shade; void main() { gl_Position = u_ProjModel * pos; v_Position = gl_Position.xy * 0.5 + 0.5; v_TexCoord = uv; - v_Scissor1 = sc1; - v_Scissor2 = sc2; - v_Alpha = alpha; v_Color = color; - v_Shade = shade; } \ No newline at end of file From 218aa2d061c6d683fb5fa7d6bca6947ca27160eb Mon Sep 17 00:00:00 2001 From: blade Date: Wed, 12 Feb 2025 01:30:43 +0300 Subject: [PATCH 091/114] Frogor to enable scissors back --- .../main/kotlin/com/lambda/gui/component/layout/Layout.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt index c20f73bfd..acb3e5259 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt @@ -401,9 +401,8 @@ open class Layout( if (renderChildren) children.forEach { it.onEvent(e) } } - block() - //if (!properties.scissor) block() - //else ScissorAdapter.scissor(rect, block) + if (!properties.scissor) block() + else ScissorAdapter.scissor(rect, block) } } From 31157fcabefdafcae12a7b52260ef45c9d7b26c1 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 14 Feb 2025 14:34:23 -0500 Subject: [PATCH 092/114] Fixed styled minecraft texts being blank --- .../com/lambda/mixin/render/ChatHudMixin.java | 35 +++++ .../mixin/render/TextRendererMixin.java | 134 ------------------ .../renderer/gui/font/FontRenderer.kt | 36 +++-- .../renderer/gui/font/core/LambdaAtlas.kt | 2 +- .../renderer/gui/font/core/LambdaEmoji.kt | 4 +- .../module/modules/client/LambdaMoji.kt | 63 +++++++- .../main/resources/lambda.mixins.common.json | 2 +- 7 files changed, 124 insertions(+), 152 deletions(-) create mode 100644 common/src/main/java/com/lambda/mixin/render/ChatHudMixin.java delete mode 100644 common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java diff --git a/common/src/main/java/com/lambda/mixin/render/ChatHudMixin.java b/common/src/main/java/com/lambda/mixin/render/ChatHudMixin.java new file mode 100644 index 000000000..a1e8dd4b5 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/render/ChatHudMixin.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.mixin.render; + +import com.lambda.module.modules.client.LambdaMoji; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.hud.ChatHud; +import net.minecraft.text.OrderedText; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ChatHud.class) +public class ChatHudMixin { + @Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTextWithShadow(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/OrderedText;III)I")) + int redirectRenderCall(DrawContext instance, TextRenderer textRenderer, OrderedText text, int x, int y, int color) { + return instance.drawTextWithShadow(textRenderer, LambdaMoji.INSTANCE.parse(text, x, y, color), 0, y, 16777215 + (color << 24)); + } +} diff --git a/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java b/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java deleted file mode 100644 index 3ac455d20..000000000 --- a/common/src/main/java/com/lambda/mixin/render/TextRendererMixin.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.mixin.render; - -import com.lambda.Lambda; -import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas; -import com.lambda.graphics.renderer.gui.font.core.LambdaEmoji; -import com.lambda.module.modules.client.LambdaMoji; -import com.lambda.module.modules.client.RenderSettings; -import com.lambda.util.math.Vec2d; -import net.minecraft.client.font.TextRenderer; -import net.minecraft.client.render.VertexConsumerProvider; -import net.minecraft.text.OrderedText; -import net.minecraft.text.Text; -import org.joml.Matrix4f; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Overwrite; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; - -import java.awt.*; -import java.util.List; - -@Mixin(TextRenderer.class) -public abstract class TextRendererMixin { - @Shadow - protected abstract int drawInternal(String text, float x, float y, int color, boolean shadow, Matrix4f matrix, VertexConsumerProvider vertexConsumers, TextRenderer.TextLayerType layerType, int backgroundColor, int light, boolean mirror); - - @Shadow - protected abstract int drawInternal(OrderedText text, float x, float y, int color, boolean shadow, Matrix4f matrix, VertexConsumerProvider vertexConsumerProvider, TextRenderer.TextLayerType layerType, int backgroundColor, int light); - - /** - * @author Edouard127 - * @reason xx - */ - @Overwrite - public int draw( - String text, - float x, - float y, - int color, - boolean shadow, - Matrix4f matrix, - VertexConsumerProvider vertexConsumers, - TextRenderer.TextLayerType layerType, - int backgroundColor, - int light, - boolean rightToLeft - ) { - String parsed = neoLambda$parseEmojisAndRender(text, x, y, color); - - return this.drawInternal(parsed, x, y, color, shadow, matrix, vertexConsumers, layerType, backgroundColor, light, rightToLeft); - } - - /** - * @author Edouard127 - * @reason xx - */ - @Overwrite - public int draw( - OrderedText text, - float x, - float y, - int color, - boolean shadow, - Matrix4f matrix, - VertexConsumerProvider vertexConsumers, - TextRenderer.TextLayerType layerType, - int backgroundColor, - int light - ) { - StringBuilder builder = new StringBuilder(); - text.accept((index, style, c) -> { - builder.appendCodePoint(c); - return true; - }); - - String parsed = neoLambda$parseEmojisAndRender(builder.toString(), x, y, color); - - return this.drawInternal(Text.literal(parsed).asOrderedText(), x, y, color, shadow, matrix, vertexConsumers, layerType, backgroundColor, light); - } - - @Unique - private String neoLambda$parseEmojisAndRender(String raw, float x, float y, int color) { - if (LambdaMoji.INSTANCE.isDisabled()) return raw; - - List emojis = LambdaEmoji.Twemoji.parse(raw); - - for (String emoji : emojis) { - String constructed = ":" + emoji + ":"; - int index = raw.indexOf(constructed); - - if (LambdaAtlas.INSTANCE.get(RenderSettings.INSTANCE.getEmojiFont(), emoji) == null || - index == -1) continue; - - int height = Lambda.getMc().textRenderer.fontHeight; - int width = Lambda.getMc().textRenderer.getWidth(raw.substring(0, index)); - - // Dude I'm sick of working with the shitcode that is minecraft's codebase :sob: - Color trueColor = switch (color) { - case 0x00E0E0E0, 0 -> new Color(255, 255, 255, 255); - default -> new Color(255, 255, 255, (color >> 24 & 0xFF)); - }; - - LambdaMoji.INSTANCE.push(constructed, new Vec2d(x + width, y + (float) height / 2), trueColor); - - // Replace the emoji with whitespaces depending on the player's settings - raw = raw.replaceFirst(constructed, neoLambda$getReplacement()); - } - - return raw; - } - - @Unique - private String neoLambda$getReplacement() { - int emojiWidth = (int) (((double) Lambda.getMc().textRenderer.fontHeight / 2 / Lambda.getMc().textRenderer.getWidth(" ")) * LambdaMoji.INSTANCE.getScale()); - return " ".repeat(emojiWidth); - } -} diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index fa2d71886..44fbdb0ba 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -19,12 +19,10 @@ package com.lambda.graphics.renderer.gui.font import com.lambda.graphics.buffer.VertexPipeline import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib -import com.lambda.graphics.pipeline.ScissorAdapter import com.lambda.graphics.renderer.gui.AbstractGUIRenderer import com.lambda.graphics.renderer.gui.font.core.GlyphInfo import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.get import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.height -import com.lambda.graphics.shader.Shader import com.lambda.graphics.shader.Shader.Companion.shader import com.lambda.graphics.texture.TextureOwner.bind import com.lambda.module.modules.client.LambdaMoji @@ -33,7 +31,6 @@ import com.lambda.util.math.Vec2d import com.lambda.util.math.a import com.lambda.util.math.setAlpha import java.awt.Color -import java.awt.Font /** * Renders text and emoji glyphs using a shader-based font rendering system. @@ -77,6 +74,29 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ } } + fun drawGlyph( + glyph: GlyphInfo, + position: Vec2d, + color: Color = Color.WHITE, + scale: Double = 1.0 + ) = render { + shader["u_FontTexture"] = 0 + shader["u_EmojiTexture"] = 1 + shader["u_SDFMin"] = 0.3 + shader["u_SDFMax"] = 1.0 + + bind(chars, emojis) + + val actualScale = getScaleFactor(scale) + val scaledSize = glyph.size * actualScale + + val posY = getHeight(scale) * -0.5 + baselineOffset * actualScale + val pos1 = Vec2d(0.0, posY) * actualScale + val pos2 = pos1 + scaledSize + + buildGlyph(glyph, position, pos1, pos2, color) + } + /** * Renders a single glyph at a given position. * @@ -89,9 +109,9 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ private fun VertexPipeline.buildGlyph( glyph: GlyphInfo, origin: Vec2d = Vec2d.ZERO, - pos1: Vec2d = Vec2d.ZERO, - pos2: Vec2d = pos1 + glyph.size, - color: Color = Color.WHITE, + pos1: Vec2d, + pos2: Vec2d, + color: Color, ) { val x1 = pos1.x + origin.x val y1 = pos1.y + origin.y @@ -198,9 +218,9 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ // Iterate the emojis from left to right val start = section.indexOf(emoji) - val end = start + emoji.length + 1 + val end = start + emoji.length - val preEmojiText = section.substring(0, start - 1) + val preEmojiText = section.substring(0, start) val postEmojiText = section.substring(end) // Draw the text without emoji diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt index d7da86104..2eaf82985 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt @@ -73,7 +73,7 @@ object LambdaAtlas : Loadable { private val heightCache = Object2DoubleArrayMap() operator fun LambdaFont.get(char: Char): GlyphInfo? = fontMap.getValue(this)[char] - operator fun LambdaEmoji.get(string: String): GlyphInfo? = emojiMap.getValue(this)[string] + operator fun LambdaEmoji.get(string: String): GlyphInfo? = emojiMap.getValue(this)[string.removeSurrounding(":")] val LambdaFont.height: Double get() = heightCache.getDouble(fontCache[this@height]) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaEmoji.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaEmoji.kt index 1220fa001..0175f2156 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaEmoji.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaEmoji.kt @@ -32,10 +32,10 @@ enum class LambdaEmoji(val url: String) { * @return A list of parsed strings that does not contain the colons */ fun parse(text: String): MutableList = - emojiRegex.findAll(text).map { it.value.drop(1).dropLast(1) }.toMutableList() + emojiRegex.findAll(text).map { it.value }.toMutableList() fun load(): String { entries.forEach { it.buildBuffer() } return "Loaded ${entries.size} emoji sets" } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt b/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt index 88d6f1e80..b97bca99b 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt @@ -17,15 +17,20 @@ package com.lambda.module.modules.client +import com.lambda.Lambda.mc import com.lambda.event.events.RenderEvent import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.graphics.renderer.gui.font.FontRenderer -import com.lambda.graphics.renderer.gui.font.FontRenderer.drawString +import com.lambda.graphics.renderer.gui.font.FontRenderer.drawGlyph +import com.lambda.graphics.renderer.gui.font.core.GlyphInfo +import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.get import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.util.math.Vec2d +import net.minecraft.text.OrderedText +import net.minecraft.text.Style import java.awt.Color +// This is the worst code I have ever wrote in my life object LambdaMoji : Module( name = "LambdaMoji", description = "", @@ -35,17 +40,63 @@ object LambdaMoji : Module( val scale by setting("Emoji Scale", 1.0, 0.5..1.5, 0.1) val suggestions by setting("Chat Suggestions", true) - private val renderQueue = mutableListOf>() + private val emojiWhitespace: String + get() = " ".repeat(((mc.textRenderer.fontHeight / 2 / mc.textRenderer.getWidth(" ")) * scale).toInt()) + + private val renderQueue = mutableListOf>() init { listen { - renderQueue.forEach { (text, position, color) -> - drawString(text, position, color, scale = scale) + renderQueue.forEach { (glyph, position, color) -> + drawGlyph(glyph, position, color) } renderQueue.clear() } } - fun push(text: String, position: Vec2d, color: Color) = renderQueue.add(Triple(text, position, color)) + // FixMe: Doesn't render properly when the chat scale is modified + fun parse(text: OrderedText, x: Float, y: Float, color: Int): OrderedText { + val saved = mutableMapOf() + val builder = StringBuilder() + + var absoluteIndex = 0 + text.accept { _, style, codePoint -> + saved[absoluteIndex++] = style + builder.appendCodePoint(codePoint) + true + } + + var raw = builder.toString() + RenderSettings.emojiFont.parse(raw) + .forEach { emoji -> + val index = raw.indexOf(emoji) + if (index == -1) return@forEach + + val height = mc.textRenderer.fontHeight + val width = mc.textRenderer.getWidth(raw.substring(0, index)) + + // Dude I'm sick of working with the shitcode that is minecraft's codebase :sob: + val trueColor = when (color) { + 0x00E0E0E0, 0 -> Color(255, 255, 255, 255) + else -> Color(255, 255, 255, (color shr 24 and 0xFF)) + } + + val glyph = RenderSettings.emojiFont[emoji]!! + renderQueue.add(Triple(glyph, Vec2d(x + width, y + height / 2), trueColor)) + + // Replace the emoji with whitespaces depending on the player's settings + raw = raw.replaceFirst(emoji, emojiWhitespace) + } + + val constructed = mutableListOf() + + // Will not work properly if the emoji is part of the style + saved.forEach { (charIndex: Int, style: Style) -> + if (charIndex >= raw.length) return@forEach + constructed.add(OrderedText.styledForwardsVisitedString(raw.substring(charIndex, charIndex + 1), style)) + } + + return OrderedText.concat(constructed) + } } diff --git a/common/src/main/resources/lambda.mixins.common.json b/common/src/main/resources/lambda.mixins.common.json index ec004c4ee..3fb046381 100644 --- a/common/src/main/resources/lambda.mixins.common.json +++ b/common/src/main/resources/lambda.mixins.common.json @@ -28,6 +28,7 @@ "render.BackgroundRendererMixin", "render.BlockRenderManagerMixin", "render.CameraMixin", + "render.ChatHudMixin", "render.ChatInputSuggestorMixin", "render.ChatScreenMixin", "render.DebugHudMixin", @@ -45,7 +46,6 @@ "render.ScreenHandlerMixin", "render.SplashOverlayMixin", "render.SplashOverlayMixin$LogoTextureMixin", - "render.TextRendererMixin", "render.VertexBufferMixin", "render.WorldRendererMixin", "world.BlockCollisionSpliteratorMixin", From 081b67716af89815fbefdc42cb779757929ddb0d Mon Sep 17 00:00:00 2001 From: "blade.kt" Date: Wed, 19 Feb 2025 00:18:54 +0300 Subject: [PATCH 093/114] misc ui changes --- .../renderer/gui/font/FontRenderer.kt | 16 +++++++----- .../com/lambda/gui/component/layout/Layout.kt | 13 ++++++++++ .../lambda/gui/component/window/TitleBar.kt | 19 ++++++++++++++ .../com/lambda/gui/component/window/Window.kt | 25 ++++--------------- .../gui/component/window/WindowContent.kt | 1 - .../lambda/gui/impl/clickgui/ModuleLayout.kt | 10 +++++--- .../lambda/gui/impl/clickgui/SettingLayout.kt | 9 +++++-- .../lambda/module/modules/client/ClickGui.kt | 14 +++++------ .../fragment/renderer/rect_filled.frag | 2 +- 9 files changed, 68 insertions(+), 41 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index 44fbdb0ba..a3774186c 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -27,6 +27,7 @@ import com.lambda.graphics.shader.Shader.Companion.shader import com.lambda.graphics.texture.TextureOwner.bind import com.lambda.module.modules.client.LambdaMoji import com.lambda.module.modules.client.RenderSettings +import com.lambda.util.math.MathUtils.toInt import com.lambda.util.math.Vec2d import com.lambda.util.math.a import com.lambda.util.math.setAlpha @@ -69,7 +70,7 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ bind(chars, emojis) - processText(text, color, scale, shadow, parseEmoji) { char, pos1, pos2, col -> + processText(text, color, scale, shadow, parseEmoji) { char, pos1, pos2, col, _ -> buildGlyph(char, position, pos1, pos2, col) } } @@ -142,7 +143,9 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ parseEmoji: Boolean = LambdaMoji.isEnabled, ): Double { var width = 0.0 - processText(text, scale = scale, parseEmoji = parseEmoji) { char, _, _, _ -> width += char.width } + processText(text, scale = scale, parseEmoji = parseEmoji) { + char, _, _, _, isShadow -> width += char.width * isShadow.toInt() + } return width * getScaleFactor(scale) } @@ -170,7 +173,7 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ scale: Double = 1.0, shadow: Boolean = RenderSettings.shadow, parseEmoji: Boolean = LambdaMoji.isEnabled, - block: (GlyphInfo, Vec2d, Vec2d, Color) -> Unit + block: (GlyphInfo, Vec2d, Vec2d, Color, Boolean) -> Unit ) { val actualScale = getScaleFactor(scale) val scaledGap = gap * actualScale @@ -183,13 +186,14 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ fun drawGlyph(info: GlyphInfo?, color: Color, offset: Double = 0.0) { if (info == null) return + val isShadow = offset != 0.0 val scaledSize = info.size * actualScale val pos1 = Vec2d(posX, posY) + offset * actualScale val pos2 = pos1 + scaledSize - block(info, pos1, pos2, color) - if (offset == 0.0) posX += scaledSize.x + scaledGap + block(info, pos1, pos2, color, isShadow) + if (!isShadow) posX += scaledSize.x + scaledGap } val parsed = if (parseEmoji) emojis.parse(text) else mutableListOf() @@ -244,7 +248,7 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ * @param scale The base scale factor. * @return The adjusted scale factor. */ - fun getScaleFactor(scale: Double): Double = scale * 9.0 / chars.height + fun getScaleFactor(scale: Double): Double = scale * 8.5 / chars.height /** * Calculates the shadow color by adjusting the brightness of the input color. diff --git a/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt index acb3e5259..5d97358ba 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt @@ -317,6 +317,19 @@ open class Layout( overrideHeight(overrideHeight) } + /** + * Removes this layout from its parent + */ + fun destroy() { + check(owner != null) { + "Unable to destroy root layout. Owner is null." + } + + check(owner.children.remove(this)) { + "destroy() called twice. The layout was already removed" + } + } + init { onUpdate { // Update the layout screenSize = RenderMain.screenSize diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt index f9d0148d4..746e064b9 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt @@ -19,11 +19,13 @@ package com.lambda.gui.component.window import com.lambda.module.modules.client.ClickGui import com.lambda.gui.component.HAlign +import com.lambda.gui.component.core.FilledRect.Companion.rect import com.lambda.gui.component.core.TextField.Companion.textField import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout import com.lambda.util.Mouse import com.lambda.util.math.Vec2d +import com.lambda.util.math.lerp /** * Represents a titlebar component @@ -58,6 +60,23 @@ class TitleBar( } } + val backgroundRect = rect { + onUpdate { + rectangle = this@TitleBar.rect + setColor(ClickGui.titleBackgroundColor) + + val radius = ClickGui.roundRadius + leftTopRadius = radius + rightTopRadius = radius + + val bottomRadius = lerp(owner.content.renderHeight, radius, 0.0) + leftBottomRadius = bottomRadius + rightBottomRadius = bottomRadius + + shade = ClickGui.backgroundShade + } + } + val textField = textField { text = title diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt index 69b0cd338..890808b6c 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt @@ -41,7 +41,7 @@ open class Window( owner: Layout, initialTitle: String = "Untitled", initialPosition: Vec2d = Vec2d.ZERO, - initialSize: Vec2d = Vec2d(120.0, 300.0), + initialSize: Vec2d = Vec2d(110, 300), draggable: Boolean = true, scrollable: Boolean = true, private val minimizing: Minimizing = Minimizing.Relative, @@ -52,26 +52,9 @@ open class Window( private val cursorController = cursorController() val titleBar = titleBar(initialTitle, draggable) - val content = windowContent(scrollable) - protected val titleBarRect = rect { - onUpdate { - rectangle = titleBar.rect - setColor(ClickGui.titleBackgroundColor) - - val radius = ClickGui.roundRadius - leftTopRadius = radius - rightTopRadius = radius - - val bottomRadius = lerp(content.renderHeight, radius, 0.0) - leftBottomRadius = bottomRadius - rightBottomRadius = bottomRadius - - shade = ClickGui.backgroundShade - } - } - - protected val contentRect = rect { + protected val titleBarBackground by titleBar::backgroundRect + protected val contentBackground = rect { // It's here because content cannot contain something by default onUpdate { rectangle = Rect(titleBar.leftBottom, this@Window.rightBottom) setColor(ClickGui.backgroundColor) @@ -83,6 +66,8 @@ open class Window( } } + val content = windowContent(scrollable) + protected val outlineRect = outline { onUpdate { rectangle = this@Window.rect diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt index 702cc01d8..52b94f6c7 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt @@ -18,7 +18,6 @@ package com.lambda.gui.component.window import com.lambda.graphics.animation.Animation.Companion.exp -import com.lambda.event.events.GuiEvent import com.lambda.gui.component.core.LayoutBuilder import com.lambda.module.modules.client.ClickGui import com.lambda.gui.component.core.UIBuilder diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt index 1d9ea72f3..e7f0fe369 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt @@ -33,11 +33,13 @@ import java.awt.Color class ModuleLayout( owner: Layout, - module: Module + module: Module, + initialPosition: Vec2d = Vec2d.ZERO, + initialSize: Vec2d = Vec2d(100, 18) ) : Window( owner, module.name, - Vec2d.ZERO, Vec2d.ZERO, + initialPosition, initialSize, false, true, Minimizing.Relative, false, AutoResize.ForceEnabled ) { @@ -117,12 +119,12 @@ class ModuleLayout( } } - titleBarRect.onUpdate { + titleBarBackground.onUpdate { setColor(lerp(enableAnimation, ClickGui.moduleDisabledColor, ClickGui.moduleEnabledColor)) correctRadius() } - contentRect.onUpdate { + contentBackground.onUpdate { setColor(lerp(enableAnimation, ClickGui.moduleDisabledColor, ClickGui.moduleEnabledColor)) correctRadius() } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt index a11cc83d5..b63c0ade9 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt @@ -35,7 +35,7 @@ abstract class SettingLayout > ( owner: Layout, val setting: T, expandable: Boolean = false -) : Window( // going to use window to easily implement expandable settings (such as color picker) +) : Window( owner, setting.name, Vec2d.ZERO, Vec2d.ZERO, @@ -73,7 +73,12 @@ abstract class SettingLayout > ( } } - children.removeAll(listOf(titleBarRect, contentRect, outlineRect)) + listOf( + titleBarBackground, + contentBackground, + outlineRect + ).forEach(Layout::destroy) + if (!expandable) children.remove(content) } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt index 7fce88356..18bdaf966 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt @@ -32,21 +32,21 @@ object ClickGui : Module( defaultTags = setOf(ModuleTag.CLIENT) ) { val titleBarHeight by setting("Title Bar Height", 18.0, 10.0..25.0, 0.1) - val moduleHeight by setting("Module Height", 18.0, 10.0..25.0, 0.1) + val moduleHeight by setting("Module Height", 16.0, 10.0..25.0, 0.1) val settingsHeight by setting("Settings Height", 14.0, 10.0..25.0, 0.1) - val padding by setting("Padding", 2.0, 1.0..6.0, 0.1) - val listStep by setting("List Step", 2.0, 0.0..6.0, 0.1) - val autoResize by setting("Auto Resize", false) + val padding by setting("Padding", 1.0, 1.0..6.0, 0.1) + val listStep by setting("List Step", 1.0, 0.0..6.0, 0.1) + val autoResize by setting("Auto Resize", true) val roundRadius by setting("Round Radius", 2.0, 0.0..10.0, 0.1) val backgroundTint by setting("Background Tint", Color.BLACK.setAlpha(0.4)) - val titleBackgroundColor by setting("Title Background Color", Color.WHITE.setAlpha(0.4)) - val backgroundColor by setting("Background Color", Color.WHITE.setAlpha(0.25)) + val titleBackgroundColor by setting("Title Background Color", Color(40, 40, 40)) + val backgroundColor by setting("Background Color", titleBackgroundColor) val backgroundShade by setting("Background Shade", true) - val outline by setting("Outline", true) + val outline by setting("Outline", false) val outlineWidth by setting("Outline Width", 10.0, 1.0..10.0, 0.1) { outline } val outlineColor by setting("Outline Color", Color.WHITE.setAlpha(0.6)) { outline } val outlineShade by setting("Outline Shade", true) { outline } diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag index e6ab31223..97c5b5d34 100644 --- a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag +++ b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag @@ -19,7 +19,7 @@ in vec4 v_Color; out vec4 color; #define SMOOTHING 0.25 -#define NOISE_GRANULARITY 0.005 +#define NOISE_GRANULARITY 0.004 vec4 noise() { // https://shader-tutorial.dev/advanced/color-banding-dithering/ From de2490c04147bd778ac767bfc78d0280ca172215 Mon Sep 17 00:00:00 2001 From: "blade.kt" Date: Wed, 19 Feb 2025 15:34:37 +0300 Subject: [PATCH 094/114] Fix: font renderer width issue --- .../renderer/gui/font/FontRenderer.kt | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index a3774186c..d62434e09 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -41,7 +41,7 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ private val chars get() = RenderSettings.textFont private val emojis get() = RenderSettings.emojiFont - private val shadowShift get() = RenderSettings.shadowShift * 5.0 + private val shadowShift get() = RenderSettings.shadowShift * 10.0 private val baselineOffset get() = RenderSettings.baselineOffset * 2.0f - 10f private val gap get() = RenderSettings.gap * 0.5f - 0.8f @@ -65,7 +65,7 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ ) = render { shader["u_FontTexture"] = 0 shader["u_EmojiTexture"] = 1 - shader["u_SDFMin"] = 0.3 + shader["u_SDFMin"] = 0.4 shader["u_SDFMax"] = 1.0 bind(chars, emojis) @@ -83,7 +83,7 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ ) = render { shader["u_FontTexture"] = 0 shader["u_EmojiTexture"] = 1 - shader["u_SDFMin"] = 0.3 + shader["u_SDFMin"] = 0.4 shader["u_SDFMax"] = 1.0 bind(chars, emojis) @@ -143,10 +143,14 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ parseEmoji: Boolean = LambdaMoji.isEnabled, ): Double { var width = 0.0 - processText(text, scale = scale, parseEmoji = parseEmoji) { - char, _, _, _, isShadow -> width += char.width * isShadow.toInt() + var gaps = -1 + + processText(text, scale = scale, parseEmoji = parseEmoji) { char, _, _, _, isShadow -> + if (isShadow) return@processText + width += char.width; gaps++ } - return width * getScaleFactor(scale) + + return (width + gaps.coerceAtLeast(0) * gap) * getScaleFactor(scale) } /** @@ -184,12 +188,11 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ var posX = 0.0 var posY = getHeight(scale) * -0.5 + baselineOffset * actualScale - fun drawGlyph(info: GlyphInfo?, color: Color, offset: Double = 0.0) { + fun drawGlyph(info: GlyphInfo?, color: Color, isShadow: Boolean = false) { if (info == null) return - val isShadow = offset != 0.0 val scaledSize = info.size * actualScale - val pos1 = Vec2d(posX, posY) + offset * actualScale + val pos1 = Vec2d(posX, posY) + shadowShift * actualScale * isShadow.toInt() val pos2 = pos1 + scaledSize block(info, pos1, pos2, color, isShadow) @@ -210,7 +213,7 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ val glyph = chars[char] ?: return@forEach - if (shadow && shadowShift > 0.0) drawGlyph(glyph, shadowColor, shadowShift) + if (shadow) drawGlyph(glyph, shadowColor, true) drawGlyph(glyph, color) } } else { From b513baf3477efd097e614a4e307bc011c1fafea4 Mon Sep 17 00:00:00 2001 From: "blade.kt" Date: Fri, 21 Feb 2025 20:17:01 +0300 Subject: [PATCH 095/114] Fancy design --- .../lambda/graphics/animation/Animation.kt | 2 +- .../lambda/graphics/buffer/VertexPipeline.kt | 5 +- .../graphics/buffer/vertex/ElementBuffer.kt | 8 +- .../graphics/buffer/vertex/VertexBuffer.kt | 2 +- .../renderer/gui/font/FontRenderer.kt | 4 +- .../renderer/gui/rect/FilledRectRenderer.kt | 2 +- .../lambda/gui/component/core/FilledRect.kt | 24 ++- .../lambda/gui/component/core/OutlineRect.kt | 26 ++- .../lambda/gui/component/core/TextField.kt | 19 +++ .../lambda/gui/component/core/UIBuilder.kt | 13 ++ .../com/lambda/gui/component/layout/Layout.kt | 20 +-- .../lambda/gui/component/window/TitleBar.kt | 1 - .../com/lambda/gui/component/window/Window.kt | 47 +++++- .../gui/component/window/WindowContent.kt | 18 ++- .../lambda/gui/impl/clickgui/ModuleLayout.kt | 153 +++++++++++------- .../lambda/gui/impl/clickgui/SettingLayout.kt | 10 +- .../impl/clickgui/settings/BooleanButton.kt | 2 +- .../com/lambda/module/hud/TickShiftCharge.kt | 8 +- .../lambda/module/modules/client/ClickGui.kt | 20 ++- .../module/modules/client/GuiSettings.kt | 2 +- 20 files changed, 265 insertions(+), 121 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt b/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt index 286740dfb..5fa588b76 100644 --- a/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt +++ b/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt @@ -52,7 +52,7 @@ class Animation(initialValue: Double, val update: (Double) -> Double) { fun AnimationTicker.exp(min: Double, max: Double, speed: Double, flag: () -> Boolean) = exp({ min }, { max }, { speed }, flag) - fun AnimationTicker.exp(target: () -> Double, speed: Double) = + fun AnimationTicker.exp(speed: Double, target: () -> Double) = exp(target, target, { speed }, { true }) fun AnimationTicker.exp(min: () -> Double, max: () -> Double, speed: () -> Double, flag: () -> Boolean) = diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt index 808b690ee..d7f4a97cf 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt @@ -39,13 +39,14 @@ import java.awt.Color class VertexPipeline( private val mode: VertexMode, attributes: VertexAttrib.Group, + usage: Int = GL_DYNAMIC_DRAW ) : IRenderContext { private val stride = attributes.stride private val size = stride * mode.indicesCount private val vao = VertexArray() - private val vbo = VertexBuffer(mode, attributes) - private val ebo = ElementBuffer(mode) + private val vbo = VertexBuffer(mode, attributes, usage) + private val ebo = ElementBuffer(mode, usage) private var vertices = byteBuffer(size * 1.kibibyte) private var verticesPointer = address(vertices) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt index 700ff8a8f..116c16a85 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt @@ -23,10 +23,10 @@ import com.lambda.graphics.gl.kibibyte import org.lwjgl.opengl.GL30C.* import java.nio.ByteBuffer -class ElementBuffer(mode: VertexMode) : - Buffer(buffers = 1) -{ - override val usage: Int = GL_DYNAMIC_DRAW +class ElementBuffer( + mode: VertexMode, + override val usage: Int = GL_DYNAMIC_DRAW, +) : Buffer(buffers = 1) { override val target: Int = GL_ELEMENT_ARRAY_BUFFER override val access: Int = GL_MAP_WRITE_BIT diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt index 1d9ff8185..94358fb09 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt @@ -27,8 +27,8 @@ import java.nio.ByteBuffer class VertexBuffer( mode: VertexMode, attributes: VertexAttrib.Group, -) : Buffer(buffers = 1) { override val usage: Int = GL_DYNAMIC_DRAW +) : Buffer(buffers = 1) { override val target: Int = GL_ARRAY_BUFFER override val access: Int = GL_MAP_WRITE_BIT diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index d62434e09..0c2d0e8a4 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -60,7 +60,7 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ position: Vec2d = Vec2d.ZERO, color: Color = Color.WHITE, scale: Double = 1.0, - shadow: Boolean = RenderSettings.shadow, + shadow: Boolean = true, parseEmoji: Boolean = LambdaMoji.isEnabled ) = render { shader["u_FontTexture"] = 0 @@ -213,7 +213,7 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ val glyph = chars[char] ?: return@forEach - if (shadow) drawGlyph(glyph, shadowColor, true) + if (shadow && RenderSettings.shadow) drawGlyph(glyph, shadowColor, true) drawGlyph(glyph, color) } } else { diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt index a4178b69b..b786e621b 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt @@ -28,7 +28,7 @@ object FilledRectRenderer : AbstractGUIRenderer( VertexAttrib.Group.RECT_FILLED, shader("renderer/rect_filled") ) { private const val MIN_SIZE = 0.5 - private const val MIN_ALPHA = 3 + private const val MIN_ALPHA = 1 fun filledRect( rect: Rect, diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt b/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt index bcf8a0266..d8a52a405 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt @@ -18,6 +18,7 @@ package com.lambda.gui.component.core import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer.filledRect +import com.lambda.gui.component.core.FilledRect.Companion.rectBehind import com.lambda.gui.component.layout.Layout import com.lambda.util.math.Rect import java.awt.Color @@ -97,7 +98,26 @@ class FilledRect( * Creates a [FilledRect] component - layout-based rect representation */ @UIBuilder - fun Layout.rect(block: FilledRect.() -> Unit = {}) = - FilledRect(this).apply(children::add).apply(block) + fun Layout.rect( + block: FilledRect.() -> Unit = {} + ) = FilledRect(this).apply(children::add).apply(block) + + /** + * Adds a [FilledRect] behind given [layout] + */ + @UIBuilder + fun Layout.rectBehind( + layout: Layout, + block: FilledRect.() -> Unit = {} + ) = FilledRect(this).relativeLayout(this, layout, false).apply(block) + + /** + * Adds a [FilledRect] over given [layout] + */ + @UIBuilder + fun Layout.rectOver( + layout: Layout, + block: FilledRect.() -> Unit = {} + ) = FilledRect(this).relativeLayout(this, layout, true).apply(block) } } diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt b/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt index c34124c2a..c3f9a8044 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt @@ -18,6 +18,7 @@ package com.lambda.gui.component.core import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer.outlineRect +import com.lambda.gui.component.core.FilledRect.Companion.rectBehind import com.lambda.gui.component.layout.Layout import java.awt.Color @@ -62,10 +63,29 @@ class OutlineRect( companion object { /** - * Creates a [OutlineRect] component - layout-based rect representation + * Creates an [OutlineRect] component - layout-based rect representation */ @UIBuilder - fun Layout.outline(block: OutlineRect.() -> Unit = {}) = - OutlineRect(this).apply(children::add).apply(block) + fun Layout.outline( + block: OutlineRect.() -> Unit = {} + ) = OutlineRect(this).apply(children::add).apply(block) + + /** + * Adds a [OutlineRect] behind given [layout] + */ + @UIBuilder + fun Layout.outlineBehind( + layout: Layout, + block: OutlineRect.() -> Unit = {} + ) = OutlineRect(this).relativeLayout(this, layout, false).apply(block) + + /** + * Creates an [OutlineRect] component - layout-based rect representation + */ + @UIBuilder + fun Layout.outlineOver( + layout: Layout, + block: OutlineRect.() -> Unit = {} + ) = OutlineRect(this).relativeLayout(this, layout, true).apply(block) } } diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt index 7b14c69e9..6fcf12c16 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt @@ -21,6 +21,7 @@ import com.lambda.graphics.renderer.gui.font.FontRenderer import com.lambda.graphics.renderer.gui.font.FontRenderer.drawString import com.lambda.gui.component.HAlign import com.lambda.gui.component.VAlign +import com.lambda.gui.component.core.OutlineRect.Companion.outlineBehind import com.lambda.gui.component.layout.Layout import com.lambda.util.math.Vec2d import com.lambda.util.math.lerp @@ -62,5 +63,23 @@ class TextField( fun Layout.textField( block: TextField.() -> Unit = {} ) = TextField(this).apply(children::add).apply(block) + + /** + * Adds a [TextField] behind given [layout] + */ + @UIBuilder + fun Layout.textFieldBehind( + layout: Layout, + block: TextField.() -> Unit = {} + ) = TextField(this).relativeLayout(this, layout, false).apply(block) + + /** + * Adds a [TextField] over given [layout] + */ + @UIBuilder + fun Layout.textFieldOver( + layout: Layout, + block: TextField.() -> Unit = {} + ) = TextField(this).relativeLayout(this, layout, true).apply(block) } } diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt b/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt index 627f45dbf..0e297575d 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt @@ -17,6 +17,9 @@ package com.lambda.gui.component.core +import com.lambda.gui.component.layout.Layout +import com.lambda.util.math.MathUtils.toInt + @DslMarker annotation class UIBuilder @@ -25,3 +28,13 @@ annotation class LayoutBuilder @DslMarker annotation class UIRenderPr0p3rty + +fun T.relativeLayout( + owner: Layout, + base: Layout, + next: Boolean +) = apply { + val index = owner.children.indexOf(base) + check(index != -1 && base.owner == owner) { "Given layout belongs to different owner" } + owner.children.add(index + next.toInt(), this) +} diff --git a/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt index 5d97358ba..ed1e6b936 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt @@ -130,16 +130,16 @@ open class Layout( var isHovered = false; get() = field && (owner?.isHovered ?: true) // Actions - private var showActions = mutableListOf Unit>() - private var hideActions = mutableListOf Unit>() - private var tickActions = mutableListOf Unit>() - private var updateActions = mutableListOf Unit>() - private var renderActions = mutableListOf Unit>() - private var keyPressActions = mutableListOf Unit>() - private var charTypedActions = mutableListOf Unit>() - private var mouseClickActions = mutableListOf Unit>() - private var mouseMoveActions = mutableListOf Unit>() - private var mouseScrollActions = mutableListOf Unit>() + private val showActions = mutableListOf Unit>() + private val hideActions = mutableListOf Unit>() + private val tickActions = mutableListOf Unit>() + private val updateActions = mutableListOf Unit>() + private val renderActions = mutableListOf Unit>() + private val keyPressActions = mutableListOf Unit>() + private val charTypedActions = mutableListOf Unit>() + private val mouseClickActions = mutableListOf Unit>() + private val mouseMoveActions = mutableListOf Unit>() + private val mouseScrollActions = mutableListOf Unit>() /** * Performs the action on this layout diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt index 746e064b9..83f11cc4b 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt @@ -79,7 +79,6 @@ class TitleBar( val textField = textField { text = title - textHAlignment = HAlign.CENTER onUpdate { diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt index 890808b6c..a018f5545 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt @@ -21,6 +21,7 @@ import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.module.modules.client.ClickGui import com.lambda.gui.ScreenLayout import com.lambda.gui.component.core.FilledRect.Companion.rect +import com.lambda.gui.component.core.LayoutBuilder import com.lambda.gui.component.core.OutlineRect.Companion.outline import com.lambda.gui.component.layout.Layout import com.lambda.gui.component.core.UIBuilder @@ -30,7 +31,6 @@ import com.lambda.util.Mouse import com.lambda.util.math.MathUtils.toInt import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d -import com.lambda.util.math.lerp /** * Represents a window component @@ -41,7 +41,7 @@ open class Window( owner: Layout, initialTitle: String = "Untitled", initialPosition: Vec2d = Vec2d.ZERO, - initialSize: Vec2d = Vec2d(110, 300), + initialSize: Vec2d = Vec2d(110, 350), draggable: Boolean = true, scrollable: Boolean = true, private val minimizing: Minimizing = Minimizing.Relative, @@ -80,6 +80,30 @@ open class Window( } } + // Actions + private val expandActions = mutableListOf Unit>() + private val minimizeActions = mutableListOf Unit>() + + /** + * Sets the action to be performed when the window content gets opened. + * + * @param action The action to be performed. + */ + @LayoutBuilder + fun T.onWindowExpand(action: T.() -> Unit) { + expandActions += { action() } + } + + /** + * Sets the action to be performed when the window content gets closed. + * + * @param action The action to be performed. + */ + @LayoutBuilder + fun T.onWindowMinimize(action: T.() -> Unit) { + minimizeActions += { action() } + } + // Position // ToDo find a way to animate this only when dragging /*private val renderX by animation.exp(position::x, 0.8) @@ -87,12 +111,19 @@ open class Window( private val renderPosition get() = Vec2d(renderX, renderY)*/ // Minimizing - var minimized = false + var isMinimized = false; set(value) { + if (field == value) return + field = value + + val actions = if (!value) expandActions else minimizeActions + actions.forEach { it(this) } + } + private var heightAnimation by animation.exp( min = { 0.0 }, max = { if (minimizing == Minimizing.Relative) targetHeight else 1.0 }, speed = 0.8, - flag = { !minimized } + flag = { !isMinimized } ) private val targetHeight get() = if (!autoResize.enabled) height - titleBar.renderHeight else content.getContentHeight() @@ -107,7 +138,7 @@ open class Window( position = initialPosition size = initialSize - overrideSize(animation.exp(::width, 0.8)::value) { + overrideSize(animation.exp(0.8, ::width)::value) { titleBar.renderHeight + when (minimizing) { Minimizing.Disabled -> targetHeight Minimizing.Relative -> heightAnimation @@ -123,7 +154,7 @@ open class Window( if (minimizing == Minimizing.Disabled) return@onMouseClick if (button != Mouse.Button.Right || action != Mouse.Action.Click) return@onMouseClick - minimized = !minimized + isMinimized = !isMinimized } onShow { @@ -132,7 +163,7 @@ open class Window( resizeXHovered = false resizeYHovered = false heightAnimation = when { - minimized -> 0.0 + isMinimized -> 0.0 minimizing == Minimizing.Relative -> targetHeight else -> 1.0 } @@ -168,7 +199,7 @@ open class Window( resizeXHovered = false resizeYHovered = false - if (!resizable || minimized) return@onMouseMove + if (!resizable || isMinimized) return@onMouseMove // Hover state update if (selectedChild != titleBar && content.selectedChild == null && isHovered) { diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt index 52b94f6c7..4f4899a44 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt @@ -26,7 +26,7 @@ import kotlin.math.abs class WindowContent( owner: Window, - private val scrollable: Boolean + private val scrollableList: Boolean ) : Layout(owner) { private val animation = animationTicker(false) @@ -34,7 +34,7 @@ class WindowContent( private var scrollOffset = 0.0 private var rubberbandDelta = 0.0 - var renderScrollOffset by animation.exp({ scrollOffset + rubberbandDelta }, 0.7) + var renderScrollOffset by animation.exp(0.7) { scrollOffset + rubberbandDelta } private var scrolling = false private var contentHeight = { @@ -85,7 +85,7 @@ class WindowContent( rubberbandDelta = 0.0 renderScrollOffset = 0.0 - if (scrollable) reorder() + if (scrollableList) reorder() } onTick { @@ -105,12 +105,14 @@ class WindowContent( if (abs(rubberbandDelta) < 0.05) rubberbandDelta = 0.0 animation.tick() + } - if (scrollable) reorder() + onUpdate { + if (scrollableList) reorder() } onMouseScroll { delta -> - if (!scrollable) return@onMouseScroll + if (!scrollableList) return@onMouseScroll dwheel += delta * 10.0 } } @@ -121,11 +123,11 @@ class WindowContent( /** * Creates an empty [WindowContent] component * - * @param scrollable Whether to let user scroll this layout + * @param scrollableList Whether to let user scroll this layout * This will also make your elements be vertically ordered */ @UIBuilder - fun Window.windowContent(scrollable: Boolean) = - WindowContent(this, scrollable).apply(children::add) + fun Window.windowContent(scrollableList: Boolean) = + WindowContent(this, scrollableList).apply(children::add) } } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt index e7f0fe369..01dd208b6 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt @@ -22,14 +22,14 @@ import com.lambda.module.Module import com.lambda.module.modules.client.ClickGui import com.lambda.gui.GuiManager.layoutOf import com.lambda.gui.component.HAlign -import com.lambda.gui.component.core.FilledRect -import com.lambda.gui.component.core.FilledRect.Companion.rect +import com.lambda.gui.component.core.FilledRect.Companion.rectBehind import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout import com.lambda.gui.component.window.Window import com.lambda.util.Mouse import com.lambda.util.math.* -import java.awt.Color +import com.lambda.util.math.MathUtils.toInt +import kotlin.math.pow class ModuleLayout( owner: Layout, @@ -47,13 +47,20 @@ class ModuleLayout( private val cursorController = cursorController() private var enableAnimation by animation.exp(0.0, 1.0, 0.6, module::isEnabled) - private var openAnimation by animation.exp(1.0, 0.0, 0.6, ::minimized) + private var openAnimation by animation.exp(1.0, 0.0, 0.6, ::isMinimized) + + private val longHovered get() = isHovered || System.currentTimeMillis() - lastHover < 80 + private var hoverAnimation by animation.exp(0.0, 1.0, { if (longHovered) 0.7 else 0.2 }, this::longHovered) + private val shrink get() = hoverAnimation.pow(3) * lerp(openAnimation, 1.0, 0.5) + + // ToDo: replace with timer + private var lastHover = 0L // Could be true only if owner is ModuleWindow var isLast = false init { - minimized = true + isMinimized = true height = 100.0 openAnimation = 0.0 @@ -61,7 +68,6 @@ class ModuleLayout( overrideWidth { owner.renderWidth - ClickGui.padding * 2 } titleBar.use { - textField.textHAlignment = HAlign.LEFT overrideHeight(ClickGui::moduleHeight) onMouseClick { button, action -> @@ -69,6 +75,80 @@ class ModuleLayout( module.toggle() } } + + textField.onUpdate { + textHAlignment = HAlign.LEFT + offsetX = ClickGui.fontOffset + lerp( + openAnimation, + hoverAnimation * 2, + 1.0 + hoverAnimation + ) + } + } + + rectBehind(titleBar) { + onUpdate { + rectangle = this@ModuleLayout.rect.shrink(shrink) + shade = ClickGui.backgroundShade + + val openRev = 1.0 - openAnimation // 1.0 <-> 0.0 + val openRevSigned = openRev * 2 - 1 // 1.0 <-> -1.0 + val enableRev = 1.0 - enableAnimation // 1.0 <-> 0.0 + + var progress = enableAnimation + + // hover: +0.1 to alpha if minimized, -0.1 to alpha if maximized + progress += hoverAnimation * ClickGui.moduleHoverAccent * openRevSigned + + // +0.4 to alpha if opened and disabled + progress += openAnimation * ClickGui.moduleOpenAccent * enableRev + + // interpolate and set the color + setColor(lerp(progress, ClickGui.moduleDisabledColor, ClickGui.moduleEnabledColor)) + } + + onUpdate { + setRadius(hoverAnimation) + + if (isLast && ClickGui.autoResize) { + leftBottomRadius = ClickGui.roundRadius - (ClickGui.padding + shrink) + rightBottomRadius = leftBottomRadius + } + } + } + + onShow { + enableAnimation = 0.0 + hoverAnimation = 0.0 + isMinimized = true + } + + onTick { + val cursor = if (titleBar.isHovered) Mouse.Cursor.Pointer else Mouse.Cursor.Arrow + cursorController.setCursor(cursor) + } + + onUpdate { + if (isHovered) lastHover = System.currentTimeMillis() + } + + onWindowExpand { + if (ClickGui.multipleSettingWindows) return@onWindowExpand + + val base = owner // window content + .owner // window + ?.owner // environment with windows + ?: return@onWindowExpand + + base.children.filterIsInstance().forEach { window -> + window.content.children.filterIsInstance().forEach { module -> + if (module != this) module.isMinimized = true + } + } + } + + module.settings.forEach { setting -> + content.layoutOf(setting) } content.overrideContentHeight { @@ -79,8 +159,7 @@ class ModuleLayout( (it.renderHeight + ClickGui.listStep) * it.visibilityAnimation } - ClickGui.listStep - val padding = ClickGui.padding * 2 - components + if (settings.isNotEmpty()) padding else 0.0 + components + ClickGui.padding * 2 * settings.isNotEmpty().toInt() } content.reorderChildren { @@ -102,59 +181,11 @@ class ModuleLayout( } } - rect { // Separator - onUpdate { - val vec = Vec2d( - lerp(openAnimation, titleBar.renderWidth * 0.5, ClickGui.fontOffset * 0.5), - -0.25 - ) - - rectangle = Rect( - pos1 = titleBar.leftBottom + vec, - pos2 = titleBar.rightBottom - vec - ) - - setColor(lerp(enableAnimation, Color.WHITE, Color.BLACK).setAlpha(0.2 * openAnimation)) - shade = ClickGui.outlineShade - } - } - - titleBarBackground.onUpdate { - setColor(lerp(enableAnimation, ClickGui.moduleDisabledColor, ClickGui.moduleEnabledColor)) - correctRadius() - } - - contentBackground.onUpdate { - setColor(lerp(enableAnimation, ClickGui.moduleDisabledColor, ClickGui.moduleEnabledColor)) - correctRadius() - } - - children.remove(outlineRect) - - onShow { - enableAnimation = 0.0 - } - - onTick { - val cursor = if (titleBar.isHovered) Mouse.Cursor.Pointer else Mouse.Cursor.Arrow - cursorController.setCursor(cursor) - } - - module.settings.forEach { setting -> - content.layoutOf(setting) - } - } - - private fun FilledRect.correctRadius() { - if (!isLast || !ClickGui.autoResize) { - setRadius(0.0) - return - } - - leftTopRadius = 0.0 - rightTopRadius = 0.0 - leftBottomRadius -= ClickGui.padding - rightBottomRadius -= ClickGui.padding + listOf( + titleBarBackground, + contentBackground, + outlineRect + ).forEach(Layout::destroy) } companion object { diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt index b63c0ade9..a0ed8ca79 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt @@ -24,6 +24,7 @@ import com.lambda.gui.component.HAlign import com.lambda.gui.component.layout.Layout import com.lambda.gui.component.window.Window import com.lambda.util.math.Vec2d +import com.lambda.util.math.lerp import com.lambda.util.math.setAlpha import com.lambda.util.math.transform import java.awt.Color @@ -53,8 +54,11 @@ abstract class SettingLayout > ( var settingValue by setting val visible get() = setting.visibility() + override val renderChildren: Boolean + get() = visibilityAnimation > 0 + init { - minimized = true + isMinimized = true overrideWidth(owner::renderWidth) titleBar.overrideHeight(ClickGui::settingsHeight) @@ -68,7 +72,7 @@ abstract class SettingLayout > ( textHAlignment = HAlign.LEFT onUpdate { - scale = ClickGui.fontScale * 0.92 + scale = ClickGui.fontScale * 0.92 * lerp(visibilityAnimation, 0.6, 1.0) color = Color.WHITE.setAlpha(visibilityAnimation) } } @@ -79,6 +83,6 @@ abstract class SettingLayout > ( outlineRect ).forEach(Layout::destroy) - if (!expandable) children.remove(content) + if (!expandable) content.destroy() } } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt index aa048667c..6b126b889 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt @@ -39,7 +39,7 @@ class BooleanButton( init { val checkBox = rect { // Checkbox - val shrink = 2.0 + val shrink = 3.0 setRadius(100.0) onUpdate { diff --git a/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt b/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt index 5b9d3e0b6..600a21271 100644 --- a/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt +++ b/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt @@ -39,11 +39,11 @@ object TickShiftCharge : HudModule( private val isActive get() = TickShift.isEnabled && TickShift.isActive && TickShift.boost private val activeAnimation by animation.exp(0.0, 1.0, 0.6, ::isActive) - private val progress - get() = if (!TickShift.isActive) 0.0 - else (TickShift.balance / TickShift.maxBalance.toDouble()).coerceIn(0.0..1.0) + private val renderProgress by animation.exp(0.8) { + if (!TickShift.isActive) return@exp 0.0 - private val renderProgress by animation.exp(::progress, 0.8) + (TickShift.balance / TickShift.maxBalance.toDouble()).coerceIn(0.0..1.0) + } override val width = 70.0 override val height = 14.0 diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt index 18bdaf966..961ebbdba 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt @@ -36,26 +36,30 @@ object ClickGui : Module( val settingsHeight by setting("Settings Height", 14.0, 10.0..25.0, 0.1) val padding by setting("Padding", 1.0, 1.0..6.0, 0.1) val listStep by setting("List Step", 1.0, 0.0..6.0, 0.1) - val autoResize by setting("Auto Resize", true) + val autoResize by setting("Auto Resize", false) - val roundRadius by setting("Round Radius", 2.0, 0.0..10.0, 0.1) + val roundRadius by setting("Round Radius", 3.0, 0.0..10.0, 0.1) val backgroundTint by setting("Background Tint", Color.BLACK.setAlpha(0.4)) - val titleBackgroundColor by setting("Title Background Color", Color(40, 40, 40)) + val titleBackgroundColor by setting("Title Background Color", Color(60, 60, 60)) val backgroundColor by setting("Background Color", titleBackgroundColor) val backgroundShade by setting("Background Shade", true) - val outline by setting("Outline", false) - val outlineWidth by setting("Outline Width", 10.0, 1.0..10.0, 0.1) { outline } + val outline by setting("Outline", true) + val outlineWidth by setting("Outline Width", 6.0, 1.0..10.0, 0.1) { outline } val outlineColor by setting("Outline Color", Color.WHITE.setAlpha(0.6)) { outline } val outlineShade by setting("Outline Shade", true) { outline } val fontScale by setting("Font Scale", 1.0, 0.5..2.0, 0.1) - val fontOffset by setting("Font Offset", 2.0, 0.0..5.0, 0.1) + val fontOffset by setting("Font Offset", 4.0, 0.0..5.0, 0.1) val dockingGridSize by setting("Docking Grid Size", 1.0, 0.1..10.0, 0.1) - val moduleEnabledColor by setting("Module Enabled Color", Color.WHITE.setAlpha(0.25)) - val moduleDisabledColor by setting("Module Disabled Color", Color.WHITE.setAlpha(0.05)) + val moduleEnabledColor by setting("Module Enabled Color", Color.WHITE.setAlpha(0.5)) + val moduleDisabledColor by setting("Module Disabled Color", Color.WHITE.setAlpha(0.0)) + val moduleHoverAccent by setting("Module Hover Accent", 0.15, 0.0..0.3, 0.01) + val moduleOpenAccent by setting("Module Open Accent", 0.3, 0.0..0.5, 0.01) + + val multipleSettingWindows by setting("Multiple Setting Windows", false) val SCREEN get() = gui("Click Gui") { rect { diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt index 0f000ae86..d242829a7 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt @@ -72,7 +72,7 @@ object GuiSettings : Module( tick() } - exp({ targetScale }, 0.5).apply { + exp(0.5) { targetScale }.apply { listenUnsafe(alwaysListen = true) { setValue(targetScale) } From a30839de50484296c7fb29a0b64bd31f5952afed Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 21 Feb 2025 21:41:54 -0500 Subject: [PATCH 096/114] Various opengl related comments --- .../com/lambda/graphics/buffer/Buffer.kt | 18 ++++++++++++------ .../kotlin/com/lambda/graphics/gl/Matrices.kt | 12 +++++------- .../graphics/renderer/gui/font/FontRenderer.kt | 17 ++++++++++++++--- .../renderer/gui/font/core/LambdaEmoji.kt | 8 +++++--- .../com/lambda/graphics/texture/Texture.kt | 13 ++++++++----- .../lambda/graphics/texture/TextureOwner.kt | 2 +- 6 files changed, 45 insertions(+), 25 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt index 0025bfdff..7af4ae588 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt @@ -179,10 +179,11 @@ abstract class Buffer( } /** - * Grows the backing buffers + * Allocates memory for each backing buffer using the specified size * This function handles the buffer binding * * @param size The size of the new buffer + * @return An [IllegalArgumentException] if validation fails; null if the allocation succeeds */ open fun allocate(size: Long): Throwable? { if (!bufferValid(target, access)) @@ -203,9 +204,11 @@ abstract class Buffer( } /** - * Create a new buffer storage + * Allocates new storage for the OpenGL buffer using the provided data * This function cannot be called twice for the same buffer * This function handles the buffer binding + * + * @return [IllegalArgumentException] for an invalid target or usage; null if storage allocation is successful */ open fun storage(data: ByteBuffer): Throwable? { if (!bufferValid(target, access)) @@ -226,11 +229,12 @@ abstract class Buffer( } /** - * Create a new buffer storage + * Allocates storage for the buffer object * This function cannot be called twice for the same buffer * This function handles the buffer binding * * @param size The size of the storage buffer + * @return [IllegalArgumentException] if the target or usage is invalid; null if storage allocation succeeds */ open fun storage(size: Long): Throwable? { if (!bufferValid(target, access)) @@ -251,12 +255,12 @@ abstract class Buffer( } /** - * Maps all or part of a buffer object's data store into the client's address space + * Maps a specified region of the buffer's data store into client memory, processes it using the provided lambda, and then unmaps the buffer * * @param size Specifies the length of the range to be mapped. * @param offset Specifies the starting offset within the buffer of the range to be mapped. * @param block Lambda scope with the mapped buffer passed in - * @return Error encountered during the mapping process + * @return [IllegalArgumentException] if there were errors during the validation, mapping or unmapping, null otherwise */ open fun map( size: Long, @@ -311,7 +315,9 @@ abstract class Buffer( } /** - * Sets the given data into the client mapped memory and executes the provided processing function to manage data transfer. + * Uploads the specified data to the buffer starting at the given offset + * + * This abstract function should be implemented to perform the actual data transfer into the buffer * * @param data Data to set in memory * @param offset The starting offset within the buffer of the range to be mapped diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt b/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt index 71189e7e5..b517c4127 100644 --- a/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt +++ b/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt @@ -78,7 +78,7 @@ object Matrices { } /** - * Translates the current matrix by the given x, y, and z values. + * Applies a translation to the top matrix on the transformation stack * * @param x The translation amount along the X axis. * @param y The translation amount along the Y axis. @@ -89,7 +89,7 @@ object Matrices { } /** - * Translates the current matrix by the given x, y, and z values. + * Applies a translation to the top matrix on the transformation stack * * @param x The translation amount along the X axis. * @param y The translation amount along the Y axis. @@ -152,12 +152,10 @@ object Matrices { } /** - * Temporarily sets a vertex offset vector for the duration of a block. + * Applies a temporary vertex offset to mitigate precision issues in matrix operations on large coordinates * - * Use this to avoid precision loss when using matrices while being on huge coordinates. - * - * @param offset The transformation offset to apply to vertices. - * @param block The block of code to execute with the transformation applied. + * @param offset the offset to apply to vertices for reducing precision loss + * @param block the block of code within which the vertex offset is active */ fun withVertexOffset(offset: Vec3d, block: () -> Unit) { vertexOffset = offset diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index a3774186c..7088b7af8 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -46,7 +46,7 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ private val gap get() = RenderSettings.gap * 0.5f - 0.8f /** - * Builds the vertex array for rendering the provided text string at a specified position. + * Renders a text string at a specified position with configurable color, scale, shadow, and emoji parsing * * @param text The text to render. * @param position The position to render the text. @@ -75,6 +75,14 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ } } + /** + * Renders a single glyph at the specified position with the given scale and color + * + * @param glyph The glyph information + * @param position The rendering position where the glyph will be drawn + * @param color The color of the glyph + * @param scale The scale factor of the glyph + */ fun drawGlyph( glyph: GlyphInfo, position: Vec2d, @@ -150,7 +158,10 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ } /** - * Calculates the height of the text based on the specified scale. + * Computes the effective height of the rendered text + * + * The height is derived from the current font's base height, adjusted by a scaling factor + * that ensures consistent visual proportions * * @param scale The scale factor for the height calculation. * @return The height of the text at the specified scale. @@ -158,7 +169,7 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ fun getHeight(scale: Double = 1.0) = chars.height * getScaleFactor(scale) * 0.7 /** - * Iterates over each character and emoji in the text and applies a block operation. + * Processes a text string by iterating over its characters and emojis, computing rendering positions, and invoking a block for each glyph * * @param text The text to iterate over. * @param color The color of the text. diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaEmoji.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaEmoji.kt index 0175f2156..95bd27e25 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaEmoji.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaEmoji.kt @@ -25,11 +25,13 @@ enum class LambdaEmoji(val url: String) { private val emojiRegex = Regex(":[a-zA-Z0-9_]+:") /** - * Parses the emojis in the given text. + * Extracts emoji names from the provided text * - * @param text The text to parse. + * The function scans the input text for patterns matching emojis in the `:name:` format and + * returns a mutable list of the emoji names * - * @return A list of parsed strings that does not contain the colons + * @param text The text to parse. + * @return A list of extract emoji names */ fun parse(text: String): MutableList = emojiRegex.findAll(text).map { it.value }.toMutableList() diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt index 5ac12882a..fe56a853e 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt @@ -75,14 +75,14 @@ open class Texture { var height = -1; protected set /** - * Binds the texture to a specific slot in the graphics pipeline. + * Binds this texture to the specified slot in the graphics pipeline */ open fun bind(slot: Int = 0) { bindTexture(id, slot) } /** - * Unbinds the currently bound texture + * Unbinds any texture from the specified slot */ open fun unbind(slot: Int = 0) { bindTexture(0, slot) @@ -90,7 +90,8 @@ open class Texture { /** * Uploads an image to the texture and generates mipmaps for the texture if applicable - * This function does not bind the texture + * + * Note that the texture must be bound before calling this function * * @param image The image to upload to the texture * @param offset The mipmap level to upload the image to @@ -112,7 +113,8 @@ open class Texture { /** * Uploads an image to the texture and generates mipmaps for the texture if applicable - * This function does not bind the texture + * + * Note that the texture must be bound before calling this function * * @param buffer The image buffer to upload to the texture * @param width The width of the texture @@ -136,7 +138,8 @@ open class Texture { /** * Updates the data of a texture - * This function does not bind the texture + * + * Note that the texture must be bound before calling this function * * @param image The image to upload to the texture * @param offset The mipmap level to upload the image to diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt index 5ace20405..98f51465b 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt @@ -34,7 +34,7 @@ object TextureOwner { get() = textureMap.getValue(this@texture)[0] /** - * Retrieves a specific texture owned by the object by its index + * Retrieves the texture associated with the receiver object at the specified index * * @param index The index of the texture to retrieve * @return The texture [T] at the given index From 150f419130fdcc4dfd0e26e91f4b82dc5dc02be9 Mon Sep 17 00:00:00 2001 From: "blade.kt" Date: Sat, 22 Feb 2025 16:39:05 +0300 Subject: [PATCH 097/114] opengl spam fix --- .../kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt b/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt index be66c63a7..dc87664b9 100644 --- a/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt +++ b/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt @@ -44,8 +44,8 @@ object ScissorAdapter { val pos1 = rect.leftTop * RenderMain.scaleFactor val pos2 = rect.rightBottom * RenderMain.scaleFactor - val width = pos2.x - pos1.x - val height = pos2.y - pos1.y + val width = (pos2.x - pos1.x).coerceAtLeast(0.0) + val height = (pos2.y - pos1.y).coerceAtLeast(0.0) val y = mc.window.framebufferHeight - pos1.y - height From 8b555a130d83706926dbd3a81f60040917426eb0 Mon Sep 17 00:00:00 2001 From: "blade.kt" Date: Fri, 28 Feb 2025 16:02:39 +0300 Subject: [PATCH 098/114] Animatio & Refactio --- .../lambda/gui/component/core/FilledRect.kt | 17 ++- .../lambda/gui/component/core/OutlineRect.kt | 5 +- .../lambda/gui/component/core/TextField.kt | 5 +- .../lambda/gui/component/core/UIBuilder.kt | 2 +- .../com/lambda/gui/component/layout/Layout.kt | 62 +++++++--- .../component/window/AnimatedWindowChild.kt | 98 +++++++++++++++ .../lambda/gui/component/window/TitleBar.kt | 16 ++- .../com/lambda/gui/component/window/Window.kt | 37 ++++-- .../gui/component/window/WindowContent.kt | 101 ++++++++-------- .../lambda/gui/impl/clickgui/ModuleLayout.kt | 114 ++++++++---------- .../lambda/gui/impl/clickgui/ModuleWindow.kt | 19 ++- .../lambda/gui/impl/clickgui/SettingLayout.kt | 32 ++--- .../impl/clickgui/settings/BooleanButton.kt | 24 ++-- .../lambda/module/modules/client/ClickGui.kt | 2 +- .../lambda/shaders/fragment/font/font.frag | 25 ++-- .../fragment/signed_distance_field.frag | 13 +- 16 files changed, 362 insertions(+), 210 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/gui/component/window/AnimatedWindowChild.kt diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt b/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt index d8a52a405..874033bec 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt @@ -18,7 +18,6 @@ package com.lambda.gui.component.core import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer.filledRect -import com.lambda.gui.component.core.FilledRect.Companion.rectBehind import com.lambda.gui.component.layout.Layout import com.lambda.util.math.Rect import java.awt.Color @@ -72,6 +71,18 @@ class FilledRect( leftBottomRadius = radius } + fun setRadius( + leftTopRadius: Double, + rightTopRadius: Double, + rightBottomRadius: Double, + leftBottomRadius: Double, + ) { + this.leftTopRadius = leftTopRadius + this.rightTopRadius = rightTopRadius + this.rightBottomRadius = rightBottomRadius + this.leftBottomRadius = leftBottomRadius + } + fun setColor(color: Color) { leftTopColor = color rightTopColor = color @@ -109,7 +120,7 @@ class FilledRect( fun Layout.rectBehind( layout: Layout, block: FilledRect.() -> Unit = {} - ) = FilledRect(this).relativeLayout(this, layout, false).apply(block) + ) = FilledRect(this).insertLayout(this, layout, false).apply(block) /** * Adds a [FilledRect] over given [layout] @@ -118,6 +129,6 @@ class FilledRect( fun Layout.rectOver( layout: Layout, block: FilledRect.() -> Unit = {} - ) = FilledRect(this).relativeLayout(this, layout, true).apply(block) + ) = FilledRect(this).insertLayout(this, layout, true).apply(block) } } diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt b/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt index c3f9a8044..c64a9cfe3 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt @@ -18,7 +18,6 @@ package com.lambda.gui.component.core import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer.outlineRect -import com.lambda.gui.component.core.FilledRect.Companion.rectBehind import com.lambda.gui.component.layout.Layout import java.awt.Color @@ -77,7 +76,7 @@ class OutlineRect( fun Layout.outlineBehind( layout: Layout, block: OutlineRect.() -> Unit = {} - ) = OutlineRect(this).relativeLayout(this, layout, false).apply(block) + ) = OutlineRect(this).insertLayout(this, layout, false).apply(block) /** * Creates an [OutlineRect] component - layout-based rect representation @@ -86,6 +85,6 @@ class OutlineRect( fun Layout.outlineOver( layout: Layout, block: OutlineRect.() -> Unit = {} - ) = OutlineRect(this).relativeLayout(this, layout, true).apply(block) + ) = OutlineRect(this).insertLayout(this, layout, true).apply(block) } } diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt index 6fcf12c16..6117d6134 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt @@ -21,7 +21,6 @@ import com.lambda.graphics.renderer.gui.font.FontRenderer import com.lambda.graphics.renderer.gui.font.FontRenderer.drawString import com.lambda.gui.component.HAlign import com.lambda.gui.component.VAlign -import com.lambda.gui.component.core.OutlineRect.Companion.outlineBehind import com.lambda.gui.component.layout.Layout import com.lambda.util.math.Vec2d import com.lambda.util.math.lerp @@ -71,7 +70,7 @@ class TextField( fun Layout.textFieldBehind( layout: Layout, block: TextField.() -> Unit = {} - ) = TextField(this).relativeLayout(this, layout, false).apply(block) + ) = TextField(this).insertLayout(this, layout, false).apply(block) /** * Adds a [TextField] over given [layout] @@ -80,6 +79,6 @@ class TextField( fun Layout.textFieldOver( layout: Layout, block: TextField.() -> Unit = {} - ) = TextField(this).relativeLayout(this, layout, true).apply(block) + ) = TextField(this).insertLayout(this, layout, true).apply(block) } } diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt b/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt index 0e297575d..3b15f0705 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt @@ -29,7 +29,7 @@ annotation class LayoutBuilder @DslMarker annotation class UIRenderPr0p3rty -fun T.relativeLayout( +fun T.insertLayout( owner: Layout, base: Layout, next: Boolean diff --git a/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt index ed1e6b936..81463a1e6 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt @@ -21,14 +21,16 @@ import com.lambda.graphics.RenderMain import com.lambda.graphics.animation.AnimationTicker import com.lambda.event.events.GuiEvent import com.lambda.graphics.pipeline.ScissorAdapter +import com.lambda.graphics.renderer.gui.font.FontRenderer +import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer import com.lambda.gui.component.HAlign import com.lambda.gui.component.VAlign -import com.lambda.gui.component.core.LayoutBuilder -import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.core.* import com.lambda.util.KeyCode import com.lambda.util.Mouse import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d +import java.awt.Color /** * Represents a component for creating complex ui structures. @@ -75,10 +77,13 @@ open class Layout( set(value) { width = value.x; height = value.y } val renderSize get() = Vec2d(renderWidth, renderHeight) + val renderWidth get() = widthTransform() val renderHeight get() = heightTransform() + private var widthTransform = { width } private var heightTransform = { height } + var width = 0.0 var height = 0.0 @@ -123,7 +128,8 @@ open class Layout( // Structure val children = mutableListOf() var selectedChild: Layout? = null - protected open val renderChildren: Boolean get() = renderWidth > 0 && renderHeight > 0 + protected open val renderSelf: Boolean get() = renderWidth > 1 && renderHeight > 1 + protected open val scissorRect get() = rect // Inputs protected var mousePosition = Vec2d.ZERO @@ -148,7 +154,7 @@ open class Layout( */ @LayoutBuilder fun T.use(action: T.() -> Unit) { - action(this).apply { } + action(this) } /** @@ -231,6 +237,18 @@ open class Layout( mouseClickActions += { button, mouseAction -> action(button, mouseAction) } } + /** + * Sets the action to be performed when mouse button gets clicked. + * + * @param action The action to be performed. + */ + @LayoutBuilder + fun T.onMouseClick(button: Mouse.Button, action: Mouse.Action, block: T.() -> Unit) { + mouseClickActions += { butt, act -> + if (butt == button && act == action) block() + } + } + /** * Sets the action to be performed when mouse moves. * @@ -272,8 +290,8 @@ open class Layout( */ @LayoutBuilder fun overridePosition(x: () -> Double, y: () -> Double) { - positionXTransform = x - positionYTransform = y + overrideX(x) + overrideY(y) } /** @@ -297,8 +315,8 @@ open class Layout( */ @LayoutBuilder fun overrideSize(width: () -> Double, height: () -> Double) { - widthTransform = width - heightTransform = height + overrideWidth(width) + overrideHeight(height) } /** @@ -374,17 +392,17 @@ open class Layout( is GuiEvent.Update -> { updateActions.forEach { it(this) } } - is GuiEvent.Render -> {} + is GuiEvent.Render -> { + if (!renderSelf) return + } is GuiEvent.MouseMove -> { mousePosition = e.mouse mouseMoveActions.forEach { it(this, e.mouse) } } is GuiEvent.MouseScroll -> { + if (!isHovered) return mousePosition = e.mouse - - if (isHovered) { - mouseScrollActions.forEach { it(this, e.delta) } - } + mouseScrollActions.forEach { it(this, e.delta) } } is GuiEvent.MouseClick -> { mousePosition = e.mouse @@ -411,11 +429,25 @@ open class Layout( if (e is GuiEvent.Render) { val block = { renderActions.forEach { it(this) } - if (renderChildren) children.forEach { it.onEvent(e) } + if (renderSelf) children.forEach { it.onEvent(e) } + + /*if (this !is FilledRect && this !is OutlineRect && this !is TextField) { + OutlineRectRenderer.outlineRect( + rect, + glowRadius = 0.5 + ) + + FontRenderer.drawString( + javaClass.simpleName, + leftTop + FontRenderer.getHeight(0.5) * 0.5, + Color.WHITE, + 0.5 + ) + }*/ } if (!properties.scissor) block() - else ScissorAdapter.scissor(rect, block) + else ScissorAdapter.scissor(scissorRect, block) } } diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/AnimatedWindowChild.kt b/common/src/main/kotlin/com/lambda/gui/component/window/AnimatedWindowChild.kt new file mode 100644 index 000000000..78902c628 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/component/window/AnimatedWindowChild.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.gui.component.window + +import com.lambda.graphics.animation.Animation.Companion.exp +import com.lambda.gui.component.HAlign +import com.lambda.gui.component.layout.Layout +import com.lambda.module.modules.client.ClickGui +import com.lambda.util.math.MathUtils.toInt +import com.lambda.util.math.Vec2d +import com.lambda.util.math.lerp +import com.lambda.util.math.setAlpha +import com.lambda.util.math.transform +import java.awt.Color + +abstract class AnimatedWindowChild( + owner: Layout, + initialTitle: String = "Untitled", + initialPosition: Vec2d = Vec2d.ZERO, + initialSize: Vec2d = Vec2d(110, 350), + draggable: Boolean = true, + scrollable: Boolean = true, + minimizing: Minimizing = Minimizing.Relative, + resizable: Boolean = true, + autoResize: AutoResize = AutoResize.Disabled +) : Window(owner, initialTitle, initialPosition, initialSize, draggable, scrollable, minimizing, resizable, autoResize) { + private val window get() = owner?.owner as? Window + private val animatedWindow get() = owner?.owner as? AnimatedWindowChild + + // Show animation for when the component is shown or hidden + open val isShown get() = true + private val isShownInternal get() = window?.isExpand != false && isShown + var showAnimation by animation.exp(0.0, 1.0, { + var speed = 0.7 + + if (lastIndex != 0) + speed = transform(index.toDouble(), 0.0, lastIndex.toDouble(), 0.4, speed) + + speed + isShownInternal.toInt() * 0.1 + }) { isShownInternal }.apply { + if (window == null) this.setValue(1.0) + }; protected set + + // Animation without index-based slowdown + var staticShowAnimation by animation.exp(0.0, 1.0, 0.7, ::isShown) + + // Index for smooth "ordered" animation + var index = 0 + var lastIndex = 0 + protected val isLast get() = index == lastIndex + + override val renderSelf: Boolean + get() = showAnimation > 0.0 && super.renderSelf + + init { + titleBar.textField.onUpdate { + textHAlignment = HAlign.LEFT + offsetX = lerp(showAnimation, -5.0, ClickGui.fontOffset) + color = Color.WHITE.setAlpha(showAnimation) + } + } + + init { + onShow { + showAnimation = 0.0 + staticShowAnimation = 0.0 + } + + onUpdate { + isHovered = isHovered && isShown + } + + titleBar.textField.use { + textHAlignment = HAlign.LEFT + + onUpdate { + offsetX = lerp(showAnimation, -5.0, ClickGui.fontOffset) + scale = lerp(showAnimation, 0.7, 1.0) + color = Color.WHITE.setAlpha(showAnimation) + } + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt index 83f11cc4b..0ada0f657 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt @@ -26,6 +26,7 @@ import com.lambda.gui.component.layout.Layout import com.lambda.util.Mouse import com.lambda.util.math.Vec2d import com.lambda.util.math.lerp +import com.lambda.util.math.transform /** * Represents a titlebar component @@ -47,10 +48,9 @@ class TitleBar( dragOffset = null } - onMouseClick { button: Mouse.Button, action: Mouse.Action -> - dragOffset = if (drag && button == Mouse.Button.Left && action == Mouse.Action.Click) { - mousePosition - owner.position - } else null + onMouseClick { _, _ -> dragOffset = null } + onMouseClick(Mouse.Button.Left, Mouse.Action.Click) { + if (drag) dragOffset = mousePosition - owner.position } onMouseMove { mouse -> @@ -69,7 +69,13 @@ class TitleBar( leftTopRadius = radius rightTopRadius = radius - val bottomRadius = lerp(owner.content.renderHeight, radius, 0.0) + val bottomRadius = transform( + owner.renderHeight, + this.renderHeight, + this.renderHeight + 1, + radius, + 0.0 + ) leftBottomRadius = bottomRadius rightBottomRadius = bottomRadius diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt index a018f5545..6a0c884cc 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt @@ -48,7 +48,7 @@ open class Window( private val resizable: Boolean = true, val autoResize: AutoResize = AutoResize.Disabled ) : Layout(owner) { - private val animation = animationTicker() + protected val animation = animationTicker() private val cursorController = cursorController() val titleBar = titleBar(initialTitle, draggable) @@ -119,14 +119,18 @@ open class Window( actions.forEach { it(this) } } - private var heightAnimation by animation.exp( + var isExpand + get() = !isMinimized + set(value) { isMinimized = !value } + + var heightAnimation by animation.exp( min = { 0.0 }, max = { if (minimizing == Minimizing.Relative) targetHeight else 1.0 }, - speed = 0.8, + speed = 0.7, flag = { !isMinimized } ) - private val targetHeight get() = if (!autoResize.enabled) height - titleBar.renderHeight else content.getContentHeight() + val targetHeight get() = if (!autoResize.enabled) height - titleBar.renderHeight else content.renderHeight // Resizing private var resizeX: Double? = null @@ -137,6 +141,7 @@ open class Window( init { position = initialPosition size = initialSize + properties.clampPosition = owner is ScreenLayout overrideSize(animation.exp(0.8, ::width)::value) { titleBar.renderHeight + when (minimizing) { @@ -146,9 +151,6 @@ open class Window( } } - properties.clampPosition = owner is ScreenLayout - content.properties.scissor = true - titleBar.onMouseClick { button, action -> // Toggle minimizing state when right-clicking title bar if (minimizing == Minimizing.Disabled) return@onMouseClick @@ -157,16 +159,29 @@ open class Window( isMinimized = !isMinimized } + content.onUpdate { + val animatedChildren = content.children + .filterIsInstance() + .filter { it.isShown } + + animatedChildren.forEachIndexed { i, it -> + it.index = i + it.lastIndex = animatedChildren.lastIndex + } + } + onShow { resizeX = null resizeY = null resizeXHovered = false resizeYHovered = false - heightAnimation = when { + + heightAnimation = 0.0 + /*heightAnimation = when { isMinimized -> 0.0 minimizing == Minimizing.Relative -> targetHeight else -> 1.0 - } + }*/ } onTick { @@ -237,8 +252,8 @@ open class Window( /** * [Disabled] -> No ability to minimize the window - * [Relative] -> Animation follows the height of the component ( animation(0.0, height) ) - * [Absolute] -> Animation does not depend on the height ( animation(0.0, 1.0) * height ) + * [Relative] -> Animation follows the height of the component ( animation(0.0, height) ) (height change is animated) + * [Absolute] -> Animation does not depend on the height ( animation(0.0, 1.0) * height ) (height change instantly affects the height) */ enum class Minimizing { Disabled, diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt index 4f4899a44..c973d2cef 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt @@ -22,70 +22,69 @@ import com.lambda.gui.component.core.LayoutBuilder import com.lambda.module.modules.client.ClickGui import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout +import com.lambda.util.math.MathUtils.toInt +import com.lambda.util.math.Rect import kotlin.math.abs class WindowContent( owner: Window, - private val scrollableList: Boolean + scrollable: Boolean ) : Layout(owner) { + private val window = owner private val animation = animationTicker(false) private var dwheel = 0.0 private var scrollOffset = 0.0 private var rubberbandDelta = 0.0 - var renderScrollOffset by animation.exp(0.7) { scrollOffset + rubberbandDelta } - private var scrolling = false + private var renderScrollOffset by animation.exp(0.7) { scrollOffset + rubberbandDelta } - private var contentHeight = { - ClickGui.padding * 2 + - children.sumOf(Layout::renderHeight) + - ClickGui.listStep * (children.size - 1).coerceAtLeast(0) - } + override val scissorRect: Rect + get() = Rect(window.titleBar.leftBottom, window.rightBottom) - private var reorder = block@ { - children.forEachIndexed { i, child -> - val prev by lazy { children[i - 1] } - - child.overrideY { - if (i == 0) { - renderPositionY + renderScrollOffset + ClickGui.padding - } else { - prev.renderPositionY + prev.renderHeight + ClickGui.listStep - } - } - } - } + override val renderSelf: Boolean + get() = window.heightAnimation > 0.05 /** - * Overrides the summary height of the content + * Orders the children set vertically */ @LayoutBuilder - fun overrideContentHeight(block: () -> Double) { - contentHeight = block - } + fun listify() { + children.forEachIndexed { i, it -> + val prev = children.getOrNull(i - 1) ?: run { + it.overrideY { + this.renderPositionY + ClickGui.padding + } - /** - * Overrides the action performed on ordering update - */ - @LayoutBuilder - fun reorderChildren(block: () -> Unit) { - reorder = block + return@forEachIndexed + } + + it.overrideY { + prev.renderPositionY + layoutHeight(prev, true) + ClickGui.listStep + } + } } init { - overrideX { owner.titleBar.renderPositionX } - overrideY { owner.titleBar.let { it.renderPositionY + it.renderHeight } } - overrideWidth { owner.renderWidth } - overrideHeight { owner.renderHeight - owner.titleBar.renderHeight } + properties.scissor = true + + overrideX(owner.titleBar::renderPositionX) + overrideY { + owner.titleBar.let { it.renderPositionY + it.renderHeight } + renderScrollOffset * scrollable.toInt() + } + + overrideWidth(owner::renderWidth) + overrideHeight { + children.sumOf { layoutHeight(it, false) } + + ClickGui.listStep * (children.size - 1).coerceAtLeast(0) + + ClickGui.padding * 2 + } onShow { dwheel = 0.0 scrollOffset = 0.0 rubberbandDelta = 0.0 renderScrollOffset = 0.0 - - if (scrollableList) reorder() } onTick { @@ -93,12 +92,12 @@ class WindowContent( scrollOffset + dwheel } else 0.0 - scrolling = dwheel != 0.0 dwheel = 0.0 val prevOffset = scrollOffset - val maxScroll = renderHeight - getContentHeight() - ClickGui.padding - scrollOffset = scrollOffset.coerceAtLeast(maxScroll).coerceAtMost(0.0) + scrollOffset = scrollOffset.coerceAtLeast( + owner.targetHeight - renderHeight + ).coerceAtMost(0.0) rubberbandDelta += prevOffset - scrollOffset rubberbandDelta *= 0.5 @@ -107,27 +106,27 @@ class WindowContent( animation.tick() } - onUpdate { - if (scrollableList) reorder() - } - onMouseScroll { delta -> - if (!scrollableList) return@onMouseScroll dwheel += delta * 10.0 } } - fun getContentHeight() = contentHeight() + private fun layoutHeight(layout: Layout, animate: Boolean): Double { + var height = layout.renderHeight + val animated = layout as? AnimatedWindowChild ?: return height + + height *= if (!animate) animated.staticShowAnimation + else animated.showAnimation + + return height + } companion object { /** * Creates an empty [WindowContent] component - * - * @param scrollableList Whether to let user scroll this layout - * This will also make your elements be vertically ordered */ @UIBuilder - fun Window.windowContent(scrollableList: Boolean) = - WindowContent(this, scrollableList).apply(children::add) + fun Window.windowContent(scrollable: Boolean) = + WindowContent(this, scrollable).apply(children::add) } } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt index 01dd208b6..ae66d7ed3 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt @@ -21,14 +21,15 @@ import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.module.Module import com.lambda.module.modules.client.ClickGui import com.lambda.gui.GuiManager.layoutOf -import com.lambda.gui.component.HAlign +import com.lambda.gui.component.core.FilledRect +import com.lambda.gui.component.core.FilledRect.Companion.rect import com.lambda.gui.component.core.FilledRect.Companion.rectBehind import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout -import com.lambda.gui.component.window.Window +import com.lambda.gui.component.window.AnimatedWindowChild import com.lambda.util.Mouse import com.lambda.util.math.* -import com.lambda.util.math.MathUtils.toInt +import java.awt.Color import kotlin.math.pow class ModuleLayout( @@ -36,14 +37,13 @@ class ModuleLayout( module: Module, initialPosition: Vec2d = Vec2d.ZERO, initialSize: Vec2d = Vec2d(100, 18) -) : Window( +) : AnimatedWindowChild( owner, module.name, initialPosition, initialSize, - false, true, Minimizing.Relative, false, + false, false, Minimizing.Absolute, false, AutoResize.ForceEnabled ) { - private val animation = animationTicker() private val cursorController = cursorController() private var enableAnimation by animation.exp(0.0, 1.0, 0.6, module::isEnabled) @@ -56,37 +56,8 @@ class ModuleLayout( // ToDo: replace with timer private var lastHover = 0L - // Could be true only if owner is ModuleWindow - var isLast = false - init { - isMinimized = true - height = 100.0 - openAnimation = 0.0 - - overrideX { owner.renderPositionX + ClickGui.padding } - overrideWidth { owner.renderWidth - ClickGui.padding * 2 } - - titleBar.use { - overrideHeight(ClickGui::moduleHeight) - - onMouseClick { button, action -> - if (button == Mouse.Button.Left && action == Mouse.Action.Click) { - module.toggle() - } - } - - textField.onUpdate { - textHAlignment = HAlign.LEFT - offsetX = ClickGui.fontOffset + lerp( - openAnimation, - hoverAnimation * 2, - 1.0 + hoverAnimation - ) - } - } - - rectBehind(titleBar) { + rectBehind(titleBar) { // base rect with lowest y to avoid children overlying onUpdate { rectangle = this@ModuleLayout.rect.shrink(shrink) shade = ClickGui.backgroundShade @@ -115,6 +86,47 @@ class ModuleLayout( rightBottomRadius = leftBottomRadius } } + + rect { // hover fx + onUpdate { + val base = this@rect.owner as FilledRect + + rectangle = base.rectangle + shade = base.shade + + setRadius( + base.leftTopRadius, + base.rightTopRadius, + base.rightBottomRadius, + base.leftBottomRadius + ) + + val hoverColor = Color.WHITE.setAlpha( + ClickGui.moduleHoverAccent * hoverAnimation * (1.0 - openAnimation) + ) + + setColorH(hoverColor.setAlpha(0.0), hoverColor) + } + } + } + + isMinimized = true + height = 100.0 + openAnimation = 0.0 + + overrideX { owner.renderPositionX + ClickGui.padding } + overrideWidth { owner.renderWidth - ClickGui.padding * 2 } + + titleBar.use { + overrideHeight(ClickGui::moduleHeight) + + onMouseClick(Mouse.Button.Left,Mouse.Action.Click) { + module.toggle() + } + + textField.onUpdate { + offsetX += hoverAnimation * 2 + } } onShow { @@ -151,35 +163,7 @@ class ModuleLayout( content.layoutOf(setting) } - content.overrideContentHeight { - val settings = content.children - .filterIsInstance>() - - val components = settings.sumOf { - (it.renderHeight + ClickGui.listStep) * it.visibilityAnimation - } - ClickGui.listStep - - components + ClickGui.padding * 2 * settings.isNotEmpty().toInt() - } - - content.reorderChildren { - val settings = content.children - .filterIsInstance>() - - var y = 0.0 - - settings.forEach { - if (it.visible) { - it.heightOffset = y - } - - y += (it.renderHeight + ClickGui.listStep) * it.visibilityAnimation - - it.overrideY { - content.renderPositionY + content.renderScrollOffset + ClickGui.padding + it.heightOffset - } - } - } + content.listify() listOf( titleBarBackground, diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt index db2fd5e8c..59998b0f1 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt @@ -32,14 +32,23 @@ class ModuleWindow( initialPosition: Vec2d ) : Window(owner, tag.name, initialPosition, minimizing = Minimizing.Absolute, autoResize = AutoResize.ByConfig) { init { - ModuleRegistry.modules + val modules = ModuleRegistry.modules .filter { it.defaultTags.firstOrNull() == tag } .map { module -> content.moduleLayout(module) } - .let { moduleLayouts -> - moduleLayouts.forEachIndexed { i, it -> - it.isLast = moduleLayouts.lastIndex == i - } + + content.listify() + + onWindowExpand { + modules.forEach { + it.isMinimized = true + } + } + + onWindowMinimize { + modules.forEach { + it.isMinimized = true } + } } companion object { diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt index a0ed8ca79..05224f164 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt @@ -18,16 +18,11 @@ package com.lambda.gui.impl.clickgui import com.lambda.config.AbstractSetting -import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.module.modules.client.ClickGui import com.lambda.gui.component.HAlign import com.lambda.gui.component.layout.Layout -import com.lambda.gui.component.window.Window -import com.lambda.util.math.Vec2d -import com.lambda.util.math.lerp -import com.lambda.util.math.setAlpha -import com.lambda.util.math.transform -import java.awt.Color +import com.lambda.gui.component.window.AnimatedWindowChild +import com.lambda.util.math.* /** * A base class for setting layouts. @@ -36,26 +31,21 @@ abstract class SettingLayout > ( owner: Layout, val setting: T, expandable: Boolean = false -) : Window( +) : AnimatedWindowChild( owner, setting.name, Vec2d.ZERO, Vec2d.ZERO, false, false, - if (expandable) Minimizing.Relative else Minimizing.Disabled, + if (expandable) Minimizing.Absolute else Minimizing.Disabled, false, AutoResize.ForceEnabled ) { - protected val animation = animationTicker() protected val cursorController = cursorController() - var visibilityAnimation by animation.exp(0.0, 1.0, 0.8, ::visible) - var heightOffset = 0.0 - - var settingValue by setting + var settingDelegate by setting val visible get() = setting.visibility() - override val renderChildren: Boolean - get() = visibilityAnimation > 0 + override val isShown: Boolean get() = super.isShown && visible init { isMinimized = true @@ -63,8 +53,9 @@ abstract class SettingLayout > ( overrideWidth(owner::renderWidth) titleBar.overrideHeight(ClickGui::settingsHeight) - overrideX { - owner.renderPositionX + transform(visibilityAnimation, 0.0, 1.0, -10.0, 0.0) + if (!expandable) { + overrideHeight(titleBar::renderHeight) + content.destroy() } titleBar.textField.use { @@ -72,8 +63,7 @@ abstract class SettingLayout > ( textHAlignment = HAlign.LEFT onUpdate { - scale = ClickGui.fontScale * 0.92 * lerp(visibilityAnimation, 0.6, 1.0) - color = Color.WHITE.setAlpha(visibilityAnimation) + scale *= 0.92 } } @@ -82,7 +72,5 @@ abstract class SettingLayout > ( contentBackground, outlineRect ).forEach(Layout::destroy) - - if (!expandable) content.destroy() } } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt index 6b126b889..b610c63a3 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt @@ -35,7 +35,7 @@ class BooleanButton( owner: Layout, setting: BooleanSetting ) : SettingLayout(owner, setting) { - private var activeAnimation by animation.exp(0.0, 1.0, 0.6, ::settingValue) + private var activeAnimation by animation.exp(0.0, 1.0, 0.6, ::settingDelegate) init { val checkBox = rect { // Checkbox @@ -47,9 +47,10 @@ class BooleanButton( val h = this@BooleanButton.renderHeight rectangle = Rect(rb - Vec2d(h * 1.65, h), rb) - .shrink(shrink) + Vec2d.LEFT * (ClickGui.fontOffset - shrink) + .shrink(shrink + (1.0 - showAnimation) * h * 0.2) + + Vec2d.RIGHT * lerp(showAnimation, 5.0, -ClickGui.fontOffset + shrink) - setColor(Color.BLACK.setAlpha(0.25 * visibilityAnimation)) + setColor(Color.BLACK.setAlpha(0.25 * showAnimation)) shade = ClickGui.backgroundShade } @@ -59,10 +60,8 @@ class BooleanButton( ) } - onMouseClick { button, action -> - if (button == Mouse.Button.Left && action == Mouse.Action.Click) { - setting.value = !setting.value - } + onMouseClick(Mouse.Button.Left, Mouse.Action.Click) { + setting.value = !setting.value } } @@ -72,9 +71,16 @@ class BooleanButton( onUpdate { val knobStart = Rect.basedOn(checkBox.leftTop, Vec2d.ONE * checkBox.renderHeight) val knobEnd = Rect(checkBox.rightBottom - checkBox.renderHeight, checkBox.rightBottom) - rectangle = lerp(activeAnimation, knobStart, knobEnd).shrink(1.0) + + rectangle = lerp( + lerp(showAnimation, 1.0 - activeAnimation, activeAnimation), + knobStart, + knobEnd + ).shrink(1.0) + shade = ClickGui.backgroundShade - setColor(Color.WHITE.setAlpha(0.25 * visibilityAnimation)) + + setColor(Color.WHITE.setAlpha(0.25 * showAnimation)) } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt index 961ebbdba..99caff45b 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt @@ -33,7 +33,7 @@ object ClickGui : Module( ) { val titleBarHeight by setting("Title Bar Height", 18.0, 10.0..25.0, 0.1) val moduleHeight by setting("Module Height", 16.0, 10.0..25.0, 0.1) - val settingsHeight by setting("Settings Height", 14.0, 10.0..25.0, 0.1) + val settingsHeight by setting("Settings Height", 16.0, 10.0..25.0, 0.1) val padding by setting("Padding", 1.0, 1.0..6.0, 0.1) val listStep by setting("List Step", 1.0, 0.0..6.0, 0.1) val autoResize by setting("Auto Resize", false) diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag b/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag index eb7d163ed..54dffb8e1 100644 --- a/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag +++ b/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag @@ -10,19 +10,26 @@ in vec4 v_Color; out vec4 color; -void main() { - vec2 coord = v_TexCoord; +float sdf(float channel) { + return 1.0 - smoothstep(u_SDFMin, u_SDFMax, 1.0 - channel); +} + +vec4 sdf(vec4 texture) { + return vec4( + sdf(texture.r), + sdf(texture.g), + sdf(texture.b), + sdf(texture.a) + ); +} - bool isEmoji = coord.x < 0.0; - if (isEmoji) coord = -v_TexCoord; +void main() { + bool isEmoji = v_TexCoord.x < 0.0; if (isEmoji) { - color = texture(u_EmojiTexture, coord) * v_Color; + color = sdf(texture(u_EmojiTexture, -v_TexCoord)) * v_Color; return; } - float sdf = texture(u_FontTexture, coord).r; - float alpha = 1.0 - smoothstep(u_SDFMin, u_SDFMax, 1.0 - sdf); - - color = vec4(1, 1, 1, alpha) * v_Color; + color = vec4(1.0, 1.0, 1.0, sdf(texture(u_FontTexture, v_TexCoord).r)) * v_Color; } diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/signed_distance_field.frag b/common/src/main/resources/assets/lambda/shaders/fragment/signed_distance_field.frag index 33114a0aa..5d4f485f1 100644 --- a/common/src/main/resources/assets/lambda/shaders/fragment/signed_distance_field.frag +++ b/common/src/main/resources/assets/lambda/shaders/fragment/signed_distance_field.frag @@ -9,21 +9,20 @@ out vec4 color; #define SPHREAD 4 void main() { - float alpha = 0.0; - float blurWeight = 0.0; + vec4 colors = vec4(0.0); + vec4 blurWeight = vec4(0.0); for (int x = -SPHREAD; x <= SPHREAD; ++x) { for (int y = -SPHREAD; y <= SPHREAD; ++y) { vec2 offset = vec2(x, y) * u_TexelSize; - float color = texture(u_Texture, v_TexCoord + offset).r; - float weight = exp(-color * color); + vec4 color = texture(u_Texture, v_TexCoord + offset); + vec4 weight = exp(-color * color); - alpha += color * weight; + colors += color * weight; blurWeight += weight; } } - alpha /= blurWeight; - color = vec4(alpha, 1.0, 1.0, 1.0); + color = colors / blurWeight; } From 8d3fa99d077d05c5e90787833e98d6fe4ca064d9 Mon Sep 17 00:00:00 2001 From: "blade.kt" Date: Fri, 28 Feb 2025 21:55:10 +0300 Subject: [PATCH 099/114] Enum setting --- .../main/kotlin/com/lambda/gui/GuiManager.kt | 6 + .../com/lambda/gui/component/window/Window.kt | 6 +- .../gui/component/window/WindowContent.kt | 17 ++- .../lambda/gui/impl/clickgui/ModuleLayout.kt | 126 ++++++++++++------ .../lambda/gui/impl/clickgui/ModuleWindow.kt | 3 + .../lambda/gui/impl/clickgui/SettingLayout.kt | 3 +- .../impl/clickgui/settings/BooleanButton.kt | 4 +- .../impl/clickgui/settings/EnumSelector.kt | 85 ++++++++++++ .../lambda/module/modules/client/ClickGui.kt | 4 +- 9 files changed, 202 insertions(+), 52 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/EnumSelector.kt diff --git a/common/src/main/kotlin/com/lambda/gui/GuiManager.kt b/common/src/main/kotlin/com/lambda/gui/GuiManager.kt index fc5feea18..86f9ce568 100644 --- a/common/src/main/kotlin/com/lambda/gui/GuiManager.kt +++ b/common/src/main/kotlin/com/lambda/gui/GuiManager.kt @@ -18,10 +18,12 @@ package com.lambda.gui import com.lambda.config.settings.comparable.BooleanSetting +import com.lambda.config.settings.comparable.EnumSetting import com.lambda.core.Loadable import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout import com.lambda.gui.impl.clickgui.settings.BooleanButton.Companion.booleanSetting +import com.lambda.gui.impl.clickgui.settings.EnumSelector.Companion.enumSetting import kotlin.reflect.KClass object GuiManager : Loadable { @@ -36,6 +38,10 @@ object GuiManager : Loadable { owner.booleanSetting(ref) } + typeAdapter> { owner, ref -> + owner.enumSetting(ref) + } + return "Loaded ${typeMap.size} gui type adapters." } diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt index 6a0c884cc..400d1f311 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt @@ -53,8 +53,8 @@ open class Window( val titleBar = titleBar(initialTitle, draggable) - protected val titleBarBackground by titleBar::backgroundRect - protected val contentBackground = rect { // It's here because content cannot contain something by default + val titleBarBackground by titleBar::backgroundRect + val contentBackground = rect { // It's here because content cannot contain something by default onUpdate { rectangle = Rect(titleBar.leftBottom, this@Window.rightBottom) setColor(ClickGui.backgroundColor) @@ -68,7 +68,7 @@ open class Window( val content = windowContent(scrollable) - protected val outlineRect = outline { + val outlineRect = outline { onUpdate { rectangle = this@Window.rect setColor(ClickGui.outlineColor) diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt index c973d2cef..898962c45 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt @@ -60,7 +60,7 @@ class WindowContent( } it.overrideY { - prev.renderPositionY + layoutHeight(prev, true) + ClickGui.listStep + prev.renderPositionY + layoutHeight(prev, true) } } } @@ -75,9 +75,14 @@ class WindowContent( overrideWidth(owner::renderWidth) overrideHeight { - children.sumOf { layoutHeight(it, false) } + - ClickGui.listStep * (children.size - 1).coerceAtLeast(0) + - ClickGui.padding * 2 + var height = ClickGui.padding * 2 + + val lastIndex = children.lastIndex + children.forEachIndexed { i, it -> + height += layoutHeight(it, false, i == lastIndex) + } + + height } onShow { @@ -111,8 +116,8 @@ class WindowContent( } } - private fun layoutHeight(layout: Layout, animate: Boolean): Double { - var height = layout.renderHeight + private fun layoutHeight(layout: Layout, animate: Boolean, isLast: Boolean = false): Double { + var height = layout.renderHeight + ClickGui.listStep * (!isLast).toInt() val animated = layout as? AnimatedWindowChild ?: return height height *= if (!animate) animated.staticShowAnimation diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt index ae66d7ed3..5f2b20fb3 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt @@ -27,6 +27,7 @@ import com.lambda.gui.component.core.FilledRect.Companion.rectBehind import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout import com.lambda.gui.component.window.AnimatedWindowChild +import com.lambda.gui.component.window.Window import com.lambda.util.Mouse import com.lambda.util.math.* import java.awt.Color @@ -56,59 +57,66 @@ class ModuleLayout( // ToDo: replace with timer private var lastHover = 0L - init { - rectBehind(titleBar) { // base rect with lowest y to avoid children overlying - onUpdate { - rectangle = this@ModuleLayout.rect.shrink(shrink) - shade = ClickGui.backgroundShade + val backgroundRect = rectBehind(titleBar) { // base rect with lowest y to avoid children overlying + onUpdate { + rectangle = this@ModuleLayout.rect.shrink(shrink) + shade = ClickGui.backgroundShade - val openRev = 1.0 - openAnimation // 1.0 <-> 0.0 - val openRevSigned = openRev * 2 - 1 // 1.0 <-> -1.0 - val enableRev = 1.0 - enableAnimation // 1.0 <-> 0.0 + val openRev = 1.0 - openAnimation // 1.0 <-> 0.0 + val openRevSigned = openRev * 2 - 1 // 1.0 <-> -1.0 + val enableRev = 1.0 - enableAnimation // 1.0 <-> 0.0 - var progress = enableAnimation + var progress = enableAnimation - // hover: +0.1 to alpha if minimized, -0.1 to alpha if maximized - progress += hoverAnimation * ClickGui.moduleHoverAccent * openRevSigned + // hover: +0.1 to alpha if minimized, -0.1 to alpha if maximized + progress += hoverAnimation * ClickGui.moduleHoverAccent * openRevSigned - // +0.4 to alpha if opened and disabled - progress += openAnimation * ClickGui.moduleOpenAccent * enableRev + // +0.4 to alpha if opened and disabled + progress += openAnimation * ClickGui.moduleOpenAccent * enableRev - // interpolate and set the color - setColor(lerp(progress, ClickGui.moduleDisabledColor, ClickGui.moduleEnabledColor)) - } + // interpolate and set the color + setColor( + lerp(progress, + ClickGui.moduleDisabledColor, + ClickGui.moduleEnabledColor + ).multAlpha(showAnimation) + ) + } - onUpdate { - setRadius(hoverAnimation) + onUpdate { + setRadius(hoverAnimation) - if (isLast && ClickGui.autoResize) { - leftBottomRadius = ClickGui.roundRadius - (ClickGui.padding + shrink) - rightBottomRadius = leftBottomRadius - } + if (isLast && ClickGui.autoResize) { + leftBottomRadius = ClickGui.roundRadius - (ClickGui.padding + shrink) + rightBottomRadius = leftBottomRadius } + } - rect { // hover fx - onUpdate { - val base = this@rect.owner as FilledRect + rect { // hover fx + onUpdate { + val base = this@rect.owner as FilledRect - rectangle = base.rectangle - shade = base.shade + rectangle = base.rectangle + shade = base.shade - setRadius( - base.leftTopRadius, - base.rightTopRadius, - base.rightBottomRadius, - base.leftBottomRadius - ) + setRadius( + base.leftTopRadius, + base.rightTopRadius, + base.rightBottomRadius, + base.leftBottomRadius + ) - val hoverColor = Color.WHITE.setAlpha( - ClickGui.moduleHoverAccent * hoverAnimation * (1.0 - openAnimation) - ) + val hoverColor = Color.WHITE.setAlpha( + ClickGui.moduleHoverAccent * hoverAnimation * (1.0 - openAnimation) * showAnimation + ) - setColorH(hoverColor.setAlpha(0.0), hoverColor) - } + setColorH(hoverColor.setAlpha(0.0), hoverColor) } } + } + + init { + backgroundTint() isMinimized = true height = 100.0 @@ -179,5 +187,47 @@ class ModuleLayout( @UIBuilder fun Layout.moduleLayout(module: Module) = ModuleLayout(this, module).apply(children::add) + + /** + * Used to dark the background of the settings a bit + * + * Not for external usage + */ + @UIBuilder + fun Window.backgroundTint(tintTitleBar: Boolean = false) { + check(this is SettingLayout<*, *> || this is ModuleLayout || this is ModuleWindow) + + val base = this@backgroundTint + + rectBehind(content) { + onUpdate { + rectangle = if (tintTitleBar) base.rect + else Rect(titleBar.leftBottom, base.rightBottom) + + setColor(Color.BLACK.setAlpha(0.08 * heightAnimation)) + + val round = (base as? ModuleLayout?)?.backgroundRect + ?: (base as? ModuleWindow)?.contentBackground + + round?.let { + leftBottomRadius = it.leftBottomRadius + rightBottomRadius = it.rightBottomRadius + } + } + + val bg = this + + rect { // top shadow + onUpdate { + rectangle = Rect( + bg.rectangle.leftTop, + bg.rectangle.rightTop + Vec2d.BOTTOM * titleBar.renderHeight * 0.2 + ) + + setColorV(Color.BLACK.setAlpha(0.1 * heightAnimation), Color.BLACK.setAlpha(0.0)) + } + } + } + } } } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt index 59998b0f1..22634c7f7 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt @@ -22,6 +22,7 @@ import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout import com.lambda.gui.component.window.Window import com.lambda.gui.component.window.WindowContent +import com.lambda.gui.impl.clickgui.ModuleLayout.Companion.backgroundTint import com.lambda.gui.impl.clickgui.ModuleLayout.Companion.moduleLayout import com.lambda.module.ModuleRegistry import com.lambda.util.math.Vec2d @@ -32,6 +33,8 @@ class ModuleWindow( initialPosition: Vec2d ) : Window(owner, tag.name, initialPosition, minimizing = Minimizing.Absolute, autoResize = AutoResize.ByConfig) { init { + backgroundTint() + val modules = ModuleRegistry.modules .filter { it.defaultTags.firstOrNull() == tag } .map { module -> content.moduleLayout(module) } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt index 05224f164..dd5ef595a 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt @@ -22,6 +22,7 @@ import com.lambda.module.modules.client.ClickGui import com.lambda.gui.component.HAlign import com.lambda.gui.component.layout.Layout import com.lambda.gui.component.window.AnimatedWindowChild +import com.lambda.gui.impl.clickgui.ModuleLayout.Companion.backgroundTint import com.lambda.util.math.* /** @@ -56,7 +57,7 @@ abstract class SettingLayout > ( if (!expandable) { overrideHeight(titleBar::renderHeight) content.destroy() - } + } else backgroundTint(true) titleBar.textField.use { text = setting.name diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt index b610c63a3..dba92b503 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt @@ -69,8 +69,8 @@ class BooleanButton( setRadius(100.0) onUpdate { - val knobStart = Rect.basedOn(checkBox.leftTop, Vec2d.ONE * checkBox.renderHeight) - val knobEnd = Rect(checkBox.rightBottom - checkBox.renderHeight, checkBox.rightBottom) + val knobStart = Rect.basedOn(checkBox.rectangle.leftTop, Vec2d.ONE * checkBox.rectangle.size.y) + val knobEnd = Rect(checkBox.rectangle.rightBottom - checkBox.rectangle.size.y, checkBox.rectangle.rightBottom) rectangle = lerp( lerp(showAnimation, 1.0 - activeAnimation, activeAnimation), diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/EnumSelector.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/EnumSelector.kt new file mode 100644 index 000000000..f1127ef08 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/EnumSelector.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.gui.impl.clickgui.settings + +import com.lambda.config.settings.comparable.EnumSetting +import com.lambda.gui.component.HAlign +import com.lambda.gui.component.core.TextField.Companion.textField +import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.layout.Layout +import com.lambda.gui.impl.clickgui.ModuleLayout +import com.lambda.gui.impl.clickgui.SettingLayout +import com.lambda.util.Mouse + +// ToDO: complete or transform to a slider +class EnumSelector >( + owner: Layout, + setting: EnumSetting +) : SettingLayout>(owner, setting, true) { + + init { + (owner.owner as? ModuleLayout)?.let { + it.onWindowExpand { + this@EnumSelector.isMinimized = true + } + + it.onWindowMinimize { + this@EnumSelector.isMinimized = true + } + } + + setting.enumValues.map { EnumEntry(this, it) } + .onEach(content.children::add) + + content.listify() + } + + class EnumEntry >( + private val base: EnumSelector, + private val entry: T + ) : Layout(base) { + init { + overrideSize(base::renderWidth) { + base.titleBar.renderHeight * 0.8 + } + + textField { + text = entry.name + textHAlignment = HAlign.CENTER + + onUpdate { + scale = base.titleBar.textField.scale + color = base.titleBar.textField.color + } + } + + onMouseClick(Mouse.Button.Left, Mouse.Action.Click) { + base.settingDelegate = entry + } + } + } + + companion object { + /** + * Creates an [EnumSelector] - visual representation of the [EnumSetting] + */ + @UIBuilder + fun > Layout.enumSetting(setting: EnumSetting) = + EnumSelector(this, setting).apply(children::add) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt index 99caff45b..e378bd1d9 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt @@ -42,7 +42,7 @@ object ClickGui : Module( val backgroundTint by setting("Background Tint", Color.BLACK.setAlpha(0.4)) - val titleBackgroundColor by setting("Title Background Color", Color(60, 60, 60)) + val titleBackgroundColor by setting("Title Background Color", Color(80, 80, 80)) val backgroundColor by setting("Background Color", titleBackgroundColor) val backgroundShade by setting("Background Shade", true) @@ -54,7 +54,7 @@ object ClickGui : Module( val fontOffset by setting("Font Offset", 4.0, 0.0..5.0, 0.1) val dockingGridSize by setting("Docking Grid Size", 1.0, 0.1..10.0, 0.1) - val moduleEnabledColor by setting("Module Enabled Color", Color.WHITE.setAlpha(0.5)) + val moduleEnabledColor by setting("Module Enabled Color", Color.WHITE.setAlpha(0.4)) val moduleDisabledColor by setting("Module Disabled Color", Color.WHITE.setAlpha(0.0)) val moduleHoverAccent by setting("Module Hover Accent", 0.15, 0.0..0.3, 0.01) val moduleOpenAccent by setting("Module Open Accent", 0.3, 0.0..0.5, 0.01) From 7730f6f24f479f53f512f363d2131dfb252c72a0 Mon Sep 17 00:00:00 2001 From: "blade.kt" Date: Sat, 1 Mar 2025 00:34:32 +0300 Subject: [PATCH 100/114] Optimized layout positioning --- .../lambda/gui/component/core/FilledRect.kt | 10 +- .../lambda/gui/component/core/OutlineRect.kt | 4 +- .../lambda/gui/component/core/TextField.kt | 10 +- .../lambda/gui/component/core/UIBuilder.kt | 2 + .../com/lambda/gui/component/layout/Layout.kt | 125 +++++++----------- .../component/window/AnimatedWindowChild.kt | 11 +- .../lambda/gui/component/window/TitleBar.kt | 19 +-- .../com/lambda/gui/component/window/Window.kt | 25 ++-- .../gui/component/window/WindowContent.kt | 14 +- .../lambda/gui/impl/clickgui/ModuleLayout.kt | 29 ++-- .../lambda/gui/impl/clickgui/SettingLayout.kt | 20 ++- .../impl/clickgui/settings/BooleanButton.kt | 18 +-- .../impl/clickgui/settings/EnumSelector.kt | 51 +++---- .../lambda/module/modules/client/ClickGui.kt | 4 +- 14 files changed, 159 insertions(+), 183 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt b/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt index 874033bec..877430948 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt @@ -25,8 +25,6 @@ import java.awt.Color class FilledRect( owner: Layout ) : Layout(owner) { - @UIRenderPr0p3rty var rectangle = Rect.ZERO - @UIRenderPr0p3rty var leftTopRadius = 0.0 @UIRenderPr0p3rty var rightTopRadius = 0.0 @UIRenderPr0p3rty var rightBottomRadius = 0.0 @@ -42,15 +40,9 @@ class FilledRect( init { properties.interactionPassthrough = true - onUpdate { - // make it pressable - position = rectangle.leftTop - size = rectangle.size - } - onRender { filledRect( - rectangle, + rect, leftTopRadius, rightTopRadius, rightBottomRadius, diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt b/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt index c64a9cfe3..a8038d181 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt @@ -24,8 +24,6 @@ import java.awt.Color class OutlineRect( owner: Layout ) : Layout(owner) { - @UIRenderPr0p3rty var rectangle = owner.rect - @UIRenderPr0p3rty var roundRadius = 0.0 @UIRenderPr0p3rty var glowRadius = 0.0 @@ -41,7 +39,7 @@ class OutlineRect( onRender { outlineRect( - rectangle, + rect, roundRadius, glowRadius, leftTopColor, diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt index 6117d6134..a1d75cfe3 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt @@ -43,12 +43,16 @@ class TextField( val textHeight get() = FontRenderer.getHeight(scale) init { - fillParent() properties.interactionPassthrough = true + onUpdate { + position = owner.position + size = owner.size + } + onRender { - val rx = renderPositionX + lerp(textHAlignment.multiplier, offsetX, renderWidth - textWidth - offsetX) - val ry = renderPositionY + lerp(textVAlignment.multiplier, offsetY, renderHeight - textHeight - offsetY) + val rx = positionX + lerp(textHAlignment.multiplier, offsetX, width - textWidth - offsetX) + val ry = positionY + lerp(textVAlignment.multiplier, offsetY, height - textHeight - offsetY) val renderPos = Vec2d(rx, ry + textHeight * 0.5) drawString(text, renderPos, color, scale, shadow) } diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt b/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt index 3b15f0705..cc61da3d0 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt @@ -17,6 +17,7 @@ package com.lambda.gui.component.core +import com.lambda.event.events.GuiEvent import com.lambda.gui.component.layout.Layout import com.lambda.util.math.MathUtils.toInt @@ -37,4 +38,5 @@ fun T.insertLayout( val index = owner.children.indexOf(base) check(index != -1 && base.owner == owner) { "Given layout belongs to different owner" } owner.children.add(index + next.toInt(), this) + this.onEvent(GuiEvent.Update) } diff --git a/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt index 81463a1e6..6e9f0210e 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt @@ -21,8 +21,6 @@ import com.lambda.graphics.RenderMain import com.lambda.graphics.animation.AnimationTicker import com.lambda.event.events.GuiEvent import com.lambda.graphics.pipeline.ScissorAdapter -import com.lambda.graphics.renderer.gui.font.FontRenderer -import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer import com.lambda.gui.component.HAlign import com.lambda.gui.component.VAlign import com.lambda.gui.component.core.* @@ -30,7 +28,6 @@ import com.lambda.util.KeyCode import com.lambda.util.Mouse import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d -import java.awt.Color /** * Represents a component for creating complex ui structures. @@ -38,7 +35,9 @@ import java.awt.Color open class Layout( val owner: Layout? ) { - val rect get() = Rect.basedOn(renderPosition, renderSize) + var rect + get() = Rect.basedOn(position, size) + set(value) { position = value.leftTop; size = value.size } // ToDo: impl alignmentLayout: Layout, instead of being able to align to the owner only // Position of the component @@ -46,28 +45,23 @@ open class Layout( get() = Vec2d(positionX, positionY) set(value) { positionX = value.x; positionY = value.y } - private var positionX: Double + var positionX: Double get() = ownerX + (relativePosX + dockingOffsetX).let { if (!properties.clampPosition) return@let it - it.coerceAtMost(ownerWidth - renderWidth).coerceAtLeast(0.0) + it.coerceAtMost(ownerWidth - width).coerceAtLeast(0.0) }; set(value) { relativePosX = value - ownerX - dockingOffsetX } - private var positionY: Double + var positionY: Double get() = ownerY + (relativePosY + dockingOffsetY).let { if (!properties.clampPosition) return@let it - it.coerceAtMost(ownerHeight - renderHeight).coerceAtLeast(0.0) + it.coerceAtMost(ownerHeight - height).coerceAtLeast(0.0) }; set(value) { relativePosY = value - ownerY - dockingOffsetY } - val leftTop get() = renderPosition - val rightTop get() = Vec2d(renderPositionX + renderWidth, renderPositionY) - val rightBottom get() = Vec2d(renderPositionX + renderWidth, renderPositionY + renderHeight) - val leftBottom get() = Vec2d(renderPositionX, renderPositionY + renderHeight) + val leftTop get() = position + val rightTop get() = Vec2d(positionX + width, positionY) + val rightBottom get() = Vec2d(positionX + width, positionY + height) + val leftBottom get() = Vec2d(positionX, positionY + height) - val renderPosition get() = Vec2d(renderPositionX, renderPositionY) - val renderPositionX get() = positionXTransform() - val renderPositionY get() = positionYTransform() - private var positionXTransform = { positionX } - private var positionYTransform = { positionY } private var relativePosX = 0.0 private var relativePosY = 0.0 @@ -76,14 +70,6 @@ open class Layout( get() = Vec2d(width, height) set(value) { width = value.x; height = value.y } - val renderSize get() = Vec2d(renderWidth, renderHeight) - - val renderWidth get() = widthTransform() - val renderHeight get() = heightTransform() - - private var widthTransform = { width } - private var heightTransform = { height } - var width = 0.0 var height = 0.0 @@ -93,11 +79,11 @@ open class Layout( field = to val delta = to.multiplier - from.multiplier - relativePosX += delta * (renderWidth - ownerWidth) + relativePosX += delta * (width - ownerWidth) } private val dockingOffsetX get() = if (horizontalAlignment == HAlign.LEFT) 0.0 - else (ownerWidth - renderWidth) * horizontalAlignment.multiplier + else (ownerWidth - width) * horizontalAlignment.multiplier // Vertical alignment var verticalAlignment = VAlign.TOP; set(to) { @@ -105,11 +91,11 @@ open class Layout( field = to val delta = to.multiplier - from.multiplier - relativePosY += delta * (renderHeight - ownerHeight) + relativePosY += delta * (height - ownerHeight) } private val dockingOffsetY get() = if (verticalAlignment == VAlign.TOP) 0.0 - else (ownerHeight - renderHeight) * verticalAlignment.multiplier + else (ownerHeight - height) * verticalAlignment.multiplier // Use screen limits if [owner] is null private var screenSize = Vec2d.ZERO @@ -128,12 +114,12 @@ open class Layout( // Structure val children = mutableListOf() var selectedChild: Layout? = null - protected open val renderSelf: Boolean get() = renderWidth > 1 && renderHeight > 1 + protected open val renderSelf: Boolean get() = width > 1 && height > 1 protected open val scissorRect get() = rect // Inputs protected var mousePosition = Vec2d.ZERO - var isHovered = false; get() = field && (owner?.isHovered ?: true) + open val isHovered get() = owner?.let { it.selectedChild == this } ?: true // Actions private val showActions = mutableListOf Unit>() @@ -244,7 +230,7 @@ open class Layout( */ @LayoutBuilder fun T.onMouseClick(button: Mouse.Button, action: Mouse.Action, block: T.() -> Unit) { - mouseClickActions += { butt, act -> + onMouseClick { butt, act -> if (butt == button && act == action) block() } } @@ -274,7 +260,11 @@ open class Layout( */ @LayoutBuilder fun overrideX(transform: () -> Double) { - positionXTransform = transform + positionX = transform() + + onUpdate { + positionX = transform() + } } /** @@ -282,7 +272,11 @@ open class Layout( */ @LayoutBuilder fun overrideY(transform: () -> Double) { - positionYTransform = transform + positionY = transform() + + onUpdate { + positionY = transform() + } } /** @@ -299,7 +293,11 @@ open class Layout( */ @LayoutBuilder fun overrideWidth(transform: () -> Double) { - widthTransform = transform + width = transform() + + onUpdate { + width = transform() + } } /** @@ -307,7 +305,11 @@ open class Layout( */ @LayoutBuilder fun overrideHeight(transform: () -> Double) { - heightTransform = transform + height = transform() + + onUpdate { + height = transform() + } } /** @@ -319,22 +321,6 @@ open class Layout( overrideHeight(height) } - /** - * Makes this layout expand up to parents rect - */ - @LayoutBuilder - fun fillParent( - overrideX: () -> Double = { owner?.renderPositionX ?: ownerX }, - overrideY: () -> Double = { owner?.renderPositionY ?: ownerY }, - overrideWidth: () -> Double = { owner?.renderWidth ?: ownerWidth }, - overrideHeight: () -> Double = { owner?.renderHeight ?: ownerHeight } - ) { - overrideX(overrideX) - overrideY(overrideY) - overrideWidth(overrideWidth) - overrideHeight(overrideHeight) - } - /** * Removes this layout from its parent */ @@ -353,19 +339,17 @@ open class Layout( screenSize = RenderMain.screenSize // Update relative position and bounds - ownerX = owner?.renderPositionX ?: ownerX - ownerY = owner?.renderPositionY ?: ownerY - ownerWidth = owner?.renderWidth ?: screenSize.x - ownerHeight = owner?.renderHeight ?: screenSize.y - - // Update hover state (don't mark as hovered if hovered pixel is outside the owner) - val xh = (mousePosition.x - renderPositionX) in 0.0..renderWidth - val yh = (mousePosition.y - renderPositionY) in 0.0..renderHeight - isHovered = xh && yh + ownerX = owner?.positionX ?: ownerX + ownerY = owner?.positionY ?: ownerY + ownerWidth = owner?.width ?: screenSize.x + ownerHeight = owner?.height ?: screenSize.y // Select an element that's on foreground selectedChild = if (isHovered) children.lastOrNull { - !it.properties.interactionPassthrough && mousePosition in it.rect + if (it.properties.interactionPassthrough) return@lastOrNull false + val xh = (mousePosition.x - it.positionX) in 0.0..it.width + val yh = (mousePosition.y - it.positionY) in 0.0..it.height + xh && yh } else null } } @@ -415,8 +399,7 @@ open class Layout( children.forEach { child -> if (e is GuiEvent.Render) return@forEach if (e is GuiEvent.MouseClick) { - val hovered = child == selectedChild || (child.isHovered && child.properties.interactionPassthrough) - val newAction = if (hovered) e.action else Mouse.Action.Release + val newAction = if (child.isHovered) e.action else Mouse.Action.Release val newEvent = GuiEvent.MouseClick(e.button, newAction, e.mouse) child.onEvent(newEvent) @@ -430,20 +413,6 @@ open class Layout( val block = { renderActions.forEach { it(this) } if (renderSelf) children.forEach { it.onEvent(e) } - - /*if (this !is FilledRect && this !is OutlineRect && this !is TextField) { - OutlineRectRenderer.outlineRect( - rect, - glowRadius = 0.5 - ) - - FontRenderer.drawString( - javaClass.simpleName, - leftTop + FontRenderer.getHeight(0.5) * 0.5, - Color.WHITE, - 0.5 - ) - }*/ } if (!properties.scissor) block() diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/AnimatedWindowChild.kt b/common/src/main/kotlin/com/lambda/gui/component/window/AnimatedWindowChild.kt index 78902c628..07e70511d 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/AnimatedWindowChild.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/AnimatedWindowChild.kt @@ -62,7 +62,12 @@ abstract class AnimatedWindowChild( // Index for smooth "ordered" animation var index = 0 var lastIndex = 0 - protected val isLast get() = index == lastIndex + + protected val isLast + get() = index == lastIndex + + override val isHovered: Boolean + get() = super.isHovered && isShown override val renderSelf: Boolean get() = showAnimation > 0.0 && super.renderSelf @@ -81,10 +86,6 @@ abstract class AnimatedWindowChild( staticShowAnimation = 0.0 } - onUpdate { - isHovered = isHovered && isShown - } - titleBar.textField.use { textHAlignment = HAlign.LEFT diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt index 0ada0f657..14c2db365 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt @@ -39,15 +39,15 @@ class TitleBar( private var dragOffset: Vec2d? = null init { - overrideSize( - owner::renderWidth, - ClickGui::titleBarHeight - ) - onShow { dragOffset = null } + onUpdate { + width = owner.width + height = ClickGui.titleBarHeight + } + onMouseClick { _, _ -> dragOffset = null } onMouseClick(Mouse.Button.Left, Mouse.Action.Click) { if (drag) dragOffset = mousePosition - owner.position @@ -62,7 +62,8 @@ class TitleBar( val backgroundRect = rect { onUpdate { - rectangle = this@TitleBar.rect + position = this@TitleBar.position + size = this@TitleBar.size setColor(ClickGui.titleBackgroundColor) val radius = ClickGui.roundRadius @@ -70,9 +71,9 @@ class TitleBar( rightTopRadius = radius val bottomRadius = transform( - owner.renderHeight, - this.renderHeight, - this.renderHeight + 1, + owner.height, + this.height, + this.height + 1, radius, 0.0 ) diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt index 400d1f311..273fd29c9 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt @@ -56,7 +56,7 @@ open class Window( val titleBarBackground by titleBar::backgroundRect val contentBackground = rect { // It's here because content cannot contain something by default onUpdate { - rectangle = Rect(titleBar.leftBottom, this@Window.rightBottom) + rect = Rect(titleBar.leftBottom, this@Window.rightBottom) setColor(ClickGui.backgroundColor) leftBottomRadius = ClickGui.roundRadius @@ -70,7 +70,9 @@ open class Window( val outlineRect = outline { onUpdate { - rectangle = this@Window.rect + position = this@Window.position + size = this@Window.size + setColor(ClickGui.outlineColor) roundRadius = ClickGui.roundRadius @@ -123,6 +125,10 @@ open class Window( get() = !isMinimized set(value) { isMinimized = !value } + var windowWidth = initialSize.x + var windowHeight = initialSize.y + + var widthAnimation by animation.exp(0.8, ::windowWidth) var heightAnimation by animation.exp( min = { 0.0 }, max = { if (minimizing == Minimizing.Relative) targetHeight else 1.0 }, @@ -130,7 +136,7 @@ open class Window( flag = { !isMinimized } ) - val targetHeight get() = if (!autoResize.enabled) height - titleBar.renderHeight else content.renderHeight + val targetHeight get() = if (!autoResize.enabled) windowHeight - titleBar.height else content.height // Resizing private var resizeX: Double? = null @@ -140,11 +146,10 @@ open class Window( init { position = initialPosition - size = initialSize properties.clampPosition = owner is ScreenLayout - overrideSize(animation.exp(0.8, ::width)::value) { - titleBar.renderHeight + when (minimizing) { + overrideSize(::widthAnimation) { + titleBar.height + when (minimizing) { Minimizing.Disabled -> targetHeight Minimizing.Relative -> heightAnimation Minimizing.Absolute -> heightAnimation * targetHeight @@ -206,8 +211,8 @@ open class Window( if (button != Mouse.Button.Left || action != Mouse.Action.Click) return@onMouseClick - if (resizeXHovered) resizeX = mousePosition.x - renderWidth - if (resizeYHovered) resizeY = mousePosition.y - renderHeight + if (resizeXHovered) resizeX = mousePosition.x - width + if (resizeYHovered) resizeY = mousePosition.y - height } onMouseMove { @@ -232,11 +237,11 @@ open class Window( // Resize if (resizeX != null || resizeY != null) { resizeX?.let { rx -> - width = (mousePosition.x - rx).coerceIn(80.0, 1000.0) + windowWidth = (mousePosition.x - rx).coerceIn(80.0, 1000.0) } resizeY?.let { ry -> - height = (mousePosition.y - ry).coerceIn(titleBar.renderHeight + RESIZE_RANGE, 1000.0) + windowHeight = (mousePosition.y - ry).coerceIn(titleBar.height + RESIZE_RANGE, 1000.0) } } } diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt index 898962c45..e1c61e8ef 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt @@ -53,14 +53,14 @@ class WindowContent( children.forEachIndexed { i, it -> val prev = children.getOrNull(i - 1) ?: run { it.overrideY { - this.renderPositionY + ClickGui.padding + this.positionY + ClickGui.padding } return@forEachIndexed } it.overrideY { - prev.renderPositionY + layoutHeight(prev, true) + prev.positionY + layoutHeight(prev, true) } } } @@ -68,12 +68,12 @@ class WindowContent( init { properties.scissor = true - overrideX(owner.titleBar::renderPositionX) + overrideX(owner.titleBar::positionX) overrideY { - owner.titleBar.let { it.renderPositionY + it.renderHeight } + renderScrollOffset * scrollable.toInt() + owner.titleBar.let { it.positionY + it.height } + renderScrollOffset * scrollable.toInt() } - overrideWidth(owner::renderWidth) + overrideWidth(owner::width) overrideHeight { var height = ClickGui.padding * 2 @@ -101,7 +101,7 @@ class WindowContent( val prevOffset = scrollOffset scrollOffset = scrollOffset.coerceAtLeast( - owner.targetHeight - renderHeight + owner.targetHeight - height ).coerceAtMost(0.0) rubberbandDelta += prevOffset - scrollOffset @@ -117,7 +117,7 @@ class WindowContent( } private fun layoutHeight(layout: Layout, animate: Boolean, isLast: Boolean = false): Double { - var height = layout.renderHeight + ClickGui.listStep * (!isLast).toInt() + var height = layout.height + ClickGui.listStep * (!isLast).toInt() val animated = layout as? AnimatedWindowChild ?: return height height *= if (!animate) animated.staticShowAnimation diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt index 5f2b20fb3..3d5433b2d 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt @@ -59,7 +59,7 @@ class ModuleLayout( val backgroundRect = rectBehind(titleBar) { // base rect with lowest y to avoid children overlying onUpdate { - rectangle = this@ModuleLayout.rect.shrink(shrink) + rect = this@ModuleLayout.rect.shrink(shrink) shade = ClickGui.backgroundShade val openRev = 1.0 - openAnimation // 1.0 <-> 0.0 @@ -96,7 +96,8 @@ class ModuleLayout( onUpdate { val base = this@rect.owner as FilledRect - rectangle = base.rectangle + position = base.position + size = base.size shade = base.shade setRadius( @@ -119,11 +120,10 @@ class ModuleLayout( backgroundTint() isMinimized = true - height = 100.0 openAnimation = 0.0 - overrideX { owner.renderPositionX + ClickGui.padding } - overrideWidth { owner.renderWidth - ClickGui.padding * 2 } + overrideX { owner.positionX + ClickGui.padding } + overrideWidth { owner.width - ClickGui.padding * 2 } titleBar.use { overrideHeight(ClickGui::moduleHeight) @@ -167,10 +167,18 @@ class ModuleLayout( } } - module.settings.forEach { setting -> + val settings = module.settings.map { setting -> content.layoutOf(setting) + }.filterIsInstance>() + + val minimizeSettings = { + settings.forEach { + it.isMinimized = true + } } + onWindowExpand { minimizeSettings() } + onWindowMinimize { minimizeSettings() } content.listify() listOf( @@ -201,7 +209,7 @@ class ModuleLayout( rectBehind(content) { onUpdate { - rectangle = if (tintTitleBar) base.rect + rect = if (tintTitleBar) base.rect else Rect(titleBar.leftBottom, base.rightBottom) setColor(Color.BLACK.setAlpha(0.08 * heightAnimation)) @@ -219,10 +227,9 @@ class ModuleLayout( rect { // top shadow onUpdate { - rectangle = Rect( - bg.rectangle.leftTop, - bg.rectangle.rightTop + Vec2d.BOTTOM * titleBar.renderHeight * 0.2 - ) + position = bg.position + width = bg.width + height = titleBar.height * 0.2 setColorV(Color.BLACK.setAlpha(0.1 * heightAnimation), Color.BLACK.setAlpha(0.0)) } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt index dd5ef595a..ca3471bb7 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt @@ -31,7 +31,7 @@ import com.lambda.util.math.* abstract class SettingLayout > ( owner: Layout, val setting: T, - expandable: Boolean = false + private val expandable: Boolean = false ) : AnimatedWindowChild( owner, setting.name, @@ -51,13 +51,25 @@ abstract class SettingLayout > ( init { isMinimized = true - overrideWidth(owner::renderWidth) + overrideWidth(owner::width) titleBar.overrideHeight(ClickGui::settingsHeight) if (!expandable) { - overrideHeight(titleBar::renderHeight) + overrideHeight(titleBar::height) content.destroy() - } else backgroundTint(true) + } else { + backgroundTint(true) + + // Minimize other expandable settings when this one gets opened + onWindowExpand { + owner.children + .filterIsInstance>() + .filter { it.expandable } + .forEach { + if (it != this) it.isMinimized = true + } + } + } titleBar.textField.use { text = setting.name diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt index dba92b503..212e5870b 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt @@ -44,9 +44,9 @@ class BooleanButton( onUpdate { val rb = this@BooleanButton.rightBottom - val h = this@BooleanButton.renderHeight + val h = this@BooleanButton.height - rectangle = Rect(rb - Vec2d(h * 1.65, h), rb) + rect = Rect(rb - Vec2d(h * 1.65, h), rb) .shrink(shrink + (1.0 - showAnimation) * h * 0.2) + Vec2d.RIGHT * lerp(showAnimation, 5.0, -ClickGui.fontOffset + shrink) @@ -59,20 +59,16 @@ class BooleanButton( if (isHovered) Mouse.Cursor.Pointer else Mouse.Cursor.Arrow ) } - - onMouseClick(Mouse.Button.Left, Mouse.Action.Click) { - setting.value = !setting.value - } } rect { // Knob setRadius(100.0) onUpdate { - val knobStart = Rect.basedOn(checkBox.rectangle.leftTop, Vec2d.ONE * checkBox.rectangle.size.y) - val knobEnd = Rect(checkBox.rectangle.rightBottom - checkBox.rectangle.size.y, checkBox.rectangle.rightBottom) + val knobStart = Rect.basedOn(checkBox.leftTop, Vec2d.ONE * checkBox.size.y) + val knobEnd = Rect(checkBox.rightBottom - checkBox.size.y, checkBox.rightBottom) - rectangle = lerp( + rect = lerp( lerp(showAnimation, 1.0 - activeAnimation, activeAnimation), knobStart, knobEnd @@ -83,6 +79,10 @@ class BooleanButton( setColor(Color.WHITE.setAlpha(0.25 * showAnimation)) } } + + onMouseClick(Mouse.Button.Left, Mouse.Action.Click) { + setting.value = !setting.value + } } companion object { diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/EnumSelector.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/EnumSelector.kt index f1127ef08..70c630dc4 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/EnumSelector.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/EnumSelector.kt @@ -22,7 +22,6 @@ import com.lambda.gui.component.HAlign import com.lambda.gui.component.core.TextField.Companion.textField import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout -import com.lambda.gui.impl.clickgui.ModuleLayout import com.lambda.gui.impl.clickgui.SettingLayout import com.lambda.util.Mouse @@ -33,45 +32,31 @@ class EnumSelector >( ) : SettingLayout>(owner, setting, true) { init { - (owner.owner as? ModuleLayout)?.let { - it.onWindowExpand { - this@EnumSelector.isMinimized = true - } - - it.onWindowMinimize { - this@EnumSelector.isMinimized = true - } - } - - setting.enumValues.map { EnumEntry(this, it) } - .onEach(content.children::add) - - content.listify() - } + setting.enumValues.forEach { enumEntry -> + content.layout { + val base = this@EnumSelector - class EnumEntry >( - private val base: EnumSelector, - private val entry: T - ) : Layout(base) { - init { - overrideSize(base::renderWidth) { - base.titleBar.renderHeight * 0.8 - } + overrideSize(base::width) { + base.titleBar.height * 0.8 + } - textField { - text = entry.name - textHAlignment = HAlign.CENTER + textField { + text = enumEntry.name + textHAlignment = HAlign.CENTER - onUpdate { - scale = base.titleBar.textField.scale - color = base.titleBar.textField.color + onUpdate { + scale = base.titleBar.textField.scale + color = base.titleBar.textField.color + } } - } - onMouseClick(Mouse.Button.Left, Mouse.Action.Click) { - base.settingDelegate = entry + onMouseClick(Mouse.Button.Left, Mouse.Action.Click) { + base.settingDelegate = enumEntry + } } } + + content.listify() } companion object { diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt index e378bd1d9..dda99992b 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt @@ -64,7 +64,7 @@ object ClickGui : Module( val SCREEN get() = gui("Click Gui") { rect { onUpdate { - rectangle = owner!!.rect + rect = owner!!.rect setColor(backgroundTint) } } @@ -73,7 +73,7 @@ object ClickGui : Module( val y = x ModuleTag.defaults.forEach { tag -> - x += moduleWindow(tag, Vec2d(x, y)).renderWidth + 5 + x += moduleWindow(tag, Vec2d(x, y)).width + 5 } } From 195086d3c4c1a358f1032dca88c4787d6fd85ebb Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:29:11 -0500 Subject: [PATCH 101/114] Removed texture compression --- .../src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt index fada950a8..16eca5626 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt @@ -26,7 +26,7 @@ import java.awt.image.BufferedImage import java.nio.ByteBuffer object TextureUtils { - private const val COMPRESSION_LEVEL = 1 + private const val COMPRESSION_LEVEL = -1 private const val THREADED_COMPRESSION = false val encoderPreset = PngEncoder() From 7591b9346d5614ebe778bccd07f0254ce9423db1 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sun, 9 Mar 2025 14:12:41 -0400 Subject: [PATCH 102/114] Added function button --- .../com/lambda/config/AbstractSetting.kt | 2 +- .../kotlin/com/lambda/config/Configurable.kt | 15 ++++--- .../lambda/config/settings/FunctionSetting.kt | 38 ++++++++++++++++ .../main/kotlin/com/lambda/gui/GuiManager.kt | 6 +++ .../gui/impl/clickgui/settings/UnitButton.kt | 44 +++++++++++++++++++ .../main/kotlin/com/lambda/module/Module.kt | 2 + .../module/modules/debug/SettingTest.kt | 6 ++- 7 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/config/settings/FunctionSetting.kt create mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/UnitButton.kt diff --git a/common/src/main/kotlin/com/lambda/config/AbstractSetting.kt b/common/src/main/kotlin/com/lambda/config/AbstractSetting.kt index 6d2b5f557..eb10e5fbd 100644 --- a/common/src/main/kotlin/com/lambda/config/AbstractSetting.kt +++ b/common/src/main/kotlin/com/lambda/config/AbstractSetting.kt @@ -126,7 +126,7 @@ abstract class AbstractSetting( listeners.add(ValueListener(false, block)) } - private fun reset() { + fun reset() { value = defaultValue } diff --git a/common/src/main/kotlin/com/lambda/config/Configurable.kt b/common/src/main/kotlin/com/lambda/config/Configurable.kt index 775709c5c..3b6375842 100644 --- a/common/src/main/kotlin/com/lambda/config/Configurable.kt +++ b/common/src/main/kotlin/com/lambda/config/Configurable.kt @@ -23,16 +23,14 @@ import com.google.gson.reflect.TypeToken import com.lambda.Lambda import com.lambda.Lambda.LOG import com.lambda.config.settings.CharSetting +import com.lambda.config.settings.FunctionSetting import com.lambda.config.settings.StringSetting import com.lambda.config.settings.collections.ListSetting import com.lambda.config.settings.collections.MapSetting import com.lambda.config.settings.collections.SetSetting import com.lambda.config.settings.comparable.BooleanSetting import com.lambda.config.settings.comparable.EnumSetting -import com.lambda.config.settings.complex.BlockPosSetting -import com.lambda.config.settings.complex.BlockSetting -import com.lambda.config.settings.complex.ColorSetting -import com.lambda.config.settings.complex.KeyBindSetting +import com.lambda.config.settings.complex.* import com.lambda.config.settings.numeric.* import com.lambda.util.Communication.logError import com.lambda.util.KeyCode @@ -173,7 +171,6 @@ abstract class Configurable( * @param name The unique identifier for the setting. * @param defaultValue The default [List] value of type [T] for the setting. * @param description A brief explanation of the setting's purpose and behavior. - * @param hackDelegates A flag that determines whether the setting should be serialized with the default value. * @param visibility A lambda expression that determines the visibility status of the setting. * * ```kotlin @@ -204,7 +201,6 @@ abstract class Configurable( * @param name The unique identifier for the setting. * @param defaultValue The default [Map] value of type [K] and [V] for the setting. * @param description A brief explanation of the setting's purpose and behavior. - * @param hackDelegates A flag that determines whether the setting should be serialized with the default value. * @param visibility A lambda expression that determines the visibility status of the setting. * * ```kotlin @@ -424,4 +420,11 @@ abstract class Configurable( description: String = "", visibility: () -> Boolean = { true }, ) = BlockSetting(name, defaultValue, description, visibility).register() + + fun setting( + name: String, + defaultValue: () -> Unit, + description: String = "", + visibility: () -> Boolean = { true } + ) = FunctionSetting(name, defaultValue, description, visibility).register() } diff --git a/common/src/main/kotlin/com/lambda/config/settings/FunctionSetting.kt b/common/src/main/kotlin/com/lambda/config/settings/FunctionSetting.kt new file mode 100644 index 000000000..235d1e8c1 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/config/settings/FunctionSetting.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.config.settings + +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.reflect.TypeToken +import com.lambda.config.AbstractSetting + +open class FunctionSetting( + override val name: String, + private val defaultValue: () -> T, + description: String, + visibility: () -> Boolean, +) : AbstractSetting<() -> T>( + defaultValue, + TypeToken.get(defaultValue::class.java).type, + description, + visibility +) { + override fun toJson(): JsonElement = JsonNull.INSTANCE + override fun loadFromJson(serialized: JsonElement) { value = defaultValue } +} diff --git a/common/src/main/kotlin/com/lambda/gui/GuiManager.kt b/common/src/main/kotlin/com/lambda/gui/GuiManager.kt index 86f9ce568..ab1c61f26 100644 --- a/common/src/main/kotlin/com/lambda/gui/GuiManager.kt +++ b/common/src/main/kotlin/com/lambda/gui/GuiManager.kt @@ -19,11 +19,13 @@ package com.lambda.gui import com.lambda.config.settings.comparable.BooleanSetting import com.lambda.config.settings.comparable.EnumSetting +import com.lambda.config.settings.FunctionSetting import com.lambda.core.Loadable import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout import com.lambda.gui.impl.clickgui.settings.BooleanButton.Companion.booleanSetting import com.lambda.gui.impl.clickgui.settings.EnumSelector.Companion.enumSetting +import com.lambda.gui.impl.clickgui.settings.UnitButton.Companion.unitSetting import kotlin.reflect.KClass object GuiManager : Loadable { @@ -42,6 +44,10 @@ object GuiManager : Loadable { owner.enumSetting(ref) } + typeAdapter> { owner, ref -> + owner.unitSetting(ref) + } + return "Loaded ${typeMap.size} gui type adapters." } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/UnitButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/UnitButton.kt new file mode 100644 index 000000000..96156b36b --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/UnitButton.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.gui.impl.clickgui.settings + +import com.lambda.config.settings.FunctionSetting +import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.layout.Layout +import com.lambda.gui.impl.clickgui.SettingLayout +import com.lambda.util.Mouse + +class UnitButton( + owner: Layout, + setting: FunctionSetting, +) : SettingLayout<() -> Unit, FunctionSetting>(owner, setting) { + init { + onMouseClick(Mouse.Button.Left, Mouse.Action.Click) { + setting.value() + } + } + + companion object { + /** + * Creates a [UnitButton] - visual representation of the [FunctionSetting] + */ + @UIBuilder + fun Layout.unitSetting(setting: FunctionSetting) = + UnitButton(this, setting).apply(children::add) + } +} diff --git a/common/src/main/kotlin/com/lambda/module/Module.kt b/common/src/main/kotlin/com/lambda/module/Module.kt index 5519ec086..4dd2f570a 100644 --- a/common/src/main/kotlin/com/lambda/module/Module.kt +++ b/common/src/main/kotlin/com/lambda/module/Module.kt @@ -34,6 +34,7 @@ import com.lambda.event.listener.UnsafeListener import com.lambda.module.tag.ModuleTag import com.lambda.sound.LambdaSound import com.lambda.sound.SoundManager.playSoundRandomly +import com.lambda.util.Communication.info import com.lambda.util.KeyCode import com.lambda.util.Nameable @@ -116,6 +117,7 @@ abstract class Module( private val isEnabledSetting = setting("Enabled", enabledByDefault, visibility = { false }) private val keybindSetting = setting("Keybind", defaultKeybind) private val isVisible = setting("Visible", true) + val reset by setting("Reset", { settings.forEach { it.reset() }; this@Module.info("Settings set to default") }) val customTags = setting("Tags", setOf(), visibility = { false }) var isEnabled by isEnabledSetting diff --git a/common/src/main/kotlin/com/lambda/module/modules/debug/SettingTest.kt b/common/src/main/kotlin/com/lambda/module/modules/debug/SettingTest.kt index b7786e85e..dad299d2e 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/debug/SettingTest.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/debug/SettingTest.kt @@ -19,6 +19,7 @@ package com.lambda.module.modules.debug import com.lambda.module.Module import com.lambda.module.tag.ModuleTag +import com.lambda.util.Communication.info import com.lambda.util.KeyCode import net.minecraft.block.Blocks import net.minecraft.util.math.BlockPos @@ -61,9 +62,12 @@ object SettingTest : Module( private val colorMap by setting("Color Map", mapOf("Primary" to Color.GREEN)) private val keyBindSet by setting("Key Bind Set", setOf(KeyCode.T)) + // Other + private val unitSetting by setting("Unit Test", { this@SettingTest.info("Unit setting") }) + enum class ExampleEnum { VALUE_ONE, VALUE_TWO, VALUE_THREE } -} \ No newline at end of file +} From a63d0a1c56c62e7264d09690ae8e0447dbfa9178 Mon Sep 17 00:00:00 2001 From: "blade.kt" Date: Mon, 10 Mar 2025 00:08:56 +0300 Subject: [PATCH 103/114] Sliders & much stuff --- .../lambda/config/settings/FunctionSetting.kt | 2 +- .../lambda/graphics/buffer/VertexPipeline.kt | 4 - .../renderer/gui/rect/FilledRectRenderer.kt | 5 +- .../renderer/gui/rect/OutlineRectRenderer.kt | 2 +- .../main/kotlin/com/lambda/gui/GuiManager.kt | 16 +- .../lambda/gui/component/core/OutlineRect.kt | 16 +- .../com/lambda/gui/component/layout/Layout.kt | 158 ++++++++---------- .../lambda/gui/component/window/TitleBar.kt | 9 +- .../com/lambda/gui/component/window/Window.kt | 26 ++- .../gui/component/window/WindowContent.kt | 25 ++- .../lambda/gui/impl/clickgui/ModuleWindow.kt | 13 +- .../clickgui/core/AnimatedChild.kt} | 71 ++++++-- .../gui/impl/clickgui/core/SliderLayout.kt | 139 +++++++++++++++ .../clickgui/{ => module}/ModuleLayout.kt | 54 ++---- .../clickgui/{ => module}/SettingLayout.kt | 43 +++-- .../{ => module}/settings/BooleanButton.kt | 26 ++- .../clickgui/module/settings/EnumSlider.kt | 56 +++++++ .../clickgui/module/settings/NumberSlider.kt | 58 +++++++ .../clickgui/module/settings/SettingSlider.kt | 85 ++++++++++ .../impl/clickgui/settings/EnumSelector.kt | 70 -------- .../gui/impl/clickgui/settings/UnitButton.kt | 12 +- .../lambda/module/modules/client/ClickGui.kt | 8 + 22 files changed, 595 insertions(+), 303 deletions(-) rename common/src/main/kotlin/com/lambda/gui/{component/window/AnimatedWindowChild.kt => impl/clickgui/core/AnimatedChild.kt} (57%) create mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/SliderLayout.kt rename common/src/main/kotlin/com/lambda/gui/impl/clickgui/{ => module}/ModuleLayout.kt (84%) rename common/src/main/kotlin/com/lambda/gui/impl/clickgui/{ => module}/SettingLayout.kt (70%) rename common/src/main/kotlin/com/lambda/gui/impl/clickgui/{ => module}/settings/BooleanButton.kt (77%) create mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/EnumSlider.kt create mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/NumberSlider.kt create mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt delete mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/EnumSelector.kt diff --git a/common/src/main/kotlin/com/lambda/config/settings/FunctionSetting.kt b/common/src/main/kotlin/com/lambda/config/settings/FunctionSetting.kt index 235d1e8c1..c876314c0 100644 --- a/common/src/main/kotlin/com/lambda/config/settings/FunctionSetting.kt +++ b/common/src/main/kotlin/com/lambda/config/settings/FunctionSetting.kt @@ -22,7 +22,7 @@ import com.google.gson.JsonNull import com.google.gson.reflect.TypeToken import com.lambda.config.AbstractSetting -open class FunctionSetting( +open class FunctionSetting( override val name: String, private val defaultValue: () -> T, description: String, diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt index d7f4a97cf..c8be117bf 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt @@ -185,10 +185,6 @@ class VertexPipeline( uploadedIndices = 0 } - fun finalize() { - - } - init { // All the buffers have been generated, all we have to // do now it bind them correctly and populate them diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt index b786e621b..073108079 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt @@ -29,6 +29,7 @@ object FilledRectRenderer : AbstractGUIRenderer( ) { private const val MIN_SIZE = 0.5 private const val MIN_ALPHA = 1 + private const val EXPAND = 0.3 fun filledRect( rect: Rect, @@ -100,8 +101,8 @@ object FilledRectRenderer : AbstractGUIRenderer( val rbr = rightBottomRadius.coerceAtMost(maxRadius).coerceAtLeast(0.0) val rtr = rightTopRadius.coerceAtMost(maxRadius).coerceAtLeast(0.0) - val p1 = pos1 - 0.25 - val p2 = pos2 + 0.25 + val p1 = pos1 - EXPAND + val p2 = pos2 + EXPAND shader["u_Size"] = size shader["u_RoundLeftTop"] = ltr diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt index 127cacc98..a43381723 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt @@ -58,7 +58,7 @@ object OutlineRectRenderer : AbstractGUIRenderer( leftBottom: Color = Color.WHITE, shade: Boolean = false, ) = render(shade) { - if (glowRadius < 1) return@render + if (glowRadius < 0.1) return@render grow(VERTICES_COUNT * 3) diff --git a/common/src/main/kotlin/com/lambda/gui/GuiManager.kt b/common/src/main/kotlin/com/lambda/gui/GuiManager.kt index ab1c61f26..d39796db5 100644 --- a/common/src/main/kotlin/com/lambda/gui/GuiManager.kt +++ b/common/src/main/kotlin/com/lambda/gui/GuiManager.kt @@ -17,14 +17,16 @@ package com.lambda.gui +import com.lambda.config.settings.NumericSetting import com.lambda.config.settings.comparable.BooleanSetting import com.lambda.config.settings.comparable.EnumSetting import com.lambda.config.settings.FunctionSetting import com.lambda.core.Loadable import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout -import com.lambda.gui.impl.clickgui.settings.BooleanButton.Companion.booleanSetting -import com.lambda.gui.impl.clickgui.settings.EnumSelector.Companion.enumSetting +import com.lambda.gui.impl.clickgui.module.settings.BooleanButton.Companion.booleanSetting +import com.lambda.gui.impl.clickgui.module.settings.EnumSlider.Companion.enumSetting +import com.lambda.gui.impl.clickgui.module.settings.NumberSlider.Companion.numericSetting import com.lambda.gui.impl.clickgui.settings.UnitButton.Companion.unitSetting import kotlin.reflect.KClass @@ -44,10 +46,14 @@ object GuiManager : Loadable { owner.enumSetting(ref) } - typeAdapter> { owner, ref -> + typeAdapter> { owner, ref -> owner.unitSetting(ref) } + typeAdapter> { owner, ref -> + owner.numericSetting(ref) + } + return "Loaded ${typeMap.size} gui type adapters." } @@ -59,5 +65,7 @@ object GuiManager : Loadable { reference: Any, block: Layout.() -> Unit = {} ): Layout? = - typeMap[reference::class]?.invoke(this, reference)?.apply(block) + (typeMap[reference::class] ?: typeMap.entries.firstOrNull { + reference::class.java.superclass == it.key.java + }?.value)?.invoke(this, reference)?.apply(block) } diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt b/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt index a8038d181..014f0fd1c 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt @@ -25,7 +25,7 @@ class OutlineRect( owner: Layout ) : Layout(owner) { @UIRenderPr0p3rty var roundRadius = 0.0 - @UIRenderPr0p3rty var glowRadius = 0.0 + @UIRenderPr0p3rty var glowRadius = 1.0 @UIRenderPr0p3rty var leftTopColor: Color = Color.WHITE @UIRenderPr0p3rty var rightTopColor: Color = Color.WHITE @@ -58,6 +58,20 @@ class OutlineRect( leftBottomColor = color } + fun setColorH(colorL: Color, colorR: Color) { + leftTopColor = colorL + rightTopColor = colorR + rightBottomColor = colorR + leftBottomColor = colorL + } + + fun setColorV(colorT: Color, colorB: Color) { + leftTopColor = colorT + rightTopColor = colorT + rightBottomColor = colorB + leftBottomColor = colorB + } + companion object { /** * Creates an [OutlineRect] component - layout-based rect representation diff --git a/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt index 6e9f0210e..e7b946d1a 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt @@ -41,16 +41,19 @@ open class Layout( // ToDo: impl alignmentLayout: Layout, instead of being able to align to the owner only // Position of the component + @UIRenderPr0p3rty var position: Vec2d get() = Vec2d(positionX, positionY) set(value) { positionX = value.x; positionY = value.y } + @UIRenderPr0p3rty var positionX: Double get() = ownerX + (relativePosX + dockingOffsetX).let { if (!properties.clampPosition) return@let it it.coerceAtMost(ownerWidth - width).coerceAtLeast(0.0) }; set(value) { relativePosX = value - ownerX - dockingOffsetX } + @UIRenderPr0p3rty var positionY: Double get() = ownerY + (relativePosY + dockingOffsetY).let { if (!properties.clampPosition) return@let it @@ -66,12 +69,13 @@ open class Layout( private var relativePosY = 0.0 // Size of the component + @UIRenderPr0p3rty var size: Vec2d get() = Vec2d(width, height) set(value) { width = value.x; height = value.y } - var width = 0.0 - var height = 0.0 + @UIRenderPr0p3rty var width = 0.0 + @UIRenderPr0p3rty var height = 0.0 // Horizontal alignment var horizontalAlignment = HAlign.LEFT; set(to) { @@ -118,9 +122,22 @@ open class Layout( protected open val scissorRect get() = rect // Inputs - protected var mousePosition = Vec2d.ZERO + protected var mousePosition = Vec2d.ZERO; set(value) { + if (field == value) return + field = value + + selectedChild = if (isHovered) children.lastOrNull { + if (it.properties.interactionPassthrough) return@lastOrNull false + val xh = (value.x - it.positionX) in 0.0..it.width + val yh = (value.y - it.positionY) in 0.0..it.height + xh && yh + } else null + } open val isHovered get() = owner?.let { it.selectedChild == this } ?: true + var pressedButton: Mouse.Button? = null + protected val isPressed get() = pressedButton != null + // Actions private val showActions = mutableListOf Unit>() private val hideActions = mutableListOf Unit>() @@ -130,6 +147,7 @@ open class Layout( private val keyPressActions = mutableListOf Unit>() private val charTypedActions = mutableListOf Unit>() private val mouseClickActions = mutableListOf Unit>() + private val mouseActions = mutableListOf Unit>() private val mouseMoveActions = mutableListOf Unit>() private val mouseScrollActions = mutableListOf Unit>() @@ -180,6 +198,7 @@ open class Layout( */ @LayoutBuilder fun T.onUpdate(action: T.() -> Unit) { + action(this) updateActions += { action() } } @@ -219,19 +238,21 @@ open class Layout( * @param action The action to be performed. */ @LayoutBuilder - fun T.onMouseClick(action: T.(button: Mouse.Button, action: Mouse.Action) -> Unit) { - mouseClickActions += { button, mouseAction -> action(button, mouseAction) } + fun T.onMouse(button: Mouse.Button? = null, action: Mouse.Action? = null, block: T.(Mouse.Button) -> Unit) { + mouseClickActions += { butt, act -> + if ((butt == button || button == null) && (act == action || action == null)) block(butt) + } } /** - * Sets the action to be performed when mouse button gets clicked. + * Sets the action to be performed when mouse button gets released and this layout was clicked. * * @param action The action to be performed. */ @LayoutBuilder - fun T.onMouseClick(button: Mouse.Button, action: Mouse.Action, block: T.() -> Unit) { - onMouseClick { butt, act -> - if (butt == button && act == action) block() + fun T.onMouseAction(button: Mouse.Button? = null, acceptNotHovered: Boolean = false, action: T.(Mouse.Button) -> Unit) { + mouseActions += { + if (it == button || button == null && (isHovered || !acceptNotHovered)) action(it) } } @@ -255,72 +276,6 @@ open class Layout( mouseScrollActions += { delta -> action(delta) } } - /** - * Force overrides drawn x position of the layout - */ - @LayoutBuilder - fun overrideX(transform: () -> Double) { - positionX = transform() - - onUpdate { - positionX = transform() - } - } - - /** - * Force overrides drawn y position of the layout - */ - @LayoutBuilder - fun overrideY(transform: () -> Double) { - positionY = transform() - - onUpdate { - positionY = transform() - } - } - - /** - * Force overrides drawn position of the layout - */ - @LayoutBuilder - fun overridePosition(x: () -> Double, y: () -> Double) { - overrideX(x) - overrideY(y) - } - - /** - * Force overrides drawn width of the layout - */ - @LayoutBuilder - fun overrideWidth(transform: () -> Double) { - width = transform() - - onUpdate { - width = transform() - } - } - - /** - * Force overrides drawn height of the layout - */ - @LayoutBuilder - fun overrideHeight(transform: () -> Double) { - height = transform() - - onUpdate { - height = transform() - } - } - - /** - * Force overrides drawn size of the layout - */ - @LayoutBuilder - fun overrideSize(width: () -> Double, height: () -> Double) { - overrideWidth(width) - overrideHeight(height) - } - /** * Removes this layout from its parent */ @@ -343,14 +298,6 @@ open class Layout( ownerY = owner?.positionY ?: ownerY ownerWidth = owner?.width ?: screenSize.x ownerHeight = owner?.height ?: screenSize.y - - // Select an element that's on foreground - selectedChild = if (isHovered) children.lastOrNull { - if (it.properties.interactionPassthrough) return@lastOrNull false - val xh = (mousePosition.x - it.positionX) in 0.0..it.width - val yh = (mousePosition.y - it.positionY) in 0.0..it.height - xh && yh - } else null } } @@ -358,6 +305,8 @@ open class Layout( // Update self when (e) { is GuiEvent.Show -> { + pressedButton = null + selectedChild = null mousePosition = Vec2d.ONE * -1000.0 showActions.forEach { it(this) } } @@ -381,16 +330,29 @@ open class Layout( } is GuiEvent.MouseMove -> { mousePosition = e.mouse + mouseMoveActions.forEach { it(this, e.mouse) } } is GuiEvent.MouseScroll -> { - if (!isHovered) return mousePosition = e.mouse + + if (!isHovered) return mouseScrollActions.forEach { it(this, e.delta) } } is GuiEvent.MouseClick -> { mousePosition = e.mouse + val action = if (isHovered) e.action else Mouse.Action.Release + + val prevPressed = pressedButton + pressedButton = e.button.takeIf { action == Mouse.Action.Click } + + if (pressedButton == null) prevPressed?.let { button -> + mouseActions.forEach { + it.invoke(this, button) + } + } + mouseClickActions.forEach { it(this, e.button, action) } } } @@ -434,6 +396,34 @@ open class Layout( ) = Layout(this) .apply(children::add).apply(block) + /** + * Creates an empty [Layout] behind given [layout] + * + * @param block Actions to perform within this component. + * + * Check [Layout] description for more info about batching. + */ + @UIBuilder + fun Layout.layoutBehind( + layout: Layout, + block: Layout.() -> Unit = {}, + ) = Layout(this) + .insertLayout(this, layout, false).apply(block) + + /** + * Creates an empty [Layout] over given [layout]. + * + * @param block Actions to perform within this component. + * + * Check [Layout] description for more info about batching. + */ + @UIBuilder + fun Layout.layoutOver( + layout: Layout, + block: Layout.() -> Unit = {}, + ) = Layout(this) + .insertLayout(this, layout, true).apply(block) + /** * Creates new [AnimationTicker]. * diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt index 14c2db365..f736c0c0f 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt @@ -25,7 +25,6 @@ import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout import com.lambda.util.Mouse import com.lambda.util.math.Vec2d -import com.lambda.util.math.lerp import com.lambda.util.math.transform /** @@ -48,8 +47,8 @@ class TitleBar( height = ClickGui.titleBarHeight } - onMouseClick { _, _ -> dragOffset = null } - onMouseClick(Mouse.Button.Left, Mouse.Action.Click) { + onMouse { dragOffset = null } + onMouse(Mouse.Button.Left, Mouse.Action.Click) { if (drag) dragOffset = mousePosition - owner.position } @@ -72,8 +71,8 @@ class TitleBar( val bottomRadius = transform( owner.height, - this.height, - this.height + 1, + height, + height + 1, radius, 0.0 ) diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt index 273fd29c9..0843e6bed 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt @@ -27,6 +27,7 @@ import com.lambda.gui.component.layout.Layout import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.window.TitleBar.Companion.titleBar import com.lambda.gui.component.window.WindowContent.Companion.windowContent +import com.lambda.gui.impl.clickgui.core.AnimatedChild import com.lambda.util.Mouse import com.lambda.util.math.MathUtils.toInt import com.lambda.util.math.Rect @@ -54,7 +55,7 @@ open class Window( val titleBar = titleBar(initialTitle, draggable) val titleBarBackground by titleBar::backgroundRect - val contentBackground = rect { // It's here because content cannot contain something by default + val contentBackground = rect { onUpdate { rect = Rect(titleBar.leftBottom, this@Window.rightBottom) setColor(ClickGui.backgroundColor) @@ -148,25 +149,24 @@ open class Window( position = initialPosition properties.clampPosition = owner is ScreenLayout - overrideSize(::widthAnimation) { - titleBar.height + when (minimizing) { + onUpdate { + width = widthAnimation + height = titleBar.height + when (minimizing) { Minimizing.Disabled -> targetHeight Minimizing.Relative -> heightAnimation Minimizing.Absolute -> heightAnimation * targetHeight } } - titleBar.onMouseClick { button, action -> + titleBar.onMouseAction(Mouse.Button.Right) { // Toggle minimizing state when right-clicking title bar - if (minimizing == Minimizing.Disabled) return@onMouseClick - if (button != Mouse.Button.Right || action != Mouse.Action.Click) return@onMouseClick - + if (minimizing == Minimizing.Disabled) return@onMouseAction isMinimized = !isMinimized } content.onUpdate { val animatedChildren = content.children - .filterIsInstance() + .filterIsInstance() .filter { it.isShown } animatedChildren.forEachIndexed { i, it -> @@ -204,13 +204,9 @@ open class Window( cursorController.setCursor(cursor) } - onMouseClick { button: Mouse.Button, action: Mouse.Action -> - // Update resize dragging offsets - resizeX = null - resizeY = null - - if (button != Mouse.Button.Left || action != Mouse.Action.Click) return@onMouseClick - + // Update resize dragging offsets + onMouse { resizeX = null; resizeY = null } + onMouse(Mouse.Button.Left, Mouse.Action.Click) { if (resizeXHovered) resizeX = mousePosition.x - width if (resizeYHovered) resizeY = mousePosition.y - height } diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt index e1c61e8ef..9a9d2cc5c 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt @@ -22,6 +22,7 @@ import com.lambda.gui.component.core.LayoutBuilder import com.lambda.module.modules.client.ClickGui import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout +import com.lambda.gui.impl.clickgui.core.AnimatedChild import com.lambda.util.math.MathUtils.toInt import com.lambda.util.math.Rect import kotlin.math.abs @@ -52,15 +53,15 @@ class WindowContent( fun listify() { children.forEachIndexed { i, it -> val prev = children.getOrNull(i - 1) ?: run { - it.overrideY { - this.positionY + ClickGui.padding + it.onUpdate { + positionY = this@WindowContent.positionY + ClickGui.padding } return@forEachIndexed } - it.overrideY { - prev.positionY + layoutHeight(prev, true) + it.onUpdate { + positionY = prev.positionY + layoutHeight(prev, true) } } } @@ -68,21 +69,17 @@ class WindowContent( init { properties.scissor = true - overrideX(owner.titleBar::positionX) - overrideY { - owner.titleBar.let { it.positionY + it.height } + renderScrollOffset * scrollable.toInt() - } + onUpdate { + positionX = owner.titleBar.positionX + positionY = owner.titleBar.let { it.positionY + it.height } + renderScrollOffset * scrollable.toInt() + width = owner.width - overrideWidth(owner::width) - overrideHeight { - var height = ClickGui.padding * 2 + height = ClickGui.padding * 2 val lastIndex = children.lastIndex children.forEachIndexed { i, it -> height += layoutHeight(it, false, i == lastIndex) } - - height } onShow { @@ -118,7 +115,7 @@ class WindowContent( private fun layoutHeight(layout: Layout, animate: Boolean, isLast: Boolean = false): Double { var height = layout.height + ClickGui.listStep * (!isLast).toInt() - val animated = layout as? AnimatedWindowChild ?: return height + val animated = layout as? AnimatedChild ?: return height height *= if (!animate) animated.staticShowAnimation else animated.showAnimation diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt index 22634c7f7..8d204562c 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt @@ -22,8 +22,8 @@ import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout import com.lambda.gui.component.window.Window import com.lambda.gui.component.window.WindowContent -import com.lambda.gui.impl.clickgui.ModuleLayout.Companion.backgroundTint -import com.lambda.gui.impl.clickgui.ModuleLayout.Companion.moduleLayout +import com.lambda.gui.impl.clickgui.module.ModuleLayout.Companion.backgroundTint +import com.lambda.gui.impl.clickgui.module.ModuleLayout.Companion.moduleLayout import com.lambda.module.ModuleRegistry import com.lambda.util.math.Vec2d @@ -41,17 +41,14 @@ class ModuleWindow( content.listify() - onWindowExpand { + val minimize = { modules.forEach { it.isMinimized = true } } - onWindowMinimize { - modules.forEach { - it.isMinimized = true - } - } + onWindowExpand { minimize() } + onWindowMinimize { minimize() } } companion object { diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/AnimatedWindowChild.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/AnimatedChild.kt similarity index 57% rename from common/src/main/kotlin/com/lambda/gui/component/window/AnimatedWindowChild.kt rename to common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/AnimatedChild.kt index 07e70511d..923065995 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/window/AnimatedWindowChild.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/AnimatedChild.kt @@ -15,11 +15,13 @@ * along with this program. If not, see . */ -package com.lambda.gui.component.window +package com.lambda.gui.impl.clickgui.core import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.gui.component.HAlign import com.lambda.gui.component.layout.Layout +import com.lambda.gui.component.window.Window +import com.lambda.gui.impl.clickgui.module.SettingLayout import com.lambda.module.modules.client.ClickGui import com.lambda.util.math.MathUtils.toInt import com.lambda.util.math.Vec2d @@ -27,20 +29,30 @@ import com.lambda.util.math.lerp import com.lambda.util.math.setAlpha import com.lambda.util.math.transform import java.awt.Color +import kotlin.math.pow -abstract class AnimatedWindowChild( +abstract class AnimatedChild( owner: Layout, initialTitle: String = "Untitled", initialPosition: Vec2d = Vec2d.ZERO, initialSize: Vec2d = Vec2d(110, 350), - draggable: Boolean = true, - scrollable: Boolean = true, - minimizing: Minimizing = Minimizing.Relative, - resizable: Boolean = true, + draggable: Boolean = false, + scrollable: Boolean = false, + minimizing: Minimizing = Minimizing.Disabled, + resizable: Boolean = false, autoResize: AutoResize = AutoResize.Disabled ) : Window(owner, initialTitle, initialPosition, initialSize, draggable, scrollable, minimizing, resizable, autoResize) { private val window get() = owner?.owner as? Window - private val animatedWindow get() = owner?.owner as? AnimatedWindowChild + private val animatedWindow get() = owner?.owner as? AnimatedChild + + // Hovering + protected val hovered get() = isHovered || isExpand || System.currentTimeMillis() - lastHover < 80 + var hoverAnimation by animation.exp(0.0, 1.0, { if (hovered) 0.8 else 0.3 }, ::hovered) + private var lastHover = 0L // ToDo: replace with timer + open val shrink get() = 0.5 * lerp(openAnimation, 1.0, 0.5) * (hoverAnimation.pow(3) + pressAnimation) + + protected var pressAnimation by animation.exp(0.0, 1.0, 0.6) { isPressed && content.selectedChild == null } + protected var openAnimation by animation.exp(1.0, 0.0, 0.6, ::isMinimized) // Show animation for when the component is shown or hidden open val isShown get() = true @@ -48,8 +60,18 @@ abstract class AnimatedWindowChild( var showAnimation by animation.exp(0.0, 1.0, { var speed = 0.7 - if (lastIndex != 0) - speed = transform(index.toDouble(), 0.0, lastIndex.toDouble(), 0.4, speed) + if (lastIndex != 0) { + var start = speed + var end = speed + when (ClickGui.animationCurve) { + ClickGui.AnimationCurve.Normal -> start = ClickGui.smoothness + ClickGui.AnimationCurve.Static -> {} + ClickGui.AnimationCurve.Reverse -> end = ClickGui.smoothness + } + speed = transform(index.toDouble(), 0.0, lastIndex.toDouble(), start, end) + } + + if ((this as? SettingLayout<*, *>)?.isVisible == true) speed *= 0.8 speed + isShownInternal.toInt() * 0.1 }) { isShownInternal }.apply { @@ -73,27 +95,40 @@ abstract class AnimatedWindowChild( get() = showAnimation > 0.0 && super.renderSelf init { - titleBar.textField.onUpdate { - textHAlignment = HAlign.LEFT - offsetX = lerp(showAnimation, -5.0, ClickGui.fontOffset) - color = Color.WHITE.setAlpha(showAnimation) - } - } + isMinimized = true + openAnimation = 0.0 - init { onShow { showAnimation = 0.0 staticShowAnimation = 0.0 } + onUpdate { + if (isHovered) lastHover = System.currentTimeMillis() + } + titleBar.textField.use { textHAlignment = HAlign.LEFT onUpdate { - offsetX = lerp(showAnimation, -5.0, ClickGui.fontOffset) - scale = lerp(showAnimation, 0.7, 1.0) + offsetX = lerp( + showAnimation, + -5.0, + ClickGui.fontOffset + hoverAnimation * 2 - pressAnimation + ) + + scale = 1.0 * + lerp(showAnimation, 0.7, 1.0) * + lerp(pressAnimation, 1.0, 0.95) + color = Color.WHITE.setAlpha(showAnimation) } } + + listOf( + titleBarBackground, + contentBackground, + outlineRect + ).forEach(Layout::destroy) } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/SliderLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/SliderLayout.kt new file mode 100644 index 000000000..15e53488c --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/SliderLayout.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.gui.impl.clickgui.core + +import com.lambda.graphics.animation.Animation.Companion.exp +import com.lambda.gui.component.core.FilledRect.Companion.rect +import com.lambda.gui.component.core.LayoutBuilder +import com.lambda.gui.component.core.OutlineRect.Companion.outline +import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.core.insertLayout +import com.lambda.gui.component.layout.Layout +import com.lambda.gui.impl.clickgui.module.settings.SettingSlider +import com.lambda.module.modules.client.ClickGui +import com.lambda.util.Mouse +import com.lambda.util.math.Vec2d +import com.lambda.util.math.setAlpha +import com.lambda.util.math.transform +import java.awt.Color + +class SliderLayout( + owner: Layout +) : AnimatedChild(owner, "") { + // Not a great solution + private val setting = owner as? SettingSlider<*, *> + private val showAnim get() = setting?.showAnimation ?: showAnimation + private val hoverAnim get() = setting?.hoverAnimation ?: hoverAnimation + private val pressedBut get() = setting?.pressedButton ?: pressedButton + + // Actions + private var getProgressBlock = { 0.0 } + private var setProgressBlock = { _: Double -> } + + @LayoutBuilder + fun progress(action: SliderLayout.() -> Double) { + getProgressBlock = { action(this) } + } + + @LayoutBuilder + fun onSlide(action: SliderLayout.(Double) -> Unit) { + setProgressBlock = { action(this, it) } + } + + private val renderProgress get() = renderProgress0 * showAnim + private val renderProgress0 by animation.exp(0.7) { + if (pressedBut == Mouse.Button.Left) dragProgress else getProgressBlock() + } + + private val bg = rect { // background + onUpdate { + rect = this@SliderLayout.rect + .moveFirst(Vec2d.RIGHT * ClickGui.fontOffset) + .moveSecond(Vec2d.LEFT * ClickGui.fontOffset) + + shade = ClickGui.backgroundShade + setColor(Color.BLACK.setAlpha(0.25 * showAnim)) + setRadius(100.0) + } + } + + private val dragProgress: Double get() = transform( + mousePosition.x - bg.positionX, + 0.0, bg.width, + 0.0, 1.0 + ).coerceIn(0.0, 1.0) + + init { + (setting ?: this).onMouseMove { + if (pressedBut != Mouse.Button.Left) return@onMouseMove + setProgressBlock(dragProgress) + } + + (setting ?: this).onMouse(Mouse.Button.Left, Mouse.Action.Click) { + if (pressedBut != Mouse.Button.Left) return@onMouse + setProgressBlock(dragProgress) + } + + val progress = rect { + onUpdate { // progress + rect = bg.rect + width *= renderProgress + + shade = ClickGui.backgroundShade + setColor(Color.WHITE.setAlpha(0.25 * showAnim)) + setRadius(100.0) + } + } + + outline { + onUpdate { + rect = progress.rect + setColor(bg.leftTopColor) + roundRadius = 100.0 + } + } + } + + companion object { + /** + * Creates a [SliderLayout]. + */ + @UIBuilder + fun Layout.slider( + block: SliderLayout.() -> Unit = {} + ) = SliderLayout(this).apply(children::add).apply(block) + + /** + * Adds a [SliderLayout] behind given [layout] + */ + @UIBuilder + fun Layout.sliderBehind( + layout: Layout, + block: SliderLayout.() -> Unit = {} + ) = SliderLayout(this).insertLayout(this, layout, false).apply(block) + + /** + * Adds a [SliderLayout] over given [layout] + */ + @UIBuilder + fun Layout.sliderOver( + layout: Layout, + block: SliderLayout.() -> Unit = {} + ) = SliderLayout(this).insertLayout(this, layout, true).apply(block) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/ModuleLayout.kt similarity index 84% rename from common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt rename to common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/ModuleLayout.kt index 3d5433b2d..b07718345 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/ModuleLayout.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Lambda + * Copyright 2025 Lambda * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.lambda.gui.impl.clickgui +package com.lambda.gui.impl.clickgui.module import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.module.Module @@ -26,19 +26,19 @@ import com.lambda.gui.component.core.FilledRect.Companion.rect import com.lambda.gui.component.core.FilledRect.Companion.rectBehind import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout -import com.lambda.gui.component.window.AnimatedWindowChild import com.lambda.gui.component.window.Window +import com.lambda.gui.impl.clickgui.ModuleWindow +import com.lambda.gui.impl.clickgui.core.AnimatedChild import com.lambda.util.Mouse import com.lambda.util.math.* import java.awt.Color -import kotlin.math.pow class ModuleLayout( owner: Layout, module: Module, initialPosition: Vec2d = Vec2d.ZERO, initialSize: Vec2d = Vec2d(100, 18) -) : AnimatedWindowChild( +) : AnimatedChild( owner, module.name, initialPosition, initialSize, @@ -48,14 +48,6 @@ class ModuleLayout( private val cursorController = cursorController() private var enableAnimation by animation.exp(0.0, 1.0, 0.6, module::isEnabled) - private var openAnimation by animation.exp(1.0, 0.0, 0.6, ::isMinimized) - - private val longHovered get() = isHovered || System.currentTimeMillis() - lastHover < 80 - private var hoverAnimation by animation.exp(0.0, 1.0, { if (longHovered) 0.7 else 0.2 }, this::longHovered) - private val shrink get() = hoverAnimation.pow(3) * lerp(openAnimation, 1.0, 0.5) - - // ToDo: replace with timer - private var lastHover = 0L val backgroundRect = rectBehind(titleBar) { // base rect with lowest y to avoid children overlying onUpdate { @@ -68,8 +60,9 @@ class ModuleLayout( var progress = enableAnimation - // hover: +0.1 to alpha if minimized, -0.1 to alpha if maximized - progress += hoverAnimation * ClickGui.moduleHoverAccent * openRevSigned + // hover: +0.1 to alpha if minimized, -0.1 to alpha if maximized and enabled + progress += hoverAnimation * ClickGui.moduleHoverAccent * + lerp(enableAnimation, 1.0, openRevSigned) // +0.4 to alpha if opened and disabled progress += openAnimation * ClickGui.moduleOpenAccent * enableRev @@ -81,9 +74,7 @@ class ModuleLayout( ClickGui.moduleEnabledColor ).multAlpha(showAnimation) ) - } - onUpdate { setRadius(hoverAnimation) if (isLast && ClickGui.autoResize) { @@ -119,21 +110,18 @@ class ModuleLayout( init { backgroundTint() - isMinimized = true - openAnimation = 0.0 - - overrideX { owner.positionX + ClickGui.padding } - overrideWidth { owner.width - ClickGui.padding * 2 } + onUpdate { + positionX = owner.positionX + ClickGui.padding + width = owner.width - ClickGui.padding * 2 + } titleBar.use { - overrideHeight(ClickGui::moduleHeight) - - onMouseClick(Mouse.Button.Left,Mouse.Action.Click) { - module.toggle() + onUpdate { + height = ClickGui.moduleHeight } - textField.onUpdate { - offsetX += hoverAnimation * 2 + onMouseAction(Mouse.Button.Left) { + module.toggle() } } @@ -148,10 +136,6 @@ class ModuleLayout( cursorController.setCursor(cursor) } - onUpdate { - if (isHovered) lastHover = System.currentTimeMillis() - } - onWindowExpand { if (ClickGui.multipleSettingWindows) return@onWindowExpand @@ -180,12 +164,6 @@ class ModuleLayout( onWindowExpand { minimizeSettings() } onWindowMinimize { minimizeSettings() } content.listify() - - listOf( - titleBarBackground, - contentBackground, - outlineRect - ).forEach(Layout::destroy) } companion object { diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/SettingLayout.kt similarity index 70% rename from common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt rename to common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/SettingLayout.kt index ca3471bb7..92fe587ac 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/SettingLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/SettingLayout.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Lambda + * Copyright 2025 Lambda * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,14 +15,13 @@ * along with this program. If not, see . */ -package com.lambda.gui.impl.clickgui +package com.lambda.gui.impl.clickgui.module import com.lambda.config.AbstractSetting import com.lambda.module.modules.client.ClickGui -import com.lambda.gui.component.HAlign import com.lambda.gui.component.layout.Layout -import com.lambda.gui.component.window.AnimatedWindowChild -import com.lambda.gui.impl.clickgui.ModuleLayout.Companion.backgroundTint +import com.lambda.gui.impl.clickgui.module.ModuleLayout.Companion.backgroundTint +import com.lambda.gui.impl.clickgui.core.AnimatedChild import com.lambda.util.math.* /** @@ -32,7 +31,7 @@ abstract class SettingLayout > ( owner: Layout, val setting: T, private val expandable: Boolean = false -) : AnimatedWindowChild( +) : AnimatedChild( owner, setting.name, Vec2d.ZERO, Vec2d.ZERO, @@ -44,18 +43,25 @@ abstract class SettingLayout > ( protected val cursorController = cursorController() var settingDelegate by setting - val visible get() = setting.visibility() + val isVisible get() = setting.visibility() - override val isShown: Boolean get() = super.isShown && visible + override val isShown: Boolean get() = super.isShown && isVisible init { isMinimized = true - overrideWidth(owner::width) - titleBar.overrideHeight(ClickGui::settingsHeight) + onUpdate { + width = owner.width + } + + titleBar.onUpdate { + height = ClickGui.settingsHeight + } if (!expandable) { - overrideHeight(titleBar::height) + onUpdate { + height = titleBar.height + } content.destroy() } else { backgroundTint(true) @@ -71,19 +77,8 @@ abstract class SettingLayout > ( } } - titleBar.textField.use { - text = setting.name - textHAlignment = HAlign.LEFT - - onUpdate { - scale *= 0.92 - } + titleBar.textField.onUpdate { + scale *= 0.92 } - - listOf( - titleBarBackground, - contentBackground, - outlineRect - ).forEach(Layout::destroy) } } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/BooleanButton.kt similarity index 77% rename from common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt rename to common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/BooleanButton.kt index 212e5870b..35fa632a0 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/BooleanButton.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/BooleanButton.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Lambda + * Copyright 2025 Lambda * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,15 +15,16 @@ * along with this program. If not, see . */ -package com.lambda.gui.impl.clickgui.settings +package com.lambda.gui.impl.clickgui.module.settings import com.lambda.config.settings.comparable.BooleanSetting import com.lambda.graphics.animation.Animation.Companion.exp import com.lambda.module.modules.client.ClickGui import com.lambda.gui.component.core.FilledRect.Companion.rect +import com.lambda.gui.component.core.OutlineRect.Companion.outline import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout -import com.lambda.gui.impl.clickgui.SettingLayout +import com.lambda.gui.impl.clickgui.module.SettingLayout import com.lambda.util.Mouse import com.lambda.util.math.Rect import com.lambda.util.math.Vec2d @@ -35,7 +36,7 @@ class BooleanButton( owner: Layout, setting: BooleanSetting ) : SettingLayout(owner, setting) { - private var activeAnimation by animation.exp(0.0, 1.0, 0.6, ::settingDelegate) + private val activeAnimation by animation.exp(0.0, 1.0, 0.6, ::settingDelegate) init { val checkBox = rect { // Checkbox @@ -47,10 +48,10 @@ class BooleanButton( val h = this@BooleanButton.height rect = Rect(rb - Vec2d(h * 1.65, h), rb) - .shrink(shrink + (1.0 - showAnimation) * h * 0.2) + + .shrink(shrink + (1.0 - showAnimation)) + Vec2d.RIGHT * lerp(showAnimation, 5.0, -ClickGui.fontOffset + shrink) - setColor(Color.BLACK.setAlpha(0.25 * showAnimation)) + setColor(lerp(activeAnimation, Color.BLACK, Color.WHITE).setAlpha(0.25 * showAnimation)) shade = ClickGui.backgroundShade } @@ -76,11 +77,20 @@ class BooleanButton( shade = ClickGui.backgroundShade - setColor(Color.WHITE.setAlpha(0.25 * showAnimation)) + setColor(lerp(activeAnimation, Color.WHITE, Color.BLACK).setAlpha(lerp(activeAnimation, 0.25, 0.4) * showAnimation)) } } - onMouseClick(Mouse.Button.Left, Mouse.Action.Click) { + outline { + onUpdate { + rect = checkBox.rect + roundRadius = 100.0 + glowRadius = 5.0 + setColor(Color.BLACK.setAlpha(0.1 * showAnimation)) + } + } + + onMouseAction(Mouse.Button.Left) { setting.value = !setting.value } } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/EnumSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/EnumSlider.kt new file mode 100644 index 000000000..4b7753f88 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/EnumSlider.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.gui.impl.clickgui.module.settings + +import com.lambda.config.settings.comparable.EnumSetting +import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.layout.Layout +import com.lambda.util.math.MathUtils.floorToInt +import com.lambda.util.math.transform + +class EnumSlider >( + owner: Layout, + setting: EnumSetting +) : SettingSlider>(owner, setting) { + init { + slider.progress { + transform( + value = settingDelegate.ordinal.toDouble(), + ogStart = 0.0, ogEnd = setting.enumValues.lastIndex.toDouble(), + nStart = 0.0, nEnd = 1.0 + ) + } + + slider.onSlide { + settingDelegate = setting.enumValues.let { entries -> + entries[(it * entries.size) + .floorToInt() + .coerceIn(0, entries.size - 1)] + } + } + } + + companion object { + /** + * Creates an [EnumSlider] - visual representation of the [EnumSetting] + */ + @UIBuilder + fun > Layout.enumSetting(setting: EnumSetting) = + EnumSlider(this, setting).apply(children::add) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/NumberSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/NumberSlider.kt new file mode 100644 index 000000000..54cfc00c1 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/NumberSlider.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.gui.impl.clickgui.module.settings + +import com.lambda.config.settings.NumericSetting +import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.layout.Layout +import com.lambda.util.math.MathUtils.roundToStep +import com.lambda.util.math.MathUtils.typeConvert +import com.lambda.util.math.lerp +import com.lambda.util.math.transform + +class NumberSlider ( + owner: Layout, setting: NumericSetting +) : SettingSlider>(owner, setting) where V : Number, V : Comparable { + private val min = setting.range.start.toDouble() + private val max = setting.range.endInclusive.toDouble() + + init { + slider.progress { + transform( + settingDelegate.toDouble(), + min, max, + 0.0, 1.0 + ) + } + + slider.onSlide { + settingDelegate = settingDelegate.typeConvert( + lerp(it, min, max).roundToStep(setting.step.toDouble()) + ) + } + } + + companion object { + /** + * Creates an [NumberSlider] - visual representation of the [NumericSetting] + */ + @UIBuilder + fun Layout.numericSetting(setting: NumericSetting) where T : Number, T : Comparable = + NumberSlider(this, setting).apply(children::add) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt new file mode 100644 index 000000000..ef5e1c7d0 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.gui.impl.clickgui.module.settings + +import com.lambda.config.AbstractSetting +import com.lambda.graphics.animation.Animation.Companion.exp +import com.lambda.gui.component.HAlign +import com.lambda.gui.component.VAlign +import com.lambda.gui.component.core.TextField.Companion.textField +import com.lambda.gui.component.layout.Layout +import com.lambda.gui.impl.clickgui.core.SliderLayout.Companion.sliderBehind +import com.lambda.gui.impl.clickgui.module.SettingLayout +import com.lambda.module.modules.client.ClickGui +import com.lambda.util.math.lerp +import kotlin.math.PI +import kotlin.math.pow +import kotlin.math.sin + +abstract class SettingSlider >( + owner: Layout, setting: T +) : SettingLayout(owner, setting, false) { + private var changeAnimation by animation.exp(0.0, 1.0, 0.5) { true } + + private val sliderHeight = 3.0 + + protected val slider = sliderBehind(titleBar) { + val sl = this@SettingSlider + + onUpdate { + positionX = sl.positionX + positionY = sl.positionY + sl.height * 0.75 - sliderHeight * 0.5 + width = sl.width + height = sliderHeight + } + } + + init { + titleBar.use { + onUpdate { + height = ClickGui.settingsHeight * 1.25 + } + + textField.onUpdate { + offsetY = 0.25 * height + textVAlignment = VAlign.TOP + } + + textField { + var lastValue = "" + + onUpdate { + lastValue = text + text = settingDelegate.toString() + if (lastValue != text) changeAnimation = 0.0 + + offsetX = textField.offsetX + offsetY = textField.offsetY + + textHAlignment = HAlign.RIGHT + textVAlignment = textField.textVAlignment + shadow = textField.shadow + color = textField.color + + scale = textField.scale * lerp(changeAnimation, 1.1, 1.0) + + } + } + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/EnumSelector.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/EnumSelector.kt deleted file mode 100644 index 70c630dc4..000000000 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/EnumSelector.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.gui.impl.clickgui.settings - -import com.lambda.config.settings.comparable.EnumSetting -import com.lambda.gui.component.HAlign -import com.lambda.gui.component.core.TextField.Companion.textField -import com.lambda.gui.component.core.UIBuilder -import com.lambda.gui.component.layout.Layout -import com.lambda.gui.impl.clickgui.SettingLayout -import com.lambda.util.Mouse - -// ToDO: complete or transform to a slider -class EnumSelector >( - owner: Layout, - setting: EnumSetting -) : SettingLayout>(owner, setting, true) { - - init { - setting.enumValues.forEach { enumEntry -> - content.layout { - val base = this@EnumSelector - - overrideSize(base::width) { - base.titleBar.height * 0.8 - } - - textField { - text = enumEntry.name - textHAlignment = HAlign.CENTER - - onUpdate { - scale = base.titleBar.textField.scale - color = base.titleBar.textField.color - } - } - - onMouseClick(Mouse.Button.Left, Mouse.Action.Click) { - base.settingDelegate = enumEntry - } - } - } - - content.listify() - } - - companion object { - /** - * Creates an [EnumSelector] - visual representation of the [EnumSetting] - */ - @UIBuilder - fun > Layout.enumSetting(setting: EnumSetting) = - EnumSelector(this, setting).apply(children::add) - } -} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/UnitButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/UnitButton.kt index 96156b36b..290a5c7ea 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/UnitButton.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/UnitButton.kt @@ -20,15 +20,15 @@ package com.lambda.gui.impl.clickgui.settings import com.lambda.config.settings.FunctionSetting import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout -import com.lambda.gui.impl.clickgui.SettingLayout +import com.lambda.gui.impl.clickgui.module.SettingLayout import com.lambda.util.Mouse -class UnitButton( +class UnitButton ( owner: Layout, - setting: FunctionSetting, -) : SettingLayout<() -> Unit, FunctionSetting>(owner, setting) { + setting: FunctionSetting, +) : SettingLayout<() -> T, FunctionSetting>(owner, setting) { init { - onMouseClick(Mouse.Button.Left, Mouse.Action.Click) { + onMouse(Mouse.Button.Left, Mouse.Action.Click) { setting.value() } } @@ -38,7 +38,7 @@ class UnitButton( * Creates a [UnitButton] - visual representation of the [FunctionSetting] */ @UIBuilder - fun Layout.unitSetting(setting: FunctionSetting) = + fun Layout.unitSetting(setting: FunctionSetting) = UnitButton(this, setting).apply(children::add) } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt index dda99992b..74a15ac70 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt @@ -60,6 +60,8 @@ object ClickGui : Module( val moduleOpenAccent by setting("Module Open Accent", 0.3, 0.0..0.5, 0.01) val multipleSettingWindows by setting("Multiple Setting Windows", false) + val animationCurve by setting("List Animation Curve", AnimationCurve.Normal) + val smoothness by setting("Smoothness", 0.4, 0.3..0.7, 0.01) { animationCurve != AnimationCurve.Static } val SCREEN get() = gui("Click Gui") { rect { @@ -77,6 +79,12 @@ object ClickGui : Module( } } + enum class AnimationCurve { + Normal, + Static, + Reverse + } + init { onEnable { SCREEN.show() From 6c25f5068b6ef4d9ee30693246e23f78e2960420 Mon Sep 17 00:00:00 2001 From: Constructor Date: Mon, 10 Mar 2025 00:40:31 +0100 Subject: [PATCH 104/114] Fix floating point errors for float settings --- .../com/lambda/graphics/animation/Animation.kt | 8 ++++---- .../clickgui/module/settings/NumberSlider.kt | 2 +- .../clickgui/module/settings/SettingSlider.kt | 2 +- .../lambda/module/modules/client/GuiSettings.kt | 2 +- .../kotlin/com/lambda/util/math/MathUtils.kt | 17 +++++++++-------- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt b/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt index 5fa588b76..01755d7ea 100644 --- a/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt +++ b/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt @@ -57,11 +57,11 @@ class Animation(initialValue: Double, val update: (Double) -> Double) { fun AnimationTicker.exp(min: () -> Double, max: () -> Double, speed: () -> Double, flag: () -> Boolean) = Animation(min()) { - val min = min() - val max = max() - val target = if (flag()) max else min + val minVal = min() + val maxVal = max() + val target = if (flag()) maxVal else minVal - if (abs(target - it) < CLAMP * abs(max - min)) target + if (abs(target - it) < CLAMP * abs(maxVal - minVal)) target else lerp(speed(), it, target) }.apply(::register) diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/NumberSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/NumberSlider.kt index 54cfc00c1..a7f910646 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/NumberSlider.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/NumberSlider.kt @@ -42,7 +42,7 @@ class NumberSlider ( slider.onSlide { settingDelegate = settingDelegate.typeConvert( - lerp(it, min, max).roundToStep(setting.step.toDouble()) + lerp(it, min, max).roundToStep(setting.step).toDouble() ) } } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt index ef5e1c7d0..07dd5b661 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt @@ -61,7 +61,7 @@ abstract class SettingSlider >( } textField { - var lastValue = "" + var lastValue: String onUpdate { lastValue = text diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt index d242829a7..449073c28 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt @@ -48,7 +48,7 @@ object GuiSettings : Module( val shadeBackground by setting("Shade Background", true, visibility = { page == Page.Colors }) val colorWidth by setting("Shade Width", 200.0, 10.0..1000.0, 10.0, visibility = { page == Page.Colors }) val colorHeight by setting("Shade Height", 200.0, 10.0..1000.0, 10.0, visibility = { page == Page.Colors }) - val colorSpeed by setting("Color Speed", 1.0, 0.1..10.0, 0.1, visibility = { page == Page.Colors }) + val colorSpeed by setting("Color Speed", 1.0, 0.1..5.0, 0.1, visibility = { page == Page.Colors }) val mainColor: Color get() = if (shade) Color.WHITE else primaryColor diff --git a/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt b/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt index f6a889191..1e0489d09 100644 --- a/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt @@ -40,14 +40,15 @@ object MathUtils { fun Double.ceilToInt() = ceil(this).toInt() fun T.roundToStep(step: T): T { - val stepD = step.toDouble() - if (stepD == 0.0) return this - - var value = round(toDouble() / stepD) * stepD - value = value.roundToPlaces(stepD.decimals) - if (abs(value) == 0.0) value = 0.0 - - return typeConvert(value) + val valueBD = BigDecimal(toString()) + val stepBD = BigDecimal(step.toString()) + if (stepBD.compareTo(BigDecimal.ZERO) == 0) return this + val scaled = valueBD.divide(stepBD, stepBD.scale(), RoundingMode.HALF_UP) + .setScale(0, RoundingMode.HALF_UP) + .multiply(stepBD) + .setScale(stepBD.scale(), RoundingMode.HALF_UP) + + return typeConvert(scaled.toDouble()) } private fun Double.roundToPlaces(places: Int) = From bb7ff6b43a8947f6534d385e135b0d5e590d06ce Mon Sep 17 00:00:00 2001 From: Constructor Date: Mon, 10 Mar 2025 00:41:39 +0100 Subject: [PATCH 105/114] Remove unused functions --- common/src/main/kotlin/com/lambda/util/math/MathUtils.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt b/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt index 1e0489d09..c3a7d7bcd 100644 --- a/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt @@ -51,12 +51,6 @@ object MathUtils { return typeConvert(scaled.toDouble()) } - private fun Double.roundToPlaces(places: Int) = - BigDecimal(this).setScale(places, RoundingMode.HALF_EVEN).toDouble() - - private val Double.decimals: Int - get() = BigDecimal.valueOf(this).scale() - fun T.typeConvert(valueIn: Double): T { @Suppress("UNCHECKED_CAST") return when (this) { From 1fa3d04296e4a940f6d902ba14366925acaf638e Mon Sep 17 00:00:00 2001 From: "blade.kt" Date: Mon, 10 Mar 2025 10:00:27 +0300 Subject: [PATCH 106/114] SDF settings --- .../lambda/graphics/renderer/gui/font/FontRenderer.kt | 10 +++++----- .../com/lambda/gui/impl/clickgui/core/SliderLayout.kt | 9 ++++++--- .../gui/impl/clickgui/module/settings/SettingSlider.kt | 6 +----- .../com/lambda/module/modules/client/RenderSettings.kt | 2 ++ 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt index 0be4a57df..16fe5d96a 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt @@ -42,7 +42,7 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ private val emojis get() = RenderSettings.emojiFont private val shadowShift get() = RenderSettings.shadowShift * 10.0 - private val baselineOffset get() = RenderSettings.baselineOffset * 2.0f - 10f + private val baselineOffset get() = RenderSettings.baselineOffset * 2.0f - 16f private val gap get() = RenderSettings.gap * 0.5f - 0.8f /** @@ -65,8 +65,8 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ ) = render { shader["u_FontTexture"] = 0 shader["u_EmojiTexture"] = 1 - shader["u_SDFMin"] = 0.4 - shader["u_SDFMax"] = 1.0 + shader["u_SDFMin"] = RenderSettings.sdfMin + shader["u_SDFMax"] = RenderSettings.sdfMax bind(chars, emojis) @@ -91,8 +91,8 @@ object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/ ) = render { shader["u_FontTexture"] = 0 shader["u_EmojiTexture"] = 1 - shader["u_SDFMin"] = 0.4 - shader["u_SDFMax"] = 1.0 + shader["u_SDFMin"] = RenderSettings.sdfMin + shader["u_SDFMax"] = RenderSettings.sdfMax bind(chars, emojis) diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/SliderLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/SliderLayout.kt index 15e53488c..55464f924 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/SliderLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/SliderLayout.kt @@ -28,6 +28,7 @@ import com.lambda.gui.impl.clickgui.module.settings.SettingSlider import com.lambda.module.modules.client.ClickGui import com.lambda.util.Mouse import com.lambda.util.math.Vec2d +import com.lambda.util.math.multAlpha import com.lambda.util.math.setAlpha import com.lambda.util.math.transform import java.awt.Color @@ -89,7 +90,7 @@ class SliderLayout( setProgressBlock(dragProgress) } - val progress = rect { + rect { onUpdate { // progress rect = bg.rect width *= renderProgress @@ -102,8 +103,10 @@ class SliderLayout( outline { onUpdate { - rect = progress.rect - setColor(bg.leftTopColor) + rect = bg.rect + val c = Color.BLACK.setAlpha(0.3 * showAnim) + val a = transform(renderProgress, 0.5, 1.0, 0.0, 1.0).coerceIn(0.0, 1.0) + setColorH(c, c.multAlpha(a)) roundRadius = 100.0 } } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt index ef5e1c7d0..5baa0e6be 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt @@ -27,9 +27,6 @@ import com.lambda.gui.impl.clickgui.core.SliderLayout.Companion.sliderBehind import com.lambda.gui.impl.clickgui.module.SettingLayout import com.lambda.module.modules.client.ClickGui import com.lambda.util.math.lerp -import kotlin.math.PI -import kotlin.math.pow -import kotlin.math.sin abstract class SettingSlider >( owner: Layout, setting: T @@ -61,7 +58,7 @@ abstract class SettingSlider >( } textField { - var lastValue = "" + var lastValue: String onUpdate { lastValue = text @@ -77,7 +74,6 @@ abstract class SettingSlider >( color = textField.color scale = textField.scale * lerp(changeAnimation, 1.1, 1.0) - } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt index 383b27813..dca5075df 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt @@ -39,6 +39,8 @@ object RenderSettings : Module( val gap by setting("Gap", 1.5, -10.0..10.0, 0.5) { page == Page.Font } val baselineOffset by setting("Vertical Offset", 0.0, -10.0..10.0, 0.5) { page == Page.Font } val highlightColor by setting("Text Highlight Color", Color(214, 55, 87), visibility = { page == Page.Font }) + val sdfMin by setting("SDF Min", 0.4, 0.0..1.0, 0.01, visibility = { page == Page.Font }) + val sdfMax by setting("SDF Max", 1.0, 0.0..1.0, 0.01, visibility = { page == Page.Font }) // ESP val uploadsPerTick by setting("Uploads", 16, 1..256, 1, unit = " chunk/tick") { page == Page.ESP } From 803e630fbee0b46ffd15b194a6620203fcd738de Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 11 Mar 2025 02:53:55 +0100 Subject: [PATCH 107/114] Show units for numeric settings --- .../gui/impl/clickgui/module/settings/EnumSlider.kt | 3 +++ .../gui/impl/clickgui/module/settings/NumberSlider.kt | 3 +++ .../gui/impl/clickgui/module/settings/SettingSlider.kt | 4 +++- .../com/lambda/module/modules/client/RenderSettings.kt | 8 ++++---- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/EnumSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/EnumSlider.kt index 4b7753f88..e6ae47828 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/EnumSlider.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/EnumSlider.kt @@ -27,6 +27,9 @@ class EnumSlider >( owner: Layout, setting: EnumSetting ) : SettingSlider>(owner, setting) { + override val settingValue: String + get() = settingDelegate.name + init { slider.progress { transform( diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/NumberSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/NumberSlider.kt index a7f910646..6999a7950 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/NumberSlider.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/NumberSlider.kt @@ -31,6 +31,9 @@ class NumberSlider ( private val min = setting.range.start.toDouble() private val max = setting.range.endInclusive.toDouble() + override val settingValue: String + get() = "${setting.value}${setting.unit}" + init { slider.progress { transform( diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt index 5baa0e6be..2e4e4ec70 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt @@ -31,6 +31,8 @@ import com.lambda.util.math.lerp abstract class SettingSlider >( owner: Layout, setting: T ) : SettingLayout(owner, setting, false) { + abstract val settingValue: String + private var changeAnimation by animation.exp(0.0, 1.0, 0.5) { true } private val sliderHeight = 3.0 @@ -62,7 +64,7 @@ abstract class SettingSlider >( onUpdate { lastValue = text - text = settingDelegate.toString() + text = settingValue if (lastValue != text) changeAnimation = 0.0 offsetX = textField.offsetX diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt index dca5075df..08936c229 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt @@ -31,8 +31,8 @@ object RenderSettings : Module( private val page by setting("Page", Page.Font) // Font - val textFont by setting("Text Font", LambdaFont.FiraSansRegular) - val emojiFont by setting("Emoji Font", LambdaEmoji.Twemoji) + val textFont by setting("Text Font", LambdaFont.FiraSansRegular) { page == Page.Font } + val emojiFont by setting("Emoji Font", LambdaEmoji.Twemoji) { page == Page.Font } val shadow by setting("Shadow", true) { page == Page.Font } val shadowBrightness by setting("Shadow Brightness", 0.35, 0.0..0.5, 0.01) { page == Page.Font && shadow } val shadowShift by setting("Shadow Shift", 1.0, 0.0..2.0, 0.05) { page == Page.Font && shadow } @@ -43,8 +43,8 @@ object RenderSettings : Module( val sdfMax by setting("SDF Max", 1.0, 0.0..1.0, 0.01, visibility = { page == Page.Font }) // ESP - val uploadsPerTick by setting("Uploads", 16, 1..256, 1, unit = " chunk/tick") { page == Page.ESP } - val rebuildsPerTick by setting("Rebuilds", 64, 1..256, 1, unit = " chunk/tick") { page == Page.ESP } + val uploadsPerTick by setting("Uploads", 16, 1..256, 1, unit = " chunks/tick") { page == Page.ESP } + val rebuildsPerTick by setting("Rebuilds", 64, 1..256, 1, unit = " chunks/tick") { page == Page.ESP } val updateFrequency by setting("Update Frequency", 2, 1..10, 1, "Frequency of block updates", unit = " ticks") { page == Page.ESP } val outlineWidth by setting("Outline Width", 1.0, 0.1..5.0, 0.1, "Width of block outlines", unit = "px") { page == Page.ESP } From 87ecb9cae29dfeabb4ad04367baebaa1131c2a99 Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 11 Mar 2025 03:06:49 +0100 Subject: [PATCH 108/114] Module List --- .../main/kotlin/com/lambda/module/Module.kt | 5 ++- .../com/lambda/module/hud/ModuleList.kt | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/module/hud/ModuleList.kt diff --git a/common/src/main/kotlin/com/lambda/module/Module.kt b/common/src/main/kotlin/com/lambda/module/Module.kt index 4dd2f570a..48a42e148 100644 --- a/common/src/main/kotlin/com/lambda/module/Module.kt +++ b/common/src/main/kotlin/com/lambda/module/Module.kt @@ -31,6 +31,7 @@ import com.lambda.event.listener.Listener import com.lambda.event.listener.SafeListener import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.event.listener.UnsafeListener +import com.lambda.module.hud.ModuleList import com.lambda.module.tag.ModuleTag import com.lambda.sound.LambdaSound import com.lambda.sound.SoundManager.playSoundRandomly @@ -116,9 +117,9 @@ abstract class Module( ) : Nameable, Muteable, Configurable(ModuleConfig) { private val isEnabledSetting = setting("Enabled", enabledByDefault, visibility = { false }) private val keybindSetting = setting("Keybind", defaultKeybind) - private val isVisible = setting("Visible", true) + val isVisible = setting("Visible", true) { ModuleList.isEnabled } val reset by setting("Reset", { settings.forEach { it.reset() }; this@Module.info("Settings set to default") }) - val customTags = setting("Tags", setOf(), visibility = { false }) + val customTags = setting("Tags", setOf()) { false } var isEnabled by isEnabledSetting val isDisabled get() = !isEnabled diff --git a/common/src/main/kotlin/com/lambda/module/hud/ModuleList.kt b/common/src/main/kotlin/com/lambda/module/hud/ModuleList.kt new file mode 100644 index 000000000..30e443620 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/hud/ModuleList.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.module.hud + +import com.lambda.graphics.renderer.gui.font.FontRenderer.drawString +import com.lambda.module.HudModule +import com.lambda.module.ModuleRegistry +import com.lambda.module.tag.ModuleTag +import com.lambda.task.TaskFlow +import com.lambda.util.math.Vec2d + +object ModuleList : HudModule( + name = "ModuleList", + defaultTags = setOf(ModuleTag.CLIENT), +) { + override val width = 200.0 + override val height = 200.0 + + init { + onRender { + val enabled = ModuleRegistry.modules + .filter { it.isEnabled } + .filter { it.isVisible.value } + drawString(enabled.joinToString("\n") { "${it.name} [${it.keybind.name}]" }, Vec2d.ZERO) + } + } +} From 20c4aac415a6bf82c478694cb6c12a12ecfceb3e Mon Sep 17 00:00:00 2001 From: "blade.kt" Date: Tue, 11 Mar 2025 16:15:38 +0300 Subject: [PATCH 109/114] Fixed sdf for emojis --- .../graphics/renderer/gui/font/core/LambdaAtlas.kt | 3 ++- .../assets/lambda/shaders/fragment/font/font.frag | 12 ++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt index 2eaf82985..b0d8b0784 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt @@ -136,7 +136,8 @@ object LambdaAtlas : Loadable { val uv1 = Vec2d(x, y) * texelSize val uv2 = Vec2d(x, y).plus(size) * texelSize - constructed[name] = GlyphInfo(size, -uv1, -uv2) + val normalized = 128.0 / size.y + constructed[name] = GlyphInfo(size * normalized, -uv1, -uv2) x += emoji.width + 2 } diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag b/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag index 54dffb8e1..37aa5a897 100644 --- a/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag +++ b/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag @@ -14,20 +14,12 @@ float sdf(float channel) { return 1.0 - smoothstep(u_SDFMin, u_SDFMax, 1.0 - channel); } -vec4 sdf(vec4 texture) { - return vec4( - sdf(texture.r), - sdf(texture.g), - sdf(texture.b), - sdf(texture.a) - ); -} - void main() { bool isEmoji = v_TexCoord.x < 0.0; if (isEmoji) { - color = sdf(texture(u_EmojiTexture, -v_TexCoord)) * v_Color; + vec4 c = texture(u_EmojiTexture, -v_TexCoord); + color = vec4(c.rgb, sdf(c.a)) * v_Color; return; } From 3b558209bcde9fc2b8e4de280163739e3240a61f Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 11 Mar 2025 21:10:16 +0100 Subject: [PATCH 110/114] Remove unused access widener --- common/src/main/resources/lambda.accesswidener | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/main/resources/lambda.accesswidener b/common/src/main/resources/lambda.accesswidener index d608c95f2..9e33b952c 100644 --- a/common/src/main/resources/lambda.accesswidener +++ b/common/src/main/resources/lambda.accesswidener @@ -28,7 +28,6 @@ accessible field net/minecraft/client/network/AbstractClientPlayerEntity playerL accessible field net/minecraft/entity/LivingEntity jumpingCooldown I accessible field net/minecraft/entity/Entity pos Lnet/minecraft/util/math/Vec3d; accessible field net/minecraft/client/network/ClientPlayerInteractionManager lastSelectedSlot I -accessible method net/minecraft/entity/Entity isAlwaysInvulnerableTo (Lnet/minecraft/entity/damage/DamageSource;)Z accessible method net/minecraft/entity/LivingEntity modifyAppliedDamage (Lnet/minecraft/entity/damage/DamageSource;F)F accessible method net/minecraft/entity/LivingEntity applyArmorToDamage (Lnet/minecraft/entity/damage/DamageSource;F)F From 0083d0e5ea31b24f072b14bee80fb1507fa7b90b Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 11 Mar 2025 21:10:41 +0100 Subject: [PATCH 111/114] Disable chat verification toast --- .../mixin/network/ClientPlayNetworkHandlerMixin.java | 7 +++++++ .../kotlin/com/lambda/module/modules/render/NoRender.kt | 3 +++ 2 files changed, 10 insertions(+) diff --git a/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java b/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java index 3db1ca635..81d615c4d 100644 --- a/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java +++ b/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java @@ -19,12 +19,14 @@ import com.lambda.event.EventFlow; import com.lambda.event.events.InventoryEvent; +import com.lambda.module.modules.render.NoRender; import net.minecraft.client.network.ClientPlayNetworkHandler; import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; import net.minecraft.network.packet.s2c.play.UpdateSelectedSlotS2CPacket; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(ClientPlayNetworkHandler.class) @@ -38,4 +40,9 @@ private void onUpdateSelectedSlot(UpdateSelectedSlotS2CPacket packet, CallbackIn private void onScreenHandlerSlotUpdate(ScreenHandlerSlotUpdateS2CPacket packet, CallbackInfo ci) { EventFlow.post(new InventoryEvent.SlotUpdate(packet.getSyncId(), packet.getRevision(), packet.getSlot(), packet.getStack())); } + + @Redirect(method = "onServerMetadata", at = @At(value = "FIELD", target = "Lnet/minecraft/client/network/ClientPlayNetworkHandler;displayedUnsecureChatWarning:Z", ordinal = 0)) + public boolean onServerMetadata(ClientPlayNetworkHandler clientPlayNetworkHandler) { + return NoRender.getNoChatVerificationToast(); + } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt b/common/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt index 35c810b4f..b1a38d5c5 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt @@ -39,4 +39,7 @@ object NoRender : Module( @JvmStatic val noInWall by setting("No In Wall Overlay", true) + + @JvmStatic + val noChatVerificationToast by setting("No Chat Verification Toast", true) } From 644e3148908a9b00c25f94fc9154e3888cc89ef2 Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 11 Mar 2025 21:10:50 +0100 Subject: [PATCH 112/114] Close UI on keybind --- .../kotlin/com/lambda/module/modules/client/ClickGui.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt index 74a15ac70..f3db6b239 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt @@ -17,11 +17,13 @@ package com.lambda.module.modules.client +import com.lambda.Lambda.mc import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.gui.ScreenLayout.Companion.gui import com.lambda.gui.component.core.FilledRect.Companion.rect import com.lambda.gui.impl.clickgui.ModuleWindow.Companion.moduleWindow +import com.lambda.util.KeyCode import com.lambda.util.math.Vec2d import com.lambda.util.math.setAlpha import java.awt.Color @@ -64,6 +66,11 @@ object ClickGui : Module( val smoothness by setting("Smoothness", 0.4, 0.3..0.7, 0.01) { animationCurve != AnimationCurve.Static } val SCREEN get() = gui("Click Gui") { + onKeyPress { + if (it.keyCode != keybind.keyCode || keybind == KeyCode.UNBOUND) return@onKeyPress + mc.currentScreen?.close() + } + rect { onUpdate { rect = owner!!.rect From 8cb2c81633bb849fcb18852b656bd11278758493 Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 11 Mar 2025 22:15:38 +0100 Subject: [PATCH 113/114] Add KeybindPicker --- .../main/kotlin/com/lambda/gui/GuiManager.kt | 6 +++ .../clickgui/module/settings/KeybindPicker.kt | 49 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/KeybindPicker.kt diff --git a/common/src/main/kotlin/com/lambda/gui/GuiManager.kt b/common/src/main/kotlin/com/lambda/gui/GuiManager.kt index d39796db5..822a8e3f2 100644 --- a/common/src/main/kotlin/com/lambda/gui/GuiManager.kt +++ b/common/src/main/kotlin/com/lambda/gui/GuiManager.kt @@ -21,11 +21,13 @@ import com.lambda.config.settings.NumericSetting import com.lambda.config.settings.comparable.BooleanSetting import com.lambda.config.settings.comparable.EnumSetting import com.lambda.config.settings.FunctionSetting +import com.lambda.config.settings.complex.KeyBindSetting import com.lambda.core.Loadable import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout import com.lambda.gui.impl.clickgui.module.settings.BooleanButton.Companion.booleanSetting import com.lambda.gui.impl.clickgui.module.settings.EnumSlider.Companion.enumSetting +import com.lambda.gui.impl.clickgui.module.settings.KeybindPicker.Companion.keybindSetting import com.lambda.gui.impl.clickgui.module.settings.NumberSlider.Companion.numericSetting import com.lambda.gui.impl.clickgui.settings.UnitButton.Companion.unitSetting import kotlin.reflect.KClass @@ -54,6 +56,10 @@ object GuiManager : Loadable { owner.numericSetting(ref) } + typeAdapter { owner, ref -> + owner.keybindSetting(ref) + } + return "Loaded ${typeMap.size} gui type adapters." } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/KeybindPicker.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/KeybindPicker.kt new file mode 100644 index 000000000..a75dd64f8 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/KeybindPicker.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.gui.impl.clickgui.module.settings + +import com.lambda.config.settings.complex.KeyBindSetting +import com.lambda.gui.component.HAlign +import com.lambda.gui.component.core.TextField.Companion.textField +import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.layout.Layout +import com.lambda.gui.impl.clickgui.module.SettingLayout +import com.lambda.util.KeyCode +import com.lambda.util.extension.displayValue + +class KeybindPicker( + owner: Layout, + setting: KeyBindSetting +) : SettingLayout(owner, setting) { + + init { + textField { + text = setting.value.displayValue + textHAlignment = HAlign.RIGHT + } + } + + companion object { + /** + * Creates a [KeybindPicker] - visual representation of the [KeyBindSetting] + */ + @UIBuilder + fun Layout.keybindSetting(setting: KeyBindSetting) = + KeybindPicker(this, setting).apply(children::add) + } +} From f1924cf7eb9e283acb4c2cd918624de2395719c5 Mon Sep 17 00:00:00 2001 From: "blade.kt" Date: Wed, 12 Mar 2025 03:00:31 +0300 Subject: [PATCH 114/114] Keybind setting picker --- .../lambda/gui/component/core/TextField.kt | 12 ++++++++ .../gui/impl/clickgui/module/SettingLayout.kt | 12 ++------ .../clickgui/module/settings/KeybindPicker.kt | 30 +++++++++++++++++-- .../clickgui/module/settings/SettingSlider.kt | 12 ++------ 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt index a1d75cfe3..0f871695a 100644 --- a/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt +++ b/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt @@ -42,6 +42,18 @@ class TextField( val textWidth get() = FontRenderer.getWidth(text, scale) val textHeight get() = FontRenderer.getHeight(scale) + fun mergeFrom(other: TextField) { + text = other.text + color = other.color + scale = other.scale + shadow = other.shadow + + textHAlignment = HAlign.RIGHT + textVAlignment = other.textVAlignment + offsetX = other.offsetX + offsetY = other.offsetY + } + init { properties.interactionPassthrough = true diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/SettingLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/SettingLayout.kt index 92fe587ac..20183318b 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/SettingLayout.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/SettingLayout.kt @@ -47,6 +47,8 @@ abstract class SettingLayout > ( override val isShown: Boolean get() = super.isShown && isVisible + + init { isMinimized = true @@ -65,16 +67,6 @@ abstract class SettingLayout > ( content.destroy() } else { backgroundTint(true) - - // Minimize other expandable settings when this one gets opened - onWindowExpand { - owner.children - .filterIsInstance>() - .filter { it.expandable } - .forEach { - if (it != this) it.isMinimized = true - } - } } titleBar.textField.onUpdate { diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/KeybindPicker.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/KeybindPicker.kt index a75dd64f8..ba786a44a 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/KeybindPicker.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/KeybindPicker.kt @@ -22,19 +22,45 @@ import com.lambda.gui.component.HAlign import com.lambda.gui.component.core.TextField.Companion.textField import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout +import com.lambda.gui.impl.clickgui.module.ModuleLayout import com.lambda.gui.impl.clickgui.module.SettingLayout import com.lambda.util.KeyCode +import com.lambda.util.Mouse import com.lambda.util.extension.displayValue class KeybindPicker( owner: Layout, setting: KeyBindSetting ) : SettingLayout(owner, setting) { + private var isListening = false init { textField { - text = setting.value.displayValue - textHAlignment = HAlign.RIGHT + onUpdate { + mergeFrom(titleBar.textField) + text = if (isListening) "..." else setting.value.displayValue + textHAlignment = HAlign.RIGHT + } + } + + onMouseAction(Mouse.Button.Left) { + isListening = !isListening + } + + onKeyPress { key -> + if (!isListening) return@onKeyPress + + settingDelegate = key + isListening = false + } + + onShow { + isListening = false + } + + onTick { + val module = (owner.owner as? ModuleLayout) ?: return@onTick + if (module.isMinimized) isListening = false } } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt index 2e4e4ec70..7b43339d7 100644 --- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt @@ -64,18 +64,12 @@ abstract class SettingSlider >( onUpdate { lastValue = text + mergeFrom(textField) text = settingValue - if (lastValue != text) changeAnimation = 0.0 - - offsetX = textField.offsetX - offsetY = textField.offsetY + if (lastValue != text) changeAnimation = 0.0 textHAlignment = HAlign.RIGHT - textVAlignment = textField.textVAlignment - shadow = textField.shadow - color = textField.color - - scale = textField.scale * lerp(changeAnimation, 1.1, 1.0) + scale *= lerp(changeAnimation, 1.1, 1.0) } } }