001package squidpony.squidgrid.gui.gdx;
002
003import java.util.LinkedList;
004import java.util.ListIterator;
005
006import com.badlogic.gdx.graphics.Color;
007import com.badlogic.gdx.graphics.g2d.Batch;
008import com.badlogic.gdx.graphics.g2d.BitmapFont;
009import com.badlogic.gdx.graphics.g2d.BitmapFont.BitmapFontData;
010import com.badlogic.gdx.graphics.g2d.BitmapFontCache;
011import com.badlogic.gdx.graphics.g2d.GlyphLayout;
012import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
013import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
014import com.badlogic.gdx.math.MathUtils;
015import com.badlogic.gdx.scenes.scene2d.Actor;
016import com.badlogic.gdx.utils.Align;
017
018import squidpony.panel.IColoredString;
019import squidpony.panel.IMarkup;
020
021/**
022 * An actor capable of drawing {@link IColoredString}s. It is lines-oriented:
023 * putting a line may erase a line put before. It is designed to write text with
024 * a serif font (as opposed to {@link SquidPanel}). It performs line wrapping by
025 * default. It can write from top to bottom or from bottom to top (the default).
026 * 
027 * <p>
028 * This
029 * <a href="https://twitter.com/hgamesdev/status/736091292132724736">tweet</a>
030 * shows an example. The panel at the top of the screenshot is implemented using
031 * this class (with {@link #drawBottomUp} being {@code true}).
032 * </p>
033 * 
034 * <p>
035 * Contrary to {@link SquidMessageBox}, this panel doesn't support scrolling
036 * (for now). So it's suited when it is fine forgetting old messages (as in
037 * brogue's messages area).
038 * </p>
039 * 
040 * @author smelC
041 * @param <T>
042 * 
043 * @see SquidMessageBox An alternative, doing similar lines-drawing business,
044 *      but being backed up by {@link SquidPanel}.
045 */
046public class LinesPanel<T extends Color> extends Actor {
047
048        /** The markup used to typeset {@link #content}. */
049        protected final IMarkup<T> markup;
050
051        /** The font used to draw {@link #content}. */
052        protected final BitmapFont font;
053
054        /** What to display. Doesn't contain {@code null} entries. */
055        protected final LinkedList<IColoredString<T>> content;
056
057        /** The maximal size of {@link #content} */
058        protected final int maxLines;
059
060        /**
061         * The renderer used by {@link #clearArea(Batch)}. Do not access directly:
062         * use {@link #getRenderer()} instead.
063         */
064        protected /* @Nullable */ ShapeRenderer renderer;
065
066        /**
067         * The horizontal offset to use when writing. If you aren't doing anything
068         * weird, should be left to {@code 0}.
069         */
070        public float xOffset = 0;
071
072        /**
073         * The vertical offset to use when writing. If you aren't doing anything
074         * weird, should be left to {@code 0}.
075         */
076        public float yOffset = 0;
077
078        /**
079         * If {@code true}, draws:
080         * 
081         * <pre>
082         * ...
083         * content[1]
084         * content[0]
085         * </pre>
086         * 
087         * If {@code false}, draws:
088         * 
089         * <pre>
090         * content[0]
091         * content[1]
092         * ...
093         * </pre>
094         */
095        public boolean drawBottomUp = false;
096
097        /**
098         * The color to use to clear the screen before drawing. Set it to
099         * {@code null} if you clean on your own.
100         */
101        public Color clearingColor = Color.BLACK;
102
103        /* Now comes the usual libgdx options */
104
105        /** Whether to wrap text */
106        public boolean wrap = true;
107
108        /** The alignment used when typesetting */
109        public int align = Align.left;
110
111        /**
112         * @param markup
113         *            The markup to use, or {@code null} if none. You likely want to
114         *            give {@link GDXMarkup}. If non-{@code null}, markup will be
115         *            enabled in {@code font}.
116         * @param font
117         *            The font to use.
118         * @param maxLines
119         *            The maximum number of lines that this panel should display.
120         *            Must be {@code >= 0}.
121         * @throws IllegalStateException
122         *             If {@code maxLines < 0}
123         */
124        public LinesPanel(/* @Nullable */ IMarkup<T> markup, BitmapFont font, int maxLines) {
125                this.markup = markup;
126                this.font = font;
127                if (markup != null)
128                        this.font.getData().markupEnabled |= true;
129                this.content = new LinkedList<IColoredString<T>>();
130                if (maxLines < 0)
131                        throw new IllegalStateException("The maximum number of lines in an instance of "
132                                        + getClass().getSimpleName() + " must be greater or equal than zero");
133                this.maxLines = maxLines;
134        }
135
136        /**
137         * @param font
138         * @param height
139         * @return The last argument to give to
140         *         {@link #WrappingLinesPanel(IMarkup, BitmapFont, int)} when the
141         *         desired <b>pixel</b> height is {@code height}
142         */
143        public static int computeMaxLines(BitmapFont font, float height) {
144                return MathUtils.ceil(height / font.getData().lineHeight);
145        }
146
147        /**
148         * Adds {@code ics} first in {@code this}, possibly removing the last entry,
149         * if {@code this}' size would grow over {@link #maxLines}.
150         * 
151         * @param ics
152         */
153        public void addFirst(IColoredString<T> ics) {
154                if (ics == null)
155                        throw new NullPointerException("Adding a null entry is forbidden");
156                if (atMax())
157                        content.removeLast();
158                content.addFirst(ics);
159        }
160
161        /**
162         * Adds {@code ics} last in {@code this}, possibly removing the last entry,
163         * if {@code this}' size would grow over {@link #maxLines}.
164         * 
165         * @param ics
166         */
167        public void addLast(IColoredString<T> ics) {
168                if (ics == null)
169                        throw new NullPointerException("Adding a null entry is forbidden");
170                if (atMax())
171                        content.removeLast();
172                content.addLast(ics);
173        }
174
175        @Override
176        public void draw(Batch batch, float parentAlpha) {
177                clearArea(batch);
178
179                final float width = getWidth();
180
181                final BitmapFontData data = font.getData();
182                final float lineHeight = data.lineHeight;
183
184                final float height = getHeight();
185
186                final float x = getX() + xOffset;
187                float y = getY() + (drawBottomUp ? lineHeight : height) - data.descent + yOffset;
188
189                final ListIterator<IColoredString<T>> it = content.listIterator();
190                int ydx = 0;
191                float consumed = 0;
192                while (it.hasNext()) {
193                        final IColoredString<T> ics = it.next();
194                        final String str = toDraw(ics, ydx);
195                        /* Let's see if the drawing would go outside this Actor */
196                        final BitmapFontCache cache = font.getCache();
197                        cache.clear();
198                        final GlyphLayout glyph = cache.addText(str, 0, y, width, align, wrap);
199                        if (height < consumed + glyph.height)
200                                /* We would draw outside this Actor's bounds */
201                                break;
202                        final int increaseAlready;
203                        if (drawBottomUp) {
204                                /*
205                                 * If the text span multiple lines and we draw bottom-up, we
206                                 * must go up *before* drawing.
207                                 */
208                                final int nbLines = MathUtils.ceil(glyph.height / lineHeight);
209                                if (1 < nbLines) {
210                                        increaseAlready = nbLines - 1;
211                                        y += increaseAlready * lineHeight;
212                                } else
213                                        increaseAlready = 0;
214                        } else
215                                increaseAlready = 0;
216                        /* Actually draw */
217                        font.draw(batch, str, x, y, width, align, wrap);
218                        y += (drawBottomUp ? /* Go up */ 1 : /* Go down */ -1) * glyph.height;
219                        y -= increaseAlready * lineHeight;
220                        consumed += glyph.height;
221                        ydx++;
222                }
223        }
224
225        /**
226         * Paints this panel with {@link #clearingColor}
227         */
228        protected void clearArea(Batch batch) {
229                if (clearingColor != null) {
230                        batch.end();
231                        UIUtil.drawRectangle(getRenderer(), getX(), getY(), getWidth(), getHeight(), ShapeType.Filled,
232                                        clearingColor);
233                        batch.begin();
234                }
235        }
236
237        protected boolean atMax() {
238                return content.size() == maxLines;
239
240        }
241
242        protected String toDraw(IColoredString<T> ics, int ydx) {
243                return applyMarkup(transform(ics, ydx));
244        }
245
246        protected String applyMarkup(IColoredString<T> ics) {
247                if (ics == null)
248                        return null;
249                else
250                        return markup == null ? ics.toString() : ics.presentWithMarkup(markup);
251        }
252
253        /**
254         * If you want to grey out "older" messages, you would do it in this method,
255         * when {@code ydx > 0} (using an {@link IColorCenter} maybe ?).
256         * 
257         * @param ics
258         * @param ydx
259         *            The index of {@link #ics} within {@link #content}.
260         * @return A variation of {@code ics}, or {@code ics} itself.
261         */
262        protected IColoredString<T> transform(IColoredString<T> ics, int ydx) {
263                return ics;
264        }
265
266        protected ShapeRenderer getRenderer() {
267                if (renderer == null)
268                        renderer = new ShapeRenderer();
269                return renderer;
270        }
271
272}