Files
Mindustry/core/src/mindustry/ui/Fonts.java
2025-10-22 06:42:01 -04:00

345 lines
13 KiB
Java

package mindustry.ui;
import arc.*;
import arc.Graphics.Cursor.*;
import arc.assets.*;
import arc.files.*;
import arc.freetype.*;
import arc.freetype.FreeTypeFontGenerator.*;
import arc.freetype.FreetypeFontLoader.*;
import arc.graphics.*;
import arc.graphics.Texture.*;
import arc.graphics.g2d.*;
import arc.graphics.g2d.Font.*;
import arc.graphics.g2d.PixmapPacker.*;
import arc.graphics.g2d.TextureAtlas.*;
import arc.math.geom.*;
import arc.scene.style.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.core.*;
import mindustry.game.*;
import mindustry.gen.*;
import java.io.*;
public class Fonts{
private static final String mainFont = "fonts/font.woff";
private static final ObjectSet<String> unscaled = ObjectSet.with("iconLarge", "logic");
private static ObjectIntMap<String> unicodeIcons = new ObjectIntMap<>();
private static IntMap<String> unicodeToName = new IntMap<>();
private static ObjectMap<String, String> stringIcons = new ObjectMap<>();
private static ObjectMap<String, TextureRegion> largeIcons = new ObjectMap<>();
public static Font def, outline, icon, iconLarge, tech, logic, monospace;
public static int getUnicode(String content){
return unicodeIcons.get(content, 0);
}
public static String getUnicodeStr(String content){
return stringIcons.get(content, "");
}
public static boolean hasUnicodeStr(String content){
return stringIcons.containsKey(content);
}
/** Called from a static context to make the cursor appear immediately upon startup.*/
public static void loadSystemCursors(){
SystemCursor.arrow.set(Core.graphics.newCursor("cursor", cursorScale()));
SystemCursor.hand.set(Core.graphics.newCursor("hand", cursorScale()));
SystemCursor.ibeam.set(Core.graphics.newCursor("ibeam", cursorScale()));
Core.graphics.restoreCursor();
}
public static int cursorScale(){
return 1;
}
public static void loadFonts(){
largeIcons.clear();
FreeTypeFontParameter param = fontParameter();
Core.assets.load("default", Font.class, new FreeTypeFontLoaderParameter(mainFont, param)).loaded = f -> Fonts.def = f;
Core.assets.load("monospace", Font.class, new FreeTypeFontLoaderParameter("fonts/monospace.woff", new FreeTypeFontParameter(){{
size = 16;
incremental = true;
//most people will never see the monospace font, so don't pre-bake anything
characters = "\u0000 ";
}})).loaded = f -> Fonts.monospace = f;
Core.assets.load("icon", Font.class, new FreeTypeFontLoaderParameter("fonts/icon.ttf", new FreeTypeFontParameter(){{
size = 30;
incremental = true;
characters = "\0";
}})).loaded = f -> Fonts.icon = f;
Core.assets.load("iconLarge", Font.class, new FreeTypeFontLoaderParameter("fonts/icon.ttf", new FreeTypeFontParameter(){{
size = 48;
incremental = false;
characters = "\0" + Iconc.all;
borderWidth = 5f;
borderColor = Color.darkGray;
}})).loaded = f -> Fonts.iconLarge = f;
Core.assets.load("logic", Font.class, new FreeTypeFontLoaderParameter("fonts/logic.ttf", new FreeTypeFontParameter(){{
size = 16;
//generated all at once, it's fast enough anyway
incremental = false;
//ASCII only
characters = "\0ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890\"!`?'.,;:()[]{}<>|/@\\^$€-%+=#_&~*";
}})).loaded = f -> Fonts.logic = f;
}
public static @Nullable String unicodeToName(int unicode){
return unicodeToName.get(unicode, () -> Iconc.codeToName.get(unicode));
}
public static TextureRegion getLargeIcon(String name){
return largeIcons.get(name, () -> {
var region = new TextureRegion();
int code = Iconc.codes.get(name, '\uF8D4');
var glyph = iconLarge.getData().getGlyph((char)code);
if(glyph == null) return Core.atlas.find(name);
region.set(iconLarge.getRegion().texture);
region.set(glyph.u, glyph.v2, glyph.u2, glyph.v);
return region;
});
}
public static void registerIcon(String name, String regionName, int ch, TextureRegion region){
int size = (int)(Fonts.def.getData().lineHeight/Fonts.def.getData().scaleY);
unicodeIcons.put(name, ch);
stringIcons.put(name, ((char)ch) + "");
unicodeToName.put(ch, regionName);
Vec2 out = Scaling.fit.apply(region.width, region.height, size, size);
Glyph glyph = new Glyph();
glyph.id = ch;
glyph.srcX = 0;
glyph.srcY = 0;
glyph.width = (int)out.x;
glyph.height = (int)out.y;
glyph.u = region.u;
glyph.v = region.v2;
glyph.u2 = region.u2;
glyph.v2 = region.v;
glyph.xoffset = 0;
glyph.yoffset = -size;
glyph.xadvance = size;
glyph.kerning = null;
glyph.fixedWidth = true;
glyph.page = 0;
Fonts.def.getData().setGlyph(ch, glyph);
Fonts.outline.getData().setGlyph(ch, glyph);
}
public static void loadContentIcons(){
Texture uitex = Core.atlas.find("logo").texture;
try(var reader = Core.files.internal("icons/icons.properties").reader(Vars.bufferSize)){
String line;
while((line = reader.readLine()) != null){
String[] split = line.split("=");
String[] nametex = split[1].split("\\|");
String character = split[0], texture = nametex[1];
int ch = Integer.parseInt(character);
TextureRegion region = Core.atlas.find(texture);
if(region.texture != uitex){
continue;
}
registerIcon(nametex[0], texture, ch, region);
}
}catch(IOException e){
throw new RuntimeException(e);
}
stringIcons.put("alphachan", stringIcons.get("alphaaaa"));
//TODO: mod emojis can't work because most mod icons are not on the UI page!
/*
if(Vars.mods.list().contains(m -> m.shouldBeEnabled())){
ContentType[] types = {ContentType.liquid, ContentType.item, ContentType.block, ContentType.status, ContentType.unit};
int startChar = 0xE000 + 1;
for(var type : types){
for(var cont : Vars.content.getBy(type)){
if(!cont.isVanilla() && cont instanceof UnlockableContent u && u.uiIcon.found()){
int id = startChar;
registerIcon(u.name, u.uiIcon instanceof AtlasRegion atlas ? atlas.name : u.name, id, u.uiIcon);
startChar ++;
}
}
}
}*/
for(Team team : Team.baseTeams){
team.emoji = stringIcons.get(team.name, "");
}
}
public static void loadContentIconsHeadless(){
try(var reader = Core.files.internal("icons/icons.properties").reader(Vars.bufferSize)){
String line;
while((line = reader.readLine()) != null){
String[] split = line.split("=");
String[] nametex = split[1].split("\\|");
String character = split[0];
int ch = Integer.parseInt(character);
unicodeIcons.put(nametex[0], ch);
stringIcons.put(nametex[0], ((char)ch) + "");
}
}catch(IOException e){
throw new RuntimeException(e);
}
stringIcons.put("alphachan", stringIcons.get("alphaaaa"));
for(Team team : Team.baseTeams){
team.emoji = stringIcons.get(team.name, "");
}
}
/** Called from a static context for use in the loading screen.*/
public static void loadDefaultFont(){
int max = Gl.getInt(Gl.maxTextureSize);
UI.packer = new PixmapPacker(max >= 4096 ? 4096 : 2048, 2048, 2, true);
Core.assets.setLoader(FreeTypeFontGenerator.class, new FreeTypeFontGeneratorLoader(Core.files::internal));
Core.assets.setLoader(Font.class, null, new FreetypeFontLoader(Core.files::internal){
ObjectSet<FreeTypeFontParameter> scaled = new ObjectSet<>();
@Override
public Font loadSync(AssetManager manager, String fileName, Fi file, FreeTypeFontLoaderParameter parameter){
if(fileName.equals("outline")){
parameter.fontParameters.borderWidth = Scl.scl(2f);
parameter.fontParameters.spaceX -= parameter.fontParameters.borderWidth;
}
if(!scaled.contains(parameter.fontParameters) && !unscaled.contains(fileName)){
parameter.fontParameters.size = (int)(Scl.scl(parameter.fontParameters.size));
scaled.add(parameter.fontParameters);
}
parameter.fontParameters.magFilter = TextureFilter.linear;
parameter.fontParameters.minFilter = TextureFilter.linear;
parameter.fontParameters.packer = UI.packer;
return super.loadSync(manager, fileName, file, parameter);
}
});
FreeTypeFontParameter param = new FreeTypeFontParameter(){{
borderColor = Color.darkGray;
incremental = true;
size = 18;
}};
Core.assets.load("outline", Font.class, new FreeTypeFontLoaderParameter(mainFont, param)).loaded = t -> Fonts.outline = t;
Core.assets.load("tech", Font.class, new FreeTypeFontLoaderParameter("fonts/tech.ttf", new FreeTypeFontParameter(){{
size = 18;
}})).loaded = f -> {
Fonts.tech = f;
Fonts.tech.getData().down *= 1.5f;
};
}
/** Merges the UI and font atlas together for better performance. */
public static void mergeFontAtlas(TextureAtlas atlas){
//grab all textures from the ui page, remove all the regions assigned to it, then copy them over to UI.packer and replace the texture in this atlas.
//grab old UI texture and regions...
Texture texture = atlas.find("logo").texture;
Page page = UI.packer.getPages().first();
Seq<AtlasRegion> regions = atlas.getRegions().select(t -> t.texture == texture);
for(AtlasRegion region : regions){
//get new pack rect
page.setDirty(false);
Rect rect = UI.packer.pack(region.name, atlas.getPixmap(region), region.splits, region.pads);
//set new texture
region.texture = UI.packer.getPages().first().getTexture();
//set its new position
region.set((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height);
//add old texture
atlas.getTextures().add(region.texture);
//clear it
region.pixmapRegion = null;
}
//remove old texture, it will no longer be used
atlas.getTextures().remove(texture);
texture.dispose();
atlas.disposePixmap(texture);
page.setDirty(true);
page.updateTexture(TextureFilter.linear, TextureFilter.linear, false);
}
public static TextureRegionDrawable getGlyph(Font font, char glyph){
Glyph found = font.getData().getGlyph(glyph);
if(found == null){
Log.warn("No icon found for glyph: @ (@)", glyph, (int)glyph);
found = font.getData().getGlyph('F');
}
Glyph g = found;
float size = Math.max(g.width, g.height);
TextureRegionDrawable draw = new TextureRegionDrawable(new TextureRegion(font.getRegion().texture, g.u, g.v2, g.u2, g.v)){
@Override
public void draw(float x, float y, float width, float height){
Draw.color(Tmp.c1.set(tint).mul(Draw.getColor()).toFloatBits());
float cx = x + width/2f - g.width/2f, cy = y + height/2f - g.height/2f;
cx = (int)cx;
cy = (int)cy;
Draw.rect(region, cx + g.width/2f, cy + g.height/2f, g.width, g.height);
}
@Override
public void draw(float x, float y, float originX, float originY, float width, float height, float scaleX, float scaleY, float rotation){
width *= scaleX;
height *= scaleY;
Draw.color(Tmp.c1.set(tint).mul(Draw.getColor()).toFloatBits());
float cx = x + width/2f - g.width/2f, cy = y + height/2f - g.height/2f;
cx = (int)cx;
cy = (int)cy;
originX = g.width/2f;
originY = g.height/2f;
Draw.rect(region, cx + g.width/2f, cy + g.height/2f, g.width * scaleX, g.height * scaleY, originX, originY, rotation);
}
@Override
public float imageSize(){
return size;
}
};
draw.setMinWidth(size);
draw.setMinHeight(size);
return draw;
}
static FreeTypeFontParameter fontParameter(){
return new FreeTypeFontParameter(){{
size = 18;
shadowColor = Color.darkGray;
shadowOffsetY = 2;
incremental = true;
}};
}
}