From d9f5cef02bba52bf2835a0059a0069b869817298 Mon Sep 17 00:00:00 2001 From: Tellinq <59412384+Tellinq@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:23:41 -0500 Subject: [PATCH] feat(hud): add a Scale setting synced with the editor's corner handles --- CHANGELOG.md | 1 + modules/hud/api/hud.api | 3 + .../org/polyfrost/oneconfig/api/hud/v1/Hud.kt | 44 +++++++++++- .../oneconfig/api/hud/v1/HudScaleTest.java | 53 ++++++++++++++ .../ui/hud/screens/HudDesignStudio.kt | 69 ++++++------------- .../ui/hud/screens/sections/Designer.kt | 5 +- .../ui/hud/screens/sections/Settings.kt | 38 ++++++---- 7 files changed, 148 insertions(+), 65 deletions(-) create mode 100644 modules/hud/src/test/java/org/polyfrost/oneconfig/api/hud/v1/HudScaleTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 22954016f..2c665c5a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ ## Unreleased changes +- Added a Scale setting to HUDs that stays in sync with the HUD editor's corner handles - Dropped compatibility with Hypixel SkyBlock mods (Apec, Firmament, Odin, Skyblocker, SkyHanni and other MoulConfig-based mods) on Minecraft versions below 26.1, which can no longer be used to play SkyBlock diff --git a/modules/hud/api/hud.api b/modules/hud/api/hud.api index 3158c0268..71f5728a2 100644 --- a/modules/hud/api/hud.api +++ b/modules/hud/api/hud.api @@ -74,6 +74,8 @@ public abstract class org/polyfrost/oneconfig/api/hud/v1/Hud : org/polyfrost/one public final fun getFullShadow ()Z public final fun getGrowthAnchor ()Lorg/polyfrost/oneconfig/api/hud/v1/HudAnchor; public fun getHidden ()Z + public final fun getHudScale ()F + public final fun getHudScaleRange ()Lkotlin/ranges/ClosedFloatingPointRange; public final fun getKeepBgWhenHidden ()Z public fun getLocked ()Z public final fun getMergeBackground ()Z @@ -158,6 +160,7 @@ public abstract class org/polyfrost/oneconfig/api/hud/v1/Hud : org/polyfrost/one public final fun setGrowthAnchor (Lorg/polyfrost/oneconfig/api/hud/v1/HudAnchor;)V public final fun setGrowthAnchorKeepingPosition (Lorg/polyfrost/oneconfig/api/hud/v1/HudAnchor;)V public fun setHidden (Z)V + public final fun setHudScale (F)V public final fun setKeepBgWhenHidden (Z)V public final fun setLayoutRefH (F)V public final fun setLayoutRefW (F)V diff --git a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/Hud.kt b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/Hud.kt index 22317d628..e84f15273 100644 --- a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/Hud.kt +++ b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/Hud.kt @@ -120,6 +120,11 @@ private const val MIN_VISIBLE = 12f internal const val POS_SCHEMA = 1 +private const val MIN_TEXT_SCALE = 6f / 14f +private const val MAX_TEXT_SCALE = 64f / 14f +private const val MIN_LEGACY_SCALE = 0.25f +private const val MAX_LEGACY_SCALE = 4f + // every open getter the cached frame geometry or the background merge key reads // a subclass overriding one of these is read live, so a new open geometry property must be added here // or an override of it would be served a stale cached value @@ -980,6 +985,35 @@ abstract class Hud(id: String, title: String, val category: Category) : Cloneabl private var _customScale: MutableState = mutableStateOf(1f) open var customScale: Float get() = _customScale.value; set(v) { _customScale.put(v) } + /** The values [hudScale] can take */ + @get:ApiStatus.Experimental + val hudScaleRange: ClosedFloatingPointRange + get() = if (this is LegacyHudMarker) MIN_LEGACY_SCALE..MAX_LEGACY_SCALE else MIN_TEXT_SCALE..MAX_TEXT_SCALE + + /** + * The size of this HUD as set by the editor's corner handles and the Scale setting + * + * That is [textScale], or [customScale] for a legacy HUD which draws its own content + * + * Setting it keeps a static size in proportion, which setting [textScale] directly does not + */ + @get:ApiStatus.Experimental + var hudScale: Float + get() = if (this is LegacyHudMarker) customScale else textScale + set(v) { + val next = v.coerceIn(hudScaleRange) + if (this is LegacyHudMarker) { + customScale = next + return + } + val previous = textScale + textScale = next + if (staticWidth && previous > 0f && staticW > 0f && staticH > 0f) { + staticW *= next / previous + staticH *= next / previous + } + } + val effectiveScale: Float get() { if (useGuiScale) return customScale @@ -1424,7 +1458,10 @@ abstract class Hud(id: String, title: String, val category: Category) : Cloneabl } tree["font"] = ktProperty(out::font).apply { addDisplayCondition(hideFromConfigUi) } tree["caseType"] = ktProperty(out::caseType).apply { addDisplayCondition(hideFromConfigUi) } - tree["textScale"] = ktProperty(out::textScale).apply { addDisplayCondition(hideFromConfigUi) } + tree["textScale"] = ktProperty(out::textScale).apply { + description = "The size of this HUD. The corner handles in the HUD editor change the same value." + addDisplayCondition(hideFromConfigUi) + } tree["textBold"] = ktProperty(out::textBold).apply { addDisplayCondition(hideFromConfigUi) } tree["textItalic"] = ktProperty(out::textItalic).apply { addDisplayCondition(hideFromConfigUi) } tree["textUnderline"] = ktProperty(out::textUnderline).apply { addDisplayCondition(hideFromConfigUi) } @@ -1435,7 +1472,10 @@ abstract class Hud(id: String, title: String, val category: Category) : Cloneabl addDisplayCondition(hideFromConfigUi) } if (out.persistOwnState) { - tree["customScale"] = ktProperty(out::customScale).apply { addDisplayCondition(hideFromConfigUi) } + tree["customScale"] = ktProperty(out::customScale).apply { + description = "Multiplies the whole HUD on top of its Scale, including its padding and background." + addDisplayCondition(hideFromConfigUi) + } } tree["showBackground"] = ktProperty(out::showBackground).apply { addDisplayCondition(hideFromConfigUi) } tree["bgColor"] = ktProperty(out::bgColor).apply { addDisplayCondition(hideFromConfigUi) } diff --git a/modules/hud/src/test/java/org/polyfrost/oneconfig/api/hud/v1/HudScaleTest.java b/modules/hud/src/test/java/org/polyfrost/oneconfig/api/hud/v1/HudScaleTest.java new file mode 100644 index 000000000..ae7ec3d4c --- /dev/null +++ b/modules/hud/src/test/java/org/polyfrost/oneconfig/api/hud/v1/HudScaleTest.java @@ -0,0 +1,53 @@ +package org.polyfrost.oneconfig.api.hud.v1; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class HudScaleTest { + static class PlainHud extends TextHud { + PlainHud() { super("scale-plain", "Plain", Hud.Category.getINFO(), "", ""); } + @Override public String getText() { return "plain"; } + } + + static class LegacyHud extends PlainHud implements LegacyHudMarker { + @Override public boolean getSupportsScale() { return true; } + } + + @Test + void scaleIsTheValueTheCornerHandlesDrag() { + Hud hud = new PlainHud(); + hud.setHudScale(2f); + assertEquals(2f, hud.getTextScale(), 0.001f); + assertEquals(2f, hud.getHudScale(), 0.001f); + assertEquals(1f, hud.getCustomScale(), 0.001f, "the scale stays separate from Custom Scale"); + } + + @Test + void aStaticBoxStaysInProportion() { + Hud hud = new PlainHud(); + hud.setStaticWidth(true); + hud.setStaticW(100f); + hud.setStaticH(20f); + hud.setHudScale(1.5f); + assertEquals(150f, hud.getStaticW(), 0.001f); + assertEquals(30f, hud.getStaticH(), 0.001f); + } + + @Test + void scaleStaysInRange() { + Hud hud = new PlainHud(); + hud.setHudScale(100f); + assertEquals(hud.getHudScaleRange().getEndInclusive(), hud.getHudScale(), 0.001f); + hud.setHudScale(-1f); + assertEquals(hud.getHudScaleRange().getStart(), hud.getHudScale(), 0.001f); + } + + @Test + void legacyHudsScaleThroughCustomScale() { + Hud hud = new LegacyHud(); + hud.setHudScale(2f); + assertEquals(2f, hud.getCustomScale(), 0.001f); + assertEquals(1f, hud.getTextScale(), 0.001f); + } +} diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudDesignStudio.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudDesignStudio.kt index d88a4f55f..2ea8ea787 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudDesignStudio.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudDesignStudio.kt @@ -698,14 +698,28 @@ private fun hitTestResizeHandle(hud: Hud, screenX: Float, screenY: Float, mcToSc } } +/** + * How far a resize from [corner] has scaled a box that started as [start] with the cursor at + * ([mouseX], [mouseY]) + * + * The cursor is projected onto the diagonal from the fixed corner to the dragged one because a + * per-axis factor turns small moves on the short axis into huge jumps + */ +private fun cornerScaleFactor(corner: ResizeCorner, start: HudBounds, mouseX: Float, mouseY: Float): Float { + val anchorX = if (corner.isLeft) start.x + start.width else start.x + val anchorY = if (corner.isTop) start.y + start.height else start.y + val cornerVecX = if (corner.isLeft) -start.width else start.width + val cornerVecY = if (corner.isTop) -start.height else start.height + val diagLenSq = cornerVecX * cornerVecX + cornerVecY * cornerVecY + if (diagLenSq <= 0f) return 1f + return ((mouseX - anchorX) * cornerVecX + (mouseY - anchorY) * cornerVecY) / diagLenSq +} + private fun resizeHud( hud: Hud, corner: ResizeCorner, startBounds: HudBounds, - startTextScale: Float, startScale: Float, - startStaticW: Float, - startStaticH: Float, mouseX: Float, mouseY: Float, freeResize: Boolean, @@ -720,22 +734,7 @@ private fun resizeHud( return } - if (hud is LegacyHud) { - val cornerVecX = if (corner.isLeft) -startBounds.width else startBounds.width - val cornerVecY = if (corner.isTop) -startBounds.height else startBounds.height - val mouseVecX = mouseX - anchorX - val mouseVecY = mouseY - anchorY - val diagLenSq = cornerVecX * cornerVecX + cornerVecY * cornerVecY - val rawFactor = if (diagLenSq > 0f) { - (mouseVecX * cornerVecX + mouseVecY * cornerVecY) / diagLenSq - } else 1f - val baseScale = startScale.coerceAtLeast(0.001f) - val newScale = (baseScale * rawFactor).coerceIn(0.25f, 4f) - hud.customScale = newScale - return - } - - if (freeResize) { + if (freeResize && hud !is LegacyHud) { val effectiveScale = hud.effectiveScale.coerceAtLeast(0.001f) val (minStaticW, minStaticH) = hud.minimumSize() val newWidth = targetWidth.coerceAtLeast(minStaticW * effectiveScale) @@ -747,26 +746,7 @@ private fun resizeHud( return } - // scale proportionally by projecting the mouse onto the diagonal from the anchor to the dragged - // corner because a per-axis max-deviation factor turns small moves on the short axis into huge jumps - val cornerVecX = if (corner.isLeft) -startBounds.width else startBounds.width - val cornerVecY = if (corner.isTop) -startBounds.height else startBounds.height - val mouseVecX = mouseX - anchorX - val mouseVecY = mouseY - anchorY - val diagLenSq = cornerVecX * cornerVecX + cornerVecY * cornerVecY - val rawFactor = if (diagLenSq > 0f) { - (mouseVecX * cornerVecX + mouseVecY * cornerVecY) / diagLenSq - } else 1f - val minTextScale = 6f / 14f - val maxTextScale = 64f / 14f - val newTextScale = (startTextScale * rawFactor).coerceIn(minTextScale, maxTextScale) - val factor = if (startTextScale > 0f) newTextScale / startTextScale else 1f - - hud.textScale = newTextScale - if (hud.staticWidth && startStaticW > 0f && startStaticH > 0f) { - hud.staticW = startStaticW * factor - hud.staticH = startStaticH * factor - } + hud.hudScale = startScale * cornerScaleFactor(corner, startBounds, mouseX, mouseY) } private fun DrawScope.drawSelectedHudBounds( @@ -1081,10 +1061,7 @@ fun HudDesignStudio(visible: Boolean = true) { var resizedHud by remember { mutableStateOf(null) } var resizeCorner by remember { mutableStateOf(null) } var resizeStartBounds by remember { mutableStateOf(null) } - var resizeStartTextScale by remember { mutableStateOf(1f) } var resizeStartScale by remember { mutableStateOf(1f) } - var resizeStartStaticW by remember { mutableStateOf(0f) } - var resizeStartStaticH by remember { mutableStateOf(0f) } var libraryVisible by remember { mutableStateOf(true) } var searchText by remember { mutableStateOf("") } var searchFocusTarget by remember { mutableStateOf(null) } @@ -1603,10 +1580,7 @@ fun HudDesignStudio(visible: Boolean = true) { resizedHud = selected resizeCorner = handle resizeStartBounds = bounds - resizeStartTextScale = selected.textScale - resizeStartScale = selected.customScale - resizeStartStaticW = selected.staticW - resizeStartStaticH = selected.staticH + resizeStartScale = selected.hudScale hoveredHud = selected closeLibrary() pasteMenuOffset = null @@ -1728,10 +1702,7 @@ fun HudDesignStudio(visible: Boolean = true) { hud = hit, corner = corner, startBounds = bounds, - startTextScale = resizeStartTextScale, startScale = resizeStartScale, - startStaticW = resizeStartStaticW, - startStaticH = resizeStartStaticH, mouseX = pos.x * s, mouseY = pos.y * s, freeResize = event.keyboardModifiers.isShiftPressed, diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Designer.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Designer.kt index f98e6b9ca..aaa4be5b6 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Designer.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Designer.kt @@ -99,7 +99,8 @@ fun Designer(hud: Hud?, query: String) { var padTop by remember { mutableStateOf(hud.padTop) } var padBottom by remember { mutableStateOf(hud.padBottom) } var font by remember { mutableStateOf(hud.font) } - var textScale by remember { mutableStateOf(hud.textScale) } + // read straight from the HUD so dragging a corner handle updates the panel while it is open + val textScale = hud.textScale var textBold by remember { mutableStateOf(hud.textBold) } var textItalic by remember { mutableStateOf(hud.textItalic) } var textUnderline by remember { mutableStateOf(hud.textUnderline) } @@ -295,7 +296,7 @@ fun Designer(hud: Hud?, query: String) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Text("Font size", color = LocalTheme.current.textColor, fontSize = 14.sp) NumberSpinner( - textScale * 14f, { Snapshot.withMutableSnapshot { val s = it / 14f; textScale = s; hud.textScale = s } }, + textScale * 14f, { Snapshot.withMutableSnapshot { hud.hudScale = it / 14f } }, 6f, 64f, 1f, width = 112.dp, unit = "px", icon = "align" ) } diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Settings.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Settings.kt index 47309ef2d..eb2029cea 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Settings.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Settings.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.polyfrost.oneconfig.api.hud.v1.Hud import org.polyfrost.oneconfig.api.hud.v1.HudManager +import org.polyfrost.oneconfig.api.hud.v1.HudResize import org.polyfrost.oneconfig.api.hud.v1.LegacyHudMarker import org.polyfrost.oneconfig.internal.ui.components.Icon import org.polyfrost.oneconfig.internal.ui.components.Text @@ -46,6 +47,25 @@ import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme private val DeleteButtonColor = Color(0xFFE5484D) +/** [Hud.hudScale], read straight from the HUD so dragging a corner handle updates it while the panel is open */ +@Composable +private fun HudScaleSpinner(hud: Hud, optionId: String) { + val range = hud.hudScaleRange + HudSettingTarget(hud, optionId) { + NumberSpinner( + hud.hudScale, + { Snapshot.withMutableSnapshot { hud.hudScale = it } }, + range.start, + range.endInclusive, + 0.25f, + width = 176.dp, + format = { "%.2f".format(it) }, + unit = "x", + label = "Scale", + ) + } +} + @Composable fun Settings(hud: Hud?, query: String, onDeleted: () -> Unit) { if (hud == null) return @@ -106,21 +126,15 @@ fun Settings(hud: Hud?, query: String, onDeleted: () -> Unit) { if (hud is LegacyHudMarker) { if (hud.supportsScale) { Section("Scale", query) { - HudSettingTarget(hud, "customScale") { - NumberSpinner( - customScale, - { Snapshot.withMutableSnapshot { customScale = it; hud.customScale = it } }, - 0.25f, - 4f, - 0.25f, - width = 176.dp, - unit = "x", - label = "Scale", - ) - } + HudScaleSpinner(hud, "customScale") } } } else { + if (hud.resizeAxes == HudResize.Both) { + Section("Scale", query) { + HudScaleSpinner(hud, "textScale") + } + } Section("GUI Scale", query, "Use Minecraft GUI Scale", "Custom Scale") { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { HudSettingTarget(hud, "useGuiScale") {