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.BitmapFont.BitmapFontData;
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.scenes.scene2d.Actor;
import com.badlogic.gdx.utils.Align;
import squidpony.panel.IColoredString;
import squidpony.panel.IMarkup;
import java.util.LinkedList;
import java.util.ListIterator;
/**
* An actor capable of drawing {@link IColoredString}s. It is lines-oriented:
* putting a line may erase a line put before. It is designed to write text with
* a variable-width font (as opposed to {@link SquidPanel}). It performs line wrapping by
* default. It can write from top to bottom or from bottom to top (the default).
*
* <p>
* This
* <a href="https://twitter.com/hgamesdev/status/736091292132724736">tweet</a>
* shows an example. The panel at the top of the screenshot is implemented using
* this class (with {@link #drawBottomUp} being {@code true}).
* </p>
*
* <p>
* This class is usually used as follows:
*
* <pre>
* final int nbLines = LinesPanel.computeMaxLines(font, pixelHeight);
* final LinesPanel<Color> lp = new LinesPanel(new GDXMarkup(), font, nbLines);
* lp.setSize(pixelWidth, pixelHeight);
* stage.addActor(lp);
* </pre>
* </p>
*
* <p>
* Contrary to {@link SquidMessageBox}, this panel doesn't support scrolling
* (for now). So it's suited when it is fine forgetting old messages (as in
* brogue's messages area).
* </p>
*
* @author smelC
* @param <T>
*
* @see SquidMessageBox An alternative, doing similar lines-drawing business,
* but being backed up by {@link SquidPanel}.
*/
public class LinesPanel<T extends Color> extends Actor {
/** The markup used to typeset {@link #content}. */
protected final IMarkup<T> markup;
/** The font used to draw {@link #content}. */
protected final BitmapFont font;
/** What to display. Doesn't contain {@code null} entries. */
protected final LinkedList<IColoredString<T>> content;
/** The maximal size of {@link #content} */
protected final int maxLines;
/**
* The renderer used by {@link #clearArea(Batch)}. Do not access directly:
* use {@link #getRenderer()} instead.
*/
protected /* @Nullable */ ShapeRenderer renderer;
/**
* The horizontal offset to use when writing. If you aren't doing anything
* weird, should be left to {@code 0}.
*/
public float xOffset = 0;
/**
* The vertical offset to use when writing. If you aren't doing anything
* weird, should be left to {@code 0}.
*/
public float yOffset = 0;
/**
* If {@code true}, draws:
*
* <pre>
* ...
* content[1]
* content[0]
* </pre>
*
* If {@code false}, draws:
*
* <pre>
* content[0]
* content[1]
* ...
* </pre>
*/
public boolean drawBottomUp = false;
/**
* The color to use to clear the screen before drawing. Set it to
* {@code null} if you clean on your own.
*/
public Color clearingColor = Color.BLACK;
/* Now comes the usual libgdx options */
/** Whether to wrap text */
public boolean wrap = true;
/** The alignment used when typesetting */
public int align = Align.left;
/**
* @param markup
* The markup to use, or {@code null} if none. You likely want to
* give {@link GDXMarkup}. If non-{@code null}, markup will be
* enabled in {@code font}.
* @param font
* The font to use.
* @param maxLines
* The maximum number of lines that this panel should display.
* Must be {@code >= 0}.
* @throws IllegalStateException
* If {@code maxLines < 0}
*/
public LinesPanel(/* @Nullable */ IMarkup<T> markup, BitmapFont font, int maxLines) {
this.markup = markup;
this.font = font;
if (markup != null)
this.font.getData().markupEnabled |= true;
this.content = new LinkedList<IColoredString<T>>();
if (maxLines < 0)
throw new IllegalStateException("The maximum number of lines in an instance of "
+ getClass().getSimpleName() + " must be greater or equal than zero");
this.maxLines = maxLines;
}
/**
* Used to help find the last parameter to give the constructor of this class.
* @param font the font being used
* @param height the height of the area you want to put text into
* @return The last argument to give to
* {@link #LinesPanel(IMarkup, BitmapFont, int)} when the
* desired <b>pixel</b> height is {@code height}
*/
public static int computeMaxLines(BitmapFont font, float height) {
return MathUtils.ceil(height / font.getData().lineHeight);
}
/**
* Adds {@code ics} first in {@code this}, possibly removing the last entry,
* if {@code this}' size would grow over {@link #maxLines}.
*
* @param ics
*/
public void addFirst(IColoredString<T> ics) {
if (ics == null)
throw new NullPointerException("Adding a null entry is forbidden");
if (atMax())
content.removeLast();
content.addFirst(ics);
}
/**
* Adds {@code ics} last in {@code this}, possibly removing the last entry,
* if {@code this}' size would grow over {@link #maxLines}.
*
* @param ics
*/
public void addLast(IColoredString<T> ics) {
if (ics == null)
throw new NullPointerException("Adding a null entry is forbidden");
if (atMax())
content.removeLast();
content.addLast(ics);
}
@Override
public void draw(Batch batch, float parentAlpha) {
clearArea(batch);
final float width = getWidth();
final BitmapFontData data = font.getData();
final float lineHeight = data.lineHeight;
final float height = getHeight();
final float x = getX() + xOffset;
float y = getY() + (drawBottomUp ? lineHeight : height) - data.descent + yOffset;
final ListIterator<IColoredString<T>> it = content.listIterator();
int ydx = 0;
float consumed = 0;
while (it.hasNext()) {
final IColoredString<T> ics = it.next();
final String str = toDraw(ics, ydx);
/* Let's see if the drawing would go outside this Actor */
final BitmapFontCache cache = font.getCache();
cache.clear();
final GlyphLayout glyph = cache.addText(str, 0, y, width, align, wrap);
if (height < consumed + glyph.height)
/* We would draw outside this Actor's bounds */
break;
final int increaseAlready;
if (drawBottomUp) {
/*
* If the text span multiple lines and we draw bottom-up, we
* must go up *before* drawing.
*/
final int nbLines = MathUtils.ceil(glyph.height / lineHeight);
if (1 < nbLines) {
increaseAlready = nbLines - 1;
y += increaseAlready * lineHeight;
} else
increaseAlready = 0;
} else
increaseAlready = 0;
/* Actually draw */
font.draw(batch, str, x, y, width, align, wrap);
y += (drawBottomUp ? /* Go up */ 1 : /* Go down */ -1) * glyph.height;
y -= increaseAlready * lineHeight;
consumed += glyph.height;
ydx++;
}
}
/**
* Paints this panel with {@link #clearingColor}
*/
protected void clearArea(Batch batch) {
if (clearingColor != null) {
batch.end();
UIUtil.drawRectangle(getRenderer(), getX(), getY(), getWidth(), getHeight(), ShapeType.Filled,
clearingColor);
batch.begin();
}
}
protected boolean atMax() {
return content.size() == maxLines;
}
protected String toDraw(IColoredString<T> ics, int ydx) {
return applyMarkup(transform(ics, ydx));
}
protected String applyMarkup(IColoredString<T> ics) {
if (ics == null)
return null;
else
return markup == null ? ics.toString() : ics.presentWithMarkup(markup);
}
/**
* If you want to grey out "older" messages, you would do it in this method,
* when {@code ydx > 0} (using an {@link squidpony.IColorCenter} maybe ?).
*
* @param ics an IColorCenter with the same generic color type as this LinesPanel
* @param ydx
* The index of {@code ics} within {@link #content}.
* @return A variation of {@code ics}, or {@code ics} itself.
*/
protected IColoredString<T> transform(IColoredString<T> ics, int ydx) {
return ics;
}
protected ShapeRenderer getRenderer() {
if (renderer == null)
renderer = new ShapeRenderer();
return renderer;
}
}