Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ Fractal introduces **subtabs for the creative menu**.
### Limitations

- More than 12 subgroups per item group, while fully functional, will look weird.
- The tiny font used for the labels does not support full Unicode.

## Examples

Expand Down
216 changes: 216 additions & 0 deletions src/main/java/de/dafuqs/fractal/impl/client/AngelFont.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package de.dafuqs.fractal.impl.client;

import static java.lang.Float.parseFloat;
import static java.lang.Integer.parseInt;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Map;

import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import net.minecraft.resources.Identifier;

public record AngelFont(
// info
String face,
float size,
boolean bold,
boolean italic,
float paddingTop,
float paddingRight,
float paddingBottom,
float paddingLeft,
int horizontalSpacing,
int verticalSpacing,

// common
float lineHeight,
float base,
int scaleW,
int scaleH,

// chars
Int2ObjectMap<CharDef> chars
) {

public record CharDef(int x, int y, int width, int height, int xOffset, int yOffset, int xAdvance, Identifier page) {}

public static AngelFont parse(Identifier reference, BufferedReader r) throws FileNotFoundException, IOException {
try (r) {
var info = tokenize(r.readLine());
if (!"info".equals(info.get("_type")))
throw new IOException("Must be the info tag at line 1");
var face = info.get("face");
var size = parseFloat(info.get("size"));
var bold = "1".equals(info.get("bold"));
var italic = "1".equals(info.get("italic"));
if (!"".equals(info.get("charset")) && !"1".equals(info.get("unicode")))
throw new IOException("Non-Unicode fonts are not supported by this implementation; expected libGDX Hiero `charset=\"\" unicode=0` or standard `charset=* unicode=1` at line 1");

if (!"100".equals(info.get("stretchH")))
throw new IOException("Stretched fonts are not supported by this implementation at line 1");

var paddingSpl = info.get("padding").split(",");
if (paddingSpl.length != 4)
throw new IOException("padding must have exactly 4 entries at line 1");
var paddingTop = parseFloat(paddingSpl[0]);
var paddingRight = parseFloat(paddingSpl[1]);
var paddingBottom = parseFloat(paddingSpl[2]);
var paddingLeft = parseFloat(paddingSpl[3]);

var spacingSpl = info.get("spacing").split(",");
if (spacingSpl.length != 2)
throw new IOException("spacing must have exactly 2 entries at line 1");
var horizontalSpacing = parseInt(spacingSpl[0]);
var verticalSpacing = parseInt(spacingSpl[1]);

var common = tokenize(r.readLine());
if (!"common".equals(common.get("_type")))
throw new IOException("Must be the common tag at line 2");

if (!"0".equals(common.get("packed")))
throw new IOException("Packed fonts are not supported by this implementation at line 2");

var lineHeight = parseFloat(common.get("lineHeight"));
var base = parseFloat(common.get("base"));
var scaleW = parseInt(common.get("scaleW"));
var scaleH = parseInt(common.get("scaleH"));
var pages = new Int2ObjectOpenHashMap<Identifier>(parseInt(common.get("pages")));

Int2ObjectMap<CharDef> chars = null;

int lineNo = 2;

while (true) {
var line = r.readLine();
if (line == null) break;
lineNo++;
var tk = tokenize(line);
switch (tk.get("_type")) {
case "info" -> throw new IOException("Unexpected late info tag at line "+lineNo);
case "common" -> throw new IOException("Unexpected late common tag at line "+lineNo);
case "page" -> {
if (chars != null)
throw new IOException("Got page tag after chars tag at line "+lineNo);
pages.put(parseInt(tk.get("id")), reference.withSuffix("/"+tk.get("file")));
}
case "chars" -> {
chars = new Int2ObjectOpenHashMap<>(parseInt(tk.get("count")));
}
case "char" -> {
if (chars == null)
throw new IOException("Got char tag before chars tag at line "+lineNo);
if (!"0".equals(tk.get("chnl")) && !"15".equals(tk.get("chnl")))
throw new IOException("Packed fonts are not supported by this implementation; expected libGDX chnl=0 or standard chnl=15 at line "+lineNo);
int page = parseInt(tk.get("page"));
if (!pages.containsKey(page))
throw new IOException("Got char tag for undeclared page id="+page+" at line "+lineNo);
chars.put(parseInt(tk.get("id")), new CharDef(
parseInt(tk.get("x")), parseInt(tk.get("y")),
parseInt(tk.get("width")), parseInt(tk.get("height")),
parseInt(tk.get("xoffset")), parseInt(tk.get("yoffset")),
parseInt(tk.get("xadvance")), pages.get(page)
));
}
case "kernings" -> {
if (!"0".equals(tk.get("count")))
throw new IOException("Kerning is not supported by this implementation at line "+lineNo);
}
case "kerning" -> {
throw new IOException("Kerning is not supported by this implementation at line "+lineNo);
}
default -> throw new IOException("Unrecognized tag type "+tk.get("_type")+" at line "+lineNo);
}
}

if (chars == null) throw new IOException("Found no char definitions");

return new AngelFont(face, size, bold, italic, paddingTop, paddingRight, paddingBottom,
paddingLeft, horizontalSpacing, verticalSpacing, lineHeight, base, scaleW, scaleH,
Int2ObjectMaps.unmodifiable(chars));
}
}

private static Map<String, String> tokenize(String line) throws IOException {
if (line == null) return Map.of();
var out = new Object2ObjectLinkedOpenHashMap<String, String>();
String key = null;
var buf = new StringBuilder();

enum S {
BEFORE_KEY,
KEY,
BEFORE_DELIMITER,
DELIMITER,
UNQUOTED_VALUE,
QUOTED_VALUE,
}

int idx = line.indexOf(' ');
String type = line.substring(0, idx);
out.put("_type", type);

var state = S.BEFORE_KEY;
for (int i = idx; i < line.length(); i++) {
var ch = line.charAt(i);
switch (state) {
case BEFORE_KEY:
if (ch == ' ') continue;
state = S.KEY;
// fall-thru
case KEY:
if (ch == ' ' || ch == '=') {
state = ch == ' ' ? S.BEFORE_DELIMITER : S.DELIMITER;
key = buf.toString();
buf.setLength(0);
} else {
buf.append(ch);
}
break;
case BEFORE_DELIMITER:
if (ch == ' ') continue;
if (ch == '=') {
state = S.DELIMITER;
}
break;
case DELIMITER:
if (ch == '=') continue;
if (ch == '"') {
state = S.QUOTED_VALUE;
continue;
} else if (ch != ' ') {
state = S.UNQUOTED_VALUE;
} else {
continue;
}
// fall-thru
case UNQUOTED_VALUE:
case QUOTED_VALUE:
char end = state == S.UNQUOTED_VALUE ? ' ' : '"';
if (ch == end) {
out.put(key, buf.toString());
buf.setLength(0);
state = S.BEFORE_KEY;
} else {
buf.append(ch);
}
break;
}
}
if (state == S.DELIMITER) {
throw new IOException("Unexpected EOF while searching for beginning of value");
}
if (state == S.QUOTED_VALUE) {
throw new IOException("Unexpected EOF while parsing quoted value");
}
if (state == S.UNQUOTED_VALUE) {
out.put(key, buf.toString());
}
return out;
}

}
102 changes: 102 additions & 0 deletions src/main/java/de/dafuqs/fractal/impl/client/SmallFontRenderer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package de.dafuqs.fractal.impl.client;

