SquirrelJME/SquirrelJME

View on GitHub
modules/midp-lcdui/src/main/java/javax/microedition/lcdui/Text.java

Summary

Maintainability
C
1 day
Test Coverage
// -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
// ---------------------------------------------------------------------------
// SquirrelJME
//     Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
// ---------------------------------------------------------------------------
// SquirrelJME is under the Mozilla Public License Version 2.0.
// See license.mkd for licensing and copyright information.
// ---------------------------------------------------------------------------

package javax.microedition.lcdui;

import cc.squirreljme.runtime.cldc.annotation.Api;
import cc.squirreljme.runtime.cldc.debug.Debugging;
import cc.squirreljme.runtime.lcdui.common.CommonColors;
import cc.squirreljme.runtime.lcdui.common.TextStorage;

/**
 * This text class is one which handles all of the text metrics and drawing and
 * such. It handles different fonts, colors, and styles on a per character
 * basis and performs all the needed operations to support text drawing.
 *
 * The color defaults to {@code Display.getColor(Display.COLOR_FOREGROUND)}.
 *
 * @since 2018/11/29
 */
@Api
public class Text
{
    @Api
    public static final int ALIGN_CENTER =
        1;
    
    @Api
    public static final int ALIGN_DEFAULT =
        4;
    
    @Api
    public static final int ALIGN_JUSTIFY =
        3;
    
    @Api
    public static final int ALIGN_LEFT =
        0;
    
    @Api
    public static final int ALIGN_RIGHT =
        2;
    
    @Api
    public static final int DIRECTION_LTR =
        10;
    
    @Api
    public static final int DIRECTION_NEUTRAL =
        12;
    
    @Api
    public static final int DIRECTION_RTL =
        11;
    
    /** Storage for the text. */
    private final TextStorage _storage =
        new TextStorage();
    
    /** The width. */
    private int _width;
    
    /** The height. */
    private int _height;
    
    /** The default font. */
    private Font _defaultfont =
        Font.getDefaultFont();
    
    /** The default foreground color. */
    private int _defaultcolor =
        CommonColors.DEFAULT_TEXT_COLOR;
    
    /** The background color. */
    private int _backgroundcolor =
        CommonColors.DEFAULT_TEXT_BACKGROUND_COLOR;
    
    /** The alignment. */
    private int _alignment;
    
    /** The caret position. */
    private int _caret =
        -1;
    
    /** The required display height, is cached. */
    private int _requiredheight;
    
    /** The required line count, is cached. */
    private int _requiredlines;
    
    /** Space above each line. */
    private int _spaceabove;
    
    /** Space below each line. */
    private int _spacebelow;
    
    /** The alignment of each line. */
    private int _align;
    
    /** Indentation. */
    private int _indentation;
    
    /** The direction of the text. */
    private int _direction = Text.DIRECTION_NEUTRAL;
    
    /** Scrolling offset (scrolled up by n pixels). */
    private int _scrolloffset;
    
    /** The highlight index. */
    private int _highlightdx =
        -1;
    
    /** The highlight length. */
    private int _highlightlen =
        0;
    
    /** Does character placement have to be updated. */
    private boolean _dirty;
    
    /**
     * Initializes the text with no width or height.
     *
     * @since 2018/11/29
     */
    @Api
    public Text()
    {
        this("", 0, 0);
    }
    
    /**
     * Initializes the text with the given width and height.
     *
     * @param __c The text to use.
     * @param __w The width.
     * @param __h The height.
     * @throws IllegalArgumentException If the width or height are negative.
     * @throws NullPointerException On null arguments.
     * @since 2018/11/29
     */
    @Api
    public Text(String __c, int __w, int __h)
        throws IllegalArgumentException, NullPointerException
    {
        this(__c, 0, __c.length(), __w, __h);
    }
    
