PandaLib/Chat/src/main/java/fr/pandacube/lib/chat/ChatUtil.java

710 lines
20 KiB
Java
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package fr.pandacube.lib.chat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.format.TextDecoration.State;
import net.md_5.bungee.api.ChatColor;
import fr.pandacube.lib.chat.Chat.FormatableChat;
public class ChatUtil {
public static final int DEFAULT_CHAR_SIZE = 6;
public static final Map<Integer, String> CHARS_SIZE = Map.ofEntries(
Map.entry(-6, "§"),
Map.entry(2, "!.,:;i|¡'"),
Map.entry(3, "`lìí"),
Map.entry(4, " I[]tï×"),
Map.entry(5, "\"()*<>fk{}"),
Map.entry(7, "@~®©«»"),
Map.entry(9, "├└")
);
public static final int DEFAULT_CHAT_WIDTH = 320;
public static final int SIGN_WIDTH = 90;
public static final int BOOK_WIDTH = 116;
public static final int MOTD_WIDTH = 270;
public static final int BEDROCK_FORM_WIDE_BUTTON = 178;
public static final int CONSOLE_NB_CHAR_DEFAULT = 50;
public static FormatableChat createURLLink(String text, String url) {
return createURLLink(ChatStatic.legacyText(text), url, null);
}
public static FormatableChat createURLLink(String text, String url, String hoverText) {
return createURLLink(ChatStatic.legacyText(text), url, hoverText != null ? ChatStatic.legacyText(hoverText) : null);
}
/* package */ static FormatableChat createURLLink(Chat element, String url, Chat hover) {
String dispURL = (url.length() > 50) ? (url.substring(0, 48) + "...") : url;
return (FormatableChat) ChatStatic.chat()
.clickURL(url)
.urlColor()
.hover(
hover != null ? hover : Chat.text(dispURL)
)
.then(element);
}
public static FormatableChat createCommandLink(String text, String commandWithSlash, String hoverText) {
return createCommandLink(text, commandWithSlash, hoverText == null ? null : ChatStatic.legacyText(hoverText));
}
public static FormatableChat createCommandLink(String text, String commandWithSlash, Chat hoverText) {
return createCommandLink(ChatStatic.legacyText(text), commandWithSlash, hoverText);
}
/* package */ static FormatableChat createCommandLink(Chat d, String commandWithSlash, Chat hoverText) {
FormatableChat c = ChatStatic.chat()
.clickCommand(commandWithSlash)
.commandColor();
if (hoverText != null)
c.hover(hoverText);
return (FormatableChat) c.then(d);
}
public static FormatableChat createCommandSuggest(String text, String commandWithSlash, String hoverText) {
return createCommandSuggest(text, commandWithSlash, hoverText == null ? null : ChatStatic.legacyText(hoverText));
}
public static FormatableChat createCommandSuggest(String text, String commandWithSlash, Chat hoverText) {
return createCommandSuggest(ChatStatic.legacyText(text), commandWithSlash, hoverText);
}
/* package */ static FormatableChat createCommandSuggest(Chat d, String commandWithSlash, Chat hoverText) {
FormatableChat c = ChatStatic.chat()
.clickSuggest(commandWithSlash)
.commandColor();
if (hoverText != null)
c.hover(hoverText);
return (FormatableChat) c.then(d);
}
/**
* @param cmdFormat the command with %d inside to be replaced with the page number (must start with slash)
*/
public static Chat createPagination(String prefix, String cmdFormat, int currentPage, int nbPages, int nbPagesToDisplay) {
Set<Integer> pagesToDisplay = new TreeSet<>();
for (int i = 0; i < nbPagesToDisplay && i < nbPages && nbPages - i > 0; i++) {
pagesToDisplay.add(i + 1);
pagesToDisplay.add(nbPages - i);
}
for (int i = currentPage - nbPagesToDisplay + 1; i < currentPage + nbPagesToDisplay; i++) {
if (i > 0 && i <= nbPages)
pagesToDisplay.add(i);
}
Chat d = ChatStatic.chat().thenLegacyText(prefix);
boolean first = true;
int previous = 0;
for (int page : pagesToDisplay) {
if (!first) {
if (page == previous + 1) {
d.thenText(" ");
}
else {
if (cmdFormat.endsWith("%d")) {
d.thenText(" ");
d.then(createCommandSuggest("...", cmdFormat.substring(0, cmdFormat.length() - 2), "Choisir la page"));
d.thenText(" ");
}
else
d.thenText(" ... ");
}
}
else
first = false;
FormatableChat pDisp = createCommandLink(Integer.toString(page), String.format(cmdFormat, page), "Aller à la page " + page);
if (page == currentPage) {
pDisp.highlightedCommandColor();
}
d.then(pDisp);
previous = page;
}
return d;
}
/**
* @param decorationColor support null values
*/
public static Chat centerText(Chat text, char repeatedChar, TextColor decorationColor, boolean console) {
return centerText(text, repeatedChar, decorationColor, console, console ? CONSOLE_NB_CHAR_DEFAULT : DEFAULT_CHAT_WIDTH);
}
public static Chat centerText(Chat text, char repeatedChar, TextColor decorationColor, boolean console, int maxWidth) {
return centerText(text, repeatedChar, decorationColor, false, console, maxWidth);
}
/**
* @param decorationColor support null values
*/
public static Chat centerText(Chat text, char repeatedChar, TextColor decorationColor, boolean decorationBold, boolean console, int maxWidth) {
int textWidth = componentWidth(text.getAdv(), console);
if (textWidth > maxWidth)
return text;
int repeatedCharWidth = charW(repeatedChar, console, decorationBold);
int sideWidth = (maxWidth - textWidth) / 2;
int sideNbChar = sideWidth / repeatedCharWidth;
if (sideNbChar == 0)
return text;
String sideChars = repeatedChar(repeatedChar, sideNbChar);
FormatableChat side = ChatStatic.text(sideChars).color(decorationColor);
if (decorationBold)
side.bold();
Chat d = Chat.chat()
.then(side)
.then(text);
if (repeatedChar != ' ')
d.then(side);
return d;
}
public static Chat leftText(Chat text, char repeatedChar, TextColor decorationColor, int nbLeft, boolean console) {
return leftText(text, repeatedChar, decorationColor, nbLeft, console, console ? CONSOLE_NB_CHAR_DEFAULT : DEFAULT_CHAT_WIDTH);
}
public static Chat leftText(Chat text, char repeatedChar, TextColor decorationColor, int nbLeft, boolean console, int maxWidth) {
int textWidth = componentWidth(text.getAdv(), console);
int repeatedCharWidth = charW(repeatedChar, console, false);
int leftWidth = nbLeft * repeatedCharWidth;
if (textWidth + leftWidth > maxWidth)
return text;
int rightNbChar = (maxWidth - (textWidth + leftWidth)) / repeatedCharWidth;
Chat d = ChatStatic.chat()
.then(ChatStatic.text(repeatedChar(repeatedChar, nbLeft)).color(decorationColor))
.then(text);
if (repeatedChar != ' ') {
d.then(ChatStatic.text(repeatedChar(repeatedChar, rightNbChar)).color(decorationColor));
}
return d;
}
public static Chat rightText(Chat text, char repeatedChar, TextColor decorationColor, int nbRight, boolean console) {
return rightText(text, repeatedChar, decorationColor, nbRight, console, console ? CONSOLE_NB_CHAR_DEFAULT : DEFAULT_CHAT_WIDTH);
}
public static Chat rightText(Chat text, char repeatedChar, TextColor decorationColor, int nbRight,
boolean console, int maxWidth) {
int textWidth = componentWidth(text.getAdv(), console);
int repeatedCharWidth = charW(repeatedChar, console, false);
int rightWidth = nbRight * repeatedCharWidth;
if (textWidth + rightWidth > maxWidth)
return text;
int leftNbChar = (maxWidth - (textWidth + rightWidth)) / repeatedCharWidth;
Chat d = ChatStatic.chat()
.then(ChatStatic.text(repeatedChar(repeatedChar, leftNbChar)).color(decorationColor))
.then(text);
if (repeatedChar != ' ') {
d.then(ChatStatic.text(repeatedChar(repeatedChar, nbRight)).color(decorationColor));
}
return d;
}
public static Chat emptyLine(char repeatedChar, TextColor decorationColor, boolean console) {
return emptyLine(repeatedChar, decorationColor, false, console);
}
public static Chat emptyLine(char repeatedChar, TextColor decorationColor, boolean decorationBold, boolean console) {
return emptyLine(repeatedChar, decorationColor, decorationBold, console, (console) ? CONSOLE_NB_CHAR_DEFAULT : DEFAULT_CHAT_WIDTH);
}
public static Chat emptyLine(char repeatedChar, TextColor decorationColor, boolean decorationBold, boolean console, int maxWidth) {
int count = maxWidth / charW(repeatedChar, console, decorationBold);
FormatableChat line = ChatStatic.text(repeatedChar(repeatedChar, count)).color(decorationColor);
if (decorationBold)
line.bold();
return line;
}
private static String repeatedChar(char repeatedChar, int count) {
char[] c = new char[count];
Arrays.fill(c, repeatedChar);
return new String(c);
}
public static int componentWidth(Component component, boolean console) {
return componentWidth(component, console, false);
}
public static int componentWidth(Component component, boolean console, boolean parentBold) {
if (component == null)
return 0;
int count = 0;
State currentBold = component.style().decoration(TextDecoration.BOLD);
boolean actuallyBold = childBold(parentBold, currentBold);
if (component instanceof TextComponent) {
count += strWidth(((TextComponent)component).content(), console, actuallyBold);
}
else if (component instanceof TranslatableComponent) {
for (Component c : ((TranslatableComponent)component).args())
count += componentWidth(c, console, actuallyBold);
}
for (Component c : component.children())
count += componentWidth(c, console, actuallyBold);
return count;
}
private static boolean childBold(boolean parent, TextDecoration.State child) {
return (parent && child != State.FALSE) || (!parent && child == State.TRUE);
}
public static int strWidth(String str, boolean console, boolean bold) {
int count = 0;
for (char c : str.toCharArray())
count += charW(c, console, bold);
return Math.max(count, 0);
}
public static int charW(char c, boolean console, boolean bold) {
if (console) return (c == '§') ? -1 : 1;
for (int px : CHARS_SIZE.keySet())
if (CHARS_SIZE.get(px).indexOf(c) >= 0) return px + (bold ? 1 : 0);
return 6 + (bold ? 1 : 0);
}
public static List<Chat> wrapInLimitedPixelsToChat(String legacyText, int pixelWidth) {
return wrapInLimitedPixels(legacyText, pixelWidth).stream()
.map(ChatStatic::legacyText)
.collect(Collectors.toList());
}
public static List<String> wrapInLimitedPixels(String legacyText, int pixelWidth) {
List<String> lines = new ArrayList<>();
legacyText += "\n"; // workaround to force algorithm to compute last lines;
String currentLine = "";
int currentLineSize = 0;
int index = 0;
StringBuilder currentWord = new StringBuilder();
int currentWordSize = 0;
boolean bold = false;
boolean firstCharCurrentWordBold = false;
do {
char c = legacyText.charAt(index);
if (c == ChatColor.COLOR_CHAR && index < legacyText.length() - 1) {
currentWord.append(c);
c = legacyText.charAt(++index);
currentWord.append(c);
if (c == 'l' || c == 'L') // bold
bold = true;
if ((c >= '0' && c <= '9') // reset bold
|| (c >= 'a' && c <= 'f')
|| (c >= 'A' && c <= 'F')
|| c == 'r' || c == 'R'
|| c == 'x' || c == 'X')
bold = false;
}
else if (c == ' ') {
if (currentLineSize + currentWordSize > pixelWidth && currentLineSize > 0) { // wrap before word
lines.add(currentLine);
String lastStyle = ChatColorUtil.getLastColors(currentLine);
if (currentWord.charAt(0) == ' ') {
currentWord = new StringBuilder(currentWord.substring(1));
currentWordSize -= charW(' ', false, firstCharCurrentWordBold);
}
currentLine = (lastStyle.equals("§r") ? "" : lastStyle) + currentWord;
currentLineSize = currentWordSize;
}
else {
currentLine += currentWord;
currentLineSize += currentWordSize;
}
currentWord = new StringBuilder("" + c);
currentWordSize = charW(c, false, bold);
firstCharCurrentWordBold = bold;
}
else if (c == '\n') {
if (currentLineSize + currentWordSize > pixelWidth && currentLineSize > 0) { // wrap before word
lines.add(currentLine);
String lastStyle = ChatColorUtil.getLastColors(currentLine);
if (currentWord.charAt(0) == ' ') {
currentWord = new StringBuilder(currentWord.substring(1));
}
currentLine = (lastStyle.equals("§r") ? "" : lastStyle) + currentWord;
}
else {
currentLine += currentWord;
}
// wrap after
lines.add(currentLine);
String lastStyle = ChatColorUtil.getLastColors(currentLine);
currentLine = lastStyle.equals("§r") ? "" : lastStyle;
currentLineSize = 0;
currentWord = new StringBuilder();
currentWordSize = 0;
firstCharCurrentWordBold = bold;
}
else {
currentWord.append(c);
currentWordSize += charW(c, false, bold);
}
} while(++index < legacyText.length());
return lines;
}
public static List<Component> renderTable(List<List<Chat>> rows, String space, boolean console) {
List<List<Component>> compRows = new ArrayList<>(rows.size());
for (List<Chat> row : rows) {
List<Component> compRow = new ArrayList<>(row.size());
for (Chat c : row) {
compRow.add(c.getAdv());
}
compRows.add(compRow);
}
return renderTableComp(compRows, space, console);
}
public static List<Component> renderTableComp(List<List<Component>> rows, String space, boolean console) {
// determine columns width
List<Integer> nbPixelPerColumn = new ArrayList<>();
for (List<Component> row : rows) {
for (int i = 0; i < row.size(); i++) {
int w = componentWidth(row.get(i), console);
if (nbPixelPerColumn.size() <= i)
nbPixelPerColumn.add(w);
else if (nbPixelPerColumn.get(i) < w)
nbPixelPerColumn.set(i, w);
}
}
// create the lines with appropriate spacing
List<Component> spacedRows = new ArrayList<>(rows.size());
for (List<Component> row : rows) {
Chat spacedRow = Chat.chat();
for (int i = 0; i < row.size() - 1; i++) {
int w = componentWidth(row.get(i), console);
int padding = nbPixelPerColumn.get(i) - w;
spacedRow.then(row.get(i));
spacedRow.then(customWidthSpace(padding, console));
spacedRow.thenText(space);
}
if (!row.isEmpty())
spacedRow.then(row.get(row.size() - 1));
spacedRows.add(spacedRow.getAdv());
}
return spacedRows;
}
public static Component customWidthSpace(int width, boolean console) {
if (console)
return Chat.text(" ".repeat(width)).getAdv();
return switch (width) {
case 0, 1 -> Component.empty();
case 2 -> Chat.text(".").black().getAdv();
case 3 -> Chat.text("`").black().getAdv();
case 6 -> Chat.text(". ").black().getAdv();
case 7 -> Chat.text("` ").black().getAdv();
case 11 -> Chat.text("` ").black().getAdv();
default -> {
int nbSpace = width / 4;
int nbBold = width % 4;
int nbNotBold = nbSpace - nbBold;
if (nbNotBold > 0) {
if (nbBold > 0) {
yield Chat.text(" ".repeat(nbNotBold)).bold(false)
.then(Chat.text(" ".repeat(nbBold)).bold(true))
.getAdv();
}
else
yield Chat.text(" ".repeat(nbNotBold)).bold(false).getAdv();
}
else if (nbBold > 0) {
yield Chat.text(" ".repeat(nbBold)).bold(true).getAdv();
}
throw new IllegalStateException("Should not be here (width=" + width + "; nbSpace=" + nbSpace + "; nbBold=" + nbBold + "; nbNotBold=" + nbNotBold + ")");
}
};
// "." is 2 px
// "`" is 3 px
// " " is 4 px
// 0 ""
// 1 ""
// 2 "."
// 3 "`"
// 4 " "
// 5 "§l "
// 6 ". "
// 7 "` "
// 8 " "
// 9 " §l "
// 10 "§l "
// 11 "` "
// 12 " "
}
private static final String PROGRESS_BAR_START = "[";
private static final String PROGRESS_BAR_END = "]";
private static final TextColor PROGRESS_BAR_EMPTY_COLOR = NamedTextColor.DARK_GRAY;
private static final char PROGRESS_BAR_EMPTY_CHAR = '.';
private static final char PROGRESS_BAR_FULL_CHAR = '|';
public static Chat progressBar(double[] values, TextColor[] colors, double total, int pixelWidth, boolean console) {
// 1. Compute char size for each values
int progressPixelWidth = pixelWidth - strWidth(PROGRESS_BAR_START + PROGRESS_BAR_END, console, false);
int charPixelWidth = charW(PROGRESS_BAR_EMPTY_CHAR, console, false);
assert charPixelWidth == charW(PROGRESS_BAR_FULL_CHAR, console, false) : "PROGRESS_BAR_EMPTY_CHAR and PROGRESS_BAR_FULL_CHAR should have the same pixel width according to #charW(...)";
int progressCharWidth = progressPixelWidth / charPixelWidth;
int[] sizes = new int[values.length];
double sumValuesBefore = 0;
int sumSizesBefore = 0;
for (int i = 0; i < values.length; i++) {
sumValuesBefore += values[i];
int charPosition = Math.min((int) Math.round(progressCharWidth * sumValuesBefore / total), progressCharWidth);
sizes[i] = charPosition - sumSizesBefore;
sumSizesBefore += sizes[i];
}
// 2. Generate rendered text
Chat c = ChatStatic.text(PROGRESS_BAR_START);
int sumSizes = 0;
for (int i = 0; i < sizes.length; i++) {
sumSizes += sizes[i];
FormatableChat subC = ChatStatic.text(repeatedChar(PROGRESS_BAR_FULL_CHAR, sizes[i]));
if (colors != null && i < colors.length && colors[i] != null)
subC.color(colors[i]);
c.then(subC);
}
return c
.then(ChatStatic.text(repeatedChar(PROGRESS_BAR_EMPTY_CHAR, progressCharWidth - sumSizes))
.color(PROGRESS_BAR_EMPTY_COLOR))
.thenText(PROGRESS_BAR_END);
}
public static Chat progressBar(double value, TextColor color, double total, int pixelWidth, boolean console) {
return progressBar(new double[] { value }, new TextColor[] { color }, total, pixelWidth, console);
}
public static String truncatePrefix(String prefix, int maxLength) {
if (prefix.length() > maxLength) {
String lastColor = ChatColorUtil.getLastColors(prefix);
prefix = truncateAtLengthWithoutReset(prefix, maxLength);
if (!ChatColorUtil.getLastColors(prefix).equals(lastColor))
prefix = truncateAtLengthWithoutReset(prefix, maxLength - lastColor.length()) + lastColor;
}
return prefix;
}
public static String truncateAtLengthWithoutReset(String prefix, int l) {
if (prefix.length() > l) {
prefix = prefix.substring(0, l);
if (prefix.endsWith("§"))
prefix = prefix.substring(0, prefix.length()-1);
}
return prefix;
}
private static final String TREE_MIDDLE_CONNECTED = "";
private static final String TREE_END_CONNECTED = "";
private static final String TREE_MIDDLE_OPEN = "│§0`§r";
private static final String TREE_END_OPEN = "§0```§r";
private static final String TREE_MIDDLE_OPEN_CONSOLE = "";
private static final String TREE_END_OPEN_CONSOLE = " "; // nbsp
/**
* Generate a tree view based on the tree structure {@code node}.
*
* Each element in the returned list represent 1 line of the tree view.
* Thus, the caller may send each line separately or at once depending of the quantity of data.
* @return A array of component, each element being a single line.
*/
public static List<Chat> treeView(DisplayTreeNode node, boolean console) {
List<Chat> ret = new ArrayList<>();
ret.add(ChatStatic.chat()
.then(node.component));
for (int i = 0; i < node.children.size(); i++) {
List<Chat> childComponents = treeView(node.children.get(i), console);
boolean last = i == node.children.size() - 1;
for (int j = 0; j < childComponents.size(); j++) {
String prefix = last ? (j == 0 ? TREE_END_CONNECTED : (console ? TREE_END_OPEN_CONSOLE : TREE_END_OPEN))
: (j == 0 ? TREE_MIDDLE_CONNECTED : (console ? TREE_MIDDLE_OPEN_CONSOLE : TREE_MIDDLE_OPEN));
ret.add(ChatStatic.text(prefix)
.then(childComponents.get(j)));
}
}
return ret;
}
public static class DisplayTreeNode {
public final Chat component;
public final List<DisplayTreeNode> children = new ArrayList<>();
public DisplayTreeNode(Chat cmp) {
component = cmp;
}
public DisplayTreeNode addChild(DisplayTreeNode child) {
children.add(child);
return this;
}
}
}