jME-TrueTypeFont Rendering Library

public abstract class TextComponent<T> extends AbstractGuiComponent
                           implements ColoredComponent {
    private HAlignment hAlign = HAlignment.Left;
    private VAlignment vAlign = VAlignment.Top;
    private Vector3f offset = null;
    private int layer;
    private float maxWidth;
    private float maxHeight;

    public TextComponent() {
    }

    public abstract void setText( String text );

    public abstract String getText();

    public void setLayer( int layer ) {
        if( this.layer == layer ) {
            return;
        }
        this.layer = layer;
        resetLayer();        
    }
    
    public int getLayer() {
        return layer;
    }

    public void setHAlignment( HAlignment a ) {
        if( hAlign == a )
            return;
        hAlign = a;
        //resetAlignment();
    }

    public HAlignment getHAlignment() {
        return hAlign;
    }

    public void setVAlignment( VAlignment a ) {
        if( vAlign == a )
            return;
        vAlign = a;
        //resetAlignment();
    }

    public VAlignment getVAlignment() {
        return vAlign;
    }

    /**
     *  For values greater than 0, this will constrain the maximum
     *  width of the text box.  Wrapping text will cause the text box
     *  to grow vertically.
     */
    public void setMaxWidth( float f ) {
        this.maxWidth = f;
        invalidate();
    }
    
    public float getMaxWidth() {
        return maxWidth;
    }
    
    public void setMaxHeight(float f) {
        this.maxHeight = f;
        invalidate();
    }
    
    public float getMaxHeight() {
        return maxHeight;
    }

    public abstract void setFont( T font );
    
    public abstract T getFont();

    public abstract void setFontSize( float size );

    public abstract float getFontSize();
    
    public abstract void setKerning(int kerning);
    
    public abstract int getKerning();
    
    public abstract void setWrapMode(WrapMode wrap);
    
    public abstract WrapMode getWrapMode();

    @Override
    public abstract void setColor( ColorRGBA color );

    @Override
    public abstract ColorRGBA getColor();
    
    public abstract void setOutlineColor(ColorRGBA color);
    
    public abstract ColorRGBA getOutlineColor();

    @Override
    public abstract void setAlpha( float f );
    
    @Override
    public abstract float getAlpha();

    public TextComponent color( ColorRGBA color ) {
        setColor(color);
        return this;
    }

    public TextComponent offset( float x, float y, float z ) {
        setOffset(x,y,z);
        return this;
    }

    public void setOffset( float x, float y, float z ) {
        if( offset == null ) {
            offset = new Vector3f(x,y,z);
        } else {
            offset.set(x,y,z);
        }
        invalidate();
    }

    public void setOffset( Vector3f offset ) {
        this.offset = offset.clone();
        invalidate();
    }

    public Vector3f getOffset() {
        return offset;
    }

    public abstract void setTextSize( float f );

    public abstract float getTextSize();
    
    protected abstract void resetLayer();
}
1 Like
public class BitmapTextComponent extends TextComponent<BitmapFont> {
    private BitmapText bitmapText;
    private Rectangle textBox;

    public BitmapTextComponent( String text, BitmapFont font ) {
        super();
        this.bitmapText = new BitmapText(font);
        setText(text);
    }

    @Override
    public BitmapTextComponent clone() {
        BitmapTextComponent result = (BitmapTextComponent)super.clone();
        result.bitmapText = bitmapText.clone();
        result.textBox = null;
        return result;
    }

    @Override
    public void attach( GuiControl parent ) {
        super.attach(parent);
        getNode().attachChild(bitmapText);
    }

    @Override
    public void detach( GuiControl parent ) {
        getNode().detachChild(bitmapText);
        super.detach(parent);
    }
    
    @Override
    public void setText( String text ) {
        if( text != null && text.equals(bitmapText.getText()) )
            return;

        bitmapText.setText(text);
        invalidate();
    }
    
    @Override
    public String getText() {
        return bitmapText.getText();
    }
    
    @Override
    public void setHAlignment( HAlignment a ) {
        super.setHAlignment(a);
        /*if( hAlign == a )
            return;
        hAlign = a;*/
        resetAlignment();
    }
    
