001package squidpony.panel;
002
003import squidpony.StringKit;
004
005import java.util.ArrayList;
006import java.util.Iterator;
007import java.util.List;
008import java.util.ListIterator;
009import java.util.NoSuchElementException;
010
011/**
012 * A {@link String} divided in chunks of different colors. Use the
013 * {@link Iterable} interface to get the pieces.
014 * 
015 * @author smelC
016 * 
017 * @param <T>
018 *            The type of colors;
019 */
020public interface IColoredString<T> extends Iterable<IColoredString.Bucket<T>> {
021
022        /**
023         * A convenience alias for {@code append(c, null)}.
024         * 
025         * @param c the char to append
026         */
027        void append(/* @Nullable */ char c);
028
029        /**
030         * Mutates {@code this} by appending {@code c} to it.
031         * 
032         * @param c
033         *            The text to append.
034         * @param color
035         *            {@code text}'s color. Or {@code null} to let the panel decide.
036         */
037        void append(char c, /* @Nullable */T color);
038
039        /**
040         * A convenience alias for {@code append(text, null)}.
041         * 
042         * @param text
043         */
044        void append(/* @Nullable */ String text);
045
046        /**
047         * Mutates {@code this} by appending {@code text} to it. Does nothing if
048         * {@code text} is {@code null}.
049         * 
050         * @param text
051         *            The text to append.
052         * @param color
053         *            {@code text}'s color. Or {@code null} to let the panel decide.
054         */
055        void append(/* @Nullable */String text, /* @Nullable */T color);
056
057        /**
058         * Mutates {@code this} by appending {@code i} to it.
059         * 
060         * @param i
061         *            The int to append.
062         * @param color
063         *            {@code text}'s color. Or {@code null} to let the panel decide.
064         */
065        void appendInt(int i, /* @Nullable */T color);
066
067        /**
068         * Mutates {@code this} by appending {@code f} to it.
069         * 
070         * @param f
071         *            The float to append.
072         * @param color
073         *            {@code text}'s color. Or {@code null} to let the panel decide.
074         */
075        void appendFloat(float f, /* @Nullable */T color);
076
077        /**
078         * Mutates {@code this} by appending {@code other} to it.
079         * 
080         * @param other
081         */
082        void append(IColoredString<T> other);
083
084        /**
085         * Replace color {@code old} by {@code new_} in all buckets of {@code this}.
086         * 
087         * @param old
088         *            The color to replace.
089         * @param new_
090         *            The replacing color.
091         */
092        void replaceColor(/* @Nullable */ T old, /* @Nullable */ T new_);
093
094        /**
095         * Set {@code color} in all buckets.
096         * 
097         * @param color
098         */
099        void setColor(/*@Nullable*/ T color);
100
101        /**
102         * Deletes all content after index {@code len} (if any).
103         * 
104         * @param len
105         */
106        void setLength(int len);
107
108        /**
109         * @return {@code true} if {@link #present()} is {@code ""}.
110         */
111        boolean isEmpty();
112
113        /**
114         * @param width
115         *            A positive integer
116         * @return {@code this} split in pieces that would fit in a display with
117         *         {@code width} columns (if all words in {@code this} are smaller
118         *         or equal in length to {@code width}, otherwise wrapping will fail
119         *         for these words).
120         */
121        List<IColoredString<T>> wrap(int width);
122
123        /**
124         * @param width
125         *            A positive integer
126         * @param buf 
127         *            A List of IColoredString with the same T type as this; will have the wrapped contents appended to it.
128         *            Cannot be null, and if it has existing contents, they will be left as-is.
129         * @return {@code buf} containing {@code this} split in pieces that would fit in a display with
130         *         {@code width} columns (if all words in {@code this} are smaller
131         *         or equal in length to {@code width}, otherwise wrapping will fail
132         *         for these words).
133         */
134        
135        List<IColoredString<T>> wrap(int width, List<IColoredString<T>> buf);
136
137        /**
138         * This method does NOT guarantee that the result's length is {@code width}.
139         * It is impossible to do correct justifying if {@code this}'s length is
140         * greater than {@code width} or if {@code this} has no space character.
141         * 
142         * @param width
143         * @return A variant of {@code this} where spaces have been introduced
144         *         in-between words, so that {@code this}'s length is as close as
145         *         possible to {@code width}. Or {@code this} itself if unaffected.
146         */
147        IColoredString<T> justify(int width);
148
149        /**
150         * Empties {@code this}.
151         */
152        void clear();
153
154        /**
155         * This method is typically more efficient than {@link #colorAt(int)}.
156         * 
157         * @return The color of the last bucket, if any.
158         */
159        /* @Nullable */ T lastColor();
160
161        /**
162         * @param index
163         * @return The color at {@code index}, if any.
164         * @throws NoSuchElementException
165         *             If {@code index} equals or is greater to {@link #length()}.
166         */
167        /* @Nullable */ T colorAt(int index);
168
169        /**
170         * @param index
171         * @return The character at {@code index}, if any.
172         * @throws NoSuchElementException
173         *             If {@code index} equals or is greater to {@link #length()}.
174         */
175        char charAt(int index);
176
177        /**
178         * @return The length of text.
179         */
180        int length();
181
182        /**
183         * @return The text that {@code this} represents.
184         */
185        String present();
186
187        /**
188         * Given some way of converting from a T value to an in-line markup tag, returns a string representation of
189         * this IColoredString with in-line markup representing colors.
190         * @param markup an IMarkup implementation
191         * @return a String with markup inserted inside.
192         */
193        String presentWithMarkup(IMarkup<T> markup);
194
195        /**
196         * Gets the Buckets as an ArrayList, allowing access by index instead of by {@link #iterator()}.
197         * @return the Buckets that would be returned by {@link #iterator()}, but in an ArrayList.
198         */
199        ArrayList<Bucket<T>> getFragments();
200        
201        /**
202         * A basic implementation of {@link IColoredString}.
203         * 
204         * @author smelC
205         * 
206         * @param <T>
207         *            The type of colors
208         */
209        class Impl<T> implements IColoredString<T> {
210
211                protected final ArrayList<Bucket<T>> fragments;
212                
213                /**
214                 * An empty instance.
215                 */
216                public Impl() {
217                        fragments = new ArrayList<>();
218                }
219
220                /**
221                 * An instance initially containing {@code text} (with {@code color}).
222                 * 
223                 * @param text
224                 *            The text that {@code this} should contain.
225                 * @param color
226                 *            The color of {@code text}.
227                 */
228                public Impl(String text, /* @Nullable */T color) {
229                        this();
230
231                        append(text, color);
232                }
233
234                /**
235                 * A static constructor, to avoid having to write {@code <T>} in the
236                 * caller.
237                 * 
238                 * @return {@code new Impl(s, t)}.
239                 */
240                public static <T> IColoredString.Impl<T> create() {
241                        return new IColoredString.Impl<>("", null);
242                }
243
244                /**
245                 * A convenience method, equivalent to {@code create(s, null)}.
246                 * 
247                 * @param s
248                 * @return {@code create(s, null)}
249                 */
250                public static <T> IColoredString.Impl<T> create(String s) {
251                        return create(s, null);
252                }
253
254                /**
255                 * A static constructor, to avoid having to write {@code <T>} in the
256                 * caller.
257                 * 
258                 * @return {@code new Impl(s, t)}.
259                 */
260                public static <T> IColoredString.Impl<T> create(String s, /* @Nullable */ T t) {
261                        return new IColoredString.Impl<>(s, t);
262                }
263
264                public static <T> IColoredString.Impl<T> clone(IColoredString<T> toClone) {
265                        final IColoredString.Impl<T> result = new IColoredString.Impl<T>();
266                        result.append(toClone);
267                        return result;
268                }
269
270                /**
271                 * @param one
272                 * @param two 
273                 * @return Whether {@code one} represents the same content as
274                 *         {@code two}.
275                 */
276                /*
277                 * Method could be smarter, i.e. return true more often, by doing some
278                 * normalization. It is unnecessary if you only create instances of
279                 * IColoredString.Impl.
280                 */
281                public static <T> boolean equals(IColoredString<T> one, IColoredString<T> two) {
282                        if (one == two)
283                                return true;
284
285                        final Iterator<IColoredString.Bucket<T>> oneIt = one.iterator();
286                        final Iterator<IColoredString.Bucket<T>> twoIt = two.iterator();
287                        while (true) {
288                                if (oneIt.hasNext()) {
289                                        if (twoIt.hasNext()) {
290                                                final Bucket<T> oneb = oneIt.next();
291                                                final Bucket<T> twob = twoIt.next();
292                                                if (!equals(oneb.getText(), twob.getText()))
293                                                        return false;
294                                                if (!equals(oneb.getColor(), twob.getColor()))
295                                                        return false;
296                                        } else
297                                                /* 'this' not terminated, but 'other' is. */
298                                                return false;
299                                } else {
300                                        if (twoIt.hasNext())
301                                                /* 'this' terminated, but not 'other'. */
302                                                return false;
303                                        else
304                                                /* Both terminated */
305                                                break;
306                                }
307
308                        }
309                        return true;
310                }
311
312                @Override
313                public void append(char c) {
314                        append(c, null);
315                }
316
317                @Override
318                public void append(char c, T color) {
319                        append(String.valueOf(c), color);
320                }
321
322                @Override
323                public void append(String text) {
324                        append(text, null);
325                }
326
327                @Override
328                public void append(String text, T color) {
329                        if (text == null || text.isEmpty())
330                                return;
331
332                        if (fragments.isEmpty())
333                                fragments.add(new Bucket<>(text, color));
334                        else {
335                                final Bucket<T> last = fragments.get(fragments.size() - 1);
336                                if (equals(last.color, color)) {
337                                        /* Append to the last bucket, to avoid extending the list */
338                                        final Bucket<T> novel = last.append(text);
339                                        fragments.remove(fragments.size() - 1);
340                                        fragments.add(novel);
341                                } else
342                                        fragments.add(new Bucket<>(text, color));
343                        }
344                }
345
346                @Override
347                public void appendInt(int i, T color) {
348                        append(String.valueOf(i), color);
349                }
350
351                @Override
352                public void appendFloat(float f, T color) {
353                        final int i = Math.round(f);
354                        append(i == f ? String.valueOf(i) : String.valueOf(f), color);
355                }
356
357                @Override
358                /* KISS implementation */
359                public void append(IColoredString<T> other) {
360                        for (IColoredString.Bucket<T> ofragment : other)
361                                append(ofragment.getText(), ofragment.getColor());
362                }
363
364                @Override
365                public void replaceColor(/* @Nullable */ T old, /* @Nullable */ T new_) {
366                        if (equals(old, new_))
367                                /* Nothing to do */
368                                return;
369
370                        final ListIterator<Bucket<T>> it = fragments.listIterator();
371                        while (it.hasNext()) {
372                                final Bucket<T> bucket = it.next();
373                                if (equals(bucket.color, old)) {
374                                        /* Replace */
375                                        it.remove();
376                                        it.add(new Bucket<T>(bucket.getText(), new_));
377                                }
378                                /* else leave untouched */
379                        }
380                }
381
382                @Override
383                public void setColor(T color) {
384                        final ListIterator<Bucket<T>> it = fragments.listIterator();
385                        while (it.hasNext()) {
386                                final Bucket<T> next = it.next();
387                                if (!equals(color, next.getColor())) {
388                                        it.remove();
389                                        it.add(next.setColor(color));
390                                }
391                        }
392                }
393
394                public void append(Bucket<T> bucket) {
395                        this.fragments.add(new Bucket<>(bucket.getText(), bucket.getColor()));
396                }
397
398                @Override
399                public void setLength(int len) {
400                        int l = 0;
401                        final ListIterator<IColoredString.Bucket<T>> it = fragments.listIterator();
402                        while (it.hasNext()) {
403                                final IColoredString.Bucket<T> next = it.next();
404                                final String ftext = next.text;
405                                final int flen = ftext.length();
406                                final int nextl = l + flen;
407                                if (nextl < len) {
408                                        /* Nothing to do */
409                                        l += flen;
410                                } else if (nextl == len) {
411                                        /* Delete all next fragments */
412                                        while (it.hasNext()) {
413                                                it.next();
414                                                it.remove();
415                                        }
416                                        /* We'll exit the outer loop right away */
417                                } else {
418                                        /* Trim this fragment */
419                                        final IColoredString.Bucket<T> trimmed = next.setLength(len - l);
420                                        /* Replace this fragment */
421                                        it.remove();
422                                        it.add(trimmed);
423                                        /* Delete all next fragments */
424                                        while (it.hasNext()) {
425                                                it.next();
426                                                it.remove();
427                                        }
428                                        /* We'll exit the outer loop right away */
429                                }
430                        }
431                }
432
433                @Override
434                public List<IColoredString<T>> wrap(int width)
435                {
436                        // the one special case that wouldn't involve buf
437                        if (width <= 0) {
438                                /* Really, you should not rely on this behavior */
439                                System.err.println("Cannot wrap string in empty display");
440                                final List<IColoredString<T>> result = new ArrayList<>();
441                                result.add(this);
442                                return result;
443                        }
444                        // delegate to the buf-appending overload
445                        return wrap(width, new ArrayList<IColoredString<T>>());
446                }
447
448                @Override
449                public List<IColoredString<T>> wrap(int width, List<IColoredString<T>> buf) {
450                        if (width <= 0) {
451                                /* Really, you should not rely on this behavior */
452                                System.err.println("Cannot wrap string in empty display");
453                                final List<IColoredString<T>> result = new ArrayList<>();
454                                result.add(this);
455                                return result;
456                        }
457                        if (isEmpty()) {
458                                /*
459                                 * Catch this case early on, as empty lines are eaten below (see
460                                 * code after the while). Checking emptiness is cheap anyway.
461                                 */
462                                buf.add(this);
463                                return buf;
464                        }
465
466                        IColoredString<T> current = create();
467                        int curlen = 0;
468                        final Iterator<Bucket<T>> it = iterator();
469                        while (it.hasNext()) {
470                                final Bucket<T> next = it.next();
471                                final String bucket = next.getText();
472                                final String[] split = StringKit.split(bucket," ");
473                                final T color = next.color;
474                                for (int i = 0; i < split.length; i++) {
475                                        // This section was needed when using String.split() above, but not for
476                                        // StringKit.split(), which keeps leading and trailing delimiters.
477//                                      if (i == split.length - 1 && bucket.endsWith(" "))
478//                                              /*
479//                                               * Do not lose trailing space that got eaten by
480//                                               * 'bucket.split'.
481//                                               */
482//                                              split[i] = split[i] + " ";
483                                        final String chunk = split[i];
484                                        final int chunklen = chunk.length();
485                                        final boolean addLeadingSpace = 0 < curlen && 0 < i;
486                                        if (curlen + chunklen + (addLeadingSpace ? 1 : 0) <= width) {
487                                                if (addLeadingSpace) {
488                                                        /*
489                                                         * Do not forget space on which chunk got split. If
490                                                         * the space is offscreen, it's harmless, hence not
491                                                         * checking it.
492                                                         */
493                                                        current.append(' ', null);
494                                                        curlen++;
495                                                }
496
497                                                /* Can add it */
498                                                current.append(chunk, color);
499                                                /* Extend size */
500                                                curlen += chunklen;
501                                        } else {
502                                                /* Need to wrap */
503                                                /* Flush content so far */
504                                                if (!current.isEmpty())
505                                                        buf.add(current);
506                                                /*
507                                                 * else: line was prepared, but did not contain anything
508                                                 */
509                                                if (chunklen <= width) {
510                                                        current = create();
511                                                        current.append(chunk, color);
512                                                        /* Reinit size */
513                                                        curlen = chunklen;
514                                                } else {
515                                                        /*
516                                                         * This word is too long. Adding it and preparing a
517                                                         * new line immediately.
518                                                         */
519                                                        /* Add */
520                                                        buf.add(new Impl<>(chunk, color));
521                                                        /* Prepare for next rolls */
522                                                        current = create();
523                                                        /* Reinit size */
524                                                        curlen = 0;
525                                                }
526                                        }
527                                }
528                        }
529
530                        if (!current.isEmpty()) {
531                                /* Flush rest */
532                                buf.add(current);
533                        }
534
535                        return buf;
536                }
537
538                @Override
539                /*
540                 * smelC: not the cutest result (we should add spaces both from the left
541                 * and the right, instead of just from the left), but better than
542                 * nothing.
543                 */
544                public IColoredString<T> justify(int width) {
545                        int length = length();
546
547                        if (width <= length)
548                                /*
549                                 * If width==length, we're good. If width<length, we cannot
550                                 * adjust
551                                 */
552                                return this;
553
554                        int totalDiff = width - length;
555                        assert 0 < totalDiff;
556
557                        if (width <= totalDiff * 3)
558                                /* Too much of a difference, it would look very weird. */
559                                return this;
560
561                        final IColoredString.Impl<T> result = create();
562
563                        ListIterator<IColoredString.Bucket<T>> it = fragments.listIterator();
564                        final int nbb = fragments.size();
565                        final int[] bucketToNbSpaces = new int[nbb];
566                        /* The number of buckets that can contribute to justifying */
567                        int totalNbSpaces = 0;
568                        /* The index of the last bucket that has spaces */
569                        int lastHopeIndex = -1;
570                        {
571                                int i = 0;
572                                while (it.hasNext()) {
573                                        final Bucket<T> next = it.next();
574                                        final int nbs = nbSpaces(next.getText());
575                                        totalNbSpaces += nbs;
576                                        bucketToNbSpaces[i] = nbs;
577                                        i++;
578                                }
579
580                                if (totalNbSpaces == 0)
581                                        /* Cannot do anything */
582                                        return this;
583
584                                for (int j = bucketToNbSpaces.length - 1; 0 <= j; j--) {
585                                        if (0 < bucketToNbSpaces[j]) {
586                                                lastHopeIndex = j;
587                                                break;
588                                        }
589                                }
590                                /* Holds because we ruled out 'totalNbSpaces == 0' before */
591                                assert 0 <= lastHopeIndex;
592                        }
593
594                        // we know totalNbSpaces cannot be 0 from prior checks, so division is OK
595                        int toAddPerSpace = (totalDiff / totalNbSpaces);
596                        int totalRest = totalDiff - (toAddPerSpace * totalNbSpaces);
597                        assert 0 <= totalRest;
598
599                        int bidx = -1;
600
601                        it = fragments.listIterator();
602
603                        while (it.hasNext() && 0 < totalDiff) {
604                                bidx++;
605                                final Bucket<T> next = it.next();
606                                final String bucket = next.getText();
607                                final int blength = bucket.length();
608                                final int localNbSpaces = bucketToNbSpaces[bidx];
609                                if (localNbSpaces == 0) {
610                                        /* Cannot change it */
611                                        result.append(next);
612                                        continue;
613                                }
614                                int localDiff = localNbSpaces * toAddPerSpace;
615                                assert localDiff <= totalDiff;
616                                int nb = localDiff / localNbSpaces;
617                                int localRest = localDiff - (nb * localNbSpaces);
618                                if (localRest == 0 && 0 < totalRest) {
619                                        /*
620                                         * Take one for the group. This avoids flushing all spaces
621                                         * needed in the 'last hope' cases below.
622                                         */
623                                        localRest = 1;
624                                }
625                                assert 0 <= localRest;
626                                assert localRest <= totalRest;
627                                StringBuilder novel = new StringBuilder();
628                                int eatenSpaces = 1;
629                                for (int i = 0; i < blength; i++) {
630                                        final char c = bucket.charAt(i);
631                                        novel.append(c);
632                                        if (c == ' ' && (0 < localDiff || 0 < totalDiff || 0 < localRest || 0 < totalRest)) {
633                                                /* Can (and should) add an extra space */
634                                                for (int j = 0; j < nb && 0 < localDiff; j++) {
635                                                        novel.append(" ");
636                                                        localDiff--;
637                                                        totalDiff--;
638                                                }
639                                                if (0 < localRest || 0 < totalRest) {
640                                                        if (eatenSpaces == localNbSpaces) {
641                                                                /* I'm the last hope for this bucket */
642                                                                for (int j = 0; j < localRest; j++) {
643                                                                        novel.append(" ");
644                                                                        localRest--;
645                                                                        totalRest--;
646                                                                }
647                                                                if (bidx == lastHopeIndex) {
648                                                                        /* I'm the last hope globally */
649                                                                        while (0 < totalRest) {
650                                                                                novel.append(" ");
651                                                                                totalRest--;
652                                                                        }
653                                                                }
654                                                        } else {
655                                                                if (0 < localRest && 0 < totalRest) {
656                                                                        /* Not the last hope: take one only */
657                                                                        novel.append(" ");
658                                                                        localRest--;
659                                                                        totalRest--;
660                                                                }
661                                                        }
662                                                }
663                                                eatenSpaces++;
664                                        }
665                                }
666                                /* I did my job */
667                                assert localRest == 0;
668                                /* If I was the hope, I did my job */
669                                assert bidx != lastHopeIndex || totalRest == 0;
670                                result.append(novel.toString(), next.getColor());
671                        }
672
673                        while (it.hasNext())
674                                result.append(it.next());
675
676                        return result;
677                }
678
679                @Override
680                public void clear() {
681                        fragments.clear();
682                }
683
684                @Override
685                public int length() {
686                        int result = 0;
687                        for (Bucket<T> fragment : fragments)
688                                result += fragment.getText().length();
689                        return result;
690                }
691
692                @Override
693                /* This implementation is resilient to empty buckets */
694                public boolean isEmpty() {
695                        for (Bucket<?> bucket : fragments) {
696                                if (bucket.text != null && !bucket.text.isEmpty()) {
697                                        return false;
698                                }
699                        }
700                        return true;
701                }
702
703                @Override
704                public T lastColor() {
705                        return fragments.isEmpty() ? null : fragments.get(fragments.size() - 1).color;
706                }
707
708                @Override
709                public T colorAt(int index) {
710                        final ListIterator<IColoredString.Bucket<T>> it = fragments.listIterator();
711                        int now = 0;
712                        while (it.hasNext()) {
713                                final IColoredString.Bucket<T> next = it.next();
714                                final String ftext = next.text;
715                                final int flen = ftext.length();
716                                final int nextl = now + flen;
717                                if (index < nextl)
718                                        return next.color;
719                                now += flen;
720                        }
721                        throw new NoSuchElementException("Color at index " + index + " in " + this);
722                }
723
724                @Override
725                public char charAt(int index) {
726                        final ListIterator<IColoredString.Bucket<T>> it = fragments.listIterator();
727                        int now = 0;
728                        while (it.hasNext()) {
729                                final IColoredString.Bucket<T> next = it.next();
730                                final String ftext = next.text;
731                                final int flen = ftext.length();
732                                final int nextl = now + flen;
733                                if (index < nextl)
734                                        return ftext.charAt(index - now);
735                                now += flen;
736                        }
737                        throw new NoSuchElementException("Character at index " + index + " in " + this);
738                }
739
740                @Override
741                public String present() {
742                        final StringBuilder result = new StringBuilder();
743                        for (Bucket<T> fragment : fragments)
744                                result.append(fragment.text);
745                        return result.toString();
746                }
747                /**
748                 * Given some way of converting from a T value to an in-line markup tag, returns a string representation of
749                 * this IColoredString with in-line markup representing colors.
750                 * @param markup an IMarkup implementation
751                 * @return a String with markup inserted inside.
752                 */
753                @Override
754                public String presentWithMarkup(IMarkup<T> markup) {
755                        final StringBuilder result = new StringBuilder();
756//                      boolean open = false;
757                        for (Bucket<T> fragment : fragments) {
758                                if(fragment.color != null) {
759                                        // extra close-markup tags can cause variable-width text to have huge spacing.
760                    // they shouldn't be needed anyway.
761//                                      if (open)
762//                                              result.append(markup.closeMarkup());
763                                        result.append(markup.getMarkup(fragment.color));
764//                                      open = true;
765                                }
766//                              else {
767////                                    if (open)
768////                                            result.append(markup.closeMarkup());
769//                                      open = false;
770//                              }
771                                // maybe try this line if escape() is re-added to IMarkup
772                                //result.append(markup.escape(fragment.text));
773                                result.append(fragment.text);
774                        }
775                        return result.toString();
776                }
777
778                @Override
779                public ListIterator<Bucket<T>> iterator() {
780                        return fragments.listIterator();
781                }
782
783                @Override
784                public String toString() {
785                        return present();
786                }
787
788                protected static boolean equals(Object o1, Object o2) {
789                        if (o1 == null)
790                                return o2 == null;
791                        else
792                                return o1.equals(o2);
793                }
794
795                private int nbSpaces(String s) {
796                        final int bd = s.length();
797                        int result = 0;
798                        for (int i = 0; i < bd; i++) {
799                                final char c = s.charAt(i);
800                                if (c == ' ')
801                                        result++;
802                        }
803                        return result;
804                }
805                /**
806                 * Gets the Buckets as an ArrayList, allowing access by index instead of by {@link #iterator()}.
807                 *
808                 * @return the Buckets that would be returned by {@link #iterator()}, but in an ArrayList.
809                 */
810                @Override
811                public ArrayList<Bucket<T>> getFragments() {
812                        return fragments;
813                }
814
815                /* Some tests */
816                /*
817                public static void main(String[] args) {
818                        final IColoredString<Object> rockNRoll = IColoredString.Impl.create();
819                        rockNRoll.append("Rock", new Object());
820                        rockNRoll.append(" ", new Object());
821                        rockNRoll.append("'n", new Object());
822                        rockNRoll.append(" ", new Object());
823                        rockNRoll.append("Roll", new Object());
824                        for (int i = 0; i < rockNRoll.length(); i++)
825                                System.out.println(rockNRoll.charAt(i));
826                        System.out.println(rockNRoll.present());
827                }
828                */
829        }
830
831        /**
832         * A piece of a {@link IColoredString}: a text and its color.
833         * 
834         * @author smelC
835         * 
836         * @param <T>
837         *            The type of colors;
838         */
839        class Bucket<T> {
840
841                protected final String text;
842                protected final/* @Nullable */T color;
843
844                public Bucket(String text, /* @Nullable */T color) {
845                        this.text = text == null ? "" : text;
846                        this.color = color;
847                }
848
849                /**
850                 * @param text
851                 * @return An instance whose text is {@code this.text + text}. Color is
852                 *         unchanged.
853                 */
854                public Bucket<T> append(String text) {
855                        if (text == null || text.isEmpty())
856                                /* Let's save an allocation */
857                                return this;
858                        else
859                                return new Bucket<>(this.text + text, color);
860                }
861
862                public int length()
863                {
864                        return text.length();
865                }
866                
867                public Bucket<T> setLength(int l) {
868                        final int here = text.length();
869                        if (here <= l)
870                                return this;
871                        else
872                                return new Bucket<>(text.substring(0, l), color);
873                }
874
875                public Bucket<T> setColor(T t) {
876                        return color == t ? this : new Bucket<T>(text, t);
877                }
878
879                /**
880                 * @return The text that this bucket contains.
881                 */
882                public String getText() {
883                        return text;
884                }
885
886                /**
887                 * @return The color of {@link #getText()}. Or {@code null} if none.
888                 */
889                public/* @Nullable */T getColor() {
890                        return color;
891                }
892
893                @Override
894                public String toString() {
895                        if (color == null)
896                                return text;
897                        else
898                                return text + '(' + color + ')';
899                }
900
901        }
902}