package squidpony.squidgrid.gui.gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.BitmapFontCache; import com.badlogic.gdx.graphics.g2d.GlyphLayout; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Matrix4; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane; import com.badlogic.gdx.utils.Align; import squidpony.panel.IColoredString; import squidpony.panel.IMarkup; import squidpony.squidgrid.gui.gdx.UIUtil.CornerStyle; import squidpony.squidgrid.gui.gdx.UIUtil.YMoveKind; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * A panel to display some text using libgdx directly (i.e. without using * {@link SquidPanel}) as in these examples (no scrolling first, then with a * scroll bar): * * <p> * <ul> * <li><img src="http://i.imgur.com/EqEXqlu.png"/></li> * <li><img src="http://i.imgur.com/LYbxQZE.png"/></li> * </ul> * </p> * * <p> * It supports vertical scrolling, i.e. it'll put a vertical scrollbar if * there's too much text to display. This class does a lot of stuff, you * typically only have to provide the textures for the scrollbars and the scroll * knobs (see example below). * </p> * * <p> * A typical usage of this class is as follows: * * <pre> * final TextPanel<Color> tp = new TextPanel<>(new GDXMarkup(), font); * tp.init(screenWidth, screenHeight, text); <- first 2 params: for fullscreen * final ScrollPane sp = tp.getScrollPane(); * sp.setScrollPaneStyle(new ScrollPaneStyle(...)); <- set textures * stage.addActor(sp); * stage.setKeyboardFocus(sp); * stage.setScrollFocus(sp); * stage.draw(); * </pre> * </p> * * <p> * In addition to what {@link ScrollPane} does (knobs, handling of the wheel); * this class plugs scrolling with arrow keys (up, down, page up, page down) and * vim shortcuts (j/k). * </p> * * @author smelC * * @see ScrollPane A libGDX widget for general scrolling through only the visible part of a large widget * @see LinesPanel An alternative for displaying lines of text in a variable-width font */ public class TextPanel<T extends Color> { /** * The color to use to paint the background (outside buttons) using * {@link ShapeRenderer}. Or {@code null} to disable background coloring. */ public /* @Nullable */ T backgroundColor; /** * The color of the border around this panel, if any. If set, it'll be * rendered using {@link ShapeRenderer} and {@link #borderStyle}. */ public /* @Nullable */ T borderColor; /** The size of the border, if any */ public float borderSize; public CornerStyle borderStyle = CornerStyle.ROUNDED; /** * Whether to use 'j' to scroll down, and 'k' to scroll up. Serious * roguelikes leave that to {@code true}, assuming they don't use j and k for movement... */ public boolean vimShortcuts = true; protected /* @Nullable */ IMarkup<T> markup; protected BitmapFont font; protected boolean distanceField; protected TextCellFactory tcf; /** The text to display */ protected List<IColoredString<T>> text; protected StringBuilder builder; protected final ScrollPane scrollPane; /** * The actor whose size is adjusted to the text. When scrolling is required, * it is bigger than {@link #scrollPane}. */ protected final Actor textActor; /** Do not access directly, use {@link #getRenderer()} */ private /* @Nullable */ ShapeRenderer renderer; /** * The text to display MUST be set later on with * {@link #init(float, float, Collection)}. * * @param markup * An optional way to compute markup. * @param font * The font to use. It can be set later using * {@link #setFont(BitmapFont)}, but it MUST be set before * drawing this panel. */ public TextPanel(/* @Nullable */IMarkup<T> markup, /* @Nullable */ BitmapFont font) { if (markup != null) setMarkup(markup); if (font != null) setFont(font); builder = new StringBuilder(512); textActor = new TextActor(); this.scrollPane = new ScrollPane(textActor); this.scrollPane.addListener(new InputListener() { @Override public boolean keyDown(InputEvent event, int keycode) { /* To receive key up */ return true; } @Override public boolean keyUp(InputEvent event, int keycode) { final YMoveKind d = UIUtil.YMoveKind.of(keycode, vimShortcuts); if (d == null) return false; else { switch (d) { case DOWN: case UP: { handleArrow(!d.isDown()); return true; } case PAGE_DOWN: case PAGE_UP: final float scrollY = scrollPane.getScrollY(); final int mult = d.isDown() ? 1 : -1; scrollPane.setScrollY(scrollY + mult * textActor.getHeight()); return true; } throw new IllegalStateException( "Unmatched " + YMoveKind.class.getSimpleName() + ": " + d); } } @Override public boolean keyTyped(InputEvent event, char character) { if (vimShortcuts && (character == 'j' || character == 'k')) return true; else return super.keyTyped(event, character); } private void handleArrow(boolean up) { final float scrollY = scrollPane.getScrollY(); final int mult = up ? -1 : 1; scrollPane.setScrollY(scrollY + (scrollPane.getHeight() * 0.8f * mult)); } }); } /** * The text to display MUST be set later on with * {@link #init(float, float, Collection)}. * * @param markup * An optional way to compute markup. * @param distanceFieldFont * A distance field font as a TextCellFactory to use. * Won't be used for drawing in cells, just the distance field code it has matters. */ public TextPanel(/* @Nullable */IMarkup<T> markup, /* @Nullable */ TextCellFactory distanceFieldFont) { if (markup != null) setMarkup(markup); if (distanceFieldFont != null) { tcf = distanceFieldFont; distanceField = distanceFieldFont.distanceField; tcf.initBySize(); font = tcf.font(); if (markup != null) font.getData().markupEnabled = true; } builder = new StringBuilder(512); textActor = new TextActor(); scrollPane = new ScrollPane(textActor); this.scrollPane.addListener(new InputListener() { @Override public boolean keyDown(InputEvent event, int keycode) { /* To receive key up */ return true; } @Override public boolean keyUp(InputEvent event, int keycode) { final YMoveKind d = UIUtil.YMoveKind.of(keycode, vimShortcuts); if (d == null) return false; else { switch (d) { case DOWN: case UP: { handleArrow(!d.isDown()); return true; } case PAGE_DOWN: case PAGE_UP: final float scrollY = scrollPane.getScrollY(); final int mult = d.isDown() ? 1 : -1; scrollPane.setScrollY(scrollY + mult * textActor.getHeight()); return true; } throw new IllegalStateException( "Unmatched " + YMoveKind.class.getSimpleName() + ": " + d); } } @Override public boolean keyTyped(InputEvent event, char character) { if (vimShortcuts && (character == 'j' || character == 'k')) return true; else return super.keyTyped(event, character); } private void handleArrow(boolean up) { final float scrollY = scrollPane.getScrollY(); final int mult = up ? -1 : 1; scrollPane.setScrollY(scrollY + (scrollPane.getHeight() * 0.8f * mult)); } }); } /** * @param m * The markup to use. */ public void setMarkup(IMarkup<T> m) { if (font != null) font.getData().markupEnabled |= true; this.markup = m; } /** * Sets the font to use. This method should be called once before * {@link #init(float, float, Collection)} if the font wasn't given at * creation-time. * * @param font * The font to use. */ public void setFont(BitmapFont font) { this.font = font; tcf = new TextCellFactory().font(font).height(MathUtils.ceil(font.getLineHeight())) .width(MathUtils.round(font.getSpaceWidth())); if (markup != null) font.getData().markupEnabled |= true; } /** * This method sets the sizes of {@link #scrollPane} and {@link #textActor}. * This method MUST be called before rendering. * * @param maxHeight * The maximum height that the scrollpane can take (equal or * smaller than the height of the text actor). * @param width * The width of the scrollpane and the text actor. * @param text */ public void init(float width, float maxHeight, Collection<? extends IColoredString<T>> text) { this.text = new ArrayList<>(text); scrollPane.setWidth(width); textActor.setWidth(width); if (tcf == null) throw new NullPointerException( "The font should be set before calling " + TextPanel.class.getSimpleName() + "::init"); final BitmapFontCache cache = font.getCache(); final List<String> toDisplay = getTypesetText(); float totalTextHeight = tcf.height(); GlyphLayout layout = cache.addText(builder, 0, 0, 0, builder.length(), width, Align.left, true); totalTextHeight += layout.height; if(totalTextHeight < 0) totalTextHeight = 0; textActor.setHeight(/* Entire height */ totalTextHeight); final boolean yscroll = maxHeight < totalTextHeight; scrollPane.setHeight(/* Maybe not the entire height */ Math.min(totalTextHeight, maxHeight)); scrollPane.setWidget(new TextActor()); yScrollingCallback(yscroll); } public void init(float width, float maxHeight, T color, String... text) { ArrayList<IColoredString.Impl<T>> coll = new ArrayList<>(text.length); for(String t : text) { coll.add(new IColoredString.Impl<T>(t, color)); } init(width, maxHeight, coll); } /** * Draws the border. You have to call this method manually, because the * border is outside the actor and hence should be drawn at the very end, * otherwise it can get overwritten by UI element. * * @param batch */ public void drawBorder(Batch batch) { if (borderColor != null && 0 < borderSize) { final boolean reset = batch.isDrawing(); if (reset) batch.end(); final ShapeRenderer sr = getRenderer(); final Matrix4 m = batch.getTransformMatrix(); sr.setTransformMatrix(m); sr.begin(ShapeType.Filled); sr.setColor(borderColor); UIUtil.drawMarginsAround(sr, scrollPane.getX(), scrollPane.getY(), scrollPane.getWidth(), scrollPane.getHeight() - 1, borderSize, borderColor, borderStyle, 1f, 1f); sr.end(); if (reset) batch.begin(); } } /** * @return The text to draw, after applying {@link #present(IColoredString)} * and {@link #applyMarkup(IColoredString)}. */ public /* @Nullable */ List<String> getTypesetText() { if (text == null) return null; builder.delete(0, builder.length()); final List<String> result = new ArrayList<>(); for (IColoredString<T> line : text) { /* This code must be consistent with #draw in the custom Actor */ final IColoredString<T> tmp = present(line); final String marked = applyMarkup(tmp); result.add(marked); builder.append(marked); builder.append('\n'); } if(builder.length() > 0) builder.deleteCharAt(builder.length() - 1); return result; } /** * @return The {@link ScrollPane} containing {@link #getTextActor()}. */ public ScrollPane getScrollPane() { return scrollPane; } /** * @return The {@link Actor} where the text is drawn. It may be bigger than * {@link #getScrollPane()}. */ public Actor getTextActor() { return textActor; } /** * @return The font used, if set. */ public /* @Nullable */ BitmapFont getFont() { return font; } public void dispose() { if (renderer != null) renderer.dispose(); } /** * Callback done to do stuff according to whether y-scrolling is required * * @param required * Whether y scrolling is required. */ protected void yScrollingCallback(boolean required) { if (required) { /* Disable borders, they don't mix well with scrollbars */ borderSize = 0; scrollPane.setFadeScrollBars(false); scrollPane.setForceScroll(false, true); } } /** * @param ics * Text set when building {@code this} * @return The text to display to screen. If you wanna * {@link squidpony.IColorCenter#filter(IColoredString) filter} your * text , do it here. */ protected IColoredString<T> present(IColoredString<T> ics) { return ics; } /** * @param ics * @return The text obtained after applying {@link #markup}. */ protected String applyMarkup(IColoredString<T> ics) { return markup == null ? ics.toString() : ics.presentWithMarkup(markup); } /** * @return A fresh renderer. */ protected ShapeRenderer buildRenderer() { return new ShapeRenderer(); } /** * @return The renderer to use. */ protected ShapeRenderer getRenderer() { if (renderer == null) renderer = buildRenderer(); return renderer; } private class TextActor extends Actor { TextActor() { } @Override public void draw(Batch batch, float parentAlpha) { final float tx = 0;//getX(); final float ty = 0;//getY(); final float twidth = getWidth(); final float theight = getHeight(); final float height = scrollPane.getHeight(); if (backgroundColor != null) { batch.setColor(backgroundColor); batch.draw(tcf.getSolid(), tx, ty, twidth, theight); batch.setColor(Color.WHITE); /* batch.end(); final Matrix4 m = batch.getTransformMatrix(); final ShapeRenderer sr = getRenderer(); sr.setTransformMatrix(m); sr.begin(ShapeType.Filled); sr.setColor(backgroundColor); UIUtil.drawRectangle(renderer, tx, ty, twidth, theight, ShapeType.Filled, backgroundColor); sr.end(); batch.begin(); */ } if (font == null) throw new NullPointerException( "The font should be set when drawing a " + getClass().getSimpleName()); if (text == null) throw new NullPointerException( "The text should be set when drawing a " + getClass().getSimpleName()); if (tcf != null) { tcf.configureShader(batch); } float yscroll = scrollPane.getScrollY(); final float destx = tx, offY = (tcf != null) ? tcf.height * 0.5f : 0; getTypesetText(); font.draw(batch, builder, destx, theight + yscroll - offY, 0, builder.length(), twidth, Align.left, true); } } }