/*
* Copyright (c) 2003-onwards Shaven Puppy Ltd
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'Shaven Puppy' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.shavenpuppy.jglib;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.StringTokenizer;
import org.lwjgl.util.Point;
import org.lwjgl.util.Rectangle;
import com.shavenpuppy.jglib.util.Decodeable;
/**
* This lays out text in a string within a specific width. The text baseline is at (0,0)
* and then the y coordinate becomes negative as the text goes down each line.
*/
public class TextLayout {
/** The font, from which we can get the ascent, descent, and leading */
private final Font font;
/** Scaling */
private final float scale;
/** The glyphs: not all used */
private final Glyph[] glyph;
/** Number of glyphs used */
private int numGlyphs;
/** The format we're going to use */
private final Format format;
/** Horizontal alignments */
public abstract static class HorizontalAlignment implements Serializable, Decodeable {
private static final long serialVersionUID = 1L;
private final String display;
private HorizontalAlignment(String display) {
this.display = display;
}
@Override
public String toString() {
return display;
}
/**
* Decode method, for Decodeable marker
* @param in
* @return
* @throws Exception
*/
public static Object decode(String in) throws Exception {
if (in.equalsIgnoreCase(LEFT.display)) {
return LEFT;
} else if (in.equalsIgnoreCase(RIGHT.display)) {
return RIGHT;
} else if (in.equalsIgnoreCase(CENTERED.display)) {
return CENTERED;
} else {
throw new Exception("Unknown horizontal alignment '"+in+"'");
}
}
}
/** The alignment */
private HorizontalAlignment alignment;
public static final HorizontalAlignment LEFT = new HorizontalAlignment("Left") {
private static final long serialVersionUID = 1L;
private Object readResolve() throws ObjectStreamException {
return LEFT;
}
};
public static final HorizontalAlignment RIGHT = new HorizontalAlignment("Right") {
private static final long serialVersionUID = 1L;
private Object readResolve() throws ObjectStreamException {
return RIGHT;
}
};
public static final HorizontalAlignment CENTERED = new HorizontalAlignment("Centered") {
private static final long serialVersionUID = 1L;
private Object readResolve() throws ObjectStreamException {
return CENTERED;
}
};
/** The glyph positions */
private final int[] x, y;
/** Leading */
private int leading;
/** The layout's width - specified during construction */
private int width;
/** The layout's height - available after construction */
private int height;
/** Keeps track of penX and penY */
private int penX, penY;
/** Handy geometry */
private static final Rectangle tempBounds = new Rectangle();
private static final Point tempPoint = new Point();
/**
* The text layout target. Once you've laid out some text int the text layout you need to
* apply it to some target.
*/
public interface Target {
/**
* Tells the target how many glyphs will be set. Called first.
*/
public void setNumGlyphs(int n);
/**
* Each glyph is set via this method
*/
public void setGlyph(int index, Glyph glyph, int x, int y);
}
/**
* Formatting class. Formats a paragraph.
*/
public static abstract class Format implements Serializable, Decodeable {
private static final long serialVersionUID = 1L;
private final String display;
private Format(String display) {
this.display = display;
}
@Override
public String toString() {
return display;
}
/**
* Format the incoming paragraph starting at the current position penX, penY.
* Should leave penX, penY in the location that the next paragraph should start.
*/
abstract void format(TextLayout layout, String paragraph, boolean lastParagraph);
/**
* Decode method, for Decodeable marker
* @param in
* @return
* @throws Exception
*/
public static Object decode(String in) throws Exception {
if (in.equalsIgnoreCase(NO_WRAP.display)) {
return NO_WRAP;
} else if (in.equalsIgnoreCase(WRAPPED.display)) {
return WRAPPED;
} else if (in.equalsIgnoreCase(JUSTIFIED.display)) {
return JUSTIFIED;
} else if (in.equalsIgnoreCase(WORD_WRAP.display)) {
return WORD_WRAP;
} else {
throw new Exception("Unknown text format '"+in+"'");
}
}
}
/** No wrap - the maximum width is ignored */
private static final class NoWrap extends Format {
private static final long serialVersionUID = 1L;
NoWrap() {
super("None");
}
@Override
void format(TextLayout layout, String paragraph, boolean lastParagraph) {
final int n = paragraph.length();
Glyph last = null;
layout.penY -= layout.font.getAscent() * layout.scale;
for (int i = 0; i < n; i++) {
Glyph next = layout.font.map(paragraph.charAt(i));
next.getBounds(tempBounds);
next.getBearing(tempPoint);
// Scale bounds and bearing and kerning
tempBounds.setBounds
(
(int) (tempBounds.getX() * layout.scale),
(int) (tempBounds.getY() * layout.scale),
(int) (tempBounds.getWidth() * layout.scale),
(int) (tempBounds.getHeight() * layout.scale)
);
tempPoint.setLocation
(
(int) (tempPoint.getX() * layout.scale),
(int) (tempPoint.getY() * layout.scale)
);
int kerning = (int) (next.getKerningAfter(last) * layout.scale);
layout.x[layout.numGlyphs] = tempPoint.getX() + layout.penX - kerning;
layout.y[layout.numGlyphs] = layout.penY + tempPoint.getY();
layout.glyph[layout.numGlyphs++] = next;
layout.penX += (int) (next.getAdvance() * layout.scale) + kerning;
}
if (lastParagraph) {
layout.align(0, layout.penX);
layout.nextLine(true);
}
}
private Object readResolve() throws ObjectStreamException {
return NO_WRAP;
}
}
/** Wrapped - the characters are wrapped at the specified width */
private static final class Wrapped extends Format {
private static final long serialVersionUID = 1L;
Wrapped() {
super("Character Wrap");
}
@Override
void format(TextLayout layout, String paragraph, boolean lastParagraph) {
final int n = paragraph.length();
Glyph last = null;
int lastWidth = 0, lastAdvance = 0, startGlyph = layout.numGlyphs;
layout.penY -= layout.font.getAscent() * layout.scale;
for (int i = 0; i < n; i++) {
Glyph next = layout.font.map(paragraph.charAt(i));
next.getBounds(tempBounds);
next.getBearing(tempPoint);
// Scale bounds and bearing and kerning
tempBounds.setBounds
(
(int) (tempBounds.getX() * layout.scale),
(int) (tempBounds.getY() * layout.scale),
(int) (tempBounds.getWidth() * layout.scale),
(int) (tempBounds.getHeight() * layout.scale)
);
tempPoint.setLocation
(
(int) (tempPoint.getX() * layout.scale),
(int) (tempPoint.getY() * layout.scale)
);
int kerning = (int) (next.getKerningAfter(last) * layout.scale);
// Does the character fit on the line? If not, we must start a new line.
if (layout.penX + tempPoint.getX() + tempBounds.getWidth() - kerning > layout.width) {
layout.align(startGlyph, layout.penX + lastWidth - lastAdvance);
startGlyph = layout.numGlyphs;
layout.nextLine(false);
layout.penY -= ((layout.font.getAscent() + layout.font.getDescent()) * layout.scale + layout.leading);
last = null;
} else {
last = next;
}
layout.x[layout.numGlyphs] = tempPoint.getX() + layout.penX - kerning;
layout.y[layout.numGlyphs] = layout.penY + tempPoint.getY();
layout.glyph[layout.numGlyphs] = next;
lastAdvance = (int) (next.getAdvance() * layout.scale) + kerning;
layout.penX += lastAdvance;
lastWidth = layout.x[layout.numGlyphs] - tempPoint.getX();
layout.numGlyphs++;
}
layout.align(startGlyph, layout.penX + lastWidth - lastAdvance);
layout.nextLine(lastParagraph);
}
private Object readResolve() throws ObjectStreamException {
return WRAPPED;
}
}
/** Word wrapped - words are wrapped at the specified width */
private static final class WordWrapped extends Format {
private static final long serialVersionUID = 1L;
WordWrapped() {
super("Word Wrap");
}
@Override
void format(TextLayout layout, String paragraph, boolean lastParagraph) {
StringTokenizer st = new StringTokenizer(paragraph, " ", true);
int lastGlyphWidth = 0, lastAdvanceWidth = 0, startGlyph = layout.numGlyphs;
boolean firstWordDone = false;
layout.penY -= layout.font.getAscent() * layout.scale;
while (st.hasMoreTokens()) {
String word = st.nextToken();
// Work out the width of the word...
Glyph last = null;
int w = 0, lastWidthOffset = 0, lastAdvance = 0;
final int n = word.length();
for (int i = 0; i < n; i++) {
Glyph next = layout.font.map(word.charAt(i));
next.getBounds(tempBounds);
next.getBearing(tempPoint);
// Scale bounds and bearing and kerning
tempBounds.setBounds
(
(int) (tempBounds.getX() * layout.scale),
(int) (tempBounds.getY() * layout.scale),
(int) (tempBounds.getWidth() * layout.scale),
(int) (tempBounds.getHeight() * layout.scale)
);
tempPoint.setLocation
(
(int) (tempPoint.getX() * layout.scale),
(int) (tempPoint.getY() * layout.scale)
);
int kerning = (int) (next.getKerningAfter(last) * layout.scale);
lastAdvance = (int) (next.getAdvance() * layout.scale) + kerning;
lastWidthOffset = tempPoint.getX() + tempBounds.getWidth() - kerning;
w += lastAdvance;
last = next;
}
w = (w - lastAdvance) + lastWidthOffset;
// Does the word fit on the line?
if (firstWordDone && layout.penX + w > layout.width) {
// No, so start a new one. First justify the current line
layout.align(startGlyph, layout.penX + lastGlyphWidth - lastAdvanceWidth);
startGlyph = layout.numGlyphs;
layout.nextLine(false);
layout.penY -= layout.font.getAscent() * layout.scale;
firstWordDone = false;
} else {
// Yes, so append a space to the previous word on the line, if there was one
// Advance by the width of a single space glyph
if (firstWordDone) {
// Glyph space = layout.font.map(' ');
// layout.penX += space.getAdvance();
}
}
// 'Draw' the word
last = null;
for (int i = 0; i < n; i++) {
Glyph next = layout.font.map(word.charAt(i));
next.getBounds(tempBounds);
next.getBearing(tempPoint);
// Scale bounds and bearing and kerning
tempBounds.setBounds
(
(int) (tempBounds.getX() * layout.scale),
(int) (tempBounds.getY() * layout.scale),
(int) (tempBounds.getWidth() * layout.scale),
(int) (tempBounds.getHeight() * layout.scale)
);
tempPoint.setLocation
(
(int) (tempPoint.getX() * layout.scale),
(int) (tempPoint.getY() * layout.scale)
);
int kerning = (int) (next.getKerningAfter(last) * layout.scale);
layout.x[layout.numGlyphs] = tempPoint.getX() + layout.penX - kerning;
layout.y[layout.numGlyphs] = layout.penY + tempPoint.getY();
layout.glyph[layout.numGlyphs] = next;
lastAdvance = (int) (next.getAdvance() * layout.scale) + kerning;
layout.penX += lastAdvance;
layout.numGlyphs++;
last = next;
}
firstWordDone = true;
}
// Align the last line
layout.align(startGlyph, layout.penX + lastGlyphWidth - lastAdvanceWidth);
layout.nextLine(lastParagraph);
}
private Object readResolve() throws ObjectStreamException {
return WORD_WRAP;
}
}
/** Justified - words are wrapped at the specified width and then whitespace is expanded to fill the line */
private static final class Justified extends Format {
private static final long serialVersionUID = 1L;
Justified() {
super("Justified");
}
@Override
void format(TextLayout layout, String paragraph, boolean lastParagraph) {
layout.penY -= layout.font.getDescent() * layout.scale;
if (!lastParagraph) {
layout.penY -= layout.leading;
}
layout.penX = 0;
}
private Object readResolve() throws ObjectStreamException {
return JUSTIFIED;
}
}
/** Formatting constants */
public static final Format NO_WRAP = new NoWrap();
public static final Format WRAPPED = new Wrapped();
public static final Format WORD_WRAP = new WordWrapped();
public static final Format JUSTIFIED = new Justified();
/** Keep whitespace without removing it - useful for editing text areas */
private boolean keepWhiteSpace;
/**
* Constructor for TextLayout.
*/
public TextLayout(Font font, float scale, int leading, String text, int width, Format format, HorizontalAlignment alignment) {
this.font = font;
this.scale = scale;
this.leading = leading;
this.width = width;
this.format = format;
this.alignment = alignment;
glyph = new Glyph[text.length()];
x = new int[glyph.length];
y = new int[glyph.length];
doLayout(text);
}
/**
* @param keepWhiteSpace the keepWhiteSpace to set
*/
public void setKeepWhiteSpace(boolean keepWhiteSpace) {
this.keepWhiteSpace = keepWhiteSpace;
}
/**
* @return the keepWhiteSpace
*/
public boolean getKeepWhiteSpace() {
return keepWhiteSpace;
}
/**
* Does the layout
*/
private void doLayout(String text) {
// Remove superfluous carriage returns at the end
if (!keepWhiteSpace) {
while (text.endsWith("\n")) {
text = text.substring(0, text.length() - 1);
}
}
StringTokenizer st = new StringTokenizer(text, "\n", true);
boolean lastTokenWasParagraph = true;
// Break the text up into paragraphs separated by newlines
while (st.hasMoreTokens()) {
String paragraph = st.nextToken();
if ("\n".equals(paragraph)) {
if (lastTokenWasParagraph) {
penY -= (int) ((font.getAscent() + font.getDescent()) * scale) + leading;
}
lastTokenWasParagraph = true;
continue;
} else {
lastTokenWasParagraph = false;
}
format.format(this, paragraph, !st.hasMoreTokens());
}
// There we go, and here's the height (making the bounding box (0, -height) - (width, 0))
height = -penY;
}
/**
* Returns the glyphs and their offsets. Make sure the incoming arrays are big enough
* to accommodate the results (ie. at least as big as numGlyphs)
*/
public void getGlyphs(Glyph[] glyph, int[] x, int[] y) {
System.arraycopy(this.glyph, 0, glyph, 0, numGlyphs);
System.arraycopy(this.x, 0, x, 0, numGlyphs);
System.arraycopy(this.y, 0, y, 0, numGlyphs);
}
/**
* Gets the font.
* @return Returns a Font
*/
public Font getFont() {
return font;
}
/**
* Gets the height.
* @return Returns a int
*/
public int getHeight() {
return height;
}
/**
* Gets the width.
* @return Returns a int
*/
public int getWidth() {
return width;
}
/**
* Gets the numGlyphs.
* @return Returns a int
*/
public int getNumGlyphs() {
return numGlyphs;
}
/**
* Convenience function to go to the next line
*/
private void nextLine(boolean isLastParagraph) {
penY -= font.getDescent() * scale;
if (!isLastParagraph) {
penY -= leading;
}
penX = 0;
}
/**
* Gets the alignment.
* @return alignment
*/
public HorizontalAlignment getAlignment() {
return alignment;
}
/**
* Align glyphs
*/
private void align(int startGlyph, int advance) {
int dx;
if (alignment == RIGHT) {
// Move to right
dx = width - advance;
} else if (alignment == CENTERED) {
// Move to middle
dx = (width - advance) >> 1;
} else {
// Do nothing
dx = 0;
}
for (int i = startGlyph; i < numGlyphs; i++) {
x[i] += dx;
}
}
/**
* Apply the text layout to a target.
*/
public void apply(Target target) {
target.setNumGlyphs(numGlyphs);
for (int i = 0; i < numGlyphs; i++) {
target.setGlyph(i, glyph[i], x[i], y[i]);
}
}
}