    /**
     * Initializes the text with the given width and height.
     *
     * @param __c The text to use.
     * @param __o The offset.
     * @param __l The length.
     * @param __w The width.
     * @param __h The height.
     * @throws IllegalArgumentException If the width or height are negative.
     * @throws IndexOutOfBoundsException If the offset and/or length are
     * negative or exceed the array bounds.
     * @throws NullPointerException On null arguments.
     * @since 2018/11/29
     */
    @Api
    public Text(String __c, int __o, int __l, int __w, int __h)
        throws IllegalArgumentException, IndexOutOfBoundsException,
            NullPointerException
    {
        if (__c == null)
            throw new NullPointerException("NARG");
        if (__o < 0 || __l < 0 || (__o + __l) > __c.length())
            throw new IndexOutOfBoundsException("IOOB");
        
        /* {@squirreljme.error EB2p The width and height cannot be negative
        for text.} */
        if (__w < 0 || __h < 0)
            throw new IllegalArgumentException("EB2p");
        
        // Set sizes first
        this._width = __w;
        this._height = __h;
        
        // Insert all of the text
        this.insert(0, __c.substring(__o, __o + __l));
    }
    
    /**
     * Deletes the specified text.
     *
     * @param __i The index to start deletion at.
     * @param __l The number of characters to delete.
     * @throws IndexOutOfBoundsException If the index is out of bounds.
     * @since 2018/12/02
     */
    @Api
    public void delete(int __i, int __l)
        throws IndexOutOfBoundsException
    {
        // Perform the delete
        this._storage.delete(__i, __l);
        
        // Deleting nothing?
        if (__l <= 0)
            return;
        
        // Mark dirty
        this._dirty = true;
        
        // Adjust the caret?
        int caret = this._caret;
        if (caret > __i)
            this._caret = caret - Math.max(__l - (caret - __i), __l);
    }
    
    /**
     * Returns the alignment of the text.
     *
     * @return The text alignment.
     * @since 2018/12/02
     */
    @Api
    public int getAlignment()
    {
        return this._alignment;
    }
    
    /**
     * Returns the background color.
     *
     * @return The background color.
     * @since 2018/12/02
     */
    @Api
    public int getBackgroundColor()
    {
        return this._backgroundcolor;
    }
    
    /**
     * Returns the caret position.
     *
     * @return The caret position or {@code -1} if it is not used.
     * @since 2018/12/02
     */
    @Api
    public int getCaret()
    {
        return this._caret;
    }
    
    /**
     * Returns the extents of the given character.
     *
     * @param __i The character to get.
     * @param __ext The extents of the character: x, y, width, height.
     * @throws IllegalArgumentException If the extend array has a length less
     * than four.
     * @throws IndexOutOfBoundsException If the character index is out of
     * bounds.
     * @throws NullPointerException On null arguments.
     * @since 2018/12/01
     */
    @Api
    public void getCharExtent(int __i, int[] __ext)
        throws IllegalArgumentException, IndexOutOfBoundsException,
            NullPointerException
    {
        if (__ext == null)
            throw new NullPointerException("NARG");
            
        // Update
        if (this._dirty)
            this.__undirty();
        
        // Need to extract the character and font to determine the width and
        // the height of it
        TextStorage storage = this._storage;
        
        // Exceeds storage size?
        if (__i >= storage.size)
            throw new IndexOutOfBoundsException("IOOB");
        
        // Default font?
        Font font = storage.font[__i];
        if (font == null)
            font = this._defaultfont;
        
        // Set extents, the width and height come from the character data
        try
        {
            __ext[0] = storage.x[__i];
            __ext[1] = storage.y[__i];
            __ext[2] = font.charWidth(storage.chars[__i]);
            __ext[3] = font.getHeight();
        }
        catch (IndexOutOfBoundsException e)
        {
            /* {@squirreljme.error EB2q Extent array length must at least
            be 4.} */
            if (__ext.length < 4)
                throw new IllegalArgumentException("EB2q");
        }
    }
    
    @Api
    public int getCharIndex(int __x, int __y)
    {
        throw Debugging.todo();
    }
    
    /**
     * Returns the default font.
     *
     * @return The default font.
     * @since 2018/12/01
     */
    @Api
    public Font getFont()
    {
        return this._defaultfont;
    }
    
