461 lines
14 KiB
Java
461 lines
14 KiB
Java
package net.minecraft.client.gui;
|
|
|
|
import com.google.common.collect.Lists;
|
|
import com.ibm.icu.text.ArabicShaping;
|
|
import com.ibm.icu.text.ArabicShapingException;
|
|
import com.ibm.icu.text.Bidi;
|
|
import com.mojang.blaze3d.font.GlyphInfo;
|
|
import com.mojang.blaze3d.vertex.VertexConsumer;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.function.Function;
|
|
import net.fabricmc.api.EnvType;
|
|
import net.fabricmc.api.Environment;
|
|
import net.minecraft.client.StringSplitter;
|
|
import net.minecraft.client.gui.font.FontSet;
|
|
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
|
|
import net.minecraft.client.gui.font.glyphs.EmptyGlyph;
|
|
import net.minecraft.client.renderer.MultiBufferSource;
|
|
import net.minecraft.locale.Language;
|
|
import net.minecraft.network.chat.Component;
|
|
import net.minecraft.network.chat.FormattedText;
|
|
import net.minecraft.network.chat.Style;
|
|
import net.minecraft.network.chat.TextColor;
|
|
import net.minecraft.resources.ResourceLocation;
|
|
import net.minecraft.util.ARGB;
|
|
import net.minecraft.util.FormattedCharSequence;
|
|
import net.minecraft.util.FormattedCharSink;
|
|
import net.minecraft.util.Mth;
|
|
import net.minecraft.util.RandomSource;
|
|
import net.minecraft.util.StringDecomposer;
|
|
import org.jetbrains.annotations.Nullable;
|
|
import org.joml.Matrix4f;
|
|
|
|
@Environment(EnvType.CLIENT)
|
|
public class Font {
|
|
private static final float EFFECT_DEPTH = 0.01F;
|
|
public static final float SHADOW_DEPTH = 0.03F;
|
|
public static final int NO_SHADOW = 0;
|
|
public static final int ALPHA_CUTOFF = 8;
|
|
public final int lineHeight = 9;
|
|
public final RandomSource random = RandomSource.create();
|
|
private final Function<ResourceLocation, FontSet> fonts;
|
|
final boolean filterFishyGlyphs;
|
|
private final StringSplitter splitter;
|
|
|
|
public Font(Function<ResourceLocation, FontSet> fonts, boolean filterFishyGlyphs) {
|
|
this.fonts = fonts;
|
|
this.filterFishyGlyphs = filterFishyGlyphs;
|
|
this.splitter = new StringSplitter((i, style) -> this.getFontSet(style.getFont()).getGlyphInfo(i, this.filterFishyGlyphs).getAdvance(style.isBold()));
|
|
}
|
|
|
|
FontSet getFontSet(ResourceLocation fontLocation) {
|
|
return (FontSet)this.fonts.apply(fontLocation);
|
|
}
|
|
|
|
/**
|
|
* Apply Unicode Bidirectional Algorithm to string and return a new possibly reordered string for visual rendering.
|
|
*/
|
|
public String bidirectionalShaping(String text) {
|
|
try {
|
|
Bidi bidi = new Bidi(new ArabicShaping(8).shape(text), 127);
|
|
bidi.setReorderingMode(0);
|
|
return bidi.writeReordered(2);
|
|
} catch (ArabicShapingException var3) {
|
|
return text;
|
|
}
|
|
}
|
|
|
|
public int drawInBatch(
|
|
String text,
|
|
float x,
|
|
float y,
|
|
int color,
|
|
boolean dropShadow,
|
|
Matrix4f pose,
|
|
MultiBufferSource bufferSource,
|
|
Font.DisplayMode displayMode,
|
|
int backgroundColor,
|
|
int packedLightCoords
|
|
) {
|
|
if (this.isBidirectional()) {
|
|
text = this.bidirectionalShaping(text);
|
|
}
|
|
|
|
return this.drawInternal(text, x, y, color, dropShadow, pose, bufferSource, displayMode, backgroundColor, packedLightCoords, true);
|
|
}
|
|
|
|
public int drawInBatch(
|
|
Component text,
|
|
float x,
|
|
float y,
|
|
int color,
|
|
boolean dropShadow,
|
|
Matrix4f pose,
|
|
MultiBufferSource bufferSource,
|
|
Font.DisplayMode displayMode,
|
|
int backgroundColor,
|
|
int packedLightCoords
|
|
) {
|
|
return this.drawInBatch(text, x, y, color, dropShadow, pose, bufferSource, displayMode, backgroundColor, packedLightCoords, true);
|
|
}
|
|
|
|
public int drawInBatch(
|
|
Component text,
|
|
float x,
|
|
float y,
|
|
int color,
|
|
boolean dropShadow,
|
|
Matrix4f pose,
|
|
MultiBufferSource bufferSource,
|
|
Font.DisplayMode displayMode,
|
|
int backgroundColor,
|
|
int packedLightCoords,
|
|
boolean inverseDepth
|
|
) {
|
|
return this.drawInternal(
|
|
text.getVisualOrderText(), x, y, color, dropShadow, pose, bufferSource, displayMode, backgroundColor, packedLightCoords, inverseDepth
|
|
);
|
|
}
|
|
|
|
public int drawInBatch(
|
|
FormattedCharSequence text,
|
|
float x,
|
|
float y,
|
|
int color,
|
|
boolean dropShadow,
|
|
Matrix4f pose,
|
|
MultiBufferSource bufferSource,
|
|
Font.DisplayMode displayMode,
|
|
int backgroundColor,
|
|
int packedLightCoords
|
|
) {
|
|
return this.drawInternal(text, x, y, color, dropShadow, pose, bufferSource, displayMode, backgroundColor, packedLightCoords, true);
|
|
}
|
|
|
|
public void drawInBatch8xOutline(
|
|
FormattedCharSequence text, float x, float y, int color, int backgroundColor, Matrix4f pose, MultiBufferSource bufferSource, int packedLightCoords
|
|
) {
|
|
int i = adjustColor(backgroundColor);
|
|
Font.StringRenderOutput stringRenderOutput = new Font.StringRenderOutput(bufferSource, 0.0F, 0.0F, i, false, pose, Font.DisplayMode.NORMAL, packedLightCoords);
|
|
|
|
for (int j = -1; j <= 1; j++) {
|
|
for (int k = -1; k <= 1; k++) {
|
|
if (j != 0 || k != 0) {
|
|
float[] fs = new float[]{x};
|
|
int l = j;
|
|
int m = k;
|
|
text.accept((lx, style, mx) -> {
|
|
boolean bl = style.isBold();
|
|
FontSet fontSet = this.getFontSet(style.getFont());
|
|
GlyphInfo glyphInfo = fontSet.getGlyphInfo(mx, this.filterFishyGlyphs);
|
|
stringRenderOutput.x = fs[0] + l * glyphInfo.getShadowOffset();
|
|
stringRenderOutput.y = y + m * glyphInfo.getShadowOffset();
|
|
fs[0] += glyphInfo.getAdvance(bl);
|
|
return stringRenderOutput.accept(lx, style.withColor(i), mx);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
stringRenderOutput.renderCharacters();
|
|
Font.StringRenderOutput stringRenderOutput2 = new Font.StringRenderOutput(
|
|
bufferSource, x, y, adjustColor(color), false, pose, Font.DisplayMode.POLYGON_OFFSET, packedLightCoords
|
|
);
|
|
text.accept(stringRenderOutput2);
|
|
stringRenderOutput2.finish(x);
|
|
}
|
|
|
|
private static int adjustColor(int color) {
|
|
return (color & -67108864) == 0 ? ARGB.opaque(color) : color;
|
|
}
|
|
|
|
private int drawInternal(
|
|
String text,
|
|
float x,
|
|
float y,
|
|
int color,
|
|
boolean dropShadow,
|
|
Matrix4f pose,
|
|
MultiBufferSource bufferSource,
|
|
Font.DisplayMode displayMode,
|
|
int backgroundColor,
|
|
int packedLightCoords,
|
|
boolean inverseDepth
|
|
) {
|
|
color = adjustColor(color);
|
|
x = this.renderText(text, x, y, color, dropShadow, pose, bufferSource, displayMode, backgroundColor, packedLightCoords, inverseDepth);
|
|
return (int)x + (dropShadow ? 1 : 0);
|
|
}
|
|
|
|
private int drawInternal(
|
|
FormattedCharSequence text,
|
|
float x,
|
|
float y,
|
|
int color,
|
|
boolean dropShadow,
|
|
Matrix4f pose,
|
|
MultiBufferSource bufferSource,
|
|
Font.DisplayMode displayMode,
|
|
int backgroundColor,
|
|
int packedLightCoords,
|
|
boolean inverseDepth
|
|
) {
|
|
color = adjustColor(color);
|
|
x = this.renderText(text, x, y, color, dropShadow, pose, bufferSource, displayMode, backgroundColor, packedLightCoords, inverseDepth);
|
|
return (int)x + (dropShadow ? 1 : 0);
|
|
}
|
|
|
|
private float renderText(
|
|
String text,
|
|
float x,
|
|
float y,
|
|
int color,
|
|
boolean dropShadow,
|
|
Matrix4f pose,
|
|
MultiBufferSource bufferSource,
|
|
Font.DisplayMode displayMode,
|
|
int backgroundColor,
|
|
int packedLightCoords,
|
|
boolean inverseDepth
|
|
) {
|
|
Font.StringRenderOutput stringRenderOutput = new Font.StringRenderOutput(
|
|
bufferSource, x, y, color, backgroundColor, dropShadow, pose, displayMode, packedLightCoords, inverseDepth
|
|
);
|
|
StringDecomposer.iterateFormatted(text, Style.EMPTY, stringRenderOutput);
|
|
return stringRenderOutput.finish(x);
|
|
}
|
|
|
|
private float renderText(
|
|
FormattedCharSequence text,
|
|
float x,
|
|
float y,
|
|
int color,
|
|
boolean dropShadow,
|
|
Matrix4f pose,
|
|
MultiBufferSource bufferSource,
|
|
Font.DisplayMode displayMode,
|
|
int backgroundColor,
|
|
int packedLightCoords,
|
|
boolean inverseDepth
|
|
) {
|
|
Font.StringRenderOutput stringRenderOutput = new Font.StringRenderOutput(
|
|
bufferSource, x, y, color, backgroundColor, dropShadow, pose, displayMode, packedLightCoords, inverseDepth
|
|
);
|
|
text.accept(stringRenderOutput);
|
|
return stringRenderOutput.finish(x);
|
|
}
|
|
|
|
/**
|
|
* Returns the width of this string. Equivalent of FontMetrics.stringWidth(String s).
|
|
*/
|
|
public int width(String text) {
|
|
return Mth.ceil(this.splitter.stringWidth(text));
|
|
}
|
|
|
|
public int width(FormattedText text) {
|
|
return Mth.ceil(this.splitter.stringWidth(text));
|
|
}
|
|
|
|
public int width(FormattedCharSequence text) {
|
|
return Mth.ceil(this.splitter.stringWidth(text));
|
|
}
|
|
|
|
public String plainSubstrByWidth(String text, int maxWidth, boolean tail) {
|
|
return tail ? this.splitter.plainTailByWidth(text, maxWidth, Style.EMPTY) : this.splitter.plainHeadByWidth(text, maxWidth, Style.EMPTY);
|
|
}
|
|
|
|
public String plainSubstrByWidth(String text, int maxWidth) {
|
|
return this.splitter.plainHeadByWidth(text, maxWidth, Style.EMPTY);
|
|
}
|
|
|
|
public FormattedText substrByWidth(FormattedText text, int maxWidth) {
|
|
return this.splitter.headByWidth(text, maxWidth, Style.EMPTY);
|
|
}
|
|
|
|
/**
|
|
* Returns the height (in pixels) of the given string if it is wordwrapped to the given max width.
|
|
*/
|
|
public int wordWrapHeight(String text, int maxWidth) {
|
|
return 9 * this.splitter.splitLines(text, maxWidth, Style.EMPTY).size();
|
|
}
|
|
|
|
public int wordWrapHeight(FormattedText text, int maxWidth) {
|
|
return 9 * this.splitter.splitLines(text, maxWidth, Style.EMPTY).size();
|
|
}
|
|
|
|
public List<FormattedCharSequence> split(FormattedText text, int maxWidth) {
|
|
return Language.getInstance().getVisualOrder(this.splitter.splitLines(text, maxWidth, Style.EMPTY));
|
|
}
|
|
|
|
/**
|
|
* Get bidiFlag that controls if the Unicode Bidirectional Algorithm should be run before rendering any string
|
|
*/
|
|
public boolean isBidirectional() {
|
|
return Language.getInstance().isDefaultRightToLeft();
|
|
}
|
|
|
|
public StringSplitter getSplitter() {
|
|
return this.splitter;
|
|
}
|
|
|
|
@Environment(EnvType.CLIENT)
|
|
public static enum DisplayMode {
|
|
NORMAL,
|
|
SEE_THROUGH,
|
|
POLYGON_OFFSET;
|
|
}
|
|
|
|
@Environment(EnvType.CLIENT)
|
|
class StringRenderOutput implements FormattedCharSink {
|
|
final MultiBufferSource bufferSource;
|
|
private final boolean drawShadow;
|
|
private final int color;
|
|
private final int backgroundColor;
|
|
private final Matrix4f pose;
|
|
private final Font.DisplayMode mode;
|
|
private final int packedLightCoords;
|
|
private final boolean inverseDepth;
|
|
float x;
|
|
float y;
|
|
private final List<BakedGlyph.GlyphInstance> glyphInstances = new ArrayList();
|
|
@Nullable
|
|
private List<BakedGlyph.Effect> effects;
|
|
|
|
private void addEffect(BakedGlyph.Effect effect) {
|
|
if (this.effects == null) {
|
|
this.effects = Lists.<BakedGlyph.Effect>newArrayList();
|
|
}
|
|
|
|
this.effects.add(effect);
|
|
}
|
|
|
|
public StringRenderOutput(
|
|
final MultiBufferSource bufferSource,
|
|
final float x,
|
|
final float y,
|
|
final int color,
|
|
final boolean dropShadow,
|
|
final Matrix4f pose,
|
|
final Font.DisplayMode mode,
|
|
final int packedLightCoords
|
|
) {
|
|
this(bufferSource, x, y, color, 0, dropShadow, pose, mode, packedLightCoords, true);
|
|
}
|
|
|
|
public StringRenderOutput(
|
|
final MultiBufferSource buferSource,
|
|
final float x,
|
|
final float y,
|
|
final int color,
|
|
final int backgroundColor,
|
|
final boolean dropShadow,
|
|
final Matrix4f pose,
|
|
final Font.DisplayMode displayMode,
|
|
final int packedLightCoords,
|
|
final boolean inverseDepth
|
|
) {
|
|
this.bufferSource = buferSource;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.drawShadow = dropShadow;
|
|
this.color = color;
|
|
this.backgroundColor = backgroundColor;
|
|
this.pose = pose;
|
|
this.mode = displayMode;
|
|
this.packedLightCoords = packedLightCoords;
|
|
this.inverseDepth = inverseDepth;
|
|
}
|
|
|
|
@Override
|
|
public boolean accept(int i, Style style, int j) {
|
|
FontSet fontSet = Font.this.getFontSet(style.getFont());
|
|
GlyphInfo glyphInfo = fontSet.getGlyphInfo(j, Font.this.filterFishyGlyphs);
|
|
BakedGlyph bakedGlyph = style.isObfuscated() && j != 32 ? fontSet.getRandomGlyph(glyphInfo) : fontSet.getGlyph(j);
|
|
boolean bl = style.isBold();
|
|
TextColor textColor = style.getColor();
|
|
int k = this.getTextColor(textColor);
|
|
int l = this.getShadowColor(style, k);
|
|
float f = glyphInfo.getAdvance(bl);
|
|
float g = i == 0 ? this.x - 1.0F : this.x;
|
|
float h = glyphInfo.getShadowOffset();
|
|
if (!(bakedGlyph instanceof EmptyGlyph)) {
|
|
float m = bl ? glyphInfo.getBoldOffset() : 0.0F;
|
|
this.glyphInstances.add(new BakedGlyph.GlyphInstance(this.x, this.y, k, l, bakedGlyph, style, m, h));
|
|
}
|
|
|
|
if (style.isStrikethrough()) {
|
|
this.addEffect(new BakedGlyph.Effect(g, this.y + 4.5F, this.x + f, this.y + 4.5F - 1.0F, this.getOverTextEffectDepth(), k, l, h));
|
|
}
|
|
|
|
if (style.isUnderlined()) {
|
|
this.addEffect(new BakedGlyph.Effect(g, this.y + 9.0F, this.x + f, this.y + 9.0F - 1.0F, this.getOverTextEffectDepth(), k, l, h));
|
|
}
|
|
|
|
this.x += f;
|
|
return true;
|
|
}
|
|
|
|
float finish(float x) {
|
|
BakedGlyph bakedGlyph = null;
|
|
if (this.backgroundColor != 0) {
|
|
BakedGlyph.Effect effect = new BakedGlyph.Effect(x - 1.0F, this.y + 9.0F, this.x, this.y - 1.0F, this.getUnderTextEffectDepth(), this.backgroundColor);
|
|
bakedGlyph = Font.this.getFontSet(Style.DEFAULT_FONT).whiteGlyph();
|
|
VertexConsumer vertexConsumer = this.bufferSource.getBuffer(bakedGlyph.renderType(this.mode));
|
|
bakedGlyph.renderEffect(effect, this.pose, vertexConsumer, this.packedLightCoords);
|
|
}
|
|
|
|
this.renderCharacters();
|
|
if (this.effects != null) {
|
|
if (bakedGlyph == null) {
|
|
bakedGlyph = Font.this.getFontSet(Style.DEFAULT_FONT).whiteGlyph();
|
|
}
|
|
|
|
VertexConsumer vertexConsumer2 = this.bufferSource.getBuffer(bakedGlyph.renderType(this.mode));
|
|
|
|
for (BakedGlyph.Effect effect2 : this.effects) {
|
|
bakedGlyph.renderEffect(effect2, this.pose, vertexConsumer2, this.packedLightCoords);
|
|
}
|
|
}
|
|
|
|
return this.x;
|
|
}
|
|
|
|
private int getTextColor(@Nullable TextColor textColor) {
|
|
if (textColor != null) {
|
|
int i = ARGB.alpha(this.color);
|
|
int j = textColor.getValue();
|
|
return ARGB.color(i, j);
|
|
} else {
|
|
return this.color;
|
|
}
|
|
}
|
|
|
|
private int getShadowColor(Style style, int textColor) {
|
|
Integer integer = style.getShadowColor();
|
|
if (integer != null) {
|
|
float f = ARGB.alphaFloat(textColor);
|
|
float g = ARGB.alphaFloat(integer);
|
|
return f != 1.0F ? ARGB.color(ARGB.as8BitChannel(f * g), integer) : integer;
|
|
} else {
|
|
return this.drawShadow ? ARGB.scaleRGB(textColor, 0.25F) : 0;
|
|
}
|
|
}
|
|
|
|
void renderCharacters() {
|
|
for (BakedGlyph.GlyphInstance glyphInstance : this.glyphInstances) {
|
|
BakedGlyph bakedGlyph = glyphInstance.glyph();
|
|
VertexConsumer vertexConsumer = this.bufferSource.getBuffer(bakedGlyph.renderType(this.mode));
|
|
bakedGlyph.renderChar(glyphInstance, this.pose, vertexConsumer, this.packedLightCoords);
|
|
}
|
|
}
|
|
|
|
private float getOverTextEffectDepth() {
|
|
return this.inverseDepth ? 0.01F : -0.01F;
|
|
}
|
|
|
|
private float getUnderTextEffectDepth() {
|
|
return this.inverseDepth ? -0.01F : 0.01F;
|
|
}
|
|
}
|
|
}
|