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 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.GlyphVisitor.1; 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.01F; private static final float OVER_EFFECT_DEPTH = 0.01F; private static final float UNDER_EFFECT_DEPTH = -0.01F; public static final float SHADOW_DEPTH = 0.03F; public static final int NO_SHADOW = 0; public final int lineHeight = 9; public final RandomSource random = RandomSource.create(); private final Function fonts; final boolean filterFishyGlyphs; private final StringSplitter splitter; public Font(Function 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 split(FormattedText text, int maxWidth) { return Language.getInstance().getVisualOrder(this.splitter.splitLines(text, maxWidth, Style.EMPTY)); } public List 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 1(bufferSource, displayMode, pose, packedLight); } 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 glyphs = new ArrayList(); @Nullable private List 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.01F, 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.01F, 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.01F, 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; } } } }