001package squidpony.squidgrid.gui.gdx;
002
003import com.badlogic.gdx.graphics.Color;
004import com.badlogic.gdx.graphics.g2d.Batch;
005import com.badlogic.gdx.graphics.g2d.BitmapFont;
006import com.badlogic.gdx.graphics.g2d.BitmapFontCache;
007import com.badlogic.gdx.graphics.g2d.GlyphLayout;
008import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
009import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
010import com.badlogic.gdx.math.MathUtils;
011import com.badlogic.gdx.math.Matrix4;
012import com.badlogic.gdx.scenes.scene2d.Actor;
013import com.badlogic.gdx.scenes.scene2d.InputEvent;
014import com.badlogic.gdx.scenes.scene2d.InputListener;
015import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
016import com.badlogic.gdx.utils.Align;
017import squidpony.panel.IColoredString;
018import squidpony.panel.IMarkup;
019import squidpony.squidgrid.gui.gdx.UIUtil.CornerStyle;
020import squidpony.squidgrid.gui.gdx.UIUtil.YMoveKind;
021
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.List;
025
026/**
027 * A panel to display some text using libgdx directly (i.e. without using
028 * {@link SquidPanel}) as in these examples (no scrolling first, then with a
029 * scroll bar):
030 * 
031 * <p>
032 * <ul>
033 * <li><img src="http://i.imgur.com/EqEXqlu.png"/></li>
034 * <li><img src="http://i.imgur.com/LYbxQZE.png"/></li>
035 * </ul>
036 * </p>
037 * 
038 * <p>
039 * It supports vertical scrolling, i.e. it'll put a vertical scrollbar if
040 * there's too much text to display. This class does a lot of stuff, you
041 * typically only have to provide the textures for the scrollbars and the scroll
042 * knobs (see example below).
043 * </p>
044 * 
045 * <p>
046 * A typical usage of this class is as follows:
047 * 
048 * <pre>
049 * final TextPanel<Color> tp = new TextPanel<>(new GDXMarkup(), font);
050 * tp.init(screenWidth, screenHeight, text); <- first 2 params: for fullscreen
051 * final ScrollPane sp = tp.getScrollPane();
052 * sp.setScrollPaneStyle(new ScrollPaneStyle(...)); <- set textures
053 * stage.addActor(sp);
054 * stage.setKeyboardFocus(sp);
055 * stage.setScrollFocus(sp);
056 * stage.draw();
057 * </pre>
058 * </p>
059 * 
060 * <p>
061 * In addition to what {@link ScrollPane} does (knobs, handling of the wheel);
062 * this class plugs scrolling with arrow keys (up, down, page up, page down) and
063 * vim shortcuts (j/k).
064 * </p>
065 * 
066 * @author smelC
067 * 
068 * @see ScrollPane
069 */
070public class TextPanel<T extends Color> {
071
072        /**
073         * The color to use to paint the background (outside buttons) using
074         * {@link ShapeRenderer}. Or {@code null} to disable background coloring.
075         */
076        public /* @Nullable */ T backgroundColor;
077
078        /**
079         * The color of the border around this panel, if any. If set, it'll be
080         * rendered using {@link ShapeRenderer} and {@link #borderStyle}.
081         */
082        public /* @Nullable */ T borderColor;
083
084        /** The size of the border, if any */
085        public float borderSize;
086
087        public CornerStyle borderStyle = CornerStyle.ROUNDED;
088
089        /**
090         * Whether to use 'j' to scroll down, and 'k' to scroll up. Serious
091         * roguelikes leave that to {@code true}.
092         */
093        public boolean vimShortcuts = true;
094
095        protected /* @Nullable */ IMarkup<T> markup;
096
097        protected BitmapFont font;
098    protected boolean distanceField;
099    protected TextCellFactory tcf;
100        /** The text to display */
101        protected List<IColoredString<T>> text;
102
103    protected StringBuilder builder;
104
105        protected final ScrollPane scrollPane;
106
107        /**
108         * The actor whose size is adjusted to the text. When scrolling is required,
109         * it is bigger than {@link #scrollPane}.
110         */
111        protected final Actor textActor;
112
113        /** Do not access directly, use {@link #getRenderer()} */
114        private /* @Nullable */ ShapeRenderer renderer;
115
116    /**
117     * The text to display MUST be set later on with
118     * {@link #init(float, float, Collection)}.
119     *
120     * @param markup
121     *            An optional way to compute markup.
122     * @param font
123     *            The font to use. It can be set later using
124     *            {@link #setFont(BitmapFont)}, but it MUST be set before
125     *            drawing this panel.
126     */
127    public TextPanel(/* @Nullable */IMarkup<T> markup, /* @Nullable */ BitmapFont font) {
128        if (markup != null)
129            setMarkup(markup);
130        if (font != null)
131            setFont(font);
132        builder = new StringBuilder(512);
133        textActor = new TextActor();
134
135        this.scrollPane = new ScrollPane(textActor);
136
137        this.scrollPane.addListener(new InputListener() {
138            @Override
139            public boolean keyDown(InputEvent event, int keycode) {
140                                /* To receive key up */
141                return true;
142            }
143
144            @Override
145            public boolean keyUp(InputEvent event, int keycode) {
146                final YMoveKind d = UIUtil.YMoveKind.of(keycode, vimShortcuts);
147                if (d == null)
148                    return false;
149                else {
150                    switch (d) {
151                        case DOWN:
152                        case UP: {
153                            handleArrow(!d.isDown());
154                            return true;
155                        }
156                        case PAGE_DOWN:
157                        case PAGE_UP:
158                            final float scrollY = scrollPane.getScrollY();
159                            final int mult = d.isDown() ? 1 : -1;
160                            scrollPane.setScrollY(scrollY + mult * textActor.getHeight());
161                            return true;
162                    }
163                    throw new IllegalStateException(
164                            "Unmatched " + YMoveKind.class.getSimpleName() + ": " + d);
165                }
166            }
167
168            @Override
169            public boolean keyTyped(InputEvent event, char character) {
170                if (vimShortcuts && (character == 'j' || character == 'k'))
171                    return true;
172                else
173                    return super.keyTyped(event, character);
174            }
175
176            private void handleArrow(boolean up) {
177                final float scrollY = scrollPane.getScrollY();
178                final int mult = up ? -1 : 1;
179                scrollPane.setScrollY(scrollY + (scrollPane.getHeight() * 0.8f * mult));
180            }
181        });
182    }
183
184    /**
185     * The text to display MUST be set later on with
186     * {@link #init(float, float, Collection)}.
187     *
188     * @param markup
189     *            An optional way to compute markup.
190     * @param distanceFieldFont
191     *            A distance field font as a TextCellFactory to use.
192     *            Won't be used for drawing in cells, just the distance field code it has matters.
193     */
194    public TextPanel(/* @Nullable */IMarkup<T> markup, /* @Nullable */ TextCellFactory distanceFieldFont) {
195        if (markup != null)
196            setMarkup(markup);
197        if (distanceFieldFont != null)
198        {
199            tcf = distanceFieldFont;
200            distanceField = distanceFieldFont.distanceField;
201            tcf.initBySize();
202            font = tcf.font();
203            if (markup != null)
204                font.getData().markupEnabled = true;
205        }
206        builder = new StringBuilder(512);
207        textActor = new TextActor();
208        scrollPane = new ScrollPane(textActor);
209
210        this.scrollPane.addListener(new InputListener() {
211            @Override
212            public boolean keyDown(InputEvent event, int keycode) {
213                                /* To receive key up */
214                return true;
215            }
216
217            @Override
218            public boolean keyUp(InputEvent event, int keycode) {
219                final YMoveKind d = UIUtil.YMoveKind.of(keycode, vimShortcuts);
220                if (d == null)
221                    return false;
222                else {
223                    switch (d) {
224                        case DOWN:
225                        case UP: {
226                            handleArrow(!d.isDown());
227                            return true;
228                        }
229                        case PAGE_DOWN:
230                        case PAGE_UP:
231                            final float scrollY = scrollPane.getScrollY();
232                            final int mult = d.isDown() ? 1 : -1;
233                            scrollPane.setScrollY(scrollY + mult * textActor.getHeight());
234                            return true;
235                    }
236                    throw new IllegalStateException(
237                            "Unmatched " + YMoveKind.class.getSimpleName() + ": " + d);
238                }
239            }
240
241            @Override
242            public boolean keyTyped(InputEvent event, char character) {
243                if (vimShortcuts && (character == 'j' || character == 'k'))
244                    return true;
245                else
246                    return super.keyTyped(event, character);
247            }
248
249            private void handleArrow(boolean up) {
250                final float scrollY = scrollPane.getScrollY();
251                final int mult = up ? -1 : 1;
252                scrollPane.setScrollY(scrollY + (scrollPane.getHeight() * 0.8f * mult));
253            }
254        });
255    }
256
257    /**
258         * @param m
259         *            The markup to use.
260         */
261        public void setMarkup(IMarkup<T> m) {
262                if (font != null)
263                        font.getData().markupEnabled |= true;
264                this.markup = m;
265        }
266
267        /**
268         * Sets the font to use. This method should be called once before
269         * {@link #init(float, float, Collection)} if the font wasn't given at
270         * creation-time.
271         * 
272         * @param font
273         *            The font to use.
274         */
275        public void setFont(BitmapFont font) {
276                this.font = font;
277        tcf = new TextCellFactory().font(font).height(MathUtils.ceil(font.getLineHeight()))
278                .width(MathUtils.round(font.getSpaceWidth()));
279                if (markup != null)
280                        font.getData().markupEnabled |= true;
281        }
282
283        /**
284         * This method sets the sizes of {@link #scrollPane} and {@link #textActor}.
285         * This method MUST be called before rendering.
286         * 
287         * @param maxHeight
288         *            The maximum height that the scrollpane can take (equal or
289         *            smaller than the height of the text actor).
290         * @param width
291         *            The width of the scrollpane and the text actor.
292         * @param text
293         */
294        public void init(float width, float maxHeight, Collection<? extends IColoredString<T>> text) {
295        this.text = new ArrayList<>(text);
296
297        scrollPane.setWidth(width);
298        textActor.setWidth(width);
299
300        if (tcf == null)
301            throw new NullPointerException(
302                    "The font should be set before calling " + TextPanel.class.getSimpleName() + "::init");
303
304        final BitmapFontCache cache = font.getCache();
305        final List<String> toDisplay = getTypesetText();
306        float totalTextHeight = tcf.height();
307        GlyphLayout layout = cache.addText(builder, 0, 0, 0, builder.length(), width, Align.left, true);
308        totalTextHeight += layout.height;
309        if(totalTextHeight < 0)
310            totalTextHeight = 0;
311                textActor.setHeight(/* Entire height */ totalTextHeight);
312                final boolean yscroll = maxHeight < totalTextHeight;
313                scrollPane.setHeight(/* Maybe not the entire height */ Math.min(totalTextHeight, maxHeight));
314        scrollPane.setWidget(new TextActor());
315                yScrollingCallback(yscroll);
316        }
317
318    public void init(float width, float maxHeight, T color, String... text)
319    {
320        ArrayList<IColoredString.Impl<T>> coll = new ArrayList<>(text.length);
321        for(String t : text)
322        {
323            coll.add(new IColoredString.Impl<T>(t, color));
324        }
325        init(width, maxHeight, coll);
326    }
327
328        /**
329         * Draws the border. You have to call this method manually, because the
330         * border is outside the actor and hence should be drawn at the very end,
331         * otherwise it can get overwritten by UI element.
332         * 
333         * @param batch
334         */
335        public void drawBorder(Batch batch) {
336                if (borderColor != null && 0 < borderSize) {
337                        final boolean reset = batch.isDrawing();
338                        if (reset)
339                                batch.end();
340
341                        final ShapeRenderer sr = getRenderer();
342                        final Matrix4 m = batch.getTransformMatrix();
343                        sr.setTransformMatrix(m);
344                        sr.begin(ShapeType.Filled);
345                        sr.setColor(borderColor);
346                        UIUtil.drawMarginsAround(sr, scrollPane.getX(), scrollPane.getY(), scrollPane.getWidth(),
347                                        scrollPane.getHeight() - 1, borderSize, borderColor, borderStyle, 1f, 1f);
348                        sr.end();
349
350                        if (reset)
351                                batch.begin();
352                }
353        }
354
355        /**
356         * @return The text to draw, after applying {@link #present(IColoredString)}
357         *         and {@link #applyMarkup(IColoredString)}.
358         */
359        public /* @Nullable */ List<String> getTypesetText() {
360                if (text == null)
361                        return null;
362        builder.delete(0, builder.length());
363                final List<String> result = new ArrayList<>();
364                for (IColoredString<T> line : text) {
365                        /* This code must be consistent with #draw in the custom Actor */
366                        final IColoredString<T> tmp = present(line);
367            final String marked = applyMarkup(tmp);
368                        result.add(marked);
369            builder.append(marked);
370            builder.append('\n');
371                }
372        if(builder.length() > 0)
373            builder.deleteCharAt(builder.length() - 1);
374                return result;
375        }
376
377        /**
378         * @return The {@link ScrollPane} containing {@link #getTextActor()}.
379         */
380        public ScrollPane getScrollPane() {
381                return scrollPane;
382        }
383
384        /**
385         * @return The {@link Actor} where the text is drawn. It may be bigger than
386         *         {@link #getScrollPane()}.
387         */
388        public Actor getTextActor() {
389                return textActor;
390        }
391
392        /**
393         * @return The font used, if set.
394         */
395        public /* @Nullable */ BitmapFont getFont() {
396                return font;
397        }
398
399        public void dispose() {
400                if (renderer != null)
401                        renderer.dispose();
402        }
403
404        /**
405         * Callback done to do stuff according to whether y-scrolling is required
406         * 
407         * @param required
408         *            Whether y scrolling is required.
409         */
410        protected void yScrollingCallback(boolean required) {
411                if (required) {
412                        /* Disable borders, they don't mix well with scrollbars */
413                        borderSize = 0;
414                        scrollPane.setFadeScrollBars(false);
415                        scrollPane.setForceScroll(false, true);
416                }
417        }
418
419        /**
420         * @param ics
421         *            Text set when building {@code this}
422         * @return The text to display to screen. If you wanna
423         *         {@link squidpony.IColorCenter#filter(IColoredString) filter} your
424         *         text , do it here.
425         */
426        protected IColoredString<T> present(IColoredString<T> ics) {
427                return ics;
428        }
429
430        /**
431         * @param ics
432         * @return The text obtained after applying {@link #markup}.
433         */
434        protected String applyMarkup(IColoredString<T> ics) {
435                return markup == null ? ics.toString() : ics.presentWithMarkup(markup);
436        }
437
438        /**
439         * @return A fresh renderer.
440         */
441        protected ShapeRenderer buildRenderer() {
442                return new ShapeRenderer();
443        }
444
445        /**
446         * @return The renderer to use.
447         */
448        protected ShapeRenderer getRenderer() {
449                if (renderer == null)
450                        renderer = buildRenderer();
451                return renderer;
452        }
453
454    private class TextActor extends Actor
455    {
456        TextActor()
457        {
458
459        }
460        @Override
461        public void draw(Batch batch, float parentAlpha) {
462
463            final float tx = 0;//getX();
464            final float ty = 0;//getY();
465            final float twidth = getWidth();
466            final float theight = getHeight();
467
468            final float height = scrollPane.getHeight();
469
470            if (backgroundColor != null) {
471                batch.setColor(backgroundColor);
472                batch.draw(tcf.getSolid(), tx, ty, twidth, theight);
473                batch.setColor(Color.WHITE);
474                /*
475                batch.end();
476
477                final Matrix4 m = batch.getTransformMatrix();
478                final ShapeRenderer sr = getRenderer();
479                sr.setTransformMatrix(m);
480                sr.begin(ShapeType.Filled);
481                sr.setColor(backgroundColor);
482                UIUtil.drawRectangle(renderer, tx, ty, twidth, theight, ShapeType.Filled,
483                        backgroundColor);
484                sr.end();
485
486                batch.begin();
487                */
488            }
489
490            if (font == null)
491                throw new NullPointerException(
492                        "The font should be set when drawing a " + getClass().getSimpleName());
493            if (text == null)
494                throw new NullPointerException(
495                        "The text should be set when drawing a " + getClass().getSimpleName());
496            if (tcf != null) {
497                tcf.configureShader(batch);
498            }
499            float yscroll = scrollPane.getScrollY();
500
501            final float destx = tx, offY = (tcf != null) ? tcf.height * 0.5f : 0;
502            getTypesetText();
503            font.draw(batch, builder, destx, theight + yscroll - offY,
504                    0, builder.length(), twidth, Align.left, true);
505
506        }
507    }
508
509}