Simple GUI Layout snippet

Post those lines of code you feel like sharing or find what you require for your project here; or simply use them as tutorials.
Post Reply
terra0nova
Posts: 3
Joined: Mon Feb 22, 2010 6:16 pm

Simple GUI Layout snippet

Post by terra0nova »

It can be quite cumbersome to position individual GUI components in Irrlicht, and while a nice tool, the GUI Editor has it's problems and doesn't allow for dynamic GUI layouts (where by dynamic, I mean GUIs that can change size and take use of the extra space). I wanted something like Java's FlowLayout and GridLayout to make it easy to build dynamic GUIs.

After searching a little, I didn't find anything that could do this. So I wrote it myself, and decided I'd share it with the community in case anyone else needed something like this. The code snippet is pretty simple, and should plug in easily to any project (it only uses core irrlicht).

Enums (these should be straightforward, just add them to the top of your file):

Code: Select all

enum ALIGNMENT_Y {L_MIDDLE = 0, L_TOP = 1, L_BOTTOM = -1};
enum ALIGNMENT_X {L_CENTER = 0, L_LEFT = 1, L_RIGHT = -1};
The Simple Layout itself. This works similarly to Java's FlowLayout:

Code: Select all

class SimpleLayout {
    private:
    s32 lineHeight, curx, cury, maxx, padX, padY;
    ALIGNMENT_Y alignment;
    public:
    SimpleLayout(const rect<s32>& bounds, ALIGNMENT_Y align=L_TOP, s32 px=5, s32 py=5) {
        padX = px; padY = py; alignment = align; lineHeight = 0;
        maxx = bounds.LowerRightCorner.X-bounds.UpperLeftCorner.X-padX;
        s32 maxy = bounds.LowerRightCorner.Y-bounds.UpperLeftCorner.Y-padY;
        curx = padX; cury = ((alignment == L_TOP)?padY:maxy);
    }
    rect<s32> getBounds(s32 width, s32 height){
        if(width <= 0){if(maxx-curx+width < 0) newLine(); width = maxx-curx+width;}
        width = min(maxx-padX, width);
        if(width > maxx-curx) newLine();
        lineHeight = max(height, lineHeight);
        rect<s32> ret(curx, cury-((alignment == L_BOTTOM)?height:0), curx+width, cury+((alignment == L_TOP)?height:0));
        curx += width+padX;
        return ret;
    }
    inline void newLine(s32 height=0){
        curx = padX; cury += alignment*(max(height, lineHeight)+padY);
        lineHeight = 0;
    }
    inline s32 getWidth() const {return maxx-padX;}
    inline s32 getPadX() const {return padX;}
    inline s32 getPadY() const {return padY;}
};
How it works:

The flow layout places elements side by side, left to right on a line until an element will not fit on the line. It then overflows to the next line, which is lineHeight pixels above or below the previous line (where lineHeight is the maximum height of any element in that line). Any element of width larger than the parent's width is clamped to the parent's width.

You construct it with the bounds of the parent element you wish to add elements to. It only uses the height and width of these bounds, so you can use the absolute or relative bounds. The align parameter can be L_TOP or L_BOTTOM, and determines whether elements are added to the top of the parent and flow down (L_TOP), or from the bottom of the parent and flow up (L_BOTTOM). Px and py define the x and y padding between the edges and each element.

The main function you will call is getBounds. It will "place" an width x height element into a line, and return the relative bounds of the placed element. If width is less than or equal to 0, then it is set to the width of the parent minus the specified width.

The newLine function will end the current line. It will increment the height by at least the height parameter passed [max(height, lineHeight)].

Here's a small example of how to use it:

Code: Select all

    IGUIEnvironment* gui = device->getGUIEnvironment();
    IGUIElement* root = gui->getRootGUIElement();
    SimpleLayout layout(root->getAbsolutePosition(), L_BOTTOM);
    gui->addButton(layout.getBounds(70, 20), root, -1, L"Button 1");
    gui->addButton(layout.getBounds(0, 20), root, -1, L"Button 2");
    gui->addButton(layout.getBounds(70, 20), root, -1, L"Button 3");
    gui->addButton(layout.getBounds(0, 20), root, -1, L"Button 4");
