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:
froslanz
2026-10-22 11:22:47 -04:00
parent d1cb9c8d3a
commit 30ccc6ea5a
14 changed files with 431 additions and 40 deletions
@@ -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);
}
}