jME-TrueTypeFont Rendering Library

Hmm… Actually my current project is a turn based strategy with single player vs AI opponents and multi-player, up to seven, battles with no story at all. Now that I think about it I suppose I could write a campaign mode. I suppose I’ll visit that thought when I finish up the network code. The game would essentially be done when I finish the network stuff unless I decide to write a story mode which would likely add a good deal of additional development time.

1 Like

Release it with just the episode one! And have the final goal of the campaign already decided (EDIT: but keep it a secret!!!), so further episodes would walk towards it, and could make them easier to implement. I believe this may also help keep ppl interested :slight_smile:

1 Like

is there some way to transform a TrueTypeFont into a com.jme3.font.BitmapFont ?

I think it has something to do with ttf.getAtlas() and TrueTypeBitmapGlyph?

Despite I think I am “almost there”, I am still getting weird results.
I am not being able to guess what to set at some fields of BitmapCharacterSet and BitmapCharacter of BitmapFont :confused:

here is basically what I am doing (based on getBitmapGlyphs() string from your OP):
https://github.com/AquariusPower/CommandsConsoleGUI/blob/master/src/com/github/commandsconsolegui/console/gui/ConsoleGuiStateAbs.java#L2433

Rather than write a method that converts from TrueTypeFont to BitmapFont I would probably create a few classes that extend some of the BitmapFont classes such as BitmapFont and BitmapText so that they work with the TrueTypeFont methods instead. For instance BitmapText is a Node with BitmapTextPage(Geometry) children. You could override BitmapText’s assemble method so that instead of using BitmapTextPage it uses a Geometry with a TrueTypeText mesh.

Unfortunately the assemble method in BitmapText has private access so you would need to re-compile the jME source code so that BitmapText’s assemble method has public access allowing you to override it.

1 Like

Yeah, BitmapFont and BitmapText are ugly (and are slightly broken). I’ve created two interfaces instead which can be implemented by any class. They currently support all public methods of BitmapText and a factory method to create BitmapText instances via BitmapFont instances.

I think all UI code should be written against two interfaces like these. There must also be a way to query the abilities of the implementation (example 1: BitmapText and BitmapFont have less features than my BitmapText2 and BitmapFont2) (example 2: Vector-Mesh can render resolution-independent, Dist-Field-Shader can render almost resolution-independent, BitmapFont with standard shader is very resolution dependent) (example: BitmapText2 can render animated glyphs, vertical Asian text, different styles, pre-compiled formatted text, etc.).

2 Likes

@Tryder I see, basically it would pipe thru TrueTypeText; may be BitmapText could be flexibilized to accommodate tweaks one day :slight_smile:

Btw, instead of loading from a file, I was also trying to directly get the font from GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts(), so it would be a system independent way, but I think I messed at the code that should do that in a correct way hehe. Yes I have a hard time understanding fonts metrics;

@Ogli is this? looks really interesting, but I cant find a download tho, I think it supports blinking text; btw, at my command’s console, I created a fading cursor (instead of blinking), so, at your library, fading text would be cool also!

Oh yes, when trying to understand .fnt files that was pure horror :chimpanzee_closedlaugh:

Yes, no download, because it’s not finished yet and the last 4 months I’ve been working on a game framework that allows me (and probably soon others too) to develop a simple game with jME and later mass-produce new games with similar mechanics and settings.

I’m hoping to return to that font thing and finish it. But I don’t like to publish things that don’t yet fully work: Since 2014 the font stuff has seen several iterations and now is stuck in the middle of the last major iteration. Most things are coded, but I did not reach the point where it can be compiled and tested.

About the blinking and fading: Nice hint, I think it would be easy to let the user animate the color channel. It’s designed to support several “geometry shakers” already which allow users to make e.g. hopping glyphs (a “sine wave” or “scary horror shake” for example).

My custom console in the new game framework uses an old-school blinking cursor which I’ve represented by a separate BitmapText containing ‘_’ as character (of course using the jME console.fnt for the font).

So, the plan is: First finish the OGF (“Open Game Framework” or “Ogli Generic Foundation”) and finish the first game. Later in 2016 finish the BitmapFont2 / BitmapText2 library and the glyph system. I don’t know yet, but I think both will be open sourced, since I would very much appreciate input from more experienced coders and code designers too. :chimpanzee_smile:

Btw I’ve another interesting thing done: A library that grabs all meta info from the UCD (Unicode database). The problem is implementing all the algorithms that they describe in their technical documents. So it is almost useless by now - but was intended to support the BitmapText2 library. I’m hoping to reactivate that idea somewhere in the future (maybe we find someone who is eager to implement the algorithms from the Unicode standard).

I think it’s really good that @Tryder has published this font thing.
For people who need a working solution now and not in half a year, it’s perfect.
Has some pros that my text library probably will not have.
Might work in tandem with the glyph system one day (seems to be an achievable goal - since glyphs are an abstract concept - whether they’re 3d meshes or 2d quads makes no significant difference…). :chimpanzee_smile:

