minecraft-src/net/minecraft/client/StringSplitter.java
2025-07-04 01:41:11 +03:00

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