    /**
     * Returns the font which is used by the given character.
     *
     * @param __i The index of the character to get.
     * @return The font for the given index.
     * @throws IndexOutOfBoundsException If the index it outside of bounds.
     * @since 2018/12/01
     */
    @Api
    public Font getFont(int __i)
        throws IndexOutOfBoundsException
    {
        TextStorage storage = this._storage;
        
        // Exceeds storage size?
        if (__i >= storage.size)
            throw new IndexOutOfBoundsException("IOOB");
        
        // Only if a font is set
        Font rv = storage.font[__i];
        if (rv != null)
            return rv;
        return this._defaultfont;
    }
    
    /**
     * Returns the default foreground color.
     *
     * @return The default foreground color.
     * @since 2018/12/02
     */
    @Api
    public int getForegroundColor()
    {
        return this._defaultcolor;
    }
    
    /**
     * Gets the foreground color for a character.
     *
     * @param __i The character to get.
     * @return The color for that character.
     * @since 2018/12/01
     */
    @Api
    public int getForegroundColor(int __i)
        throws IndexOutOfBoundsException
    {
        TextStorage storage = this._storage;
        
        // Exceeds storage size?
        if (__i >= storage.size)
            throw new IndexOutOfBoundsException("IOOB");
        
        // Zero means that the default color is to be used
        int rv = storage.color[__i];
        if (rv != 0)
            return rv;
        return this._defaultcolor;
    }
    
    /**
     * Returns the height of the text.
     *
     * @return The height of the text.
     * @since 2018/12/01
     */
    @Api
    public int getHeight()
    {
        return this._height;
    }
    
    /**
     * Returns the highlight index.
     *
     * @return The highlight index or {@code -1} if none.
     * @since 2018/12/02
     */
    @Api
    public int getHighlightIndex()
    {
        return this._highlightdx;
    }
    
    /**
     * Returns the length of highlighted characters.
     *
     * @return The number of characters highlighted, {@code 0} if none.
     * @since 2018/12/02
     */
    @Api
    public int getHighlightLength()
    {
        return this._highlightlen;
    }
    
    /**
     * Returns the current indentation.
     *
     * @return The indentation.
     * @since 2018/12/02
     */
    @Api
    public int getIndent()
    {
        return this._indentation;
    }
    
    /**
     * Returns the initial direction.
     *
     * @return The initial direction.
     * @since 2018/12/02
     */
    @Api
    public int getInitialDirection()
    {
        return this._direction;
    }
    
    /**
     * Returns the height which is required to completely display all of the
     * text within.
     *
     * @return The required height to draw.
     * @since 2018/12/01
     */
    @Api
    public int getRequiredHeight()
    {
        // Update
        if (this._dirty)
            this.__undirty();
        
        return this._requiredheight;
    }
    
    /**
     * Returns the number of lines which are required to display all of the
     * text within.
     *
     * @return The number of lines which are required.
     * @since 2018/12/01
     */
    @Api
    public int getRequiredLineCount()
    {
        // Update
        if (this._dirty)
            this.__undirty();
        
        return this._requiredlines;
    }
    
    /**
     * Returns the scroll offset.
     *
     * @return The scroll offset.
     * @since 2018/12/02
     */
    @Api
    public int getScrollOffset()
    {
        return this._scrolloffset;
    }
    
    /**
     * Returns the space above each line.
     *
     * @return The space above.
     * @since 2018/12/02
     */
    @Api
    public int getSpaceAbove()
    {
        return this._spaceabove;
    }
    
    /**
     * Returns the space below each line.
     *
     * @return The space below.
     * @since 2018/12/02
     */
    @Api
    public int getSpaceBelow()
    {
        return this._spacebelow;
    }
    
    /**
     * Returns the text contained within this object.
     *
     * @param __i The starting index.
     * @param __l The length.
     * @return The string for the text.
     * @throws IndexOutOfBoundsException If the index and/or length exceed
     * the text bounds.
     * @since 2018/12/01
     */
    @Api
    public String getText(int __i, int __l)
        throws IndexOutOfBoundsException
    {
        TextStorage storage = this._storage;
        
        /* {@squirreljme.error EB2r Text outside of bounds. (The starting
        index; The ending index; The size of the text)} */
        int size = storage.size;
        if (__i < 0 || __l < 0 || (__i + __l) > size)
            throw new IndexOutOfBoundsException(
                String.format("EB2r %d %d %d", __i, __l, size));
        
        // Create string from it
        return new String(storage.chars, __i, __l);
    }
    
