Tutorials»Dynamic GUI 2

Dynamic GUI 2

Let's say we'd like to create a program that displays a widget and lets the user dynamically change the widget's properties using some controls.

We can tackle it by having a Combo with some available widgets. Upon selecting a combo item, the widget will be shown. Below it, there will be a list of auto-generated fields for setting and getting the widget's properties.

Layout

Let's start from something simple:

module Dynamic2;

private {
    import xf.hybrid.Hybrid;
    import xf.hybrid.backend.GL;
    import xf.hybrid.WidgetFactory;
    import xf.hybrid.Property;

    // for Thread.yield
    import tango.core.Thread;
}



void main() {
    gui.vfsMountDir(`../../`);
    scope cfg = loadHybridConfig(`./Dynamic2.cfg`);
    scope renderer = new Renderer;

    char[][] widgetsToUse = [
        "Button", "Check", "Label", "Combo", "Input",
        "InputArea", "Progressbar", "FloatInputSpinner"
    ];

    bool programRunning = true;
    while (programRunning) {
        gui.begin(cfg);
            if (gui().getProperty!(bool)("main.frame.closeClicked")) {
                programRunning = false;
            }

            auto dynWidgetParent = Group(`main.dynamicWidget`);

            VBox(`main.controls`) [{
                auto combo = Combo();
                if (0 == combo.items.length) {
                    foreach (w; widgetsToUse) {
                        combo.addItem(w);
                    }
                }
            }];
        gui.end();
        gui.render(renderer);
        Thread.yield();
    }
}

Everything should be pretty clear. The program is similar to the Hello World, except it acquires a Group widget and creates a Combo with the widget names. Since combo items are retained, we only add them if there are none in the widget. Note that this could also be done before the main loop, within a separate gui.begin .. gui.end call pair. As for the Group(`main.dynamicWidget`), we'll use it to spawn the selected widget as its child.

We use two additional imports, xf.hybrid.WidgetFactory and xf.hybrid.Property. The former allows us to create widgets by name - we'll avoid some code duplication this way. The latter allows us to get some meta info about properties registered in a widget and operate on them.

Let's take a look at the GUI config now:

import "themes/default.cfg"

new FramedTopLevelWindow main {
    frame.text = "Dynamic GUI Generation 2";
    layout = {
        padding = 5 5;
        spacing = 10;
    }
    size = 350 250;

    [hexpand hfill] new VBox {
        layout = {
            padding = 10 10;
        }
        style.normal = {
            background = solid(rgb(.1, .1, .1));
        }

        [hexpand hfill] dynamicWidget;
    }

    [hexpand hfill] controls {
        layout = {
            attribs = "hexpand hfill";
            spacing = 10;
        }
    }

} @overlay {
    [hexpand vexpand hfill vfill] new Group .overlay {
        layout = Ghost;
    }
}

The dynamically created widget - dynamicWidget - will have a black background and some padding.

We also create a slot for the widget property controls. The only twist there is that we set the layout's default widget layout attribs to "hexpand hfill", so we don't have to specify it at every item in the controls pane.

When we compile and run the program...

