package com.simon816.chatui.util; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.simon816.chatui.lib.ChatUILib; import com.simon816.chatui.lib.lang.LanguagePackManager; import org.spongepowered.api.text.Text; import org.spongepowered.api.text.TranslatableText; import org.spongepowered.api.text.action.ClickAction; import org.spongepowered.api.text.action.HoverAction; import org.spongepowered.api.text.action.ShiftClickAction; import org.spongepowered.api.text.format.TextColors; import org.spongepowered.api.text.format.TextFormat; import org.spongepowered.api.text.format.TextStyle; import org.spongepowered.api.text.format.TextStyles; import org.spongepowered.api.text.translation.Translation; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; class TextSplitter { private static class Format { private static final Format DEFAULTS = new Format(); public final TextFormat format; public final ClickAction<?> onClick; public final HoverAction<?> onHover; public final ShiftClickAction<?> onShiftClick; private Format() { this.format = TextFormat.of(TextColors.RESET, TextStyles.RESET); this.onClick = null; this.onHover = null; this.onShiftClick = null; } public Format(Text root) { this(DEFAULTS, root); } public Format(Format parent, Text text) { this.format = TextFormat.of(text.getColor() == TextColors.NONE ? parent.format.getColor() : text.getColor(), inheritStyle(parent.format.getStyle(), text.getStyle())); this.onClick = text.getClickAction().orElse(parent.onClick); this.onHover = text.getHoverAction().orElse(parent.onHover); this.onShiftClick = text.getShiftClickAction().orElse(parent.onShiftClick); } private static TextStyle inheritStyle(TextStyle base, TextStyle overrides) { // styles from base always exist as it must derive from DEFAULTS return new TextStyle(overrides.isBold().orElse(base.isBold().get()), overrides.isItalic().orElse(base.isItalic().get()), overrides.hasUnderline().orElse(base.hasUnderline().get()), overrides.hasStrikethrough().orElse(base.hasStrikethrough().get()), overrides.isObfuscated().orElse(base.isObfuscated().get())); } public Format with(Text text) { return new Format(this, text); } public Text.Builder applyToBuilder(Text.Builder builder) { return builder.format(this.format).onClick(this.onClick).onHover(this.onHover).onShiftClick(this.onShiftClick); } public Text createText(String content) { return applyToBuilder(Text.builder(content)).build(); } } private static final Splitter LINE_SPLITTER = Splitter.on('\n'); private static final Pattern MARKER_PATTERN = Pattern.compile("\\$MARKER(\\d+)\\$"); public static void splitLines(Text original, List<Text> output, int maxWidth, Locale locale, TextUtils utils) { if (maxWidth < 1) { throw new IllegalArgumentException("Max width must be at least 1, was " + maxWidth); } Stack<Format> formatStack = new Stack<>(); Object[] ret = apply(0, Text.builder(), formatStack, original, output, maxWidth, locale, utils); output.add(((Text.Builder) ret[1]).build()); } private static Object[] apply(int currLineLength, Text.Builder currLineBuilder, Stack<Format> formatStack, Text text, List<Text> output, int maxWidth, Locale locale, TextUtils utils) { if (text instanceof TranslatableText) { text = transformTranslationText((TranslatableText) text, locale); } Format format = pushFormat(formatStack, text); String plainText = text.toPlainSingle(); List<String> lines = LINE_SPLITTER.splitToList(plainText); boolean first = true; String next = null; for (int i = 0; next != null || i < lines.size(); i++) { String line; if (next != null) { line = next; next = null; i--; } else { line = lines.get(i); } if (!first) { output.add(currLineBuilder.build()); currLineBuilder = Text.builder(); currLineLength = 0; } first = false; if (line.isEmpty()) { continue; } boolean isBold = format.format.getStyle().isBold().get(); int lineW = utils.getStringWidth(line, isBold); if (currLineLength + lineW > maxWidth) { String oldLine = line; int trimPos = trimToMaxWidth(oldLine, isBold, maxWidth - currLineLength, utils); line = oldLine.substring(0, trimPos); if (currLineLength == 0 && line.isEmpty()) { // Cannot fit this within the maxWidth break; // give up } lineW = utils.getStringWidth(line, isBold); next = oldLine.substring(trimPos); } currLineLength += lineW; currLineBuilder.append(format.createText(line)); } for (Text child : text.getChildren()) { Object[] ret = apply(currLineLength, currLineBuilder, formatStack, child, output, maxWidth, locale, utils); currLineLength = (Integer) ret[0]; currLineBuilder = (Text.Builder) ret[1]; } formatStack.pop(); return new Object[] {currLineLength, currLineBuilder}; } private static Format pushFormat(Stack<Format> formatStack, Text text) { Format format; if (formatStack.isEmpty()) { format = new Format(text); } else { format = formatStack.peek().with(text); } formatStack.push(format); return format; } // Transforms a TranslatableText into a LiteralText private static Text transformTranslationText(TranslatableText text, Locale locale) { // This is bad, don't look Translation translation = text.getTranslation(); ImmutableList<Object> arguments = text.getArguments(); Object[] markers = new Object[arguments.size()]; for (int i = 0; i < markers.length; i++) { markers[i] = "$MARKER" + i + "$"; } LanguagePackManager langMgr = ChatUILib.getInstance().getLanguageManager(); if (!langMgr.isDefault(locale)) { translation = langMgr.forTranslation(translation); } String patched = translation.get(locale, markers); List<Object> sections = Lists.newArrayList(); Matcher m = MARKER_PATTERN.matcher(patched); int prevPos = 0; while (m.find()) { if (m.start() != prevPos) { sections.add(patched.substring(prevPos, m.start())); } int index = Integer.parseInt(m.group(1)); sections.add(arguments.get(index)); prevPos = m.end(); } if (prevPos != patched.length() || prevPos == 0) { sections.add(patched.substring(prevPos)); } Text.Builder builder = new Format(text).applyToBuilder(Text.builder()); for (Object val : sections) { builder.append(Text.of(val)); } builder.append(text.getChildren()); return builder.build(); } private static int trimToMaxWidth(String text, boolean bold, int maxWidth, TextUtils utils) { int currLen = 0; int pos = 0; while (currLen < maxWidth && pos < text.length()) { currLen += utils.getWidth(text.codePointAt(pos++), bold); } if (currLen > maxWidth) { pos--; } return pos; } public static void splitLines(String original, List<String> output, int maxWidth, TextUtils utils) { String next = null; Iterator<String> iterator = LINE_SPLITTER.split(original).iterator(); int currLineWidth = 0; while (iterator.hasNext() || next != null) { String part = next != null ? next : iterator.next(); next = null; int partW = utils.getStringWidth(part, false); if (partW + currLineWidth > maxWidth) { String oldPart = part; int trimPos = trimToMaxWidth(oldPart, false, maxWidth - currLineWidth, utils); part = part.substring(0, trimPos); if (currLineWidth == 0 && part.isEmpty()) { break; } next = oldPart.substring(trimPos); partW = utils.getStringWidth(part, false); currLineWidth = 0; } else { currLineWidth += partW; } output.add(part); } } }