469 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
			
		
		
	
	
			469 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
| package net.minecraft.client;
 | |
| 
 | |
| import com.google.common.collect.Lists;
 | |
| import java.util.List;
 | |
| import java.util.ListIterator;
 | |
| import java.util.Optional;
 | |
| import java.util.function.BiConsumer;
 | |
| import java.util.stream.Collectors;
 | |
| import net.fabricmc.api.EnvType;
 | |
| import net.fabricmc.api.Environment;
 | |
| import net.minecraft.network.chat.FormattedText;
 | |
| import net.minecraft.network.chat.Style;
 | |
| import net.minecraft.util.FormattedCharSequence;
 | |
| import net.minecraft.util.FormattedCharSink;
 | |
| import net.minecraft.util.StringDecomposer;
 | |
| import org.apache.commons.lang3.mutable.MutableFloat;
 | |
| import org.apache.commons.lang3.mutable.MutableInt;
 | |
| import org.apache.commons.lang3.mutable.MutableObject;
 | |
| import org.jetbrains.annotations.Nullable;
 | |
| 
 | |
| @Environment(EnvType.CLIENT)
 | |
| public class StringSplitter {
 | |
| 	final StringSplitter.WidthProvider widthProvider;
 | |
| 
 | |
| 	public StringSplitter(StringSplitter.WidthProvider widthProvider) {
 | |
| 		this.widthProvider = widthProvider;
 | |
| 	}
 | |
| 
 | |
| 	public float stringWidth(@Nullable String content) {
 | |
| 		if (content == null) {
 | |
| 			return 0.0F;
 | |
| 		} else {
 | |
| 			MutableFloat mutableFloat = new MutableFloat();
 | |
| 			StringDecomposer.iterateFormatted(content, Style.EMPTY, (i, style, j) -> {
 | |
| 				mutableFloat.add(this.widthProvider.getWidth(j, style));
 | |
| 				return true;
 | |
| 			});
 | |
| 			return mutableFloat.floatValue();
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	public float stringWidth(FormattedText content) {
 | |
| 		MutableFloat mutableFloat = new MutableFloat();
 | |
| 		StringDecomposer.iterateFormatted(content, Style.EMPTY, (i, style, j) -> {
 | |
| 			mutableFloat.add(this.widthProvider.getWidth(j, style));
 | |
| 			return true;
 | |
| 		});
 | |
| 		return mutableFloat.floatValue();
 | |
| 	}
 | |
| 
 | |
| 	public float stringWidth(FormattedCharSequence content) {
 | |
| 		MutableFloat mutableFloat = new MutableFloat();
 | |
| 		content.accept((i, style, j) -> {
 | |
| 			mutableFloat.add(this.widthProvider.getWidth(j, style));
 | |
| 			return true;
 | |
| 		});
 | |
| 		return mutableFloat.floatValue();
 | |
| 	}
 | |
| 
 | |
| 	public int plainIndexAtWidth(String content, int maxWidth, Style style) {
 | |
| 		StringSplitter.WidthLimitedCharSink widthLimitedCharSink = new StringSplitter.WidthLimitedCharSink(maxWidth);
 | |
| 		StringDecomposer.iterate(content, style, widthLimitedCharSink);
 | |
| 		return widthLimitedCharSink.getPosition();
 | |
| 	}
 | |
| 
 | |
| 	public String plainHeadByWidth(String content, int maxWidth, Style style) {
 | |
| 		return content.substring(0, this.plainIndexAtWidth(content, maxWidth, style));
 | |
| 	}
 | |
| 
 | |
| 	public String plainTailByWidth(String content, int maxWidth, Style style) {
 | |
| 		MutableFloat mutableFloat = new MutableFloat();
 | |
| 		MutableInt mutableInt = new MutableInt(content.length());
 | |
| 		StringDecomposer.iterateBackwards(content, style, (j, stylex, k) -> {
 | |
| 			float f = mutableFloat.addAndGet(this.widthProvider.getWidth(k, stylex));
 | |
| 			if (f > maxWidth) {
 | |
| 				return false;
 | |
| 			} else {
 | |
| 				mutableInt.setValue(j);
 | |
| 				return true;
 | |
| 			}
 | |
| 		});
 | |
| 		return content.substring(mutableInt.intValue());
 | |
| 	}
 | |
| 
 | |
| 	public int formattedIndexByWidth(String content, int maxWidth, Style style) {
 | |
| 		StringSplitter.WidthLimitedCharSink widthLimitedCharSink = new StringSplitter.WidthLimitedCharSink(maxWidth);
 | |
| 		StringDecomposer.iterateFormatted(content, style, widthLimitedCharSink);
 | |
| 		return widthLimitedCharSink.getPosition();
 | |
| 	}
 | |
| 
 | |
| 	@Nullable
 | |
| 	public Style componentStyleAtWidth(FormattedText content, int maxWidth) {
 | |
| 		StringSplitter.WidthLimitedCharSink widthLimitedCharSink = new StringSplitter.WidthLimitedCharSink(maxWidth);
 | |
| 		return (Style)content.visit(
 | |
| 				(style, string) -> StringDecomposer.iterateFormatted(string, style, widthLimitedCharSink) ? Optional.empty() : Optional.of(style), Style.EMPTY
 | |
| 			)
 | |
| 			.orElse(null);
 | |
| 	}
 | |
| 
 | |
| 	@Nullable
 | |
| 	public Style componentStyleAtWidth(FormattedCharSequence content, int maxWidth) {
 | |
| 		StringSplitter.WidthLimitedCharSink widthLimitedCharSink = new StringSplitter.WidthLimitedCharSink(maxWidth);
 | |
| 		MutableObject<Style> mutableObject = new MutableObject<>();
 | |
| 		content.accept((i, style, j) -> {
 | |
| 			if (!widthLimitedCharSink.accept(i, style, j)) {
 | |
| 				mutableObject.setValue(style);
 | |
| 				return false;
 | |
| 			} else {
 | |
| 				return true;
 | |
| 			}
 | |
| 		});
 | |
| 		return mutableObject.getValue();
 | |
| 	}
 | |
| 
 | |
| 	public String formattedHeadByWidth(String content, int maxWidth, Style style) {
 | |
| 		return content.substring(0, this.formattedIndexByWidth(content, maxWidth, style));
 | |
| 	}
 | |
| 
 | |
| 	public FormattedText headByWidth(FormattedText content, int maxWidth, Style style) {
 | |
| 		final StringSplitter.WidthLimitedCharSink widthLimitedCharSink = new StringSplitter.WidthLimitedCharSink(maxWidth);
 | |
| 		return (FormattedText)content.visit(new FormattedText.StyledContentConsumer<FormattedText>() {
 | |
| 			private final ComponentCollector collector = new ComponentCollector();
 | |
| 
 | |
| 			@Override
 | |
| 			public Optional<FormattedText> accept(Style style, String string) {
 | |
| 				widthLimitedCharSink.resetPosition();
 | |
| 				if (!StringDecomposer.iterateFormatted(string, style, widthLimitedCharSink)) {
 | |
| 					String string2 = string.substring(0, widthLimitedCharSink.getPosition());
 | |
| 					if (!string2.isEmpty()) {
 | |
| 						this.collector.append(FormattedText.of(string2, style));
 | |
| 					}
 | |
| 
 | |
| 					return Optional.of(this.collector.getResultOrEmpty());
 | |
| 				} else {
 | |
| 					if (!string.isEmpty()) {
 | |
| 						this.collector.append(FormattedText.of(string, style));
 | |
| 					}
 | |
| 
 | |
| 					return Optional.empty();
 | |
| 				}
 | |
| 			}
 | |
| 		}, style).orElse(content);
 | |
| 	}
 | |
| 
 | |
| 	public int findLineBreak(String content, int maxWidth, Style style) {
 | |
| 		StringSplitter.LineBreakFinder lineBreakFinder = new StringSplitter.LineBreakFinder(maxWidth);
 | |
| 		StringDecomposer.iterateFormatted(content, style, lineBreakFinder);
 | |
| 		return lineBreakFinder.getSplitPosition();
 | |
| 	}
 | |
| 
 | |
| 	public static int getWordPosition(String content, int skipCount, int cursorPoint, boolean includeWhitespace) {
 | |
| 		int i = cursorPoint;
 | |
| 		boolean bl = skipCount < 0;
 | |
| 		int j = Math.abs(skipCount);
 | |
| 
 | |
| 		for (int k = 0; k < j; k++) {
 | |
| 			if (bl) {
 | |
| 				while (includeWhitespace && i > 0 && (content.charAt(i - 1) == ' ' || content.charAt(i - 1) == '\n')) {
 | |
| 					i--;
 | |
| 				}
 | |
| 
 | |
| 				while (i > 0 && content.charAt(i - 1) != ' ' && content.charAt(i - 1) != '\n') {
 | |
| 					i--;
 | |
| 				}
 | |
| 			} else {
 | |
| 				int l = content.length();
 | |
| 				int m = content.indexOf(32, i);
 | |
| 				int n = content.indexOf(10, i);
 | |
| 				if (m == -1 && n == -1) {
 | |
| 					i = -1;
 | |
| 				} else if (m != -1 && n != -1) {
 | |
| 					i = Math.min(m, n);
 | |
| 				} else if (m != -1) {
 | |
| 					i = m;
 | |
| 				} else {
 | |
| 					i = n;
 | |
| 				}
 | |
| 
 | |
| 				if (i == -1) {
 | |
| 					i = l;
 | |
| 				} else {
 | |
| 					while (includeWhitespace && i < l && (content.charAt(i) == ' ' || content.charAt(i) == '\n')) {
 | |
| 						i++;
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return i;
 | |
| 	}
 | |
| 
 | |
| 	public void splitLines(String content, int maxWidth, Style style, boolean withNewLines, StringSplitter.LinePosConsumer linePos) {
 | |
| 		int i = 0;
 | |
| 		int j = content.length();
 | |
| 		Style style2 = style;
 | |
| 
 | |
| 		while (i < j) {
 | |
| 			StringSplitter.LineBreakFinder lineBreakFinder = new StringSplitter.LineBreakFinder(maxWidth);
 | |
| 			boolean bl = StringDecomposer.iterateFormatted(content, i, style2, style, lineBreakFinder);
 | |
| 			if (bl) {
 | |
| 				linePos.accept(style2, i, j);
 | |
| 				break;
 | |
| 			}
 | |
| 
 | |
| 			int k = lineBreakFinder.getSplitPosition();
 | |
| 			char c = content.charAt(k);
 | |
| 			int l = c != '\n' && c != ' ' ? k : k + 1;
 | |
| 			linePos.accept(style2, i, withNewLines ? l : k);
 | |
| 			i = l;
 | |
| 			style2 = lineBreakFinder.getSplitStyle();
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	public List<FormattedText> splitLines(String content, int maxWidth, Style style) {
 | |
| 		List<FormattedText> list = Lists.<FormattedText>newArrayList();
 | |
| 		this.splitLines(content, maxWidth, style, false, (stylex, i, j) -> list.add(FormattedText.of(content.substring(i, j), stylex)));
 | |
| 		return list;
 | |
| 	}
 | |
| 
 | |
| 	public List<FormattedText> splitLines(FormattedText content, int maxWidth, Style style) {
 | |
| 		List<FormattedText> list = Lists.<FormattedText>newArrayList();
 | |
| 		this.splitLines(content, maxWidth, style, (formattedText, boolean_) -> list.add(formattedText));
 | |
| 		return list;
 | |
| 	}
 | |
| 
 | |
| 	public List<FormattedText> splitLines(FormattedText content, int maxWidth, Style style, FormattedText prefix) {
 | |
| 		List<FormattedText> list = Lists.<FormattedText>newArrayList();
 | |
| 		this.splitLines(content, maxWidth, style, (formattedText2, boolean_) -> list.add(boolean_ ? FormattedText.composite(prefix, formattedText2) : formattedText2));
 | |
| 		return list;
 | |
| 	}
 | |
| 
 | |
| 	public void splitLines(FormattedText content, int maxWidth, Style style, BiConsumer<FormattedText, Boolean> splitifier) {
 | |
| 		List<StringSplitter.LineComponent> list = Lists.<StringSplitter.LineComponent>newArrayList();
 | |
| 		content.visit((stylex, string) -> {
 | |
| 			if (!string.isEmpty()) {
 | |
| 				list.add(new StringSplitter.LineComponent(string, stylex));
 | |
| 			}
 | |
| 
 | |
| 			return Optional.empty();
 | |
| 		}, style);
 | |
| 		StringSplitter.FlatComponents flatComponents = new StringSplitter.FlatComponents(list);
 | |
| 		boolean bl = true;
 | |
| 		boolean bl2 = false;
 | |
| 		boolean bl3 = false;
 | |
| 
 | |
| 		while (bl) {
 | |
| 			bl = false;
 | |
| 			StringSplitter.LineBreakFinder lineBreakFinder = new StringSplitter.LineBreakFinder(maxWidth);
 | |
| 
 | |
| 			for (StringSplitter.LineComponent lineComponent : flatComponents.parts) {
 | |
| 				boolean bl4 = StringDecomposer.iterateFormatted(lineComponent.contents, 0, lineComponent.style, style, lineBreakFinder);
 | |
| 				if (!bl4) {
 | |
| 					int i = lineBreakFinder.getSplitPosition();
 | |
| 					Style style2 = lineBreakFinder.getSplitStyle();
 | |
| 					char c = flatComponents.charAt(i);
 | |
| 					boolean bl5 = c == '\n';
 | |
| 					boolean bl6 = bl5 || c == ' ';
 | |
| 					bl2 = bl5;
 | |
| 					FormattedText formattedText = flatComponents.splitAt(i, bl6 ? 1 : 0, style2);
 | |
| 					splitifier.accept(formattedText, bl3);
 | |
| 					bl3 = !bl5;
 | |
| 					bl = true;
 | |
| 					break;
 | |
| 				}
 | |
| 
 | |
| 				lineBreakFinder.addToOffset(lineComponent.contents.length());
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		FormattedText formattedText2 = flatComponents.getRemainder();
 | |
| 		if (formattedText2 != null) {
 | |
| 			splitifier.accept(formattedText2, bl3);
 | |
| 		} else if (bl2) {
 | |
| 			splitifier.accept(FormattedText.EMPTY, false);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	@Environment(EnvType.CLIENT)
 | |
| 	static class FlatComponents {
 | |
| 		final List<StringSplitter.LineComponent> parts;
 | |
| 		private String flatParts;
 | |
| 
 | |
| 		public FlatComponents(List<StringSplitter.LineComponent> parts) {
 | |
| 			this.parts = parts;
 | |
| 			this.flatParts = (String)parts.stream().map(lineComponent -> lineComponent.contents).collect(Collectors.joining());
 | |
| 		}
 | |
| 
 | |
| 		public char charAt(int codePoint) {
 | |
| 			return this.flatParts.charAt(codePoint);
 | |
| 		}
 | |
| 
 | |
| 		public FormattedText splitAt(int begin, int end, Style style) {
 | |
| 			ComponentCollector componentCollector = new ComponentCollector();
 | |
| 			ListIterator<StringSplitter.LineComponent> listIterator = this.parts.listIterator();
 | |
| 			int i = begin;
 | |
| 			boolean bl = false;
 | |
| 
 | |
| 			while (listIterator.hasNext()) {
 | |
| 				StringSplitter.LineComponent lineComponent = (StringSplitter.LineComponent)listIterator.next();
 | |
| 				String string = lineComponent.contents;
 | |
| 				int j = string.length();
 | |
| 				if (!bl) {
 | |
| 					if (i > j) {
 | |
| 						componentCollector.append(lineComponent);
 | |
| 						listIterator.remove();
 | |
| 						i -= j;
 | |
| 					} else {
 | |
| 						String string2 = string.substring(0, i);
 | |
| 						if (!string2.isEmpty()) {
 | |
| 							componentCollector.append(FormattedText.of(string2, lineComponent.style));
 | |
| 						}
 | |
| 
 | |
| 						i += end;
 | |
| 						bl = true;
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				if (bl) {
 | |
| 					if (i <= j) {
 | |
| 						String string2 = string.substring(i);
 | |
| 						if (string2.isEmpty()) {
 | |
| 							listIterator.remove();
 | |
| 						} else {
 | |
| 							listIterator.set(new StringSplitter.LineComponent(string2, style));
 | |
| 						}
 | |
| 						break;
 | |
| 					}
 | |
| 
 | |
| 					listIterator.remove();
 | |
| 					i -= j;
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			this.flatParts = this.flatParts.substring(begin + end);
 | |
| 			return componentCollector.getResultOrEmpty();
 | |
| 		}
 | |
| 
 | |
| 		@Nullable
 | |
| 		public FormattedText getRemainder() {
 | |
| 			ComponentCollector componentCollector = new ComponentCollector();
 | |
| 			this.parts.forEach(componentCollector::append);
 | |
| 			this.parts.clear();
 | |
| 			return componentCollector.getResult();
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	@Environment(EnvType.CLIENT)
 | |
| 	class LineBreakFinder implements FormattedCharSink {
 | |
| 		private final float maxWidth;
 | |
| 		private int lineBreak = -1;
 | |
| 		private Style lineBreakStyle = Style.EMPTY;
 | |
| 		private boolean hadNonZeroWidthChar;
 | |
| 		private float width;
 | |
| 		private int lastSpace = -1;
 | |
| 		private Style lastSpaceStyle = Style.EMPTY;
 | |
| 		private int nextChar;
 | |
| 		private int offset;
 | |
| 
 | |
| 		public LineBreakFinder(final float maxWidth) {
 | |
| 			this.maxWidth = Math.max(maxWidth, 1.0F);
 | |
| 		}
 | |
| 
 | |
| 		@Override
 | |
| 		public boolean accept(int i, Style style, int j) {
 | |
| 			int k = i + this.offset;
 | |
| 			switch (j) {
 | |
| 				case 10:
 | |
| 					return this.finishIteration(k, style);
 | |
| 				case 32:
 | |
| 					this.lastSpace = k;
 | |
| 					this.lastSpaceStyle = style;
 | |
| 				default:
 | |
| 					float f = StringSplitter.this.widthProvider.getWidth(j, style);
 | |
| 					this.width += f;
 | |
| 					if (!this.hadNonZeroWidthChar || !(this.width > this.maxWidth)) {
 | |
| 						this.hadNonZeroWidthChar |= f != 0.0F;
 | |
| 						this.nextChar = k + Character.charCount(j);
 | |
| 						return true;
 | |
| 					} else {
 | |
| 						return this.lastSpace != -1 ? this.finishIteration(this.lastSpace, this.lastSpaceStyle) : this.finishIteration(k, style);
 | |
| 					}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		private boolean finishIteration(int lineBreak, Style lineBreakStyle) {
 | |
| 			this.lineBreak = lineBreak;
 | |
| 			this.lineBreakStyle = lineBreakStyle;
 | |
| 			return false;
 | |
| 		}
 | |
| 
 | |
| 		private boolean lineBreakFound() {
 | |
| 			return this.lineBreak != -1;
 | |
| 		}
 | |
| 
 | |
| 		public int getSplitPosition() {
 | |
| 			return this.lineBreakFound() ? this.lineBreak : this.nextChar;
 | |
| 		}
 | |
| 
 | |
| 		public Style getSplitStyle() {
 | |
| 			return this.lineBreakStyle;
 | |
| 		}
 | |
| 
 | |
| 		public void addToOffset(int offset) {
 | |
| 			this.offset += offset;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	@Environment(EnvType.CLIENT)
 | |
| 	static class LineComponent implements FormattedText {
 | |
| 		final String contents;
 | |
| 		final Style style;
 | |
| 
 | |
| 		public LineComponent(String contents, Style style) {
 | |
| 			this.contents = contents;
 | |
| 			this.style = style;
 | |
| 		}
 | |
| 
 | |
| 		@Override
 | |
| 		public <T> Optional<T> visit(FormattedText.ContentConsumer<T> acceptor) {
 | |
| 			return acceptor.accept(this.contents);
 | |
| 		}
 | |
| 
 | |
| 		@Override
 | |
| 		public <T> Optional<T> visit(FormattedText.StyledContentConsumer<T> acceptor, Style style) {
 | |
| 			return acceptor.accept(this.style.applyTo(style), this.contents);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	@FunctionalInterface
 | |
| 	@Environment(EnvType.CLIENT)
 | |
| 	public interface LinePosConsumer {
 | |
| 		void accept(Style style, int i, int j);
 | |
| 	}
 | |
| 
 | |
| 	@Environment(EnvType.CLIENT)
 | |
| 	class WidthLimitedCharSink implements FormattedCharSink {
 | |
| 		private float maxWidth;
 | |
| 		private int position;
 | |
| 
 | |
| 		public WidthLimitedCharSink(final float maxWidth) {
 | |
| 			this.maxWidth = maxWidth;
 | |
| 		}
 | |
| 
 | |
| 		@Override
 | |
| 		public boolean accept(int i, Style style, int j) {
 | |
| 			this.maxWidth = this.maxWidth - StringSplitter.this.widthProvider.getWidth(j, style);
 | |
| 			if (this.maxWidth >= 0.0F) {
 | |
| 				this.position = i + Character.charCount(j);
 | |
| 				return true;
 | |
| 			} else {
 | |
| 				return false;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		public int getPosition() {
 | |
| 			return this.position;
 | |
| 		}
 | |
| 
 | |
| 		public void resetPosition() {
 | |
| 			this.position = 0;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	@FunctionalInterface
 | |
| 	@Environment(EnvType.CLIENT)
 | |
| 	public interface WidthProvider {
 | |
| 		float getWidth(int i, Style style);
 | |
| 	}
 | |
| }
 |