rebuild -I../../../.. -I../../../ext Dynamic2
(assuming we're two directories above the hybrid dir)

... then should get something like this:

Creating the widget by name

Let's now add code to create the selected widget. It's actually quite simple. We just have to remember that code that references widgets should be put inside gui.begin .. gui.end. Let's first add the following code after the char[][] widgetsToUse declaration:

IWidget[char[]] createdWidgets;

    gui.begin(cfg);
        foreach (name; widgetsToUse) {
            createdWidgets[name] = createWidget(name)
            .layoutAttribs("hexpand vexpand hfill vfill");
        }
    gui.end();

We'll be retaining the created widgets inside an AA, indexed by their type names. The createWidget function comes from xf.hybrid.WidgetFactory and instantiates the desired widget without adding it into the GUI structure.

Say we'll want the widget to expand and fill all available space within its dark frame. Nothing simpler, just call layoutAttribs.

Since we've got the widgets created, let's make them show up when we select the right name in the Combo. Add...

auto sel = combo.selected();               
                if (auto w = sel in createdWidgets) {
                    dynWidgetParent.addChild(*w);
                }

... just after the combo item population.

Since Hybrid has an immediate-GUI nature, using addChild will only add the widget to the dynWidgetParent for the next frame, so it resides in the main loop.

When we compile and run, we should be able to select the widget just like this:

Dynamic controls

Let's first add some code and then analyze it again. We'll start from adding the following snippet after dynWidgetParent.addChild:

int i = 0;
foreach (prop; &w.iterExportedProperties) {
    if (prop.readOnly) {
        continue;
    }

    HBox(i++).cfg(`layout = { spacing = 5; }`) [{
        Label().text(prop.name).halign(2)
        .layoutAttribs(`vexpand`).userSize = vec2(75, 0);

        if (prop.type is typeid(char[])) {
            auto editField = Input();
            editField.layoutAttribs("hexpand hfill vexpand");

            char[] backup = void;
            bool backupValid = false;
            try {
                backup = getProperty!(char[])(*w, prop.name);
                backupValid = true;
            } catch {}

            if (backupValid && Button().text("get").clicked) {
                try {
                    editField.text = backup;
                } catch {}
            }

            if (Button().text("set").clicked) {
                try {
                    setProperty(*w, prop.name, editField.text);
                } catch {
                    if (backupValid) {
                        setProperty(*w, prop.name, backup);
                    }
                }
            }
        }
    }];
}

WTF? Well, let's start from the beginning

int i = 0;
foreach (prop; &w.iterExportedProperties) {
    if (prop.readOnly) {
        continue;
    }

    HBox(i++).cfg(`layout = { spacing = 5; }`) [{

Here we iterate through all properties exported by the dynamically created widget. Since the iterExportedProperties delegate doesn't keep track of the index, we do it manually. We skip readOnly properties since we won't be able to have much fun with them.

Then we have the HBox(i++) line. It creates an HBox container for each i index, thus for each property that's not read-only. We set the spacing of its layout to 5, so widgets don't stick together.

Inside the HBox, we create a label with the property's name, an Input and two Buttons.

Even deeper, we do a bit of logic with the buttons, properties and the text input. We first try to acquire the current value using the getProperty template from xf.hybrid.Property:

backup = getProperty!(char[])(*w, prop.name);

Whether something goes wrong or well, we note it down in the backupValid boolean. The property setter/getter might throw an exception and for this simple program, we'd like to just silently ignore it.

The 'get' button's handler boils down to:

if (backupValid && Button().text("get").clicked) {
    try {
        editField.text = backup;
    } catch {}
}

... because we already have the property value in the backup variable.

As for the setter, we'll want to try to set the value to the current Input field's text property and if somehow the dynamic widget's property rejects it by throwing an exception, we'll restore the backup:

if (Button().text("set").clicked) {
    try {
        setProperty(*w, prop.name, editField.text);
    } catch {
        if (backupValid) {
            setProperty(*w, prop.name, backup);
        }
    }
}

Summing it up

We've arrived at the following code:

module Dynamic2;

private {
    import xf.hybrid.Hybrid;
    import xf.hybrid.backend.GL;
    import xf.hybrid.WidgetFactory;
    import xf.hybrid.Property;

    // for Thread.yield
    import tango.core.Thread;
}



void main() {
    gui.vfsMountDir(`../../`);
    scope cfg = loadHybridConfig(`./Dynamic2.cfg`);
    scope renderer = new Renderer;

    char[][] widgetsToUse = [
        "Button", "Check", "Label", "Combo", "Input",
        "InputArea", "Progressbar", "FloatInputSpinner"
    ];

    IWidget[char[]] createdWidgets;

    gui.begin(cfg);
        foreach (name; widgetsToUse) {
            createdWidgets[name] = createWidget(name)
            .layoutAttribs("hexpand vexpand hfill vfill");
        }
    gui.end();

    bool programRunning = true;
    while (programRunning) {
        gui.begin(cfg);
            if (gui().getProperty!(bool)("main.frame.closeClicked")) {
                programRunning = false;
            }

            auto dynWidgetParent = Group(`main.dynamicWidget`);

            VBox(`main.controls`) [{
                auto combo = Combo();
                if (0 == combo.items.length) {
                    foreach (w; widgetsToUse) {
                        combo.addItem(w);
                    }
                }

                auto sel = combo.selected();               
                if (auto w = sel in createdWidgets) {
                    dynWidgetParent.addChild(*w);

                    int i = 0;
                    foreach (prop; &w.iterExportedProperties) {
                        if (prop.readOnly) {
                            continue;
                        }

                        HBox(i++).cfg(`layout = { spacing = 5; }`) [{
                            Label().text(prop.name).halign(2)
                            .layoutAttribs(`vexpand`).userSize = vec2(75, 0);

                            if (prop.type is typeid(char[])) {
                                auto editField = Input();
                                editField.layoutAttribs("hexpand hfill vexpand");

                                char[] backup = void;
                                bool backupValid = false;
                                try {
                                    backup = getProperty!(char[])(*w, prop.name);
                                    backupValid = true;
                                } catch {}

                                if (backupValid && Button().text("get").clicked) {
                                    try {
                                        editField.text = backup;
                                    } catch {}
                                }

                                if (Button().text("set").clicked) {
                                    try {
                                        setProperty(*w, prop.name, editField.text);
                                    } catch {
                                        if (backupValid) {
                                            setProperty(*w, prop.name, backup);
                                        }
                                    }
                                }
                            }
                        }];
                    }
                }
            }];
        gui.end();
        gui.render(renderer);
        Thread.yield();
    }
}

When we compile and run it, we should be able to get something like this on screen:

Adding some polish

The program we've created doesn't allow for much manipulation of the selected widget, but we can easily generalize it. We may create specialized input fields for other property types. Another cool feature would be to have the property edit fields automatically update the widget instead of having to click 'set' and 'get' all the time.

Well, I've got good and bad news. Bad news comes first. This tutorial won't cover the more complete approach or the auto-updating, because it would get even more bloated.

As for the good news, the Hybrid repo contains such an improved sample in hybrid/demos/dynamic2. It uses some mixining of CTFE-generated code, so approach it with care. But if you do, the result is kind of neat: