From a96f686993684f6b82b6e12b3e9ee9099ca89070 Mon Sep 17 00:00:00 2001 From: Tellinq <59412384+Tellinq@users.noreply.github.com> Date: Sun, 8 Nov 2026 13:01:13 -0500 Subject: [PATCH] Separate Static width and height and allow min/max to be set instead --- modules/hud/api/hud.api | 18 ++ .../org/polyfrost/oneconfig/api/hud/v1/Hud.kt | 302 ++++++++++++++---- .../oneconfig/api/hud/v1/HudManager.kt | 13 +- .../polyfrost/oneconfig/api/hud/v1/TextHud.kt | 15 +- .../internal/ui/hud/HudSettingsReset.kt | 45 +-- .../internal/ui/hud/components/HudPreview.kt | 10 +- .../ui/hud/screens/HudDesignStudio.kt | 65 ++-- .../ui/hud/screens/sections/Layout.kt | 165 ++++++---- .../internal/ui/hud/screens/sections/Style.kt | 3 - modules/poly-compose/api/poly-compose.api | 17 + .../compose/composables/PolyModifier.kt | 8 + .../compose/layout/PolyLayoutEngine.kt | 32 +- .../compose/layout/PolyLayoutStyle.kt | 5 + .../org/polyfrost/compose/node/PolyNode.kt | 11 + 14 files changed, 478 insertions(+), 231 deletions(-) diff --git a/modules/hud/api/hud.api b/modules/hud/api/hud.api index 1d45c059d..f3e9f5ceb 100644 --- a/modules/hud/api/hud.api +++ b/modules/hud/api/hud.api @@ -45,8 +45,12 @@ public abstract class org/polyfrost/oneconfig/api/hud/v1/Hud : org/polyfrost/one public synthetic fun clone ()Ljava/lang/Object; public fun clone ()Lorg/polyfrost/oneconfig/api/hud/v1/Hud; protected final fun contentChanged ()V + public final fun currentHeight ()F + public final fun currentWidth ()F public fun defaultPosition ()Lkotlin/Pair; public fun deletable ()Z + public final fun fixHeight (Z)V + public final fun fixWidth (Z)V public final fun getAlignment ()Lorg/polyfrost/compose/layout/PolyAlign; public fun getAlwaysRedraw ()Z public final fun getAnchorOffsetX ()F @@ -70,14 +74,20 @@ public abstract class org/polyfrost/oneconfig/api/hud/v1/Hud : org/polyfrost/one public final fun getEffectiveScale ()F public final fun getEffectiveSelfAnchorPoint ()Lorg/polyfrost/oneconfig/api/hud/v1/HudAnchor; public fun getExternalPlacement ()Lorg/polyfrost/oneconfig/api/hud/v1/ExternalPlacement; + public final fun getFitContent ()Z public final fun getFont ()Lorg/polyfrost/oneconfig/api/hud/v1/Font; public final fun getFullShadow ()Z public final fun getGrowthAnchor ()Lorg/polyfrost/oneconfig/api/hud/v1/HudAnchor; + public final fun getHasSizeLimits ()Z public fun getHidden ()Z public final fun getKeepBgWhenHidden ()Z public fun getLocked ()Z + public final fun getMaxH ()F + public final fun getMaxW ()F public final fun getMergeBackground ()Z public final fun getMergeDiagonally ()Z + public final fun getMinH ()F + public final fun getMinW ()F public final fun getPadBottom ()F public final fun getPadLeft ()F public final fun getPadRight ()F @@ -132,6 +142,8 @@ public abstract class org/polyfrost/oneconfig/api/hud/v1/Hud : org/polyfrost/one public final fun hudBackground (Lorg/polyfrost/compose/composables/PolyModifier;)Lorg/polyfrost/compose/composables/PolyModifier; public static synthetic fun hudBackground$default (Lorg/polyfrost/oneconfig/api/hud/v1/Hud;Lorg/polyfrost/compose/composables/PolyModifier;ILjava/lang/Object;)Lorg/polyfrost/compose/composables/PolyModifier; public final fun isAnchored ()Z + public final fun isFixedH ()Z + public final fun isFixedW ()Z public final fun isMergeAnchored ()Z public final fun isReal ()Z protected final fun measureNaturalContentSize ()Lkotlin/Pair; @@ -156,6 +168,7 @@ public abstract class org/polyfrost/oneconfig/api/hud/v1/Hud : org/polyfrost/one public final fun setCaseType (I)V public final fun setConfigId (Ljava/lang/String;)V public fun setCustomScale (F)V + public final fun setFitContent (Z)V public final fun setFont (Lorg/polyfrost/oneconfig/api/hud/v1/Font;)V public final fun setFullShadow (Z)V public final fun setGrowthAnchor (Lorg/polyfrost/oneconfig/api/hud/v1/HudAnchor;)V @@ -165,8 +178,12 @@ public abstract class org/polyfrost/oneconfig/api/hud/v1/Hud : org/polyfrost/one public final fun setLayoutRefH (F)V public final fun setLayoutRefW (F)V public fun setLocked (Z)V + public final fun setMaxH (F)V + public final fun setMaxW (F)V public final fun setMergeBackground (Z)V public final fun setMergeDiagonally (Z)V + public final fun setMinH (F)V + public final fun setMinW (F)V public final fun setPadBottom (F)V public final fun setPadLeft (F)V public final fun setPadRight (F)V @@ -209,6 +226,7 @@ public abstract class org/polyfrost/oneconfig/api/hud/v1/Hud : org/polyfrost/one public fun setup ()V public fun shouldShow ()Z public fun showByDefault ()Z + public final fun sizeConstraints (Lorg/polyfrost/compose/composables/PolyModifier;)Lorg/polyfrost/compose/composables/PolyModifier; public abstract fun update ()Z public final fun updateAndRecalculate ()V public fun updateFrequency ()J 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 4ba9e2307..af21ee8c3 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 @@ -39,8 +39,14 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshots.Snapshot import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.MustBeInvokedByOverriders +import org.polyfrost.compose.composables.PolyBox import org.polyfrost.compose.composables.PolyModifier import org.polyfrost.compose.composables.background +import org.polyfrost.compose.composables.fitContent +import org.polyfrost.compose.composables.height +import org.polyfrost.compose.composables.heightIn +import org.polyfrost.compose.composables.width +import org.polyfrost.compose.composables.widthIn import org.polyfrost.compose.layout.PolyAlign import org.polyfrost.compose.render.PolyColor import org.polyfrost.compose.runtime.PolyComposeRuntime @@ -180,40 +186,155 @@ public abstract class Hud( @get:ApiStatus.Experimental public open val externalPlacement: ExternalPlacement? get() = null - private var _staticWidth: MutableState = mutableStateOf(false) + private var _minW: MutableState = mutableStateOf(0f) + /** The narrowest this HUD may get, where 0 means no minimum */ + public var minW: Float get() = _minW.value; set(v) { _minW.put(v); if (maxW > 0f && maxW < v) _maxW.put(v) } + + private var _maxW: MutableState = mutableStateOf(0f) + /** The widest this HUD may get, where 0 means no maximum */ + public var maxW: Float get() = _maxW.value; set(v) { _maxW.put(if (v > 0f && minW > 0f) maxOf(v, minW) else v) } + + private var _minH: MutableState = mutableStateOf(0f) + /** The shortest this HUD may get, where 0 means no minimum */ + public var minH: Float get() = _minH.value; set(v) { _minH.put(v); if (maxH > 0f && maxH < v) _maxH.put(v) } + + private var _maxH: MutableState = mutableStateOf(0f) + /** The tallest this HUD may get, where 0 means no maximum */ + public var maxH: Float get() = _maxH.value; set(v) { _maxH.put(if (v > 0f && minH > 0f) maxOf(v, minH) else v) } + + private var _fitContent: MutableState = mutableStateOf(false) + public var fitContent: Boolean get() = _fitContent.value; set(v) { _fitContent.put(v) } + + private var _measuringNatural: MutableState = mutableStateOf(false) + + // a legacy fixed size requested before the content could be measured, applied by the first render + private var pendingFixW = false + private var pendingFixH = false + + public val isFixedW: Boolean get() = minW > 0f && minW == maxW + + public val isFixedH: Boolean get() = minH > 0f && minH == maxH + + public val hasSizeLimits: Boolean get() = minW > 0f || maxW > 0f || minH > 0f || maxH > 0f + + /** Both axes pinned to one size, kept for HUDs written before sizes had a minimum and maximum */ + @Deprecated("Limit the axes independently with minW, maxW, minH and maxH") public var staticWidth: Boolean - get() = _staticWidth.value + get() = isFixedW && isFixedH set(v) { - val wasStatic = _staticWidth.value - _staticWidth.put(v) - if (!wasStatic && v) { - val (minW, minH) = minimumSize() - if (renderedW > 0f || renderedH > 0f) { - val scale = effectiveScale - staticW = maxOf(renderedW / scale, minW) - staticH = maxOf(renderedH / scale, minH) - } else { - val natural = measureNaturalContentSize() - if (natural != null) { - staticW = maxOf(natural.first, minW) - staticH = maxOf(natural.second, minH) - } - } - } else if (wasStatic && !v) { - // seed rendered dimensions from the static values so the HUD does not - // collapse to 0x0 until the render loop recalculates the natural size - val (minW, minH) = minimumSize() - val scale = effectiveScale - renderedW = maxOf(staticW, minW) * scale - renderedH = maxOf(staticH, minH) * scale - } + fixWidth(v) + fixHeight(v) + } + + /** + * Pins the width at its current size, or removes every width limit + * + * When the size is not known yet because nothing has rendered or been measurable, it is pinned + * once the first render reports it + */ + public fun fixWidth(fixed: Boolean) { + pendingFixW = false + if (!fixed) { + _minW.put(0f); _maxW.put(0f) + seedRenderedWidth() + return } + val size = currentWidth().takeIf { it > 0f } ?: staticW.takeIf { it > 0f } + if (size != null) pinWidth(size) else pendingFixW = true + } + + /** Pins the height at its current size, or removes every height limit, see [fixWidth] */ + public fun fixHeight(fixed: Boolean) { + pendingFixH = false + if (!fixed) { + _minH.put(0f); _maxH.put(0f) + seedRenderedHeight() + return + } + val size = currentHeight().takeIf { it > 0f } ?: staticH.takeIf { it > 0f } + if (size != null) pinHeight(size) else pendingFixH = true + } + + private fun pinWidth(size: Float) { + _minW.put(size); _maxW.put(size); _staticW.put(size) + } + + private fun pinHeight(size: Float) { + _minH.put(size); _maxH.put(size); _staticH.put(size) + } + + /** The unscaled width right now, measuring the content if it has not rendered yet */ + public fun currentWidth(): Float { + val floor = minimumSize().first + if (renderedW > 0f) return maxOf(renderedW / effectiveScale, floor) + return measureNaturalContentSize()?.let { maxOf(it.first, floor) } ?: 0f + } + + /** The unscaled height right now, measuring the content if it has not rendered yet */ + public fun currentHeight(): Float { + val floor = minimumSize().second + if (renderedH > 0f) return maxOf(renderedH / effectiveScale, floor) + return measureNaturalContentSize()?.let { maxOf(it.second, floor) } ?: 0f + } + + // keeps the HUD from collapsing to 0x0 until the render loop recalculates the natural size + private fun seedRenderedWidth() { + if (renderedW <= 0f) renderedW = maxOf(staticW, minimumSize().first) * effectiveScale + } + + private fun seedRenderedHeight() { + if (renderedH <= 0f) renderedH = maxOf(staticH, minimumSize().second) * effectiveScale + } + + /** + * Applies this HUD's width and height limits and [fitContent] to [modifier] + * + * Limits cover the whole box including padding, so apply it before the padding + * + * Every HUD is limited at its root already, so this is only needed to make an inner box fill the limits + * which [hudBackground] already does + * + * Content measured for its natural size is never limited + */ + public fun sizeConstraints(modifier: PolyModifier): PolyModifier { + if (_measuringNatural.value) return modifier + var constrained = modifier + if (minW > 0f || maxW > 0f) constrained = constrained.widthIn(maxOf(minW, minimumSize().first), maxW.takeIf { it > 0f } ?: Float.NaN) + if (minH > 0f || maxH > 0f) constrained = constrained.heightIn(maxOf(minH, minimumSize().second), maxH.takeIf { it > 0f } ?: Float.NaN) + return if (fitContent && hasSizeLimits) constrained.fitContent() else constrained + } + + /** + * Size of this HUD's box at [scale], or null while it has no usable size + * + * A fixed axis uses its limit raised to the same [frameMinimumSize] floor the rendered box obeys + */ + @ApiStatus.Internal + public fun boundsSize(scale: Float): Pair? { + val (floorW, floorH) = frameMinimumSize() + val fallbackW = (if (isFixedW) minW else staticW).takeIf { it > 0f }?.let { maxOf(it, floorW) * scale } + val fallbackH = (if (isFixedH) minH else staticH).takeIf { it > 0f }?.let { maxOf(it, floorH) * scale } + val w = if (isFixedW) fallbackW else renderedW.takeIf { it > 0f } ?: fallbackW + val h = if (isFixedH) fallbackH else renderedH.takeIf { it > 0f } ?: fallbackH + return if (w != null && h != null) w to h else null + } + + /** + * Turns the fixed size of a HUD saved before sizes had a minimum and maximum into limits + * + * [data] is the stored tree, which only has `staticWidth` for such a HUD + */ + internal fun migrateLegacySize(data: Tree) { + if (data.getProp("minW") != null || data.getProp("minH") != null) return + if (data.getProp("staticWidth")?.getAs() != true) return + if (staticW > 0f) pinWidth(staticW) else pendingFixW = true + if (staticH > 0f) pinHeight(staticH) else pendingFixH = true + } protected fun measureNaturalContentSize(): Pair? { val rt = _runtime ?: return null - val wasStatic = _staticWidth.value return try { - _staticWidth.value = false + _measuringNatural.value = true rt.frame(2000f, 2000f) val w = rt.root.width val h = rt.root.height @@ -221,45 +342,43 @@ public abstract class Hud( } catch (_: Throwable) { null } finally { - _staticWidth.value = wasStatic + _measuringNatural.value = false } } /** - * Fills in [staticW]/[staticH] when static sizing is enabled but dimensions are unset or invalid + * Fills in [staticW]/[staticH] when their axis is fixed but the dimension is unset or invalid * * One such invalid value is [TextHud]'s unmeasured sentinel of {@code -1f} * * Safe to call after a reset-to-default */ public fun reseedStaticSizeIfNeeded() { - if (!staticWidth) return - if (staticW > 0f && staticH > 0f) return - reseedStaticWidth() - reseedStaticHeight() + if (isFixedW && staticW <= 0f) reseedStaticWidth() + if (isFixedH && staticH <= 0f) reseedStaticHeight() } /** Re-measures content and applies a new static width */ public fun reseedStaticWidth() { - if (!staticWidth) return - val (minW, _) = minimumSize() + if (!isFixedW) return + val floor = minimumSize().first val natural = measureNaturalContentSize() staticW = if (natural != null && natural.first > 0f) { - maxOf(natural.first, minW) + maxOf(natural.first, floor) } else { - maxOf(padLeft + padRight + 80f, minW) + maxOf(padLeft + padRight + 80f, floor) } } /** Re-measures content and applies a new static height */ public fun reseedStaticHeight() { - if (!staticWidth) return - val (_, minH) = minimumSize() + if (!isFixedH) return + val floor = minimumSize().second val natural = measureNaturalContentSize() staticH = if (natural != null && natural.second > 0f) { - maxOf(natural.second, minH) + maxOf(natural.second, floor) } else { - maxOf(padTop + padBottom + 16f, minH) + maxOf(padTop + padBottom + 16f, floor) } } @@ -276,16 +395,15 @@ public abstract class Hud( */ public fun captureStaticSizeDefaults(force: Boolean = false) { val t = tree ?: return - if (!staticWidth) return val wProp = t.getProp("staticW") val hProp = t.getProp("staticH") - val needW = wProp != null && (force || wProp.getMetadata("default") == null) - val needH = hProp != null && (force || hProp.getMetadata("default") == null) + val needW = isFixedW && wProp != null && (force || wProp.getMetadata("default") == null) + val needH = isFixedH && hProp != null && (force || hProp.getMetadata("default") == null) if (!needW && !needH) return val (naturalW, naturalH) = measureNaturalContentSize() ?: return - val (minW, minH) = minimumSize() - if (needW && naturalW > 0f) wProp.addMetadata("default", maxOf(naturalW, minW)) - if (needH && naturalH > 0f) hProp.addMetadata("default", maxOf(naturalH, minH)) + val (floorW, floorH) = minimumSize() + if (needW && naturalW > 0f) wProp.addMetadata("default", maxOf(naturalW, floorW)) + if (needH && naturalH > 0f) hProp.addMetadata("default", maxOf(naturalH, floorH)) } /** @@ -367,10 +485,16 @@ public abstract class Hud( set(v) { if (v > 0f) HudManager.layoutRefHeight = v } private var _renderedW: MutableState = mutableStateOf(0f) - public open var renderedW: Float get() = _renderedW.value; set(v) { _renderedW.put(v) } + public open var renderedW: Float get() = _renderedW.value; set(v) { + _renderedW.put(v) + if (pendingFixW && v > 0f) { pendingFixW = false; pinWidth(maxOf(v / effectiveScale, minimumSize().first)) } + } private var _renderedH: MutableState = mutableStateOf(0f) - public open var renderedH: Float get() = _renderedH.value; set(v) { _renderedH.put(v) } + public open var renderedH: Float get() = _renderedH.value; set(v) { + _renderedH.put(v) + if (pendingFixH && v > 0f) { pendingFixH = false; pinHeight(maxOf(v / effectiveScale, minimumSize().second)) } + } private var minSizeFrame = Long.MIN_VALUE private var minSize: Pair = 0f to 0f @@ -391,16 +515,23 @@ public abstract class Hud( public open val scaledWidth: Float get() { val scale = effectiveScale - val w = if (staticWidth || renderedW <= 0f) staticW * scale else renderedW - val (minW, _) = frameMinimumSize() - return maxOf(w, minW * scale).coerceAtLeast(1f) + val w = when { + isFixedW -> this.minW * scale + renderedW <= 0f -> staticW * scale + else -> renderedW + } + // the same floor the rendered box obeys, so a fixed limit below it does not shrink the bounds + return maxOf(w, frameMinimumSize().first * scale).coerceAtLeast(1f) } public open val scaledHeight: Float get() { val scale = effectiveScale - val h = if (staticWidth || renderedH <= 0f) staticH * scale else renderedH - val (_, minH) = frameMinimumSize() - return maxOf(h, minH * scale).coerceAtLeast(1f) + val h = when { + isFixedH -> this.minH * scale + renderedH <= 0f -> staticH * scale + else -> renderedH + } + return maxOf(h, frameMinimumSize().second * scale).coerceAtLeast(1f) } private fun fracX(anchor: HudAnchor): Float = when (anchor) { @@ -643,8 +774,8 @@ public abstract class Hud( @ApiStatus.Internal public open fun applyEditorWidth(width: Float) { - staticWidth = true - staticW = width + pendingFixW = false + pinWidth(width) } // compose state so a composable reading the pivot is invalidated when an editor resize pins or unpins @@ -1029,10 +1160,16 @@ public abstract class Hud( public var padRight: Float get() = _padRight.value; set(v) { _padRight.put(v) } private var _staticW: MutableState = mutableStateOf(200f) - public open var staticW: Float get() = _staticW.value; set(v) { _staticW.put(v) } + public open var staticW: Float get() = _staticW.value; set(v) { + _staticW.put(v) + if (isFixedW && v > 0f) { _minW.put(v); _maxW.put(v) } + } private var _staticH: MutableState = mutableStateOf(48f) - public open var staticH: Float get() = _staticH.value; set(v) { _staticH.put(v) } + public open var staticH: Float get() = _staticH.value; set(v) { + _staticH.put(v) + if (isFixedH && v > 0f) { _minH.put(v); _maxH.put(v) } + } private var _font: MutableState = mutableStateOf(Font.Minecraft) public var font: Font get() = _font.value; set(v) { _font.value = v } @@ -1459,7 +1596,10 @@ public abstract class Hud( _runtime = it it.setContent { CompositionLocalProvider(LocalHud provides this) { - Content() + // limits are enforced here so every HUD honours them, not only those that opt in + PolyBox(modifier = sizeConstraints(PolyModifier)) { + Content() + } } } } @@ -1506,13 +1646,29 @@ public abstract class Hud( tree["padBottom"] = ktProperty(out::padBottom).apply { addDisplayCondition(hideFromConfigUi) } tree["padLeft"] = ktProperty(out::padLeft).apply { addDisplayCondition(hideFromConfigUi) } tree["padRight"] = ktProperty(out::padRight).apply { addDisplayCondition(hideFromConfigUi) } - tree["staticWidth"] = ktProperty(out::staticWidth).apply { - description = "Keeps this HUD at a fixed width and height. This also enables content alignment inside the HUD box." + tree["fitContent"] = ktProperty(out::fitContent).apply { + description = "Shrinks the content to fit when it is larger than the size limits, while the background stays within them." addDisplayCondition(hideFromConfigUi) } if (out.persistOwnState) { tree["staticW"] = ktProperty(out::staticW).apply { addDisplayCondition(hideFromConfigUi) } tree["staticH"] = ktProperty(out::staticH).apply { addDisplayCondition(hideFromConfigUi) } + tree["minW"] = ktProperty(out::minW).apply { + description = "The narrowest this HUD may get. 0 means no limit." + addDisplayCondition(hideFromConfigUi) + } + tree["maxW"] = ktProperty(out::maxW).apply { + description = "The widest this HUD may get. 0 means no limit." + addDisplayCondition(hideFromConfigUi) + } + tree["minH"] = ktProperty(out::minH).apply { + description = "The shortest this HUD may get. 0 means no limit." + addDisplayCondition(hideFromConfigUi) + } + tree["maxH"] = ktProperty(out::maxH).apply { + description = "The tallest this HUD may get. 0 means no limit." + addDisplayCondition(hideFromConfigUi) + } } tree["font"] = ktProperty(out::font).apply { addDisplayCondition(hideFromConfigUi) } tree["caseType"] = ktProperty(out::caseType).apply { addDisplayCondition(hideFromConfigUi) } @@ -1638,14 +1794,19 @@ public abstract class Hud( * * Skipped when the background is turned off or is currently being drawn as part of a fused * neighbour shape (see [bgMerged]) + * + * Also applies the size limits (see [sizeConstraints]) so the box fills them, which means it must come + * before any padding */ @JvmOverloads public fun hudBackground(modifier: PolyModifier = PolyModifier): PolyModifier = - if (showBackground && hasBackground() && !bgMerged) { - modifier.background(PolyColor(bgColor, bgChroma, bgChromaSpeed), bgRadius) - } else { - modifier - } + sizeConstraints( + if (showBackground && hasBackground() && !bgMerged) { + modifier.background(PolyColor(bgColor, bgChroma, bgChromaSpeed), bgRadius) + } else { + modifier + } + ) public open fun backgroundColor(): PolyColor? = null public open fun multipleInstancesAllowed(): Boolean = true @@ -1703,7 +1864,8 @@ public abstract class Hud( geomValid = false showKey = -1 toggleKey = -1 - _staticWidth = mutableStateOf(this@Hud.staticWidth) + _fitContent = mutableStateOf(this@Hud.fitContent) + _measuringNatural = mutableStateOf(false) _section = mutableStateOf(this@Hud.section) _relativeX = mutableStateOf(this@Hud.relativeX) _relativeY = mutableStateOf(this@Hud.relativeY) @@ -1724,6 +1886,10 @@ public abstract class Hud( _padRight = mutableStateOf(this@Hud.padRight) _staticW = mutableStateOf(this@Hud.staticW) _staticH = mutableStateOf(this@Hud.staticH) + _minW = mutableStateOf(this@Hud.minW) + _maxW = mutableStateOf(this@Hud.maxW) + _minH = mutableStateOf(this@Hud.minH) + _maxH = mutableStateOf(this@Hud.maxH) _font = mutableStateOf(this@Hud.font) _caseType = mutableStateOf(this@Hud.caseType) _textScale = mutableStateOf(this@Hud.textScale) diff --git a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/HudManager.kt b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/HudManager.kt index f9bab2b14..07357496a 100644 --- a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/HudManager.kt +++ b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/HudManager.kt @@ -509,17 +509,7 @@ public object HudManager { } private fun screenBounds(hud: Hud): FloatArray? { - val scale = hud.frameEffectiveScale - val w = if (hud.staticWidth) { - hud.staticW.takeIf { it > 0f }?.times(scale) - } else { - hud.renderedW.takeIf { it > 0f } ?: hud.staticW.takeIf { it > 0f }?.times(scale) - } ?: return null - val h = if (hud.staticWidth) { - hud.staticH.takeIf { it > 0f }?.times(scale) - } else { - hud.renderedH.takeIf { it > 0f } ?: hud.staticH.takeIf { it > 0f }?.times(scale) - } ?: return null + val (w, h) = hud.boundsSize(hud.frameEffectiveScale) ?: return null return floatArrayOf(hud.x, hud.y, w, h) } @@ -1419,6 +1409,7 @@ public object HudManager { ConfigManager.active().unregister(data.id) val savedSchema = data.getProp("posSchema")?.getAs()?.toInt() ?: 0 val hud = h.make(data) + hud.migrateLegacySize(data) candidate = hud val sec = data.getProp("section")?.getAs() if (sec != null) { diff --git a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/TextHud.kt b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/TextHud.kt index ea0a6cd11..746ecda56 100644 --- a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/TextHud.kt +++ b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/TextHud.kt @@ -38,7 +38,6 @@ import org.polyfrost.compose.composables.PolyMcText import org.polyfrost.compose.composables.PolyModifier import org.polyfrost.compose.composables.align import org.polyfrost.compose.composables.padding -import org.polyfrost.compose.composables.size import org.polyfrost.compose.layout.PolyAlign import org.polyfrost.compose.layout.PolyInsets import org.polyfrost.compose.layout.PolySize @@ -71,7 +70,6 @@ public abstract class TextHud( padRight = 4f padTop = 4f padBottom = 4f - staticWidth = false staticW = UNMEASURED staticH = UNMEASURED } @@ -94,14 +92,11 @@ public abstract class TextHud( // at render time without needing recomposition val fgColor = PolyColor(textColor, textChroma, textChromaSpeed) - val isStaticValid = staticWidth && staticW > 0f && staticH > 0f + val aligned = hasSizeLimits // when merged HudManager draws this background as part of the fused neighbour shape - // which hudBackground() accounts for - val bgModifier = hudBackground() - val outerModifier = - if (isStaticValid) bgModifier.size(staticW, staticH).padding(padInsets) - else bgModifier.padding(padInsets) + // which hudBackground() accounts for, along with the size limits + val outerModifier = hudBackground().padding(padInsets) PolyBox(modifier = outerModifier) { if (font == Font.Poppins) { @@ -121,7 +116,7 @@ public abstract class TextHud( update = { set(provider) { text = it } set(spec) { this.spec = it } - set(if (isStaticValid) contentAlign else PolyAlign.TopLeft) { style.align = it } + set(if (aligned) contentAlign else PolyAlign.TopLeft) { style.align = it } }, ) } else { @@ -139,7 +134,7 @@ public abstract class TextHud( shadow = showShadow && !fullShadow, outlineColor = if (showShadow && fullShadow) PolyColor(shadowColor, shadowChroma, shadowChromaSpeed) else null, scale = textScale, - modifier = if (isStaticValid) PolyModifier.align(contentAlign) else PolyModifier, + modifier = if (aligned) PolyModifier.align(contentAlign) else PolyModifier, ) } } diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/HudSettingsReset.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/HudSettingsReset.kt index fa1c2ebf7..01e9a440c 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/HudSettingsReset.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/HudSettingsReset.kt @@ -76,7 +76,6 @@ private val MenuPadding = 4.dp private val hudUiEpochs = mutableStateMapOf() -private val STATIC_SIZE_OPTIONS = setOf("staticW", "staticH") private val POSITION_OPTIONS = setOf( "section", "relativeX", "relativeY", "anchorTargetId", "anchorPoint", "selfAnchorPoint", "anchorOffsetX", "anchorOffsetY", @@ -115,19 +114,10 @@ private fun treeHasResettableDefaults(tree: Tree, root: Boolean): Boolean { fun resetAllHudProperties(hud: Hud) { val tree = hud.tree ?: return - resetHudStaticSize(hud) resetTreeDefaults(tree, root = true) finishHudReset(hud) } -private fun resetHudStaticSize(hud: Hud) { - if (!hud.staticWidth) return - for (id in STATIC_SIZE_OPTIONS) { - val prop = hudProperty(hud, id) ?: continue - performHudOptionReset(hud, prop, id) - } -} - fun hudHasPositionDefaults(hud: Hud): Boolean = hud.defaultPosition() != null /** Counts the HUD's position too when [position] is set */ @@ -175,43 +165,17 @@ private fun resetPositionOptions(hud: Hud): Boolean { } private fun finishHudReset(hud: Hud) { - hud.reseedStaticSizeIfNeeded() - hud.captureStaticSizeDefaults() bumpHudUiEpoch(hud) hud.updateAndRecalculate() } private fun canResetHudOption(hud: Hud, prop: Property<*>, optionId: String): Boolean { - if (optionHasDefault(prop)) return true - return hud.staticWidth && optionId in STATIC_SIZE_OPTIONS + return optionHasDefault(prop) } private fun performHudOptionReset(hud: Hud, prop: Property<*>, optionId: String) { - when (optionId) { - "staticW" -> { - if (optionHasDefault(prop)) resetOption(prop) else hud.reseedStaticWidth() - } - "staticH" -> { - if (optionHasDefault(prop)) resetOption(prop) else hud.reseedStaticHeight() - } - else -> { - if (optionId in POSITION_OPTIONS) hud.dropMergeLinks() - resetOption(prop) - } - } - hud.captureStaticSizeDefaults(force = optionId in STATIC_SIZE_OPTIONS) -} - -/** Fixes invalid static dimensions such as after a bad reset and refreshes the settings UI */ -fun repairHudStaticSize(hud: Hud) { - if (!hud.staticWidth) return - if (hud.staticW > 0f && hud.staticH > 0f) { - hud.captureStaticSizeDefaults() - return - } - hud.reseedStaticSizeIfNeeded() - hud.captureStaticSizeDefaults(force = true) - bumpHudUiEpoch(hud) + if (optionId in POSITION_OPTIONS) hud.dropMergeLinks() + resetOption(prop) } private fun resetTreeDefaults(tree: Tree, root: Boolean, seen: MutableSet> = identitySet()) { @@ -219,7 +183,8 @@ private fun resetTreeDefaults(tree: Tree, root: Boolean, seen: MutableSet -> { val optionId = node.id ?: node.getID() - val builtIn = root && (optionId in STATIC_SIZE_OPTIONS || optionId in POSITION_OPTIONS) + // staticW/staticH only mirror a fixed size, so the min/max limits are what resets + val builtIn = root && (optionId in POSITION_OPTIONS || optionId == "staticW" || optionId == "staticH") if (!builtIn && optionHasDefault(node)) resetOption(node, seen) } is Tree -> resetTreeDefaults(node, root = false, seen = seen) diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/components/HudPreview.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/components/HudPreview.kt index b204f38b2..571854906 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/components/HudPreview.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/components/HudPreview.kt @@ -111,13 +111,13 @@ internal fun rememberHudPreview(hud: Hud): HudPreviewState { } } LaunchedEffect(state) { - val static = hud.staticWidth && hud.staticW > 0f && hud.staticH > 0f + val static = hud.isFixedW && hud.isFixedH if (!state.ready) { hud.updateForPreview() if (static) { - state.runtime.frame(hud.staticW, hud.staticH) - state.naturalWidth = hud.staticW - state.naturalHeight = hud.staticH + state.runtime.frame(hud.minW, hud.minH) + state.naturalWidth = hud.minW + state.naturalHeight = hud.minH } else { state.runtime.frame(MEASURE_BOUNDS, MEASURE_BOUNDS) state.naturalWidth = state.runtime.root.width @@ -130,7 +130,7 @@ internal fun rememberHudPreview(hud: Hud): HudPreviewState { // whenever a value flips between digit counts. snapshotFlow { HudManager.previewRevision.intValue }.collect { // text is read outside composition, so even fixed-size previews need a layout pass to pick up new values - if (static) return@collect state.runtime.layout(hud.staticW, hud.staticH) + if (static) return@collect state.runtime.layout(hud.minW, hud.minH) state.runtime.layout(MEASURE_BOUNDS, MEASURE_BOUNDS) state.naturalWidth = maxOf(state.naturalWidth, state.runtime.root.width) state.naturalHeight = maxOf(state.naturalHeight, state.runtime.root.height) 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 498527596..a0303e298 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 @@ -105,7 +105,6 @@ import org.polyfrost.oneconfig.internal.ui.hud.components.HudPreviewCanvas import org.polyfrost.oneconfig.internal.ui.hud.components.HudPreviewState import org.polyfrost.oneconfig.internal.ui.hud.components.rememberHudPreview import org.polyfrost.oneconfig.internal.ui.hud.modNameFor -import org.polyfrost.oneconfig.internal.ui.hud.repairHudStaticSize import org.polyfrost.oneconfig.internal.ui.hud.screens.sections.Layout import org.polyfrost.oneconfig.internal.ui.hud.screens.sections.Settings import org.polyfrost.oneconfig.internal.ui.hud.screens.sections.Style @@ -403,16 +402,7 @@ private fun hudBounds(hud: Hud): HudBounds? { ?: return null return HudBounds(hud.x, hud.y, width, height) } - val width = if (hud.staticWidth) { - hud.staticW.takeIf { it > 0f }?.times(scale) - } else { - hud.renderedW.takeIf { it > 0f } ?: hud.staticW.takeIf { it > 0f }?.times(scale) - } ?: return null - val height = if (hud.staticWidth) { - hud.staticH.takeIf { it > 0f }?.times(scale) - } else { - hud.renderedH.takeIf { it > 0f } ?: hud.staticH.takeIf { it > 0f }?.times(scale) - } ?: return null + val (width, height) = hud.boundsSize(scale) ?: return null return HudBounds(hud.x, hud.y, width, height) } @@ -730,9 +720,8 @@ private fun resizeHud( val newWidth = targetWidth.coerceAtLeast(minStaticW * effectiveScale) val newHeight = targetHeight.coerceAtLeast(minStaticH * effectiveScale) - hud.staticWidth = true - hud.staticW = newWidth / effectiveScale - hud.staticH = newHeight / effectiveScale + resizeWidth(hud, newWidth / effectiveScale) + resizeHeight(hud, newHeight / effectiveScale) return } @@ -749,6 +738,38 @@ private fun resizeHud( hud.customScale = (startScale.coerceAtLeast(0.001f) * rawFactor).coerceIn(0.25f, 4f) } +/** Moves the limits the user is dragging: both when the axis is fixed, else the maximum if there is one, else the minimum, else pins the size */ +private fun resizeWidth(hud: Hud, width: Float) { + when { + hud.isFixedW -> { + // the minimum first so a growing width raises the maximum and a shrinking one is not held up by it + hud.minW = width + hud.maxW = width + } + hud.maxW > 0f -> { + if (hud.minW > width) hud.minW = width + hud.maxW = width + } + hud.minW > 0f -> hud.minW = width + else -> { hud.maxW = width; hud.minW = width } + } +} + +private fun resizeHeight(hud: Hud, height: Float) { + when { + hud.isFixedH -> { + hud.minH = height + hud.maxH = height + } + hud.maxH > 0f -> { + if (hud.minH > height) hud.minH = height + hud.maxH = height + } + hud.minH > 0f -> hud.minH = height + else -> { hud.maxH = height; hud.minH = height } + } +} + private fun DrawScope.drawSelectedHudBounds( bounds: HudBounds, mcToScreen: Float, @@ -1015,13 +1036,13 @@ private fun placeHudCentered(provider: Hud): Hud? = try { val screenH = HudManager.guiScreenHeight val scale = instance.effectiveScale val (minW, minH) = instance.minimumSize() - val w = if (instance.staticWidth) { - instance.staticW.takeIf { it > 0f }?.times(scale) + val w = if (instance.isFixedW) { + maxOf(instance.minW, minW).times(scale) } else { instance.renderedW.takeIf { it > 0f } ?: minW.takeIf { it > 0f }?.times(scale) ?: instance.staticW.takeIf { it > 0f }?.times(scale) } ?: 60f - val h = if (instance.staticWidth) { - instance.staticH.takeIf { it > 0f }?.times(scale) + val h = if (instance.isFixedH) { + maxOf(instance.minH, minH).times(scale) } else { instance.renderedH.takeIf { it > 0f } ?: minH.takeIf { it > 0f }?.times(scale) ?: instance.staticH.takeIf { it > 0f }?.times(scale) } ?: 20f @@ -1277,8 +1298,6 @@ fun HudDesignStudio(visible: Boolean = true) { } LaunchedEffect(selectedHuds, tourBlocksEditor) { - val primary = primaryHud() - primary?.let { repairHudStaticSize(it) } HudDesignSession.activeSelection = if (tourBlocksEditor) emptyList() else selectedHuds.toList() } @@ -2760,9 +2779,13 @@ internal fun duplicateHud( fresh.tree.overwrite(detachedValues(copied.tree), true, false, null) fresh.hidden = copied.hidden fresh.locked = copied.locked - fresh.staticWidth = copied.staticWidth fresh.staticW = copied.staticW fresh.staticH = copied.staticH + fresh.minW = copied.minW + fresh.maxW = copied.maxW + fresh.minH = copied.minH + fresh.maxH = copied.maxH + fresh.fitContent = copied.fitContent fresh.customScale = copied.customScale if (mcPosition != null) { val sw = HudManager.guiScreenWidth diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Layout.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Layout.kt index f859441e7..cd9d98d5e 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Layout.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Layout.kt @@ -1,10 +1,11 @@ package org.polyfrost.oneconfig.internal.ui.hud.screens.sections import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -27,13 +28,14 @@ import org.polyfrost.oneconfig.api.hud.v1.LegacyHudMarker import org.polyfrost.oneconfig.api.platform.v1.Platform import org.polyfrost.oneconfig.internal.ui.components.Text import org.polyfrost.oneconfig.internal.ui.components.settings.NumberSpinner +import org.polyfrost.oneconfig.internal.ui.components.settings.RangeSeparatorWidth +import org.polyfrost.oneconfig.internal.ui.components.settings.formatSpinnerValue import org.polyfrost.oneconfig.internal.ui.components.settings.SwitchControl import org.polyfrost.oneconfig.internal.ui.hud.HudPositionTarget import org.polyfrost.oneconfig.internal.ui.hud.HudSettingTarget import org.polyfrost.oneconfig.internal.ui.hud.HudSettingsContent import org.polyfrost.oneconfig.internal.ui.hud.components.AlignmentPicker import org.polyfrost.oneconfig.internal.ui.hud.hudOptionsCanReset -import org.polyfrost.oneconfig.internal.ui.hud.repairHudStaticSize import org.polyfrost.oneconfig.internal.ui.hud.resetHudOptions import org.polyfrost.oneconfig.internal.ui.hud.screens.HudEditorViewport import org.polyfrost.oneconfig.internal.ui.hud.screens.beginHudMove @@ -46,21 +48,15 @@ import org.polyfrost.oneconfig.internal.ui.hud.screens.selectionOrigin import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme private val POSITION_SCALE_OPTIONS = listOf("locked", "useGuiScale", "customScale") -private val SIZE_OPTIONS = listOf("staticWidth", "staticW", "staticH", "alignment", "growthAnchor") +private val SIZE_OPTIONS = listOf("minW", "maxW", "minH", "maxH", "fitContent", "alignment", "growthAnchor") private val PADDING_OPTIONS = listOf("padLeft", "padRight", "padTop", "padBottom") @Composable fun Layout(hud: Hud?, query: String) { if (hud == null) return - LaunchedEffect(hud) { repairHudStaticSize(hud) } - HudSettingsContent(hud) { var locked by remember { mutableStateOf(hud.locked) } - var staticWidth by remember { mutableStateOf(hud.staticWidth) } - var staticW by remember { mutableStateOf(hud.staticW) } - var staticH by remember { mutableStateOf(hud.staticH) } - var alignment by remember { mutableStateOf(hud.alignment) } var growthAnchor by remember { mutableStateOf(hud.effectiveGrowthAnchor) } val growthAlign = growthAnchor.toAlign() ?: PolyAlign.Center var padLeft by remember { mutableStateOf(hud.padLeft) } @@ -115,78 +111,66 @@ fun Layout(hud: Hud?, query: String) { if (hud !is LegacyHudMarker) { CardSection( - "Size & Alignment", query, "Static size", "Width", "Height", "Anchor", "Content alignment", - "Alignment", "Growth anchor", + "Size & Alignment", query, "Width", "Height", "Min width", "Max width", "Min height", "Max height", + "Scale content to fit", "Anchor", "Content alignment", "Alignment", "Growth anchor", onReset = { resetHudOptions(hud, SIZE_OPTIONS) }, canReset = hudOptionsCanReset(hud, SIZE_OPTIONS), ) { col1 -> - RowWithFold( - staticWidth, - row = { - HudSettingTarget(hud, "staticWidth") { - SwitchRow("Static size", staticWidth) { - Snapshot.withMutableSnapshot { - staticWidth = it; hud.staticWidth = it - if (it) { - staticW = hud.staticW - staticH = hud.staticH - } else { - hud.updateAndRecalculate() - } + // the limits are read from the HUD every time so a canvas resize drag is reflected right away + val capped = hud.maxW > 0f || hud.maxH > 0f + val limited = hud.hasSizeLimits + val pinned = hud.isFixedW && hud.isFixedH + + SizeRangeRow(hud, "width", 20f, hud.minW, hud.maxW, { hud.minW = it }, { hud.maxW = it }) + SizeRangeRow(hud, "height", 8f, hud.minH, hud.maxH, { hud.minH = it }, { hud.maxH = it }) + + if (capped) { + HudSettingTarget(hud, "fitContent") { + SwitchRow("Scale content to fit", hud.fitContent) { + Snapshot.withMutableSnapshot { hud.fitContent = it } + } + } + } + + if (limited) { + HudSettingTarget(hud, "alignment") { + LabeledControl("Content alignment") { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + AlignmentPicker(hud.alignment) { + Snapshot.withMutableSnapshot { hud.alignment = it } } + Text( + "Aligns the content inside the size limits", + color = LocalTheme.current.textColorSecondary, + fontSize = 12.sp + ) } } - }, - folded = { - GridRow( - col1, - first = { - HudSettingTarget(hud, "staticW") { - LabeledControl("Width") { - NumberSpinner( - staticW, { Snapshot.withMutableSnapshot { staticW = it; hud.staticW = it } }, - 20f, 2000f, 1f, width = col1, unit = "px", scrub = true, math = true, - ) - } - } - }, - second = { - HudSettingTarget(hud, "staticH") { - LabeledControl("Height") { - NumberSpinner( - staticH, { Snapshot.withMutableSnapshot { staticH = it; hud.staticH = it } }, - 8f, 2000f, 1f, width = GridCol2, unit = "px", scrub = true, math = true, - ) - } - } - }, - ) - }, - ) + } + } - HudSettingTarget(hud, if (staticWidth) "alignment" else "growthAnchor") { - LabeledControl(if (staticWidth) "Content alignment" else "Anchor") { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - AlignmentPicker(if (staticWidth) alignment else growthAlign) { - Snapshot.withMutableSnapshot { - if (staticWidth) { - alignment = it - hud.alignment = it - } else { + if (!pinned) { + HudSettingTarget(hud, "growthAnchor") { + LabeledControl("Anchor") { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + AlignmentPicker(growthAlign) { + Snapshot.withMutableSnapshot { growthAnchor = HudAnchor.of(it) hud.setGrowthAnchorKeepingPosition(growthAnchor) } } + Text( + "Pins this corner or edge in place;\nthe HUD only grows away from it", + color = LocalTheme.current.textColorSecondary, + fontSize = 12.sp + ) } - Text( - if (staticWidth) "Aligns the content inside the fixed box" - else "Pins this corner or edge in place;\nthe HUD only grows away from it", - color = LocalTheme.current.textColorSecondary, - fontSize = 12.sp - ) } } } @@ -331,3 +315,46 @@ private fun setFollowGuiScale(hud: Hud, follow: Boolean) { if (follow) hud.customScale *= hud.fixedGuiScale / gui else hud.fixedGuiScale = gui hud.useGuiScale = follow } + +/** + * One axis' size limits as a minimum and maximum, where 0 is no limit and reads as None + * + * Giving both ends the same value pins the axis to that size + */ +@Composable +private fun SizeRangeRow( + hud: Hud, + axis: String, + floor: Float, + min: Float, + max: Float, + onMin: (Float) -> Unit, + onMax: (Float) -> Unit, +) { + val id = if (axis == "width") "W" else "H" + val none: (Float) -> String = { if (it <= 0f) "None" else formatSpinnerValue(it, 1f) } + GridRow( + GridCol3, + wide = { + LabeledControl(axis.replaceFirstChar { it.uppercase() }) { + Row(verticalAlignment = Alignment.CenterVertically) { + HudSettingTarget(hud, "min$id") { + NumberSpinner( + min, { v -> Snapshot.withMutableSnapshot { onMin(v); hud.updateAndRecalculate() } }, + 0f, 2000f, 1f, width = GridCol3, format = none, unit = "px", scrub = true, math = true, label = "Min", + ) + } + Box(Modifier.width(RangeSeparatorWidth), contentAlignment = Alignment.Center) { + Text("-", color = LocalTheme.current.textColorSecondary, fontSize = 13.sp) + } + HudSettingTarget(hud, "max$id") { + NumberSpinner( + max, { v -> Snapshot.withMutableSnapshot { onMax(v); hud.updateAndRecalculate() } }, + 0f, 2000f, 1f, width = GridCol3, format = none, unit = "px", scrub = true, math = true, label = "Max", + ) + } + } + } + }, + ) +} diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Style.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Style.kt index b9e59342b..c82ccf99c 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Style.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/sections/Style.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -38,7 +37,6 @@ import org.polyfrost.oneconfig.internal.ui.components.settings.SwitchControl import org.polyfrost.oneconfig.internal.ui.hud.HudSettingTarget import org.polyfrost.oneconfig.internal.ui.hud.HudSettingsContent import org.polyfrost.oneconfig.internal.ui.hud.hudOptionsCanReset -import org.polyfrost.oneconfig.internal.ui.hud.repairHudStaticSize import org.polyfrost.oneconfig.internal.ui.hud.resetHudOptions import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme @@ -78,7 +76,6 @@ private enum class MergeMode(val title: String) { fun Style(hud: Hud?, query: String) { if (hud == null) return - LaunchedEffect(hud) { repairHudStaticSize(hud) } HudSettingsContent(hud) { var font by remember { mutableStateOf(hud.font) } diff --git a/modules/poly-compose/api/poly-compose.api b/modules/poly-compose/api/poly-compose.api index e500a4b10..8376b4226 100644 --- a/modules/poly-compose/api/poly-compose.api +++ b/modules/poly-compose/api/poly-compose.api @@ -382,8 +382,11 @@ public final class org/polyfrost/compose/composables/PolyModifierKt { public static final fun fill (Lorg/polyfrost/compose/composables/PolyModifier;)Lorg/polyfrost/compose/composables/PolyModifier; public static final fun fillHeight (Lorg/polyfrost/compose/composables/PolyModifier;)Lorg/polyfrost/compose/composables/PolyModifier; public static final fun fillWidth (Lorg/polyfrost/compose/composables/PolyModifier;)Lorg/polyfrost/compose/composables/PolyModifier; + public static final fun fitContent (Lorg/polyfrost/compose/composables/PolyModifier;)Lorg/polyfrost/compose/composables/PolyModifier; public static final fun height (Lorg/polyfrost/compose/composables/PolyModifier;F)Lorg/polyfrost/compose/composables/PolyModifier; public static final fun height (Lorg/polyfrost/compose/composables/PolyModifier;Lorg/polyfrost/compose/layout/PolySize;)Lorg/polyfrost/compose/composables/PolyModifier; + public static final fun heightIn (Lorg/polyfrost/compose/composables/PolyModifier;FF)Lorg/polyfrost/compose/composables/PolyModifier; + public static synthetic fun heightIn$default (Lorg/polyfrost/compose/composables/PolyModifier;FFILjava/lang/Object;)Lorg/polyfrost/compose/composables/PolyModifier; public static final fun margin (Lorg/polyfrost/compose/composables/PolyModifier;F)Lorg/polyfrost/compose/composables/PolyModifier; public static final fun margin (Lorg/polyfrost/compose/composables/PolyModifier;FF)Lorg/polyfrost/compose/composables/PolyModifier; public static final fun margin (Lorg/polyfrost/compose/composables/PolyModifier;FFFF)Lorg/polyfrost/compose/composables/PolyModifier; @@ -402,6 +405,8 @@ public final class org/polyfrost/compose/composables/PolyModifierKt { public static final fun tag (Lorg/polyfrost/compose/composables/PolyModifier;Ljava/lang/Object;)Lorg/polyfrost/compose/composables/PolyModifier; public static final fun width (Lorg/polyfrost/compose/composables/PolyModifier;F)Lorg/polyfrost/compose/composables/PolyModifier; public static final fun width (Lorg/polyfrost/compose/composables/PolyModifier;Lorg/polyfrost/compose/layout/PolySize;)Lorg/polyfrost/compose/composables/PolyModifier; + public static final fun widthIn (Lorg/polyfrost/compose/composables/PolyModifier;FF)Lorg/polyfrost/compose/composables/PolyModifier; + public static synthetic fun widthIn$default (Lorg/polyfrost/compose/composables/PolyModifier;FFILjava/lang/Object;)Lorg/polyfrost/compose/composables/PolyModifier; public static final fun wrapHeight (Lorg/polyfrost/compose/composables/PolyModifier;)Lorg/polyfrost/compose/composables/PolyModifier; public static final fun wrapWidth (Lorg/polyfrost/compose/composables/PolyModifier;)Lorg/polyfrost/compose/composables/PolyModifier; } @@ -485,10 +490,15 @@ public final class org/polyfrost/compose/layout/PolyLayoutStyle { public final fun getBorderWidth ()F public final fun getClipToBounds ()Z public final fun getFade ()Lkotlin/jvm/functions/Function0; + public final fun getFitContent ()Z public final fun getGap ()F public final fun getHeight ()Lorg/polyfrost/compose/layout/PolySize; public final fun getLayoutType ()Lorg/polyfrost/compose/layout/LayoutType; public final fun getMargin ()Lorg/polyfrost/compose/layout/PolyInsets; + public final fun getMaxHeight ()F + public final fun getMaxWidth ()F + public final fun getMinHeight ()F + public final fun getMinWidth ()F public final fun getPadding ()Lorg/polyfrost/compose/layout/PolyInsets; public final fun getPositionMode ()Lorg/polyfrost/compose/layout/PolyPositionMode; public final fun getRadius ()F @@ -503,10 +513,15 @@ public final class org/polyfrost/compose/layout/PolyLayoutStyle { public final fun setBorderWidth (F)V public final fun setClipToBounds (Z)V public final fun setFade (Lkotlin/jvm/functions/Function0;)V + public final fun setFitContent (Z)V public final fun setGap (F)V public final fun setHeight (Lorg/polyfrost/compose/layout/PolySize;)V public final fun setLayoutType (Lorg/polyfrost/compose/layout/LayoutType;)V public final fun setMargin (Lorg/polyfrost/compose/layout/PolyInsets;)V + public final fun setMaxHeight (F)V + public final fun setMaxWidth (F)V + public final fun setMinHeight (F)V + public final fun setMinWidth (F)V public final fun setPadding (Lorg/polyfrost/compose/layout/PolyInsets;)V public final fun setPositionMode (Lorg/polyfrost/compose/layout/PolyPositionMode;)V public final fun setRadius (F)V @@ -660,6 +675,7 @@ public class org/polyfrost/compose/node/PolyNode { public static final field $stable I public fun ()V public final fun getChildren ()Ljava/util/List; + public final fun getContentScale ()F public final fun getHeight ()F public final fun getParent ()Lorg/polyfrost/compose/node/PolyNode; public final fun getStyle ()Lorg/polyfrost/compose/layout/PolyLayoutStyle; @@ -668,6 +684,7 @@ public class org/polyfrost/compose/node/PolyNode { public final fun getY ()F public fun measureContent ()V public fun render (Lorg/polyfrost/compose/render/RenderContext;)V + public final fun setContentScale (F)V public final fun setHeight (F)V public final fun setParent (Lorg/polyfrost/compose/node/PolyNode;)V public final fun setWidth (F)V diff --git a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/composables/PolyModifier.kt b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/composables/PolyModifier.kt index 6816df2f8..a11c673ac 100644 --- a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/composables/PolyModifier.kt +++ b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/composables/PolyModifier.kt @@ -11,6 +11,11 @@ public open class PolyModifier internal constructor(internal val block: PolyLayo val g = style.gap style.width = PolySize.Wrap style.height = PolySize.Wrap + style.minWidth = Float.NaN + style.maxWidth = Float.NaN + style.minHeight = Float.NaN + style.maxHeight = Float.NaN + style.fitContent = false style.align = PolyAlign.TopLeft style.padding = PolyInsets() style.margin = PolyInsets() @@ -39,6 +44,9 @@ public fun PolyModifier.width(w: Float): PolyModifier = then(PolyModifier { widt public fun PolyModifier.width(w: PolySize): PolyModifier = then(PolyModifier { width = w }) public fun PolyModifier.height(h: Float): PolyModifier = then(PolyModifier { height = PolySize.Px(h) }) public fun PolyModifier.height(h: PolySize): PolyModifier = then(PolyModifier { height = h }) +public fun PolyModifier.widthIn(min: Float = Float.NaN, max: Float = Float.NaN): PolyModifier = then(PolyModifier { minWidth = min; maxWidth = max }) +public fun PolyModifier.heightIn(min: Float = Float.NaN, max: Float = Float.NaN): PolyModifier = then(PolyModifier { minHeight = min; maxHeight = max }) +public fun PolyModifier.fitContent(): PolyModifier = then(PolyModifier { fitContent = true }) public fun PolyModifier.fillWidth(): PolyModifier = then(PolyModifier { width = PolySize.Fill }) public fun PolyModifier.fillHeight(): PolyModifier = then(PolyModifier { height = PolySize.Fill }) public fun PolyModifier.fill(): PolyModifier = then(PolyModifier { width = PolySize.Fill; height = PolySize.Fill }) diff --git a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/layout/PolyLayoutEngine.kt b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/layout/PolyLayoutEngine.kt index 87bda90bf..6cc186a4f 100644 --- a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/layout/PolyLayoutEngine.kt +++ b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/layout/PolyLayoutEngine.kt @@ -3,6 +3,7 @@ package org.polyfrost.compose.layout import org.polyfrost.compose.node.PolyNode public object PolyLayoutEngine { + private const val MIN_FIT_SCALE = 0.01f public fun layout(node: PolyNode, parentWidth: Float, parentHeight: Float) { measure(node, parentWidth, parentHeight) @@ -17,8 +18,12 @@ public object PolyLayoutEngine { } } - node.width = resolveSize(node.style.width, parentWidth, node, horizontal = true) - node.height = resolveSize(node.style.height, parentHeight, node, horizontal = false) + val style = node.style + val naturalWidth = resolveSize(style.width, parentWidth, node, horizontal = true) + val naturalHeight = resolveSize(style.height, parentHeight, node, horizontal = false) + node.width = naturalWidth.constrained(style.minWidth, style.maxWidth) + node.height = naturalHeight.constrained(style.minHeight, style.maxHeight) + node.contentScale = if (style.fitContent) fitScale(node, naturalWidth, naturalHeight) else 1f val cw = (node.width - node.style.padding.horizontal).coerceAtLeast(0f) val ch = (node.height - node.style.padding.vertical).coerceAtLeast(0f) @@ -29,6 +34,25 @@ public object PolyLayoutEngine { } } + private fun fitScale(node: PolyNode, naturalWidth: Float, naturalHeight: Float): Float { + val padding = node.style.padding + val naturalContentWidth = naturalWidth - padding.horizontal + val naturalContentHeight = naturalHeight - padding.vertical + val contentWidth = (node.width - padding.horizontal).coerceAtLeast(0f) + val contentHeight = (node.height - padding.vertical).coerceAtLeast(0f) + var scale = 1f + if (naturalContentWidth > contentWidth && naturalContentWidth > 0f) scale = minOf(scale, contentWidth / naturalContentWidth) + if (naturalContentHeight > contentHeight && naturalContentHeight > 0f) scale = minOf(scale, contentHeight / naturalContentHeight) + return scale.coerceAtLeast(MIN_FIT_SCALE) + } + + private fun Float.constrained(min: Float, max: Float): Float { + var value = this + if (!max.isNaN()) value = minOf(value, max) + if (!min.isNaN()) value = maxOf(value, min) + return value + } + private fun resolveSize(size: PolySize, parentSize: Float, node: PolyNode, horizontal: Boolean): Float { return when (size) { is PolySize.Px -> size.value @@ -62,8 +86,8 @@ public object PolyLayoutEngine { val s = node.style val cx = node.x + s.padding.left val cy = node.y + s.padding.top - val cw = (node.width - s.padding.horizontal).coerceAtLeast(0f) - val ch = (node.height - s.padding.vertical).coerceAtLeast(0f) + val cw = (node.width - s.padding.horizontal).coerceAtLeast(0f) / node.contentScale + val ch = (node.height - s.padding.vertical).coerceAtLeast(0f) / node.contentScale when (s.layoutType) { LayoutType.Row -> { diff --git a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/layout/PolyLayoutStyle.kt b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/layout/PolyLayoutStyle.kt index bdffc2c18..4a9c51949 100644 --- a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/layout/PolyLayoutStyle.kt +++ b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/layout/PolyLayoutStyle.kt @@ -7,6 +7,11 @@ public enum class LayoutType { Box, Row, Column } public class PolyLayoutStyle { public var width: PolySize = PolySize.Wrap public var height: PolySize = PolySize.Wrap + public var minWidth: Float = Float.NaN + public var maxWidth: Float = Float.NaN + public var minHeight: Float = Float.NaN + public var maxHeight: Float = Float.NaN + public var fitContent: Boolean = false public var align: PolyAlign = PolyAlign.TopLeft public var padding: PolyInsets = PolyInsets() public var margin: PolyInsets = PolyInsets() diff --git a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/node/PolyNode.kt b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/node/PolyNode.kt index 574240d2b..66af818eb 100644 --- a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/node/PolyNode.kt +++ b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/node/PolyNode.kt @@ -12,6 +12,7 @@ public open class PolyNode { public var y: Float = 0f public var width: Float = 0f public var height: Float = 0f + public var contentScale: Float = 1f private var decoration: Decoration? = null @@ -25,6 +26,15 @@ public open class PolyNode { ctx.save() if (s.radius > 0f) ctx.clipRRect(x, y, width, height, s.radius) else ctx.clipRect(x, y, width, height) } + val scaled = contentScale != 1f + if (scaled) { + val cx = x + s.padding.left + val cy = y + s.padding.top + ctx.save() + ctx.translate(cx, cy) + ctx.scale(contentScale, contentScale) + ctx.translate(-cx, -cy) + } children.forEach { child -> val fade = child.style.fade if (fade == null) { @@ -40,6 +50,7 @@ public open class PolyNode { } } } + if (scaled) ctx.restore() if (s.clipToBounds) ctx.restore() }