import java.io.IOException;
import java.io.UncheckedIOException;

import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.resources.Identifier;
import net.minecraft.server.packs.resources.ReloadableResourceManager;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.server.packs.resources.SimplePreparableReloadListener;
import net.minecraft.util.profiling.ProfilerFiller;

public class SmallFontRenderer {

private static boolean hasRegisteredReloader = false;

private static final Identifier TINYFONT_TEXTURE = Identifier.fromNamespaceAndPath("fractal", "textures/gui/tinyfont.png");
private static final Identifier SILVER_REF = Identifier.fromNamespaceAndPath("fractal", "textures/gui");
private static final Identifier SILVER_FNT = Identifier.fromNamespaceAndPath("fractal", "textures/gui/silver.fnt");
private static AngelFont SILVER;

public static void draw(GuiGraphics ctx, String str, int x, int y, boolean rtl, int textColor) {
int[] codePoints = str.codePoints().toArray();
if (rtl) {
int hf = codePoints.length/2;
int anc = codePoints.length-1;
for (int i = 0; i < hf; i++) {
int swp = codePoints[i];
codePoints[i] = codePoints[anc-i];
codePoints[anc-i] = swp;
}
}
boolean forceUnicode = Minecraft.getInstance().isEnforceUnicode();
if (!forceUnicode) {
for (int i = 0; i < codePoints.length; i++) {
int c = codePoints[i];
if (c > 0x7F) {
// avoid disjointed character rendering in e.g. Polish
forceUnicode = true;
break;
}
}
}
ctx.pose().pushMatrix();
ctx.pose().translate(x, y);
ctx.pose().scale(0.5f);
int xPos = 0;
for (int i = 0; i < codePoints.length; i++) {
int c = codePoints[i];
int advance;
if (!forceUnicode && c <= 0x7F) {
int u = (c % 16) * 4;
int v = (c / 16) * 6;
ctx.blit(RenderPipelines.GUI_TEXTURED, TINYFONT_TEXTURE, xPos+(rtl?-8:8), 0, u*2, v*2, 8, 12, 128, 96, textColor);
advance = 8;
} else {
if (!hasRegisteredReloader) {
hasRegisteredReloader = true;
((ReloadableResourceManager)Minecraft.getInstance().getResourceManager()).registerReloadListener(new Reloader());
}
if (SILVER == null) {
SILVER = Reloader.parse(Minecraft.getInstance().getResourceManager());
}
var ch = SILVER.chars().get(c);
if (ch == null) continue;
advance = ch.xAdvance();
ctx.blit(RenderPipelines.GUI_TEXTURED, ch.page(), xPos+ch.xOffset()+(rtl?-advance:8), ch.yOffset()-2, ch.x(), ch.y(), ch.width(), ch.height(), SILVER.scaleW(), SILVER.scaleH(), textColor);
}
if (rtl) {
xPos -= advance;
} else {
xPos += advance;
}
}
ctx.pose().popMatrix();
}

private static final class Reloader extends SimplePreparableReloadListener<AngelFont> {

private static AngelFont parse(ResourceManager manager) {
try {
return AngelFont.parse(SILVER_REF, manager.getResourceOrThrow(SILVER_FNT).openAsReader());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

@Override
protected AngelFont prepare(ResourceManager manager, ProfilerFiller profiler) {
return parse(manager);
}

@Override
protected void apply(AngelFont cache, ResourceManager manager, ProfilerFiller profiler) {
SILVER = cache;
}

}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.dafuqs.fractal.mixin.client;

import de.dafuqs.fractal.api.*;
import de.dafuqs.fractal.impl.client.SmallFontRenderer;
import de.dafuqs.fractal.interfaces.*;
import net.minecraft.client.gui.*;
import net.minecraft.client.gui.screens.inventory.*;
Expand All @@ -25,9 +26,6 @@ public abstract class CreativeInventoryScreenAddTabsMixin extends AbstractContai
@Unique
private static final int LAST_TAB_INDEX_RENDERING_LEFT = 11;

@Unique
private static final Identifier TINYFONT_TEXTURE = Identifier.fromNamespaceAndPath("fractal", "textures/gui/tinyfont.png");

public CreativeInventoryScreenAddTabsMixin(CreativeModeInventoryScreen.ItemPickerMenu screenHandler, Inventory playerInventory, Component text) {
super(screenHandler, playerInventory, text);
}
Expand Down Expand Up @@ -84,27 +82,13 @@ public CreativeInventoryScreenAddTabsMixin(CreativeModeInventoryScreen.ItemPicke

guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, subtabTextureID, curX - tabStartOffset, curY, 72, 11);

int textOffset = thisChildSelected ? 8 : 5; // makes the text pop slightly outwards if selected
int textOffset = thisChildSelected ? 3 : 0; // makes the text pop slightly outwards if selected
int textColor = child.getStyle().subtabNameTextColor();
String tabDisplayName = child.getDisplayName().getString();
if (rendersOnTheRight) {
for (int i = 0; i < tabDisplayName.length(); i++) {
char c = tabDisplayName.charAt(i);
if (c > 0x7F) continue;
int u = (c % 16) * 4;
int v = (c / 16) * 6;
guiGraphics.blit(RenderPipelines.GUI_TEXTURED, TINYFONT_TEXTURE, curX + 1 - tabStartOffset + textOffset, curY + 3, u, v, 4, 6, 64, 48, textColor);
curX += 4;
}
SmallFontRenderer.draw(guiGraphics, tabDisplayName, curX + 1 - tabStartOffset + textOffset, curY + 3, false, textColor);
} else {
for (int i = tabDisplayName.length() - 1; i >= 0; i--) {
char c = tabDisplayName.charAt(i);
if (c > 0x7F) continue;
int u = (c % 16) * 4;
int v = (c / 16) * 6;
guiGraphics.blit(RenderPipelines.GUI_TEXTURED, TINYFONT_TEXTURE, curX - textOffset, curY + 3, u, v, 4, 6, 64, 48, textColor);
curX -= 4;
}
SmallFontRenderer.draw(guiGraphics, tabDisplayName, curX - textOffset, curY + 3, true, textColor);
}

int index = child.getIndexInParent();
Expand Down
Loading