From 3eec84b5b7e796899c81335da3752b844f231604 Mon Sep 17 00:00:00 2001 From: forbai Date: Wed, 11 Feb 2026 09:41:53 +0100 Subject: [PATCH 01/10] feat(hud): add settings support to HudModule base class --- .../kotlin/org/cobalt/api/hud/HudModule.kt | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/main/kotlin/org/cobalt/api/hud/HudModule.kt diff --git a/src/main/kotlin/org/cobalt/api/hud/HudModule.kt b/src/main/kotlin/org/cobalt/api/hud/HudModule.kt new file mode 100644 index 0000000..581237c --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/hud/HudModule.kt @@ -0,0 +1,58 @@ +package org.cobalt.api.hud + +import org.cobalt.api.module.setting.Setting + +abstract class HudModule( + val id: String, + val name: String, + val description: String = "", +) { + + var enabled: Boolean = true + var anchor: HudAnchor = HudAnchor.TOP_LEFT + var offsetX: Float = 10f + var offsetY: Float = 10f + var scale: Float = 1.0f + + protected open val defaultAnchor: HudAnchor = HudAnchor.TOP_LEFT + protected open val defaultOffsetX: Float = 10f + protected open val defaultOffsetY: Float = 10f + protected open val defaultScale: Float = 1.0f + + private val settingsList = mutableListOf>() + + fun addSetting(vararg settings: Setting<*>) { + settingsList.addAll(listOf(*settings)) + } + + fun getSettings(): List> { + return settingsList + } + + abstract fun getBaseWidth(): Float + abstract fun getBaseHeight(): Float + abstract fun render(screenX: Float, screenY: Float, scale: Float) + + fun getScaledWidth(): Float = getBaseWidth() * scale + fun getScaledHeight(): Float = getBaseHeight() * scale + + fun getScreenPosition(screenWidth: Float, screenHeight: Float): Pair = + anchor.computeScreenPosition( + offsetX, offsetY, + getScaledWidth(), getScaledHeight(), + screenWidth, screenHeight + ) + + fun resetPosition() { + anchor = defaultAnchor + offsetX = defaultOffsetX + offsetY = defaultOffsetY + scale = defaultScale + } + + fun containsPoint(px: Float, py: Float, screenWidth: Float, screenHeight: Float): Boolean { + val (sx, sy) = getScreenPosition(screenWidth, screenHeight) + return px >= sx && px <= sx + getScaledWidth() && + py >= sy && py <= sy + getScaledHeight() + } +} From c99ef467009e0df098c3df10f91c3ac48de70753 Mon Sep 17 00:00:00 2001 From: forbai Date: Wed, 11 Feb 2026 09:44:11 +0100 Subject: [PATCH 02/10] feat(hud): add configurable settings to WatermarkModule --- .../cobalt/api/hud/modules/WatermarkModule.kt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/main/kotlin/org/cobalt/api/hud/modules/WatermarkModule.kt diff --git a/src/main/kotlin/org/cobalt/api/hud/modules/WatermarkModule.kt b/src/main/kotlin/org/cobalt/api/hud/modules/WatermarkModule.kt new file mode 100644 index 0000000..a6579b6 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/hud/modules/WatermarkModule.kt @@ -0,0 +1,51 @@ +package org.cobalt.api.hud.modules + +import org.cobalt.api.hud.HudAnchor +import org.cobalt.api.hud.HudModule +import org.cobalt.api.module.setting.impl.TextSetting +import org.cobalt.api.module.setting.impl.ColorSetting +import org.cobalt.api.module.setting.impl.CheckboxSetting +import org.cobalt.api.ui.theme.ThemeManager +import org.cobalt.api.util.ui.NVGRenderer + +class WatermarkModule : HudModule( + id = "watermark", + name = "Watermark", + description = "Displays Cobalt branding", +) { + + override val defaultAnchor = HudAnchor.TOP_LEFT + override val defaultOffsetX = 10f + override val defaultOffsetY = 10f + override val defaultScale = 1.0f + + private val textSize = 18f + + private val textSetting = TextSetting("Text", "Display text", "Cobalt") + private val colorSetting = ColorSetting("Color", "Text color", ThemeManager.currentTheme.accent) + private val shadowSetting = CheckboxSetting("Shadow", "Show text shadow", false) + private val backgroundSetting = CheckboxSetting("Background", "Show background box", false) + + init { + addSetting(textSetting, colorSetting, shadowSetting, backgroundSetting) + resetPosition() + } + + override fun getBaseWidth(): Float = NVGRenderer.textWidth(textSetting.value, textSize) + (if (backgroundSetting.value) 16f else 0f) + + override fun getBaseHeight(): Float = textSize + (if (backgroundSetting.value) 12f else 4f) + + override fun render(screenX: Float, screenY: Float, scale: Float) { + if (backgroundSetting.value) { + NVGRenderer.rect(screenX - 8f, screenY - 6f, + getBaseWidth(), getBaseHeight(), + ThemeManager.currentTheme.panel, 6f) + } + + if (shadowSetting.value) { + NVGRenderer.textShadow(textSetting.value, screenX, screenY, textSize, colorSetting.value) + } else { + NVGRenderer.text(textSetting.value, screenX, screenY, textSize, colorSetting.value) + } + } +} From bc245614e9cd00bccd8ffb78c87b9fce69a23996 Mon Sep 17 00:00:00 2001 From: forbai Date: Wed, 11 Feb 2026 09:47:05 +0100 Subject: [PATCH 03/10] feat(hud): persist module settings in HudConfig --- .../org/cobalt/internal/helper/HudConfig.kt | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/main/kotlin/org/cobalt/internal/helper/HudConfig.kt diff --git a/src/main/kotlin/org/cobalt/internal/helper/HudConfig.kt b/src/main/kotlin/org/cobalt/internal/helper/HudConfig.kt new file mode 100644 index 0000000..e42b73a --- /dev/null +++ b/src/main/kotlin/org/cobalt/internal/helper/HudConfig.kt @@ -0,0 +1,83 @@ +package org.cobalt.internal.helper + +import com.google.gson.GsonBuilder +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import java.io.File +import net.minecraft.client.Minecraft +import org.cobalt.api.hud.HudAnchor +import org.cobalt.api.hud.HudModuleManager + +internal object HudConfig { + + private val mc: Minecraft = Minecraft.getInstance() + private val gson = GsonBuilder().setPrettyPrinting().create() + private val hudFile = File(mc.gameDirectory, "config/cobalt/hud.json") + + fun load() { + if (!hudFile.exists()) { + hudFile.parentFile?.mkdirs() + hudFile.createNewFile() + return + } + + val text = hudFile.bufferedReader().use { it.readText() } + if (text.isEmpty()) return + + runCatching { + val root = JsonParser.parseString(text).asJsonObject + val modulesObj = root.getAsJsonObject("modules") ?: return + + for ((id, element) in modulesObj.entrySet()) { + val module = HudModuleManager.getModule(id) ?: continue + val obj = element.asJsonObject + + module.enabled = obj.get("enabled")?.asBoolean ?: true + module.anchor = obj.get("anchor")?.asString?.let { + runCatching { HudAnchor.valueOf(it) }.getOrNull() + } ?: HudAnchor.TOP_LEFT + module.offsetX = obj.get("offsetX")?.asFloat ?: 10f + module.offsetY = obj.get("offsetY")?.asFloat ?: 10f + module.scale = obj.get("scale")?.asFloat?.coerceIn(0.5f, 3.0f) ?: 1.0f + + // Load settings + val settingsObj = obj.getAsJsonObject("settings") + if (settingsObj != null) { + module.getSettings().forEach { setting -> + settingsObj.get(setting.name)?.let { element -> + runCatching { setting.read(element) } + } + } + } + } + } + } + + fun save() { + val root = JsonObject() + val modulesObj = JsonObject() + + HudModuleManager.getModules().forEach { module -> + val obj = JsonObject().apply { + addProperty("enabled", module.enabled) + addProperty("anchor", module.anchor.name) + addProperty("offsetX", module.offsetX) + addProperty("offsetY", module.offsetY) + addProperty("scale", module.scale) + + // Add settings persistence + val settingsObj = JsonObject() + module.getSettings().forEach { setting -> + settingsObj.add(setting.name, setting.write()) + } + add("settings", settingsObj) + } + modulesObj.add(module.id, obj) + } + + root.add("modules", modulesObj) + + hudFile.parentFile?.mkdirs() + hudFile.bufferedWriter().use { it.write(gson.toJson(root)) } + } +} From bf8d9f7c0347a4ff81d5222d26044af6fe7d77bf Mon Sep 17 00:00:00 2001 From: forbai Date: Wed, 11 Feb 2026 10:22:21 +0100 Subject: [PATCH 04/10] feat(hud): implement HUD module settings management and editor UI --- src/main/kotlin/org/cobalt/Cobalt.kt | 5 +- .../kotlin/org/cobalt/api/hud/HudAnchor.kt | 36 +++ .../kotlin/org/cobalt/api/hud/HudModule.kt | 7 +- .../org/cobalt/api/hud/HudModuleManager.kt | 62 ++++ .../cobalt/api/hud/modules/WatermarkModule.kt | 23 +- .../kotlin/org/cobalt/api/module/Module.kt | 7 +- .../org/cobalt/api/module/setting/Setting.kt | 9 +- .../api/module/setting/SettingsContainer.kt | 9 + .../internal/ui/hud/HudSettingsPopup.kt | 154 ++++++++++ .../org/cobalt/internal/ui/hud/SnapHelper.kt | 95 ++++++ .../internal/ui/panel/panels/UIHudList.kt | 254 ++++++++++++++++ .../internal/ui/panel/panels/UISidebar.kt | 18 +- .../org/cobalt/internal/ui/screen/UIConfig.kt | 2 + .../cobalt/internal/ui/screen/UIHudEditor.kt | 285 ++++++++++++++++++ 14 files changed, 940 insertions(+), 26 deletions(-) create mode 100644 src/main/kotlin/org/cobalt/api/hud/HudAnchor.kt create mode 100644 src/main/kotlin/org/cobalt/api/hud/HudModuleManager.kt create mode 100644 src/main/kotlin/org/cobalt/api/module/setting/SettingsContainer.kt create mode 100644 src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt create mode 100644 src/main/kotlin/org/cobalt/internal/ui/hud/SnapHelper.kt create mode 100644 src/main/kotlin/org/cobalt/internal/ui/panel/panels/UIHudList.kt create mode 100644 src/main/kotlin/org/cobalt/internal/ui/screen/UIHudEditor.kt diff --git a/src/main/kotlin/org/cobalt/Cobalt.kt b/src/main/kotlin/org/cobalt/Cobalt.kt index fde274b..d93dc50 100644 --- a/src/main/kotlin/org/cobalt/Cobalt.kt +++ b/src/main/kotlin/org/cobalt/Cobalt.kt @@ -3,12 +3,14 @@ package org.cobalt import net.fabricmc.api.ClientModInitializer import org.cobalt.api.command.CommandManager import org.cobalt.api.event.EventBus +import org.cobalt.api.hud.HudModuleManager import org.cobalt.api.module.ModuleManager import org.cobalt.api.notification.NotificationManager import org.cobalt.api.rotation.RotationExecutor import org.cobalt.api.util.TickScheduler import org.cobalt.internal.command.MainCommand import org.cobalt.internal.helper.Config +import org.cobalt.internal.helper.HudConfig import org.cobalt.internal.loader.AddonLoader @Suppress("UNUSED") @@ -25,10 +27,11 @@ object Cobalt : ClientModInitializer { listOf( TickScheduler, MainCommand, NotificationManager, - RotationExecutor, + RotationExecutor, HudModuleManager, ).forEach { EventBus.register(it) } Config.loadModulesConfig() + HudConfig.load() EventBus.register(this) println("Cobalt Mod Initialized") } diff --git a/src/main/kotlin/org/cobalt/api/hud/HudAnchor.kt b/src/main/kotlin/org/cobalt/api/hud/HudAnchor.kt new file mode 100644 index 0000000..c8fd856 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/hud/HudAnchor.kt @@ -0,0 +1,36 @@ +package org.cobalt.api.hud + +enum class HudAnchor { + TOP_LEFT, + TOP_CENTER, + TOP_RIGHT, + CENTER_LEFT, + CENTER, + CENTER_RIGHT, + BOTTOM_LEFT, + BOTTOM_CENTER, + BOTTOM_RIGHT; + + fun computeScreenPosition( + offsetX: Float, + offsetY: Float, + moduleWidth: Float, + moduleHeight: Float, + screenWidth: Float, + screenHeight: Float, + ): Pair { + val x = when (this) { + TOP_LEFT, CENTER_LEFT, BOTTOM_LEFT -> offsetX + TOP_CENTER, CENTER, BOTTOM_CENTER -> screenWidth / 2f - moduleWidth / 2f + offsetX + TOP_RIGHT, CENTER_RIGHT, BOTTOM_RIGHT -> screenWidth - moduleWidth - offsetX + } + + val y = when (this) { + TOP_LEFT, TOP_CENTER, TOP_RIGHT -> offsetY + CENTER_LEFT, CENTER, CENTER_RIGHT -> screenHeight / 2f - moduleHeight / 2f + offsetY + BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT -> screenHeight - moduleHeight - offsetY + } + + return Pair(x, y) + } +} diff --git a/src/main/kotlin/org/cobalt/api/hud/HudModule.kt b/src/main/kotlin/org/cobalt/api/hud/HudModule.kt index 581237c..c8d2908 100644 --- a/src/main/kotlin/org/cobalt/api/hud/HudModule.kt +++ b/src/main/kotlin/org/cobalt/api/hud/HudModule.kt @@ -1,12 +1,13 @@ package org.cobalt.api.hud import org.cobalt.api.module.setting.Setting +import org.cobalt.api.module.setting.SettingsContainer abstract class HudModule( val id: String, val name: String, val description: String = "", -) { +) : SettingsContainer { var enabled: Boolean = true var anchor: HudAnchor = HudAnchor.TOP_LEFT @@ -21,11 +22,11 @@ abstract class HudModule( private val settingsList = mutableListOf>() - fun addSetting(vararg settings: Setting<*>) { + override fun addSetting(vararg settings: Setting<*>) { settingsList.addAll(listOf(*settings)) } - fun getSettings(): List> { + override fun getSettings(): List> { return settingsList } diff --git a/src/main/kotlin/org/cobalt/api/hud/HudModuleManager.kt b/src/main/kotlin/org/cobalt/api/hud/HudModuleManager.kt new file mode 100644 index 0000000..814a0f3 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/hud/HudModuleManager.kt @@ -0,0 +1,62 @@ +package org.cobalt.api.hud + +import net.minecraft.client.Minecraft +import org.cobalt.api.event.annotation.SubscribeEvent +import org.cobalt.api.event.impl.render.NvgEvent +import org.cobalt.api.hud.modules.WatermarkModule +import org.cobalt.api.util.ui.NVGRenderer + +object HudModuleManager { + + private val mc: Minecraft = Minecraft.getInstance() + private val modules = mutableListOf() + + @Volatile + var isEditorOpen: Boolean = false + + init { + register(WatermarkModule()) + } + + fun register(module: HudModule) { + if (modules.none { it.id == module.id }) { + modules.add(module) + } + } + + fun unregister(id: String) { + modules.removeIf { it.id == id } + } + + fun getModules(): List = modules.toList() + + fun getModule(id: String): HudModule? = modules.find { it.id == id } + + fun resetAllPositions() { + modules.forEach { it.resetPosition() } + } + + @Suppress("unused") + @SubscribeEvent + fun onRender(event: NvgEvent) { + if (mc.screen != null && !isEditorOpen) return + + val window = mc.window + val screenWidth = window.screenWidth.toFloat() + val screenHeight = window.screenHeight.toFloat() + + NVGRenderer.beginFrame(screenWidth, screenHeight) + + modules.filter { it.enabled }.forEach { module -> + val (screenX, screenY) = module.getScreenPosition(screenWidth, screenHeight) + + NVGRenderer.push() + NVGRenderer.translate(screenX, screenY) + NVGRenderer.scale(module.scale, module.scale) + module.render(0f, 0f, module.scale) + NVGRenderer.pop() + } + + NVGRenderer.endFrame() + } +} diff --git a/src/main/kotlin/org/cobalt/api/hud/modules/WatermarkModule.kt b/src/main/kotlin/org/cobalt/api/hud/modules/WatermarkModule.kt index a6579b6..a563a3f 100644 --- a/src/main/kotlin/org/cobalt/api/hud/modules/WatermarkModule.kt +++ b/src/main/kotlin/org/cobalt/api/hud/modules/WatermarkModule.kt @@ -20,32 +20,31 @@ class WatermarkModule : HudModule( override val defaultScale = 1.0f private val textSize = 18f - - private val textSetting = TextSetting("Text", "Display text", "Cobalt") - private val colorSetting = ColorSetting("Color", "Text color", ThemeManager.currentTheme.accent) - private val shadowSetting = CheckboxSetting("Shadow", "Show text shadow", false) - private val backgroundSetting = CheckboxSetting("Background", "Show background box", false) + + private var text by TextSetting("Text", "Display text", "Cobalt") + private var color by ColorSetting("Color", "Text color", ThemeManager.currentTheme.accent) + private var shadow by CheckboxSetting("Shadow", "Show text shadow", false) + private var background by CheckboxSetting("Background", "Show background box", false) init { - addSetting(textSetting, colorSetting, shadowSetting, backgroundSetting) resetPosition() } - override fun getBaseWidth(): Float = NVGRenderer.textWidth(textSetting.value, textSize) + (if (backgroundSetting.value) 16f else 0f) + override fun getBaseWidth(): Float = NVGRenderer.textWidth(text, textSize) + (if (background) 16f else 0f) - override fun getBaseHeight(): Float = textSize + (if (backgroundSetting.value) 12f else 4f) + override fun getBaseHeight(): Float = textSize + (if (background) 12f else 4f) override fun render(screenX: Float, screenY: Float, scale: Float) { - if (backgroundSetting.value) { + if (background) { NVGRenderer.rect(screenX - 8f, screenY - 6f, getBaseWidth(), getBaseHeight(), ThemeManager.currentTheme.panel, 6f) } - if (shadowSetting.value) { - NVGRenderer.textShadow(textSetting.value, screenX, screenY, textSize, colorSetting.value) + if (shadow) { + NVGRenderer.textShadow(text, screenX, screenY, textSize, color) } else { - NVGRenderer.text(textSetting.value, screenX, screenY, textSize, colorSetting.value) + NVGRenderer.text(text, screenX, screenY, textSize, color) } } } diff --git a/src/main/kotlin/org/cobalt/api/module/Module.kt b/src/main/kotlin/org/cobalt/api/module/Module.kt index e70f2a4..e69d42d 100644 --- a/src/main/kotlin/org/cobalt/api/module/Module.kt +++ b/src/main/kotlin/org/cobalt/api/module/Module.kt @@ -1,16 +1,17 @@ package org.cobalt.api.module import org.cobalt.api.module.setting.Setting +import org.cobalt.api.module.setting.SettingsContainer -abstract class Module(val name: String) { +abstract class Module(val name: String) : SettingsContainer { private val settingsList = mutableListOf>() - fun addSetting(vararg settings: Setting<*>) { + override fun addSetting(vararg settings: Setting<*>) { settingsList.addAll(listOf(*settings)) } - fun getSettings(): List> { + override fun getSettings(): List> { return settingsList } diff --git a/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt b/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt index 9ecd9ec..b967aec 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt @@ -4,24 +4,23 @@ import com.google.gson.JsonElement import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty -import org.cobalt.api.module.Module abstract class Setting( val name: String, val description: String, var value: T, -) : ReadWriteProperty, PropertyDelegateProvider> { +) : ReadWriteProperty, PropertyDelegateProvider> { - override operator fun provideDelegate(thisRef: Module, property: KProperty<*>): ReadWriteProperty { + override operator fun provideDelegate(thisRef: SettingsContainer, property: KProperty<*>): ReadWriteProperty { thisRef.addSetting(this) return this } - override operator fun getValue(thisRef: Module, property: KProperty<*>): T { + override operator fun getValue(thisRef: SettingsContainer, property: KProperty<*>): T { return value } - override operator fun setValue(thisRef: Module, property: KProperty<*>, value: T) { + override operator fun setValue(thisRef: SettingsContainer, property: KProperty<*>, value: T) { this.value = value } diff --git a/src/main/kotlin/org/cobalt/api/module/setting/SettingsContainer.kt b/src/main/kotlin/org/cobalt/api/module/setting/SettingsContainer.kt new file mode 100644 index 0000000..1e07ee7 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/module/setting/SettingsContainer.kt @@ -0,0 +1,9 @@ +package org.cobalt.api.module.setting + +interface SettingsContainer { + + fun addSetting(vararg settings: Setting<*>) + + fun getSettings(): List> + +} diff --git a/src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt b/src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt new file mode 100644 index 0000000..9b46db4 --- /dev/null +++ b/src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt @@ -0,0 +1,154 @@ +package org.cobalt.internal.ui.hud + +import org.cobalt.api.hud.HudModule +import org.cobalt.api.ui.theme.ThemeManager +import org.cobalt.api.util.ui.NVGRenderer +import org.cobalt.internal.ui.animation.ColorAnimation +import org.cobalt.internal.ui.util.isHoveringOver + +internal class HudSettingsPopup { + var visible: Boolean = false + var module: HudModule? = null + var popupX: Float = 0f + var popupY: Float = 0f + + private val popupWidth = 180f + private val popupPadding = 12f + private val rowHeight = 28f + private val headerHeight = 26f + private val toggleAnim = ColorAnimation(150L) + private val buttonAnim = ColorAnimation(150L) + + private val popupHeight: Float + get() = popupPadding * 2f + headerHeight + rowHeight + rowHeight + 8f + + fun show(module: HudModule, x: Float, y: Float) { + this.module = module + popupX = x + popupY = y + visible = true + } + + fun hide() { + visible = false + module = null + } + + fun render() { + if (!visible) return + val target = module ?: return + + NVGRenderer.rect(popupX, popupY, popupWidth, popupHeight, ThemeManager.currentTheme.panel, 8f) + NVGRenderer.hollowRect(popupX, popupY, popupWidth, popupHeight, 1f, ThemeManager.currentTheme.controlBorder, 8f) + + NVGRenderer.text( + target.name, + popupX + popupPadding, + popupY + popupPadding + 2f, + 14f, + ThemeManager.currentTheme.accent + ) + + val toggleY = popupY + popupPadding + headerHeight + val toggleText = if (target.enabled) "Disable" else "Enable" + val toggleWidth = NVGRenderer.textWidth(toggleText, 12f) + 30f + val toggleX = popupX + popupWidth - toggleWidth - popupPadding + val isToggleHover = isHoveringOver(toggleX, toggleY, toggleWidth, rowHeight - 6f) + val toggleBg = toggleAnim.get( + ThemeManager.currentTheme.controlBg, + ThemeManager.currentTheme.selectedOverlay, + !isToggleHover + ) + val toggleBorder = toggleAnim.get( + ThemeManager.currentTheme.controlBorder, + ThemeManager.currentTheme.accent, + !isToggleHover + ) + + NVGRenderer.text( + "Enabled", + popupX + popupPadding, + toggleY + 6f, + 12f, + ThemeManager.currentTheme.textSecondary + ) + NVGRenderer.rect(toggleX, toggleY, toggleWidth, rowHeight - 6f, toggleBg, 10f) + NVGRenderer.hollowRect(toggleX, toggleY, toggleWidth, rowHeight - 6f, 1.5f, toggleBorder, 10f) + NVGRenderer.text( + toggleText, + toggleX + 12f, + toggleY + 5f, + 12f, + ThemeManager.currentTheme.textPrimary + ) + + val resetY = toggleY + rowHeight + 6f + val resetHover = isHoveringOver(popupX + popupPadding, resetY, popupWidth - popupPadding * 2f, rowHeight - 6f) + val resetBg = buttonAnim.get( + ThemeManager.currentTheme.controlBg, + ThemeManager.currentTheme.selectedOverlay, + !resetHover + ) + val resetBorder = buttonAnim.get( + ThemeManager.currentTheme.controlBorder, + ThemeManager.currentTheme.accent, + !resetHover + ) + + NVGRenderer.rect( + popupX + popupPadding, + resetY, + popupWidth - popupPadding * 2f, + rowHeight - 6f, + resetBg, + 8f + ) + NVGRenderer.hollowRect( + popupX + popupPadding, + resetY, + popupWidth - popupPadding * 2f, + rowHeight - 6f, + 1.5f, + resetBorder, + 8f + ) + NVGRenderer.text( + "Reset Position", + popupX + popupPadding + 10f, + resetY + 5f, + 12f, + ThemeManager.currentTheme.textPrimary + ) + } + + fun mouseClicked(mouseX: Float, mouseY: Float, button: Int): Boolean { + if (!visible) return false + val target = module ?: return false + if (button != 0) return false + + val toggleY = popupY + popupPadding + headerHeight + val toggleText = if (target.enabled) "Disable" else "Enable" + val toggleWidth = NVGRenderer.textWidth(toggleText, 12f) + 30f + val toggleX = popupX + popupWidth - toggleWidth - popupPadding + + if (mouseX >= toggleX && mouseX <= toggleX + toggleWidth && mouseY >= toggleY && mouseY <= toggleY + rowHeight - 6f) { + target.enabled = !target.enabled + toggleAnim.start() + return true + } + + val resetY = toggleY + rowHeight + 6f + if (mouseX >= popupX + popupPadding && mouseX <= popupX + popupWidth - popupPadding && mouseY >= resetY && mouseY <= resetY + rowHeight - 6f) { + target.resetPosition() + buttonAnim.start() + return true + } + + return containsPoint(mouseX, mouseY) + } + + fun containsPoint(px: Float, py: Float): Boolean { + if (!visible) return false + return px >= popupX && px <= popupX + popupWidth && py >= popupY && py <= popupY + popupHeight + } +} diff --git a/src/main/kotlin/org/cobalt/internal/ui/hud/SnapHelper.kt b/src/main/kotlin/org/cobalt/internal/ui/hud/SnapHelper.kt new file mode 100644 index 0000000..cccd6c3 --- /dev/null +++ b/src/main/kotlin/org/cobalt/internal/ui/hud/SnapHelper.kt @@ -0,0 +1,95 @@ +package org.cobalt.internal.ui.hud + +import kotlin.math.abs +import kotlin.math.round + +internal class SnapHelper( + private val gridSize: Float = 10f, + private val snapThreshold: Float = 5f, +) { + data class GuideLine(val isVertical: Boolean, val position: Float) + + var activeGuides: List = emptyList() + private set + + fun clearGuides() { + activeGuides = emptyList() + } + + fun snapToGrid(x: Float, y: Float): Pair { + val snappedX = round(x / gridSize) * gridSize + val snappedY = round(y / gridSize) * gridSize + return snappedX to snappedY + } + + fun findAlignmentGuides( + moduleX: Float, + moduleY: Float, + moduleW: Float, + moduleH: Float, + screenWidth: Float, + screenHeight: Float, + otherModuleBounds: List, + ): Pair { + val left = moduleX + val right = moduleX + moduleW + val centerX = moduleX + moduleW / 2f + val top = moduleY + val bottom = moduleY + moduleH + val centerY = moduleY + moduleH / 2f + + val xTargets = mutableListOf(0f, screenWidth / 2f, screenWidth) + val yTargets = mutableListOf(0f, screenHeight / 2f, screenHeight) + + otherModuleBounds.forEach { bounds -> + xTargets.add(bounds.x) + xTargets.add(bounds.x + bounds.w) + xTargets.add(bounds.x + bounds.w / 2f) + yTargets.add(bounds.y) + yTargets.add(bounds.y + bounds.h) + yTargets.add(bounds.y + bounds.h / 2f) + } + + var snappedX = moduleX + var snappedY = moduleY + var bestXDiff = snapThreshold + 1f + var bestYDiff = snapThreshold + 1f + var xGuide: GuideLine? = null + var yGuide: GuideLine? = null + + fun checkX(target: Float, edge: Float, newX: Float) { + val diff = abs(edge - target) + if (diff <= snapThreshold && diff < bestXDiff) { + bestXDiff = diff + snappedX = newX + xGuide = GuideLine(true, target) + } + } + + fun checkY(target: Float, edge: Float, newY: Float) { + val diff = abs(edge - target) + if (diff <= snapThreshold && diff < bestYDiff) { + bestYDiff = diff + snappedY = newY + yGuide = GuideLine(false, target) + } + } + + xTargets.forEach { target -> + checkX(target, left, target) + checkX(target, centerX, target - moduleW / 2f) + checkX(target, right, target - moduleW) + } + + yTargets.forEach { target -> + checkY(target, top, target) + checkY(target, centerY, target - moduleH / 2f) + checkY(target, bottom, target - moduleH) + } + + activeGuides = listOfNotNull(xGuide, yGuide) + return snappedX to snappedY + } + + data class ModuleBounds(val x: Float, val y: Float, val w: Float, val h: Float) +} diff --git a/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UIHudList.kt b/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UIHudList.kt new file mode 100644 index 0000000..baad328 --- /dev/null +++ b/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UIHudList.kt @@ -0,0 +1,254 @@ +package org.cobalt.internal.ui.panel.panels + +import java.awt.Color +import org.cobalt.api.hud.HudModule +import org.cobalt.api.hud.HudModuleManager +import org.cobalt.api.ui.theme.ThemeManager +import org.cobalt.api.util.ui.NVGRenderer +import org.cobalt.internal.ui.UIComponent +import org.cobalt.internal.ui.components.UITopbar +import org.cobalt.internal.ui.panel.UIPanel +import org.cobalt.internal.ui.screen.UIHudEditor +import org.cobalt.internal.ui.util.ScrollHandler +import org.cobalt.internal.ui.util.isHoveringOver +import org.cobalt.internal.ui.components.settings.* +import org.cobalt.api.module.setting.impl.* + +internal class UIHudList : UIPanel( + x = 0F, + y = 0F, + width = 890F, + height = 600F +) { + + private val topBar = UITopbar("HUD Modules") + private val allEntries = HudModuleManager.getModules().map { HudModuleEntry(it) } + private var entries = allEntries + + private val scrollHandler = ScrollHandler() + + private val editButton = ActionButton( + label = "Edit HUD", + width = 170F, + height = 40F, + background = { ThemeManager.currentTheme.accent }, + textColor = { ThemeManager.currentTheme.textOnAccent } + ) { + UIHudEditor().openUI() + } + + private val resetButton = ActionButton( + label = "Reset All", + width = 140F, + height = 40F, + background = { ThemeManager.currentTheme.controlBg }, + textColor = { ThemeManager.currentTheme.text } + ) { + HudModuleManager.resetAllPositions() + } + + init { + components.addAll(allEntries) + components.add(editButton) + components.add(resetButton) + components.add(topBar) + + topBar.searchChanged { searchText -> + entries = if (searchText.isEmpty()) { + allEntries + } else { + val searchLower = searchText.lowercase() + allEntries.filter { + it.module.name.lowercase().contains(searchLower) || + it.module.description.lowercase().contains(searchLower) + } + } + } + } + + override fun render() { + NVGRenderer.rect(x, y, width, height, ThemeManager.currentTheme.background, 10F) + + topBar + .updateBounds(x, y) + .render() + + val buttonsY = y + topBar.height + 15F + val buttonGap = 12F + val totalButtonsWidth = editButton.width + resetButton.width + buttonGap + val buttonsStartX = x + width / 2F - totalButtonsWidth / 2F + + editButton + .updateBounds(buttonsStartX, buttonsY) + .render() + + resetButton + .updateBounds(buttonsStartX + editButton.width + buttonGap, buttonsY) + .render() + + val listStartY = buttonsY + editButton.height + 20F + val visibleHeight = height - (listStartY - y) + val entryGap = 10F + val contentHeight = if (entries.isEmpty()) 0F else { + entries.sumOf { it.height.toDouble() }.toFloat() + (entries.size - 1) * entryGap + } + + scrollHandler.setMaxScroll(contentHeight + 20F, visibleHeight) + NVGRenderer.pushScissor(x, listStartY, width, visibleHeight) + + val scrollOffset = scrollHandler.getOffset() + entries.forEachIndexed { index, entry -> + val entryY = listStartY + 10F + index * (entry.height + entryGap) - scrollOffset + entry.updateBounds(x, entryY) + entry.render() + } + + NVGRenderer.popScissor() + } + + override fun mouseScrolled(horizontalAmount: Double, verticalAmount: Double): Boolean { + if (isHoveringOver(x, y, width, height)) { + scrollHandler.handleScroll(verticalAmount) + return true + } + + return false + } + + private class ActionButton( + private val label: String, + width: Float, + height: Float, + private val background: () -> Int, + private val textColor: () -> Int, + private val onClick: () -> Unit, + ) : UIComponent(0F, 0F, width, height) { + + override fun render() { + val hovering = isHoveringOver(x, y, width, height) + val baseColor = background() + val color = if (hovering) Color(baseColor).darker().rgb else baseColor + + NVGRenderer.rect(x, y, width, height, color, 8F) + NVGRenderer.text( + label, + x + width / 2F - NVGRenderer.textWidth(label, 14F) / 2F, + y + height / 2F - 7F, + 14F, + textColor() + ) + } + + override fun mouseClicked(button: Int): Boolean { + if (button == 0 && isHoveringOver(x, y, width, height)) { + onClick() + return true + } + return false + } + } + + private class HudModuleEntry( + val module: HudModule, + ) : UIComponent(0F, 0F, 890F, 60F) { + + private var expanded = false + private val baseHeight = 60F + private val settings = module.getSettings().map { + when (it) { + is CheckboxSetting -> UICheckboxSetting(it) + is ColorSetting -> UIColorSetting(it) + else -> UITextSetting(it as TextSetting) + } + } + + private fun computeHeight(): Float { + return if (expanded && settings.isNotEmpty()) { + baseHeight + settings.size * 70F + } else { + baseHeight + } + } + + override fun render() { + height = computeHeight() + val theme = ThemeManager.currentTheme + NVGRenderer.rect(x, y, width, height, theme.controlBg, 8F) + + NVGRenderer.text( + module.name, + x + 20F, + y + 18F, + 14F, + theme.text + ) + + NVGRenderer.text( + module.description, + x + 20F, + y + 36F, + 12F, + theme.textSecondary + ) + + val toggleWidth = 40F + val toggleHeight = 22F + val toggleX = x + width - 20F - toggleWidth + val toggleY = y + height / 2F - toggleHeight / 2F + val toggleColor = if (module.enabled) theme.accent else theme.controlBg + + NVGRenderer.rect(toggleX, toggleY, toggleWidth, toggleHeight, toggleColor, toggleHeight / 2F) + + val knobRadius = 9F + val knobX = if (module.enabled) { + toggleX + toggleWidth - 11F + } else { + toggleX + 11F + } + val knobY = toggleY + toggleHeight / 2F + NVGRenderer.circle(knobX, knobY, knobRadius, theme.textOnAccent) + + NVGRenderer.line(x, y + height, x + width, y + height, 1F, theme.moduleDivider) + + if (expanded && settings.isNotEmpty()) { + var settingY = y + baseHeight + 10F + settings.forEach { setting -> + setting.updateBounds(x + 20F, settingY) + setting.width = width - 40F + setting.height = 60F + setting.render() + settingY += 70F + } + } + } + + override fun mouseClicked(button: Int): Boolean { + if (button == 0) { + val toggleWidth = 40F + val toggleHeight = 22F + val toggleX = x + width - 20F - toggleWidth + val toggleY = y + baseHeight / 2F - toggleHeight / 2F + + if (isHoveringOver(toggleX, toggleY, toggleWidth, toggleHeight)) { + module.enabled = !module.enabled + return true + } + + if (isHoveringOver(x, y, width, baseHeight)) { + expanded = !expanded + return true + } + } + + if (expanded && button == 0) { + for (setting in settings) { + if (setting.mouseClicked(button)) { + return true + } + } + } + + return false + } + } +} diff --git a/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt b/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt index b901c2f..0711bee 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt @@ -10,6 +10,7 @@ import org.cobalt.internal.ui.components.tooltips.UITooltip import org.cobalt.internal.ui.components.tooltips.impl.UITextTooltip import org.cobalt.internal.ui.panel.UIPanel import org.cobalt.internal.ui.screen.UIConfig +import org.cobalt.internal.ui.screen.UIHudEditor import org.cobalt.internal.ui.util.isHoveringOver internal class UISidebar : UIPanel( @@ -21,8 +22,16 @@ internal class UISidebar : UIPanel( private val moduleButton = UIButton("/assets/cobalt/icons/box.svg") { UIConfig.swapBodyPanel(UIAddonList()) + isHudActive = false } + private val hudButton = UIButton("/assets/cobalt/icons/palette.svg") { + UIHudEditor().openUI() + isHudActive = true + } + + private var isHudActive = false + private val steveIcon = NVGRenderer.createImage("/assets/cobalt/steve.png") private val userIcon = try { NVGRenderer.createImage("https://mc-heads.net/avatar/${Minecraft.getInstance().user.profileId}/100/face.png") @@ -37,7 +46,7 @@ internal class UISidebar : UIPanel( init { components.addAll( - listOf(moduleButton) + listOf(moduleButton, hudButton) ) } @@ -46,10 +55,15 @@ internal class UISidebar : UIPanel( NVGRenderer.text("cb", x + width / 2F - 15F, y + 25F, 25F, ThemeManager.currentTheme.text) moduleButton - .setSelected(true) + .setSelected(!isHudActive) .updateBounds(x + (width / 2F) - (moduleButton.width / 2F), y + 75F) .render() + hudButton + .setSelected(isHudActive) + .updateBounds(x + (width / 2F) - (hudButton.width / 2F), y + 75F + 35F) + .render() + val userIconX = x + (width / 2F) - 16F val userIconY = y + height - 32F - 20F diff --git a/src/main/kotlin/org/cobalt/internal/ui/screen/UIConfig.kt b/src/main/kotlin/org/cobalt/internal/ui/screen/UIConfig.kt index 2a72290..265bc01 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/screen/UIConfig.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/screen/UIConfig.kt @@ -8,6 +8,7 @@ import org.cobalt.api.event.annotation.SubscribeEvent import org.cobalt.api.event.impl.render.NvgEvent import org.cobalt.api.util.ui.NVGRenderer import org.cobalt.internal.helper.Config +import org.cobalt.internal.helper.HudConfig import org.cobalt.internal.ui.UIScreen import org.cobalt.internal.ui.animation.BounceAnimation import org.cobalt.internal.ui.components.tooltips.TooltipManager @@ -111,6 +112,7 @@ internal object UIConfig : UIScreen() { override fun onClose() { Config.saveModulesConfig() + HudConfig.save() wasClosed = true super.onClose() } diff --git a/src/main/kotlin/org/cobalt/internal/ui/screen/UIHudEditor.kt b/src/main/kotlin/org/cobalt/internal/ui/screen/UIHudEditor.kt new file mode 100644 index 0000000..ec7680f --- /dev/null +++ b/src/main/kotlin/org/cobalt/internal/ui/screen/UIHudEditor.kt @@ -0,0 +1,285 @@ +package org.cobalt.internal.ui.screen + +import java.awt.Color +import net.minecraft.client.input.MouseButtonEvent +import org.cobalt.api.event.EventBus +import org.cobalt.api.event.annotation.SubscribeEvent +import org.cobalt.api.event.impl.render.NvgEvent +import org.cobalt.api.hud.HudAnchor +import org.cobalt.api.hud.HudModule +import org.cobalt.api.hud.HudModuleManager +import org.cobalt.api.ui.theme.ThemeManager +import org.cobalt.api.util.ui.NVGRenderer +import org.cobalt.internal.helper.HudConfig +import org.cobalt.internal.ui.UIScreen +import org.cobalt.internal.ui.hud.HudSettingsPopup +import org.cobalt.internal.ui.hud.SnapHelper +import org.cobalt.internal.ui.util.mouseX +import org.cobalt.internal.ui.util.mouseY + +internal class UIHudEditor : UIScreen() { + private var selectedModule: HudModule? = null + private var dragging = false + private var dragOffsetX = 0f + private var dragOffsetY = 0f + + private val snapHelper = SnapHelper() + private val settingsPopup = HudSettingsPopup() + + init { + EventBus.register(this) + } + + @Suppress("unused") + @SubscribeEvent + fun onRender(event: NvgEvent) { + if (mc.screen != this) return + + val window = mc.window + val width = window.screenWidth.toFloat() + val height = window.screenHeight.toFloat() + + NVGRenderer.beginFrame(width, height) + NVGRenderer.rect(0f, 0f, width, height, Color(0, 0, 0, 128).rgb) + + renderGrid(width, height) + renderModuleBounds(width, height) + renderGuides(width, height) + settingsPopup.render() + renderInstructions(width, height) + NVGRenderer.endFrame() + } + + private fun renderGrid(width: Float, height: Float) { + val gridSize = 20f + val gridColor = Color(255, 255, 255, 20).rgb + var x = 0f + while (x <= width) { + NVGRenderer.line(x, 0f, x, height, 1f, gridColor) + x += gridSize + } + var y = 0f + while (y <= height) { + NVGRenderer.line(0f, y, width, y, 1f, gridColor) + y += gridSize + } + } + + private fun renderModuleBounds(width: Float, height: Float) { + HudModuleManager.getModules().forEach { module -> + val (sx, sy) = module.getScreenPosition(width, height) + val w = module.getScaledWidth() + val h = module.getScaledHeight() + val isSelected = module == selectedModule + val borderColor = if (isSelected) ThemeManager.currentTheme.accent else ThemeManager.currentTheme.controlBorder + val borderThickness = if (isSelected) 2f else 1f + NVGRenderer.hollowRect(sx, sy, w, h, borderThickness, borderColor, 4f) + NVGRenderer.text(module.name, sx, sy + h + 6f, 12f, ThemeManager.currentTheme.textSecondary) + } + } + + private fun renderGuides(width: Float, height: Float) { + if (!dragging) return + snapHelper.activeGuides.forEach { guide -> + if (guide.isVertical) { + NVGRenderer.line(guide.position, 0f, guide.position, height, 1.5f, ThemeManager.currentTheme.accent) + } else { + NVGRenderer.line(0f, guide.position, width, guide.position, 1.5f, ThemeManager.currentTheme.accent) + } + } + } + + private fun renderInstructions(width: Float, height: Float) { + val text = "Left-click to select, drag to move | Right-click for settings | Scroll to resize | ESC to save and exit" + val textWidth = NVGRenderer.textWidth(text, 12f) + val padding = 14f + val boxWidth = textWidth + padding * 2f + val boxHeight = 26f + val x = width / 2f - boxWidth / 2f + val y = height - boxHeight - 20f + NVGRenderer.rect(x, y, boxWidth, boxHeight, Color(0, 0, 0, 140).rgb, 8f) + NVGRenderer.hollowRect(x, y, boxWidth, boxHeight, 1f, ThemeManager.currentTheme.controlBorder, 8f) + NVGRenderer.text(text, x + padding, y + 7f, 12f, ThemeManager.currentTheme.textSecondary) + } + + override fun mouseClicked(click: MouseButtonEvent, doubled: Boolean): Boolean { + val screenWidth = mc.window.screenWidth.toFloat() + val screenHeight = mc.window.screenHeight.toFloat() + val mx = mouseX.toFloat() + val my = mouseY.toFloat() + val button = click.button() + + if (settingsPopup.visible) { + if (settingsPopup.mouseClicked(mx, my, button)) return true + if (!settingsPopup.containsPoint(mx, my)) settingsPopup.hide() + } + + if (button == 1) { + val target = findModuleUnderCursor(mx, my, screenWidth, screenHeight) + if (target != null) { + selectedModule = target + settingsPopup.show(target, mx + 8f, my + 6f) + return true + } + } + + if (button == 0) { + val target = findModuleUnderCursor(mx, my, screenWidth, screenHeight) + selectedModule = target + if (target != null) { + val (sx, sy) = target.getScreenPosition(screenWidth, screenHeight) + dragOffsetX = mx - sx + dragOffsetY = my - sy + dragging = true + settingsPopup.hide() + return true + } + } + + return super.mouseClicked(click, doubled) + } + + override fun mouseReleased(click: MouseButtonEvent): Boolean { + if (click.button() == 0 && dragging) { + dragging = false + selectedModule?.let { module -> + val screenWidth = mc.window.screenWidth.toFloat() + val screenHeight = mc.window.screenHeight.toFloat() + val (sx, sy) = module.getScreenPosition(screenWidth, screenHeight) + val (snappedX, snappedY) = snapHelper.snapToGrid(sx, sy) + updateModulePosition(module, snappedX, snappedY, screenWidth, screenHeight) + snapHelper.clearGuides() + } + return true + } + return super.mouseReleased(click) + } + + override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean { + if (click.button() != 0 || !dragging) return super.mouseDragged(click, offsetX, offsetY) + + val module = selectedModule ?: return super.mouseDragged(click, offsetX, offsetY) + val screenWidth = mc.window.screenWidth.toFloat() + val screenHeight = mc.window.screenHeight.toFloat() + val mx = mouseX.toFloat() + val my = mouseY.toFloat() + val newScreenX = mx - dragOffsetX + val newScreenY = my - dragOffsetY + + val otherBounds = HudModuleManager.getModules() + .filter { it != module } + .map { + val (sx, sy) = it.getScreenPosition(screenWidth, screenHeight) + SnapHelper.ModuleBounds(sx, sy, it.getScaledWidth(), it.getScaledHeight()) + } + + val (alignedX, alignedY) = snapHelper.findAlignmentGuides( + newScreenX, + newScreenY, + module.getScaledWidth(), + module.getScaledHeight(), + screenWidth, + screenHeight, + otherBounds, + ) + + updateModulePosition(module, alignedX, alignedY, screenWidth, screenHeight) + return true + } + + override fun mouseScrolled( + mouseX: Double, + mouseY: Double, + horizontalAmount: Double, + verticalAmount: Double, + ): Boolean { + val screenWidth = mc.window.screenWidth.toFloat() + val screenHeight = mc.window.screenHeight.toFloat() + val target = findModuleUnderCursor(mouseX.toFloat(), mouseY.toFloat(), screenWidth, screenHeight) + if (target != null) { + target.scale = (target.scale + (if (verticalAmount > 0) 0.1f else -0.1f)).coerceIn(0.5f, 3.0f) + return true + } + return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount) + } + + override fun init() { + HudModuleManager.isEditorOpen = true + super.init() + } + + override fun onClose() { + HudModuleManager.isEditorOpen = false + HudConfig.save() + EventBus.unregister(this) + super.onClose() + } + + private fun findModuleUnderCursor( + mouseX: Float, + mouseY: Float, + screenWidth: Float, + screenHeight: Float, + ): HudModule? { + return HudModuleManager.getModules().lastOrNull { + it.containsPoint(mouseX, mouseY, screenWidth, screenHeight) + } + } + + private fun updateModulePosition( + module: HudModule, + newScreenX: Float, + newScreenY: Float, + screenWidth: Float, + screenHeight: Float, + ) { + val w = module.getScaledWidth() + val h = module.getScaledHeight() + when (module.anchor) { + HudAnchor.TOP_LEFT -> { + module.offsetX = newScreenX + module.offsetY = newScreenY + } + + HudAnchor.TOP_CENTER -> { + module.offsetX = newScreenX - (screenWidth / 2f - w / 2f) + module.offsetY = newScreenY + } + + HudAnchor.TOP_RIGHT -> { + module.offsetX = screenWidth - w - newScreenX + module.offsetY = newScreenY + } + + HudAnchor.CENTER_LEFT -> { + module.offsetX = newScreenX + module.offsetY = newScreenY - (screenHeight / 2f - h / 2f) + } + + HudAnchor.CENTER -> { + module.offsetX = newScreenX - (screenWidth / 2f - w / 2f) + module.offsetY = newScreenY - (screenHeight / 2f - h / 2f) + } + + HudAnchor.CENTER_RIGHT -> { + module.offsetX = screenWidth - w - newScreenX + module.offsetY = newScreenY - (screenHeight / 2f - h / 2f) + } + + HudAnchor.BOTTOM_LEFT -> { + module.offsetX = newScreenX + module.offsetY = screenHeight - h - newScreenY + } + + HudAnchor.BOTTOM_CENTER -> { + module.offsetX = newScreenX - (screenWidth / 2f - w / 2f) + module.offsetY = screenHeight - h - newScreenY + } + + HudAnchor.BOTTOM_RIGHT -> { + module.offsetX = screenWidth - w - newScreenX + module.offsetY = screenHeight - h - newScreenY + } + } + } +} From 8e492ec08af1868399271ec2b3ae19fc523550ea Mon Sep 17 00:00:00 2001 From: forbai Date: Wed, 11 Feb 2026 15:12:30 +0100 Subject: [PATCH 05/10] feat(hud): introduce HudElement and DSL for HUD element creation --- src/main/kotlin/org/cobalt/Cobalt.kt | 5 +- .../api/hud/{HudModule.kt => HudElement.kt} | 2 +- .../kotlin/org/cobalt/api/hud/HudModuleDSL.kt | 105 ++++++ .../org/cobalt/api/hud/HudModuleManager.kt | 32 +- .../cobalt/api/hud/modules/WatermarkModule.kt | 71 ++-- .../kotlin/org/cobalt/api/module/Module.kt | 10 + .../org/cobalt/internal/helper/Config.kt | 85 ++++- .../org/cobalt/internal/helper/HudConfig.kt | 83 ----- .../internal/ui/hud/HudSettingsPopup.kt | 323 ++++++++++++++---- .../internal/ui/panel/panels/UIHudList.kt | 24 +- .../org/cobalt/internal/ui/screen/UIConfig.kt | 2 - .../cobalt/internal/ui/screen/UIHudEditor.kt | 260 +++++++++----- 12 files changed, 677 insertions(+), 325 deletions(-) rename src/main/kotlin/org/cobalt/api/hud/{HudModule.kt => HudElement.kt} (98%) create mode 100644 src/main/kotlin/org/cobalt/api/hud/HudModuleDSL.kt delete mode 100644 src/main/kotlin/org/cobalt/internal/helper/HudConfig.kt diff --git a/src/main/kotlin/org/cobalt/Cobalt.kt b/src/main/kotlin/org/cobalt/Cobalt.kt index d93dc50..655f143 100644 --- a/src/main/kotlin/org/cobalt/Cobalt.kt +++ b/src/main/kotlin/org/cobalt/Cobalt.kt @@ -4,19 +4,21 @@ import net.fabricmc.api.ClientModInitializer import org.cobalt.api.command.CommandManager import org.cobalt.api.event.EventBus import org.cobalt.api.hud.HudModuleManager +import org.cobalt.api.hud.modules.WatermarkModule import org.cobalt.api.module.ModuleManager import org.cobalt.api.notification.NotificationManager import org.cobalt.api.rotation.RotationExecutor import org.cobalt.api.util.TickScheduler import org.cobalt.internal.command.MainCommand import org.cobalt.internal.helper.Config -import org.cobalt.internal.helper.HudConfig import org.cobalt.internal.loader.AddonLoader @Suppress("UNUSED") object Cobalt : ClientModInitializer { override fun onInitializeClient() { + ModuleManager.addModules(listOf(WatermarkModule())) + AddonLoader.getAddons().map { it.second }.forEach { it.onLoad() ModuleManager.addModules(it.getModules()) @@ -31,7 +33,6 @@ object Cobalt : ClientModInitializer { ).forEach { EventBus.register(it) } Config.loadModulesConfig() - HudConfig.load() EventBus.register(this) println("Cobalt Mod Initialized") } diff --git a/src/main/kotlin/org/cobalt/api/hud/HudModule.kt b/src/main/kotlin/org/cobalt/api/hud/HudElement.kt similarity index 98% rename from src/main/kotlin/org/cobalt/api/hud/HudModule.kt rename to src/main/kotlin/org/cobalt/api/hud/HudElement.kt index c8d2908..76d13b5 100644 --- a/src/main/kotlin/org/cobalt/api/hud/HudModule.kt +++ b/src/main/kotlin/org/cobalt/api/hud/HudElement.kt @@ -3,7 +3,7 @@ package org.cobalt.api.hud import org.cobalt.api.module.setting.Setting import org.cobalt.api.module.setting.SettingsContainer -abstract class HudModule( +abstract class HudElement( val id: String, val name: String, val description: String = "", diff --git a/src/main/kotlin/org/cobalt/api/hud/HudModuleDSL.kt b/src/main/kotlin/org/cobalt/api/hud/HudModuleDSL.kt new file mode 100644 index 0000000..9284682 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/hud/HudModuleDSL.kt @@ -0,0 +1,105 @@ +package org.cobalt.api.hud + +import org.cobalt.api.module.Module +import org.cobalt.api.module.setting.Setting +import org.cobalt.api.module.setting.SettingsContainer + +/** + * DSL extension to create a HUD element inside a Module. + * + * Example: + * ``` + * class MyModule : Module("My Module") { + * val speedHud = hudElement("speed", "Speed Display") { + * width { 80f } + * height { 20f } + * anchor = HudAnchor.TOP_RIGHT + * val showDecimals = setting(CheckboxSetting("Decimals", "", false)) + * render { x, y, scale -> /* use showDecimals.value */ } + * } + * } + * ``` + */ +fun Module.hudElement( + id: String, + name: String, + description: String = "", + init: HudElementBuilder.() -> Unit +): HudElement { + val builder = HudElementBuilder(id, name, description) + builder.init() + val element = builder.build() + addHudElement(element) + return element +} + +class HudElementBuilder( + private val id: String, + private val name: String, + private val description: String = "" +) : SettingsContainer { + + private var widthProvider: () -> Float = { 100f } + private var heightProvider: () -> Float = { 20f } + var anchor: HudAnchor = HudAnchor.TOP_LEFT + var offsetX: Float = 10f + var offsetY: Float = 10f + var scale: Float = 1.0f + private var renderLambda: ((Float, Float, Float) -> Unit)? = null + + private val settingsList = mutableListOf>() + + override fun addSetting(vararg settings: Setting<*>) { + settingsList.addAll(listOf(*settings)) + } + + override fun getSettings(): List> { + return settingsList + } + + fun width(provider: () -> Float) { + widthProvider = provider + } + + fun height(provider: () -> Float) { + heightProvider = provider + } + + fun > setting(setting: S): S { + addSetting(setting) + return setting + } + + fun render(block: (screenX: Float, screenY: Float, scale: Float) -> Unit) { + renderLambda = block + } + + fun build(): HudElement { + val capturedRender = renderLambda ?: { _, _, _ -> } + val capturedWidth = widthProvider + val capturedHeight = heightProvider + val capturedSettings = settingsList.toList() + val capturedAnchor = anchor + val capturedOffsetX = offsetX + val capturedOffsetY = offsetY + val capturedScale = scale + + return object : HudElement(id, name, description) { + override val defaultAnchor = capturedAnchor + override val defaultOffsetX = capturedOffsetX + override val defaultOffsetY = capturedOffsetY + override val defaultScale = capturedScale + + init { + capturedSettings.forEach { addSetting(it) } + resetPosition() + } + + override fun getBaseWidth(): Float = capturedWidth() + override fun getBaseHeight(): Float = capturedHeight() + override fun render(screenX: Float, screenY: Float, scale: Float) { + capturedRender(screenX, screenY, scale) + } + } + } +} diff --git a/src/main/kotlin/org/cobalt/api/hud/HudModuleManager.kt b/src/main/kotlin/org/cobalt/api/hud/HudModuleManager.kt index 814a0f3..b48f38c 100644 --- a/src/main/kotlin/org/cobalt/api/hud/HudModuleManager.kt +++ b/src/main/kotlin/org/cobalt/api/hud/HudModuleManager.kt @@ -3,37 +3,21 @@ package org.cobalt.api.hud import net.minecraft.client.Minecraft import org.cobalt.api.event.annotation.SubscribeEvent import org.cobalt.api.event.impl.render.NvgEvent -import org.cobalt.api.hud.modules.WatermarkModule +import org.cobalt.api.module.ModuleManager import org.cobalt.api.util.ui.NVGRenderer object HudModuleManager { private val mc: Minecraft = Minecraft.getInstance() - private val modules = mutableListOf() @Volatile var isEditorOpen: Boolean = false - init { - register(WatermarkModule()) - } - - fun register(module: HudModule) { - if (modules.none { it.id == module.id }) { - modules.add(module) - } - } - - fun unregister(id: String) { - modules.removeIf { it.id == id } - } - - fun getModules(): List = modules.toList() - - fun getModule(id: String): HudModule? = modules.find { it.id == id } + fun getElements(): List = + ModuleManager.getModules().flatMap { it.getHudElements() } fun resetAllPositions() { - modules.forEach { it.resetPosition() } + getElements().forEach { it.resetPosition() } } @Suppress("unused") @@ -47,13 +31,13 @@ object HudModuleManager { NVGRenderer.beginFrame(screenWidth, screenHeight) - modules.filter { it.enabled }.forEach { module -> - val (screenX, screenY) = module.getScreenPosition(screenWidth, screenHeight) + getElements().filter { it.enabled }.forEach { element -> + val (screenX, screenY) = element.getScreenPosition(screenWidth, screenHeight) NVGRenderer.push() NVGRenderer.translate(screenX, screenY) - NVGRenderer.scale(module.scale, module.scale) - module.render(0f, 0f, module.scale) + NVGRenderer.scale(element.scale, element.scale) + element.render(0f, 0f, element.scale) NVGRenderer.pop() } diff --git a/src/main/kotlin/org/cobalt/api/hud/modules/WatermarkModule.kt b/src/main/kotlin/org/cobalt/api/hud/modules/WatermarkModule.kt index a563a3f..f25a31f 100644 --- a/src/main/kotlin/org/cobalt/api/hud/modules/WatermarkModule.kt +++ b/src/main/kotlin/org/cobalt/api/hud/modules/WatermarkModule.kt @@ -1,50 +1,51 @@ package org.cobalt.api.hud.modules import org.cobalt.api.hud.HudAnchor -import org.cobalt.api.hud.HudModule +import org.cobalt.api.hud.hudElement +import org.cobalt.api.module.Module import org.cobalt.api.module.setting.impl.TextSetting import org.cobalt.api.module.setting.impl.ColorSetting import org.cobalt.api.module.setting.impl.CheckboxSetting import org.cobalt.api.ui.theme.ThemeManager import org.cobalt.api.util.ui.NVGRenderer -class WatermarkModule : HudModule( - id = "watermark", - name = "Watermark", - description = "Displays Cobalt branding", -) { - - override val defaultAnchor = HudAnchor.TOP_LEFT - override val defaultOffsetX = 10f - override val defaultOffsetY = 10f - override val defaultScale = 1.0f +class WatermarkModule : Module("Watermark") { private val textSize = 18f - private var text by TextSetting("Text", "Display text", "Cobalt") - private var color by ColorSetting("Color", "Text color", ThemeManager.currentTheme.accent) - private var shadow by CheckboxSetting("Shadow", "Show text shadow", false) - private var background by CheckboxSetting("Background", "Show background box", false) - - init { - resetPosition() - } - - override fun getBaseWidth(): Float = NVGRenderer.textWidth(text, textSize) + (if (background) 16f else 0f) - - override fun getBaseHeight(): Float = textSize + (if (background) 12f else 4f) - - override fun render(screenX: Float, screenY: Float, scale: Float) { - if (background) { - NVGRenderer.rect(screenX - 8f, screenY - 6f, - getBaseWidth(), getBaseHeight(), - ThemeManager.currentTheme.panel, 6f) - } - - if (shadow) { - NVGRenderer.textShadow(text, screenX, screenY, textSize, color) - } else { - NVGRenderer.text(text, screenX, screenY, textSize, color) + val watermark = hudElement("watermark", "Watermark", "Displays Cobalt branding") { + anchor = HudAnchor.TOP_LEFT + offsetX = 10f + offsetY = 10f + + val text = setting(TextSetting("Text", "Display text", "Cobalt")) + val color = setting(ColorSetting("Color", "Text color", ThemeManager.currentTheme.accent)) + val shadow = setting(CheckboxSetting("Shadow", "Show text shadow", false)) + val background = setting(CheckboxSetting("Background", "Show background box", false)) + + width { NVGRenderer.textWidth(text.value, textSize) + (if (background.value) 16f else 0f) } + height { textSize + (if (background.value) 12f else 4f) } + + render { screenX, screenY, _ -> + val padX = if (background.value) 8f else 0f + val padY = if (background.value) 6f else 0f + + if (background.value) { + NVGRenderer.rect( + screenX, screenY, + NVGRenderer.textWidth(text.value, textSize) + 16f, + textSize + 12f, + ThemeManager.currentTheme.panel, 6f + ) + } + + val textX = screenX + padX + val textY = screenY + padY + if (shadow.value) { + NVGRenderer.textShadow(text.value, textX, textY, textSize, color.value) + } else { + NVGRenderer.text(text.value, textX, textY, textSize, color.value) + } } } } diff --git a/src/main/kotlin/org/cobalt/api/module/Module.kt b/src/main/kotlin/org/cobalt/api/module/Module.kt index e69d42d..8213905 100644 --- a/src/main/kotlin/org/cobalt/api/module/Module.kt +++ b/src/main/kotlin/org/cobalt/api/module/Module.kt @@ -1,11 +1,13 @@ package org.cobalt.api.module +import org.cobalt.api.hud.HudElement import org.cobalt.api.module.setting.Setting import org.cobalt.api.module.setting.SettingsContainer abstract class Module(val name: String) : SettingsContainer { private val settingsList = mutableListOf>() + private val hudElementsList = mutableListOf() override fun addSetting(vararg settings: Setting<*>) { settingsList.addAll(listOf(*settings)) @@ -15,4 +17,12 @@ abstract class Module(val name: String) : SettingsContainer { return settingsList } + fun addHudElement(element: HudElement) { + hudElementsList.add(element) + } + + fun getHudElements(): List { + return hudElementsList + } + } diff --git a/src/main/kotlin/org/cobalt/internal/helper/Config.kt b/src/main/kotlin/org/cobalt/internal/helper/Config.kt index 7519277..d792298 100644 --- a/src/main/kotlin/org/cobalt/internal/helper/Config.kt +++ b/src/main/kotlin/org/cobalt/internal/helper/Config.kt @@ -6,6 +6,9 @@ import com.google.gson.JsonObject import com.google.gson.JsonParser import java.io.File import net.minecraft.client.Minecraft +import org.cobalt.api.hud.HudAnchor +import org.cobalt.api.module.Module +import org.cobalt.api.module.ModuleManager import org.cobalt.api.ui.theme.ThemeManager import org.cobalt.api.ui.theme.impl.CustomTheme import org.cobalt.internal.loader.AddonLoader @@ -13,11 +16,33 @@ import org.cobalt.internal.ui.theme.ThemeSerializer internal object Config { + private const val BUILTIN_ADDON_ID = "cobalt" + private val mc: Minecraft = Minecraft.getInstance() private val gson = GsonBuilder().setPrettyPrinting().create() private val modulesFile = File(mc.gameDirectory, "config/cobalt/addons.json") private val themesFile = File(mc.gameDirectory, "config/cobalt/themes.json") + private fun buildGroupedModules(): Map> { + val addonModules = mutableSetOf() + val grouped = mutableMapOf>() + + AddonLoader.getAddons().forEach { (metadata, addon) -> + val modules = addon.getModules() + addonModules.addAll(modules) + if (modules.isNotEmpty()) { + grouped[metadata.id] = modules.toMutableList() + } + } + + val builtinModules = ModuleManager.getModules().filter { it !in addonModules } + if (builtinModules.isNotEmpty()) { + grouped[BUILTIN_ADDON_ID] = builtinModules.toMutableList() + } + + return grouped + } + fun loadModulesConfig() { loadThemesConfig() if (!modulesFile.exists()) { @@ -29,26 +54,50 @@ internal object Config { val text = modulesFile.bufferedReader().use { it.readText() } if (text.isEmpty()) return - val addonsMap = AddonLoader.getAddons().associate { it.first.id to it.second } + val grouped = buildGroupedModules() runCatching { JsonParser.parseString(text).asJsonArray }.getOrNull()?.forEach { element -> val addonObj = element.asJsonObject val addonId = addonObj.get("addon").asString - val addon = addonsMap[addonId] ?: return@forEach + val modules = grouped[addonId] ?: return@forEach - val modulesMap = addon.getModules().associateBy { it.name } - val settingsMap = modulesMap.values.flatMap { it.getSettings() }.associateBy { it.name } + val modulesMap = modules.associateBy { it.name } addonObj.getAsJsonArray("modules")?.forEach { moduleElement -> val moduleObj = moduleElement.asJsonObject val moduleName = moduleObj.get("name").asString - modulesMap[moduleName] ?: return@forEach + val module = modulesMap[moduleName] ?: return@forEach + val settingsMap = module.getSettings().associateBy { it.name } moduleObj.getAsJsonObject("settings")?.entrySet()?.forEach { (key, value) -> settingsMap[key]?.read(value) } + + val hudElementsMap = module.getHudElements().associateBy { it.id } + moduleObj.getAsJsonArray("hudElements")?.forEach { hudEl -> + val hudObj = hudEl.asJsonObject + val hudId = hudObj.get("id")?.asString ?: return@forEach + val hudElement = hudElementsMap[hudId] ?: return@forEach + + hudElement.enabled = hudObj.get("enabled")?.asBoolean ?: true + hudElement.anchor = hudObj.get("anchor")?.asString?.let { + runCatching { HudAnchor.valueOf(it) }.getOrNull() + } ?: HudAnchor.TOP_LEFT + hudElement.offsetX = hudObj.get("offsetX")?.asFloat ?: 10f + hudElement.offsetY = hudObj.get("offsetY")?.asFloat ?: 10f + hudElement.scale = hudObj.get("scale")?.asFloat?.coerceIn(0.5f, 3.0f) ?: 1.0f + + val hudSettingsObj = hudObj.getAsJsonObject("settings") + if (hudSettingsObj != null) { + hudElement.getSettings().forEach { setting -> + hudSettingsObj.get(setting.name)?.let { jsonEl -> + runCatching { setting.read(jsonEl) } + } + } + } + } } } } @@ -56,12 +105,12 @@ internal object Config { fun saveModulesConfig() { val jsonArray = JsonArray() - AddonLoader.getAddons().forEach { (metadata, addon) -> + buildGroupedModules().forEach { (addonId, modules) -> val addonObject = JsonObject() - addonObject.addProperty("addon", metadata.id) + addonObject.addProperty("addon", addonId) val modulesArray = JsonArray() - addon.getModules().forEach { module -> + modules.forEach { module -> val moduleObject = JsonObject() moduleObject.addProperty("name", module.name) @@ -70,6 +119,26 @@ internal object Config { settingsObject.add(setting.name, setting.write()) } moduleObject.add("settings", settingsObject) + + val hudElementsArray = JsonArray() + module.getHudElements().forEach { hudElement -> + val hudObj = JsonObject() + hudObj.addProperty("id", hudElement.id) + hudObj.addProperty("enabled", hudElement.enabled) + hudObj.addProperty("anchor", hudElement.anchor.name) + hudObj.addProperty("offsetX", hudElement.offsetX) + hudObj.addProperty("offsetY", hudElement.offsetY) + hudObj.addProperty("scale", hudElement.scale) + + val hudSettingsObj = JsonObject() + hudElement.getSettings().forEach { setting -> + hudSettingsObj.add(setting.name, setting.write()) + } + hudObj.add("settings", hudSettingsObj) + hudElementsArray.add(hudObj) + } + moduleObject.add("hudElements", hudElementsArray) + modulesArray.add(moduleObject) } diff --git a/src/main/kotlin/org/cobalt/internal/helper/HudConfig.kt b/src/main/kotlin/org/cobalt/internal/helper/HudConfig.kt deleted file mode 100644 index e42b73a..0000000 --- a/src/main/kotlin/org/cobalt/internal/helper/HudConfig.kt +++ /dev/null @@ -1,83 +0,0 @@ -package org.cobalt.internal.helper - -import com.google.gson.GsonBuilder -import com.google.gson.JsonObject -import com.google.gson.JsonParser -import java.io.File -import net.minecraft.client.Minecraft -import org.cobalt.api.hud.HudAnchor -import org.cobalt.api.hud.HudModuleManager - -internal object HudConfig { - - private val mc: Minecraft = Minecraft.getInstance() - private val gson = GsonBuilder().setPrettyPrinting().create() - private val hudFile = File(mc.gameDirectory, "config/cobalt/hud.json") - - fun load() { - if (!hudFile.exists()) { - hudFile.parentFile?.mkdirs() - hudFile.createNewFile() - return - } - - val text = hudFile.bufferedReader().use { it.readText() } - if (text.isEmpty()) return - - runCatching { - val root = JsonParser.parseString(text).asJsonObject - val modulesObj = root.getAsJsonObject("modules") ?: return - - for ((id, element) in modulesObj.entrySet()) { - val module = HudModuleManager.getModule(id) ?: continue - val obj = element.asJsonObject - - module.enabled = obj.get("enabled")?.asBoolean ?: true - module.anchor = obj.get("anchor")?.asString?.let { - runCatching { HudAnchor.valueOf(it) }.getOrNull() - } ?: HudAnchor.TOP_LEFT - module.offsetX = obj.get("offsetX")?.asFloat ?: 10f - module.offsetY = obj.get("offsetY")?.asFloat ?: 10f - module.scale = obj.get("scale")?.asFloat?.coerceIn(0.5f, 3.0f) ?: 1.0f - - // Load settings - val settingsObj = obj.getAsJsonObject("settings") - if (settingsObj != null) { - module.getSettings().forEach { setting -> - settingsObj.get(setting.name)?.let { element -> - runCatching { setting.read(element) } - } - } - } - } - } - } - - fun save() { - val root = JsonObject() - val modulesObj = JsonObject() - - HudModuleManager.getModules().forEach { module -> - val obj = JsonObject().apply { - addProperty("enabled", module.enabled) - addProperty("anchor", module.anchor.name) - addProperty("offsetX", module.offsetX) - addProperty("offsetY", module.offsetY) - addProperty("scale", module.scale) - - // Add settings persistence - val settingsObj = JsonObject() - module.getSettings().forEach { setting -> - settingsObj.add(setting.name, setting.write()) - } - add("settings", settingsObj) - } - modulesObj.add(module.id, obj) - } - - root.add("modules", modulesObj) - - hudFile.parentFile?.mkdirs() - hudFile.bufferedWriter().use { it.write(gson.toJson(root)) } - } -} diff --git a/src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt b/src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt index 9b46db4..1371d70 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt @@ -1,59 +1,155 @@ package org.cobalt.internal.ui.hud -import org.cobalt.api.hud.HudModule +import java.awt.Color +import net.minecraft.client.input.CharacterEvent +import net.minecraft.client.input.KeyEvent +import org.cobalt.api.hud.HudElement +import org.cobalt.api.module.setting.impl.* import org.cobalt.api.ui.theme.ThemeManager import org.cobalt.api.util.ui.NVGRenderer +import org.cobalt.internal.ui.UIComponent import org.cobalt.internal.ui.animation.ColorAnimation +import org.cobalt.internal.ui.components.settings.* +import org.cobalt.internal.ui.util.GridLayout +import org.cobalt.internal.ui.util.ScrollHandler import org.cobalt.internal.ui.util.isHoveringOver internal class HudSettingsPopup { var visible: Boolean = false - var module: HudModule? = null - var popupX: Float = 0f - var popupY: Float = 0f - - private val popupWidth = 180f - private val popupPadding = 12f - private val rowHeight = 28f - private val headerHeight = 26f + var module: HudElement? = null + + private var panelX: Float = 0f + private var panelY: Float = 0f + private val panelWidth = 520f + private val panelHeight = 420f + + private val headerHeight = 50f + private val controlsHeight = 50f + private val padding = 20f + private val cornerRadius = 10f + private val toggleAnim = ColorAnimation(150L) private val buttonAnim = ColorAnimation(150L) + private val closeAnim = ColorAnimation(150L) - private val popupHeight: Float - get() = popupPadding * 2f + headerHeight + rowHeight + rowHeight + 8f + private var settingComponents: List = emptyList() - fun show(module: HudModule, x: Float, y: Float) { + private val settingsScroll = ScrollHandler() + private val settingsLayout = GridLayout( + columns = 1, + itemWidth = panelWidth - padding * 2f, + itemHeight = 60f, + gap = 10f + ) + + fun show(module: HudElement, screenWidth: Float, screenHeight: Float) { this.module = module - popupX = x - popupY = y + panelX = (screenWidth - panelWidth) / 2f + panelY = (screenHeight - panelHeight) / 2f visible = true + settingsScroll.reset() + + settingComponents = module.getSettings().map { + when (it) { + is CheckboxSetting -> UICheckboxSetting(it) + is ColorSetting -> UIColorSetting(it) + is InfoSetting -> UIInfoSetting(it) + is KeyBindSetting -> UIKeyBindSetting(it) + is ModeSetting -> UIModeSetting(it) + is RangeSetting -> UIRangeSetting(it) + is SliderSetting -> UISliderSetting(it) + else -> UITextSetting(it as TextSetting) + } + } + + val settingWidth = panelWidth - padding * 2f + settingComponents.forEach { component -> + component.width = settingWidth + component.height = 60f + } } fun hide() { visible = false module = null + settingComponents = emptyList() + settingsScroll.reset() } fun render() { if (!visible) return val target = module ?: return - NVGRenderer.rect(popupX, popupY, popupWidth, popupHeight, ThemeManager.currentTheme.panel, 8f) - NVGRenderer.hollowRect(popupX, popupY, popupWidth, popupHeight, 1f, ThemeManager.currentTheme.controlBorder, 8f) + NVGRenderer.rect(0f, 0f, 10000f, 10000f, Color(0, 0, 0, 100).rgb) + + NVGRenderer.rect(panelX, panelY, panelWidth, panelHeight, ThemeManager.currentTheme.background, cornerRadius) + NVGRenderer.hollowRect(panelX, panelY, panelWidth, panelHeight, 1f, ThemeManager.currentTheme.controlBorder, cornerRadius) + renderHeader(target) + + val dividerY = panelY + headerHeight + NVGRenderer.line( + panelX + padding, dividerY, + panelX + panelWidth - padding, dividerY, + 1f, ThemeManager.currentTheme.moduleDivider + ) + + renderControls(target) + + val controlsDividerY = dividerY + controlsHeight + NVGRenderer.line( + panelX + padding, controlsDividerY, + panelX + panelWidth - padding, controlsDividerY, + 1f, ThemeManager.currentTheme.moduleDivider + ) + + renderSettings(controlsDividerY) + } + + private fun renderHeader(target: HudElement) { NVGRenderer.text( target.name, - popupX + popupPadding, - popupY + popupPadding + 2f, - 14f, + panelX + padding, + panelY + 17f, + 16f, ThemeManager.currentTheme.accent ) - val toggleY = popupY + popupPadding + headerHeight + val closeX = panelX + panelWidth - padding - 26f + val closeY = panelY + 12f + val closeSize = 26f + val closeHover = isHoveringOver(closeX, closeY, closeSize, closeSize) + + val closeBg = closeAnim.get( + ThemeManager.currentTheme.controlBg, + ThemeManager.currentTheme.selectedOverlay, + !closeHover + ) + val closeBorder = closeAnim.get( + ThemeManager.currentTheme.controlBorder, + ThemeManager.currentTheme.accent, + !closeHover + ) + + NVGRenderer.rect(closeX, closeY, closeSize, closeSize, closeBg, 6f) + NVGRenderer.hollowRect(closeX, closeY, closeSize, closeSize, 1f, closeBorder, 6f) + + val cx = closeX + closeSize / 2f + val cy = closeY + closeSize / 2f + val half = 5f + NVGRenderer.line(cx - half, cy - half, cx + half, cy + half, 1.5f, ThemeManager.currentTheme.textPrimary) + NVGRenderer.line(cx + half, cy - half, cx - half, cy + half, 1.5f, ThemeManager.currentTheme.textPrimary) + } + + private fun renderControls(target: HudElement) { + val controlsY = panelY + headerHeight + 10f + val buttonHeight = 30f + val toggleText = if (target.enabled) "Disable" else "Enable" - val toggleWidth = NVGRenderer.textWidth(toggleText, 12f) + 30f - val toggleX = popupX + popupWidth - toggleWidth - popupPadding - val isToggleHover = isHoveringOver(toggleX, toggleY, toggleWidth, rowHeight - 6f) + val toggleWidth = NVGRenderer.textWidth(toggleText, 13f) + 30f + val toggleX = panelX + padding + + val isToggleHover = isHoveringOver(toggleX, controlsY, toggleWidth, buttonHeight) val toggleBg = toggleAnim.get( ThemeManager.currentTheme.controlBg, ThemeManager.currentTheme.selectedOverlay, @@ -65,25 +161,18 @@ internal class HudSettingsPopup { !isToggleHover ) - NVGRenderer.text( - "Enabled", - popupX + popupPadding, - toggleY + 6f, - 12f, - ThemeManager.currentTheme.textSecondary - ) - NVGRenderer.rect(toggleX, toggleY, toggleWidth, rowHeight - 6f, toggleBg, 10f) - NVGRenderer.hollowRect(toggleX, toggleY, toggleWidth, rowHeight - 6f, 1.5f, toggleBorder, 10f) - NVGRenderer.text( - toggleText, - toggleX + 12f, - toggleY + 5f, - 12f, - ThemeManager.currentTheme.textPrimary - ) + NVGRenderer.rect(toggleX, controlsY, toggleWidth, buttonHeight, toggleBg, 8f) + NVGRenderer.hollowRect(toggleX, controlsY, toggleWidth, buttonHeight, 1.5f, toggleBorder, 8f) + NVGRenderer.text(toggleText, toggleX + 15f, controlsY + 8f, 13f, ThemeManager.currentTheme.textPrimary) - val resetY = toggleY + rowHeight + 6f - val resetHover = isHoveringOver(popupX + popupPadding, resetY, popupWidth - popupPadding * 2f, rowHeight - 6f) + val statusText = if (target.enabled) "Enabled" else "Disabled" + val statusColor = if (target.enabled) ThemeManager.currentTheme.accent else ThemeManager.currentTheme.textSecondary + NVGRenderer.text(statusText, toggleX + toggleWidth + 12f, controlsY + 8f, 12f, statusColor) + + val resetText = "Reset Position" + val resetWidth = NVGRenderer.textWidth(resetText, 13f) + 30f + val resetX = panelX + panelWidth - padding - resetWidth + val resetHover = isHoveringOver(resetX, controlsY, resetWidth, buttonHeight) val resetBg = buttonAnim.get( ThemeManager.currentTheme.controlBg, ThemeManager.currentTheme.selectedOverlay, @@ -95,60 +184,150 @@ internal class HudSettingsPopup { !resetHover ) - NVGRenderer.rect( - popupX + popupPadding, - resetY, - popupWidth - popupPadding * 2f, - rowHeight - 6f, - resetBg, - 8f - ) - NVGRenderer.hollowRect( - popupX + popupPadding, - resetY, - popupWidth - popupPadding * 2f, - rowHeight - 6f, - 1.5f, - resetBorder, - 8f - ) - NVGRenderer.text( - "Reset Position", - popupX + popupPadding + 10f, - resetY + 5f, - 12f, - ThemeManager.currentTheme.textPrimary + NVGRenderer.rect(resetX, controlsY, resetWidth, buttonHeight, resetBg, 8f) + NVGRenderer.hollowRect(resetX, controlsY, resetWidth, buttonHeight, 1.5f, resetBorder, 8f) + NVGRenderer.text(resetText, resetX + 15f, controlsY + 8f, 13f, ThemeManager.currentTheme.textPrimary) + } + + private fun renderSettings(startY: Float) { + if (settingComponents.isEmpty()) { + val noText = "No settings available" + NVGRenderer.text( + noText, + panelX + panelWidth / 2f - NVGRenderer.textWidth(noText, 13f) / 2f, + startY + 30f, + 13f, + ThemeManager.currentTheme.textSecondary + ) + return + } + + val settingsAreaY = startY + 10f + val settingsAreaHeight = panelY + panelHeight - settingsAreaY - 10f + val settingWidth = panelWidth - padding * 2f + + settingsScroll.setMaxScroll( + settingsLayout.contentHeight(settingComponents.size) + 10f, + settingsAreaHeight ) + + NVGRenderer.pushScissor(panelX + padding, settingsAreaY, settingWidth, settingsAreaHeight) + + val scrollOffset = settingsScroll.getOffset() + settingsLayout.layout(panelX + padding, settingsAreaY - scrollOffset, settingComponents) + settingComponents.forEach { component -> + component.width = settingWidth + component.render() + } + + NVGRenderer.popScissor() + + settingComponents.forEach { setting -> + when (setting) { + is UIModeSetting -> setting.renderDropdown() + is UIColorSetting -> setting.drawColorPicker() + } + } } fun mouseClicked(mouseX: Float, mouseY: Float, button: Int): Boolean { if (!visible) return false val target = module ?: return false - if (button != 0) return false - val toggleY = popupY + popupPadding + headerHeight + if (button == 0) { + val closeX = panelX + panelWidth - padding - 26f + val closeY = panelY + 12f + if (mouseX >= closeX && mouseX <= closeX + 26f && mouseY >= closeY && mouseY <= closeY + 26f) { + hide() + return true + } + } + + if (button != 0) return containsPoint(mouseX, mouseY) + + val controlsY = panelY + headerHeight + 10f + val buttonHeight = 30f + val toggleText = if (target.enabled) "Disable" else "Enable" - val toggleWidth = NVGRenderer.textWidth(toggleText, 12f) + 30f - val toggleX = popupX + popupWidth - toggleWidth - popupPadding + val toggleWidth = NVGRenderer.textWidth(toggleText, 13f) + 30f + val toggleX = panelX + padding - if (mouseX >= toggleX && mouseX <= toggleX + toggleWidth && mouseY >= toggleY && mouseY <= toggleY + rowHeight - 6f) { + if (mouseX >= toggleX && mouseX <= toggleX + toggleWidth && + mouseY >= controlsY && mouseY <= controlsY + buttonHeight + ) { target.enabled = !target.enabled toggleAnim.start() return true } - val resetY = toggleY + rowHeight + 6f - if (mouseX >= popupX + popupPadding && mouseX <= popupX + popupWidth - popupPadding && mouseY >= resetY && mouseY <= resetY + rowHeight - 6f) { + val resetText = "Reset Position" + val resetWidth = NVGRenderer.textWidth(resetText, 13f) + 30f + val resetX = panelX + panelWidth - padding - resetWidth + + if (mouseX >= resetX && mouseX <= resetX + resetWidth && + mouseY >= controlsY && mouseY <= controlsY + buttonHeight + ) { target.resetPosition() buttonAnim.start() return true } + for (component in settingComponents) { + if (component.mouseClicked(button)) return true + } + return containsPoint(mouseX, mouseY) } + fun mouseReleased(button: Int): Boolean { + if (!visible) return false + for (component in settingComponents) { + if (component.mouseReleased(button)) return true + } + return false + } + + fun mouseDragged(button: Int, offsetX: Double, offsetY: Double): Boolean { + if (!visible) return false + for (component in settingComponents) { + if (component.mouseDragged(button, offsetX, offsetY)) return true + } + return false + } + + fun mouseScrolled(horizontalAmount: Double, verticalAmount: Double): Boolean { + if (!visible) return false + + for (component in settingComponents) { + if (component.mouseScrolled(horizontalAmount, verticalAmount)) return true + } + + if (isHoveringOver(panelX, panelY, panelWidth, panelHeight)) { + settingsScroll.handleScroll(verticalAmount) + return true + } + + return false + } + + fun keyPressed(input: KeyEvent): Boolean { + if (!visible) return false + for (component in settingComponents) { + if (component.keyPressed(input)) return true + } + return false + } + + fun charTyped(input: CharacterEvent): Boolean { + if (!visible) return false + for (component in settingComponents) { + if (component.charTyped(input)) return true + } + return false + } + fun containsPoint(px: Float, py: Float): Boolean { if (!visible) return false - return px >= popupX && px <= popupX + popupWidth && py >= popupY && py <= popupY + popupHeight + return px >= panelX && px <= panelX + panelWidth && py >= panelY && py <= panelY + panelHeight } } diff --git a/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UIHudList.kt b/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UIHudList.kt index baad328..fff1f94 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UIHudList.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UIHudList.kt @@ -1,7 +1,7 @@ package org.cobalt.internal.ui.panel.panels import java.awt.Color -import org.cobalt.api.hud.HudModule +import org.cobalt.api.hud.HudElement import org.cobalt.api.hud.HudModuleManager import org.cobalt.api.ui.theme.ThemeManager import org.cobalt.api.util.ui.NVGRenderer @@ -22,7 +22,7 @@ internal class UIHudList : UIPanel( ) { private val topBar = UITopbar("HUD Modules") - private val allEntries = HudModuleManager.getModules().map { HudModuleEntry(it) } + private var allEntries = HudModuleManager.getElements().map { HudElementEntry(it) } private var entries = allEntries private val scrollHandler = ScrollHandler() @@ -47,6 +47,13 @@ internal class UIHudList : UIPanel( HudModuleManager.resetAllPositions() } + fun refreshEntries() { + allEntries = HudModuleManager.getElements().map { HudElementEntry(it) } + entries = allEntries + components.removeAll { it is HudElementEntry } + components.addAll(0, allEntries) + } + init { components.addAll(allEntries) components.add(editButton) @@ -59,8 +66,8 @@ internal class UIHudList : UIPanel( } else { val searchLower = searchText.lowercase() allEntries.filter { - it.module.name.lowercase().contains(searchLower) || - it.module.description.lowercase().contains(searchLower) + it.module.name.lowercase().contains(searchLower) || + it.module.description.lowercase().contains(searchLower) } } } @@ -148,8 +155,8 @@ internal class UIHudList : UIPanel( } } - private class HudModuleEntry( - val module: HudModule, + private class HudElementEntry( + val module: HudElement, ) : UIComponent(0F, 0F, 890F, 60F) { private var expanded = false @@ -158,6 +165,11 @@ internal class UIHudList : UIPanel( when (it) { is CheckboxSetting -> UICheckboxSetting(it) is ColorSetting -> UIColorSetting(it) + is InfoSetting -> UIInfoSetting(it) + is KeyBindSetting -> UIKeyBindSetting(it) + is ModeSetting -> UIModeSetting(it) + is RangeSetting -> UIRangeSetting(it) + is SliderSetting -> UISliderSetting(it) else -> UITextSetting(it as TextSetting) } } diff --git a/src/main/kotlin/org/cobalt/internal/ui/screen/UIConfig.kt b/src/main/kotlin/org/cobalt/internal/ui/screen/UIConfig.kt index 265bc01..2a72290 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/screen/UIConfig.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/screen/UIConfig.kt @@ -8,7 +8,6 @@ import org.cobalt.api.event.annotation.SubscribeEvent import org.cobalt.api.event.impl.render.NvgEvent import org.cobalt.api.util.ui.NVGRenderer import org.cobalt.internal.helper.Config -import org.cobalt.internal.helper.HudConfig import org.cobalt.internal.ui.UIScreen import org.cobalt.internal.ui.animation.BounceAnimation import org.cobalt.internal.ui.components.tooltips.TooltipManager @@ -112,7 +111,6 @@ internal object UIConfig : UIScreen() { override fun onClose() { Config.saveModulesConfig() - HudConfig.save() wasClosed = true super.onClose() } diff --git a/src/main/kotlin/org/cobalt/internal/ui/screen/UIHudEditor.kt b/src/main/kotlin/org/cobalt/internal/ui/screen/UIHudEditor.kt index ec7680f..535fc03 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/screen/UIHudEditor.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/screen/UIHudEditor.kt @@ -1,16 +1,19 @@ package org.cobalt.internal.ui.screen import java.awt.Color +import kotlin.math.round +import net.minecraft.client.input.CharacterEvent +import net.minecraft.client.input.KeyEvent import net.minecraft.client.input.MouseButtonEvent import org.cobalt.api.event.EventBus import org.cobalt.api.event.annotation.SubscribeEvent import org.cobalt.api.event.impl.render.NvgEvent import org.cobalt.api.hud.HudAnchor -import org.cobalt.api.hud.HudModule +import org.cobalt.api.hud.HudElement import org.cobalt.api.hud.HudModuleManager import org.cobalt.api.ui.theme.ThemeManager import org.cobalt.api.util.ui.NVGRenderer -import org.cobalt.internal.helper.HudConfig +import org.cobalt.internal.helper.Config import org.cobalt.internal.ui.UIScreen import org.cobalt.internal.ui.hud.HudSettingsPopup import org.cobalt.internal.ui.hud.SnapHelper @@ -18,10 +21,14 @@ import org.cobalt.internal.ui.util.mouseX import org.cobalt.internal.ui.util.mouseY internal class UIHudEditor : UIScreen() { - private var selectedModule: HudModule? = null + private var selectedElement: HudElement? = null private var dragging = false private var dragOffsetX = 0f private var dragOffsetY = 0f + private var resizing = false + private var initialMouseX = 0f + private var initialMouseY = 0f + private var initialWidth = 0f private val snapHelper = SnapHelper() private val settingsPopup = HudSettingsPopup() @@ -30,6 +37,10 @@ internal class UIHudEditor : UIScreen() { EventBus.register(this) } + companion object { + private const val RESIZE_HANDLE_SIZE = 8f + } + @Suppress("unused") @SubscribeEvent fun onRender(event: NvgEvent) { @@ -43,7 +54,7 @@ internal class UIHudEditor : UIScreen() { NVGRenderer.rect(0f, 0f, width, height, Color(0, 0, 0, 128).rgb) renderGrid(width, height) - renderModuleBounds(width, height) + renderElementBounds(width, height) renderGuides(width, height) settingsPopup.render() renderInstructions(width, height) @@ -65,16 +76,22 @@ internal class UIHudEditor : UIScreen() { } } - private fun renderModuleBounds(width: Float, height: Float) { - HudModuleManager.getModules().forEach { module -> - val (sx, sy) = module.getScreenPosition(width, height) - val w = module.getScaledWidth() - val h = module.getScaledHeight() - val isSelected = module == selectedModule + private fun renderElementBounds(width: Float, height: Float) { + HudModuleManager.getElements().forEach { element -> + val (sx, sy) = element.getScreenPosition(width, height) + val w = element.getScaledWidth() + val h = element.getScaledHeight() + val isSelected = element == selectedElement val borderColor = if (isSelected) ThemeManager.currentTheme.accent else ThemeManager.currentTheme.controlBorder val borderThickness = if (isSelected) 2f else 1f NVGRenderer.hollowRect(sx, sy, w, h, borderThickness, borderColor, 4f) - NVGRenderer.text(module.name, sx, sy + h + 6f, 12f, ThemeManager.currentTheme.textSecondary) + NVGRenderer.text(element.name, sx, sy + h + 6f, 12f, ThemeManager.currentTheme.textSecondary) + + if (element == selectedElement) { + val handleX = sx + w - RESIZE_HANDLE_SIZE + val handleY = sy + h - RESIZE_HANDLE_SIZE + NVGRenderer.rect(handleX, handleY, RESIZE_HANDLE_SIZE, RESIZE_HANDLE_SIZE, ThemeManager.currentTheme.accent, 2f) + } } } @@ -90,7 +107,7 @@ internal class UIHudEditor : UIScreen() { } private fun renderInstructions(width: Float, height: Float) { - val text = "Left-click to select, drag to move | Right-click for settings | Scroll to resize | ESC to save and exit" + val text = "Left-click to select, drag to move | Right-click for settings | Drag corner to resize | ESC to save and exit" val textWidth = NVGRenderer.textWidth(text, 12f) val padding = 14f val boxWidth = textWidth + padding * 2f @@ -115,17 +132,35 @@ internal class UIHudEditor : UIScreen() { } if (button == 1) { - val target = findModuleUnderCursor(mx, my, screenWidth, screenHeight) + val target = findElementUnderCursor(mx, my, screenWidth, screenHeight) if (target != null) { - selectedModule = target - settingsPopup.show(target, mx + 8f, my + 6f) + selectedElement = target + settingsPopup.show(target, screenWidth, screenHeight) return true } } if (button == 0) { - val target = findModuleUnderCursor(mx, my, screenWidth, screenHeight) - selectedModule = target + if (selectedElement != null) { + val element = selectedElement!! + val (sx, sy) = element.getScreenPosition(screenWidth, screenHeight) + val w = element.getScaledWidth() + val h = element.getScaledHeight() + val handleX = sx + w - RESIZE_HANDLE_SIZE + val handleY = sy + h - RESIZE_HANDLE_SIZE + + if (mx >= handleX && mx <= handleX + RESIZE_HANDLE_SIZE && + my >= handleY && my <= handleY + RESIZE_HANDLE_SIZE) { + resizing = true + initialMouseX = mx + initialMouseY = my + initialWidth = w + return true + } + } + + val target = findElementUnderCursor(mx, my, screenWidth, screenHeight) + selectedElement = target if (target != null) { val (sx, sy) = target.getScreenPosition(screenWidth, screenHeight) dragOffsetX = mx - sx @@ -140,67 +175,108 @@ internal class UIHudEditor : UIScreen() { } override fun mouseReleased(click: MouseButtonEvent): Boolean { + if (settingsPopup.mouseReleased(click.button())) return true + if (click.button() == 0 && dragging) { dragging = false - selectedModule?.let { module -> + selectedElement?.let { element -> val screenWidth = mc.window.screenWidth.toFloat() val screenHeight = mc.window.screenHeight.toFloat() - val (sx, sy) = module.getScreenPosition(screenWidth, screenHeight) - val (snappedX, snappedY) = snapHelper.snapToGrid(sx, sy) - updateModulePosition(module, snappedX, snappedY, screenWidth, screenHeight) + val mx = mouseX.toFloat() + val my = mouseY.toFloat() + val newScreenX = mx - dragOffsetX + val newScreenY = my - dragOffsetY + + val otherBounds = HudModuleManager.getElements() + .filter { it != element } + .map { + val (sx, sy) = it.getScreenPosition(screenWidth, screenHeight) + SnapHelper.ModuleBounds(sx, sy, it.getScaledWidth(), it.getScaledHeight()) + } + + val (alignedX, alignedY) = snapHelper.findAlignmentGuides( + newScreenX, newScreenY, + element.getScaledWidth(), element.getScaledHeight(), + screenWidth, screenHeight, + otherBounds, + ) + + updateElementPosition(element, round(alignedX), round(alignedY), screenWidth, screenHeight) snapHelper.clearGuides() } return true } + + if (resizing) { + resizing = false + return true + } + return super.mouseReleased(click) } - override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean { - if (click.button() != 0 || !dragging) return super.mouseDragged(click, offsetX, offsetY) - - val module = selectedModule ?: return super.mouseDragged(click, offsetX, offsetY) - val screenWidth = mc.window.screenWidth.toFloat() - val screenHeight = mc.window.screenHeight.toFloat() - val mx = mouseX.toFloat() - val my = mouseY.toFloat() - val newScreenX = mx - dragOffsetX - val newScreenY = my - dragOffsetY - - val otherBounds = HudModuleManager.getModules() - .filter { it != module } - .map { - val (sx, sy) = it.getScreenPosition(screenWidth, screenHeight) - SnapHelper.ModuleBounds(sx, sy, it.getScaledWidth(), it.getScaledHeight()) - } - - val (alignedX, alignedY) = snapHelper.findAlignmentGuides( - newScreenX, - newScreenY, - module.getScaledWidth(), - module.getScaledHeight(), - screenWidth, - screenHeight, - otherBounds, - ) - - updateModulePosition(module, alignedX, alignedY, screenWidth, screenHeight) - return true + override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean { + if (settingsPopup.mouseDragged(click.button(), offsetX, offsetY)) return true + + if (click.button() != 0) return super.mouseDragged(click, offsetX, offsetY) + + val element = selectedElement ?: return super.mouseDragged(click, offsetX, offsetY) + val mx = mouseX.toFloat() + + if (resizing) { + val newWidth = initialWidth + (mx - initialMouseX) + val newScale = newWidth / element.getBaseWidth() + element.scale = newScale.coerceIn(0.5f, 3.0f) + return true + } + + if (!dragging) return super.mouseDragged(click, offsetX, offsetY) + + val screenWidth = mc.window.screenWidth.toFloat() + val screenHeight = mc.window.screenHeight.toFloat() + val my = mouseY.toFloat() + val newScreenX = mx - dragOffsetX + val newScreenY = my - dragOffsetY + + val otherBounds = HudModuleManager.getElements() + .filter { it != element } + .map { + val (sx, sy) = it.getScreenPosition(screenWidth, screenHeight) + SnapHelper.ModuleBounds(sx, sy, it.getScaledWidth(), it.getScaledHeight()) + } + + val (alignedX, alignedY) = snapHelper.findAlignmentGuides( + newScreenX, + newScreenY, + element.getScaledWidth(), + element.getScaledHeight(), + screenWidth, + screenHeight, + otherBounds, + ) + + updateElementPosition(element, round(alignedX), round(alignedY), screenWidth, screenHeight) + return true + } + + override fun mouseScrolled( + mouseX: Double, + mouseY: Double, + horizontalAmount: Double, + verticalAmount: Double, + ): Boolean { + if (settingsPopup.mouseScrolled(horizontalAmount, verticalAmount)) return true + return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount) + } + + override fun keyPressed(input: KeyEvent): Boolean { + if (settingsPopup.keyPressed(input)) return true + return super.keyPressed(input) } - override fun mouseScrolled( - mouseX: Double, - mouseY: Double, - horizontalAmount: Double, - verticalAmount: Double, - ): Boolean { - val screenWidth = mc.window.screenWidth.toFloat() - val screenHeight = mc.window.screenHeight.toFloat() - val target = findModuleUnderCursor(mouseX.toFloat(), mouseY.toFloat(), screenWidth, screenHeight) - if (target != null) { - target.scale = (target.scale + (if (verticalAmount > 0) 0.1f else -0.1f)).coerceIn(0.5f, 3.0f) - return true - } - return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount) + override fun charTyped(input: CharacterEvent): Boolean { + if (settingsPopup.charTyped(input)) return true + return super.charTyped(input) } override fun init() { @@ -210,75 +286,75 @@ internal class UIHudEditor : UIScreen() { override fun onClose() { HudModuleManager.isEditorOpen = false - HudConfig.save() + Config.saveModulesConfig() EventBus.unregister(this) super.onClose() } - private fun findModuleUnderCursor( + private fun findElementUnderCursor( mouseX: Float, mouseY: Float, screenWidth: Float, screenHeight: Float, - ): HudModule? { - return HudModuleManager.getModules().lastOrNull { + ): HudElement? { + return HudModuleManager.getElements().lastOrNull { it.containsPoint(mouseX, mouseY, screenWidth, screenHeight) } } - private fun updateModulePosition( - module: HudModule, + private fun updateElementPosition( + element: HudElement, newScreenX: Float, newScreenY: Float, screenWidth: Float, screenHeight: Float, ) { - val w = module.getScaledWidth() - val h = module.getScaledHeight() - when (module.anchor) { + val w = element.getScaledWidth() + val h = element.getScaledHeight() + when (element.anchor) { HudAnchor.TOP_LEFT -> { - module.offsetX = newScreenX - module.offsetY = newScreenY + element.offsetX = newScreenX + element.offsetY = newScreenY } HudAnchor.TOP_CENTER -> { - module.offsetX = newScreenX - (screenWidth / 2f - w / 2f) - module.offsetY = newScreenY + element.offsetX = newScreenX - (screenWidth / 2f - w / 2f) + element.offsetY = newScreenY } HudAnchor.TOP_RIGHT -> { - module.offsetX = screenWidth - w - newScreenX - module.offsetY = newScreenY + element.offsetX = screenWidth - w - newScreenX + element.offsetY = newScreenY } HudAnchor.CENTER_LEFT -> { - module.offsetX = newScreenX - module.offsetY = newScreenY - (screenHeight / 2f - h / 2f) + element.offsetX = newScreenX + element.offsetY = newScreenY - (screenHeight / 2f - h / 2f) } HudAnchor.CENTER -> { - module.offsetX = newScreenX - (screenWidth / 2f - w / 2f) - module.offsetY = newScreenY - (screenHeight / 2f - h / 2f) + element.offsetX = newScreenX - (screenWidth / 2f - w / 2f) + element.offsetY = newScreenY - (screenHeight / 2f - h / 2f) } HudAnchor.CENTER_RIGHT -> { - module.offsetX = screenWidth - w - newScreenX - module.offsetY = newScreenY - (screenHeight / 2f - h / 2f) + element.offsetX = screenWidth - w - newScreenX + element.offsetY = newScreenY - (screenHeight / 2f - h / 2f) } HudAnchor.BOTTOM_LEFT -> { - module.offsetX = newScreenX - module.offsetY = screenHeight - h - newScreenY + element.offsetX = newScreenX + element.offsetY = screenHeight - h - newScreenY } HudAnchor.BOTTOM_CENTER -> { - module.offsetX = newScreenX - (screenWidth / 2f - w / 2f) - module.offsetY = screenHeight - h - newScreenY + element.offsetX = newScreenX - (screenWidth / 2f - w / 2f) + element.offsetY = screenHeight - h - newScreenY } HudAnchor.BOTTOM_RIGHT -> { - module.offsetX = screenWidth - w - newScreenX - module.offsetY = screenHeight - h - newScreenY + element.offsetX = screenWidth - w - newScreenX + element.offsetY = screenHeight - h - newScreenY } } } From a3019d97ef8f8268d7d6574d64e2aefb10f59834 Mon Sep 17 00:00:00 2001 From: forbai Date: Wed, 11 Feb 2026 15:24:56 +0100 Subject: [PATCH 06/10] feat(hud): enhance documentation for HUD-related classes and settings tyy opus 4.6 for the docs --- src/main/kotlin/org/cobalt/api/addon/Addon.kt | 9 ++++ .../kotlin/org/cobalt/api/hud/HudAnchor.kt | 10 ++++ .../kotlin/org/cobalt/api/hud/HudElement.kt | 30 ++++++++++++ .../kotlin/org/cobalt/api/hud/HudModuleDSL.kt | 48 ++++++++++++++++--- .../kotlin/org/cobalt/api/module/Module.kt | 11 +++++ .../org/cobalt/api/module/setting/Setting.kt | 18 +++++++ .../api/module/setting/SettingsContainer.kt | 6 +++ .../module/setting/impl/CheckboxSetting.kt | 1 + .../api/module/setting/impl/ColorSetting.kt | 1 + .../api/module/setting/impl/KeyBindSetting.kt | 4 ++ .../api/module/setting/impl/ModeSetting.kt | 6 +++ .../api/module/setting/impl/RangeSetting.kt | 6 +++ .../api/module/setting/impl/SliderSetting.kt | 6 +++ .../api/module/setting/impl/TextSetting.kt | 1 + 14 files changed, 151 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/org/cobalt/api/addon/Addon.kt b/src/main/kotlin/org/cobalt/api/addon/Addon.kt index 30fc313..a25d9f1 100644 --- a/src/main/kotlin/org/cobalt/api/addon/Addon.kt +++ b/src/main/kotlin/org/cobalt/api/addon/Addon.kt @@ -2,11 +2,20 @@ package org.cobalt.api.addon import org.cobalt.api.module.Module +/** + * Base class for Cobalt addons. Implement this to provide modules, HUD elements, and commands. + * + * Define your addon entry in `cobalt.addon.json` and return your modules from [getModules]. + */ abstract class Addon { + /** Called when the addon is loaded during game startup. */ abstract fun onLoad() + + /** Called when the addon is unloaded. */ abstract fun onUnload() + /** Returns the list of [Module]s this addon provides. Override to register your modules. */ open fun getModules(): List = emptyList() diff --git a/src/main/kotlin/org/cobalt/api/hud/HudAnchor.kt b/src/main/kotlin/org/cobalt/api/hud/HudAnchor.kt index c8fd856..47319d3 100644 --- a/src/main/kotlin/org/cobalt/api/hud/HudAnchor.kt +++ b/src/main/kotlin/org/cobalt/api/hud/HudAnchor.kt @@ -1,5 +1,15 @@ package org.cobalt.api.hud +/** + * Screen anchor point for positioning a [HudElement]. + * + * Offsets push the element **inward** from the anchor edge: + * - LEFT anchors: `offsetX` moves right from the left edge + * - RIGHT anchors: `offsetX` moves left from the right edge + * - TOP anchors: `offsetY` moves down from the top edge + * - BOTTOM anchors: `offsetY` moves up from the bottom edge + * - CENTER anchors: offsets adjust from the screen center + */ enum class HudAnchor { TOP_LEFT, TOP_CENTER, diff --git a/src/main/kotlin/org/cobalt/api/hud/HudElement.kt b/src/main/kotlin/org/cobalt/api/hud/HudElement.kt index 76d13b5..015d806 100644 --- a/src/main/kotlin/org/cobalt/api/hud/HudElement.kt +++ b/src/main/kotlin/org/cobalt/api/hud/HudElement.kt @@ -3,16 +3,36 @@ package org.cobalt.api.hud import org.cobalt.api.module.setting.Setting import org.cobalt.api.module.setting.SettingsContainer +/** + * A HUD overlay element rendered on the in-game screen. + * + * Created via the [hudElement][org.cobalt.api.hud.hudElement] DSL inside a [Module][org.cobalt.api.module.Module]. + * Each element is independently draggable, scalable, and toggleable through the HUD editor. + * Position, scale, enabled state, and settings are automatically persisted. + * + * @property id Unique identifier used for serialization. Must be stable across versions. + * @property name Display name shown in the HUD editor and settings popup. + * @property description Optional description shown in the UI. + */ abstract class HudElement( val id: String, val name: String, val description: String = "", ) : SettingsContainer { + /** Whether this element is rendered. Toggled by the user in the HUD editor. */ var enabled: Boolean = true + + /** Screen anchor point. Determines which edge/corner offsets are relative to. */ var anchor: HudAnchor = HudAnchor.TOP_LEFT + + /** Horizontal offset from the [anchor] edge, in pixels. */ var offsetX: Float = 10f + + /** Vertical offset from the [anchor] edge, in pixels. */ var offsetY: Float = 10f + + /** Render scale factor, clamped to 0.5-3.0 on load. */ var scale: Float = 1.0f protected open val defaultAnchor: HudAnchor = HudAnchor.TOP_LEFT @@ -30,8 +50,17 @@ abstract class HudElement( return settingsList } + /** Returns the unscaled width of this element in pixels. */ abstract fun getBaseWidth(): Float + + /** Returns the unscaled height of this element in pixels. */ abstract fun getBaseHeight(): Float + + /** + * Called every frame when this element is [enabled]. + * Draw using [NVGRenderer][org.cobalt.api.util.ui.NVGRenderer] — coordinates are pre-translated, + * so draw relative to (0, 0). + */ abstract fun render(screenX: Float, screenY: Float, scale: Float) fun getScaledWidth(): Float = getBaseWidth() * scale @@ -44,6 +73,7 @@ abstract class HudElement( screenWidth, screenHeight ) + /** Resets position, anchor, and scale to the defaults set in the DSL builder. */ fun resetPosition() { anchor = defaultAnchor offsetX = defaultOffsetX diff --git a/src/main/kotlin/org/cobalt/api/hud/HudModuleDSL.kt b/src/main/kotlin/org/cobalt/api/hud/HudModuleDSL.kt index 9284682..f2cbefd 100644 --- a/src/main/kotlin/org/cobalt/api/hud/HudModuleDSL.kt +++ b/src/main/kotlin/org/cobalt/api/hud/HudModuleDSL.kt @@ -5,20 +5,30 @@ import org.cobalt.api.module.setting.Setting import org.cobalt.api.module.setting.SettingsContainer /** - * DSL extension to create a HUD element inside a Module. + * Creates and registers a [HudElement] on this module using a DSL builder. * - * Example: + * Usage: * ``` * class MyModule : Module("My Module") { - * val speedHud = hudElement("speed", "Speed Display") { - * width { 80f } - * height { 20f } + * val hud = hudElement("my-hud", "My HUD", "Shows something") { * anchor = HudAnchor.TOP_RIGHT + * offsetX = 10f + * offsetY = 10f + * * val showDecimals = setting(CheckboxSetting("Decimals", "", false)) - * render { x, y, scale -> /* use showDecimals.value */ } + * + * width { 80f } + * height { 20f } + * render { screenX, screenY, scale -> /* use showDecimals.value */ } * } * } * ``` + * + * @param id Stable unique identifier used for serialization. + * @param name Display name shown in the HUD editor. + * @param description Optional description for the settings popup. + * @param init Builder block — configure position, settings, size, and rendering. + * @return The constructed [HudElement] instance. */ fun Module.hudElement( id: String, @@ -33,6 +43,13 @@ fun Module.hudElement( return element } +/** + * Builder for configuring a [HudElement] inside the [hudElement] DSL block. + * + * Register settings with [setting] and read their values via `.value`. + * Do **not** use `by` delegation for settings inside this builder — it won't compile + * because Kotlin local delegates require a different type signature. + */ class HudElementBuilder( private val id: String, private val name: String, @@ -41,9 +58,17 @@ class HudElementBuilder( private var widthProvider: () -> Float = { 100f } private var heightProvider: () -> Float = { 20f } + + /** Screen anchor point. Determines which edge/corner offsets are relative to. */ var anchor: HudAnchor = HudAnchor.TOP_LEFT + + /** Horizontal offset from the [anchor] edge, in pixels. */ var offsetX: Float = 10f + + /** Vertical offset from the [anchor] edge, in pixels. */ var offsetY: Float = 10f + + /** Default render scale (clamped to 0.5-3.0 on load). */ var scale: Float = 1.0f private var renderLambda: ((Float, Float, Float) -> Unit)? = null @@ -57,19 +82,30 @@ class HudElementBuilder( return settingsList } + /** Sets the dynamic width provider. Called every frame — can return values based on setting state. */ fun width(provider: () -> Float) { widthProvider = provider } + /** Sets the dynamic height provider. Called every frame — can return values based on setting state. */ fun height(provider: () -> Float) { heightProvider = provider } + /** + * Registers a setting on this HUD element and returns it. + * Access the current value via `.value`: + * ``` + * val speed = setting(SliderSetting("Speed", "Movement speed", 1.0, 0.1, 5.0)) + * render { _, _, _ -> /* use speed.value */ } + * ``` + */ fun > setting(setting: S): S { addSetting(setting) return setting } + /** Sets the render callback, called every frame when this element is enabled. */ fun render(block: (screenX: Float, screenY: Float, scale: Float) -> Unit) { renderLambda = block } diff --git a/src/main/kotlin/org/cobalt/api/module/Module.kt b/src/main/kotlin/org/cobalt/api/module/Module.kt index 8213905..4504c5c 100644 --- a/src/main/kotlin/org/cobalt/api/module/Module.kt +++ b/src/main/kotlin/org/cobalt/api/module/Module.kt @@ -4,6 +4,15 @@ import org.cobalt.api.hud.HudElement import org.cobalt.api.module.setting.Setting import org.cobalt.api.module.setting.SettingsContainer +/** + * Base class for all modules. Extend this to create addon functionality. + * + * Modules can contain settings (via [SettingsContainer]) and HUD elements + * (via the [hudElement][org.cobalt.api.hud.hudElement] DSL). Return your modules + * from [Addon.getModules][org.cobalt.api.addon.Addon.getModules]. + * + * @property name Display name shown in the UI. + */ abstract class Module(val name: String) : SettingsContainer { private val settingsList = mutableListOf>() @@ -17,10 +26,12 @@ abstract class Module(val name: String) : SettingsContainer { return settingsList } + /** Registers a HUD element on this module. Called automatically by the [hudElement] DSL. */ fun addHudElement(element: HudElement) { hudElementsList.add(element) } + /** Returns all HUD elements registered on this module. */ fun getHudElements(): List { return hudElementsList } diff --git a/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt b/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt index b967aec..1b560d8 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt @@ -5,6 +5,24 @@ import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty +/** + * Base class for all module and HUD element settings. + * + * Settings are automatically serialized to/from the config file. + * Inside a [Module][org.cobalt.api.module.Module], use `by` delegation: + * ``` + * val speed by SliderSetting("Speed", "Movement speed", 1.0, 0.0, 5.0) + * ``` + * Inside a [HudElementBuilder][org.cobalt.api.hud.HudElementBuilder], use `setting()` + `.value`: + * ``` + * val speed = setting(SliderSetting("Speed", "Movement speed", 1.0, 0.0, 5.0)) + * // access via speed.value + * ``` + * + * @property name Display name shown in the settings UI. + * @property description Tooltip/description shown in the settings UI. + * @property value Current value. Updated by the UI and persisted automatically. + */ abstract class Setting( val name: String, val description: String, diff --git a/src/main/kotlin/org/cobalt/api/module/setting/SettingsContainer.kt b/src/main/kotlin/org/cobalt/api/module/setting/SettingsContainer.kt index 1e07ee7..c579ced 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/SettingsContainer.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/SettingsContainer.kt @@ -1,9 +1,15 @@ package org.cobalt.api.module.setting +/** + * Interface for objects that hold [Setting]s (modules and HUD elements). + * Settings registered here appear in the UI and are automatically persisted. + */ interface SettingsContainer { + /** Registers one or more settings on this container. */ fun addSetting(vararg settings: Setting<*>) + /** Returns all registered settings. */ fun getSettings(): List> } diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/CheckboxSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/CheckboxSetting.kt index edde773..a3082c9 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/CheckboxSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/CheckboxSetting.kt @@ -4,6 +4,7 @@ import com.google.gson.JsonElement import com.google.gson.JsonPrimitive import org.cobalt.api.module.setting.Setting +/** Boolean toggle setting. Renders as a checkbox in the UI. */ class CheckboxSetting( name: String, description: String, diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/ColorSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/ColorSetting.kt index 5392548..76a6aac 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/ColorSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/ColorSetting.kt @@ -4,6 +4,7 @@ import com.google.gson.JsonElement import com.google.gson.JsonPrimitive import org.cobalt.api.module.setting.Setting +/** ARGB color picker setting. Value is an ARGB integer (e.g. `0xFFFF0000.toInt()` for red). */ class ColorSetting( name: String, description: String, diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/KeyBindSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/KeyBindSetting.kt index 166a07c..31bccb8 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/KeyBindSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/KeyBindSetting.kt @@ -6,6 +6,10 @@ import org.cobalt.api.module.setting.Setting import org.cobalt.api.util.helper.KeyBind import org.lwjgl.glfw.GLFW +/** + * Key binding setting. Value is a [KeyBind] — use `value.isPressed()` to check for key presses. + * Default to `KeyBind(-1)` for unbound. + */ class KeyBindSetting( name: String, description: String, diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/ModeSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/ModeSetting.kt index abfb625..523ba0a 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/ModeSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/ModeSetting.kt @@ -4,6 +4,12 @@ import com.google.gson.JsonElement import com.google.gson.JsonPrimitive import org.cobalt.api.module.setting.Setting +/** + * Dropdown/cycle setting that selects from a list of named options. + * Value is the selected index. Access the label via `options[value]`. + * + * @property options The available option labels. + */ class ModeSetting( name: String, description: String, diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/RangeSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/RangeSetting.kt index 68c8f17..e7a4b85 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/RangeSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/RangeSetting.kt @@ -7,6 +7,12 @@ import java.math.BigDecimal import java.math.RoundingMode import org.cobalt.api.module.setting.Setting +/** + * Dual-handle range slider setting. Value is a `Pair` (start, end). + * + * @property min Minimum allowed value for both handles. + * @property max Maximum allowed value for both handles. + */ class RangeSetting( name: String, description: String, diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/SliderSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/SliderSetting.kt index 6164927..61d7d35 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/SliderSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/SliderSetting.kt @@ -4,6 +4,12 @@ import com.google.gson.JsonElement import com.google.gson.JsonPrimitive import org.cobalt.api.module.setting.Setting +/** + * Numeric slider setting with min/max bounds. + * + * @property min Minimum allowed value. + * @property max Maximum allowed value. + */ class SliderSetting( name: String, description: String, diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/TextSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/TextSetting.kt index 4f41b5f..df7bf75 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/TextSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/TextSetting.kt @@ -4,6 +4,7 @@ import com.google.gson.JsonElement import com.google.gson.JsonPrimitive import org.cobalt.api.module.setting.Setting +/** String input setting. Renders as a text field in the UI. */ class TextSetting( name: String, description: String, From f355e89362c2be4b20c51d70440bf3606567aa0b Mon Sep 17 00:00:00 2001 From: forbai Date: Wed, 11 Feb 2026 15:41:42 +0100 Subject: [PATCH 07/10] feat(ui): update UISidebar button selection logic for HUD activation --- .../org/cobalt/internal/ui/panel/panels/UISidebar.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt b/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt index 0711bee..6199b91 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt @@ -22,16 +22,12 @@ internal class UISidebar : UIPanel( private val moduleButton = UIButton("/assets/cobalt/icons/box.svg") { UIConfig.swapBodyPanel(UIAddonList()) - isHudActive = false } private val hudButton = UIButton("/assets/cobalt/icons/palette.svg") { UIHudEditor().openUI() - isHudActive = true } - private var isHudActive = false - private val steveIcon = NVGRenderer.createImage("/assets/cobalt/steve.png") private val userIcon = try { NVGRenderer.createImage("https://mc-heads.net/avatar/${Minecraft.getInstance().user.profileId}/100/face.png") @@ -55,12 +51,12 @@ internal class UISidebar : UIPanel( NVGRenderer.text("cb", x + width / 2F - 15F, y + 25F, 25F, ThemeManager.currentTheme.text) moduleButton - .setSelected(!isHudActive) + .setSelected(true) .updateBounds(x + (width / 2F) - (moduleButton.width / 2F), y + 75F) .render() hudButton - .setSelected(isHudActive) + .setSelected(if (isHoveringOver(x + (width / 2F) - (hudButton.width / 2F), y + 75F + 35F, hudButton.width, hudButton.height)) true else false) .updateBounds(x + (width / 2F) - (hudButton.width / 2F), y + 75F + 35F) .render() From feb9522b2dad9de3c25cc1c8fbfec50e2cdb5eec Mon Sep 17 00:00:00 2001 From: ForBai Date: Mon, 16 Feb 2026 01:55:13 +0100 Subject: [PATCH 08/10] feat: implement dynamic color modes for ColorSetting with support for static, rainbow, theme, and tweaked theme colors --- .../org/cobalt/api/module/setting/Setting.kt | 2 +- .../api/module/setting/impl/ColorMode.kt | 76 ++ .../api/module/setting/impl/ColorSetting.kt | 180 ++- .../setting/impl/RainbowPhaseProvider.kt | 24 + .../module/setting/impl/ThemeColorResolver.kt | 198 ++++ .../ui/components/settings/UIColorSetting.kt | 1008 +++++++++++++++-- .../internal/ui/panel/panels/UISidebar.kt | 2 +- 7 files changed, 1370 insertions(+), 120 deletions(-) create mode 100644 src/main/kotlin/org/cobalt/api/module/setting/impl/ColorMode.kt create mode 100644 src/main/kotlin/org/cobalt/api/module/setting/impl/RainbowPhaseProvider.kt create mode 100644 src/main/kotlin/org/cobalt/api/module/setting/impl/ThemeColorResolver.kt diff --git a/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt b/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt index 1b560d8..6427aff 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt @@ -26,7 +26,7 @@ import kotlin.reflect.KProperty abstract class Setting( val name: String, val description: String, - var value: T, + open var value: T, ) : ReadWriteProperty, PropertyDelegateProvider> { override operator fun provideDelegate(thisRef: SettingsContainer, property: KProperty<*>): ReadWriteProperty { diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/ColorMode.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/ColorMode.kt new file mode 100644 index 0000000..910021a --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/ColorMode.kt @@ -0,0 +1,76 @@ +package org.cobalt.api.module.setting.impl + +/** + * Sealed class representing different color modes for the color picker system. + * + * Supports: + * - Static ARGB color + * - Per-instance rainbow with adjustable parameters + * - Globally synced rainbow + * - Theme property reference + * - Theme property with HSB tweaks + */ +sealed class ColorMode { + + /** + * Static ARGB color. + * @param argb The ARGB integer value (e.g. 0xFFFF0000.toInt() for red) + */ + data class Static(val argb: Int) : ColorMode() + + /** + * Per-instance rainbow with adjustable saturation, brightness, and opacity. + * Phase is computed from ColorSetting's instanceStartTime field. + * + * @param speed Rainbow speed multiplier (default 1.0, higher = faster) + * @param saturation Saturation 0..1 (default 1.0) + * @param brightness Brightness 0..1 (default 1.0) + * @param opacity Opacity 0..1 (default 1.0) + */ + data class Rainbow( + val speed: Float = 1f, + val saturation: Float = 1f, + val brightness: Float = 1f, + val opacity: Float = 1f + ) : ColorMode() + + /** + * Globally synced rainbow - all instances with this mode share the same phase. + * Phase is computed from RainbowPhaseProvider. + * + * @param speed Rainbow speed multiplier (default 1.0, higher = faster) + * @param saturation Saturation 0..1 (default 1.0) + * @param brightness Brightness 0..1 (default 1.0) + * @param opacity Opacity 0..1 (default 1.0) + */ + data class SyncedRainbow( + val speed: Float = 1f, + val saturation: Float = 1f, + val brightness: Float = 1f, + val opacity: Float = 1f + ) : ColorMode() + + /** + * Reference to a theme property by name (e.g. "accent", "background"). + * @param propertyName The theme property name (see ThemeColorResolver for valid names) + */ + data class ThemeColor(val propertyName: String) : ColorMode() + + /** + * Reference to a theme property with HSB tweaks applied. + * + * @param propertyName The base theme property name + * @param hueOffset Hue offset in degrees -180..180 (default 0) + * @param saturationMultiplier Saturation multiplier 0..2 (default 1.0, <1 = desaturate, >1 = saturate) + * @param brightnessMultiplier Brightness multiplier 0..2 (default 1.0, <1 = darken, >1 = brighten) + * @param opacityMultiplier Opacity multiplier 0..1 (default 1.0) + */ + data class TweakedTheme( + val propertyName: String, + val hueOffset: Float = 0f, + val saturationMultiplier: Float = 1f, + val brightnessMultiplier: Float = 1f, + val opacityMultiplier: Float = 1f + ) : ColorMode() + +} diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/ColorSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/ColorSetting.kt index 76a6aac..3ccc816 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/ColorSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/ColorSetting.kt @@ -1,8 +1,10 @@ package org.cobalt.api.module.setting.impl import com.google.gson.JsonElement +import com.google.gson.JsonObject import com.google.gson.JsonPrimitive import org.cobalt.api.module.setting.Setting +import java.awt.Color /** ARGB color picker setting. Value is an ARGB integer (e.g. `0xFFFF0000.toInt()` for red). */ class ColorSetting( @@ -11,12 +13,186 @@ class ColorSetting( defaultValue: Int, ) : Setting(name, description, defaultValue) { + /** Current color mode (static, rainbow, theme, etc.). */ + var mode: ColorMode = ColorMode.Static(defaultValue) + + /** Instance creation timestamp for per-instance rainbow phase offset. */ + private val instanceStartTime = System.currentTimeMillis() + + /** Dynamically resolved color based on current mode. */ + override var value: Int + get() = resolveColor() + set(newValue) { + mode = ColorMode.Static(newValue) + super.value = newValue + } + + /** + * Resolves the current color based on the active mode. + * Called on every read of `value` to support dynamic modes (rainbow, theme). + */ + private fun resolveColor(): Int { + return when (val m = mode) { + is ColorMode.Static -> m.argb + + is ColorMode.Rainbow -> { + // Per-instance rainbow: use instanceStartTime for phase offset + val elapsed = (System.currentTimeMillis() - instanceStartTime) / 1000.0 + val hue = ((elapsed * m.speed) % 1.0 + 1.0).toFloat() % 1f + val rgb = Color.HSBtoRGB(hue, m.saturation, m.brightness) + val alpha = (m.opacity * 255).toInt().coerceIn(0, 255) + (alpha shl 24) or (rgb and 0x00FFFFFF) + } + + is ColorMode.SyncedRainbow -> { + // Global synced rainbow: use RainbowPhaseProvider for shared phase + val hue = RainbowPhaseProvider.getHue(m.speed) + val rgb = Color.HSBtoRGB(hue, m.saturation, m.brightness) + val alpha = (m.opacity * 255).toInt().coerceIn(0, 255) + (alpha shl 24) or (rgb and 0x00FFFFFF) + } + + is ColorMode.ThemeColor -> { + ThemeColorResolver.resolve(m.propertyName) + } + + is ColorMode.TweakedTheme -> { + val baseColor = ThemeColorResolver.resolve(m.propertyName) + tweakColor(baseColor, m.hueOffset, m.saturationMultiplier, m.brightnessMultiplier, m.opacityMultiplier) + } + } + } + + /** + * Applies HSB/opacity adjustments to a base ARGB color. + * Used by TweakedTheme mode. + */ + private fun tweakColor( + argb: Int, + hueShift: Float, + saturationMult: Float, + brightnessMult: Float, + opacityMult: Float + ): Int { + val color = Color(argb, true) + val hsb = Color.RGBtoHSB(color.red, color.green, color.blue, null) + + val newHue = (hsb[0] + hueShift + 1f) % 1f + val newSat = (hsb[1] * saturationMult).coerceIn(0f, 1f) + val newBri = (hsb[2] * brightnessMult).coerceIn(0f, 1f) + val newAlpha = ((color.alpha / 255f) * opacityMult).coerceIn(0f, 1f) + + val rgb = Color.HSBtoRGB(newHue, newSat, newBri) + val alpha = (newAlpha * 255).toInt().coerceIn(0, 255) + return (alpha shl 24) or (rgb and 0x00FFFFFF) + } + override fun read(element: JsonElement) { - this.value = element.asInt + if (element.isJsonPrimitive) { + // Legacy format: plain ARGB int → Static mode + val argb = element.asInt + mode = ColorMode.Static(argb) + super.value = argb + } else if (element.isJsonObject) { + // New format: JSON object with mode discriminator + val obj = element.asJsonObject + val modeType = obj.get("mode")?.asString ?: "static" + + mode = when (modeType) { + "static" -> { + val argb = obj.get("argb")?.asInt ?: super.value + super.value = argb + ColorMode.Static(argb) + } + + "rainbow" -> { + ColorMode.Rainbow( + speed = obj.get("speed")?.asFloat ?: 1f, + saturation = obj.get("saturation")?.asFloat ?: 1f, + brightness = obj.get("brightness")?.asFloat ?: 1f, + opacity = obj.get("opacity")?.asFloat ?: 1f + ) + } + + "synced_rainbow" -> { + ColorMode.SyncedRainbow( + speed = obj.get("speed")?.asFloat ?: 1f, + saturation = obj.get("saturation")?.asFloat ?: 1f, + brightness = obj.get("brightness")?.asFloat ?: 1f, + opacity = obj.get("opacity")?.asFloat ?: 1f + ) + } + + "theme" -> { + ColorMode.ThemeColor( + propertyName = obj.get("propertyName")?.asString ?: "accent" + ) + } + + "tweaked_theme" -> { + ColorMode.TweakedTheme( + propertyName = obj.get("propertyName")?.asString ?: "accent", + hueOffset = obj.get("hueOffset")?.asFloat ?: 0f, + saturationMultiplier = obj.get("saturationMultiplier")?.asFloat ?: 1f, + brightnessMultiplier = obj.get("brightnessMultiplier")?.asFloat ?: 1f, + opacityMultiplier = obj.get("opacityMultiplier")?.asFloat ?: 1f + ) + } + + else -> { + // Unknown mode: fallback to static + val argb = super.value + ColorMode.Static(argb) + } + } + } } override fun write(): JsonElement { - return JsonPrimitive(value) + return when (val m = mode) { + is ColorMode.Static -> { + // Backward compatible: write as plain int + JsonPrimitive(m.argb) + } + + is ColorMode.Rainbow -> { + JsonObject().apply { + addProperty("mode", "rainbow") + addProperty("speed", m.speed) + addProperty("saturation", m.saturation) + addProperty("brightness", m.brightness) + addProperty("opacity", m.opacity) + } + } + + is ColorMode.SyncedRainbow -> { + JsonObject().apply { + addProperty("mode", "synced_rainbow") + addProperty("speed", m.speed) + addProperty("saturation", m.saturation) + addProperty("brightness", m.brightness) + addProperty("opacity", m.opacity) + } + } + + is ColorMode.ThemeColor -> { + JsonObject().apply { + addProperty("mode", "theme") + addProperty("propertyName", m.propertyName) + } + } + + is ColorMode.TweakedTheme -> { + JsonObject().apply { + addProperty("mode", "tweaked_theme") + addProperty("propertyName", m.propertyName) + addProperty("hueOffset", m.hueOffset) + addProperty("saturationMultiplier", m.saturationMultiplier) + addProperty("brightnessMultiplier", m.brightnessMultiplier) + addProperty("opacityMultiplier", m.opacityMultiplier) + } + } + } } } diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/RainbowPhaseProvider.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/RainbowPhaseProvider.kt new file mode 100644 index 0000000..842b4cd --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/RainbowPhaseProvider.kt @@ -0,0 +1,24 @@ +package org.cobalt.api.module.setting.impl + +/** + * Singleton provider for globally synced rainbow phase computation. + * + * All ColorSettings using SyncedRainbow mode share the same phase from this provider. + * Phase is computed based on elapsed time since the provider was initialized. + */ +object RainbowPhaseProvider { + + private val startTime = System.currentTimeMillis() + + /** + * Get the current hue value (0..1) for globally synced rainbow. + * + * @param speed Speed multiplier (default 1.0, higher = faster rotation) + * @return Hue value in range 0..1 (wraps at 1.0) + */ + fun getHue(speed: Float = 1f): Float { + val elapsed = (System.currentTimeMillis() - startTime) / 1000.0 + return ((elapsed * speed) % 1.0 + 1.0).toFloat() % 1f + } + +} diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/ThemeColorResolver.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/ThemeColorResolver.kt new file mode 100644 index 0000000..1f9ebd8 --- /dev/null +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/ThemeColorResolver.kt @@ -0,0 +1,198 @@ +package org.cobalt.api.module.setting.impl + +import org.cobalt.api.ui.theme.Theme +import org.cobalt.api.ui.theme.ThemeManager + +/** + * Explicit property name resolver for theme colors. + * + * Maps theme property names (e.g. "accent", "background") to getter lambdas. + * Does NOT use kotlin-reflect to avoid reflection dependency. + * + * Properties are organized into logical groups for UI display. + */ +object ThemeColorResolver { + + /** + * Logical grouping of theme properties for UI organization. + * Keys are group names, values are lists of property names. + */ + val groups: Map> = linkedMapOf( + "Base" to listOf( + "background", + "panel", + "inset", + "overlay" + ), + "Text" to listOf( + "text", + "textPrimary", + "textSecondary", + "textDisabled", + "textPlaceholder", + "textOnAccent", + "selectionText", + "searchPlaceholderText" + ), + "Accent" to listOf( + "accent", + "accentPrimary", + "accentSecondary", + "selection", + "selectedOverlay" + ), + "Controls" to listOf( + "controlBg", + "controlBorder", + "inputBg", + "inputBorder" + ), + "Status" to listOf( + "success", + "warning", + "error", + "info" + ), + "Scrollbar" to listOf( + "scrollbarThumb", + "scrollbarTrack" + ), + "Slider" to listOf( + "sliderTrack", + "sliderFill", + "sliderThumb" + ), + "Tooltip" to listOf( + "tooltipBackground", + "tooltipBorder", + "tooltipText" + ), + "Notification" to listOf( + "notificationBackground", + "notificationBorder", + "notificationText", + "notificationTextSecondary" + ), + "Status BG" to listOf( + "infoBackground", + "infoBorder", + "infoIcon", + "warningBackground", + "warningBorder", + "warningIcon", + "successBackground", + "successBorder", + "successIcon", + "errorBackground", + "errorBorder", + "errorIcon" + ), + "Other" to listOf( + "moduleDivider", + "white", + "black", + "transparent" + ) + ) + + /** + * Explicit map of property name to getter lambda. + * All 54 theme color properties are mapped here. + */ + private val resolvers: Map Int> = mapOf( + // Base colors (4) + "background" to { it.background }, + "panel" to { it.panel }, + "inset" to { it.inset }, + "overlay" to { it.overlay }, + + // Text colors (8) + "text" to { it.text }, + "textPrimary" to { it.textPrimary }, + "textSecondary" to { it.textSecondary }, + "textDisabled" to { it.textDisabled }, + "textPlaceholder" to { it.textPlaceholder }, + "textOnAccent" to { it.textOnAccent }, + "selectionText" to { it.selectionText }, + "searchPlaceholderText" to { it.searchPlaceholderText }, + + // Accent colors (5) + "accent" to { it.accent }, + "accentPrimary" to { it.accentPrimary }, + "accentSecondary" to { it.accentSecondary }, + "selection" to { it.selection }, + "selectedOverlay" to { it.selectedOverlay }, + + // Control colors (4) + "controlBg" to { it.controlBg }, + "controlBorder" to { it.controlBorder }, + "inputBg" to { it.inputBg }, + "inputBorder" to { it.inputBorder }, + + // Status colors (4) + "success" to { it.success }, + "warning" to { it.warning }, + "error" to { it.error }, + "info" to { it.info }, + + // Scrollbar colors (2) + "scrollbarThumb" to { it.scrollbarThumb }, + "scrollbarTrack" to { it.scrollbarTrack }, + + // Slider colors (3) + "sliderTrack" to { it.sliderTrack }, + "sliderFill" to { it.sliderFill }, + "sliderThumb" to { it.sliderThumb }, + + // Tooltip colors (3) + "tooltipBackground" to { it.tooltipBackground }, + "tooltipBorder" to { it.tooltipBorder }, + "tooltipText" to { it.tooltipText }, + + // Notification colors (4) + "notificationBackground" to { it.notificationBackground }, + "notificationBorder" to { it.notificationBorder }, + "notificationText" to { it.notificationText }, + "notificationTextSecondary" to { it.notificationTextSecondary }, + + // Status background colors (12) + "infoBackground" to { it.infoBackground }, + "infoBorder" to { it.infoBorder }, + "infoIcon" to { it.infoIcon }, + "warningBackground" to { it.warningBackground }, + "warningBorder" to { it.warningBorder }, + "warningIcon" to { it.warningIcon }, + "successBackground" to { it.successBackground }, + "successBorder" to { it.successBorder }, + "successIcon" to { it.successIcon }, + "errorBackground" to { it.errorBackground }, + "errorBorder" to { it.errorBorder }, + "errorIcon" to { it.errorIcon }, + + // Other colors (4) + "moduleDivider" to { it.moduleDivider }, + "white" to { it.white }, + "black" to { it.black }, + "transparent" to { it.transparent } + ) + + /** + * Resolve a theme property name to its current ARGB color value. + * + * @param propertyName The property name (e.g. "accent", "background") + * @return The ARGB integer value from the current theme + * @throws IllegalStateException if propertyName is unknown (fallback to "accent") + */ + fun resolve(propertyName: String): Int { + val theme = ThemeManager.currentTheme + val resolver = resolvers[propertyName] ?: resolvers["accent"]!! + return resolver(theme) + } + + /** + * Get all valid property names. + * @return Set of all 54 valid property names + */ + fun getPropertyNames(): Set = resolvers.keys + +} diff --git a/src/main/kotlin/org/cobalt/internal/ui/components/settings/UIColorSetting.kt b/src/main/kotlin/org/cobalt/internal/ui/components/settings/UIColorSetting.kt index d9d1b47..422db1c 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/components/settings/UIColorSetting.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/components/settings/UIColorSetting.kt @@ -1,14 +1,23 @@ package org.cobalt.internal.ui.components.settings import java.awt.Color +import net.minecraft.client.Minecraft +import net.minecraft.client.input.CharacterEvent +import net.minecraft.client.input.KeyEvent +import org.cobalt.api.module.setting.impl.ColorMode import org.cobalt.api.module.setting.impl.ColorSetting +import org.cobalt.api.module.setting.impl.ThemeColorResolver import org.cobalt.api.ui.theme.ThemeManager import org.cobalt.api.util.ui.NVGRenderer import org.cobalt.api.util.ui.helper.Gradient import org.cobalt.internal.ui.UIComponent +import org.cobalt.internal.ui.animation.ColorAnimation +import org.cobalt.internal.ui.util.ScrollHandler +import org.cobalt.internal.ui.util.TextInputHandler import org.cobalt.internal.ui.util.isHoveringOver import org.cobalt.internal.ui.util.mouseX import org.cobalt.internal.ui.util.mouseY +import org.lwjgl.glfw.GLFW internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( x = 0F, @@ -18,23 +27,74 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( ) { private var pickerOpen = false - private var hue = 0f - private var saturation = 1f - private var lightness = 0.5f - private var opacity = 1f - private var draggingHue = false + // Mode dropdown state + private var modeDropdownOpen = false + private val modeDropdownHoverAnim = ColorAnimation(150L) + private var modeDropdownWasHovering = false + + // Static mode (HSB picker) state + private var staticHue = 0f + private var staticSaturation = 1f + private var staticBrightness = 0.5f + private var staticOpacity = 1f + private var draggingStaticHue = false + private var draggingStaticOpacity = false + private var draggingStaticColor = false + + // Rainbow/Synced mode slider states + private var draggingSpeed = false + private var draggingSaturation = false + private var draggingBrightness = false private var draggingOpacity = false - private var draggingColor = false - init { - val color = Color(setting.value) - val hsb = Color.RGBtoHSB(color.red, color.green, color.blue, null) + // Theme mode state + private val themeScrollHandler = ScrollHandler() + private var selectedThemeProperty = "accent" + + // Tweaked mode state + private var tweakedPropertyDropdownOpen = false + private val tweakedPropertyScrollHandler = ScrollHandler() + private var draggingHueOffset = false + private var draggingSaturationMult = false + private var draggingBrightnessMult = false + private var draggingOpacityMult = false - hue = hsb[0] - saturation = hsb[1] - lightness = hsb[2] - opacity = color.alpha / 255f + // Hex input state + private val hexInputHandler = TextInputHandler("", 9) + private var hexFocused = false + private var hexDragging = false + private var hexValid = true + + init { + // Initialize static HSB from current mode if Static + when (val mode = setting.mode) { + is ColorMode.Static -> { + val color = Color(mode.argb, true) + val hsb = Color.RGBtoHSB(color.red, color.green, color.blue, null) + staticHue = hsb[0] + staticSaturation = hsb[1] + staticBrightness = hsb[2] + staticOpacity = color.alpha / 255f + hexInputHandler.setText(argbToHex(mode.argb)) + } + is ColorMode.ThemeColor -> { + selectedThemeProperty = mode.propertyName + } + is ColorMode.TweakedTheme -> { + selectedThemeProperty = mode.propertyName + } + else -> { + // Initialize with default values for other modes + val color = Color(setting.value, true) + val hsb = Color.RGBtoHSB(color.red, color.green, color.blue, null) + staticHue = hsb[0] + staticSaturation = hsb[1] + staticBrightness = hsb[2] + staticOpacity = color.alpha / 255f + hexInputHandler.setText(argbToHex(setting.value)) + } + } } override fun render() { @@ -51,81 +111,444 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( fun drawColorPicker() { if (!pickerOpen) return - val px = x + width - 220F + val px = x + width - 360F val py = y + height - 10F + + val pickerHeight = when (setting.mode) { + is ColorMode.Static -> 450F + is ColorMode.Rainbow, is ColorMode.SyncedRainbow -> 360F + is ColorMode.ThemeColor -> 400F + is ColorMode.TweakedTheme -> 470F + } + + NVGRenderer.rect(px + 2F, py + 2F, 340F, pickerHeight, Color(0, 0, 0, 50).rgb, 10F) + + NVGRenderer.rect(px, py, 340F, pickerHeight, ThemeManager.currentTheme.panel, 10F) + NVGRenderer.hollowRect(px, py, 340F, pickerHeight, 2F, ThemeManager.currentTheme.controlBorder, 10F) + + drawSourceTabs(px, py) + drawEffectToggles(px, py) + + val controlsY = py + 75F + when (setting.mode) { + is ColorMode.Static -> drawStaticPanel(px, controlsY) + is ColorMode.Rainbow -> drawRainbowPanel(px, controlsY, setting.mode as ColorMode.Rainbow) + is ColorMode.SyncedRainbow -> drawSyncedRainbowPanel(px, controlsY, setting.mode as ColorMode.SyncedRainbow) + is ColorMode.ThemeColor -> drawThemePanel(px, controlsY, setting.mode as ColorMode.ThemeColor) + is ColorMode.TweakedTheme -> drawTweakedPanel(px, controlsY, setting.mode as ColorMode.TweakedTheme) + } + + drawPreviewSwatch(px, py + pickerHeight - 70F) + } + + private fun drawSourceTabs(px: Float, py: Float) { val bx = px + 10F val by = py + 10F - val size = 180F - - NVGRenderer.rect(px, py, 200F, 250F, ThemeManager.currentTheme.panel, 8F) - NVGRenderer.hollowRect(px, py, 200F, 250F, 2F, ThemeManager.currentTheme.controlBorder, 8F) - - val hueColor = Color.HSBtoRGB(hue, 1f, 1f) - - NVGRenderer.rect(bx, by, size, size, hueColor, 0F) - NVGRenderer.gradientRect( - bx, - by, - size, - size, - ThemeManager.currentTheme.white, - ThemeManager.currentTheme.transparent, - Gradient.LeftToRight, - 0F - ) - NVGRenderer.gradientRect( - bx, - by, - size, - size, - ThemeManager.currentTheme.transparent, - ThemeManager.currentTheme.black, - Gradient.TopToBottom, - 0F - ) - NVGRenderer.hollowRect(bx, by, size, size, 1F, ThemeManager.currentTheme.controlBorder, 0F) + val totalWidth = 320F + val tabWidth = (totalWidth - 10F) / 2F + val tabHeight = 28F + + // Custom Tab + val isCustom = setting.mode !is ColorMode.ThemeColor && setting.mode !is ColorMode.TweakedTheme + val customColor = if (isCustom) ThemeManager.currentTheme.accent else ThemeManager.currentTheme.controlBg + + NVGRenderer.rect(bx, by, tabWidth, tabHeight, customColor, 5F) + NVGRenderer.hollowRect(bx, by, tabWidth, tabHeight, 1F, ThemeManager.currentTheme.controlBorder, 5F) + + val customText = "Custom" + val customTextWidth = NVGRenderer.textWidth(customText, 13F) + val customTextColor = if (isCustom) ThemeManager.currentTheme.white else ThemeManager.currentTheme.text + NVGRenderer.text(customText, bx + (tabWidth - customTextWidth) / 2F, by + 9F, 13F, customTextColor) + + // Theme Tab + val themeX = bx + tabWidth + 10F + val themeColor = if (!isCustom) ThemeManager.currentTheme.accent else ThemeManager.currentTheme.controlBg + + NVGRenderer.rect(themeX, by, tabWidth, tabHeight, themeColor, 5F) + NVGRenderer.hollowRect(themeX, by, tabWidth, tabHeight, 1F, ThemeManager.currentTheme.controlBorder, 5F) + + val themeText = "Theme" + val themeTextWidth = NVGRenderer.textWidth(themeText, 13F) + val themeTextColor = if (!isCustom) ThemeManager.currentTheme.white else ThemeManager.currentTheme.text + NVGRenderer.text(themeText, themeX + (tabWidth - themeTextWidth) / 2F, by + 9F, 13F, themeTextColor) + } + + private fun drawEffectToggles(px: Float, py: Float) { + val bx = px + 10F + val by = py + 48F + val checkboxSize = 20F + + val isCustom = setting.mode !is ColorMode.ThemeColor && setting.mode !is ColorMode.TweakedTheme + + if (isCustom) { + val isRainbow = setting.mode is ColorMode.Rainbow || setting.mode is ColorMode.SyncedRainbow + drawCheckbox(bx, by, checkboxSize, isRainbow, "Rainbow") + + val syncX = bx + 100F + val isSynced = setting.mode is ColorMode.SyncedRainbow + drawCheckbox(syncX, by, checkboxSize, isSynced, "Sync") + } else { + val isAdjusted = setting.mode is ColorMode.TweakedTheme + drawCheckbox(bx, by, checkboxSize, isAdjusted, "Adjust") + } + } + + private fun drawCheckbox(x: Float, y: Float, size: Float, checked: Boolean, label: String) { + val bgColor = if (checked) ThemeManager.currentTheme.selectedOverlay else ThemeManager.currentTheme.controlBg + val borderColor = if (checked) ThemeManager.currentTheme.accent else ThemeManager.currentTheme.controlBorder + + NVGRenderer.rect(x, y, size, size, bgColor, 5F) + NVGRenderer.hollowRect(x, y, size, size, 1F, borderColor, 5F) + + if (checked) { + NVGRenderer.image(checkmarkIcon, x + 2F, y + 2F, size - 4F, size - 4F, 0F, ThemeManager.currentTheme.accent) + } + + NVGRenderer.text(label, x + size + 5F, y + 5F, 13F, ThemeManager.currentTheme.text) + } + + + private fun drawStaticPanel(px: Float, py: Float) { + val bx = px + 10F + val by = py + 10F + val boxWidth = 320F + val boxHeight = 180F + + val hueColor = Color.HSBtoRGB(staticHue, 1f, 1f) + + NVGRenderer.pushScissor(bx, by, boxWidth, boxHeight) + NVGRenderer.rect(bx, by, boxWidth, boxHeight, hueColor, 6F) + NVGRenderer.gradientRect(bx, by, boxWidth, boxHeight, ThemeManager.currentTheme.white, ThemeManager.currentTheme.transparent, Gradient.LeftToRight, 6F) + NVGRenderer.gradientRect(bx, by, boxWidth, boxHeight, ThemeManager.currentTheme.transparent, ThemeManager.currentTheme.black, Gradient.TopToBottom, 6F) + NVGRenderer.popScissor() + NVGRenderer.hollowRect(bx, by, boxWidth, boxHeight, 1F, ThemeManager.currentTheme.controlBorder, 6F) + + val selectorX = bx + staticSaturation * boxWidth + val selectorY = by + (1f - staticBrightness) * boxHeight + + val currentRgb = Color.HSBtoRGB(staticHue, staticSaturation, staticBrightness) + NVGRenderer.circle(selectorX, selectorY, 7F, ThemeManager.currentTheme.white) + NVGRenderer.circle(selectorX, selectorY, 5F, currentRgb) + + val hueY = py + boxHeight + 20F + val sliderWidth = 320F + + for (i in 0..35) { + val x1 = bx + (sliderWidth / 36f) * i + val x2 = bx + (sliderWidth / 36f) * (i + 1) + val color1 = Color.HSBtoRGB(i / 36f, 1f, 1f) + val color2 = Color.HSBtoRGB((i + 1) / 36f, 1f, 1f) + NVGRenderer.gradientRect(x1, hueY, x2 - x1, 6F, color1, color2, Gradient.LeftToRight, 3F) + } + + NVGRenderer.hollowRect(bx, hueY, sliderWidth, 6F, 1F, ThemeManager.currentTheme.controlBorder, 3F) + NVGRenderer.circle(bx + staticHue * sliderWidth, hueY + 3F, 8F, ThemeManager.currentTheme.white) + + val opacityY = hueY + 20F + + NVGRenderer.rect(bx, opacityY, sliderWidth, 6F, ThemeManager.currentTheme.white, 3F) + for (i in 0..31) { + if (i % 2 == 0) { + NVGRenderer.rect(bx + i * 10F, opacityY, 10F, 3F, ThemeManager.currentTheme.textSecondary, 0F) + NVGRenderer.rect(bx + i * 10F, opacityY + 3F, 10F, 3F, ThemeManager.currentTheme.white, 0F) + } else { + NVGRenderer.rect(bx + i * 10F, opacityY, 10F, 3F, ThemeManager.currentTheme.white, 0F) + NVGRenderer.rect(bx + i * 10F, opacityY + 3F, 10F, 3F, ThemeManager.currentTheme.textSecondary, 0F) + } + } + + val currentColor = Color.HSBtoRGB(staticHue, staticSaturation, staticBrightness) + val opaqueColor = Color(currentColor or (255 shl 24), true).rgb + val transparentColor = Color(currentColor and 0x00FFFFFF, true).rgb + + NVGRenderer.gradientRect(bx, opacityY, sliderWidth, 6F, transparentColor, opaqueColor, Gradient.LeftToRight, 3F) + NVGRenderer.hollowRect(bx, opacityY, sliderWidth, 6F, 1F, ThemeManager.currentTheme.controlBorder, 3F) + NVGRenderer.circle(bx + staticOpacity * sliderWidth, opacityY + 3F, 8F, ThemeManager.currentTheme.white) + + val hexY = opacityY + 20F + NVGRenderer.text("Hex Code", bx, hexY, 13F, ThemeManager.currentTheme.text) + + val inputY = hexY + 20F + val inputX = bx + val inputWidth = 320F + val inputHeight = 30F + + val borderColor = if (hexFocused) { + ThemeManager.currentTheme.accent + } else if (!hexValid) { + ThemeManager.currentTheme.error + } else { + ThemeManager.currentTheme.inputBorder + } + + NVGRenderer.rect(inputX, inputY, inputWidth, inputHeight, ThemeManager.currentTheme.inputBg, 5F) + NVGRenderer.hollowRect(inputX, inputY, inputWidth, inputHeight, 2F, borderColor, 5F) - val selectorX = bx + saturation * size - val selectorY = by + (1f - lightness) * size + val textX = inputX + 10F + val textY = inputY + 9F - NVGRenderer.circle(selectorX, selectorY, 5F, ThemeManager.currentTheme.white) - NVGRenderer.circle(selectorX, selectorY, 3F, ThemeManager.currentTheme.black) + if (hexFocused) hexInputHandler.updateScroll(300F, 13F) - val hueY = py + size + 20F + NVGRenderer.pushScissor(inputX + 10F, inputY, 300F, inputHeight) - for (i in 0..5) { - val x1 = bx + (size / 6f) * i - val x2 = bx + (size / 6f) * (i + 1) - val color1 = Color.HSBtoRGB(i / 6f, 1f, 1f) - val color2 = Color.HSBtoRGB((i + 1) / 6f, 1f, 1f) + if (hexFocused) { + hexInputHandler.renderSelection(textX, textY, 13F, 13F, ThemeManager.currentTheme.selection) + } - NVGRenderer.gradientRect(x1, hueY, x2 - x1, 15F, color1, color2, Gradient.LeftToRight, 0F) + NVGRenderer.text(hexInputHandler.getText(), textX - hexInputHandler.getTextOffset(), textY, 13F, ThemeManager.currentTheme.text) + + if (hexFocused) { + hexInputHandler.renderCursor(textX, textY, 13F, ThemeManager.currentTheme.text) + } + + NVGRenderer.popScissor() + } + + private fun drawRainbowPanel(px: Float, py: Float, mode: ColorMode.Rainbow) { + val bx = px + 20F + drawRainbowSliders(bx, py, mode.speed, mode.saturation, mode.brightness, mode.opacity) + } + + private fun drawSyncedRainbowPanel(px: Float, py: Float, mode: ColorMode.SyncedRainbow) { + val bx = px + 20F + drawRainbowSliders(bx, py, mode.speed, mode.saturation, mode.brightness, mode.opacity) + } + + private fun drawRainbowSliders(bx: Float, by: Float, speed: Float, saturation: Float, brightness: Float, opacity: Float) { + val labels = listOf("Speed", "Saturation", "Brightness", "Opacity") + val values = listOf(speed, saturation, brightness, opacity) + val sliderWidth = 300F + + values.forEachIndexed { index, value -> + val sliderY = by + index * 50F + + NVGRenderer.text(labels[index], bx, sliderY + 2F, 13F, ThemeManager.currentTheme.text) + + val valueText = String.format("%.2f", value) + val valueWidth = NVGRenderer.textWidth(valueText, 12F) + NVGRenderer.text(valueText, bx + sliderWidth - valueWidth, sliderY + 2F, 12F, ThemeManager.currentTheme.textSecondary) + + val trackY = sliderY + 24F + val normalizedValue = when (index) { + 0 -> (value / 2f).coerceIn(0f, 1f) + else -> value + } + val thumbX = bx + normalizedValue * sliderWidth + + NVGRenderer.rect(bx, trackY, sliderWidth, 6F, ThemeManager.currentTheme.sliderTrack, 3F) + NVGRenderer.rect(bx, trackY, thumbX - bx, 6F, ThemeManager.currentTheme.sliderFill, 3F) + NVGRenderer.circle(thumbX, trackY + 3F, 8F, ThemeManager.currentTheme.sliderThumb) + } + } + + private fun drawThemePanel(px: Float, py: Float, mode: ColorMode.ThemeColor) { + val bx = px + 10F + val by = py + 10F + val panelWidth = 320F + val panelHeight = 240F + + NVGRenderer.rect(bx, by, panelWidth, panelHeight, ThemeManager.currentTheme.inset, 5F) + NVGRenderer.hollowRect(bx, by, panelWidth, panelHeight, 1F, ThemeManager.currentTheme.controlBorder, 5F) + + NVGRenderer.pushScissor(bx, by, panelWidth, panelHeight) + + var currentY = by + 10F - themeScrollHandler.getOffset() + var totalHeight = 10F + + ThemeColorResolver.groups.forEach { (groupName, properties) -> + // Group header + NVGRenderer.text(groupName, bx + 10F, currentY, 12F, ThemeManager.currentTheme.textSecondary) + currentY += 22F + totalHeight += 22F + + // Properties + properties.forEach { propertyName -> + val isSelected = propertyName == mode.propertyName + val itemY = currentY + val isHovering = isHoveringOver(bx + 5F, itemY - 2F, panelWidth - 10F, 22F) + + if (isSelected) { + NVGRenderer.rect(bx + 5F, itemY - 2F, panelWidth - 10F, 22F, ThemeManager.currentTheme.selectedOverlay, 4F) + } else if (isHovering) { + NVGRenderer.rect(bx + 5F, itemY - 2F, panelWidth - 10F, 22F, ThemeManager.currentTheme.controlBg, 4F) + } + + // Color preview box + val previewColor = ThemeColorResolver.resolve(propertyName) + NVGRenderer.rect(bx + 15F, itemY, 18F, 18F, previewColor, 3F) + NVGRenderer.hollowRect(bx + 15F, itemY, 18F, 18F, 1F, ThemeManager.currentTheme.controlBorder, 3F) + + // Property name + val textColor = if (isSelected) ThemeManager.currentTheme.accent else ThemeManager.currentTheme.text + NVGRenderer.text(propertyName, bx + 40F, itemY + 4F, 12F, textColor) + + currentY += 22F + totalHeight += 22F + } + + currentY += 8F + totalHeight += 8F + } + + NVGRenderer.popScissor() + + themeScrollHandler.setMaxScroll(totalHeight, panelHeight) + + // Scrollbar + if (themeScrollHandler.isScrollable()) { + val scrollbarX = bx + panelWidth - 7F + val scrollbarY = by + 3F + val scrollbarHeight = panelHeight - 6F + val thumbHeight = (panelHeight / totalHeight) * scrollbarHeight + val thumbY = scrollbarY + (themeScrollHandler.getOffset() / themeScrollHandler.getMaxScroll()) * (scrollbarHeight - thumbHeight) + + NVGRenderer.rect(scrollbarX, thumbY, 4F, thumbHeight, ThemeManager.currentTheme.scrollbarThumb, 2F) } + } + + private fun drawTweakedPanel(px: Float, py: Float, mode: ColorMode.TweakedTheme) { + val bx = px + 20F + var currentY = py + 10F + + // Property dropdown + NVGRenderer.text("Base Property", bx, currentY, 13F, ThemeManager.currentTheme.text) + currentY += 22F + + val dropdownWidth = 300F + val dropdownHeight = 30F + val isHovering = isHoveringOver(bx, currentY, dropdownWidth, dropdownHeight) + + val bgColor = if (isHovering) ThemeManager.currentTheme.selectedOverlay else ThemeManager.currentTheme.controlBg + NVGRenderer.rect(bx, currentY, dropdownWidth, dropdownHeight, bgColor, 5F) + NVGRenderer.hollowRect(bx, currentY, dropdownWidth, dropdownHeight, 1F, ThemeManager.currentTheme.controlBorder, 5F) + + // Preview color + text + val previewColor = ThemeColorResolver.resolve(mode.propertyName) + NVGRenderer.rect(bx + 8F, currentY + 6F, 18F, 18F, previewColor, 3F) + NVGRenderer.hollowRect(bx + 8F, currentY + 6F, 18F, 18F, 1F, ThemeManager.currentTheme.controlBorder, 3F) + NVGRenderer.text(mode.propertyName, bx + 32F, currentY + 8F, 12F, ThemeManager.currentTheme.text) + + currentY += 40F + + // Tweaking sliders + val labels = listOf("Hue Offset", "Saturation", "Brightness", "Opacity") + val values = listOf( + mode.hueOffset / 180f, // Normalize -180..180 to -1..1 + mode.saturationMultiplier / 2f, // 0..2 to 0..1 + mode.brightnessMultiplier / 2f, + mode.opacityMultiplier + ) + + values.forEachIndexed { index, normalizedValue -> + val sliderY = currentY + index * 50F + val displayValue = when (index) { + 0 -> mode.hueOffset + 1 -> mode.saturationMultiplier + 2 -> mode.brightnessMultiplier + 3 -> mode.opacityMultiplier + else -> 0f + } - NVGRenderer.hollowRect(bx, hueY, size, 15F, 1F, ThemeManager.currentTheme.controlBorder, 0F) - NVGRenderer.rect(bx + hue * size - 2F, hueY - 2F, 4F, 19F, ThemeManager.currentTheme.white, 1F) + NVGRenderer.text(labels[index], bx, sliderY + 2F, 13F, ThemeManager.currentTheme.text) - val opacityY = hueY + 25F + val valueText = String.format("%.2f", displayValue) + val valueWidth = NVGRenderer.textWidth(valueText, 12F) + NVGRenderer.text(valueText, bx + 300F - valueWidth, sliderY + 2F, 12F, ThemeManager.currentTheme.textSecondary) - NVGRenderer.rect(bx, opacityY, size, 15F, ThemeManager.currentTheme.white, 0F) + val trackY = sliderY + 24F + val sliderWidth = 300F + + val thumbX = if (index == 0) { + bx + (normalizedValue + 1f) / 2f * sliderWidth + } else { + bx + normalizedValue * sliderWidth + } - for (i in 0..17) { - if (i % 2 == 0) { - NVGRenderer.rect(bx + i * 10F, opacityY, 10F, 7.5F, ThemeManager.currentTheme.textSecondary, 0F) - NVGRenderer.rect(bx + i * 10F, opacityY + 7.5F, 10F, 7.5F, ThemeManager.currentTheme.white, 0F) + NVGRenderer.rect(bx, trackY, sliderWidth, 6F, ThemeManager.currentTheme.sliderTrack, 3F) + if (index == 0) { + val centerX = bx + sliderWidth / 2f + if (thumbX > centerX) { + NVGRenderer.rect(centerX, trackY, thumbX - centerX, 6F, ThemeManager.currentTheme.sliderFill, 3F) + } else { + NVGRenderer.rect(thumbX, trackY, centerX - thumbX, 6F, ThemeManager.currentTheme.sliderFill, 3F) + } } else { - NVGRenderer.rect(bx + i * 10F, opacityY, 10F, 7.5F, ThemeManager.currentTheme.white, 0F) - NVGRenderer.rect(bx + i * 10F, opacityY + 7.5F, 10F, 7.5F, ThemeManager.currentTheme.textSecondary, 0F) + NVGRenderer.rect(bx, trackY, thumbX - bx, 6F, ThemeManager.currentTheme.sliderFill, 3F) + } + NVGRenderer.circle(thumbX, trackY + 3F, 8F, ThemeManager.currentTheme.sliderThumb) + } + } + + private fun drawPreviewSwatch(px: Float, py: Float) { + val panelX = px + 10F + val panelY = py + 8F + val panelWidth = 320F + val panelHeight = 56F + + NVGRenderer.rect(panelX, panelY, panelWidth, panelHeight, ThemeManager.currentTheme.inset, 6F) + + val swatchX = panelX + 8F + val swatchY = panelY + 8F + val swatchSize = 40F + + NVGRenderer.rect(swatchX, swatchY, swatchSize, swatchSize, setting.value, 6F) + NVGRenderer.hollowRect(swatchX, swatchY, swatchSize, swatchSize, 1.5F, ThemeManager.currentTheme.controlBorder, 6F) + + NVGRenderer.text("Preview", swatchX + 50F, swatchY + 14F, 13F, ThemeManager.currentTheme.text) + + val hexText = argbToHex(setting.value) + val hexWidth = NVGRenderer.textWidth(hexText, 12F) + NVGRenderer.text(hexText, panelX + panelWidth - hexWidth - 10F, swatchY + 15F, 12F, ThemeManager.currentTheme.textSecondary) + } + + private fun argbToHex(argb: Int): String { + return String.format("#%08X", argb) + } + + private fun parseHexToARGB(hex: String): Int? { + val stripped = hex.removePrefix("#").uppercase() + return when (stripped.length) { + 3 -> { + val r = stripped[0].toString().repeat(2) + val g = stripped[1].toString().repeat(2) + val b = stripped[2].toString().repeat(2) + (0xFF000000.toInt() or r.toInt(16).shl(16) or g.toInt(16).shl(8) or b.toInt(16)) + } + 6 -> { + (0xFF000000.toInt() or stripped.toInt(16)) + } + 8 -> { + stripped.toLong(16).toInt() } + else -> null } + } + + private fun validateHexInput(hex: String): Boolean { + val stripped = hex.removePrefix("#").uppercase() + return stripped.matches(Regex("^[0-9A-F]{3}$|^[0-9A-F]{6}$|^[0-9A-F]{8}$")) + } - val currentColor = Color.HSBtoRGB(hue, saturation, lightness) - val opaqueColor = Color(currentColor or (255 shl 24), true).rgb - val transparentColor = Color(currentColor and 0x00FFFFFF, true).rgb + private fun commitHexInput() { + parseHexToARGB(hexInputHandler.getText())?.let { argb -> + val color = Color(argb, true) + val hsb = Color.RGBtoHSB(color.red, color.green, color.blue, null) + staticHue = hsb[0] + staticSaturation = hsb[1] + staticBrightness = hsb[2] + staticOpacity = color.alpha / 255f + updateStaticColor() + } + } - NVGRenderer.gradientRect(bx, opacityY, size, 15F, transparentColor, opaqueColor, Gradient.LeftToRight, 0F) - NVGRenderer.hollowRect(bx, opacityY, size, 15F, 1F, ThemeManager.currentTheme.controlBorder, 0F) - NVGRenderer.rect(bx + opacity * size - 2F, opacityY - 2F, 4F, 19F, ThemeManager.currentTheme.white, 1F) + private fun updateHexFromCurrentColor() { + if (!hexFocused) { + val rgb = Color.HSBtoRGB(staticHue, staticSaturation, staticBrightness) + val alpha = (staticOpacity * 255).toInt() + val argb = (alpha shl 24) or (rgb and 0x00FFFFFF) + hexInputHandler.setText(argbToHex(argb)) + hexValid = true + } } override fun mouseClicked(button: Int): Boolean { @@ -139,47 +562,201 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( return true } - if (!pickerOpen) { - return false - } + if (!pickerOpen) return false - val px = x + width - 220F + val px = x + width - 360F val py = y + height - 10F + + // Tab Clicks val bx = px + 10F val by = py + 10F + val totalWidth = 320F + val tabWidth = (totalWidth - 10F) / 2F + val tabHeight = 28F + + if (isHoveringOver(bx, by, tabWidth, tabHeight)) { + if (setting.mode is ColorMode.ThemeColor || setting.mode is ColorMode.TweakedTheme) { + val rgb = Color.HSBtoRGB(staticHue, staticSaturation, staticBrightness) + val alpha = (staticOpacity * 255).toInt() + setting.mode = ColorMode.Static((alpha shl 24) or (rgb and 0x00FFFFFF)) + } + return true + } - if (isHoveringOver(bx, by, 180F, 180F)) { - draggingColor = true - updateColorFromBox(bx, by) + if (isHoveringOver(bx + tabWidth + 10F, by, tabWidth, tabHeight)) { + if (setting.mode !is ColorMode.ThemeColor && setting.mode !is ColorMode.TweakedTheme) { + setting.mode = ColorMode.ThemeColor(selectedThemeProperty) + } return true } - val hueY = py + 200F + // Checkbox Clicks + val checkboxY = py + 48F + val checkboxSize = 20F + val isCustom = setting.mode !is ColorMode.ThemeColor && setting.mode !is ColorMode.TweakedTheme + + if (isCustom) { + if (isHoveringOver(bx, checkboxY, checkboxSize + 60F, checkboxSize)) { + val isRainbow = setting.mode is ColorMode.Rainbow || setting.mode is ColorMode.SyncedRainbow + if (isRainbow) { + val rgb = Color.HSBtoRGB(staticHue, staticSaturation, staticBrightness) + val alpha = (staticOpacity * 255).toInt() + setting.mode = ColorMode.Static((alpha shl 24) or (rgb and 0x00FFFFFF)) + } else { + setting.mode = ColorMode.Rainbow() + } + return true + } - if (isHoveringOver(bx, hueY, 180F, 15F)) { - draggingHue = true - updateHueFromSlider(bx) - return true + val syncX = bx + 100F + if (isHoveringOver(syncX, checkboxY, checkboxSize + 40F, checkboxSize)) { + val isSynced = setting.mode is ColorMode.SyncedRainbow + if (isSynced) { + val old = setting.mode as ColorMode.SyncedRainbow + setting.mode = ColorMode.Rainbow(old.speed, old.saturation, old.brightness, old.opacity) + } else { + val old = setting.mode as? ColorMode.Rainbow + if (old != null) { + setting.mode = ColorMode.SyncedRainbow(old.speed, old.saturation, old.brightness, old.opacity) + } else { + setting.mode = ColorMode.SyncedRainbow() + } + } + return true + } + } else { + if (isHoveringOver(bx, checkboxY, checkboxSize + 60F, checkboxSize)) { + val isAdjusted = setting.mode is ColorMode.TweakedTheme + if (isAdjusted) { + setting.mode = ColorMode.ThemeColor(selectedThemeProperty) + } else { + setting.mode = ColorMode.TweakedTheme(selectedThemeProperty) + } + return true + } } - val opacityY = hueY + 25F + val controlsY = py + 75F + return when (setting.mode) { + is ColorMode.Static -> handleStaticPanelClick(px, controlsY) + is ColorMode.Rainbow -> handleRainbowPanelClick(px, controlsY) + is ColorMode.SyncedRainbow -> handleSyncedRainbowPanelClick(px, controlsY) + is ColorMode.ThemeColor -> handleThemePanelClick(px, controlsY) + is ColorMode.TweakedTheme -> handleTweakedPanelClick(px, controlsY) + } + } - if (isHoveringOver(bx, opacityY, 180F, 15F)) { - draggingOpacity = true - updateOpacityFromSlider(bx) - return true + private fun handleStaticPanelClick(px: Float, py: Float): Boolean { + val bx = px + 10F + val by = py + 10F + + if (isHoveringOver(bx, by, 320F, 180F)) { + draggingStaticColor = true + updateStaticColorFromBox(bx, by) + return true + } + + val hueY = py + 200F + if (isHoveringOver(bx, hueY, 320F, 6F)) { + draggingStaticHue = true + updateStaticHueFromSlider(bx) + return true + } + + val opacityY = hueY + 20F + if (isHoveringOver(bx, opacityY, 320F, 6F)) { + draggingStaticOpacity = true + updateStaticOpacityFromSlider(bx) + return true + } + + val hexInputY = opacityY + 40F + if (isHoveringOver(bx, hexInputY, 320F, 30F)) { + hexFocused = true + hexDragging = true + hexInputHandler.startSelection(mouseX.toFloat(), bx + 10F, 13F) + return true + } + + if (hexFocused) { + if (hexValid) commitHexInput() + hexFocused = false + return true + } + + return false + } + + private fun handleRainbowPanelClick(px: Float, py: Float): Boolean { + val bx = px + 20F + val sliderWidth = 300F + + for (i in 0..3) { + val sliderY = py + i * 50F + 24F + if (isHoveringOver(bx, sliderY - 5F, sliderWidth, 16F)) { + when (i) { + 0 -> draggingSpeed = true + 1 -> draggingSaturation = true + 2 -> draggingBrightness = true + 3 -> draggingOpacity = true + } + updateRainbowSlider(i, bx, sliderWidth) + return true + } } - return isHoveringOver(px, py, 200F, 250F).also { - if (!it) pickerOpen = false + return false + } + + private fun handleSyncedRainbowPanelClick(px: Float, py: Float): Boolean { + return handleRainbowPanelClick(px, py) + } + + private fun handleThemePanelClick(px: Float, py: Float): Boolean { + val bx = px + 10F + val by = py + 10F + val panelWidth = 320F + val panelHeight = 240F + + if (!isHoveringOver(bx, by, panelWidth, panelHeight)) return false + + var currentY = by + 10F - themeScrollHandler.getOffset() + + ThemeColorResolver.groups.forEach { (_, properties) -> + currentY += 22F // Skip group header + + properties.forEach { propertyName -> + val itemY = currentY + if (isHoveringOver(bx + 5F, itemY - 2F, panelWidth - 10F, 22F)) { + selectedThemeProperty = propertyName + setting.mode = ColorMode.ThemeColor(propertyName) + return true + } + currentY += 22F + } + + currentY += 8F } + + return true } - override fun mouseReleased(button: Int): Boolean { - if (button == 0) { - draggingHue = false - draggingOpacity = false - draggingColor = false + private fun handleTweakedPanelClick(px: Float, py: Float): Boolean { + val bx = px + 20F + val sliderWidth = 300F + + for (i in 0..3) { + val sliderY = py + 102F + i * 50F + if (isHoveringOver(bx, sliderY - 5F, sliderWidth, 16F)) { + when (i) { + 0 -> draggingHueOffset = true + 1 -> draggingSaturationMult = true + 2 -> draggingBrightnessMult = true + 3 -> draggingOpacityMult = true + } + updateTweakedSlider(i, bx, sliderWidth) + return true + } } return false @@ -188,40 +765,239 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( override fun mouseDragged(button: Int, offsetX: Double, offsetY: Double): Boolean { if (button != 0 || !pickerOpen) return false - val px = x + width - 220F + val px = x + width - 360F val py = y + height - 10F - val bx = px + 10F - - when { - draggingColor -> updateColorFromBox(bx, py + 10F) - draggingHue -> updateHueFromSlider(bx) - draggingOpacity -> updateOpacityFromSlider(bx) + val controlsY = py + 75F + + when (setting.mode) { + is ColorMode.Static -> { + val bx = px + 10F + when { + draggingStaticColor -> updateStaticColorFromBox(bx, controlsY + 10F) + draggingStaticHue -> updateStaticHueFromSlider(bx) + draggingStaticOpacity -> updateStaticOpacityFromSlider(bx) + hexDragging && hexFocused -> { + hexInputHandler.updateSelection(mouseX.toFloat(), bx + 10F, 13F) + return true + } + else -> return false + } + return true + } + is ColorMode.Rainbow, is ColorMode.SyncedRainbow -> { + val bx = px + 20F + val sliderWidth = 300F + when { + draggingSpeed -> { updateRainbowSlider(0, bx, sliderWidth); return true } + draggingSaturation -> { updateRainbowSlider(1, bx, sliderWidth); return true } + draggingBrightness -> { updateRainbowSlider(2, bx, sliderWidth); return true } + draggingOpacity -> { updateRainbowSlider(3, bx, sliderWidth); return true } + } + } + is ColorMode.TweakedTheme -> { + val bx = px + 20F + val sliderWidth = 300F + when { + draggingHueOffset -> { updateTweakedSlider(0, bx, sliderWidth); return true } + draggingSaturationMult -> { updateTweakedSlider(1, bx, sliderWidth); return true } + draggingBrightnessMult -> { updateTweakedSlider(2, bx, sliderWidth); return true } + draggingOpacityMult -> { updateTweakedSlider(3, bx, sliderWidth); return true } + } + } else -> return false } - return true + return false + } + + override fun mouseReleased(button: Int): Boolean { + if (button == 0) { + draggingStaticHue = false + draggingStaticOpacity = false + draggingStaticColor = false + draggingSpeed = false + draggingSaturation = false + draggingBrightness = false + draggingOpacity = false + draggingHueOffset = false + draggingSaturationMult = false + draggingBrightnessMult = false + draggingOpacityMult = false + hexDragging = false + } + return false + } + + override fun mouseScrolled(horizontalAmount: Double, verticalAmount: Double): Boolean { + if (!pickerOpen) return false + + if (setting.mode is ColorMode.ThemeColor) { + val px = x + width - 360F + val py = y + height - 10F + val bx = px + 10F + val by = py + 60F + val panelWidth = 320F + val panelHeight = 240F + + if (isHoveringOver(bx, by, panelWidth, panelHeight)) { + themeScrollHandler.handleScroll(verticalAmount) + return true + } + } + + return false + } + + private fun updateStaticColorFromBox(boxX: Float, boxY: Float) { + staticSaturation = ((mouseX.toFloat() - boxX) / 320F).coerceIn(0f, 1f) + staticBrightness = (1f - (mouseY.toFloat() - boxY) / 180F).coerceIn(0f, 1f) + updateStaticColor() + } + + private fun updateStaticHueFromSlider(sliderX: Float) { + staticHue = ((mouseX.toFloat() - sliderX) / 320F).coerceIn(0f, 1f) + updateStaticColor() + } + + private fun updateStaticOpacityFromSlider(sliderX: Float) { + staticOpacity = ((mouseX.toFloat() - sliderX) / 320F).coerceIn(0f, 1f) + updateStaticColor() + } + + private fun updateStaticColor() { + val rgb = Color.HSBtoRGB(staticHue, staticSaturation, staticBrightness) + val alpha = (staticOpacity * 255).toInt() + setting.mode = ColorMode.Static((alpha shl 24) or (rgb and 0x00FFFFFF)) + updateHexFromCurrentColor() + } + + private fun updateRainbowSlider(index: Int, bx: Float, sliderWidth: Float) { + val normalized = ((mouseX.toFloat() - bx) / sliderWidth).coerceIn(0f, 1f) + + val mode = setting.mode + val newMode = when (mode) { + is ColorMode.Rainbow -> when (index) { + 0 -> mode.copy(speed = normalized * 2f) + 1 -> mode.copy(saturation = normalized) + 2 -> mode.copy(brightness = normalized) + 3 -> mode.copy(opacity = normalized) + else -> mode + } + is ColorMode.SyncedRainbow -> when (index) { + 0 -> mode.copy(speed = normalized * 2f) + 1 -> mode.copy(saturation = normalized) + 2 -> mode.copy(brightness = normalized) + 3 -> mode.copy(opacity = normalized) + else -> mode + } + else -> mode + } + + setting.mode = newMode } - private fun updateColorFromBox(boxX: Float, boxY: Float) { - saturation = ((mouseX.toFloat() - boxX) / 180F).coerceIn(0f, 1f) - lightness = (1f - (mouseY.toFloat() - boxY) / 180F).coerceIn(0f, 1f) - updateColor() + private fun updateTweakedSlider(index: Int, bx: Float, sliderWidth: Float) { + val normalized = ((mouseX.toFloat() - bx) / sliderWidth).coerceIn(0f, 1f) + + val mode = setting.mode as? ColorMode.TweakedTheme ?: return + + val newMode = when (index) { + 0 -> mode.copy(hueOffset = (normalized - 0.5f) * 360f) // -180 to 180 + 1 -> mode.copy(saturationMultiplier = normalized * 2f) // 0 to 2 + 2 -> mode.copy(brightnessMultiplier = normalized * 2f) // 0 to 2 + 3 -> mode.copy(opacityMultiplier = normalized) // 0 to 1 + else -> mode + } + + setting.mode = newMode } - private fun updateHueFromSlider(sliderX: Float) { - hue = ((mouseX.toFloat() - sliderX) / 180F).coerceIn(0f, 1f) - updateColor() + override fun charTyped(input: CharacterEvent): Boolean { + if (!hexFocused || !pickerOpen || setting.mode !is ColorMode.Static) return false + + val char = input.codepoint.toChar() + if (char in '0'..'9' || char in 'a'..'f' || char in 'A'..'F' || char == '#') { + if (char.code >= 32 && char != '\u007f') { + hexInputHandler.insertText(char.toString()) + hexValid = validateHexInput(hexInputHandler.getText()) + return true + } + } + + return false } - private fun updateOpacityFromSlider(sliderX: Float) { - opacity = ((mouseX.toFloat() - sliderX) / 180F).coerceIn(0f, 1f) - updateColor() + override fun keyPressed(input: KeyEvent): Boolean { + if (!hexFocused || !pickerOpen || setting.mode !is ColorMode.Static) return false + + val ctrl = input.modifiers and GLFW.GLFW_MOD_CONTROL != 0 + val shift = input.modifiers and GLFW.GLFW_MOD_SHIFT != 0 + + when (input.key) { + GLFW.GLFW_KEY_ESCAPE, GLFW.GLFW_KEY_ENTER -> { + if (hexValid) commitHexInput() + hexFocused = false + return true + } + + GLFW.GLFW_KEY_BACKSPACE -> { + hexInputHandler.backspace() + hexValid = validateHexInput(hexInputHandler.getText()) + return true + } + + GLFW.GLFW_KEY_DELETE -> { + hexInputHandler.delete() + hexValid = validateHexInput(hexInputHandler.getText()) + return true + } + + GLFW.GLFW_KEY_LEFT -> { + hexInputHandler.moveCursorLeft(shift); return true + } + + GLFW.GLFW_KEY_RIGHT -> { + hexInputHandler.moveCursorRight(shift); return true + } + + GLFW.GLFW_KEY_HOME -> { + hexInputHandler.moveCursorToStart(shift); return true + } + + GLFW.GLFW_KEY_END -> { + hexInputHandler.moveCursorToEnd(shift); return true + } + + GLFW.GLFW_KEY_A -> if (ctrl) { + hexInputHandler.selectAll(); return true + } + + GLFW.GLFW_KEY_C -> if (ctrl) { + hexInputHandler.copy()?.let { Minecraft.getInstance().keyboardHandler.clipboard = it } + return true + } + + GLFW.GLFW_KEY_X -> if (ctrl) { + hexInputHandler.cut()?.let { Minecraft.getInstance().keyboardHandler.clipboard = it } + hexValid = validateHexInput(hexInputHandler.getText()) + return true + } + + GLFW.GLFW_KEY_V -> if (ctrl) { + val clipboard = Minecraft.getInstance().keyboardHandler.clipboard + if (clipboard.isNotEmpty()) { + hexInputHandler.insertText(clipboard) + hexValid = validateHexInput(hexInputHandler.getText()) + } + return true + } + } + + return false } - private fun updateColor() { - val rgb = Color.HSBtoRGB(hue, saturation, lightness) - val alpha = (opacity * 255).toInt() - setting.value = (alpha shl 24) or (rgb and 0x00FFFFFF) + companion object { + private val checkmarkIcon = NVGRenderer.createImage("/assets/cobalt/textures/ui/checkmark.svg") } } diff --git a/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt b/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt index 675e840..4c8f2d2 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt @@ -23,7 +23,7 @@ internal class UISidebar : UIPanel( UIConfig.swapBodyPanel(UIAddonList()) } - private val hudButton = UIButton("/assets/cobalt/icons/palette.svg") { + private val hudButton = UIButton("/assets/cobalt/textures/ui/palette.svg") { UIHudEditor().openUI() } From 96796140e61c3782f18a606efd78f1809e6c76a2 Mon Sep 17 00:00:00 2001 From: ForBai Date: Wed, 18 Feb 2026 18:29:44 +0100 Subject: [PATCH 09/10] feat: add reset settings functionality and enhance theme color management --- src/main/kotlin/org/cobalt/Cobalt.kt | 2 +- .../kotlin/org/cobalt/api/hud/HudElement.kt | 9 + .../org/cobalt/api/module/setting/Setting.kt | 3 + .../module/setting/impl/CheckboxSetting.kt | 2 + .../api/module/setting/impl/ColorSetting.kt | 11 +- .../api/module/setting/impl/InfoSetting.kt | 2 + .../api/module/setting/impl/KeyBindSetting.kt | 2 + .../api/module/setting/impl/ModeSetting.kt | 2 + .../setting/impl/RainbowPhaseProvider.kt | 9 +- .../api/module/setting/impl/RangeSetting.kt | 6 +- .../api/module/setting/impl/SliderSetting.kt | 2 + .../api/module/setting/impl/TextSetting.kt | 2 + .../kotlin/org/cobalt/api/ui/theme/Theme.kt | 6 + .../org/cobalt/api/ui/theme/ThemeManager.kt | 26 ++- .../cobalt/api/ui/theme/impl/CustomTheme.kt | 4 + .../org/cobalt/api/ui/theme/impl/DarkTheme.kt | 5 + .../cobalt/api/ui/theme/impl/LightTheme.kt | 5 + .../ui/components/settings/UIColorSetting.kt | 37 ++-- .../internal/ui/hud/HudSettingsPopup.kt | 171 +++++++++++------- .../internal/ui/theme/ThemeSerializer.kt | 8 + 20 files changed, 208 insertions(+), 106 deletions(-) diff --git a/src/main/kotlin/org/cobalt/Cobalt.kt b/src/main/kotlin/org/cobalt/Cobalt.kt index 971fa87..0761659 100644 --- a/src/main/kotlin/org/cobalt/Cobalt.kt +++ b/src/main/kotlin/org/cobalt/Cobalt.kt @@ -18,7 +18,7 @@ object Cobalt : ClientModInitializer { override fun onInitializeClient() { - ModuleManager.addModules(listOf(WatermarkModule())) + ModuleManager.addModules(listOf(WatermarkModule(), WatermarkModule())) AddonLoader.getAddons().map { it.second }.forEach { it.onLoad() diff --git a/src/main/kotlin/org/cobalt/api/hud/HudElement.kt b/src/main/kotlin/org/cobalt/api/hud/HudElement.kt index 015d806..8818016 100644 --- a/src/main/kotlin/org/cobalt/api/hud/HudElement.kt +++ b/src/main/kotlin/org/cobalt/api/hud/HudElement.kt @@ -81,6 +81,15 @@ abstract class HudElement( scale = defaultScale } + /** Resets all settings to their default values. */ + fun resetSettings() { + for (setting in getSettings()) { + @Suppress("UNCHECKED_CAST") + val typedSetting = setting as Setting + typedSetting.value = typedSetting.defaultValue + } + } + fun containsPoint(px: Float, py: Float, screenWidth: Float, screenHeight: Float): Boolean { val (sx, sy) = getScreenPosition(screenWidth, screenHeight) return px >= sx && px <= sx + getScaledWidth() && diff --git a/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt b/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt index 6427aff..6a237fe 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/Setting.kt @@ -29,6 +29,9 @@ abstract class Setting( open var value: T, ) : ReadWriteProperty, PropertyDelegateProvider> { + open val defaultValue: T + get() = value + override operator fun provideDelegate(thisRef: SettingsContainer, property: KProperty<*>): ReadWriteProperty { thisRef.addSetting(this) return this diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/CheckboxSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/CheckboxSetting.kt index a3082c9..c8d00a1 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/CheckboxSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/CheckboxSetting.kt @@ -11,6 +11,8 @@ class CheckboxSetting( defaultValue: Boolean, ) : Setting(name, description, defaultValue) { + override val defaultValue: Boolean = defaultValue + override fun read(element: JsonElement) { this.value = element.asBoolean } diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/ColorSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/ColorSetting.kt index 3ccc816..ad1e047 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/ColorSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/ColorSetting.kt @@ -4,6 +4,7 @@ import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonPrimitive import org.cobalt.api.module.setting.Setting +import org.cobalt.api.ui.theme.ThemeManager import java.awt.Color /** ARGB color picker setting. Value is an ARGB integer (e.g. `0xFFFF0000.toInt()` for red). */ @@ -13,6 +14,8 @@ class ColorSetting( defaultValue: Int, ) : Setting(name, description, defaultValue) { + override val defaultValue: Int = defaultValue + /** Current color mode (static, rainbow, theme, etc.). */ var mode: ColorMode = ColorMode.Static(defaultValue) @@ -45,9 +48,11 @@ class ColorSetting( } is ColorMode.SyncedRainbow -> { - // Global synced rainbow: use RainbowPhaseProvider for shared phase - val hue = RainbowPhaseProvider.getHue(m.speed) - val rgb = Color.HSBtoRGB(hue, m.saturation, m.brightness) + val theme = ThemeManager.currentTheme + val hue = ThemeManager.getRainbowHue() + val sat = theme.rainbowSaturation + val bri = theme.rainbowBrightness + val rgb = Color.HSBtoRGB(hue, sat, bri) val alpha = (m.opacity * 255).toInt().coerceIn(0, 255) (alpha shl 24) or (rgb and 0x00FFFFFF) } diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/InfoSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/InfoSetting.kt index 1438b50..651066a 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/InfoSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/InfoSetting.kt @@ -10,6 +10,8 @@ internal class InfoSetting( val type: InfoType = InfoType.INFO, ) : Setting(name ?: "", "Info", "") { + override val defaultValue: String = "" + override fun read(element: JsonElement) { // It exists just to show text in the UI, so there is nothing to read. } diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/KeyBindSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/KeyBindSetting.kt index 31bccb8..f99e658 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/KeyBindSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/KeyBindSetting.kt @@ -16,6 +16,8 @@ class KeyBindSetting( defaultValue: KeyBind, ) : Setting(name, description, defaultValue) { + override val defaultValue: KeyBind = defaultValue + val keyName: String get() = when (value.keyCode) { -1 -> "None" diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/ModeSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/ModeSetting.kt index 523ba0a..0d2de09 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/ModeSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/ModeSetting.kt @@ -17,6 +17,8 @@ class ModeSetting( val options: Array, ) : Setting(name, description, defaultValue) { + override val defaultValue: Int = defaultValue + override fun read(element: JsonElement) { this.value = element.asInt } diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/RainbowPhaseProvider.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/RainbowPhaseProvider.kt index 842b4cd..4ddd61f 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/RainbowPhaseProvider.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/RainbowPhaseProvider.kt @@ -1,15 +1,15 @@ package org.cobalt.api.module.setting.impl +import org.cobalt.api.ui.theme.ThemeManager + /** * Singleton provider for globally synced rainbow phase computation. * * All ColorSettings using SyncedRainbow mode share the same phase from this provider. - * Phase is computed based on elapsed time since the provider was initialized. + * Phase computation is delegated to ThemeManager for theme-level synchronization. */ object RainbowPhaseProvider { - private val startTime = System.currentTimeMillis() - /** * Get the current hue value (0..1) for globally synced rainbow. * @@ -17,8 +17,7 @@ object RainbowPhaseProvider { * @return Hue value in range 0..1 (wraps at 1.0) */ fun getHue(speed: Float = 1f): Float { - val elapsed = (System.currentTimeMillis() - startTime) / 1000.0 - return ((elapsed * speed) % 1.0 + 1.0).toFloat() % 1f + return ThemeManager.getRainbowHue(speed) } } diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/RangeSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/RangeSetting.kt index e7a4b85..29652dc 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/RangeSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/RangeSetting.kt @@ -16,10 +16,12 @@ import org.cobalt.api.module.setting.Setting class RangeSetting( name: String, description: String, - private val defaultValue: Pair, + default: Pair, val min: Double, val max: Double, -) : Setting>(name, description, defaultValue) { +) : Setting>(name, description, default) { + + override val defaultValue: Pair = default override fun read(element: JsonElement) { if (element.isJsonObject) { diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/SliderSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/SliderSetting.kt index 61d7d35..02b82ef 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/SliderSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/SliderSetting.kt @@ -18,6 +18,8 @@ class SliderSetting( val max: Double, ) : Setting(name, description, defaultValue) { + override val defaultValue: Double = defaultValue + override fun read(element: JsonElement) { this.value = element.asDouble.coerceIn(min, max) } diff --git a/src/main/kotlin/org/cobalt/api/module/setting/impl/TextSetting.kt b/src/main/kotlin/org/cobalt/api/module/setting/impl/TextSetting.kt index df7bf75..f3303b7 100644 --- a/src/main/kotlin/org/cobalt/api/module/setting/impl/TextSetting.kt +++ b/src/main/kotlin/org/cobalt/api/module/setting/impl/TextSetting.kt @@ -11,6 +11,8 @@ class TextSetting( defaultValue: String, ) : Setting(name, description, defaultValue) { + override val defaultValue: String = defaultValue + override fun read(element: JsonElement) { this.value = element.asString } diff --git a/src/main/kotlin/org/cobalt/api/ui/theme/Theme.kt b/src/main/kotlin/org/cobalt/api/ui/theme/Theme.kt index f18df7d..2c26343 100644 --- a/src/main/kotlin/org/cobalt/api/ui/theme/Theme.kt +++ b/src/main/kotlin/org/cobalt/api/ui/theme/Theme.kt @@ -4,6 +4,12 @@ interface Theme { val name: String + // Rainbow settings (globally synced) + val rainbowEnabled: Boolean + val rainbowSpeed: Float + val rainbowSaturation: Float + val rainbowBrightness: Float + val background: Int val panel: Int val inset: Int diff --git a/src/main/kotlin/org/cobalt/api/ui/theme/ThemeManager.kt b/src/main/kotlin/org/cobalt/api/ui/theme/ThemeManager.kt index 5f2f007..79f8dd9 100644 --- a/src/main/kotlin/org/cobalt/api/ui/theme/ThemeManager.kt +++ b/src/main/kotlin/org/cobalt/api/ui/theme/ThemeManager.kt @@ -2,6 +2,7 @@ package org.cobalt.api.ui.theme import org.cobalt.api.ui.theme.impl.DarkTheme import org.cobalt.api.ui.theme.impl.LightTheme +import java.awt.Color object ThemeManager { @@ -9,6 +10,8 @@ object ThemeManager { var currentTheme: Theme = DarkTheme() private set + private val rainbowStartTime = System.currentTimeMillis() + init { registerTheme(DarkTheme()) registerTheme(LightTheme()) @@ -29,12 +32,10 @@ object ThemeManager { } fun unregisterTheme(theme: Theme): Boolean { - // Prevent deleting built-in themes if (theme.name == "Dark" || theme.name == "Light") return false val removed = themes.removeIf { it.name == theme.name } - // If deleted theme was current, switch to DarkTheme if (removed && currentTheme.name == theme.name) { currentTheme = themes.first { it.name == "Dark" } } @@ -42,4 +43,25 @@ object ThemeManager { return removed } + fun getRainbowHue(speedMultiplier: Float = 1f): Float { + val themeSpeed = currentTheme.rainbowSpeed + val effectiveSpeed = themeSpeed * speedMultiplier + val elapsed = (System.currentTimeMillis() - rainbowStartTime) / 1000.0 + return ((elapsed * effectiveSpeed) % 1.0 + 1.0).toFloat() % 1f + } + + fun getRainbowColor( + speedMultiplier: Float = 1f, + saturationOverride: Float? = null, + brightnessOverride: Float? = null, + opacity: Float = 1f + ): Int { + val hue = getRainbowHue(speedMultiplier) + val sat = saturationOverride ?: currentTheme.rainbowSaturation + val bri = brightnessOverride ?: currentTheme.rainbowBrightness + val rgb = Color.HSBtoRGB(hue, sat, bri) + val alpha = (opacity * 255).toInt().coerceIn(0, 255) + return (alpha shl 24) or (rgb and 0x00FFFFFF) + } + } diff --git a/src/main/kotlin/org/cobalt/api/ui/theme/impl/CustomTheme.kt b/src/main/kotlin/org/cobalt/api/ui/theme/impl/CustomTheme.kt index fac6381..a3eea68 100644 --- a/src/main/kotlin/org/cobalt/api/ui/theme/impl/CustomTheme.kt +++ b/src/main/kotlin/org/cobalt/api/ui/theme/impl/CustomTheme.kt @@ -5,6 +5,10 @@ import org.cobalt.api.ui.theme.Theme data class CustomTheme( override var name: String = "Custom", + override var rainbowEnabled: Boolean = false, + override var rainbowSpeed: Float = 1f, + override var rainbowSaturation: Float = 1f, + override var rainbowBrightness: Float = 1f, override var background: Int = Color(18, 18, 18).rgb, override var panel: Int = Color(24, 24, 24).rgb, override var inset: Int = Color(30, 30, 30).rgb, diff --git a/src/main/kotlin/org/cobalt/api/ui/theme/impl/DarkTheme.kt b/src/main/kotlin/org/cobalt/api/ui/theme/impl/DarkTheme.kt index 2e3c662..0563493 100644 --- a/src/main/kotlin/org/cobalt/api/ui/theme/impl/DarkTheme.kt +++ b/src/main/kotlin/org/cobalt/api/ui/theme/impl/DarkTheme.kt @@ -7,6 +7,11 @@ class DarkTheme : Theme { override val name = "Dark" + override val rainbowEnabled = false + override val rainbowSpeed = 1f + override val rainbowSaturation = 1f + override val rainbowBrightness = 1f + override val background = Color(18, 18, 18).rgb override val panel = Color(24, 24, 24).rgb override val inset = Color(30, 30, 30).rgb diff --git a/src/main/kotlin/org/cobalt/api/ui/theme/impl/LightTheme.kt b/src/main/kotlin/org/cobalt/api/ui/theme/impl/LightTheme.kt index 6b4bece..8e7f419 100644 --- a/src/main/kotlin/org/cobalt/api/ui/theme/impl/LightTheme.kt +++ b/src/main/kotlin/org/cobalt/api/ui/theme/impl/LightTheme.kt @@ -7,6 +7,11 @@ class LightTheme : Theme { override val name = "Light" + override val rainbowEnabled = false + override val rainbowSpeed = 1f + override val rainbowSaturation = 1f + override val rainbowBrightness = 1f + override val background = Color(240, 240, 240).rgb override val panel = Color(255, 255, 255).rgb override val inset = Color(232, 232, 232).rgb diff --git a/src/main/kotlin/org/cobalt/internal/ui/components/settings/UIColorSetting.kt b/src/main/kotlin/org/cobalt/internal/ui/components/settings/UIColorSetting.kt index 422db1c..f58902a 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/components/settings/UIColorSetting.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/components/settings/UIColorSetting.kt @@ -151,10 +151,10 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( // Custom Tab val isCustom = setting.mode !is ColorMode.ThemeColor && setting.mode !is ColorMode.TweakedTheme val customColor = if (isCustom) ThemeManager.currentTheme.accent else ThemeManager.currentTheme.controlBg - + NVGRenderer.rect(bx, by, tabWidth, tabHeight, customColor, 5F) NVGRenderer.hollowRect(bx, by, tabWidth, tabHeight, 1F, ThemeManager.currentTheme.controlBorder, 5F) - + val customText = "Custom" val customTextWidth = NVGRenderer.textWidth(customText, 13F) val customTextColor = if (isCustom) ThemeManager.currentTheme.white else ThemeManager.currentTheme.text @@ -183,7 +183,7 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( if (isCustom) { val isRainbow = setting.mode is ColorMode.Rainbow || setting.mode is ColorMode.SyncedRainbow drawCheckbox(bx, by, checkboxSize, isRainbow, "Rainbow") - + val syncX = bx + 100F val isSynced = setting.mode is ColorMode.SyncedRainbow drawCheckbox(syncX, by, checkboxSize, isSynced, "Sync") @@ -238,7 +238,7 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( val x2 = bx + (sliderWidth / 36f) * (i + 1) val color1 = Color.HSBtoRGB(i / 36f, 1f, 1f) val color2 = Color.HSBtoRGB((i + 1) / 36f, 1f, 1f) - NVGRenderer.gradientRect(x1, hueY, x2 - x1, 6F, color1, color2, Gradient.LeftToRight, 3F) + NVGRenderer.gradientRect(x1, hueY, x2 - x1, 6F, color1, color2, Gradient.LeftToRight,0f) } NVGRenderer.hollowRect(bx, hueY, sliderWidth, 6F, 1F, ThemeManager.currentTheme.controlBorder, 3F) @@ -246,18 +246,9 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( val opacityY = hueY + 20F - NVGRenderer.rect(bx, opacityY, sliderWidth, 6F, ThemeManager.currentTheme.white, 3F) - for (i in 0..31) { - if (i % 2 == 0) { - NVGRenderer.rect(bx + i * 10F, opacityY, 10F, 3F, ThemeManager.currentTheme.textSecondary, 0F) - NVGRenderer.rect(bx + i * 10F, opacityY + 3F, 10F, 3F, ThemeManager.currentTheme.white, 0F) - } else { - NVGRenderer.rect(bx + i * 10F, opacityY, 10F, 3F, ThemeManager.currentTheme.white, 0F) - NVGRenderer.rect(bx + i * 10F, opacityY + 3F, 10F, 3F, ThemeManager.currentTheme.textSecondary, 0F) - } - } + NVGRenderer.rect(bx, opacityY, sliderWidth, 6F, ThemeManager.currentTheme.white, 3F) - val currentColor = Color.HSBtoRGB(staticHue, staticSaturation, staticBrightness) + val currentColor = Color.HSBtoRGB(staticHue, staticSaturation, staticBrightness) val opaqueColor = Color(currentColor or (255 shl 24), true).rgb val transparentColor = Color(currentColor and 0x00FFFFFF, true).rgb @@ -321,20 +312,20 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( values.forEachIndexed { index, value -> val sliderY = by + index * 50F - + NVGRenderer.text(labels[index], bx, sliderY + 2F, 13F, ThemeManager.currentTheme.text) - + val valueText = String.format("%.2f", value) val valueWidth = NVGRenderer.textWidth(valueText, 12F) NVGRenderer.text(valueText, bx + sliderWidth - valueWidth, sliderY + 2F, 12F, ThemeManager.currentTheme.textSecondary) - + val trackY = sliderY + 24F val normalizedValue = when (index) { 0 -> (value / 2f).coerceIn(0f, 1f) else -> value } val thumbX = bx + normalizedValue * sliderWidth - + NVGRenderer.rect(bx, trackY, sliderWidth, 6F, ThemeManager.currentTheme.sliderTrack, 3F) NVGRenderer.rect(bx, trackY, thumbX - bx, 6F, ThemeManager.currentTheme.sliderFill, 3F) NVGRenderer.circle(thumbX, trackY + 3F, 8F, ThemeManager.currentTheme.sliderThumb) @@ -457,7 +448,7 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( val trackY = sliderY + 24F val sliderWidth = 300F - + val thumbX = if (index == 0) { bx + (normalizedValue + 1f) / 2f * sliderWidth } else { @@ -873,7 +864,7 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( private fun updateRainbowSlider(index: Int, bx: Float, sliderWidth: Float) { val normalized = ((mouseX.toFloat() - bx) / sliderWidth).coerceIn(0f, 1f) - + val mode = setting.mode val newMode = when (mode) { is ColorMode.Rainbow -> when (index) { @@ -898,9 +889,9 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( private fun updateTweakedSlider(index: Int, bx: Float, sliderWidth: Float) { val normalized = ((mouseX.toFloat() - bx) / sliderWidth).coerceIn(0f, 1f) - + val mode = setting.mode as? ColorMode.TweakedTheme ?: return - + val newMode = when (index) { 0 -> mode.copy(hueOffset = (normalized - 0.5f) * 360f) // -180 to 180 1 -> mode.copy(saturationMultiplier = normalized * 2f) // 0 to 2 diff --git a/src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt b/src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt index 1371d70..dc8335a 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt @@ -141,53 +141,72 @@ internal class HudSettingsPopup { NVGRenderer.line(cx + half, cy - half, cx - half, cy + half, 1.5f, ThemeManager.currentTheme.textPrimary) } - private fun renderControls(target: HudElement) { - val controlsY = panelY + headerHeight + 10f - val buttonHeight = 30f - - val toggleText = if (target.enabled) "Disable" else "Enable" - val toggleWidth = NVGRenderer.textWidth(toggleText, 13f) + 30f - val toggleX = panelX + padding - - val isToggleHover = isHoveringOver(toggleX, controlsY, toggleWidth, buttonHeight) - val toggleBg = toggleAnim.get( - ThemeManager.currentTheme.controlBg, - ThemeManager.currentTheme.selectedOverlay, - !isToggleHover - ) - val toggleBorder = toggleAnim.get( - ThemeManager.currentTheme.controlBorder, - ThemeManager.currentTheme.accent, - !isToggleHover - ) - - NVGRenderer.rect(toggleX, controlsY, toggleWidth, buttonHeight, toggleBg, 8f) - NVGRenderer.hollowRect(toggleX, controlsY, toggleWidth, buttonHeight, 1.5f, toggleBorder, 8f) - NVGRenderer.text(toggleText, toggleX + 15f, controlsY + 8f, 13f, ThemeManager.currentTheme.textPrimary) - - val statusText = if (target.enabled) "Enabled" else "Disabled" - val statusColor = if (target.enabled) ThemeManager.currentTheme.accent else ThemeManager.currentTheme.textSecondary - NVGRenderer.text(statusText, toggleX + toggleWidth + 12f, controlsY + 8f, 12f, statusColor) - - val resetText = "Reset Position" - val resetWidth = NVGRenderer.textWidth(resetText, 13f) + 30f - val resetX = panelX + panelWidth - padding - resetWidth - val resetHover = isHoveringOver(resetX, controlsY, resetWidth, buttonHeight) - val resetBg = buttonAnim.get( - ThemeManager.currentTheme.controlBg, - ThemeManager.currentTheme.selectedOverlay, - !resetHover - ) - val resetBorder = buttonAnim.get( - ThemeManager.currentTheme.controlBorder, - ThemeManager.currentTheme.accent, - !resetHover - ) - - NVGRenderer.rect(resetX, controlsY, resetWidth, buttonHeight, resetBg, 8f) - NVGRenderer.hollowRect(resetX, controlsY, resetWidth, buttonHeight, 1.5f, resetBorder, 8f) - NVGRenderer.text(resetText, resetX + 15f, controlsY + 8f, 13f, ThemeManager.currentTheme.textPrimary) - } + private fun renderControls(target: HudElement) { + val controlsY = panelY + headerHeight + 10f + val buttonHeight = 30f + + val toggleText = if (target.enabled) "Disable" else "Enable" + val toggleWidth = NVGRenderer.textWidth(toggleText, 13f) + 30f + val toggleX = panelX + padding + + val isToggleHover = isHoveringOver(toggleX, controlsY, toggleWidth, buttonHeight) + val toggleBg = toggleAnim.get( + ThemeManager.currentTheme.controlBg, + ThemeManager.currentTheme.selectedOverlay, + !isToggleHover + ) + val toggleBorder = toggleAnim.get( + ThemeManager.currentTheme.controlBorder, + ThemeManager.currentTheme.accent, + !isToggleHover + ) + + NVGRenderer.rect(toggleX, controlsY, toggleWidth, buttonHeight, toggleBg, 8f) + NVGRenderer.hollowRect(toggleX, controlsY, toggleWidth, buttonHeight, 1.5f, toggleBorder, 8f) + NVGRenderer.text(toggleText, toggleX + 15f, controlsY + 8f, 13f, ThemeManager.currentTheme.textPrimary) + + val statusText = if (target.enabled) "Enabled" else "Disabled" + val statusColor = if (target.enabled) ThemeManager.currentTheme.accent else ThemeManager.currentTheme.textSecondary + NVGRenderer.text(statusText, toggleX + toggleWidth + 12f, controlsY + 8f, 12f, statusColor) + + val resetSettingsText = "Reset Settings" + val resetSettingsWidth = NVGRenderer.textWidth(resetSettingsText, 13f) + 30f + val resetSettingsX = panelX + panelWidth - padding - resetSettingsWidth + val resetSettingsHover = isHoveringOver(resetSettingsX, controlsY, resetSettingsWidth, buttonHeight) + val resetSettingsBg = buttonAnim.get( + ThemeManager.currentTheme.controlBg, + ThemeManager.currentTheme.selectedOverlay, + !resetSettingsHover + ) + val resetSettingsBorder = buttonAnim.get( + ThemeManager.currentTheme.controlBorder, + ThemeManager.currentTheme.accent, + !resetSettingsHover + ) + + NVGRenderer.rect(resetSettingsX, controlsY, resetSettingsWidth, buttonHeight, resetSettingsBg, 8f) + NVGRenderer.hollowRect(resetSettingsX, controlsY, resetSettingsWidth, buttonHeight, 1.5f, resetSettingsBorder, 8f) + NVGRenderer.text(resetSettingsText, resetSettingsX + 15f, controlsY + 8f, 13f, ThemeManager.currentTheme.textPrimary) + + val resetText = "Reset Position" + val resetWidth = NVGRenderer.textWidth(resetText, 13f) + 30f + val resetX = resetSettingsX - resetWidth - 10f + val resetHover = isHoveringOver(resetX, controlsY, resetWidth, buttonHeight) + val resetBg = buttonAnim.get( + ThemeManager.currentTheme.controlBg, + ThemeManager.currentTheme.selectedOverlay, + !resetHover + ) + val resetBorder = buttonAnim.get( + ThemeManager.currentTheme.controlBorder, + ThemeManager.currentTheme.accent, + !resetHover + ) + + NVGRenderer.rect(resetX, controlsY, resetWidth, buttonHeight, resetBg, 8f) + NVGRenderer.hollowRect(resetX, controlsY, resetWidth, buttonHeight, 1.5f, resetBorder, 8f) + NVGRenderer.text(resetText, resetX + 15f, controlsY + 8f, 13f, ThemeManager.currentTheme.textPrimary) + } private fun renderSettings(startY: Float) { if (settingComponents.isEmpty()) { @@ -248,29 +267,41 @@ internal class HudSettingsPopup { val controlsY = panelY + headerHeight + 10f val buttonHeight = 30f - val toggleText = if (target.enabled) "Disable" else "Enable" - val toggleWidth = NVGRenderer.textWidth(toggleText, 13f) + 30f - val toggleX = panelX + padding - - if (mouseX >= toggleX && mouseX <= toggleX + toggleWidth && - mouseY >= controlsY && mouseY <= controlsY + buttonHeight - ) { - target.enabled = !target.enabled - toggleAnim.start() - return true - } - - val resetText = "Reset Position" - val resetWidth = NVGRenderer.textWidth(resetText, 13f) + 30f - val resetX = panelX + panelWidth - padding - resetWidth - - if (mouseX >= resetX && mouseX <= resetX + resetWidth && - mouseY >= controlsY && mouseY <= controlsY + buttonHeight - ) { - target.resetPosition() - buttonAnim.start() - return true - } + val toggleText = if (target.enabled) "Disable" else "Enable" + val toggleWidth = NVGRenderer.textWidth(toggleText, 13f) + 30f + val toggleX = panelX + padding + + if (mouseX >= toggleX && mouseX <= toggleX + toggleWidth && + mouseY >= controlsY && mouseY <= controlsY + buttonHeight + ) { + target.enabled = !target.enabled + toggleAnim.start() + return true + } + + val resetSettingsText = "Reset Settings" + val resetSettingsWidth = NVGRenderer.textWidth(resetSettingsText, 13f) + 30f + val resetSettingsX = panelX + panelWidth - padding - resetSettingsWidth + + if (mouseX >= resetSettingsX && mouseX <= resetSettingsX + resetSettingsWidth && + mouseY >= controlsY && mouseY <= controlsY + buttonHeight + ) { + target.resetSettings() + buttonAnim.start() + return true + } + + val resetText = "Reset Position" + val resetWidth = NVGRenderer.textWidth(resetText, 13f) + 30f + val resetX = resetSettingsX - resetWidth - 10f + + if (mouseX >= resetX && mouseX <= resetX + resetWidth && + mouseY >= controlsY && mouseY <= controlsY + buttonHeight + ) { + target.resetPosition() + buttonAnim.start() + return true + } for (component in settingComponents) { if (component.mouseClicked(button)) return true diff --git a/src/main/kotlin/org/cobalt/internal/ui/theme/ThemeSerializer.kt b/src/main/kotlin/org/cobalt/internal/ui/theme/ThemeSerializer.kt index 3fc6413..731877a 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/theme/ThemeSerializer.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/theme/ThemeSerializer.kt @@ -27,6 +27,10 @@ internal object ThemeSerializer { fun toJson(theme: CustomTheme): JsonObject = JsonObject().apply { addProperty("name", theme.name) + addProperty("rainbowEnabled", theme.rainbowEnabled) + addProperty("rainbowSpeed", theme.rainbowSpeed) + addProperty("rainbowSaturation", theme.rainbowSaturation) + addProperty("rainbowBrightness", theme.rainbowBrightness) addProperty("background", theme.background) addProperty("panel", theme.panel) addProperty("inset", theme.inset) @@ -86,6 +90,10 @@ internal object ThemeSerializer { val defaults = CustomTheme() return CustomTheme( name = json.get("name")?.asString ?: defaults.name, + rainbowEnabled = json.get("rainbowEnabled")?.asBoolean ?: defaults.rainbowEnabled, + rainbowSpeed = json.get("rainbowSpeed")?.asFloat ?: defaults.rainbowSpeed, + rainbowSaturation = json.get("rainbowSaturation")?.asFloat ?: defaults.rainbowSaturation, + rainbowBrightness = json.get("rainbowBrightness")?.asFloat ?: defaults.rainbowBrightness, background = json.get("background")?.asInt ?: defaults.background, panel = json.get("panel")?.asInt ?: defaults.panel, inset = json.get("inset")?.asInt ?: defaults.inset, From 364ceceb56707f2694cbe01bf9a56bfd1733232a Mon Sep 17 00:00:00 2001 From: ForBai Date: Thu, 19 Feb 2026 18:47:12 +0100 Subject: [PATCH 10/10] refactor: streamline mouse event handling and improve code readability in UI components --- .../ui/components/settings/UIColorSetting.kt | 260 ++++++++++-------- .../internal/ui/hud/HudSettingsPopup.kt | 109 +++++--- .../internal/ui/panel/panels/UISidebar.kt | 2 +- .../cobalt/internal/ui/screen/UIHudEditor.kt | 98 ++++--- 4 files changed, 276 insertions(+), 193 deletions(-) diff --git a/src/main/kotlin/org/cobalt/internal/ui/components/settings/UIColorSetting.kt b/src/main/kotlin/org/cobalt/internal/ui/components/settings/UIColorSetting.kt index f58902a..7e8187c 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/components/settings/UIColorSetting.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/components/settings/UIColorSetting.kt @@ -557,10 +557,23 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( val px = x + width - 360F val py = y + height - 10F - - // Tab Clicks val bx = px + 10F val by = py + 10F + + if (handleTabClicks(bx, by)) return true + if (handleCheckboxClicks(bx, py)) return true + + val controlsY = py + 75F + return when (setting.mode) { + is ColorMode.Static -> handleStaticPanelClick(px, controlsY) + is ColorMode.Rainbow -> handleRainbowPanelClick(px, controlsY) + is ColorMode.SyncedRainbow -> handleSyncedRainbowPanelClick(px, controlsY) + is ColorMode.ThemeColor -> handleThemePanelClick(px, controlsY) + is ColorMode.TweakedTheme -> handleTweakedPanelClick(px, controlsY) + } + } + + private fun handleTabClicks(bx: Float, by: Float): Boolean { val totalWidth = 320F val tabWidth = (totalWidth - 10F) / 2F val tabHeight = 28F @@ -581,60 +594,72 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( return true } - // Checkbox Clicks + return false + } + + private fun handleCheckboxClicks(bx: Float, py: Float): Boolean { val checkboxY = py + 48F val checkboxSize = 20F val isCustom = setting.mode !is ColorMode.ThemeColor && setting.mode !is ColorMode.TweakedTheme - if (isCustom) { - if (isHoveringOver(bx, checkboxY, checkboxSize + 60F, checkboxSize)) { - val isRainbow = setting.mode is ColorMode.Rainbow || setting.mode is ColorMode.SyncedRainbow - if (isRainbow) { - val rgb = Color.HSBtoRGB(staticHue, staticSaturation, staticBrightness) - val alpha = (staticOpacity * 255).toInt() - setting.mode = ColorMode.Static((alpha shl 24) or (rgb and 0x00FFFFFF)) - } else { - setting.mode = ColorMode.Rainbow() - } - return true - } + return if (isCustom) { + handleCustomCheckboxClicks(bx, checkboxY, checkboxSize) + } else { + handleThemeCheckboxClicks(bx, checkboxY, checkboxSize) + } + } - val syncX = bx + 100F - if (isHoveringOver(syncX, checkboxY, checkboxSize + 40F, checkboxSize)) { - val isSynced = setting.mode is ColorMode.SyncedRainbow - if (isSynced) { - val old = setting.mode as ColorMode.SyncedRainbow - setting.mode = ColorMode.Rainbow(old.speed, old.saturation, old.brightness, old.opacity) - } else { - val old = setting.mode as? ColorMode.Rainbow - if (old != null) { - setting.mode = ColorMode.SyncedRainbow(old.speed, old.saturation, old.brightness, old.opacity) - } else { - setting.mode = ColorMode.SyncedRainbow() - } - } - return true - } + private fun handleCustomCheckboxClicks(bx: Float, checkboxY: Float, checkboxSize: Float): Boolean { + if (isHoveringOver(bx, checkboxY, checkboxSize + 60F, checkboxSize)) { + toggleRainbowMode() + return true + } + + val syncX = bx + 100F + if (isHoveringOver(syncX, checkboxY, checkboxSize + 40F, checkboxSize)) { + toggleSyncedMode() + return true + } + + return false + } + + private fun toggleRainbowMode() { + val isRainbow = setting.mode is ColorMode.Rainbow || setting.mode is ColorMode.SyncedRainbow + if (isRainbow) { + val rgb = Color.HSBtoRGB(staticHue, staticSaturation, staticBrightness) + val alpha = (staticOpacity * 255).toInt() + setting.mode = ColorMode.Static((alpha shl 24) or (rgb and 0x00FFFFFF)) } else { - if (isHoveringOver(bx, checkboxY, checkboxSize + 60F, checkboxSize)) { - val isAdjusted = setting.mode is ColorMode.TweakedTheme - if (isAdjusted) { - setting.mode = ColorMode.ThemeColor(selectedThemeProperty) - } else { - setting.mode = ColorMode.TweakedTheme(selectedThemeProperty) - } - return true + setting.mode = ColorMode.Rainbow() + } + } + + private fun toggleSyncedMode() { + when (val current = setting.mode) { + is ColorMode.SyncedRainbow -> { + setting.mode = ColorMode.Rainbow(current.speed, current.saturation, current.brightness, current.opacity) + } + is ColorMode.Rainbow -> { + setting.mode = ColorMode.SyncedRainbow(current.speed, current.saturation, current.brightness, current.opacity) + } + else -> { + setting.mode = ColorMode.SyncedRainbow() } } + } - val controlsY = py + 75F - return when (setting.mode) { - is ColorMode.Static -> handleStaticPanelClick(px, controlsY) - is ColorMode.Rainbow -> handleRainbowPanelClick(px, controlsY) - is ColorMode.SyncedRainbow -> handleSyncedRainbowPanelClick(px, controlsY) - is ColorMode.ThemeColor -> handleThemePanelClick(px, controlsY) - is ColorMode.TweakedTheme -> handleTweakedPanelClick(px, controlsY) + private fun handleThemeCheckboxClicks(bx: Float, checkboxY: Float, checkboxSize: Float): Boolean { + if (isHoveringOver(bx, checkboxY, checkboxSize + 60F, checkboxSize)) { + val isAdjusted = setting.mode is ColorMode.TweakedTheme + setting.mode = if (isAdjusted) { + ColorMode.ThemeColor(selectedThemeProperty) + } else { + ColorMode.TweakedTheme(selectedThemeProperty) + } + return true } + return false } private fun handleStaticPanelClick(px: Float, py: Float): Boolean { @@ -865,26 +890,11 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( private fun updateRainbowSlider(index: Int, bx: Float, sliderWidth: Float) { val normalized = ((mouseX.toFloat() - bx) / sliderWidth).coerceIn(0f, 1f) - val mode = setting.mode - val newMode = when (mode) { - is ColorMode.Rainbow -> when (index) { - 0 -> mode.copy(speed = normalized * 2f) - 1 -> mode.copy(saturation = normalized) - 2 -> mode.copy(brightness = normalized) - 3 -> mode.copy(opacity = normalized) - else -> mode - } - is ColorMode.SyncedRainbow -> when (index) { - 0 -> mode.copy(speed = normalized * 2f) - 1 -> mode.copy(saturation = normalized) - 2 -> mode.copy(brightness = normalized) - 3 -> mode.copy(opacity = normalized) - else -> mode - } + setting.mode = when (val mode = setting.mode) { + is ColorMode.Rainbow -> updateRainbowModeValues(mode, index, normalized) + is ColorMode.SyncedRainbow -> updateSyncedRainbowModeValues(mode, index, normalized) else -> mode } - - setting.mode = newMode } private fun updateTweakedSlider(index: Int, bx: Float, sliderWidth: Float) { @@ -903,16 +913,37 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( setting.mode = newMode } + private fun updateRainbowModeValues(mode: ColorMode.Rainbow, index: Int, normalized: Float): ColorMode.Rainbow { + return when (index) { + 0 -> mode.copy(speed = normalized * 2f) + 1 -> mode.copy(saturation = normalized) + 2 -> mode.copy(brightness = normalized) + 3 -> mode.copy(opacity = normalized) + else -> mode + } + } + + private fun updateSyncedRainbowModeValues(mode: ColorMode.SyncedRainbow, index: Int, normalized: Float): ColorMode.SyncedRainbow { + return when (index) { + 0 -> mode.copy(speed = normalized * 2f) + 1 -> mode.copy(saturation = normalized) + 2 -> mode.copy(brightness = normalized) + 3 -> mode.copy(opacity = normalized) + else -> mode + } + } + override fun charTyped(input: CharacterEvent): Boolean { if (!hexFocused || !pickerOpen || setting.mode !is ColorMode.Static) return false val char = input.codepoint.toChar() - if (char in '0'..'9' || char in 'a'..'f' || char in 'A'..'F' || char == '#') { - if (char.code >= 32 && char != '\u007f') { - hexInputHandler.insertText(char.toString()) - hexValid = validateHexInput(hexInputHandler.getText()) - return true - } + val isHexChar = char in '0'..'9' || char in 'a'..'f' || char in 'A'..'F' || char == '#' + val isPrintable = char.code >= 32 && char != '\u007f' + + if (isHexChar && isPrintable) { + hexInputHandler.insertText(char.toString()) + hexValid = validateHexInput(hexInputHandler.getText()) + return true } return false @@ -924,67 +955,76 @@ internal class UIColorSetting(private val setting: ColorSetting) : UIComponent( val ctrl = input.modifiers and GLFW.GLFW_MOD_CONTROL != 0 val shift = input.modifiers and GLFW.GLFW_MOD_SHIFT != 0 - when (input.key) { + if (ctrl) { + val handled = handleCtrlKeyCombo(input.key) + if (handled) return true + } + + return handleHexInputKey(input.key, shift) + } + + private fun handleCtrlKeyCombo(key: Int): Boolean { + return when (key) { + GLFW.GLFW_KEY_A -> { + hexInputHandler.selectAll() + true + } + GLFW.GLFW_KEY_C -> { + hexInputHandler.copy()?.let { Minecraft.getInstance().keyboardHandler.clipboard = it } + true + } + GLFW.GLFW_KEY_X -> { + hexInputHandler.cut()?.let { Minecraft.getInstance().keyboardHandler.clipboard = it } + hexValid = validateHexInput(hexInputHandler.getText()) + true + } + GLFW.GLFW_KEY_V -> { + val clipboard = Minecraft.getInstance().keyboardHandler.clipboard + if (clipboard.isNotEmpty()) { + hexInputHandler.insertText(clipboard) + hexValid = validateHexInput(hexInputHandler.getText()) + } + true + } + else -> false + } + } + + private fun handleHexInputKey(key: Int, shift: Boolean): Boolean { + return when (key) { GLFW.GLFW_KEY_ESCAPE, GLFW.GLFW_KEY_ENTER -> { if (hexValid) commitHexInput() hexFocused = false - return true + true } - GLFW.GLFW_KEY_BACKSPACE -> { hexInputHandler.backspace() hexValid = validateHexInput(hexInputHandler.getText()) - return true + true } - GLFW.GLFW_KEY_DELETE -> { hexInputHandler.delete() hexValid = validateHexInput(hexInputHandler.getText()) - return true + true } - GLFW.GLFW_KEY_LEFT -> { - hexInputHandler.moveCursorLeft(shift); return true + hexInputHandler.moveCursorLeft(shift) + true } - GLFW.GLFW_KEY_RIGHT -> { - hexInputHandler.moveCursorRight(shift); return true + hexInputHandler.moveCursorRight(shift) + true } - GLFW.GLFW_KEY_HOME -> { - hexInputHandler.moveCursorToStart(shift); return true + hexInputHandler.moveCursorToStart(shift) + true } - GLFW.GLFW_KEY_END -> { - hexInputHandler.moveCursorToEnd(shift); return true - } - - GLFW.GLFW_KEY_A -> if (ctrl) { - hexInputHandler.selectAll(); return true - } - - GLFW.GLFW_KEY_C -> if (ctrl) { - hexInputHandler.copy()?.let { Minecraft.getInstance().keyboardHandler.clipboard = it } - return true - } - - GLFW.GLFW_KEY_X -> if (ctrl) { - hexInputHandler.cut()?.let { Minecraft.getInstance().keyboardHandler.clipboard = it } - hexValid = validateHexInput(hexInputHandler.getText()) - return true - } - - GLFW.GLFW_KEY_V -> if (ctrl) { - val clipboard = Minecraft.getInstance().keyboardHandler.clipboard - if (clipboard.isNotEmpty()) { - hexInputHandler.insertText(clipboard) - hexValid = validateHexInput(hexInputHandler.getText()) - } - return true + hexInputHandler.moveCursorToEnd(shift) + true } + else -> false } - - return false } companion object { diff --git a/src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt b/src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt index dc8335a..b29545e 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/hud/HudSettingsPopup.kt @@ -253,61 +253,88 @@ internal class HudSettingsPopup { if (!visible) return false val target = module ?: return false - if (button == 0) { - val closeX = panelX + panelWidth - padding - 26f - val closeY = panelY + 12f - if (mouseX >= closeX && mouseX <= closeX + 26f && mouseY >= closeY && mouseY <= closeY + 26f) { - hide() - return true - } + if (button == 0 && handleCloseButtonClick(mouseX, mouseY)) { + return true } if (button != 0) return containsPoint(mouseX, mouseY) + if (handleControlButtonClicks(mouseX, mouseY, target)) return true + + for (component in settingComponents) { + if (component.mouseClicked(button)) return true + } + + return containsPoint(mouseX, mouseY) + } + + private fun handleCloseButtonClick(mouseX: Float, mouseY: Float): Boolean { + val closeX = panelX + panelWidth - padding - 26f + val closeY = panelY + 12f + if (mouseX >= closeX && mouseX <= closeX + 26f && mouseY >= closeY && mouseY <= closeY + 26f) { + hide() + return true + } + return false + } + + private fun handleControlButtonClicks(mouseX: Float, mouseY: Float, target: HudElement): Boolean { val controlsY = panelY + headerHeight + 10f val buttonHeight = 30f - val toggleText = if (target.enabled) "Disable" else "Enable" - val toggleWidth = NVGRenderer.textWidth(toggleText, 13f) + 30f - val toggleX = panelX + padding - - if (mouseX >= toggleX && mouseX <= toggleX + toggleWidth && - mouseY >= controlsY && mouseY <= controlsY + buttonHeight - ) { - target.enabled = !target.enabled - toggleAnim.start() - return true - } + if (handleToggleButtonClick(mouseX, mouseY, controlsY, buttonHeight, target)) return true + if (handleResetSettingsClick(mouseX, mouseY, controlsY, buttonHeight, target)) return true + if (handleResetPositionClick(mouseX, mouseY, controlsY, buttonHeight, target)) return true - val resetSettingsText = "Reset Settings" - val resetSettingsWidth = NVGRenderer.textWidth(resetSettingsText, 13f) + 30f - val resetSettingsX = panelX + panelWidth - padding - resetSettingsWidth + return false + } - if (mouseX >= resetSettingsX && mouseX <= resetSettingsX + resetSettingsWidth && - mouseY >= controlsY && mouseY <= controlsY + buttonHeight - ) { - target.resetSettings() - buttonAnim.start() - return true - } + private fun handleToggleButtonClick(mouseX: Float, mouseY: Float, controlsY: Float, buttonHeight: Float, target: HudElement): Boolean { + val toggleText = if (target.enabled) "Disable" else "Enable" + val toggleWidth = NVGRenderer.textWidth(toggleText, 13f) + 30f + val toggleX = panelX + padding - val resetText = "Reset Position" - val resetWidth = NVGRenderer.textWidth(resetText, 13f) + 30f - val resetX = resetSettingsX - resetWidth - 10f + if (mouseX >= toggleX && mouseX <= toggleX + toggleWidth && + mouseY >= controlsY && mouseY <= controlsY + buttonHeight + ) { + target.enabled = !target.enabled + toggleAnim.start() + return true + } + return false + } - if (mouseX >= resetX && mouseX <= resetX + resetWidth && - mouseY >= controlsY && mouseY <= controlsY + buttonHeight - ) { - target.resetPosition() - buttonAnim.start() - return true - } + private fun handleResetSettingsClick(mouseX: Float, mouseY: Float, controlsY: Float, buttonHeight: Float, target: HudElement): Boolean { + val resetSettingsText = "Reset Settings" + val resetSettingsWidth = NVGRenderer.textWidth(resetSettingsText, 13f) + 30f + val resetSettingsX = panelX + panelWidth - padding - resetSettingsWidth - for (component in settingComponents) { - if (component.mouseClicked(button)) return true + if (mouseX >= resetSettingsX && mouseX <= resetSettingsX + resetSettingsWidth && + mouseY >= controlsY && mouseY <= controlsY + buttonHeight + ) { + target.resetSettings() + buttonAnim.start() + return true } + return false + } - return containsPoint(mouseX, mouseY) + private fun handleResetPositionClick(mouseX: Float, mouseY: Float, controlsY: Float, buttonHeight: Float, target: HudElement): Boolean { + val resetSettingsText = "Reset Settings" + val resetSettingsWidth = NVGRenderer.textWidth(resetSettingsText, 13f) + 30f + val resetSettingsX = panelX + panelWidth - padding - resetSettingsWidth + val resetText = "Reset Position" + val resetWidth = NVGRenderer.textWidth(resetText, 13f) + 30f + val resetX = resetSettingsX - resetWidth - 10f + + if (mouseX >= resetX && mouseX <= resetX + resetWidth && + mouseY >= controlsY && mouseY <= controlsY + buttonHeight + ) { + target.resetPosition() + buttonAnim.start() + return true + } + return false } fun mouseReleased(button: Int): Boolean { diff --git a/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt b/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt index 4c8f2d2..0acb38e 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/panel/panels/UISidebar.kt @@ -55,7 +55,7 @@ internal class UISidebar : UIPanel( .render() hudButton - .setSelected(if (isHoveringOver(x + (width / 2F) - (hudButton.width / 2F), y + 75F + 35F, hudButton.width, hudButton.height)) true else false) + .setSelected(isHoveringOver(x + (width / 2F) - (hudButton.width / 2F), y + 75F + 35F, hudButton.width, hudButton.height)) .updateBounds(x + (width / 2F) - (hudButton.width / 2F), y + 75F + 35F) .render() diff --git a/src/main/kotlin/org/cobalt/internal/ui/screen/UIHudEditor.kt b/src/main/kotlin/org/cobalt/internal/ui/screen/UIHudEditor.kt index 535fc03..36ba13e 100644 --- a/src/main/kotlin/org/cobalt/internal/ui/screen/UIHudEditor.kt +++ b/src/main/kotlin/org/cobalt/internal/ui/screen/UIHudEditor.kt @@ -126,52 +126,68 @@ internal class UIHudEditor : UIScreen() { val my = mouseY.toFloat() val button = click.button() - if (settingsPopup.visible) { - if (settingsPopup.mouseClicked(mx, my, button)) return true - if (!settingsPopup.containsPoint(mx, my)) settingsPopup.hide() - } + if (handleSettingsPopupClick(mx, my, button)) return true - if (button == 1) { - val target = findElementUnderCursor(mx, my, screenWidth, screenHeight) - if (target != null) { - selectedElement = target - settingsPopup.show(target, screenWidth, screenHeight) - return true - } - } + if (button == 1 && handleRightClick(mx, my, screenWidth, screenHeight)) return true - if (button == 0) { - if (selectedElement != null) { - val element = selectedElement!! - val (sx, sy) = element.getScreenPosition(screenWidth, screenHeight) - val w = element.getScaledWidth() - val h = element.getScaledHeight() - val handleX = sx + w - RESIZE_HANDLE_SIZE - val handleY = sy + h - RESIZE_HANDLE_SIZE - - if (mx >= handleX && mx <= handleX + RESIZE_HANDLE_SIZE && - my >= handleY && my <= handleY + RESIZE_HANDLE_SIZE) { - resizing = true - initialMouseX = mx - initialMouseY = my - initialWidth = w - return true - } - } - - val target = findElementUnderCursor(mx, my, screenWidth, screenHeight) + if (button == 0 && handleLeftClick(mx, my, screenWidth, screenHeight)) return true + + return super.mouseClicked(click, doubled) + } + + private fun handleSettingsPopupClick(mx: Float, my: Float, button: Int): Boolean { + if (!settingsPopup.visible) return false + if (settingsPopup.mouseClicked(mx, my, button)) return true + if (!settingsPopup.containsPoint(mx, my)) settingsPopup.hide() + return false + } + + private fun handleRightClick(mx: Float, my: Float, screenWidth: Float, screenHeight: Float): Boolean { + val target = findElementUnderCursor(mx, my, screenWidth, screenHeight) + if (target != null) { selectedElement = target - if (target != null) { - val (sx, sy) = target.getScreenPosition(screenWidth, screenHeight) - dragOffsetX = mx - sx - dragOffsetY = my - sy - dragging = true - settingsPopup.hide() - return true - } + settingsPopup.show(target, screenWidth, screenHeight) + return true } + return false + } - return super.mouseClicked(click, doubled) + private fun handleLeftClick(mx: Float, my: Float, screenWidth: Float, screenHeight: Float): Boolean { + if (tryStartResizing(mx, my, screenWidth, screenHeight)) return true + return tryStartDragging(mx, my, screenWidth, screenHeight) + } + + private fun tryStartResizing(mx: Float, my: Float, screenWidth: Float, screenHeight: Float): Boolean { + val element = selectedElement ?: return false + val (sx, sy) = element.getScreenPosition(screenWidth, screenHeight) + val w = element.getScaledWidth() + val h = element.getScaledHeight() + val handleX = sx + w - RESIZE_HANDLE_SIZE + val handleY = sy + h - RESIZE_HANDLE_SIZE + + if (mx >= handleX && mx <= handleX + RESIZE_HANDLE_SIZE && + my >= handleY && my <= handleY + RESIZE_HANDLE_SIZE) { + resizing = true + initialMouseX = mx + initialMouseY = my + initialWidth = w + return true + } + return false + } + + private fun tryStartDragging(mx: Float, my: Float, screenWidth: Float, screenHeight: Float): Boolean { + val target = findElementUnderCursor(mx, my, screenWidth, screenHeight) + selectedElement = target + if (target != null) { + val (sx, sy) = target.getScreenPosition(screenWidth, screenHeight) + dragOffsetX = mx - sx + dragOffsetY = my - sy + dragging = true + settingsPopup.hide() + return true + } + return false } override fun mouseReleased(click: MouseButtonEvent): Boolean {