1package com.jme3.font;
2
3import com.jme3.font.BitmapFont.Align;
4import com.jme3.font.BitmapFont.VAlign;
5import com.jme3.font.ColorTags.Range;
6import com.jme3.math.ColorRGBA;
7import java.util.LinkedList;
8
9/**
10 * Manage and align LetterQuads
11 * @author YongHoon
12 */
13class Letters {
14    private final LetterQuad head;
15    private final LetterQuad tail;
16    private final BitmapFont font;
17    private LetterQuad current;
18    private StringBlock block;
19    private float totalWidth;
20    private float totalHeight;
21    private ColorTags colorTags = new ColorTags();
22    private ColorRGBA baseColor = null;
23
24    Letters(BitmapFont font, StringBlock bound, boolean rightToLeft) {
25        final String text = bound.getText();
26        this.block = bound;
27        this.font = font;
28        head = new LetterQuad(font, rightToLeft);
29        tail = new LetterQuad(font, rightToLeft);
30        setText(text);
31    }
32
33    void setText(final String text) {
34        colorTags.setText(text);
35        String plainText = colorTags.getPlainText();
36
37        head.setNext(tail);
38        tail.setPrevious(head);
39        current = head;
40        if (text != null && plainText.length() > 0) {
41            LetterQuad l = head;
42            for (int i = 0; i < plainText.length(); i++) {
43                l = l.addNextCharacter(plainText.charAt(i));
44                if (baseColor != null) {
45                    // Give the letter a default color if
46                    // one has been provided.
47                    l.setColor( baseColor );
48                }
49            }
50        }
51
52        LinkedList<Range> ranges = colorTags.getTags();
53        if (!ranges.isEmpty()) {
54            for (int i = 0; i < ranges.size()-1; i++) {
55                Range start = ranges.get(i);
56                Range end = ranges.get(i+1);
57                setColor(start.start, end.start, start.color);
58            }
59            Range end = ranges.getLast();
60            setColor(end.start, plainText.length(), end.color);
61        }
62
63        invalidate();
64    }
65
66    LetterQuad getHead() {
67        return head;
68    }
69
70    LetterQuad getTail() {
71        return tail;
72    }
73
74    void update() {
75        LetterQuad l = head;
76        int lineCount = 1;
77        BitmapCharacter ellipsis = font.getCharSet().getCharacter(block.getEllipsisChar());
78        float ellipsisWidth = ellipsis!=null? ellipsis.getWidth()*getScale(): 0;
79
80        while (!l.isTail()) {
81            if (l.isInvalid()) {
82                l.update(block);
83
84                if (l.isInvalid(block)) {
85                    switch (block.getLineWrapMode()) {
86                    case Character:
87                        lineWrap(l);
88                        lineCount++;
89                        break;
90                    case Word:
91                        if (!l.isBlank()) {
92                            // search last blank character before this word
93                            LetterQuad blank = l;
94                            while (!blank.isBlank()) {
95                                if (blank.isLineStart() || blank.isHead()) {
96                                    lineWrap(l);
97                                    lineCount++;
98                                    blank = null;
99                                    break;
100                                }
101                                blank = blank.getPrevious();
102                            }
103                            if (blank != null) {
104                                blank.setEndOfLine();
105                                lineCount++;
106                                while (blank != l) {
107                                    blank = blank.getNext();
108                                    blank.invalidate();
109                                    blank.update(block);
110                                }
111                            }
112                        }
113                        break;
114                    case NoWrap:
115                        // search last blank character before this word
116                        LetterQuad cursor = l.getPrevious();
117                        while (cursor.isInvalid(block, ellipsisWidth) && !cursor.isLineStart()) {
118                            cursor = cursor.getPrevious();
119                        }
120                        cursor.setBitmapChar(ellipsis);
121                        cursor.update(block);
122                        cursor = cursor.getNext();
123                        while (!cursor.isTail() && !cursor.isLineFeed()) {
124                            cursor.setBitmapChar(null);
125                            cursor.update(block);
126                            cursor = cursor.getNext();
127                        }
128                        break;
129                    }
130                }
131            } else if (current.isInvalid(block)) {
132                invalidate(current);
133            }
134            if (l.isEndOfLine()) {
135                lineCount++;
136            }
137            l = l.getNext();
138        }
139
140        align();
141        block.setLineCount(lineCount);
142        rewind();
143    }
144
145    private void align() {
146        final Align alignment = block.getAlignment();
147        final VAlign valignment = block.getVerticalAlignment();
148        if (block.getTextBox() == null || (alignment == Align.Left && valignment == VAlign.Top))
149            return;
150        LetterQuad cursor = tail.getPrevious();
151        cursor.setEndOfLine();
152        final float width = block.getTextBox().width;
153        final float height = block.getTextBox().height;
154        float lineWidth = 0;
155        float gapX = 0;
156        float gapY = 0;
157        validateSize();
158        if (totalHeight < height) { // align vertically only for no overflow
159            switch (valignment) {
160            case Top:
161                gapY = 0;
162                break;
163            case Center:
164                gapY = (height-totalHeight)*0.5f;
165                break;
166            case Bottom:
167                gapY = height-totalHeight;
168                break;
169            }
170        }
171        while (!cursor.isHead()) {
172            if (cursor.isEndOfLine()) {
173                lineWidth = cursor.getX1()-block.getTextBox().x;
174                if (alignment == Align.Center) {
175                    gapX = (width-lineWidth)/2;
176                } else if (alignment == Align.Right) {
177                    gapX = width-lineWidth;
178                } else {
179                    gapX = 0;
180                }
181            }
182            cursor.setAlignment(gapX, gapY);
183            cursor = cursor.getPrevious();
184        }
185    }
186
187    private void lineWrap(LetterQuad l) {
188        if (l.isHead() || l.isBlank())
189            return;
190        l.getPrevious().setEndOfLine();
191        l.invalidate();
192        l.update(block); // TODO: update from l
193    }
194
195    float getCharacterX0() {
196        return current.getX0();
197    }
198
199    float getCharacterY0() {
200        return current.getY0();
201    }
202
203    float getCharacterX1() {
204        return current.getX1();
205    }
206
207    float getCharacterY1() {
208        return current.getY1();
209    }
210
211    float getCharacterAlignX() {
212        return current.getAlignX();
213    }
214
215    float getCharacterAlignY() {
216        return current.getAlignY();
217    }
218
219    float getCharacterWidth() {
220        return current.getWidth();
221    }
222
223    float getCharacterHeight() {
224        return current.getHeight();
225    }
226
227    public boolean nextCharacter() {
228        if (current.isTail())
229            return false;
230        current = current.getNext();
231        return true;
232    }
233
234    public int getCharacterSetPage() {
235        return current.getBitmapChar().getPage();
236    }
237
238    public LetterQuad getQuad() {
239        return current;
240    }
241
242    public void rewind() {
243        current = head;
244    }
245
246    public void invalidate() {
247        invalidate(head);
248    }
249
250    public void invalidate(LetterQuad cursor) {
251        totalWidth = -1;
252        totalHeight = -1;
253
254        while (!cursor.isTail() && !cursor.isInvalid()) {
255            cursor.invalidate();
256            cursor = cursor.getNext();
257        }
258    }
259
260    float getScale() {
261        return block.getSize() / font.getCharSet().getRenderedSize();
262    }
263
264    public boolean isPrintable() {
265        return current.getBitmapChar() != null;
266    }
267
268    float getTotalWidth() {
269        validateSize();
270        return totalWidth;
271    }
272
273    float getTotalHeight() {
274        validateSize();
275        return totalHeight;
276    }
277
278    void validateSize() {
279        if (totalWidth < 0) {
280            LetterQuad l = head;
281            while (!l.isTail()) {
282                totalWidth = Math.max(totalWidth, l.getX1());
283                l = l.getNext();
284                totalHeight = Math.max(totalHeight, -l.getY1());
285            }
286        }
287    }
288
289    /**
290     * @param start start index to set style. inclusive.
291     * @param end   end index to set style. EXCLUSIVE.
292     * @param style
293     */
294    void setStyle(int start, int end, int style) {
295        LetterQuad cursor = head.getNext();
296        while (!cursor.isTail()) {
297            if (cursor.getIndex() >= start && cursor.getIndex() < end) {
298                cursor.setStyle(style);
299            }
300            cursor = cursor.getNext();
301        }
302    }
303
304    /**
305     * Sets the base color for all new letter quads and resets
306     * the color of existing letter quads.
307     */
308    void setColor( ColorRGBA color ) {
309        baseColor = color;
310        setColor( 0, block.getText().length(), color );
311    }
312
313    ColorRGBA getBaseColor() {
314        return baseColor;
315    }
316
317    /**
318     * @param start start index to set style. inclusive.
319     * @param end   end index to set style. EXCLUSIVE.
320     * @param color
321     */
322    void setColor(int start, int end, ColorRGBA color) {
323        LetterQuad cursor = head.getNext();
324        while (!cursor.isTail()) {
325            if (cursor.getIndex() >= start && cursor.getIndex() < end) {
326                cursor.setColor(color);
327            }
328            cursor = cursor.getNext();
329        }
330    }
331}