Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/main/java/top/fpsmaster/ui/kit/EdgeCanvas.java
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,18 @@ public void popClip() {
applyClip(clips.peek());
}

/**
* Current composed clip, or {@code null} when scissor is off. Used by
* {@link EdgeInput} so hit-tests match what {@link #pushClip} actually draws.
*/
float[] currentClip() {
float[] clip = clips.peek();
if (clip == null) {
return null;
}
return new float[] {clip[0], clip[1], clip[2], clip[3]};
}

private static void applyClip(float[] clip) {
GL11.glEnable(GL11.GL_SCISSOR_TEST);
if (clip[2] <= 0f || clip[3] <= 0f) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/top/fpsmaster/ui/kit/EdgeHost.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ final class EdgeHost implements UiHost {

EdgeHost(ScaledGuiScreen screen, FrameInput fallback, float width, float height) {
this.screen = screen;
this.input = new EdgeInput(screen, fallback);
this.input = new EdgeInput(screen, fallback, canvas);
this.width = width;
this.height = height;
}
Expand Down
43 changes: 35 additions & 8 deletions src/main/java/top/fpsmaster/ui/kit/EdgeInput.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package top.fpsmaster.ui.kit;

import top.fpsmaster.utils.render.gui.ScaledGuiScreen;
import top.fpsmaster.utils.render.gui.Scissor;
import top.fpsmaster.prism.input.FrameInput;
import top.fpsmaster.prism.input.Input;
import top.fpsmaster.prism.input.PointerEvent;

final class EdgeInput implements Input {
private final ScaledGuiScreen screen;
private final FrameInput fallback;
private final EdgeCanvas canvas;

EdgeInput(ScaledGuiScreen screen, FrameInput fallback) {
EdgeInput(ScaledGuiScreen screen, FrameInput fallback, EdgeCanvas canvas) {
this.screen = screen;
this.fallback = fallback;
this.canvas = canvas;
}

public int mouseX() {
Expand All @@ -27,10 +30,14 @@ public boolean isButtonDown(int button) {
}

public PointerEvent consumePressInBounds(float x, float y, float w, float h, int button) {
float[] hit = clippedHit(x, y, w, h);
if (!Scissor.hasArea(hit)) {
return null;
}
if (screen == null) {
return fallback.consumePressInBounds(x, y, w, h, button);
return fallback.consumePressInBounds(hit[0], hit[1], hit[2], hit[3], button);
}
ScaledGuiScreen.PointerEvent event = screen.consumePressInBounds(x, y, w, h, button);
ScaledGuiScreen.PointerEvent event = screen.consumePressInBounds(hit[0], hit[1], hit[2], hit[3], button);
return event == null ? null : new PointerEvent(event.x, event.y, event.button);
}

Expand All @@ -54,14 +61,24 @@ public boolean hasPressOutside(float x, float y, float w, float h) {
}

public int consumeWheelDelta(float x, float y, float w, float h) {
return screen != null ? screen.consumeWheelDelta(x, y, w, h) : fallback.consumeWheelDelta(x, y, w, h);
float[] hit = clippedHit(x, y, w, h);
if (!Scissor.hasArea(hit)) {
return 0;
}
return screen != null
? screen.consumeWheelDelta(hit[0], hit[1], hit[2], hit[3])
: fallback.consumeWheelDelta(hit[0], hit[1], hit[2], hit[3]);
}

public void markHovered(Object id, float x, float y, float w, float h) {
float[] hit = clippedHit(x, y, w, h);
if (!Scissor.hasArea(hit)) {
return;
}
if (screen != null) {
screen.isHovered(id, x, y, w, h);
screen.isHovered(id, hit[0], hit[1], hit[2], hit[3]);
} else {
fallback.markHovered(id, x, y, w, h);
fallback.markHovered(id, hit[0], hit[1], hit[2], hit[3]);
}
}

Expand All @@ -70,8 +87,13 @@ public boolean wasHovered(Object id) {
}

public boolean beginDrag(Object owner, int button, float x, float y, float w, float h) {
return screen != null ? screen.beginDrag(owner, button, x, y, w, h)
: fallback.beginDrag(owner, button, x, y, w, h);
float[] hit = clippedHit(x, y, w, h);
if (!Scissor.hasArea(hit)) {
return screen != null ? screen.isDragging(owner) : fallback.isDragging(owner);
}
return screen != null
? screen.beginDrag(owner, button, hit[0], hit[1], hit[2], hit[3])
: fallback.beginDrag(owner, button, hit[0], hit[1], hit[2], hit[3]);
}

public boolean isDragging(Object owner) {
Expand Down Expand Up @@ -109,4 +131,9 @@ public String clipboard() {
public void setClipboard(String text) {
fallback.setClipboard(text);
}

/** Intersect the widget with the active canvas clip so off-screen rows cannot steal clicks. */
private float[] clippedHit(float x, float y, float w, float h) {
return Scissor.constrainHit(canvas.currentClip(), x, y, w, h);
}
}
22 changes: 22 additions & 0 deletions src/main/java/top/fpsmaster/utils/render/gui/Scissor.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,28 @@ public static float[] intersect(
};
}

/**
* Shrink a widget hit box to the current clip. {@code clip == null} keeps the
* original rectangle (no scissor is active). Empty width/height means the
* widget is fully outside the clip and must not consume the pointer.
*/
public static float[] constrainHit(float[] clip, float x, float y, float w, float h) {
if (clip == null) {
return new float[] {x, y, w, h};
}
return intersect(clip[0], clip[1], clip[2], clip[3], x, y, w, h);
}

public static boolean hasArea(float[] rect) {
return rect != null && rect[2] > 0f && rect[3] > 0f;
}

public static boolean contains(float[] rect, float px, float py) {
return hasArea(rect)
&& px >= rect[0] && px <= rect[0] + rect[2]
&& py >= rect[1] && py <= rect[1] + rect[3];
}

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class ScissorIntersectTest {

Expand Down Expand Up @@ -40,4 +42,34 @@ void negativeExtentsDoNotExpandTheOtherRect() {
float[] clipped = Scissor.intersect(0f, 0f, 20f, 20f, 4f, 4f, -8f, 10f);
assertEquals(0f, clipped[2], 0.01f);
}

@Test
void constrainHitDropsRowsBelowTheComposedClip() {
float[] list = {140f, 40f, 330f, 270f};
float[] settings = {146f, 70f, 318f, 950f};
float[] clip = Scissor.intersect(
list[0], list[1], list[2], list[3],
settings[0], settings[1], settings[2], settings[3]);
float[] hiddenRow = Scissor.constrainHit(clip, 146f, 400f, 318f, 18f);
assertFalse(Scissor.hasArea(hiddenRow));
assertFalse(Scissor.contains(clip, 200f, 400f));
}

@Test
void constrainHitKeepsTheVisibleOverlap() {
float[] clip = {140f, 40f, 330f, 270f};
float[] visible = Scissor.constrainHit(clip, 146f, 280f, 318f, 40f);
assertEquals(146f, visible[0], 0.01f);
assertEquals(280f, visible[1], 0.01f);
assertEquals(318f, visible[2], 0.01f);
assertEquals(30f, visible[3], 0.01f);
assertTrue(Scissor.contains(visible, 200f, 300f));
assertFalse(Scissor.contains(visible, 200f, 320f));
}

@Test
void constrainHitWithoutClipLeavesTheWidgetAlone() {
assertArrayEquals(new float[] {8f, 12f, 40f, 16f},
Scissor.constrainHit(null, 8f, 12f, 40f, 16f), 0.01f);
}
}
Loading