Main»API Overview

API Overview


Introduction

A simple program using Hybrid may look as follows:

import xf.hybrid.Hybrid;
import xf.hybrid.backend.GL;

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

    while (true) {
        gui.begin(cfg);
        // ... gui code ...
        gui.end();
        gui.render(renderer);
    }
}

Importing xf.hybrid.Hybrid gives access to all standard widgets, plus some utility modules. But in order for any application to function, a backend must be used as well. Importing xf.hybrid.backend.GL makes a few extra widgets available. With a standard backend, the config or code may then use a TopLevelWindow widget.

As standard widgets need to be defined in config files, such file must be loaded. This is easily accomplished by using loadHybridConfig.

Then, we need a Renderer instance, in order to be able to render our GUI. The Renderer class is provided by the backend module imported earlier.

Once we have all the basic stuff, we can proceed with the application's loop.

First, we tell the gui context, that we'll be referencing a specific config, so it may know what widgets should be created, what is their layout, theme, etc. We do this by calling gui.begin(cfg).

What follows next is a section of user code - empty in this case - terminated by a call to gui.end(), which tells the gui context that we're no longer using that config. At this point, Hybrid finalizes GUI data, handles layout and prepares rendering batches.

What's left for a GUI to show up, is a gui.render(renderer) call, which will flush rendering buffers and draw the GUI to the screen.

In order to see a simple working example, please refer to Hello World

Widgets

In order to reference a widget, use its static opCall, e.g. Button().

Calling opCall without any arguments from the same place in code and within the same parent widgets will always produce the same widget reference.

In order to reference a widget definition from a Hybrid config, use static opCall with a name argument, e.g. Button("myButton1").

When widgets are referenced for the first time, they're created and configured using the currently bound config.

Widget properties

Setters return typeof(this), so they can be chained. For instance:

if (Button().text("click me").clicked) { ... }

Using the GUI context

Properties may be read and written using the gui context through the getProperty and setProperty templates. For instance:

if (gui().getProperty!(bool)("foo.frame.closeClicked")) {
    programRunning = false;
}
gui().setProperty("myButton.text", "click me!");

Immediate and retained modes

Normally, referencing a widget within a single gui.begin .. gui.end pair will make it appear only in this particular frame (unless the widget is a concrete one, defined in the config).
This behavior can be changed by entering retained mode. Do this by calling gui.retained(). Return to immediate mode by gui.immediate(). For instance:

gui.begin(cfg).retained;
    Button(`foo.button1`).text = "button1";
gui.immediate.end();

Name stack

Widget names in configs are trees. The fully qualified name for a widget called "foo" within a "bar" parent will be "bar.foo". In order to avoid typing the fully qualified names each time a widget is referenced in code, a name stack can be used. Use gui.push(name) to push a name and gui.pop() to pop the name on top of the stack. To access a top-level name within a push .. pop pair, use a trailing dot in the name.

For example:

gui.push("parent");
Button("child1");
Button("child2");
Button("child3");
Button(".global");
gui.pop();
... is equivalent to:
Button("parent.child1");
Button("parent.child2");
Button("parent.child3");
Button("global");

Child slots

Container widgets need to be opened in order to add children to them (unless the config does it). There are multiple ways to do it:

  1. gui.open()
  2. gui.open(slot name)
  3. someWidget.open()
  4. someWidget.open(slot name)

1. and 2. Open the child slots of the last referenced widget. The parameter-less versions of open() access the default child slot.

After child widgets have been added, the child slot needs to be closed. This can be accomplished by calling gui.close();.

Alternatively, the default child slot can be opened and closed automatically if the following approach is used:

someWidget [{
    // code that references sub-widgets
}];


Examples:

Group(`extras`) [{
    HBox();
    gui.open;
        Button().text = "foo";    // first child of the HBox above
        Button().text = "bar";

        Button().text("baz").open(`rightExtra`);
            Label().text("spam");
        gui.close;

    gui.close;
}];

Event handlers

Event handlers in Hybrid have the following form:
EventHandling delegate(EventType).

EventHandling is an enum containing two fields: Continue and Stop. It determines whether child widgets should receive the event or not.

To add a handler to some widget, use widget.addHandler(the handler).

All events in Hybrid except TODO use a two-phase propagation algorithm called Sinking and Bubbling. The general idea boils down to first pushing the event down the widget tree, then going back up and calling handlers again. This allows a parent widget to see if children wanted to consume the event (by returning EventHandling.Stop) and take appropriate actions.

An implication of this design choice is that handlers will often be called twice for each event. To restrict the handler's actions to just one phase of this processing, it may use the sinking or bubbling properties of the event, e.g.:

myWidget.addHandler((MouseButtonEvent e) {
    if (e.sinking) {
        // do something with the event
        return EventHandling.Stop;
    }
    return EventHandling.Continue;
}

ID management

Since each parameter-less static opCall used in the same code location for a widget class yields the same reference, writing:

for (int i = 0; i < 10; ++i) {
    Button();
}

... will produce only one Button widget. In order to make it spawn ten buttons, an ID must be given to the opCall. Each unique ID will yield a unique widget instance.


for (int i = 0; i < 10; ++i) {
    Button(i);
}

... will get us ten Button widgets.

Virtual File System

Hybrid does all of its file reading using Tango's VFS.

Calling gui.vfs() yields a VfsFolder, an instance of LinkedFolder. The default instance contains a FileFolder("."), thus the current directory is accessible directly.

gui.resetVfs() deletes the previous LinkedFolder instance and creates a new one with no mounts.

For example, if you'd like to keep all GUI files inside a gui.zip file, add the following call at the beginning of your program (before loadHybridConfig):

gui.resetVfs.mount(new ZipFolder("gui.zip"));

And if you wanted to add the contents of gui.zip to the vfs, you could use:

gui.vfs.mount(new ZipFolder("gui.zip"));