From e89649ca25ac5408e4e0f0f5f80b3d88e7fc280a Mon Sep 17 00:00:00 2001
From: beanbag44 <107891830+beanbag44@users.noreply.github.com>
Date: Wed, 29 Jul 2026 16:24:31 +0100
Subject: [PATCH 1/9] changes
---
build.gradle.kts | 23 ++
gradle.properties | 1 +
settings.gradle.kts | 1 +
.../lambda/gui/components/ClickGuiLayout.kt | 8 +-
.../com/lambda/newui/ClickGuiContent.kt | 54 ++++
.../com/lambda/newui/ComposeClickGui.kt | 81 +++++
.../com/lambda/newui/ComposeRenderer.kt | 301 ++++++++++++++++++
.../kotlin/com/lambda/newui/ComposeScreen.kt | 92 ++++++
.../lambda/newui/components/CategoryPanel.kt | 83 +++++
.../com/lambda/newui/components/ModuleCard.kt | 85 +++++
.../com/lambda/newui/theme/LambdaTheme.kt | 89 ++++++
11 files changed, 814 insertions(+), 4 deletions(-)
create mode 100644 src/main/kotlin/com/lambda/newui/ClickGuiContent.kt
create mode 100644 src/main/kotlin/com/lambda/newui/ComposeClickGui.kt
create mode 100644 src/main/kotlin/com/lambda/newui/ComposeRenderer.kt
create mode 100644 src/main/kotlin/com/lambda/newui/ComposeScreen.kt
create mode 100644 src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt
create mode 100644 src/main/kotlin/com/lambda/newui/components/ModuleCard.kt
create mode 100644 src/main/kotlin/com/lambda/newui/theme/LambdaTheme.kt
diff --git a/build.gradle.kts b/build.gradle.kts
index d33ca031d..a4a726d0c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -37,6 +37,7 @@ val lwjglVersion: String by project
val sodiumVersion: String by project
val litematicaVersion: String by project
val maLiLibVersion: String by project
+val composeVersion: String by project
val libs = file("libs")
val targets = listOf("fabric.mod.json")
@@ -46,6 +47,7 @@ val replacements = file("gradle.properties").inputStream().use { stream ->
plugins {
kotlin("jvm") version "2.3.0"
+ kotlin("plugin.compose") version "2.3.0"
id("org.jetbrains.dokka") version "2.1.0"
id("fabric-loom") version "1.16-SNAPSHOT"
id("com.gradleup.shadow") version "9.3.0"
@@ -72,6 +74,8 @@ repositories {
maven("https://maven.2b2t.vc/releases") // Baritone
maven("https://jitpack.io") // KDiscordIPC
maven("https://api.modrinth.com/maven")
+ maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") // Compose Multiplatform
+ google() // AndroidX dependencies (transitive from Compose)
mavenCentral()
// Allow the use of local libraries
@@ -187,6 +191,25 @@ dependencies {
modCompileOnly("maven.modrinth:malilib:$maLiLibVersion")
modCompileOnly("maven.modrinth:litematica:$litematicaVersion")
+ // Compose Multiplatform (alternative GUI)
+ shadowLib("org.jetbrains.compose.runtime:runtime-desktop:$composeVersion") {
+ exclude(group = "org.jetbrains.kotlin")
+ exclude(group = "org.jetbrains.kotlinx")
+ }
+ shadowLib("org.jetbrains.compose.ui:ui-desktop:$composeVersion") {
+ exclude(group = "org.jetbrains.kotlin")
+ exclude(group = "org.jetbrains.kotlinx")
+ }
+ shadowLib("org.jetbrains.compose.foundation:foundation-desktop:$composeVersion") {
+ exclude(group = "org.jetbrains.kotlin")
+ exclude(group = "org.jetbrains.kotlinx")
+ }
+ shadowLib("org.jetbrains.compose.material3:material3-desktop:$composeVersion") {
+ exclude(group = "org.jetbrains.kotlin")
+ exclude(group = "org.jetbrains.kotlinx")
+ }
+ shadowLib("org.jetbrains.skiko:skiko-awt-runtime-windows-x64:0.9.4") // Skiko native library required by Compose scene
+
// DevLogin
modRuntimeOnly("com.ptsmods:devlogin:3.5")
diff --git a/gradle.properties b/gradle.properties
index 71b45c1cf..7defe22ce 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -38,6 +38,7 @@ ktorVersion=3.3.3
jacksonVersion=3.1.1
mockkVersion=1.14.7
spairVersion=1.90.0
+composeVersion=1.8.0
# Fabric https://fabricmc.net/develop/
fabricLoaderVersion=0.18.3
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 1a052703d..fafcae91d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -21,6 +21,7 @@ pluginManagement {
repositories {
maven("https://maven.fabricmc.net/")
maven("https://jitpack.io")
+ maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
mavenCentral()
gradlePluginPortal()
}
diff --git a/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt b/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt
index 156fb6062..5b02e3577 100644
--- a/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt
+++ b/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt
@@ -26,7 +26,6 @@ import com.lambda.config.settings.complex.KeybindSetting.Companion.onPressUnsafe
import com.lambda.core.Loadable
import com.lambda.event.events.GuiEvent
import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
-import com.lambda.gui.DearImGui
import com.lambda.gui.LambdaScreen
import com.lambda.gui.MenuBar
import com.lambda.gui.MenuBar.buildMenuBar
@@ -49,6 +48,7 @@ import com.lambda.module.modules.client.Client
import com.lambda.module.modules.combat.autodisconnect.AutoDisconnectScreen
import com.lambda.module.tag.ModuleTag
import com.lambda.module.tag.ModuleTag.Companion.shownTags
+import com.lambda.newui.ComposeClickGui
import com.lambda.sound.LambdaSound
import com.lambda.sound.SoundHandler.play
import com.lambda.util.Describable
@@ -75,11 +75,11 @@ object ClickGuiLayout : Loadable, Config(
var developerMode = false
// onPressUnsafe (not onPress) so the GUI can also be toggled from menu screens
// (title, multiplayer, world-select) where there is no SafeContext, not just in-game.
+ // Temporarily delegates to ComposeClickGui instead of the ImGui toggle.
val keybind by setting("Keybind", KeyCode.Y, screenCheck = false)
.onPressUnsafe {
- if (DearImGui.io.wantTextInput) return@onPressUnsafe
- if (!open && !canOpenOver(mc.currentScreen)) return@onPressUnsafe
- toggle()
+ if (!ComposeClickGui.open && !canOpenOver(mc.currentScreen)) return@onPressUnsafe
+ ComposeClickGui.toggle()
}
/**
diff --git a/src/main/kotlin/com/lambda/newui/ClickGuiContent.kt b/src/main/kotlin/com/lambda/newui/ClickGuiContent.kt
new file mode 100644
index 000000000..b548d3dea
--- /dev/null
+++ b/src/main/kotlin/com/lambda/newui/ClickGuiContent.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2026 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.newui
+
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.lambda.module.tag.ModuleTag
+import com.lambda.newui.components.CategoryPanel
+import com.lambda.newui.theme.LambdaTheme
+
+/**
+ * Top-level composable for the Lambda click GUI.
+ *
+ * Renders a horizontally scrollable row of [CategoryPanel]s — one per [ModuleTag] in the
+ * default tag set. Each panel lists its modules as clickable toggle cards.
+ */
+@Composable
+fun ClickGuiContent() {
+ LambdaTheme {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(20.dp)
+ .horizontalScroll(rememberScrollState())
+ ) {
+ ModuleTag.shownTags.forEach { tag ->
+ CategoryPanel(tag)
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/com/lambda/newui/ComposeClickGui.kt b/src/main/kotlin/com/lambda/newui/ComposeClickGui.kt
new file mode 100644
index 000000000..e4bb941da
--- /dev/null
+++ b/src/main/kotlin/com/lambda/newui/ComposeClickGui.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2026 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.newui
+
+import com.lambda.Lambda.mc
+import com.lambda.core.Loadable
+import com.lambda.event.events.GuiEvent
+import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
+import com.lambda.gui.OverlayBackgroundScreen
+import com.lambda.gui.components.ClickGuiLayout
+import com.lambda.module.modules.client.Client
+import com.lambda.sound.LambdaSound
+import com.lambda.sound.SoundHandler.play
+
+/**
+ * Orchestrator for the Compose-based click GUI.
+ *
+ * Manages the open/close lifecycle, hooks into the render pipeline via [GuiEvent.EndImguiFrame],
+ * and delegates rendering to [ComposeRenderer].
+ *
+ * Implements [Loadable] so it is auto-discovered by [com.lambda.core.Loader].
+ */
+object ComposeClickGui : Loadable {
+ var open = false
+ private set
+
+ override fun load(): String {
+ return "Loaded ComposeClickGui"
+ }
+
+ init {
+ // Render the Compose GUI after ImGui finishes (MC framebuffer is bound at this point).
+ listenUnsafe {
+ if (!open) return@listenUnsafe
+ ComposeRenderer.render()
+ }
+ }
+
+ /**
+ * Toggle the Compose click GUI open/closed.
+ * Uses the same screen-check logic as [ClickGuiLayout.toggle].
+ */
+ fun toggle() {
+ if (open) {
+ // ComposeScreen.close() restores the background screen and triggers
+ // removed() -> close(), which flips `open` off and plays the sound.
+ ComposeScreen.close()
+ } else {
+ val current = mc.currentScreen
+ if (current is ComposeScreen) return
+ if (Client.clientSounds) LambdaSound.ModuleOn.play()
+
+ ComposeRenderer.initialize()
+
+ ComposeScreen.parentScreen = if (current is ComposeScreen) null else current
+ (current as? OverlayBackgroundScreen)?.onOverlaidByGui()
+ mc.setScreen(ComposeScreen)
+ open = true
+ }
+ }
+
+ fun close() {
+ if (Client.clientSounds) LambdaSound.ModuleOff.play()
+ open = false
+ }
+}
diff --git a/src/main/kotlin/com/lambda/newui/ComposeRenderer.kt b/src/main/kotlin/com/lambda/newui/ComposeRenderer.kt
new file mode 100644
index 000000000..52d112e8f
--- /dev/null
+++ b/src/main/kotlin/com/lambda/newui/ComposeRenderer.kt
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2026 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.newui
+
+import androidx.compose.ui.InternalComposeUiApi
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.asComposeCanvas
+import androidx.compose.ui.input.pointer.PointerButton
+import androidx.compose.ui.input.pointer.PointerButtons
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.scene.CanvasLayersComposeScene
+import androidx.compose.ui.scene.ComposeScene
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import com.lambda.Lambda.LOG
+import com.lambda.Lambda.mc
+import kotlinx.coroutines.Dispatchers
+import org.jetbrains.skia.BackendRenderTarget
+import org.jetbrains.skia.ColorSpace
+import org.jetbrains.skia.DirectContext
+import org.jetbrains.skia.FramebufferFormat
+import org.jetbrains.skia.Surface
+import org.jetbrains.skia.SurfaceColorFormat
+import org.jetbrains.skia.SurfaceOrigin
+import org.lwjgl.opengl.GL11
+import org.lwjgl.opengl.GL13
+import org.lwjgl.opengl.GL14
+import org.lwjgl.opengl.GL15
+import org.lwjgl.opengl.GL20
+import org.lwjgl.opengl.GL30
+
+/**
+ * Bridges Jetpack Compose Multiplatform rendering into Minecraft's OpenGL pipeline.
+ *
+ * Uses Skiko's OpenGL backend to render Compose UI directly into the current framebuffer.
+ * All OpenGL state is saved before rendering and restored afterwards to prevent
+ * interference with Minecraft's rendering.
+ */
+@OptIn(InternalComposeUiApi::class)
+object ComposeRenderer {
+ private var directContext: DirectContext? = null
+ private var renderTarget: BackendRenderTarget? = null
+ private var surface: Surface? = null
+ private var scene: ComposeScene? = null
+
+ private var currentWidth = 0
+ private var currentHeight = 0
+ private var initialized = false
+
+ // Pending input events
+ private var mouseX = 0f
+ private var mouseY = 0f
+ private var buttonsDown = 0
+
+ fun initialize() {
+ if (initialized) return
+
+ scene = CanvasLayersComposeScene(
+ coroutineContext = Dispatchers.Default,
+ density = Density(1f),
+ invalidate = {} // We re-render every frame when the GUI is open
+ ).apply {
+ setContent {
+ ClickGuiContent()
+ }
+ }
+
+ initialized = true
+ LOG.info("Compose renderer initialized")
+ }
+
+ fun render() {
+ if (!initialized || scene == null) return
+
+ val width = mc.window.framebufferWidth
+ val height = mc.window.framebufferHeight
+ if (width <= 0 || height <= 0) return
+
+ // Save all OpenGL state
+ val glState = saveGLState()
+
+ try {
+ // Create or recreate the Skia GPU context
+ if (directContext == null) {
+ directContext = DirectContext.makeGL()
+ }
+
+ // Reset Skia's GL context tracking — Minecraft modifies GL state between frames
+ directContext?.resetGLAll()
+
+ // Recreate render target if size changed
+ if (currentWidth != width || currentHeight != height) {
+ renderTarget?.close()
+ surface?.close()
+
+ val fbId = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING)
+ renderTarget = BackendRenderTarget.makeGL(
+ width, height,
+ /* sampleCount */ 0,
+ /* stencilBits */ 8,
+ fbId,
+ FramebufferFormat.GR_GL_RGBA8
+ )
+
+ surface = Surface.makeFromBackendRenderTarget(
+ directContext!!,
+ renderTarget!!,
+ SurfaceOrigin.BOTTOM_LEFT,
+ SurfaceColorFormat.RGBA_8888,
+ ColorSpace.sRGB
+ )
+
+ currentWidth = width
+ currentHeight = height
+
+ // Update scene constraints to match framebuffer size
+ scene?.size = IntSize(width, height)
+ }
+
+ val canvas = surface?.canvas ?: return
+
+ // Render Compose scene to the Skia canvas → writes directly to the bound framebuffer via OpenGL
+ scene?.render(canvas.asComposeCanvas(), System.nanoTime())
+
+ // Flush Skia's rendering commands through OpenGL
+ surface?.flushAndSubmit()
+ directContext?.flush()
+ } catch (e: Exception) {
+ LOG.error("Error rendering Compose UI", e)
+ } finally {
+ // Restore all OpenGL state
+ restoreGLState(glState)
+ }
+ }
+
+ // --- Input forwarding ---
+
+ fun sendMouseMove(x: Double, y: Double) {
+ val scale = mc.window.scaleFactor.toFloat()
+ mouseX = (x * scale).toFloat()
+ mouseY = (y * scale).toFloat()
+ scene?.sendPointerEvent(
+ eventType = PointerEventType.Move,
+ position = Offset(mouseX, mouseY),
+ buttons = PointerButtons(buttonsDown),
+ button = PointerButton.Primary,
+ type = PointerType.Mouse
+ )
+ }
+
+ fun sendMousePress(x: Double, y: Double, button: Int) {
+ val scale = mc.window.scaleFactor.toFloat()
+ mouseX = (x * scale).toFloat()
+ mouseY = (y * scale).toFloat()
+ buttonsDown = buttonsDown or (1 shl button)
+ val pointerButton = when (button) {
+ 0 -> PointerButton.Primary
+ 1 -> PointerButton.Secondary
+ 2 -> PointerButton.Tertiary
+ else -> PointerButton.Primary
+ }
+ scene?.sendPointerEvent(
+ eventType = PointerEventType.Press,
+ position = Offset(mouseX, mouseY),
+ buttons = PointerButtons(buttonsDown),
+ button = pointerButton,
+ type = PointerType.Mouse
+ )
+ }
+
+ fun sendMouseRelease(x: Double, y: Double, button: Int) {
+ val scale = mc.window.scaleFactor.toFloat()
+ mouseX = (x * scale).toFloat()
+ mouseY = (y * scale).toFloat()
+ buttonsDown = buttonsDown and (1 shl button).inv()
+ val pointerButton = when (button) {
+ 0 -> PointerButton.Primary
+ 1 -> PointerButton.Secondary
+ 2 -> PointerButton.Tertiary
+ else -> PointerButton.Primary
+ }
+ scene?.sendPointerEvent(
+ eventType = PointerEventType.Release,
+ position = Offset(mouseX, mouseY),
+ buttons = PointerButtons(buttonsDown),
+ button = pointerButton,
+ type = PointerType.Mouse
+ )
+ }
+
+ fun sendMouseScroll(x: Double, y: Double, deltaX: Double, deltaY: Double) {
+ val scale = mc.window.scaleFactor.toFloat()
+ scene?.sendPointerEvent(
+ eventType = PointerEventType.Scroll,
+ position = Offset((x * scale).toFloat(), (y * scale).toFloat()),
+ scrollDelta = Offset(deltaX.toFloat(), deltaY.toFloat()),
+ buttons = PointerButtons(buttonsDown),
+ button = PointerButton.Primary,
+ type = PointerType.Mouse
+ )
+ }
+
+ fun destroy() {
+ scene?.close()
+ scene = null
+ surface?.close()
+ surface = null
+ renderTarget?.close()
+ renderTarget = null
+ directContext?.close()
+ directContext = null
+ currentWidth = 0
+ currentHeight = 0
+ initialized = false
+ }
+
+ // --- OpenGL state save/restore ---
+
+ private data class GLState(
+ val activeTexture: Int,
+ val program: Int,
+ val texture2D: Int,
+ val arrayBuffer: Int,
+ val elementArrayBuffer: Int,
+ val vertexArray: Int,
+ val framebuffer: Int,
+ val viewport: IntArray,
+ val scissorBox: IntArray,
+ val blendSrcRgb: Int,
+ val blendDstRgb: Int,
+ val blendSrcAlpha: Int,
+ val blendDstAlpha: Int,
+ val blendEquationRgb: Int,
+ val blendEquationAlpha: Int,
+ val blendEnabled: Boolean,
+ val cullFaceEnabled: Boolean,
+ val depthTestEnabled: Boolean,
+ val stencilTestEnabled: Boolean,
+ val scissorTestEnabled: Boolean,
+ )
+
+ private fun saveGLState(): GLState {
+ return GLState(
+ activeTexture = GL11.glGetInteger(GL13.GL_ACTIVE_TEXTURE),
+ program = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM),
+ texture2D = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D),
+ arrayBuffer = GL11.glGetInteger(GL15.GL_ARRAY_BUFFER_BINDING),
+ elementArrayBuffer = GL11.glGetInteger(GL15.GL_ELEMENT_ARRAY_BUFFER_BINDING),
+ vertexArray = GL11.glGetInteger(GL30.GL_VERTEX_ARRAY_BINDING),
+ framebuffer = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING),
+ viewport = IntArray(4).also { GL11.glGetIntegerv(GL11.GL_VIEWPORT, it) },
+ scissorBox = IntArray(4).also { GL11.glGetIntegerv(GL11.GL_SCISSOR_BOX, it) },
+ blendSrcRgb = GL11.glGetInteger(GL14.GL_BLEND_SRC_RGB),
+ blendDstRgb = GL11.glGetInteger(GL14.GL_BLEND_DST_RGB),
+ blendSrcAlpha = GL11.glGetInteger(GL14.GL_BLEND_SRC_ALPHA),
+ blendDstAlpha = GL11.glGetInteger(GL14.GL_BLEND_DST_ALPHA),
+ blendEquationRgb = GL11.glGetInteger(GL20.GL_BLEND_EQUATION_RGB),
+ blendEquationAlpha = GL11.glGetInteger(GL20.GL_BLEND_EQUATION_ALPHA),
+ blendEnabled = GL11.glIsEnabled(GL11.GL_BLEND),
+ cullFaceEnabled = GL11.glIsEnabled(GL11.GL_CULL_FACE),
+ depthTestEnabled = GL11.glIsEnabled(GL11.GL_DEPTH_TEST),
+ stencilTestEnabled = GL11.glIsEnabled(GL11.GL_STENCIL_TEST),
+ scissorTestEnabled = GL11.glIsEnabled(GL11.GL_SCISSOR_TEST),
+ )
+ }
+
+ private fun restoreGLState(state: GLState) {
+ GL20.glUseProgram(state.program)
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, state.texture2D)
+ GL13.glActiveTexture(state.activeTexture)
+ GL30.glBindVertexArray(state.vertexArray)
+ GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, state.arrayBuffer)
+ GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, state.elementArrayBuffer)
+ GL20.glBlendEquationSeparate(state.blendEquationRgb, state.blendEquationAlpha)
+ GL14.glBlendFuncSeparate(state.blendSrcRgb, state.blendDstRgb, state.blendSrcAlpha, state.blendDstAlpha)
+ if (state.blendEnabled) GL11.glEnable(GL11.GL_BLEND) else GL11.glDisable(GL11.GL_BLEND)
+ if (state.cullFaceEnabled) GL11.glEnable(GL11.GL_CULL_FACE) else GL11.glDisable(GL11.GL_CULL_FACE)
+ if (state.depthTestEnabled) GL11.glEnable(GL11.GL_DEPTH_TEST) else GL11.glDisable(GL11.GL_DEPTH_TEST)
+ if (state.stencilTestEnabled) GL11.glEnable(GL11.GL_STENCIL_TEST) else GL11.glDisable(GL11.GL_STENCIL_TEST)
+ if (state.scissorTestEnabled) GL11.glEnable(GL11.GL_SCISSOR_TEST) else GL11.glDisable(GL11.GL_SCISSOR_TEST)
+ GL11.glViewport(state.viewport[0], state.viewport[1], state.viewport[2], state.viewport[3])
+ GL11.glScissor(state.scissorBox[0], state.scissorBox[1], state.scissorBox[2], state.scissorBox[3])
+ GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, state.framebuffer)
+ }
+}
diff --git a/src/main/kotlin/com/lambda/newui/ComposeScreen.kt b/src/main/kotlin/com/lambda/newui/ComposeScreen.kt
new file mode 100644
index 000000000..8fd9fe887
--- /dev/null
+++ b/src/main/kotlin/com/lambda/newui/ComposeScreen.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2026 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.newui
+
+import com.lambda.gui.components.ClickGuiLayout
+import com.lambda.util.text.buildText
+import com.lambda.util.text.literal
+import net.minecraft.client.gui.Click
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.gui.screen.Screen
+
+object ComposeScreen : Screen(buildText { literal("Lambda Screen") }) {
+ var parentScreen: Screen? = null
+
+ override fun shouldPause() = false
+
+ override fun removed() = ComposeClickGui.close()
+
+ override fun render(context: DrawContext?, mouseX: Int, mouseY: Int, deltaTicks: Float) {
+ // Compose rendering is done via the GuiEvent.EndImguiFrame hook in ComposeClickGui,
+ // not through MC's screen render cycle.
+ }
+
+ override fun renderBackground(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
+ if (parentScreen == null) {
+ // In-game: keep vanilla behavior (blur + darken the rendered world).
+ super.renderBackground(context, mouseX, mouseY, delta)
+ return
+ }
+ // Off-screen mouse coords keep the parent's widgets from showing a hover state.
+ parentScreen?.renderBackground(context, -1, -1, delta)
+ parentScreen?.render(context, -1, -1, delta)
+ context.drawDeferredElements()
+ }
+
+ override fun resize(width: Int, height: Int) {
+ super.resize(width, height)
+ parentScreen?.resize(width, height)
+ }
+
+ override fun close() {
+ val previous = parentScreen
+ parentScreen = null
+ client?.setScreen(previous)
+ }
+
+ override fun applyBlur(context: DrawContext?) {
+ if (!ClickGuiLayout.backgroundBlur) return
+ super.applyBlur(context)
+ }
+
+ override fun renderDarkening(context: DrawContext?) {
+ if (!ClickGuiLayout.backgroundDarkening) return
+ super.renderDarkening(context)
+ }
+
+ // --- Input forwarding to Compose ---
+
+ override fun mouseClicked(click: Click, doubled: Boolean): Boolean {
+ ComposeRenderer.sendMousePress(click.x(), click.y(), click.button())
+ return true
+ }
+
+ override fun mouseReleased(click: Click): Boolean {
+ ComposeRenderer.sendMouseRelease(click.x(), click.y(), click.button())
+ return true
+ }
+
+ override fun mouseMoved(mouseX: Double, mouseY: Double) {
+ ComposeRenderer.sendMouseMove(mouseX, mouseY)
+ }
+
+ override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
+ ComposeRenderer.sendMouseScroll(mouseX, mouseY, horizontalAmount, verticalAmount)
+ return true
+ }
+}
diff --git a/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt b/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt
new file mode 100644
index 000000000..26621b9f4
--- /dev/null
+++ b/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2026 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.newui.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.lambda.module.ModuleRegistry
+import com.lambda.module.tag.ModuleTag
+import com.lambda.newui.theme.LambdaColors
+
+/**
+ * A vertical panel displaying all modules belonging to a specific [ModuleTag] category.
+ * Each module is rendered as a [ModuleCard].
+ */
+@Composable
+fun CategoryPanel(tag: ModuleTag) {
+ val modules = ModuleRegistry.modules.filter { it.tag == tag && it.showInClickGui.value }
+ if (modules.isEmpty()) return
+
+ val shape = RoundedCornerShape(8.dp)
+
+ Column(
+ modifier = Modifier
+ .width(180.dp)
+ .fillMaxHeight()
+ .clip(shape)
+ .background(LambdaColors.Surface.copy(alpha = 0.92f))
+ .border(1.dp, LambdaColors.Border.copy(alpha = 0.6f), shape)
+ ) {
+ // Category header
+ Text(
+ text = tag.name,
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ color = LambdaColors.OnSurface,
+ modifier = Modifier
+ .background(LambdaColors.HeaderBg.copy(alpha = 0.9f))
+ .padding(horizontal = 12.dp, vertical = 8.dp)
+ .width(180.dp)
+ )
+
+ // Scrollable module list
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .verticalScroll(rememberScrollState())
+ .padding(4.dp)
+ ) {
+ modules.forEach { module ->
+ ModuleCard(module)
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/com/lambda/newui/components/ModuleCard.kt b/src/main/kotlin/com/lambda/newui/components/ModuleCard.kt
new file mode 100644
index 000000000..0ea250b10
--- /dev/null
+++ b/src/main/kotlin/com/lambda/newui/components/ModuleCard.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2026 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.newui.components
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.lambda.module.Module
+import com.lambda.newui.theme.LambdaColors
+
+/**
+ * A clickable card representing a single [Module].
+ * Displays the module name, toggles on click, and animates the background color
+ * between enabled (blue highlight) and disabled (dark muted) states.
+ */
+@Composable
+fun ModuleCard(module: Module) {
+ // Track module enabled state reactively.
+ // We re-read isEnabled each recomposition; Compose will recompose when the value changes
+ // because we wrap it in a mutableStateOf + remember pattern with a key.
+ var enabled by remember { mutableStateOf(module.isEnabled) }
+
+ // Sync with actual module state each recomposition
+ enabled = module.isEnabled
+
+ val backgroundColor by animateColorAsState(
+ targetValue = if (enabled) LambdaColors.ModuleEnabled else LambdaColors.ModuleDisabled,
+ animationSpec = tween(durationMillis = 150),
+ label = "moduleCardBg"
+ )
+
+ val textColor = if (enabled) LambdaColors.OnSurface else LambdaColors.OnSurfaceVariant
+ val shape = RoundedCornerShape(4.dp)
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 4.dp, vertical = 2.dp)
+ .clip(shape)
+ .background(backgroundColor)
+ .clickable {
+ module.toggle()
+ enabled = module.isEnabled
+ }
+ .padding(horizontal = 10.dp, vertical = 6.dp)
+ ) {
+ Text(
+ text = module.name,
+ fontSize = 12.sp,
+ fontWeight = if (enabled) FontWeight.SemiBold else FontWeight.Normal,
+ color = textColor
+ )
+ }
+}
diff --git a/src/main/kotlin/com/lambda/newui/theme/LambdaTheme.kt b/src/main/kotlin/com/lambda/newui/theme/LambdaTheme.kt
new file mode 100644
index 000000000..f944457db
--- /dev/null
+++ b/src/main/kotlin/com/lambda/newui/theme/LambdaTheme.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2026 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.newui.theme
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.Typography
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+/**
+ * Lambda color palette — derived from the existing ImGui ClickGuiLayout colors.
+ */
+object LambdaColors {
+ val Primary = Color(130, 200, 255) // Bright blue accent
+ val Secondary = Color(225, 130, 225) // Pink/magenta accent
+ val Surface = Color(35, 0, 14) // Dark background
+ val SurfaceVariant = Color(55, 5, 25) // Slightly lighter dark
+ val OnSurface = Color(255, 255, 255) // White text
+ val OnSurfaceVariant = Color(180, 180, 180) // Dimmed text
+ val ModuleEnabled = Color(130, 200, 255, 180) // Blue glow when module is on
+ val ModuleDisabled = Color(80, 10, 40, 150) // Muted dark when module is off
+ val Border = Color(130, 12, 60) // Pink border
+ val HeaderBg = Color(125, 0, 50) // Category header background
+}
+
+private val LambdaColorScheme = darkColorScheme(
+ primary = LambdaColors.Primary,
+ secondary = LambdaColors.Secondary,
+ surface = LambdaColors.Surface,
+ surfaceVariant = LambdaColors.SurfaceVariant,
+ onSurface = LambdaColors.OnSurface,
+ onSurfaceVariant = LambdaColors.OnSurfaceVariant,
+ outline = LambdaColors.Border,
+ primaryContainer = LambdaColors.HeaderBg,
+)
+
+private val LambdaTypography = Typography(
+ titleMedium = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp,
+ color = LambdaColors.OnSurface,
+ ),
+ bodyMedium = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Normal,
+ fontSize = 13.sp,
+ color = LambdaColors.OnSurface,
+ ),
+ bodySmall = TextStyle(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Normal,
+ fontSize = 11.sp,
+ color = LambdaColors.OnSurfaceVariant,
+ ),
+)
+
+/**
+ * Lambda Material3 theme wrapper.
+ * Applies the dark color scheme and typography derived from the existing ImGui GUI.
+ */
+@Composable
+fun LambdaTheme(content: @Composable () -> Unit) {
+ MaterialTheme(
+ colorScheme = LambdaColorScheme,
+ typography = LambdaTypography,
+ content = content
+ )
+}
From d1e8e9d7d00400f624b0a7c69a9f9e7adf94ec79 Mon Sep 17 00:00:00 2001
From: beanbag44 <107891830+beanbag44@users.noreply.github.com>
Date: Wed, 29 Jul 2026 17:27:57 +0100
Subject: [PATCH 2/9] right click categories
---
build.gradle.kts | 3 +-
.../com/lambda/newui/ComposeClickGui.kt | 23 +----
.../com/lambda/newui/ComposeRenderer.kt | 73 +++++++---------
.../kotlin/com/lambda/newui/ComposeScreen.kt | 5 +-
.../lambda/newui/components/CategoryPanel.kt | 83 +++++++++++++------
.../com/lambda/newui/theme/LambdaTheme.kt | 2 +-
6 files changed, 94 insertions(+), 95 deletions(-)
diff --git a/build.gradle.kts b/build.gradle.kts
index a4a726d0c..250cc5402 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -191,7 +191,6 @@ dependencies {
modCompileOnly("maven.modrinth:malilib:$maLiLibVersion")
modCompileOnly("maven.modrinth:litematica:$litematicaVersion")
- // Compose Multiplatform (alternative GUI)
shadowLib("org.jetbrains.compose.runtime:runtime-desktop:$composeVersion") {
exclude(group = "org.jetbrains.kotlin")
exclude(group = "org.jetbrains.kotlinx")
@@ -208,7 +207,7 @@ dependencies {
exclude(group = "org.jetbrains.kotlin")
exclude(group = "org.jetbrains.kotlinx")
}
- shadowLib("org.jetbrains.skiko:skiko-awt-runtime-windows-x64:0.9.4") // Skiko native library required by Compose scene
+ shadowLib("org.jetbrains.skiko:skiko-awt-runtime-windows-x64:0.9.4")
// DevLogin
modRuntimeOnly("com.ptsmods:devlogin:3.5")
diff --git a/src/main/kotlin/com/lambda/newui/ComposeClickGui.kt b/src/main/kotlin/com/lambda/newui/ComposeClickGui.kt
index e4bb941da..3713d7c4b 100644
--- a/src/main/kotlin/com/lambda/newui/ComposeClickGui.kt
+++ b/src/main/kotlin/com/lambda/newui/ComposeClickGui.kt
@@ -22,19 +22,10 @@ import com.lambda.core.Loadable
import com.lambda.event.events.GuiEvent
import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
import com.lambda.gui.OverlayBackgroundScreen
-import com.lambda.gui.components.ClickGuiLayout
import com.lambda.module.modules.client.Client
import com.lambda.sound.LambdaSound
import com.lambda.sound.SoundHandler.play
-/**
- * Orchestrator for the Compose-based click GUI.
- *
- * Manages the open/close lifecycle, hooks into the render pipeline via [GuiEvent.EndImguiFrame],
- * and delegates rendering to [ComposeRenderer].
- *
- * Implements [Loadable] so it is auto-discovered by [com.lambda.core.Loader].
- */
object ComposeClickGui : Loadable {
var open = false
private set
@@ -44,30 +35,22 @@ object ComposeClickGui : Loadable {
}
init {
- // Render the Compose GUI after ImGui finishes (MC framebuffer is bound at this point).
listenUnsafe {
if (!open) return@listenUnsafe
ComposeRenderer.render()
}
}
- /**
- * Toggle the Compose click GUI open/closed.
- * Uses the same screen-check logic as [ClickGuiLayout.toggle].
- */
fun toggle() {
- if (open) {
- // ComposeScreen.close() restores the background screen and triggers
- // removed() -> close(), which flips `open` off and plays the sound.
- ComposeScreen.close()
- } else {
+ if (open) ComposeScreen.close()
+ else {
val current = mc.currentScreen
if (current is ComposeScreen) return
if (Client.clientSounds) LambdaSound.ModuleOn.play()
ComposeRenderer.initialize()
- ComposeScreen.parentScreen = if (current is ComposeScreen) null else current
+ ComposeScreen.parentScreen = current
(current as? OverlayBackgroundScreen)?.onOverlaidByGui()
mc.setScreen(ComposeScreen)
open = true
diff --git a/src/main/kotlin/com/lambda/newui/ComposeRenderer.kt b/src/main/kotlin/com/lambda/newui/ComposeRenderer.kt
index 52d112e8f..7484de6dd 100644
--- a/src/main/kotlin/com/lambda/newui/ComposeRenderer.kt
+++ b/src/main/kotlin/com/lambda/newui/ComposeRenderer.kt
@@ -30,6 +30,8 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import com.lambda.Lambda.LOG
import com.lambda.Lambda.mc
+import com.lambda.event.events.ClientEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
import kotlinx.coroutines.Dispatchers
import org.jetbrains.skia.BackendRenderTarget
import org.jetbrains.skia.ColorSpace
@@ -45,17 +47,10 @@ import org.lwjgl.opengl.GL15
import org.lwjgl.opengl.GL20
import org.lwjgl.opengl.GL30
-/**
- * Bridges Jetpack Compose Multiplatform rendering into Minecraft's OpenGL pipeline.
- *
- * Uses Skiko's OpenGL backend to render Compose UI directly into the current framebuffer.
- * All OpenGL state is saved before rendering and restored afterwards to prevent
- * interference with Minecraft's rendering.
- */
@OptIn(InternalComposeUiApi::class)
object ComposeRenderer {
private var directContext: DirectContext? = null
- private var renderTarget: BackendRenderTarget? = null
+ private var previousRenderTarget: BackendRenderTarget? = null
private var surface: Surface? = null
private var scene: ComposeScene? = null
@@ -74,13 +69,17 @@ object ComposeRenderer {
scene = CanvasLayersComposeScene(
coroutineContext = Dispatchers.Default,
density = Density(1f),
- invalidate = {} // We re-render every frame when the GUI is open
+ invalidate = {}
).apply {
setContent {
ClickGuiContent()
}
}
+ listen {
+ destroy()
+ }
+
initialized = true
LOG.info("Compose renderer initialized")
}
@@ -92,35 +91,35 @@ object ComposeRenderer {
val height = mc.window.framebufferHeight
if (width <= 0 || height <= 0) return
- // Save all OpenGL state
val glState = saveGLState()
try {
- // Create or recreate the Skia GPU context
- if (directContext == null) {
- directContext = DirectContext.makeGL()
- }
+ val directContext = directContext
+ ?: DirectContext.makeGL().also { context ->
+ directContext = context
+ }
- // Reset Skia's GL context tracking — Minecraft modifies GL state between frames
- directContext?.resetGLAll()
+ directContext.resetGLAll()
- // Recreate render target if size changed
if (currentWidth != width || currentHeight != height) {
- renderTarget?.close()
+ previousRenderTarget?.close()
surface?.close()
val fbId = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING)
- renderTarget = BackendRenderTarget.makeGL(
- width, height,
- /* sampleCount */ 0,
- /* stencilBits */ 8,
- fbId,
- FramebufferFormat.GR_GL_RGBA8
- )
+ val currentTarget =
+ BackendRenderTarget.makeGL(
+ width, height,
+ sampleCnt = 0,
+ stencilBits = 8,
+ fbId,
+ FramebufferFormat.GR_GL_RGBA8
+ ).also { target ->
+ previousRenderTarget = target
+ }
surface = Surface.makeFromBackendRenderTarget(
- directContext!!,
- renderTarget!!,
+ directContext,
+ currentTarget,
SurfaceOrigin.BOTTOM_LEFT,
SurfaceColorFormat.RGBA_8888,
ColorSpace.sRGB
@@ -129,28 +128,22 @@ object ComposeRenderer {
currentWidth = width
currentHeight = height
- // Update scene constraints to match framebuffer size
scene?.size = IntSize(width, height)
}
val canvas = surface?.canvas ?: return
- // Render Compose scene to the Skia canvas → writes directly to the bound framebuffer via OpenGL
scene?.render(canvas.asComposeCanvas(), System.nanoTime())
- // Flush Skia's rendering commands through OpenGL
surface?.flushAndSubmit()
- directContext?.flush()
+ directContext.flush()
} catch (e: Exception) {
LOG.error("Error rendering Compose UI", e)
} finally {
- // Restore all OpenGL state
restoreGLState(glState)
}
}
- // --- Input forwarding ---
-
fun sendMouseMove(x: Double, y: Double) {
val scale = mc.window.scaleFactor.toFloat()
mouseX = (x * scale).toFloat()
@@ -221,8 +214,8 @@ object ComposeRenderer {
scene = null
surface?.close()
surface = null
- renderTarget?.close()
- renderTarget = null
+ previousRenderTarget?.close()
+ previousRenderTarget = null
directContext?.close()
directContext = null
currentWidth = 0
@@ -230,8 +223,6 @@ object ComposeRenderer {
initialized = false
}
- // --- OpenGL state save/restore ---
-
private data class GLState(
val activeTexture: Int,
val program: Int,
@@ -240,8 +231,6 @@ object ComposeRenderer {
val elementArrayBuffer: Int,
val vertexArray: Int,
val framebuffer: Int,
- val viewport: IntArray,
- val scissorBox: IntArray,
val blendSrcRgb: Int,
val blendDstRgb: Int,
val blendSrcAlpha: Int,
@@ -264,8 +253,6 @@ object ComposeRenderer {
elementArrayBuffer = GL11.glGetInteger(GL15.GL_ELEMENT_ARRAY_BUFFER_BINDING),
vertexArray = GL11.glGetInteger(GL30.GL_VERTEX_ARRAY_BINDING),
framebuffer = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING),
- viewport = IntArray(4).also { GL11.glGetIntegerv(GL11.GL_VIEWPORT, it) },
- scissorBox = IntArray(4).also { GL11.glGetIntegerv(GL11.GL_SCISSOR_BOX, it) },
blendSrcRgb = GL11.glGetInteger(GL14.GL_BLEND_SRC_RGB),
blendDstRgb = GL11.glGetInteger(GL14.GL_BLEND_DST_RGB),
blendSrcAlpha = GL11.glGetInteger(GL14.GL_BLEND_SRC_ALPHA),
@@ -294,8 +281,6 @@ object ComposeRenderer {
if (state.depthTestEnabled) GL11.glEnable(GL11.GL_DEPTH_TEST) else GL11.glDisable(GL11.GL_DEPTH_TEST)
if (state.stencilTestEnabled) GL11.glEnable(GL11.GL_STENCIL_TEST) else GL11.glDisable(GL11.GL_STENCIL_TEST)
if (state.scissorTestEnabled) GL11.glEnable(GL11.GL_SCISSOR_TEST) else GL11.glDisable(GL11.GL_SCISSOR_TEST)
- GL11.glViewport(state.viewport[0], state.viewport[1], state.viewport[2], state.viewport[3])
- GL11.glScissor(state.scissorBox[0], state.scissorBox[1], state.scissorBox[2], state.scissorBox[3])
GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, state.framebuffer)
}
}
diff --git a/src/main/kotlin/com/lambda/newui/ComposeScreen.kt b/src/main/kotlin/com/lambda/newui/ComposeScreen.kt
index 8fd9fe887..87e42e3fd 100644
--- a/src/main/kotlin/com/lambda/newui/ComposeScreen.kt
+++ b/src/main/kotlin/com/lambda/newui/ComposeScreen.kt
@@ -31,10 +31,7 @@ object ComposeScreen : Screen(buildText { literal("Lambda Screen") }) {
override fun removed() = ComposeClickGui.close()
- override fun render(context: DrawContext?, mouseX: Int, mouseY: Int, deltaTicks: Float) {
- // Compose rendering is done via the GuiEvent.EndImguiFrame hook in ComposeClickGui,
- // not through MC's screen render cycle.
- }
+ override fun render(context: DrawContext?, mouseX: Int, mouseY: Int, deltaTicks: Float) {}
override fun renderBackground(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
if (parentScreen == null) {
diff --git a/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt b/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt
index 26621b9f4..1d3e5b2ca 100644
--- a/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt
+++ b/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt
@@ -17,10 +17,15 @@
package com.lambda.newui.components
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
@@ -28,8 +33,17 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.isSecondaryPressed
+import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -37,47 +51,68 @@ import com.lambda.module.ModuleRegistry
import com.lambda.module.tag.ModuleTag
import com.lambda.newui.theme.LambdaColors
-/**
- * A vertical panel displaying all modules belonging to a specific [ModuleTag] category.
- * Each module is rendered as a [ModuleCard].
- */
@Composable
fun CategoryPanel(tag: ModuleTag) {
val modules = ModuleRegistry.modules.filter { it.tag == tag && it.showInClickGui.value }
if (modules.isEmpty()) return
- val shape = RoundedCornerShape(8.dp)
+ // 1. State to track if the dropdown is expanded
+ var expanded by remember { mutableStateOf(true) } // Set to false if you want collapsed by default
+
+ val shape = RoundedCornerShape(3.dp)
Column(
modifier = Modifier
.width(180.dp)
- .fillMaxHeight()
.clip(shape)
.background(LambdaColors.Surface.copy(alpha = 0.92f))
.border(1.dp, LambdaColors.Border.copy(alpha = 0.6f), shape)
) {
- // Category header
- Text(
- text = tag.name,
- fontSize = 14.sp,
- fontWeight = FontWeight.Bold,
- color = LambdaColors.OnSurface,
+ // 2. Clickable Header (Row)
+ Row(
modifier = Modifier
.background(LambdaColors.HeaderBg.copy(alpha = 0.9f))
.padding(horizontal = 12.dp, vertical = 8.dp)
- .width(180.dp)
- )
+ .fillMaxWidth()
+ .pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent(PointerEventPass.Main)
+ if (event.type == PointerEventType.Press) {
+ if (event.buttons.isSecondaryPressed) {
+ expanded = !expanded
+ }
+ }
+ }
+ }
+ },
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = tag.name,
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ color = LambdaColors.OnSurface,
+ )
+ }
- // Scrollable module list
- Column(
- modifier = Modifier
- .weight(1f)
- .verticalScroll(rememberScrollState())
- .padding(4.dp)
+ // 5. Content (Modules) - only visible when expanded
+ AnimatedVisibility(
+ visible = expanded,
+ enter = expandVertically(),
+ exit = shrinkVertically()
) {
- modules.forEach { module ->
- ModuleCard(module)
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .verticalScroll(rememberScrollState())
+ .padding(4.dp)
+ ) {
+ modules.forEach { module ->
+ ModuleCard(module)
+ }
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/newui/theme/LambdaTheme.kt b/src/main/kotlin/com/lambda/newui/theme/LambdaTheme.kt
index f944457db..8ca69d6e5 100644
--- a/src/main/kotlin/com/lambda/newui/theme/LambdaTheme.kt
+++ b/src/main/kotlin/com/lambda/newui/theme/LambdaTheme.kt
@@ -18,8 +18,8 @@
package com.lambda.newui.theme
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.Typography
+import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
From d6f7437bb8610bcfb7ec0b53587edb23c830bf48 Mon Sep 17 00:00:00 2001
From: beanbag44 <107891830+beanbag44@users.noreply.github.com>
Date: Wed, 29 Jul 2026 18:29:57 +0100
Subject: [PATCH 3/9] better theming, grid snapping (todo, make it optional or
activated by key press), better sizing, etc
---
.../com/lambda/newui/ClickGuiContent.kt | 26 ++++++---
.../com/lambda/newui/ComposeRenderer.kt | 2 +-
.../kotlin/com/lambda/newui/ComposeScreen.kt | 2 +-
.../lambda/newui/components/CategoryPanel.kt | 56 +++++++++++++------
.../com/lambda/newui/components/ModuleCard.kt | 23 +++-----
.../com/lambda/newui/theme/LambdaTheme.kt | 23 ++++----
6 files changed, 78 insertions(+), 54 deletions(-)
diff --git a/src/main/kotlin/com/lambda/newui/ClickGuiContent.kt b/src/main/kotlin/com/lambda/newui/ClickGuiContent.kt
index b548d3dea..edf48d424 100644
--- a/src/main/kotlin/com/lambda/newui/ClickGuiContent.kt
+++ b/src/main/kotlin/com/lambda/newui/ClickGuiContent.kt
@@ -17,13 +17,13 @@
package com.lambda.newui
-import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.lambda.module.tag.ModuleTag
@@ -33,21 +33,33 @@ import com.lambda.newui.theme.LambdaTheme
/**
* Top-level composable for the Lambda click GUI.
*
- * Renders a horizontally scrollable row of [CategoryPanel]s — one per [ModuleTag] in the
+ * Renders a row of [CategoryPanel]s — one per [ModuleTag] in the
* default tag set. Each panel lists its modules as clickable toggle cards.
+ * Panels are individually draggable by their header.
+ * Most recently interacted panel renders on top of others.
*/
@Composable
fun ClickGuiContent() {
LambdaTheme {
+ // Track z-order: last element = highest z-index (renders on top)
+ val zOrder = remember { mutableStateListOf(*ModuleTag.shownTags.toTypedArray()) }
+
Row(
- horizontalArrangement = Arrangement.spacedBy(8.dp),
+ horizontalArrangement = Arrangement.spacedBy(3.dp),
modifier = Modifier
.fillMaxSize()
- .padding(20.dp)
- .horizontalScroll(rememberScrollState())
+ .padding(6.dp)
) {
ModuleTag.shownTags.forEach { tag ->
- CategoryPanel(tag)
+ val zIndex = zOrder.indexOf(tag).toFloat()
+ CategoryPanel(
+ tag = tag,
+ zIndex = zIndex,
+ onFocus = {
+ zOrder.remove(tag)
+ zOrder.add(tag) // Move to end = highest z-index
+ }
+ )
}
}
}
diff --git a/src/main/kotlin/com/lambda/newui/ComposeRenderer.kt b/src/main/kotlin/com/lambda/newui/ComposeRenderer.kt
index 7484de6dd..bd7646a67 100644
--- a/src/main/kotlin/com/lambda/newui/ComposeRenderer.kt
+++ b/src/main/kotlin/com/lambda/newui/ComposeRenderer.kt
@@ -68,7 +68,7 @@ object ComposeRenderer {
scene = CanvasLayersComposeScene(
coroutineContext = Dispatchers.Default,
- density = Density(1f),
+ density = Density(3f),
invalidate = {}
).apply {
setContent {
diff --git a/src/main/kotlin/com/lambda/newui/ComposeScreen.kt b/src/main/kotlin/com/lambda/newui/ComposeScreen.kt
index 87e42e3fd..fe4665e7c 100644
--- a/src/main/kotlin/com/lambda/newui/ComposeScreen.kt
+++ b/src/main/kotlin/com/lambda/newui/ComposeScreen.kt
@@ -83,7 +83,7 @@ object ComposeScreen : Screen(buildText { literal("Lambda Screen") }) {
}
override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
- ComposeRenderer.sendMouseScroll(mouseX, mouseY, horizontalAmount, verticalAmount)
+ ComposeRenderer.sendMouseScroll(mouseX, mouseY, horizontalAmount, -verticalAmount)
return true
}
}
diff --git a/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt b/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt
index 1d3e5b2ca..3b515644b 100644
--- a/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt
+++ b/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt
@@ -22,10 +22,12 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.border
+import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
@@ -39,65 +41,88 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.isSecondaryPressed
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.compose.ui.zIndex
import com.lambda.module.ModuleRegistry
import com.lambda.module.tag.ModuleTag
import com.lambda.newui.theme.LambdaColors
+import kotlin.math.roundToInt
+
+private const val GRID_SIZE_DP = 4f
+
+private fun snapToGrid(value: Float, gridSize: Float): Float {
+ return (value / gridSize).roundToInt() * gridSize
+}
@Composable
-fun CategoryPanel(tag: ModuleTag) {
+fun CategoryPanel(tag: ModuleTag, zIndex: Float = 0f, onFocus: () -> Unit = {}) {
val modules = ModuleRegistry.modules.filter { it.tag == tag && it.showInClickGui.value }
if (modules.isEmpty()) return
- // 1. State to track if the dropdown is expanded
- var expanded by remember { mutableStateOf(true) } // Set to false if you want collapsed by default
+ var expanded by remember { mutableStateOf(true) }
+ var dragOffset by remember { mutableStateOf(Offset.Zero) }
- val shape = RoundedCornerShape(3.dp)
+ val shape = RoundedCornerShape(1.dp)
Column(
modifier = Modifier
- .width(180.dp)
- .clip(shape)
- .background(LambdaColors.Surface.copy(alpha = 0.92f))
- .border(1.dp, LambdaColors.Border.copy(alpha = 0.6f), shape)
+ .zIndex(zIndex)
+ .offset {
+ IntOffset(
+ snapToGrid(dragOffset.x, GRID_SIZE_DP * density).roundToInt(),
+ snapToGrid(dragOffset.y, GRID_SIZE_DP * density).roundToInt()
+ )
+ }
+ .width(100.dp)
+ .background(LambdaColors.Surface, shape)
+ .border(1.dp, LambdaColors.Border, shape)
) {
- // 2. Clickable Header (Row)
Row(
modifier = Modifier
- .background(LambdaColors.HeaderBg.copy(alpha = 0.9f))
- .padding(horizontal = 12.dp, vertical = 8.dp)
+ .background(LambdaColors.HeaderBg)
.fillMaxWidth()
+ .pointerInput(Unit) {
+ detectDragGestures(
+ onDragStart = { onFocus() },
+ onDrag = { _, dragAmount ->
+ dragOffset += dragAmount
+ }
+ )
+ }
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Main)
if (event.type == PointerEventType.Press) {
+ onFocus()
if (event.buttons.isSecondaryPressed) {
expanded = !expanded
}
}
}
}
- },
+ }
+ .padding(horizontal = 5.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = tag.name,
- fontSize = 14.sp,
+ fontSize = 10.sp,
+ lineHeight = 10.sp,
fontWeight = FontWeight.Bold,
color = LambdaColors.OnSurface,
)
}
- // 5. Content (Modules) - only visible when expanded
AnimatedVisibility(
visible = expanded,
enter = expandVertically(),
@@ -107,7 +132,6 @@ fun CategoryPanel(tag: ModuleTag) {
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
- .padding(4.dp)
) {
modules.forEach { module ->
ModuleCard(module)
diff --git a/src/main/kotlin/com/lambda/newui/components/ModuleCard.kt b/src/main/kotlin/com/lambda/newui/components/ModuleCard.kt
index 0ea250b10..ec7686f55 100644
--- a/src/main/kotlin/com/lambda/newui/components/ModuleCard.kt
+++ b/src/main/kotlin/com/lambda/newui/components/ModuleCard.kt
@@ -24,7 +24,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -32,8 +31,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.lambda.module.Module
@@ -46,14 +44,8 @@ import com.lambda.newui.theme.LambdaColors
*/
@Composable
fun ModuleCard(module: Module) {
- // Track module enabled state reactively.
- // We re-read isEnabled each recomposition; Compose will recompose when the value changes
- // because we wrap it in a mutableStateOf + remember pattern with a key.
var enabled by remember { mutableStateOf(module.isEnabled) }
- // Sync with actual module state each recomposition
- enabled = module.isEnabled
-
val backgroundColor by animateColorAsState(
targetValue = if (enabled) LambdaColors.ModuleEnabled else LambdaColors.ModuleDisabled,
animationSpec = tween(durationMillis = 150),
@@ -61,25 +53,24 @@ fun ModuleCard(module: Module) {
)
val textColor = if (enabled) LambdaColors.OnSurface else LambdaColors.OnSurfaceVariant
- val shape = RoundedCornerShape(4.dp)
Box(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 4.dp, vertical = 2.dp)
- .clip(shape)
.background(backgroundColor)
.clickable {
module.toggle()
enabled = module.isEnabled
}
- .padding(horizontal = 10.dp, vertical = 6.dp)
+ .padding(horizontal = 5.dp, vertical = 3.dp)
) {
Text(
text = module.name,
- fontSize = 12.sp,
- fontWeight = if (enabled) FontWeight.SemiBold else FontWeight.Normal,
- color = textColor
+ fontSize = 9.sp,
+ lineHeight = 9.sp,
+ color = textColor,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
)
}
}
diff --git a/src/main/kotlin/com/lambda/newui/theme/LambdaTheme.kt b/src/main/kotlin/com/lambda/newui/theme/LambdaTheme.kt
index 8ca69d6e5..c1602d501 100644
--- a/src/main/kotlin/com/lambda/newui/theme/LambdaTheme.kt
+++ b/src/main/kotlin/com/lambda/newui/theme/LambdaTheme.kt
@@ -27,20 +27,17 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
-/**
- * Lambda color palette — derived from the existing ImGui ClickGuiLayout colors.
- */
object LambdaColors {
- val Primary = Color(130, 200, 255) // Bright blue accent
- val Secondary = Color(225, 130, 225) // Pink/magenta accent
- val Surface = Color(35, 0, 14) // Dark background
- val SurfaceVariant = Color(55, 5, 25) // Slightly lighter dark
- val OnSurface = Color(255, 255, 255) // White text
- val OnSurfaceVariant = Color(180, 180, 180) // Dimmed text
- val ModuleEnabled = Color(130, 200, 255, 180) // Blue glow when module is on
- val ModuleDisabled = Color(80, 10, 40, 150) // Muted dark when module is off
- val Border = Color(130, 12, 60) // Pink border
- val HeaderBg = Color(125, 0, 50) // Category header background
+ val Primary = Color(100, 180, 255) // Bright blue accent
+ val Secondary = Color(225, 130, 225) // Pink/magenta accent
+ val Surface = Color(18, 0, 8) // Deep dark background
+ val SurfaceVariant = Color(35, 3, 18) // Slightly lighter dark
+ val OnSurface = Color(240, 240, 245) // Near-white text
+ val OnSurfaceVariant = Color(140, 140, 150) // Dimmed text
+ val ModuleEnabled = Color(88, 0, 35, 255) // Solid blue when module is on
+ val ModuleDisabled = Color(40, 5, 22, 200) // Muted dark when module is off
+ val Border = Color(140, 15, 65) // Pink border — higher contrast
+ val HeaderBg = Color(130, 5, 55) // Category header background
}
private val LambdaColorScheme = darkColorScheme(
From 0673ea53af310ccdb979477a9459c25b2b6371ef Mon Sep 17 00:00:00 2001
From: pothemagicdragon
Date: Wed, 29 Jul 2026 15:27:58 -0700
Subject: [PATCH 4/9] adds macos support
---
build.gradle.kts | 36 +++++++++++++++-
gradle.properties | 2 +
.../com/lambda/newui/ComposeRenderer.kt | 42 +++++++++++++++++++
3 files changed, 79 insertions(+), 1 deletion(-)
diff --git a/build.gradle.kts b/build.gradle.kts
index 250cc5402..4a4f4384d 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -38,6 +38,29 @@ val sodiumVersion: String by project
val litematicaVersion: String by project
val maLiLibVersion: String by project
val composeVersion: String by project
+val skikoVersion: String by project
+
+// Skiko publishes its natives as one artifact per platform, and none of them are pulled in
+// transitively by Compose. Dev runs only need the host's, so resolve that here.
+// TODO: production jars need every target bundled, or downloaded at runtime.
+val skikoTarget = run {
+ val osName = System.getProperty("os.name").lowercase()
+ val archName = System.getProperty("os.arch").lowercase()
+
+ val os = when {
+ osName.startsWith("windows") -> "windows"
+ osName.startsWith("mac") || osName.contains("darwin") -> "macos"
+ osName.contains("linux") -> "linux"
+ else -> throw GradleException("No skiko natives for host OS '$osName'")
+ }
+ val arch = when (archName) {
+ "x86_64", "amd64" -> "x64"
+ "aarch64", "arm64" -> "arm64"
+ else -> throw GradleException("No skiko natives for host architecture '$archName'")
+ }
+
+ "$os-$arch"
+}
val libs = file("libs")
val targets = listOf("fabric.mod.json")
@@ -65,6 +88,11 @@ configurations.all {
if (requested.group == "org.lwjgl") {
useVersion(lwjglVersion)
}
+ // The native artifact and the classes Compose pulls in transitively must agree,
+ // or skiko fails to find its library at runtime
+ if (requested.group == "org.jetbrains.skiko") {
+ useVersion(skikoVersion)
+ }
}
}
@@ -115,6 +143,12 @@ loom {
property("org.lwjgl.util.DebugFunctions", "true")
property("mixin.debug.export", "true")
+ // Skia refuses to initialise on macOS without both of these
+ if (skikoTarget.startsWith("macos")) {
+ property("skiko.renderApi", "OPENGL")
+ property("skiko.macos.opengl.enabled", "true")
+ }
+
vmArgs("-XX:+HeapDumpOnOutOfMemoryError", "-XX:+CreateCoredumpOnCrash")
programArgs("--username", "Steve", "--uuid", "8667ba71b85a4004af54457a9734eed7", "--accessToken", "****")
}
@@ -207,7 +241,7 @@ dependencies {
exclude(group = "org.jetbrains.kotlin")
exclude(group = "org.jetbrains.kotlinx")
}
- shadowLib("org.jetbrains.skiko:skiko-awt-runtime-windows-x64:0.9.4")
+ shadowLib("org.jetbrains.skiko:skiko-awt-runtime-$skikoTarget:$skikoVersion")
// DevLogin
modRuntimeOnly("com.ptsmods:devlogin:3.5")
diff --git a/gradle.properties b/gradle.properties
index 7defe22ce..c86cf55e6 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -39,6 +39,8 @@ jacksonVersion=3.1.1
mockkVersion=1.14.7
spairVersion=1.90.0
composeVersion=1.8.0
+# Must match the skiko version Compose depends on transitively
+skikoVersion=0.9.4
# Fabric https://fabricmc.net/develop/
fabricLoaderVersion=0.18.3
diff --git a/src/main/kotlin/com/lambda/newui/ComposeRenderer.kt b/src/main/kotlin/com/lambda/newui/ComposeRenderer.kt
index bd7646a67..1679f8ab6 100644
--- a/src/main/kotlin/com/lambda/newui/ComposeRenderer.kt
+++ b/src/main/kotlin/com/lambda/newui/ComposeRenderer.kt
@@ -41,6 +41,7 @@ import org.jetbrains.skia.Surface
import org.jetbrains.skia.SurfaceColorFormat
import org.jetbrains.skia.SurfaceOrigin
import org.lwjgl.opengl.GL11
+import org.lwjgl.opengl.GL12
import org.lwjgl.opengl.GL13
import org.lwjgl.opengl.GL14
import org.lwjgl.opengl.GL15
@@ -92,6 +93,7 @@ object ComposeRenderer {
if (width <= 0 || height <= 0) return
val glState = saveGLState()
+ resetPixelStore()
try {
val directContext = directContext
@@ -242,6 +244,14 @@ object ComposeRenderer {
val depthTestEnabled: Boolean,
val stencilTestEnabled: Boolean,
val scissorTestEnabled: Boolean,
+ val unpackSwapBytes: Int,
+ val unpackLsbFirst: Int,
+ val unpackRowLength: Int,
+ val unpackImageHeight: Int,
+ val unpackSkipRows: Int,
+ val unpackSkipPixels: Int,
+ val unpackSkipImages: Int,
+ val unpackAlignment: Int,
)
private fun saveGLState(): GLState {
@@ -264,9 +274,33 @@ object ComposeRenderer {
depthTestEnabled = GL11.glIsEnabled(GL11.GL_DEPTH_TEST),
stencilTestEnabled = GL11.glIsEnabled(GL11.GL_STENCIL_TEST),
scissorTestEnabled = GL11.glIsEnabled(GL11.GL_SCISSOR_TEST),
+ unpackSwapBytes = GL11.glGetInteger(GL11.GL_UNPACK_SWAP_BYTES),
+ unpackLsbFirst = GL11.glGetInteger(GL11.GL_UNPACK_LSB_FIRST),
+ unpackRowLength = GL11.glGetInteger(GL11.GL_UNPACK_ROW_LENGTH),
+ unpackImageHeight = GL11.glGetInteger(GL12.GL_UNPACK_IMAGE_HEIGHT),
+ unpackSkipRows = GL11.glGetInteger(GL11.GL_UNPACK_SKIP_ROWS),
+ unpackSkipPixels = GL11.glGetInteger(GL11.GL_UNPACK_SKIP_PIXELS),
+ unpackSkipImages = GL11.glGetInteger(GL12.GL_UNPACK_SKIP_IMAGES),
+ unpackAlignment = GL11.glGetInteger(GL11.GL_UNPACK_ALIGNMENT),
)
}
+ /**
+ * Minecraft leaves the pixel-store unpack state non-zero after its own texture uploads.
+ * Skia would inherit it when uploading its glyph atlas, reading each glyph from the wrong
+ * offset and stride, which renders all text as garbage. Reset to the GL defaults first.
+ */
+ private fun resetPixelStore() {
+ GL11.glPixelStorei(GL11.GL_UNPACK_SWAP_BYTES, 0)
+ GL11.glPixelStorei(GL11.GL_UNPACK_LSB_FIRST, 0)
+ GL11.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, 0)
+ GL11.glPixelStorei(GL12.GL_UNPACK_IMAGE_HEIGHT, 0)
+ GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_ROWS, 0)
+ GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_PIXELS, 0)
+ GL11.glPixelStorei(GL12.GL_UNPACK_SKIP_IMAGES, 0)
+ GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 4)
+ }
+
private fun restoreGLState(state: GLState) {
GL20.glUseProgram(state.program)
GL11.glBindTexture(GL11.GL_TEXTURE_2D, state.texture2D)
@@ -282,5 +316,13 @@ object ComposeRenderer {
if (state.stencilTestEnabled) GL11.glEnable(GL11.GL_STENCIL_TEST) else GL11.glDisable(GL11.GL_STENCIL_TEST)
if (state.scissorTestEnabled) GL11.glEnable(GL11.GL_SCISSOR_TEST) else GL11.glDisable(GL11.GL_SCISSOR_TEST)
GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, state.framebuffer)
+ GL11.glPixelStorei(GL11.GL_UNPACK_SWAP_BYTES, state.unpackSwapBytes)
+ GL11.glPixelStorei(GL11.GL_UNPACK_LSB_FIRST, state.unpackLsbFirst)
+ GL11.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, state.unpackRowLength)
+ GL11.glPixelStorei(GL12.GL_UNPACK_IMAGE_HEIGHT, state.unpackImageHeight)
+ GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_ROWS, state.unpackSkipRows)
+ GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_PIXELS, state.unpackSkipPixels)
+ GL11.glPixelStorei(GL12.GL_UNPACK_SKIP_IMAGES, state.unpackSkipImages)
+ GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, state.unpackAlignment)
}
}
From b438c1f4eab7b6a8ba806ee75da2d4eead46a0ab Mon Sep 17 00:00:00 2001
From: pothemagicdragon
Date: Thu, 30 Jul 2026 07:29:15 -0700
Subject: [PATCH 5/9] Adds fix for compose watching data
---
.../kotlin/com/lambda/module/tag/ModuleTag.kt | 32 +++++-
.../com/lambda/newui/ClickGuiContent.kt | 52 +++++++---
.../lambda/newui/components/CategoryPanel.kt | 15 ++-
.../com/lambda/newui/components/ModuleCard.kt | 14 ++-
.../com/lambda/newui/state/LambdaState.kt | 99 +++++++++++++++++++
5 files changed, 182 insertions(+), 30 deletions(-)
create mode 100644 src/main/kotlin/com/lambda/newui/state/LambdaState.kt
diff --git a/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt b/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt
index caab346cc..243ccbfdd 100644
--- a/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt
+++ b/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt
@@ -49,16 +49,38 @@ data class ModuleTag(override val name: String) : Nameable {
val defaults = setOf(COMBAT, MOVEMENT, RENDER, PLAYER, WORLD, NETWORK, CHAT, CLIENT, HUD)
- val shownTags = defaults.toMutableSet()
+ private val mutableShownTags = defaults.toMutableSet()
+ private val shownTagListeners = mutableListOf<(Set) -> Unit>()
+
+ /**
+ * Read-only view of the tags the ClickGui should display.
+ *
+ * Mutate only through [toggleTag] — direct mutation would skip the
+ * [onShownTagsChanged] notification that retained-state GUIs rely on to
+ * know when to redraw.
+ */
+ val shownTags: Set get() = mutableShownTags
+
+ /**
+ * Registers [block] to run whenever [shownTags] changes.
+ *
+ * Listeners are never removed, so only register from a permanent owner
+ * (a [com.lambda.core.Loadable], an object, or a cached-per-key registry).
+ */
+ fun onShownTagsChanged(block: (Set) -> Unit) {
+ shownTagListeners.add(block)
+ }
fun toggleTag(tag: ModuleTag) {
- if (shownTags.contains(tag)) {
- shownTags.remove(tag)
+ if (mutableShownTags.contains(tag)) {
+ mutableShownTags.remove(tag)
} else {
- shownTags.add(tag)
+ mutableShownTags.add(tag)
}
+ val snapshot = mutableShownTags.toSet()
+ shownTagListeners.forEach { it(snapshot) }
}
- fun isTagShown(tag: ModuleTag) = shownTags.contains(tag)
+ fun isTagShown(tag: ModuleTag) = mutableShownTags.contains(tag)
}
}
diff --git a/src/main/kotlin/com/lambda/newui/ClickGuiContent.kt b/src/main/kotlin/com/lambda/newui/ClickGuiContent.kt
index edf48d424..1cddbdce9 100644
--- a/src/main/kotlin/com/lambda/newui/ClickGuiContent.kt
+++ b/src/main/kotlin/com/lambda/newui/ClickGuiContent.kt
@@ -22,27 +22,44 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.lambda.module.tag.ModuleTag
import com.lambda.newui.components.CategoryPanel
+import com.lambda.newui.state.LambdaState.observeShownTags
import com.lambda.newui.theme.LambdaTheme
/**
* Top-level composable for the Lambda click GUI.
*
- * Renders a row of [CategoryPanel]s — one per [ModuleTag] in the
- * default tag set. Each panel lists its modules as clickable toggle cards.
- * Panels are individually draggable by their header.
+ * Renders a row of [CategoryPanel]s — one per shown [ModuleTag]. Each panel lists its
+ * modules as clickable toggle cards. Panels are individually draggable by their header.
* Most recently interacted panel renders on top of others.
+ *
+ * The shown-tag set is observed, so toggling a category from the ImGui menu bar adds or
+ * removes its panel live.
*/
@Composable
fun ClickGuiContent() {
LambdaTheme {
- // Track z-order: last element = highest z-index (renders on top)
- val zOrder = remember { mutableStateListOf(*ModuleTag.shownTags.toTypedArray()) }
+ val shownTags by observeShownTags()
+
+ // Focus history, most recently focused last. Derived from rather than mirroring
+ // the shown set, so it tolerates tags appearing and disappearing: stale entries
+ // are simply never looked up, and a tag that has never been focused is absent.
+ val focusOrder = remember { mutableStateListOf() }
+
+ // Never-focused tags yield indexOf == -1 and so sort to the front, i.e. render
+ // beneath focused ones. sortedBy is stable, so they keep shownTags order among
+ // themselves instead of collapsing onto a shared z-index.
+ val zIndexOf = shownTags
+ .sortedBy { focusOrder.indexOf(it) }
+ .withIndex()
+ .associate { (index, tag) -> tag to index.toFloat() }
Row(
horizontalArrangement = Arrangement.spacedBy(3.dp),
@@ -50,16 +67,21 @@ fun ClickGuiContent() {
.fillMaxSize()
.padding(6.dp)
) {
- ModuleTag.shownTags.forEach { tag ->
- val zIndex = zOrder.indexOf(tag).toFloat()
- CategoryPanel(
- tag = tag,
- zIndex = zIndex,
- onFocus = {
- zOrder.remove(tag)
- zOrder.add(tag) // Move to end = highest z-index
- }
- )
+ // Layout order stays the shown-tag order so panels don't jump around
+ // horizontally when one is focused; only zIndex reacts to focus.
+ shownTags.forEach { tag ->
+ // Keyed so a tag being shown or hidden doesn't shift the panels after it
+ // onto the wrong remembered drag offset and expanded state.
+ key(tag) {
+ CategoryPanel(
+ tag = tag,
+ zIndex = zIndexOf[tag] ?: 0f,
+ onFocus = {
+ focusOrder.remove(tag)
+ focusOrder.add(tag) // Move to end = highest z-index
+ }
+ )
+ }
}
}
}
diff --git a/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt b/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt
index 3b515644b..340a84315 100644
--- a/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt
+++ b/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt
@@ -36,6 +36,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -53,6 +54,7 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import com.lambda.module.ModuleRegistry
import com.lambda.module.tag.ModuleTag
+import com.lambda.newui.state.LambdaState.observe
import com.lambda.newui.theme.LambdaColors
import kotlin.math.roundToInt
@@ -64,7 +66,11 @@ private fun snapToGrid(value: Float, gridSize: Float): Float {
@Composable
fun CategoryPanel(tag: ModuleTag, zIndex: Float = 0f, onFocus: () -> Unit = {}) {
- val modules = ModuleRegistry.modules.filter { it.tag == tag && it.showInClickGui.value }
+ // ModuleRegistry.modules is fixed after load, so the tag filter can be cached.
+ // "Show In ClickGui" is a live setting, so it has to be observed on every module —
+ // reading it here re-runs this filter whenever any of them changes.
+ val tagged = remember(tag) { ModuleRegistry.modules.filter { it.tag == tag } }
+ val modules = tagged.filter { it.showInClickGui.observe().value }
if (modules.isEmpty()) return
var expanded by remember { mutableStateOf(true) }
@@ -134,7 +140,12 @@ fun CategoryPanel(tag: ModuleTag, zIndex: Float = 0f, onFocus: () -> Unit = {})
.verticalScroll(rememberScrollState())
) {
modules.forEach { module ->
- ModuleCard(module)
+ // Keyed because the list is now dynamic — `remember` is positional,
+ // so without a key a module appearing or disappearing would shift
+ // every card below it onto the wrong retained state.
+ key(module.name) {
+ ModuleCard(module)
+ }
}
}
}
diff --git a/src/main/kotlin/com/lambda/newui/components/ModuleCard.kt b/src/main/kotlin/com/lambda/newui/components/ModuleCard.kt
index ec7686f55..cddf68056 100644
--- a/src/main/kotlin/com/lambda/newui/components/ModuleCard.kt
+++ b/src/main/kotlin/com/lambda/newui/components/ModuleCard.kt
@@ -27,24 +27,25 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.lambda.module.Module
+import com.lambda.newui.state.LambdaState.observeEnabled
import com.lambda.newui.theme.LambdaColors
/**
* A clickable card representing a single [Module].
* Displays the module name, toggles on click, and animates the background color
* between enabled (blue highlight) and disabled (dark muted) states.
+ *
+ * The enabled flag is observed from the module itself, so the card stays correct when
+ * the module is toggled by a keybind, a command, or a config load rather than a click.
*/
@Composable
fun ModuleCard(module: Module) {
- var enabled by remember { mutableStateOf(module.isEnabled) }
+ val enabled by module.observeEnabled()
val backgroundColor by animateColorAsState(
targetValue = if (enabled) LambdaColors.ModuleEnabled else LambdaColors.ModuleDisabled,
@@ -58,10 +59,7 @@ fun ModuleCard(module: Module) {
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor)
- .clickable {
- module.toggle()
- enabled = module.isEnabled
- }
+ .clickable { module.toggle() }
.padding(horizontal = 5.dp, vertical = 3.dp)
) {
Text(
diff --git a/src/main/kotlin/com/lambda/newui/state/LambdaState.kt b/src/main/kotlin/com/lambda/newui/state/LambdaState.kt
new file mode 100644
index 000000000..9f6d8d8d5
--- /dev/null
+++ b/src/main/kotlin/com/lambda/newui/state/LambdaState.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2026 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.newui.state
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import com.lambda.config.entries.Setting
+import com.lambda.config.entries.Setting.Companion.onValueChangeUnsafe
+import com.lambda.module.Module
+import com.lambda.module.tag.ModuleTag
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * Bridges Lambda's push-based config system onto Compose's snapshot state.
+ *
+ * ImGui re-read every property every frame, so nothing had to be observable. Compose
+ * only redraws what it is told changed, so anything a composable reads must be backed
+ * by snapshot state or the UI silently goes stale — a module toggled by keybind,
+ * command, or config load would leave the GUI showing the old value.
+ *
+ * Use [observe] for any [Setting], and [observeEnabled] for a [Module]'s enabled flag
+ * (which is deliberately not exposed as a [Setting], to keep writes going through
+ * [Module.toggle] and its events).
+ *
+ * ### Why the states are cached rather than per-composition
+ *
+ * [Setting.listeners] has no removal API, and `Setting.setValue` iterates it without
+ * synchronization, so registering per-composition (in a `DisposableEffect`) would both
+ * leak a listener on every recomposition and risk a `ConcurrentModificationException`
+ * against a concurrent config write. Instead exactly one listener is registered per
+ * source, the first time it is observed, and the resulting state is cached forever.
+ * Settings and modules are process-lifetime singletons, so this holds no memory the
+ * client was not already holding.
+ *
+ * ### Threading
+ *
+ * Config writes arrive from arbitrary threads (commands, packet handlers, config load).
+ * Snapshot state writes are safe from any thread — they take the global snapshot lock —
+ * and the recomposer picks them up on its own dispatcher, so listeners write directly
+ * with no marshalling.
+ */
+object LambdaState {
+ private val settingStates = ConcurrentHashMap, MutableState<*>>()
+ private val enabledStates = ConcurrentHashMap>()
+
+ private val shownTagsState: MutableState> =
+ mutableStateOf(ModuleTag.shownTags.toSet()).also { state ->
+ ModuleTag.onShownTagsChanged { tags -> state.value = tags }
+ }
+
+ /**
+ * Observes this setting's value as snapshot state. Reading the returned state from a
+ * composable subscribes it to future changes.
+ */
+ @Suppress("UNCHECKED_CAST")
+ fun Setting.observe(): State =
+ settingStates.computeIfAbsent(this) { _ ->
+ val state = mutableStateOf(value)
+ onValueChangeUnsafe { _, to -> state.value = to }
+ // The listener is registered after the initial read, so re-sync in case the
+ // value changed in between.
+ state.value = value
+ state
+ } as State
+
+ /**
+ * Observes [Module.isEnabled] as snapshot state.
+ *
+ * Goes through [Module.onToggleUnsafe] rather than the backing setting so that the
+ * setting stays private and every write keeps flowing through
+ * [Module.enable]/[Module.disable] and their events.
+ */
+ fun Module.observeEnabled(): State =
+ enabledStates.computeIfAbsent(this) { module ->
+ val state = mutableStateOf(module.isEnabled)
+ module.onToggleUnsafe { to -> state.value = to }
+ state.value = module.isEnabled
+ state
+ }
+
+ /** Observes [ModuleTag.shownTags] as snapshot state. */
+ fun observeShownTags(): State> = shownTagsState
+}
From 1139fc9dfcb62eb905f784492d42faf8ff780896 Mon Sep 17 00:00:00 2001
From: beanbag44 <107891830+beanbag44@users.noreply.github.com>
Date: Thu, 30 Jul 2026 21:02:53 +0100
Subject: [PATCH 6/9] resources and menu bar with lambda image
---
build.gradle.kts | 16 +++
gradle.properties | 2 +-
.../mixin/render/ConnectScreenMixin.java | 1 -
.../mixin/render/SplashOverlayMixin.java | 4 +-
.../com/lambda/graphics/text/FontHandler.kt | 6 +-
src/main/kotlin/com/lambda/gui/DearImGui.kt | 6 +-
src/main/kotlin/com/lambda/gui/MenuBar.kt | 6 +-
.../kotlin/com/lambda/module/hud/Watermark.kt | 2 +-
.../com/lambda/newui/ClickGuiContent.kt | 103 +++++++++++-------
.../lambda/newui/components/CategoryPanel.kt | 3 +
.../com/lambda/newui/components/ModuleCard.kt | 6 +-
.../kotlin/com/lambda/util/LambdaResource.kt | 12 +-
.../kotlin/com/lambda/util/WindowUtils.kt | 10 +-
src/main/resources/assets/lambda/chika.gif | Bin 452195 -> 0 bytes
.../drawable}/github_logo.png | Bin
.../drawable}/icon/logo_128.png | Bin
.../drawable}/icon/logo_16.png | Bin
.../drawable}/icon/logo_24.png | Bin
.../drawable}/icon/logo_256.png | Bin
.../drawable}/icon/logo_32.png | Bin
.../drawable}/icon/logo_48.png | Bin
.../drawable}/icon/logo_64.png | Bin
.../{textures => compose/drawable}/lambda.png | Bin
.../drawable}/lambda_banner.png | Bin
.../drawable}/lambda_mono.png | Bin
.../drawable}/lambda_text_color.png | Bin
.../{fonts => compose/font}/FiraSans-Bold.ttf | Bin
.../font}/FiraSans-Regular.ttf | Bin
.../font}/MinecraftDefault-Regular.ttf | Bin
.../resources/assets/lambda/fonts/emojis.zip | Bin 1597986 -> 0 bytes
30 files changed, 114 insertions(+), 63 deletions(-)
delete mode 100644 src/main/resources/assets/lambda/chika.gif
rename src/main/resources/assets/lambda/{textures => compose/drawable}/github_logo.png (100%)
rename src/main/resources/assets/lambda/{textures => compose/drawable}/icon/logo_128.png (100%)
rename src/main/resources/assets/lambda/{textures => compose/drawable}/icon/logo_16.png (100%)
rename src/main/resources/assets/lambda/{textures => compose/drawable}/icon/logo_24.png (100%)
rename src/main/resources/assets/lambda/{textures => compose/drawable}/icon/logo_256.png (100%)
rename src/main/resources/assets/lambda/{textures => compose/drawable}/icon/logo_32.png (100%)
rename src/main/resources/assets/lambda/{textures => compose/drawable}/icon/logo_48.png (100%)
rename src/main/resources/assets/lambda/{textures => compose/drawable}/icon/logo_64.png (100%)
rename src/main/resources/assets/lambda/{textures => compose/drawable}/lambda.png (100%)
rename src/main/resources/assets/lambda/{textures => compose/drawable}/lambda_banner.png (100%)
rename src/main/resources/assets/lambda/{textures => compose/drawable}/lambda_mono.png (100%)
rename src/main/resources/assets/lambda/{textures => compose/drawable}/lambda_text_color.png (100%)
rename src/main/resources/assets/lambda/{fonts => compose/font}/FiraSans-Bold.ttf (100%)
rename src/main/resources/assets/lambda/{fonts => compose/font}/FiraSans-Regular.ttf (100%)
rename src/main/resources/assets/lambda/{fonts => compose/font}/MinecraftDefault-Regular.ttf (100%)
delete mode 100644 src/main/resources/assets/lambda/fonts/emojis.zip
diff --git a/build.gradle.kts b/build.gradle.kts
index 4a4f4384d..264317c99 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -71,6 +71,7 @@ val replacements = file("gradle.properties").inputStream().use { stream ->
plugins {
kotlin("jvm") version "2.3.0"
kotlin("plugin.compose") version "2.3.0"
+ id("org.jetbrains.compose") version "1.7.0"
id("org.jetbrains.dokka") version "2.1.0"
id("fabric-loom") version "1.16-SNAPSHOT"
id("com.gradleup.shadow") version "9.3.0"
@@ -228,21 +229,28 @@ dependencies {
shadowLib("org.jetbrains.compose.runtime:runtime-desktop:$composeVersion") {
exclude(group = "org.jetbrains.kotlin")
exclude(group = "org.jetbrains.kotlinx")
+ exclude(group = "org.jetbrains.skiko")
}
shadowLib("org.jetbrains.compose.ui:ui-desktop:$composeVersion") {
exclude(group = "org.jetbrains.kotlin")
exclude(group = "org.jetbrains.kotlinx")
+ exclude(group = "org.jetbrains.skiko")
}
shadowLib("org.jetbrains.compose.foundation:foundation-desktop:$composeVersion") {
exclude(group = "org.jetbrains.kotlin")
exclude(group = "org.jetbrains.kotlinx")
+ exclude(group = "org.jetbrains.skiko")
}
shadowLib("org.jetbrains.compose.material3:material3-desktop:$composeVersion") {
exclude(group = "org.jetbrains.kotlin")
exclude(group = "org.jetbrains.kotlinx")
+ exclude(group = "org.jetbrains.skiko")
}
+
shadowLib("org.jetbrains.skiko:skiko-awt-runtime-$skikoTarget:$skikoVersion")
+ implementation(compose.components.resources)
+
// DevLogin
modRuntimeOnly("com.ptsmods:devlogin:3.5")
@@ -254,6 +262,14 @@ dependencies {
setupConfigurations()
}
+compose.resources {
+ customDirectory(
+ "main",
+ provider { layout.projectDirectory.dir("src/main/resources/assets/lambda/compose/") }
+ )
+ packageOfResClass = "com.lambda.newui"
+}
+
tasks {
test {
useJUnitPlatform()
diff --git a/gradle.properties b/gradle.properties
index c86cf55e6..a597e98a9 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -20,7 +20,7 @@ modId=lambda
modName=Lambda
modVersion=0.1.0
modDescription=Minecraft utility mod for automation
-modIcon=assets/lambda/textures/lambda.png
+modIcon=assets/lambda/compose/drawable/lambda.png
mavenGroup=com.lambda
modAuthors=Constructor (Avanatiker), Blade, beanbag44, Emy
diff --git a/src/main/java/com/lambda/mixin/render/ConnectScreenMixin.java b/src/main/java/com/lambda/mixin/render/ConnectScreenMixin.java
index 96c877fa1..c9ce6f394 100644
--- a/src/main/java/com/lambda/mixin/render/ConnectScreenMixin.java
+++ b/src/main/java/com/lambda/mixin/render/ConnectScreenMixin.java
@@ -31,7 +31,6 @@
@Mixin(value = ConnectScreen.class)
public class ConnectScreenMixin {
-
@Inject(method = "connect(Lnet/minecraft/client/MinecraftClient;Lnet/minecraft/client/network/ServerAddress;Lnet/minecraft/client/network/ServerInfo;Lnet/minecraft/client/network/CookieStorage;)V", at= @At("HEAD"))
private void connectHead(MinecraftClient client, ServerAddress address, ServerInfo info, CookieStorage cookieStorage, CallbackInfo ci) {
AutoDisconnect.INSTANCE.setLastReconnectTarget(new MultiplayerReconnectTarget(address, info, cookieStorage));
diff --git a/src/main/java/com/lambda/mixin/render/SplashOverlayMixin.java b/src/main/java/com/lambda/mixin/render/SplashOverlayMixin.java
index eb42468e1..5ed4e3f02 100644
--- a/src/main/java/com/lambda/mixin/render/SplashOverlayMixin.java
+++ b/src/main/java/com/lambda/mixin/render/SplashOverlayMixin.java
@@ -45,7 +45,7 @@ public class SplashOverlayMixin {
@Inject(method = "", at = @At("RETURN"))
private void onInit(CallbackInfo ci) {
- LOGO = Identifier.of("lambda", "textures/lambda_banner.png");
+ LOGO = Identifier.of("lambda", "compose/drawable/lambda_banner.png");
}
@WrapOperation(method = "render", at = @At(value = "INVOKE", target = "Ljava/util/function/IntSupplier;getAsInt()I"))
@@ -61,7 +61,7 @@ public LogoTextureMixin(Identifier location) {
@WrapOperation(method = "loadContents", at = @At(value = "INVOKE", target = "Lnet/minecraft/resource/ResourceFactory;open(Lnet/minecraft/util/Identifier;)Ljava/io/InputStream;"))
InputStream wrapLoadTextureData(ResourceFactory instance, Identifier id, Operation original) {
- return LambdaResourceKt.getStream("textures/lambda_banner.png");
+ return LambdaResourceKt.getStream("compose/drawable/lambda_banner.png");
}
}
}
diff --git a/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt b/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt
index edb196076..68022b7b9 100644
--- a/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt
+++ b/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt
@@ -44,9 +44,9 @@ object FontHandler : Loadable, Config(
private val loadedAtlases = ConcurrentHashMap()
private val baseFonts = listOf(
- FontInfo("Minecraft Default", "fonts/MinecraftDefault-Regular.ttf"),
- FontInfo("FiraSans Regular", "fonts/FiraSans-Regular.ttf"),
- FontInfo("FiraSans Bold", "fonts/FiraSans-Bold.ttf")
+ FontInfo("Minecraft Default", "font/MinecraftDefault-Regular.ttf"),
+ FontInfo("FiraSans Regular", "font/FiraSans-Regular.ttf"),
+ FontInfo("FiraSans Bold", "font/FiraSans-Bold.ttf")
)
private val discoveredFonts = baseFonts.toMutableList()
diff --git a/src/main/kotlin/com/lambda/gui/DearImGui.kt b/src/main/kotlin/com/lambda/gui/DearImGui.kt
index fba0d5cf1..ed1ae6493 100644
--- a/src/main/kotlin/com/lambda/gui/DearImGui.kt
+++ b/src/main/kotlin/com/lambda/gui/DearImGui.kt
@@ -67,9 +67,9 @@ object DearImGui : Loadable {
val size = BASE_FONT_SCALE * scale
with(io.fonts) {
clear()
- addFontFromMemoryTTF("fonts/FiraSans-Regular.ttf".stream.readAllBytes(), size, fontConfig, glyphRanges)
- addFontFromMemoryTTF("fonts/FiraSans-Bold.ttf".stream.readAllBytes(), size, fontConfig, glyphRanges)
- addFontFromMemoryTTF("fonts/MinecraftDefault-Regular.ttf".stream.readAllBytes(), size, fontConfig, glyphRanges)
+ addFontFromMemoryTTF("font/FiraSans-Regular.ttf".stream.readAllBytes(), size, fontConfig, glyphRanges)
+ addFontFromMemoryTTF("font/FiraSans-Bold.ttf".stream.readAllBytes(), size, fontConfig, glyphRanges)
+ addFontFromMemoryTTF("font/MinecraftDefault-Regular.ttf".stream.readAllBytes(), size, fontConfig, glyphRanges)
build()
}
implGl3.createFontsTexture()
diff --git a/src/main/kotlin/com/lambda/gui/MenuBar.kt b/src/main/kotlin/com/lambda/gui/MenuBar.kt
index 2b205382b..56023e868 100644
--- a/src/main/kotlin/com/lambda/gui/MenuBar.kt
+++ b/src/main/kotlin/com/lambda/gui/MenuBar.kt
@@ -63,9 +63,9 @@ import java.util.*
object MenuBar {
private var aboutRequested = false
var newConfigName = ""
- val headerLogo = upload("textures/lambda_text_color.png")
- val lambdaLogo = upload("textures/lambda.png")
- val githubLogo = upload("textures/github_logo.png")
+ val headerLogo = upload("drawable/lambda_text_color.png")
+ val lambdaLogo = upload("drawable/lambda.png")
+ val githubLogo = upload("drawable/github_logo.png")
var height = 0f
diff --git a/src/main/kotlin/com/lambda/module/hud/Watermark.kt b/src/main/kotlin/com/lambda/module/hud/Watermark.kt
index fa46da254..8ca398524 100644
--- a/src/main/kotlin/com/lambda/module/hud/Watermark.kt
+++ b/src/main/kotlin/com/lambda/module/hud/Watermark.kt
@@ -29,7 +29,7 @@ object Watermark : HudModule(
tag = ModuleTag.HUD,
enabledByDefault = true,
) {
- private val texture = upload("textures/lambda.png")
+ private val texture = upload("drawable/lambda.png")
private val scale by setting("Scale", 0.15f, 0.01f..1f, 0.01f)
override fun ImGuiBuilder.buildLayout() {
diff --git a/src/main/kotlin/com/lambda/newui/ClickGuiContent.kt b/src/main/kotlin/com/lambda/newui/ClickGuiContent.kt
index 1cddbdce9..510d2ab2b 100644
--- a/src/main/kotlin/com/lambda/newui/ClickGuiContent.kt
+++ b/src/main/kotlin/com/lambda/newui/ClickGuiContent.kt
@@ -17,21 +17,32 @@
package com.lambda.newui
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.lambda.module.tag.ModuleTag
import com.lambda.newui.components.CategoryPanel
import com.lambda.newui.state.LambdaState.observeShownTags
import com.lambda.newui.theme.LambdaTheme
+import org.jetbrains.compose.resources.painterResource
/**
* Top-level composable for the Lambda click GUI.
@@ -45,44 +56,62 @@ import com.lambda.newui.theme.LambdaTheme
*/
@Composable
fun ClickGuiContent() {
- LambdaTheme {
- val shownTags by observeShownTags()
+ LambdaTheme {
+ val shownTags by observeShownTags()
- // Focus history, most recently focused last. Derived from rather than mirroring
- // the shown set, so it tolerates tags appearing and disappearing: stale entries
- // are simply never looked up, and a tag that has never been focused is absent.
- val focusOrder = remember { mutableStateListOf() }
+ // Focus history, most recently focused last. Derived from rather than mirroring
+ // the shown set, so it tolerates tags appearing and disappearing: stale entries
+ // are simply never looked up, and a tag that has never been focused is absent.
+ val focusOrder = remember { mutableStateListOf() }
- // Never-focused tags yield indexOf == -1 and so sort to the front, i.e. render
- // beneath focused ones. sortedBy is stable, so they keep shownTags order among
- // themselves instead of collapsing onto a shared z-index.
- val zIndexOf = shownTags
- .sortedBy { focusOrder.indexOf(it) }
- .withIndex()
- .associate { (index, tag) -> tag to index.toFloat() }
+ // Never-focused tags yield indexOf == -1 and so sort to the front, i.e. render
+ // beneath focused ones. sortedBy is stable, so they keep shownTags order among
+ // themselves instead of collapsing onto a shared z-index.
+ val zIndexOf = shownTags
+ .sortedBy { focusOrder.indexOf(it) }
+ .withIndex()
+ .associate { (index, tag) -> tag to index.toFloat() }
- Row(
- horizontalArrangement = Arrangement.spacedBy(3.dp),
- modifier = Modifier
- .fillMaxSize()
- .padding(6.dp)
- ) {
- // Layout order stays the shown-tag order so panels don't jump around
- // horizontally when one is focused; only zIndex reacts to focus.
- shownTags.forEach { tag ->
- // Keyed so a tag being shown or hidden doesn't shift the panels after it
- // onto the wrong remembered drag offset and expanded state.
- key(tag) {
- CategoryPanel(
- tag = tag,
- zIndex = zIndexOf[tag] ?: 0f,
- onFocus = {
- focusOrder.remove(tag)
- focusOrder.add(tag) // Move to end = highest z-index
- }
- )
- }
- }
- }
- }
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(20.dp)
+ .background(colorScheme.background)
+ .border(BorderStroke(1.dp, colorScheme.outline))
+ ) {
+ Image(
+ painter = painterResource(Res.drawable.lambda),
+ contentDescription = "Lambda logo"
+ )
+ }
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(3.dp, Alignment.CenterHorizontally),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(6.dp)
+ .offset(y = 100.dp),
+ ) {
+ // Layout order stays the shown-tag order so panels don't jump around
+ // horizontally when one is focused; only zIndex reacts to focus.
+ shownTags.forEach { tag ->
+ // Keyed so a tag being shown or hidden doesn't shift the panels after it
+ // onto the wrong remembered drag offset and expanded state.
+ key(tag) {
+ CategoryPanel(
+ tag = tag,
+ zIndex = zIndexOf[tag] ?: 0f,
+ onFocus = {
+ focusOrder.remove(tag)
+ focusOrder.add(tag) // Move to end = highest z-index
+ }
+ )
+ }
+ }
+ }
+ }
+ }
}
diff --git a/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt b/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt
index 340a84315..b2f9489e0 100644
--- a/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt
+++ b/src/main/kotlin/com/lambda/newui/components/CategoryPanel.kt
@@ -43,10 +43,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.isSecondaryPressed
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
@@ -126,6 +128,7 @@ fun CategoryPanel(tag: ModuleTag, zIndex: Float = 0f, onFocus: () -> Unit = {})
lineHeight = 10.sp,
fontWeight = FontWeight.Bold,
color = LambdaColors.OnSurface,
+ style = TextStyle(shadow = Shadow(offset = Offset(2f, 2f)))
)
}
diff --git a/src/main/kotlin/com/lambda/newui/components/ModuleCard.kt b/src/main/kotlin/com/lambda/newui/components/ModuleCard.kt
index cddf68056..6b9746858 100644
--- a/src/main/kotlin/com/lambda/newui/components/ModuleCard.kt
+++ b/src/main/kotlin/com/lambda/newui/components/ModuleCard.kt
@@ -28,6 +28,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -68,7 +71,8 @@ fun ModuleCard(module: Module) {
lineHeight = 9.sp,
color = textColor,
maxLines = 1,
- overflow = TextOverflow.Ellipsis
+ overflow = TextOverflow.Ellipsis,
+ style = TextStyle(shadow = Shadow(offset = Offset(2f, 2f)))
)
}
}
diff --git a/src/main/kotlin/com/lambda/util/LambdaResource.kt b/src/main/kotlin/com/lambda/util/LambdaResource.kt
index 6bfd43650..987251bd4 100644
--- a/src/main/kotlin/com/lambda/util/LambdaResource.kt
+++ b/src/main/kotlin/com/lambda/util/LambdaResource.kt
@@ -29,15 +29,15 @@ import javax.imageio.ImageIO
typealias LambdaResource = String
val LambdaResource.stream: InputStream
- get() = Lambda::class.java.getResourceAsStream("/assets/lambda/$this")
- ?: throw FileNotFoundException("File \"/assets/lambda/$this\" not found")
+ get() = Lambda::class.java.getResourceAsStream("/assets/lambda/compose/$this")
+ ?: throw FileNotFoundException("File \"/assets/lambda/compose/$this\" not found")
val LambdaResource.text: String
- get() = Lambda::class.java.getResourceAsStream("/assets/lambda/$this")?.readAllBytes()?.decodeToString()
- ?: throw FileNotFoundException("File \"/assets/lambda/$this\" not found")
+ get() = Lambda::class.java.getResourceAsStream("/assets/lambda/compose/$this")?.readAllBytes()?.decodeToString()
+ ?: throw FileNotFoundException("File \"/assets/lambda/compose/$this\" not found")
val LambdaResource.url: URL
- get() = Lambda::class.java.getResource("/assets/lambda/$this")
- ?: throw FileNotFoundException("File \"/assets/lambda/$this\" not found")
+ get() = Lambda::class.java.getResource("/assets/lambda/compose/$this")
+ ?: throw FileNotFoundException("File \"/assets/lambda/compose/$this\" not found")
fun LambdaResource.readImage(): BufferedImage = ImageIO.read(this.stream)
diff --git a/src/main/kotlin/com/lambda/util/WindowUtils.kt b/src/main/kotlin/com/lambda/util/WindowUtils.kt
index c8e269a88..9ad83066b 100644
--- a/src/main/kotlin/com/lambda/util/WindowUtils.kt
+++ b/src/main/kotlin/com/lambda/util/WindowUtils.kt
@@ -41,7 +41,7 @@ object WindowUtils {
}
fun setLambdaWindowIcon() {
- val icons = listOf(16, 24, 32, 48, 64, 128, 256).map { "textures/icon/logo_$it.png" }
+ val icons = listOf(16, 24, 32, 48, 64, 128, 256).map { "drawable/icon/logo_$it.png" }
setWindowIcon(*icons.toTypedArray())
}
@@ -53,10 +53,10 @@ object WindowUtils {
* Example:
* ```
* WindowIcons.setWindowIcon(
- * "textures/icon16.png",
- * "textures/icon32.png",
- * "textures/icon48.png",
- * "textures/icon128.png",
+ * "drawable/icon16.png",
+ * "drawable/icon32.png",
+ * "drawable/icon48.png",
+ * "drawable/icon128.png",
* )
* ```
*/
diff --git a/src/main/resources/assets/lambda/chika.gif b/src/main/resources/assets/lambda/chika.gif
deleted file mode 100644
index 2def218eca631e2483daa5033ff3fd5a9204b1a3..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 452195
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