GUIScale only scales HUD modules; renamed to HUDScale

- ClickGui and the hud editor widget panel are unscaled again (raw mouse,
  scroll step 10)
- FrosFont rasterizes per ui-scale state: hud rendering sets a context
  scale so its glyphs stay 1:1 device pixels, clickgui text keeps its
  own raster; the glyph cache is keyed by scale so switching between
  the two contexts never invalidates either
- scale changes no longer clear the glyph cache, only font/size changes do
This commit is contained in:
froslanz
2026-10-24 13:13:02 -04:00
parent 6593a3f089
commit 2f5c54e675
5 changed files with 106 additions and 73 deletions
@@ -4,7 +4,6 @@ import cc.fros.Fros;
import cc.fros.features.Feature;
import cc.fros.features.gui.items.buttons.ModuleButton;
import cc.fros.features.modules.Module;
import cc.fros.features.modules.client.ClickGuiModule;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.Screen;
@@ -86,38 +85,27 @@ public class FrosGui extends Screen {
context.fill(0, 0, context.guiWidth(), context.guiHeight(), new Color(0, 0, 0, 120).hashCode());
// scale the whole clickgui; widgets work in layout units so mouse
// coordinates are divided back down before they reach them
float scale = ClickGuiModule.scale();
int layoutX = (int) (mouseX / scale);
int layoutY = (int) (mouseY / scale);
context.pose().pushMatrix();
context.pose().scale(scale, scale);
this.widgets.forEach(components -> components.drawScreen(context, layoutX, layoutY, delta));
context.pose().popMatrix();
this.widgets.forEach(components -> components.drawScreen(context, mouseX, mouseY, delta));
}
@Override
public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {
float scale = ClickGuiModule.scale();
this.widgets.forEach(components -> components.mouseClicked((int) (click.x() / scale), (int) (click.y() / scale), click.button()));
this.widgets.forEach(components -> components.mouseClicked((int) click.x(), (int) click.y(), click.button()));
return super.mouseClicked(click, doubled);
}
@Override
public boolean mouseReleased(MouseButtonEvent click) {
float scale = ClickGuiModule.scale();
this.widgets.forEach(components -> components.mouseReleased((int) (click.x() / scale), (int) (click.y() / scale), click.button()));
this.widgets.forEach(components -> components.mouseReleased((int) click.x(), (int) click.y(), click.button()));
return super.mouseReleased(click);
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
int step = Math.max(1, Math.round((float) (10 / ClickGuiModule.scale())));
if (verticalAmount < 0) {
this.widgets.forEach(component -> component.setY(component.getY() - step));
this.widgets.forEach(component -> component.setY(component.getY() - 10));
} else if (verticalAmount > 0) {
this.widgets.forEach(component -> component.setY(component.getY() + step));
this.widgets.forEach(component -> component.setY(component.getY() + 10));
}
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount);
}
@@ -4,7 +4,6 @@ import cc.fros.Fros;
import cc.fros.features.Feature;
import cc.fros.features.gui.items.buttons.ModuleButton;
import cc.fros.features.modules.Module;
import cc.fros.features.modules.client.ClickGuiModule;
import cc.fros.features.modules.client.HudModule;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.Screen;
@@ -41,37 +40,27 @@ public class HudEditorScreen extends Screen {
@Override
public void render(GuiGraphics context, int mouseX, int mouseY, float delta) {
anyHover = false;
// same ui scale treatment as the clickgui
float scale = ClickGuiModule.scale();
int layoutX = (int) (mouseX / scale);
int layoutY = (int) (mouseY / scale);
context.pose().pushMatrix();
context.pose().scale(scale, scale);
this.components.forEach(component -> component.drawScreen(context, layoutX, layoutY, delta));
context.pose().popMatrix();
this.components.forEach(component -> component.drawScreen(context, mouseX, mouseY, delta));
}
@Override
public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {
float scale = ClickGuiModule.scale();
this.components.forEach(component -> component.mouseClicked((int) (click.x() / scale), (int) (click.y() / scale), click.button()));
this.components.forEach(component -> component.mouseClicked((int) click.x(), (int) click.y(), click.button()));
return super.mouseClicked(click, doubled);
}
@Override
public boolean mouseReleased(MouseButtonEvent click) {
float scale = ClickGuiModule.scale();
this.components.forEach(component -> component.mouseReleased((int) (click.x() / scale), (int) (click.y() / scale), click.button()));
this.components.forEach(component -> component.mouseReleased((int) click.x(), (int) click.y(), click.button()));
return super.mouseReleased(click);
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
int step = Math.max(1, Math.round((float) (10 / ClickGuiModule.scale())));
if (verticalAmount < 0) {
this.components.forEach(component -> component.setY(component.getY() - step));
this.components.forEach(component -> component.setY(component.getY() - 10));
} else if (verticalAmount > 0) {
this.components.forEach(component -> component.setY(component.getY() + step));
this.components.forEach(component -> component.setY(component.getY() + 10));
}
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount);
}
@@ -18,7 +18,7 @@ public class ClickGuiModule extends Module {
public final Setting<String> font = str("Font", "SansSerif");
public final Setting<Integer> fontSize = num("FontSize", 10, 6, 24);
public final Setting<Float> guiScale = num("GUIScale", 1.0f, 0.5f, 2.0f);
public final Setting<Float> hudScale = num("HUDScale", 1.0f, 0.5f, 2.0f);
public final Setting<String> prefix = str("Prefix", ".");
public final Setting<Color> color = color("Color", 0, 0, 255, 180);
public final Setting<Color> topColor = color("TopColor", 0, 0, 150, 240);
@@ -75,8 +75,8 @@ public class ClickGuiModule extends Module {
return INSTANCE;
}
/** ui scale factor applied to the client's own hud and clickgui */
public static float scale() {
return INSTANCE == null ? 1f : Math.max(0.5f, INSTANCE.guiScale.getValue());
/** scale factor applied to the client's own hud modules */
public static float hudScale() {
return INSTANCE == null ? 1f : Math.max(0.5f, INSTANCE.hudScale.getValue());
}
}
@@ -37,7 +37,7 @@ public abstract class HudModule extends Module {
}
public float getY() {
float scaledHeight = getHeight() * ClickGuiModule.scale();
float scaledHeight = getHeight() * ClickGuiModule.hudScale();
float heightWithChat = mc.getWindow().getGuiScaledHeight() - 14;
float baseY = mc.getWindow().getGuiScaledHeight() * pos.getValue().y();
float combined = baseY + scaledHeight;
@@ -50,7 +50,9 @@ public abstract class HudModule extends Module {
@Subscribe
public void onRender2DHud(Render2DEvent e) {
float scale = ClickGuiModule.scale();
float scale = ClickGuiModule.hudScale();
// glyphs rasterize for the hud's transform so they stay crisp
FrosFont.setContextScale(scale);
if (scale == 1f) {
render(e);
return;
@@ -66,6 +68,8 @@ public abstract class HudModule extends Module {
render(e);
} finally {
e.getContext().pose().popMatrix();
// everything else in the frame draws unscaled
FrosFont.setContextScale(1f);
}
}
@@ -99,7 +103,7 @@ public abstract class HudModule extends Module {
}
if (dragging) {
float scale = ClickGuiModule.scale();
float scale = ClickGuiModule.hudScale();
float maxX = mc.getWindow().getGuiScaledWidth() - width * scale;
float maxY = mc.getWindow().getGuiScaledHeight() - height * scale;
float rawX = Math.min(Math.max(getMouseX() - dragX, 0), maxX);
@@ -123,7 +127,7 @@ public abstract class HudModule extends Module {
if (shouldDrawDescription) {
int textWidth = FrosFont.width(getName());
int textHeight = FrosFont.lineHeight();
float scale = ClickGuiModule.scale();
float scale = ClickGuiModule.hudScale();
float textX = x + width + 5;
// the label is drawn inside the element's scale transform
if (x + (textX + textWidth - x) * scale > mc.getWindow().getGuiScaledWidth()) {
@@ -159,7 +163,7 @@ public abstract class HudModule extends Module {
float y = getY();
int mouseX = getMouseX();
int mouseY = getMouseY();
float scale = ClickGuiModule.scale();
float scale = ClickGuiModule.hudScale();
return mouseX >= x - 1 && mouseX <= x + width * scale + 1 &&
mouseY >= y - 1 && mouseY <= y + height * scale + 1;
+83 -31
View File
@@ -18,6 +18,7 @@ import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
@@ -67,12 +68,24 @@ public final class FrosFont {
private static final List<Identifier> PENDING_RELEASE = new ArrayList<>();
private static long lastQueuedAt;
private record Key(int color, String text, boolean shadow) {
private record Key(int color, String text, boolean shadow, float scale) {
}
private record Glyph(Identifier id, int width, int height) {
}
/** a raster font for one ui scale; the clickgui and the scaled hud each get their own */
private record FontState(java.awt.Font font, FontMetrics metrics, float visualCenterRaster) {
}
// the family picked in the settings, independent of any scale
private static java.awt.Font baseFont;
private static final Map<Float, FontState> STATES = new HashMap<>();
// outer transform applied by the hud's render pose; glyphs rasterize for
// it so they stay 1:1 device pixels at any hud scale
private static float contextScale = 1f;
// current state pointers, re-pointed by sync() for the active context
private static java.awt.Font awtFont;
private static FontMetrics metrics;
private static String loadedName;
@@ -82,6 +95,16 @@ public final class FrosFont {
private static float visualCenterRaster = 4f;
private static int glyphCounter;
/**
* Sets the ui transform currently applied to drawing outside this class.
* hud modules scale their pose by HUDScale, so glyphs rasterize larger for
* them; the unscaled clickgui keeps its own, so switching between the two
* never invalidates the other's glyph cache.
*/
public static void setContextScale(float scale) {
contextScale = Math.max(0.5f, scale);
}
private FrosFont() {
}
@@ -111,9 +134,9 @@ public final class FrosFont {
private static float currentScale() {
try {
// one raster pixel per screen pixel, following both mc's gui scale
// and the client's own ui scale slider
return Math.max(0.5f, (float) (mc.getWindow().getGuiScale() * ClickGuiModule.scale()));
// one raster pixel per screen pixel, following mc's gui scale and
// the hud's own scale (contextScale)
return Math.max(0.5f, (float) (mc.getWindow().getGuiScale() * contextScale));
} catch (Exception e) {
return 1f;
}
@@ -151,56 +174,85 @@ public final class FrosFont {
String name = gui == null ? "" : gui.font.getValue();
int size = gui == null ? 10 : gui.fontSize.getValue();
if (name == null) name = "";
float scale = currentScale();
if (!Objects.equals(name, loadedName) || size != loadedSize || scale != loadedScale) {
reset(name, size, scale);
// the settings determine the family and its unit size; a scale change
// only adds another raster state and must not clear the glyph cache
if (!Objects.equals(name, loadedName) || size != loadedSize) {
reset(name, size);
}
if (baseFont == null) {
awtFont = null;
metrics = null;
return;
}
float scale = currentScale();
FontState state = STATES.get(scale);
if (state == null) {
state = createState(scale);
if (state == null) {
baseFont = null;
awtFont = null;
metrics = null;
return;
}
STATES.put(scale, state);
}
awtFont = state.font();
metrics = state.metrics();
visualCenterRaster = state.visualCenterRaster();
loadedScale = scale;
}
private static void reset(String name, float size, float scale) {
private static void reset(String name, float size) {
loadedName = name;
loadedSize = size;
loadedScale = scale;
baseFont = null;
awtFont = null;
metrics = null;
STATES.clear();
clearGlyphs();
if (name.isBlank()) return;
try {
java.awt.Font installed = installedFont(name);
if (installed != null) {
applyFont(installed, size * scale, "Using system font '" + name + "'", true);
baseFont = installed;
LOGGER.info("Using system font '{}' ({}pt)", name, size);
return;
}
LOGGER.warn("Font '{}' not found", name);
} catch (Exception e) {
LOGGER.error("Failed to load font '{}'", name, e);
awtFont = null;
metrics = null;
}
}
private static void applyFont(java.awt.Font font, float size, String source, boolean verbose) {
awtFont = font.deriveFont(java.awt.Font.PLAIN, size);
BufferedImage probe = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = probe.createGraphics();
graphics.setFont(awtFont);
metrics = graphics.getFontMetrics();
// distance from the top of the line box to the middle of the cap band:
// drawString puts the baseline at metrics.getAscent(), and "A" spans
// from the baseline up to the cap line
java.awt.geom.Rectangle2D capBounds = awtFont
.createGlyphVector(graphics.getFontRenderContext(), "A")
.getVisualBounds();
float capHeight = (float) capBounds.getHeight();
if (capHeight <= 0f || capHeight > metrics.getAscent()) {
capHeight = metrics.getAscent();
private static FontState createState(float scale) {
try {
java.awt.Font font = baseFont.deriveFont(java.awt.Font.PLAIN, loadedSize * scale);
BufferedImage probe = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = probe.createGraphics();
graphics.setFont(font);
FontMetrics fontMetrics = graphics.getFontMetrics();
// distance from the top of the line box to the middle of the cap band:
// drawString puts the baseline at metrics.getAscent(), and "A" spans
// from the baseline up to the cap line
java.awt.geom.Rectangle2D capBounds = font
.createGlyphVector(graphics.getFontRenderContext(), "A")
.getVisualBounds();
float capHeight = (float) capBounds.getHeight();
if (capHeight <= 0f || capHeight > fontMetrics.getAscent()) {
capHeight = fontMetrics.getAscent();
}
float visualCenter = fontMetrics.getAscent() - capHeight / 2f;
graphics.dispose();
return new FontState(font, fontMetrics, visualCenter);
} catch (Exception e) {
LOGGER.error("Failed to create font at scale {}", scale, e);
return null;
}
visualCenterRaster = metrics.getAscent() - capHeight / 2f;
graphics.dispose();
if (verbose) LOGGER.info("{} ({}pt)", source, size);
}
/**
@@ -259,7 +311,7 @@ public final class FrosFont {
private static void drawGlyphs(GuiGraphics context, String text, int x, int y, int color, boolean shadow) {
flushReleases();
Key key = new Key(color, text, shadow);
Key key = new Key(color, text, shadow, loadedScale);
Glyph glyph = GLYPHS.get(key);
if (glyph == null) {
glyph = createGlyph(text, color, shadow);