diff --git a/src/jogl/classes/com/jogamp/opengl/util/awt/TextRenderer.java b/src/jogl/classes/com/jogamp/opengl/util/awt/TextRenderer.java index e6f5aaa2e7..e2231bf3ab 100644 --- a/src/jogl/classes/com/jogamp/opengl/util/awt/TextRenderer.java +++ b/src/jogl/classes/com/jogamp/opengl/util/awt/TextRenderer.java @@ -1,2019 +1,1147 @@ /* - * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved. - * Copyright (c) 2010 JogAmp Community. All rights reserved. + * Copyright 2012 JogAmp Community. All rights reserved. * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: * - * - Redistribution of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. * - * - Redistribution 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. + * 2. 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 Sun Microsystems, Inc. or the names of - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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. * - * This software is provided "AS IS," without a warranty of any kind. ALL - * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, - * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A - * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN - * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR - * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR - * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR - * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR - * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE - * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, - * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF - * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - * - * You acknowledge that this software is not designed or intended for use - * in the design, construction, operation or maintenance of any nuclear - * facility. - * - * Sun gratefully acknowledges that this software was originally authored - * and developed by Kenneth Bradley Russell and Christopher John Kline. + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. */ package com.jogamp.opengl.util.awt; -import com.jogamp.common.nio.Buffers; -import com.jogamp.common.util.PropertyAccess; -import com.jogamp.opengl.GLExtensions; -import com.jogamp.opengl.util.*; -import com.jogamp.opengl.util.packrect.*; -import com.jogamp.opengl.util.texture.*; +import com.jogamp.opengl.GL; +import com.jogamp.opengl.GLContext; +import com.jogamp.opengl.GLException; +import com.jogamp.opengl.util.texture.TextureCoords; -import java.awt.AlphaComposite; import java.awt.Color; - -// For debugging purposes -import java.awt.EventQueue; import java.awt.Font; -import java.awt.Frame; import java.awt.Graphics2D; -import java.awt.Image; -import java.awt.Point; -import java.awt.RenderingHints; -import java.awt.event.*; -import java.awt.font.*; -import java.awt.geom.*; -import java.nio.*; -import java.text.*; -import java.util.*; - -import com.jogamp.opengl.*; -import com.jogamp.opengl.fixedfunc.GLPointerFunc; -import com.jogamp.opengl.glu.*; -import com.jogamp.opengl.awt.*; - -import jogamp.opengl.Debug; - - -/** Renders bitmapped Java 2D text into an OpenGL window with high - performance, full Unicode support, and a simple API. Performs - appropriate caching of text rendering results in an OpenGL texture - internally to avoid repeated font rasterization. The caching is - completely automatic, does not require any user intervention, and - has no visible controls in the public API.
-
- Using the {@link TextRenderer TextRenderer} is simple. Add a
- "TextRenderer renderer;" field to your {@link
- com.jogamp.opengl.GLEventListener GLEventListener}. In your {@link
- com.jogamp.opengl.GLEventListener#init init} method, add:
-
-
- renderer = new TextRenderer(new Font("SansSerif", Font.BOLD, 36));
-
-
- In the {@link com.jogamp.opengl.GLEventListener#display display} method of your - {@link com.jogamp.opengl.GLEventListener GLEventListener}, add: -
- renderer.beginRendering(drawable.getWidth(), drawable.getHeight());
- // optionally set the color
- renderer.setColor(1.0f, 0.2f, 0.2f, 0.8f);
- renderer.draw("Text to draw", xPosition, yPosition);
- // ... more draw commands, color changes, etc.
- renderer.endRendering();
-
-
- Unless you are sharing textures and display lists between OpenGL
- contexts, you do not need to call the {@link #dispose dispose}
- method of the TextRenderer; the OpenGL resources it uses
- internally will be cleaned up automatically when the OpenGL
- context is destroyed. - - Note that the TextRenderer may cause the vertex and texture - coordinate array buffer bindings to change, or to be unbound. This - is important to note if you are using Vertex Buffer Objects (VBOs) - in your application.
- - Internally, the renderer uses a rectangle packing algorithm to - pack both glyphs and full Strings' rendering results (which are - variable size) onto a larger OpenGL texture. The internal backing - store is maintained using a {@link - com.jogamp.opengl.util.awt.TextureRenderer TextureRenderer}. A least - recently used (LRU) algorithm is used to discard previously - rendered strings; the specific algorithm is undefined, but is - currently implemented by flushing unused Strings' rendering - results every few hundred rendering cycles, where a rendering - cycle is defined as a pair of calls to {@link #beginRendering - beginRendering} / {@link #endRendering endRendering}. - - @author John Burkey - @author Kenneth Russell -*/ -public class TextRenderer { - private static final boolean DEBUG; - - static { - Debug.initSingleton(); - DEBUG = PropertyAccess.isPropertyDefined("jogl.debug.TextRenderer", true); - } +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.geom.Rectangle2D; +import java.lang.Character.UnicodeBlock; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import jogamp.opengl.util.awt.text.Check; +import jogamp.opengl.util.awt.text.Glyph; +import jogamp.opengl.util.awt.text.GlyphCache; +import jogamp.opengl.util.awt.text.GlyphProducer; +import jogamp.opengl.util.awt.text.GlyphProducers; +import jogamp.opengl.util.awt.text.GlyphRenderer; +import jogamp.opengl.util.awt.text.GlyphRenderers; + + +/** + * Utility for rendering bitmapped Java 2D text into an OpenGL window. + * + *
+ * {@code TextRenderer} has high performance, full Unicode support, and a simple API. It performs + * appropriate caching of text rendering results in an OpenGL texture internally to avoid repeated + * font rasterization. The caching is completely automatic, does not require any user + * intervention, and has no visible controls in the public API. + * + *
+ * Using {@code TextRenderer} is simple. Add a {@code TextRenderer} field to your {@code + * GLEventListener} and in your {@code init} method, add: + * + *
+ * renderer = new TextRenderer(new Font("SansSerif", Font.BOLD, 36));
+ *
+ *
+ * + * In the {@code display} method of your {@code GLEventListener}, add: + * + *
+ * renderer.beginRendering(drawable.getWidth(), drawable.getHeight());
+ * // optionally set the color
+ * renderer.setColor(1.0f, 0.2f, 0.2f, 0.8f);
+ * renderer.draw("Text to draw", xPosition, yPosition);
+ * // ... more draw commands, color changes, etc.
+ * renderer.endRendering();
+ *
+ *
+ * + * Unless you are sharing textures between OpenGL contexts, you do not need to call the {@link + * #dispose dispose} method of the {@code TextRenderer}; the OpenGL resources it uses internally + * will be cleaned up automatically when the OpenGL context is destroyed. + * + *
+ * Note that a {@code TextRenderer} will cause the Vertex Array Object binding to change, or to be + * unbound. + * + *
+ * Internally, the renderer uses a rectangle packing algorithm to pack both glyphs and full
+ * strings' rendering results (which are variable size) onto a larger OpenGL texture. The internal
+ * backing store is maintained using a {@link com.jogamp.opengl.util.awt.TextureRenderer
+ * TextureRenderer}. A least recently used (LRU) algorithm is used to discard previously rendered
+ * strings; the specific algorithm is undefined, but is currently implemented by flushing unused
+ * strings' rendering results every few hundred rendering cycles, where a rendering cycle is
+ * defined as a pair of calls to {@link #beginRendering beginRendering} / {@link #endRendering
+ * endRendering}.
+ *
+ * @author John Burkey
+ * @author Kenneth Russell
+ */
+/*@NotThreadSafe*/
+public final class TextRenderer {
- // These are occasionally useful for more in-depth debugging
- private static final boolean DISABLE_GLYPH_CACHE = false;
- private static final boolean DRAW_BBOXES = false;
-
- static final int kSize = 256;
-
- // Every certain number of render cycles, flush the strings which
- // haven't been used recently
- private static final int CYCLES_PER_FLUSH = 100;
-
- // The amount of vertical dead space on the backing store before we
- // force a compaction
- private static final float MAX_VERTICAL_FRAGMENTATION = 0.7f;
- static final int kQuadsPerBuffer = 100;
- static final int kCoordsPerVertVerts = 3;
- static final int kCoordsPerVertTex = 2;
- static final int kVertsPerQuad = 4;
- static final int kTotalBufferSizeVerts = kQuadsPerBuffer * kVertsPerQuad;
- static final int kTotalBufferSizeCoordsVerts = kQuadsPerBuffer * kVertsPerQuad * kCoordsPerVertVerts;
- static final int kTotalBufferSizeCoordsTex = kQuadsPerBuffer * kVertsPerQuad * kCoordsPerVertTex;
- static final int kTotalBufferSizeBytesVerts = kTotalBufferSizeCoordsVerts * 4;
- static final int kTotalBufferSizeBytesTex = kTotalBufferSizeCoordsTex * 4;
- static final int kSizeInBytes_OneVertices_VertexData = kCoordsPerVertVerts * 4;
- static final int kSizeInBytes_OneVertices_TexData = kCoordsPerVertTex * 4;
- private final Font font;
- private final boolean antialiased;
- private final boolean useFractionalMetrics;
-
- // Whether we're attempting to use automatic mipmap generation support
- private boolean mipmap;
- private RectanglePacker packer;
- private boolean haveMaxSize;
- private final RenderDelegate renderDelegate;
- private TextureRenderer cachedBackingStore;
- private Graphics2D cachedGraphics;
- private FontRenderContext cachedFontRenderContext;
- private final Map
+ * The resulting {@code TextRenderer} will use no antialiasing or fractional metrics and the
+ * default render delegate. It will not attempt to use OpenGL's automatic mipmap generation
+ * for better scaling. All Unicode characters will be available.
+ *
+ * @param font Font to render text with
+ * @throws NullPointerException if font is null
+ */
+ public TextRenderer(/*@Nonnull*/ final Font font) {
+ this(font, false, false, null, false, null);
}
- /** Returns the bounding rectangle of the given String, assuming it
- was rendered at the origin. See {@link #getBounds(CharSequence)
- getBounds(CharSequence)}. */
- public Rectangle2D getBounds(final String str) {
- return getBounds((CharSequence) str);
+ /**
+ * Constructs a {@link TextRenderer} with optional mipmapping.
+ *
+ *
+ * The resulting {@code TextRenderer} will use no antialiasing or fractional metrics, and the
+ * default render delegate. If mipmapping is requested, the text renderer will attempt to use
+ * OpenGL's automatic mipmap generation for better scaling. All Unicode characters will be
+ * available.
+ *
+ * @param font Font to render text with
+ * @param mipmap True to generate mipmaps (to make the text scale better)
+ * @throws NullPointerException if font is null
+ */
+ public TextRenderer(/*@Nonnull*/ final Font font, final boolean mipmap) {
+ this(font, false, false, null, mipmap, null);
}
- /** Returns the bounding rectangle of the given CharSequence,
- assuming it was rendered at the origin. The coordinate system of
- the returned rectangle is Java 2D's, with increasing Y
- coordinates in the downward direction. The relative coordinate
- (0, 0) in the returned rectangle corresponds to the baseline of
- the leftmost character of the rendered string, in similar
- fashion to the results returned by, for example, {@link
- java.awt.font.GlyphVector#getVisualBounds}. Most applications
- will use only the width and height of the returned Rectangle for
- the purposes of centering or justifying the String. It is not
- specified which Java 2D bounds ({@link
- java.awt.font.GlyphVector#getVisualBounds getVisualBounds},
- {@link java.awt.font.GlyphVector#getPixelBounds getPixelBounds},
- etc.) the returned bounds correspond to, although every effort
- is made to ensure an accurate bound. */
- public Rectangle2D getBounds(final CharSequence str) {
- // FIXME: this should be more optimized and use the glyph cache
- final Rect r = stringLocations.get(str);
-
- if (r != null) {
- final TextData data = (TextData) r.getUserData();
-
- // Reconstitute the Java 2D results based on the cached values
- return new Rectangle2D.Double(-data.origin().x, -data.origin().y,
- r.w(), r.h());
- }
+ /**
+ * Constructs a {@link TextRenderer} with optional text properties.
+ *
+ *
+ * The resulting {@code TextRenderer} will use antialiasing and fractional metrics if
+ * requested, and the default render delegate. It will not attempt to use OpenGL's automatic
+ * mipmap generation for better scaling. All Unicode characters will be available.
+ *
+ * @param font Font to render text with
+ * @param antialias True to smooth edges of text
+ * @param subpixel True to use subpixel accuracy
+ * @throws NullPointerException if font is null
+ */
+ public TextRenderer(/*@Nonnull*/ final Font font,
+ final boolean antialias,
+ final boolean subpixel) {
+ this(font, antialias, subpixel, null, false, null);
+ }
- // Must return a Rectangle compatible with the layout algorithm --
- // must be idempotent
- return normalize(renderDelegate.getBounds(str, font,
- getFontRenderContext()));
+ /**
+ * Constructs a {@link TextRenderer} with optional text properties and a render delegate.
+ *
+ *
+ * The resulting {@code TextRenderer} will use antialiasing and fractional metrics if
+ * requested. The optional render delegate provides more control over the text rendered. The
+ * {@code TextRenderer} will not attempt to use OpenGL's automatic mipmap generation for better
+ * scaling. All Unicode characters will be available.
+ *
+ * @param font Font to render text with
+ * @param antialias True to smooth edges of text
+ * @param subpixel True to use subpixel accuracy
+ * @param rd Optional controller of rendering details
+ * @throws NullPointerException if font is null
+ */
+ public TextRenderer(/*@Nonnull*/ final Font font,
+ final boolean antialias,
+ final boolean subpixel,
+ /*@CheckForNull*/ final RenderDelegate rd) {
+ this(font, antialias, subpixel, rd, false, null);
}
- /** Returns the Font this renderer is using. */
- public Font getFont() {
- return font;
+ /**
+ * Constructs a {@link TextRenderer} with optional text properties, a render delegate, and
+ * mipmapping.
+ *
+ *
+ * The resulting {@code TextRenderer} will use antialiasing and fractional metrics if
+ * requested. The optional render delegate provides more control over the text rendered. If
+ * mipmapping is requested, the {@code TextRenderer} will attempt to use OpenGL's automatic
+ * mipmap generation for better scaling. All Unicode characters will be available.
+ *
+ * @param font Font to render text with
+ * @param antialias True to smooth edges of text
+ * @param subpixel True to use subpixel accuracy
+ * @param rd Optional controller of rendering details
+ * @param mipmap Whether to generate mipmaps to make the text scale better
+ * @throws NullPointerException if font is null
+ */
+ public TextRenderer(/*@Nonnull*/ final Font font,
+ final boolean antialias,
+ final boolean subpixel,
+ /*CheckForNull*/ final RenderDelegate rd,
+ final boolean mipmap) {
+ this(font, antialias, subpixel, rd, mipmap, null);
}
- /** Returns a FontRenderContext which can be used for external
- text-related size computations. This object should be considered
- transient and may become invalidated between {@link
- #beginRendering beginRendering} / {@link #endRendering
- endRendering} pairs. */
- public FontRenderContext getFontRenderContext() {
- if (cachedFontRenderContext == null) {
- cachedFontRenderContext = getGraphics2D().getFontRenderContext();
+ /**
+ * Constructs a {@link TextRenderer} with optional text properties, a render delegate,
+ * mipmapping, and a range of characters.
+ *
+ *
+ * The resulting {@code TextRenderer} will use antialiasing and fractional metrics if
+ * requested. The optional render delegate provides more control over the text rendered. If
+ * mipmapping is requested, the text renderer will attempt to use OpenGL's automatic mipmap
+ * generation for better scaling. If a character range is specified, the text renderer will
+ * limit itself to those characters to try to achieve better performance. Otherwise all
+ * Unicode characters will be available.
+ *
+ * @param font Font to render text with
+ * @param antialias True to smooth edges of text
+ * @param subpixel True to use subpixel accuracy
+ * @param rd Controller of rendering details, or null to use the default
+ * @param mipmap Whether to generate mipmaps to make the text scale better
+ * @param ub Range of unicode characters, or null to use the default
+ * @throws NullPointerException if font is null
+ */
+ public TextRenderer(/*@Nonnull*/ final Font font,
+ final boolean antialias,
+ final boolean subpixel,
+ /*@CheckForNull*/ RenderDelegate rd,
+ final boolean mipmap,
+ /*@CheckForNull*/ final UnicodeBlock ub) {
+
+ Check.notNull(font, "Font cannot be null");
+ if (rd == null) {
+ rd = DEFAULT_RENDER_DELEGATE;
}
- return cachedFontRenderContext;
+ this.font = font;
+ this.glyphCache = GlyphCache.newInstance(font, rd, antialias, subpixel, mipmap);
+ this.glyphProducer = GlyphProducers.get(font, rd, glyphCache.getFontRenderContext(), ub);
}
- /** Begins rendering with this {@link TextRenderer TextRenderer}
- into the current OpenGL drawable, pushing the projection and
- modelview matrices and some state bits and setting up a
- two-dimensional orthographic projection with (0, 0) as the
- lower-left coordinate and (width, height) as the upper-right
- coordinate. Binds and enables the internal OpenGL texture
- object, sets the texture environment mode to GL_MODULATE, and
- changes the current color to the last color set with this
- TextRenderer via {@link #setColor setColor}. This method
- disables the depth test and is equivalent to
- beginRendering(width, height, true).
-
- @param width the width of the current on-screen OpenGL drawable
- @param height the height of the current on-screen OpenGL drawable
- @throws com.jogamp.opengl.GLException If an OpenGL context is not current when this method is called
- */
- public void beginRendering(final int width, final int height) throws GLException {
- beginRendering(width, height, true);
+ /**
+ * Starts a 3D render cycle.
+ *
+ *
+ * Assumes the end user is responsible for setting up the modelview and projection matrices,
+ * and will render text using the {@link #draw3D} method.
+ *
+ * @throws GLException if an OpenGL context is not current
+ */
+ public void begin3DRendering() {
+ beginRendering(false, 0, 0, false);
}
- /** Begins rendering with this {@link TextRenderer TextRenderer}
- into the current OpenGL drawable, pushing the projection and
- modelview matrices and some state bits and setting up a
- two-dimensional orthographic projection with (0, 0) as the
- lower-left coordinate and (width, height) as the upper-right
- coordinate. Binds and enables the internal OpenGL texture
- object, sets the texture environment mode to GL_MODULATE, and
- changes the current color to the last color set with this
- TextRenderer via {@link #setColor setColor}. Disables the depth
- test if the disableDepthTest argument is true.
-
- @param width the width of the current on-screen OpenGL drawable
- @param height the height of the current on-screen OpenGL drawable
- @param disableDepthTest whether to disable the depth test
- @throws GLException If an OpenGL context is not current when this method is called
- */
- public void beginRendering(final int width, final int height, final boolean disableDepthTest)
- throws GLException {
- beginRendering(true, width, height, disableDepthTest);
+ /**
+ * Starts an orthographic render cycle.
+ *
+ *
+ * Sets up a two-dimensional orthographic projection with (0,0) as the lower-left coordinate
+ * and (width, height) as the upper-right coordinate. Binds and enables the internal OpenGL
+ * texture object, sets the texture environment mode to GL_MODULATE, and changes the current
+ * color to the last color set with this text drawer via {@link #setColor}.
+ *
+ *
+ * This method disables the depth test and is equivalent to beginRendering(width, height,
+ * true).
+ *
+ * @param width Width of the current on-screen OpenGL drawable
+ * @param height Height of the current on-screen OpenGL drawable
+ * @throws GLException if an OpenGL context is not current
+ * @throws IllegalArgumentException if width or height is negative
+ */
+ public void beginRendering(/*@Nonnegative*/ final int width,
+ /*@Nonnegative*/ final int height) {
+ beginRendering(true, width, height, true);
}
- /** Begins rendering of 2D text in 3D with this {@link TextRenderer
- TextRenderer} into the current OpenGL drawable. Assumes the end
- user is responsible for setting up the modelview and projection
- matrices, and will render text using the {@link #draw3D draw3D}
- method. This method pushes some OpenGL state bits, binds and
- enables the internal OpenGL texture object, sets the texture
- environment mode to GL_MODULATE, and changes the current color
- to the last color set with this TextRenderer via {@link
- #setColor setColor}.
-
- @throws GLException If an OpenGL context is not current when this method is called
- */
- public void begin3DRendering() throws GLException {
- beginRendering(false, 0, 0, false);
+ /**
+ * Starts an orthographic render cycle.
+ *
+ *
+ * Sets up a two-dimensional orthographic projection with (0,0) as the lower-left coordinate
+ * and (width, height) as the upper-right coordinate. Binds and enables the internal OpenGL
+ * texture object, sets the texture environment mode to GL_MODULATE, and changes the current
+ * color to the last color set with this text drawer via {@link #setColor}.
+ *
+ *
+ * Disables the depth test if requested.
+ *
+ * @param width Width of the current on-screen OpenGL drawable
+ * @param height Height of the current on-screen OpenGL drawable
+ * @param disableDepthTest True to disable the depth test
+ * @throws GLException if an OpenGL context is not current
+ * @throws IllegalArgumentException if width or height is negative
+ */
+ public void beginRendering(/*@Nonnegative*/ final int width,
+ /*@Nonnegative*/ final int height,
+ final boolean disableDepthTest) {
+ beginRendering(true, width, height, disableDepthTest);
}
- /** Changes the current color of this TextRenderer to the supplied
- one. The default color is opaque white.
-
- @param color the new color to use for rendering text
- @throws GLException If an OpenGL context is not current when this method is called
- */
- public void setColor(final Color color) throws GLException {
- final boolean noNeedForFlush = (haveCachedColor && (cachedColor != null) &&
- color.equals(cachedColor));
+ /**
+ * Starts a render cycle.
+ *
+ * @param ortho True to use orthographic projection
+ * @param width Width of the current OpenGL viewport
+ * @param height Height of the current OpenGL viewport
+ * @param disableDepthTest True to ignore depth values
+ * @throws GLException if no OpenGL context is current or it's an unexpected version
+ * @throws IllegalArgumentException if width or height is negative
+ */
+ private void beginRendering(final boolean ortho,
+ /*@Nonnegative*/ final int width,
+ /*@Nonnegative*/ final int height,
+ final boolean disableDepthTest) {
- if (!noNeedForFlush) {
- flushGlyphPipeline();
- }
+ Check.argument(width >= 0, "Width cannot be negative");
+ Check.argument(height >= 0, "Height cannot be negative");
- getBackingStore().setColor(color);
- haveCachedColor = true;
- cachedColor = color;
- }
+ // Get the current OpenGL context
+ final GL gl = GLContext.getCurrentGL();
- /** Changes the current color of this TextRenderer to the supplied
- one, where each component ranges from 0.0f - 1.0f. The alpha
- component, if used, does not need to be premultiplied into the
- color channels as described in the documentation for {@link
- com.jogamp.opengl.util.texture.Texture Texture}, although
- premultiplied colors are used internally. The default color is
- opaque white.
-
- @param r the red component of the new color
- @param g the green component of the new color
- @param b the blue component of the new color
- @param a the alpha component of the new color, 0.0f = completely
- transparent, 1.0f = completely opaque
- @throws GLException If an OpenGL context is not current when this method is called
- */
- public void setColor(final float r, final float g, final float b, final float a)
- throws GLException {
- final boolean noNeedForFlush = (haveCachedColor && (cachedColor == null) &&
- (r == cachedR) && (g == cachedG) && (b == cachedB) &&
- (a == cachedA));
-
- if (!noNeedForFlush) {
- flushGlyphPipeline();
+ // Make sure components are set up properly
+ if (!ready) {
+ glyphCache.addListener(mediator);
+ glyphRenderer.addListener(mediator);
+ ready = true;
}
- getBackingStore().setColor(r, g, b, a);
- haveCachedColor = true;
- cachedR = r;
- cachedG = g;
- cachedB = b;
- cachedA = a;
- cachedColor = null;
+ // Delegate to components
+ glyphCache.beginRendering(gl);
+ glyphRenderer.beginRendering(gl, ortho, width, height, disableDepthTest);
}
- /** Draws the supplied CharSequence at the desired location using
- the renderer's current color. The baseline of the leftmost
- character is at position (x, y) specified in OpenGL coordinates,
- where the origin is at the lower-left of the drawable and the Y
- coordinate increases in the upward direction.
-
- @param str the string to draw
- @param x the x coordinate at which to draw
- @param y the y coordinate at which to draw
- @throws GLException If an OpenGL context is not current when this method is called
- */
- public void draw(final CharSequence str, final int x, final int y) throws GLException {
- draw3D(str, x, y, 0, 1);
- }
+ /**
+ * Destroys resources used by the text renderer.
+ *
+ * @throws GLException if no OpenGL context is current, or is unexpected version
+ */
+ public void dispose() {
- /** Draws the supplied String at the desired location using the
- renderer's current color. See {@link #draw(CharSequence, int,
- int) draw(CharSequence, int, int)}. */
- public void draw(final String str, final int x, final int y) throws GLException {
- draw3D(str, x, y, 0, 1);
- }
+ // Get the current OpenGL context
+ final GL gl = GLContext.getCurrentGL();
- /** Draws the supplied CharSequence at the desired 3D location using
- the renderer's current color. The baseline of the leftmost
- character is placed at position (x, y, z) in the current
- coordinate system.
-
- @param str the string to draw
- @param x the x coordinate at which to draw
- @param y the y coordinate at which to draw
- @param z the z coordinate at which to draw
- @param scaleFactor a uniform scale factor applied to the width and height of the drawn rectangle
- @throws GLException If an OpenGL context is not current when this method is called
- */
- public void draw3D(final CharSequence str, final float x, final float y, final float z,
- final float scaleFactor) {
- internal_draw3D(str, x, y, z, scaleFactor);
- }
+ // Destroy the glyph cache
+ glyphCache.dispose(gl);
- /** Draws the supplied String at the desired 3D location using the
- renderer's current color. See {@link #draw3D(CharSequence,
- float, float, float, float) draw3D(CharSequence, float, float,
- float, float)}. */
- public void draw3D(final String str, final float x, final float y, final float z, final float scaleFactor) {
- internal_draw3D(str, x, y, z, scaleFactor);
+ // Destroy the glyph renderer
+ glyphRenderer.dispose(gl);
}
- /** Returns the pixel width of the given character. */
- public float getCharWidth(final char inChar) {
- return mGlyphProducer.getGlyphPixelWidth(inChar);
+ /**
+ * Draws a character sequence at a location.
+ *
+ *
+ * The baseline of the leftmost character is at position (x, y) specified in OpenGL
+ * coordinates, where the origin is at the lower-left of the drawable and the Y coordinate
+ * increases in the upward direction.
+ *
+ * @param text Text to draw
+ * @param x Position to draw on X axis
+ * @param y Position to draw on Y axis
+ * @throws NullPointerException if text is null
+ * @throws GLException if an OpenGL context is not current, or is unexpected version
+ */
+ public void draw(/*@Nonnull*/ final CharSequence text,
+ /*@CheckForSigned*/ final int x,
+ /*@CheckForSigned*/ final int y) {
+ draw3D(text, x, y, 0, 1);
}
- /** Causes the TextRenderer to flush any internal caches it may be
- maintaining and draw its rendering results to the screen. This
- should be called after each call to draw() if you are setting
- OpenGL state such as the modelview matrix between calls to
- draw(). */
- public void flush() {
- flushGlyphPipeline();
+ /**
+ * Draws a string at a location.
+ *
+ *
+ * The baseline of the leftmost character is at position (x, y) specified in OpenGL
+ * coordinates, where the origin is at the lower-left of the drawable and the Y coordinate
+ * increases in the upward direction.
+ *
+ * @param text Text to draw
+ * @param x Position to draw on X axis
+ * @param y Position to draw on Y axis
+ * @throws NullPointerException if text is null
+ * @throws GLException if an OpenGL context is not current, or is unexpected version
+ */
+ public void draw(/*@Nonnull*/ final String text,
+ /*@CheckForSigned*/ final int x,
+ /*@CheckForSigned*/ final int y) {
+ draw3D(text, x, y, 0, 1);
}
- /** Ends a render cycle with this {@link TextRenderer TextRenderer}.
- Restores the projection and modelview matrices as well as
- several OpenGL state bits. Should be paired with {@link
- #beginRendering beginRendering}.
-
- @throws GLException If an OpenGL context is not current when this method is called
- */
- public void endRendering() throws GLException {
- endRendering(true);
+ /**
+ * Draws a character sequence at a location in 3D space.
+ *
+ *
+ * The baseline of the leftmost character is placed at position (x, y, z) in the current
+ * coordinate system.
+ *
+ * @param text Text to draw
+ * @param x X coordinate at which to draw
+ * @param y Y coordinate at which to draw
+ * @param z Z coordinate at which to draw
+ * @param scale Uniform scale applied to width and height of text
+ * @throws NullPointerException if text is null
+ * @throws GLException if an OpenGL context is not current, or is unexpected version
+ */
+ public void draw3D(/*@Nonnull*/ final CharSequence text,
+ /*@CheckForSigned*/ final float x,
+ /*@CheckForSigned*/ final float y,
+ /*@CheckForSigned*/ final float z,
+ /*@CheckForSigned*/ final float scale) {
+ draw3D(text.toString(), x, y, z, scale);
}
- /** Ends a 3D render cycle with this {@link TextRenderer TextRenderer}.
- Restores several OpenGL state bits. Should be paired with {@link
- #begin3DRendering begin3DRendering}.
+ /**
+ * Draws text at a location in 3D space.
+ *
+ *
+ * Uses the renderer's current color. The baseline of the leftmost character is placed at
+ * position (x, y, z) in the current coordinate system.
+ *
+ * @param text Text to draw
+ * @param x Position to draw on X axis
+ * @param y Position to draw on Y axis
+ * @param z Position to draw on Z axis
+ * @param scale Uniform scale applied to width and height of text
+ * @throws GLException if no OpenGL context is current, or is unexpected version
+ * @throws NullPointerException if text is null
+ */
+ public void draw3D(/*@Nonnull*/ final String text,
+ /*@CheckForSigned*/ float x,
+ /*@CheckForSigned*/ final float y,
+ /*@CheckForSigned*/ final float z,
+ /*@CheckForSigned*/ final float scale) {
- @throws GLException If an OpenGL context is not current when this method is called
- */
- public void end3DRendering() throws GLException {
- endRendering(false);
- }
+ Check.notNull(text, "Text cannot be null");
- /** Disposes of all resources this TextRenderer is using. It is not
- valid to use the TextRenderer after this method is called.
+ // Get the current OpenGL context
+ final GL gl = GLContext.getCurrentGL();
- @throws GLException If an OpenGL context is not current when this method is called
- */
- public void dispose() throws GLException {
- packer.dispose();
- packer = null;
- cachedBackingStore = null;
- cachedGraphics = null;
- cachedFontRenderContext = null;
+ // Get all the glyphs for the string
+ final List
+ * This should be called after each call to {@code draw} if you are setting OpenGL state such
+ * as the modelview matrix between calls to {@code draw}.
+ *
+ * @throws GLException if no OpenGL context is current, or is unexpected version
+ * @throws IllegalStateException if not in a render cycle
+ */
+ public void flush() {
- if (renderer != cachedBackingStore) {
- // Backing store changed since last time; discard any cached Graphics2D
- if (cachedGraphics != null) {
- cachedGraphics.dispose();
- cachedGraphics = null;
- cachedFontRenderContext = null;
- }
+ // Get the current OpenGL context
+ final GL gl = GLContext.getCurrentGL();
- cachedBackingStore = renderer;
- }
+ // Make sure glyph cache is up to date
+ glyphCache.update(gl);
- return cachedBackingStore;
+ // Render outstanding glyphs
+ glyphRenderer.flush(gl);
}
- private Graphics2D getGraphics2D() {
- final TextureRenderer renderer = getBackingStore();
-
- if (cachedGraphics == null) {
- cachedGraphics = renderer.createGraphics();
-
- // Set up composite, font and rendering hints
- cachedGraphics.setComposite(AlphaComposite.Src);
- cachedGraphics.setColor(Color.WHITE);
- cachedGraphics.setFont(font);
- cachedGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
- (antialiased ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON
- : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF));
- cachedGraphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
- (useFractionalMetrics
- ? RenderingHints.VALUE_FRACTIONALMETRICS_ON
- : RenderingHints.VALUE_FRACTIONALMETRICS_OFF));
- }
-
- return cachedGraphics;
+ /**
+ * Determines the bounding box of a character sequence.
+ *
+ *
+ * Assumes it was rendered at the origin.
+ *
+ *
+ * The coordinate system of the returned rectangle is Java 2D's, with increasing Y coordinates
+ * in the downward direction. The relative coordinate (0,0) in the returned rectangle
+ * corresponds to the baseline of the leftmost character of the rendered string, in similar
+ * fashion to the results returned by, for example, {@link GlyphVector#getVisualBounds
+ * getVisualBounds}.
+ *
+ *
+ * Most applications will use only the width and height of the returned Rectangle for the
+ * purposes of centering or justifying the String. It is not specified which Java 2D bounds
+ * ({@link GlyphVector#getVisualBounds getVisualBounds}, {@link GlyphVector#getPixelBounds
+ * getPixelBounds}, etc.) the returned bounds correspond to, although every effort is made to
+ * ensure an accurate bound.
+ *
+ * @param text Text to get bounding box for
+ * @return Rectangle surrounding the given text, not null
+ * @throws NullPointerException if text is null
+ */
+ /*@Nonnull*/
+ public Rectangle2D getBounds(/*@Nonnull*/ final CharSequence text) {
+ Check.notNull(text, "Text cannot be null");
+ return getBounds(text.toString());
}
- private void beginRendering(final boolean ortho, final int width, final int height,
- final boolean disableDepthTestForOrtho) {
- final GL2 gl = GLContext.getCurrentGL().getGL2();
-
- if (DEBUG && !debugged) {
- debug(gl);
- }
-
- inBeginEndPair = true;
- isOrthoMode = ortho;
- beginRenderingWidth = width;
- beginRenderingHeight = height;
- beginRenderingDepthTestDisabled = disableDepthTestForOrtho;
-
- if (ortho) {
- getBackingStore().beginOrthoRendering(width, height,
- disableDepthTestForOrtho);
- } else {
- getBackingStore().begin3DRendering();
- }
-
- // Push client attrib bits used by the pipelined quad renderer
- gl.glPushClientAttrib((int) GL2.GL_ALL_CLIENT_ATTRIB_BITS);
-
- if (!haveMaxSize) {
- // Query OpenGL for the maximum texture size and set it in the
- // RectanglePacker to keep it from expanding too large
- final int[] sz = new int[1];
- gl.glGetIntegerv(GL.GL_MAX_TEXTURE_SIZE, sz, 0);
- packer.setMaxSize(sz[0], sz[0]);
- haveMaxSize = true;
- }
-
- if (needToResetColor && haveCachedColor) {
- if (cachedColor == null) {
- getBackingStore().setColor(cachedR, cachedG, cachedB, cachedA);
- } else {
- getBackingStore().setColor(cachedColor);
- }
-
- needToResetColor = false;
- }
-
- // Disable future attempts to use mipmapping if TextureRenderer
- // doesn't support it
- if (mipmap && !getBackingStore().isUsingAutoMipmapGeneration()) {
- if (DEBUG) {
- System.err.println("Disabled mipmapping in TextRenderer");
- }
-
- mipmap = false;
- }
+ /**
+ * Determines the bounding box of a string.
+ *
+ * @param text Text to get bounding box for
+ * @return Rectangle surrounding the given text, not null
+ * @throws NullPointerException if text is null
+ */
+ /*@Nonnull*/
+ public Rectangle2D getBounds(/*@Nonnull*/ final String text) {
+ Check.notNull(text, "Text cannot be null");
+ return glyphProducer.findBounds(text);
}
/**
- * emzic: here the call to glBindBuffer crashes on certain graphicscard/driver combinations
- * this is why the ugly try-catch block has been added, which falls back to the old textrenderer
+ * Determines the pixel width of a character.
*
- * @param ortho
- * @throws GLException
+ * @param c Character to get pixel width of
+ * @return Number of pixels required to advance past the character
*/
- private void endRendering(final boolean ortho) throws GLException {
- flushGlyphPipeline();
-
- inBeginEndPair = false;
-
- final GL2 gl = GLContext.getCurrentGL().getGL2();
-
- // Pop client attrib bits used by the pipelined quad renderer
- gl.glPopClientAttrib();
-
- // The OpenGL spec is unclear about whether this changes the
- // buffer bindings, so preemptively zero out the GL_ARRAY_BUFFER
- // binding
- if (getUseVertexArrays() && is15Available(gl)) {
- try {
- gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0);
- } catch (final Exception e) {
- isExtensionAvailable_GL_VERSION_1_5 = false;
- }
- }
-
- if (ortho) {
- getBackingStore().endOrthoRendering();
- } else {
- getBackingStore().end3DRendering();
- }
-
- if (++numRenderCycles >= CYCLES_PER_FLUSH) {
- numRenderCycles = 0;
-
- if (DEBUG) {
- System.err.println("Clearing unused entries in endRendering()");
- }
-
- clearUnusedEntries();
- }
+ public float getCharWidth(final char c) {
+ return glyphProducer.findAdvance(c);
}
- private void clearUnusedEntries() {
- final java.util.List
+ * Indicates whether vertex arrays are being used internally for rendering, or whether text is
+ * rendered using the OpenGL immediate mode commands. Defaults to true.
+ */
+ public boolean getUseVertexArrays() {
+ return glyphRenderer.getUseVertexArrays();
}
- private void draw3D_ROBUST(final CharSequence str, final float x, final float y, final float z,
- final float scaleFactor) {
- String curStr;
- if (str instanceof String) {
- curStr = (String) str;
- } else {
- curStr = str.toString();
- }
-
- // Look up the string on the backing store
- Rect rect = stringLocations.get(curStr);
-
- if (rect == null) {
- // Rasterize this string and place it on the backing store
- Graphics2D g = getGraphics2D();
- final Rectangle2D origBBox = preNormalize(renderDelegate.getBounds(curStr, font, getFontRenderContext()));
- final Rectangle2D bbox = normalize(origBBox);
- final Point origin = new Point((int) -bbox.getMinX(),
- (int) -bbox.getMinY());
- rect = new Rect(0, 0, (int) bbox.getWidth(),
- (int) bbox.getHeight(),
- new TextData(curStr, origin, origBBox, -1));
-
- packer.add(rect);
- stringLocations.put(curStr, rect);
-
- // Re-fetch the Graphics2D in case the addition of the rectangle
- // caused the old backing store to be thrown away
- g = getGraphics2D();
-
- // OK, should now have an (x, y) for this rectangle; rasterize
- // the String
- final int strx = rect.x() + origin.x;
- final int stry = rect.y() + origin.y;
-
- // Clear out the area we're going to draw into
- g.setComposite(AlphaComposite.Clear);
- g.fillRect(rect.x(), rect.y(), rect.w(), rect.h());
- g.setComposite(AlphaComposite.Src);
-
- // Draw the string
- renderDelegate.draw(g, curStr, strx, stry);
-
- if (DRAW_BBOXES) {
- final TextData data = (TextData) rect.getUserData();
- // Draw a bounding box on the backing store
- g.drawRect(strx - data.origOriginX(),
- stry - data.origOriginY(),
- (int) data.origRect().getWidth(),
- (int) data.origRect().getHeight());
- g.drawRect(strx - data.origin().x,
- stry - data.origin().y,
- rect.w(),
- rect.h());
- }
-
- // Mark this region of the TextureRenderer as dirty
- getBackingStore().markDirty(rect.x(), rect.y(), rect.w(),
- rect.h());
- }
-
- // OK, now draw the portion of the backing store to the screen
- final TextureRenderer renderer = getBackingStore();
-
- // NOTE that the rectangles managed by the packer have their
- // origin at the upper-left but the TextureRenderer's origin is
- // at its lower left!!!
- final TextData data = (TextData) rect.getUserData();
- data.markUsed();
+ /**
+ * Specifies the current color of this {@link TextRenderer} using a {@link Color}.
+ *
+ * @param color Color to use for rendering text
+ * @throws NullPointerException if color is null
+ * @throws GLException if an OpenGL context is not current
+ */
+ public void setColor(/*@Nonnull*/ final Color color) {
- final Rectangle2D origRect = data.origRect();
+ Check.notNull(color, "Color cannot be null");
- // Align the leftmost point of the baseline to the (x, y, z) coordinate requested
- renderer.draw3DRect(x - (scaleFactor * data.origOriginX()),
- y - (scaleFactor * ((float) origRect.getHeight() - data.origOriginY())), z,
- rect.x() + (data.origin().x - data.origOriginX()),
- renderer.getHeight() - rect.y() - (int) origRect.getHeight() -
- (data.origin().y - data.origOriginY()),
- (int) origRect.getWidth(), (int) origRect.getHeight(), scaleFactor);
+ final float r = ((float) color.getRed()) / 255f;
+ final float g = ((float) color.getGreen()) / 255f;
+ final float b = ((float) color.getBlue()) / 255f;
+ final float a = ((float) color.getAlpha()) / 255f;
+ setColor(r, g, b, a);
}
- //----------------------------------------------------------------------
- // Debugging functionality
- //
- private void debug(final GL gl) {
- dbgFrame = new Frame("TextRenderer Debug Output");
-
- final GLCanvas dbgCanvas = new GLCanvas(new GLCapabilities(gl.getGLProfile()));
- dbgCanvas.setSharedContext(GLContext.getCurrent());
- dbgCanvas.addGLEventListener(new DebugListener(gl, dbgFrame));
- dbgFrame.add(dbgCanvas);
-
- final FPSAnimator anim = new FPSAnimator(dbgCanvas, 10);
- dbgFrame.addWindowListener(new WindowAdapter() {
- @Override
- public void windowClosing(final WindowEvent e) {
- // Run this on another thread than the AWT event queue to
- // make sure the call to Animator.stop() completes before
- // exiting
- new Thread(new Runnable() {
- @Override
- public void run() {
- anim.stop();
- }
- }).start();
- }
- });
- dbgFrame.setSize(kSize, kSize);
- dbgFrame.setVisible(true);
- anim.start();
- debugged = true;
+ /**
+ * Specifies the current color of this {@link TextRenderer} using individual components.
+ *
+ *
+ * Each component ranges from 0.0f to 1.0f. The alpha component, if used, does not need to be
+ * premultiplied into the color channels as described in the documentation for {@link
+ * com.jogamp.opengl.util.texture.Texture Texture} (although premultiplied colors are used
+ * internally). The default color is opaque white.
+ *
+ * @param r Red component of the new color
+ * @param g Green component of the new color
+ * @param b Blue component of the new color
+ * @param a Alpha component of the new color
+ */
+ public void setColor(/*@CheckForSigned*/ final float r,
+ /*@CheckForSigned*/ final float g,
+ /*@CheckForSigned*/ final float b,
+ /*@CheckForSigned*/ final float a) {
+ glyphRenderer.setColor(r, g, b, a);
}
- /** Class supporting more full control over the process of rendering
- the bitmapped text. Allows customization of whether the backing
- store text bitmap is full-color or intensity only, the size of
- each individual rendered text rectangle, and the contents of
- each individual rendered text string. The default implementation
- of this interface uses an intensity-only texture, a
- closely-cropped rectangle around the text, and renders text
- using the color white, which is modulated by the set color
- during the rendering process. */
- public static interface RenderDelegate {
- /** Indicates whether the backing store of this TextRenderer
- should be intensity-only (the default) or full-color. */
- public boolean intensityOnly();
-
- /** Computes the bounds of the given String relative to the
- origin. */
- public Rectangle2D getBounds(String str, Font font,
- FontRenderContext frc);
-
- /** Computes the bounds of the given character sequence relative
- to the origin. */
- public Rectangle2D getBounds(CharSequence str, Font font,
- FontRenderContext frc);
-
- /** Computes the bounds of the given GlyphVector, already
- assumed to have been created for a particular Font,
- relative to the origin. */
- public Rectangle2D getBounds(GlyphVector gv, FontRenderContext frc);
-
- /** Render the passed character sequence at the designated
- location using the supplied Graphics2D instance. The
- surrounding region will already have been cleared to the RGB
- color (0, 0, 0) with zero alpha. The initial drawing context
- of the passed Graphics2D will be set to use
- AlphaComposite.Src, the color white, the Font specified in the
- TextRenderer's constructor, and the rendering hints specified
- in the TextRenderer constructor. Changes made by the end user
- may be visible in successive calls to this method, but are not
- guaranteed to be preserved. Implementors of this method
- should reset the Graphics2D's state to that desired each time
- this method is called, in particular those states which are
- not the defaults. */
- public void draw(Graphics2D graphics, String str, int x, int y);
-
- /** Render the passed GlyphVector at the designated location using
- the supplied Graphics2D instance. The surrounding region will
- already have been cleared to the RGB color (0, 0, 0) with zero
- alpha. The initial drawing context of the passed Graphics2D
- will be set to use AlphaComposite.Src, the color white, the
- Font specified in the TextRenderer's constructor, and the
- rendering hints specified in the TextRenderer constructor.
- Changes made by the end user may be visible in successive
- calls to this method, but are not guaranteed to be preserved.
- Implementors of this method should reset the Graphics2D's
- state to that desired each time this method is called, in
- particular those states which are not the defaults. */
- public void drawGlyphVector(Graphics2D graphics, GlyphVector str,
- int x, int y);
+ /**
+ * Specifies whether the backing texture will use linear interpolation.
+ *
+ *
+ * If smoothing is enabled, {@code GL_LINEAR} will be used. Otherwise it uses {@code
+ * GL_NEAREST}.
+ *
+ *
+ * Defaults to true.
+ *
+ *
+ * A few graphics cards do not behave well when this is enabled, resulting in fuzzy text.
+ */
+ public void setSmoothing(final boolean smoothing) {
+ glyphCache.setUseSmoothing(smoothing);
}
- private static class CharSequenceIterator implements CharacterIterator {
- CharSequence mSequence;
- int mLength;
- int mCurrentIndex;
-
- CharSequenceIterator() {
- }
-
- CharSequenceIterator(final CharSequence sequence) {
- initFromCharSequence(sequence);
- }
-
- public void initFromCharSequence(final CharSequence sequence) {
- mSequence = sequence;
- mLength = mSequence.length();
- mCurrentIndex = 0;
- }
-
- @Override
- public char last() {
- mCurrentIndex = Math.max(0, mLength - 1);
+ /**
+ * Changes the transformation matrix used for drawing text in 3D.
+ *
+ * @param matrix Transformation matrix in column-major order
+ * @throws NullPointerException if matrix is null
+ * @throws IndexOutOfBoundsException if length of matrix is less than sixteen
+ * @throws IllegalStateException if in orthographic mode
+ */
+ public void setTransform(/*@Nonnull*/ final float matrix[]) {
+ Check.notNull(matrix, "Matrix cannot be null");
+ glyphRenderer.setTransform(matrix, false);
+ }
- return current();
- }
+ /**
+ * Changes whether vertex arrays are in use.
+ *
+ *
+ * This is provided as a concession for certain graphics cards which have poor vertex array
+ * performance. If passed true, the text renderer will use vertex arrays or a vertex buffer
+ * internally for rendering. Otherwise it will use immediate mode commands. Defaults to
+ * true.
+ *
+ * @param useVertexArrays True to render with vertex arrays
+ */
+ public void setUseVertexArrays(final boolean useVertexArrays) {
+ glyphRenderer.setUseVertexArrays(useVertexArrays);
+ }
- @Override
- public char current() {
- if ((mLength == 0) || (mCurrentIndex >= mLength)) {
- return CharacterIterator.DONE;
- }
+ /**
+ * Utility supporting more full control over rendering the bitmapped text.
+ *
+ *
+ * Allows customization of whether the backing store text bitmap is full-color or intensity
+ * only, the size of each individual rendered text rectangle, and the contents of each
+ * individual rendered text string.
+ */
+ public interface RenderDelegate {
+
+ /**
+ * Renders text into a graphics instance at a specific location.
+ *
+ *
+ * The surrounding region will already have been cleared to the RGB color (0, 0, 0) with
+ * zero alpha. The initial drawing context of the passed Graphics2D will be set to use
+ * AlphaComposite.Src, the color white, the Font specified in the TextRenderer's
+ * constructor, and the rendering hints specified in the TextRenderer constructor.
+ *
+ *
+ * Changes made by the end user may be visible in successive calls to this method, but are
+ * not guaranteed to be preserved. Implementations should reset the Graphics2D's state to
+ * that desired each time this method is called, in particular those states which are not
+ * the defaults.
+ *
+ * @param g2d Graphics to render into
+ * @param str Text to render
+ * @param x Location on X axis to render at
+ * @param y Location on Y axis to render at
+ * @throws NullPointerException if graphics or text is null
+ */
+ void draw(/*@Nonnull*/ Graphics2D g2d,
+ /*@Nonnull*/ String str,
+ /*@CheckForSigned*/ int x,
+ /*@CheckForSigned*/ int y);
+
+ /**
+ * Renders a glyph into a graphics instance at a specific location.
+ *
+ *
+ * The surrounding region will already have been cleared to the RGB color (0, 0, 0) with
+ * zero alpha. The initial drawing context of the passed Graphics2D will be set to use
+ * AlphaComposite.Src, the color white, the Font specified in the TextRenderer's
+ * constructor, and the rendering hints specified in the TextRenderer constructor.
+ *
+ *
+ * Changes made by the end user may be visible in successive calls to this method, but are
+ * not guaranteed to be preserved. Implementations should reset the Graphics2D's state to
+ * that desired each time this method is called, in particular those states which are not
+ * the defaults.
+ *
+ * @param g2d Graphics to render into
+ * @param gv Glyph to render
+ * @param x Location on X axis to render at
+ * @param y Location on Y axis to render at
+ * @throws NullPointerException if graphics or glyph is null
+ */
+ void drawGlyphVector(/*@Nonnull*/ Graphics2D g2d,
+ /*@Nonnull*/ GlyphVector gv,
+ /*@CheckForSigned*/ int x,
+ /*@CheckForSigned*/ int y);
+
+ /**
+ * Computes the bounds of a character sequence relative to the origin.
+ *
+ * @param text Text to compute bounds of
+ * @param font Font text renderer is using
+ * @param frc Device-dependent details of how text should be rendered
+ * @return Rectangle surrounding the text, not null
+ * @throws NullPointerException if text, font, or font render context is null
+ */
+ /*@Nonnull*/
+ Rectangle2D getBounds(/*@Nonnull*/ CharSequence text,
+ /*@Nonnull*/ Font font,
+ /*@Nonnull*/ FontRenderContext frc);
+
+ /**
+ * Computes the bounds of a glyph relative to the origin.
+ *
+ * @param gv Glyph to compute bounds of (non-null)
+ * @param frc Device-dependent details of how text should be rendered (non-null)
+ * @return Rectangle surrounding the text (non-null)
+ * @throws NullPointerException if glyph or font render context is null
+ */
+ Rectangle2D getBounds(/*@Nonnull*/ GlyphVector gv, /*@Nonnull*/ FontRenderContext frc);
+
+ /**
+ * Computes the bounds of a string relative to the origin.
+ *
+ * @param text Text to compute bounds of
+ * @param font Font text renderer is using
+ * @param frc Device-dependent details of how text should be rendered (non-null)
+ * @return Rectangle surrounding the text, not null
+ * @throws NullPointerException if text, font, or font render context is null
+ */
+ Rectangle2D getBounds(/*@Nonnull*/ String text,
+ /*@Nonnull*/ Font font,
+ /*@Nonnull*/ FontRenderContext frc);
+
+ /**
+ * Indicates whether the backing store should be intensity-only or full-color.
+ *
+ *
+ * Note that currently the text renderer does not support full-color. It will throw an
+ * {@link UnsupportedOperationException} if the render delegate requests full-color.
+ *
+ * @return True if the backing store should be intensity-only
+ */
+ boolean intensityOnly();
+ }
- return mSequence.charAt(mCurrentIndex);
- }
+ /**
+ * Simple render delegate if one is not specified by the user.
+ */
+ /*@Immmutable*/
+ public static class DefaultRenderDelegate implements RenderDelegate {
@Override
- public char next() {
- mCurrentIndex++;
-
- return current();
- }
+ public void draw(/*@Nonnull*/ final Graphics2D g2d,
+ /*@Nonnull*/ final String str,
+ /*@CheckForSigned*/ final int x,
+ /*@CheckForSigned*/ final int y) {
- @Override
- public char previous() {
- mCurrentIndex = Math.max(mCurrentIndex - 1, 0);
+ Check.notNull(g2d, "Graphics cannot be null");
+ Check.notNull(str, "String cannot be null");
- return current();
+ g2d.drawString(str, x, y);
}
@Override
- public char setIndex(final int position) {
- mCurrentIndex = position;
+ public void drawGlyphVector(/*@Nonnull*/ final Graphics2D g2d,
+ /*@Nonnull*/ final GlyphVector gv,
+ /*@CheckForSigned*/ final int x,
+ /*@CheckForSigned*/ final int y) {
- return current();
- }
+ Check.notNull(g2d, "Graphics cannot be null");
+ Check.notNull(gv, "Glyph vector cannot be null");
- @Override
- public int getBeginIndex() {
- return 0;
+ g2d.drawGlyphVector(gv, x, y);
}
+ /*@Nonnull*/
@Override
- public int getEndIndex() {
- return mLength;
- }
+ public Rectangle2D getBounds(/*@Nonnull*/ final CharSequence text,
+ /*@Nonnull*/ final Font font,
+ /*@Nonnull*/ final FontRenderContext frc) {
- @Override
- public int getIndex() {
- return mCurrentIndex;
- }
+ Check.notNull(text, "Text cannot be null");
+ Check.notNull(font, "Font cannot be null");
+ Check.notNull(frc, "Font render context cannot be null");
- @Override
- public Object clone() {
- final CharSequenceIterator iter = new CharSequenceIterator(mSequence);
- iter.mCurrentIndex = mCurrentIndex;
-
- return iter;
+ return getBounds(text.toString(), font, frc);
}
+ /*@Nonnull*/
@Override
- public char first() {
- if (mLength == 0) {
- return CharacterIterator.DONE;
- }
+ public Rectangle2D getBounds(/*@Nonnull*/ final GlyphVector gv,
+ /*@Nonnull*/ final FontRenderContext frc) {
- mCurrentIndex = 0;
+ Check.notNull(gv, "Glyph vector cannot be null");
+ Check.notNull(frc, "Font render context cannot be null");
- return current();
- }
- }
-
- // Data associated with each rectangle of text
- static class TextData {
- // Back-pointer to String this TextData describes, if it
- // represents a String rather than a single glyph
- private final String str;
-
- // If this TextData represents a single glyph, this is its
- // unicode ID
- int unicodeID;
-
- // The following must be defined and used VERY precisely. This is
- // the offset from the upper-left corner of this rectangle (Java
- // 2D coordinate system) at which the string must be rasterized in
- // order to fit within the rectangle -- the leftmost point of the
- // baseline.
- private final Point origin;
-
- // This represents the pre-normalized rectangle, which fits
- // within the rectangle on the backing store. We keep a
- // one-pixel border around entries on the backing store to
- // prevent bleeding of adjacent letters when using GL_LINEAR
- // filtering for rendering. The origin of this rectangle is
- // equivalent to the origin above.
- private final Rectangle2D origRect;
-
- private boolean used; // Whether this text was used recently
-
- TextData(final String str, final Point origin, final Rectangle2D origRect, final int unicodeID) {
- this.str = str;
- this.origin = origin;
- this.origRect = origRect;
- this.unicodeID = unicodeID;
- }
-
- String string() {
- return str;
- }
-
- Point origin() {
- return origin;
- }
-
- // The following three methods are used to locate the glyph
- // within the expanded rectangle coming from normalize()
- int origOriginX() {
- return (int) -origRect.getMinX();
- }
-
- int origOriginY() {
- return (int) -origRect.getMinY();
- }
-
- Rectangle2D origRect() {
- return origRect;
- }
-
- boolean used() {
- return used;
- }
-
- void markUsed() {
- used = true;
+ return gv.getVisualBounds();
}
- void clearUsed() {
- used = false;
- }
- }
-
- class Manager implements BackingStoreManager {
- private Graphics2D g;
-
+ /*@Nonnull*/
@Override
- public Object allocateBackingStore(final int w, final int h) {
- // FIXME: should consider checking Font's attributes to see
- // whether we're likely to need to support a full RGBA backing
- // store (i.e., non-default Paint, foreground color, etc.), but
- // for now, let's just be more efficient
- TextureRenderer renderer;
-
- if (renderDelegate.intensityOnly()) {
- renderer = TextureRenderer.createAlphaOnlyRenderer(w, h, mipmap);
- } else {
- renderer = new TextureRenderer(w, h, true, mipmap);
- }
- renderer.setSmoothing(smoothing);
+ public Rectangle2D getBounds(/*@Nonnull*/ final String text,
+ /*@Nonnull*/ final Font font,
+ /*@Nonnull*/ final FontRenderContext frc) {
- if (DEBUG) {
- System.err.println(" TextRenderer allocating backing store " +
- w + " x " + h);
- }
+ Check.notNull(text, "Text cannot be null");
+ Check.notNull(font, "Font cannot be null");
+ Check.notNull(frc, "Font render context cannot be null");
- return renderer;
+ return getBounds(font.createGlyphVector(frc, text), frc);
}
@Override
- public void deleteBackingStore(final Object backingStore) {
- ((TextureRenderer) backingStore).dispose();
+ public boolean intensityOnly() {
+ return true;
}
+ }
- @Override
- public boolean preExpand(final Rect cause, final int attemptNumber) {
- // Only try this one time; clear out potentially obsolete entries
- // NOTE: this heuristic and the fact that it clears the used bit
- // of all entries seems to cause cycling of entries in some
- // situations, where the backing store becomes small compared to
- // the amount of text on the screen (see the TextFlow demo) and
- // the entries continually cycle in and out of the backing
- // store, decreasing performance. If we added a little age
- // information to the entries, and only cleared out entries
- // above a certain age, this behavior would be eliminated.
- // However, it seems the system usually stabilizes itself, so
- // for now we'll just keep things simple. Note that if we don't
- // clear the used bit here, the backing store tends to increase
- // very quickly to its maximum size, at least with the TextFlow
- // demo when the text is being continually re-laid out.
- if (attemptNumber == 0) {
- if (DEBUG) {
- System.err.println(
- "Clearing unused entries in preExpand(): attempt number " +
- attemptNumber);
- }
-
- if (inBeginEndPair) {
- // Draw any outstanding glyphs
- flush();
- }
-
- clearUnusedEntries();
-
- return true;
- }
-
- return false;
- }
+ /**
+ * Utility for coordinating text renderer components.
+ */
+ private final class Mediator implements GlyphCache.EventListener, GlyphRenderer.EventListener {
@Override
- public boolean additionFailed(final Rect cause, final int attemptNumber) {
- // Heavy hammer -- might consider doing something different
- packer.clear();
- stringLocations.clear();
- mGlyphProducer.clearAllCacheEntries();
-
- if (DEBUG) {
- System.err.println(
- " *** Cleared all text because addition failed ***");
- }
+ public void onGlyphCacheEvent(/*@Nonnull*/ final GlyphCache.EventType type,
+ /*@Nonnull*/ final Object data) {
- if (attemptNumber == 0) {
- return true;
- }
+ Check.notNull(type, "Event type cannot be null");
+ Check.notNull(data, "Data cannot be null");
- return false;
- }
-
- @Override
- public boolean canCompact() {
- return true;
- }
-
- @Override
- public void beginMovement(final Object oldBackingStore, final Object newBackingStore) {
- // Exit the begin / end pair if necessary
- if (inBeginEndPair) {
- // Draw any outstanding glyphs
+ switch (type) {
+ case REALLOCATE:
flush();
-
- final GL2 gl = GLContext.getCurrentGL().getGL2();
-
- // Pop client attrib bits used by the pipelined quad renderer
- gl.glPopClientAttrib();
-
- // The OpenGL spec is unclear about whether this changes the
- // buffer bindings, so preemptively zero out the GL_ARRAY_BUFFER
- // binding
- if (getUseVertexArrays() && is15Available(gl)) {
- try {
- gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0);
- } catch (final Exception e) {
- isExtensionAvailable_GL_VERSION_1_5 = false;
- }
- }
-
- if (isOrthoMode) {
- ((TextureRenderer) oldBackingStore).endOrthoRendering();
- } else {
- ((TextureRenderer) oldBackingStore).end3DRendering();
- }
+ break;
+ case CLEAR:
+ glyphProducer.clearGlyphs();
+ break;
+ case CLEAN:
+ glyphProducer.removeGlyph((Glyph) data);
+ break;
}
-
- final TextureRenderer newRenderer = (TextureRenderer) newBackingStore;
- g = newRenderer.createGraphics();
}
@Override
- public void move(final Object oldBackingStore, final Rect oldLocation,
- final Object newBackingStore, final Rect newLocation) {
- final TextureRenderer oldRenderer = (TextureRenderer) oldBackingStore;
- final TextureRenderer newRenderer = (TextureRenderer) newBackingStore;
-
- if (oldRenderer == newRenderer) {
- // Movement on the same backing store -- easy case
- g.copyArea(oldLocation.x(), oldLocation.y(), oldLocation.w(),
- oldLocation.h(), newLocation.x() - oldLocation.x(),
- newLocation.y() - oldLocation.y());
- } else {
- // Need to draw from the old renderer's image into the new one
- final Image img = oldRenderer.getImage();
- g.drawImage(img, newLocation.x(), newLocation.y(),
- newLocation.x() + newLocation.w(),
- newLocation.y() + newLocation.h(), oldLocation.x(),
- oldLocation.y(), oldLocation.x() + oldLocation.w(),
- oldLocation.y() + oldLocation.h(), null);
- }
- }
+ public void onGlyphRendererEvent(/*@Nonnull*/ final GlyphRenderer.EventType type) {
- @Override
- public void endMovement(final Object oldBackingStore, final Object newBackingStore) {
- g.dispose();
-
- // Sync the whole surface
- final TextureRenderer newRenderer = (TextureRenderer) newBackingStore;
- newRenderer.markDirty(0, 0, newRenderer.getWidth(),
- newRenderer.getHeight());
-
- // Re-enter the begin / end pair if necessary
- if (inBeginEndPair) {
- if (isOrthoMode) {
- ((TextureRenderer) newBackingStore).beginOrthoRendering(beginRenderingWidth,
- beginRenderingHeight, beginRenderingDepthTestDisabled);
- } else {
- ((TextureRenderer) newBackingStore).begin3DRendering();
- }
+ Check.notNull(type, "Event type cannot be null");
- // Push client attrib bits used by the pipelined quad renderer
- final GL2 gl = GLContext.getCurrentGL().getGL2();
- gl.glPushClientAttrib((int) GL2.GL_ALL_CLIENT_ATTRIB_BITS);
-
- if (haveCachedColor) {
- if (cachedColor == null) {
- ((TextureRenderer) newBackingStore).setColor(cachedR,
- cachedG, cachedB, cachedA);
- } else {
- ((TextureRenderer) newBackingStore).setColor(cachedColor);
- }
- }
- } else {
- needToResetColor = true;
+ switch (type) {
+ case AUTOMATIC_FLUSH:
+ final GL gl = GLContext.getCurrentGL();
+ glyphCache.update(gl);
+ break;
}
}
}
- public static class DefaultRenderDelegate implements RenderDelegate {
- @Override
- public boolean intensityOnly() {
- return true;
+ /**
+ * Proxy for a {@link GlyphRenderer}.
+ */
+ /*@NotThreadSafe*/
+ private static final class GlyphRendererProxy implements GlyphRenderer {
+
+ /**
+ * Delegate to actually render.
+ */
+ /*@CheckForNull*/
+ private GlyphRenderer delegate;
+
+ /**
+ * Listeners added before a delegate is chosen.
+ */
+ /*@Nonnull*/
+ private final List
-
- Glyphs need to be able to re-upload themselves to the backing
- store on demand as we go along in the render sequence.
- */
-
- class Glyph {
- // If this Glyph represents an individual unicode glyph, this
- // is its unicode ID. If it represents a String, this is -1.
- private int unicodeID;
- // If the above field isn't -1, then these fields are used.
- // The glyph code in the font
- private int glyphCode;
- // The GlyphProducer which created us
- private GlyphProducer producer;
- // The advance of this glyph
- private float advance;
- // The GlyphVector for this single character; this is passed
- // in during construction but cleared during the upload
- // process
- private GlyphVector singleUnicodeGlyphVector;
- // The rectangle of this glyph on the backing store, or null
- // if it has been cleared due to space pressure
- private Rect glyphRectForTextureMapping;
- // If this Glyph represents a String, this is the sequence of
- // characters
- private String str;
- // Whether we need a valid advance when rendering this string
- // (i.e., whether it has other single glyphs coming after it)
- private boolean needAdvance;
-
- // Creates a Glyph representing an individual Unicode character
- public Glyph(final int unicodeID,
- final int glyphCode,
- final float advance,
- final GlyphVector singleUnicodeGlyphVector,
- final GlyphProducer producer) {
- this.unicodeID = unicodeID;
- this.glyphCode = glyphCode;
- this.advance = advance;
- this.singleUnicodeGlyphVector = singleUnicodeGlyphVector;
- this.producer = producer;
- }
+ public void beginRendering(/*@Nonnull*/ final GL gl,
+ final boolean ortho,
+ /*@Nonnegative*/ final int width,
+ /*@Nonnegative*/ final int height,
+ final boolean disableDepthTest) {
- // Creates a Glyph representing a sequence of characters, with
- // an indication of whether additional single glyphs are being
- // rendered after it
- public Glyph(final String str, final boolean needAdvance) {
- this.str = str;
- this.needAdvance = needAdvance;
- }
+ Check.notNull(gl, "GL cannot be null");
+ Check.argument(width >= 0, "Width cannot be negative");
+ Check.argument(height >= 0, "Height cannot be negative");
- /** Returns this glyph's unicode ID */
- public int getUnicodeID() {
- return unicodeID;
- }
+ if (delegate == null) {
- /** Returns this glyph's (font-specific) glyph code */
- public int getGlyphCode() {
- return glyphCode;
- }
+ // Create the glyph renderer
+ delegate = GlyphRenderers.get(gl);
- /** Returns the advance for this glyph */
- public float getAdvance() {
- return advance;
- }
-
- /** Draws this glyph and returns the (x) advance for this glyph */
- public float draw3D(final float inX, final float inY, final float z, final float scaleFactor) {
- if (str != null) {
- draw3D_ROBUST(str, inX, inY, z, scaleFactor);
- if (!needAdvance) {
- return 0;
- }
- // Compute and return the advance for this string
- final GlyphVector gv = font.createGlyphVector(getFontRenderContext(), str);
- float totalAdvance = 0;
- for (int i = 0; i < gv.getNumGlyphs(); i++) {
- totalAdvance += gv.getGlyphMetrics(i).getAdvance();
+ // Add the event listeners
+ for (EventListener listener : listeners) {
+ delegate.addListener(listener);
}
- return totalAdvance;
- }
-
- // This is the code path taken for individual glyphs
- if (glyphRectForTextureMapping == null) {
- upload();
- }
- try {
- if (mPipelinedQuadRenderer == null) {
- mPipelinedQuadRenderer = new Pipelined_QuadRenderer();
+ // Specify the color
+ if ((r != null) && (g != null) && (b != null) && (a != null)) {
+ delegate.setColor(r, g, b, a);
}
- final TextureRenderer renderer = getBackingStore();
- // Handles case where NPOT texture is used for backing store
- final TextureCoords wholeImageTexCoords = renderer.getTexture().getImageTexCoords();
- final float xScale = wholeImageTexCoords.right();
- final float yScale = wholeImageTexCoords.bottom();
-
- final Rect rect = glyphRectForTextureMapping;
- final TextData data = (TextData) rect.getUserData();
- data.markUsed();
-
- final Rectangle2D origRect = data.origRect();
-
- final float x = inX - (scaleFactor * data.origOriginX());
- final float y = inY - (scaleFactor * ((float) origRect.getHeight() - data.origOriginY()));
-
- final int texturex = rect.x() + (data.origin().x - data.origOriginX());
- final int texturey = renderer.getHeight() - rect.y() - (int) origRect.getHeight() -
- (data.origin().y - data.origOriginY());
- final int width = (int) origRect.getWidth();
- final int height = (int) origRect.getHeight();
-
- final float tx1 = xScale * texturex / renderer.getWidth();
- final float ty1 = yScale * (1.0f -
- ((float) texturey / (float) renderer.getHeight()));
- final float tx2 = xScale * (texturex + width) / renderer.getWidth();
- final float ty2 = yScale * (1.0f -
- ((float) (texturey + height) / (float) renderer.getHeight()));
-
- mPipelinedQuadRenderer.glTexCoord2f(tx1, ty1);
- mPipelinedQuadRenderer.glVertex3f(x, y, z);
- mPipelinedQuadRenderer.glTexCoord2f(tx2, ty1);
- mPipelinedQuadRenderer.glVertex3f(x + (width * scaleFactor), y,
- z);
- mPipelinedQuadRenderer.glTexCoord2f(tx2, ty2);
- mPipelinedQuadRenderer.glVertex3f(x + (width * scaleFactor),
- y + (height * scaleFactor), z);
- mPipelinedQuadRenderer.glTexCoord2f(tx1, ty2);
- mPipelinedQuadRenderer.glVertex3f(x,
- y + (height * scaleFactor), z);
- } catch (final Exception e) {
- e.printStackTrace();
- }
- return advance;
- }
-
- /** Notifies this glyph that it's been cleared out of the cache */
- public void clear() {
- glyphRectForTextureMapping = null;
- }
-
- private void upload() {
- final GlyphVector gv = getGlyphVector();
- final Rectangle2D origBBox = preNormalize(renderDelegate.getBounds(gv, getFontRenderContext()));
- final Rectangle2D bbox = normalize(origBBox);
- final Point origin = new Point((int) -bbox.getMinX(),
- (int) -bbox.getMinY());
- final Rect rect = new Rect(0, 0, (int) bbox.getWidth(),
- (int) bbox.getHeight(),
- new TextData(null, origin, origBBox, unicodeID));
- packer.add(rect);
- glyphRectForTextureMapping = rect;
- final Graphics2D g = getGraphics2D();
- // OK, should now have an (x, y) for this rectangle; rasterize
- // the glyph
- final int strx = rect.x() + origin.x;
- final int stry = rect.y() + origin.y;
-
- // Clear out the area we're going to draw into
- g.setComposite(AlphaComposite.Clear);
- g.fillRect(rect.x(), rect.y(), rect.w(), rect.h());
- g.setComposite(AlphaComposite.Src);
-
- // Draw the string
- renderDelegate.drawGlyphVector(g, gv, strx, stry);
-
- if (DRAW_BBOXES) {
- final TextData data = (TextData) rect.getUserData();
- // Draw a bounding box on the backing store
- g.drawRect(strx - data.origOriginX(),
- stry - data.origOriginY(),
- (int) data.origRect().getWidth(),
- (int) data.origRect().getHeight());
- g.drawRect(strx - data.origin().x,
- stry - data.origin().y,
- rect.w(),
- rect.h());
- }
-
- // Mark this region of the TextureRenderer as dirty
- getBackingStore().markDirty(rect.x(), rect.y(), rect.w(),
- rect.h());
- // Re-register ourselves with our producer
- producer.register(this);
- }
-
- private GlyphVector getGlyphVector() {
- final GlyphVector gv = singleUnicodeGlyphVector;
- if (gv != null) {
- singleUnicodeGlyphVector = null; // Don't need this anymore
- return gv;
- }
- singleUnicode[0] = (char) unicodeID;
- return font.createGlyphVector(getFontRenderContext(), singleUnicode);
- }
- }
-
- class GlyphProducer {
- static final int undefined = -2;
- final FontRenderContext fontRenderContext = null; // FIXME: Never initialized!
- List
+ * This method was formally called "normalize."
+ *
+ *
+ * Give ourselves a boundary around each entity on the backing store in order to prevent
+ * bleeding of nearby Strings due to the fact that we use linear filtering
+ *
+ *
+ * Note that this boundary is quite heuristic and is related to how far away in 3D we may view
+ * the text -- heuristically, 1.5% of the font's height.
+ *
+ * @param src Original rectangle
+ * @param font Font being used to create glyphs
+ * @return Rectangle with margin added, not null
+ * @throws NullPointerException if rectangle or font is null
+ */
+ /*@Nonnull*/
+ private static Rectangle2D addMarginTo(/*@Nonnull*/ final Rectangle2D src,
+ /*@Nonnull*/ final Font font) {
+
+ final int boundary = (int) Math.max(1, 0.015 * font.getSize());
+ final int x = (int) Math.floor(src.getMinX() - boundary);
+ final int y = (int) Math.floor(src.getMinY() - boundary);
+ final int w = (int) Math.ceil(src.getWidth() + 2 * boundary);
+ final int h = (int) Math.ceil(src.getHeight() + 2 * boundary);;
+
+ return new Rectangle2D.Float(x, y, w, h);
+ }
+
+ /**
+ * Adds inner space to a rectangle.
+ *
+ *
+ * This method was formally called "preNormalize."
+ *
+ *
+ * Need to round to integer coordinates.
+ *
+ *
+ * Also give ourselves a little slop around the reported bounds of glyphs because it looks like
+ * neither the visual nor the pixel bounds works perfectly well.
+ *
+ * @param src Original rectangle
+ * @return Rectangle with padding added, not null
+ * @throws NullPointerException if rectangle is null
+ */
+ /*@Nonnull*/
+ private static Rectangle2D addPaddingTo(/*@Nonnull*/ final Rectangle2D src) {
+
+ final int minX = (int) Math.floor(src.getMinX()) - 1;
+ final int minY = (int) Math.floor(src.getMinY()) - 1;
+ final int maxX = (int) Math.ceil(src.getMaxX()) + 1;
+ final int maxY = (int) Math.ceil(src.getMaxY()) + 1;
+
+ return new Rectangle2D.Float(minX, minY, maxX - minX, maxY - minY);
+ }
+
+ /**
+ * Adds a glyph to the reusable list for output.
+ *
+ * @param glyph Glyph to add to output
+ * @throws NullPointerException if glyph is null
+ */
+ protected final void addToOutput(/*@Nonnull*/ final Glyph glyph) {
+ Check.notNull(glyph, "Glyph cannot be null");
+ output.add(glyph);
+ }
+
+ /**
+ * Clears the reusable list for output.
+ */
+ protected final void clearOutput() {
+ output.clear();
+ }
+
+ /**
+ * Makes a glyph vector for a character.
+ *
+ * @param c Character to create glyph vector from
+ * @return Glyph vector for the character, not null
+ */
+ /*@Nonnull*/
+ protected final GlyphVector createGlyphVector(final char c) {
+ characters[0] = c;
+ return font.createGlyphVector(fontRenderContext, characters);
+ }
+
+ /**
+ * Makes a glyph vector for a string.
+ *
+ * @param font Style of text
+ * @param frc Details on how to render font
+ * @param str Text as a string
+ * @return Glyph vector for the string, not null
+ * @throws NullPointerException if string is null
+ */
+ /*@Nonnull*/
+ protected final GlyphVector createGlyphVector(/*@Nonnull*/ final String str) {
+
+ Check.notNull(str, "String cannot be null");
+
+ GlyphVector gv = glyphVectors.get(str);
+
+ // Check if already made
+ if (gv != null) {
+ return gv;
+ }
+
+ // Otherwise make and store it
+ final char[] text = str.toCharArray();
+ final int len = str.length();
+ gv = font.layoutGlyphVector(fontRenderContext, text, 0, len, 0);
+ glyphVectors.put(str, gv);
+ return gv;
+ }
+
+ /*@CheckForSigned*/
+ @Override
+ public final float findAdvance(final char c) {
+
+ // Check producer's inventory first
+ final Glyph glyph = createGlyph(c);
+ if (glyph != null) {
+ return glyph.advance;
+ }
+
+ // Otherwise create the glyph vector
+ final GlyphVector gv = createGlyphVector(c);
+ final GlyphMetrics gm = gv.getGlyphMetrics(0);
+ return gm.getAdvance();
+ }
+
+ /*@Nonnull*/
+ @Override
+ public final Rectangle2D findBounds(/*@Nonnull*/ final String str) {
+
+ Check.notNull(str, "String cannot be null");
+
+ final List
+ * The process of creating and laying out glyph vectors is relatively complex and can slow down
+ * text rendering significantly. This method is intended to increase performance by not
+ * creating glyph vectors for strings with characters that can be treated independently.
+ *
+ *
+ * Currently the decision is very simple. It just treats any characters above the IPA
+ * Extensions block as complex. This is convenient because most Latin characters are
+ * treated as simple but Spacing Modifier Letters and Combining Diacritical Marks
+ * are not. Ideally it would also be nice to have a few other blocks included, especially
+ * Greek and maybe symbols, but that is perhaps best left for later work.
+ *
+ *
+ * A truly correct implementation may require a lot of research or developers with more
+ * experience in the area. However, the following Unicode blocks are known to require full
+ * layout in some form:
+ *
+ *
+ * Asian scripts will also have letters that combine together, but it appears that the input
+ * method may take care of that so it may not be necessary to check for them here.
+ *
+ *
+ * Finally, it should be noted that even Latin has characters that can combine into glyphs
+ * called ligatures. The classic example is an 'f' and an 'i'. Java however will not make the
+ * replacements itself so we do not need to consider that here.
+ *
+ * @param str Text of unknown character types
+ * @return True if a complex character is found
+ * @throws NullPointerException if string is null
+ */
+ protected static boolean hasComplexCharacters(/*@Nonnull*/ final String str) {
+
+ Check.notNull(str, "String cannot be null");
+
+ final int len = str.length();
+ for (int i = 0; i < len; ++i) {
+ if (str.charAt(i) > 0x2AE) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if a glyph vector is complex.
+ *
+ * @param gv Glyph vector to check
+ * @return True if glyph vector is complex
+ * @throws NullPointerException if glyph vector is null
+ */
+ protected static boolean isComplex(/*@CheckForNull*/ final GlyphVector gv) {
+
+ Check.notNull(gv, "Glyph vector cannot be null");
+
+ return gv.getLayoutFlags() != 0;
+ }
+
+ /**
+ * Measures a glyph.
+ *
+ *
+ * Sets all the measurements in a glyph after it's created.
+ *
+ * @param glyph Visual representation of a character
+ * @throws NullPointerException if glyph is null
+ */
+ protected final void measure(/*@Nonnull*/ final Glyph glyph) {
+
+ Check.notNull(glyph, "Glyph cannot be null");
+
+ // Compute visual boundary
+ final Rectangle2D visualBox;
+ if (glyph.str != null) {
+ visualBox = renderDelegate.getBounds(glyph.str, font, fontRenderContext);
+ } else {
+ visualBox = renderDelegate.getBounds(glyph.glyphVector, fontRenderContext);
+ }
+
+ // Compute rectangles
+ final Rectangle2D paddingBox = addPaddingTo(visualBox);
+ final Rectangle2D marginBox = addMarginTo(paddingBox, font);
+
+ // Set fields
+ glyph.padding = new Glyph.Boundary(paddingBox, visualBox);
+ glyph.margin = new Glyph.Boundary(marginBox, paddingBox);
+ glyph.width = (float) paddingBox.getWidth();
+ glyph.height = (float) paddingBox.getHeight();
+ glyph.ascent = (float) paddingBox.getMinY() * -1;
+ glyph.descent = (float) paddingBox.getMaxY();
+ glyph.kerning = (float) paddingBox.getMinX();
+ glyph.bounds = paddingBox;
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/awt/text/AbstractGlyphRenderer.java b/src/jogl/classes/jogamp/opengl/util/awt/text/AbstractGlyphRenderer.java
new file mode 100644
index 0000000000..bb80501723
--- /dev/null
+++ b/src/jogl/classes/jogamp/opengl/util/awt/text/AbstractGlyphRenderer.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright 2012 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ *
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package jogamp.opengl.util.awt.text;
+
+import com.jogamp.opengl.GL;
+import com.jogamp.opengl.GLContext;
+import com.jogamp.opengl.util.texture.TextureCoords;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Skeletal implementation of {@link GlyphRenderer}.
+ */
+abstract class AbstractGlyphRenderer implements GlyphRenderer, QuadPipeline.EventListener {
+
+ // Default color
+ private static float DEFAULT_RED = 1.0f;
+ private static float DEFAULT_GREEN = 1.0f;
+ private static float DEFAULT_BLUE = 1.0f;
+ private static float DEFAULT_ALPHA = 1.0f;
+
+ /**
+ * Listeners to send events to.
+ */
+ /*@Nonnull*/
+ private final List
+ * The reason for the dual behavior is so that we can take in a sequence of unicode characters and
+ * partition them into runs of individual glyphs, but if we encounter complex text and/or unicode
+ * sequences we don't understand, we can render them using the string-by-string method.
+ *
+ *
+ * In an effort to make positioning glyphs more intuitive for both Java2D's and OpenGL's coordinate
+ * systems, {@code Glyph} now stores its measurements differently. This new way is patterned off
+ * of HTML's box model.
+ *
+ *
+ * Of course, as expected each glyph maintains its width and height. For spacing however, rather
+ * than storing positions in Java2D space that must be manipulated on a case-by-case basis,
+ * {@code Glyph} stores two separate pre-computed boundaries representing space around the text.
+ * Each of the boundaries has separate top, bottom, left, and right components. These components
+ * should generally be considered positive, but negative values are sometimes necessary in rare
+ * situations.
+ *
+ *
+ * The first boundary is called padding. Padding is the space between the actual glyph
+ * itself and its border. It is included in the width and height of the glyph. The second
+ * boundary that a glyph stores is called margin, which is extra space around the glyph's
+ * border. The margin is generally used for separating the glyph from other glyphs when it's
+ * stored.
+ *
+ *
+ * The diagram below shows the boundaries of a glyph and how they relate to its width and height.
+ * The inner rectangle is the glyph's boundary, and the outer rectangle is the edge of the margin.
+ *
+ *
+ * In addition, {@code Glyph} also keeps a few other measurements useful for positioning.
+ * Ascent is the distance between the baseline and the top border, while descent is
+ * the distance between the baseline and the bottom border. Kerning is the distance between
+ * the vertical baseline and the left border. Note that in some cases some of these fields can
+ * match up with padding components, but in general they should be considered separate.
+ *
+ *
+ * Below is a diagram showing ascent, descent, and kerning.
+ *
+ *
+ * {@code GlyphCache} andles storing glyphs in a 2D texture and retrieving their coordinates.
+ *
+ *
+ * The first step in using a {@code GlyphCache} is to make sure it's set up by calling {@link
+ * #beginRendering(GL)}. Then glyphs can be added using {@link #upload(Glyph)}. Each glyph will
+ * be packed efficiently into the texture with a small amount of space around it using {@link
+ * RectanglePacker}. When all glyphs have been added, be sure to call {@link #update(GL)} or
+ * {@link #endRendering(GL)} before trying to render with the texture, as the glyphs are not
+ * actually drawn into the texture right away in order to increase performance. Texture
+ * coordinates of individual glyphs can be determined with {@link #find(Glyph)}. When reusing the
+ * glyph cache, {@link #contains(Glyph)} should be called to make sure a glyph is not already
+ * stored.
+ *
+ *
+ * Events fired when:
+ *
+ * GlyphCache is compatible with GL2 or GL3.
+ *
+ * @see TextureBackingStore
+ */
+/*@NotThreadSafe*/
+public final class GlyphCache implements TextureBackingStore.EventListener {
+
+ /**
+ * Whether or not glyph cache should print debugging information.
+ */
+ private static final boolean DEBUG = false;
+
+ /**
+ * Number used to determine size of cache based on font size.
+ */
+ /*@Nonnegative*/
+ private static final int FONT_SIZE_MULTIPLIER = 5;
+
+ /**
+ * How much fragmentation to allow before compacting.
+ */
+ /*@Nonnegative*/
+ private static final float MAX_VERTICAL_FRAGMENTATION = 0.7f;
+
+ /**
+ * Number of render cycles before clearing unused entries.
+ */
+ /*@Nonnegative*/
+ private static final int CYCLES_PER_FLUSH = 100;
+
+ /**
+ * Minimum size of backing store in pixels.
+ */
+ /*@Nonnegative*/
+ private static final int MIN_BACKING_STORE_SIZE = 256;
+
+ /**
+ * Delegate to render text.
+ */
+ /*@Nonnull*/
+ private final RenderDelegate renderDelegate;
+
+ /**
+ * Observers of glyph cache.
+ */
+ /*@Nonnull*/
+ private final List
+ * This will be null until {@link #beginRendering} is called.
+ */
+ /*@CheckForNull*/
+ private TextureBackingStore backingStore;
+
+ /**
+ * Times cache has been used.
+ */
+ /*@Nonnegative*/
+ private int numRenderCycles = 0;
+
+ /**
+ * True if done initializing.
+ */
+ private boolean ready = false;
+
+ /**
+ * Constructs a {@link GlyphCache}.
+ *
+ * @param font Font that was used to create glyphs that will be stored, assumed not null
+ * @param rd Controller of rendering bitmapped text, assumed not null
+ * @param antialias True to render glyphs with smooth edges
+ * @param subpixel True to consider subpixel positioning
+ * @param mipmap True to create multiple sizes of texture
+ * @see #newInstance
+ */
+ private GlyphCache(/*@Nonnull*/ final Font font,
+ /*@Nonnull*/ final RenderDelegate rd,
+ final boolean antialias,
+ final boolean subpixel,
+ final boolean mipmap) {
+ this.renderDelegate = rd;
+ this.manager = new TextureBackingStoreManager(font, antialias, subpixel, mipmap);
+ this.packer = createPacker(font, manager);
+ }
+
+ /**
+ * Registers an {@link EventListener} with this {@link GlyphCache}.
+ *
+ * @param listener Listener to register
+ * @throws NullPointerException if listener is null
+ */
+ public void addListener(/*@Nonnull*/ final EventListener listener) {
+
+ Check.notNull(listener, "Listener cannot be null");
+
+ listeners.add(listener);
+ }
+
+ /**
+ * Sets up the cache for rendering.
+ *
+ *
+ * After calling this method the texture storing the glyphs will be bound.
+ *
+ * @param gl Current OpenGL context
+ * @throws NullPointerException if context is null
+ */
+ public void beginRendering(/*@Nonnull*/ final GL gl) {
+
+ Check.notNull(gl, "Context cannot be null");
+
+ // Set up if first time rendering
+ if (!ready) {
+ setMaxSize(gl);
+ ready = true;
+ }
+
+ // Bind the backing store
+ final TextureBackingStore bs = getBackingStore();
+ bs.bind(gl, GL.GL_TEXTURE0);
+ }
+
+ /**
+ * Clears all the texture coordinates stored in glyphs.
+ */
+ private void clearTextureCoordinates() {
+
+ log("Clearing texture coordinates");
+
+ packer.visit(new RectVisitor() {
+
+ @Override
+ public void visit(/*@Nonnull*/ final Rect rect) {
+ final Glyph glyph = ((TextData) rect.getUserData()).glyph;
+ glyph.coordinates = null;
+ }
+ });
+ }
+
+ /**
+ * Clears entries that haven't been used in awhile.
+ */
+ private void clearUnusedEntries() {
+
+ log("Trying to clear unused entries...");
+
+ // Find rectangles in backing store that haven't been used recently
+ final List
+ * After calling this method, all uploaded glyphs will be guaranteed to be present in the
+ * underlying OpenGL texture.
+ *
+ * @param gl Current OpenGL context
+ * @throws NullPointerException if context is null
+ */
+ public void endRendering(/*@Nonnull*/ final GL gl) {
+
+ Check.notNull(gl, "Context cannot be null");
+
+ update(gl);
+
+ // Check if reached render cycle limit
+ if (++numRenderCycles >= CYCLES_PER_FLUSH) {
+ numRenderCycles = 0;
+ log("Reached cycle limit.");
+ clearUnusedEntries();
+ }
+ }
+
+ /**
+ * Determines the texture coordinates of a glyph in the cache.
+ *
+ *
+ * Notes:
+ *
+ * This object should be considered transient and may become invalidated between {@link
+ * #beginRendering} and {@link #endRendering} pairs.
+ *
+ * @return Font render context used for text size computations, not null
+ */
+ /*@Nonnull*/
+ public FontRenderContext getFontRenderContext() {
+ return getBackingStore().getGraphics().getFontRenderContext();
+ }
+
+ /**
+ * Returns the height of this {@link GlyphCache}.
+ *
+ * @return Height of this cache, not negative
+ */
+ /*@Nonnegative*/
+ int getHeight() {
+ return getBackingStore().getHeight();
+ }
+
+ /**
+ * Determines the location of a glyph's left baseline.
+ *
+ * @param glyph Glyph to determine left baseline for, assumed not null
+ * @return Location of glyph's left baseline, which may be negative
+ */
+ /*@CheckForSigned*/
+ private int getLeftBaselineLocation(/*@Nonnull*/ final Glyph glyph) {
+ return (int) (glyph.location.x() + glyph.margin.left - glyph.kerning);
+ }
+
+ /**
+ * Determines the location of a glyph's left border.
+ *
+ * @param glyph Glyph to determine left border for, assumed not null
+ * @return Location of glyph's left border, which may be negative
+ */
+ /*@CheckForSigned*/
+ private int getLeftBorderLocation(/*@Nonnull*/ final Glyph glyph) {
+ return glyph.location.x() + glyph.margin.left;
+ }
+
+ /**
+ * Checks if this {@link GlyphCache} is interpolating when sampling.
+ *
+ * @return True if this glyph cache is interpolating when it samples
+ */
+ public boolean getUseSmoothing() {
+ return ((TextureBackingStoreManager) manager).getUseSmoothing();
+ }
+
+ /**
+ * Returns the width of this {@link GlyphCache}.
+ *
+ * @return Width of this cache, not negative
+ */
+ /*@Nonnegative*/
+ int getWidth() {
+ return getBackingStore().getWidth();
+ }
+
+ /**
+ * Checks if Non-Power-Of-Two textures are available.
+ *
+ * @param gl Current OpenGL context
+ * @return True if NPOT textures are available
+ * @throws NullPointerException if context is null
+ */
+ static boolean isNpotTextureAvailable(/*@Nonnull*/ final GL gl) {
+
+ Check.notNull(gl, "GL cannot be null");
+
+ return gl.isExtensionAvailable("GL_ARB_texture_non_power_of_two");
+ }
+
+ private static void log(/*@Nonnull*/ final String message) {
+ if (DEBUG) {
+ System.err.println(message);
+ }
+ }
+
+ private static void log(/*@Nonnull*/ final String message,
+ /*@CheckForNull*/ final Object arg) {
+ if (DEBUG) {
+ System.err.println(String.format(message, arg));
+ }
+ }
+
+ /**
+ * Marks a glyph's location as used.
+ *
+ * @param glyph Glyph to mark
+ * @throws NullPointerException if glyph is null
+ */
+ static void markGlyphLocationUsed(/*@Nonnull*/ final Glyph glyph) {
+
+ Check.notNull(glyph, "Glyph cannot be null");
+
+ ((TextData) glyph.location.getUserData()).markUsed();
+ }
+
+ /**
+ * Creates a new {@link GlyphCache}.
+ *
+ * @param font Font that was used to create glyphs that will be stored
+ * @param rd Controller of rendering bitmapped text
+ * @param antialias Whether to render glyphs with smooth edges
+ * @param subpixel Whether to consider subpixel positioning
+ * @param mipmap Whether to create multiple sizes for texture
+ * @return New glyph cache instance, not null
+ * @throws NullPointerException if font or render delegate is null
+ * @throws IllegalArgumentException if render delegate wants full color
+ */
+ /*@Nonnull*/
+ public static GlyphCache newInstance(/*@Nonnull*/ final Font font,
+ /*@Nonnull*/ final RenderDelegate rd,
+ final boolean antialias,
+ final boolean subpixel,
+ final boolean mipmap) {
+
+ Check.notNull(font, "Font cannot be null");
+ Check.notNull(rd, "Render delegate cannot be null");
+
+ final GlyphCache gc = new GlyphCache(font, rd, antialias, subpixel, mipmap);
+ gc.manager.addListener(gc);
+ return gc;
+ }
+
+ /**
+ * Responds to an event from the backing store.
+ *
+ * @param type Kind of backing store event
+ * @throws NullPointerException if type is null
+ */
+ @Override
+ public void onBackingStoreEvent(/*@Nonnull*/ final TextureBackingStore.EventType type) {
+
+ Check.notNull(type, "Event type cannot be null");
+
+ switch (type) {
+ case REALLOCATE:
+ onBackingStoreReallocate();
+ break;
+ case FAILURE:
+ onBackingStoreFailure();
+ break;
+ }
+ }
+
+ /**
+ * Responds to the backing store failing (reallocation).
+ */
+ private void onBackingStoreFailure() {
+ packer.clear();
+ fireEvent(EventType.CLEAR, null);
+ }
+
+ /**
+ * Handles when a backing store is reallocated.
+ *
+ *
+ * First notifies observers, then tries to remove any unused entries, and finally erases the
+ * texture coordinates of each entry since the width and height of the total texture has
+ * changed. Note that since the backing store is just expanded without moving any entries,
+ * only the texture coordinates need to be recalculated. The locations will still be the same.
+ *
+ *
+ * This heuristic and the fact that it clears the used bit of all entries seems to cause
+ * cycling of entries in some situations, where the backing store becomes small compared to the
+ * amount of text on the screen (see the TextFlow demo) and the entries continually cycle in
+ * and out of the backing store, decreasing performance. If we added a little age information
+ * to the entries, and only cleared out entries above a certain age, this behavior would be
+ * eliminated. However, it seems the system usually stabilizes itself, so for now we'll just
+ * keep things simple. Note that if we don't clear the used bit here, the backing store tends
+ * to increase very quickly to its maximum size, at least with the TextFlow demo when the text
+ * is being continually re-laid out.
+ */
+ private void onBackingStoreReallocate() {
+ fireEvent(EventType.REALLOCATE, null);
+ clearUnusedEntries();
+ clearTextureCoordinates();
+ }
+
+ /**
+ * Changes the maximum size of this {@link GlyphCache}'s rectangle packer.
+ *
+ * @param gl Current OpenGL context, assumed not null
+ */
+ private void setMaxSize(/*@Nonnull*/ final GL gl) {
+ final int maxSize = findMaxSize(gl);
+ packer.setMaxSize(maxSize, maxSize);
+ }
+
+ /**
+ * Changes whether this {@link GlyphCache}'s texture should interpolate when sampling.
+ *
+ * @param useSmoothing True to use linear interpolation
+ */
+ public void setUseSmoothing(boolean useSmoothing) {
+ ((TextureBackingStoreManager) manager).setUseSmoothing(useSmoothing);
+ getBackingStore().setUseSmoothing(useSmoothing);
+ }
+
+ /**
+ * Forces the cache to update the underlying OpenGL texture.
+ *
+ *
+ * After calling this method, all uploaded glyphs will be guaranteed to be present in the
+ * underlying OpenGL texture.
+ *
+ * @param gl Current OpenGL context
+ * @throws NullPointerException if context is null
+ */
+ public void update(/*@Nonnull*/ final GL gl) {
+
+ Check.notNull(gl, "GL cannot be null");
+
+ final TextureBackingStore bs = getBackingStore();
+ bs.update(gl);
+ }
+
+ /**
+ * Stores a glyph in the cache.
+ *
+ *
+ * Determines a place to put the glyph in the underlying OpenGL texture, computes the glyph's
+ * texture coordinates for that position, and requests the glyph be drawn into the texture.
+ * (Note however that to increase performance the glyph is not guaranteed to actually be in the
+ * texture until {@link #update(GL)} or {@link #endRendering(GL)} is called.)
+ *
+ * @param glyph Glyph not already stored in cache
+ * @throws NullPointerException if glyph is null
+ */
+ public void upload(/*@Nonnull*/ final Glyph glyph) {
+
+ Check.notNull(glyph, "Glyph cannot be null");
+
+ // Perform upload steps
+ findLocation(glyph);
+ computeCoordinates(glyph);
+ drawInBackingStore(glyph);
+
+ // Make sure it's marked as used
+ markGlyphLocationUsed(glyph);
+ }
+
+ /**
+ * Object that wants to be notified of cache events.
+ */
+ public interface EventListener {
+
+ /**
+ * Responds to an event from a {@link GlyphCache}.
+ *
+ * @param type Type of event
+ * @param data Object that triggered the event, i.e., a glyph
+ * @throws NullPointerException if event type or data is null (optional)
+ */
+ void onGlyphCacheEvent(/*@Nonnull*/ EventType type, /*@Nonnull*/ Object data);
+ }
+
+ /**
+ * Type of event fired from the cache.
+ */
+ public enum EventType {
+
+ /**
+ * All entries were removed from cache.
+ */
+ CLEAR,
+
+ /**
+ * Unused entries were removed from cache.
+ */
+ CLEAN,
+
+ /**
+ * Backing store changed size.
+ */
+ REALLOCATE;
+ }
+
+ /**
+ * Data associated with each rectangle of text.
+ */
+ /*@NotThreadSafe*/
+ static final class TextData {
+
+ /**
+ * Visual representation of text.
+ */
+ /*@Nonnull*/
+ final Glyph glyph;
+
+ /**
+ * True if text was used recently.
+ */
+ private boolean used;
+
+ /**
+ * Constructs a {@link TextData} from a glyph.
+ *
+ * @param glyph Visual representation of text
+ * @throws NullPointerException if glyph is null
+ */
+ TextData(/*@Nonnull*/ final Glyph glyph) {
+ this.glyph = Check.notNull(glyph, "Glyph cannot be null");
+ }
+
+ /**
+ * Indicates this {@link TextData} is no longer being used.
+ */
+ void clearUsed() {
+ used = false;
+ }
+
+ /**
+ * Indicates this {@link TextData} was just used.
+ */
+ void markUsed() {
+ used = true;
+ }
+
+ /**
+ * Returns the actual text stored with a rectangle.
+ *
+ * @return Actual text stored with a rectangle, not null
+ */
+ /*@CheckForNull*/
+ String string() {
+ return glyph.str;
+ }
+
+ /**
+ * Returns true if text has been used recently.
+ */
+ boolean used() {
+ return used;
+ }
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/awt/text/GlyphMap.java b/src/jogl/classes/jogamp/opengl/util/awt/text/GlyphMap.java
new file mode 100644
index 0000000000..e1981c887d
--- /dev/null
+++ b/src/jogl/classes/jogamp/opengl/util/awt/text/GlyphMap.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2012 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ *
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package jogamp.opengl.util.awt.text;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * Utility for mapping text to glyphs.
+ */
+/*@NotThreadSafe*/
+final class GlyphMap {
+
+ /**
+ * Fast map for ASCII chars.
+ */
+ /*@Nonnull*/
+ private final Glyph[] ascii = new Glyph[128];
+
+ /**
+ * Map from char to code.
+ */
+ /*@Nonnull*/
+ private final Map
+ * {@code QuadPipelineGL30} draws quads using OpenGL 3 features. It uses a Vertex Buffer Object to
+ * store vertices in graphics memory and a Vertex Array Object to quickly switch which vertex
+ * attributes are enabled.
+ *
+ *
+ * Since {@code GL_QUAD} has been deprecated in OpenGL 3, this implementation uses two triangles to
+ * represent one quad. An alternative implementation using one {@code GL_FAN} per quad was also
+ * tested, but proved slower in most cases. Apparently the penalty imposed by the extra work
+ * required by the driver outweighed the benefit of transferring less vertices.
+ */
+/*@VisibleForTesting*/
+/*@NotThreadSafe*/
+public final class QuadPipelineGL30 extends AbstractQuadPipeline {
+
+ /**
+ * Name of point attribute in shader program.
+ */
+ /*@Nonnull*/
+ private static final String POINT_ATTRIB_NAME = "MCVertex";
+
+ /**
+ * Name of texture coordinate attribute in shader program.
+ */
+ /*@Nonnull*/
+ private static final String COORD_ATTRIB_NAME = "TexCoord0";
+
+ /**
+ * Number of vertices per primitive.
+ */
+ /*@Nonnegative*/
+ private static final int VERTS_PER_PRIM = 3;
+
+ /**
+ * Number of primitives per quad.
+ */
+ /*@Nonnegative*/
+ private static final int PRIMS_PER_QUAD = 2;
+
+ /**
+ * Vertex Buffer Object with vertex data.
+ */
+ /*@Nonnegative*/
+ private final int vbo;
+
+ /**
+ * Vertex Array Object with vertex attribute state.
+ */
+ /*@Nonnegative*/
+ private final int vao;
+
+ /**
+ * Constructs a {@link QuadPipelineGL30}.
+ *
+ * @param gl Current OpenGL context
+ * @param shaderProgram Shader program to render quads with
+ * @throws NullPointerException if context is null
+ * @throws IllegalArgumentException if shader program is less than one
+ */
+ /*@VisibleForTesting*/
+ public QuadPipelineGL30(/*@Nonnull*/ final GL3 gl, /*@Nonnegative*/ final int shaderProgram) {
+
+ super(VERTS_PER_PRIM, PRIMS_PER_QUAD);
+
+ Check.notNull(gl, "GL cannot be null");
+ Check.argument(shaderProgram > 0, "Shader program cannot be less than one");
+
+ this.vbo = createVertexBufferObject(gl, BYTES_PER_BUFFER);
+ this.vao = createVertexArrayObject(gl, shaderProgram, vbo);
+ }
+
+ @Override
+ public void beginRendering(/*@Nonnull*/ final GL gl) {
+
+ super.beginRendering(gl);
+
+ final GL3 gl3 = gl.getGL3();
+
+ // Bind the VBO and VAO
+ gl3.glBindBuffer(GL3.GL_ARRAY_BUFFER, vbo);
+ gl3.glBindVertexArray(vao);
+ }
+
+ /**
+ * Creates a vertex array object for use with the pipeline.
+ *
+ * @param gl Current OpenGL context, assumed not null
+ * @param program OpenGL handle to the shader program, assumed not negative
+ * @param vbo OpenGL handle to VBO holding vertices, assumed not negative
+ * @return OpenGL handle to resulting VAO
+ */
+ /*@Nonnegative*/
+ private static int createVertexArrayObject(/*@Nonnull*/ final GL3 gl,
+ /*@Nonnegative*/ final int program,
+ /*@Nonnegative*/ final int vbo) {
+
+ // Generate
+ final int[] handles = new int[1];
+ gl.glGenVertexArrays(1, handles, 0);
+ final int vao = handles[0];
+
+ // Bind
+ gl.glBindVertexArray(vao);
+ gl.glBindBuffer(GL3.GL_ARRAY_BUFFER, vbo);
+
+ // Points
+ final int pointLoc = gl.glGetAttribLocation(program, POINT_ATTRIB_NAME);
+ if (pointLoc == -1) {
+ throw new IllegalStateException("Could not find point attribute location!");
+ } else {
+ gl.glEnableVertexAttribArray(pointLoc);
+ gl.glVertexAttribPointer(
+ pointLoc, // location
+ FLOATS_PER_POINT, // number of components
+ GL3.GL_FLOAT, // type
+ false, // normalized
+ STRIDE, // stride
+ POINT_OFFSET); // offset
+ }
+
+ // Coords
+ final int coordLoc = gl.glGetAttribLocation(program, COORD_ATTRIB_NAME);
+ if (coordLoc != -1) {
+ gl.glEnableVertexAttribArray(coordLoc);
+ gl.glVertexAttribPointer(
+ coordLoc, // location
+ FLOATS_PER_COORD, // number of components
+ GL3.GL_FLOAT, // type
+ false, // normalized
+ STRIDE, // stride
+ COORD_OFFSET); // offset
+ }
+
+ // Unbind
+ gl.glBindBuffer(GL3.GL_ARRAY_BUFFER, 0);
+ gl.glBindVertexArray(0);
+
+ return vao;
+ }
+
+ @Override
+ public void dispose(/*@Nonnull*/ final GL gl) {
+
+ super.dispose(gl);
+
+ final GL3 gl3 = gl.getGL3();
+
+ // Delete VBO and VAO
+ final int[] handles = new int[1];
+ handles[0] = vbo;
+ gl3.glDeleteBuffers(1, handles, 0);
+ handles[0] = vao;
+ gl3.glDeleteVertexArrays(1, handles, 0);
+ }
+
+ @Override
+ protected void doAddQuad(/*@Nonnull*/ final Quad quad) {
+
+ Check.notNull(quad, "Quad cannot be null");
+
+ // Add upper-left triangle
+ addPoint(quad.xr, quad.yt, quad.z);
+ addCoord(quad.sr, quad.tt);
+ addPoint(quad.xl, quad.yt, quad.z);
+ addCoord(quad.sl, quad.tt);
+ addPoint(quad.xl, quad.yb, quad.z);
+ addCoord(quad.sl, quad.tb);
+
+ // Add lower-right triangle
+ addPoint(quad.xr, quad.yt, quad.z);
+ addCoord(quad.sr, quad.tt);
+ addPoint(quad.xl, quad.yb, quad.z);
+ addCoord(quad.sl, quad.tb);
+ addPoint(quad.xr, quad.yb, quad.z);
+ addCoord(quad.sr, quad.tb);
+ }
+
+ @Override
+ protected void doFlush(/*@Nonnull*/ final GL gl) {
+
+ Check.notNull(gl, "GL cannot be null");
+
+ final GL3 gl3 = gl.getGL3();
+
+ // Upload data
+ rewind();
+ gl3.glBufferSubData(
+ GL3.GL_ARRAY_BUFFER, // target
+ 0, // offset
+ getSizeInBytes(), // size
+ getData()); // data
+
+ // Draw
+ gl3.glDrawArrays(
+ GL3.GL_TRIANGLES, // mode
+ 0, // first
+ getSizeInVertices()); // count
+ clear();
+ }
+
+ @Override
+ public void endRendering(/*@Nonnull*/ final GL gl) {
+
+ super.endRendering(gl);
+
+ final GL3 gl3 = gl.getGL3();
+
+ // Unbind the VBO and VAO
+ gl3.glBindBuffer(GL3.GL_ARRAY_BUFFER, 0);
+ gl3.glBindVertexArray(0);
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/awt/text/QuadPipelines.java b/src/jogl/classes/jogamp/opengl/util/awt/text/QuadPipelines.java
new file mode 100644
index 0000000000..9fcab42d09
--- /dev/null
+++ b/src/jogl/classes/jogamp/opengl/util/awt/text/QuadPipelines.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2012 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ *
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package jogamp.opengl.util.awt.text;
+
+import com.jogamp.opengl.GL;
+import com.jogamp.opengl.GL2;
+import com.jogamp.opengl.GL3;
+import com.jogamp.opengl.GLExtensions;
+import com.jogamp.opengl.GLProfile;
+
+
+/**
+ * Utility for working with {@link QuadPipeline}'s.
+ */
+/*ThreadSafe*/
+public final class QuadPipelines {
+
+ /**
+ * Prevents instantiation.
+ */
+ private QuadPipelines() {
+ // pass
+ }
+
+ /**
+ * Creates a {@link QuadPipeline} based on the current OpenGL context.
+ *
+ * @param gl Current OpenGL context
+ * @param program Shader program to use, or zero to use default
+ * @return New quad pipeline for the version of OpenGL in use, not null
+ * @throws NullPointerException if context is null
+ * @throws IllegalArgumentException if shader program is negative
+ * @throws UnsupportedOperationException if GL is unsupported
+ */
+ /*@Nonnull*/
+ public QuadPipeline get(/*@Nonnull*/ final GL gl,
+ /*@Nonnegative*/ final int program) {
+
+ Check.notNull(gl, "Context cannot be null");
+ Check.argument(program >= 0, "Program cannot be negative");
+
+ final GLProfile profile = gl.getGLProfile();
+
+ if (profile.isGL3()) {
+ final GL3 gl3 = gl.getGL3();
+ return new QuadPipelineGL30(gl3, program);
+ } else if (profile.isGL2()) {
+ final GL2 gl2 = gl.getGL2();
+ if (gl2.isExtensionAvailable(GLExtensions.VERSION_1_5)) {
+ return new QuadPipelineGL15(gl2);
+ } else if (gl2.isExtensionAvailable("GL_VERSION_1_1")) {
+ return new QuadPipelineGL11();
+ } else {
+ return new QuadPipelineGL10();
+ }
+ } else {
+ throw new UnsupportedOperationException("Profile currently unsupported");
+ }
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/awt/text/ShaderLoader.java b/src/jogl/classes/jogamp/opengl/util/awt/text/ShaderLoader.java
new file mode 100644
index 0000000000..f6abcee817
--- /dev/null
+++ b/src/jogl/classes/jogamp/opengl/util/awt/text/ShaderLoader.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2012 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ *
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package jogamp.opengl.util.awt.text;
+
+import com.jogamp.opengl.GL2ES2;
+import com.jogamp.opengl.GLException;
+import com.jogamp.opengl.util.glsl.ShaderUtil;
+
+
+/**
+ * Utility to load shaders from files, URLs, and strings.
+ *
+ *
+ * {@code ShaderLoader} is a simple utility for loading shaders. It takes shaders directly as
+ * strings. It will create and compile the shaders, and link them together into a program. Both
+ * compiling and linking are verified. If a problem occurs a {@link GLException} is thrown with
+ * the appropriate log attached.
+ *
+ *
+ * Note it is highly recommended that if the developer passes the strings directly to {@code
+ * ShaderLoader} that they contain newlines. That way if any errors do occur their line numbers
+ * will be reported correctly. This means that if the shader is to be embedded in Java code, a
+ * "\n" should be appended to every line.
+ */
+/*@VisibleForTesting*/
+/*@ThreadSafe*/
+public final class ShaderLoader {
+
+ /**
+ * Prevents instantiation.
+ */
+ private ShaderLoader() {
+ // empty
+ }
+
+ /**
+ * Checks that a shader was compiled correctly.
+ *
+ * @param gl OpenGL context, assumed not null
+ * @param shader OpenGL handle to a shader
+ * @return True if shader was compiled without errors
+ */
+ private static boolean isShaderCompiled(/*@Nonnull*/ final GL2ES2 gl, final int shader) {
+ return ShaderUtil.isShaderStatusValid(gl, shader, GL2ES2.GL_COMPILE_STATUS, null);
+ }
+
+ /**
+ * Checks that a shader program was linked successfully.
+ *
+ * @param gl OpenGL context, assumed not null
+ * @param program OpenGL handle to a shader program
+ * @return True if program was linked successfully
+ */
+ private static boolean isProgramLinked(/*@Nonnull*/ final GL2ES2 gl, final int program) {
+ return ShaderUtil.isProgramStatusValid(gl, program, GL2ES2.GL_LINK_STATUS);
+ }
+
+ /**
+ * Checks that a shader program was validated successfully.
+ *
+ * @param gl OpenGL context, assumed not null
+ * @param program OpenGL handle to a shader program
+ * @return True if program was validated successfully
+ */
+ private static boolean isProgramValidated(/*@Nonnull*/ final GL2ES2 gl, final int program) {
+ return ShaderUtil.isProgramStatusValid(gl, program, GL2ES2.GL_VALIDATE_STATUS);
+ }
+
+ /**
+ * Loads a shader program from a pair of strings.
+ *
+ * @param gl Current OpenGL context
+ * @param vss Vertex shader source
+ * @param fss Fragment shader source
+ * @return OpenGL handle to the shader program, not negative
+ * @throws NullPointerException if context or either source is null
+ * @throws IllegalArgumentException if either source is empty
+ * @throws GLException if program did not compile, link, or validate successfully
+ */
+ /*@Nonnegative*/
+ public static int loadProgram(/*@Nonnull*/ final GL2ES2 gl,
+ /*@Nonnull*/ final String vss,
+ /*@Nonnull*/ final String fss) {
+
+ Check.notNull(gl, "GL cannot be null");
+ Check.notNull(vss, "Vertex shader source cannot be null");
+ Check.notNull(fss, "Fragment shader source cannot be null");
+ Check.argument(!vss.isEmpty(), "Vertex shader source cannot be empty");
+ Check.argument(!fss.isEmpty(), "Fragment shader source cannot be empty");
+
+ // Create the shaders
+ final int vs = loadShader(gl, vss, GL2ES2.GL_VERTEX_SHADER);
+ final int fs = loadShader(gl, fss, GL2ES2.GL_FRAGMENT_SHADER);
+
+ // Create a program and attach the shaders
+ final int program = gl.glCreateProgram();
+ gl.glAttachShader(program, vs);
+ gl.glAttachShader(program, fs);
+
+ // Link and validate the program
+ gl.glLinkProgram(program);
+ gl.glValidateProgram(program);
+ if ((!isProgramLinked(gl, program)) || (!isProgramValidated(gl, program))) {
+ final String log = ShaderUtil.getProgramInfoLog(gl, program);
+ throw new GLException(log);
+ }
+
+ // Clean up the shaders
+ gl.glDeleteShader(vs);
+ gl.glDeleteShader(fs);
+
+ return program;
+ }
+
+ /**
+ * Loads a shader from a string.
+ *
+ * @param gl Current OpenGL context, assumed not null
+ * @param source Source code of the shader as one long string, assumed not null or empty
+ * @param type Type of shader, assumed valid
+ * @return OpenGL handle to the shader, not negative
+ * @throws GLException if a GLSL-capable context is not active or could not compile shader
+ */
+ /*@Nonnegative*/
+ private static int loadShader(/*@Nonnull*/ final GL2ES2 gl,
+ /*@Nonnull*/ final String source,
+ final int type) {
+
+ // Create and read source
+ final int shader = gl.glCreateShader(type);
+ gl.glShaderSource(
+ shader, // shader handle
+ 1, // number of strings
+ new String[] { source }, // array of strings
+ null); // lengths of strings
+
+ // Compile
+ gl.glCompileShader(shader);
+ if (!isShaderCompiled(gl, shader)) {
+ final String log = ShaderUtil.getShaderInfoLog(gl, shader);
+ throw new GLException(log);
+ }
+
+ return shader;
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/awt/text/Texture.java b/src/jogl/classes/jogamp/opengl/util/awt/text/Texture.java
new file mode 100644
index 0000000000..5ff3283d00
--- /dev/null
+++ b/src/jogl/classes/jogamp/opengl/util/awt/text/Texture.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2012 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ *
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package jogamp.opengl.util.awt.text;
+
+import com.jogamp.opengl.GL;
+import com.jogamp.opengl.GL3;
+
+
+/**
+ * OpenGL texture.
+ */
+abstract class Texture {
+
+ /**
+ * ID of internal OpenGL texture.
+ */
+ /*@Nonnegative*/
+ protected final int handle;
+
+ /**
+ * {@code GL_TEXTURE2D}, etc.
+ */
+ protected final int type;
+
+ /**
+ * True for quality texturing.
+ */
+ protected final boolean mipmap;
+
+ /**
+ * Constructs a {@link Texture}.
+ *
+ * @param gl Current OpenGL context
+ * @param type Type of texture
+ * @param mipmap True for quality texturing
+ * @throws NullPointerException if context is null
+ * @throws IllegalArgumentException if type is invalid
+ */
+ Texture(/*@Nonnull*/ final GL gl, final int type, final boolean mipmap) {
+
+ Check.notNull(gl, "GL cannot be null");
+ Check.argument(isValidTextureType(type), "Texture type is invalid");
+
+ this.handle = generate(gl);
+ this.type = type;
+ this.mipmap = mipmap;
+ }
+
+ /**
+ * Binds underlying OpenGL texture on a texture unit.
+ *
+ * @param gl Current OpenGL context
+ * @param unit OpenGL enumeration for a texture unit, i.e., {@code GL_TEXTURE0}
+ * @throws NullPointerException if context is null
+ * @throws IllegalArgumentException if unit is invalid
+ */
+ void bind(/*@Nonnull*/ final GL gl, final int unit) {
+
+ Check.notNull(gl, "GL cannot be null");
+ Check.argument(isValidTextureUnit(unit), "Texture unit is invalid");
+
+ gl.glActiveTexture(unit);
+ gl.glBindTexture(type, handle);
+ }
+
+ /**
+ * Destroys the texture.
+ *
+ * @param gl Current OpenGL context
+ * @throws NullPointerException if context is null
+ */
+ void dispose(/*@Nonnull*/ final GL gl) {
+
+ Check.notNull(gl, "GL cannot be null");
+
+ final int[] handles = new int[] { handle };
+ gl.glDeleteTextures(1, handles, 0);
+ }
+
+ /**
+ * Generates an OpenGL texture object.
+ *
+ * @param gl Current OpenGL context, assumed not null
+ * @return Handle to the OpenGL texture
+ */
+ private static int generate(/*@Nonnull*/ final GL gl) {
+ final int[] handles = new int[1];
+ gl.glGenTextures(1, handles, 0);
+ return handles[0];
+ }
+
+ /**
+ * Checks if an integer is a valid OpenGL enumeration for a texture type.
+ *
+ * @param type Integer to check
+ * @return True if type is valid
+ */
+ private static boolean isValidTextureType(final int type) {
+ switch (type) {
+ case GL3.GL_TEXTURE_1D:
+ case GL3.GL_TEXTURE_2D:
+ case GL3.GL_TEXTURE_3D:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Checks if an integer is a valid OpenGL enumeration for a texture unit.
+ *
+ * @param unit Integer to check
+ * @return True if unit is valid
+ */
+ private static boolean isValidTextureUnit(final int unit) {
+ return (unit >= GL.GL_TEXTURE0) && (unit <= GL.GL_TEXTURE31);
+ }
+
+ /**
+ * Updates filter parameters for the texture.
+ *
+ * @param gl Current OpenGL context
+ * @param smooth True to interpolate samples
+ * @throws NullPointerException if context is null
+ */
+ void setFiltering(/*@Nonnull*/ final GL gl, final boolean smooth) {
+
+ Check.notNull(gl, "GL cannot be null");
+
+ final int mag;
+ final int min;
+ if (smooth) {
+ mag = GL.GL_LINEAR;
+ min = mipmap ? GL.GL_LINEAR_MIPMAP_NEAREST : GL.GL_LINEAR;
+ } else {
+ mag = GL.GL_NEAREST;
+ min = mipmap ? GL.GL_NEAREST_MIPMAP_NEAREST : GL.GL_NEAREST;
+ }
+
+ setParameter(gl, GL.GL_TEXTURE_MAG_FILTER, mag);
+ setParameter(gl, GL.GL_TEXTURE_MIN_FILTER, min);
+ }
+
+ /**
+ * Changes a texture parameter for a 2D texture.
+ *
+ * @param gl Current OpenGL context, assumed not null
+ * @param name Name of the parameter, assumed valid
+ * @param value Value of the parameter, assumed valid
+ */
+ private void setParameter(/*@Nonnull*/ final GL gl, final int name, final int value) {
+ gl.glTexParameteri(type, name, value);
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/awt/text/Texture2D.java b/src/jogl/classes/jogamp/opengl/util/awt/text/Texture2D.java
new file mode 100644
index 0000000000..08ac0442ff
--- /dev/null
+++ b/src/jogl/classes/jogamp/opengl/util/awt/text/Texture2D.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2012 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ *
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package jogamp.opengl.util.awt.text;
+
+import com.jogamp.opengl.GL;
+import com.jogamp.opengl.GL2;
+
+import java.awt.Rectangle;
+import java.nio.ByteBuffer;
+
+
+/**
+ * Two-dimensional OpenGL texture.
+ */
+abstract class Texture2D extends Texture {
+
+ // Size on X axis
+ /*@Nonnegative*/
+ protected final int width;
+
+ // Size on Y axis
+ /*@Nonnegative*/
+ protected final int height;
+
+ /**
+ * Creates a 2D texture.
+ *
+ * @param gl Current OpenGL context
+ * @param width Size of texture on X axis
+ * @param height Size of texture on Y axis
+ * @param smooth True to interpolate samples
+ * @param mipmap True for high quality
+ * @throws NullPointerException if context is null
+ * @throws IllegalArgumentException if width or height is negative
+ */
+ Texture2D(/*@Nonnull*/ final GL gl,
+ /*@Nonnegative*/ final int width,
+ /*@Nonnegative*/ final int height,
+ final boolean smooth,
+ final boolean mipmap) {
+
+ super(gl, GL.GL_TEXTURE_2D, mipmap);
+
+ Check.argument(width >= 0, "Width cannot be negative");
+ Check.argument(height >= 0, "Height cannot be negative");
+
+ // Copy parameters
+ this.width = width;
+ this.height = height;
+
+ // Set up
+ bind(gl, GL.GL_TEXTURE0);
+ allocate(gl);
+ setFiltering(gl, smooth);
+ }
+
+ /**
+ * Allocates a 2D texture for use with a backing store.
+ *
+ * @param gl Current OpenGL context, assumed not null
+ * @param width Width of texture, assumed not negative
+ * @param height Height of texture, assumed not negative
+ */
+ private void allocate(/*@Nonnull*/ final GL gl) {
+ gl.glTexImage2D(
+ GL.GL_TEXTURE_2D, // target
+ 0, // level
+ getInternalFormat(gl), // internal format
+ width, // width
+ height, // height
+ 0, // border
+ GL.GL_RGB, // format (unused)
+ GL.GL_UNSIGNED_BYTE, // type (unused)
+ null); // pixels
+ }
+
+ /**
+ * Determines the proper texture format for an OpenGL context.
+ *
+ * @param gl Current OpenGL context
+ * @return Texture format enumeration for OpenGL context
+ * @throws NullPointerException if context is null (optional)
+ */
+ protected abstract int getFormat(/*@Nonnull*/ GL gl);
+
+ /**
+ * Determines the proper internal texture format for an OpenGL context.
+ *
+ * @param gl Current OpenGL context
+ * @return Internal texture format enumeration for OpenGL context
+ * @throws NullPointerException if context is null (optional)
+ */
+ protected abstract int getInternalFormat(/*@Nonnull*/ GL gl);
+
+ /**
+ * Updates the texture.
+ *
+ *
+ * Copies any areas marked with {@link #mark(int, int, int, int)} from the local image to the
+ * OpenGL texture. Only those areas will be modified.
+ *
+ * @param gl Current OpenGL context
+ * @param pixels Data of entire image
+ * @param area Region to update
+ * @throws NullPointerException if context, pixels, or area is null
+ */
+ void update(/*@Nonnull*/ final GL gl,
+ /*@Nonnull*/ final ByteBuffer pixels,
+ /*@Nonnull*/ final Rectangle area) {
+
+ Check.notNull(gl, "GL cannot be null");
+ Check.notNull(pixels, "Pixels cannot be null");
+ Check.notNull(area, "Area cannot be null");
+
+ final int parameters[] = new int[4];
+
+ // Store unpack parameters
+ gl.glGetIntegerv(GL.GL_UNPACK_ALIGNMENT, parameters, 0);
+ gl.glGetIntegerv(GL2.GL_UNPACK_SKIP_ROWS, parameters, 1);
+ gl.glGetIntegerv(GL2.GL_UNPACK_SKIP_PIXELS, parameters, 2);
+ gl.glGetIntegerv(GL2.GL_UNPACK_ROW_LENGTH, parameters, 3);
+
+ // Change unpack parameters
+ gl.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1);
+ gl.glPixelStorei(GL2.GL_UNPACK_SKIP_ROWS, area.y);
+ gl.glPixelStorei(GL2.GL_UNPACK_SKIP_PIXELS, area.x);
+ gl.glPixelStorei(GL2.GL_UNPACK_ROW_LENGTH, width);
+
+ // Update the texture
+ gl.glTexSubImage2D(
+ GL.GL_TEXTURE_2D, // target
+ 0, // mipmap level
+ area.x, // x offset
+ area.y, // y offset
+ area.width, // width
+ area.height, // height
+ getFormat(gl), // format
+ GL.GL_UNSIGNED_BYTE, // type
+ pixels); // pixels
+
+ // Reset unpack parameters
+ gl.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, parameters[0]);
+ gl.glPixelStorei(GL2.GL_UNPACK_SKIP_ROWS, parameters[1]);
+ gl.glPixelStorei(GL2.GL_UNPACK_SKIP_PIXELS, parameters[2]);
+ gl.glPixelStorei(GL2.GL_UNPACK_ROW_LENGTH, parameters[3]);
+
+ // Generate mipmaps
+ if (mipmap) {
+ gl.glGenerateMipmap(GL.GL_TEXTURE_2D);
+ }
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/awt/text/TextureBackingStore.java b/src/jogl/classes/jogamp/opengl/util/awt/text/TextureBackingStore.java
new file mode 100644
index 0000000000..67a317b713
--- /dev/null
+++ b/src/jogl/classes/jogamp/opengl/util/awt/text/TextureBackingStore.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright 2012 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ *
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package jogamp.opengl.util.awt.text;
+
+import com.jogamp.opengl.GL;
+
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferByte;
+import java.nio.ByteBuffer;
+
+
+/**
+ * Wrapper for an OpenGL texture that can be drawn into.
+ *
+ *
+ * {@code TextureBackingStore} provides the ability to draw into a grayscale texture using Java 2D.
+ * To increase performance, the backing store maintains a local copy as a {@link BufferedImage}.
+ * Changes are applied to the image first and then pushed to the texture all at once.
+ *
+ *
+ * After creating a backing store, a client simply needs to grab its {@link Graphics2D} and use its
+ * AWT or Java 2D drawing methods. Then the area that was drawn to should be noted with the {@link
+ * #mark(int, int, int, int)} method. After everything is drawn, activate the texture using {@link
+ * #bind(GL, int)} and call {@link #update(GL)} to actually push the dirty regions to the texture.
+ * If further changes need to made, consider using {@link #clear(int, int, int, int)} to erase old
+ * data.
+ *
+ *
+ * Note that since texturing hasn't changed much, BackingStore is compatible with GL2 or GL3. For
+ * that reason, it only requests simple GL objects.
+ */
+/*@NotThreadSafe*/
+final class TextureBackingStore {
+
+ /**
+ * Size in X direction.
+ */
+ /*@Nonnegative*/
+ private final int width;
+
+ /**
+ * Size in Y direction.
+ */
+ /*@Nonnegative*/
+ private final int height;
+
+ /**
+ * Local copy of texture.
+ */
+ /*@Nonnull*/
+ private final BufferedImage image;
+
+ /**
+ * Java2D utility for drawing into image.
+ */
+ /*@Nonnull*/
+ private final Graphics2D g2d;
+
+ /**
+ * Raw image data for pushing to texture.
+ */
+ /*@Nonnull*/
+ private final ByteBuffer pixels;
+
+ /**
+ * True for quality texturing.
+ */
+ /*@Nonnull*/
+ private final boolean mipmap;
+
+ /**
+ * OpenGL texture on video card.
+ */
+ /*@CheckForNull*/
+ private Texture2D texture = null;
+
+ /**
+ * Area in image not pushed to texture.
+ */
+ /*@CheckForNull*/
+ private Rectangle dirtyRegion = null;
+
+ /**
+ * True to interpolate samples.
+ */
+ private boolean smooth;
+
+ /**
+ * True if interpolation has changed.
+ */
+ private boolean smoothChanged = false;
+
+ /**
+ * Constructs a {@link TextureBackingStore}.
+ *
+ * @param width Width of backing store
+ * @param height Height of backing store
+ * @param font Style of text
+ * @param antialias True to render smooth edges
+ * @param subpixel True to use subpixel accuracy
+ * @param smooth True to interpolate samples
+ * @param mipmap True for quality texturing
+ * @throws IllegalArgumentException if width or height is negative
+ * @throws NullPointerException if font is null
+ */
+ TextureBackingStore(/*@Nonnegative*/ final int width,
+ /*@Nonnegative*/ final int height,
+ /*@Nonnull*/ final Font font,
+ final boolean antialias,
+ final boolean subpixel,
+ final boolean smooth,
+ final boolean mipmap) {
+
+ Check.argument(width >= 0, "Width cannot be negative");
+ Check.argument(height >= 0, "Height cannot be negative");
+ Check.notNull(font, "Font cannot be null");
+
+ this.width = width;
+ this.height = height;
+ this.image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
+ this.g2d = createGraphics(image, font, antialias, subpixel);
+ this.pixels = getPixels(image);
+ this.mipmap = mipmap;
+ this.smooth = smooth;
+ }
+
+ /**
+ * Binds the underlying OpenGL texture on a texture unit.
+ *
+ * @param gl Current OpenGL context
+ * @param unit OpenGL enumeration for a texture unit (e.g., {@code GL_TEXTURE0})
+ * @throws NullPointerException if context is null
+ * @throws IllegalArgumentException if unit is invalid
+ */
+ void bind(/*@Nonnull*/ final GL gl, final int unit) {
+
+ Check.notNull(gl, "GL cannot be null");
+ Check.argument(unit >= GL.GL_TEXTURE0, "Unit is invalid");
+
+ ensureTexture(gl);
+ texture.bind(gl, unit);
+ }
+
+ /**
+ * Clears out an area in the backing store.
+ *
+ * @param x Position of area's left edge
+ * @param y Position of area's top edge
+ * @param width Width of area
+ * @param height Height of area
+ * @throws IllegalArgumentException if x, y, width, or height is negative
+ */
+ void clear(/*@Nonnegative*/ final int x,
+ /*@Nonnegative*/ final int y,
+ /*@Nonnegative*/ final int width,
+ /*@Nonnegative*/ final int height) {
+
+ Check.argument(x >= 0, "X cannot be negative");
+ Check.argument(y >= 0, "Y cannot be negative");
+ Check.argument(width >= 0, "Width cannot be negative");
+ Check.argument(height >= 0, "Height cannot be negative");
+
+ g2d.setComposite(AlphaComposite.Clear);
+ g2d.fillRect(x, y, width, height);
+ g2d.setComposite(AlphaComposite.Src);
+ }
+
+ /**
+ * Creates a graphics for a backing store.
+ *
+ * @param image Backing store's local copy of data, assumed not null
+ * @param font Style of text, assumed not null
+ * @param antialias True to smooth edges
+ * @param subpixel True to use subpixel accuracy
+ * @return Graphics2D for rendering into image, not null
+ */
+ /*@Nonnull*/
+ private static Graphics2D createGraphics(/*@Nonnull*/ final BufferedImage image,
+ /*@Nonnull*/ final Font font,
+ final boolean antialias,
+ final boolean subpixel) {
+
+ final Graphics2D g2d = image.createGraphics();
+
+ g2d.setComposite(AlphaComposite.Src);
+ g2d.setColor(Color.WHITE);
+ g2d.setFont(font);
+ g2d.setRenderingHint(
+ RenderingHints.KEY_TEXT_ANTIALIASING,
+ antialias ?
+ RenderingHints.VALUE_TEXT_ANTIALIAS_ON :
+ RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
+ g2d.setRenderingHint(
+ RenderingHints.KEY_FRACTIONALMETRICS,
+ subpixel ?
+ RenderingHints.VALUE_FRACTIONALMETRICS_ON :
+ RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
+ return g2d;
+ }
+
+ /**
+ * Releases resources used by the backing store.
+ *
+ * @param gl Current OpenGL context
+ * @throws NullPointerException if context is null
+ */
+ void dispose(/*@Nonnull*/ final GL gl) {
+
+ Check.notNull(gl, "GL cannot be null");
+
+ // Dispose of image
+ if (image != null) {
+ image.flush();
+ }
+
+ // Dispose of texture
+ if (texture != null) {
+ texture.dispose(gl);
+ }
+ }
+
+ /**
+ * Makes sure the texture has been created.
+ *
+ * @param gl Current OpenGL context, assumed not null
+ */
+ private void ensureTexture(/*@Nonnull*/ final GL gl) {
+ if (texture == null) {
+ texture = new GrayTexture2D(gl, width, height, smooth, mipmap);
+ }
+ }
+
+ /**
+ * Returns Java2D Graphics2D object for drawing into this store.
+ *
+ * @return Java2D graphics for drawing into this store, not null
+ */
+ /*@Nonnull*/
+ final Graphics2D getGraphics() {
+ return g2d;
+ }
+
+ /**
+ * Returns height of the underlying image and texture.
+ *
+ * @return Height of the underlying image, not negative
+ */
+ /*@Nonnegative*/
+ final int getHeight() {
+ return height;
+ }
+
+ /**
+ * Returns local copy of texture.
+ *
+ * @return Local copy of texture, not null
+ */
+ /*@Nonnull*/
+ final BufferedImage getImage() {
+ return image;
+ }
+
+ /**
+ * Retrieves the underlying pixels of a buffered image.
+ *
+ * @param image Image with underlying pixel buffer, assumed not null
+ * @return Pixel data of the image as a byte buffer, not null
+ * @throws IllegalStateException if image is not stored as bytes
+ */
+ /*@Nonnull*/
+ private static ByteBuffer getPixels(/*@Nonnull*/ final BufferedImage image) {
+
+ final DataBuffer db = image.getRaster().getDataBuffer();
+ final byte[] arr;
+
+ if (db instanceof DataBufferByte) {
+ arr = ((DataBufferByte) db).getData();
+ } else {
+ throw new IllegalStateException("Unexpected format in image.");
+ }
+ return ByteBuffer.wrap(arr);
+ }
+
+ /**
+ * Returns true if texture is interpolating samples.
+ *
+ * @return True if texture is interpolating samples
+ */
+ final boolean getUseSmoothing() {
+ return smooth;
+ }
+
+ /**
+ * Returns width of the underlying image and texture.
+ *
+ * @return Width of the underlying image, not negative
+ */
+ /*@Nonnegative*/
+ final int getWidth() {
+ return width;
+ }
+
+ /**
+ * Marks an area of the backing store to be updated.
+ *
+ *
+ * The next time the backing store is updated, the area will be pushed to the texture.
+ *
+ * @param x Position of area's left edge
+ * @param y Position of area's top edge
+ * @param width Width of area
+ * @param height Height of area
+ * @throws IllegalArgumentException if x, y, width, or height is negative
+ */
+ void mark(/*@Nonnegative*/ final int x,
+ /*@Nonnegative*/ final int y,
+ /*@Nonnegative*/ final int width,
+ /*@Nonnegative*/ final int height) {
+
+ Check.argument(x >= 0, "X cannot be negative");
+ Check.argument(y >= 0, "Y cannot be negative");
+ Check.argument(width >= 0, "Width cannot be negative");
+ Check.argument(height >= 0, "Height cannot be negative");
+
+ final Rectangle region = new Rectangle(x, y, width, height);
+ if (dirtyRegion == null) {
+ dirtyRegion = region;
+ } else {
+ dirtyRegion.add(region);
+ }
+ }
+
+ /**
+ * Specifies whether the texture should interpolate samples.
+ */
+ final void setUseSmoothing(final boolean useSmoothing) {
+ smoothChanged = (this.smooth != useSmoothing);
+ this.smooth = useSmoothing;
+ }
+
+ /**
+ * Uploads any recently drawn data to the texture.
+ *
+ * @param gl Current OpenGL context
+ * @throws NullPointerException if context is null
+ */
+ void update(/*@Nonnull*/ final GL gl) {
+
+ Check.notNull(gl, "GL cannot be null");
+
+ // Make sure texture is created
+ ensureTexture(gl);
+
+ // Check smoothing
+ if (smoothChanged) {
+ texture.setFiltering(gl, smooth);
+ smoothChanged = false;
+ }
+
+ // Check texture
+ if (dirtyRegion != null) {
+ texture.update(gl, pixels, dirtyRegion);
+ dirtyRegion = null;
+ }
+ }
+
+ /**
+ * Observer of texture backing store events.
+ */
+ interface EventListener {
+
+ /**
+ * Responds to an event from a texture backing store.
+ *
+ * @param type Type of event
+ * @throws NullPointerException if event type is null (optional)
+ */
+ public void onBackingStoreEvent(/*@Nonnull*/ EventType type);
+ }
+
+ /**
+ * Type of event fired from the backing store.
+ */
+ enum EventType {
+
+ /**
+ * Backing store being resized.
+ */
+ REALLOCATE,
+
+ /**
+ * Backing store could not be resized.
+ */
+ FAILURE;
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/awt/text/TextureBackingStoreManager.java b/src/jogl/classes/jogamp/opengl/util/awt/text/TextureBackingStoreManager.java
new file mode 100644
index 0000000000..f08b09a8de
--- /dev/null
+++ b/src/jogl/classes/jogamp/opengl/util/awt/text/TextureBackingStoreManager.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright 2012 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ *
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package jogamp.opengl.util.awt.text;
+
+import com.jogamp.opengl.GL;
+import com.jogamp.opengl.GLContext;
+import com.jogamp.opengl.util.packrect.BackingStoreManager;
+import com.jogamp.opengl.util.packrect.Rect;
+
+import java.awt.Font;
+import java.util.ArrayList;
+import java.util.List;
+
+import jogamp.opengl.util.awt.text.TextureBackingStore.EventListener;
+import jogamp.opengl.util.awt.text.TextureBackingStore.EventType;
+
+
+/**
+ * Handler for allocating and reallocating texture backing stores.
+ *
+ *
+ * When a backing store is no longer big enough a new backing store needs to be created to replace
+ * it. Accordingly, the data from the old backing store should be copied to the new one.
+ *
+ *
+ * Throughout this process decisions may need to be made about getting rid of old entries and
+ * handling failures. {@code TextureBackingStoreManager} handles these issues, although it
+ * delegates some actions to observers by firing backing store events.
+ */
+final class TextureBackingStoreManager implements BackingStoreManager {
+
+ /**
+ * Whether or not texture backing store manager should print debugging information.
+ */
+ private static final boolean DEBUG = false;
+
+ /**
+ * Observers of backing store events.
+ */
+ /*@Nonnull*/
+ private final List
+ * Will happen if the backing store ever reaches its maximum size, which in this case is
+ * dictated by the maximum texture size supported by the video card. Fires an event of type
+ * {@link EventType.FAILURE} so that an observer of the backing store can decide what to
+ * actually do.
+ *
+ * @param cause Rectangle that could not be added
+ * @param attempt Number of times it has been tried so far
+ * @return False if can do nothing more to free space
+ * @throws NullPointerException if cause is null
+ */
+ @Override
+ public boolean additionFailed(/*@Nonnull*/ final Rect cause, final int attempt) {
+
+ Check.notNull(cause, "Cause cannot be null");
+
+ // Print debugging information
+ if (DEBUG) {
+ System.err.println("*** Addition failed! ***");
+ }
+
+ // Pass event to observers
+ fireEvent(EventType.FAILURE);
+ if (attempt == 0) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Adds an object that wants to be notified of events.
+ *
+ * The observer will be notified when:
+ *
+ *
+ * Happens immediately before a backing store needs to be expanded, since the manager will
+ * actually make a new one.
+ *
+ * @param bs Backing store being deleted
+ * @throws NullPointerException if backing store is null
+ * @throws ClassCastException if backing store is not a {@code TextureBackingStore}
+ */
+ @Override
+ public void deleteBackingStore(/*@Nonnull*/ final Object bs) {
+
+ Check.notNull(bs, "Backing store cannot be null");
+
+ // Dispose the backing store
+ final GL gl = GLContext.getCurrentGL();
+ final TextureBackingStore tbs = (TextureBackingStore) bs;
+ tbs.dispose(gl);
+ }
+
+ /**
+ * Finishes a copy from an old backing store to a new one.
+ *
+ *
+ * Marks all of the new backing store dirty. The next time it is updated all of the new data
+ * will be copied to the texture.
+ *
+ * @param obs Backing store being copied from
+ * @param nbs Backing store being copied to
+ * @throws NullPointerException if new backing store is null
+ * @throws ClassCastException if new backing store is not a {@code TextureBackingStore}
+ */
+ @Override
+ public void endMovement(final Object obs, /*@Nonnull*/ final Object nbs) {
+
+ Check.notNull(nbs, "Backing store cannot be null");
+
+ // Mark the entire backing store as dirty
+ final TextureBackingStore ntbs = (TextureBackingStore) nbs;
+ final int width = ntbs.getWidth();
+ final int height = ntbs.getHeight();
+ ntbs.mark(0, 0, width, height);
+ }
+
+ /**
+ * Sends an event to all listeners.
+ *
+ * @param type Type of event to send, assumed not null
+ */
+ private void fireEvent(/*@Nonnull*/ final EventType type) {
+ for (final EventListener listener : listeners) {
+ assert listener != null : "addListener rejects null";
+ listener.onBackingStoreEvent(type);
+ }
+ }
+
+ /**
+ * Returns true if is interpolating samples.
+ */
+ final boolean getUseSmoothing() {
+ return smooth;
+ }
+
+ /**
+ * Copies part of an old backing store to a new one.
+ *
+ *
+ * This method is normally called when a backing store runs out of room and needs to be
+ * resized, but it can also be called when a backing store is compacted. In that case {@code
+ * obs} will be equal to {@code nbs}. This situation may need to be handled differently.
+ *
+ * @param obs Old backing store being copied from
+ * @param ol Area of old backing store to copy
+ * @param nbs New backing store being copied to
+ * @param nl Area of new backing store to copy to
+ * @throws NullPointerException if either backing store or area is null
+ * @throws ClassCastException if either backing store is not the right type
+ */
+ @Override
+ public void move(/*@Nonnull*/ final Object obs,
+ /*@Nonnull*/ final Rect ol,
+ /*@Nonnull*/ final Object nbs,
+ /*@Nonnull*/ final Rect nl) {
+
+ Check.notNull(obs, "Old backing store cannot be null");
+ Check.notNull(ol, "Old location cannot be null");
+ Check.notNull(nbs, "New backing store cannot be null");
+ Check.notNull(nl, "New location cannot be null");
+
+ final TextureBackingStore otbs = (TextureBackingStore) obs;
+ final TextureBackingStore ntbs = (TextureBackingStore) nbs;
+
+ if (otbs == ntbs) {
+ otbs.getGraphics().copyArea(
+ ol.x(), ol.y(),
+ ol.w(), ol.h(),
+ nl.x() - ol.x(),
+ nl.y() - ol.y());
+ } else {
+ ntbs.getGraphics().drawImage(
+ otbs.getImage(),
+ nl.x(), nl.y(),
+ nl.x() + nl.w(),
+ nl.y() + nl.h(),
+ ol.x(), ol.y(),
+ ol.x() + ol.w(),
+ ol.y() + ol.h(),
+ null);
+ }
+ }
+
+ /**
+ * Performs an action when a store needs to be expanded.
+ *
+ *
+ * Fires an event of type {@link EventType.REALLOCATE} so that an observer of the backing store
+ * can decide what to actually do. This will only happen on the first attempt.
+ *
+ * @param cause Rectangle that is being added
+ * @param attempt Number of times it has been tried so far
+ * @return True if packer should retry addition
+ * @throws NullPointerException if cause is null
+ */
+ @Override
+ public boolean preExpand(/*@Nonnull*/ final Rect cause, final int attempt) {
+
+ Check.notNull(cause, "Cause cannot be null");
+
+ // Print debugging information
+ if (DEBUG) {
+ System.err.println("In preExpand: attempt number " + attempt);
+ }
+
+ // Pass event to observers
+ if (attempt == 0) {
+ fireEvent(EventType.REALLOCATE);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Changes whether texture should interpolate samples.
+ *
+ * @param useSmoothing True if texture should interpolate
+ */
+ final void setUseSmoothing(final boolean useSmoothing) {
+ this.smooth = useSmoothing;
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/awt/text/UnicodeGlyphProducer.java b/src/jogl/classes/jogamp/opengl/util/awt/text/UnicodeGlyphProducer.java
new file mode 100644
index 0000000000..e1a0e29fcf
--- /dev/null
+++ b/src/jogl/classes/jogamp/opengl/util/awt/text/UnicodeGlyphProducer.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2012 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ *
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package jogamp.opengl.util.awt.text;
+
+import com.jogamp.opengl.util.awt.TextRenderer.RenderDelegate;
+
+import java.awt.Font;
+import java.awt.font.FontRenderContext;
+import java.awt.font.GlyphVector;
+import java.util.List;
+
+
+/**
+ * {@link GlyphProducer} for creating glyphs of all characters in the basic unicode block.
+ */
+/*@NotThreadSafe*/
+final class UnicodeGlyphProducer extends AbstractGlyphProducer {
+
+ /**
+ * Storage for glyphs.
+ */
+ /*@Nonnull*/
+ private final GlyphMap glyphMap = new GlyphMap();
+
+ /**
+ * Constructs a {@link UnicodeGlyphProducer}.
+ *
+ * @param font Font glyphs will be made of
+ * @param rd Object for controlling rendering
+ * @param frc Details on how to render fonts
+ * @throws NullPointerException if font, render delegate, or font render context is null
+ */
+ UnicodeGlyphProducer(/*@Nonnull*/ final Font font,
+ /*@Nonnull*/ final RenderDelegate rd,
+ /*@Nonnull*/ final FontRenderContext frc) {
+ super(font, rd, frc);
+ }
+
+ @Override
+ public void clearGlyphs() {
+ glyphMap.clear();
+ }
+
+ /**
+ * Creates a single glyph from text with a complex layout.
+ *
+ * @param str Text with a complex layout
+ * @param gv Glyph vector of entire text
+ * @return Read-only pointer to list of glyphs valid until next call
+ * @throws NullPointerException if string is null
+ * @throws NullPointerException if glyph vector is null
+ */
+ /*@Nonnull*/
+ private List Performs the following:
+ * Results:
+ * Notes:
+ * Performs the following:
+ * Results:
+ * Notes:
+ * Performs the following:
+ * Results:
+ * Performs the following:
+ * Results:
+ * Performs the following:
+ * Results:
+ * Performs the following:
+ * Results:
+ * TestRunner makes using windows in unit tests easy by performing
+ * several tasks for the developer.
+ *
+ * Most importantly, it allows the developer to open a window and have JUnit
+ * wait for it to close. Normally this is not a concern with a normal
+ * main method because the JVM waits for the Swing thread to finish
+ * before exiting. However, the JUnit runner exits as soon it gets to the end
+ * of the test methods. Therefore windows in JUnit tests are closed
+ * immediately, or never show up in the first place.
+ *
+ * TestRunner solves the problem by providing a static run
+ * method that shows the frame and then calls
+ * wait on it. When a test launches the window in this manner, the JUnit
+ * thread pauses. Then when the frame is closed, instead of exiting, it
+ * hides itself and calls {@link notifyAll}. The paused JUnit thread therefore
+ * restarts, disposes of the frame, and continues onto the next test.
+ *
+ * Besides this important functionality, JTestFrame also
+ * Performs the following:
+ * Performs the following:
+ * TextRenderer(font, false,
- false).
-
- @param font the font to render with
- */
- public TextRenderer(final Font font) {
- this(font, false, false, null, false);
- }
+ /**
+ * True to print debugging information.
+ */
+ static final boolean DEBUG = false;
- /** Creates a new TextRenderer with the given font, using no
- antialiasing or fractional metrics, and the default
- RenderDelegate. If mipmap is true, attempts to use
- OpenGL's automatic mipmap generation for better smoothing when
- rendering the TextureRenderer's contents at a distance.
- Equivalent to TextRenderer(font, false, false).
-
- @param font the font to render with
- @param mipmap whether to attempt use of automatic mipmap generation
- */
- public TextRenderer(final Font font, final boolean mipmap) {
- this(font, false, false, null, mipmap);
- }
+ /**
+ * Common instance of the default render delegate.
+ */
+ /*@Nonnull*/
+ private static final RenderDelegate DEFAULT_RENDER_DELEGATE = new DefaultRenderDelegate();
- /** Creates a new TextRenderer with the given Font, specified font
- properties, and default RenderDelegate. The
- antialiased and useFractionalMetrics
- flags provide control over the same properties at the Java 2D
- level. No mipmap support is requested. Equivalent to
- TextRenderer(font, antialiased, useFractionalMetrics,
- null).
-
- @param font the font to render with
- @param antialiased whether to use antialiased fonts
- @param useFractionalMetrics whether to use fractional font
- metrics at the Java 2D level
- */
- public TextRenderer(final Font font, final boolean antialiased,
- final boolean useFractionalMetrics) {
- this(font, antialiased, useFractionalMetrics, null, false);
- }
+ /**
+ * Face, style, and size of text to render with.
+ */
+ /*@Nonnull*/
+ private final Font font;
- /** Creates a new TextRenderer with the given Font, specified font
- properties, and given RenderDelegate. The
- antialiased and useFractionalMetrics
- flags provide control over the same properties at the Java 2D
- level. The renderDelegate provides more control
- over the text rendered. No mipmap support is requested.
-
- @param font the font to render with
- @param antialiased whether to use antialiased fonts
- @param useFractionalMetrics whether to use fractional font
- metrics at the Java 2D level
- @param renderDelegate the render delegate to use to draw the
- text's bitmap, or null to use the default one
- */
- public TextRenderer(final Font font, final boolean antialiased,
- final boolean useFractionalMetrics, final RenderDelegate renderDelegate) {
- this(font, antialiased, useFractionalMetrics, renderDelegate, false);
- }
+ /**
+ * Delegate to store glyphs.
+ */
+ /*@Nonnull*/
+ private final GlyphCache glyphCache;
- /** Creates a new TextRenderer with the given Font, specified font
- properties, and given RenderDelegate. The
- antialiased and useFractionalMetrics
- flags provide control over the same properties at the Java 2D
- level. The renderDelegate provides more control
- over the text rendered. If mipmap is true, attempts
- to use OpenGL's automatic mipmap generation for better smoothing
- when rendering the TextureRenderer's contents at a distance.
-
- @param font the font to render with
- @param antialiased whether to use antialiased fonts
- @param useFractionalMetrics whether to use fractional font
- metrics at the Java 2D level
- @param renderDelegate the render delegate to use to draw the
- text's bitmap, or null to use the default one
- @param mipmap whether to attempt use of automatic mipmap generation
- */
- public TextRenderer(final Font font, final boolean antialiased,
- final boolean useFractionalMetrics, RenderDelegate renderDelegate,
- final boolean mipmap) {
- this.font = font;
- this.antialiased = antialiased;
- this.useFractionalMetrics = useFractionalMetrics;
- this.mipmap = mipmap;
+ /**
+ * Delegate to create glyphs.
+ */
+ /*@Nonnull*/
+ private final GlyphProducer glyphProducer;
- // FIXME: consider adjusting the size based on font size
- // (it will already automatically resize if necessary)
- packer = new RectanglePacker(new Manager(), kSize, kSize);
+ /**
+ * Delegate to draw glyphs.
+ */
+ /*@Nonnull*/
+ private final GlyphRenderer glyphRenderer = new GlyphRendererProxy();
- if (renderDelegate == null) {
- renderDelegate = new DefaultRenderDelegate();
- }
+ /**
+ * Mediator coordinating components.
+ */
+ /*@Nonnull*/
+ private final Mediator mediator = new Mediator();
- this.renderDelegate = renderDelegate;
+ /**
+ * True if this text renderer is ready to be used.
+ */
+ private boolean ready = false;
- mGlyphProducer = new GlyphProducer(font.getNumGlyphs());
+ /**
+ * Constructs a {@link TextRenderer}.
+ *
+ *
+ *
+ *
+ * Positioning
+ *
+ *
+ * +--------------------------------------+
+ * | top margin |
+ * | |
+ * | |------ WIDTH -------| |
+ * | - +--------------------+ |
+ * | | | top padding | |
+ * | | | l ________ r | |
+ * | l | | e / \ i | r |
+ * | e | f | | g | i |
+ * | f H | t | | h | g |
+ * | t E | | | t | h |
+ * | I | p | _____ | t |
+ * | m G | a | | p | |
+ * | a H | d | | a | m |
+ * | r T | d | | d | a |
+ * | g | i | | d | r |
+ * | i | | n | | i | g |
+ * | n | | g \________/ n | i |
+ * | | | g | n |
+ * | | | bottom padding | |
+ * | - +--------------------+ |
+ * | |
+ * | |
+ * | bottom margin |
+ * +--------------------------------------+
+ *
+ *
+ *
+ * +--------------------+ -
+ * | | |
+ * | ________ | |
+ * | / \ | |
+ * | | | | |
+ * | | | | |
+ * | | | | |
+ * | | _____ | | ascent
+ * | | | | |
+ * | | | | |
+ * | | | | |
+ * | | | | |
+ * | | | | |
+ * | \________/ | -
+ * | | |
+ * | | | descent
+ * +--------------------+ -
+ *
+ * |--| kerning
+ *
+ */
+/*@NotThreadSafe*/
+public final class Glyph {
+
+ // TODO: Create separate Glyph implementations -- one for character one for string?
+
+ /**
+ * Unicode ID if this glyph represents a single character, otherwise -1.
+ */
+ /*@CheckForSigned*/
+ final int id;
+
+ /**
+ * String if this glyph represents multiple characters, otherwise null.
+ */
+ /*@CheckForNull*/
+ final String str;
+
+ /**
+ * Font's identifier of glyph.
+ */
+ final int code;
+
+ /**
+ * Distance to next glyph.
+ */
+ final float advance;
+
+ /**
+ * Java2D shape of glyph.
+ */
+ /*@Nonnull*/
+ final GlyphVector glyphVector;
+
+ /**
+ * Actual character if this glyph represents a single character, otherwise NUL.
+ */
+ final char character;
+
+ /**
+ * Width of text with inner padding.
+ */
+ /*@VisibleForTesting*/
+ public float width;
+
+ /**
+ * Height of text with inner padding.
+ */
+ /*@VisibleForTesting*/
+ public float height;
+
+ /**
+ * Length from baseline to top border.
+ */
+ float ascent;
+
+ /**
+ * Length from baseline to bottom border.
+ */
+ float descent;
+
+ /**
+ * Length from baseline to left padding.
+ */
+ float kerning;
+
+ /**
+ * Outer boundary excluded from size.
+ */
+ /*@CheckForNull*/
+ Boundary margin;
+
+ /**
+ * Inner boundary included in size.
+ */
+ /*@CheckForNull*/
+ Boundary padding;
+
+ /**
+ * Position of this glyph in texture.
+ */
+ /*@CheckForNull*/
+ public Rect location;
+
+ /**
+ * Coordinates of this glyph in texture.
+ */
+ /*@CheckForNull*/
+ TextureCoords coordinates;
+
+ /**
+ * Cached bounding box of glyph.
+ */
+ /*@CheckForNull*/
+ Rectangle2D bounds;
+
+ /**
+ * Constructs a {@link Glyph} representing an individual Unicode character.
+ *
+ * @param id Unicode ID of character
+ * @param gv Vector shape of character
+ * @throws IllegalArgumentException if ID is negative
+ * @throws NullPointerException if glyph is null
+ */
+ public Glyph(/*@Nonnegative*/ final int id, /*@Nonnull*/ final GlyphVector gv) {
+
+ Check.argument(id >= 0, "ID cannot be negative");
+ Check.notNull(gv, "Glyph vector cannot be null");
+
+ this.id = id;
+ this.str = null;
+ this.code = gv.getGlyphCode(0);
+ this.advance = gv.getGlyphMetrics(0).getAdvance();
+ this.glyphVector = gv;
+ this.character = (char) id;
+ }
+
+ /**
+ * Constructs a {@link Glyph} representing a sequence of characters.
+ *
+ * @param str Sequence of characters
+ * @param gv Vector shape of sequence
+ * @throws NullPointerException if string or glyph vector is null
+ */
+ public Glyph(/*@Nonnull*/ final String str, /*@Nonnull*/ final GlyphVector gv) {
+
+ Check.notNull(str, "String cannot be null");
+ Check.notNull(gv, "Glyph vector cannot be null");
+
+ this.id = -1;
+ this.str = str;
+ this.code = -1;
+ this.advance = 0;
+ this.glyphVector = gv;
+ this.character = '\0';
+ }
+
+ /*@Nonnull*/
+ @Override
+ public String toString() {
+ return (str != null) ? str : Character.toString(character);
+ }
+
+ /**
+ * Space around a rectangle.
+ */
+ /*@Immutable*/
+ static final class Boundary {
+
+ /**
+ * Space above rectangle.
+ */
+ final int top;
+
+ /**
+ * Space below rectangle.
+ */
+ final int bottom;
+
+ /**
+ * Space beside rectangle to left.
+ */
+ final int left;
+
+ /**
+ * Space beside rectangle to right.
+ */
+ final int right;
+
+ /**
+ * Constructs a {@link Boundary} by computing the distances between two rectangles.
+ *
+ * @param large Outer rectangle
+ * @param small Inner rectangle
+ * @throws NullPointerException if either rectangle is null
+ */
+ Boundary(/*@Nonnull*/ final Rectangle2D large, /*@Nonnull*/ final Rectangle2D small) {
+
+ Check.notNull(large, "Large rectangle cannot be null");
+ Check.notNull(small, "Small rectangle cannot be null");
+
+ top = (int) (large.getMinY() - small.getMinY()) * -1;
+ left = (int) (large.getMinX() - small.getMinX()) * -1;
+ bottom = (int) (large.getMaxY() - small.getMaxY());
+ right = (int) (large.getMaxX() - small.getMaxX());
+ }
+ }
+}
diff --git a/src/jogl/classes/jogamp/opengl/util/awt/text/GlyphCache.java b/src/jogl/classes/jogamp/opengl/util/awt/text/GlyphCache.java
new file mode 100644
index 0000000000..853db75f20
--- /dev/null
+++ b/src/jogl/classes/jogamp/opengl/util/awt/text/GlyphCache.java
@@ -0,0 +1,855 @@
+/*
+ * Copyright 2012 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ *
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package jogamp.opengl.util.awt.text;
+
+import com.jogamp.opengl.GL;
+import com.jogamp.opengl.GLException;
+import com.jogamp.opengl.util.awt.TextRenderer.RenderDelegate;
+import com.jogamp.opengl.util.packrect.BackingStoreManager;
+import com.jogamp.opengl.util.packrect.Rect;
+import com.jogamp.opengl.util.packrect.RectVisitor;
+import com.jogamp.opengl.util.packrect.RectanglePacker;
+import com.jogamp.opengl.util.texture.TextureCoords;
+
+import java.awt.Font;
+import java.awt.font.FontRenderContext;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Storage of glyphs in an OpenGL texture.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * @param glyph Glyph already in cache
+ * @return Texture coordinates of glyph in the cache, not null
+ * @throws NullPointerException if glyph is null
+ */
+ /*@Nonnull*/
+ public TextureCoords find(/*@Nonnull*/ final Glyph glyph) {
+
+ Check.notNull(glyph, "Glyph cannot be null");
+
+ // Mark the glyph as being used
+ markGlyphLocationUsed(glyph);
+
+ // Find the coordinates, recalculating if necessary
+ if (glyph.coordinates == null) {
+ computeCoordinates(glyph);
+ }
+ return glyph.coordinates;
+ }
+
+ /**
+ * Returns the initial size of a {@link GlyphCache} for a font.
+ *
+ * @param font Font to create glyphs from, assumed not null
+ */
+ /*@Nonnegative*/
+ private static int findBackingStoreSizeForFont(/*@Nonnull*/ final Font font) {
+ return Math.max(MIN_BACKING_STORE_SIZE, font.getSize() * FONT_SIZE_MULTIPLIER);
+ }
+
+ /**
+ * Finds a location in the backing store for a glyph.
+ *
+ * @param glyph Glyph being uploaded, assumed not null
+ */
+ private void findLocation(/*@Nonnull*/ final Glyph glyph) {
+
+ // Compute a rectangle that includes glyph's margin
+ final int x = 0;
+ final int y = 0;
+ final int w = glyph.margin.left + ((int) glyph.width) + glyph.margin.right;
+ final int h = glyph.margin.top + ((int) glyph.height) + glyph.margin.bottom;
+ final Rect rect = new Rect(x, y, w, h, new TextData(glyph));
+
+ // Pack it into the cache and store its location
+ packer.add(rect);
+ glyph.location = rect;
+ markGlyphLocationUsed(glyph);
+ }
+
+ /**
+ * Determines the maximum texture size supported by OpenGL.
+ *
+ * @param gl Current OpenGL context, assumed not null
+ * @return Maximum texture size
+ */
+ private static int findMaxSize(/*@Nonnull*/ final GL gl) {
+ final int[] size = new int[1];
+ gl.glGetIntegerv(GL.GL_MAX_TEXTURE_SIZE, size, 0);
+ return size[0];
+ }
+
+ /**
+ * Sends an event to all the listeners.
+ *
+ * @param type Kind of event, assumed not null
+ * @param data Information to send with event, assumed not null
+ */
+ private void fireEvent(/*@Nonnull*/ final EventType type, /*@Nonnull*/ final Object data) {
+ for (final EventListener listener : listeners) {
+ assert listener != null : "addListener rejects null";
+ listener.onGlyphCacheEvent(type, data);
+ }
+ }
+
+ /**
+ * Returns object actually storing the rasterized glyphs.
+ *
+ * @return Object actually storing the rasterized glyphs, not null
+ */
+ /*@Nonnull*/
+ TextureBackingStore getBackingStore() {
+ return (TextureBackingStore) packer.getBackingStore();
+ }
+
+ /**
+ * Determines the location of a glyph's bottom baseline.
+ *
+ * @param glyph Glyph to determine bottom baseline for, assumed not null
+ * @return Location of glyph's bottom baseline, which may be negative
+ */
+ /*@CheckForSigned*/
+ private int getBottomBaselineLocation(/*@Nonnull*/ final Glyph glyph) {
+ return (int) (glyph.location.y() + glyph.margin.top + glyph.ascent);
+ }
+
+ /**
+ * Determines the location of a glyph's bottom border.
+ *
+ * @param glyph Glyph to determine bottom border for, assumed not null
+ * @return Location of glyph's bottom border, which may be negative
+ */
+ /*@CheckForSigned*/
+ private int getBottomBorderLocation(/*@Nonnull*/ final Glyph glyph) {
+ return (int) (glyph.location.y() + glyph.margin.top + glyph.height);
+ }
+
+ /**
+ * Returns the font render context used for text size computations by this {@link GlyphCache}.
+ *
+ *
+ *
+ *
+ * @param listener Observer of backing store events
+ * @throws NullPointerException if listener is null
+ */
+ void addListener(/*@Nonnull*/ final EventListener listener) {
+
+ Check.notNull(listener, "Listener cannot be null");
+
+ listeners.add(listener);
+ }
+
+ /**
+ * Creates a new backing store for the packer.
+ *
+ * @param width Width of new backing store
+ * @param height Height of new backing store
+ * @return New backing store, not null
+ * @throws IllegalArgumentException if width or height is negative
+ */
+ /*@Nonnull*/
+ @Override
+ public Object allocateBackingStore(/*@Nonnegative*/ final int width,
+ /*@Nonnegative*/ final int height) {
+
+ Check.argument(width >= 0, "Width is negative");
+ Check.argument(height >= 0, "Height is negative");
+
+ // Print debugging information
+ if (DEBUG) {
+ System.err.printf("Make back store %d x %d\n", width, height);
+ }
+
+ // Make a new backing store
+ return new TextureBackingStore(
+ width, height,
+ font,
+ antialias, subpixel,
+ smooth, mipmap);
+ }
+
+ /**
+ * Starts a copy from an old backing store to a new one.
+ *
+ * @param obs Backing store being copied from
+ * @param nbs Backing store being copied to
+ */
+ @Override
+ public void beginMovement(final Object obs, final Object nbs) {
+ // empty
+ }
+
+ /**
+ * Determines if a backing store can be compacted.
+ *
+ * @return True if backing store can be compacted
+ */
+ @Override
+ public boolean canCompact() {
+ return true;
+ }
+
+ /**
+ * Disposes of a backing store.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+ @Test
+ public void testGlyphRendererGL2() throws Exception {
+
+ final JFrame frame = new JFrame("testGlyphRendererGL2");
+ final GLCanvas canvas = canvasFactory.createGLCanvas("GL2");
+
+ frame.add(canvas);
+ canvas.addGLEventListener(new DebugGL2EventAdapter() {
+
+ @Override
+ public void doInit(final GL2 gl) {
+
+ // Set up glyph renderer
+ glyphRenderer = new GlyphRendererGL2();
+ glyphRenderer.addListener(new GlyphRenderer.EventListener() {
+ @Override
+ public void onGlyphRendererEvent(GlyphRenderer.EventType type) {
+ if (type == GlyphRenderer.EventType.AUTOMATIC_FLUSH) {
+ System.out.println("Automatically flushing!");
+ }
+ }
+ });
+
+ // Set up texture
+ final GlyphTexture texture = new GlyphTexture(gl);
+ texture.bind(gl);
+ texture.upload(gl);
+ }
+
+ @Override
+ public void doDisplay(final GL2 gl) {
+
+ // View
+ gl.glClearColor(0, 1, 1, 1);
+ gl.glClear(GL.GL_COLOR_BUFFER_BIT);
+
+ // Draw glyph
+ final TextureCoords coordinates = new TextureCoords(0, 1, 1, 0);
+ glyphRenderer.beginRendering(gl, true, 512, 512, true);
+ glyphRenderer.setColor(1, 0, 1, 1); // magenta
+ glyphRenderer.drawGlyph(gl, glyph, 40, 80, 0, 1.0f, coordinates);
+ glyphRenderer.setColor(1, 1, 0, 1); // yellow
+ glyphRenderer.drawGlyph(gl, glyph, 260, 80, 0, 1.0f, coordinates);
+ glyphRenderer.endRendering(gl);
+ }
+ });
+ TestRunner.run(frame, WAIT_TIME);
+ }
+
+ /**
+ * Test case for {@link GlyphRendererGL3}.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+ @Test
+ public void testGlyphRendererGL3() throws Exception {
+
+ final JFrame frame = new JFrame("testGlyphRendererGL3");
+ final GLCanvas canvas = canvasFactory.createGLCanvas("GL3");
+
+ frame.add(canvas);
+ canvas.addGLEventListener(new DebugGL3EventAdapter() {
+
+ @Override
+ public void doInit(final GL3 gl) {
+
+ // Set up glyph renderer
+ glyphRenderer = new GlyphRendererGL3(gl);
+ glyphRenderer.addListener(new GlyphRenderer.EventListener() {
+ public void onGlyphRendererEvent(GlyphRenderer.EventType type) {
+ if (type == GlyphRenderer.EventType.AUTOMATIC_FLUSH) {
+ System.out.println("Automatically flushing!");
+ }
+ }
+ });
+
+ // Set up texture
+ final GlyphTexture texture = new GlyphTexture(gl);
+ texture.bind(gl);
+ texture.upload(gl);
+ }
+
+ @Override
+ public void doDisplay(final GL3 gl) {
+
+ // Clear
+ gl.glClearColor(0, 1, 1, 1);
+ gl.glClear(GL3.GL_COLOR_BUFFER_BIT);
+
+ // Draw glyph
+ final TextureCoords coordinates = new TextureCoords(0, 1, 1, 0);
+ glyphRenderer.beginRendering(gl, true, 512, 512, true);
+ glyphRenderer.setColor(1, 0, 1, 1); // magenta
+ glyphRenderer.drawGlyph(gl, glyph, 40, 80, 0, 1.0f, coordinates);
+ glyphRenderer.setColor(1, 1, 0, 1); // yellow
+ glyphRenderer.drawGlyph(gl, glyph, 260, 80, 0, 1.0f, coordinates);
+ glyphRenderer.endRendering(gl);
+ }
+ });
+ TestRunner.run(frame, WAIT_TIME);
+ }
+
+ //-----------------------------------------------------------------
+ // Helpers
+ //
+
+ /**
+ * Returns a glyph for a character.
+ */
+ private static Glyph createGlyph(final char c) {
+ final GlyphVector gv = createGlyphVector(c);
+ final Glyph glyph = new Glyph(c, gv);
+ glyph.width = 54;
+ glyph.height = 72;
+ return glyph;
+ }
+
+ /**
+ * Returns a glyph vector for a character.
+ */
+ private static GlyphVector createGlyphVector(final char c) {
+ final FontRenderContext frc = createFontRenderContext();
+ final char[] chars = new char[] { c };
+ return FONT.createGlyphVector(frc, chars);
+ }
+
+ /**
+ * Returns a blank font render context.
+ */
+ private static FontRenderContext createFontRenderContext() {
+ return new FontRenderContext(new AffineTransform(), false, false);
+ }
+
+ //-----------------------------------------------------------------
+ // Nested classes
+ //
+
+ /**
+ * Texture with a giant 'G' in it.
+ */
+ private static class GlyphTexture {
+
+ // Width and height of the texture
+ private static final int SIZE = 256;
+
+ // Internal OpenGL identifier for texture
+ private int handle;
+
+ GlyphTexture(final GL gl) {
+ handle = createHandle(gl);
+ }
+
+ void bind(final GL gl) {
+ gl.glActiveTexture(GL.GL_TEXTURE0);
+ gl.glBindTexture(GL.GL_TEXTURE_2D, handle);
+ }
+
+ void upload(final GL gl) {
+
+ final BufferedImage image = createBufferedImage();
+ final Graphics2D graphics = image.createGraphics();
+ final ByteBuffer buffer = createByteBuffer(image);
+
+ // Draw character into image
+ graphics.setFont(FONT);
+ graphics.drawString("G", 0.09f * SIZE, 0.80f * SIZE);
+
+ // Upload it to the texture
+ final GLProfile profile = gl.getGLProfile();
+ gl.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1);
+ gl.glTexImage2D(
+ GL.GL_TEXTURE_2D,
+ 0,
+ profile.isGL3() ? GL3.GL_RED : GL2.GL_INTENSITY,
+ SIZE, SIZE,
+ 0,
+ profile.isGL3() ? GL3.GL_RED : GL2.GL_LUMINANCE,
+ GL.GL_UNSIGNED_BYTE,
+ buffer);
+ setParameter(gl, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
+ setParameter(gl, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
+ }
+
+ private static BufferedImage createBufferedImage() {
+ return new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_BYTE_GRAY);
+ }
+
+ private static ByteBuffer createByteBuffer(final BufferedImage image) {
+ final Raster raster = image.getRaster();
+ final DataBufferByte dbb = (DataBufferByte) raster.getDataBuffer();
+ final byte[] array = dbb.getData();
+ return ByteBuffer.wrap(array);
+ }
+
+ private static int createHandle(final GL gl) {
+ final int[] handles = new int[1];
+ gl.glGenTextures(1, handles, 0);
+ return handles[0];
+ }
+
+ private static void setParameter(final GL gl, final int name, final int value) {
+ gl.glTexParameteri(GL.GL_TEXTURE_2D, name, value);
+ }
+ }
+}
+
diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/awt/text/TestQuadPipelineAWT.java b/src/test/com/jogamp/opengl/test/junit/jogl/awt/text/TestQuadPipelineAWT.java
new file mode 100644
index 0000000000..10b61716b3
--- /dev/null
+++ b/src/test/com/jogamp/opengl/test/junit/jogl/awt/text/TestQuadPipelineAWT.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright 2012 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ *
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package com.jogamp.opengl.test.junit.jogl.awt.text;
+
+import static org.junit.Assert.*;
+
+import com.jogamp.opengl.GL2;
+import com.jogamp.opengl.GL2ES2;
+import com.jogamp.opengl.GL3;
+import com.jogamp.opengl.awt.GLCanvas;
+
+import javax.swing.JFrame;
+
+import jogamp.opengl.util.awt.text.Quad;
+import jogamp.opengl.util.awt.text.QuadPipeline;
+import jogamp.opengl.util.awt.text.QuadPipelineGL10;
+import jogamp.opengl.util.awt.text.QuadPipelineGL11;
+import jogamp.opengl.util.awt.text.QuadPipelineGL15;
+import jogamp.opengl.util.awt.text.QuadPipelineGL30;
+import jogamp.opengl.util.awt.text.ShaderLoader;
+
+import org.junit.Test;
+
+
+/**
+ * Test for {@link QuadPipeline}.
+ */
+public class TestQuadPipelineAWT {
+
+ // Amount of time to wait before closing the window
+ private static final int WAIT_TIME = 1000;
+
+ // Shader code for OpenGL 3 tests
+ private static final String VERT_SOURCE;
+ private static final String FRAG_SOURCE;
+
+ // Shader program for OpenGL 3 test
+ private int program;
+
+ // Pipeline to render with
+ private QuadPipeline pipeline;
+
+ // Quad to render
+ private Quad quad;
+
+ // Utility for making canvases
+ private final GLCanvasFactory canvasFactory = new GLCanvasFactory();
+
+ /**
+ * Initializes static fields.
+ */
+ static {
+ VERT_SOURCE =
+ "#version 140\n" +
+ "uniform mat4 MVPMatrix=mat4(1);\n" +
+ "in vec4 MCVertex;\n" +
+ "void main() {\n" +
+ " gl_Position = MVPMatrix * MCVertex;\n" +
+ "}\n";
+ FRAG_SOURCE =
+ "#version 140\n" +
+ "uniform vec4 Color=vec4(1,1,1,1);\n" +
+ "out vec4 FragColor;\n" +
+ "void main() {\n" +
+ " FragColor = Color;\n" +
+ "}\n";
+ }
+
+ /**
+ * Test case for {@link QuadPipelineGL10}.
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+ @Test
+ public void testQuadPipelineGL10() throws Exception {
+
+ final JFrame frame = new JFrame("testQuadPipelineGL10");
+ final GLCanvas canvas = canvasFactory.createGLCanvas("GL2");
+
+ frame.add(canvas);
+ canvas.addGLEventListener(new GL2EventAdapter() {
+
+ @Override
+ public void doInit(final GL2 gl) {
+ pipeline = new QuadPipelineGL10();
+ quad = createQuad();
+ }
+
+ @Override
+ public void doDisplay(final GL2 gl) {
+
+ // View
+ gl.glViewport(0, 0, 512, 512);
+ gl.glClearColor(0, 1, 1, 1);
+ gl.glClear(GL2.GL_COLOR_BUFFER_BIT);
+
+ // Create geometry
+ pipeline.beginRendering(gl);
+ pipeline.addQuad(gl, quad);
+ pipeline.endRendering(gl);
+ }
+ });
+ TestRunner.run(frame, WAIT_TIME);
+ }
+
+ /**
+ * Test case for {@link QuadPipelineGL11}.
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+ @Test
+ public void testQuadPipelineGL11() throws Exception {
+
+ final JFrame frame = new JFrame("testQuadPipelineGL11");
+ final GLCanvas canvas = canvasFactory.createGLCanvas("GL2");
+
+ frame.add(canvas);
+ canvas.addGLEventListener(new DebugGL2EventAdapter() {
+
+ @Override
+ public void doInit(final GL2 gl) {
+ pipeline = new QuadPipelineGL11();
+ quad = createQuad();
+ }
+
+ @Override
+ public void doDisplay(final GL2 gl) {
+
+ // View
+ gl.glViewport(0, 0, 512, 512);
+ gl.glClearColor(0, 1, 1, 1);
+ gl.glClear(GL2.GL_COLOR_BUFFER_BIT);
+
+ // Create geometry
+ pipeline.beginRendering(gl);
+ pipeline.addQuad(gl, quad);
+ pipeline.endRendering(gl);
+ }
+ });
+ TestRunner.run(frame, WAIT_TIME);
+ }
+
+ /**
+ * Test case for {@link QuadPipelineGL15}.
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+ @Test
+ public void testQuadPipelineGL15() throws Exception {
+
+ final JFrame frame = new JFrame("testQuadPipelineGL15");
+ final GLCanvas canvas = canvasFactory.createGLCanvas("GL2");
+
+ frame.add(canvas);
+ canvas.addGLEventListener(new DebugGL2EventAdapter() {
+
+ @Override
+ public void doInit(final GL2 gl) {
+ pipeline = new QuadPipelineGL15(gl);
+ quad = createQuad();
+ }
+
+ @Override
+ public void doDisplay(final GL2 gl) {
+
+ // View
+ gl.glViewport(0, 0, 512, 512);
+ gl.glClearColor(0, 1, 1, 1);
+ gl.glClear(GL2.GL_COLOR_BUFFER_BIT);
+
+ // Create geometry
+ pipeline.beginRendering(gl);
+ pipeline.addQuad(gl, quad);
+ pipeline.endRendering(gl);
+ }
+ });
+ TestRunner.run(frame, WAIT_TIME);
+ }
+
+ /**
+ * Test case for {@link QuadPipelineGL30}.
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+ @Test
+ public void testQuadPipelineGL30() throws Exception {
+
+ final JFrame frame = new JFrame("testQuadPipelineGL30");
+ final GLCanvas canvas = canvasFactory.createGLCanvas("GL3");
+
+ frame.add(canvas);
+ canvas.addGLEventListener(new DebugGL3EventAdapter() {
+
+ @Override
+ public void doInit(final GL3 gl) {
+ program = createProgram(gl);
+ pipeline = new QuadPipelineGL30(gl, program);
+ quad = createQuad();
+ }
+
+ @Override
+ public void doDisplay(final GL3 gl) {
+
+ // View
+ gl.glViewport(0, 0, 512, 512);
+ gl.glClearColor(0, 1, 1, 1);
+ gl.glClear(GL3.GL_COLOR_BUFFER_BIT);
+
+ // Create geometry
+ gl.glUseProgram(program);
+ pipeline.beginRendering(gl);
+ pipeline.addQuad(gl, quad);
+ pipeline.endRendering(gl);
+ }
+ });
+ TestRunner.run(frame, WAIT_TIME);
+ }
+
+ //-----------------------------------------------------------------
+ // Helpers
+ //
+
+ /**
+ * Returns a shader program for use with pipeline.
+ */
+ private static int createProgram(final GL2ES2 gl) {
+ int program = -1;
+ try {
+ program = ShaderLoader.loadProgram(gl, VERT_SOURCE, FRAG_SOURCE);
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail();
+ }
+ return program;
+ }
+
+ /**
+ * Returns a quad structure for use with pipeline.
+ */
+ private static Quad createQuad() {
+ final Quad q = new Quad();
+ q.xl = -0.5f;
+ q.xr = +0.5f;
+ q.yb = -0.5f;
+ q.yt = +0.5f;
+ q.z = 0;
+ q.sl = 0;
+ q.sr = 1;
+ q.tb = 0;
+ q.tt = 1;
+ return q;
+ }
+}
diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/awt/text/TestRunner.java b/src/test/com/jogamp/opengl/test/junit/jogl/awt/text/TestRunner.java
new file mode 100644
index 0000000000..f8b1c4d3fd
--- /dev/null
+++ b/src/test/com/jogamp/opengl/test/junit/jogl/awt/text/TestRunner.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2012 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ *
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package com.jogamp.opengl.test.junit.jogl.awt.text;
+
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+
+import javax.swing.JFrame;
+import javax.swing.SwingUtilities;
+
+
+/**
+ * Utility for using windows in JUnit tests.
+ *
+ *
+ *
+ */
+class TestRunner {
+
+ // Default amount of time to wait before closing window
+ private static final long DEFAULT_WAIT_TIME = 1000;
+
+ // Initial location on screen
+ private static final int DEFAULT_LOCATION_X = 50;
+ private static final int DEFAULT_LOCATION_Y = 50;
+
+ // Key that closes the window
+ private static final int CLOSE_KEY = KeyEvent.VK_ESCAPE;
+
+ /**
+ * Prevents instantiation.
+ */
+ private TestRunner() {
+ ;
+ }
+
+ /**
+ * Shows frame, waits for it to close, then disposes of it.
+ *
+ * @param frame Window to use
+ * @throws NullPointerException if frame is null
+ */
+ public static void run(final JFrame frame) {
+ run(frame, DEFAULT_WAIT_TIME);
+ }
+
+ /**
+ * Shows frame, waits for it a certain amount of time, then disposes of it.
+ *
+ * @param frame Window to use
+ * @param timeout Amount of milliseconds to wait (<= 0 for indefinitely)
+ * @throws NullPointerException if frame is null
+ */
+ public static void run(final JFrame frame, final long timeout) {
+
+ // Change behavior
+ frame.setLocation(DEFAULT_LOCATION_X, DEFAULT_LOCATION_Y);
+ frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+
+ // Add listeners
+ frame.addKeyListener(new KeyObserver(frame));
+ frame.addWindowListener(new WindowObserver(frame));
+
+ // Show the frame
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ if (!hasExplicitSize(frame)) {
+ frame.pack();
+ }
+ frame.setVisible(true);
+ }
+ });
+
+ // Wait for it to close
+ synchronized (frame) {
+ try {
+ if (timeout < 0) {
+ frame.wait();
+ } else {
+ frame.wait(timeout);
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while waiting!");
+ }
+ }
+
+ // Hide and dispose of it
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ frame.setVisible(false);
+ frame.dispose();
+ }
+ });
+ }
+
+ //-----------------------------------------------------------------
+ // Helpers
+ //
+
+ /**
+ * Checks if a frame has an explicit size.
+ *
+ * @param frame Frame to check
+ * @return true if an explicit size has been set
+ * @throws NullPointerException if frame is null
+ */
+ private static boolean hasExplicitSize(final JFrame frame) {
+ final int width = frame.getWidth();
+ final int height = frame.getHeight();
+ return (width > 0) || (height > 0);
+ }
+
+ /**
+ * Wakes any threads waiting on object.
+ *
+ * @param object Object that other objects are waiting on
+ * @throws NullPointerException if object is null
+ */
+ private static void wakeWaitingThreads(final Object object) {
+ synchronized (object) {
+ object.notifyAll();
+ }
+ }
+
+ //-----------------------------------------------------------------
+ // Nested classes
+ //
+
+ /**
+ * Listener for key events.
+ */
+ private static class KeyObserver extends KeyAdapter {
+
+ // Frame to listen to
+ private final JFrame frame;
+
+ /**
+ * Constructs a key observer from a frame.
+ *
+ * @param frame Frame to listen to
+ * @throws AssertionError if frame is null
+ */
+ public KeyObserver(final JFrame frame) {
+ assert (frame != null);
+ this.frame = frame;
+ }
+
+ /**
+ * Wakes threads when {@link #CLOSE_KEY} key is released.
+ *
+ * @param event Event with key status
+ * @throws NullPointerException if event is null
+ */
+ public void keyReleased(final KeyEvent event) {
+ switch (event.getKeyCode()) {
+ case CLOSE_KEY:
+ wakeWaitingThreads(frame);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Listener for window events.
+ */
+ private static class WindowObserver extends WindowAdapter {
+
+ // Frame to listen to
+ private final JFrame frame;
+
+ /**
+ * Constructs a window observer from a frame.
+ *
+ * @param frame Frame to listen to
+ * @throws AssertionError if frame is null
+ */
+ public WindowObserver(final JFrame frame) {
+ assert (frame != null);
+ this.frame = frame;
+ }
+
+ /**
+ * Wakes threads when window is closing.
+ *
+ * @param event Event with window status
+ * @throws NullPointerException if event is null
+ */
+ @Override
+ public void windowClosing(final WindowEvent event) {
+ wakeWaitingThreads(frame);
+ }
+ }
+}
diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/awt/text/TestTextRendererAWT.java b/src/test/com/jogamp/opengl/test/junit/jogl/awt/text/TestTextRendererAWT.java
new file mode 100644
index 0000000000..a6553e3e24
--- /dev/null
+++ b/src/test/com/jogamp/opengl/test/junit/jogl/awt/text/TestTextRendererAWT.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2012 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ *
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``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 JogAmp Community 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.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package com.jogamp.opengl.test.junit.jogl.awt.text;
+
+import com.jogamp.opengl.GL;
+import com.jogamp.opengl.GL2;
+import com.jogamp.opengl.GL3;
+import com.jogamp.opengl.awt.GLCanvas;
+import com.jogamp.opengl.util.FPSAnimator;
+import com.jogamp.opengl.util.awt.TextRenderer;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import javax.swing.JFrame;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+
+/**
+ * Test for {@link TextRenderer}.
+ */
+public class TestTextRendererAWT {
+
+ // Font to render with
+ private static final Font FONT = new Font("Monospace", Font.PLAIN, 110);
+
+ // Time to wait before closing window
+ private static final int LONG_WAIT_TIME = 4000;
+ private static final int SHORT_WAIT_TIME = 1000;
+
+ // Random collection of words to render
+ private final WordBank wordBank = new WordBank();
+
+ // Instance to render with
+ private TextRenderer textRenderer = null;
+
+ // Utility for making canvases
+ private final GLCanvasFactory canvasFactory = new GLCanvasFactory();
+
+ /**
+ * Ensures the text renderer can draw text properly with an OpenGL 2 context.
+ *
+ *
+ *
+ */
+ @Test
+ public void testWithGL2() {
+
+ final JFrame frame = new JFrame("testWithGL2");
+ final GLCanvas canvas = canvasFactory.createGLCanvas("GL2");
+ final FPSAnimator animator = new FPSAnimator(canvas, 1);
+
+ frame.add(canvas);
+ canvas.addGLEventListener(new DebugGL2EventAdapter() {
+
+ @Override
+ public void doInit(final GL2 gl) {
+ textRenderer = new TextRenderer(FONT);
+ }
+
+ @Override
+ public void doDisplay(final GL2 gl) {
+
+ // Clear
+ gl.glClearColor(0.85f, 0.85f, 0.85f, 1);
+ gl.glClear(GL.GL_COLOR_BUFFER_BIT);
+
+ // Draw
+ final int width = canvas.getWidth();
+ final int height = canvas.getHeight();
+ textRenderer.beginRendering(width, height);
+ textRenderer.setColor(Color.RED);
+ textRenderer.draw(wordBank.next(), getX(width), getY(height));
+ textRenderer.setColor(Color.GREEN);
+ textRenderer.draw(wordBank.next(), getX(width), getY(height));
+ textRenderer.endRendering();
+ }
+
+ @Override
+ public void doDispose(final GL2 gl) {
+ textRenderer.dispose();
+ }
+ });
+ animator.start();
+ TestRunner.run(frame, LONG_WAIT_TIME);
+ animator.stop();
+ }
+
+ /**
+ * Ensures the text renderer can draw text properly with an OpenGL 3 context.
+ *
+ *
+ *
+ */
+ @Test
+ public void testWithGL3() {
+
+ final JFrame frame = new JFrame("testWithGL3");
+ final GLCanvas canvas = canvasFactory.createGLCanvas("GL3");
+ final FPSAnimator animator = new FPSAnimator(canvas, 1);
+
+ frame.add(canvas);
+ canvas.addGLEventListener(new DebugGL3EventAdapter() {
+
+ @Override
+ public void doInit(final GL3 gl) {
+ textRenderer = new TextRenderer(FONT);
+ }
+
+ @Override
+ public void doDisplay(final GL3 gl) {
+
+ // Clear
+ gl.glClearColor(0.85f, 0.85f, 0.85f, 1);
+ gl.glClear(GL.GL_COLOR_BUFFER_BIT);
+
+ // Draw
+ final int width = canvas.getWidth();
+ final int height = canvas.getHeight();
+ textRenderer.beginRendering(width, height);
+ textRenderer.setColor(Color.RED);
+ textRenderer.draw(wordBank.next(), getX(width), getY(height));
+ textRenderer.setColor(Color.GREEN);
+ textRenderer.draw(wordBank.next(), getX(width), getY(height));
+ textRenderer.endRendering();
+ }
+
+ @Override
+ public void doDispose(final GL3 gl) {
+ textRenderer.dispose();
+ }
+ });
+ animator.start();
+ TestRunner.run(frame, LONG_WAIT_TIME);
+ animator.stop();
+ }
+
+ /**
+ * Ensures the user can request whether vertex arrays are used before beginRendering is called.
+ */
+ @Test
+ public void testSetUseVertexArraysBeforeBeginRendering() {
+
+ final JFrame frame = new JFrame("testSetUseVertexArraysBeforeBeginRendering");
+ final GLCanvas canvas = canvasFactory.createGLCanvas("GL2");
+
+ frame.add(canvas);
+ canvas.addGLEventListener(new DebugGL2EventAdapter() {
+
+ @Override
+ public void doInit(final GL2 gl) {
+ textRenderer = new TextRenderer(FONT);
+ Assert.assertTrue(textRenderer.getUseVertexArrays());
+ textRenderer.setUseVertexArrays(false);
+ Assert.assertFalse(textRenderer.getUseVertexArrays());
+ }
+
+ @Override
+ public void doDisplay(final GL2 gl) {
+ final int width = canvas.getWidth();
+ final int height = canvas.getHeight();
+ textRenderer.beginRendering(width, height);
+ textRenderer.endRendering();
+ Assert.assertFalse(textRenderer.getUseVertexArrays());
+ }
+
+ @Override
+ public void doDispose(final GL2 gl) {
+ textRenderer.dispose();
+ }
+ });
+ TestRunner.run(frame, SHORT_WAIT_TIME);
+ }
+
+ //-----------------------------------------------------------------
+ // Helpers
+ //
+
+ /**
+ * Returns random X coordinate in left-hand side of window.
+ */
+ private static int getX(final int width) {
+ return (int) (Math.random() * width / 2);
+ }
+
+ /**
+ * Returns random Y coordinate in window.
+ */
+ private static int getY(final int height) {
+ return (int) (Math.random() * height);
+ }
+}
+
+
+/**
+ * Random collection of words.
+ */
+class WordBank {
+
+ // Available words
+ private List