minecraft-src/net/minecraft/client/gui/Font.java
2025-09-18 12:27:44 +00:00

419 lines
14 KiB
Java

package net.minecraft.client.gui;
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.gui.navigation.ScreenRectangle;
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.001F;
private static final float OVER_EFFECT_DEPTH = 0.001F;
private static final float UNDER_EFFECT_DEPTH = -0.001F;
public static final float SHADOW_DEPTH = 0.003F;
public static final int NO_SHADOW = 0;
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 void drawInBatch(
String text,
float x,
float y,
int color,
boolean drawShadow,
Matrix4f pose,
MultiBufferSource bufferSource,
Font.DisplayMode mode,
int backgroundColor,
int packedLightCoords
) {
Font.PreparedText preparedText = this.prepareText(text, x, y, color, drawShadow, backgroundColor);
preparedText.visit(Font.GlyphVisitor.forMultiBufferSource(bufferSource, pose, mode, packedLightCoords));
}
public void drawInBatch(
Component text,
float x,
float y,
int color,
boolean drawShadow,
Matrix4f pose,
MultiBufferSource bufferSource,
Font.DisplayMode mode,
int backgroundColor,
int packedLightCoords
) {
Font.PreparedText preparedText = this.prepareText(text.getVisualOrderText(), x, y, color, drawShadow, backgroundColor);
preparedText.visit(Font.GlyphVisitor.forMultiBufferSource(bufferSource, pose, mode, packedLightCoords));
}
public void drawInBatch(
FormattedCharSequence text,
float x,
float y,
int color,
boolean drawShadow,
Matrix4f pose,
MultiBufferSource bufferSource,
Font.DisplayMode mode,
int backgroundColor,
int packedLightCoords
) {
Font.PreparedText preparedText = this.prepareText(text, x, y, color, drawShadow, backgroundColor);
preparedText.visit(Font.GlyphVisitor.forMultiBufferSource(bufferSource, pose, mode, packedLightCoords));
}
public void drawInBatch8xOutline(
FormattedCharSequence text, float x, float y, int color, int backgroundColor, Matrix4f pose, MultiBufferSource bufferSource, int packedLightCoords
) {
Font.PreparedTextBuilder preparedTextBuilder = new Font.PreparedTextBuilder(0.0F, 0.0F, backgroundColor, false);
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
if (i != 0 || j != 0) {
float[] fs = new float[]{x};
int k = i;
int l = j;
text.accept((lx, style, m) -> {
boolean bl = style.isBold();
FontSet fontSet = this.getFontSet(style.getFont());
GlyphInfo glyphInfo = fontSet.getGlyphInfo(m, this.filterFishyGlyphs);
preparedTextBuilder.x = fs[0] + k * glyphInfo.getShadowOffset();
preparedTextBuilder.y = y + l * glyphInfo.getShadowOffset();
fs[0] += glyphInfo.getAdvance(bl);
return preparedTextBuilder.accept(lx, style.withColor(backgroundColor), m);
});
}
}
}
Font.GlyphVisitor glyphVisitor = Font.GlyphVisitor.forMultiBufferSource(bufferSource, pose, Font.DisplayMode.NORMAL, packedLightCoords);
for (BakedGlyph.GlyphInstance glyphInstance : preparedTextBuilder.glyphs) {
glyphVisitor.acceptGlyph(glyphInstance);
}
Font.PreparedTextBuilder preparedTextBuilder2 = new Font.PreparedTextBuilder(x, y, color, false);
text.accept(preparedTextBuilder2);
preparedTextBuilder2.visit(Font.GlyphVisitor.forMultiBufferSource(bufferSource, pose, Font.DisplayMode.POLYGON_OFFSET, packedLightCoords));
}
public Font.PreparedText prepareText(String text, float x, float y, int color, boolean dropShadow, int backgroundColor) {
if (this.isBidirectional()) {
text = this.bidirectionalShaping(text);
}
Font.PreparedTextBuilder preparedTextBuilder = new Font.PreparedTextBuilder(x, y, color, backgroundColor, dropShadow);
StringDecomposer.iterateFormatted(text, Style.EMPTY, preparedTextBuilder);
return preparedTextBuilder;
}
public Font.PreparedText prepareText(FormattedCharSequence text, float x, float y, int color, boolean dropShadow, int backgroundColor) {
Font.PreparedTextBuilder preparedTextBuilder = new Font.PreparedTextBuilder(x, y, color, backgroundColor, dropShadow);
text.accept(preparedTextBuilder);
return preparedTextBuilder;
}
/**
* 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));
}
public List<FormattedText> splitIgnoringLanguage(FormattedText text, int maxWidth) {
return 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)
public interface GlyphVisitor {
static Font.GlyphVisitor forMultiBufferSource(MultiBufferSource bufferSource, Matrix4f pose, Font.DisplayMode displayMode, int packedLight) {
return new Font.GlyphVisitor() {
@Override
public void acceptGlyph(BakedGlyph.GlyphInstance glyph) {
BakedGlyph bakedGlyph = glyph.glyph();
VertexConsumer vertexConsumer = bufferSource.getBuffer(bakedGlyph.renderType(displayMode));
bakedGlyph.renderChar(glyph, pose, vertexConsumer, packedLight, false);
}
@Override
public void acceptEffect(BakedGlyph glyph, BakedGlyph.Effect effect) {
VertexConsumer vertexConsumer = bufferSource.getBuffer(glyph.renderType(displayMode));
glyph.renderEffect(effect, pose, vertexConsumer, packedLight, false);
}
};
}
void acceptGlyph(BakedGlyph.GlyphInstance glyph);
void acceptEffect(BakedGlyph glyph, BakedGlyph.Effect effect);
}
@Environment(EnvType.CLIENT)
public interface PreparedText {
void visit(Font.GlyphVisitor visitor);
@Nullable
ScreenRectangle bounds();
}
@Environment(EnvType.CLIENT)
class PreparedTextBuilder implements FormattedCharSink, Font.PreparedText {
private final boolean drawShadow;
private final int color;
private final int backgroundColor;
float x;
float y;
private float left = Float.MAX_VALUE;
private float top = Float.MAX_VALUE;
private float right = -Float.MAX_VALUE;
private float bottom = -Float.MAX_VALUE;
private float backgroundLeft = Float.MAX_VALUE;
private float backgroundTop = Float.MAX_VALUE;
private float backgroundRight = -Float.MAX_VALUE;
private float backgroundBottom = -Float.MAX_VALUE;
final List<BakedGlyph.GlyphInstance> glyphs = new ArrayList();
@Nullable
private List<BakedGlyph.Effect> effects;
public PreparedTextBuilder(final float x, final float y, final int color, final boolean dropShadow) {
this(x, y, color, 0, dropShadow);
}
public PreparedTextBuilder(final float x, final float y, final int color, final int backgroundColor, final boolean dropShadow) {
this.x = x;
this.y = y;
this.drawShadow = dropShadow;
this.color = color;
this.backgroundColor = backgroundColor;
this.markBackground(x, y, 0.0F);
}
private void markSize(float left, float top, float right, float bottom) {
this.left = Math.min(this.left, left);
this.top = Math.min(this.top, top);
this.right = Math.max(this.right, right);
this.bottom = Math.max(this.bottom, bottom);
}
private void markBackground(float x, float y, float advance) {
if (ARGB.alpha(this.backgroundColor) != 0) {
this.backgroundLeft = Math.min(this.backgroundLeft, x - 1.0F);
this.backgroundTop = Math.min(this.backgroundTop, y - 1.0F);
this.backgroundRight = Math.max(this.backgroundRight, x + advance);
this.backgroundBottom = Math.max(this.backgroundBottom, y + 9.0F);
this.markSize(this.backgroundLeft, this.backgroundTop, this.backgroundRight, this.backgroundBottom);
}
}
private void addGlyph(BakedGlyph.GlyphInstance glyph) {
this.glyphs.add(glyph);
this.markSize(glyph.left(), glyph.top(), glyph.right(), glyph.bottom());
}
private void addEffect(BakedGlyph.Effect effect) {
if (this.effects == null) {
this.effects = new ArrayList();
}
this.effects.add(effect);
this.markSize(effect.left(), effect.top(), effect.right(), effect.bottom());
}
@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.addGlyph(new BakedGlyph.GlyphInstance(this.x, this.y, k, l, bakedGlyph, style, m, h));
}
this.markBackground(this.x, this.y, f);
if (style.isStrikethrough()) {
this.addEffect(new BakedGlyph.Effect(g, this.y + 4.5F - 1.0F, this.x + f, this.y + 4.5F, 0.001F, k, l, h));
}
if (style.isUnderlined()) {
this.addEffect(new BakedGlyph.Effect(g, this.y + 9.0F - 1.0F, this.x + f, this.y + 9.0F, 0.001F, k, l, h));
}
this.x += f;
return true;
}
@Override
public void visit(Font.GlyphVisitor visitor) {
BakedGlyph bakedGlyph = null;
if (ARGB.alpha(this.backgroundColor) != 0) {
BakedGlyph.Effect effect = new BakedGlyph.Effect(
this.backgroundLeft, this.backgroundTop, this.backgroundRight, this.backgroundBottom, -0.001F, this.backgroundColor
);
bakedGlyph = Font.this.getFontSet(Style.DEFAULT_FONT).whiteGlyph();
visitor.acceptEffect(bakedGlyph, effect);
}
for (BakedGlyph.GlyphInstance glyphInstance : this.glyphs) {
visitor.acceptGlyph(glyphInstance);
}
if (this.effects != null) {
if (bakedGlyph == null) {
bakedGlyph = Font.this.getFontSet(Style.DEFAULT_FONT).whiteGlyph();
}
for (BakedGlyph.Effect effect2 : this.effects) {
visitor.acceptEffect(bakedGlyph, effect2);
}
}
}
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;
}
}
@Nullable
@Override
public ScreenRectangle bounds() {
if (!(this.left >= this.right) && !(this.top >= this.bottom)) {
int i = Mth.floor(this.left);
int j = Mth.floor(this.top);
int k = Mth.ceil(this.right);
int l = Mth.ceil(this.bottom);
return new ScreenRectangle(i, j, k - i, l - j);
} else {
return null;
}
}
}
}