I’ve updated the jME-TrueTypeFont library with a few new features. First and foremost I have parted out the jME-TrueTypeFont and jME-TrueTypeFontLite libraries so that the jME-TrueTypeFontLite code is no longer part of jME-TrueTypeFont. The old jME-TrueTypeFont library is now jME-TrueType3d while jME-TrueTypeFontLite is now jME-TrueTypeFont and jME-TrueType3d depends on the new jME-TrueTypeFont library.

The jME-TrueTypeFont library now has improved glyph rendering quality through the use of sub-pixel sampling and now includes formatting options. An area of the screen can be defined via a com.jme3.font.Rectangle and the text can be aligned vertically/horizontally left, right, top, bottom and center. Additionally text can be wrapped according to the Rectangle’s specifications. Wrapping can be constrained by the width and optionally the height of the Rectangle.

Wrap modes include Clip which will display a single line cut off when it reaches the edge of the Rectangle and a supplied ellipsis appended to the end. Char wraps the text breaking words where necessary, Word wraps the text between words, CharClip and WordClip do the same as Char and Word, but also constrain the text to the height of the Rectangle. Lines that fall outside of the clipping area are removed and the last visible line has a supplied ellipsis appended to the end.

The below image shows white 22pt text with a grey outline Left/Top aligned using WordClip wrapping so it is cut off at the end with the default ellipsis appended:

Documentation on my web-site has been updated to reflect these changes: 1337 Gallery - jME-TTF

@teique I’m not sure if you’re still using this package or not, but I figured I’d tag you just in case :slight_smile:

3 Likes

@teique By the by I thought about working these changes so they would more closely mirror jME’s built-in bitmap font classes, but I found jME’s built in font system appears a bit overly complicated and didn’t see a need to bring that complication over to this package.

I took a quick look at the Lemur source code and it looks as though, if you were using Lemur, it wouldn’t be terribly complicated to modify Lemur’s Label class to work with jME-TrueTypeFont.

Just make sure that, if you modify Lemur’s Labels, you are sure to register a TTF_AtlasListener with the Label’s TrueTypeFont so that you update the texture on your geometry whenever the texture is modified and if the texture has been resized, oldWidth != newWidth || oldHeight != newHeight, you update your Geometry, TrueTypeContainer.updateGeometry(), because the UVs will have changed. Then be sure to remove the listener when the Label is detached from the scene.

1 Like

To integrate with Lemur, you’d create a new kind of TextComponent that deals with this class instead of BitmapText. And it has life cycle methods for attach/detach/resize, etc… Hope that info is helpful.

3 Likes

@pspeed but these fields are not accessible, in other words, I would have to maintain a fork about a code that I barely understand, or not? btw, I wonder if the Label’s “text” (TextComponent) could be exposed/pluggable? or event better, it could request such TextComponent from a factory, so we would prepare such factory with our custom one, to feed to all elements!
Or… as a shortcut, I could just prepare a BitmapFont using JME-TrueTypeFont at the styles, in some way, so it could be used by Label at this point.

@Tryder by using the shortcut above, I am not sure but I think it would remove the dynamicity of JME-TrueTypeFont usage right? I understand that using it directly at a new TextComponent, it would work better/faster in case we are want any effects that require a non static font (so such effects would just required a modified glyph and not the entire font map).

Not sure what you’d have to fork or what fields are inaccessible. You can already replace a label’s text component. It doesn’t solve the ‘create with this component’ problem but it would at least let you reuse label.

The thing is that many of the default GUI elements are just thin wrappers around their base components. So in this case Label is designed to be a thin wrapper around a TextComponent. If you had some other type of component then another option is to create a different GUI element that is a thin wrapper around that one. Maybe not the best way but it is another way. I guess it prevents it from being used as a base for Button, though.

Anyway, there are many options. I’m not sure that a TextComponent factory is the best one. It would be easier to expose the text component as a style element and just make it easier to replace that way.

Edit: and note: I only mentioned Lemur integration because some one else did and might have made it sound more complicated than it needed to be.

1 Like
Offtopic about Lemur gui

I dont get it? it is private here, and instanced here, and I cant find a method to overwrite that field value with a custom/extended one using this TTF lib.

Oh cool ok!

So, considering we have (ex.) a cell renderer for the listbox, I can basically do anything customized. I could then create my ButtonTTF, with my LabelTTF, with my TextComponentTTF (at this point it is almost a fork I guess), but still I would have to keep checking improvements you make at Label, TextComponent, Button (as I dont know how some things work) and try to adapt them from time to time :slight_smile:

This sounds interesting!

Yes, you are right. The TextComponent can be replaced in the render stack for the GUI element but the label will still be dealing with the wrong one.

I will think some more on that.

1 Like

I had been working on a modified version of Lemur that supports both TrueTypeFont and BitmapFont, but I’ve been busy with other things lately, not the least of which being Tropico 4 thanks to @pspeed mentioning it in another thread, so I haven’t been doing a whole lot of programming.

I just started revisiting it and am still working on converting the TextEntryComponent to allow TrueTypeFonts, but if my TextComponent classes aren’t too long I’ll post them below.

1 Like
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?