    @Override
    public void setVAlignment( VAlignment a ) {
        super.setVAlignment(a);
        /*if( vAlign == a )
            return;
        vAlign = a;*/
        resetAlignment();
    }
    
    @Override
    public void setFont( BitmapFont font ) {
        if( font == bitmapText.getFont() )
            return;
            
        if( isAttached() ) {
            bitmapText.removeFromParent();
        }

        // Can't change the font once created so we'll
        // have to create it fresh
        BitmapText newText = new BitmapText(font);
        newText.setText(getText());
        newText.setColor(getColor());
        newText.setLocalTranslation(bitmapText.getLocalTranslation());
        float currentSize = getFontSize();
        if( currentSize != bitmapText.getSize() ) {
            // The caller has overridden the default font size so we'll keep it.
            newText.setSize(getFontSize());
        }
        this.bitmapText = newText;
        resetLayer();

        // Need to invalidate because we probably changed size
        // And that will realign us, etc. anyway.
        invalidate();

        if( isAttached() ) {
            getNode().attachChild(bitmapText);
        }
    }
    
    @Override
    public BitmapFont getFont() {
        return bitmapText.getFont();
    }
    
    @Override
    public void setFontSize( float size ) {
        if( bitmapText.getSize() == size )
            return;
        bitmapText.setSize(size);
        invalidate();
    }

    @Override
    public float getFontSize() {
        return bitmapText.getSize();
    }
    
    @Override
    public void setKerning(int kerning) {
    }
    
    @Override
    public int getKerning() {
        return 0;
    }
    
    @Override
    public void setWrapMode(StringContainer.WrapMode wrap) {
    }
    
    @Override
    public StringContainer.WrapMode getWrapMode() {
        return StringContainer.WrapMode.Char;
    }

    @Override
    public void setColor( ColorRGBA color ) {
        float alpha = bitmapText.getAlpha();
        bitmapText.setColor(color);
        if( alpha != 1 ) {
            bitmapText.setAlpha(alpha);
        }
    }

    @Override
    public ColorRGBA getColor() {
        return bitmapText.getColor();
    }
    
    @Override
    public void setOutlineColor(ColorRGBA color){
        
    }
    
    @Override
    public ColorRGBA getOutlineColor() {
        return null;
    }

    @Override
    public void setAlpha( float f ) {
        bitmapText.setAlpha(f);
    }
    
    @Override
    public float getAlpha() {
        return bitmapText.getAlpha();
    }
    
    @Override
    public void setTextSize( float f ) {
        this.bitmapText.setSize(f);
    }

    @Override
    public float getTextSize() {
        return bitmapText.getSize();
    }

    @Override
    public void reshape( Vector3f pos, Vector3f size ) {

        if( getOffset() != null ) {
            // My gut is that we need to treat positive and negative
            // differently...  I will need to think about that some more
            // or have some examples where this is failing.
            // In the case where we have a positive offset then it is ok
            // to draw ourselves spaced out and then shrink the size.
            // If we have a negative offset, then we should be drawing
            // ourselves where we are and then adjusting pos+size for the
            // next guy.
            // I'll fix it later FIXME
            // Notes as of component stack refactoring... when testing
            // I discovered that because of the way this is arranged, shadows
            // are pushed back instead of pushing the layered text forward.
            // Essentially, text does not at all play nice in layers.
            // I need to test some other things before swing back to fix this
            // because I may have already broken things with the component stack
            // refactoring.
            // Ok, so upon more reflection, I think offset will work like one
            // would expect.  Offset will set the position of this text relative
            // to the passed in position... but that means that negative offsets
            // are really just 0 and we instead push out the position.
            // This means that something like shadow text with a -1 z will end up
            // -1 behind the regular text because the regular text will get pushed
            // out by 1.
            // So a negative z offset results in z=0 for this text but pos.z += abs(z).
            // A positive Z pushes us out and also moves pos.z+= z. 
            // Because we use offset z for size, this is really the only way it
            // makes sense.  offset.z will control the thickness and positive or 
            // negative indicates where in the "box" it falls (back or front)
            float effectiveZ = Math.max(0, getOffset().z);           
            bitmapText.setLocalTranslation(pos.x + getOffset().x, pos.y + getOffset().y, pos.z + effectiveZ);
            size.x -= Math.abs(getOffset().x);
            size.y -= Math.abs(getOffset().y);
            size.z -= Math.abs(getOffset().z);
            pos.z += Math.abs(getOffset().z);            
        } else {
            bitmapText.setLocalTranslation(pos.x, pos.y, pos.z);
        }
        textBox = new Rectangle(0, 0, size.x, size.y);
        bitmapText.setBox( textBox );
        resetAlignment();
    }