    /**
     * Returns the length of this text object.
     *
     * @return The length of the text object.
     * @since 2018/12/01
     */
    @Api
    public int getTextLength()
    {
        return this._storage.size;
    }
    
    /**
     * Returns the width of this text.
     *
     * @return The text width.
     * @since 2018/12/01
     */
    @Api
    public int getWidth()
    {
        return this._width;
    }
    
    /**
     * Inserts the given string at the position.
     *
     * @param __i The index to insert at, the index is always forced within
     * the bounds of the buffer (negative values are inserted at zero and
     * positions greater than the size are inserted at the end).
     * @param __s The stirng to index.
     * @throws NullPointerException On null arguments.
     * @since 2018/11/30
     */
    @Api
    public void insert(int __i, String __s)
        throws NullPointerException
    {
        if (__s == null)
            throw new NullPointerException("NARG");
        
        // Ignore adding empty strings as there is no purpose to it
        if (__s.isEmpty())
            return;
        
        // The index is always in the bounds of the storage
        TextStorage storage = this._storage;
        if (__i < 0)
            __i = 0;
        else if (__i > storage.size)
            __i = storage.size;
        
        // Insert space to store the characters
        int sn = __s.length();
        storage.insert(__i, sn);
        
        // Set character data here
        char[] chars = storage.chars;
        for (int i = 0; i < sn; i++)
            chars[__i++] = __s.charAt(i);
        
        // Adjust the caret?
        int caret = this._caret;
        if (caret >= 0 && __i < caret)
            this._caret = caret + sn;
        
        // Becomes dirty
        this._dirty = true;
    }
    
    @Api
    public int lastRenderedIndex()
    {
        throw Debugging.todo();
    }
    
    @Api
    public void moveCaret(int __nl)
    {
        throw Debugging.todo();
    }
    
    /**
     * Sets the alignment of the text.
     *
     * @param __a If the alignment is not valid.
     * @throws IllegalArgumentException If the alignment is not valid.
     * @since 2018/12/02
     */
    @Api
    public void setAlignment(int __a)
        throws IllegalArgumentException
    {
        /* {@squirreljme.error EB2s Invalid alignment. (The alignment)} */
        if (__a != Text.ALIGN_LEFT && __a != Text.ALIGN_CENTER && __a != Text.ALIGN_RIGHT &&
            __a != Text.ALIGN_JUSTIFY && __a != Text.ALIGN_DEFAULT)
            throw new IllegalArgumentException("EB2s " + __a);
        
        this._alignment = __a;
    }
    
    /**
     * Sets the background color.
     *
     * @param __argb The color to use.
     * @since 2018/12/02
     */
    @Api
    public void setBackgroundColor(int __argb)
    {
        this._backgroundcolor = __argb;
    }
    
    /**
     * Sets the position of the caret.
     *
     * @param __i The position to use, {@code -1} clears the caret
     * @throws IndexOutOfBoundsException If the caret is outside of the
     * text bounds.
     * @since 2018/12/02
     */
    @Api
    public void setCaret(int __i)
        throws IndexOutOfBoundsException
    {
        // Clear it
        if (__i == -1)
        {
            this._caret = -1;
            return;
        }
        
        throw Debugging.todo();
    }
    
    /**
     * Sets the default font to use when no font has been specified.
     *
     * @param __f The default font to use.
     * @throws NullPointerException On null arguments.
     * @since 2018/11/30
     */
    @Api
    public void setFont(Font __f)
        throws NullPointerException
    {
        if (__f == null)
            throw new NullPointerException("NARG");
        
        // If this is the same font, just ignore it
        Font olddefaultfont = this._defaultfont;
        if (__f.equals(olddefaultfont))
            return;
        
        // Changing the font becomes dirty since we need to measure the
        // metrics again
        this._defaultfont = __f;
        this._dirty = true;
    }
    
