From bcf0a364e2a4a9057428411034503a970cc59a1e Mon Sep 17 00:00:00 2001 From: Deftu Date: Mon, 2 Nov 2026 09:17:01 +0200 Subject: [PATCH 01/12] feat: add backdrop glass, colour ring strokes and bounded live ops to the UI renderer --- modules/poly-compose/api/poly-compose.api | 15 ++++++ .../compose/backend/BackdropGlass.kt | 14 +++++ .../polyfrost/compose/render/PolyRenderer.kt | 13 +++++ .../compose/render/PolyTessellator.kt | 52 +++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 modules/poly-compose/src/main/kotlin/org/polyfrost/compose/backend/BackdropGlass.kt diff --git a/modules/poly-compose/api/poly-compose.api b/modules/poly-compose/api/poly-compose.api index a1553489c..f49907acb 100644 --- a/modules/poly-compose/api/poly-compose.api +++ b/modules/poly-compose/api/poly-compose.api @@ -72,6 +72,10 @@ public final class androidx/compose/ui/text/platform/PlatformFont_skikoKt { public static synthetic fun Font-wCLgNak$default (Ljava/lang/String;[BLandroidx/compose/ui/text/font/FontWeight;IILjava/lang/Object;)Landroidx/compose/ui/text/font/Font; } +public final class org/polyfrost/compose/backend/BackdropGlassKt { + public static final fun drawBackdropGlass-bGhzSjQ (Landroidx/compose/ui/graphics/drawscope/DrawScope;JFFFF)V +} + public final class org/polyfrost/compose/backend/PolyCanvas : org/polyfrost/compose/backend/PolyDrawCanvas { public static final field $stable I public fun (Lorg/polyfrost/compose/render/PolyRenderer;)V @@ -1179,6 +1183,7 @@ public abstract interface class org/polyfrost/compose/render/PolyRenderer { public static final field JOIN_MITER I public static final field JOIN_ROUND I public fun animatedScale (FF)V + public fun backdropGlass (FFFFFFFF)V public fun clipOutRect (FFFF)V public fun clipPolygon ([F)V public abstract fun clipRRect (FFFFF)V @@ -1191,6 +1196,7 @@ public abstract interface class org/polyfrost/compose/render/PolyRenderer { public static synthetic fun image$default (Lorg/polyfrost/compose/render/PolyRenderer;Lorg/polyfrost/compose/render/PolyBitmap;FFFFFFFFILorg/polyfrost/compose/render/FilterQuality;ILjava/lang/Object;)V public static synthetic fun image$default (Lorg/polyfrost/compose/render/PolyRenderer;Lorg/polyfrost/compose/render/PolyBitmap;FFFFLorg/polyfrost/compose/render/FilterQuality;ILjava/lang/Object;)V public abstract fun line (FFFFIF)V + public fun live (FFFFLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V public abstract fun live (Lkotlin/jvm/functions/Function1;)V public abstract fun mcText (Ljava/lang/String;FFIZF)V public fun mcTextLive (Ljava/lang/String;FFLkotlin/jvm/functions/Function0;ZF)V @@ -1208,6 +1214,8 @@ public abstract interface class org/polyfrost/compose/render/PolyRenderer { public abstract fun scale (FF)V public abstract fun shadow (FFFFFLorg/polyfrost/compose/render/PolyShadow;)V public fun shadowWithHole (FFFFFLorg/polyfrost/compose/render/PolyShadow;FFFFF)V + public fun strokeColorRing ([F[F[IF[F)V + public static synthetic fun strokeColorRing$default (Lorg/polyfrost/compose/render/PolyRenderer;[F[F[IF[FILjava/lang/Object;)V public fun strokePolyline ([FZIFII)V public static synthetic fun strokePolyline$default (Lorg/polyfrost/compose/render/PolyRenderer;[FZIFIIILjava/lang/Object;)V public fun strokePolylineGradient ([FZFFFFLorg/polyfrost/compose/render/PolyGradient;FII)V @@ -1228,6 +1236,7 @@ public final class org/polyfrost/compose/render/PolyRenderer$Companion { public final class org/polyfrost/compose/render/PolyRenderer$DefaultImpls { public static fun animatedScale (Lorg/polyfrost/compose/render/PolyRenderer;FF)V + public static fun backdropGlass (Lorg/polyfrost/compose/render/PolyRenderer;FFFFFFFF)V public static fun clipOutRect (Lorg/polyfrost/compose/render/PolyRenderer;FFFF)V public static fun clipPolygon (Lorg/polyfrost/compose/render/PolyRenderer;[F)V public static fun deviceTransform (Lorg/polyfrost/compose/render/PolyRenderer;)[F @@ -1235,10 +1244,13 @@ public final class org/polyfrost/compose/render/PolyRenderer$DefaultImpls { public static fun image (Lorg/polyfrost/compose/render/PolyRenderer;Lorg/polyfrost/compose/render/PolyBitmap;FFFFLorg/polyfrost/compose/render/FilterQuality;)V public static synthetic fun image$default (Lorg/polyfrost/compose/render/PolyRenderer;Lorg/polyfrost/compose/render/PolyBitmap;FFFFFFFFILorg/polyfrost/compose/render/FilterQuality;ILjava/lang/Object;)V public static synthetic fun image$default (Lorg/polyfrost/compose/render/PolyRenderer;Lorg/polyfrost/compose/render/PolyBitmap;FFFFLorg/polyfrost/compose/render/FilterQuality;ILjava/lang/Object;)V + public static fun live (Lorg/polyfrost/compose/render/PolyRenderer;FFFFLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V public static fun mcTextLive (Lorg/polyfrost/compose/render/PolyRenderer;Ljava/lang/String;FFLkotlin/jvm/functions/Function0;ZF)V public static fun rotate (Lorg/polyfrost/compose/render/PolyRenderer;F)V public static fun saveLayerFade (Lorg/polyfrost/compose/render/PolyRenderer;Lorg/polyfrost/compose/render/PolyFade;)V public static fun shadowWithHole (Lorg/polyfrost/compose/render/PolyRenderer;FFFFFLorg/polyfrost/compose/render/PolyShadow;FFFFF)V + public static fun strokeColorRing (Lorg/polyfrost/compose/render/PolyRenderer;[F[F[IF[F)V + public static synthetic fun strokeColorRing$default (Lorg/polyfrost/compose/render/PolyRenderer;[F[F[IF[FILjava/lang/Object;)V public static fun strokePolyline (Lorg/polyfrost/compose/render/PolyRenderer;[FZIFII)V public static synthetic fun strokePolyline$default (Lorg/polyfrost/compose/render/PolyRenderer;[FZIFIIILjava/lang/Object;)V public static fun strokePolylineGradient (Lorg/polyfrost/compose/render/PolyRenderer;[FZFFFFLorg/polyfrost/compose/render/PolyGradient;FII)V @@ -1283,6 +1295,7 @@ public abstract class org/polyfrost/compose/render/PolyTessellator : org/polyfro public fun ()V public fun animatedScale (FF)V protected final fun appendDrawBoxes ([F)V + public fun backdropGlass (FFFFFFFF)V protected final fun begin (Ljava/lang/Object;ZZ)V public static synthetic fun begin$default (Lorg/polyfrost/compose/render/PolyTessellator;Ljava/lang/Object;ZZILjava/lang/Object;)V protected final fun beginDraw ()V @@ -1307,6 +1320,7 @@ public abstract class org/polyfrost/compose/render/PolyTessellator : org/polyfro protected fun layer (Ljava/lang/Object;F)V protected fun layerMark ()Ljava/lang/Object; public fun line (FFFFIF)V + public fun live (FFFFLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V public fun mcText (Ljava/lang/String;FFIZF)V public fun mcTextLive (Ljava/lang/String;FFLkotlin/jvm/functions/Function0;ZF)V public fun mcTextOutline (Ljava/lang/String;FFIIF)V @@ -1331,6 +1345,7 @@ public abstract class org/polyfrost/compose/render/PolyTessellator : org/polyfro public fun shadow (FFFFFLorg/polyfrost/compose/render/PolyShadow;)V public fun shadowWithHole (FFFFFLorg/polyfrost/compose/render/PolyShadow;FFFFF)V public final fun stateKey ()Ljava/lang/Object; + public fun strokeColorRing ([F[F[IF[F)V public fun strokePolyline ([FZIFII)V public fun strokePolylineGradient ([FZFFFFLorg/polyfrost/compose/render/PolyGradient;FII)V protected abstract fun submit (Ljava/lang/Object;ZZ[F[I[F)V diff --git a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/backend/BackdropGlass.kt b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/backend/BackdropGlass.kt new file mode 100644 index 000000000..b78a970f4 --- /dev/null +++ b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/backend/BackdropGlass.kt @@ -0,0 +1,14 @@ +package org.polyfrost.compose.backend + +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas + +fun DrawScope.drawBackdropGlass(size: Size, cornerRadiusPx: Float, bandPx: Float, refraction: Float, alpha: Float) { + if (size.width <= 0f || size.height <= 0f || alpha <= 0f) return + drawIntoCanvas { c -> + (c as? PolyDrawCanvas)?.polyOp { + it.renderer.backdropGlass(0f, 0f, size.width, size.height, cornerRadiusPx, bandPx, refraction, alpha) + } + } +} diff --git a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/render/PolyRenderer.kt b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/render/PolyRenderer.kt index 4c9fa0feb..f2f8ac1e0 100644 --- a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/render/PolyRenderer.kt +++ b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/render/PolyRenderer.kt @@ -42,6 +42,15 @@ interface PolyRenderer { points: FloatArray, closed: Boolean, x: Float, y: Float, w: Float, h: Float, gradient: PolyGradient, strokeWidth: Float, cap: Int, join: Int, ) = strokePolyline(points, closed, gradient.colorAt(x + w / 2f, y + h / 2f, x, y, w, h), strokeWidth, cap, join) + fun strokeColorRing(points: FloatArray, normals: FloatArray, colors: IntArray, strokeWidth: Float, shape: FloatArray? = null) { + val n = colors.size + for (i in 0 until n) { + val j = (i + 1) % n + if (colors[i] ushr 24 == 0 && colors[j] ushr 24 == 0) continue + line(points[i * 2], points[i * 2 + 1], points[j * 2], points[j * 2 + 1], PolyGradient.lerpArgb(colors[i], colors[j], 0.5f), strokeWidth) + } + } + fun text(text: String, x: Float, y: Float, color: Int, font: PolyFont) fun text(text: String, x: Float, y: Float, color: Int, font: PolyFont, letterSpacing: Float) { @@ -76,6 +85,10 @@ interface PolyRenderer { fun live(draw: (PolyRenderer) -> Unit) + fun live(x: Float, y: Float, w: Float, h: Float, key: () -> Int, draw: (PolyRenderer) -> Unit) = live(draw) + + fun backdropGlass(x: Float, y: Float, w: Float, h: Float, radius: Float, band: Float, refraction: Float, alpha: Float) {} + fun save() fun restore() diff --git a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/render/PolyTessellator.kt b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/render/PolyTessellator.kt index 8be40db28..81961235e 100644 --- a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/render/PolyTessellator.kt +++ b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/render/PolyTessellator.kt @@ -249,6 +249,58 @@ abstract class PolyTessellator : PolyRenderer { } } + override fun strokeColorRing(points: FloatArray, normals: FloatArray, colors: IntArray, strokeWidth: Float, shape: FloatArray?) { + beginDraw() + if (shape == null) return colorRing(points, normals, colors, strokeWidth) + unclipped(shape[0], shape[1], shape[2], shape[3], shape[4]) { colorRing(points, normals, colors, strokeWidth) } + } + + private var ringColors = IntArray(0) + private var ringPoints = FloatArray(0) + + private fun colorRing(points: FloatArray, normals: FloatArray, colors: IntArray, strokeWidth: Float) { + val n = colors.size + if (n < 2) return + val scale = scaleAvg() + if (scale <= 0f) return + val swPx = strokeWidth * scale + val alphaMul = if (swPx < 1f) swPx else 1f + val halfPx = max(swPx, 1f) / 2f + val band = max(halfPx - 0.5f, 0f) / scale + val edge = (halfPx + 0.5f) / scale + if (ringColors.size < n) { + ringColors = IntArray(n) + ringPoints = FloatArray(n * 8) + } + val c = ringColors + val p = ringPoints + val alpha = alphaMul * state.alpha + for (i in 0 until n) c[i] = mulAlpha(colors[i], alpha) + val s = state + for (i in 0 until n) { + if (c[(i + n - 1) % n] ushr 24 == 0 && c[i] ushr 24 == 0 && c[(i + 1) % n] ushr 24 == 0) continue + val x = points[i * 2]; val y = points[i * 2 + 1] + val nx = normals[i * 2]; val ny = normals[i * 2 + 1] + val o = i * 8 + p[o] = (x + nx * band) * s.sx + s.tx; p[o + 1] = (y + ny * band) * s.sy + s.ty + p[o + 2] = (x + nx * edge) * s.sx + s.tx; p[o + 3] = (y + ny * edge) * s.sy + s.ty + p[o + 4] = (x - nx * band) * s.sx + s.tx; p[o + 5] = (y - ny * band) * s.sy + s.ty + p[o + 6] = (x - nx * edge) * s.sx + s.tx; p[o + 7] = (y - ny * edge) * s.sy + s.ty + } + begin(whitePage(), false) + val w = PolyGlyphPage.WHITE_UV + for (i in 0 until n) { + val j = (i + 1) % n + val ci = c[i]; val cj = c[j] + if (ci ushr 24 == 0 && cj ushr 24 == 0) continue + val ti = ci and 0x00FFFFFF; val tj = cj and 0x00FFFFFF + val a = i * 8; val b = j * 8 + if (band > 0f) quadVerts(p[a], p[a + 1], w, w, ci, p[a + 4], p[a + 5], w, w, ci, p[b + 4], p[b + 5], w, w, cj, p[b], p[b + 1], w, w, cj) + quadVerts(p[a], p[a + 1], w, w, ci, p[a + 2], p[a + 3], w, w, ti, p[b + 2], p[b + 3], w, w, tj, p[b], p[b + 1], w, w, cj) + quadVerts(p[a + 4], p[a + 5], w, w, ci, p[a + 6], p[a + 7], w, w, ti, p[b + 6], p[b + 7], w, w, tj, p[b + 4], p[b + 5], w, w, cj) + } + } + override fun line(x1: Float, y1: Float, x2: Float, y2: Float, color: Int, strokeWidth: Float) { beginDraw() val dx = x2 - x1 From d5e0f2dc2cdef6480f20604f543fd18dffc609a6 Mon Sep 17 00:00:00 2001 From: Deftu Date: Mon, 2 Nov 2026 09:17:24 +0200 Subject: [PATCH 02/12] feat: draw backdrop glass and cache bounded live ops with Blaze3D on 1.21.11 and newer --- .../render/Mixin_GuiRendererKeptMeshes.java | 48 +++++ .../internal/ui/compose/BlazeRenderer.kt | 170 ++++++++++++--- .../internal/ui/compose/LiquidGlass.kt | 203 ++++++++++++++++++ .../oneconfig/shaders/core/liquid_glass.fsh | 75 +++++++ .../shaders/core/liquid_glass_legacy.fsh | 73 +++++++ 5 files changed, 543 insertions(+), 26 deletions(-) create mode 100644 minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/LiquidGlass.kt create mode 100644 minecraft/src/main/resources/assets/oneconfig/shaders/core/liquid_glass.fsh create mode 100644 minecraft/src/main/resources/assets/oneconfig/shaders/core/liquid_glass_legacy.fsh diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/render/Mixin_GuiRendererKeptMeshes.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/render/Mixin_GuiRendererKeptMeshes.java index afa9b2ccf..5d1c8441c 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/render/Mixin_GuiRendererKeptMeshes.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/render/Mixin_GuiRendererKeptMeshes.java @@ -1,6 +1,12 @@ package org.polyfrost.oneconfig.internal.mixin.render; //? if >= 1.21.11 { +import com.mojang.blaze3d.pipeline.RenderTarget; +//? if < 26.2 { +/*import com.mojang.blaze3d.vertex.VertexFormat; +import com.mojang.renderpearl.api.buffers.GpuBuffer; +*///?} +import com.mojang.renderpearl.api.buffers.GpuBufferSlice; import com.mojang.renderpearl.api.commands.RenderPass; import net.minecraft.client.gui.render.GuiRenderer; //? if >= 26.1 { @@ -9,24 +15,29 @@ //import net.minecraft.client.gui.render.state.GuiElementRenderState; import org.polyfrost.oneconfig.internal.ui.compose.GlHost; import org.polyfrost.oneconfig.internal.ui.compose.KeptMeshes; +import org.polyfrost.oneconfig.internal.ui.compose.LiquidGlass; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Coerce; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.util.List; +import java.util.function.Supplier; @Mixin(GuiRenderer.class) public class Mixin_GuiRendererKeptMeshes { @Shadow @Final private List draws; + @Unique private boolean oneconfig$splitting; //? if >= 26.2 { @Inject(method = "addElementToMesh", at = @At("TAIL")) private void oneconfig$noteKept(GuiElementRenderState element, CallbackInfo ci) { KeptMeshes.added(draws.get(draws.size() - 1), element); + LiquidGlass.added(draws.get(draws.size() - 1), element); } @Inject(method = "executeDraw", at = @At("HEAD"), cancellable = true) @@ -39,11 +50,13 @@ public class Mixin_GuiRendererKeptMeshes { @Inject(method = "addElementToMesh", at = @At("TAIL")) private void oneconfig$noteKept(GuiElementRenderState element, CallbackInfo ci) { KeptMeshes.added(meshesToDraw.size(), element); + LiquidGlass.added(meshesToDraw.size(), element); } @Inject(method = "recordDraws", at = @At("TAIL")) private void oneconfig$bindKept(CallbackInfo ci) { KeptMeshes.recorded(draws); + LiquidGlass.recorded(draws); } @Inject(method = "executeDraw", at = @At("HEAD"), cancellable = true) @@ -55,9 +68,44 @@ public class Mixin_GuiRendererKeptMeshes { } *///?} + //? if >= 26.2 { + @Shadow + private void executeDrawRange(Supplier label, RenderTarget mainRenderTarget, GpuBufferSlice dynamicTransforms, int startIndex, int endIndex) { + } + + @Inject(method = "executeDrawRange", at = @At("HEAD"), cancellable = true) + private void oneconfig$splitForGlass(Supplier label, RenderTarget mainRenderTarget, GpuBufferSlice dynamicTransforms, int startIndex, int endIndex, CallbackInfo ci) { + if (oneconfig$splitting || !LiquidGlass.has(draws, startIndex, endIndex)) return; + ci.cancel(); + oneconfig$splitting = true; + try { + LiquidGlass.split(draws, startIndex, endIndex, mainRenderTarget, (from, to) -> executeDrawRange(label, mainRenderTarget, dynamicTransforms, from, to)); + } finally { + oneconfig$splitting = false; + } + } + //?} else { + /*@Shadow + private void executeDrawRange(Supplier label, RenderTarget mainRenderTarget, GpuBufferSlice fogBuffer, GpuBufferSlice dynamicTransforms, GpuBuffer indexBuffer, VertexFormat.IndexType indexType, int startIndex, int endIndex) { + } + + @Inject(method = "executeDrawRange", at = @At("HEAD"), cancellable = true) + private void oneconfig$splitForGlass(Supplier label, RenderTarget mainRenderTarget, GpuBufferSlice fogBuffer, GpuBufferSlice dynamicTransforms, GpuBuffer indexBuffer, VertexFormat.IndexType indexType, int startIndex, int endIndex, CallbackInfo ci) { + if (oneconfig$splitting || !LiquidGlass.has(draws, startIndex, endIndex)) return; + ci.cancel(); + oneconfig$splitting = true; + try { + LiquidGlass.split(draws, startIndex, endIndex, mainRenderTarget, (from, to) -> executeDrawRange(label, mainRenderTarget, fogBuffer, dynamicTransforms, indexBuffer, indexType, from, to)); + } finally { + oneconfig$splitting = false; + } + } + *///?} + @Inject(method = "render", at = @At("TAIL")) private void oneconfig$endFrame(CallbackInfo ci) { KeptMeshes.endFrame(); + LiquidGlass.endFrame(); } } //?} diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/BlazeRenderer.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/BlazeRenderer.kt index 24a567c21..14354f0c1 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/BlazeRenderer.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/BlazeRenderer.kt @@ -76,6 +76,7 @@ import org.polyfrost.compose.render.PolyRenderer import org.polyfrost.oneconfig.internal.ui.TextureRenderer import org.polyfrost.oneconfig.internal.mixin.render.BufferBuilderAccessor import org.polyfrost.oneconfig.internal.mixin.render.RenderPipelinesAccessor +import kotlin.math.abs import kotlin.math.ceil import kotlin.math.floor import kotlin.math.max @@ -150,8 +151,21 @@ class BlazeRenderer(private val guiScale: Float, private val keepVertices: Boole override fun layer(mark: Any, alpha: Float) { val sub = ops.subList(mark as Int, ops.size) if (sub.isEmpty()) return - val op = LayerOp.of(ArrayList(sub), alpha, guiScale, screenW, screenH) + val all = ArrayList(sub) sub.clear() + var from = 0 + for (i in all.indices) { + val op = all[i] + if (op !is MeshOp || !op.mesh.glass) continue + if (i > from) addLayer(all.subList(from, i), alpha) + ops.add(MeshOp(op.mesh.faded(alpha))) + from = i + 1 + } + if (from == 0) addLayer(all, alpha) else if (from < all.size) addLayer(all.subList(from, all.size), alpha) + } + + private fun addLayer(list: List<(GuiGraphicsExtractor) -> Unit>, alpha: Float) { + val op = LayerOp.of(list, alpha, guiScale, screenW, screenH) if (op != null) ops.add(op) } @@ -254,7 +268,7 @@ class BlazeRenderer(private val guiScale: Float, private val keepVertices: Boole // the cached mask is white, so the tint is applied here as a premultiplied vertex color val color4 = (a shl 24) or ((outlineColor ushr 16 and 0xFF) * a / 255 shl 16) or ((outlineColor ushr 8 and 0xFF) * a / 255 shl 8) or ((outlineColor and 0xFF) * a / 255) - ops.add { g -> outline.draw(g, elementPose, x0, y0, color4, bounds, scissor) } + ops.add(OutlineOp(outline, elementPose, x0, y0, color4, bounds, scissor)) } } } @@ -328,6 +342,29 @@ class BlazeRenderer(private val guiScale: Float, private val keepVertices: Boole quadPx(x0, y0, x0 + w * state.sx, y0 + h * state.sy, u0, v0, u1, v1, (a shl 24) or (a shl 16) or (a shl 8) or a) } + override fun backdropGlass(x: Float, y: Float, w: Float, h: Float, radius: Float, band: Float, refraction: Float, alpha: Float) { + flush() + beginDraw() + val s = state + if (s.rot != null) return + val a = (alpha * s.alpha * 255f).roundToInt().coerceIn(0, 255) + if (a == 0) return + val x0 = min(x * s.sx + s.tx, (x + w) * s.sx + s.tx) + val x1 = max(x * s.sx + s.tx, (x + w) * s.sx + s.tx) + val y0 = min(y * s.sy + s.ty, (y + h) * s.sy + s.ty) + val y1 = max(y * s.sy + s.ty, (y + h) * s.sy + s.ty) + val scale = (abs(s.sx) + abs(s.sy)) / 2f + val bandPx = (band * scale).coerceIn(1f, 255f) + val box = rectGui(x0 - bandPx, y0 - bandPx, x1 + bandPx, y1 + bandPx) ?: return + val scissor = s.clip?.let { rectGui(it[0], it[1], it[2], it[3]) ?: return } + touch(x0, y0, x1, y1) + val r = (radius * scale).roundToInt().coerceIn(0, 255) + val k = (refraction.coerceIn(0f, 1f) * 255f).roundToInt() + val color = (a shl 24) or (r shl 16) or (bandPx.roundToInt() shl 8) or k + val verts = floatArrayOf(x0, y0, 0f, 0f, x0, y1, 0f, 1f, x1, y1, 1f, 1f, x1, y0, 1f, 0f) + ops.add(MeshOp(Mesh(LiquidGlass.pipeline, TextureSetup.noTexture(), elementPose, verts, IntArray(4) { color }, box, false, scissor))) + } + private var liveRenderer: BlazeRenderer? = null override fun live(draw: (PolyRenderer) -> Unit) { @@ -337,19 +374,77 @@ class BlazeRenderer(private val guiScale: Float, private val keepVertices: Boole ops.add(LiveOp(state.copy(), draw)) } - private inner class LiveOp(private val s: State, private val draw: (PolyRenderer) -> Unit) : (GuiGraphicsExtractor) -> Unit { + override fun live(x: Float, y: Float, w: Float, h: Float, key: () -> Int, draw: (PolyRenderer) -> Unit) { + flush() + beginDraw() + val s = state + val ax = x * s.sx + s.tx + val bx = (x + w) * s.sx + s.tx + val ay = y * s.sy + s.ty + val by = (y + h) * s.sy + s.ty + var x0 = min(ax, bx) + var x1 = max(ax, bx) + var y0 = min(ay, by) + var y1 = max(ay, by) + val m = s.rot + if (m != null) { + val l = min(m[0] * x0, m[0] * x1) + min(m[1] * y0, m[1] * y1) + m[4] + val r = max(m[0] * x0, m[0] * x1) + max(m[1] * y0, m[1] * y1) + m[4] + val t = min(m[2] * x0, m[2] * x1) + min(m[3] * y0, m[3] * y1) + m[5] + val b = max(m[2] * x0, m[2] * x1) + max(m[3] * y0, m[3] * y1) + m[5] + x0 = l + x1 = r + y0 = t + y1 = b + } + touch(x0, y0, x1, y1) + val box = rectGui(x0, y0, x1, y1) ?: return + ops.add(LiveOp(s.copy(), draw, key, box)) + } + + private inner class LiveOp( + private val s: State, private val draw: (PolyRenderer) -> Unit, private val key: (() -> Int)? = null, + override val box: ScreenRectangle? = null, private val dx: Int = 0, private val dy: Int = 0, + ) : (GuiGraphicsExtractor) -> Unit, LiveDraw { private var held: List = emptyList() + private var drawn: List<(GuiGraphicsExtractor) -> Unit>? = null + private var drawnKey = 0 + private var drawnBitmaps: Set = emptySet() + + fun faded(alpha: Float) = LiveOp(s.copy().also { it.alpha *= alpha }, draw, key, box, dx, dy) + + override fun shifted(dx: Int, dy: Int): (GuiGraphicsExtractor) -> Unit = LiveOp(s, draw, key, box?.shifted(dx, dy), this.dx + dx, this.dy + dy) - fun faded(alpha: Float) = LiveOp(s.copy().also { it.alpha *= alpha }, draw) + override fun changed(): Boolean { + val k = key ?: return true + val d = drawn ?: return true + return k() != drawnKey || d.any { LayerOp.stale(it) } + } override fun invoke(g: GuiGraphicsExtractor) { + val k = key?.invoke() + val cached = drawn + if (cached != null && k == drawnKey && cached.none { it is MeshOp && it.mesh.closed }) { + BitmapTextures.touch(drawnBitmaps) + for (op in cached) op(g) + return + } val r = liveRenderer ?: BlazeRenderer(guiScale).also { liveRenderer = it } r.state = s.copy() draw(r) - val out = r.finish() - for (layer in held) layer.release() - held = out.filterIsInstance() + val finished = r.finish() + val out = if (dx != 0 || dy != 0) finished.map { shiftOp(it, dx, dy) } else if (k != null) ArrayList(finished) else finished + if (held.isNotEmpty()) { + for (layer in held) layer.release() + held = emptyList() + } + if (out.any { it is LayerOp }) held = out.filterIsInstance() for (op in out) op(g) + if (k != null) { + drawn = out + drawnKey = k + drawnBitmaps = if (r.bitmaps.isEmpty()) emptySet() else HashSet(r.bitmaps) + } r.reset() GlyphAtlas.uploadDirty() } @@ -370,6 +465,7 @@ class BlazeRenderer(private val guiScale: Float, private val keepVertices: Boole internal class MeshOp(val mesh: Mesh) : (GuiGraphicsExtractor) -> Unit { override fun invoke(g: GuiGraphicsExtractor) { + if (mesh.glass) LiquidGlass.submit() //~ if >= 26.1 'submitGuiElement' -> 'addGuiElement' g.guiRenderState.addGuiElement(mesh) } @@ -434,6 +530,7 @@ class BlazeRenderer(private val guiScale: Float, private val keepVertices: Boole private var drawn = false val vertexCount get() = colors.size val closed: Boolean get() = textureSetup.texure0()?.isClosed == true + val glass: Boolean get() = pipeline === LiquidGlass.pipeline fun shifted(dx: Int, dy: Int) = Mesh( @@ -502,7 +599,7 @@ class BlazeRenderer(private val guiScale: Float, private val keepVertices: Boole } override fun pipeline() = if (gpu != null) KeptMeshes.pipeline(pipeline) else pipeline - override fun textureSetup() = textureSetup + override fun textureSetup() = if (glass) LiquidGlass.setup() else textureSetup override fun scissorArea(): ScreenRectangle? = scissor override fun bounds() = bounds } @@ -1172,18 +1269,11 @@ internal class LayerOp private constructor( private var composite: BlazeRenderer.Mesh? = null private var lastUsed = 0L - private fun moved(dx: Int, dy: Int) = LayerOp(ops, alpha, guiScale, bounds.shifted(dx, dy)) + fun moved(dx: Int, dy: Int) = LayerOp(ops, alpha, guiScale, bounds.shifted(dx, dy)) fun faded(a: Float) = LayerOp(ops, alpha * a, guiScale, bounds) - private fun stale(): Boolean = ops.any { - when (it) { - is BlazeRenderer.MeshOp, is BlazeRenderer.BakedTextOp -> false - is BlazeRenderer.LiveTextOp -> it.changed() - is LayerOp -> it.stale() - else -> true - } - } + private fun stale(): Boolean = ops.any { stale(it) } override fun invoke(g: GuiGraphicsExtractor) { lastUsed = System.nanoTime() @@ -1261,6 +1351,14 @@ internal class LayerOp private constructor( private val active = ArrayList() private var sweptAt = 0L + fun stale(op: (GuiGraphicsExtractor) -> Unit): Boolean = when (op) { + is BlazeRenderer.MeshOp, is BlazeRenderer.BakedTextOp -> false + is BlazeRenderer.LiveTextOp -> op.changed() + is LayerOp -> op.stale() + is LiveDraw -> op.changed() + else -> true + } + fun of(ops: List<(GuiGraphicsExtractor) -> Unit>, alpha: Float, guiScale: Float, screenW: Int, screenH: Int): LayerOp? { var bounds: ScreenRectangle? = null fun add(b: ScreenRectangle) { bounds = bounds?.let { union(it, b) } ?: b } @@ -1269,20 +1367,13 @@ internal class LayerOp private constructor( is BlazeRenderer.BakedTextOp -> op.meshes?.forEach { add(it.bounds()) } ?: return LayerOp(ops, alpha, guiScale, ScreenRectangle(0, 0, screenW, screenH)) is BlazeRenderer.LiveTextOp -> op.meshes?.forEach { add(it.bounds()) } ?: return LayerOp(ops, alpha, guiScale, ScreenRectangle(0, 0, screenW, screenH)) is LayerOp -> add(op.bounds) + is LiveDraw -> add(op.box ?: return LayerOp(ops, alpha, guiScale, ScreenRectangle(0, 0, screenW, screenH))) else -> return LayerOp(ops, alpha, guiScale, ScreenRectangle(0, 0, screenW, screenH)) } val b = bounds?.intersection(ScreenRectangle(0, 0, screenW, screenH)) ?: return null if (b.width() <= 0 || b.height() <= 0) return null val dx = b.left(); val dy = b.top() - val shifted = ops.map { op -> - when (op) { - is BlazeRenderer.MeshOp -> BlazeRenderer.MeshOp(op.mesh.shifted(dx, dy)) - is BlazeRenderer.BakedTextOp -> op.shifted(dx, dy) - is BlazeRenderer.LiveTextOp -> op.shifted(dx, dy) - else -> (op as LayerOp).moved(dx, dy) - } - } - return LayerOp(shifted, alpha, guiScale, b) + return LayerOp(ops.map { shiftOp(it, dx, dy) }, alpha, guiScale, b) } fun retire(old: List<(GuiGraphicsExtractor) -> Unit>, next: List<(GuiGraphicsExtractor) -> Unit>) { @@ -1313,6 +1404,33 @@ internal class LayerOp private constructor( private fun ScreenRectangle.shifted(dx: Int, dy: Int) = ScreenRectangle(left() - dx, top() - dy, width(), height()) +internal interface LiveDraw { + val box: ScreenRectangle? + fun changed(): Boolean + fun shifted(dx: Int, dy: Int): (GuiGraphicsExtractor) -> Unit +} + +private class OutlineOp( + private val outline: McOutlines.Outline, private val pose: Matrix3x2f, private val x0: Float, private val y0: Float, + private val color: Int, private val bounds: ScreenRectangle, private val scissor: ScreenRectangle?, +) : (GuiGraphicsExtractor) -> Unit { + fun shifted(dx: Int, dy: Int) = OutlineOp( + outline, Matrix3x2f().translation(-dx.toFloat(), -dy.toFloat()).mul(pose), x0, y0, color, bounds.shifted(dx, dy), scissor?.shifted(dx, dy), + ) + + override fun invoke(g: GuiGraphicsExtractor) = outline.draw(g, pose, x0, y0, color, bounds, scissor) +} + +private fun shiftOp(op: (GuiGraphicsExtractor) -> Unit, dx: Int, dy: Int): (GuiGraphicsExtractor) -> Unit = when (op) { + is BlazeRenderer.MeshOp -> BlazeRenderer.MeshOp(op.mesh.shifted(dx, dy)) + is BlazeRenderer.BakedTextOp -> op.shifted(dx, dy) + is BlazeRenderer.LiveTextOp -> op.shifted(dx, dy) + is LayerOp -> op.moved(dx, dy) + is LiveDraw -> op.shifted(dx, dy) + is OutlineOp -> op.shifted(dx, dy) + else -> { g -> shiftedDraw(g, dx, dy, op) } +} + private fun shiftedDraw(g: GuiGraphicsExtractor, dx: Int, dy: Int, draw: (GuiGraphicsExtractor) -> Unit) { g.pose().pushMatrix() g.pose().translate(-dx.toFloat(), -dy.toFloat()) diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/LiquidGlass.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/LiquidGlass.kt new file mode 100644 index 000000000..617a67713 --- /dev/null +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/LiquidGlass.kt @@ -0,0 +1,203 @@ +//~ main_render_target +package org.polyfrost.oneconfig.internal.ui.compose + +//? if >= 1.21.11 { +import com.mojang.blaze3d.pipeline.RenderTarget +import com.mojang.blaze3d.systems.RenderSystem +//? if >= 26.3 { +import com.mojang.renderpearl.api.pipeline.RenderPipeline +//?} else +//import com.mojang.blaze3d.pipeline.RenderPipeline +import com.mojang.renderpearl.api.textures.FilterMode +import com.mojang.renderpearl.api.textures.GpuTexture +import com.mojang.renderpearl.api.textures.GpuTextureView +import java.util.Collections +import java.util.IdentityHashMap +//? if < 26.2 +//import java.lang.reflect.Method +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.navigation.ScreenRectangle +import net.minecraft.client.gui.render.TextureSetup +//? if >= 26.1 { +import net.minecraft.client.renderer.state.gui.GuiElementRenderState +//?} else +//import net.minecraft.client.gui.render.state.GuiElementRenderState +import net.minecraft.resources.Identifier +import org.polyfrost.oneconfig.internal.mixin.render.RenderPipelinesAccessor +import org.polyfrost.oneconfig.internal.ui.hud.GuiTargetRedirect +import kotlin.math.max +import kotlin.math.min + +internal object LiquidGlass { + val pipeline: RenderPipeline = RenderPipeline.builder(RenderPipelinesAccessor.`oneconfig$getGuiTexturedSnippet`()) + .withLocation(Identifier.fromNamespaceAndPath("oneconfig", "pipeline/liquid_glass")) + //? if >= 26.3 { + .withFragmentShader(Identifier.fromNamespaceAndPath("oneconfig", "core/liquid_glass")) + //?} else + //.withFragmentShader(Identifier.fromNamespaceAndPath("oneconfig", "core/liquid_glass_legacy")) + .build() + + private var texture: GpuTexture? = null + private var view: GpuTextureView? = null + private var setup: TextureSetup = TextureSetup.noTexture() + private val drawBounds = IdentityHashMap() + private val glassDraws: MutableSet = Collections.newSetFromMap(IdentityHashMap()) + private val dirty = ArrayList() + private var submitted = false + + fun interface Range { + fun draw(from: Int, to: Int) + } + + fun setup(): TextureSetup { + val main = Minecraft.getInstance().gameRenderer.mainRenderTarget() + val color = main.colorTexture ?: return setup + val current = texture + if (current == null || current.isClosed || current.getWidth(0) != main.width || current.getHeight(0) != main.height || current.format != color.format) { + view?.close() + current?.close() + val device = RenderSystem.getDevice() + val created = device.createTexture({ "OneConfig liquid glass backdrop" }, GpuTexture.USAGE_COPY_DST or GpuTexture.USAGE_TEXTURE_BINDING, color.format, main.width, main.height, 1, 1) + val createdView = device.createTextureView(created) + texture = created + view = createdView + setup = TextureSetup.singleTexture(createdView, RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR)) + } + return setup + } + + fun submit() { + submitted = true + } + + //? if < 26.2 { + /*private val byIndex = HashMap() + private var drawClass: Class<*>? = null + private var drawPipeline: Method? = null + + @JvmStatic + fun added(index: Int, element: GuiElementRenderState) { + if (!submitted) return + added(byIndex.getOrPut(index) { Any() }, element) + } + + private fun pipelineOf(draw: Any): Any? { + val cls = draw.javaClass + if (cls !== drawClass) { + drawClass = cls + drawPipeline = runCatching { cls.recordComponents?.firstOrNull { it.type === RenderPipeline::class.java }?.accessor?.also { it.isAccessible = true } }.getOrNull() + } + return drawPipeline?.invoke(draw) + } + + @JvmStatic + fun recorded(draws: List<*>) { + if (byIndex.isEmpty()) return + val byPipeline = draws.firstOrNull()?.let { pipelineOf(it) } != null + var aligned = true + for ((i, key) in byIndex) { + val glass = glassDraws.remove(key) + val known = drawBounds.containsKey(key) + val bounds = drawBounds.remove(key) + val draw = draws.getOrNull(i) + if (draw == null) { + if (glass) aligned = false + continue + } + if (byPipeline && glass != (pipelineOf(draw) === pipeline)) aligned = false + if (known) drawBounds[draw] = bounds + if (!byPipeline && glass) glassDraws.add(draw) + } + if (byPipeline) for (draw in draws) if (draw != null && pipelineOf(draw) === pipeline) glassDraws.add(draw) + if (!aligned) for (draw in draws) drawBounds.remove(draw) + byIndex.clear() + } + *///?} + + @JvmStatic + fun added(draw: Any, element: GuiElementRenderState) { + if (!submitted) return + val b = element.bounds() + if (drawBounds.containsKey(draw)) { + val prev = drawBounds[draw] + drawBounds[draw] = if (prev == null || b == null) null else union(prev, b) + } else { + drawBounds[draw] = b + } + if (element.pipeline() === pipeline) glassDraws.add(draw) + } + + @JvmStatic + fun has(draws: List<*>, start: Int, end: Int): Boolean { + if (glassDraws.isEmpty()) return false + for (i in start until end) if (draws[i] in glassDraws) return true + return false + } + + @JvmStatic + fun split(draws: List<*>, start: Int, end: Int, target: RenderTarget, range: Range) { + val redirected = GuiTargetRedirect.target != null + var copied = false + dirty.clear() + var from = start + var scanned = start + for (i in start until end) { + val draw = draws[i] ?: continue + if (draw !in glassDraws) continue + if (redirected) { + if (i > from) range.draw(from, i) + from = i + 1 + continue + } + for (k in scanned until i) dirty.add(draws[k]?.let { drawBounds[it] }) + scanned = i + if (copied && !covers(drawBounds[draw])) continue + if (i > from) range.draw(from, i) + if (!copy(target)) { + from = i + 1 + scanned = i + 1 + continue + } + copied = true + dirty.clear() + from = i + } + if (end > from) range.draw(from, end) + dirty.clear() + } + + @JvmStatic + fun endFrame() { + if (GuiTargetRedirect.target != null) return + submitted = false + drawBounds.clear() + glassDraws.clear() + } + + private fun covers(region: ScreenRectangle?): Boolean { + if (region == null) return true + for (d in dirty) { + if (d == null) return true + if (d.left() < region.right() && region.left() < d.right() && d.top() < region.bottom() && region.top() < d.bottom()) return true + } + return false + } + + private fun copy(target: RenderTarget): Boolean { + val src = target.colorTexture ?: return false + val dst = texture ?: return false + if (dst.isClosed) return false + val w = src.getWidth(0) + val h = src.getHeight(0) + if (dst.getWidth(0) != w || dst.getHeight(0) != h || dst.format != src.format) return false + RenderSystem.getDevice().createCommandEncoder().copyTextureToTexture(src, dst, 0, 0, 0, 0, 0, w, h) + return true + } + + private fun union(a: ScreenRectangle, b: ScreenRectangle): ScreenRectangle { + val l = min(a.left(), b.left()) + val t = min(a.top(), b.top()) + return ScreenRectangle(l, t, max(a.right(), b.right()) - l, max(a.bottom(), b.bottom()) - t) + } +} +//?} diff --git a/minecraft/src/main/resources/assets/oneconfig/shaders/core/liquid_glass.fsh b/minecraft/src/main/resources/assets/oneconfig/shaders/core/liquid_glass.fsh new file mode 100644 index 000000000..317f08265 --- /dev/null +++ b/minecraft/src/main/resources/assets/oneconfig/shaders/core/liquid_glass.fsh @@ -0,0 +1,75 @@ +#version 330 +#extension GL_ARB_separate_shader_objects : require + +layout(std140) uniform DynamicTransforms { + mat4 ModelViewMat; + mat4 TextureMat; + vec4 ColorModulator; + vec3 ModelOffset; +}; + +uniform sampler2D Sampler0; + +layout(location = 0) in vec2 texCoord0; +layout(location = 1) in vec4 vertexColor; + +#define FRAG_OUT layout(location = 0) out +FRAG_OUT vec4 fragColor; + +float roundedBox(vec2 p, vec2 halfSize, float radius) { + vec2 q = abs(p) - halfSize + radius; + return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius; +} + +vec3 frosted(vec2 frag, vec2 texel) { + vec3 c = textureLod(Sampler0, frag * texel, 0.0).rgb * 0.2; + c += textureLod(Sampler0, (frag + vec2(2.5, 0.0)) * texel, 0.0).rgb * 0.1; + c += textureLod(Sampler0, (frag - vec2(2.5, 0.0)) * texel, 0.0).rgb * 0.1; + c += textureLod(Sampler0, (frag + vec2(0.0, 2.5)) * texel, 0.0).rgb * 0.1; + c += textureLod(Sampler0, (frag - vec2(0.0, 2.5)) * texel, 0.0).rgb * 0.1; + c += textureLod(Sampler0, (frag + vec2(1.8, 1.8)) * texel, 0.0).rgb * 0.1; + c += textureLod(Sampler0, (frag - vec2(1.8, 1.8)) * texel, 0.0).rgb * 0.1; + c += textureLod(Sampler0, (frag + vec2(1.8, -1.8)) * texel, 0.0).rgb * 0.1; + c += textureLod(Sampler0, (frag - vec2(1.8, -1.8)) * texel, 0.0).rgb * 0.1; + return c; +} + +void main() { + vec2 step = vec2(dFdx(texCoord0.x), dFdy(texCoord0.y)); + vec2 size = 1.0 / max(abs(step), vec2(1e-6)); + vec2 halfSize = size * 0.5; + vec2 p = (texCoord0 - 0.5) * size; + float radius = min(vertexColor.r * 255.0, min(halfSize.x, halfSize.y)); + float band = max(vertexColor.g * 255.0, 1.0); + float strength = vertexColor.b; + + float d = roundedBox(p, halfSize, radius); + float coverage = clamp(0.5 - d, 0.0, 1.0) * vertexColor.a; + if (coverage <= 0.0) { + discard; + } + + float t = clamp(1.0 + d / band, 0.0, 1.0); + float bend = 0.0; + vec2 offset = vec2(0.0); + if (t > 0.0) { + vec2 e = vec2(0.5, 0.0); + vec2 n = vec2( + roundedBox(p + e.xy, halfSize, radius) - roundedBox(p - e.xy, halfSize, radius), + roundedBox(p + e.yx, halfSize, radius) - roundedBox(p - e.yx, halfSize, radius) + ); + n = n / max(length(n), 1e-4); + bend = 1.0 - sqrt(max(1.0 - t * t, 0.0)); + offset = n * band * strength * bend * sign(step); + } + float spread = 0.12 * bend; + + vec2 texel = 1.0 / vec2(textureSize(Sampler0, 0)); + vec2 frag = gl_FragCoord.xy; + vec3 color = frosted(frag + offset, texel); + if (bend > 0.02) { + color.r = frosted(frag + offset * (1.0 + spread), texel).r; + color.b = frosted(frag + offset * (1.0 - spread), texel).b; + } + fragColor = vec4(color, coverage) * ColorModulator; +} diff --git a/minecraft/src/main/resources/assets/oneconfig/shaders/core/liquid_glass_legacy.fsh b/minecraft/src/main/resources/assets/oneconfig/shaders/core/liquid_glass_legacy.fsh new file mode 100644 index 000000000..788f06ea0 --- /dev/null +++ b/minecraft/src/main/resources/assets/oneconfig/shaders/core/liquid_glass_legacy.fsh @@ -0,0 +1,73 @@ +#version 330 + +layout(std140) uniform DynamicTransforms { + mat4 ModelViewMat; + vec4 ColorModulator; + vec3 ModelOffset; + mat4 TextureMat; +}; + +uniform sampler2D Sampler0; + +in vec2 texCoord0; +in vec4 vertexColor; + +out vec4 fragColor; + +float roundedBox(vec2 p, vec2 halfSize, float radius) { + vec2 q = abs(p) - halfSize + radius; + return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius; +} + +vec3 frosted(vec2 frag, vec2 texel) { + vec3 c = textureLod(Sampler0, frag * texel, 0.0).rgb * 0.2; + c += textureLod(Sampler0, (frag + vec2(2.5, 0.0)) * texel, 0.0).rgb * 0.1; + c += textureLod(Sampler0, (frag - vec2(2.5, 0.0)) * texel, 0.0).rgb * 0.1; + c += textureLod(Sampler0, (frag + vec2(0.0, 2.5)) * texel, 0.0).rgb * 0.1; + c += textureLod(Sampler0, (frag - vec2(0.0, 2.5)) * texel, 0.0).rgb * 0.1; + c += textureLod(Sampler0, (frag + vec2(1.8, 1.8)) * texel, 0.0).rgb * 0.1; + c += textureLod(Sampler0, (frag - vec2(1.8, 1.8)) * texel, 0.0).rgb * 0.1; + c += textureLod(Sampler0, (frag + vec2(1.8, -1.8)) * texel, 0.0).rgb * 0.1; + c += textureLod(Sampler0, (frag - vec2(1.8, -1.8)) * texel, 0.0).rgb * 0.1; + return c; +} + +void main() { + vec2 step = vec2(dFdx(texCoord0.x), dFdy(texCoord0.y)); + vec2 size = 1.0 / max(abs(step), vec2(1e-6)); + vec2 halfSize = size * 0.5; + vec2 p = (texCoord0 - 0.5) * size; + float radius = min(vertexColor.r * 255.0, min(halfSize.x, halfSize.y)); + float band = max(vertexColor.g * 255.0, 1.0); + float strength = vertexColor.b; + + float d = roundedBox(p, halfSize, radius); + float coverage = clamp(0.5 - d, 0.0, 1.0) * vertexColor.a; + if (coverage <= 0.0) { + discard; + } + + float t = clamp(1.0 + d / band, 0.0, 1.0); + float bend = 0.0; + vec2 offset = vec2(0.0); + if (t > 0.0) { + vec2 e = vec2(0.5, 0.0); + vec2 n = vec2( + roundedBox(p + e.xy, halfSize, radius) - roundedBox(p - e.xy, halfSize, radius), + roundedBox(p + e.yx, halfSize, radius) - roundedBox(p - e.yx, halfSize, radius) + ); + n = n / max(length(n), 1e-4); + bend = 1.0 - sqrt(max(1.0 - t * t, 0.0)); + offset = n * band * strength * bend * sign(step); + } + float spread = 0.12 * bend; + + vec2 texel = 1.0 / vec2(textureSize(Sampler0, 0)); + vec2 frag = gl_FragCoord.xy; + vec3 color = frosted(frag + offset, texel); + if (bend > 0.02) { + color.r = frosted(frag + offset * (1.0 + spread), texel).r; + color.b = frosted(frag + offset * (1.0 - spread), texel).b; + } + fragColor = vec4(color, coverage) * ColorModulator; +} From d9ab88995bdb389456bb74f35787e506dbaf41c1 Mon Sep 17 00:00:00 2001 From: Deftu Date: Mon, 2 Nov 2026 09:17:24 +0200 Subject: [PATCH 03/12] feat: draw backdrop glass and cache bounded live ops with the OpenGL renderer --- .../oneconfig/internal/ui/compose/GlGlass.kt | 238 ++++++++++++++++++ .../oneconfig/internal/ui/compose/GlHost.kt | 1 + .../internal/ui/compose/GlRenderer.kt | 195 +++++++++++--- 3 files changed, 397 insertions(+), 37 deletions(-) create mode 100644 minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/GlGlass.kt diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/GlGlass.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/GlGlass.kt new file mode 100644 index 000000000..ff34b6f2d --- /dev/null +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/GlGlass.kt @@ -0,0 +1,238 @@ +package org.polyfrost.oneconfig.internal.ui.compose + +import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.GL12 +import org.lwjgl.opengl.GL20 +import org.lwjgl.opengl.GL30 +import org.slf4j.LoggerFactory +import java.nio.ByteBuffer +import kotlin.math.ceil +import kotlin.math.floor +import kotlin.math.max +import kotlin.math.min + +internal object GlGlass { + class Key + + private val LOG = LoggerFactory.getLogger(GlGlass::class.java) + + private var program = 0 + private var failed = false + private var uScale = 0 + private var uOffset = 0 + private var uTexel = 0 + private var texture = 0 + private var fbo = 0 + private var texW = 0 + private var texH = 0 + private var target = -1 + private var targetW = 0 + private var targetH = 0 + + fun begin(target: Int, w: Int, h: Int) { + this.target = target + targetW = w + targetH = h + } + + fun end() { + target = -1 + } + + fun use(b: GlProgram.Batch, core: Boolean, scaleX: Float, scaleY: Float, offsetX: Float, offsetY: Float): Boolean { + if (target < 0 || failed || b.colors.isEmpty() || targetW <= 0 || targetH <= 0) return false + if (!copy(b, scaleX, scaleY, offsetX, offsetY)) return false + if (program == 0 && !init(core)) { + release() + return false + } + GL20.glUseProgram(program) + GL20.glUniform2f(uScale, scaleX, scaleY) + GL20.glUniform2f(uOffset, offsetX, offsetY) + GL20.glUniform2f(uTexel, 1f / texW, 1f / texH) + GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture) + return true + } + + fun release() { + if (fbo != 0) GL30.glDeleteFramebuffers(fbo) + if (texture != 0) GL11.glDeleteTextures(texture) + fbo = 0 + texture = 0 + texW = 0 + texH = 0 + } + + private fun copy(glass: GlProgram.Batch, scaleX: Float, scaleY: Float, offsetX: Float, offsetY: Float): Boolean { + val v = glass.verts + val c = glass.colors + var left = Float.MAX_VALUE + var top = Float.MAX_VALUE + var right = -Float.MAX_VALUE + var bottom = -Float.MAX_VALUE + for (i in c.indices) { + val o = i * 4 + val fx = (v[o] * scaleX + offsetX + 1f) * 0.5f * targetW + val fy = (v[o + 1] * scaleY + offsetY + 1f) * 0.5f * targetH + val reach = ((c[i] ushr 8) and 0xFF) * (c[i] and 0xFF) / 255f * 1.12f + 2.5f + left = min(left, fx - reach) + top = min(top, fy - reach) + right = max(right, fx + reach) + bottom = max(bottom, fy + reach) + } + val x0 = max(floor(left).toInt() - 1, 0) + val y0 = max(floor(top).toInt() - 1, 0) + val x1 = min(ceil(right).toInt() + 1, targetW) + val y1 = min(ceil(bottom).toInt() + 1, targetH) + if (x1 <= x0 || y1 <= y0) return false + try { + ensure(targetW, targetH) + GL30.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, target) + GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fbo) + GL30.glBlitFramebuffer(x0, y0, x1, y1, x0, y0, x1, y1, GL11.GL_COLOR_BUFFER_BIT, GL11.GL_NEAREST) + GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, target) + return true + } catch (e: Throwable) { + LOG.warn("OneConfig backdrop glass is unavailable", e) + failed = true + GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, target) + GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0) + release() + return false + } + } + + private fun ensure(w: Int, h: Int) { + if (texture != 0 && texW == w && texH == h) return + val max = GL11.glGetInteger(GL11.GL_MAX_TEXTURE_SIZE) + check(max <= 0 || (w <= max && h <= max)) { "viewport ${w}x$h is past the max texture size ($max)" } + if (texture == 0) texture = GL11.glGenTextures() + if (fbo == 0) fbo = GL30.glGenFramebuffers() + texW = 0 + texH = 0 + GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture) + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR) + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR) + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE) + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE) + GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, w, h, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, null as ByteBuffer?) + GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo) + GL30.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, texture, 0) + val status = GL30.glCheckFramebufferStatus(GL30.GL_FRAMEBUFFER) + check(status == GL30.GL_FRAMEBUFFER_COMPLETE) { "snapshot framebuffer is incomplete (0x${Integer.toHexString(status)})" } + texW = w + texH = h + } + + private fun init(core: Boolean): Boolean { + val p = GL20.glCreateProgram() + val vs = GL20.glCreateShader(GL20.GL_VERTEX_SHADER) + val fs = GL20.glCreateShader(GL20.GL_FRAGMENT_SHADER) + try { + compile(vs, if (core) GlProgram.VS_150 else GlProgram.VS_120) + compile(fs, if (core) FS_150 else FS_120) + GL20.glAttachShader(p, vs) + GL20.glAttachShader(p, fs) + GL20.glBindAttribLocation(p, 0, "aPos") + GL20.glBindAttribLocation(p, 1, "aUv") + GL20.glBindAttribLocation(p, 2, "aColor") + GL20.glLinkProgram(p) + check(GL20.glGetProgrami(p, GL20.GL_LINK_STATUS) != 0) { GL20.glGetProgramInfoLog(p, 4096) } + } catch (e: Throwable) { + LOG.warn("OneConfig backdrop glass is unavailable", e) + GL20.glDeleteProgram(p) + GL20.glDeleteShader(vs) + GL20.glDeleteShader(fs) + failed = true + return false + } + GL20.glDeleteShader(vs) + GL20.glDeleteShader(fs) + uScale = GL20.glGetUniformLocation(p, "uScale") + uOffset = GL20.glGetUniformLocation(p, "uOffset") + uTexel = GL20.glGetUniformLocation(p, "uTexel") + GL20.glUseProgram(p) + GL20.glUniform1i(GL20.glGetUniformLocation(p, "uTex"), 0) + program = p + return true + } + + private fun compile(shader: Int, src: String) { + GL20.glShaderSource(shader, src) + GL20.glCompileShader(shader) + check(GL20.glGetShaderi(shader, GL20.GL_COMPILE_STATUS) != 0) { GL20.glGetShaderInfoLog(shader, 4096) } + } + + private const val GLASS_BODY = """ +uniform sampler2D uTex; uniform vec2 uTexel; + +float roundedBox(vec2 p, vec2 halfSize, float radius) { + vec2 q = abs(p) - halfSize + radius; + return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius; +} + +vec3 frosted(vec2 frag) { + vec3 c = TEX(uTex, frag * uTexel).rgb * 0.2; + c += TEX(uTex, (frag + vec2(2.5, 0.0)) * uTexel).rgb * 0.1; + c += TEX(uTex, (frag - vec2(2.5, 0.0)) * uTexel).rgb * 0.1; + c += TEX(uTex, (frag + vec2(0.0, 2.5)) * uTexel).rgb * 0.1; + c += TEX(uTex, (frag - vec2(0.0, 2.5)) * uTexel).rgb * 0.1; + c += TEX(uTex, (frag + vec2(1.8, 1.8)) * uTexel).rgb * 0.1; + c += TEX(uTex, (frag - vec2(1.8, 1.8)) * uTexel).rgb * 0.1; + c += TEX(uTex, (frag + vec2(1.8, -1.8)) * uTexel).rgb * 0.1; + c += TEX(uTex, (frag - vec2(1.8, -1.8)) * uTexel).rgb * 0.1; + return c; +} + +vec4 glass() { + vec2 st = vec2(dFdx(vUv.x), dFdy(vUv.y)); + vec2 size = 1.0 / max(abs(st), vec2(1e-6)); + vec2 halfSize = size * 0.5; + vec2 p = (vUv - 0.5) * size; + float radius = min(vColor.r * 255.0, min(halfSize.x, halfSize.y)); + float band = max(vColor.g * 255.0, 1.0); + float strength = vColor.b; + + float d = roundedBox(p, halfSize, radius); + float coverage = clamp(0.5 - d, 0.0, 1.0) * vColor.a; + if (coverage <= 0.0) { + return vec4(0.0); + } + + vec2 e = vec2(0.5, 0.0); + vec2 n = vec2( + roundedBox(p + e.xy, halfSize, radius) - roundedBox(p - e.xy, halfSize, radius), + roundedBox(p + e.yx, halfSize, radius) - roundedBox(p - e.yx, halfSize, radius) + ); + n = n / max(length(n), 1e-4); + + float t = clamp(1.0 + d / band, 0.0, 1.0); + float bend = 1.0 - sqrt(max(1.0 - t * t, 0.0)); + vec2 offset = n * band * strength * bend * sign(st); + float spread = 0.12 * bend; + + vec2 frag = gl_FragCoord.xy; + vec3 color = frosted(frag + offset); + if (bend > 0.02) { + color.r = frosted(frag + offset * (1.0 + spread)).r; + color.b = frosted(frag + offset * (1.0 - spread)).b; + } + return vec4(color * coverage, coverage); +} +""" + + private const val FS_150 = """#version 150 +#define TEX texture +in vec2 vUv; in vec4 vColor; +out vec4 fragColor; +$GLASS_BODY +void main() { fragColor = glass(); } +""" + + private const val FS_120 = """#version 120 +#define TEX texture2D +varying vec2 vUv; varying vec4 vColor; +$GLASS_BODY +void main() { gl_FragColor = glass(); } +""" +} diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/GlHost.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/GlHost.kt index 9b436ac85..a1921096d 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/GlHost.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/GlHost.kt @@ -436,6 +436,7 @@ object GlHost { if (paint == null) { screenFrame = null GlBlur.release() + GlGlass.release() } else { glScreen = null if (glBlurAlpha > 0f && !vanillaPipeline) drawBlur(fbo, glBlurAlpha) diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/GlRenderer.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/GlRenderer.kt index f17d93716..ac3836e76 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/GlRenderer.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/GlRenderer.kt @@ -23,6 +23,7 @@ import java.nio.ByteBuffer import java.nio.ByteOrder import java.nio.IntBuffer import java.util.IdentityHashMap +import kotlin.math.abs import kotlin.math.ceil import kotlin.math.floor import kotlin.math.max @@ -47,15 +48,36 @@ class GlRenderer private constructor(private val inLive: Boolean) : PolyTessella private val batches = ArrayList() - private class Live(val at: Int, val state: State, val draw: (PolyRenderer) -> Unit, val key: (() -> Int)? = null) { - private var last: Int? = null + private class Live( + val at: Int, val state: State, val draw: (PolyRenderer) -> Unit, val key: (() -> Int)? = null, val box: FloatArray? = null, + private val memo: Memo = Memo(), + ) { + class Memo { + var key = 0 + var out: List? = null + } + + fun moved(at: Int) = Live(at, state, draw, key, box, memo) - fun moved(at: Int) = Live(at, state, draw, key).also { it.last = last } + val stamp: Int get() = memo.key + + fun changed(since: Int): Boolean { + val k = key ?: return true + return memo.out == null || k() != since + } - fun changed() = key == null || key() != last + fun cached(): List? { + val k = key ?: return null + val value = k() + val out = memo.out + if (out != null && value == memo.key && out.all { GlTextures.alive(it.texture) }) return out + memo.key = value + memo.out = null + return null + } - fun commit() { - last = key?.invoke() + fun keep(out: List) { + if (key != null) memo.out = out } } private val lives = ArrayList() @@ -116,12 +138,40 @@ class GlRenderer private constructor(private val inLive: Boolean) : PolyTessella val sub = batches.subList(m[0], batches.size) val subLives = lives.subList(m[1], lives.size) if (sub.isEmpty() && subLives.isEmpty()) return - val shifted = subLives.map { it.moved(it.at - m[0]) } - val live = if (shifted.isEmpty()) null else ({ shifted.forEach(Live::commit); segments(shifted) }) - val group = Group.of(ArrayList(sub), live, { shifted.any(Live::changed) }, alpha, screenW, screenH) + val out = ArrayList() + var start = 0 + var li = 0 + for (i in 0..sub.size) { + if (i < sub.size && sub[i].texture !is GlGlass.Key) continue + val run = ArrayList() + while (li < subLives.size && subLives[li].at - m[0] <= i) { + val l = subLives[li++] + run.add(l.moved(l.at - m[0] - start)) + } + grouped(ArrayList(sub.subList(start, i)), run, alpha)?.let { out.add(it.batch) } + if (i < sub.size) out.add(sub[i].faded(alpha)) + start = i + 1 + } sub.clear() subLives.clear() - if (group != null) batches.add(group.batch) + batches.addAll(out) + } + + private fun grouped(run: List, runLives: List, alpha: Float): Group? { + if (run.isEmpty() && runLives.isEmpty()) return null + val keys = IntArray(runLives.size) + val live = if (runLives.isEmpty()) null else ({ segments(runLives).also { for (i in runLives.indices) keys[i] = runLives[i].stamp } }) + return Group.of(run, live, { runLives.indices.any { runLives[it].changed(keys[it]) } }, liveBox(runLives), alpha, screenW, screenH) + } + + private fun liveBox(lives: List): FloatArray? { + if (lives.isEmpty()) return null + val out = floatArrayOf(Float.MAX_VALUE, Float.MAX_VALUE, -Float.MAX_VALUE, -Float.MAX_VALUE) + for (l in lives) { + val b = l.box ?: return null + out[0] = min(out[0], b[0]); out[1] = min(out[1], b[1]); out[2] = max(out[2], b[2]); out[3] = max(out[3], b[3]) + } + return out } override fun fade(mark: Any, alpha: Float): Boolean { @@ -129,7 +179,7 @@ class GlRenderer private constructor(private val inLive: Boolean) : PolyTessella for (i in m[0] until batches.size) batches[i] = batches[i].faded(alpha) for (i in m[1] until lives.size) { val l = lives[i] - lives[i] = Live(l.at, l.state.copy().also { it.alpha *= alpha }, l.draw, l.key) + lives[i] = Live(l.at, l.state.copy().also { it.alpha *= alpha }, l.draw, l.key, l.box) } return true } @@ -142,6 +192,24 @@ class GlRenderer private constructor(private val inLive: Boolean) : PolyTessella lives.add(Live(batches.size, state.copy(), draw)) } + override fun live(x: Float, y: Float, w: Float, h: Float, key: () -> Int, draw: (PolyRenderer) -> Unit) { + if (inLive) return draw(this) + flush() + beginDraw() + val s = state + val px = floatArrayOf(x * s.sx + s.tx, (x + w) * s.sx + s.tx, (x + w) * s.sx + s.tx, x * s.sx + s.tx) + val py = floatArrayOf(y * s.sy + s.ty, y * s.sy + s.ty, (y + h) * s.sy + s.ty, (y + h) * s.sy + s.ty) + val m = s.rot + val box = floatArrayOf(Float.MAX_VALUE, Float.MAX_VALUE, -Float.MAX_VALUE, -Float.MAX_VALUE) + for (i in 0 until 4) { + val rx = if (m == null) px[i] else m[0] * px[i] + m[1] * py[i] + m[4] + val ry = if (m == null) py[i] else m[2] * px[i] + m[3] * py[i] + m[5] + box[0] = min(box[0], rx); box[1] = min(box[1], ry); box[2] = max(box[2], rx); box[3] = max(box[3], ry) + } + touch(box[0], box[1], box[2], box[3]) + lives.add(Live(batches.size, s.copy(), draw, key, box)) + } + override fun mcTextLive(text: String, x: Float, y: Float, color: () -> Int, shadow: Boolean, scale: Float) { if (inLive || 'ยง' in text) return super.mcTextLive(text, x, y, color, shadow, scale) flush() @@ -153,6 +221,11 @@ class GlRenderer private constructor(private val inLive: Boolean) : PolyTessella val baked = ArrayList(tail) tail.clear() val base = baked.map { it.colors.copyOf() } + val box = floatArrayOf(Float.MAX_VALUE, Float.MAX_VALUE, -Float.MAX_VALUE, -Float.MAX_VALUE) + for (b in baked) for (i in b.colors.indices) { + val vx = b.verts[i * 4]; val vy = b.verts[i * 4 + 1] + box[0] = min(box[0], vx); box[1] = min(box[1], vy); box[2] = max(box[2], vx); box[3] = max(box[3], vy) + } lives.add(Live(batches.size, state.copy(), { r -> val alpha = (r as GlRenderer).state.alpha val c = mulAlpha(color(), alpha) @@ -165,7 +238,7 @@ class GlRenderer private constructor(private val inLive: Boolean) : PolyTessella } r.batches.addAll(baked) } - }, color)) + }, color, box)) } override fun texture(target: RenderTarget, x: Float, y: Float, w: Float, h: Float, u0: Float, v0: Float, u1: Float, v1: Float, alpha: Float) { @@ -178,6 +251,27 @@ class GlRenderer private constructor(private val inLive: Boolean) : PolyTessella quadPx(x0, y0, x0 + w * state.sx, y0 + h * state.sy, u0, v0, u1, v1, (a shl 24) or (a shl 16) or (a shl 8) or a) } + override fun backdropGlass(x: Float, y: Float, w: Float, h: Float, radius: Float, band: Float, refraction: Float, alpha: Float) { + beginDraw() + if (GlHost.vanillaPipeline) return + val s = state + if (s.rot != null) return + val a = (alpha * s.alpha * 255f).roundToInt().coerceIn(0, 255) + if (a == 0) return + val x0 = min(x * s.sx + s.tx, (x + w) * s.sx + s.tx) + val x1 = max(x * s.sx + s.tx, (x + w) * s.sx + s.tx) + val y0 = min(y * s.sy + s.ty, (y + h) * s.sy + s.ty) + val y1 = max(y * s.sy + s.ty, (y + h) * s.sy + s.ty) + if (x1 <= x0 || y1 <= y0) return + val scale = (abs(s.sx) + abs(s.sy)) / 2f + val bandPx = (band * scale).coerceIn(1f, 255f).roundToInt() + val r = (radius * scale).roundToInt().coerceIn(0, 255) + val k = (refraction.coerceIn(0f, 1f) * 255f).roundToInt() + begin(GlGlass.Key(), false) + quadPx(x0, y0, x1, y1, 0f, 0f, 1f, 1f, (a shl 24) or (r shl 16) or (bandPx shl 8) or k) + flush() + } + fun draw(fbo: Int, w: Int, h: Int, topDown: Boolean, clear: Boolean, kept: Boolean = false) { flush() drawBatches(fbo, w, h, topDown, clear, batches, segments(lives), kept, false) @@ -195,11 +289,13 @@ class GlRenderer private constructor(private val inLive: Boolean) : PolyTessella private fun flushPending() = flush() private fun replay(lives: List): List = lives.map { l -> - reset() - state = l.state.copy() - l.draw(this) - flush() - GlProgram.Segment(l.at, ArrayList(batches)) + GlProgram.Segment(l.at, l.cached() ?: run { + reset() + state = l.state.copy() + l.draw(this) + flush() + ArrayList(batches).also(l::keep) + }) } internal class Layer(private val coalesce: Boolean = false) { @@ -250,7 +346,7 @@ class GlRenderer private constructor(private val inLive: Boolean) : PolyTessella if (li < rawLives.size && rawLives[li].at == i) emit() while (li < rawLives.size && rawLives[li].at == i) { val l = rawLives[li++] - outLives.add(Live(out.size, l.state, l.draw)) + outLives.add(l.moved(out.size)) } if (i == raw.size) break val b = raw[i] @@ -268,7 +364,7 @@ class GlRenderer private constructor(private val inLive: Boolean) : PolyTessella val all = ArrayList() val allLives = ArrayList() for (layer in layers) { - for (l in layer.lives) allLives.add(Live(l.at + all.size, l.state, l.draw)) + for (l in layer.lives) allLives.add(l.moved(l.at + all.size)) all.addAll(layer.batches) } drawBatches(fbo, w, h, topDown, clear, all, segments(allLives), false, true) @@ -336,10 +432,14 @@ class GlRenderer private constructor(private val inLive: Boolean) : PolyTessella private val active = ArrayList() private var frame = 0L - fun of(batches: List, live: (() -> List)?, changed: () -> Boolean, alpha: Float, screenW: Int, screenH: Int): Group? { + fun of( + batches: List, live: (() -> List)?, changed: () -> Boolean, box: FloatArray?, + alpha: Float, screenW: Int, screenH: Int, + ): Group? { var l = 0; var t = 0; var r = screenW; var b = screenH - if (live == null) { - var x0 = Float.MAX_VALUE; var y0 = Float.MAX_VALUE; var x1 = -Float.MAX_VALUE; var y1 = -Float.MAX_VALUE + if (live == null || box != null) { + var x0 = box?.get(0) ?: Float.MAX_VALUE; var y0 = box?.get(1) ?: Float.MAX_VALUE + var x1 = box?.get(2) ?: -Float.MAX_VALUE; var y1 = box?.get(3) ?: -Float.MAX_VALUE for (batch in batches) for (i in batch.colors.indices) { val vx = batch.verts[i * 4]; val vy = batch.verts[i * 4 + 1] x0 = min(x0, vx); y0 = min(y0, vy); x1 = max(x1, vx); y1 = max(y1, vy) @@ -410,7 +510,7 @@ class GlRenderer private constructor(private val inLive: Boolean) : PolyTessella GL11.glClear(GL11.GL_COLOR_BUFFER_BIT) } if (batches.isNotEmpty() || segments.isNotEmpty()) { - if (arena) GlProgram.drawArena(batches, w, h, topDown, segments) else GlProgram.draw(batches, w, h, topDown, segments, kept) + if (arena) GlProgram.drawArena(batches, w, h, topDown, segments) else GlProgram.draw(batches, w, h, topDown, segments, kept, glass = fbo) } for (g in transient) g.release() Group.endFrame() @@ -535,6 +635,10 @@ internal object GlProgram { private var uScale = 0 private var uOffset = 0 private var uPremul = 0 + private var scaleX = 0f + private var scaleY = 0f + private var offsetX = 0f + private var offsetY = 0f private var boundTex = -1 private var boundPremul = -1 private var vertexData: ByteBuffer = BufferUtils.createByteBuffer(1 shl 16) @@ -583,7 +687,7 @@ internal object GlProgram { keptVbo.uploaded = null } - fun draw(batches: List, w: Int, h: Int, topDown: Boolean, live: List = emptyList(), kept: Boolean = false, ox: Float = 0f, oy: Float = 0f) { + fun draw(batches: List, w: Int, h: Int, topDown: Boolean, live: List = emptyList(), kept: Boolean = false, ox: Float = 0f, oy: Float = 0f, glass: Int = -1) { if (program == 0) init() var staticVerts = 0 for (b in batches) staticVerts += b.colors.size @@ -609,17 +713,22 @@ internal object GlProgram { GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ibo) ensureIndices(vertices / 4) setupState(w, h, topDown, ox, oy) + GlGlass.begin(glass, w, h) - var first = 0 - var liveFirst = (vertices - liveVerts) / 4 - var seg = 0 - for (i in 0..batches.size) { - while (seg < live.size && live[seg].at == i) { - for (b in live[seg++].batches) { drawBatch(b, liveFirst); liveFirst += b.colors.size / 4 } + try { + var first = 0 + var liveFirst = (vertices - liveVerts) / 4 + var seg = 0 + for (i in 0..batches.size) { + while (seg < live.size && live[seg].at == i) { + for (b in live[seg++].batches) { drawBatch(b, liveFirst); liveFirst += b.colors.size / 4 } + } + if (i == batches.size) break + drawBatch(batches[i], first) + first += batches[i].colors.size / 4 } - if (i == batches.size) break - drawBatch(batches[i], first) - first += batches[i].colors.size / 4 + } finally { + GlGlass.end() } GL20.glDisableVertexAttribArray(0) GL20.glDisableVertexAttribArray(1) @@ -689,6 +798,7 @@ internal object GlProgram { GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ibo) ensureIndices(maxOf(arenaVbo.size, scratchVbo.size) / 4) setupState(w, h, topDown) + GlGlass.end() var bound = -1 fun use(id: Int) { @@ -729,8 +839,12 @@ internal object GlProgram { } private fun setupState(w: Int, h: Int, topDown: Boolean, ox: Float = 0f, oy: Float = 0f) { - GL20.glUniform2f(uScale, 2f / w, if (topDown) 2f / h else -2f / h) - GL20.glUniform2f(uOffset, -1f - ox * 2f / w, if (topDown) -1f - oy * 2f / h else 1f + oy * 2f / h) + scaleX = 2f / w + scaleY = if (topDown) 2f / h else -2f / h + offsetX = -1f - ox * 2f / w + offsetY = if (topDown) -1f - oy * 2f / h else 1f + oy * 2f / h + GL20.glUniform2f(uScale, scaleX, scaleY) + GL20.glUniform2f(uOffset, offsetX, offsetY) GL11.glDisable(GL11.GL_CULL_FACE) GL11.glDisable(GL11.GL_DEPTH_TEST) GL11.glDisable(GL11.GL_STENCIL_TEST) @@ -746,6 +860,13 @@ internal object GlProgram { } private fun drawBatch(b: Batch, first: Int) { + if (b.texture is GlGlass.Key) { + boundTex = -1 + if (!GlGlass.use(b, core, scaleX, scaleY, offsetX, offsetY)) return + GL11.glDrawElements(GL11.GL_TRIANGLES, b.colors.size / 4 * 6, GL11.GL_UNSIGNED_INT, first * 6L * 4L) + GL20.glUseProgram(program) + return + } val tex = GlTextures.id(b.texture) if (tex != boundTex) { GL11.glBindTexture(GL11.GL_TEXTURE_2D, tex) @@ -813,7 +934,7 @@ internal object GlProgram { iboQuads = n } - private const val VS_150 = """#version 150 + const val VS_150 = """#version 150 in vec2 aPos; in vec2 aUv; in vec4 aColor; uniform vec2 uScale; uniform vec2 uOffset; out vec2 vUv; out vec4 vColor; @@ -825,7 +946,7 @@ uniform sampler2D uTex; uniform int uPremul; out vec4 fragColor; void main() { vec4 c = texture(uTex, vUv) * vColor; fragColor = uPremul == 1 ? c : vec4(c.rgb * c.a, c.a); } """ - private const val VS_120 = """#version 120 + const val VS_120 = """#version 120 attribute vec2 aPos; attribute vec2 aUv; attribute vec4 aColor; uniform vec2 uScale; uniform vec2 uOffset; varying vec2 vUv; varying vec4 vColor; From 3c10193a8bc61fef1584d8c170473b97e7412f62 Mon Sep 17 00:00:00 2001 From: Deftu Date: Mon, 2 Nov 2026 09:17:43 +0200 Subject: [PATCH 04/12] refactor: rename the PolyGlass themes to Polyfrosted --- .../oneconfig/internal/ThemeConfig.java | 2 +- .../OneConfigUIComponentsBridgeImpl.kt | 8 ++++---- .../oneconfig/internal/ui/themes/Impls.kt | 8 ++++---- .../internal/ui/themes/ThemeRegistry.kt | 18 ++++++++++++------ ...polyglass-dark.svg => polyfrosted-dark.svg} | 0 ...lyglass-light.svg => polyfrosted-light.svg} | 0 modules/ui/api/ui.api | 2 ++ .../v1/internal/OneConfigUIComponentsBridge.kt | 4 ++-- .../api/ui/v1/theme/OneConfigTheme.kt | 14 +++++++++++--- 9 files changed, 36 insertions(+), 20 deletions(-) rename modules/internal/src/main/resources/assets/oneconfig/images/{polyglass-dark.svg => polyfrosted-dark.svg} (100%) rename modules/internal/src/main/resources/assets/oneconfig/images/{polyglass-light.svg => polyfrosted-light.svg} (100%) diff --git a/modules/internal/src/main/java/org/polyfrost/oneconfig/internal/ThemeConfig.java b/modules/internal/src/main/java/org/polyfrost/oneconfig/internal/ThemeConfig.java index ba401e6d6..ed337da63 100644 --- a/modules/internal/src/main/java/org/polyfrost/oneconfig/internal/ThemeConfig.java +++ b/modules/internal/src/main/java/org/polyfrost/oneconfig/internal/ThemeConfig.java @@ -8,7 +8,7 @@ public class ThemeConfig extends Config { @Text(title = "Active Theme") - public static String activeTheme = "PolyGlass Dark"; + public static String activeTheme = "Polyfrosted Dark"; @Color(title = "Accent Color", description = "The accent color of the OneConfig Interface", icon = "paintbrush", gradient = true) public static PolyColor accentColor = new PolyColor(0xFF2B4BFF); diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt index 3c5efd6e3..3caa0a972 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt @@ -31,8 +31,8 @@ import org.polyfrost.oneconfig.internal.ui.themes.Accent import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme import org.polyfrost.oneconfig.internal.ui.themes.MinecraftDark import org.polyfrost.oneconfig.internal.ui.themes.MinecraftLight -import org.polyfrost.oneconfig.internal.ui.themes.PolyGlassDark -import org.polyfrost.oneconfig.internal.ui.themes.PolyGlassLight +import org.polyfrost.oneconfig.internal.ui.themes.PolyfrostedDark +import org.polyfrost.oneconfig.internal.ui.themes.PolyfrostedLight import org.polyfrost.oneconfig.internal.ui.themes.Theme import org.polyfrost.oneconfig.internal.ui.themes.ThemeRegistry import org.polyfrost.oneconfig.internal.ui.themes.UITheme @@ -48,9 +48,9 @@ class OneConfigUIComponentsBridgeImpl : OneConfigUIComponentsBridge { @Composable override fun ProvideTheme(content: @Composable () -> Unit) = Theme(content) - override fun polyGlassDark(): OneConfigUITheme = PolyGlassDark + override fun polyfrostedDark(): OneConfigUITheme = PolyfrostedDark - override fun polyGlassLight(): OneConfigUITheme = PolyGlassLight + override fun polyfrostedLight(): OneConfigUITheme = PolyfrostedLight override fun minecraftDark(): OneConfigUITheme = MinecraftDark diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Impls.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Impls.kt index 23f5b275d..bd2904eee 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Impls.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Impls.kt @@ -74,8 +74,8 @@ private val MinecraftIconOverrides = mapOf( "x-circle" to "/assets/oneconfig/ico/minecraft/x-circle.svg", ) -val PolyGlassDark = UITheme( - "polyglass-dark", "PolyGlass Dark", +val PolyfrostedDark = UITheme( + "polyfrosted-dark", "Polyfrosted Dark", Color(0xC411171C), Color(0xB3151C22), @@ -118,8 +118,8 @@ val PolyGlassDark = UITheme( ) ) -val PolyGlassLight = UITheme( - "polyglass-light", "PolyGlass Light", +val PolyfrostedLight = UITheme( + "polyfrosted-light", "Polyfrosted Light", Color(0xB0D0D7F3), Color(0xB2D0D7F3), diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt index 8bc66fdd5..af8f211ca 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt @@ -16,16 +16,21 @@ object ThemeRegistry { internal val registry = mutableStateListOf() internal var activeTheme by mutableStateOf(null) - private const val DEFAULT_THEME_NAME = "PolyGlass Dark" + private const val DEFAULT_THEME_NAME = "Polyfrosted Dark" + + private val renamedThemes = mapOf( + "PolyGlass Dark" to "Polyfrosted Dark", + "PolyGlass Light" to "Polyfrosted Light", + ) init { - register(PolyGlassDark) - register(PolyGlassLight) + register(PolyfrostedDark) + register(PolyfrostedLight) register(MinecraftDark) register(MinecraftLight) - activeTheme = PolyGlassDark - syncNotificationTheme(PolyGlassDark) + activeTheme = PolyfrostedDark + syncNotificationTheme(PolyfrostedDark) BuiltinVisualizers.register() @@ -58,7 +63,8 @@ object ThemeRegistry { fun loadFromConfig() { updateAccent() try { - val theme = registry.find { it.name == ThemeConfig.activeTheme } + val saved = renamedThemes[ThemeConfig.activeTheme] ?: ThemeConfig.activeTheme + val theme = registry.find { it.name == saved } ?: registry.find { it.name == DEFAULT_THEME_NAME } if (theme != null) { activeTheme = theme diff --git a/modules/internal/src/main/resources/assets/oneconfig/images/polyglass-dark.svg b/modules/internal/src/main/resources/assets/oneconfig/images/polyfrosted-dark.svg similarity index 100% rename from modules/internal/src/main/resources/assets/oneconfig/images/polyglass-dark.svg rename to modules/internal/src/main/resources/assets/oneconfig/images/polyfrosted-dark.svg diff --git a/modules/internal/src/main/resources/assets/oneconfig/images/polyglass-light.svg b/modules/internal/src/main/resources/assets/oneconfig/images/polyfrosted-light.svg similarity index 100% rename from modules/internal/src/main/resources/assets/oneconfig/images/polyglass-light.svg rename to modules/internal/src/main/resources/assets/oneconfig/images/polyfrosted-light.svg diff --git a/modules/ui/api/ui.api b/modules/ui/api/ui.api index 5dc807019..408d65e2f 100644 --- a/modules/ui/api/ui.api +++ b/modules/ui/api/ui.api @@ -589,6 +589,8 @@ public final class org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme { public static final fun getLogoOverride ()Ljava/lang/String; public static final fun getMINECRAFT_DARK ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; public static final fun getMINECRAFT_LIGHT ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; + public static final fun getPOLYFROSTED_DARK ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; + public static final fun getPOLYFROSTED_LIGHT ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; public static final fun getPOLY_GLASS_DARK ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; public static final fun getPOLY_GLASS_LIGHT ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; public static final fun overrideLogo (Ljava/lang/String;)V diff --git a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt index a698b23b4..d06e18b81 100644 --- a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt +++ b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt @@ -28,9 +28,9 @@ interface OneConfigUIComponentsBridge { @Composable fun ProvideTheme(content: @Composable () -> Unit) - fun polyGlassDark(): OneConfigUITheme + fun polyfrostedDark(): OneConfigUITheme - fun polyGlassLight(): OneConfigUITheme + fun polyfrostedLight(): OneConfigUITheme fun minecraftDark(): OneConfigUITheme diff --git a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt index 4bfb8a565..4b6fe78eb 100644 --- a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt +++ b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt @@ -11,10 +11,18 @@ import org.polyfrost.oneconfig.api.ui.v1.internal.componentsBridge @ExperimentalOneConfigUiApi object OneConfigTheme { @JvmStatic - val POLY_GLASS_DARK: OneConfigUITheme by lazy { componentsBridge().polyGlassDark() } + val POLYFROSTED_DARK: OneConfigUITheme by lazy { componentsBridge().polyfrostedDark() } @JvmStatic - val POLY_GLASS_LIGHT: OneConfigUITheme by lazy { componentsBridge().polyGlassLight() } + val POLYFROSTED_LIGHT: OneConfigUITheme by lazy { componentsBridge().polyfrostedLight() } + + @JvmStatic + @Deprecated("Renamed to Polyfrosted", ReplaceWith("OneConfigTheme.POLYFROSTED_DARK")) + val POLY_GLASS_DARK: OneConfigUITheme get() = POLYFROSTED_DARK + + @JvmStatic + @Deprecated("Renamed to Polyfrosted", ReplaceWith("OneConfigTheme.POLYFROSTED_LIGHT")) + val POLY_GLASS_LIGHT: OneConfigUITheme get() = POLYFROSTED_LIGHT @JvmStatic val MINECRAFT_DARK: OneConfigUITheme by lazy { componentsBridge().minecraftDark() } @@ -33,7 +41,7 @@ object OneConfigTheme { get() = componentsBridge().activeTheme() @JvmStatic - fun builtIn(): List = listOf(POLY_GLASS_DARK, POLY_GLASS_LIGHT, MINECRAFT_DARK, MINECRAFT_LIGHT) + fun builtIn(): List = listOf(POLYFROSTED_DARK, POLYFROSTED_LIGHT, MINECRAFT_DARK, MINECRAFT_LIGHT) @JvmStatic fun activate(theme: OneConfigUITheme) { From b24cc7006a8ae05dfd700dae7c23ab33c3c5a479 Mon Sep 17 00:00:00 2001 From: Deftu Date: Mon, 2 Nov 2026 09:17:48 +0200 Subject: [PATCH 05/12] feat: add the Frosted Polyglass themes --- .../internal/ui/compose/ComposeScreen.kt | 18 +- .../OneConfigUIComponentsBridgeImpl.kt | 6 + .../internal/ui/api/TextComponentRenderer.kt | 4 +- .../internal/ui/components/Borders.kt | 12 +- .../oneconfig/internal/ui/components/Chips.kt | 2 + .../internal/ui/components/GlassHover.kt | 164 +++++++++ .../internal/ui/components/GlassSurface.kt | 348 ++++++++++++++++++ .../internal/ui/components/Header.kt | 4 + .../ui/components/NotificationsCenter.kt | 1 + .../internal/ui/components/Sidebar.kt | 26 +- .../ui/components/settings/ButtonOption.kt | 4 + .../ui/components/settings/ColorOption.kt | 2 + .../ui/components/settings/DropdownOption.kt | 2 + .../settings/InheritableSliderOption.kt | 2 + .../settings/MultiSelectDropdownOption.kt | 3 + .../ui/components/settings/Options.kt | 2 + .../components/settings/RadioButtonOption.kt | 2 + .../ui/components/settings/RangeOption.kt | 2 + .../ui/components/settings/SliderControl.kt | 2 + .../ui/components/settings/TileListOption.kt | 2 + .../internal/ui/hud/HudSettingsReset.kt | 3 + .../internal/ui/hud/screens/HudTour.kt | 2 + .../ui/hud/screens/sections/Settings.kt | 2 + .../internal/ui/screens/Changelog.kt | 2 + .../internal/ui/screens/ConfigScreen.kt | 3 + .../oneconfig/internal/ui/screens/Keybinds.kt | 2 + .../internal/ui/screens/ModSortControl.kt | 5 + .../oneconfig/internal/ui/screens/Mods.kt | 25 +- .../oneconfig/internal/ui/screens/Profiles.kt | 6 + .../oneconfig/internal/ui/screens/Themes.kt | 153 ++++++-- .../oneconfig/internal/ui/shell/Shell.kt | 12 +- .../oneconfig/internal/ui/themes/Impls.kt | 66 ++++ .../oneconfig/internal/ui/themes/Provider.kt | 4 + .../oneconfig/internal/ui/themes/Structs.kt | 45 +++ .../internal/ui/themes/ThemeRegistry.kt | 2 + .../oneconfig/brand/oneconfig-white.svg | 16 + .../images/frosted-polyglass-dark.svg | 87 +++++ .../images/frosted-polyglass-light.svg | 87 +++++ modules/ui/api/ui.api | 2 + .../internal/OneConfigUIComponentsBridge.kt | 4 + .../api/ui/v1/theme/OneConfigTheme.kt | 12 +- 41 files changed, 1106 insertions(+), 42 deletions(-) create mode 100644 modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/GlassHover.kt create mode 100644 modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/GlassSurface.kt create mode 100644 modules/internal/src/main/resources/assets/oneconfig/brand/oneconfig-white.svg create mode 100644 modules/internal/src/main/resources/assets/oneconfig/images/frosted-polyglass-dark.svg create mode 100644 modules/internal/src/main/resources/assets/oneconfig/images/frosted-polyglass-light.svg diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt index 21666a6c5..56a97246e 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt @@ -37,6 +37,7 @@ import org.apache.logging.log4j.LogManager import org.polyfrost.oneconfig.api.platform.v1.DesktopHelper import org.polyfrost.oneconfig.api.platform.v1.Platform import org.polyfrost.oneconfig.internal.OneConfigConfig +import org.polyfrost.oneconfig.internal.ui.components.GlassLight import org.polyfrost.oneconfig.internal.ui.components.LocalUiOversample import org.polyfrost.oneconfig.internal.ui.components.LocalWheelRouter import org.polyfrost.oneconfig.internal.ui.components.WheelRouter @@ -296,6 +297,7 @@ abstract class ComposeScreen( private var awaitingFirstFrame = false private var lastPointer: Offset? = null + private var lightInside = false private var lastSceneW = -1 private var lastSceneH = -1 private var lastFbWidth = -1 @@ -325,6 +327,7 @@ abstract class ComposeScreen( if (!hadScene) sceneDirty = true awaitingFirstFrame = true lastPointer = null + lightInside = false syncSceneMetrics() lastSceneW = -1 @@ -451,6 +454,8 @@ abstract class ComposeScreen( ComposeSceneContextImpl.resetPointerIcon() releaseHeldButtons() consumedButtons.clear() + lightInside = false + GlassLight.reset() releaseScene() super.removed() } @@ -515,10 +520,15 @@ abstract class ComposeScreen( wasFocused = focused if (focused) { val pointerPosition = pointerPosition() - if (pointerPosition != lastPointer) { + val moved = pointerPosition != lastPointer + if (moved) { lastPointer = pointerPosition withScene { it.sendPointerEvent(PointerEventType.Move, pointerPosition) } } + if (moved || !lightInside) trackLight(pointerPosition) + } else if (lightInside) { + lightInside = false + GlassLight.leave() } if (metricsChanged) { @@ -544,6 +554,12 @@ abstract class ComposeScreen( //GlHost.screen(wasDirty, blurAlpha(), underlay) { paint(it, nanos) } } + private fun trackLight(pointer: Offset) { + val inside = pointer.x >= 0f && pointer.y >= 0f && pointer.x < lastSceneW && pointer.y < lastSceneH + if (inside) GlassLight.moveTo(pointer) else if (lightInside) GlassLight.leave() + lightInside = inside + } + //? if >= 1.21.11 private val recording = Recording() diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt index 3caa0a972..41adea50d 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt @@ -28,6 +28,8 @@ import org.polyfrost.oneconfig.internal.ui.components.settings.LocalOptionWidth import org.polyfrost.oneconfig.internal.ui.components.settings.colorToHex as internalColorToHex import org.polyfrost.oneconfig.internal.ui.shell.pageSearchQuery as shellPageSearchQuery import org.polyfrost.oneconfig.internal.ui.themes.Accent +import org.polyfrost.oneconfig.internal.ui.themes.FrostedPolyglassDark +import org.polyfrost.oneconfig.internal.ui.themes.FrostedPolyglassLight import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme import org.polyfrost.oneconfig.internal.ui.themes.MinecraftDark import org.polyfrost.oneconfig.internal.ui.themes.MinecraftLight @@ -52,6 +54,10 @@ class OneConfigUIComponentsBridgeImpl : OneConfigUIComponentsBridge { override fun polyfrostedLight(): OneConfigUITheme = PolyfrostedLight + override fun frostedPolyglassDark(): OneConfigUITheme = FrostedPolyglassDark + + override fun frostedPolyglassLight(): OneConfigUITheme = FrostedPolyglassLight + override fun minecraftDark(): OneConfigUITheme = MinecraftDark override fun minecraftLight(): OneConfigUITheme = MinecraftLight diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/api/TextComponentRenderer.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/api/TextComponentRenderer.kt index 360d29848..7a37dc983 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/api/TextComponentRenderer.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/api/TextComponentRenderer.kt @@ -26,6 +26,7 @@ import net.kyori.adventure.text.ComponentIteratorType import net.kyori.adventure.text.event.HoverEvent import net.kyori.adventure.text.format.TextDecoration as KyoriDecoration import org.polyfrost.oneconfig.api.platform.v1.Platform +import org.polyfrost.oneconfig.internal.ui.components.liquidGlass import org.polyfrost.oneconfig.internal.ui.components.parseLegacyFormatting import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme @@ -128,7 +129,8 @@ fun Tooltip( TooltipArea( content = content, delayMillis = 200, tooltip = @Composable { Box( - modifier = modifier.background(theme.popupBackground, shape = RoundedCornerShape(8.dp)) + modifier = modifier.liquidGlass(RoundedCornerShape(8.dp), frosted = true) + .background(theme.popupBackground, shape = RoundedCornerShape(8.dp)) .border(1.dp, theme.borderColor, shape = RoundedCornerShape(8.dp)) .padding(horizontal = 12.dp, vertical = 8.dp) ) { diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Borders.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Borders.kt index 9c335a849..315bfb290 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Borders.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Borders.kt @@ -2,6 +2,7 @@ package org.polyfrost.oneconfig.internal.ui.components import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -14,6 +15,7 @@ import androidx.compose.ui.geometry.isSimple import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.drawOutline import androidx.compose.ui.graphics.drawscope.Stroke @@ -22,6 +24,7 @@ import androidx.compose.ui.unit.dp import kotlin.math.ceil import kotlin.math.min import org.polyfrost.oneconfig.internal.ui.themes.AccentBrush +import org.polyfrost.oneconfig.internal.ui.themes.AccentSheen fun Modifier.animatedFrame( shape: Shape, @@ -36,11 +39,15 @@ fun Modifier.animatedFrame( val stroke = Stroke(strokeWidth) val inset = Size(size.width - strokeWidth, size.height - strokeWidth) val accent = AccentBrush + val sheen = AccentSheen onDrawWithContent { val f = fill() if (f.alpha > 0f) drawOutline(outline, f) val a = accentAlpha() - if (a > 0f) drawOutline(outline, accent, a) + if (a > 0f) { + drawOutline(outline, accent, a) + if (sheen != null) drawOutline(outline, sheen, a) + } drawContent() val b = border() if (b.alpha <= 0f) return@onDrawWithContent @@ -54,6 +61,9 @@ fun Modifier.animatedFrame( } } +fun Modifier.accentSheen(shape: Shape = RectangleShape, alpha: Float = 1f): Modifier = + AccentSheen?.let { background(it, shape, alpha) } ?: this + fun Modifier.topBorder( color: Color, height: Float, diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Chips.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Chips.kt index dc280a49f..8eea71681 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Chips.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Chips.kt @@ -45,7 +45,9 @@ fun Chip( Row( modifier = Modifier .defaultMinSize(minHeight = 33.dp) + .glassHover(theme.sideBarNavigationEntryShape) .graphicsLayer {} + .liquidGlass(theme.sideBarNavigationEntryShape) .animatedFrame( theme.sideBarNavigationEntryShape, fill = { backgroundColor.value }, diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/GlassHover.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/GlassHover.kt new file mode 100644 index 000000000..0b1efaa29 --- /dev/null +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/GlassHover.kt @@ -0,0 +1,164 @@ +package org.polyfrost.oneconfig.internal.ui.components + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.isUnspecified +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toSize +import kotlin.math.max +import kotlin.math.roundToInt +import kotlin.math.sqrt +import kotlinx.coroutines.launch +import org.polyfrost.compose.render.PolyBitmap +import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme + +private const val PRESS_SCALE = 1.03f + +@Composable +fun Modifier.glassHover(shape: Shape, pull: Dp = 1.dp): Modifier { + val theme = LocalTheme.current + if (!theme.cursorEffects) return this + val scope = rememberCoroutineScope() + val offset = remember { Animatable(Offset.Zero, Offset.VectorConverter) } + val scale = remember { Animatable(1f) } + val bloom = remember { Animatable(1f) } + var pressedAt by remember { mutableStateOf(Offset.Zero) } + val blooming = remember { derivedStateOf { bloom.value < 1f } } + val light = theme.textColor.luminance() <= 0.5f + val glow = if (light) 0.45f else 0.2f + return this + .graphicsLayer { + translationX = offset.value.x.roundToInt().toFloat() + translationY = offset.value.y.roundToInt().toFloat() + scaleX = scale.value + scaleY = scale.value + } + .pointerInput(pull) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent(PointerEventPass.Initial) + val position = event.changes.firstOrNull()?.position ?: continue + when (event.type) { + PointerEventType.Exit -> scope.launch { + launch { scale.animateTo(1f, spring(Spring.DampingRatioMediumBouncy)) } + offset.animateTo(Offset.Zero, spring(Spring.DampingRatioMediumBouncy, Spring.StiffnessLow)) + } + PointerEventType.Enter, PointerEventType.Move -> { + val maxPull = pull.toPx() + val nx = ((position.x / size.width.coerceAtLeast(1) - 0.5f) * 2f).coerceIn(-1f, 1f) + val ny = ((position.y / size.height.coerceAtLeast(1) - 0.5f) * 2f).coerceIn(-1f, 1f) + scope.launch { + offset.animateTo(Offset(nx * maxPull, ny * maxPull), spring(stiffness = Spring.StiffnessMediumLow)) + } + } + PointerEventType.Press -> { + pressedAt = position + scope.launch { + launch { scale.animateTo(PRESS_SCALE, spring(stiffness = Spring.StiffnessMedium)) } + bloom.snapTo(0f) + bloom.animateTo(1f, tween(520, easing = LinearOutSlowInEasing)) + } + } + PointerEventType.Release -> scope.launch { + scale.animateTo(1f, spring(Spring.DampingRatioMediumBouncy)) + } + } + } + } + } + .drawWithContent { + drawContent() + if (blooming.value) drawLive { + val b = bloom.value + if (b >= 1f) return@drawLive + val outline = shape.createOutline(size, layoutDirection, this) + val reach = max(size.width, size.height) * (0.25f + b) + drawOutline( + outline, + Brush.radialGradient(listOf(Color.White.copy(alpha = glow * (1f - b)), Color.White.copy(alpha = 0f)), center = pressedAt, radius = reach), + ) + } + } + .hoverGlow(shape, isolate = false) +} + +@Composable +fun Modifier.hoverGlow(shape: Shape, radius: Dp = 90.dp, isolate: Boolean = true): Modifier { + val theme = LocalTheme.current + if (!theme.cursorEffects) return this + var bounds by remember { mutableStateOf(Rect.Zero) } + val density = LocalDensity.current + val reach = with(density) { radius.toPx() } + val slack = with(density) { GateSlack.toPx() } + val strength = if (theme.textColor.luminance() <= 0.5f) 0.2f else 0.055f + val near = remember(reach, slack) { derivedStateOf { lightNear(bounds, reach + slack) } } + return this + .then(if (isolate) Modifier.graphicsLayer {} else Modifier) + .onGloballyPositioned { bounds = Rect(it.positionInWindow(), it.size.toSize()) } + .drawWithCache { + val outline = shape.createOutline(size, layoutDirection, this) + val corner = (outline as? Outline.Rounded)?.roundRect?.topLeftCornerRadius?.x ?: 0f + val w = size.width + val h = size.height + onDrawWithContent { + drawContent() + if (near.value) drawLight(0f, 0f, w, h, { lightKey(cursorNear(bounds, reach), strength) }) { r -> + val c = cursorNear(bounds, reach) + if (c.isUnspecified) return@drawLight + val a = strength * GlassLight.presence + if (a <= 0f) return@drawLight + r.save() + r.clipRRect(0f, 0f, w, h, corner) + r.image(GlowBitmap, c.x - reach, c.y - reach, reach * 2f, reach * 2f, 0f, 0f, 1f, 1f, white(a)) + r.restore() + } + } + } +} + +private val GlowBitmap: PolyBitmap by lazy { + val n = 128 + val pixels = ByteArray(n * n * 4) + for (y in 0 until n) { + for (x in 0 until n) { + val dx = (x + 0.5f) / n * 2f - 1f + val dy = (y + 0.5f) / n * 2f - 1f + val o = (y * n + x) * 4 + pixels[o] = -1 + pixels[o + 1] = -1 + pixels[o + 2] = -1 + pixels[o + 3] = ((1f - sqrt(dx * dx + dy * dy)).coerceAtLeast(0f) * 255f).roundToInt().toByte() + } + } + PolyBitmap(n, n, pixels) +} diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/GlassSurface.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/GlassSurface.kt new file mode 100644 index 000000000..7c4bb6d71 --- /dev/null +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/GlassSurface.kt @@ -0,0 +1,348 @@ +package org.polyfrost.oneconfig.internal.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.isUnspecified +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawscope.CanvasDrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toSize +import kotlin.math.PI +import kotlin.math.ceil +import kotlin.math.cos +import kotlin.math.exp +import kotlin.math.floor +import kotlin.math.hypot +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt +import kotlin.math.sin +import kotlin.math.sqrt +import org.polyfrost.compose.backend.PolyCanvas +import org.polyfrost.compose.backend.PolyDrawCanvas +import org.polyfrost.compose.backend.drawBackdropGlass +import org.polyfrost.compose.backend.drawPoly +import org.polyfrost.compose.render.PolyRenderer +import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme + +private const val PANEL_REFRACTION = 0.85f +private val PanelBand = 22.dp +private val LightReach = 160.dp +private val RestingLight = Offset(-0.57f, -0.82f) +private const val LIGHT_HEIGHT = 0.25f +private const val RIM_TILT_XY = 0.6f +private const val RIM_TILT_Z = 0.8f +private const val MIN_LIGHT = 0.008f +private const val CELL = 32f +internal val GateSlack = 160.dp +private val Follow = sqrt(200f) +private const val FADE = 12f +private const val FADED = 0.002f +private const val SNAP = 0.02f +private const val NO_CELL = Int.MIN_VALUE +private const val RIM_CACHE = 256 + +object GlassLight { + private var targetX = 0f + private var targetY = 0f + private var x = 0f + private var y = 0f + private var vx = 0f + private var vy = 0f + private var presenceTarget = 0f + private var last = 0L + private var cellX = NO_CELL + private var cellY = NO_CELL + private var trailX = NO_CELL + private var trailY = NO_CELL + var known = false + private set + var presence = 0f + private set + val position: Offset get() = Offset(x, y) + var cell by mutableStateOf(null) + private set + var trail by mutableStateOf(null) + private set + + fun moveTo(p: Offset) { + if (!known || presenceTarget == 0f && presence < SNAP) { + known = true + x = p.x + y = p.y + vx = 0f + vy = 0f + last = System.nanoTime() + follow() + } + targetX = p.x + targetY = p.y + presenceTarget = 1f + val cx = floor(p.x / CELL).toInt() + val cy = floor(p.y / CELL).toInt() + if (cx == cellX && cy == cellY) return + cellX = cx + cellY = cy + cell = center(cx, cy) + } + + fun leave() { + presenceTarget = 0f + } + + fun reset() { + known = false + presence = 0f + presenceTarget = 0f + vx = 0f + vy = 0f + clearCells() + } + + fun sample() { + val now = System.nanoTime() + val dt = ((now - last) / 1e9f).coerceIn(0f, 0.1f) + last = now + if (dt <= 0f) return + val decay = exp(-Follow * dt) + val ex = x - targetX + val ey = y - targetY + val kx = vx + Follow * ex + val ky = vy + Follow * ey + x = targetX + (ex + kx * dt) * decay + y = targetY + (ey + ky * dt) * decay + vx = (vx - Follow * kx * dt) * decay + vy = (vy - Follow * ky * dt) * decay + presence += (presenceTarget - presence) * (1f - exp(-FADE * dt)) + if (presenceTarget == 0f && presence < FADED) { + clearCells() + return + } + follow() + } + + private fun follow() { + val cx = floor(x / CELL).toInt() + val cy = floor(y / CELL).toInt() + if (cx == trailX && cy == trailY) return + trailX = cx + trailY = cy + trail = center(cx, cy) + } + + private fun clearCells() { + if (cellX != NO_CELL) { + cellX = NO_CELL + cellY = NO_CELL + cell = null + } + if (trailX != NO_CELL) { + trailX = NO_CELL + trailY = NO_CELL + trail = null + } + } + + private fun center(cx: Int, cy: Int) = Offset(cx * CELL + CELL / 2f, cy * CELL + CELL / 2f) +} + +@Composable +fun Modifier.liquidGlass(shape: Shape, frosted: Boolean = false, alpha: () -> Float = { 1f }): Modifier { + if (!LocalTheme.current.liquidGlass) return this + var bounds by remember { mutableStateOf(Rect.Zero) } + val density = LocalDensity.current + val reach = with(density) { LightReach.toPx() } + val range = reach * 2f + val slack = with(density) { GateSlack.toPx() } + val near = remember(range, slack) { derivedStateOf { lightNear(bounds, range + slack) } } + return this + .onGloballyPositioned { bounds = Rect(it.positionInWindow(), it.size.toSize()) } + .graphicsLayer {} + .glassSurface(shape, frosted, alpha, reach, { near.value }) { cursorNear(bounds, range) } +} + +internal fun lightNear(bounds: Rect, range: Float): Boolean { + if (bounds.isEmpty) return false + val c = GlassLight.cell + val t = GlassLight.trail + return c != null && distance(bounds, c) <= range || t != null && distance(bounds, t) <= range +} + +internal fun cursorNear(bounds: Rect, range: Float): Offset { + GlassLight.sample() + if (!GlassLight.known || bounds.isEmpty || GlassLight.presence <= FADED) return Offset.Unspecified + val p = GlassLight.position + if (distance(bounds, p) > range) return Offset.Unspecified + return p - bounds.topLeft +} + +internal fun lightKey(cursor: Offset, alpha: Float): Int { + if (cursor.isUnspecified) return 0 + var h = (cursor.x * 8f).roundToInt() + h = 31 * h + (cursor.y * 8f).roundToInt() + h = 31 * h + (GlassLight.presence * 255f).roundToInt() + return 31 * h + alpha.toRawBits() +} + +private fun distance(bounds: Rect, p: Offset): Float { + val dx = max(max(bounds.left - p.x, p.x - bounds.right), 0f) + val dy = max(max(bounds.top - p.y, p.y - bounds.bottom), 0f) + return sqrt(dx * dx + dy * dy) +} + +private class Tint(val alpha: Float, val colors: IntArray) + +private class Perimeter(val points: FloatArray, val normals: FloatArray, val shape: FloatArray) { + val size: Int get() = points.size / 2 + val resting = FloatArray(size) { restingLight(normals[it * 2], normals[it * 2 + 1]) } + + @Volatile + private var tint: Tint? = null + + fun restingColors(alpha: Float): IntArray { + tint?.let { if (it.alpha == alpha) return it.colors } + val colors = IntArray(size) { white(resting[it] * alpha) } + tint = Tint(alpha, colors) + return colors + } +} + +private data class RimKey(val w: Float, val h: Float, val radius: Float, val width: Float, val lineStep: Float, val cornerStep: Float) + +private val rims = object : LinkedHashMap(64, 0.75f, true) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = size > RIM_CACHE +} + +private fun cachedPerimeter(w: Float, h: Float, radius: Float, width: Float, lineStep: Float, cornerStep: Float): Perimeter = synchronized(rims) { + rims.getOrPut(RimKey(w, h, radius, width, lineStep, cornerStep)) { perimeter(w, h, radius, width / 2f, lineStep, cornerStep) } +} + +private fun perimeter(w: Float, h: Float, radius: Float, inset: Float, lineStep: Float, cornerStep: Float): Perimeter { + val pts = ArrayList() + val nrm = ArrayList() + val l = inset + val t = inset + val rt = w - inset + val b = h - inset + val r = min(max(radius - inset, 0f), min((rt - l) / 2f, (b - t) / 2f)) + fun add(px: Float, py: Float, nx: Float, ny: Float) { + pts += px + pts += py + nrm += nx + nrm += ny + } + fun line(x0: Float, y0: Float, x1: Float, y1: Float, nx: Float, ny: Float) { + val n = max(1, ceil(hypot(x1 - x0, y1 - y0) / lineStep).toInt()) + for (i in 0 until n) add(x0 + (x1 - x0) * i / n, y0 + (y1 - y0) * i / n, nx, ny) + } + fun corner(cx: Float, cy: Float, from: Double) { + if (r <= 0f) return + val n = max(2, ceil(r * PI / 2 / cornerStep).toInt()) + for (i in 0 until n) { + val a = from + PI / 2 * i / n + val c = cos(a).toFloat() + val s = sin(a).toFloat() + add(cx + c * r, cy + s * r, c, s) + } + } + line(l + r, t, rt - r, t, 0f, -1f) + corner(rt - r, t + r, -PI / 2) + line(rt, t + r, rt, b - r, 1f, 0f) + corner(rt - r, b - r, 0.0) + line(rt - r, b, l + r, b, 0f, 1f) + corner(l + r, b - r, PI / 2) + line(l, b - r, l, t + r, -1f, 0f) + corner(l + r, t + r, PI) + return Perimeter(pts.toFloatArray(), nrm.toFloatArray(), floatArrayOf(0f, 0f, w, h, radius)) +} + +private fun Modifier.glassSurface(shape: Shape, frosted: Boolean, alpha: () -> Float, reach: Float, near: () -> Boolean, cursor: () -> Offset) = drawWithCache { + val outline = shape.createOutline(size, layoutDirection, this) + val radius = (outline as? Outline.Rounded)?.roundRect?.topLeftCornerRadius?.x ?: 0f + val band = PanelBand.toPx() + val width = 1.dp.toPx() + val rim = if (outline is Outline.Generic) null else cachedPerimeter(size.width, size.height, radius, width, 8.dp.toPx(), 3.dp.toPx()) + val lit = rim?.let { IntArray(it.size) } + val fringe = width / 2f + 2f + onDrawWithContent { + val a = alpha() + if (frosted) drawBackdropGlass(size, radius, band, PANEL_REFRACTION, a) + drawContent() + if (rim == null || lit == null || a <= 0f) return@onDrawWithContent + drawRestingRim(rim, width, a) + if (near()) drawLight(-fringe, -fringe, size.width + fringe * 2f, size.height + fringe * 2f, { lightKey(cursor(), a) }) { + val c = cursor() + if (!c.isUnspecified) it.drawRimLight(rim, lit, c, reach, width, a) + } + } +} + +private val liveScope = CanvasDrawScope() + +internal fun DrawScope.drawLight(x: Float, y: Float, w: Float, h: Float, key: () -> Int, block: (PolyRenderer) -> Unit) = drawPoly { it.renderer.live(x, y, w, h, key, block) } + +internal fun DrawScope.drawLive(block: DrawScope.() -> Unit) { + val density = Density(density, fontScale) + val direction = layoutDirection + val size = size + drawPoly { liveScope.draw(density, direction, PolyCanvas(it.renderer), size, block) } +} + +private fun restingLight(nx: Float, ny: Float): Float { + val facing = max(nx * RestingLight.x + ny * RestingLight.y, 0f) + val back = max(-(nx * RestingLight.x + ny * RestingLight.y), 0f) + return 0.05f + 0.3f * facing * facing + 0.08f * back * back +} + +internal fun white(alpha: Float) = ((alpha * 255f).roundToInt().coerceIn(0, 255) shl 24) or 0xFFFFFF + +private fun DrawScope.drawRestingRim(rim: Perimeter, width: Float, alpha: Float) { + val colors = rim.restingColors(alpha) + drawIntoCanvas { canvas -> (canvas as? PolyDrawCanvas)?.polyOp { it.renderer.strokeColorRing(rim.points, rim.normals, colors, width, rim.shape) } } +} + +private fun PolyRenderer.drawRimLight(rim: Perimeter, colors: IntArray, cursor: Offset, reach: Float, width: Float, alpha: Float) { + val presence = GlassLight.presence + if (presence <= 0f) return + val n = rim.size + val pts = rim.points + val nrm = rim.normals + val resting = rim.resting + val lift = reach * LIGHT_HEIGHT + val lift2 = lift * lift + val invReach2 = 1f / (reach * reach) + var any = false + for (i in 0 until n) { + val dx = cursor.x - pts[i * 2] + val dy = cursor.y - pts[i * 2 + 1] + val d2 = dx * dx + dy * dy + val facing = max((nrm[i * 2] * dx + nrm[i * 2 + 1] * dy) * RIM_TILT_XY + lift * RIM_TILT_Z, 0f) + val lit = 0.4f * facing * facing / (d2 + lift2) / (1f + d2 * invReach2) * presence + val base = resting[i] * alpha + val total = min(resting[i] + lit, 0.7f) * alpha + val over = 1f - (1f - total) / (1f - base) + colors[i] = if (over < MIN_LIGHT) { + 0 + } else { + any = true + white(over) + } + } + if (any) strokeColorRing(pts, nrm, colors, width, rim.shape) +} diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Header.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Header.kt index ebabdef00..8876d4515 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Header.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Header.kt @@ -219,7 +219,9 @@ private fun OpenOriginalScreenButton(runnable: Runnable) { Box( modifier = Modifier .pointerHoverIcon(PointerIcon.Hand) + .glassHover(theme.buttonShape) .graphicsLayer {} + .liquidGlass(theme.buttonShape) .animatedFrame(theme.buttonShape, accentAlpha = { accentAlpha.value }, border = { theme.borderColor }) .onClick(interactionSource) { runnable.run() } .padding(horizontal = 16.dp, vertical = 6.dp), @@ -286,6 +288,7 @@ private fun TitleInfoTooltip(title: HeaderTitle) { verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier .widthIn(max = 260.dp) + .liquidGlass(LocalTheme.current.popupShape, frosted = true) .background(LocalTheme.current.popupBackground, shape = LocalTheme.current.popupShape) .border(1.dp, LocalTheme.current.borderColor, shape = LocalTheme.current.popupShape) .padding(horizontal = 12.dp, vertical = 8.dp) @@ -375,6 +378,7 @@ fun GlobalSearchBar() { Box( modifier = Modifier .width(256.dp).height(32.dp) + .liquidGlass(LocalTheme.current.sideBarNavigationEntryShape) .background( LocalTheme.current.chipBackground, LocalTheme.current.sideBarNavigationEntryShape diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/NotificationsCenter.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/NotificationsCenter.kt index b17777042..1d1848b34 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/NotificationsCenter.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/NotificationsCenter.kt @@ -52,6 +52,7 @@ fun NotificationsCenter() { modifier = Modifier .width(PanelWidth) .clip(PanelShape) + .liquidGlass(PanelShape, frosted = true) .background(panelBg, PanelShape) .border(1.dp, theme.borderColor, PanelShape) .padding(horizontal = 14.dp, vertical = 16.dp), diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Sidebar.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Sidebar.kt index 5e9203570..aaa05a192 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Sidebar.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Sidebar.kt @@ -41,8 +41,9 @@ import org.polyfrost.oneconfig.internal.ui.navigation.isOn import org.polyfrost.oneconfig.internal.ui.navigation.sidebarSections import org.polyfrost.oneconfig.internal.ui.shell.LocalShell import org.polyfrost.oneconfig.internal.ui.shell.LocalShellNavController +import org.polyfrost.oneconfig.internal.ui.themes.DEFAULT_SIDEBAR_OPACITY import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme -import org.polyfrost.oneconfig.internal.ui.themes.withOpacityPercent +import org.polyfrost.oneconfig.internal.ui.themes.surfaceOpacity internal val SidebarWidth = 264.dp @@ -53,7 +54,7 @@ fun Sidebar(background: Boolean = true) { Column( modifier = Modifier.fillMaxHeight() .width(SidebarWidth) - .then(if (background) Modifier.background(LocalTheme.current.sidebarBackground.withOpacityPercent(appearance.sidebarOpacity)).rightBorder(LocalTheme.current.borderColor, 1f) else Modifier), + .then(if (background) Modifier.background(LocalTheme.current.surfaceOpacity(LocalTheme.current.sidebarBackground, appearance.sidebarOpacity, DEFAULT_SIDEBAR_OPACITY)).rightBorder(LocalTheme.current.borderColor, 1f) else Modifier), verticalArrangement = Arrangement.SpaceBetween ) { Navigation(showTopOptions = !flipTopOrder) @@ -167,25 +168,32 @@ private fun NavigationEntry(icon: String, title: String, route: Any? = null, isS val isHovered by interactionSource.collectIsHoveredAsState() + val theme = LocalTheme.current + val translucent = theme.translucentSelection val selectedAlpha = animateFloatAsState(if (isSelected) 1f else 0f) val textAndIconColor = animateColorAsState( - if (isSelected) LocalTheme.current.accentTextColor - else if (isHovered) LocalTheme.current.textColor - else LocalTheme.current.textColor.copy(0.8f) + if (isSelected && translucent) theme.textColor + else if (isSelected) theme.accentTextColor + else if (isHovered) theme.textColor + else theme.textColor.copy(0.8f) ) val borderColor = animateColorAsState( - if (isHovered || isSelected) LocalTheme.current.borderColor - else LocalTheme.current.borderColor.copy(0f) + if (isSelected && translucent) theme.borderColor.copy(0f) + else if (isHovered || isSelected) theme.borderColor + else theme.borderColor.copy(0f) ) Row( modifier = Modifier.size(225.dp, 33.dp) + .glassHover(theme.sideBarNavigationEntryShape, pull = 0.75.dp) .graphicsLayer {} .animatedFrame( - LocalTheme.current.sideBarNavigationEntryShape, - accentAlpha = { selectedAlpha.value }, + theme.sideBarNavigationEntryShape, + fill = { if (translucent) theme.textColor.copy(alpha = 0.12f * selectedAlpha.value) else Color.Transparent }, + accentAlpha = { if (translucent) 0f else selectedAlpha.value }, border = { borderColor.value }, ) + .then(if (translucent) Modifier.liquidGlass(theme.sideBarNavigationEntryShape) { selectedAlpha.value } else Modifier) .onClick(interactionSource) { onSelect(route) } .pointerHoverIcon(PointerIcon.Hand), verticalAlignment = Alignment.CenterVertically, diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/ButtonOption.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/ButtonOption.kt index 15b236d6d..c1b6481af 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/ButtonOption.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/ButtonOption.kt @@ -23,6 +23,8 @@ import androidx.compose.ui.unit.sp import org.polyfrost.oneconfig.internal.ui.api.settings.ButtonOptionData import org.polyfrost.oneconfig.internal.ui.components.Text import org.polyfrost.oneconfig.internal.ui.components.animatedFrame +import org.polyfrost.oneconfig.internal.ui.components.glassHover +import org.polyfrost.oneconfig.internal.ui.components.liquidGlass import org.polyfrost.oneconfig.internal.ui.components.onClick import org.polyfrost.oneconfig.internal.ui.components.rememberInteractionSource import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme @@ -53,7 +55,9 @@ fun ButtonOption(data: ButtonOptionData) { modifier = Modifier .widthIn(max = LocalOptionWidth.current) .pointerHoverIcon(PointerIcon.Hand) + .glassHover(LocalTheme.current.buttonShape) .graphicsLayer {} + .liquidGlass(LocalTheme.current.buttonShape) .animatedFrame(LocalTheme.current.buttonShape, fill = { bgColor.value }, accentAlpha = { accentAlpha.value }, border = { Color.Transparent }) .onClick(interactionSource) { val clickedAt = firstClickAt diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/ColorOption.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/ColorOption.kt index 3a0bcbd30..d67e85d1b 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/ColorOption.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/ColorOption.kt @@ -61,6 +61,7 @@ import org.polyfrost.oneconfig.internal.ui.api.shown import org.polyfrost.oneconfig.internal.ui.components.Icon import org.polyfrost.oneconfig.internal.ui.components.Text import org.polyfrost.oneconfig.internal.ui.components.animatedFrame +import org.polyfrost.oneconfig.internal.ui.components.liquidGlass import org.polyfrost.oneconfig.internal.ui.components.onClick import org.polyfrost.oneconfig.internal.ui.components.rememberInteractionSource import org.polyfrost.oneconfig.internal.ui.components.shellTransition @@ -328,6 +329,7 @@ internal fun ColorPickerPopup( modifier = Modifier .width(280.dp) .shellTransition() + .liquidGlass(PickerShape, frosted = true) .background(theme.popupBackground, PickerShape) .border(1.dp, theme.borderColor, PickerShape) .padding(16.dp), diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/DropdownOption.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/DropdownOption.kt index a0471b56b..7cc1c6e20 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/DropdownOption.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/DropdownOption.kt @@ -90,6 +90,7 @@ fun DropdownOption(data: DropdownOptionData) { .width(LocalOptionWidth.current) .height(32.dp) .graphicsLayer {} + .liquidGlass(DropdownShape) .animatedFrame(DropdownShape, fill = { backgroundColor.value }, border = { borderColor.value }) .onClick(triggerInteraction) { expanded = !expanded } .hoverable(triggerInteraction) @@ -120,6 +121,7 @@ fun DropdownOption(data: DropdownOptionData) { modifier = Modifier .width(LocalOptionWidth.current) .clip(DropdownShape) + .liquidGlass(DropdownShape, frosted = true) .background(theme.componentBackground, DropdownShape) .border(1.dp, theme.borderColor, DropdownShape) ) { diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/InheritableSliderOption.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/InheritableSliderOption.kt index bbedd098d..e26307abe 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/InheritableSliderOption.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/InheritableSliderOption.kt @@ -38,6 +38,7 @@ import kotlin.math.roundToInt import org.polyfrost.oneconfig.internal.ui.api.settings.InheritableSliderOptionData import org.polyfrost.oneconfig.internal.ui.components.Text import org.polyfrost.oneconfig.internal.ui.components.animatedFrame +import org.polyfrost.oneconfig.internal.ui.components.liquidGlass import org.polyfrost.oneconfig.internal.ui.components.onClick import org.polyfrost.oneconfig.internal.ui.components.rememberInteractionSource import org.polyfrost.oneconfig.internal.ui.sound.UiSoundEvent @@ -184,6 +185,7 @@ private fun InheritChip(label: String, active: Boolean, onClick: () -> Unit) { Box( modifier = Modifier .graphicsLayer {} + .liquidGlass(theme.sideBarNavigationEntryShape) .animatedFrame(theme.sideBarNavigationEntryShape, fill = { background.value }, border = { border.value }) .onClick(interactionSource, onClick) .pointerHoverIcon(PointerIcon.Hand) diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/MultiSelectDropdownOption.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/MultiSelectDropdownOption.kt index 9170f72c7..aa1f50788 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/MultiSelectDropdownOption.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/MultiSelectDropdownOption.kt @@ -42,6 +42,7 @@ import org.polyfrost.oneconfig.internal.ui.components.Icon import org.polyfrost.oneconfig.internal.ui.components.Text import org.polyfrost.oneconfig.internal.ui.components.animatedFrame import org.polyfrost.oneconfig.internal.ui.components.fadingEdges +import org.polyfrost.oneconfig.internal.ui.components.liquidGlass import org.polyfrost.oneconfig.internal.ui.components.onClick import org.polyfrost.oneconfig.internal.ui.components.dropdown.SimpleDropdownMenu import org.polyfrost.oneconfig.internal.ui.components.rememberInteractionSource @@ -123,6 +124,7 @@ fun MultiSelectDropdownOption(data: MultiSelectDropdownOptionData) { .width(LocalOptionWidth.current) .height(32.dp) .graphicsLayer {} + .liquidGlass(DropdownShape) .animatedFrame(DropdownShape, fill = { backgroundColor.value }, border = { borderColor.value }) .onClick(triggerInteraction) { expanded = !expanded } .hoverable(triggerInteraction) @@ -153,6 +155,7 @@ fun MultiSelectDropdownOption(data: MultiSelectDropdownOptionData) { modifier = Modifier .width(LocalOptionWidth.current) .clip(DropdownShape) + .liquidGlass(DropdownShape, frosted = true) .background(theme.componentBackground, DropdownShape) .border(1.dp, theme.borderColor, DropdownShape) .heightIn(max = 280.dp) diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/Options.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/Options.kt index 1af5456c6..30aa79d61 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/Options.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/Options.kt @@ -46,6 +46,7 @@ import org.polyfrost.oneconfig.internal.ui.api.settings.BooleanOptionData import org.polyfrost.oneconfig.internal.ui.api.shown import org.polyfrost.oneconfig.internal.ui.components.Icon import org.polyfrost.oneconfig.internal.ui.components.Text +import org.polyfrost.oneconfig.internal.ui.components.liquidGlass import org.polyfrost.oneconfig.internal.ui.components.onClick import org.polyfrost.oneconfig.internal.ui.components.rememberInteractionSource import org.polyfrost.oneconfig.internal.ui.components.shellTransition @@ -198,6 +199,7 @@ fun OptionContextMenu( modifier = Modifier .width(IntrinsicSize.Max) .shellTransition() + .liquidGlass(theme.popupShape, frosted = true) .background(theme.popupBackground, theme.popupShape) .border(1.dp, theme.borderColor, theme.popupShape) .padding(ContextMenuPadding), diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/RadioButtonOption.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/RadioButtonOption.kt index bd2bdf870..181905aad 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/RadioButtonOption.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/RadioButtonOption.kt @@ -26,6 +26,7 @@ import org.polyfrost.oneconfig.api.config.v1.Property import org.polyfrost.oneconfig.internal.ui.api.settings.RadioButtonOptionData import org.polyfrost.oneconfig.internal.ui.api.shown import org.polyfrost.oneconfig.internal.ui.components.Text +import org.polyfrost.oneconfig.internal.ui.components.liquidGlass import org.polyfrost.oneconfig.internal.ui.components.onClick import org.polyfrost.oneconfig.internal.ui.components.rememberInteractionSource import org.polyfrost.oneconfig.internal.ui.themes.Accent @@ -72,6 +73,7 @@ fun RadioButtonOption(data: RadioButtonOptionData) { Box( modifier = Modifier .widthIn(max = LocalOptionWidth.current) + .liquidGlass(RadioShape) .border(1.dp, theme.borderColor, RadioShape) .clip(RadioShape) .background(theme.componentBackground), diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/RangeOption.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/RangeOption.kt index 633061466..052abfd77 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/RangeOption.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/RangeOption.kt @@ -41,6 +41,7 @@ import kotlin.math.abs import kotlin.math.roundToInt import org.polyfrost.oneconfig.internal.ui.api.settings.RangeOptionData import org.polyfrost.oneconfig.internal.ui.components.Text +import org.polyfrost.oneconfig.internal.ui.components.accentSheen import org.polyfrost.oneconfig.internal.ui.sound.UiSoundEvent import org.polyfrost.oneconfig.internal.ui.sound.UiSounds import org.polyfrost.oneconfig.internal.ui.themes.AccentBrush @@ -256,6 +257,7 @@ internal fun RangeSliderTrack( .height(trackHeight) .clip(theme.checkBoxShape) .background(AccentBrush) + .accentSheen() ) listOf(startFraction to false, endFraction to true).forEach { (fraction, pointsLeft) -> TriangleThumb( diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/SliderControl.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/SliderControl.kt index 6e70180a0..dc6b986a9 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/SliderControl.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/SliderControl.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import kotlin.math.roundToInt +import org.polyfrost.oneconfig.internal.ui.components.accentSheen import org.polyfrost.oneconfig.internal.ui.sound.UiSoundEvent import org.polyfrost.oneconfig.internal.ui.sound.UiSounds import org.polyfrost.oneconfig.internal.ui.themes.AccentBrush @@ -112,6 +113,7 @@ fun SliderControl( .align(Alignment.CenterStart) .clip(theme.checkBoxShape) .background(AccentBrush) + .accentSheen() ) Box( Modifier diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/TileListOption.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/TileListOption.kt index 693e32ff1..77dc1d2ce 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/TileListOption.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/settings/TileListOption.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.unit.dp import org.polyfrost.oneconfig.internal.ui.api.settings.TileListOptionData import org.polyfrost.oneconfig.internal.ui.components.Icon import org.polyfrost.oneconfig.internal.ui.components.dropdown.SimpleDropdownMenu +import org.polyfrost.oneconfig.internal.ui.components.liquidGlass import org.polyfrost.oneconfig.internal.ui.components.picker.PickerEntry import org.polyfrost.oneconfig.internal.ui.components.picker.PickerLabel import org.polyfrost.oneconfig.internal.ui.components.picker.PickerStrings @@ -91,6 +92,7 @@ fun TileListOption( icon = icon, modifier = Modifier .clip(SelectorShape) + .liquidGlass(SelectorShape, frosted = true) .background(theme.popupBackground, SelectorShape) .border(1.dp, theme.borderColor, SelectorShape) .padding(vertical = 12.dp), 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 56481a80c..5c05ae904 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 @@ -55,6 +55,7 @@ import org.polyfrost.oneconfig.internal.ui.components.Icon import org.polyfrost.oneconfig.internal.ui.components.Text import org.polyfrost.oneconfig.internal.ui.components.blockInteraction import org.polyfrost.oneconfig.internal.ui.components.isEmptyText +import org.polyfrost.oneconfig.internal.ui.components.liquidGlass import org.polyfrost.oneconfig.internal.ui.components.localizedDescription import org.polyfrost.oneconfig.internal.ui.components.onClick import org.polyfrost.oneconfig.internal.ui.components.rememberInteractionSource @@ -324,6 +325,7 @@ fun HudCanvasResetMenu( .blockInteraction(preview) .widthIn(min = 190.dp, max = 240.dp) .shellTransition() + .liquidGlass(theme.popupShape, frosted = true) .background(theme.popupBackground, theme.popupShape) .border(1.dp, theme.borderColor, theme.popupShape) .padding(MenuPadding), @@ -497,6 +499,7 @@ fun HudCanvasPasteMenu( modifier = Modifier .widthIn(min = 190.dp, max = 240.dp) .shellTransition() + .liquidGlass(theme.popupShape, frosted = true) .background(theme.popupBackground, theme.popupShape) .border(1.dp, theme.borderColor, theme.popupShape) .padding(MenuPadding), diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudTour.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudTour.kt index 5afa1c75b..a094cbe4b 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudTour.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudTour.kt @@ -43,6 +43,7 @@ import kotlin.math.roundToInt import org.polyfrost.oneconfig.api.platform.v1.Platform import org.polyfrost.oneconfig.internal.ui.components.Icon import org.polyfrost.oneconfig.internal.ui.components.Text +import org.polyfrost.oneconfig.internal.ui.components.liquidGlass import org.polyfrost.oneconfig.internal.ui.components.onClick import org.polyfrost.oneconfig.internal.ui.components.rememberInteractionSource import org.polyfrost.oneconfig.internal.ui.themes.Accent @@ -214,6 +215,7 @@ internal fun HudTourOverlay( .then(cardModifier) .onSizeChanged { cardSize = it } .width(TOUR_CARD_WIDTH) + .liquidGlass(theme.popupShape, frosted = true) .background(theme.popupBackground, theme.popupShape) .border(1.dp, theme.borderColor, theme.popupShape) .padding(16.dp), 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 3d77115e8..d11181383 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 @@ -30,6 +30,7 @@ import org.polyfrost.oneconfig.api.hud.v1.HudManager import org.polyfrost.oneconfig.api.hud.v1.LegacyHudMarker import org.polyfrost.oneconfig.internal.ui.components.Icon import org.polyfrost.oneconfig.internal.ui.components.Text +import org.polyfrost.oneconfig.internal.ui.components.accentSheen import org.polyfrost.oneconfig.internal.ui.components.onClick import org.polyfrost.oneconfig.internal.ui.components.rememberInteractionSource import org.polyfrost.oneconfig.internal.ui.components.settings.NumberSpinner @@ -366,6 +367,7 @@ private fun ResetHudPositionButton(hud: Hud) { .then(if (enabled) Modifier.pointerHoverIcon(PointerIcon.Hand) else Modifier) .background(theme.borderColor.copy(alpha = theme.borderColor.alpha * disabledAlpha), theme.buttonShape) .background(AccentBrush, theme.buttonShape, alpha = accentAlpha) + .accentSheen(theme.buttonShape, accentAlpha) .then( if (enabled) { Modifier.onClick(interactionSource) { resetHudPosition(hud) } diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Changelog.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Changelog.kt index 53e859bc0..736b76529 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Changelog.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Changelog.kt @@ -64,6 +64,7 @@ import org.polyfrost.oneconfig.internal.ui.api.ChangelogData import org.polyfrost.oneconfig.internal.ui.api.ChangelogSection import org.polyfrost.oneconfig.internal.ui.components.ScrollbarBox import org.polyfrost.oneconfig.internal.ui.components.Text +import org.polyfrost.oneconfig.internal.ui.components.hoverGlow import org.polyfrost.oneconfig.internal.ui.navigation.graph.ChangeLogEntryRoute import org.polyfrost.oneconfig.internal.ui.search.searchMatches import org.polyfrost.oneconfig.internal.ui.shell.LocalShell @@ -345,6 +346,7 @@ fun ChangelogEntry(data: ChangelogSection, index: Int) { Box( modifier = Modifier.fillMaxWidth() + .hoverGlow(theme.modCardShape) .background(theme.modCardBackground, theme.modCardShape) .border( 1.dp, Brush.verticalGradient( diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/ConfigScreen.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/ConfigScreen.kt index c11ef8c03..f83bb6cf3 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/ConfigScreen.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/ConfigScreen.kt @@ -75,6 +75,7 @@ import org.polyfrost.oneconfig.internal.ui.components.ScrollbarBox import org.polyfrost.oneconfig.internal.ui.components.Text import org.polyfrost.oneconfig.internal.ui.components.asRenderText import org.polyfrost.oneconfig.internal.ui.components.blockInteraction +import org.polyfrost.oneconfig.internal.ui.components.hoverGlow import org.polyfrost.oneconfig.internal.ui.components.isEmptyText import org.polyfrost.oneconfig.internal.ui.components.localizedDescription import org.polyfrost.oneconfig.internal.ui.components.localizedTitle @@ -396,6 +397,7 @@ private fun AccordionRow(node: SettingNode.Accordion, compact: Boolean = false) modifier = Modifier .fillMaxWidth() .clip(shape) + .hoverGlow(shape) .background(theme.modCardBackground, shape) .border(1.dp, remember(theme.borderColor) { Brush.verticalGradient(listOf(theme.borderColor, theme.borderColor.copy(0f))) @@ -513,6 +515,7 @@ private fun SettingRowContent(prop: Property<*>, compact: Boolean) { modifier = Modifier .fillMaxWidth() .alpha(displayAlpha(display)) + .hoverGlow(theme.modCardShape) .background(theme.modCardBackground, theme.modCardShape) .border(1.dp, remember(theme.borderColor) { Brush.verticalGradient(listOf(theme.borderColor, theme.borderColor.copy(0f))) diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Keybinds.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Keybinds.kt index 14826f124..cf3152ba4 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Keybinds.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Keybinds.kt @@ -54,6 +54,7 @@ import org.polyfrost.oneconfig.internal.ui.components.ScrollbarBox import org.polyfrost.oneconfig.internal.ui.components.Text import org.polyfrost.oneconfig.internal.ui.components.asRenderText import org.polyfrost.oneconfig.internal.ui.components.blockInteraction +import org.polyfrost.oneconfig.internal.ui.components.hoverGlow import org.polyfrost.oneconfig.internal.ui.components.isEmptyText import org.polyfrost.oneconfig.internal.ui.components.localizedDescription import org.polyfrost.oneconfig.internal.ui.components.localizedTitle @@ -250,6 +251,7 @@ private fun KeybindRow(entry: KeybindEntry, conflictsWith: List>) { } } } + .hoverGlow(shape) .background(theme.modCardBackground, shape) .border( 1.dp, diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/ModSortControl.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/ModSortControl.kt index 5dae92ea2..1cd2f984e 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/ModSortControl.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/ModSortControl.kt @@ -32,6 +32,8 @@ import org.polyfrost.oneconfig.internal.ui.components.Chip import org.polyfrost.oneconfig.internal.ui.components.Icon import org.polyfrost.oneconfig.internal.ui.components.ShellPopup import org.polyfrost.oneconfig.internal.ui.components.Text +import org.polyfrost.oneconfig.internal.ui.components.glassHover +import org.polyfrost.oneconfig.internal.ui.components.liquidGlass import org.polyfrost.oneconfig.internal.ui.components.onClick import org.polyfrost.oneconfig.internal.ui.components.rememberInteractionSource import org.polyfrost.oneconfig.internal.ui.shell.PageGutter @@ -57,6 +59,7 @@ fun ModSortControl(modifier: Modifier = Modifier) { Row( Modifier .height(36.dp) + .liquidGlass(theme.buttonShape) .background(theme.componentBackground, theme.buttonShape) .border(1.dp, theme.borderColor, theme.buttonShape) .padding(3.dp), @@ -78,6 +81,7 @@ fun ModSortControl(modifier: Modifier = Modifier) { Column( Modifier .width(280.dp) + .liquidGlass(theme.popupShape, frosted = true) .background(theme.popupBackground, theme.popupShape) .border(1.dp, theme.borderColor, theme.popupShape) .padding(12.dp), @@ -109,6 +113,7 @@ private fun SortSegment(mode: ModSortMode, selected: Boolean, onSwitch: () -> Un Box( Modifier .fillMaxHeight() + .glassHover(theme.buttonShape.concentric(3.dp), pull = 0.75.dp) .background( if (selected) theme.textColor.copy(alpha = 0.08f) else Color.Transparent, theme.buttonShape.concentric(3.dp), diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Mods.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Mods.kt index ec6fae7ca..4fe1d5e28 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Mods.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Mods.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue @@ -94,8 +95,12 @@ import org.polyfrost.oneconfig.internal.ui.components.Chip import org.polyfrost.oneconfig.internal.ui.components.Icon import org.polyfrost.oneconfig.internal.ui.components.ScrollbarBox import org.polyfrost.oneconfig.internal.ui.components.Text +import org.polyfrost.oneconfig.internal.ui.components.accentSheen import org.polyfrost.oneconfig.internal.ui.components.asRenderText import org.polyfrost.oneconfig.internal.ui.components.canRenderIcon +import org.polyfrost.oneconfig.internal.ui.components.glassHover +import org.polyfrost.oneconfig.internal.ui.components.hoverGlow +import org.polyfrost.oneconfig.internal.ui.components.liquidGlass import org.polyfrost.oneconfig.internal.ui.components.localizedLabel import org.polyfrost.oneconfig.internal.ui.components.onClick import org.polyfrost.oneconfig.internal.ui.components.rememberGridReorderState @@ -444,6 +449,8 @@ private val ModCardFooterHeight = 36.dp private val ModCardGlowHeight = 50.dp +private val ModCardInsetFooterShape = RoundedCornerShape(10.dp) + private val FavoriteStarColor = Color(0xFFFFD700) private val ModCardToggleDot = 6.dp @@ -474,8 +481,9 @@ fun ModCard( val toggle = remember(mod, toggleRevision) { mod.toggle } var enabled by rememberModEnabled(toggle) val footerAccent by animateFloatAsState(if (enabled) 1f else 0f) + val insetFooter = theme.insetModFooter val footerTextColor by animateColorAsState( - if (enabled) theme.accentTextColor else theme.textColor.copy(alpha = 0.75f) + if (enabled) (if (insetFooter) theme.textColor else theme.accentTextColor) else theme.textColor.copy(alpha = 0.75f) ) val glowStrength by animateFloatAsState(if (enabled) 0.5f else 0f) val navigation = LocalShell.current.navigation @@ -489,6 +497,7 @@ fun ModCard( Box( modifier = modifier.fillMaxWidth().height(140.dp) + .hoverGlow(theme.modCardShape) .background(theme.modCardBackground, theme.modCardShape) .border(1.dp, remember(theme.borderColor) { Brush.verticalGradient(listOf(theme.borderColor, theme.borderColor.copy(0f))) @@ -530,10 +539,22 @@ fun ModCard( } Box( modifier = Modifier + .then( + if (insetFooter) { + Modifier + .padding(start = 5.dp, end = 5.dp, bottom = 5.dp) + .glassHover(ModCardInsetFooterShape, pull = 0.75.dp) + .liquidGlass(ModCardInsetFooterShape) + .clip(ModCardInsetFooterShape) + } else { + Modifier + }, + ) .fillMaxWidth() .heightIn(min = ModCardFooterHeight) .background(theme.textColor.copy(alpha = 0.08f * (1f - footerAccent))) .background(AccentBrush, alpha = footerAccent) + .accentSheen(alpha = footerAccent) .then( if (toggle != null && interactive) { Modifier @@ -565,7 +586,7 @@ fun ModCard( } } - if (LocalTheme.current.shadowEnabled) { + if (LocalTheme.current.shadowEnabled && !insetFooter) { val vignetteColor = theme.textColor Box( Modifier.fillMaxSize().drawWithCache { diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Profiles.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Profiles.kt index 2f9028619..cb1de164f 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Profiles.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Profiles.kt @@ -87,6 +87,8 @@ import org.polyfrost.oneconfig.internal.ui.api.settings.KeybindOptionData import org.polyfrost.oneconfig.internal.ui.components.Chip import org.polyfrost.oneconfig.internal.ui.components.Icon import org.polyfrost.oneconfig.internal.ui.components.Text +import org.polyfrost.oneconfig.internal.ui.components.hoverGlow +import org.polyfrost.oneconfig.internal.ui.components.liquidGlass import org.polyfrost.oneconfig.internal.ui.components.onClick import org.polyfrost.oneconfig.internal.ui.components.rememberInteractionSource import org.polyfrost.oneconfig.internal.ui.components.settings.KeybindOption @@ -397,6 +399,7 @@ private fun ProfileBusyOverlay() { ) { Column( modifier = Modifier + .liquidGlass(theme.modCardShape, frosted = true) .background(theme.modCardBackground, theme.modCardShape) .padding(horizontal = 32.dp, vertical = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, @@ -610,6 +613,7 @@ private fun CreateProfileCard( modifier = Modifier .fillMaxWidth() .height(ProfileCardHeight) + .hoverGlow(shape) .background(theme.modCardBackground, shape) .border( 1.dp, Brush.verticalGradient( @@ -754,6 +758,7 @@ private fun ProfileCard( modifier = Modifier .fillMaxWidth() .height(ProfileCardHeight) + .hoverGlow(shape) .background(theme.modCardBackground, shape) .border(3.dp, selectionBorderColor, shape) .border( @@ -937,6 +942,7 @@ private fun ProfileActionsMenu( modifier = Modifier .width(IntrinsicSize.Max) .shellTransition() + .liquidGlass(theme.popupShape, frosted = true) .background(theme.popupBackground, theme.popupShape) .border(1.dp, theme.borderColor, theme.popupShape) .padding(4.dp), diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Themes.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Themes.kt index 2ce1531ac..167989ff6 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Themes.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Themes.kt @@ -9,36 +9,69 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import org.polyfrost.oneconfig.internal.ui.api.ConfigRegistry +import org.polyfrost.oneconfig.internal.ui.components.Icon import org.polyfrost.oneconfig.internal.ui.components.Text +import org.polyfrost.oneconfig.internal.ui.components.glassHover +import org.polyfrost.oneconfig.internal.ui.components.hoverGlow import org.polyfrost.oneconfig.internal.ui.components.keepSearchFocus +import org.polyfrost.oneconfig.internal.ui.components.liquidGlass import org.polyfrost.oneconfig.internal.ui.components.rememberInteractionSource import org.polyfrost.oneconfig.internal.ui.components.wheelScroll import org.polyfrost.oneconfig.internal.ui.shell.PageGutter +import org.polyfrost.oneconfig.internal.ui.sound.UiSoundEvent +import org.polyfrost.oneconfig.internal.ui.sound.UiSounds import org.polyfrost.oneconfig.internal.ui.themes.Accent import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme import org.polyfrost.oneconfig.internal.ui.themes.ThemeRegistry import org.polyfrost.oneconfig.internal.ui.themes.UITheme +import org.polyfrost.oneconfig.internal.ui.themes.concentric const val THEMES_ID = "themes.json" +private val ThemeCardGap = 20.dp + +private val ThemeCardMinWidth = 290.dp + +private const val DARK_SUFFIX = " Dark" + +private const val LIGHT_SUFFIX = " Light" + +private val UITheme.family: String get() = name.removeSuffix(DARK_SUFFIX).removeSuffix(LIGHT_SUFFIX) + +private val UITheme.isLight: Boolean get() = name.endsWith(LIGHT_SUFFIX) + @Composable fun Themes() { val revision = ConfigRegistry.revision @@ -46,15 +79,21 @@ fun Themes() { Column( verticalArrangement = Arrangement.spacedBy(20.dp) ) { - val rowState = rememberLazyListState() - LazyRow( - state = rowState, - modifier = Modifier.wheelScroll(rowState, Orientation.Horizontal), - horizontalArrangement = Arrangement.spacedBy(26.dp), - contentPadding = PaddingValues(horizontal = PageGutter), - ) { - items(ThemeRegistry.registry) { - ThemeCard(it) + val families = ThemeRegistry.registry.groupBy { it.family } + BoxWithConstraints(Modifier.fillMaxWidth()) { + val count = families.size.coerceAtLeast(1) + val available = maxWidth - PageGutter * 2 + val cardWidth = maxOf(ThemeCardMinWidth, (available - ThemeCardGap * (count - 1)) / count) + val rowState = rememberLazyListState() + LazyRow( + state = rowState, + modifier = Modifier.wheelScroll(rowState, Orientation.Horizontal), + horizontalArrangement = Arrangement.spacedBy(ThemeCardGap), + contentPadding = PaddingValues(horizontal = PageGutter), + ) { + items(families.entries.toList(), key = { it.key }) { (family, variants) -> + ThemeCard(family, variants, Modifier.width(cardWidth)) + } } } if (tree == null) { @@ -70,36 +109,104 @@ fun Themes() { } @Composable -private fun ThemeCard(theme: UITheme) { +private fun ThemeCard(family: String, variants: List, modifier: Modifier) { val interactionSource = rememberInteractionSource() - val isSelected = theme == LocalTheme.current + val theme = LocalTheme.current + val active = variants.firstOrNull { it.name == theme.name } + var picked by remember(family) { mutableStateOf(active ?: variants.firstOrNull { it.isLight == theme.isLight } ?: variants.first()) } + LaunchedEffect(active) { + if (active != null) picked = active + } + val shown = active ?: picked + val isSelected = active != null val borderColor by animateColorAsState( - if (isSelected) Accent else LocalTheme.current.borderColor + if (isSelected) Accent else theme.borderColor ) val borderWidth by animateDpAsState( if (isSelected) 3.dp else 1.dp ) - Column( - modifier = Modifier - .background(LocalTheme.current.modCardBackground, LocalTheme.current.backgroundShape) - .border(borderWidth, borderColor, LocalTheme.current.backgroundShape) + Row( + modifier = modifier + .hoverGlow(theme.backgroundShape) + .background(theme.modCardBackground, theme.backgroundShape) + .border(borderWidth, borderColor, theme.backgroundShape) .keepSearchFocus() .clickable(interactionSource, indication = null) { // Use clickable because onClick always plays CLICK. ThemeRegistry.activate chooses CLICK or OPEN - ThemeRegistry.activate(theme) + ThemeRegistry.activate(shown) } .pointerHoverIcon(PointerIcon.Hand) - .padding(14.dp), - verticalArrangement = Arrangement.spacedBy(14.dp), - horizontalAlignment = Alignment.CenterHorizontally, + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically, ) { Image( - painter = painterResource("/assets/oneconfig/images/${theme.previewImage}.svg"), + painter = painterResource("/assets/oneconfig/images/${shown.previewImage}.svg"), contentDescription = null, - modifier = Modifier.size(168.dp, 108.dp) + modifier = Modifier.size(140.dp, 90.dp) + ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + family, + color = theme.textColor, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + if (variants.size > 1) { + VariantToggle(variants, shown) { variant -> + picked = variant + if (isSelected) ThemeRegistry.activate(variant) else UiSounds.play(UiSoundEvent.CLICK) + } + } + } + } +} + +@Composable +private fun VariantToggle(variants: List, shown: UITheme, onPick: (UITheme) -> Unit) { + val theme = LocalTheme.current + Row( + Modifier + .height(30.dp) + .liquidGlass(theme.buttonShape) + .background(theme.componentBackground, theme.buttonShape) + .border(1.dp, theme.borderColor, theme.buttonShape) + .padding(3.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + variants.forEach { variant -> + VariantSegment(variant, variant == shown) { onPick(variant) } + } + } +} + +@Composable +private fun VariantSegment(variant: UITheme, selected: Boolean, onPick: () -> Unit) { + val theme = LocalTheme.current + val interactionSource = rememberInteractionSource() + val shape = theme.buttonShape.concentric(3.dp) + Box( + Modifier + .fillMaxHeight() + .width(32.dp) + .glassHover(shape, pull = 0.75.dp) + .background(if (selected) theme.textColor.copy(alpha = 0.1f) else Color.Transparent, shape) + .then(if (selected) Modifier else Modifier.keepSearchFocus()) + .clickable(interactionSource, indication = null, enabled = !selected, onClick = onPick) + .pointerHoverIcon(PointerIcon.Hand), + contentAlignment = Alignment.Center, + ) { + Icon( + if (variant.isLight) "sun" else "moon", + color = if (selected) theme.textColor else theme.textColorSecondary, + modifier = Modifier.size(14.dp), ) - Text(theme.name, color = LocalTheme.current.textColor) } } diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/Shell.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/Shell.kt index 861322524..8a47e3c9b 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/Shell.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/Shell.kt @@ -57,13 +57,16 @@ import org.polyfrost.oneconfig.internal.OneConfigConfig import org.polyfrost.oneconfig.internal.ui.components.Header import org.polyfrost.oneconfig.internal.ui.components.Sidebar import org.polyfrost.oneconfig.internal.ui.components.SidebarWidth +import org.polyfrost.oneconfig.internal.ui.components.liquidGlass import org.polyfrost.oneconfig.internal.ui.navigation.graph.ModsGraph import org.polyfrost.oneconfig.internal.ui.navigation.graph.ModsRoute import org.polyfrost.oneconfig.internal.ui.navigation.navigation import org.polyfrost.oneconfig.internal.ui.screens.SearchResultsScreen import org.polyfrost.oneconfig.internal.ui.themes.Accent +import org.polyfrost.oneconfig.internal.ui.themes.DEFAULT_PAGE_OPACITY +import org.polyfrost.oneconfig.internal.ui.themes.DEFAULT_SIDEBAR_OPACITY import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme -import org.polyfrost.oneconfig.internal.ui.themes.withOpacityPercent +import org.polyfrost.oneconfig.internal.ui.themes.surfaceOpacity import org.polyfrost.oneconfig.internal.ui.util.cachedDropShadow private const val GLOW_RADIUS = 500f @@ -162,11 +165,11 @@ fun Shell( } val shell = LocalShell.current - val glowAlpha = (shell.appearance.glowOpacity / 100f).coerceIn(0f, 1f) + val glowAlpha = if (theme.glowEnabled) (shell.appearance.glowOpacity / 100f).coerceIn(0f, 1f) else 0f val sidebarGlow = remember { GlowLayers() } val pageGlow = remember { GlowLayers() } - val sidebarColor = theme.sidebarBackground.withOpacityPercent(shell.appearance.sidebarOpacity) - val pageColor = theme.pageBackground.withOpacityPercent(shell.appearance.pageOpacity) + val sidebarColor = theme.surfaceOpacity(theme.sidebarBackground, shell.appearance.sidebarOpacity, DEFAULT_SIDEBAR_OPACITY) + val pageColor = theme.surfaceOpacity(theme.pageBackground, shell.appearance.pageOpacity, DEFAULT_PAGE_OPACITY) Row( modifier = Modifier.onGloballyPositioned { windowOffset = it.positionInRoot() @@ -175,6 +178,7 @@ fun Shell( ).let { mod -> if (theme.shadowEnabled) mod.cachedDropShadow(theme.backgroundShape, backgroundShadow, hole = glowAlpha > 0f) else mod }.clip(theme.backgroundShape) + .liquidGlass(theme.backgroundShape, frosted = true) .border(1.dp, theme.borderColor, theme.backgroundShape) .drawBehind { backdrop(windowOffset) diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Impls.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Impls.kt index bd2904eee..02d359fe8 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Impls.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Impls.kt @@ -237,3 +237,69 @@ val MinecraftLight = UITheme( ), MinecraftIconOverrides ).withShapes(RoundedCornerShape(0.dp), RoundedCornerShape(0.dp)) + +val FrostedPolyglassDark = UITheme( + "frosted-polyglass-dark", "Frosted Polyglass Dark", + + Color(0x8C16181D), + Color(0x99121419), + Color(0x7322252C), + Color(0x6620232A), + Color(0x7322252C), + Color(0xCC16181D), + + Color(0x14FFFFFF), + Color(0xFFFFFFFF), + Color(0xB3FFFFFF), + Color(0xFFFFFFFF), + + Color(0xFF000000), + Color(0xFFFFFFFF), + Color(0xFFFFD60A), + + true, + + RoundedCornerShape(Radii.LG), + RoundedCornerShape(Radii.SM), + RoundedCornerShape(Radii.MD), + RoundedCornerShape(Radii.XS), + RoundedCornerShape(Radii.SM), + RoundedCornerShape(Radii.MD), + CircleShape, + + UIBranding("assets/oneconfig/brand/oneconfig-white.svg"), + PolyfrostedDark.typography, +).withoutGlow().withInsetModFooter().withTranslucentSelection().withLiquidGlass().withCursorEffects() + +val FrostedPolyglassLight = UITheme( + "frosted-polyglass-light", "Frosted Polyglass Light", + + Color(0xA6FAFBFF), + Color(0xB8F2F4F9), + Color(0x66FFFFFF), + Color(0x57FFFFFF), + Color(0x66FFFFFF), + Color(0xD1F6F8FC), + + Color(0x8CFFFFFF), + Color(0xFF0E1118), + Color(0x9E0E1118), + Color(0xFFFFFFFF), + + Color(0xFF1A2440), + Color(0xFFFFFFFF), + Color(0xFFC98A00), + + true, + + RoundedCornerShape(Radii.LG), + RoundedCornerShape(Radii.SM), + RoundedCornerShape(Radii.MD), + RoundedCornerShape(Radii.XS), + RoundedCornerShape(Radii.SM), + RoundedCornerShape(Radii.MD), + CircleShape, + + UIBranding("assets/oneconfig/brand/oneconfig-light.svg"), + PolyfrostedLight.typography, +).withoutGlow().withInsetModFooter().withTranslucentSelection().withLiquidGlass().withCursorEffects() diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Provider.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Provider.kt index f299f4063..413af7e57 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Provider.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Provider.kt @@ -47,6 +47,10 @@ val AccentGradient: Boolean get() = _accentEnd != null val AccentBrush: Brush get() = accentHorizontalBrush() +private val SheenBrush = Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.2f), Color.White.copy(alpha = 0.04f))) + +val AccentSheen: Brush? get() = if (ThemeRegistry.activeTheme?.liquidGlass == true) SheenBrush else null + fun accentHorizontalBrush(startX: Float = 0f, endX: Float = Float.POSITIVE_INFINITY): Brush = _accentEnd?.let { Brush.horizontalGradient(listOf(_accent, it), startX, endX) } ?: SolidColor(_accent) diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Structs.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Structs.kt index cd71e6a57..1eea9144c 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Structs.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Structs.kt @@ -13,6 +13,16 @@ import org.polyfrost.oneconfig.api.ui.v1.theme.OneConfigUITheme fun Color.withOpacityPercent(percent: Float): Color = copy(alpha = (percent / 100f).coerceIn(0f, 1f)) +const val DEFAULT_PAGE_OPACITY = 88f + +const val DEFAULT_SIDEBAR_OPACITY = 80f + +fun Color.scaleOpacity(percent: Float, defaultPercent: Float): Color = + copy(alpha = (alpha * percent / defaultPercent).coerceIn(0f, 1f)) + +fun UITheme.surfaceOpacity(color: Color, percent: Float, defaultPercent: Float): Color = + if (liquidGlass) color.scaleOpacity(percent, defaultPercent) else color.withOpacityPercent(percent) + data class UITheme( val previewImage: String, override val name: String, @@ -67,12 +77,47 @@ data class UITheme( fun withShapes(modIconShape: Shape, scrollBarShape: Shape): UITheme = also { extraShapes[previewImage] = modIconShape to scrollBarShape } + val glowEnabled: Boolean get() = previewImage !in glowlessThemes + + fun withoutGlow(): UITheme = + also { glowlessThemes += previewImage } + + val insetModFooter: Boolean get() = previewImage in insetFooterThemes + + fun withInsetModFooter(): UITheme = + also { insetFooterThemes += previewImage } + + val translucentSelection: Boolean get() = previewImage in translucentSelectionThemes + + fun withTranslucentSelection(): UITheme = + also { translucentSelectionThemes += previewImage } + + val liquidGlass: Boolean get() = previewImage in liquidGlassThemes + + fun withLiquidGlass(): UITheme = + also { liquidGlassThemes += previewImage } + + val cursorEffects: Boolean get() = previewImage in cursorEffectThemes + + fun withCursorEffects(): UITheme = + also { cursorEffectThemes += previewImage } + companion object { val DefaultControlTrackColor = Color(0xFF74777F) private val DefaultExtraShapes: Pair = RoundedCornerShape(4.dp) to RoundedCornerShape(8.dp) private val extraShapes = ConcurrentHashMap>() + + private val glowlessThemes: MutableSet = ConcurrentHashMap.newKeySet() + + private val insetFooterThemes: MutableSet = ConcurrentHashMap.newKeySet() + + private val translucentSelectionThemes: MutableSet = ConcurrentHashMap.newKeySet() + + private val liquidGlassThemes: MutableSet = ConcurrentHashMap.newKeySet() + + private val cursorEffectThemes: MutableSet = ConcurrentHashMap.newKeySet() } } diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt index af8f211ca..944b3aaf2 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt @@ -26,6 +26,8 @@ object ThemeRegistry { init { register(PolyfrostedDark) register(PolyfrostedLight) + register(FrostedPolyglassDark) + register(FrostedPolyglassLight) register(MinecraftDark) register(MinecraftLight) diff --git a/modules/internal/src/main/resources/assets/oneconfig/brand/oneconfig-white.svg b/modules/internal/src/main/resources/assets/oneconfig/brand/oneconfig-white.svg new file mode 100644 index 000000000..e76a7dcfc --- /dev/null +++ b/modules/internal/src/main/resources/assets/oneconfig/brand/oneconfig-white.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/modules/internal/src/main/resources/assets/oneconfig/images/frosted-polyglass-dark.svg b/modules/internal/src/main/resources/assets/oneconfig/images/frosted-polyglass-dark.svg new file mode 100644 index 000000000..9cf8b765e --- /dev/null +++ b/modules/internal/src/main/resources/assets/oneconfig/images/frosted-polyglass-dark.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/internal/src/main/resources/assets/oneconfig/images/frosted-polyglass-light.svg b/modules/internal/src/main/resources/assets/oneconfig/images/frosted-polyglass-light.svg new file mode 100644 index 000000000..85b92c661 --- /dev/null +++ b/modules/internal/src/main/resources/assets/oneconfig/images/frosted-polyglass-light.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/ui/api/ui.api b/modules/ui/api/ui.api index 408d65e2f..791b27270 100644 --- a/modules/ui/api/ui.api +++ b/modules/ui/api/ui.api @@ -586,6 +586,8 @@ public final class org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme { public final fun getAccent (Landroidx/compose/runtime/Composer;I)J public static final fun getActive ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; public final fun getCurrent (Landroidx/compose/runtime/Composer;I)Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; + public static final fun getFROSTED_POLYGLASS_DARK ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; + public static final fun getFROSTED_POLYGLASS_LIGHT ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; public static final fun getLogoOverride ()Ljava/lang/String; public static final fun getMINECRAFT_DARK ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; public static final fun getMINECRAFT_LIGHT ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; diff --git a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt index d06e18b81..0b99e2166 100644 --- a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt +++ b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt @@ -32,6 +32,10 @@ interface OneConfigUIComponentsBridge { fun polyfrostedLight(): OneConfigUITheme + fun frostedPolyglassDark(): OneConfigUITheme + + fun frostedPolyglassLight(): OneConfigUITheme + fun minecraftDark(): OneConfigUITheme fun minecraftLight(): OneConfigUITheme diff --git a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt index 4b6fe78eb..673aee6ac 100644 --- a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt +++ b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt @@ -16,6 +16,12 @@ object OneConfigTheme { @JvmStatic val POLYFROSTED_LIGHT: OneConfigUITheme by lazy { componentsBridge().polyfrostedLight() } + @JvmStatic + val FROSTED_POLYGLASS_DARK: OneConfigUITheme by lazy { componentsBridge().frostedPolyglassDark() } + + @JvmStatic + val FROSTED_POLYGLASS_LIGHT: OneConfigUITheme by lazy { componentsBridge().frostedPolyglassLight() } + @JvmStatic @Deprecated("Renamed to Polyfrosted", ReplaceWith("OneConfigTheme.POLYFROSTED_DARK")) val POLY_GLASS_DARK: OneConfigUITheme get() = POLYFROSTED_DARK @@ -41,7 +47,11 @@ object OneConfigTheme { get() = componentsBridge().activeTheme() @JvmStatic - fun builtIn(): List = listOf(POLYFROSTED_DARK, POLYFROSTED_LIGHT, MINECRAFT_DARK, MINECRAFT_LIGHT) + fun builtIn(): List = listOf( + POLYFROSTED_DARK, POLYFROSTED_LIGHT, + FROSTED_POLYGLASS_DARK, FROSTED_POLYGLASS_LIGHT, + MINECRAFT_DARK, MINECRAFT_LIGHT, + ) @JvmStatic fun activate(theme: OneConfigUITheme) { From 5985930de78f538c144a4546f5d0e6207a144581 Mon Sep 17 00:00:00 2001 From: Deftu Date: Mon, 2 Nov 2026 09:25:39 +0200 Subject: [PATCH 06/12] refactor: rename the Frosted Polyglass themes to PolyGlass --- .../v1/internal/OneConfigUIComponentsBridgeImpl.kt | 8 ++++---- .../oneconfig/internal/ui/themes/Impls.kt | 8 ++++---- .../oneconfig/internal/ui/themes/ThemeRegistry.kt | 12 +++--------- ...osted-polyglass-dark.svg => polyglass-dark.svg} | 0 ...ted-polyglass-light.svg => polyglass-light.svg} | 0 modules/ui/api/ui.api | 2 -- .../ui/v1/internal/OneConfigUIComponentsBridge.kt | 4 ++-- .../oneconfig/api/ui/v1/theme/OneConfigTheme.kt | 14 +++----------- 8 files changed, 16 insertions(+), 32 deletions(-) rename modules/internal/src/main/resources/assets/oneconfig/images/{frosted-polyglass-dark.svg => polyglass-dark.svg} (100%) rename modules/internal/src/main/resources/assets/oneconfig/images/{frosted-polyglass-light.svg => polyglass-light.svg} (100%) diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt index 41adea50d..2148fd70d 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt @@ -28,11 +28,11 @@ import org.polyfrost.oneconfig.internal.ui.components.settings.LocalOptionWidth import org.polyfrost.oneconfig.internal.ui.components.settings.colorToHex as internalColorToHex import org.polyfrost.oneconfig.internal.ui.shell.pageSearchQuery as shellPageSearchQuery import org.polyfrost.oneconfig.internal.ui.themes.Accent -import org.polyfrost.oneconfig.internal.ui.themes.FrostedPolyglassDark -import org.polyfrost.oneconfig.internal.ui.themes.FrostedPolyglassLight import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme import org.polyfrost.oneconfig.internal.ui.themes.MinecraftDark import org.polyfrost.oneconfig.internal.ui.themes.MinecraftLight +import org.polyfrost.oneconfig.internal.ui.themes.PolyGlassDark +import org.polyfrost.oneconfig.internal.ui.themes.PolyGlassLight import org.polyfrost.oneconfig.internal.ui.themes.PolyfrostedDark import org.polyfrost.oneconfig.internal.ui.themes.PolyfrostedLight import org.polyfrost.oneconfig.internal.ui.themes.Theme @@ -54,9 +54,9 @@ class OneConfigUIComponentsBridgeImpl : OneConfigUIComponentsBridge { override fun polyfrostedLight(): OneConfigUITheme = PolyfrostedLight - override fun frostedPolyglassDark(): OneConfigUITheme = FrostedPolyglassDark + override fun polyGlassDark(): OneConfigUITheme = PolyGlassDark - override fun frostedPolyglassLight(): OneConfigUITheme = FrostedPolyglassLight + override fun polyGlassLight(): OneConfigUITheme = PolyGlassLight override fun minecraftDark(): OneConfigUITheme = MinecraftDark diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Impls.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Impls.kt index 02d359fe8..01270c6ed 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Impls.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Impls.kt @@ -238,8 +238,8 @@ val MinecraftLight = UITheme( MinecraftIconOverrides ).withShapes(RoundedCornerShape(0.dp), RoundedCornerShape(0.dp)) -val FrostedPolyglassDark = UITheme( - "frosted-polyglass-dark", "Frosted Polyglass Dark", +val PolyGlassDark = UITheme( + "polyglass-dark", "PolyGlass Dark", Color(0x8C16181D), Color(0x99121419), @@ -271,8 +271,8 @@ val FrostedPolyglassDark = UITheme( PolyfrostedDark.typography, ).withoutGlow().withInsetModFooter().withTranslucentSelection().withLiquidGlass().withCursorEffects() -val FrostedPolyglassLight = UITheme( - "frosted-polyglass-light", "Frosted Polyglass Light", +val PolyGlassLight = UITheme( + "polyglass-light", "PolyGlass Light", Color(0xA6FAFBFF), Color(0xB8F2F4F9), diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt index 944b3aaf2..0885ed1c5 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt @@ -18,16 +18,11 @@ object ThemeRegistry { private const val DEFAULT_THEME_NAME = "Polyfrosted Dark" - private val renamedThemes = mapOf( - "PolyGlass Dark" to "Polyfrosted Dark", - "PolyGlass Light" to "Polyfrosted Light", - ) - init { register(PolyfrostedDark) register(PolyfrostedLight) - register(FrostedPolyglassDark) - register(FrostedPolyglassLight) + register(PolyGlassDark) + register(PolyGlassLight) register(MinecraftDark) register(MinecraftLight) @@ -65,8 +60,7 @@ object ThemeRegistry { fun loadFromConfig() { updateAccent() try { - val saved = renamedThemes[ThemeConfig.activeTheme] ?: ThemeConfig.activeTheme - val theme = registry.find { it.name == saved } + val theme = registry.find { it.name == ThemeConfig.activeTheme } ?: registry.find { it.name == DEFAULT_THEME_NAME } if (theme != null) { activeTheme = theme diff --git a/modules/internal/src/main/resources/assets/oneconfig/images/frosted-polyglass-dark.svg b/modules/internal/src/main/resources/assets/oneconfig/images/polyglass-dark.svg similarity index 100% rename from modules/internal/src/main/resources/assets/oneconfig/images/frosted-polyglass-dark.svg rename to modules/internal/src/main/resources/assets/oneconfig/images/polyglass-dark.svg diff --git a/modules/internal/src/main/resources/assets/oneconfig/images/frosted-polyglass-light.svg b/modules/internal/src/main/resources/assets/oneconfig/images/polyglass-light.svg similarity index 100% rename from modules/internal/src/main/resources/assets/oneconfig/images/frosted-polyglass-light.svg rename to modules/internal/src/main/resources/assets/oneconfig/images/polyglass-light.svg diff --git a/modules/ui/api/ui.api b/modules/ui/api/ui.api index 791b27270..408d65e2f 100644 --- a/modules/ui/api/ui.api +++ b/modules/ui/api/ui.api @@ -586,8 +586,6 @@ public final class org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme { public final fun getAccent (Landroidx/compose/runtime/Composer;I)J public static final fun getActive ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; public final fun getCurrent (Landroidx/compose/runtime/Composer;I)Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; - public static final fun getFROSTED_POLYGLASS_DARK ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; - public static final fun getFROSTED_POLYGLASS_LIGHT ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; public static final fun getLogoOverride ()Ljava/lang/String; public static final fun getMINECRAFT_DARK ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; public static final fun getMINECRAFT_LIGHT ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; diff --git a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt index 0b99e2166..0121181e8 100644 --- a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt +++ b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt @@ -32,9 +32,9 @@ interface OneConfigUIComponentsBridge { fun polyfrostedLight(): OneConfigUITheme - fun frostedPolyglassDark(): OneConfigUITheme + fun polyGlassDark(): OneConfigUITheme - fun frostedPolyglassLight(): OneConfigUITheme + fun polyGlassLight(): OneConfigUITheme fun minecraftDark(): OneConfigUITheme diff --git a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt index 673aee6ac..5a2fe2bcf 100644 --- a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt +++ b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt @@ -17,18 +17,10 @@ object OneConfigTheme { val POLYFROSTED_LIGHT: OneConfigUITheme by lazy { componentsBridge().polyfrostedLight() } @JvmStatic - val FROSTED_POLYGLASS_DARK: OneConfigUITheme by lazy { componentsBridge().frostedPolyglassDark() } + val POLY_GLASS_DARK: OneConfigUITheme by lazy { componentsBridge().polyGlassDark() } @JvmStatic - val FROSTED_POLYGLASS_LIGHT: OneConfigUITheme by lazy { componentsBridge().frostedPolyglassLight() } - - @JvmStatic - @Deprecated("Renamed to Polyfrosted", ReplaceWith("OneConfigTheme.POLYFROSTED_DARK")) - val POLY_GLASS_DARK: OneConfigUITheme get() = POLYFROSTED_DARK - - @JvmStatic - @Deprecated("Renamed to Polyfrosted", ReplaceWith("OneConfigTheme.POLYFROSTED_LIGHT")) - val POLY_GLASS_LIGHT: OneConfigUITheme get() = POLYFROSTED_LIGHT + val POLY_GLASS_LIGHT: OneConfigUITheme by lazy { componentsBridge().polyGlassLight() } @JvmStatic val MINECRAFT_DARK: OneConfigUITheme by lazy { componentsBridge().minecraftDark() } @@ -49,7 +41,7 @@ object OneConfigTheme { @JvmStatic fun builtIn(): List = listOf( POLYFROSTED_DARK, POLYFROSTED_LIGHT, - FROSTED_POLYGLASS_DARK, FROSTED_POLYGLASS_LIGHT, + POLY_GLASS_DARK, POLY_GLASS_LIGHT, MINECRAFT_DARK, MINECRAFT_LIGHT, ) From 4568fd46e6975895cc4cbc64e80db861208254b3 Mon Sep 17 00:00:00 2001 From: Deftu Date: Mon, 2 Nov 2026 09:32:53 +0200 Subject: [PATCH 07/12] fix: keep POLY_GLASS_* and old saves on Polyfrosted --- .../polyfrost/oneconfig/internal/ThemeConfig.java | 4 ++++ .../v1/internal/OneConfigUIComponentsBridgeImpl.kt | 4 ++-- .../oneconfig/internal/ui/themes/ThemeRegistry.kt | 12 +++++++++++- modules/ui/api/ui.api | 2 ++ .../ui/v1/internal/OneConfigUIComponentsBridge.kt | 4 ++-- .../oneconfig/api/ui/v1/theme/OneConfigTheme.kt | 14 +++++++++++--- 6 files changed, 32 insertions(+), 8 deletions(-) diff --git a/modules/internal/src/main/java/org/polyfrost/oneconfig/internal/ThemeConfig.java b/modules/internal/src/main/java/org/polyfrost/oneconfig/internal/ThemeConfig.java index ed337da63..cbc11fb7a 100644 --- a/modules/internal/src/main/java/org/polyfrost/oneconfig/internal/ThemeConfig.java +++ b/modules/internal/src/main/java/org/polyfrost/oneconfig/internal/ThemeConfig.java @@ -16,6 +16,9 @@ public class ThemeConfig extends Config { @Switch(title = "Animations", description = "Smooth, clean animations in the OneConfig Interface", icon = "refresh") public static boolean animations = true; + @Switch(title = "Polyfrosted Rename Applied") + public static boolean polyfrostedRenameApplied = false; + public ThemeConfig() { super("themes.json", "assets/oneconfig/brand/oneconfig-icon.svg", "Themes", Category.QOL); } @@ -24,5 +27,6 @@ public ThemeConfig() { protected void initialize(boolean byConfigManager) { super.initialize(byConfigManager); hideIf("activeTheme", () -> true); + hideIf("polyfrostedRenameApplied", () -> true); } } diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt index 2148fd70d..40b8a8db6 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridgeImpl.kt @@ -54,9 +54,9 @@ class OneConfigUIComponentsBridgeImpl : OneConfigUIComponentsBridge { override fun polyfrostedLight(): OneConfigUITheme = PolyfrostedLight - override fun polyGlassDark(): OneConfigUITheme = PolyGlassDark + override fun glassDark(): OneConfigUITheme = PolyGlassDark - override fun polyGlassLight(): OneConfigUITheme = PolyGlassLight + override fun glassLight(): OneConfigUITheme = PolyGlassLight override fun minecraftDark(): OneConfigUITheme = MinecraftDark diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt index 0885ed1c5..da18fdca7 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt @@ -18,6 +18,11 @@ object ThemeRegistry { private const val DEFAULT_THEME_NAME = "Polyfrosted Dark" + private val renamedThemes = mapOf( + "PolyGlass Dark" to "Polyfrosted Dark", + "PolyGlass Light" to "Polyfrosted Light", + ) + init { register(PolyfrostedDark) register(PolyfrostedLight) @@ -60,13 +65,18 @@ object ThemeRegistry { fun loadFromConfig() { updateAccent() try { - val theme = registry.find { it.name == ThemeConfig.activeTheme } + val saved = if (ThemeConfig.polyfrostedRenameApplied) ThemeConfig.activeTheme else renamedThemes[ThemeConfig.activeTheme] ?: ThemeConfig.activeTheme + val theme = registry.find { it.name == saved } ?: registry.find { it.name == DEFAULT_THEME_NAME } if (theme != null) { activeTheme = theme ThemeConfig.activeTheme = theme.name syncNotificationTheme(theme) } + if (!ThemeConfig.polyfrostedRenameApplied) { + ThemeConfig.polyfrostedRenameApplied = true + saveThemeToConfig() + } } catch (_: Exception) {} } diff --git a/modules/ui/api/ui.api b/modules/ui/api/ui.api index 408d65e2f..699ab7050 100644 --- a/modules/ui/api/ui.api +++ b/modules/ui/api/ui.api @@ -586,6 +586,8 @@ public final class org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme { public final fun getAccent (Landroidx/compose/runtime/Composer;I)J public static final fun getActive ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; public final fun getCurrent (Landroidx/compose/runtime/Composer;I)Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; + public static final fun getGLASS_DARK ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; + public static final fun getGLASS_LIGHT ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; public static final fun getLogoOverride ()Ljava/lang/String; public static final fun getMINECRAFT_DARK ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; public static final fun getMINECRAFT_LIGHT ()Lorg/polyfrost/oneconfig/api/ui/v1/theme/OneConfigUITheme; diff --git a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt index 0121181e8..6aa0b7b4c 100644 --- a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt +++ b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/internal/OneConfigUIComponentsBridge.kt @@ -32,9 +32,9 @@ interface OneConfigUIComponentsBridge { fun polyfrostedLight(): OneConfigUITheme - fun polyGlassDark(): OneConfigUITheme + fun glassDark(): OneConfigUITheme - fun polyGlassLight(): OneConfigUITheme + fun glassLight(): OneConfigUITheme fun minecraftDark(): OneConfigUITheme diff --git a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt index 5a2fe2bcf..7a6f26716 100644 --- a/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt +++ b/modules/ui/src/main/kotlin/org/polyfrost/oneconfig/api/ui/v1/theme/OneConfigTheme.kt @@ -17,10 +17,18 @@ object OneConfigTheme { val POLYFROSTED_LIGHT: OneConfigUITheme by lazy { componentsBridge().polyfrostedLight() } @JvmStatic - val POLY_GLASS_DARK: OneConfigUITheme by lazy { componentsBridge().polyGlassDark() } + val GLASS_DARK: OneConfigUITheme by lazy { componentsBridge().glassDark() } @JvmStatic - val POLY_GLASS_LIGHT: OneConfigUITheme by lazy { componentsBridge().polyGlassLight() } + val GLASS_LIGHT: OneConfigUITheme by lazy { componentsBridge().glassLight() } + + @JvmStatic + @Deprecated("Renamed to Polyfrosted", ReplaceWith("OneConfigTheme.POLYFROSTED_DARK")) + val POLY_GLASS_DARK: OneConfigUITheme get() = POLYFROSTED_DARK + + @JvmStatic + @Deprecated("Renamed to Polyfrosted", ReplaceWith("OneConfigTheme.POLYFROSTED_LIGHT")) + val POLY_GLASS_LIGHT: OneConfigUITheme get() = POLYFROSTED_LIGHT @JvmStatic val MINECRAFT_DARK: OneConfigUITheme by lazy { componentsBridge().minecraftDark() } @@ -41,7 +49,7 @@ object OneConfigTheme { @JvmStatic fun builtIn(): List = listOf( POLYFROSTED_DARK, POLYFROSTED_LIGHT, - POLY_GLASS_DARK, POLY_GLASS_LIGHT, + GLASS_DARK, GLASS_LIGHT, MINECRAFT_DARK, MINECRAFT_LIGHT, ) From ac0787988901eddef4ea272c7cdec363c7916c38 Mon Sep 17 00:00:00 2001 From: Deftu Date: Thu, 5 Nov 2026 11:46:43 +0200 Subject: [PATCH 08/12] fix: redo the Polyfrosted rename check for each profile --- .../oneconfig/internal/ui/themes/ThemeRegistry.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt index da18fdca7..cdf8f7d3b 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt @@ -36,9 +36,15 @@ object ThemeRegistry { BuiltinVisualizers.register() - ConfigManager.addProfileChangeListener { - Platform.screen().runOnUiThread { loadFromConfig() } - } + ConfigManager.addProfileChangeListener(object : ConfigManager.ProfileChangeListener { + override fun onProfileUnloading(profile: String) { + ThemeConfig.polyfrostedRenameApplied = false + } + + override fun onProfileChanged(newProfile: String) { + Platform.screen().runOnUiThread { loadFromConfig() } + } + }) } fun init() {} From e06bf494b942b99d53ada2a5eb5372f5a85d6cdf Mon Sep 17 00:00:00 2001 From: Deftu Date: Thu, 5 Nov 2026 11:47:00 +0200 Subject: [PATCH 09/12] fix: make 100% opacity fully opaque on PolyGlass themes --- .../polyfrost/oneconfig/internal/ui/themes/Structs.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Structs.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Structs.kt index 1eea9144c..596783adc 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Structs.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Structs.kt @@ -17,8 +17,14 @@ const val DEFAULT_PAGE_OPACITY = 88f const val DEFAULT_SIDEBAR_OPACITY = 80f -fun Color.scaleOpacity(percent: Float, defaultPercent: Float): Color = - copy(alpha = (alpha * percent / defaultPercent).coerceIn(0f, 1f)) +fun Color.scaleOpacity(percent: Float, defaultPercent: Float): Color { + val scaled = if (percent <= defaultPercent) { + alpha * percent / defaultPercent + } else { + alpha + (1f - alpha) * (percent - defaultPercent) / (100f - defaultPercent) + } + return copy(alpha = scaled.coerceIn(0f, 1f)) +} fun UITheme.surfaceOpacity(color: Color, percent: Float, defaultPercent: Float): Color = if (liquidGlass) color.scaleOpacity(percent, defaultPercent) else color.withOpacityPercent(percent) From 655f0055655aa7768f1bae3ebb91085e51333553 Mon Sep 17 00:00:00 2001 From: Deftu Date: Thu, 5 Nov 2026 11:47:00 +0200 Subject: [PATCH 10/12] fix: free the glass backdrop texture when glass stops being drawn --- .../oneconfig/internal/ui/compose/LiquidGlass.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/LiquidGlass.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/LiquidGlass.kt index 617a67713..ce9e8da82 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/LiquidGlass.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/LiquidGlass.kt @@ -29,6 +29,8 @@ import kotlin.math.max import kotlin.math.min internal object LiquidGlass { + private const val RELEASE_AFTER_NANOS = 5_000_000_000L + val pipeline: RenderPipeline = RenderPipeline.builder(RenderPipelinesAccessor.`oneconfig$getGuiTexturedSnippet`()) .withLocation(Identifier.fromNamespaceAndPath("oneconfig", "pipeline/liquid_glass")) //? if >= 26.3 { @@ -44,12 +46,14 @@ internal object LiquidGlass { private val glassDraws: MutableSet = Collections.newSetFromMap(IdentityHashMap()) private val dirty = ArrayList() private var submitted = false + private var lastUsed = 0L fun interface Range { fun draw(from: Int, to: Int) } fun setup(): TextureSetup { + lastUsed = System.nanoTime() val main = Minecraft.getInstance().gameRenderer.mainRenderTarget() val color = main.colorTexture ?: return setup val current = texture @@ -172,6 +176,15 @@ internal object LiquidGlass { submitted = false drawBounds.clear() glassDraws.clear() + if (texture != null && System.nanoTime() - lastUsed > RELEASE_AFTER_NANOS) release() + } + + private fun release() { + view?.close() + texture?.close() + view = null + texture = null + setup = TextureSetup.noTexture() } private fun covers(region: ScreenRectangle?): Boolean { From b763f79144fa6950b8ac350609688fd9a7f6e20c Mon Sep 17 00:00:00 2001 From: Julian Chang Date: Fri, 6 Nov 2026 01:14:59 -0500 Subject: [PATCH 11/12] fixes --- .../v1/internal/ScreenPlatformImpl.java | 6 ++++ .../internal/ui/compose/BlazeRenderer.kt | 19 ++++++++----- .../internal/ui/components/GlassSurface.kt | 17 +++++++++++ .../oneconfig/internal/ui/screens/Themes.kt | 8 ++++++ .../oneconfig/internal/ui/shell/Shell.kt | 10 ++++++- .../oneconfig/internal/ui/themes/Structs.kt | 3 ++ .../internal/ui/themes/ThemeRegistry.kt | 5 ++-- .../internal/ui/themes/GlassOpacityTest.java | 15 ++++++++++ .../ui/themes/ThemeMigrationTest.java | 28 +++++++++++++++++++ modules/utils/api/utils.api | 1 + .../api/platform/v1/ScreenPlatform.java | 5 ++++ 11 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 modules/internal/src/test/java/org/polyfrost/oneconfig/internal/ui/themes/GlassOpacityTest.java create mode 100644 modules/internal/src/test/java/org/polyfrost/oneconfig/internal/ui/themes/ThemeMigrationTest.java diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/api/platform/v1/internal/ScreenPlatformImpl.java b/minecraft/src/main/java/org/polyfrost/oneconfig/api/platform/v1/internal/ScreenPlatformImpl.java index 3b4f3b981..5686a4bbb 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/api/platform/v1/internal/ScreenPlatformImpl.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/api/platform/v1/internal/ScreenPlatformImpl.java @@ -36,6 +36,7 @@ import org.polyfrost.oneconfig.api.platform.v1.ScreenPlatform; import org.polyfrost.oneconfig.internal.ui.compose.ComposeScreen; import org.polyfrost.oneconfig.internal.ui.compose.ComposeSupport; +import org.polyfrost.oneconfig.internal.ui.compose.GlHost; //? if = 1.8.9 //import org.polyfrost.oneconfig.internal.legacy.LegacyPanoramaTracker; @@ -47,6 +48,11 @@ public void runOnUiThread(Runnable action) { else minecraft.execute(action); } + @Override + public boolean supportsBackdropGlass() { + return !GlHost.vanillaPipeline; + } + @Override public int viewportWidth() { return Minecraft.getInstance().getWindow().getWidth(); diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/BlazeRenderer.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/BlazeRenderer.kt index 14354f0c1..fdaeb5c21 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/BlazeRenderer.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/BlazeRenderer.kt @@ -196,6 +196,12 @@ class BlazeRenderer(private val guiScale: Float, private val keepVertices: Boole } override fun submit(texture: Any, premultiplied: Boolean, nearest: Boolean, verts: FloatArray, colors: IntArray, bounds: FloatArray) { + if (texture === GlassBatch) { + val band = (colors[0] ushr 8 and 0xFF).toFloat() + val box = rectGui(bounds[0] - band, bounds[1] - band, bounds[2] + band, bounds[3] + band) ?: return + ops.add(MeshOp(Mesh(LiquidGlass.pipeline, TextureSetup.noTexture(), elementPose, verts, colors, box, false))) + return + } val box = rectGui(bounds[0], bounds[1], bounds[2], bounds[3]) ?: return val setup = when (texture) { is AbstractTextureRef -> texture.setup(nearest) @@ -354,17 +360,16 @@ class BlazeRenderer(private val guiScale: Float, private val keepVertices: Boole val y0 = min(y * s.sy + s.ty, (y + h) * s.sy + s.ty) val y1 = max(y * s.sy + s.ty, (y + h) * s.sy + s.ty) val scale = (abs(s.sx) + abs(s.sy)) / 2f - val bandPx = (band * scale).coerceIn(1f, 255f) - val box = rectGui(x0 - bandPx, y0 - bandPx, x1 + bandPx, y1 + bandPx) ?: return - val scissor = s.clip?.let { rectGui(it[0], it[1], it[2], it[3]) ?: return } - touch(x0, y0, x1, y1) + val bandPx = (band * scale).coerceIn(1f, 255f).roundToInt() val r = (radius * scale).roundToInt().coerceIn(0, 255) val k = (refraction.coerceIn(0f, 1f) * 255f).roundToInt() - val color = (a shl 24) or (r shl 16) or (bandPx.roundToInt() shl 8) or k - val verts = floatArrayOf(x0, y0, 0f, 0f, x0, y1, 0f, 1f, x1, y1, 1f, 1f, x1, y0, 1f, 0f) - ops.add(MeshOp(Mesh(LiquidGlass.pipeline, TextureSetup.noTexture(), elementPose, verts, IntArray(4) { color }, box, false, scissor))) + begin(GlassBatch, false) + quadPx(x0, y0, x1, y1, 0f, 0f, 1f, 1f, (a shl 24) or (r shl 16) or (bandPx shl 8) or k) + flush() } + private object GlassBatch + private var liveRenderer: BlazeRenderer? = null override fun live(draw: (PolyRenderer) -> Unit) { diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/GlassSurface.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/GlassSurface.kt index 7c4bb6d71..8dca1c475 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/GlassSurface.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/GlassSurface.kt @@ -15,12 +15,14 @@ import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.drawscope.CanvasDrawScope import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize import kotlin.math.PI @@ -177,6 +179,21 @@ fun Modifier.liquidGlass(shape: Shape, frosted: Boolean = false, alpha: () -> Fl .glassSurface(shape, frosted, alpha, reach, { near.value }) { cursorNear(bounds, range) } } +@Composable +fun Modifier.splitFrostedGlass(shape: Shape, splitX: Dp, leftAlpha: Float, rightAlpha: Float): Modifier { + if (!LocalTheme.current.liquidGlass) return this + return drawWithCache { + val radius = (shape.createOutline(size, layoutDirection, this) as? Outline.Rounded)?.roundRect?.topLeftCornerRadius?.x ?: 0f + val band = PanelBand.toPx() + val split = splitX.roundToPx().coerceIn(0, size.width.roundToInt()).toFloat() + onDrawWithContent { + clipRect(right = split) { drawBackdropGlass(size, radius, band, PANEL_REFRACTION, leftAlpha) } + clipRect(left = split) { drawBackdropGlass(size, radius, band, PANEL_REFRACTION, rightAlpha) } + drawContent() + } + } +} + internal fun lightNear(bounds: Rect, range: Float): Boolean { if (bounds.isEmpty) return false val c = GlassLight.cell diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Themes.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Themes.kt index 167989ff6..52986f1c5 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Themes.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/Themes.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import org.polyfrost.oneconfig.api.platform.v1.Platform import org.polyfrost.oneconfig.internal.ui.api.ConfigRegistry import org.polyfrost.oneconfig.internal.ui.components.Icon import org.polyfrost.oneconfig.internal.ui.components.Text @@ -96,6 +97,13 @@ fun Themes() { } } } + if (!Platform.screen().supportsBackdropGlass()) { + Text( + "PolyGlass shows without frosted glass while VulkanMod is installed on this Minecraft version.", + color = LocalTheme.current.textColorSecondary, + modifier = Modifier.padding(horizontal = PageGutter), + ) + } if (tree == null) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("No preferences available.", color = LocalTheme.current.textColorSecondary) diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/Shell.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/Shell.kt index 8a47e3c9b..f635a0c07 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/Shell.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/Shell.kt @@ -58,6 +58,7 @@ import org.polyfrost.oneconfig.internal.ui.components.Header import org.polyfrost.oneconfig.internal.ui.components.Sidebar import org.polyfrost.oneconfig.internal.ui.components.SidebarWidth import org.polyfrost.oneconfig.internal.ui.components.liquidGlass +import org.polyfrost.oneconfig.internal.ui.components.splitFrostedGlass import org.polyfrost.oneconfig.internal.ui.navigation.graph.ModsGraph import org.polyfrost.oneconfig.internal.ui.navigation.graph.ModsRoute import org.polyfrost.oneconfig.internal.ui.navigation.navigation @@ -66,6 +67,7 @@ import org.polyfrost.oneconfig.internal.ui.themes.Accent import org.polyfrost.oneconfig.internal.ui.themes.DEFAULT_PAGE_OPACITY import org.polyfrost.oneconfig.internal.ui.themes.DEFAULT_SIDEBAR_OPACITY import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme +import org.polyfrost.oneconfig.internal.ui.themes.glassOpacity import org.polyfrost.oneconfig.internal.ui.themes.surfaceOpacity import org.polyfrost.oneconfig.internal.ui.util.cachedDropShadow @@ -178,7 +180,13 @@ fun Shell( ).let { mod -> if (theme.shadowEnabled) mod.cachedDropShadow(theme.backgroundShape, backgroundShadow, hole = glowAlpha > 0f) else mod }.clip(theme.backgroundShape) - .liquidGlass(theme.backgroundShape, frosted = true) + .liquidGlass(theme.backgroundShape) + .splitFrostedGlass( + theme.backgroundShape, + SidebarWidth, + glassOpacity(shell.appearance.sidebarOpacity, DEFAULT_SIDEBAR_OPACITY), + glassOpacity(shell.appearance.pageOpacity, DEFAULT_PAGE_OPACITY), + ) .border(1.dp, theme.borderColor, theme.backgroundShape) .drawBehind { backdrop(windowOffset) diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Structs.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Structs.kt index 596783adc..9e8b9dfc7 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Structs.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/Structs.kt @@ -26,6 +26,9 @@ fun Color.scaleOpacity(percent: Float, defaultPercent: Float): Color { return copy(alpha = scaled.coerceIn(0f, 1f)) } +fun glassOpacity(percent: Float, defaultPercent: Float): Float = + (percent / defaultPercent).coerceIn(0f, 1f) + fun UITheme.surfaceOpacity(color: Color, percent: Float, defaultPercent: Float): Color = if (liquidGlass) color.scaleOpacity(percent, defaultPercent) else color.withOpacityPercent(percent) diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt index cdf8f7d3b..71c3cb636 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt @@ -8,6 +8,7 @@ import org.polyfrost.oneconfig.api.config.v1.ConfigManager import org.polyfrost.oneconfig.api.platform.v1.Platform import org.polyfrost.oneconfig.internal.ThemeConfig import org.polyfrost.oneconfig.internal.ui.api.settings.BuiltinVisualizers +import org.polyfrost.oneconfig.internal.ui.screens.THEMES_ID import org.polyfrost.oneconfig.internal.ui.sound.UiSoundEvent import org.polyfrost.oneconfig.internal.ui.sound.UiSoundTheme import org.polyfrost.oneconfig.internal.ui.sound.UiSounds @@ -79,7 +80,7 @@ object ThemeRegistry { ThemeConfig.activeTheme = theme.name syncNotificationTheme(theme) } - if (!ThemeConfig.polyfrostedRenameApplied) { + if (!ThemeConfig.polyfrostedRenameApplied && ConfigManager.active().exists(THEMES_ID)) { ThemeConfig.polyfrostedRenameApplied = true saveThemeToConfig() } @@ -88,7 +89,7 @@ object ThemeRegistry { private fun saveThemeToConfig() { try { - ConfigManager.active().save("themes.json") + ConfigManager.active().save(THEMES_ID) } catch (_: Exception) {} } } diff --git a/modules/internal/src/test/java/org/polyfrost/oneconfig/internal/ui/themes/GlassOpacityTest.java b/modules/internal/src/test/java/org/polyfrost/oneconfig/internal/ui/themes/GlassOpacityTest.java new file mode 100644 index 000000000..86b43b266 --- /dev/null +++ b/modules/internal/src/test/java/org/polyfrost/oneconfig/internal/ui/themes/GlassOpacityTest.java @@ -0,0 +1,15 @@ +package org.polyfrost.oneconfig.internal.ui.themes; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class GlassOpacityTest { + @Test + void zeroOpacityRemovesGlassAndDefaultKeepsIt() { + assertEquals(0f, StructsKt.glassOpacity(0f, 88f)); + assertEquals(0.5f, StructsKt.glassOpacity(44f, 88f)); + assertEquals(1f, StructsKt.glassOpacity(88f, 88f)); + assertEquals(1f, StructsKt.glassOpacity(100f, 88f)); + } +} diff --git a/modules/internal/src/test/java/org/polyfrost/oneconfig/internal/ui/themes/ThemeMigrationTest.java b/modules/internal/src/test/java/org/polyfrost/oneconfig/internal/ui/themes/ThemeMigrationTest.java new file mode 100644 index 000000000..d69d278cc --- /dev/null +++ b/modules/internal/src/test/java/org/polyfrost/oneconfig/internal/ui/themes/ThemeMigrationTest.java @@ -0,0 +1,28 @@ +package org.polyfrost.oneconfig.internal.ui.themes; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.polyfrost.oneconfig.internal.ThemeConfig; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ThemeMigrationTest { + @Test + void themeLoadBeforeConfigInitKeepsRename() throws Exception { + Path file = Path.of("config", "themes.json"); + Files.createDirectories(file.getParent()); + Files.writeString(file, "{\"activeTheme\":\"PolyGlass Light\"}"); + try { + ThemeConfig config = new ThemeConfig(); + ThemeRegistry.INSTANCE.loadFromConfig(); + config.preload(); + ThemeRegistry.INSTANCE.loadFromConfig(); + assertEquals("Polyfrosted Light", ThemeConfig.activeTheme); + assertTrue(ThemeConfig.polyfrostedRenameApplied); + } finally { + Files.deleteIfExists(file); + } + } +} diff --git a/modules/utils/api/utils.api b/modules/utils/api/utils.api index b186512c3..78b537418 100644 --- a/modules/utils/api/utils.api +++ b/modules/utils/api/utils.api @@ -177,6 +177,7 @@ public abstract interface class org/polyfrost/oneconfig/api/platform/v1/ScreenPl public fun runOnUiThread (Ljava/lang/Runnable;)V public fun screenToMcScale ()F public fun showMessage (Ljava/lang/String;)V + public fun supportsBackdropGlass ()Z public fun surfaceRatio ()F public abstract fun viewportHeight ()I public abstract fun viewportWidth ()I diff --git a/modules/utils/src/main/java/org/polyfrost/oneconfig/api/platform/v1/ScreenPlatform.java b/modules/utils/src/main/java/org/polyfrost/oneconfig/api/platform/v1/ScreenPlatform.java index 8e16a8150..07b4c4df5 100644 --- a/modules/utils/src/main/java/org/polyfrost/oneconfig/api/platform/v1/ScreenPlatform.java +++ b/modules/utils/src/main/java/org/polyfrost/oneconfig/api/platform/v1/ScreenPlatform.java @@ -35,6 +35,11 @@ default void runOnUiThread(Runnable action) { action.run(); } + /** Whether the UI can draw frosted glass over what is behind it on the active renderer. */ + default boolean supportsBackdropGlass() { + return true; + } + int viewportWidth(); int viewportHeight(); From ee64f1a6047f9d7312d9adca2150a5e01f276fc3 Mon Sep 17 00:00:00 2001 From: Julian Chang Date: Fri, 6 Nov 2026 01:30:14 -0500 Subject: [PATCH 12/12] fix --- modules/internal/build.gradle.kts | 8 +++++ .../oneconfig/internal/ThemeConfig.java | 3 ++ .../internal/ui/themes/ThemeRegistry.kt | 10 +----- .../ui/themes/ThemeMigrationTest.java | 35 ++++++++++++++++--- 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/modules/internal/build.gradle.kts b/modules/internal/build.gradle.kts index 641cf2973..22367ad87 100644 --- a/modules/internal/build.gradle.kts +++ b/modules/internal/build.gradle.kts @@ -49,3 +49,11 @@ kotlin { optIn.add("org.polyfrost.oneconfig.api.ui.v1.ExperimentalOneConfigUiApi") } } + +tasks.named("j21Tests") { + workingDir = layout.buildDirectory.dir("test-run").get().asFile + doFirst { + workingDir.deleteRecursively() + workingDir.mkdirs() + } +} diff --git a/modules/internal/src/main/java/org/polyfrost/oneconfig/internal/ThemeConfig.java b/modules/internal/src/main/java/org/polyfrost/oneconfig/internal/ThemeConfig.java index cbc11fb7a..bc8309b92 100644 --- a/modules/internal/src/main/java/org/polyfrost/oneconfig/internal/ThemeConfig.java +++ b/modules/internal/src/main/java/org/polyfrost/oneconfig/internal/ThemeConfig.java @@ -1,7 +1,9 @@ package org.polyfrost.oneconfig.internal; +import java.nio.file.Files; import org.polyfrost.compose.render.PolyColor; import org.polyfrost.oneconfig.api.config.v1.Config; +import org.polyfrost.oneconfig.api.config.v1.ConfigManager; import org.polyfrost.oneconfig.api.config.v1.annotations.Color; import org.polyfrost.oneconfig.api.config.v1.annotations.Switch; import org.polyfrost.oneconfig.api.config.v1.annotations.Text; @@ -25,6 +27,7 @@ public ThemeConfig() { @Override protected void initialize(boolean byConfigManager) { + if (tree == null) polyfrostedRenameApplied = !Files.exists(ConfigManager.active().getFolder().resolve(id)); super.initialize(byConfigManager); hideIf("activeTheme", () -> true); hideIf("polyfrostedRenameApplied", () -> true); diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt index 71c3cb636..96085cdf7 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/themes/ThemeRegistry.kt @@ -37,15 +37,7 @@ object ThemeRegistry { BuiltinVisualizers.register() - ConfigManager.addProfileChangeListener(object : ConfigManager.ProfileChangeListener { - override fun onProfileUnloading(profile: String) { - ThemeConfig.polyfrostedRenameApplied = false - } - - override fun onProfileChanged(newProfile: String) { - Platform.screen().runOnUiThread { loadFromConfig() } - } - }) + ConfigManager.addProfileChangeListener { Platform.screen().runOnUiThread { loadFromConfig() } } } fun init() {} diff --git a/modules/internal/src/test/java/org/polyfrost/oneconfig/internal/ui/themes/ThemeMigrationTest.java b/modules/internal/src/test/java/org/polyfrost/oneconfig/internal/ui/themes/ThemeMigrationTest.java index d69d278cc..b2576eed6 100644 --- a/modules/internal/src/test/java/org/polyfrost/oneconfig/internal/ui/themes/ThemeMigrationTest.java +++ b/modules/internal/src/test/java/org/polyfrost/oneconfig/internal/ui/themes/ThemeMigrationTest.java @@ -2,27 +2,52 @@ import java.nio.file.Files; import java.nio.file.Path; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.polyfrost.oneconfig.api.config.v1.ConfigManager; import org.polyfrost.oneconfig.internal.ThemeConfig; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) class ThemeMigrationTest { + private static final ThemeConfig CONFIG = new ThemeConfig(); + @Test + @Order(1) void themeLoadBeforeConfigInitKeepsRename() throws Exception { Path file = Path.of("config", "themes.json"); Files.createDirectories(file.getParent()); Files.writeString(file, "{\"activeTheme\":\"PolyGlass Light\"}"); + ThemeRegistry.INSTANCE.loadFromConfig(); + CONFIG.preload(); + ThemeRegistry.INSTANCE.loadFromConfig(); + assertEquals("Polyfrosted Light", ThemeConfig.activeTheme); + assertTrue(ThemeConfig.polyfrostedRenameApplied); + } + + @Test + @Order(2) + void profileSwitchRenamesOnlySavedThemes() throws Exception { + CONFIG.preload(); + Path oldSave = ConfigManager.PROFILES_DIR.resolve("theme-old-save"); + Files.createDirectories(ConfigManager.PROFILES_DIR.resolve("theme-no-save")); + Files.createDirectories(oldSave); + Files.writeString(oldSave.resolve("themes.json"), "{\"activeTheme\":\"PolyGlass Dark\"}"); + ThemeConfig.activeTheme = "PolyGlass Dark"; try { - ThemeConfig config = new ThemeConfig(); + ConfigManager.openProfile("theme-no-save"); ThemeRegistry.INSTANCE.loadFromConfig(); - config.preload(); + assertEquals("PolyGlass Dark", ThemeConfig.activeTheme); + + ConfigManager.openProfile("theme-old-save"); ThemeRegistry.INSTANCE.loadFromConfig(); - assertEquals("Polyfrosted Light", ThemeConfig.activeTheme); - assertTrue(ThemeConfig.polyfrostedRenameApplied); + assertEquals("Polyfrosted Dark", ThemeConfig.activeTheme); } finally { - Files.deleteIfExists(file); + ConfigManager.openProfile(""); } } }