    @Override
    public void calculatePreferredSize( Vector3f size ) {    
        // Make sure that the bitmapText reports a reliable
        // preferred size
        bitmapText.setBox(null);

        if( getMaxWidth() > 0 ) {
            // Give the text a box that constrains the width
            bitmapText.setBox(new Rectangle(0, 0, getMaxWidth(), 0));
        }

        size.x = bitmapText.getLineWidth();
        size.y = bitmapText.getHeight();

        if( getOffset() != null ) {
            size.x += Math.abs(getOffset().x);
            size.y += Math.abs(getOffset().y);
            size.z += Math.abs(getOffset().z);
        }

        size.x += 0.01f;

        // Reset any text box we already had
        bitmapText.setBox(textBox);
    }
    
    @Override
    public void adjustSize(Vector3f size, boolean prefCalculated) {
        bitmapText.setBox(new Rectangle(0, 0,
                getMaxWidth() > 0 ? Math.min(getMaxWidth(), size.x) : size.x,
                getMaxHeight() > 0 ? Math.min(getMaxHeight(), size.y) : size.y));

        size.x -= bitmapText.getLineWidth();
        size.y -= bitmapText.getHeight();

        if( getOffset() != null ) {
            size.x -= Math.abs(getOffset().x);
            size.y -= Math.abs(getOffset().y);
            size.z -= Math.abs(getOffset().z);
        }

        size.x -= 0.01f;
        
        bitmapText.setBox(textBox);
    }
    
    protected void resetAlignment() {
        if( textBox == null )
            return;

        switch( getHAlignment() ) {
            case Left:
                bitmapText.setAlignment(Align.Left);
                break;
            case Right:
                bitmapText.setAlignment(Align.Right);
                break;
            case Center:
                bitmapText.setAlignment(Align.Center);
                break;
        }
        switch( getVAlignment() ) {
            case Top:
                bitmapText.setVerticalAlignment(VAlign.Top);
                break;
            case Bottom:
                bitmapText.setVerticalAlignment(VAlign.Bottom);
                break;
            case Center:
                bitmapText.setVerticalAlignment(VAlign.Center);
                break;
        }
        
        invalidate();
    }
    
    @Override
    protected void resetLayer() {
        LayerComparator.resetLayer(bitmapText, getLayer());    
    }
}
1 Like
public class TrueTypeTextComponent extends TextComponent<TrueTypeFont> {
    private TrueTypeFont font;
    private float fontScale;
    private final StringContainer stringContainer;
    private TrueTypeContainer ttc;
    
    private TTF_AtlasListener atlasListener;
    private float oldAtlasWidth = 0;
    private float oldAtlasHeight = 0;
    
    private ColorRGBA color = new ColorRGBA(1, 1, 1, 1);
    private ColorRGBA outlineColor = new ColorRGBA(0, 0, 0, 1);
    float alpha = 1;
    
    public TrueTypeTextComponent(String text, TrueTypeFont font) {
        this.font = font;
        fontScale = font.getScale();
        
        stringContainer = new StringContainer(font, text);
    }
    
    @Override
    public void setText( String text ) {
        if (text != null && text.equals(stringContainer.getText()))
            return;
        
        stringContainer.setText(text);
        invalidate();
    }
    
    @Override
    public String getText() {
        return stringContainer.getText();
    }
    
    @Override
    public void setHAlignment( HAlignment a ) {
        super.setHAlignment(a);
        switch(a) {
            case Right:
                stringContainer.setAlignment(StringContainer.Align.Right);
                break;
            case Center:
                stringContainer.setAlignment(StringContainer.Align.Center);
                break;
            default:
                stringContainer.setAlignment(StringContainer.Align.Left);
        }
        invalidate();
    }
    
