407 lines
15 KiB
Java
407 lines
15 KiB
Java
/*
|
|
* Decompiled with CFR 0.152.
|
|
*
|
|
* Could not load the following classes:
|
|
* com.google.common.collect.Lists
|
|
* org.apache.commons.lang3.mutable.MutableFloat
|
|
* org.apache.commons.lang3.mutable.MutableInt
|
|
* org.jspecify.annotations.Nullable
|
|
*/
|
|
package net.minecraft.client;
|
|
|
|
import com.google.common.collect.Lists;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.ListIterator;
|
|
import java.util.Optional;
|
|
import java.util.function.BiConsumer;
|
|
import java.util.stream.Collectors;
|
|
import net.minecraft.client.ComponentCollector;
|
|
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.jspecify.annotations.Nullable;
|
|
|
|
public class StringSplitter {
|
|
private final WidthProvider widthProvider;
|
|
|
|
public StringSplitter(WidthProvider widthProvider) {
|
|
this.widthProvider = widthProvider;
|
|
}
|
|
|
|
public float stringWidth(@Nullable String str) {
|
|
if (str == null) {
|
|
return 0.0f;
|
|
}
|
|
MutableFloat result = new MutableFloat();
|
|
StringDecomposer.iterateFormatted(str, Style.EMPTY, (position, style, codepoint) -> {
|
|
result.add(this.widthProvider.getWidth(codepoint, style));
|
|
return true;
|
|
});
|
|
return result.floatValue();
|
|
}
|
|
|
|
public float stringWidth(FormattedText text) {
|
|
MutableFloat result = new MutableFloat();
|
|
StringDecomposer.iterateFormatted(text, Style.EMPTY, (position, style, codepoint) -> {
|
|
result.add(this.widthProvider.getWidth(codepoint, style));
|
|
return true;
|
|
});
|
|
return result.floatValue();
|
|
}
|
|
|
|
public float stringWidth(FormattedCharSequence text) {
|
|
MutableFloat result = new MutableFloat();
|
|
text.accept((position, style, codepoint) -> {
|
|
result.add(this.widthProvider.getWidth(codepoint, style));
|
|
return true;
|
|
});
|
|
return result.floatValue();
|
|
}
|
|
|
|
public int plainIndexAtWidth(String str, int maxWidth, Style style) {
|
|
WidthLimitedCharSink output = new WidthLimitedCharSink(maxWidth);
|
|
StringDecomposer.iterate(str, style, output);
|
|
return output.getPosition();
|
|
}
|
|
|
|
public String plainHeadByWidth(String str, int maxWidth, Style style) {
|
|
return str.substring(0, this.plainIndexAtWidth(str, maxWidth, style));
|
|
}
|
|
|
|
public String plainTailByWidth(String str, int maxWidth, Style style) {
|
|
MutableFloat currentWidth = new MutableFloat();
|
|
MutableInt result = new MutableInt(str.length());
|
|
StringDecomposer.iterateBackwards(str, style, (position, s, codepoint) -> {
|
|
float w = currentWidth.addAndGet(this.widthProvider.getWidth(codepoint, s));
|
|
if (w > (float)maxWidth) {
|
|
return false;
|
|
}
|
|
result.setValue(position);
|
|
return true;
|
|
});
|
|
return str.substring(result.intValue());
|
|
}
|
|
|
|
public FormattedText headByWidth(FormattedText text, int width, Style initialStyle) {
|
|
final WidthLimitedCharSink output = new WidthLimitedCharSink(width);
|
|
return text.visit(new FormattedText.StyledContentConsumer<FormattedText>(){
|
|
private final ComponentCollector collector = new ComponentCollector();
|
|
|
|
@Override
|
|
public Optional<FormattedText> accept(Style style, String contents) {
|
|
output.resetPosition();
|
|
if (!StringDecomposer.iterateFormatted(contents, style, (FormattedCharSink)output)) {
|
|
String partial = contents.substring(0, output.getPosition());
|
|
if (!partial.isEmpty()) {
|
|
this.collector.append(FormattedText.of(partial, style));
|
|
}
|
|
return Optional.of(this.collector.getResultOrEmpty());
|
|
}
|
|
if (!contents.isEmpty()) {
|
|
this.collector.append(FormattedText.of(contents, style));
|
|
}
|
|
return Optional.empty();
|
|
}
|
|
}, initialStyle).orElse(text);
|
|
}
|
|
|
|
public int findLineBreak(String input, int max, Style initialStyle) {
|
|
LineBreakFinder finder = new LineBreakFinder(max);
|
|
StringDecomposer.iterateFormatted(input, initialStyle, (FormattedCharSink)finder);
|
|
return finder.getSplitPosition();
|
|
}
|
|
|
|
public static int getWordPosition(String text, int dir, int from, boolean stripSpaces) {
|
|
int result = from;
|
|
boolean reverse = dir < 0;
|
|
int abs = Math.abs(dir);
|
|
for (int i = 0; i < abs; ++i) {
|
|
if (reverse) {
|
|
while (stripSpaces && result > 0 && (text.charAt(result - 1) == ' ' || text.charAt(result - 1) == '\n')) {
|
|
--result;
|
|
}
|
|
while (result > 0 && text.charAt(result - 1) != ' ' && text.charAt(result - 1) != '\n') {
|
|
--result;
|
|
}
|
|
continue;
|
|
}
|
|
int length = text.length();
|
|
int index1 = text.indexOf(32, result);
|
|
int index2 = text.indexOf(10, result);
|
|
result = index1 == -1 && index2 == -1 ? -1 : (index1 != -1 && index2 != -1 ? Math.min(index1, index2) : (index1 != -1 ? index1 : index2));
|
|
if (result == -1) {
|
|
result = length;
|
|
continue;
|
|
}
|
|
while (stripSpaces && result < length && (text.charAt(result) == ' ' || text.charAt(result) == '\n')) {
|
|
++result;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public void splitLines(String input, int maxWidth, Style initialStyle, boolean includeAll, LinePosConsumer output) {
|
|
int start = 0;
|
|
int size = input.length();
|
|
Style workStyle = initialStyle;
|
|
while (start < size) {
|
|
LineBreakFinder finder = new LineBreakFinder(maxWidth);
|
|
boolean endOfText = StringDecomposer.iterateFormatted(input, start, workStyle, initialStyle, finder);
|
|
if (endOfText) {
|
|
output.accept(workStyle, start, size);
|
|
break;
|
|
}
|
|
int lineBreak = finder.getSplitPosition();
|
|
char firstTailChar = input.charAt(lineBreak);
|
|
int adjustedBreak = firstTailChar == '\n' || firstTailChar == ' ' ? lineBreak + 1 : lineBreak;
|
|
output.accept(workStyle, start, includeAll ? adjustedBreak : lineBreak);
|
|
start = adjustedBreak;
|
|
workStyle = finder.getSplitStyle();
|
|
}
|
|
}
|
|
|
|
public List<FormattedText> splitLines(String input, int maxWidth, Style initialStyle) {
|
|
ArrayList result = Lists.newArrayList();
|
|
this.splitLines(input, maxWidth, initialStyle, false, (style, start, end) -> result.add(FormattedText.of(input.substring(start, end), style)));
|
|
return result;
|
|
}
|
|
|
|
public List<FormattedText> splitLines(FormattedText input, int maxWidth, Style initialStyle) {
|
|
ArrayList result = Lists.newArrayList();
|
|
this.splitLines(input, maxWidth, initialStyle, (text, wrapped) -> result.add(text));
|
|
return result;
|
|
}
|
|
|
|
public void splitLines(FormattedText input, int maxWidth, Style initialStyle, BiConsumer<FormattedText, Boolean> output) {
|
|
ArrayList partList = Lists.newArrayList();
|
|
input.visit((style, contents) -> {
|
|
if (!contents.isEmpty()) {
|
|
partList.add(new LineComponent(contents, style));
|
|
}
|
|
return Optional.empty();
|
|
}, initialStyle);
|
|
FlatComponents parts = new FlatComponents(partList);
|
|
boolean shouldRestart = true;
|
|
boolean forceNewLine = false;
|
|
boolean isWrapped = false;
|
|
block0: while (shouldRestart) {
|
|
shouldRestart = false;
|
|
LineBreakFinder finder = new LineBreakFinder(maxWidth);
|
|
for (LineComponent part : parts.parts) {
|
|
boolean endOfText = StringDecomposer.iterateFormatted(part.contents, 0, part.style, initialStyle, finder);
|
|
if (!endOfText) {
|
|
int lineBreak = finder.getSplitPosition();
|
|
Style lineBreakStyle = finder.getSplitStyle();
|
|
char firstTailChar = parts.charAt(lineBreak);
|
|
boolean isNewLine = firstTailChar == '\n';
|
|
boolean skipNextChar = isNewLine || firstTailChar == ' ';
|
|
forceNewLine = isNewLine;
|
|
FormattedText result = parts.splitAt(lineBreak, skipNextChar ? 1 : 0, lineBreakStyle);
|
|
output.accept(result, isWrapped);
|
|
isWrapped = !isNewLine;
|
|
shouldRestart = true;
|
|
continue block0;
|
|
}
|
|
finder.addToOffset(part.contents.length());
|
|
}
|
|
}
|
|
FormattedText lastLine = parts.getRemainder();
|
|
if (lastLine != null) {
|
|
output.accept(lastLine, isWrapped);
|
|
} else if (forceNewLine) {
|
|
output.accept(FormattedText.EMPTY, false);
|
|
}
|
|
}
|
|
|
|
@FunctionalInterface
|
|
public static interface WidthProvider {
|
|
public float getWidth(int var1, Style var2);
|
|
}
|
|
|
|
private class WidthLimitedCharSink
|
|
implements FormattedCharSink {
|
|
private float maxWidth;
|
|
private int position;
|
|
|
|
public WidthLimitedCharSink(float maxWidth) {
|
|
this.maxWidth = maxWidth;
|
|
}
|
|
|
|
@Override
|
|
public boolean accept(int position, Style style, int codepoint) {
|
|
this.maxWidth -= StringSplitter.this.widthProvider.getWidth(codepoint, style);
|
|
if (this.maxWidth >= 0.0f) {
|
|
this.position = position + Character.charCount(codepoint);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public int getPosition() {
|
|
return this.position;
|
|
}
|
|
|
|
public void resetPosition() {
|
|
this.position = 0;
|
|
}
|
|
}
|
|
|
|
private 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(float maxWidth) {
|
|
this.maxWidth = Math.max(maxWidth, 1.0f);
|
|
}
|
|
|
|
@Override
|
|
public boolean accept(int position, Style style, int codepoint) {
|
|
int adjustedPosition = position + this.offset;
|
|
switch (codepoint) {
|
|
case 10: {
|
|
return this.finishIteration(adjustedPosition, style);
|
|
}
|
|
case 32: {
|
|
this.lastSpace = adjustedPosition;
|
|
this.lastSpaceStyle = style;
|
|
}
|
|
}
|
|
float charWidth = StringSplitter.this.widthProvider.getWidth(codepoint, style);
|
|
this.width += charWidth;
|
|
if (this.hadNonZeroWidthChar && this.width > this.maxWidth) {
|
|
if (this.lastSpace != -1) {
|
|
return this.finishIteration(this.lastSpace, this.lastSpaceStyle);
|
|
}
|
|
return this.finishIteration(adjustedPosition, style);
|
|
}
|
|
this.hadNonZeroWidthChar |= charWidth != 0.0f;
|
|
this.nextChar = adjustedPosition + Character.charCount(codepoint);
|
|
return true;
|
|
}
|
|
|
|
private boolean finishIteration(int lineBreak, Style style) {
|
|
this.lineBreak = lineBreak;
|
|
this.lineBreakStyle = style;
|
|
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 delta) {
|
|
this.offset += delta;
|
|
}
|
|
}
|
|
|
|
@FunctionalInterface
|
|
public static interface LinePosConsumer {
|
|
public void accept(Style var1, int var2, int var3);
|
|
}
|
|
|
|
private static class FlatComponents {
|
|
private final List<LineComponent> parts;
|
|
private String flatParts;
|
|
|
|
public FlatComponents(List<LineComponent> parts) {
|
|
this.parts = parts;
|
|
this.flatParts = parts.stream().map(p -> p.contents).collect(Collectors.joining());
|
|
}
|
|
|
|
public char charAt(int position) {
|
|
return this.flatParts.charAt(position);
|
|
}
|
|
|
|
public FormattedText splitAt(int skipPosition, int skipSize, Style splitStyle) {
|
|
ComponentCollector result = new ComponentCollector();
|
|
ListIterator<LineComponent> it = this.parts.listIterator();
|
|
int position = skipPosition;
|
|
boolean inSkip = false;
|
|
while (it.hasNext()) {
|
|
LineComponent element = it.next();
|
|
String contents = element.contents;
|
|
int contentsSize = contents.length();
|
|
if (!inSkip) {
|
|
if (position > contentsSize) {
|
|
result.append(element);
|
|
it.remove();
|
|
position -= contentsSize;
|
|
} else {
|
|
String beforeSplit = contents.substring(0, position);
|
|
if (!beforeSplit.isEmpty()) {
|
|
result.append(FormattedText.of(beforeSplit, element.style));
|
|
}
|
|
position += skipSize;
|
|
inSkip = true;
|
|
}
|
|
}
|
|
if (!inSkip) continue;
|
|
if (position > contentsSize) {
|
|
it.remove();
|
|
position -= contentsSize;
|
|
continue;
|
|
}
|
|
String afterSplit = contents.substring(position);
|
|
if (afterSplit.isEmpty()) {
|
|
it.remove();
|
|
break;
|
|
}
|
|
it.set(new LineComponent(afterSplit, splitStyle));
|
|
break;
|
|
}
|
|
this.flatParts = this.flatParts.substring(skipPosition + skipSize);
|
|
return result.getResultOrEmpty();
|
|
}
|
|
|
|
public @Nullable FormattedText getRemainder() {
|
|
ComponentCollector result = new ComponentCollector();
|
|
this.parts.forEach(result::append);
|
|
this.parts.clear();
|
|
return result.getResult();
|
|
}
|
|
}
|
|
|
|
private static class LineComponent
|
|
implements FormattedText {
|
|
private final String contents;
|
|
private final Style style;
|
|
|
|
public LineComponent(String contents, Style style) {
|
|
this.contents = contents;
|
|
this.style = style;
|
|
}
|
|
|
|
@Override
|
|
public <T> Optional<T> visit(FormattedText.ContentConsumer<T> output) {
|
|
return output.accept(this.contents);
|
|
}
|
|
|
|
@Override
|
|
public <T> Optional<T> visit(FormattedText.StyledContentConsumer<T> output, Style parentStyle) {
|
|
return output.accept(this.style.applyTo(parentStyle), this.contents);
|
|
}
|
|
}
|
|
}
|
|
|