    /**
     * Sets the font at the given positions.
     *
     * @param __f The font to set, {@code null} clears.
     * @param __i The index.
     * @param __l The length.
     * @throws IndexOutOfBoundsException If the given range is out of bounds.
     * @since 2018/12/02
     */
    @Api
    public void setFont(Font __f, int __i, int __l)
    {
        TextStorage storage = this._storage;
        
        // Exceeds storage size?
        int size = storage.size;
        if (__i < 0 || __l < 0 || __i >= size || (__i + __l) > size)
            throw new IndexOutOfBoundsException("IOOB");
        
        // Set
        Font[] font = storage.font;
        for (int i = 0; i < __l; i++)
            font[__i++] = __f;
        
        // Is dirty now
        this._dirty = true;
    }
    
    /**
     * Sets the default foreground color to use when drawing.
     *
     * @param __argb The color to use.
     * @since 2018/12/01
     */
    @Api
    public void setForegroundColor(int __argb)
    {
        // This does not dirty anything because it is just a color change,
        // nothing needs to be recomputed
        this._defaultcolor = __argb;
    }
    
    /**
     * Sets the color at the given positions.
     *
     * @param __argb The ARGB color, zero removes the color.
     * @param __i The index.
     * @param __l The length.
     * @throws IndexOutOfBoundsException If the given range is out of bounds.
     * @since 2018/12/02
     */
    @Api
    public void setForegroundColor(int __argb, int __i, int __l)
    {
        TextStorage storage = this._storage;
        
        // Exceeds storage size?
        int size = storage.size;
        if (__i < 0 || __l < 0 || __i >= size || (__i + __l) > size)
            throw new IndexOutOfBoundsException("IOOB");
        
        // Set
        int[] color = storage.color;
        for (int i = 0; i < __l; i++)
            color[__i++] = __argb;
        
        // Is dirty now
        this._dirty = true;
    }
    
    /**
     * Sets the height of the text.
     *
     * @param __h The new height.
     * @throws IllegalArgumentException If the height is negative.
     * @since 2018/12/01
     */
    @Api
    public void setHeight(int __h)
        throws IllegalArgumentException
    {
        /* {@squirreljme.error EB2t The height of a font cannot be
        negative.} */
        if (__h < 0)
            throw new IllegalArgumentException("EB2t");
        
        // Just set the height, we do not need to clear the dirty bit because
        // as long as the requiredheight is still within the height the text
        // fits
        this._height = __h;
    }
    
    /**
     * Sets the highlight position.
     *
     * @param __i The starting index, {@code -1} clears the highlight.
     * @param __l The number of characters to highlight.
     * @throws IndexOutOfBoundsException If the index and/or length are out
     * of bounds.
     */
    @Api
    public void setHighlight(int __i, int __l)
        throws IndexOutOfBoundsException
    {
        // Clear it
        if (__i == -1)
        {
            this._highlightdx = -1;
            this._highlightlen = 0;
            return;
        }
        
        throw Debugging.todo();
    }
    
    /**
     * Sets the indentation.
     *
     * @param __i The indentation.
     * @since 2018/12/02
     */
    @Api
    public void setIndent(int __i)
    {
        this._indentation = __i;
    }
    
    /**
     * Sets the initial direction.
     *
     * @param __dir The initial direction.
     * @throws IllegalArgumentException If the direction is not valid.
     * @since 2018/12/02
     */
    @Api
    public void setInitialDirection(int __dir)
        throws IllegalArgumentException
    {
        /* {@squirreljme.error EB2u The direction to use. (The direction)} */
        if (__dir != Text.DIRECTION_LTR && __dir != Text.DIRECTION_RTL &&
            __dir != Text.DIRECTION_NEUTRAL)
            throw new IllegalArgumentException("EB2u " + __dir);
        
        this._direction = __dir;
    }
    
    /**
     * Sets the scroll offset.
     *
     * @param __o The offset.
     * @since 2018/12/02
     */
    @Api
    public void setScrollOffset(int __o)
    {
        this._scrolloffset = __o;
    }
    
    /**
     * Sets the space above each line.
     *
     * @param __sa The space above in pixels.
     * @since 2018/12/02
     */
    @Api
    public void setSpaceAbove(int __sa)
    {
        this._spaceabove = __sa;
    }
    
