Skip to content
Merged
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
13 changes: 8 additions & 5 deletions src/main/java/top/fpsmaster/features/GlobalListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,14 @@ public void onRender(EventRender2D e) {

if (ClientSettings.blur.getValue()) {
StencilUtil.initStencilToWrite();
EventDispatcher.dispatchEvent(new EventShader());
FPSMaster.componentsManager.drawBackgroundMasks();
StencilUtil.readStencilBuffer(1);
KawaseBlur.renderBlur(3, 3);
StencilUtil.uninitStencilBuffer();
try {
EventDispatcher.dispatchEvent(new EventShader());
FPSMaster.componentsManager.drawBackgroundMasks();
StencilUtil.readStencilBuffer(1);
KawaseBlur.renderBlur(3, 3);
} finally {
StencilUtil.uninitStencilBuffer();
}
}

FPSMaster.componentsManager.draw((int) mouseX, (int) mouseY);
Expand Down
16 changes: 12 additions & 4 deletions src/main/java/top/fpsmaster/ui/click/MainPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,13 @@ public void keyTyped(char typedChar, int keyCode) throws IOException {
super.keyTyped(typedChar, keyCode);
}

@Override
protected float[] getOccludingBounds() {
// Only the panel itself, not the whole screen: dragging a HUD element that sits beside the
// ClickGUI is exactly what the user opened it for.
return new float[]{x, y, width, height};
}

private void handlePointerPress() {
ScaledGuiScreen.PointerEvent press = peekAnyPress();
if (press == null) {
Expand All @@ -420,10 +427,11 @@ private void handlePointerPress() {
return;
}

if (hasPointerCapture()) {
return;
}

// A hasPointerCapture() guard used to sit here. It compensated for peekAnyPress() returning
// presses that another widget had already consumed — beginDrag consumes the press that starts
// a slider drag, yet the category strip still saw it and switched category underneath. Now that
// peekAnyPress() skips consumed presses the guard is redundant: the press is gone on the frame
// a drag begins, and later frames of the same drag produce no press at all.
float my = getCategoryStartY();
for (Category c : Category.values()) {
if (Hover.is(x, my - 8, leftWidth, 24f, mouseX, mouseY)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,9 @@ public void render(ScaledGuiScreen screen, float x, float y, float width, float
settingHeight = (float) AnimMath.base(settingHeight, settingsHeight, 0.2);
this.height = settingHeight;

ScaledGuiScreen.PointerEvent click = screen.consumePressInBounds(x + 5, y, width - 10, 40f);
// Gated on z-order: the module header sits under the settings of the module expanded above it,
// and under any panel drawn later. `mod` is a stable per-frame identity.
ScaledGuiScreen.PointerEvent click = screen.consumePressAsHovered(mod, x + 5, y, width - 10, 40f);
if (click != null) {
if (click.button == 0) {
mod.toggle();
Expand Down
9 changes: 8 additions & 1 deletion src/main/java/top/fpsmaster/ui/custom/Component.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import top.fpsmaster.modules.logger.ClientLogger;
import top.fpsmaster.ui.click.MainPanel;
import top.fpsmaster.utils.core.Utility;
import top.fpsmaster.utils.render.gui.GuiOcclusion;
import top.fpsmaster.utils.math.anim.AnimMath;

import java.awt.*;
Expand Down Expand Up @@ -227,8 +228,14 @@ public void display(ScaledResolution sr, int mouseX, int mouseY) {
}

if (hovered || overHandle || drag) {
if (Utility.mc.currentScreen instanceof MainPanel && ((MainPanel) Utility.mc.currentScreen).hasPointerCapture())
// The HUD editor runs from EventRender2D, which 1.8.9 fires while drawing the in-game
// overlay — before currentScreen.drawScreen(). Without this it happily drags components
// that are sitting *underneath* an open panel. The previous guard only covered the case
// where a ClickGUI slider already held a drag capture, which is a small fraction of the
// panel's area.
if (GuiOcclusion.covers(Mouse.getX(), Utility.mc.displayHeight - Mouse.getY())) {
return;
}
if (allowScale && ClientSettings.isZoomBindDown()) {
int dWheel = Mouse.getDWheel();
if (dWheel > 0) scaleUp();
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/top/fpsmaster/ui/screens/oobe/OobeDropdown.java
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,9 @@ public void renderInScreen(ScaledGuiScreen screen, float x, float y, float width
}
}

ScaledGuiScreen.PointerEvent outside = screen.peekAnyPress();
if (outside != null && openProgress > 0.95f && !Hover.is(x, y, width, height + panelHeight + 4f, outside.x, outside.y)) {
// Consume the dismissing click, otherwise it also activates whatever sits behind the popup.
if (openProgress > 0.95f
&& screen.consumePressOutside(x, y, width, height + panelHeight + 4f) != null) {
open = false;
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/top/fpsmaster/utils/render/StencilUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,21 @@ public static void readStencilBuffer(int ref) {
GL11.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP);
}

/**
* Ends a stencil pass and restores everything {@link #initStencilToWrite()} changed.
*
* <p>The colour mask used to be reopened only by {@link #readStencilBuffer(int)}, which is a
* "switch to reading" call, not a teardown call. Any caller that threw — or returned early —
* between writing and reading left the mask fully closed for the rest of the process. The symptom
* is remote from the cause: the next {@code glClear(GL_COLOR_BUFFER_BIT)} anywhere (KawaseBlur's
* framebuffer clear, for instance) asks the driver to clear a colour buffer it is forbidden to
* write, which on Apple's Metal-backed GL aborts with "missing clear mask bits".
*
* <p>Reopening it here as well is idempotent and makes teardown total.
*/
public static void uninitStencilBuffer() {
GL11.glDisable(GL11.GL_STENCIL_TEST);
GL11.glColorMask(true, true, true, true);
}
}

Expand Down
11 changes: 7 additions & 4 deletions src/main/java/top/fpsmaster/utils/render/effects/Blur.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ public static void area(int x, int y, int width, int height, int radius, Color c

public static void area(float x, float y, float width, float height, int radius, Color color, int iterations, int offset) {
StencilUtil.initStencilToWrite();
RoundedUtil.drawRound(x, y, width, height, radius, true, color);
StencilUtil.readStencilBuffer(1);
KawaseBlur.renderBlur(iterations, offset);
StencilUtil.uninitStencilBuffer();
try {
RoundedUtil.drawRound(x, y, width, height, radius, true, color);
StencilUtil.readStencilBuffer(1);
KawaseBlur.renderBlur(iterations, offset);
} finally {
StencilUtil.uninitStencilBuffer();
}
}
}
66 changes: 65 additions & 1 deletion src/main/java/top/fpsmaster/utils/render/gui/GuiInputState.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ private void consume() {
private int mouseY;
private int wheelDelta;

private Object hoveredId;
private Object lastHoveredId;

public void updateMousePosition(int mouseX, int mouseY) {
this.mouseX = mouseX;
this.mouseY = mouseY;
Expand Down Expand Up @@ -87,8 +90,41 @@ public boolean hasPressEvent() {
return !pressEvents.isEmpty();
}

/**
* The most recent press that nobody has claimed yet.
*
* <p>This used to return the press regardless of consumption, so a widget peeking at it could act
* on a click another widget had already handled — which is why {@code MainPanel} needed a
* {@code hasPointerCapture()} guard to undo the double handling.
*/
public MouseButtonEvent getLatestPress() {
return latestPress;
return latestPress != null && latestPress.isConsumed() ? null : latestPress;
}

/**
* Records that the cursor is inside this widget, overwriting whatever was recorded earlier in the
* frame.
*
* <p>This is how z-order falls out of an immediate-mode GUI: widgets are visited in paint order,
* so the <em>last</em> one to claim the cursor is the one drawn on top. No comparison is needed —
* later simply wins. The result is only usable next frame, once the whole frame has been walked,
* which is why {@link #wasHovered} reads the previous frame's value. GUI layout barely moves
* between frames, so the one-frame lag is not observable.
*/
public void markHovered(Object id, float x, float y, float width, float height) {
if (mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height) {
hoveredId = id;
}
}

/** Whether {@code id} was the topmost widget under the cursor at the end of the previous frame. */
public boolean wasHovered(Object id) {
return id != null && id.equals(lastHoveredId);
}

/** True until any widget has claimed the cursor, i.e. nothing is known to be on top yet. */
public boolean hasHoveredId() {
return lastHoveredId != null;
}

public int getMouseX() {
Expand Down Expand Up @@ -120,7 +156,34 @@ public MouseButtonEvent consumePressInBounds(float x, float y, float width, floa
return null;
}

/**
* Claims a press that landed <em>outside</em> the given rectangle — the "click away to dismiss"
* gesture for popups.
*
* <p>Consuming it is the whole point: a popup that merely notices the outside click and closes
* itself lets that same click through to whatever it was covering, so dismissing a dropdown also
* activates the button behind it.
*/
public MouseButtonEvent consumePressOutside(float x, float y, float width, float height) {
for (MouseButtonEvent event : pressEvents) {
if (event.isConsumed()) {
continue;
}
boolean inside = event.getX() >= x && event.getX() <= x + width
&& event.getY() >= y && event.getY() <= y + height;
if (inside) {
continue;
}
event.consume();
return event;
}
return null;
}

public void finishFrame() {
// Hand this frame's topmost-under-cursor result to the next frame, where it arbitrates clicks.
lastHoveredId = hoveredId;
hoveredId = null;
pressEvents.clear();
latestPress = null;
wheelDelta = 0;
Expand All @@ -132,6 +195,7 @@ public void reset() {
}
mouseX = 0;
mouseY = 0;
lastHoveredId = null;
finishFrame();
}
}
49 changes: 49 additions & 0 deletions src/main/java/top/fpsmaster/utils/render/gui/GuiOcclusion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package top.fpsmaster.utils.render.gui;

/**
* Where the topmost custom screen is painting, in device pixels.
*
* <p>Hit-test ordering within one screen is handled by {@code GuiInputState}'s hovered-id. This solves
* the other half: the HUD component editor runs from {@code EventRender2D}, which 1.8.9 fires while
* drawing the in-game overlay — that is, <em>before</em> {@code currentScreen.drawScreen()}. The editor
* therefore reacts to presses underneath a screen that has not even been painted yet, using a
* completely separate input path, with no way for the two to arbitrate.
*
* <p>The fix is not to disable HUD editing while a panel is open — opening the ClickGUI is precisely
* when a user wants to move HUD elements. It is to make the panel occlude the cursor: components under
* the panel rectangle stop responding, components beside it keep working.
*
* <p>Device pixels are used because that is the only space the HUD editor and the ClickGUI agree on;
* they scale their own coordinates by different factors.
*/
public final class GuiOcclusion {

private static float x;
private static float y;
private static float width;
private static float height;
private static boolean active;

private GuiOcclusion() {
}

/** Reported once per frame by the screen that is painting, before the next frame's HUD pass. */
public static void set(float deviceX, float deviceY, float deviceWidth, float deviceHeight) {
x = deviceX;
y = deviceY;
width = deviceWidth;
height = deviceHeight;
active = deviceWidth > 0f && deviceHeight > 0f;
}

public static void clear() {
active = false;
}

/** @param deviceX cursor position in device pixels, y measured from the top */
public static boolean covers(float deviceX, float deviceY) {
return active
&& deviceX >= x && deviceX <= x + width
&& deviceY >= y && deviceY <= y + height;
}
}
75 changes: 75 additions & 0 deletions src/main/java/top/fpsmaster/utils/render/gui/ScaledGuiScreen.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) {
activeScreen = this;
GL11.glScalef(renderScale, renderScale, 1f);
render(inputState.getMouseX(), inputState.getMouseY(), partialTicks);
// Reported after render() so subclasses that compute their layout there (MainPanel centres
// itself every frame) have already done so. The HUD editor reads it on the next frame,
// which is fine — it runs ahead of this one within a frame anyway.
publishOcclusion();
} finally {
inputState.finishFrame();
activeScreen = null;
Expand All @@ -74,6 +78,27 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) {
}
}

/**
* The rectangle this screen paints over, in logical coordinates, or {@code null} for "the whole
* screen". Override in screens that only cover part of the display so the HUD stays interactive
* around them.
*
* @return {@code {x, y, width, height}}
*/
protected float[] getOccludingBounds() {
return null;
}

private void publishOcclusion() {
float[] bounds = getOccludingBounds();
if (bounds == null) {
GuiOcclusion.set(0f, 0f, mc.displayWidth, mc.displayHeight);
return;
}
GuiOcclusion.set(bounds[0] * scaleFactor, bounds[1] * scaleFactor,
bounds[2] * scaleFactor, bounds[3] * scaleFactor);
}

@Override
public void onResize(Minecraft mcIn, int w, int h) {
super.onResize(mcIn, w, h);
Expand All @@ -88,6 +113,14 @@ public void initGui() {
super.initGui();
}

@Override
public void onGuiClosed() {
// Nothing is covering the HUD any more; leaving this set would freeze the editor under a
// rectangle that is no longer painted.
GuiOcclusion.clear();
super.onGuiClosed();
}

@Override
public void handleMouseInput() throws IOException {
refreshScaleAndMetrics();
Expand Down Expand Up @@ -261,6 +294,48 @@ public PointerEvent consumeClickInBounds(float x, float y, float width, float he
return consumePressInBounds(x, y, width, height, button);
}

/**
* Claims a click only if this widget was the topmost one under the cursor.
*
* <p>{@link #consumePressInBounds} alone is first-come-first-served, and widgets call it right
* after painting themselves — so the <em>first painted</em>, i.e. bottom-most, widget wins a
* contested click. That is the inverse of what z-order requires. This variant adds the missing
* ordering: {@code markHovered} records the topmost widget as the frame is walked, and the
* previous frame's answer gates the click here.
*
* <p>Widgets that still call {@code consumePressInBounds} keep the old behaviour, so this can be
* adopted one widget at a time.
*
* @param id stable identity for this widget across frames — the setting/module object itself works
* well; avoid values that are rebuilt every frame
*/
public PointerEvent consumePressAsHovered(Object id, float x, float y, float width, float height, int button) {
inputState.markHovered(id, x, y, width, height);
if (!inputState.wasHovered(id)) {
return null;
}
return consumePressInBounds(x, y, width, height, button);
}

public PointerEvent consumePressAsHovered(Object id, float x, float y, float width, float height) {
return consumePressAsHovered(id, x, y, width, height, -1);
}

/**
* Claims a press that landed outside the given rectangle — "click away to dismiss" for popups.
* The press is consumed so it does not also reach whatever the popup was covering.
*/
public PointerEvent consumePressOutside(float x, float y, float width, float height) {
GuiInputState.MouseButtonEvent event = inputState.consumePressOutside(x, y, width, height);
return event == null ? null : new PointerEvent(event.getX(), event.getY(), event.getButton());
}

/** Hover feedback should follow the same z-order rule as clicks, or highlights double up. */
public boolean isHovered(Object id, float x, float y, float width, float height) {
inputState.markHovered(id, x, y, width, height);
return inputState.wasHovered(id);
}

private int getLogicalMouseX() {
return clampLogicalMouseX((int) (Mouse.getX() / scaleFactor));
}
Expand Down
Loading