Lemur Text Wrapped Label or TextArea (with scroll bar)

It was about a year ago that I posted a request for text wrapped Lemur Labels, and there was a comment (by pspeed) about making something similar to a JTextArea.

Has there been any advance on this? I have come to a stage in my project where I could really use a TextArea type GUI element (with a scroll bar like JTextArea for oversized texts).

I have built my entire application GUI with Lemur, and have had no problem whatsoever, but I am currently lacking this one element to get that little bit closer to finished.

Is there something available in LemurProto? Or should I just make a work-around for now?

blush … The label element is in-fact wrapping. Maybe that will do for the moment. I’ll see how I get on. I can’t see at the moment how to control the wrapping.

Goodness … it even handles “\n” character returns. Cool bananas !!

Yeah, the only thing you don’t get is a scroll bar.

I was originally going to repurpose a bunch of the TextField code to be read-only basically. It doesn’t directly support a scroll bar but it does support indexing into the text by line.

Sorry I didn’t get to this sooner.

So… Is there going to be a way in the futur?

I mean, since lemur is working with nodes and stuff, is there anyway we could have a very long panel, move it along x or y coordinates and just cull everything that is outside the border of another panel?

Just a though…

Yes, there will be a way in the future. JME does not expose this sort of clipping (it kind of hacked it in for Nifty but the rest of us go hungry)… but there has been at least one proposed code solution for it. It just was a bit invasive.

In the mean time, the only way to have panel clipping as you describe is to create a viewport (totally doable at the app level but hard/impossible at the ‘inside a UI liv’ level). Or to hack a shader to build clipping into it… ie: discard all pixels outside some range. Lemur has tried really hard to use only JME stock shaders.

The TextArea I had in mind would have split the text up into lines and used a grid panel + scroll bar like ListBox does. In fact, splitting text and dumping it into a ListBox is a viable solution even for users until I get off by butt and implement something. :smile:

There will be a solution. I can’t promise when yet and I feel bad about it.

And to be clear here… it doesn’t mean the solution was wrong just that it required more scrutiny and so hasn’t been applied yet. I think it’s probably pretty close to the right solution but my recollection is that it missed one or two things. I may have to take a look again because having a true scroll panel container would make a lot of things way easier and better.

I have a question about laying out a GUI with a large text area in the center.

As per my code, when the text is laid out in the SpringGridLayout without the following row of buttons, it looks nice. When the next row is added, which is a button bar, the bar covers the text. Any ideas of how to improve this?

Maybe a BorderLayout would be better than the SpringGridLayout, since I could add the text to the BorderLayout.South position.

Here is the code:

public class AS_Level01 extends AbstractAppState {

    private Node guiNode;
    private Main main;
    float screenWidth, screenHeight;
    
    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        main = (Main)app;
        guiNode = main.getGuiNode();
        screenWidth = (float)main.getScreenWidth();
        screenHeight = (float)main.getScreenHeight();
        
        SpringGridLayout lvlWinLayout = new SpringGridLayout(Axis.X, Axis.Y, FillMode.Even, FillMode.None);
        Container lvlWindow = new Container(lvlWinLayout);
        lvlWindow.setBackground(new QuadBackgroundComponent(ColorRGBA.Blue, 5, 5));
        Vector3f winDim = new Vector3f(screenHeight, screenHeight * 0.75f, 0f);
        lvlWindow.setPreferredSize(winDim);
        lvlWindow.setLocalTranslation(
            (main.getScreenWidth() - winDim.x) / 2, 
            main.getScreenHeight() / 2 + winDim.y / 2,    
            //main.getScreenHeight() - (main.getScreenHeight() - winDim.y) / 2, 
            // h - (h/2 - d/2) = h/2 + d/2
            10f
        );
        guiNode.attachChild(lvlWindow);
        
        // ADD LABEL
        Label label = new Label("Challenge # 1");
        label.setColor(ColorRGBA.Yellow);
        label.setTextHAlignment(HAlignment.Center);
        label.setFontSize(50);
        lvlWinLayout.addChild(0, 0, label);
        
        String objectives = ""
                + "Roses are red, violets are blue.  Some poems rhyme and some don't. \n "
                + "And also ... \n"
                + "Roses are red and violets are blue.  When it rains, I think of you.  Drip, Drip, Drip. \n \n"
                + "Some say the world will end in fire, some say in ice.  From what I've tasted of desire, "
                + "I hold with those who favor fire.  But if it had to perish twice, I know that for destruction "
                + "ice is also great, and will suffice.\n\n"
                + "Sam I am, I am Sam.  Do you like green eggs and ham? "
                + "Do you like them in a box?  Do you like them with a fox? \n\n"
                + "You need high points to win the game.  Low points will only make you lose miserably.  ";

        Label lab1 = new Label(objectives);
        lab1.setFontSize(16);
        lab1.setColor(ColorRGBA.White);
        lab1.setTextHAlignment(HAlignment.Center);
        lvlWinLayout.addChild(0, 1, lab1);
        
        // BUTTON CONTAINER
        
        // UNCOMMENT THESE LINES TO SEE THE LAYOUT PROBLEM WHEN USING A LARGE TEXT AREA.
        
//        SpringGridLayout btnLayout = new SpringGridLayout(Axis.X, Axis.Y, FillMode.Even, FillMode.None);
//        Container btnBar = new Container(btnLayout);
//        btnBar.setBackground(new QuadBackgroundComponent(ColorRGBA.Cyan, 5, 5));
//        lvlWinLayout.addChild(0, 2, btnBar);
//        
//        Vector3f btnSize = new Vector3f(50f, 50f, 0f);
//        float fontSize = 30;
//        Button backBtn = new Button("Back");
//        backBtn.setFontSize(fontSize);
//        backBtn.setTextHAlignment(HAlignment.Center);
//        backBtn.setTextVAlignment(VAlignment.Center);
//        backBtn.setPreferredSize(btnSize);
//        btnLayout.addChild(0, 0, backBtn);
//        
//        Panel spacer = new Panel();
//        spacer.setPreferredSize(btnSize);
//        spacer.setBackground(new QuadBackgroundComponent(ColorRGBA.Cyan, 5, 5));
//        btnLayout.addChild(1, 0, spacer);
//        btnLayout.addChild(2, 0, spacer);
//        
//        Button acceptBtn = new Button("Accept");
//        acceptBtn.setFontSize(fontSize);
//        acceptBtn.setTextHAlignment(HAlignment.Center);
//        acceptBtn.setTextVAlignment(VAlignment.Center);
//        acceptBtn.setPreferredSize(btnSize);
//        btnLayout.addChild(3, 0, acceptBtn);
        
        
    }
 
    @Override
    public void cleanup() {
    }

    @Override
    public void update(float tpf) {
    }

    
}

BorderLayout works fine. So that is an easy fix.

Code:

public class AS_Level01A extends AbstractAppState {
    
    private Node guiNode;
    private Main main;
    float screenWidth, screenHeight;
    
    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        main = (Main)app;
        guiNode = main.getGuiNode();
        screenWidth = (float)main.getScreenWidth();
        screenHeight = (float)main.getScreenHeight();
        
        BorderLayout lvlWinLayout = new BorderLayout();
        //SpringGridLayout lvlWinLayout = new SpringGridLayout(Axis.X, Axis.Y, FillMode.Even, FillMode.None);
        Container lvlWindow = new Container(lvlWinLayout);
        lvlWindow.setBackground(new QuadBackgroundComponent(ColorRGBA.Blue, 5, 5));
        Vector3f winDim = new Vector3f(screenHeight, screenHeight * 0.75f, 0f);
        lvlWindow.setPreferredSize(winDim);
        lvlWindow.setLocalTranslation(
            (main.getScreenWidth() - winDim.x) / 2, 
            main.getScreenHeight() / 2 + winDim.y / 2,    
            //main.getScreenHeight() - (main.getScreenHeight() - winDim.y) / 2, 
            // h - (h/2 - d/2) = h/2 + d/2
            10f
        );
        guiNode.attachChild(lvlWindow);
        
        // ADD LABEL
        Label label = new Label("Challenge # 1");
        label.setColor(ColorRGBA.Yellow);
        label.setTextHAlignment(HAlignment.Center);
        label.setFontSize(50);
        lvlWinLayout.addChild(BorderLayout.Position.North, label);
        
        String objectives = ""
                + "Roses are red, violets are blue.  Some poems rhyme and some don't. \n "
                + "And also ... \n"
                + "Roses are red and violets are blue.  When it rains, I think of you.  Drip, Drip, Drip. \n \n"
                + "Some say the world will end in fire, some say in ice.  From what I've tasted of desire, "
                + "I hold with those who favor fire.  But if it had to perish twice, I know that for destruction "
                + "ice is also great, and will suffice.\n\n"
                + "Sam I am, I am Sam.  Do you like green eggs and ham? "
                + "Do you like them in a box?  Do you like them with a fox? \n\n"
                + "You need high points to win the game.  Low points will only make you lose miserably.  ";

        Label lab1 = new Label(objectives);
        lab1.setFontSize(16);
        lab1.setColor(ColorRGBA.White);
        lab1.setTextHAlignment(HAlignment.Center);
        lab1.setTextVAlignment(VAlignment.Center);
        lvlWinLayout.addChild(BorderLayout.Position.Center, lab1);
        
        // BUTTON CONTAINER
        
        // UNCOMMENT THESE LINES TO SEE THE LAYOUT PROBLEM WHEN USING A LARGE TEXT AREA.
        
        SpringGridLayout btnLayout = new SpringGridLayout(Axis.X, Axis.Y, FillMode.Even, FillMode.None);
        Container btnBar = new Container(btnLayout);
        btnBar.setBackground(new QuadBackgroundComponent(ColorRGBA.Cyan, 5, 5));
        lvlWinLayout.addChild(BorderLayout.Position.South, btnBar);
        
        Vector3f btnSize = new Vector3f(50f, 50f, 0f);
        float fontSize = 30;
        Button backBtn = new Button("Back");
        backBtn.setFontSize(fontSize);
        backBtn.setTextHAlignment(HAlignment.Center);
        backBtn.setTextVAlignment(VAlignment.Center);
        backBtn.setPreferredSize(btnSize);
        btnLayout.addChild(0, 0, backBtn);
        
        Panel spacer = new Panel();
        spacer.setPreferredSize(btnSize);
        spacer.setBackground(new QuadBackgroundComponent(ColorRGBA.Cyan, 5, 5));
        btnLayout.addChild(1, 0, spacer);
        btnLayout.addChild(2, 0, spacer);
        
        Button acceptBtn = new Button("Accept");
        acceptBtn.setFontSize(fontSize);
        acceptBtn.setTextHAlignment(HAlignment.Center);
        acceptBtn.setTextVAlignment(VAlignment.Center);
        acceptBtn.setPreferredSize(btnSize);
        btnLayout.addChild(3, 0, acceptBtn);
               
    }
 
    @Override
    public void cleanup() {
    }

    @Override
    public void update(float tpf) {
    }

    
}

Colors need some work. Haha … but it’s just proof of concept at the moment.

I think the point I’d be trying to put forward here is that … the SpringGridLayout does not seem to take into account the size of the multi-line label so as to put the button bar in the right place. The BorderLayout seems to know about the label’s size and even positions it certered in the Center region very nicely.

Yeah, it could be that when the layout size is forced upon the children that SpringGridLayout doesn’t behave properly here… but it’s a bug if so.

Thanks for the test case. :smile:

If you are interesting in knowing how to clean that code up a bit with some styling, I can offer some advice.

Regarding the clipping of scrollable text: I recently thought about that and came up with 3 solutions:
a) render to texture (problem: another render, maybe mobile devices have problems with that)
b) split text into lines (problem: text can’t scroll free, e.g. in auto-scroll of a story + icons/images problematic)
c) write a simple shader that multiplies output from unshaded with an alpha-mask (problem: no stock shader)

I do want another feature in my GUI library: decorators
Need this for two things: scrollbars and some buttons on the outside of GUI elements.
The scrollbars will … ehm … scroll.
The buttons will allow to resize, move, rotate GUI elements (and one button will delete the GUI element).

Not sure if I will use Lemur or write my own GUI library. Probably will take a look at Lemur first and then do my own GUI library even though that might result in some organizational blindness (but it might also save many hours of work and intense thinking)…

Lemur was designed for writing custom GUI libraries… it just happens to include its own also.

The text field already does this for multiline input. It’s not particularly hard to write a line-based scrollable text area. I just haven’t done it myself yet.

