浏览代码

solve issue #1158 (enabling Right-to-Left fonts) (#1685)

* Display Right to Left text correctly (Issue #1158).
Some parts need testing

* Just a suggestion to set the Right to Left text parameter with the font. Not sure if that will break the import from assetmanager.loadFont

* Display Right to Left text correctly (Issue #1158).
Some parts need testing

* Edited out the reverse function of the textinput in BitmapText.
If the boolean RtL should not be set via Bitmaptext, te whole class could be unchanged

* Changed the behaviour of RtL text without a textblock

* Updating the linewidth dor correct calculation if unbounded.

* Changed the behaviour of how xoffset is treated while calculating x0 for RtL text. (it was deducted twice before)
Still not sure if it is correct that way.

* Edited out the reverse function of the textinput in BitmapText.
If the boolean RtL should not be set via Bitmaptext, te whole class could be unchanged

* validateSize() which is used in BitMapText.getLineWidth was changed to have the same result for LtR texts as it is given by BitMapFont.getLineWidth.

* Merged with the adaptions and changes of Ali-RS. BitmapText.getLineWidth() and BitmapFont.getLineWidth() will now have the same results.
A few open ToDo and tests have been solvd/closed.

* Removed ToDos no longer needed due to the last changes

* Detected small (incorrect) changes while comparing, fixed

* Delete local.properties

* Get back local.properties

* Fix javadoc & formatting issues.

* Added license to GlyphParser.

* BitmapFont: serialize "rightToLeft" boolean field.

* Made GlyphParser savable.

* Deprecate BitmapText(BitmapFont font, boolean rightToLeft). The "rightToLeft" should be specified in the font.

* Fixed alignment issue where Align.Right was not working when vertical alignment was set to anything other than VAlign.Top also made some code cleanup.

* GlyphParser:added link to an example implementation for Persian text.

Co-authored-by: Ali-RS <[email protected]>
Co-authored-by: Stephen Gold <[email protected]>
Aufricer 3 年之前
父节点
当前提交
118b6c8a29

+ 79 - 26
jme3-core/src/main/java/com/jme3/font/BitmapFont.java

@@ -33,6 +33,7 @@ package com.jme3.font;
 
 import com.jme3.export.*;
 import com.jme3.material.Material;
+
 import java.io.IOException;
 
 /**
@@ -88,6 +89,24 @@ public class BitmapFont implements Savable {
 
     private BitmapCharacterSet charSet;
     private Material[] pages;
+    private boolean rightToLeft = false;
+    // For cursive bitmap fonts in which letter shape is determined by the adjacent glyphs.
+    private GlyphParser glyphParser;
+
+    /**
+     * @return true, if this is a right-to-left font, otherwise it will return false.
+     */
+    public boolean isRightToLeft() {
+        return rightToLeft;
+    }
+
+    /**
+     * Specify if this is a right-to-left font. By default it is set to false.
+     * This can be "overwritten" in the BitmapText constructor.
+     */
+    public void setRightToLeft(boolean rightToLeft) {
+        this.rightToLeft = rightToLeft;
+    }
 
     public BitmapFont() {
     }
@@ -123,7 +142,23 @@ public class BitmapFont implements Savable {
     public BitmapCharacterSet getCharSet() {
         return charSet;
     }
-    
+
+    /**
+     * For cursive fonts a GlyphParser needs to be specified which is used
+     * to determine glyph shape by the adjacent glyphs. If nothing is set,
+     * all glyphs will be rendered isolated.
+     */
+    public void setGlyphParser(GlyphParser glyphParser) {
+        this.glyphParser = glyphParser;
+    }
+
+    /**
+     * @return The GlyphParser set on the font, or null if it has no glyph parser.
+     */
+    public GlyphParser getGlyphParser() {
+        return glyphParser;
+    }
+
     /**
      * Gets the line height of a StringBlock.
      *
@@ -156,6 +191,8 @@ public class BitmapFont implements Savable {
         OutputCapsule oc = ex.getCapsule(this);
         oc.write(charSet, "charSet", null);
         oc.write(pages, "pages", null);
+        oc.write(rightToLeft, "rightToLeft", false);
+        oc.write(glyphParser, "glyphParser", null);
     }
 
     @Override
@@ -165,6 +202,8 @@ public class BitmapFont implements Savable {
         Savable[] pagesSavable = ic.readSavableArray("pages", null);
         pages = new Material[pagesSavable.length];
         System.arraycopy(pagesSavable, 0, pages, 0, pages.length);
+        rightToLeft = ic.readBoolean("rightToLeft", false);
+        glyphParser = (GlyphParser) ic.readSavable("glyphParser", null);
     }
 
     public float getLineWidth(CharSequence text){
@@ -208,8 +247,10 @@ public class BitmapFont implements Savable {
         boolean firstCharOfLine = true;
 //        float sizeScale = (float) block.getSize() / charSet.getRenderedSize();
         float sizeScale = 1f;
-        for (int i = 0; i < text.length(); i++){
-            char theChar = text.charAt(i);
+        CharSequence characters = glyphParser != null ? glyphParser.parse(text) : text;
+
+        for (int i = 0; i < characters.length(); i++){
+            char theChar = characters.charAt(i);
             if (theChar == '\n'){
                 maxLineWidth = Math.max(maxLineWidth, lineWidth);
                 lineWidth = 0f;
@@ -217,38 +258,50 @@ public class BitmapFont implements Savable {
                 continue;
             }
             BitmapCharacter c = charSet.getCharacter(theChar);
-            if (c != null){
-                if (theChar == '\\' && i<text.length()-1 && text.charAt(i+1)=='#'){
-                    if (i+5<text.length() && text.charAt(i+5)=='#'){
-                        i+=5;
+            if (c != null) {
+                if (theChar == '\\' && i < characters.length() - 1 && characters.charAt(i + 1) == '#') {
+                    if (i + 5 < characters.length() && characters.charAt(i + 5) == '#') {
+                        i += 5;
                         continue;
-                    }else if (i+8<text.length() && text.charAt(i+8)=='#'){
-                        i+=8;
+                    } else if (i + 8 < characters.length() && characters.charAt(i + 8) == '#') {
+                        i += 8;
                         continue;
                     }
                 }
-                if (!firstCharOfLine){
-                    lineWidth += findKerningAmount(lastChar, theChar) * sizeScale;                    
+                if (!firstCharOfLine) {
+                    lineWidth += findKerningAmount(lastChar, theChar) * sizeScale;
                 } else {
-                    // The first character needs to add in its xOffset, but it
-                    // is the only one... and negative offsets = positive width
-                    // because we're trying to account for the part that hangs
-                    // over the left.  So we subtract. 
-                    lineWidth -= c.getXOffset() * sizeScale;                    
+                    if (rightToLeft) {
+                        // Ignore offset, so it will be compatible with BitmapText.getLineWidth().
+                    } else {
+                        // The first character needs to add in its xOffset but it
+                        // is the only one... and negative offsets = positive width
+                        // because we're trying to account for the part that hangs
+                        // over the left.  So we subtract.
+                        lineWidth -= c.getXOffset() * sizeScale;
+                    }
                     firstCharOfLine = false;
                 }
                 float xAdvance = c.getXAdvance() * sizeScale;
-                
-                // If this is the last character, then we really should have
-                // only added its width.  The advance may include extra spacing
+
+                // If this is the last character of a line, then we really should
+                // have only added its width. The advance may include extra spacing
                 // that we don't care about.
-                if (i == text.length() - 1) {
-                    lineWidth += c.getWidth() * sizeScale;
- 
-                    // Since the width includes the xOffset then we need
-                    // to take it out again by adding it, ie: offset the width
-                    // we just added by the appropriate amount.
-                    lineWidth += c.getXOffset() * sizeScale;                      
+                if (i == characters.length() - 1 || characters.charAt(i + 1) == '\n') {
+                    if (rightToLeft) {
+                        // In RTL text we move the letter x0 by it's xAdvance so
+                        // we should add it to lineWidth
+                        lineWidth += xAdvance;
+                        // Then we move letter by it's xOffset.
+                        // Negative offsets = positive width.
+                        lineWidth -= c.getXOffset() * sizeScale;
+                    } else {
+                        lineWidth += c.getWidth() * sizeScale;
+                        // Since the width includes the xOffset then we need
+                        // to take it out again by adding it, ie: offset the width
+                        // we just added by the appropriate amount.
+                        lineWidth += c.getXOffset() * sizeScale;
+                    }
                 } else {                 
                     lineWidth += xAdvance;
                 }

+ 7 - 2
jme3-core/src/main/java/com/jme3/font/BitmapText.java

@@ -53,9 +53,14 @@ public class BitmapText extends Node {
     private Letters letters;
 
     public BitmapText(BitmapFont font) {
-        this(font, false, false);
+        this(font, font.isRightToLeft(), false);
     }
 
+    /**
+     * @deprecated The "rightToLeft" flag should be specified in the font.
+     * Use {@link BitmapText#BitmapText(com.jme3.font.BitmapFont)}
+     */
+    @Deprecated
     public BitmapText(BitmapFont font, boolean rightToLeft) {
         this(font, rightToLeft, false);
     }
@@ -138,7 +143,6 @@ public class BitmapText extends Node {
      */
     public void setText(String text) {
         text = text == null ? "" : text;
-
         if (text == block.getText() || block.getText().equals(text)) {
             return;
         }
@@ -268,6 +272,7 @@ public class BitmapText extends Node {
         if (textBox != null) {
             return Math.max(letters.getTotalWidth(), textBox.width);
         }
+      //  Please note that BitMaptext.getLineWidth() might differ from Font.getLineWidth() -->   scale it with Font.getPreferredSize()/BitMaptext.getSize()
         return letters.getTotalWidth();
     }
 

+ 50 - 0
jme3-core/src/main/java/com/jme3/font/GlyphParser.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2009-2021 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.font;
+
+import com.jme3.export.Savable;
+
+/**
+ * Used for selecting character shape in cursive bitmap text. In cursive scripts,
+ * the appearance of a letter changes depending on its position:
+ * isolated, initial (joined on the left), medial (joined on both sides)
+ * and final (joined on the right) of a word.
+ *
+ * For an example implementation see: https://github.com/Ali-RS/JME-PersianGlyphParser
+ *
+ * @author Ali-RS
+ */
+public interface GlyphParser extends Savable {
+
+    public CharSequence parse(CharSequence text);
+
+}

+ 65 - 31
jme3-core/src/main/java/com/jme3/font/LetterQuad.java

@@ -170,7 +170,12 @@ class LetterQuad {
         if (bound == null) {
             return false;
         }
-        return x0 > 0 && bound.x+bound.width-gap < getX1();
+        if (isRightToLeft()) {
+            return x0 <0 && x0<bound.x;
+            // ToDo check for ellipsis, not sure if it is on both sides of a character and need to be deducted as well
+        } else {
+            return x0 > 0 && bound.x+bound.width-gap < getX1();
+        }
     }
     
     void clip(StringBlock block) {
@@ -182,6 +187,7 @@ class LetterQuad {
         // to the string block
         float x1 = Math.min(bound.x + bound.width, x0 + width);
         float newWidth = x1 - x0;
+        if (isRightToLeft()) newWidth = x1; // only the available space to the left
         if( newWidth == width )
             return;
             
@@ -195,13 +201,12 @@ class LetterQuad {
     }
 
     float getX1() {
-        return x0+width;
+        return x0 + width;
     }
-    
     float getNextX() {
-        return x0+xAdvance;
+        return rightToLeft ? x0 - xAdvance : x0 + xAdvance;
     }
-    
+
     float getNextLine() {
         return lineY+LINE_DIR*font.getCharSet().getLineHeight() * sizeScale;
     }
@@ -314,6 +319,9 @@ class LetterQuad {
 
         if (isHead()) {
             x0 = getBound(block).x;
+            if (isRightToLeft() && getBound(block) != UNBOUNDED) {
+                x0 += getBound(block).width;
+            }
             y0 = lineY;
             width = 0;
             height = 0;
@@ -333,6 +341,7 @@ class LetterQuad {
             xAdvance = width;
         } else if (bitmapChar == null) {
             x0 = getPrevious().getX1();
+            if (rightToLeft) x0 = getPrevious().getX0();
             y0 = lineY;
             width = 0;
             height = 0;
@@ -347,30 +356,56 @@ class LetterQuad {
             float kernAmount = 0f;
 
             if (previous.isHead() || previous.eol) {
-                x0 = bound.x;
-                
-                // The first letter quad will be drawn right at the first
-                // position... but it does not offset by the characters offset
-                // amount.  This means that we've potentially accumulated extra
-                // pixels, and the next letter won't get drawn far enough unless
-                // we add this offset back into xAdvance, by subtracting it.
-                // This is the same thing that's done below because we've
-                // technically baked the offset in just like below.  It doesn't
-                // look like it at first glance, so I'm keeping it separate with
-                // this comment.
-                xAdvance -= xOffset * incrScale; 
-                
+                if (rightToLeft) {
+                    // In RTL text we advance toward left by the letter xAdvance. (subtract xAdvance)
+                    // Note, positive offset will move the letter quad toward right and negative offset
+                    // will move it toward left.
+                    if (previous.isHead()) {
+                        x0 = previous.getNextX() - xAdvance - xOffset * incrScale;
+                    } else if (previous.eol) {
+                        // For bounded bitmap text the first letter of a line is always
+                        // on the right end of the textbox and for unbounded bitmap text
+                        // we start from the x=0 and advance toward left.
+                        x0 = getBound(block).x + (getBound(block) != UNBOUNDED ? getBound(block).width : 0) - xAdvance - xOffset * incrScale;
+                    }
+                    // Since x0 has xAdvance baked into it, we need to zero out xAdvance.
+                    // Since x0 will have offset baked into it, we need to counteract that
+                    // in xAdvance. The next x position will be (x0 - xAdvance).
+                    xAdvance = -xOffset * incrScale;
+                } else {
+                    x0 = bound.x;
+
+                    // The first letter quad will be drawn right at the first
+                    // position, but it does not offset by the character's offset
+                    // amount.  This means that we've potentially accumulated extra
+                    // pixels, and the next letter won't get drawn far enough unless
+                    // we add this offset back into xAdvance, by subtracting it.
+                    // This is the same thing that's done below, because we've
+                    // technically baked the offset in just like below.  It doesn't
+                    // look like it at first glance, so I'm keeping it separate with
+                    // this comment.
+                    xAdvance -= xOffset * incrScale;
+                }
             } else {
-                x0 = previous.getNextX() + xOffset * incrScale;
-                
-                // Since x0 will have offset baked into it then we
-                // need to counteract that in xAdvance.  This is better
-                // than removing it in getNextX() because we also need
-                // to take kerning into account below... which will also
-                // get baked in.
-                // Without this, getNextX() will return values too far to
-                // the left, for example.
-                xAdvance -= xOffset * incrScale; 
+               if (isRightToLeft()) {
+                   // For RTL text the xAdvance of the current letter is deducted,
+                   // while for LTR text the advance of the letter before is added.
+                   x0 = previous.getNextX() - xAdvance - xOffset * incrScale;
+                   // Since x0 has xAdvance baked into it, we need to zero out xAdvance.
+                   // Since x0 will have offset baked into it we need to counteract that
+                   // in xAdvance. The next x position will be (x0 - xAdvance)
+                   xAdvance = - xOffset * incrScale;
+                } else {
+                    x0 = previous.getNextX() + xOffset * incrScale;
+                   // Since x0 will have offset baked into it, we
+                   // need to counteract that in xAdvance.  This is better
+                   // than removing it in getNextX() because we also need
+                   // to take kerning into account below, which will also
+                   // get baked in.
+                   // Without this, getNextX() will return values too far to
+                   // the left, for example.
+                   xAdvance -= xOffset * incrScale;
+                }
             }
             y0 = lineY + LINE_DIR*yOffset;
 
@@ -379,12 +414,11 @@ class LetterQuad {
             if (lastChar != null && block.isKerning()) {
                 kernAmount = lastChar.getKerning(c) * sizeScale;
                 x0 += kernAmount * incrScale;
-                
-                // Need to unbake the kerning from xAdvance since it
+                 // Need to unbake the kerning from xAdvance since it
                 // is baked into x0... see above.
                 //xAdvance -= kernAmount * incrScale;
                 // No, kerning is an inter-character spacing and _does_ affect
-                // all subsequent cursor positions. 
+                // all subsequent cursor positions.
             }
         }
         if (isEndOfLine()) {

+ 76 - 29
jme3-core/src/main/java/com/jme3/font/Letters.java

@@ -73,8 +73,13 @@ class Letters {
         current = head;
         if (text != null && plainText.length() > 0) {
             LetterQuad l = head;
-            for (int i = 0; i < plainText.length(); i++) {
-                l = l.addNextCharacter(plainText.charAt(i));
+            CharSequence characters = plainText;
+            if (font.getGlyphParser() != null) {
+                characters = font.getGlyphParser().parse(plainText);
+            }
+
+            for (int i = 0; i < characters.length(); i++) {
+                l = l.addNextCharacter(characters.charAt(i));
                 if (baseColor != null) {
                     // Give the letter a default color if
                     // one has been provided.
@@ -114,7 +119,7 @@ class Letters {
         while (!l.isTail()) {
             if (l.isInvalid()) {
                 l.update(block);
-
+                // Without a textblock the next line returns always false = no textwrap at all will be applied
                 if (l.isInvalid(block)) {
                     switch (block.getLineWrapMode()) {
                     case Character:
@@ -164,6 +169,7 @@ class Letters {
                         l.clip(block);
 
                         // Clear the rest up to the next line feed.
+                        // = for texts attached to a textblock all coming characters are cleared except a linefeed is explicitly used
                         for( LetterQuad q = l.getNext(); !q.isTail() && !q.isLineFeed(); q = q.getNext() ) {
                             q.setBitmapChar(null);
                             q.update(block);
@@ -186,44 +192,80 @@ class Letters {
     }
 
     private void align() {
+        if (block.getTextBox() == null) {
+            // Without a textblock there is no alignment.
+            return;
+
+            // For unbounded left-to-right texts the letters will simply be shown starting from
+            // x0 = 0 and advance toward right as line length is considered to be infinite.
+            // For unbounded right-to-left texts the letters will be shown starting from x0 = 0
+            // (at the same position as left-to-right texts) but move toward the left from there.
+        }
+
         final Align alignment = block.getAlignment();
         final VAlign valignment = block.getVerticalAlignment();
-        if (block.getTextBox() == null || (alignment == Align.Left && valignment == VAlign.Top))
-            return;
-        LetterQuad cursor = tail.getPrevious();
-        cursor.setEndOfLine();
         final float width = block.getTextBox().width;
         final float height = block.getTextBox().height;
         float lineWidth = 0;
         float gapX = 0;
         float gapY = 0;
+
         validateSize();
         if (totalHeight < height) { // align vertically only for no overflow
             switch (valignment) {
-            case Top:
-                gapY = 0;
-                break;
-            case Center:
-                gapY = (height - totalHeight) * 0.5f;
-                break;
-            case Bottom:
-                gapY = height - totalHeight;
-                break;
+                case Top:
+                    gapY = 0;
+                    break;
+                case Center:
+                    gapY = (height - totalHeight) * 0.5f;
+                    break;
+                case Bottom:
+                    gapY = height - totalHeight;
+                    break;
             }
         }
-        while (!cursor.isHead()) {
-            if (cursor.isEndOfLine()) {
-                lineWidth = cursor.getX1()-block.getTextBox().x;
-                if (alignment == Align.Center) {
-                    gapX = (width-lineWidth)/2;
-                } else if (alignment == Align.Right) {
-                    gapX = width-lineWidth;
-                } else {
-                    gapX = 0;
+
+        if (font.isRightToLeft()) {
+            if ((alignment == Align.Right && valignment == VAlign.Top)) {
+                return;
+            }
+            LetterQuad cursor = tail.getPrevious();
+            // Temporary set the flag, it will be reset when invalidated.
+            cursor.setEndOfLine();
+            while (!cursor.isHead()) {
+                if (cursor.isEndOfLine()) {
+                    if (alignment == Align.Left) {
+                        gapX = block.getTextBox().x - cursor.getX0();
+                    } else if (alignment == Align.Center) {
+                        gapX = (block.getTextBox().x - cursor.getX0()) / 2;
+                    } else {
+                        gapX = 0;
+                    }
                 }
+                cursor.setAlignment(gapX, gapY);
+                cursor = cursor.getPrevious();
+            }
+        } else { // left-to-right
+            if (alignment == Align.Left && valignment == VAlign.Top) {
+                return;
+            }
+            LetterQuad cursor = tail.getPrevious();
+            // Temporary set the flag, it will be reset when invalidated.
+            cursor.setEndOfLine();
+            while (!cursor.isHead()) {
+                if (cursor.isEndOfLine()) {
+                    lineWidth = cursor.getX1() - block.getTextBox().x;
+                    if (alignment == Align.Center) {
+                        gapX = (width - lineWidth) / 2;
+                    } else if (alignment == Align.Right) {
+                        gapX = width - lineWidth;
+                    } else {
+                        gapX = 0;
+                    }
+                }
+                cursor.setAlignment(gapX, gapY);
+                cursor = cursor.getPrevious();
             }
-            cursor.setAlignment(gapX, gapY);
-            cursor = cursor.getPrevious();
         }
     }
 
@@ -232,7 +274,7 @@ class Letters {
             return;
         l.getPrevious().setEndOfLine();
         l.invalidate();
-        l.update(block); // TODO: update from l
+        l.update(block);
     }
 
     float getCharacterX0() {
@@ -319,10 +361,15 @@ class Letters {
     }
 
     void validateSize() {
+        // also called from BitMaptext.getLineWidth() via getTotalWidth()
         if (totalWidth < 0) {
             LetterQuad l = head;
             while (!l.isTail()) {
-                totalWidth = Math.max(totalWidth, l.getX1());
+                if (font.isRightToLeft()) {
+                    totalWidth = Math.max(totalWidth, Math.abs(l.getX0()));
+                } else {
+                    totalWidth = Math.max(totalWidth, l.getX1());
+                }
                 l = l.getNext();
             }
         }

+ 14 - 9
jme3-examples/src/main/java/jme3test/gui/TestRtlBitmapText.java

@@ -32,18 +32,17 @@
 package jme3test.gui;
 
 import com.jme3.app.SimpleApplication;
-import com.jme3.font.BitmapFont;
-import com.jme3.font.BitmapText;
-import com.jme3.font.LineWrapMode;
-import com.jme3.font.Rectangle;
+import com.jme3.app.StatsAppState;
+import com.jme3.font.*;
 
 /**
  * Test case for JME issue #1158: BitmapText right to left line wrapping not work
  */
 public class TestRtlBitmapText extends SimpleApplication {
 
-    // A right to left text.
-    final private String text = ".text left to right test a is This";
+    private String text = "This is a test right to left text.";
+    private BitmapFont fnt;
+    private BitmapText txt;
 
     public static void main(String[] args) {
         TestRtlBitmapText app = new TestRtlBitmapText();
@@ -52,14 +51,20 @@ public class TestRtlBitmapText extends SimpleApplication {
 
     @Override
     public void simpleInitApp() {
-        BitmapFont fnt = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        float x = 400;
+        float y = 500;
+        getStateManager().detach(stateManager.getState(StatsAppState.class));
+        fnt = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        fnt.setRightToLeft(true);
+
         // A right to left BitmapText
-        BitmapText txt = new BitmapText(fnt, true);
+        txt = new BitmapText(fnt);
         txt.setBox(new Rectangle(0, 0, 150, 0));
         txt.setLineWrapMode(LineWrapMode.Word);
         txt.setAlignment(BitmapFont.Align.Right);
         txt.setText(text);
-        txt.setLocalTranslation(cam.getWidth() / 2, cam.getHeight() / 2, 0);
+
+        txt.setLocalTranslation(x, y, 0);
         guiNode.attachChild(txt);
     }
 }