minecraft-src/net/minecraft/client/gui/Font.java
2025-07-04 03:15:13 +03:00

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;
}
}
}