Option (d) is to support screen clipping in JME. It has some implications, good and bad. For Lemur it’s only half a solution anyway because Lemur tries to be 3D capable. Still, it would be nice to have a ScrollPanel2D that worked only in 2D.

This is possible in Lemur, I guess. Hard to tell from your description but I don’t see an issue with it.

“Decorator” is a design-pattern found in the book “Design Patterns” (1994).

Every window under Windows and Ubuntu has this button to “delete” itself (typically a “x”).
“resize” and “move” can be achieved via right mouse click → context menu.
Many GUI elements remember their previous position under Windows:
Open a media player or web browser, move window, close, re-open → appears at old position.

So … no really new ideas here from my side. :chimpanzee_smile:

Yes, I know what a “decorator” is as per the decorator pattern. (I bought my Gang off Four book when it was only a few years old… 1996 I think.)

That’s just not really what’s being described nor could I understand why it would be tricky to do. So I thought I might have misunderstood something.

(For example, a Swing JFrame is not really a decorator of its elements nor are the ‘decorations’ like border, close button, etc. ‘decorators’ as per the decorator pattern. But Swing borders are decorators.)

All of that is doable in Lemur, of course. Trivially even. Your original post made it sound like you wanted all of this for every GUI element (like each button, label, etc.) which while doable didn’t really make sense to me.

Windows only remembers the position of the windows because the apps are written that way. It doesn’t ‘remember’ the position of buttons… the app puts the buttons where they are supposed to go.

You can do all of this pretty trivially in Lemur. I’d assume that a UI toolkit that didn’t let you do this was not really a UI toolkit but something else. (Though it occurs to me this would all be harder to do in Nifty, it’s also not impossible.)

Well the general idea is that the game will have a config screen for the ingame UI:
Let’s call that option “customize UI”.
Player will be able to place and customize all widgets (minimap, scores, ammo, healthbar, spells,…).
This idea will work for even the craziest kinds of displays (such as my surround 3 monitors setup).
During the customization some buttons will appear next to the widgets.

Hm… in the design patterns book a scrollbar is the first example for a decorator.
I think it is due to historical reasons - back then this windowing stuff was still quite new and in the making…

Yes, a scroll “panel” often is implemented as a decorator… even in swing. But a close button on a window, not really.

I always feel bad when reviving old topics but in this case it makes sense I think.
Has there been any progress regarding this?

There is a hackish way to get text areas with scroll bars. You can use a listbox, which has scroll bars, and manually edit the lines in an array.

    private void showHelp() {
        String[] msg = {
        "Grid Name - The name used for the grid. The agents used in the grid will all be named according ",
        "to their insertion point into the grid of agents. This is a required setting.",
        " ",
[snip]
        "Start Position - Starting position the agents will spread out evenly from to form the grid. This is ",
        "only used to generate the agents for the grid so you can drop your agents from above the ",
        "navMesh if you wish. The actual starting point of the agent is determined by its final position on ",
        "the navMesh prior to being added to the crowd."
        };
        
        Container window = new Container(new MigLayout("wrap"));
        ListBox<String> listScroll = window.addChild(new ListBox<>());
        listScroll.getModel().addAll(Arrays.asList(msg));
        listScroll.setPreferredSize(new Vector3f(500, 400, 0));
        listScroll.setVisibleItems(20);
        window.addChild(new ActionButton(new CallMethodAction("Close", window, "removeFromParent")), "align 50%");
        getState(UtilState.class).centerComp(window);
        //This assures clicking outside of the message should close the window 
        //but not activate underlying UI components.
        GuiGlobals.getInstance().getPopupState().showPopup(window, PopupState.ClickMode.ConsumeAndClose, null, null);
    }

It takes alot of effort to make it right though because the text will wrap at setPreferredSize() and expand the row size. To keep each row equal, you have to manually move things to the next element in the array, which equals a new row in the listbox.

I do something similar right now (though without the line breaking). I’m more interested in smooth scrolling in order to implement a dialog and console window similar to Baldur’s gate and the likes. I assume the clipping on jME level is the way to go, right?

No idea what this is sorry.

To implement a console with the listBox just add a textField to the container for input and grab the input. Setting setVisibleItems(20) gives you the scroll for visible lines.

I may not be understanding what you mean because when you say console, to me that equals a line for entering text.