diff --git a/src/main/java/top/fpsmaster/features/GlobalListener.java b/src/main/java/top/fpsmaster/features/GlobalListener.java
index 30c1517f..9efe172a 100644
--- a/src/main/java/top/fpsmaster/features/GlobalListener.java
+++ b/src/main/java/top/fpsmaster/features/GlobalListener.java
@@ -75,6 +75,10 @@ public void onRender(EventRender2D e) {
float mouseX = (float) Mouse.getX() / scaledResolution.getScaleFactor();
float mouseY = scaledResolution.getScaledHeight() - (float) Mouse.getY() / scaledResolution.getScaleFactor();
+ // Size everything first: the blur mask and the anchor math below both read width/height, and
+ // components only assign those while drawing.
+ FPSMaster.componentsManager.measureAll();
+
if (ClientSettings.blur.getValue()) {
StencilUtil.initStencilToWrite();
EventDispatcher.dispatchEvent(new EventShader());
diff --git a/src/main/java/top/fpsmaster/ui/custom/Component.java b/src/main/java/top/fpsmaster/ui/custom/Component.java
index d7b0aa79..e156f9f2 100644
--- a/src/main/java/top/fpsmaster/ui/custom/Component.java
+++ b/src/main/java/top/fpsmaster/ui/custom/Component.java
@@ -71,6 +71,48 @@ public Component(Class> clazz) {
public void draw(float x, float y) {
}
+ /**
+ * Computes {@link #width}/{@link #height} for this frame, before anything reads them.
+ *
+ *
Historically every component assigned its size inside {@code draw()}, but anchoring
+ * ({@link #getRealPosition}), hover testing, drag clamping and the blur mask all run before
+ * {@code draw()} — so they used the previous frame's size. A component whose size changes each
+ * frame (FPS going {@code 99fps} → {@code 100fps}, a potion expiring) visibly jitters, and on its
+ * very first frame {@code width} is still 0.
+ *
+ *
Default implementation does nothing, so components that have not been migrated keep their old
+ * behaviour. Override it, move the sizing math here, and {@code draw()} becomes pure rendering.
+ */
+ public void measure() {
+ }
+
+ /** Receives the rectangles that make up a component's background. */
+ public interface ShapeSink {
+ /**
+ * @param x absolute left edge, already scaled by the caller (same convention as
+ * {@link #drawRect}: positions are the caller's job, sizes are the base class's)
+ * @param y absolute top edge
+ * @param width logical width; the base class multiplies by {@link #scale}
+ * @param height logical height
+ */
+ void rect(float x, float y, float width, float height);
+ }
+
+ /**
+ * Declares the geometry of this component's background so the blur mask can reproduce it exactly
+ * instead of guessing.
+ *
+ *
The mask used to assume every component draws one rectangle at {@code (rX - 2, rY)} sized
+ * {@code width × height}. That holds for about two thirds of them; the rest either use a different
+ * origin (Keystrokes, PotionDisplay) or draw several disjoint boxes (ArmorDisplay), so the blur
+ * leaked into the gaps or sat a couple of pixels off.
+ *
+ *
Whatever a component declares here must match what it actually paints in {@code draw()}.
+ */
+ public void backgroundShape(ShapeSink sink, float originX, float originY) {
+ sink.rect(originX - 2f, originY, width, height);
+ }
+
public float alpha = 0f;
public boolean shouldDisplay() {
@@ -131,13 +173,21 @@ public void drawBlurMask(ScaledResolution sr) {
return;
}
float[] pos = getRealPosition(sr);
- float scaledWidth = width * scale;
- float scaledHeight = height * scale;
- if (mod.rounded.getValue()) {
- Rects.roundedImage(Math.round(pos[0] - 2), Math.round(pos[1]), Math.round(scaledWidth), Math.round(scaledHeight), mod.roundRadius.getValue().intValue(), STENCIL_MASK_COLOR);
- } else {
- Rects.fill(pos[0] - 2, pos[1], scaledWidth, scaledHeight, STENCIL_MASK_COLOR);
- }
+ boolean round = mod.rounded.getValue();
+ int radius = mod.roundRadius.getValue().intValue();
+ // Same geometry the component paints, so the mask can never drift from the background.
+ backgroundShape((rectX, rectY, rectWidth, rectHeight) -> {
+ float scaledWidth = rectWidth * scale;
+ float scaledHeight = rectHeight * scale;
+ if (scaledWidth <= 0f || scaledHeight <= 0f) {
+ return;
+ }
+ if (round) {
+ Rects.roundedImage(Math.round(rectX), Math.round(rectY), Math.round(scaledWidth), Math.round(scaledHeight), radius, STENCIL_MASK_COLOR);
+ } else {
+ Rects.fill(rectX, rectY, scaledWidth, scaledHeight, STENCIL_MASK_COLOR);
+ }
+ }, pos[0], pos[1]);
}
public void display(ScaledResolution sr, int mouseX, int mouseY) {
diff --git a/src/main/java/top/fpsmaster/ui/custom/ComponentsManager.java b/src/main/java/top/fpsmaster/ui/custom/ComponentsManager.java
index 76072c84..db3211f7 100644
--- a/src/main/java/top/fpsmaster/ui/custom/ComponentsManager.java
+++ b/src/main/java/top/fpsmaster/ui/custom/ComponentsManager.java
@@ -94,6 +94,24 @@ private void resetRenderState() {
GlStateManager.color(1f, 1f, 1f, 1f);
}
+ /**
+ * Sizes every visible component for this frame. Must run before anything reads width/height —
+ * anchoring, hover testing, drag clamping and the blur mask all do, and all of them used to see
+ * the previous frame's values. Called once per frame from GlobalListener, ahead of both the blur
+ * mask pass and the draw pass.
+ */
+ public void measureAll() {
+ components.forEach(component -> {
+ if (component.shouldDisplay()) {
+ try {
+ component.measure();
+ } catch (Throwable throwable) {
+ onComponentFailure(component, "measure", throwable);
+ }
+ }
+ });
+ }
+
public void drawBackgroundMasks() {
GL11.glPushMatrix();
net.minecraft.client.gui.ScaledResolution sr = new net.minecraft.client.gui.ScaledResolution(Utility.mc);
diff --git a/src/main/java/top/fpsmaster/ui/custom/TextComponent.java b/src/main/java/top/fpsmaster/ui/custom/TextComponent.java
new file mode 100644
index 00000000..15302349
--- /dev/null
+++ b/src/main/java/top/fpsmaster/ui/custom/TextComponent.java
@@ -0,0 +1,93 @@
+package top.fpsmaster.ui.custom;
+
+import top.fpsmaster.FPSMaster;
+
+/**
+ * A HUD component that is one background panel with a single line of text on it.
+ *
+ *
Nine components repeated the same four statements verbatim — measure the string, set a fixed
+ * height, paint the panel, paint the text — differing only in font size, padding, box height and text
+ * offset. Expressing that as a template also fixes the sizing order for free: {@link #measure()} runs
+ * before anything reads {@code width}/{@code height}, instead of the size being assigned midway
+ * through {@code draw()}.
+ */
+public abstract class TextComponent extends Component {
+
+ private String measured;
+
+ protected TextComponent(Class> clazz) {
+ super(clazz);
+ }
+
+ /** The line to render, or {@code null} when there is nothing to show this frame. */
+ protected abstract String text();
+
+ protected abstract int fontSize();
+
+ protected abstract int textColor();
+
+ protected float horizontalPadding() {
+ return 4f;
+ }
+
+ protected float boxHeight() {
+ return 14f;
+ }
+
+ protected float textOffsetX() {
+ return 0f;
+ }
+
+ protected float textOffsetY() {
+ return 2f;
+ }
+
+ /**
+ * Resolves a user-editable label, falling back to the translation and then to a built-in string.
+ *
+ *
Three components carried a private copy of this. Each also wrote the resolved value back with
+ * {@code setValue} from inside {@code draw()}, which fires EventValueChange and therefore a config
+ * save — from the render path, potentially every frame. Resolving without persisting keeps the same
+ * display and drops the write entirely: an empty setting simply resolves again next frame.
+ *
+ * @param i18nKey translation key; {@code Language.get} returns the key itself when unmapped
+ * @param fallback used when the key has no translation
+ */
+ protected String resolveLabel(String configured, String i18nKey, String fallback) {
+ String label = configured;
+ if (label == null || label.trim().isEmpty()) {
+ label = FPSMaster.i18n.get(i18nKey);
+ if (i18nKey.equals(label)) {
+ label = fallback;
+ }
+ }
+ if (!label.endsWith(":") && !label.endsWith(":") && !label.endsWith(" ")) {
+ label += ": ";
+ }
+ return label;
+ }
+
+ @Override
+ public void measure() {
+ measured = text();
+ if (measured == null) {
+ // Zero the size rather than leaving last frame's: a stale width would have the blur mask
+ // stamp a panel this component is no longer drawing.
+ width = 0f;
+ height = 0f;
+ return;
+ }
+ width = getStringWidth(fontSize(), measured) + horizontalPadding();
+ height = boxHeight();
+ }
+
+ @Override
+ public void draw(float x, float y) {
+ if (measured == null) {
+ return;
+ }
+ drawRect(x - 2, y, width, height, mod.backgroundColor.getColor());
+ // Offsets are positions, so they scale; drawString scales the glyphs itself.
+ drawString(fontSize(), measured, x + textOffsetX() * scale, y + textOffsetY() * scale, textColor());
+ }
+}
diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/ArmorDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/ArmorDisplayComponent.java
index 049de81c..e63b0c32 100644
--- a/src/main/java/top/fpsmaster/ui/custom/impl/ArmorDisplayComponent.java
+++ b/src/main/java/top/fpsmaster/ui/custom/impl/ArmorDisplayComponent.java
@@ -19,29 +19,56 @@ public ArmorDisplayComponent() {
allowScale = true;
}
+ private static final int SLOTS = 4;
+ private static final float SLOT_SIZE = 16f;
+
+ @Override
+ public void measure() {
+ float spacing = mod.spacing.getValue().floatValue();
+ if (ArmorDisplay.mode.getValue() == 0) {
+ width = 70f + spacing;
+ height = 18f;
+ } else {
+ width = 70f;
+ height = 4 + spacing + SLOTS * SLOT_SIZE;
+ }
+ }
+
+ /**
+ * Top-left of slot {@code index}, already scaled. Shared by draw() and backgroundShape() so the
+ * blur mask lands on the same boxes — the default single-rectangle mask would cover the gaps
+ * between slots, which this HUD leaves empty.
+ */
+ private float slotOffset(int index) {
+ float spacing = mod.spacing.getValue().floatValue();
+ // The step was previously left unscaled, so enlarging the HUD made the slots overlap.
+ return (index * (spacing + 18) - spacing) * scale;
+ }
+
+ @Override
+ public void backgroundShape(ShapeSink sink, float originX, float originY) {
+ boolean horizontal = ArmorDisplay.mode.getValue() == 0;
+ for (int i = 0; i < SLOTS; i++) {
+ float offset = slotOffset(i);
+ sink.rect(horizontal ? originX + offset : originX,
+ horizontal ? originY : originY + offset,
+ SLOT_SIZE, SLOT_SIZE);
+ }
+ }
+
@Override
public void draw(float x, float y) {
super.draw(x, y);
List armorInventory = Arrays.asList(mc.thePlayer.inventory.armorInventory);
+ boolean horizontal = ArmorDisplay.mode.getValue() == 0;
for (int i = 0; i < armorInventory.size(); i++) {
- ItemStack itemStack = armorInventory.get(i);
- int x1 = (int) (x + i * (mod.spacing.getValue().intValue() + 18)) - mod.spacing.getValue().intValue();
- int y1 = (int) y;
-
- switch (ArmorDisplay.mode.getValue()) {
- case 0:
- itemStack = armorInventory.get(armorInventory.size() - 1 - i);
- break;
- case 1:
- case 2:
- itemStack = armorInventory.get(armorInventory.size() - 1 - i);
- x1 = (int) x;
- y1 = (int) (y + i * (mod.spacing.getValue().intValue() + 18)) - mod.spacing.getValue().intValue();
- break;
- }
+ ItemStack itemStack = armorInventory.get(armorInventory.size() - 1 - i);
+ float offset = slotOffset(i);
+ int x1 = (int) (horizontal ? x + offset : x);
+ int y1 = (int) (horizontal ? y : y + offset);
- drawRect(x1, y1, 16f, 16f, mod.backgroundColor.getColor());
+ drawRect(x1, y1, SLOT_SIZE, SLOT_SIZE, mod.backgroundColor.getColor());
if (itemStack == null) continue;
GlStateManager.disableCull();
@@ -87,17 +114,6 @@ public void draw(float x, float y) {
}
}
- switch (ArmorDisplay.mode.getValue()) {
- case 0:
- width = 70f + mod.spacing.getValue().intValue();
- height = 18f;
- break;
- case 1:
- case 2:
- width = 70f;
- height = 4 + mod.spacing.getValue().intValue() + armorInventory.size() * 16;
- break;
- }
}
}
diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/CPSDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/CPSDisplayComponent.java
index f9f45045..ab543ac1 100644
--- a/src/main/java/top/fpsmaster/ui/custom/impl/CPSDisplayComponent.java
+++ b/src/main/java/top/fpsmaster/ui/custom/impl/CPSDisplayComponent.java
@@ -1,10 +1,10 @@
package top.fpsmaster.ui.custom.impl;
-import top.fpsmaster.features.impl.interfaces.CPSDisplay;
-import top.fpsmaster.ui.custom.Component;
import net.minecraft.util.EnumChatFormatting;
+import top.fpsmaster.features.impl.interfaces.CPSDisplay;
+import top.fpsmaster.ui.custom.TextComponent;
-public class CPSDisplayComponent extends Component {
+public class CPSDisplayComponent extends TextComponent {
public CPSDisplayComponent() {
super(CPSDisplay.class);
@@ -14,21 +14,21 @@ public CPSDisplayComponent() {
}
@Override
- public void draw(float x, float y) {
- super.draw(x, y);
- String text = String.format("CPS: %d%s | %s%d",
+ protected String text() {
+ return String.format("CPS: %d%s | %s%d",
CPSDisplay.lcps,
EnumChatFormatting.GRAY,
EnumChatFormatting.RESET,
CPSDisplay.rcps);
+ }
- width = getStringWidth(16, text) + 4;
- height = 14f;
+ @Override
+ protected int fontSize() {
+ return 16;
+ }
- drawRect(x - 2, y, width, height, mod.backgroundColor.getColor());
- drawString(16, text, x, y + 2, CPSDisplay.textColor.getRGB());
+ @Override
+ protected int textColor() {
+ return CPSDisplay.textColor.getRGB();
}
}
-
-
-
diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/ClockDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/ClockDisplayComponent.java
index 468f1c4b..ba2681ff 100644
--- a/src/main/java/top/fpsmaster/ui/custom/impl/ClockDisplayComponent.java
+++ b/src/main/java/top/fpsmaster/ui/custom/impl/ClockDisplayComponent.java
@@ -1,13 +1,13 @@
package top.fpsmaster.ui.custom.impl;
-import top.fpsmaster.FPSMaster;
import top.fpsmaster.features.impl.interfaces.ClockDisplay;
-import top.fpsmaster.ui.custom.Component;
+import top.fpsmaster.ui.custom.TextComponent;
import java.text.SimpleDateFormat;
import java.util.Date;
-public class ClockDisplayComponent extends Component {
+public class ClockDisplayComponent extends TextComponent {
+
public ClockDisplayComponent() {
super(ClockDisplay.class);
x = 0.60f;
@@ -16,10 +16,8 @@ public ClockDisplayComponent() {
}
@Override
- public void draw(float x, float y) {
- super.draw(x, y);
+ protected String text() {
ClockDisplay module = getModule();
-
String pattern = module.hour24Mode.getValue() ? "HH:mm" : "hh:mm";
if (module.showSeconds.getValue()) {
pattern += ":ss";
@@ -27,30 +25,38 @@ public void draw(float x, float y) {
if (!module.hour24Mode.getValue()) {
pattern += " a";
}
+ String label = resolveLabel(module.label.getValue(), "clockdisplay.defaultlabel", "Time: ");
+ return label + new SimpleDateFormat(pattern).format(new Date());
+ }
- String timeStr = new SimpleDateFormat(pattern).format(new Date());
- String text = getLabel(module) + timeStr;
+ @Override
+ protected int fontSize() {
+ return 16;
+ }
- width = getStringWidth(16, text) + 8;
- height = 16f;
+ @Override
+ protected int textColor() {
+ return getModule().textColor.getRGB();
+ }
- drawRect(x - 2, y, width, height, mod.backgroundColor.getColor());
- drawString(16, text, x + 2, y + 3, module.textColor.getRGB());
+ @Override
+ protected float horizontalPadding() {
+ return 8f;
}
- private String getLabel(ClockDisplay module) {
- String label = module.label.getValue();
- if (label == null || label.trim().isEmpty()) {
- label = FPSMaster.i18n.get("clockdisplay.defaultlabel");
- if ("clockdisplay.defaultlabel".equals(label)) {
- label = "Time: ";
- }
- module.label.setValue(label);
- }
- if (!label.endsWith(":") && !label.endsWith(":") && !label.endsWith(" ")) {
- label += ": ";
- }
- return label;
+ @Override
+ protected float boxHeight() {
+ return 16f;
+ }
+
+ @Override
+ protected float textOffsetX() {
+ return 2f;
+ }
+
+ @Override
+ protected float textOffsetY() {
+ return 3f;
}
private ClockDisplay getModule() {
diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/ComboDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/ComboDisplayComponent.java
index 319adb4a..0e2f385d 100644
--- a/src/main/java/top/fpsmaster/ui/custom/impl/ComboDisplayComponent.java
+++ b/src/main/java/top/fpsmaster/ui/custom/impl/ComboDisplayComponent.java
@@ -1,9 +1,9 @@
package top.fpsmaster.ui.custom.impl;
import top.fpsmaster.features.impl.interfaces.ComboDisplay;
-import top.fpsmaster.ui.custom.Component;
+import top.fpsmaster.ui.custom.TextComponent;
-public class ComboDisplayComponent extends Component {
+public class ComboDisplayComponent extends TextComponent {
public ComboDisplayComponent() {
super(ComboDisplay.class);
@@ -11,21 +11,27 @@ public ComboDisplayComponent() {
}
@Override
- public void draw(float x, float y) {
- super.draw(x, y);
- String text = "Combo: " + ComboDisplay.combo;
- if (ComboDisplay.combo == 0) {
- text = "No Combo";
- }
-
- width = getStringWidth(16, text) + 4;
- height = 16;
-
- drawRect(x - 2, y, width, height, mod.backgroundColor.getColor());
- drawString(16, text, x, y + 4, ComboDisplay.textColor.getRGB());
+ protected String text() {
+ return ComboDisplay.combo == 0 ? "No Combo" : "Combo: " + ComboDisplay.combo;
+ }
+ @Override
+ protected int fontSize() {
+ return 16;
}
-}
+ @Override
+ protected int textColor() {
+ return ComboDisplay.textColor.getRGB();
+ }
+ @Override
+ protected float boxHeight() {
+ return 16f;
+ }
+ @Override
+ protected float textOffsetY() {
+ return 4f;
+ }
+}
diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/CoordsDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/CoordsDisplayComponent.java
index f36c72ab..ba8b6a42 100644
--- a/src/main/java/top/fpsmaster/ui/custom/impl/CoordsDisplayComponent.java
+++ b/src/main/java/top/fpsmaster/ui/custom/impl/CoordsDisplayComponent.java
@@ -3,12 +3,12 @@
import org.jetbrains.annotations.NotNull;
import top.fpsmaster.features.impl.interfaces.CoordsDisplay;
import top.fpsmaster.features.impl.interfaces.FPSDisplay;
-import top.fpsmaster.ui.custom.Component;
+import top.fpsmaster.ui.custom.TextComponent;
import net.minecraft.util.EnumChatFormatting;
import static top.fpsmaster.utils.core.Utility.mc;
-public class CoordsDisplayComponent extends Component {
+public class CoordsDisplayComponent extends TextComponent {
public CoordsDisplayComponent() {
super(CoordsDisplay.class);
@@ -16,28 +16,31 @@ public CoordsDisplayComponent() {
}
@Override
- public void draw(float x, float y) {
- super.draw(x, y);
- String s = String.format("X:%d Y:%d Z:%d",
- (int) mc.thePlayer.posX,
- (int) mc.thePlayer.posY,
- (int) mc.thePlayer.posZ);
-
+ protected String text() {
+ if (mc.thePlayer == null) {
+ return null;
+ }
if (((CoordsDisplay) mod).limitDisplay.getValue()) {
- String yStr = getString();
-
- s = String.format("X:%d Y:%d(%s) Z:%d",
- (int) mc.thePlayer.posX,
- (int) mc.thePlayer.posY,
- yStr,
+ return String.format("X:%d Y:%d(%s) Z:%d",
+ (int) mc.thePlayer.posX,
+ (int) mc.thePlayer.posY,
+ getString(),
(int) mc.thePlayer.posZ);
}
+ return String.format("X:%d Y:%d Z:%d",
+ (int) mc.thePlayer.posX,
+ (int) mc.thePlayer.posY,
+ (int) mc.thePlayer.posZ);
+ }
- width = getStringWidth(18, s) + 4;
- height = 14f;
+ @Override
+ protected int fontSize() {
+ return 18;
+ }
- drawRect(x - 2, y, width, height, mod.backgroundColor.getColor());
- drawString(18, s, x, y + 2, FPSDisplay.textColor.getRGB());
+ @Override
+ protected int textColor() {
+ return FPSDisplay.textColor.getRGB();
}
private @NotNull String getString() {
@@ -55,6 +58,3 @@ public void draw(float x, float y) {
return yStr;
}
}
-
-
-
diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/FPSDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/FPSDisplayComponent.java
index b1140d98..8a25b14e 100644
--- a/src/main/java/top/fpsmaster/ui/custom/impl/FPSDisplayComponent.java
+++ b/src/main/java/top/fpsmaster/ui/custom/impl/FPSDisplayComponent.java
@@ -2,9 +2,9 @@
import net.minecraft.client.Minecraft;
import top.fpsmaster.features.impl.interfaces.FPSDisplay;
-import top.fpsmaster.ui.custom.Component;
+import top.fpsmaster.ui.custom.TextComponent;
-public class FPSDisplayComponent extends Component {
+public class FPSDisplayComponent extends TextComponent {
public FPSDisplayComponent() {
super(FPSDisplay.class);
@@ -14,17 +14,17 @@ public FPSDisplayComponent() {
}
@Override
- public void draw(float x, float y) {
- super.draw(x, y);
- String s = Minecraft.getDebugFPS() + "fps";
-
- width = getStringWidth(18, s) + 4;
- height = 14f;
-
- drawRect(x - 2, y, width, height, mod.backgroundColor.getColor());
- drawString(18, s, x, y + 2, FPSDisplay.textColor.getRGB());
+ protected String text() {
+ return Minecraft.getDebugFPS() + "fps";
}
-}
-
+ @Override
+ protected int fontSize() {
+ return 18;
+ }
+ @Override
+ protected int textColor() {
+ return FPSDisplay.textColor.getRGB();
+ }
+}
diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/KeystrokesComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/KeystrokesComponent.java
index 40cc1e1a..a080a35c 100644
--- a/src/main/java/top/fpsmaster/ui/custom/impl/KeystrokesComponent.java
+++ b/src/main/java/top/fpsmaster/ui/custom/impl/KeystrokesComponent.java
@@ -37,40 +37,62 @@ public KeystrokesComponent() {
}
@Override
- public void draw(float x, float y) {
- super.draw(x, y);
- double dt = animClock.tick();
+ public void measure() {
+ // Key offsets are laid out here rather than in draw() so backgroundShape — which the blur mask
+ // consults before drawing — sees this frame's positions.
+ int spacing = mod.spacing.getValue().intValue();
for (Key key : keys) {
switch (key.keyCode) {
case Keyboard.KEY_W:
- key.yOffset = key.defaultYOffset - mod.spacing.getValue().intValue();
+ key.yOffset = key.defaultYOffset - spacing;
break;
case Keyboard.KEY_A:
- key.xOffset = key.defaultXOffset - mod.spacing.getValue().intValue();
+ key.xOffset = key.defaultXOffset - spacing;
break;
case Keyboard.KEY_D:
- key.xOffset = key.defaultXOffset + mod.spacing.getValue().intValue();
+ key.xOffset = key.defaultXOffset + spacing;
break;
case -1:
- key.xOffset = key.defaultXOffset - mod.spacing.getValue().intValue();
- key.yOffset = key.defaultYOffset + mod.spacing.getValue().intValue();
+ key.xOffset = key.defaultXOffset - spacing;
+ key.yOffset = key.defaultYOffset + spacing;
break;
case -2:
- key.xOffset = key.defaultXOffset + mod.spacing.getValue().intValue();
- key.yOffset = key.defaultYOffset + mod.spacing.getValue().intValue();
+ key.xOffset = key.defaultXOffset + spacing;
+ key.yOffset = key.defaultYOffset + spacing;
break;
case Keyboard.KEY_SPACE:
- key.xOffset = key.defaultXOffset - mod.spacing.getValue().intValue();
- key.yOffset = key.defaultYOffset + mod.spacing.getValue().intValue() * 2;
+ key.xOffset = key.defaultXOffset - spacing;
+ key.yOffset = key.defaultYOffset + spacing * 2;
break;
}
- key.render(x, y, (float) dt, mod.backgroundColor.getColor(), Keystrokes.pressedColor.getColor());
}
- float spacing = mod.spacing.getValue().floatValue();
width = 60f + spacing * 2f;
height = (Keystrokes.showSpace.getValue() ? 72f : 60f) + spacing * 2f;
}
+ /**
+ * Keystrokes paints one panel per key, not a single box, so the default single-rectangle mask
+ * would blur the gaps between keys as well.
+ */
+ @Override
+ public void backgroundShape(ShapeSink sink, float originX, float originY) {
+ for (Key key : keys) {
+ if (key.isHidden()) {
+ continue;
+ }
+ sink.rect(originX + key.xOffset * scale, originY + key.yOffset * scale, key.renderWidth(), key.baseHeight);
+ }
+ }
+
+ @Override
+ public void draw(float x, float y) {
+ super.draw(x, y);
+ double dt = animClock.tick();
+ for (Key key : keys) {
+ key.render(x, y, (float) dt, mod.backgroundColor.getColor(), Keystrokes.pressedColor.getColor());
+ }
+ }
+
public class Key {
private final String name;
private final int keyCode;
@@ -96,14 +118,26 @@ public Key(String name, int keyCode, int xOffset, int yOffset, float width, floa
this.yOffset = defaultYOffset;
}
+ /** The space bar is the one key that can be switched off, so it must be skipped by the mask too. */
+ private boolean isHidden() {
+ return keyCode == Keyboard.KEY_SPACE && !Keystrokes.showSpace.getValue();
+ }
+
+ /** Single source of truth for key width, shared by render() and backgroundShape(). */
+ private float renderWidth() {
+ return keyCode == Keyboard.KEY_SPACE
+ ? baseWidth + mod.spacing.getValue().floatValue() * 2f
+ : baseWidth;
+ }
+
public void render(float x, float y, float speed, Color color, Color color1) {
- if (keyCode == Keyboard.KEY_SPACE && !Keystrokes.showSpace.getValue()) {
+ if (isHidden()) {
pressAnims.clear();
lastPressed = false;
return;
}
boolean pressed;
- float keyW = baseWidth;
+ float keyW = renderWidth();
float keyH = baseHeight;
float keyX;
float keyY;
@@ -122,11 +156,6 @@ public void render(float x, float y, float speed, Color color, Color color1) {
keyX = x + xOffset * scale;
keyY = y + yOffset * scale;
}
- if (keyCode == Keyboard.KEY_SPACE) {
- keyW = baseWidth + spacing * 2f;
- keyX = x + xOffset * scale;
- }
-
this.color.base(pressed ? color1 : color);
if (pressed && !lastPressed) {
diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/PingDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/PingDisplayComponent.java
index ff77c311..3088c806 100644
--- a/src/main/java/top/fpsmaster/ui/custom/impl/PingDisplayComponent.java
+++ b/src/main/java/top/fpsmaster/ui/custom/impl/PingDisplayComponent.java
@@ -1,11 +1,11 @@
package top.fpsmaster.ui.custom.impl;
-import top.fpsmaster.features.impl.interfaces.PingDisplay;
import net.minecraft.client.Minecraft;
import net.minecraft.client.network.NetworkPlayerInfo;
-import top.fpsmaster.ui.custom.Component;
+import top.fpsmaster.features.impl.interfaces.PingDisplay;
+import top.fpsmaster.ui.custom.TextComponent;
-public class PingDisplayComponent extends Component {
+public class PingDisplayComponent extends TextComponent {
public PingDisplayComponent() {
super(PingDisplay.class);
@@ -13,26 +13,22 @@ public PingDisplayComponent() {
}
@Override
- public void draw(float x, float y) {
- super.draw(x, y);
-
- // Get ping of connection
+ protected String text() {
Minecraft mc = Minecraft.getMinecraft();
if (mc.thePlayer == null || mc.getNetHandler() == null) {
- return;
+ return null;
}
-
NetworkPlayerInfo info = mc.getNetHandler().getPlayerInfo(mc.thePlayer.getUniqueID());
- String ping = (info != null ? info.getResponseTime() : 0) + "ms";
- String text = "Ping: " + ping;
+ return "Ping: " + (info != null ? info.getResponseTime() : 0) + "ms";
+ }
- width = getStringWidth(16, text) + 4;
- height = 14f;
+ @Override
+ protected int fontSize() {
+ return 16;
+ }
- drawRect(x - 2, y, width, height, mod.backgroundColor.getColor());
- drawString(16, text, x, y + 2, PingDisplay.textColor.getRGB());
+ @Override
+ protected int textColor() {
+ return PingDisplay.textColor.getRGB();
}
}
-
-
-
diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/PlayTimeComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/PlayTimeComponent.java
index 7ce339df..c0363abc 100644
--- a/src/main/java/top/fpsmaster/ui/custom/impl/PlayTimeComponent.java
+++ b/src/main/java/top/fpsmaster/ui/custom/impl/PlayTimeComponent.java
@@ -4,11 +4,11 @@
import top.fpsmaster.FPSMaster;
import top.fpsmaster.features.impl.interfaces.PlayTime;
import top.fpsmaster.modules.statistics.PlayTimeStatistics;
-import top.fpsmaster.ui.custom.Component;
+import top.fpsmaster.ui.custom.TextComponent;
import java.util.Locale;
-public class PlayTimeComponent extends Component {
+public class PlayTimeComponent extends TextComponent {
public PlayTimeComponent() {
super(PlayTime.class);
x = 0.05f;
@@ -17,33 +17,43 @@ public PlayTimeComponent() {
}
@Override
- public void draw(float x, float y) {
- super.draw(x, y);
+ protected String text() {
PlayTime module = getModule();
- String text = getLabel(module) + formatTime(PlayTimeStatistics.getDisplayMillis(Minecraft.getMinecraft(), module.displayMode.getMode()));
+ String label = resolveLabel(module.label.getValue(), "playtime.defaultlabel", "Played: ");
+ return label + formatTime(PlayTimeStatistics.getDisplayMillis(Minecraft.getMinecraft(), module.displayMode.getMode()));
+ }
- width = getStringWidth(16, text) + 8;
- height = 16f;
+ @Override
+ protected int fontSize() {
+ return 16;
+ }
- drawRect(x - 2, y, width, height, mod.backgroundColor.getColor());
- drawString(16, text, x + 2, y + 3, module.textColor.getRGB());
+ @Override
+ protected int textColor() {
+ return getModule().textColor.getRGB();
}
- private String getLabel(PlayTime module) {
- String label = module.label.getValue();
- if (label == null || label.trim().isEmpty()) {
- label = FPSMaster.i18n.get("playtime.defaultlabel");
- if ("playtime.defaultlabel".equals(label)) {
- label = "游玩时间:";
- }
- module.label.setValue(label);
- }
- if (!label.endsWith(":") && !label.endsWith(":") && !label.endsWith(" ")) {
- label += ":";
- }
- return label;
+ @Override
+ protected float horizontalPadding() {
+ return 8f;
}
+ @Override
+ protected float boxHeight() {
+ return 16f;
+ }
+
+ @Override
+ protected float textOffsetX() {
+ return 2f;
+ }
+
+ @Override
+ protected float textOffsetY() {
+ return 3f;
+ }
+
+
private String formatTime(long millis) {
long totalSeconds = Math.max(0L, millis / 1000L);
if (totalSeconds < 60L) {
diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/ReachDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/ReachDisplayComponent.java
index 8dc279f7..8de47468 100644
--- a/src/main/java/top/fpsmaster/ui/custom/impl/ReachDisplayComponent.java
+++ b/src/main/java/top/fpsmaster/ui/custom/impl/ReachDisplayComponent.java
@@ -1,9 +1,9 @@
package top.fpsmaster.ui.custom.impl;
import top.fpsmaster.features.impl.interfaces.ReachDisplay;
-import top.fpsmaster.ui.custom.Component;
+import top.fpsmaster.ui.custom.TextComponent;
-public class ReachDisplayComponent extends Component {
+public class ReachDisplayComponent extends TextComponent {
public ReachDisplayComponent() {
super(ReachDisplay.class);
@@ -11,15 +11,17 @@ public ReachDisplayComponent() {
}
@Override
- public void draw(float x, float y) {
- super.draw(x, y);
- String s = ReachDisplay.reach + " b";
- width = getStringWidth(18, s) + 4;
- height = 14f;
- drawRect(x - 2, y, width, height, mod.backgroundColor.getColor());
- drawString(18, s, x, y + 2, ReachDisplay.textColor.getRGB());
+ protected String text() {
+ return ReachDisplay.reach + " b";
}
-}
-
+ @Override
+ protected int fontSize() {
+ return 18;
+ }
+ @Override
+ protected int textColor() {
+ return ReachDisplay.textColor.getRGB();
+ }
+}
diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/ServerAddressDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/ServerAddressDisplayComponent.java
index 93e28cdb..823ca0fe 100644
--- a/src/main/java/top/fpsmaster/ui/custom/impl/ServerAddressDisplayComponent.java
+++ b/src/main/java/top/fpsmaster/ui/custom/impl/ServerAddressDisplayComponent.java
@@ -4,9 +4,9 @@
import net.minecraft.client.multiplayer.ServerData;
import top.fpsmaster.FPSMaster;
import top.fpsmaster.features.impl.interfaces.ServerAddressDisplay;
-import top.fpsmaster.ui.custom.Component;
+import top.fpsmaster.ui.custom.TextComponent;
-public class ServerAddressDisplayComponent extends Component {
+public class ServerAddressDisplayComponent extends TextComponent {
public ServerAddressDisplayComponent() {
super(ServerAddressDisplay.class);
x = 0.02f;
@@ -15,21 +15,43 @@ public ServerAddressDisplayComponent() {
}
@Override
- public void draw(float x, float y) {
- super.draw(x, y);
- ServerAddressDisplay module = getModule();
-
+ protected String text() {
String address = getServerAddress();
if (address == null) {
- return;
+ return null;
}
+ ServerAddressDisplay module = getModule();
+ return resolveLabel(module.label.getValue(), "serveraddressdisplay.defaultlabel", "Server: ") + address;
+ }
+
+ @Override
+ protected int fontSize() {
+ return 16;
+ }
+
+ @Override
+ protected int textColor() {
+ return getModule().textColor.getRGB();
+ }
+
+ @Override
+ protected float horizontalPadding() {
+ return 8f;
+ }
- String text = getLabel(module) + address;
- width = getStringWidth(16, text) + 8;
- height = 16f;
+ @Override
+ protected float boxHeight() {
+ return 16f;
+ }
- drawRect(x - 2, y, width, height, mod.backgroundColor.getColor());
- drawString(16, text, x + 2, y + 3, module.textColor.getRGB());
+ @Override
+ protected float textOffsetX() {
+ return 2f;
+ }
+
+ @Override
+ protected float textOffsetY() {
+ return 3f;
}
@Override
@@ -53,20 +75,6 @@ private String getServerAddress() {
return null;
}
- private String getLabel(ServerAddressDisplay module) {
- String label = module.label.getValue();
- if (label == null || label.trim().isEmpty()) {
- label = FPSMaster.i18n.get("serveraddressdisplay.defaultlabel");
- if ("serveraddressdisplay.defaultlabel".equals(label)) {
- label = "Server: ";
- }
- module.label.setValue(label);
- }
- if (!label.endsWith(":") && !label.endsWith(":") && !label.endsWith(" ")) {
- label += ": ";
- }
- return label;
- }
private ServerAddressDisplay getModule() {
return (ServerAddressDisplay) mod;