diff --git a/src/main/java/dan200/computercraft/shared/command/framework/TextFormatter.java b/src/main/java/dan200/computercraft/shared/command/framework/TextFormatter.java new file mode 100644 index 000000000..a32b0d140 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/command/framework/TextFormatter.java @@ -0,0 +1,150 @@ +package dan200.computercraft.shared.command.framework; + +import net.minecraft.command.ICommandSender; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.TextFormatting; +import net.minecraftforge.common.util.FakePlayer; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; + +import static dan200.computercraft.shared.command.framework.ChatHelpers.coloured; + +/** + * Adapated from Sponge's PaginationCalculator + */ +public class TextFormatter +{ + private static final int SPACE_WIDTH = 4; + private static final char PADDING_CHAR = 'ˌ'; + + /** + * Yoinked from FontRenderer + * + * @see net.minecraft.client.gui.FontRenderer#charWidth + * @see net.minecraft.client.gui.FontRenderer#getCharWidth(char) + */ + private static final String CHARACTERS = "\u00c0\u00c1\u00c2\u00c8\u00ca\u00cb\u00cd\u00d3\u00d4\u00d5\u00da\u00df\u00e3\u00f5\u011f\u0130\u0131\u0152\u0153\u015e\u015f\u0174\u0175\u017e\u0207\u0000\u0000\u0000\u0000\u0000\u0000\u0000 !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u0000\u00c7\u00fc\u00e9\u00e2\u00e4\u00e0\u00e5\u00e7\u00ea\u00eb\u00e8\u00ef\u00ee\u00ec\u00c4\u00c5\u00c9\u00e6\u00c6\u00f4\u00f6\u00f2\u00fb\u00f9\u00ff\u00d6\u00dc\u00f8\u00a3\u00d8\u00d7\u0192\u00e1\u00ed\u00f3\u00fa\u00f1\u00d1\u00aa\u00ba\u00bf\u00ae\u00ac\u00bd\u00bc\u00a1\u00ab\u00bb\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510\u2514\u2534\u252c\u251c\u2500\u253c\u255e\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256b\u256a\u2518\u250c\u2588\u2584\u258c\u2590\u2580\u03b1\u03b2\u0393\u03c0\u03a3\u03c3\u03bc\u03c4\u03a6\u0398\u03a9\u03b4\u221e\u2205\u2208\u2229\u2261\u00b1\u2265\u2264\u2320\u2321\u00f7\u2248\u00b0\u2219\u00b7\u221a\u207f\u00b2\u25a0\u0000"; + private static final int[] CHAR_WIDTHS = new int[]{ + 6, 6, 6, 6, 6, 6, 4, 6, 6, 6, 6, 6, 6, 6, 6, 4, + 4, 6, 7, 6, 6, 6, 6, 6, 6, 1, 1, 1, 1, 1, 1, 1, + 1, 2, 5, 6, 6, 6, 6, 3, 5, 5, 5, 6, 2, 6, 2, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 2, 2, 5, 6, 5, 6, + 7, 6, 6, 6, 6, 6, 6, 6, 6, 4, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 6, 4, 6, 6, + 3, 6, 6, 6, 6, 6, 5, 6, 6, 2, 6, 5, 3, 6, 6, 6, + 6, 6, 6, 6, 4, 6, 6, 6, 6, 6, 6, 5, 2, 5, 7, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 6, 3, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 6, + 6, 3, 6, 6, 6, 6, 6, 6, 6, 7, 6, 6, 6, 2, 6, 6, + 8, 9, 9, 6, 6, 6, 8, 8, 6, 8, 8, 8, 8, 8, 6, 6, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 6, 9, 9, 9, 5, 9, 9, + 8, 7, 7, 8, 7, 8, 8, 8, 7, 8, 8, 7, 9, 9, 6, 7, + 7, 7, 7, 7, 9, 6, 7, 8, 7, 6, 6, 9, 7, 6, 7, 1 + }; + + private static final int[] EXTRA_CHARS = new int[]{ + '\u20e2', '\u261b', + }; + + private static final byte[] EXTRA_WIDTHS = new byte[]{ + 8, 4, + }; + + private static int getWidth( int codePoint ) + { + // Escape codes + if( codePoint == 167 ) return -1; + + // Space and non-breaking space + if( codePoint == 32 || codePoint == 160 ) return 4; + + // Built-in characters + int nonUnicodeIdx = CHARACTERS.indexOf( codePoint ); + if( codePoint > 0 && nonUnicodeIdx != -1 ) return CHAR_WIDTHS[nonUnicodeIdx]; + + // Other special characters we use. + int extraIdx = Arrays.binarySearch( EXTRA_CHARS, codePoint ); + if( extraIdx >= 0 ) return EXTRA_WIDTHS[extraIdx]; + + return 0; + } + + private static int getWidth( ITextComponent component ) + { + int total = 0; + if( component instanceof TextComponentString ) + { + String contents = component.getUnformattedComponentText(); + + int bold = component.getStyle().getBold() ? 1 : 0; + for( int i = 0; i < contents.length(); i++ ) + { + int cp = contents.charAt( i ); + assert cp != '\n'; + int width = getWidth( cp ); + if( width < 0 ) + { + i++; + } + else + { + total += width + bold; + } + } + } + + for( ITextComponent child : component.getSiblings() ) + { + total += getWidth( child ); + } + + return total; + } + + public static boolean isPlayer( ICommandSender sender ) + { + return sender instanceof EntityPlayerMP && !(sender instanceof FakePlayer); + } + + public static int getMaxWidth( ICommandSender sender ) + { + return isPlayer( sender ) ? 320 : 80; + } + + public static int getWidthFor( ITextComponent component, ICommandSender sender ) + { + return isPlayer( sender ) ? getWidth( component ) : component.getUnformattedText().length(); + } + + public static int getWidthFor( int codepoint, ICommandSender sender ) + { + return isPlayer( sender ) ? getWidth( codepoint ) : 1; + } + + public static void appendFixedWidth( ITextComponent out, ICommandSender sender, ITextComponent entry, int maxWidth ) + { + out.appendSibling( entry ); + + int width = getWidthFor( entry, sender ); + int delta = maxWidth - width; + + if( delta > 0 ) + { + int spaces = delta / SPACE_WIDTH; + int extra = delta % SPACE_WIDTH; + + // Append a fixed number of spaces + if( spaces > 0 ) out.appendSibling( new TextComponentString( StringUtils.repeat( ' ', spaces ) ) ); + + // Append several minor characters to pad to a full string + if( extra > 0 ) + { + out.appendSibling( coloured( StringUtils.repeat( PADDING_CHAR, extra ), TextFormatting.GRAY ) ); + } + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/framework/TextTable.java b/src/main/java/dan200/computercraft/shared/command/framework/TextTable.java index b1603ea8d..802fe26b5 100644 --- a/src/main/java/dan200/computercraft/shared/command/framework/TextTable.java +++ b/src/main/java/dan200/computercraft/shared/command/framework/TextTable.java @@ -2,11 +2,9 @@ package dan200.computercraft.shared.command.framework; import com.google.common.collect.Lists; import net.minecraft.command.ICommandSender; -import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.util.text.ITextComponent; import net.minecraft.util.text.TextComponentString; import net.minecraft.util.text.TextFormatting; -import net.minecraftforge.common.util.FakePlayer; import org.apache.commons.lang3.StringUtils; import javax.annotation.Nonnull; @@ -14,83 +12,13 @@ import java.util.List; import static dan200.computercraft.shared.command.framework.ChatHelpers.coloured; import static dan200.computercraft.shared.command.framework.ChatHelpers.text; +import static dan200.computercraft.shared.command.framework.TextFormatter.*; public class TextTable { - private static final String CHARACTERS = "\u00c0\u00c1\u00c2\u00c8\u00ca\u00cb\u00cd\u00d3\u00d4\u00d5\u00da\u00df\u00e3\u00f5\u011f\u0130\u0131\u0152\u0153\u015e\u015f\u0174\u0175\u017e\u0207\u0000\u0000\u0000\u0000\u0000\u0000\u0000 !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u0000\u00c7\u00fc\u00e9\u00e2\u00e4\u00e0\u00e5\u00e7\u00ea\u00eb\u00e8\u00ef\u00ee\u00ec\u00c4\u00c5\u00c9\u00e6\u00c6\u00f4\u00f6\u00f2\u00fb\u00f9\u00ff\u00d6\u00dc\u00f8\u00a3\u00d8\u00d7\u0192\u00e1\u00ed\u00f3\u00fa\u00f1\u00d1\u00aa\u00ba\u00bf\u00ae\u00ac\u00bd\u00bc\u00a1\u00ab\u00bb\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510\u2514\u2534\u252c\u251c\u2500\u253c\u255e\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256b\u256a\u2518\u250c\u2588\u2584\u258c\u2590\u2580\u03b1\u03b2\u0393\u03c0\u03a3\u03c3\u03bc\u03c4\u03a6\u0398\u03a9\u03b4\u221e\u2205\u2208\u2229\u2261\u00b1\u2265\u2264\u2320\u2321\u00f7\u2248\u00b0\u2219\u00b7\u221a\u207f\u00b2\u25a0\u0000"; - private static final int[] CHAR_WIDTHS = new int[]{ - 6, 6, 6, 6, 6, 6, 4, 6, 6, 6, 6, 6, 6, 6, 6, 4, - 4, 6, 7, 6, 6, 6, 6, 6, 6, 1, 1, 1, 1, 1, 1, 1, - 1, 2, 5, 6, 6, 6, 6, 3, 5, 5, 5, 6, 2, 6, 2, 6, - 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 2, 2, 5, 6, 5, 6, - 7, 6, 6, 6, 6, 6, 6, 6, 6, 4, 6, 6, 6, 6, 6, 6, - 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 6, 4, 6, 6, - 3, 6, 6, 6, 6, 6, 5, 6, 6, 2, 6, 5, 3, 6, 6, 6, - 6, 6, 6, 6, 4, 6, 6, 6, 6, 6, 6, 5, 2, 5, 7, 6, - 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 6, 3, 6, 6, - 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 6, - 6, 3, 6, 6, 6, 6, 6, 6, 6, 7, 6, 6, 6, 2, 6, 6, - 8, 9, 9, 6, 6, 6, 8, 8, 6, 8, 8, 8, 8, 8, 6, 6, - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, - 9, 9, 9, 9, 9, 9, 9, 9, 9, 6, 9, 9, 9, 5, 9, 9, - 8, 7, 7, 8, 7, 8, 8, 8, 7, 8, 8, 7, 9, 9, 6, 7, - 7, 7, 7, 7, 9, 6, 7, 8, 7, 6, 6, 9, 7, 6, 7, 1 - }; - - private static final ITextComponent SEPARATOR = coloured( " | ", TextFormatting.GRAY ); + private static final ITextComponent SEPARATOR = coloured( "| ", TextFormatting.GRAY ); private static final ITextComponent LINE = text( "\n" ); - private static int getWidth( char character, ICommandSender sender ) - { - if( sender instanceof EntityPlayerMP && !(sender instanceof FakePlayer) ) - { - // Use font widths here. - if( character == 167 ) - { - return -1; - } - else if( character == 32 ) - { - return 4; - } - else if( CHARACTERS.indexOf( character ) != -1 ) - { - return CHAR_WIDTHS[character]; - } - else - { - // Eh, close enough. - return 6; - } - } - else - { - return 1; - } - } - - private static int getWidth( ITextComponent text, ICommandSender sender ) - { - int sum = 0; - String chars = text.getUnformattedText(); - for( int i = 0; i < chars.length(); i++ ) - { - sum += getWidth( chars.charAt( i ), sender ); - } - - return sum; - } - - private static boolean isPlayer( ICommandSender sender ) - { - return sender instanceof EntityPlayerMP && !(sender instanceof FakePlayer); - } - - private static int getMaxWidth( ICommandSender sender ) - { - return isPlayer( sender ) ? 320 : 80; - } - private int columns = -1; private final ITextComponent[] header; private final List rows = Lists.newArrayList(); @@ -134,17 +62,13 @@ public class TextTable { if( columns <= 0 ) return; - final int maxWidth = getMaxWidth( sender ); - - int[] minWidths = new int[columns]; int[] maxWidths = new int[columns]; - int[] rowWidths = new int[columns]; if( header != null ) { for( int i = 0; i < columns; i++ ) { - maxWidths[i] = minWidths[i] = getWidth( header[i], sender ); + maxWidths[i] = getWidthFor( header[i], sender ); } } @@ -157,22 +81,16 @@ public class TextTable ITextComponent[] row = rows.get( y ); for( int i = 0; i < row.length; i++ ) { - int width = getWidth( row[i], sender ); - rowWidths[i] += width; - if( width > maxWidths[i] ) - { - maxWidths[i] = width; - } + int width = getWidthFor( row[i], sender ) + 3; + if( width > maxWidths[i] ) maxWidths[i] = width; } } - // Calculate the average width - for( int i = 0; i < columns; i++ ) - { - rowWidths[i] = Math.max( rowWidths[i], rows.size() ); - } + // Add a small amount of extra padding. We include this here instead of the separator to allow + // for "extra" characters + for( int i = 0; i < maxWidths.length; i++ ) maxWidths[i] += 4; - int totalWidth = (columns - 1) * getWidth( SEPARATOR, sender ); + int totalWidth = (columns - 1) * getWidthFor( SEPARATOR, sender ); for( int x : maxWidths ) totalWidth += x; // TODO: Limit the widths of some entries if totalWidth > maxWidth @@ -181,15 +99,16 @@ public class TextTable if( header != null ) { - for( int i = 0; i < columns; i++ ) + for( int i = 0; i < columns - 1; i++ ) { - if( i != 0 ) out.appendSibling( SEPARATOR ); - appendFixed( out, sender, header[i], maxWidths[i] ); + appendFixedWidth( out, sender, header[i], maxWidths[i] ); + out.appendSibling( SEPARATOR ); } + out.appendSibling( header[columns - 1] ); out.appendSibling( LINE ); // Round the width up rather than down - int rowCharWidth = getWidth( '=', sender ); + int rowCharWidth = getWidthFor( '=', sender ); int rowWidth = totalWidth / rowCharWidth + (totalWidth % rowCharWidth == 0 ? 0 : 1); out.appendSibling( coloured( StringUtils.repeat( '=', rowWidth ), TextFormatting.GRAY ) ); out.appendSibling( LINE ); @@ -199,11 +118,12 @@ public class TextTable { ITextComponent[] row = rows.get( i ); if( i != 0 ) out.appendSibling( LINE ); - for( int j = 0; j < columns; j++ ) + for( int j = 0; j < columns - 1; j++ ) { - if( j != 0 ) out.appendSibling( SEPARATOR ); - appendFixed( out, sender, row[j], maxWidths[j] ); + appendFixedWidth( out, sender, row[j], maxWidths[j] ); + out.appendSibling( SEPARATOR ); } + out.appendSibling( row[columns - 1] ); } if( limit != rows.size() ) @@ -214,48 +134,4 @@ public class TextTable sender.sendMessage( out ); } - - private static void appendFixed( ITextComponent out, ICommandSender sender, ITextComponent entry, int maxWidth ) - { - int length = getWidth( entry, sender ); - int delta = length - maxWidth; - if( delta < 0 ) - { - // Convert to overflow; - delta = -delta; - - // We have to remove some padding as there is a padding added between formatted and unformatted text - if( !entry.getStyle().isEmpty() && isPlayer( sender ) ) delta -= 1; - - out.appendSibling( entry ); - - int spaceWidth = getWidth( ' ', sender ); - - int spaces = delta / spaceWidth; - int missing = delta % spaceWidth; - spaces -= missing; - - ITextComponent component = new TextComponentString( StringUtils.repeat( ' ', spaces < 0 ? 0 : spaces ) ); - if( missing > 0 ) - { - ITextComponent bold = new TextComponentString( StringUtils.repeat( ' ', missing ) ); - bold.getStyle().setBold( true ); - component.appendSibling( bold ); - } - - out.appendSibling( component ); - } - else if( delta > 0 ) - { - out.appendSibling( entry ); - } - else - { - out.appendSibling( entry ); - - // We have to add some padding as we expect a padding between formatted and unformatted text - // and there won't be. - if( entry.getStyle().isEmpty() && isPlayer( sender ) ) out.appendText( " " ); - } - } }