    /**
     * Sets the space below each line.
     *
     * @param __sb The space below in pixels.
     * @since 2018/12/02
     */
    @Api
    public void setSpaceBelow(int __sb)
    {
        this._spacebelow = __sb;
    }
    
    /**
     * Sets the width of this text to the specified width.
     *
     * @param __w The width to set.
     * @throws IllegalArgumentException If the width is negative.
     * @since 2018/11/30
     */
    @Api
    public void setWidth(int __w)
        throws IllegalArgumentException
    {
        /* {@squirreljme.error EB2v Cannot set the width to a negative value.} */
        if (__w < 0)
            throw new IllegalArgumentException("EB2v");
        
        // Ignore on no changes
        int oldwidth = this._width;
        if (__w == oldwidth)
            return;
        
        // Set and mark dirty
        this._width = __w;
        this._dirty = true;
    }
    
    /**
     * Returns whether all of the text fits within the bounds of the box.
     *
     * @return If all of the text fits within the bounds of the box.
     * @since 2018/12/01
     */
    @Api
    public boolean textFits()
    {
        // Update
        if (this._dirty)
            this.__undirty();
        
        // The text will fit if the height needed to display everything is at
        // or below the height of the actual box. This is to make height
        // changes not require a recalculate.
        return this._requiredheight <= this._height;
    }
    
