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.ui.ScrollPane;
014import com.badlogic.gdx.utils.Align;
015import squidpony.panel.IColoredString;
016import squidpony.squidgrid.gui.gdx.UIUtil.CornerStyle;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021
022/**
023 * A panel to display some text using libgdx directly (i.e. without using
024 * {@link SquidPanel}) as in these examples (no scrolling first, then with a
025 * scroll bar):
026 *
027 * <p>
028 * <ul>
029 * <li><img src="http://i.imgur.com/EqEXqlu.png"/></li>
030 * <li><img src="http://i.imgur.com/LYbxQZE.png"/></li>
031 * </ul>
032 * </p>
033 *
034 * <p>
035 * It supports vertical scrolling, i.e. it'll put a vertical scrollbar if
036 * there's too much text to display. This class does a lot of stuff, you
037 * typically only have to provide the textures for the scrollbars and the scroll
038 * knobs (see example below).
039 * </p>
040 *
041 * <p>
042 * A typical usage of this class is as follows:
043 *
044 * <pre>{@code
045 * final TextPanel<Color> tp = new TextPanel<>(new GDXMarkup(), font);
046 * tp.init(screenWidth, screenHeight, text); <- first 2 params: for fullscreen
047 * final ScrollPane sp = tp.getScrollPane();
048 * sp.setScrollPaneStyle(new ScrollPaneStyle(...)); <- set textures
049 * stage.addActor(sp);
050 * stage.setScrollFocus(sp);
051 * stage.draw();
052 * }</pre>
053 * </p>
054 *
055 * <p>
056 * This class shares what {@link ScrollPane} does (knobs, handling of the wheel).
057 * </p>
058 * <p>
059 * Drawing the result of {@link #getScrollPane()} will set the shader of {@code batch} if using a distance field or MSDF
060 * font and the shader is currently not configured for such a font; it does not reset the shader to the default so that
061 * multiple Actors can all use the same shader and so specific extra glyphs or other items can be rendered after calling
062 * draw(). If you need to draw both a distance field font and full-color art, you should set the shader on the Batch to
063 * null when you want to draw full-color art, and end the Batch between drawing this object and the other art.
064 * </p>
065 * @author smelC
066 *
067 * @see ScrollPane A libGDX widget for general scrolling through only the visible part of a large widget
068 */
069public class TextPanel {
070
071        /**
072         * The color to use to paint the background (outside buttons) using
073         * {@link ShapeRenderer}. Or {@code null} to disable background coloring.
074         */
075        public /* @Nullable */ Color backgroundColor;
076
077        /**
078         * The color of the border around this panel, if any. If set, it'll be
079         * rendered using {@link ShapeRenderer} and {@link #borderStyle}.
080         */
081        public /* @Nullable */ Color borderColor;
082
083        /** The size of the border, if any */
084        public float borderSize;
085
086        public CornerStyle borderStyle = CornerStyle.ROUNDED;
087        
088        protected BitmapFont font;
089        protected TextCellFactory tcf;
090        /** The text to display */
091        public ArrayList<CharSequence> text;
092
093        protected final ScrollPane scrollPane;
094
095        /**
096         * The actor whose size is adjusted to the text. When scrolling is required,
097         * it is bigger than {@link #scrollPane}.
098         */
099        protected final Actor textActor;
100
101        /** Do not access directly, use {@link #getRenderer()} */
102        private /* @Nullable */ ShapeRenderer renderer;
103
104        /**
105         * The text to display MUST be set later on with
106         * {@link #init(float, float, Collection)}.
107         *
108         * @param font
109         *            The font to use. It can be set later using
110         *            {@link #setFont(BitmapFont)}, but it MUST be set before
111         *            drawing this panel.
112         */
113        public TextPanel(/* @Nullable */ BitmapFont font) {
114                if (font != null)
115                        setFont(font);
116                textActor = new TextActor();
117
118                this.scrollPane = new ScrollPane(textActor);
119        }
120
121        /**
122         * The text to display MUST be set later on with {@link #init(float, float, Collection)} (which can't be updated) or
123         * {@link #initShared(float, float, ArrayList)} (which reflects changes in the given ArrayList).
124         *
125         * @param font
126         *            A TextCellFactory, typically holding a distance field font ("stretchable" or "crisp" in
127         *            DefaultResources). This won't force glyphs into same-size cells, despite the name.
128         */
129        public TextPanel(/* @Nullable */ TextCellFactory font) {
130                if (font != null)
131                {
132                        tcf = font;
133                        tcf.initBySize();
134                        this.font = tcf.font();
135                        this.font.getData().markupEnabled = true;
136                }
137                textActor = new TextActor();
138                scrollPane = new ScrollPane(textActor);
139        }
140        
141        /**
142         * Sets the font to use. This method should be called once before {@link #init(float, float, Collection)} if the
143         * font wasn't given at creation-time.
144         *
145         * @param font The font to use as a BitmapFont.
146         */
147        public void setFont(BitmapFont font) {
148                font.getData().markupEnabled = true;
149                this.font = font;
150                tcf = new TextCellFactory().font(font).height(MathUtils.ceil(font.getLineHeight()))
151                                .width(MathUtils.round(font.getSpaceXadvance()));
152        }
153
154        /**
155         * Sets the font to use. This method should be called once before {@link #init(float, float, Collection)} if the
156         * font wasn't given at creation-time.
157         *
158         * @param font The font to use as a TextCellFactory.
159         */
160        public void setFont(TextCellFactory font) {
161                if (font != null)
162                {
163                        tcf = font;
164                        tcf.initBySize();
165                        this.font = tcf.font();
166                        this.font.getData().markupEnabled = true;
167                }
168        }
169
170        /**
171         * This method sets the sizes of {@link #scrollPane} and {@link #textActor}.
172         * This method MUST be called before rendering.
173         *
174         * @param maxHeight
175         *            The maximum height that the scrollpane can take (equal or
176         *            smaller than the height of the text actor).
177         * @param width
178         *            The width of the scrollpane and the text actor.
179         * @param coloredText any Collection of IColoredString that use Color or a subclass as their color type
180         */
181        public void init(float width, float maxHeight, Collection<? extends IColoredString<Color>> coloredText) {
182                if (tcf == null)
183                        throw new NullPointerException(
184                                        "The font should be set before calling TextPanel.init()");
185                
186                this.text = new ArrayList<>(coloredText.size());
187                for (IColoredString<Color> ics : coloredText)
188                        text.add(ics.presentWithMarkup(GDXMarkup.instance));
189                scrollPane.setWidth(width);
190                textActor.setWidth(width);
191                
192                scrollPane.setHeight(maxHeight);
193                scrollPane.setActor(textActor);
194                scrollPane.layout();
195        }
196
197        /**
198         * This method sets the sizes of {@link #scrollPane} and {@link #textActor}, and shares a direct reference to
199         * {@code text} so changes to that ArrayList will also be picked up here and rendered.
200         * This method MUST be called before rendering.
201         *
202         * @param maxHeight
203         *            The maximum height that the scrollpane can take (equal or
204         *            smaller than the height of the text actor).
205         * @param width
206         *            The width of the scrollpane and the text actor.
207         * @param text an ArrayList of CharSequence that will be used directly by this TextPanel (changes
208         *             to the ArrayList will show up in the TextPanel)
209         */
210        public void initShared(float width, float maxHeight, ArrayList<CharSequence> text) {
211                this.text = text;
212
213                scrollPane.setWidth(width);
214                textActor.setWidth(width);
215
216                if (tcf == null)
217                        throw new NullPointerException(
218                                        "The font should be set before calling TextPanel.init()");
219
220                //prepareText();
221//              final boolean yscroll = maxHeight < textActor.getHeight();
222                scrollPane.setHeight(maxHeight);
223                scrollPane.setActor(textActor);
224                //yScrollingCallback(yscroll);
225                scrollPane.layout();
226        }
227
228        public void init(float width, float maxHeight, Color color, String... text)
229        {
230                if (tcf == null)
231                        throw new NullPointerException(
232                                        "The font should be set before calling TextPanel.init()");
233
234                this.text = new ArrayList<>(text.length);
235                Collections.addAll(this.text, text);
236                scrollPane.setWidth(width);
237                textActor.setWidth(width);
238
239                scrollPane.setHeight(maxHeight);
240                scrollPane.setActor(textActor);
241                scrollPane.layout();
242        }
243
244        /**
245         * Draws the border. You have to call this method manually, because the
246         * border is outside the actor and hence should be drawn at the very end,
247         * otherwise it can get overwritten by UI elements.
248         *
249         * @param batch
250         */
251        public void drawBorder(Batch batch) {
252                if (borderColor != null && 0 < borderSize) {
253                        final boolean reset = batch.isDrawing();
254                        if (reset)
255                                batch.end();
256
257                        final ShapeRenderer sr = getRenderer();
258                        final Matrix4 m = batch.getTransformMatrix();
259                        sr.setTransformMatrix(m);
260                        sr.begin(ShapeType.Filled);
261                        sr.setColor(borderColor);
262//                      prepareText();
263                        final float down = font.getData().down;
264                        UIUtil.drawMarginsAround(sr, scrollPane.getX(), scrollPane.getY() + down * -0.5f + 4f, scrollPane.getWidth(),
265                                        scrollPane.getHeight() + down * -1.5f - 4f, borderSize * 2f, borderColor, borderStyle, 1f, 1f);
266                        sr.end();
267
268                        if (reset)
269                                batch.begin();
270                }
271        }
272
273        /**
274         * @return The text to draw, without color information present in {@link #text}.
275         */
276        public /* @Nullable */ ArrayList<String> getTypesetText() {
277                if (text == null)
278                        return null;
279                final ArrayList<String> result = new ArrayList<>();
280                for (CharSequence line : text) {
281                        result.add(GDXMarkup.instance.removeMarkup(line).toString());
282                }
283                return result;
284        }
285
286        /**
287         * Updates the text this will show based on the current contents of the ArrayList of IColoredString values that may
288         * be shared due to {@link #initShared(float, float, ArrayList)}, then resizes the {@link #getTextActor()} to fit
289         * the current text and lays out the {@link #getScrollPane()} to match. Called in the text actor's draw() method. 
290         */
291        protected void prepareText() {
292                if (text == null)
293                        return;
294                final BitmapFontCache cache = font.getCache();
295                //cache.clear();
296                final float w = scrollPane.getWidth();
297                float lineHeight = -font.getData().down, capHeight = font.getCapHeight();
298                int lines = 1;//, ci = 0;
299                StringBuilder sb = new StringBuilder(256);
300                if(tcf.supportedStyles() <= 1) {
301                        for (int m = 0, textSize = text.size(); m < textSize; m++) {
302                                sb.append(GDXMarkup.instance.colorStringOnlyMarkup(text.get(m))).append('\n');
303                        }
304                }
305                else
306                {
307                        for (int m = 0, textSize = text.size(); m < textSize; m++) {
308                                sb.append(GDXMarkup.instance.colorStringMarkup(text.get(m))).append('\n');
309                        }
310                }
311
312                GlyphLayout layout = cache.setText(sb, 0, 0, w, Align.left, true);                      
313                lines += layout.height / capHeight;
314                
315                ////TODO: BitmapFontCache.setColors(float, int, int) is broken in libGDX 1.9.10; find some workaround
316//              for (int m = 0, textSize = text.size(); m < textSize; m++) {
317//                      IColoredString<Color> line = text.get(m);
318//                      ArrayList<IColoredString.Bucket<Color>> frags = line.getFragments();
319//                      for (int i = 0; i < frags.size(); i++) {
320//                              final IColoredString.Bucket<Color> b = frags.get(i);
321////                            Color c = b.getColor();
322////                            if(c != null) 
323////                                    cache.setColors(c, ci, (ci += b.length()));
324////                            else
325//                                      ci += b.length();
326//                      }
327//
328////                    if(m + 1 < textSize)
329////                    {
330//                              //cache.addText("\n", pos, (-lines) * totalTextHeight, w, Align.left, true);
331//                              //lines++;
332////                    }
333//              }
334                lineHeight *= lines;
335                if(lineHeight < 0)
336                        lineHeight = 0;
337                textActor.setHeight(/* Entire height */ lineHeight);
338                scrollPane.layout();
339
340        }
341
342        /**
343         * Scrolls the scroll pane this holds down by some number of rows (which may be fractional, and may be negative to
344         * scroll up). This is not a smooth scroll, and will not be animated.
345         * @param downDistance The distance in rows to scroll down, which can be negative to scroll up instead
346         */
347        public void scroll(final float downDistance)
348        {
349                prepareText();
350                scrollPane.setScrollY(scrollPane.getScrollY() + downDistance * tcf.actualCellHeight);
351        }
352
353        /**
354         * If the parameter is true, scrolls to the top of this scroll pane; otherwise scrolls to the bottom. This is not a
355         * smooth scroll, and will not be animated.
356         * @param goToTop If true, will scroll to the top edge; if false, will scroll to the bottom edge.
357         */
358        public void scrollToEdge(final boolean goToTop)
359        {
360                prepareText();
361                scrollPane.setScrollPercentY(goToTop ? 0f : 1f);
362        }
363
364        /**
365         * @return The {@link ScrollPane} containing {@link #getTextActor()}.
366         */
367        public ScrollPane getScrollPane() {
368                return scrollPane;
369        }
370
371        /**
372         * @return The {@link Actor} where the text is drawn. It may be bigger than
373         *         {@link #getScrollPane()}.
374         */
375        public Actor getTextActor() {
376                return textActor;
377        }
378
379        /**
380         * @return The font used, if set, as a TextCellFactory (one is always created even if only given a BitmapFont).
381         */
382        public /* @Nullable */ TextCellFactory getFont() {
383                return tcf;
384        }
385
386        public void dispose() {
387                if (renderer != null)
388                        renderer.dispose();
389        }
390
391        /**
392         * Callback done to do stuff according to whether y-scrolling is required
393         *
394         * @param required
395         *            Whether y scrolling is required.
396         */
397        protected void yScrollingCallback(boolean required) {
398                if (required) {
399                        /* Disable borders, they don't mix well with scrollbars */
400                        borderSize = 0;
401                        scrollPane.setFadeScrollBars(false);
402                        scrollPane.setForceScroll(false, true);
403                }
404        }
405
406        /**
407         * @return A fresh renderer.
408         */
409        protected ShapeRenderer buildRenderer() {
410                return new ShapeRenderer();
411        }
412
413        /**
414         * @return The renderer to use.
415         */
416        protected ShapeRenderer getRenderer() {
417                if (renderer == null)
418                        renderer = buildRenderer();
419                return renderer;
420        }
421
422        private class TextActor extends Actor
423        {
424                TextActor()
425                {
426
427                }
428                @Override
429                public void draw(Batch batch, float parentAlpha) {
430                        prepareText();
431                        final float tx = 0f;//scrollPane.getX();
432                        final float ty = 0f;//scrollPane.getY();
433                        final float twidth = scrollPane.getWidth();
434                        final float theight = scrollPane.getHeight();
435
436                        if (backgroundColor != null) {
437                                batch.setColor(backgroundColor);
438                                batch.draw(tcf.getSolid(), tx, ty, twidth, theight);
439                                batch.setColor(SColor.WHITE);
440                /*
441                batch.end();
442
443                final Matrix4 m = batch.getTransformMatrix();
444                final ShapeRenderer sr = getRenderer();
445                sr.setTransformMatrix(m);
446                sr.begin(ShapeType.Filled);
447                sr.setColor(backgroundColor);
448                UIUtil.drawRectangle(renderer, tx, ty, twidth, theight, ShapeType.Filled,
449                        backgroundColor);
450                sr.end();
451
452                batch.begin();
453                */
454                        }
455
456                        if (font == null)
457                                throw new NullPointerException(
458                                                "The font should be set when drawing a TextPanel's TextActor");
459                        if (text == null)
460                                throw new NullPointerException(
461                                                "The font should be set when drawing a TextPanel's TextActor");
462                        if (tcf != null) {
463                                tcf.configureShader(batch);
464                        }
465//                      final float offY = 0;//(tcf != null) ? tcf.actualCellHeight * 0.5f : 0;
466                        final BitmapFontCache cache = font.getCache();
467                        cache.setPosition(tx, scrollPane.getHeight() + scrollPane.getScrollY());
468                        cache.draw(batch);
469                }
470        }
471
472}