add custom font support for all client ui
- new FrosFont renderer: loads ttf/otf from fros/fonts/, rasterizes text to cached textures, falls back to the vanilla font when unset - Font/FontSize settings in ClickGui (fontSize only shows when a font is set); missing files retry automatically every 2s - all gui buttons, panels and hud modules now render through it - handles section codes and styled components (watermark accent color)
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
package cc.fros.features.gui;
|
||||
|
||||
import cc.fros.util.render.FrosFont;
|
||||
|
||||
import cc.fros.features.Feature;
|
||||
import cc.fros.features.gui.items.Item;
|
||||
import cc.fros.features.gui.items.buttons.Button;
|
||||
@@ -193,6 +195,6 @@ public class Widget
|
||||
}
|
||||
|
||||
protected void drawString(String text, double x, double y, int color) {
|
||||
context.drawString(mc.font, text, (int) x, (int) y, color);
|
||||
FrosFont.draw(context, text, (int) x, (int) y, color);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package cc.fros.features.gui.items;
|
||||
|
||||
import cc.fros.util.render.FrosFont;
|
||||
|
||||
import cc.fros.features.Feature;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
|
||||
@@ -78,7 +80,7 @@ public class Item
|
||||
}
|
||||
|
||||
protected void drawString(GuiGraphics context, String text, double x, double y, int color) {
|
||||
context.drawString(mc.font, text, (int) x, (int) y, color);
|
||||
FrosFont.draw(context, text, (int) x, (int) y, color);
|
||||
}
|
||||
|
||||
public boolean isHovering(int mouseX, int mouseY) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package cc.fros.features.gui.items.buttons;
|
||||
|
||||
import cc.fros.util.render.FrosFont;
|
||||
|
||||
import cc.fros.Fros;
|
||||
import cc.fros.features.gui.FrosGui;
|
||||
import cc.fros.features.gui.Widget;
|
||||
@@ -119,12 +121,12 @@ public class ColorButton extends Button {
|
||||
int buttonWidth = availableWidth / 2;
|
||||
RenderUtil.rect(context, pickerX, this.y + yOffset, pickerX + buttonWidth, this.y + yOffset + 14,
|
||||
hoveringCopy ? Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().topColor.getValue().getAlpha()) : 0x11555555);
|
||||
drawString(context, "Copy", pickerX + buttonWidth / 2.0 - mc.font.width("Copy") / 2.0, this.y + yOffset + 3, -1);
|
||||
drawString(context, "Copy", pickerX + buttonWidth / 2.0 - FrosFont.width("Copy") / 2.0, this.y + yOffset + 3, -1);
|
||||
hoveringCopy = isHoveringArea(mouseX, mouseY, pickerX, this.y + yOffset, pickerX + buttonWidth, this.y + yOffset + 14);
|
||||
|
||||
RenderUtil.rect(context, pickerX + buttonWidth + 1, this.y + yOffset, pickerX + buttonWidth * 2 + 1, this.y + yOffset + 14,
|
||||
hoveringPaste ? Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().topColor.getValue().getAlpha()) : 0x11555555);
|
||||
drawString(context, "Paste", pickerX + buttonWidth + buttonWidth / 2.0 - mc.font.width("Paste") / 2.0 + 1, this.y + yOffset + 3, -1);
|
||||
drawString(context, "Paste", pickerX + buttonWidth + buttonWidth / 2.0 - FrosFont.width("Paste") / 2.0 + 1, this.y + yOffset + 3, -1);
|
||||
hoveringPaste = isHoveringArea(mouseX, mouseY, pickerX + buttonWidth + 1, this.y + yOffset, pickerX + buttonWidth * 2 + 1, this.y + yOffset + 14);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import static cc.fros.features.commands.MessageSignatures.GENERAL;
|
||||
public class ClickGuiModule extends Module {
|
||||
private static ClickGuiModule INSTANCE;
|
||||
|
||||
public final Setting<String> font = str("Font", "");
|
||||
public final Setting<Integer> fontSize = num("FontSize", 10, 6, 24);
|
||||
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);
|
||||
@@ -27,6 +29,7 @@ public class ClickGuiModule extends Module {
|
||||
public ClickGuiModule() {
|
||||
super("ClickGui", "Opens the ClickGui", Module.Category.CLIENT);
|
||||
setBind(GLFW.GLFW_KEY_RIGHT_SHIFT);
|
||||
fontSize.setVisibility(v -> !font.getValue().isEmpty());
|
||||
rainbowHue.setVisibility(v -> rainbow.getValue());
|
||||
rainbowBrightness.setVisibility(v -> rainbow.getValue());
|
||||
rainbowSaturation.setVisibility(v -> rainbow.getValue());
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package cc.fros.features.modules.client;
|
||||
|
||||
import cc.fros.util.render.FrosFont;
|
||||
|
||||
import cc.fros.Fros;
|
||||
import cc.fros.event.impl.input.MouseInputEvent;
|
||||
import cc.fros.event.impl.render.Render2DEvent;
|
||||
@@ -91,13 +93,13 @@ public abstract class HudModule extends Module {
|
||||
}
|
||||
|
||||
if (shouldDrawDescription) {
|
||||
int textWidth = mc.font.width(getName());
|
||||
int textHeight = mc.font.lineHeight;
|
||||
int textWidth = FrosFont.width(getName());
|
||||
int textHeight = FrosFont.lineHeight();
|
||||
float textX = x + width + 5;
|
||||
if (textX + textWidth > mc.getWindow().getGuiScaledWidth()) {
|
||||
textX = x - 5 - textWidth;
|
||||
}
|
||||
e.getContext().drawString(mc.font, getName(),
|
||||
FrosFont.draw(e.getContext(), getName(),
|
||||
(int) textX, (int) (y + height / 2f - textHeight / 2f), -1);
|
||||
HudEditorScreen.getInstance().anyHover = true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package cc.fros.features.modules.hud;
|
||||
|
||||
import cc.fros.util.render.FrosFont;
|
||||
|
||||
import cc.fros.Fros;
|
||||
import cc.fros.event.impl.render.Render2DEvent;
|
||||
import cc.fros.features.modules.client.HudModule;
|
||||
@@ -38,20 +40,20 @@ public class ArrayListHudModule extends HudModule {
|
||||
|
||||
float x = getX();
|
||||
float y = getY();
|
||||
int lineHeight = mc.font.lineHeight;
|
||||
int lineHeight = FrosFont.lineHeight();
|
||||
int color = Fros.colorManager.getColorAsInt();
|
||||
boolean right = alignment.getValue() == Align.RIGHT;
|
||||
int nameBlock = 0;
|
||||
int content = 0;
|
||||
|
||||
for (Module module : shown) {
|
||||
int nameWidth = mc.font.width(module.getDisplayName());
|
||||
int nameWidth = FrosFont.width(module.getDisplayName());
|
||||
nameBlock = Math.max(nameBlock, nameWidth);
|
||||
|
||||
int rowWidth = nameWidth;
|
||||
String info = module.getDisplayInfo();
|
||||
if (info != null) {
|
||||
rowWidth += mc.font.width(" ") + infoWidth(info);
|
||||
rowWidth += FrosFont.width(" ") + infoWidth(info);
|
||||
}
|
||||
content = Math.max(content, rowWidth);
|
||||
}
|
||||
@@ -64,26 +66,26 @@ public class ArrayListHudModule extends HudModule {
|
||||
|
||||
int rowY = (int) (y + i * lineHeight);
|
||||
int baseColor = on ? color : DISABLED_COLOR;
|
||||
int nameWidth = mc.font.width(name);
|
||||
int nameWidth = FrosFont.width(name);
|
||||
|
||||
// name is anchored to the alignment side using names only, so it
|
||||
// never moves when dynamic module info appears or changes
|
||||
int nameX = (int) (right ? x + nameBlock - nameWidth : x);
|
||||
e.getContext().drawString(mc.font, name, nameX, rowY, baseColor);
|
||||
FrosFont.draw(e.getContext(), name, nameX, rowY, baseColor);
|
||||
|
||||
if (info != null) {
|
||||
// info sits directly on the opposite side of the name,
|
||||
// separated by a single space instead of a gap
|
||||
int blockWidth = infoWidth(info);
|
||||
int space = mc.font.width(" ");
|
||||
int space = FrosFont.width(" ");
|
||||
int infoX = right ? nameX - space - blockWidth : nameX + nameWidth + space;
|
||||
int bracketColor = on ? BRACKET_COLOR : DISABLED_COLOR;
|
||||
|
||||
e.getContext().drawString(mc.font, "[", infoX, rowY, bracketColor);
|
||||
infoX += mc.font.width("[");
|
||||
e.getContext().drawString(mc.font, info, infoX, rowY, on ? -1 : DISABLED_COLOR);
|
||||
infoX += mc.font.width(info);
|
||||
e.getContext().drawString(mc.font, "]", infoX, rowY, bracketColor);
|
||||
FrosFont.draw(e.getContext(), "[", infoX, rowY, bracketColor);
|
||||
infoX += FrosFont.width("[");
|
||||
FrosFont.draw(e.getContext(), info, infoX, rowY, on ? -1 : DISABLED_COLOR);
|
||||
infoX += FrosFont.width(info);
|
||||
FrosFont.draw(e.getContext(), "]", infoX, rowY, bracketColor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +94,7 @@ public class ArrayListHudModule extends HudModule {
|
||||
}
|
||||
|
||||
private int infoWidth(String info) {
|
||||
return mc.font.width("[") + mc.font.width(info) + mc.font.width("]");
|
||||
return FrosFont.width("[") + FrosFont.width(info) + FrosFont.width("]");
|
||||
}
|
||||
|
||||
private Comparator<Module> comparator() {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package cc.fros.features.modules.hud;
|
||||
|
||||
import cc.fros.util.render.FrosFont;
|
||||
|
||||
import cc.fros.event.impl.render.Render2DEvent;
|
||||
import cc.fros.features.modules.client.HudModule;
|
||||
import cc.fros.features.settings.Setting;
|
||||
@@ -33,11 +35,11 @@ public class CoordinatesHudModule extends HudModule {
|
||||
}
|
||||
}
|
||||
|
||||
e.getContext().drawString(mc.font, coordsStr,
|
||||
FrosFont.draw(e.getContext(), coordsStr,
|
||||
(int) getX(), (int) getY(), -1);
|
||||
|
||||
setWidth(mc.font.width(coordsStr));
|
||||
setHeight(mc.font.lineHeight);
|
||||
setWidth(FrosFont.width(coordsStr));
|
||||
setHeight(FrosFont.lineHeight());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package cc.fros.features.modules.hud;
|
||||
|
||||
import cc.fros.util.render.FrosFont;
|
||||
|
||||
import cc.fros.event.impl.render.Render2DEvent;
|
||||
import cc.fros.features.modules.client.HudModule;
|
||||
import cc.fros.features.settings.Setting;
|
||||
@@ -18,10 +20,10 @@ public class FpsHudModule extends HudModule {
|
||||
int fps = mc.getFps();
|
||||
String fpsStr = label.getValue() ? "FPS: " + fps : String.valueOf(fps);
|
||||
|
||||
e.getContext().drawString(mc.font, fpsStr,
|
||||
FrosFont.draw(e.getContext(), fpsStr,
|
||||
(int) getX(), (int) getY(), -1);
|
||||
|
||||
setWidth(mc.font.width(fpsStr));
|
||||
setHeight(mc.font.lineHeight);
|
||||
setWidth(FrosFont.width(fpsStr));
|
||||
setHeight(FrosFont.lineHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package cc.fros.features.modules.hud;
|
||||
|
||||
import cc.fros.util.render.FrosFont;
|
||||
|
||||
import cc.fros.event.impl.render.Render2DEvent;
|
||||
import cc.fros.features.modules.client.HudModule;
|
||||
import cc.fros.features.settings.Setting;
|
||||
@@ -36,11 +38,11 @@ public class PingHudModule extends HudModule {
|
||||
}
|
||||
|
||||
private void draw(Render2DEvent e, String text, int color) {
|
||||
e.getContext().drawString(mc.font, text,
|
||||
FrosFont.draw(e.getContext(), text,
|
||||
(int) getX(), (int) getY(), color);
|
||||
|
||||
setWidth(mc.font.width(text));
|
||||
setHeight(mc.font.lineHeight);
|
||||
setWidth(FrosFont.width(text));
|
||||
setHeight(FrosFont.lineHeight());
|
||||
}
|
||||
|
||||
private static int colorForPing(int ping) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package cc.fros.features.modules.hud;
|
||||
|
||||
import cc.fros.util.render.FrosFont;
|
||||
|
||||
import cc.fros.Fros;
|
||||
import cc.fros.event.impl.render.Render2DEvent;
|
||||
import cc.fros.features.modules.client.HudModule;
|
||||
@@ -47,7 +49,7 @@ public class RadarHudModule extends HudModule {
|
||||
|
||||
float x = getX();
|
||||
float y = getY();
|
||||
int lineHeight = mc.font.lineHeight;
|
||||
int lineHeight = FrosFont.lineHeight();
|
||||
int widest = 0;
|
||||
|
||||
for (int i = 0; i < nearby.size(); i++) {
|
||||
@@ -57,8 +59,8 @@ public class RadarHudModule extends HudModule {
|
||||
: entry.name();
|
||||
int color = entry.friend() ? 0xFF55FF55 : -1;
|
||||
|
||||
e.getContext().drawString(mc.font, text, (int) x, (int) (y + i * lineHeight), color);
|
||||
widest = Math.max(widest, mc.font.width(text));
|
||||
FrosFont.draw(e.getContext(), text, (int) x, (int) (y + i * lineHeight), color);
|
||||
widest = Math.max(widest, FrosFont.width(text));
|
||||
}
|
||||
|
||||
setWidth(widest);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package cc.fros.features.modules.hud;
|
||||
|
||||
import cc.fros.util.render.FrosFont;
|
||||
|
||||
import cc.fros.event.impl.render.Render2DEvent;
|
||||
import cc.fros.features.modules.client.HudModule;
|
||||
import cc.fros.features.settings.Setting;
|
||||
@@ -24,10 +26,10 @@ public class TimeHudModule extends HudModule {
|
||||
DateTimeFormatter formatter = twentyFourHour.getValue() ? TWENTY_FOUR_HOUR : TWELVE_HOUR;
|
||||
String timeStr = LocalTime.now().format(formatter);
|
||||
|
||||
e.getContext().drawString(mc.font, timeStr,
|
||||
FrosFont.draw(e.getContext(), timeStr,
|
||||
(int) getX(), (int) getY(), -1);
|
||||
|
||||
setWidth(mc.font.width(timeStr));
|
||||
setHeight(mc.font.lineHeight);
|
||||
setWidth(FrosFont.width(timeStr));
|
||||
setHeight(FrosFont.lineHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package cc.fros.features.modules.hud;
|
||||
|
||||
import cc.fros.util.render.FrosFont;
|
||||
|
||||
import cc.fros.event.impl.render.Render2DEvent;
|
||||
import cc.fros.features.modules.client.HudModule;
|
||||
import cc.fros.features.settings.Setting;
|
||||
@@ -27,10 +29,10 @@ public class WatermarkHudModule extends HudModule {
|
||||
}
|
||||
|
||||
Component component = TextUtil.text(watermarkString, text.getValue(), BuildConfig.VERSION);
|
||||
e.getContext().drawString(mc.font, component, (int) getX(), (int) getY(), -1);
|
||||
FrosFont.draw(e.getContext(), component, (int) getX(), (int) getY(), -1);
|
||||
|
||||
setWidth(mc.font.width(component));
|
||||
setHeight(mc.font.lineHeight);
|
||||
setWidth(FrosFont.width(component));
|
||||
setHeight(FrosFont.lineHeight());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package cc.fros.features.modules.hud;
|
||||
|
||||
import cc.fros.util.render.FrosFont;
|
||||
|
||||
import cc.fros.event.impl.render.Render2DEvent;
|
||||
import cc.fros.features.modules.client.HudModule;
|
||||
import cc.fros.features.settings.Setting;
|
||||
@@ -17,10 +19,10 @@ public class WelcomerHudModule extends HudModule {
|
||||
|
||||
String welcomeStr = greeting.getValue() + ", " + mc.getUser().getName();
|
||||
|
||||
e.getContext().drawString(mc.font, welcomeStr,
|
||||
FrosFont.draw(e.getContext(), welcomeStr,
|
||||
(int) getX(), (int) getY(), -1);
|
||||
|
||||
setWidth(mc.font.width(welcomeStr));
|
||||
setHeight(mc.font.lineHeight);
|
||||
setWidth(FrosFont.width(welcomeStr));
|
||||
setHeight(FrosFont.lineHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
package cc.fros.util.render;
|
||||
|
||||
import cc.fros.features.modules.client.ClickGuiModule;
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.Style;
|
||||
import net.minecraft.network.chat.TextColor;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static cc.fros.util.traits.Util.mc;
|
||||
|
||||
/**
|
||||
* Renders client UI text with a user supplied ttf/otf font.
|
||||
* Text is rasterized with AWT and cached as textures; when no font
|
||||
* is configured everything falls through to the vanilla font.
|
||||
*/
|
||||
public final class FrosFont {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("FrosFont");
|
||||
private static final Path FONT_DIR = FabricLoader.getInstance().getGameDir().resolve("fros").resolve("fonts");
|
||||
private static final int MAX_CACHE = 400;
|
||||
|
||||
private static final int[] PALETTE = {
|
||||
0x000000, 0x0000AA, 0x00AA00, 0x00AAAA,
|
||||
0xAA0000, 0xAA00AA, 0xFFAA00, 0xAAAAAA,
|
||||
0x555555, 0x5555FF, 0x55FF55, 0x55FFFF,
|
||||
0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF
|
||||
};
|
||||
private static final char[] HEX = "0123456789abcdef".toCharArray();
|
||||
|
||||
private static final Map<Key, Glyph> GLYPHS = new LinkedHashMap<>(128, 0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<Key, Glyph> eldest) {
|
||||
if (size() <= MAX_CACHE) return false;
|
||||
mc.getTextureManager().release(eldest.getValue().id());
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private record Key(int color, String text) {
|
||||
}
|
||||
|
||||
private record Glyph(Identifier id, int width, int height) {
|
||||
}
|
||||
|
||||
private static java.awt.Font awtFont;
|
||||
private static FontMetrics metrics;
|
||||
private static String loadedName;
|
||||
private static float loadedSize = Float.NaN;
|
||||
private static long lastAttempt;
|
||||
private static int glyphCounter;
|
||||
|
||||
private FrosFont() {
|
||||
}
|
||||
|
||||
public static int width(String text) {
|
||||
if (text == null || text.isEmpty()) return 0;
|
||||
sync();
|
||||
if (awtFont == null) return mc.font.width(text);
|
||||
return metrics.stringWidth(stripCodes(text));
|
||||
}
|
||||
|
||||
public static int width(Component component) {
|
||||
if (component == null) return 0;
|
||||
sync();
|
||||
if (awtFont == null) return mc.font.width(component);
|
||||
return width(toCodeString(component));
|
||||
}
|
||||
|
||||
public static int lineHeight() {
|
||||
sync();
|
||||
return awtFont == null ? mc.font.lineHeight : metrics.getHeight();
|
||||
}
|
||||
|
||||
public static void draw(GuiGraphics context, String text, int x, int y, int color) {
|
||||
draw(context, text, x, y, color, false);
|
||||
}
|
||||
|
||||
public static void draw(GuiGraphics context, String text, int x, int y, int color, boolean shadow) {
|
||||
if (text == null || text.isEmpty()) return;
|
||||
sync();
|
||||
if (awtFont == null) {
|
||||
context.drawString(mc.font, text, x, y, color, shadow);
|
||||
return;
|
||||
}
|
||||
if (shadow) drawGlyphs(context, text, x + 1, y + 1, shadowColor(color));
|
||||
drawGlyphs(context, text, x, y, color);
|
||||
}
|
||||
|
||||
public static void draw(GuiGraphics context, Component component, int x, int y, int color) {
|
||||
if (component == null) return;
|
||||
sync();
|
||||
if (awtFont == null) {
|
||||
context.drawString(mc.font, component, x, y, color);
|
||||
return;
|
||||
}
|
||||
draw(context, toCodeString(component), x, y, color, false);
|
||||
}
|
||||
|
||||
// --- loading ---
|
||||
|
||||
private static void sync() {
|
||||
ClickGuiModule gui = ClickGuiModule.getInstance();
|
||||
String name = gui == null ? "" : gui.font.getValue();
|
||||
int size = gui == null ? 10 : gui.fontSize.getValue();
|
||||
if (name == null) name = "";
|
||||
|
||||
boolean changed = !Objects.equals(name, loadedName) || size != loadedSize;
|
||||
if (!changed) {
|
||||
boolean missing = awtFont == null && !name.isBlank();
|
||||
// retry a missing file every couple of seconds so dropping one
|
||||
// into the folder loads without touching the setting again
|
||||
if (!missing || System.currentTimeMillis() - lastAttempt < 2000) return;
|
||||
}
|
||||
reset(name, size, changed);
|
||||
}
|
||||
|
||||
private static void reset(String name, float size, boolean verbose) {
|
||||
loadedName = name;
|
||||
loadedSize = size;
|
||||
lastAttempt = System.currentTimeMillis();
|
||||
awtFont = null;
|
||||
metrics = null;
|
||||
clearGlyphs();
|
||||
|
||||
try {
|
||||
Files.createDirectories(FONT_DIR);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Could not create font directory {}", FONT_DIR, e);
|
||||
return;
|
||||
}
|
||||
if (name.isBlank()) return;
|
||||
try {
|
||||
Path file = resolveFontFile(name);
|
||||
if (file == null) {
|
||||
if (verbose) LOGGER.warn("Font '{}' not found in {}", name, FONT_DIR);
|
||||
return;
|
||||
}
|
||||
awtFont = java.awt.Font.createFont(java.awt.Font.TRUETYPE_FONT, file.toFile())
|
||||
.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();
|
||||
graphics.dispose();
|
||||
if (verbose) LOGGER.info("Loaded font {} ({}pt)", file.getFileName(), size);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to load font '{}'", name, e);
|
||||
awtFont = null;
|
||||
metrics = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Path resolveFontFile(String name) {
|
||||
try {
|
||||
Files.createDirectories(FONT_DIR);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Could not create font directory {}", FONT_DIR, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
String lower = name.toLowerCase(Locale.ROOT);
|
||||
if (lower.endsWith(".ttf") || lower.endsWith(".otf")) {
|
||||
Path direct = FONT_DIR.resolve(name);
|
||||
return Files.isRegularFile(direct) ? direct : null;
|
||||
}
|
||||
|
||||
try (Stream<Path> files = Files.list(FONT_DIR)) {
|
||||
return files.filter(Files::isRegularFile)
|
||||
.filter(path -> {
|
||||
String fileName = path.getFileName().toString().toLowerCase(Locale.ROOT);
|
||||
return fileName.equals(lower)
|
||||
|| fileName.equals(lower + ".ttf")
|
||||
|| fileName.equals(lower + ".otf");
|
||||
})
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Could not list font directory {}", FONT_DIR, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearGlyphs() {
|
||||
for (Glyph glyph : new ArrayList<>(GLYPHS.values())) {
|
||||
mc.getTextureManager().release(glyph.id());
|
||||
}
|
||||
GLYPHS.clear();
|
||||
}
|
||||
|
||||
// --- rasterizing ---
|
||||
|
||||
private static void drawGlyphs(GuiGraphics context, String text, int x, int y, int color) {
|
||||
Key key = new Key(color, text);
|
||||
Glyph glyph = GLYPHS.get(key);
|
||||
if (glyph == null) {
|
||||
glyph = createGlyph(text, color);
|
||||
if (glyph == null) return;
|
||||
GLYPHS.put(key, glyph);
|
||||
}
|
||||
context.blit(RenderPipelines.GUI_TEXTURED, glyph.id(),
|
||||
x - 1, y - 1, 0.0f, 0.0f, glyph.width(), glyph.height(), glyph.width(), glyph.height());
|
||||
}
|
||||
|
||||
private static Glyph createGlyph(String text, int baseColor) {
|
||||
String plain = stripCodes(text);
|
||||
if (plain.isEmpty()) return null;
|
||||
|
||||
int width = Math.max(1, metrics.stringWidth(plain));
|
||||
int height = metrics.getHeight();
|
||||
int imageWidth = width + 2;
|
||||
int imageHeight = height + 2;
|
||||
|
||||
BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
graphics.setFont(awtFont);
|
||||
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
drawSegments(graphics, text, baseColor);
|
||||
graphics.dispose();
|
||||
|
||||
NativeImage nativeImage = new NativeImage(imageWidth, imageHeight, false);
|
||||
for (int py = 0; py < imageHeight; py++) {
|
||||
for (int px = 0; px < imageWidth; px++) {
|
||||
int argb = image.getRGB(px, py);
|
||||
int abgr = (argb & 0xFF000000)
|
||||
| ((argb & 0x000000FF) << 16)
|
||||
| (argb & 0x0000FF00)
|
||||
| ((argb & 0x00FF0000) >> 16);
|
||||
nativeImage.setPixelABGR(px, py, abgr);
|
||||
}
|
||||
}
|
||||
|
||||
Identifier id = Identifier.fromNamespaceAndPath("fros", "font/glyph_" + glyphCounter++);
|
||||
DynamicTexture texture = new DynamicTexture(() -> id.toString(), nativeImage);
|
||||
texture.upload();
|
||||
mc.getTextureManager().register(id, texture);
|
||||
return new Glyph(id, imageWidth, imageHeight);
|
||||
}
|
||||
|
||||
private static void drawSegments(Graphics2D graphics, String text, int baseColor) {
|
||||
int baseline = metrics.getAscent() + 1;
|
||||
int cursor = 1;
|
||||
int current = baseColor;
|
||||
int segmentStart = 0;
|
||||
int i = 0;
|
||||
|
||||
while (i < text.length()) {
|
||||
if (text.charAt(i) == '\u00A7' && i + 1 < text.length()) {
|
||||
cursor = flush(graphics, text, segmentStart, i, cursor, current, baseline);
|
||||
|
||||
char code = Character.toLowerCase(text.charAt(i + 1));
|
||||
i += 2;
|
||||
segmentStart = i;
|
||||
|
||||
if (code == 'x') {
|
||||
int rgb = 0;
|
||||
int nibbles = 0;
|
||||
while (nibbles < 6 && i + 1 < text.length() && text.charAt(i) == '\u00A7') {
|
||||
int digit = Character.digit(Character.toLowerCase(text.charAt(i + 1)), 16);
|
||||
if (digit < 0) break;
|
||||
rgb = (rgb << 4) | digit;
|
||||
i += 2;
|
||||
nibbles++;
|
||||
}
|
||||
current = 0xFF000000 | rgb;
|
||||
segmentStart = i;
|
||||
} else if (code == 'r') {
|
||||
current = baseColor;
|
||||
} else {
|
||||
int index = Character.digit(code, 16);
|
||||
if (index >= 0) current = 0xFF000000 | PALETTE[index];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
flush(graphics, text, segmentStart, text.length(), cursor, current, baseline);
|
||||
}
|
||||
|
||||
private static int flush(Graphics2D graphics, String text, int start, int end, int cursor, int color, int baseline) {
|
||||
if (end <= start) return cursor;
|
||||
String segment = text.substring(start, end);
|
||||
graphics.setColor(new Color(color, true));
|
||||
graphics.drawString(segment, cursor, baseline);
|
||||
return cursor + metrics.stringWidth(segment);
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
private static String stripCodes(String text) {
|
||||
if (text.indexOf('\u00A7') < 0) return text;
|
||||
StringBuilder builder = new StringBuilder(text.length());
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (c == '\u00A7' && i + 1 < text.length()) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
builder.append(c);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String toCodeString(Component component) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
TextColor[] previous = {null};
|
||||
|
||||
component.visit((style, text) -> {
|
||||
TextColor color = style.getColor();
|
||||
if (Objects.equals(color, previous[0])) {
|
||||
builder.append(text);
|
||||
return Optional.empty();
|
||||
}
|
||||
if (color == null) {
|
||||
builder.append('\u00A7').append('r');
|
||||
} else {
|
||||
appendColorCode(builder, color.getValue());
|
||||
}
|
||||
previous[0] = color;
|
||||
builder.append(text);
|
||||
return Optional.empty();
|
||||
}, Style.EMPTY);
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static void appendColorCode(StringBuilder builder, int rgb) {
|
||||
for (int i = 0; i < PALETTE.length; i++) {
|
||||
if (PALETTE[i] == (rgb & 0xFFFFFF)) {
|
||||
builder.append('\u00A7').append(HEX[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
builder.append('\u00A7').append('x');
|
||||
for (int i = 5; i >= 0; i--) {
|
||||
builder.append('\u00A7').append(HEX[(rgb >> (i * 4)) & 0xF]);
|
||||
}
|
||||
}
|
||||
|
||||
private static int shadowColor(int color) {
|
||||
return ((color & 0xFCFCFC) >> 2) | (color & 0xFF000000);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user