    @Override
    public void setVAlignment( VAlignment a ) {
        super.setVAlignment(a);
        switch(a) {
            case Bottom:
                stringContainer.setVerticalAlignment(StringContainer.VAlign.Top);
                break;
            case Center:
                stringContainer.setVerticalAlignment(StringContainer.VAlign.Center);
                break;
            default:
                stringContainer.setVerticalAlignment(StringContainer.VAlign.Bottom);
        }
        invalidate();
    }
    
    @Override
    public void setFont(TrueTypeFont font) {
        if (atlasListener != null) {
            this.font.removeAtlasListener(atlasListener);
            font.addAtlasListener(atlasListener);
            oldAtlasWidth = 0;
            oldAtlasHeight = 0;
        }
        
        this.font = font;
        stringContainer.setFont(font);
        
        invalidate();
    }
    
    @Override
    public TrueTypeFont getFont() {
        return font;
    }
    
    @Override
    public void setFontSize( float size ) {
        //fontScale = size;
        //font.setScale(size);
        //invalidate();
    }

    @Override
    public float getFontSize() {
        return 1;
    }
    
    @Override
    public void setKerning(int kerning) {
        stringContainer.setKerning(kerning);
        invalidate();
    }
    
    @Override
    public int getKerning() {
        return stringContainer.getKerning();
    }
    
    @Override
    public void setWrapMode(StringContainer.WrapMode wrap) {
        stringContainer.setWrapMode(wrap);
        invalidate();
    }
    
    @Override
    public StringContainer.WrapMode getWrapMode() {
        return stringContainer.getWrapMode();
    }
    
    public void setMaxLines(int maxLines) {
        if (maxLines >= 0) {
            stringContainer.setMaxLines(maxLines);
            setMaxHeight(stringContainer.getTextBox().height);
        } else {
            setMaxHeight(0);
        }
    }
    
    @Override
    public void setColor( ColorRGBA color ) {
        this.color = color;
        if (ttc != null)
            ttc.getMaterial().setColor("Color", new ColorRGBA(color.r, color.g,
                    color.b, color.a * alpha));
    }

    @Override
    public ColorRGBA getColor() {
        return color;
    }
    
    @Override
    public void setOutlineColor(ColorRGBA color){
        outlineColor = color;
        if (ttc != null && font.getOutline() > 0)
            ttc.getMaterial().setColor("Outline", new ColorRGBA(outlineColor.r, outlineColor.g,
                    outlineColor.b, outlineColor.a * alpha));
    }
    
    @Override
    public ColorRGBA getOutlineColor() {
        return outlineColor;
    }
    
    @Override
    public void setAlpha( float f ) {
        alpha = f;
        setColor(color);
        setOutlineColor(outlineColor);
    }
    
    @Override
    public float getAlpha() {
        return alpha;
    }
    
    @Override
    public void setTextSize( float f ) {
        //setFontSize(f);
    }

    @Override
    public float getTextSize() {
        return 1;
    }
    
    private void attach() {
        stringContainer.getLines();
        if (stringContainer.getNumNonSpaceCharacters() <= 0) {
            if (ttc == null)
                return;
            
            getNode().detachChild(ttc);
            removeListener();
            ttc = null;
            
            return;
        }
        
        if (ttc == null) {
            font.setScale(fontScale);
            TrueTypeContainer ttc = font.getFormattedText(stringContainer, color, outlineColor);
            this.ttc = ttc;
            oldAtlasWidth = font.getAtlas().getImage().getWidth();
            oldAtlasHeight = font.getAtlas().getImage().getHeight();
            resetLayer();
            addListener();
            getNode().attachChild(ttc);
        } else {
            ttc.getMaterial().setTexture("Texture", font.getAtlas());
            if (oldAtlasWidth != font.getAtlas().getImage().getWidth()
                    || oldAtlasHeight != font.getAtlas().getImage().getHeight()) {
                oldAtlasWidth = font.getAtlas().getImage().getWidth();
                oldAtlasHeight = font.getAtlas().getImage().getHeight();
                font.setScale(fontScale);
                ttc.updateGeometry();
            }
        }
    }
    
    @Override
    public void attach(GuiControl parent) {
        super.attach(parent);
        attach();
    }
    