    /**
     * Undirties the text within and calculates all of the needed bounds and
     * metrics for each character.
     *
     * @since 2018/12/01
     */
    private void __undirty()
    {
        if (!this._dirty)
            return;
            
        // Undirty state
        __TextUndirtyState__ ts = new __TextUndirtyState__();
        
        // Using this global stuff
        ts.defaultfont = this._defaultfont;
        ts.width = this._width;
        ts.spaceabove = this._spaceabove;
        ts.spacebelow = this._spacebelow;
        ts.align = this._align;
        ts.indentation = this._indentation;
        ts.direction = this._direction;
        ts.scrolloffset = this._scrolloffset;
        
        // If the direction is neutral, this just becomes the locale default
        // For now just treat it as LTR
        if (ts.direction == Text.DIRECTION_NEUTRAL)
            ts.direction = Text.DIRECTION_LTR;
        
        // Are we going right to left?
        boolean dortl = (ts.direction == Text.DIRECTION_RTL);
        
        // Will use this storage stuff
        TextStorage storage = this._storage;
        char[] chars = storage.chars;
        Font[] font = storage.font;
        short[] cx = storage.x;
        short[] cy = storage.y;
        
        // The starting Y position is 
        // The starting X and Y position is always zero, when other alignments
        // and such are used they are calculated when the line ends
        // X is offset by the indentation and Y is offset by the scrolling
        int y = -ts.scrolloffset + ts.spaceabove,
            nexty = y;
        
        // X starts with indentation, but that might be modified in right
        // to left mode
        int x = (dortl ? ts.width : ts.indentation),
            startx = x;
        
        // Cache parameters of font
        Font lastfont = null;
        int fontheight = 0;
        int fontascent = 0;
        int fontdescent = 0;
        
        // For the end of line calculator, these are the indexes which are
        // used for each character
        int linedxstart = 0;
        int linedxend = 0;
        
        // Redo handling of the current character, this will happen if
        // the line overflows
        boolean redo = false;
        
        // Go through and calculate every character line by line, carefully
        // handling alignment and justification
        // The line height is calculated so that if different fonts of
        // different sizes are on the same line, they all are on the baseline
        int linecount = 0;
        int linemaxheight = 0;
        int linemaxascent = 0;
        int linemaxdescent = 0;
        for (int i = 0, n = storage.size; i <= n; i++)
        {
            // Since we need to handle line indentation, justification and
            // otherwise we need a flag to know when the next line was hit
            // to calculate
            // But the last character always has line stuff done to handle the
            // final alignment/directions and such
            // For now only the X positions are considered because
            boolean donextline = (i == n),
                wasnewlinech = false;
            if (!donextline)
            {
                // Need the character and font here, for metrics
                char ch = chars[i];
                Font f = font[i];
                if (f == null)
                    f = ts.defaultfont;
                
                // Font has changed?
                if (lastfont != f)
                {
                    // Cache parameters
                    fontheight = f.getHeight();
                    fontascent = f.getAscent();
                    fontdescent = f.getDescent();
                    
                    // Keep track so we do not need to update every time
                    lastfont = f;
                    
                    // Properties of the line changed due to the font?
                    // Cache them and check here accordingly
                    if (fontascent > linemaxascent)
                        linemaxascent = fontascent;
                    if (fontheight > linemaxheight)
                        linemaxheight = fontheight;
                    if (fontdescent > linemaxdescent)
                        linemaxdescent = fontdescent;
                }
                
                // Ignore carriage returns
                if (ch == '\r')
                    continue;
                
                // Newlines do go to the next line, if a newline was detected
                // then the X position will be set to indentation, otherwise
                // zero.
                else if (ch == '\n')
                {
                    donextline = true;
                    wasnewlinech = true;
                    
                    // Clear a redo
                    redo = false;
                }
                
                // Draw every other character
                else
                {
                    // Get the properties of this character
                    int chw = f.charWidth(ch);
                    
                    // Calculate draw position and the next X position
                    // accordingly
                    int dx, nx;
                    if (dortl)
                    {
                        dx = x - chw;
                        nx = dx;
                    }
                    else
                    {
                        dx = x;
                        nx = x + chw;
                    }
                    
                    // Character is still within the bounds?
                    // Additionally if the X coordinate is at the start and
                    // cannot even fit in the width just force it to be
                    // placed
                    if ((nx >= 0 && nx <= ts.width) ||
                        (x <= startx && x + chw <= ts.width))
                    {
                        // Store current X position, this may change due to
                        // right to left mode
                        cx[i] = (short)dx;
                        
                        // Next character will be here
                        x = nx;
                        
                        // Store the ascent of the character in the Y slot,
                        // this is later used at end of line handler to
                        // correctly place each character on the baseline
                        cy[i] = (short)fontascent;
                        
                        // Clear a redo
                        redo = false;
                    }
                    
                    // Character exceeds the bounds, need to redo and handle
                    // end of line
                    else
                    {
                        // Redo should not be triggered twice, so just
                        // clear it and never bother again
                        if (redo)
                        {
                            redo = false;
                            continue;
                        }
                        
                        donextline = true;
                        redo = true;
                    }
                }
            }
            
            // End of line reached, handle alignment, justification, etc.
            // Perform any position updates as needed
            if (donextline)
            {
                // The line ends on this index (this is either strlen, a
                // newline, or a character placed on a newline so this is
                // always going to be exclusive)
                linedxend = i;
                
                // The next Y position is going to be the max font height
                // for this line.
                // An extra space above is only added if this was a newline,
                // so that way the next line has the actual space above
                nexty = y + linemaxheight + ts.spacebelow +
                    (wasnewlinech ? ts.spaceabove : 0);
                
                // Calculate the correct Y position for each character
                for (int q = linedxstart; q < linedxend; q++)
                {
                    // Get the original ascent of the character
                    int origascent = cy[q];
                    
                    // The Y position is just the difference in space between
                    // the line's max ascent and the character's actual ascent
                    cy[q] = (short)(y + (linemaxascent - origascent));
                }
                
                // Handle non-default alignments
                if ((dortl && ts.align != Text.ALIGN_RIGHT) ||
                    (!dortl && ts.align != Text.ALIGN_LEFT))
                {
                    throw Debugging.todo();
                }
                
                // A line was here so the line count goes up
                linecount++;
                
                // If redoing this character, decrement i so that way it
                // negates the loop increment
                if (redo)
                    i--;
                
                // The new start index
                linedxstart = i + 1;
                
                // Set the previously calculated Y position
                y = nexty;
                
                // Move X back
                x = startx;
            }
        }
        
        // Update other needed parameters
        // The required height is our nexty because either at the end or
        // a newline this will always be set
        this._requiredheight = nexty;
        this._requiredlines = linecount;
            
        // Has been updated, no longer dirty
        this._dirty = false;
    }
}