From 2f5c54e67508464758c6cd2557907701863ffec5 Mon Sep 17 00:00:00 2001 From: froslanz Date: Sat, 24 Oct 2026 13:13:02 -0400 Subject: [PATCH] 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 --- .../java/cc/fros/features/gui/FrosGui.java | 22 +--- .../cc/fros/features/gui/HudEditorScreen.java | 21 +--- .../modules/client/ClickGuiModule.java | 8 +- .../features/modules/client/HudModule.java | 14 ++- .../java/cc/fros/util/render/FrosFont.java | 114 +++++++++++++----- 5 files changed, 106 insertions(+), 73 deletions(-) diff --git a/src/main/java/cc/fros/features/gui/FrosGui.java b/src/main/java/cc/fros/features/gui/FrosGui.java index 7b328cc..7a1c2ff 100644 --- a/src/main/java/cc/fros/features/gui/FrosGui.java +++ b/src/main/java/cc/fros/features/gui/FrosGui.java @@ -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); } diff --git a/src/main/java/cc/fros/features/gui/HudEditorScreen.java b/src/main/java/cc/fros/features/gui/HudEditorScreen.java index 6ca65bd..80aee19 100644 --- a/src/main/java/cc/fros/features/gui/HudEditorScreen.java +++ b/src/main/java/cc/fros/features/gui/HudEditorScreen.java @@ -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); } diff --git a/src/main/java/cc/fros/features/modules/client/ClickGuiModule.java b/src/main/java/cc/fros/features/modules/client/ClickGuiModule.java index 0617043..caff975 100644 --- a/src/main/java/cc/fros/features/modules/client/ClickGuiModule.java +++ b/src/main/java/cc/fros/features/modules/client/ClickGuiModule.java @@ -18,7 +18,7 @@ public class ClickGuiModule extends Module { public final Setting font = str("Font", "SansSerif"); public final Setting fontSize = num("FontSize", 10, 6, 24); - public final Setting guiScale = num("GUIScale", 1.0f, 0.5f, 2.0f); + public final Setting hudScale = num("HUDScale", 1.0f, 0.5f, 2.0f); public final Setting prefix = str("Prefix", "."); public final Setting color = color("Color", 0, 0, 255, 180); public final Setting 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()); } } \ No newline at end of file diff --git a/src/main/java/cc/fros/features/modules/client/HudModule.java b/src/main/java/cc/fros/features/modules/client/HudModule.java index 721a28c..05bb41f 100644 --- a/src/main/java/cc/fros/features/modules/client/HudModule.java +++ b/src/main/java/cc/fros/features/modules/client/HudModule.java @@ -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; diff --git a/src/main/java/cc/fros/util/render/FrosFont.java b/src/main/java/cc/fros/util/render/FrosFont.java index 5c52ee4..fa34f7d 100644 --- a/src/main/java/cc/fros/util/render/FrosFont.java +++ b/src/main/java/cc/fros/util/render/FrosFont.java @@ -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 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 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);