    @Override
    public void detach(GuiControl parent) {
        if (ttc != null)
            getNode().detachChild(ttc);
        
        removeListener();
        
        super.detach(parent);
    }
    
    public void removeListener() {
        if (atlasListener != null) {
            font.removeAtlasListener(atlasListener);
            atlasListener = null;
        }
    }
    
    public void addListener() {
        if (atlasListener == null) {
            atlasListener = (assetManager, oldWidth, oldHeight, newWidth,
                    newHeight, ttf) -> {
                if (ttc == null)
                    return;

                if (oldWidth != newWidth || oldHeight != newHeight) {
                    font.setScale(fontScale);
                    ttc.updateGeometry();
                }

                ttc.getMaterial().setTexture("Texture", ttf.getAtlas());

                oldAtlasWidth = newWidth;
                oldAtlasHeight = newHeight;
            };
            font.addAtlasListener(atlasListener);
        }
    }
    
    @Override
    public void reshape(Vector3f pos, Vector3f size) {
        float w = Math.min(size.x, getMaxWidth() <= 0 ? Float.MAX_VALUE : getMaxWidth());
        float h = Math.min(size.y, getMaxHeight() <= 0 ? Float.MAX_VALUE : getMaxHeight());
        
        Rectangle textBox;
        if (getOffset() != null) {
            float effectiveZ = Math.max(0, getOffset().z);
            if (ttc != null)
                ttc.setLocalTranslation(pos.x + getOffset().x, pos.y + getOffset().y, pos.z + effectiveZ);
            size.x -= Math.abs(getOffset().x);
            size.y -= Math.abs(getOffset().y);
            size.z -= Math.abs(getOffset().z);
            pos.z += Math.abs(getOffset().z);
            
            textBox = new Rectangle(pos.x + getOffset().x, pos.y + getOffset().y, w, h);
        } else {
            if (ttc != null)
                ttc.setLocalTranslation(pos.x, pos.y, pos.z);
            
            textBox = new Rectangle(pos.x, pos.y, w, h);
        }
        
        stringContainer.setTextBox(textBox);
        font.setScale(fontScale);
        stringContainer.getLines();
        if (stringContainer.getNumNonSpaceCharacters() <= 0) {
            if (ttc != null) {
                getNode().detachChild(ttc);
                removeListener();
                ttc = null;
            }
            return;
        }
        if (ttc == null) {
            /*ttc = null;
            TrueTypeContainer ttc = font.getFormattedText(stringContainer, color, outlineColor);
            this.ttc = ttc;
            resetLayer();
            oldAtlasWidth = font.getAtlas().getImage().getWidth();
            oldAtlasHeight = font.getAtlas().getImage().getHeight();*/
            attach();
            
            if (getOffset() != null) {
                ttc.setLocalTranslation(pos.x + getOffset().x, pos.y + getOffset().y,
                        (pos.z - Math.abs(getOffset().z)) + Math.max(0, getOffset().z));
            } else {
                ttc.setLocalTranslation(pos.x, pos.y, pos.z);
            }
        } else
            ttc.updateGeometry();
    }
    
    @Override
    public void calculatePreferredSize(Vector3f size) {
        Rectangle textBox = stringContainer.getTextBox();
        float maxWidth = getMaxWidth() <= 0 ? Float.MAX_VALUE : getMaxWidth();
        float maxHeight = getMaxHeight() <= 0 ? Float.MAX_VALUE : getMaxHeight();
        if (stringContainer.getTextBox().width != maxWidth
                || stringContainer.getTextBox().height != maxHeight) {
            stringContainer.setTextBox(new Rectangle(textBox.x, textBox.y, maxWidth, maxHeight));
            //textBox = stringContainer.getTextBox();
        }
        
        font.setScale(fontScale);
        stringContainer.getLines();
        float x = stringContainer.getTextWidth();
        float y = stringContainer.getTextHeight();
        
        size.x = Math.max(x, size.x);
        size.y = Math.max(y, size.y);
        if (getOffset() != null) {
            size.x += Math.abs(getOffset().x);
            size.y += Math.abs(getOffset().y);
            size.z += Math.abs(getOffset().z);
        }
    }
    
