Font renderer ported
This commit is contained in:
@@ -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;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
public interface IResourceListener {
|
public interface IResourceListener {
|
||||||
void name();
|
|
||||||
CompletableFuture<Void> reload(ReloadContext context, ISyncer sync);
|
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 {
|
public static interface ISyncer {
|
||||||
<T> CompletableFuture<T> sync(T value);
|
<T> CompletableFuture<T> sync(T value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package speiger.src.coreengine.assets.api;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.Executor;
|
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) {
|
public boolean hasChanged(ID id) {
|
||||||
return filter.isEmpty() || filter.contains(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());
|
if(builder.listeners.isEmpty()) builder.withListener(tracker.listeners());
|
||||||
return new ReloadTask(get(), builder);
|
return new ReloadTask(get(), builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isReloadable() {
|
public boolean isReloadable() {
|
||||||
return manager != null;
|
return manager != null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import speiger.src.coreengine.assets.providers.SingleProvider;
|
|||||||
|
|
||||||
public class ReloadBuilder {
|
public class ReloadBuilder {
|
||||||
Executor mainRunner = Runnable::run;
|
Executor mainRunner = Runnable::run;
|
||||||
|
Executor taskRunner = Runnable::run;
|
||||||
Executor syncRunner = Runnable::run;
|
Executor syncRunner = Runnable::run;
|
||||||
Set<ID> filter = ObjectSets.empty();
|
Set<ID> filter = ObjectSets.empty();
|
||||||
Optional<BaseProvider> packages = Optional.empty();
|
Optional<BaseProvider> packages = Optional.empty();
|
||||||
@@ -26,11 +27,16 @@ public class ReloadBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReloadBuilder withMain(Executor main) {
|
public ReloadBuilder withMainThread(Executor main) {
|
||||||
mainRunner = Objects.requireNonNull(main);
|
mainRunner = Objects.requireNonNull(main);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ReloadBuilder withTasks(Executor tasks) {
|
||||||
|
taskRunner = Objects.requireNonNull(tasks);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public ReloadBuilder withSync(Executor sync) {
|
public ReloadBuilder withSync(Executor sync) {
|
||||||
syncRunner = Objects.requireNonNull(sync);
|
syncRunner = Objects.requireNonNull(sync);
|
||||||
return this;
|
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.IResourceListener.ISyncer;
|
||||||
import speiger.src.coreengine.assets.api.ReloadContext;
|
import speiger.src.coreengine.assets.api.ReloadContext;
|
||||||
|
|
||||||
public class ReloadTask {
|
public class ReloadTask {
|
||||||
AtomicInteger started = new AtomicInteger();
|
AtomicInteger started = new AtomicInteger();
|
||||||
AtomicInteger finished = new AtomicInteger();
|
AtomicInteger finished = new AtomicInteger();
|
||||||
CompletableFuture<?> future;
|
CompletableFuture<?> future;
|
||||||
|
|
||||||
public ReloadTask(IAssetProvider provider, ReloadBuilder builder) {
|
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(); }
|
public int started() { return started.get(); }
|
||||||
@@ -36,14 +36,14 @@ public class ReloadTask {
|
|||||||
List<CompletableFuture<Void>> tasks = new ObjectArrayList<>();
|
List<CompletableFuture<Void>> tasks = new ObjectArrayList<>();
|
||||||
for(IResourceListener listener : listeners) {
|
for(IResourceListener listener : listeners) {
|
||||||
CompletableFuture<Void> current = chainedTask;
|
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);
|
tasks.add(chainedTask);
|
||||||
}
|
}
|
||||||
return CompletableFuture.allOf(tasks.toArray(CompletableFuture[]::new)).thenRunAsync(() -> {
|
return CompletableFuture.allOf(tasks.toArray(CompletableFuture[]::new)).thenRunAsync(() -> {
|
||||||
finished.getAndIncrement();
|
finished.getAndIncrement();
|
||||||
try { context.provider().close(); }
|
try { context.provider().close(); }
|
||||||
catch(Exception e) { e.printStackTrace(); }
|
catch(Exception e) { e.printStackTrace(); }
|
||||||
}, context.main());
|
}, context.tasks());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+39
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+75
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -17,6 +17,8 @@ import speiger.src.coreengine.platform.input.window.Window;
|
|||||||
|
|
||||||
public interface GraphicsDevice {
|
public interface GraphicsDevice {
|
||||||
public static final ScopedValue<GraphicsDevice> SCOPE = ScopedValue.newInstance();
|
public static final ScopedValue<GraphicsDevice> SCOPE = ScopedValue.newInstance();
|
||||||
|
public static GraphicsDevice get() { return SCOPE.get(); }
|
||||||
|
|
||||||
Window window();
|
Window window();
|
||||||
GraphicsSurface createSurface();
|
GraphicsSurface createSurface();
|
||||||
GraphicsCommandBuffer createCommandBuffer(ExecutionType type);
|
GraphicsCommandBuffer createCommandBuffer(ExecutionType type);
|
||||||
|
|||||||
+2
@@ -10,6 +10,7 @@ import java.util.OptionalInt;
|
|||||||
import java.util.function.IntPredicate;
|
import java.util.function.IntPredicate;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.lwjgl.opengl.GL;
|
||||||
import org.lwjgl.opengl.GL11;
|
import org.lwjgl.opengl.GL11;
|
||||||
import org.lwjgl.opengl.GL12;
|
import org.lwjgl.opengl.GL12;
|
||||||
import org.lwjgl.opengl.GL20;
|
import org.lwjgl.opengl.GL20;
|
||||||
@@ -88,6 +89,7 @@ public class GLGraphicsDevice implements GraphicsDevice {
|
|||||||
this.owner = owner;
|
this.owner = owner;
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
this.capabilities = capbilities;
|
this.capabilities = capbilities;
|
||||||
|
GL.setCapabilities(capbilities);
|
||||||
this.queue = new GLCommandQueue(this);
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user