406 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
			
		
		
	
	
			406 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 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<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 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<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.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;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 |