    @Override
    public void adjustSize(Vector3f size, boolean prefCalculated) {
        Rectangle textBox = stringContainer.getTextBox();
        float maxWidth = getMaxWidth() <= 0 ? Float.MAX_VALUE : getMaxWidth();
        float maxHeight = getMaxHeight() <= 0 ? Float.MAX_VALUE : getMaxHeight();
        maxWidth = Math.min(size.x, maxWidth);
        //maxHeight = Math.min(size.y, maxHeight);
        
        stringContainer.setTextBox(new Rectangle(textBox.x, textBox.y, maxWidth, maxHeight));
        stringContainer.getLines();
        size.x -= stringContainer.getTextWidth();
        size.y -= stringContainer.getTextHeight();
        
        if (getOffset() != null) {
            size.x -= Math.abs(getOffset().x);
            size.y -= Math.abs(getOffset().y);
            size.z -= Math.abs(getOffset().z);
        }
    }
    
    @Override
    protected void resetLayer() {
        if (ttc != null)
            LayerComparator.resetLayer(ttc, getLayer());
    }
}
1 Like

This method automagically works with labels and buttons with no additional modifications required for those classes.

Note there is, at least one that I can recall, modification in those classes that will not be compatible with stock Lemur and that is the adjustSize method. This is a method I created which is called from the GuiControlā€™s revalidate method. This method is intended to modify the size requirements of the Element, specifically text based Elements such as labels and buttons, because they may require more or less height, due to word wrapping, if the originally requested width is not available.

Originally Lemur would obtain whatever size the Element requested and use that to calculate the size of the parent element, if the requested size was not available the Element would be forced into whatever space was available. For word wrapped text this caused a problem, because itā€™s possible that the available width was smaller than what the Element requested, but there was more height available than what was requested so the text could be wrapped. What Iā€™m doing here is giving the Element the opportunity to take in the new size availability and request more or less height than was originally requested, as it may need to adjust its height for text that is wrapped differently based on the new constraints, allowing the parent element to resize again if possible and/or necessary.

ā€¦and then possibly againā€¦ and possibly againā€¦ forever and ever.

The bloom of complexity that iterative layouts cause is enormous. Just look at Swingā€™s need to have getMinimumSize(), getPreferredSize(), and getMaximumSize()ā€¦ and still arbitrarily ignore them.

I chose non-iterative layouts on purpose because 9 times out of 10 there is a more declarative way to fix the problem. It wonā€™t cover all possible UI use-cases but it should cover most game UI use-cases. And is a ton simpler.

Not poo-pooing your idea in general but it is unlikely Lemur will switch to an iterative layout model as it is much more complex than just this change.

Edit: so is yours just a two-pass layout then?

It does calculatePreferredSize then adjustSize then reshape, it doesnā€™t do it again and again.

I mean the only way it would work, originally, with wrapping text would be to specify absolute sizes or not use text wrapping because it would never take into account the new height requirement. Text might wrap, but the text would go outside the bounds of the element because the height would remain whatever was requested before the text was wrapped.

I have on my to-do list to create the equivalent of a swing TextArea. In that case, the TextArea would have a ā€˜preferred widthā€™ that could be set to let the text expand vertically as needed to fit the preferred width. This covers most of the use-cases I could think of without complicating the layout.

Not sure if it would cover your use case or not.

1 Like

Probably not. In the long run I want this to allow me to create a single UI that could work on multiple platforms with multiple densities. For instance I might pick a point size, but that size will be scaled at runtime based on screen density, for mobiles, resulting in text that will span different widths depending on the device itā€™s running on so it becomes necessary for the layout to calculate the space requirement/availability because Iā€™m not going to know it in advance and thus would not be able to accurately predict what to set for preferred size.

1 Like

What do you do in the case where labels compete vertically for space because of their wrapping?

Depends on the layout settings. Each label requests whatever width is needed to display the supplied text without wrapping it. Then they are told what the available width is so they all wrap if necessary and request a new height. The layout adjusts its height to accommodate, if it can, to what is requested or the maximum available, whichever is smaller. Then the layout adjusts the height of all the labels, and all elements, expanding and contracting according to the supplied settings such as EVEN, FIRST, PROPORTIONAL, etcetera. If a given labelā€™s new height is less than is needed the text is clipped at the point where it exceeds the available space.