This places 4 buttons into the root GUI element - button 1 on the bottom left that is 70 pixels wide and 20 pixels tall, button 2 next to it, which is 20 pixels tall, but set to fill the remaining line (0 width). Button 3 is set to the same size as button 1, but on the next line up, and button 4 is just like button 2 next to button 3. All of these elements have the 5 pixel padding.

I also wrote a GridLayout class, which allocates blocks using a simplelayout that fit in a grid. It mixes well with SimpleLayout, as it calls getBounds from a SimpleLayout object.

Code: Select all

class GridLayout {
    private:
    SimpleLayout& layout;
    s32 cols, rowWidth, cellHeight;
    public:
    GridLayout(SimpleLayout& l, s32 c, s32 cell_height) : layout(l){
        cols = c;
        rowWidth = layout.getWidth()-(cols-1)*layout.getPadX();
        cellHeight = cell_height;
    }
    rect<s32> getCell(){return getCell(1.f/cols);}
    rect<s32> getCell(f32 per){return layout.getBounds((s32)(rowWidth*per), cellHeight);}
    rect<s32> getCell(s32 width, s32 height, ALIGNMENT_X xalign=L_CENTER, ALIGNMENT_Y yalign=L_MIDDLE){
        return getCell(1.f/cols, width, height, xalign, yalign);
    }
    rect<s32> getCell(f32 per, s32 width, s32 height, ALIGNMENT_X xalign=L_CENTER, ALIGNMENT_Y yalign=L_MIDDLE){
        s32 cellWidth = (s32)(rowWidth*per);
        if(width <= 0) width = max(0, cellWidth+width);
        if(height <= 0) height = max(0, cellHeight+height);
        rect<s32> ret = layout.getBounds(cellWidth, cellHeight);
        s32 dx = max(0, cellWidth-width), dy = max(0, cellHeight-height);
        if(xalign == L_LEFT) ret.LowerRightCorner.X -= dx;
        else if(xalign == L_RIGHT) ret.UpperLeftCorner.X += dx;
        else{
            s32 half = dx/2;
            ret.LowerRightCorner.X -= half;
            ret.UpperLeftCorner.X += half;
        }
        if(yalign == L_TOP) ret.LowerRightCorner.Y -= dy;
        else if(yalign == L_BOTTOM) ret.UpperLeftCorner.Y += dy;
        else{
            s32 half = dy/2;
            ret.LowerRightCorner.Y -= half;
            ret.UpperLeftCorner.Y += half;
        }
        return ret;
    }
};
It is constructed with a simple layout, the number of columns (needed to compute amount of padding), and the height of each cell/row. Then there is one overloaded getCell function, which work as follows:

getCell() - returns the bounds of the next cell, has the height used in the constructor, and parent width / number of columns width.

getCell(f32 per) - same as getCell, but returns a cell that is some percent of the full width instead of a uniform 1 / number of columns. These should add up to 1 in a row.

getCell(s32 width, s32 height, ALIGNMENT_X xalign, ALIGNMENT_Y yalign) - allocates the full size of the next cell, but returns a rectangle that is width x height aligned inside the next cell by the xalign and yalign parameters. The final override is the same as this one, just includes the per parameter that changes the size of the cell.

That's everything you need to know to use it. Hopefully it helps you as much as it has helped me. It could be easier to use, like Java's LayoutManagers, but that would require building it in to the GUI framework and recompiling the whole source, and also requiring a getPreferredSize for each GUI element, so not as easy to add to a project. Let me know if you have any questions using this script, and feel free to post improvements to it.

And yes, this code is free to use/modify in any project. Use at your own risk, if it breaks something, it's not my fault (insert permissive open source license here).
Post Reply