diff --git a/src/main/java/top/fpsmaster/ui/kit/EdgeCanvas.java b/src/main/java/top/fpsmaster/ui/kit/EdgeCanvas.java index fa8f20d1..150c306c 100644 --- a/src/main/java/top/fpsmaster/ui/kit/EdgeCanvas.java +++ b/src/main/java/top/fpsmaster/ui/kit/EdgeCanvas.java @@ -17,7 +17,7 @@ import java.util.Deque; final class EdgeCanvas implements Canvas { - private int clipDepth; + private final Deque clips = new ArrayDeque(); private final Deque alpha = new ArrayDeque(); EdgeCanvas() { @@ -98,18 +98,33 @@ public void drawImage(ImageHandle image, float x, float y, float w, float h, int } public void pushClip(float x, float y, float w, float h) { - clipDepth++; - GL11.glEnable(GL11.GL_SCISSOR_TEST); - Scissor.apply(x, y, w, h); + float[] parent = clips.peek(); + float[] next = parent == null + ? new float[] {x, y, w, h} + : Scissor.intersect(parent[0], parent[1], parent[2], parent[3], x, y, w, h); + clips.push(next); + applyClip(next); } public void popClip() { - if (clipDepth > 0) { - clipDepth--; + if (clips.isEmpty()) { + return; } - if (clipDepth == 0) { + clips.pop(); + if (clips.isEmpty()) { GL11.glDisable(GL11.GL_SCISSOR_TEST); + return; + } + applyClip(clips.peek()); + } + + private static void applyClip(float[] clip) { + GL11.glEnable(GL11.GL_SCISSOR_TEST); + if (clip[2] <= 0f || clip[3] <= 0f) { + GL11.glScissor(0, 0, 0, 0); + return; } + Scissor.apply(clip[0], clip[1], clip[2], clip[3]); } public void pushAlpha(float a) { diff --git a/src/main/java/top/fpsmaster/utils/render/gui/Scissor.java b/src/main/java/top/fpsmaster/utils/render/gui/Scissor.java index 730e0b19..0bf632a5 100644 --- a/src/main/java/top/fpsmaster/utils/render/gui/Scissor.java +++ b/src/main/java/top/fpsmaster/utils/render/gui/Scissor.java @@ -4,6 +4,30 @@ import org.lwjgl.opengl.GL11; public class Scissor { + /** + * Intersect two axis-aligned rectangles. Width/height are {@code 0} when they + * do not overlap. Used so nested {@code pushClip} calls compose instead of + * replacing the current scissor (expanded ClickGUI modules must stay inside + * the list viewport). + */ + public static float[] intersect( + float ax, float ay, float aw, float ah, + float bx, float by, float bw, float bh + ) { + float aRight = ax + Math.max(0f, aw); + float aBottom = ay + Math.max(0f, ah); + float bRight = bx + Math.max(0f, bw); + float bBottom = by + Math.max(0f, bh); + float x = Math.max(ax, bx); + float y = Math.max(ay, by); + return new float[] { + x, + y, + Math.max(0f, Math.min(aRight, bRight) - x), + Math.max(0f, Math.min(aBottom, bBottom) - y) + }; + } + public static void apply(float x, float y, float width, float height) { float scale = UiScale.isActive() ? UiScale.getLayoutScale() : 1.0f; applyScaled(x, y, width, height, scale); diff --git a/src/test/java/top/fpsmaster/utils/render/gui/ScissorIntersectTest.java b/src/test/java/top/fpsmaster/utils/render/gui/ScissorIntersectTest.java new file mode 100644 index 00000000..94204970 --- /dev/null +++ b/src/test/java/top/fpsmaster/utils/render/gui/ScissorIntersectTest.java @@ -0,0 +1,43 @@ +package top.fpsmaster.utils.render.gui; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ScissorIntersectTest { + + @Test + void nestedSettingsClipStaysInsideListViewport() { + // SharedClickGui clips the module list (panel body), then each expanded module + // clips its settings to the card — Performance's card is taller than the panel. + float[] list = {140f, 40f, 330f, 270f}; + float[] settings = {146f, 70f, 318f, 950f}; + float[] clipped = Scissor.intersect( + list[0], list[1], list[2], list[3], + settings[0], settings[1], settings[2], settings[3]); + assertEquals(146f, clipped[0], 0.01f); + assertEquals(70f, clipped[1], 0.01f); + assertEquals(318f, clipped[2], 0.01f); + assertEquals(240f, clipped[3], 0.01f); + } + + @Test + void disjointRectsAreEmpty() { + float[] clipped = Scissor.intersect(0f, 0f, 10f, 10f, 20f, 20f, 5f, 5f); + assertEquals(0f, clipped[2], 0.01f); + assertEquals(0f, clipped[3], 0.01f); + } + + @Test + void identicalRectsStayIdentical() { + assertArrayEquals(new float[] {8f, 12f, 40f, 16f}, + Scissor.intersect(8f, 12f, 40f, 16f, 8f, 12f, 40f, 16f), 0.01f); + } + + @Test + void negativeExtentsDoNotExpandTheOtherRect() { + float[] clipped = Scissor.intersect(0f, 0f, 20f, 20f, 4f, 4f, -8f, 10f); + assertEquals(0f, clipped[2], 0.01f); + } +}