I donā€™t think this can be done with BitmapFonts, but TrueTypeFont allows for clipping text after wrapping it so you might have a single line wrapped to six lines, but clipped at the end of the third line.

1 Like

jME-TrueTypeFont has been updated to version 1.2 which includes a few important bug fixes and some new features. @teique thought you might like to know about it. You can grab the new version from the link in the original post.

New features:
-The ability to lock the texture atlas through TrueTypeFont.lockAtlas(boolean). When locked the texture atlas will not be updated with new characters. This is intended for those who donā€™t want to mess with keeping track of updated textures with atlasListeners. If a character not in the atlas is requested while the atlas is locked it will be replaced with the default character which is added to the atlas in the constructor.

After instantiating a TrueTypeFont add all the characters you want to be able to use such as:
TrueTypeFont.getBitmapGlyphs("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890.,");
Then:
TrueTypeFont.lockAtlas(true);

-You can now set a maximum resolution for the texture atlas, default 2048, to prevent the atlas from becoming larger than the video card supports. Once the max is reached all characters not in the atlas will be replaced with the default character.

-The StringContainer now allows for setting an offset. Characters in the string before the offset will not be displayed.

Bug Fixes:
-WrapModes Word and WordClip now properly account for leading/trailing spaces and line breaks.

-The WordClip WrapMode had a problem where it would repeat the same line multiple times if the word on that line was longer than the available space. The word is now properly wrapped by cutting it down to size and continuing the rest of the word on the following line(s).

P.S. Iā€™ll probably have the source to a modified Lemur that can use jME-TrueTypeFont available soon, gonna add a few bells and whistles and should probably do some more stress testing especially on the TextField. Getting the cursor and carat to track changes properly on word wrapped text was somewhat of a bitch, but pretty sure I nailed it down good and tight.

3 Likes

Cool! How about ligature support, like on Firacode?

1 Like

I donā€™t see why not, not sure how many use cases there would be though, maybe a hacking game or something. What this library does is create a Java canvas and draw glyphs to it keeping track of what glyphs have been drawn and where they are in the texture. For what youā€™re suggesting it would need to, you know, draw the glyphs as usual, but for certain glygh combinations instead draw a series of lines thus creating shapes corresponding to arrows, elongated equal signs etceteraā€¦

1 Like

Cool font rendering, like this:

1 Like

honestly I prefer the not ligatured font.
Anyway this is just a matter of taste.

Ligatures are very cool: http://www.sansbullshitsans.com

4 Likes

jME-TrueTypeFont has been updated again to version 1.21 which fixes a few bugs. Some more issues with spaces and word wrapping, an issue causing incorrect center/right alignment on some lines in word and wordclip wrap modes and an issue that could cause glyphs to display a small amount of the glyph stored in the atlas to the immediate right.

4 Likes

By the by another quick update to 1.22 was uploaded today. It just fixes a rare issue where the ellipses could extend beyond the defined bounds in WordClip text. The update is available from the links on the jME-TrueTypeFont page, but I didnā€™t mention it on my website. To be honest Weebly is kind of a pain in the ass, but better than nothing.

Weebly says their in browser editor is better than FTP access, which they donā€™t support, but I would heartily disagree.

1 Like

And the download button is in the upper right, and not clicking on the file (what let us browse itā€™s contents), I always fall on that trick :sweat_smile:

EDIT: btw, I always create the -sources.jar file as whenever I am in debug I like to see what is happening on every library (funny enough JME itself doesnt provide the sources for many EDIT:ā€œnon-jme libsā€ and I have a hard time trying to find them on the internet heheā€¦)

Haha yeah, I always found that bit on Google Drive a bit confusing too.

For the uninitiated the link on my page leads to the file stored on Google Drive and itā€™s a zip file. When you click the link rather than just start downloading or take you to a page with a download link Google Drive displays the contents of the zip file where you can browse and download individual items from the zip. To download the whole zip file you have to click the little down arrow icon at the top right of the page:

I used to use a cloud storage service that allowed direct links with no intermediary page. Actually I used two, first I used Ubuntu One, which was fucking awesome by the way, when they shut down I switched to Copy.com who, themselves, have recently closed their doors so I had to move back to Google Drive :frowning:

1 Like