Font renderer ported

This commit is contained in:
2026-08-06 13:56:16 +02:00
parent 9362707ccc
commit 3b074a2684
27 changed files with 1605 additions and 9 deletions
@@ -0,0 +1,35 @@
package speiger.src.coreengine.assets.api;
import java.util.Map;
public record AssetFilter(String prefix, String extension) {
public static AssetFilter json(String prefix) {
return new AssetFilter(prefix, ".json");
}
public ID id(ID file) {
String location = file.fileLocation();
return ID.of(file.domain(), location.substring(prefix().length()+1, location.length()-extension().length()));
}
public ID file(ID id) {
return ID.of(id.domain(), prefix()+"/"+id.fileLocation()+extension());
}
public Map<ID, IAsset> list(IAssetProvider provider) {
return provider.list(prefix, T -> T.endsWith(extension));
}
public Map<ID, IAsset> listRoot(IAssetProvider provider) {
return provider.list(prefix, T -> T.endsWith(extension) && T.isRootFolder(prefix));
}
public Map<ID, MultiAsset> multi(IAssetProvider provider) {
return provider.listAll(prefix, T -> T.endsWith(extension));
}
public Map<ID, MultiAsset> multiRoot(IAssetProvider provider) {
return provider.listAll(prefix, T -> T.endsWith(extension) && T.isRootFolder(prefix));
}
}
@@ -3,11 +3,17 @@ package speiger.src.coreengine.assets.api;
import java.util.concurrent.CompletableFuture;
public interface IResourceListener {
void name();
CompletableFuture<Void> reload(ReloadContext context, ISyncer sync);
void release();
public static interface ISimpleResourceListener extends IResourceListener {
@Override
default CompletableFuture<Void> reload(ReloadContext context, ISyncer sync) { return CompletableFuture.runAsync(() -> reload(context), context.tasks()); }
void reload(ReloadContext context);
}
public static interface ISyncer {
<T> CompletableFuture<T> sync(T value);
}
}
@@ -3,7 +3,7 @@ package speiger.src.coreengine.assets.api;
import java.util.Set;
import java.util.concurrent.Executor;
public record ReloadContext(IAssetProvider provider, Set<ID> filter, Executor main, Executor background) {
public record ReloadContext(IAssetProvider provider, Set<ID> filter, Executor mainThread, Executor tasks, Executor background) {
public boolean hasChanged(ID id) {
return filter.isEmpty() || filter.contains(id);
@@ -0,0 +1,13 @@
package speiger.src.coreengine.assets.api;
import java.util.concurrent.CompletableFuture;
public abstract class SteppedResourceListener<T> implements IResourceListener {
@Override
public CompletableFuture<Void> reload(ReloadContext context, ISyncer sync) {
return CompletableFuture.supplyAsync(() -> prepare(context), context.tasks()).thenCompose(sync::sync).thenAccept(T -> apply(T, context));
}
protected abstract T prepare(ReloadContext context);
protected abstract void apply(T value, ReloadContext context);
}
@@ -76,7 +76,7 @@ public class AssetManager {
if(builder.listeners.isEmpty()) builder.withListener(tracker.listeners());
return new ReloadTask(get(), builder);
}
public boolean isReloadable() {
return manager != null;
}
@@ -16,6 +16,7 @@ import speiger.src.coreengine.assets.providers.SingleProvider;
public class ReloadBuilder {
Executor mainRunner = Runnable::run;
Executor taskRunner = Runnable::run;
Executor syncRunner = Runnable::run;
Set<ID> filter = ObjectSets.empty();
Optional<BaseProvider> packages = Optional.empty();
@@ -26,11 +27,16 @@ public class ReloadBuilder {
return this;
}
public ReloadBuilder withMain(Executor main) {
public ReloadBuilder withMainThread(Executor main) {
mainRunner = Objects.requireNonNull(main);
return this;
}
public ReloadBuilder withTasks(Executor tasks) {
taskRunner = Objects.requireNonNull(tasks);
return this;
}
public ReloadBuilder withSync(Executor sync) {
syncRunner = Objects.requireNonNull(sync);
return this;
@@ -13,13 +13,13 @@ import speiger.src.coreengine.assets.api.IResourceListener;
import speiger.src.coreengine.assets.api.IResourceListener.ISyncer;
import speiger.src.coreengine.assets.api.ReloadContext;
public class ReloadTask {
public class ReloadTask {
AtomicInteger started = new AtomicInteger();
AtomicInteger finished = new AtomicInteger();
CompletableFuture<?> future;
public ReloadTask(IAssetProvider provider, ReloadBuilder builder) {
future = reload(new ReloadContext(provider, builder.filter, new CountingExecutor(builder.mainRunner, started, finished), new CountingExecutor(builder.syncRunner, started, finished)), new ObjectArrayList<>(builder.listeners.get()));
future = reload(new ReloadContext(provider, builder.filter, new CountingExecutor(builder.mainRunner, started, finished), new CountingExecutor(builder.taskRunner, started, finished), new CountingExecutor(builder.syncRunner, started, finished)), new ObjectArrayList<>(builder.listeners.get()));
}
public int started() { return started.get(); }
@@ -36,14 +36,14 @@ public class ReloadTask {
List<CompletableFuture<Void>> tasks = new ObjectArrayList<>();
for(IResourceListener listener : listeners) {
CompletableFuture<Void> current = chainedTask;
chainedTask = listener.reload(context, new Syncer(context.main(), current, syncFuture, listener, todo));
chainedTask = listener.reload(context, new Syncer(context.tasks(), current, syncFuture, listener, todo));
tasks.add(chainedTask);
}
return CompletableFuture.allOf(tasks.toArray(CompletableFuture[]::new)).thenRunAsync(() -> {
finished.getAndIncrement();
try { context.provider().close(); }
catch(Exception e) { e.printStackTrace(); }
}, context.main());
}, context.tasks());
}
@@ -0,0 +1,39 @@
package speiger.src.coreengine.platform.graphics.api.buffer;
public class GrowableVertexBuffer
{
VertexBuffer buffer;
final int bytesPerIndex;
final int limit;
final int startSize;
int currentCapacity;
public GrowableVertexBuffer(VertexBuffer buffer, int bytesPerIndex, int limit, int startSize)
{
this.buffer = buffer;
this.bytesPerIndex = bytesPerIndex;
this.limit = limit;
this.startSize = startSize;
currentCapacity = startSize;
buffer.bind().allocate(startSize * bytesPerIndex).unbind();
}
public VertexBuffer ensureSize(int capacity)
{
if(capacity > currentCapacity)
{
if(currentCapacity == limit)
{
throw new RuntimeException("THIS SHOULD NEVER HAPPEN!: "+currentCapacity+":"+limit);
}
currentCapacity = Math.min(Math.max(currentCapacity + (currentCapacity / 2), capacity), limit);
buffer.bind().grow(currentCapacity * bytesPerIndex).unbind();
}
else if(capacity < currentCapacity / 2 && currentCapacity != 10)
{
currentCapacity = Math.max(10, capacity);
buffer.bind().shrink(currentCapacity * bytesPerIndex).unbind();
}
return buffer;
}
}
@@ -0,0 +1,75 @@
package speiger.src.coreengine.platform.graphics.api.buffer;
public class MultiVertexBuffer {
VertexBuffer buffer;
final int bytesPerIndex;
final int limit;
final int startSize;
final float growRate;
final int padding;
final int[][] offsets;
int currentCapacity;
public MultiVertexBuffer(VertexBuffer buffer, int bytesPerIndex, int limit, int startSize, int padding, float growRate, int[][] offsets) {
super();
this.buffer = buffer;
this.bytesPerIndex = bytesPerIndex;
this.limit = limit;
this.startSize = startSize;
this.padding = padding;
this.growRate = growRate;
this.offsets = offsets;
currentCapacity = startSize;
buffer.bind().allocate(startSize * bytesPerIndex).unbind();
}
public boolean ensureSize(int index, int capacity) {
int[] offset = offsets[index];
int room = offset[2] - offset[0];
if(capacity >= room) {
int newCap = Math.max((int)(room * growRate), capacity + 1);
offset[2] = offset[0] + newCap;
grow(index + 1, newCap - room);
return true;
}
return false;
}
protected void grow(int index, int extra) {
if(index == offsets.length) {
int newCap = offsets[offsets.length - 1][2] + extra;
if(newCap > currentCapacity) {
if(currentCapacity == limit) { throw new RuntimeException("THIS SHOULD NEVER HAPPEN!: "+currentCapacity+":"+limit); }
currentCapacity = Math.min(Math.max(currentCapacity + (currentCapacity / 2), newCap), limit);
buffer.bind().grow(currentCapacity * bytesPerIndex).unbind();
}
return;
}
int startBytes = offsets[index][0] * bytesPerIndex;
int endBytes = offsets[offsets.length - 1][2] * bytesPerIndex;
for(int i = index;i < offsets.length;i++) {
offsets[i][0] += extra;
offsets[i][2] += extra;
}
grow(offsets.length, extra);
buffer.bind().shift(startBytes, endBytes - startBytes, extra * bytesPerIndex).unbind();
}
protected void shrink(int index, int extra) {
if(index == offsets.length) {
int newCap = offsets[offsets.length - 1][2] - extra;
if(newCap < currentCapacity / 2 && currentCapacity != 10) {
currentCapacity = Math.max(10, newCap);
buffer.bind().shrink(currentCapacity * bytesPerIndex).unbind();
}
return;
}
int startBytes = offsets[index][0] * bytesPerIndex;
buffer.bind().shift(startBytes, (offsets[offsets.length - 1][2] * bytesPerIndex) - startBytes, -(extra * bytesPerIndex)).unbind();
for(int i = index;i < offsets.length;i++) {
offsets[i][0] -= extra;
offsets[i][2] -= extra;
}
shrink(offsets.length, extra);
}
}
@@ -17,6 +17,8 @@ import speiger.src.coreengine.platform.input.window.Window;
public interface GraphicsDevice {
public static final ScopedValue<GraphicsDevice> SCOPE = ScopedValue.newInstance();
public static GraphicsDevice get() { return SCOPE.get(); }
Window window();
GraphicsSurface createSurface();
GraphicsCommandBuffer createCommandBuffer(ExecutionType type);
@@ -10,6 +10,7 @@ import java.util.OptionalInt;
import java.util.function.IntPredicate;
import java.util.function.Supplier;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL20;
@@ -88,6 +89,7 @@ public class GLGraphicsDevice implements GraphicsDevice {
this.owner = owner;
this.manager = manager;
this.capabilities = capbilities;
GL.setCapabilities(capbilities);
this.queue = new GLCommandQueue(this);
}
@@ -0,0 +1,109 @@
package speiger.src.coreengine.ui.gui.font;
import java.util.Map;
import java.util.function.Consumer;
import speiger.src.coreengine.assets.api.ID;
import speiger.src.coreengine.platform.graphics.api.texture.Texture;
import speiger.src.coreengine.platform.graphics.api.vertex.builder.IVertexBuilder;
import speiger.src.coreengine.ui.gui.font.glyth.Glyth;
import speiger.src.coreengine.ui.gui.font.glyth.GlythData;
import speiger.src.coreengine.ui.gui.font.glyth.UnbakedGlyth.GlythBaker;
import speiger.src.coreengine.utils.helpers.TextUtil;
import speiger.src.coreengine.utils.misc.CodepointSequence;
public class Font {
public static final int FORMAT_CODE_POINT = '§';
FontSplitter splitter;
Map<ID, FontGroup> fonts;
GlythBaker baker;
float oversample;
GlythCache[] styledCache = new GlythCache[] {new GlythCache(this, 0), new GlythCache(this, 1), new GlythCache(this, 2), new GlythCache(this, 3)};
protected Font(Map<ID, FontGroup> fonts, GlythBaker baker, float oversample, Consumer<Runnable> clearing) {
this.fonts = fonts;
this.baker = baker;
this.oversample = oversample;
clearing.accept(this::reset);
splitter = new FontSplitter(this);
}
private void reset() {
for(int i = 0;i<4;i++) {
styledCache[i].reset();
}
}
public GlythData data(FontStyle font, int codepoint) {
return styledCache[font.style() & 0x3].data(font, codepoint);
}
public Glyth glyth(FontStyle font, int codepoint) {
return styledCache[font.style() & 0x3].glyth(font, codepoint);
}
public float width(FontStyle font, int codepoint) {
return styledCache[font.style() & 0x3].data(font, codepoint).advance();
}
protected FontGroup font(ID font) {
return fonts.get(font);
}
public float drawText(String text, float x, float y, int color, TexturedBuffer buffer, boolean end) {
return drawText(TextStyle.DEFAULT, text, x, y, color, buffer, 1F, end);
}
public float drawText(TextStyle style, String text, float x, float y, int color, TexturedBuffer buffer, float scale, boolean end) {
TextStyle currentStyle = style;
int currentColor = currentStyle.hasColor() ? currentStyle.color() : color;
float xStart = x;
int previousCodepoint = -1;
for(int i = 0,m=text.length();i<m;) {
int codepoint = text.codePointAt(i);
CodepointSequence result;
if(codepoint == FORMAT_CODE_POINT && (result = findValue(CodepointSequence.of(text, i))) != null) {
currentStyle = currentStyle.parseArguments(style, result.toString().split(","));
currentColor = currentStyle.hasColor() ? currentStyle.color() : color;
i += result.length()+3;
}
GlythData data = data(currentStyle.font(), codepoint);
if(previousCodepoint != -1) {
x -= data.kerning(previousCodepoint);
}
Glyth glyth = glyth(currentStyle.font(), codepoint);
if(glyth.isValid()) {
float minX = (glyth.left() + x) * scale;
float minY = (glyth.top() + y) * scale;
float maxX = (glyth.right() + x) * scale;
float maxY = (glyth.bottom() + y) * scale;
IVertexBuilder builder = buffer.builderForTexture(glyth.texture());
builder.pos(minX, minY, 0F).tex(glyth.minU(), glyth.minV()).rgba(currentColor);
builder.pos(maxX, minY, 0F).tex(glyth.maxU(), glyth.minV()).rgba(currentColor);
builder.pos(maxX, maxY, 0F).tex(glyth.maxU(), glyth.maxV()).rgba(currentColor);
builder.pos(maxX, maxY, 0F).tex(glyth.maxU(), glyth.maxV()).rgba(currentColor);
builder.pos(minX, maxY, 0F).tex(glyth.minU(), glyth.maxV()).rgba(currentColor);
builder.pos(minX, minY, 0F).tex(glyth.minU(), glyth.minV()).rgba(currentColor);
}
x += data.advance();
i += Character.charCount(codepoint);
previousCodepoint = codepoint;
}
if(end) buffer.end();
return x - xStart;
}
static CodepointSequence findValue(CodepointSequence input) {
if(input.length() < 3 || input.codePointAt(0) != FORMAT_CODE_POINT || input.codePointAt(1) != '<') return null;
int end = TextUtil.findEnd(input, "§<", ">");
if(end == -1) return null;
return input.subSequence(2, end-1);
}
public static interface TexturedBuffer {
public IVertexBuilder builderForTexture(Texture texture);
public void end();
}
}
@@ -0,0 +1,30 @@
package speiger.src.coreengine.ui.gui.font;
import java.util.List;
import speiger.src.coreengine.assets.api.ID;
import speiger.src.coreengine.ui.gui.font.glyth.UnbakedGlyth;
import speiger.src.coreengine.ui.gui.font.providers.IFontProvider;
public class FontGroup {
ID locations;
List<IFontProvider> providers;
public FontGroup(ID locations, List<IFontProvider> providers) {
this.locations = locations;
this.providers = providers;
}
public UnbakedGlyth data(int codepoint, int style, float size, float oversample) {
for(int i = 0,m=providers.size();i<m;i++) {
UnbakedGlyth data = providers.get(i).glythData(codepoint, style, size, oversample);
if(data != null) return data;
}
return null;
}
public void close() {
providers.forEach(IFontProvider::close);
providers.clear();
}
}
@@ -0,0 +1,118 @@
package speiger.src.coreengine.ui.gui.font;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
import com.google.gson.JsonObject;
import speiger.src.collections.floats.maps.interfaces.Float2ObjectMap;
import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.collections.objects.lists.ObjectList;
import speiger.src.collections.objects.maps.interfaces.Object2ObjectMap;
import speiger.src.coreengine.assets.api.AssetFilter;
import speiger.src.coreengine.assets.api.IAssetProvider;
import speiger.src.coreengine.assets.api.ID;
import speiger.src.coreengine.assets.api.MultiAsset;
import speiger.src.coreengine.assets.api.ReloadContext;
import speiger.src.coreengine.assets.api.SteppedResourceListener;
import speiger.src.coreengine.assets.impl.parsers.Serialized;
import speiger.src.coreengine.ui.gui.font.glyth.Glyth;
import speiger.src.coreengine.ui.gui.font.glyth.IGlythSheetInfo;
import speiger.src.coreengine.ui.gui.font.providers.FreeTypeProvider;
import speiger.src.coreengine.ui.gui.font.providers.IFontProvider;
import speiger.src.coreengine.ui.gui.font.providers.STBTrueTypeProvider;
import speiger.src.coreengine.utils.helpers.JsonUtil;
public class FontManager extends SteppedResourceListener<Map<ID, ObjectList<IFontProvider>>> {
private static final AssetFilter FILTER = AssetFilter.json("font");
Float2ObjectMap<Font> cachedFonts = Float2ObjectMap.builder().map();
Map<ID, FontGroup> fonts = Object2ObjectMap.builder().linkedMap();
Map<String, BiFunction<JsonObject, IAssetProvider, IFontProvider>> fontParsers = Object2ObjectMap.builder().map();
List<Runnable> listeners = new ObjectArrayList<>();
List<FontTexture> textures = new ObjectArrayList<>();
public FontManager() {
registerParser("stb-ttf", STBTrueTypeProvider::create);
registerParser("free-ttf", FreeTypeProvider::load);
}
public void registerParser(String id, BiFunction<JsonObject, IAssetProvider, IFontProvider> parser) {
fontParsers.putIfAbsent(id, parser);
}
@Override
protected Map<ID, ObjectList<IFontProvider>> prepare(ReloadContext context) {
Map<ID, IFontProvider> loadingCache = Object2ObjectMap.builder().linkedMap();
Object2ObjectMap<ID, ObjectList<IFontProvider>> providers = Object2ObjectMap.builder().linkedMap();
for(Entry<ID, MultiAsset> entry : FILTER.multiRoot(context.provider()).entrySet()) {
ID id = entry.getKey();
for(JsonObject obj : entry.getValue().map(Serialized.JSON_OBJ, JsonObject::new)) {
JsonUtil.iterateValues(obj.get("providers"), T -> {
ID location = ID.tryOf(T.getAsString());
if(location == null || location.isRoot()) return;
IFontProvider font = loadingCache.computeIfAbsent(location.prefix("font"), E -> getFont(E, context.provider()));
if(font == null) return;
providers.supplyIfAbsent(FILTER.id(id), ObjectArrayList::new).add(font);
});
}
}
return providers;
}
@Override
protected void apply(Map<ID, ObjectList<IFontProvider>> value, ReloadContext context) {
reset(context.mainThread());
value.forEach((K, V) -> fonts.put(K, new FontGroup(K, V)));
}
public void destroy() {
reset(Runnable::run);
listeners.clear();
}
private void reset(Executor execute) {
listeners.forEach(Runnable::run);
execute.execute(() -> {
textures.forEach(FontTexture::delete);
textures.clear();
});
fonts.values().forEach(FontGroup::close);
fonts.clear();
cachedFonts.clear();
}
public Font createFont() {
return createFont(1F);
}
public Font createFont(float oversample) {
return cachedFonts.computeIfAbsent(oversample, T -> new Font(fonts, this::stitch, T, listeners::add));
}
private Glyth stitch(IGlythSheetInfo info) {
for(int i = 0,m=textures.size();i<m;i++) {
Glyth glyth = textures.get(i).build(info);
if(glyth != null) return glyth;
}
FontTexture texture = new FontTexture(512, info.isColored());
textures.add(texture);
Glyth glyth = texture.build(info);
return glyth;
}
private IFontProvider getFont(ID location, IAssetProvider provider) {
try {
JsonObject obj = provider.get(location).jsonObj();
BiFunction<JsonObject, IAssetProvider, IFontProvider> builder = fontParsers.get(obj.get("type").getAsString());
return builder == null ? null : builder.apply(obj, provider);
}
catch(Exception e) {
e.printStackTrace();
}
return null;
}
}
@@ -0,0 +1,106 @@
package speiger.src.coreengine.ui.gui.font;
import speiger.src.coreengine.utils.misc.CodepointSequence;
public class FontSplitter {
Font font;
public FontSplitter(Font font) {
this.font = font;
}
public float width(String text) {
return width(TextStyle.DEFAULT, text, true);
}
public float width(TextStyle style, String text) {
return width(style, text, true);
}
public float width(TextStyle style, String text, boolean applyStyleChanges) {
TextStyle currentStyle = style;
float total = 0F;
for(int i = 0,m=text.length();i<m;i++) {
int codepoint = text.codePointAt(i);
CodepointSequence result;
if(codepoint == Font.FORMAT_CODE_POINT && applyStyleChanges && (result = Font.findValue(CodepointSequence.of(text, i))) != null) {
currentStyle = currentStyle.parseArguments(style, result.toString().split(","));
i += result.length()+3;
continue;
}
total += font.width(currentStyle.font(), codepoint);
}
return total;
}
public String[] split(TextStyle style, String text, boolean applyTextStyles) {
return null;
}
public static String removeStyleChanges(String input) {
StringBuilder builder = new StringBuilder(input);
for(int i = 0;i<builder.length();i++) {
int codepoint = builder.codePointAt(i);
CodepointSequence result;
if(codepoint == Font.FORMAT_CODE_POINT && (result = Font.findValue(CodepointSequence.of(builder, i))) != null) {
builder.delete(i, (i--)+result.length()+3);
}
}
return builder.toString();
}
public float[] countWidths(TextStyle style, String text) {
float[] widths = new float[text.length()];
TextStyle current = style;
for(int i = 0,m=text.length();i<m;i++) {
int codepoint = text.codePointAt(i);
CodepointSequence result;
if(codepoint == Font.FORMAT_CODE_POINT && (result = Font.findValue(CodepointSequence.of(text, i))) != null) {
style = current.parseArguments(style, result.toString());
i += result.length()+3;
continue;
}
float width = font.width(current.font(), codepoint);
widths[i] = width;
}
return widths;
}
public String splitTailByWidth(TextStyle style, String text, float maxWidth, boolean applyStyleChanges) {
if(applyStyleChanges) {
float[] widths = countWidths(style, text);
float totalWidth = 0;
for(int i = text.length()-1;i>=0;i--) {
totalWidth += widths[i];
if(totalWidth >= maxWidth) return text.substring(i, text.length());
}
}
else {
float totalWidth = 0F;
for(int i = text.length()-1;i>=0;i--) {
int codepoint = text.codePointAt(i);
totalWidth += font.width(style.font(), codepoint);
if(totalWidth >= maxWidth) return text.substring(i, text.length());
}
}
return text;
}
public String splitHeadByWidth(TextStyle style, String text, float maxWidth, boolean applyStyleChanges) {
TextStyle current = style;
float width = 0F;
for(int i = 0,m=text.length();i<m;i++) {
int codepoint = text.codePointAt(i);
CodepointSequence result;
if(codepoint == Font.FORMAT_CODE_POINT && applyStyleChanges && (result = Font.findValue(CodepointSequence.of(text, i))) != null) {
style = current.parseArguments(style, result.toString());
i += result.length()+3;
continue;
}
width += font.width(current.font(), codepoint);
if(width >= maxWidth) return text.substring(0, i);
}
return text;
}
}
@@ -0,0 +1,59 @@
package speiger.src.coreengine.ui.gui.font;
import speiger.src.coreengine.assets.api.ID;
public record FontStyle(ID font, int style, float size) {
public static final FontStyle DEFAULT = new FontStyle(ID.of("default"), 0, 16F);
public static final int REGULAR = 0;
public static final int BOLD = 1;
public static final int ITALIC = 2;
public static final int BOLD_ITCALIC = 3;
public FontStyle {
if(style < 0 || style > 3) throw new IllegalArgumentException("Style can only be between 0-3. Style found: "+style);
}
public FontStyle(ID font, float size) {
this(font, REGULAR, size);
}
public FontStyle(ID font, boolean bold, boolean italic, float size) {
this(font, (bold ? BOLD : 0) | (italic ? ITALIC : 0), size);
}
public FontStyle with(ID font, int style, float size) {
return new FontStyle(font, style, size);
}
public FontStyle with(ID font, float size) {
return new FontStyle(font, style, size);
}
public FontStyle with(ID font) {
return new FontStyle(font, style, size);
}
public FontStyle with(float size) {
return new FontStyle(font, style, size);
}
public FontStyle with(int style) {
return new FontStyle(font, style, size);
}
public FontStyle withBold(boolean bold) {
return new FontStyle(font, bold ? (style | BOLD) : (style & ~BOLD), size);
}
public FontStyle withItalic(boolean italic) {
return new FontStyle(font, italic ? (style | ITALIC) : (style & ~ITALIC), size);
}
public boolean bold() {
return (style() & BOLD) != 0;
}
public boolean italic() {
return (style() & ITALIC) != 0;
}
}
@@ -0,0 +1,107 @@
package speiger.src.coreengine.ui.gui.font;
import speiger.src.coreengine.math.MathUtils;
import speiger.src.coreengine.platform.graphics.api.core.GraphicsDevice;
import speiger.src.coreengine.platform.graphics.api.texture.Texture;
import speiger.src.coreengine.platform.graphics.api.texture.TextureSettings;
import speiger.src.coreengine.platform.graphics.api.texture.states.SwizzleMask;
import speiger.src.coreengine.platform.graphics.api.texture.states.TextureFormat;
import speiger.src.coreengine.ui.gui.font.glyth.Glyth;
import speiger.src.coreengine.ui.gui.font.glyth.IGlythSheetInfo;
public class FontTexture {
private static final TextureSettings COLOR = TextureSettings.builder().build();
private static final TextureSettings NO_COLOR = TextureSettings.builder()
.format(TextureFormat.LUMINANCE)
.swizzle(SwizzleMask.RED, SwizzleMask.RED, SwizzleMask.RED, SwizzleMask.RED)
.build();
Texture texture;
boolean color;
int bounds;
Slot slot;
public FontTexture(int bounds, boolean color) {
this.texture = GraphicsDevice.get().createTexture(color ? COLOR : NO_COLOR, bounds, bounds);
this.bounds = bounds;
this.color = color;
this.slot = new Slot(0, 0, bounds, bounds);
}
public static TextureFormat formatByColor(boolean colored) {
return colored ? TextureFormat.RGBA : TextureFormat.R;
}
public Glyth build(IGlythSheetInfo info) {
if(color != info.isColored()) return null;
Slot result = slot.insert(info);
if(result != null) {
info.upload(texture, result.x, result.y);
float minU = (float)(result.x) / (float)bounds;
float maxU = (float)((result.x) + info.width()) / (float)bounds;
float minV = (float)(result.y) / (float)bounds;
float maxV = (float)((result.y) + info.height()) / (float)bounds;
return new Glyth(texture, minU, minV, maxU, maxV, info.left(), info.right(), info.top(), info.bottom());
}
return null;
}
public void delete() {
texture.remove();
}
public boolean colored() { return color; }
public static class Slot {
final int x;
final int y;
final int width;
final int height;
Slot[] children = null;
boolean occupied;
public Slot(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public Slot insert(IGlythSheetInfo info) {
if(children == null) {
if(occupied) return null;
int iw = info.width();
int ih = info.height();
if(iw > width || ih > height) return null;
if(iw == width && ih == height) {
occupied = true;
return this;
}
int dw = width - iw;
int dh = height - ih;
children = new Slot[2];
if(dw > dh) {
int offset = MathUtils.ceil(iw / 50F);
children[0] = new Slot(x, y, iw, height);
children[1] = new Slot(x + iw + offset, y, dw - offset, height);
}
else {
int offset = MathUtils.ceil(ih / 50F);
children[0] = new Slot(x, y, width, ih);
children[1] = new Slot(x, y + ih + offset, width, dh - offset);
}
return children[0].insert(info);
}
for(int i = 0,m=children.length;i<m;i++) {
Slot slot = children[i].insert(info);
if(slot != null) return slot;
}
return null;
}
@Override
public String toString() {
return "Slot[x="+x+", y="+y+", w="+width+", h="+height+"]";
}
}
}
@@ -0,0 +1,77 @@
package speiger.src.coreengine.ui.gui.font;
import java.util.Map;
import speiger.src.collections.ints.maps.impl.hash.Int2ObjectOpenHashMap;
import speiger.src.collections.ints.maps.interfaces.Int2ObjectMap;
import speiger.src.collections.objects.maps.impl.hash.Object2ObjectOpenHashMap;
import speiger.src.coreengine.ui.gui.font.glyth.Glyth;
import speiger.src.coreengine.ui.gui.font.glyth.GlythData;
import speiger.src.coreengine.ui.gui.font.glyth.MissingGlyth;
import speiger.src.coreengine.ui.gui.font.glyth.UnbakedGlyth;
public class GlythCache {
private Font font;
final int style;
Map<FontStyle, FontCache> cache = new Object2ObjectOpenHashMap<>();
public GlythCache(Font font, int style) {
this.font = font;
this.style = style;
}
public void reset() {
cache.values().forEach(FontCache::reset);
cache.clear();
}
private FontCache cache(FontStyle font) {
return cache.computeIfAbsent(font, FontCache::new);
}
public GlythData data(FontStyle fontId, int codepoint) {
return cache(fontId).data(codepoint);
}
public Glyth glyth(FontStyle fontId, int codepoint) {
return cache(fontId).glyth(codepoint);
}
public class FontCache {
Int2ObjectMap<GlythData> data = new Int2ObjectOpenHashMap<>();
Int2ObjectMap<Glyth> glyth = new Int2ObjectOpenHashMap<>();
FontStyle fontStyle;
MissingGlyth missingGlyth;
GlythData missingData;
public FontCache(FontStyle fontStyle) {
this.fontStyle = fontStyle;
this.missingGlyth = new MissingGlyth(fontStyle.size(), font.oversample * 2F);
missingData = new GlythData(missingGlyth);
}
private void reset() {
missingGlyth.cleanCache();
missingData.cleanCache();
}
public GlythData data(int codepoint) {
return data.computeIfAbsent(codepoint, this::compute);
}
public Glyth glyth(int codepoint) {
return glyth.computeIfAbsent(codepoint, this::bake);
}
private GlythData compute(int codepoint) {
UnbakedGlyth data = font.font(fontStyle.font()).data(codepoint, style, fontStyle.size(), font.oversample);
return data == null ? missingData : new GlythData(data);
}
private Glyth bake(int codepoint) {
return data(codepoint).unbaked().bake(font.baker);
}
}
}
@@ -0,0 +1,140 @@
package speiger.src.coreengine.ui.gui.font;
import speiger.src.coreengine.assets.api.ID;
import speiger.src.coreengine.math.color.ColorUtils;
import speiger.src.coreengine.utils.helpers.TextUtil;
public record TextStyle(FontStyle font, int styleFlags, int color) {
public static final TextStyle DEFAULT = new TextStyle(FontStyle.DEFAULT);
public static final int NONE = 0;
public static final int UNDERLINE = 1;
public static final int STRIKETHROUGH = 2;
public static final int HAS_COLOR = 4;
public TextStyle(FontStyle font) {
this(font, 0, 0xFFFFFFFF);
}
public TextStyle(FontStyle font, int color) {
this(font, 0, color);
}
public TextStyle color(int color) {
return new TextStyle(font, styleFlags | HAS_COLOR, color);
}
public TextStyle removeColor() {
return new TextStyle(font, styleFlags & ~HAS_COLOR, color);
}
public TextStyle font(FontStyle font) {
return new TextStyle(font, styleFlags, color);
}
public TextStyle font(ID location) {
return new TextStyle(font.with(location), styleFlags, color);
}
public TextStyle size(float size) {
return new TextStyle(font.with(size), styleFlags, color);
}
public TextStyle bold(boolean value) {
return new TextStyle(font.withBold(value), styleFlags, color);
}
public TextStyle italic(boolean value) {
return new TextStyle(font.withItalic(value), styleFlags, color);
}
public TextStyle underline(boolean value) {
return new TextStyle(font, value ? styleFlags | UNDERLINE : styleFlags & ~UNDERLINE, color);
}
public TextStyle strikethrough(boolean value) {
return new TextStyle(font, value ? styleFlags | STRIKETHROUGH : styleFlags & ~STRIKETHROUGH, color);
}
public TextStyle parseArguments(TextStyle original, String... args) {
if(args == null || args.length <= 0) return this;
ID font = font().font();
boolean bold = font().bold();
boolean italic = font().italic();
boolean underline = underline();
boolean strikethrough = strikethrough();
int color = color();
boolean hasColor = hasColor();
float size = font().size();
for(int i = 0,m=args.length;i<m;i++) {
String entry = args[i].trim();
if(entry.length() < 3 || entry.charAt(1) != '=') continue;
String data = entry.substring(2);
if(data.length() <= 0) continue;
switch(Character.toLowerCase(entry.charAt(0))) {
case 'f':
if(data.length() == 1 && data.charAt(0) == 'r') font = original.font().font();
else {
ID newFont = ID.tryOf(data);
if(newFont != null) font = newFont;
}
break;
case 'c':
if(data.length() == 1 && data.charAt(0) == 'r') {
color = original.color();
hasColor = original.hasColor();
}
else if(data.length() == 1 && data.charAt(0) == 'd') {
color = -1;
hasColor = false;
}
else {
color = ColorUtils.parse(data, color);
hasColor = true;
}
break;
case 's': {
if(data.length() == 1 && data.charAt(0) == 'r') size = original.font().size();
else size = TextUtil.parseFloat(data, size);
break;
}
case 'b':
bold = TextUtil.parseBoolean(data, bold, original.bold());
break;
case 'i':
italic = TextUtil.parseBoolean(data, italic, original.italic());
break;
case 'u':
underline = TextUtil.parseBoolean(data, underline, original.underline());
break;
case 'l':
strikethrough = TextUtil.parseBoolean(data, strikethrough, original.strikethrough());
break;
}
}
if(color != color() || hasColor != hasColor() || bold != bold() || italic != italic() || underline != underline() || strikethrough != strikethrough() || font != font().font() || size != size) {
return new TextStyle(font().with(font, (bold ? FontStyle.BOLD : FontStyle.REGULAR) | (italic ? FontStyle.ITALIC : FontStyle.REGULAR), size), (underline ? UNDERLINE : NONE) | (strikethrough ? STRIKETHROUGH : NONE) | (hasColor ? HAS_COLOR : NONE), color);
}
return this;
}
public boolean hasColor() {
return (styleFlags & HAS_COLOR) != 0;
}
public boolean bold() {
return font.bold();
}
public boolean italic() {
return font.italic();
}
public boolean underline() {
return (styleFlags & UNDERLINE) != 0;
}
public boolean strikethrough() {
return (styleFlags & STRIKETHROUGH) != 0;
}
}
@@ -0,0 +1,17 @@
package speiger.src.coreengine.ui.gui.font.glyth;
import speiger.src.coreengine.platform.graphics.api.texture.Texture;
public record Glyth(Texture texture, float minU, float minV, float maxU, float maxV, float left, float right, float top, float bottom) {
public static final Glyth EMPTY = new Glyth();
private Glyth() {
this(null, 0F, 0F, 0F, 0F, 0F, 0F, 0F, 0F);
}
public Glyth(Texture texture, float minU, float minV, float maxU, float maxV) {
this(texture, minU, minV, maxU, maxV, 0, 0, 0, 0);
}
public boolean isValid() { return texture != null; }
}
@@ -0,0 +1,19 @@
package speiger.src.coreengine.ui.gui.font.glyth;
import speiger.src.collections.ints.maps.impl.hash.Int2FloatOpenHashMap;
import speiger.src.collections.ints.maps.interfaces.Int2FloatMap;
public class GlythData {
UnbakedGlyth data;
Int2FloatMap kernings = new Int2FloatOpenHashMap();
public GlythData(UnbakedGlyth data) {
this.data = data;
}
public float advance() { return data.advance(); }
public float shadowOffset() { return data.shadowOffset(); }
public float kerning(int codepoint) { return kernings.computeFloatIfAbsent(codepoint, data::kerning); }
public UnbakedGlyth unbaked() { return data; }
public void cleanCache() { kernings.clear(); }
}
@@ -0,0 +1,19 @@
package speiger.src.coreengine.ui.gui.font.glyth;
import speiger.src.coreengine.platform.graphics.api.texture.Texture;
public interface IGlythSheetInfo {
public int width();
public int height();
public default float xOffset() { return 0F; }
public default float yOffset() { return 3F; }
public float oversample();
public boolean isColored();
public void upload(Texture texture, int x, int y);
public default float left() { return xOffset(); }
public default float right() { return xOffset() + width() / oversample(); }
public default float top() { return yOffset(); }
public default float bottom() { return yOffset() + height() / oversample(); }
}
@@ -0,0 +1,70 @@
package speiger.src.coreengine.ui.gui.font.glyth;
import speiger.src.coreengine.math.MathUtils;
import speiger.src.coreengine.platform.graphics.api.core.GraphicsDevice;
import speiger.src.coreengine.platform.graphics.api.texture.Texture;
import speiger.src.coreengine.platform.graphics.api.texture.drawable.Drawable;
import speiger.src.coreengine.ui.gui.font.FontTexture;
public class MissingGlyth implements UnbakedGlyth {
private static final int WIDTH = 15;
private static final int HEIGHT = 28;
float size;
float oversample;
float advance;
float scale;
int width;
int height;
Glyth cached;
public MissingGlyth(float size, float oversample) {
this.size = size;
this.oversample = oversample;
this.scale = (size * oversample) / HEIGHT;
this.advance = WIDTH * (size / HEIGHT);
this.width = (int)(WIDTH * scale);
this.height = (int)(HEIGHT * scale * 0.5F);
}
public void cleanCache() {
cached = null;
}
@Override
public float advance() { return advance; }
@Override
public float kerning(int codepoint) { return 0; }
@Override
public Glyth bake(GlythBaker baker) {
if(cached == null) {
cached = baker.bake(new IGlythSheetInfo() {
@Override
public int width() { return width; }
@Override
public int height() { return height; }
@Override
public float yOffset() {
return -height * 0.5F;
}
@Override
public void upload(Texture texture, int x, int y) {
Drawable drawable = new Drawable(FontTexture.formatByColor(false), width, (int)(height));
draw(drawable);
drawable.upload(GraphicsDevice.get().queue(), texture, x, y, 0, 0, width, height);
drawable.close();
}
@Override
public boolean isColored() { return false; }
@Override
public float oversample() { return oversample; }
});
}
return cached;
}
private void draw(Drawable texture) {
texture.fill(0, 0, width, height, 255);
int offset = MathUtils.ceil(width / 5F);
texture.fill(offset, offset, width - offset * 2, height - offset * 2, 0);
}
}
@@ -0,0 +1,20 @@
package speiger.src.coreengine.ui.gui.font.glyth;
public interface UnbakedGlyth {
public float advance();
public float kerning(int codepoint);
public default float shadowOffset() { return 1F; }
public Glyth bake(GlythBaker baker);
public static record EmptyGlythData(float advance) implements UnbakedGlyth {
@Override
public Glyth bake(GlythBaker baker) { return Glyth.EMPTY; }
@Override
public float kerning(int codepoint) { return 0F; }
}
public static interface GlythBaker {
Glyth bake(IGlythSheetInfo info);
}
}
@@ -0,0 +1,298 @@
package speiger.src.coreengine.ui.gui.font.providers;
import java.nio.ByteBuffer;
import org.lwjgl.PointerBuffer;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import org.lwjgl.util.freetype.FT_Bitmap;
import org.lwjgl.util.freetype.FT_Face;
import org.lwjgl.util.freetype.FT_GlyphSlot;
import org.lwjgl.util.freetype.FT_Vector;
import org.lwjgl.util.freetype.FreeType;
import org.lwjgl.util.harfbuzz.HarfBuzz;
import org.lwjgl.util.harfbuzz.hb_glyph_position_t;
import com.google.gson.JsonObject;
import speiger.src.collections.ints.sets.IntOpenHashSet;
import speiger.src.collections.ints.sets.IntSet;
import speiger.src.collections.longs.misc.pairs.LongObjectPair;
import speiger.src.coreengine.assets.api.IAssetProvider;
import speiger.src.coreengine.assets.api.ID;
import speiger.src.coreengine.assets.impl.parsers.Buffers;
import speiger.src.coreengine.platform.graphics.api.core.GraphicsDevice;
import speiger.src.coreengine.platform.graphics.api.texture.Texture;
import speiger.src.coreengine.platform.graphics.api.texture.drawable.Drawable;
import speiger.src.coreengine.ui.gui.font.FontTexture;
import speiger.src.coreengine.ui.gui.font.glyth.Glyth;
import speiger.src.coreengine.ui.gui.font.glyth.IGlythSheetInfo;
import speiger.src.coreengine.ui.gui.font.glyth.UnbakedGlyth;
import speiger.src.coreengine.ui.gui.font.glyth.UnbakedGlyth.EmptyGlythData;
import speiger.src.coreengine.utils.helpers.JsonUtil;
public class FreeTypeProvider implements IFontProvider {
FreeTypeInstance[] instance;
public FreeTypeProvider(FreeTypeInstance[] instance) {
this.instance = instance;
}
public static IFontProvider create(ID location, IAssetProvider provider) {
try {
return load(provider.get(location).jsonObj(), provider);
}
catch(Exception e) { e.printStackTrace(); }
return null;
}
public static IFontProvider load(JsonObject data, IAssetProvider provider) {
long library = FreeTypeLibrary.get();
if(library == 0L) return null;
FreeTypeInstance[] instances = new FreeTypeInstance[4];
instances[0] = create(library, 0, data.getAsJsonObject("regular"), provider);
if(instances[0] == null) return null;
instances[1] = create(library, 1, data.getAsJsonObject("bold"), provider);
instances[2] = create(library, 2, data.getAsJsonObject("italic"), provider);
instances[3] = create(library, 3, data.getAsJsonObject("bold_italic"), provider);
return new FreeTypeProvider(instances);
}
private static FreeTypeInstance create(long library, int style, JsonObject obj, IAssetProvider provider) {
if(obj == null || !obj.has("file")) return null;
ID location = ID.of(obj.get("file").getAsString());
float oversample = JsonUtil.getOrDefault(obj, "oversample", 1F);
float shadowOffset = JsonUtil.getOrDefault(obj, "shadowOffset", 1F);
float xOff = 0;
float yOff = 0;
StringBuilder builder = new StringBuilder();
JsonObject shift = obj.getAsJsonObject("offset");
if(shift != null) {
xOff = JsonUtil.getOrDefault(shift, "x", 0F);
yOff = JsonUtil.getOrDefault(shift, "y", 0F);
}
JsonUtil.iterateValues(obj.get("skip"), T -> builder.append(T.getAsString()));
LongObjectPair<FT_Face> value = parse(location, provider, library);
if(value == null) return null;
return new FreeTypeInstance(style, value.getLongKey(), value.getValue(), oversample, xOff, yOff, shadowOffset, builder.toString());
}
private static LongObjectPair<FT_Face> parse(ID location, IAssetProvider provider, long library) {
try(MemoryStack stack = MemoryStack.stackPush()) {
ByteBuffer buffer = provider.get(location).parse(Buffers.DIRECT);
PointerBuffer facePointer = stack.mallocPointer(1);
if(FreeTypeLibrary.parseError(FreeType.FT_New_Memory_Face(library, buffer, 0L, facePointer), "Creating Font Face")) {
MemoryUtil.memFree(buffer);
return null;
}
FT_Face face = FT_Face.create(facePointer.get());
String s = FreeType.FT_Get_Font_Format(face);
if(!"TrueType".equals(s)) {
MemoryUtil.memFree(buffer);
throw new IllegalStateException("Font type ["+s+"] is not true type");
}
if(FreeTypeLibrary.parseError(FreeType.FT_Select_Charmap(face, FreeType.FT_ENCODING_UNICODE), "Applying Unicode Encoding")) {
MemoryUtil.memFree(buffer);
return null;
}
return LongObjectPair.of(MemoryUtil.memAddress(buffer), face);
}
catch(Exception exception) {
exception.printStackTrace();
return null;
}
}
@Override
public UnbakedGlyth glythData(int codepoint, int style, float size, float oversample) {
FreeTypeInstance instance = this.instance[style & 0x3];
return instance != null ? instance.gylthData(codepoint, size, oversample) : null;
}
@Override
public void close() {
for(int i = 0;i<4;i++) {
if(instance[i] != null) {
instance[i].free();
}
}
instance = null;
}
public static class FreeTypeGlyth implements UnbakedGlyth {
final FT_Face face;
final int width;
final int height;
final float xOff;
final float yOff;
final float oversample;
private final float advance;
final int glyth;
final int glythCodepoint;
final long harfBuzzFont;
public FreeTypeGlyth(FT_Face face, long harfBuzzFont, float xOff, float yOff, int width, int height, float advance, float oversample, int glyth, int glythCodepoint) {
this.face = face;
this.harfBuzzFont = harfBuzzFont;
this.width = width;
this.height = height;
this.oversample = oversample;
this.advance = advance / oversample;
this.xOff = xOff / oversample;
this.yOff = yOff / oversample;
this.glyth = glyth;
this.glythCodepoint = glythCodepoint;
}
@Override
public float advance() {
return advance;
}
@Override
public float kerning(int codepoint) {
int index = FreeType.FT_Get_Char_Index(face, codepoint);
if(index == 0) return 0;
StringBuilder builder = new StringBuilder();
builder.append(Character.toChars(codepoint));
builder.append(Character.toChars(glythCodepoint));
float hbresult = (getAdvance(Character.toString(codepoint)) - getAdvance(builder.toString())) / 64F;
return hbresult;
}
private float getAdvance(String adv) {
long id = HarfBuzz.hb_buffer_create();
try {
HarfBuzz.hb_buffer_add_utf8(id, adv, 0, adv.length());
HarfBuzz.hb_buffer_guess_segment_properties(id);
HarfBuzz.hb_shape(harfBuzzFont, id, null);
hb_glyph_position_t.Buffer positions = HarfBuzz.hb_buffer_get_glyph_positions(id);
float result = positions.hasRemaining() ? positions.x_advance() : 0F;
HarfBuzz.hb_buffer_destroy(positions.address());
return result;
}
finally {
HarfBuzz.hb_buffer_destroy(id);
}
}
@Override
public Glyth bake(GlythBaker baker) {
return baker.bake(new IGlythSheetInfo() {
@Override
public float xOffset() { return xOff; }
@Override
public float yOffset() { return yOff; }
@Override
public int width() { return width; }
@Override
public int height() { return height; }
@Override
public float oversample() { return oversample; }
@Override
public void upload(Texture texture, int x, int y) {
Drawable drawable = new Drawable(FontTexture.formatByColor(false), width, height);
if(drawable.drawFont(face, glyth)) {
drawable.upload(GraphicsDevice.get().queue(), texture, x, y, 0, 0, width, height);
}
drawable.close();
}
@Override
public boolean isColored() { return false; }
});
}
}
public static class FreeTypeInstance {
final int style;
long data;
final FT_Face face;
IntSet skip = new IntOpenHashSet();
final float oversample;
final float xOff;
final float yOff;
final float shadowOffset;
final long harfBuzzFont;
public FreeTypeInstance(int style, long data, FT_Face face, float oversample, float xOff, float yOff, float shadowOffset, String skip) {
this.style = style;
this.data = data;
this.face = face;
this.harfBuzzFont = HarfBuzz.hb_ft_font_create_referenced(face.address());
skip.codePoints().forEach(this.skip::add);
this.oversample = oversample;
this.xOff = xOff;
this.yOff = yOff;
this.shadowOffset = shadowOffset;
try(MemoryStack stack = MemoryStack.stackPush()) {
FT_Vector ft_vector = FT_Vector.malloc(stack).set(Math.round(oversample * xOff * 64F), Math.round(oversample * -yOff * 64F));
FreeType.FT_Set_Transform(face, null, ft_vector);
}
}
public void free() {
if(data == 0L) return;
FreeType.FT_Done_Face(face);
MemoryUtil.nmemFree(data);
data = 0L;
}
public UnbakedGlyth gylthData(int codepoint, float size, float oversample) {
if(skip.contains(codepoint)) return null;
int index = FreeType.FT_Get_Char_Index(face, codepoint);
if(index == 0) return null;
oversample *= this.oversample;
int pixels = Math.round(size * oversample);
if(FreeTypeLibrary.parseError(FreeType.FT_Set_Pixel_Sizes(face, 0, pixels), "Set Pixel Size")) return null;
if(FreeTypeLibrary.parseError(FreeType.FT_Load_Glyph(face, index, FreeType.FT_LOAD_NO_BITMAP | FreeType.FT_LOAD_BITMAP_METRICS_ONLY), "Loading Glyth")) return null;
FT_GlyphSlot slot = face.glyph();
if(slot == null) {
System.out.println("Glyth didn't load for some reason");
return null;
}
float advance = slot.advance().x() / 64F;
FT_Bitmap bitmap = slot.bitmap();
int left = slot.bitmap_left();
int top = slot.bitmap_top();
int width = bitmap.width();
int height = bitmap.rows();
if(width > 0 && height > 0) return new FreeTypeGlyth(face, harfBuzzFont, left, -top, width, height, advance, oversample, index, codepoint);
return new EmptyGlythData(advance / oversample);
}
}
public static class FreeTypeLibrary {
private static long pointer = 0L;
public static long get() {
if(pointer == 0L) {
try(MemoryStack stack = MemoryStack.stackPush()) {
PointerBuffer pointBuffer = stack.callocPointer(1);
int result = FreeType.FT_Init_FreeType(pointBuffer);
if(result != 0) {
throw new IllegalStateException(FreeType.FT_Error_String(result));
}
pointer = pointBuffer.get();
}
}
return pointer;
}
public static boolean parseError(int result, String action) {
if(result == 0) return false;
String error = FreeType.FT_Error_String(result);
System.out.println("Couldn't do ["+action+"] because of "+error);
return true;
}
public static void close() {
if(pointer == 0L) return;
FreeType.FT_Done_Library(pointer);
pointer = 0L;
}
}
}
@@ -0,0 +1,14 @@
package speiger.src.coreengine.ui.gui.font.providers;
import speiger.src.coreengine.ui.gui.font.glyth.UnbakedGlyth;
public interface IFontProvider {
public static final int REGULAR = 0;
public static final int BOLD = 1;
public static final int ITALIC = 2;
public static final int ITALIC_BOLD = 3;
public UnbakedGlyth glythData(int codepoint, int style, float size, float oversample);
public void close();
}
@@ -0,0 +1,215 @@
package speiger.src.coreengine.ui.gui.font.providers;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import org.lwjgl.stb.STBTTFontinfo;
import org.lwjgl.stb.STBTruetype;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import com.google.gson.JsonObject;
import speiger.src.collections.ints.sets.IntOpenHashSet;
import speiger.src.collections.ints.sets.IntSet;
import speiger.src.coreengine.assets.api.IAssetProvider;
import speiger.src.coreengine.assets.api.ID;
import speiger.src.coreengine.assets.impl.parsers.Buffers;
import speiger.src.coreengine.platform.graphics.api.core.GraphicsDevice;
import speiger.src.coreengine.platform.graphics.api.texture.Texture;
import speiger.src.coreengine.platform.graphics.api.texture.drawable.Drawable;
import speiger.src.coreengine.ui.gui.font.FontTexture;
import speiger.src.coreengine.ui.gui.font.glyth.Glyth;
import speiger.src.coreengine.ui.gui.font.glyth.IGlythSheetInfo;
import speiger.src.coreengine.ui.gui.font.glyth.UnbakedGlyth;
import speiger.src.coreengine.ui.gui.font.glyth.UnbakedGlyth.EmptyGlythData;
import speiger.src.coreengine.utils.helpers.JsonUtil;
public class STBTrueTypeProvider implements IFontProvider {
TrueTypeInstance[] instances;
public STBTrueTypeProvider(TrueTypeInstance[] instances) {
this.instances = instances;
}
public static IFontProvider create(ID location, IAssetProvider provider) {
try {
return create(provider.get(location).jsonObj(), provider);
}
catch(Exception e) { e.printStackTrace(); }
return null;
}
public static IFontProvider create(JsonObject data, IAssetProvider provider) {
TrueTypeInstance[] instances = new TrueTypeInstance[4];
instances[0] = create(0, data.getAsJsonObject("regular"), provider);
if(instances[0] == null) return null;
instances[1] = create(1, data.getAsJsonObject("bold"), provider);
instances[2] = create(2, data.getAsJsonObject("italic"), provider);
instances[3] = create(3, data.getAsJsonObject("bold_italic"), provider);
return new STBTrueTypeProvider(instances);
}
private static TrueTypeInstance create(int style, JsonObject obj, IAssetProvider provider) {
if(obj == null || !obj.has("file")) return null;
ID location = ID.of(obj.get("file").getAsString());
float oversample = JsonUtil.getOrDefault(obj, "oversample", 1F);
float shadowOffset = JsonUtil.getOrDefault(obj, "shadowOffset", 1F);
float xOff = 0;
float yOff = 0;
StringBuilder builder = new StringBuilder();
JsonObject shift = obj.getAsJsonObject("offset");
if(shift != null) {
xOff = JsonUtil.getOrDefault(shift, "x", 0);
yOff = JsonUtil.getOrDefault(shift, "y", 0);
}
JsonUtil.iterateValues(obj.get("skip"), T -> builder.append(T.getAsString()));
try {
ByteBuffer buffer = provider.get(location).parse(Buffers.DIRECT);
STBTTFontinfo info = STBTTFontinfo.create();
if(!STBTruetype.stbtt_InitFont(info, buffer)) {
System.out.println("Couldn't load font");
MemoryUtil.memFree(buffer);
info.free();
return null;
}
return new TrueTypeInstance(style, MemoryUtil.memAddress(buffer), info, oversample, xOff, yOff, shadowOffset, builder.toString());
}
catch(Exception e) {
e.printStackTrace();
}
return null;
}
public static class TrueTypeInstance {
final int style;
long data;
final STBTTFontinfo info;
IntSet skip = new IntOpenHashSet();
final float oversample;
final float xOff;
final float yOff;
final float shadowOffset;
final float ascent;
public TrueTypeInstance(int style, long data, STBTTFontinfo info, float oversample, float xOff, float yOff, float shadowOffset, String skip) {
this.style = style;
this.data = data;
this.info = info;
skip.codePoints().forEach(this.skip::add);
this.oversample = oversample;
this.xOff = xOff;
this.yOff = yOff;
this.shadowOffset = shadowOffset;
int[] ascent = new int[1];
STBTruetype.stbtt_GetFontVMetrics(info, ascent, new int[1], new int[1]);
this.ascent = ascent[0];
}
public void free() {
if(data == 0L) return;
info.free();
MemoryUtil.nmemFree(data);
data = 0L;
}
public UnbakedGlyth glythData(int codepoint, float size, float oversample) {
if(skip.contains(codepoint)) return null;
int glyth = STBTruetype.nstbtt_FindGlyphIndex(info.address(), codepoint);
if(glyth == 0) return null;
oversample *= this.oversample;
float scale = STBTruetype.stbtt_ScaleForPixelHeight(info, size * oversample);
try(MemoryStack stack = MemoryStack.stackPush()) {
IntBuffer left = stack.mallocInt(1);
IntBuffer bottom = stack.mallocInt(1);
IntBuffer right = stack.mallocInt(1);
IntBuffer top = stack.mallocInt(1);
IntBuffer advance = stack.mallocInt(1);
IntBuffer leftSideBearing = stack.mallocInt(1);
STBTruetype.stbtt_GetGlyphHMetrics(info, glyth, advance, leftSideBearing);
STBTruetype.stbtt_GetGlyphBitmapBoxSubpixel(info, glyth, scale, scale, xOff, yOff, left, bottom, right, top);
int minX = left.get(0);
int minY = -top.get(0);
int maxX = right.get(0);
int maxY = -bottom.get(0);
if(maxX - minX <= 0 || maxY - minY <= 0) return new EmptyGlythData(advance.get(0) * scale / oversample);
return new STBGlyth(this, minX, minY, maxX, maxY, advance.get(0), leftSideBearing.get(0), scale, oversample, glyth);
}
catch(Exception e) {
e.printStackTrace();
return null;
}
}
}
@Override
public UnbakedGlyth glythData(int codepoint, int style, float size, float oversample) {
TrueTypeInstance instance = instances[style & 0x3];
return instance == null ? null : instance.glythData(codepoint, size, oversample);
}
@Override
public void close() {
if(instances == null) return;
for(int i = 0;i<4;i++) {
if(instances[i] == null) continue;
instances[i].free();
}
instances = null;
}
private static class STBGlyth implements UnbakedGlyth {
final TrueTypeInstance owner;
final float xOffset;
final float yOffset;
final int width;
final int height;
final float oversample;
final float scale;
final float advance;
final int glyth;
public STBGlyth(TrueTypeInstance owner, int minX, int minY, int maxX, int maxY, float advance, float leftPadding, float scale, float oversample, int glyth) {
this.owner = owner;
this.width = maxX - minX;
this.height = maxY - minY;
this.scale = scale;
this.oversample = oversample;
this.xOffset = ((leftPadding * scale) + minX + owner.xOff) / oversample;
this.yOffset = ((owner.ascent * scale) - maxY + owner.yOff) / oversample;
this.advance = advance * scale / oversample;
this.glyth = glyth;
}
@Override
public float advance() { return advance; }
@Override
public float shadowOffset() { return owner.shadowOffset; }
@Override
public float kerning(int codepoint) { return STBTruetype.stbtt_GetCodepointKernAdvance(owner.info, codepoint, glyth) * scale / oversample; }
@Override
public Glyth bake(GlythBaker baker) {
return baker.bake(new IGlythSheetInfo() {
@Override
public float xOffset() { return xOffset; }
@Override
public float yOffset() { return yOffset; }
@Override
public int width() { return width; }
@Override
public int height() { return height; }
@Override
public float oversample() { return oversample; }
@Override
public boolean isColored() { return false; }
@Override
public void upload(Texture texture, int x, int y) {
Drawable drawable = new Drawable(FontTexture.formatByColor(false), width, height);
drawable.drawFont(owner.info, glyth, owner.xOff, owner.yOff, 0, 0, width, height, scale, scale);
drawable.upload(GraphicsDevice.get().queue(), texture, x, y, 0, 0, width, height);
drawable.close();
}
});
}
}
}