Tutorials»Custom Layout

Custom Layout

Creating a custom layout boils down to implementing the ILayout interface. According to its documentation, we should have two phases for our layout's operation - the shrinking and the expanding.

For the sake of this tutorial, we'll skip the expanding phase and do it all in one step, hard-coding some stuff.

Let's say we'd like to arrange our widgets in a circle. No built-in layout can do it, so it looks like a candidate for a custom one. We'll hard-code the radius of the circle and the size we'll use for it. While the former might be OK, the latter is really only to simplify the tutorial and not compute minimal bounds which would simply be slightly bloated.

Let's start from the following stub layout:

class MyLayout : ILayout {
    float   radius = 100.f;
    vec2    size        = { x: 320, y: 240 };

    void minimize(IWidget parent) {
        return;
    }

    void expand(IWidget parent) {
        return;
    }

    void configure(PropAssign[]) {
        return;
    }
}

... and implement the minimize function.

First, we'll need to obtain the number of children managed attached to the parent. We'll simply iterate through all and count them. (TODO: investigate adding numChildren to Widget)

int numChildren = 0;
foreach (ch; &parent.children) {
    ++numChildren;
}

Once we have this, we can compute the position on a circle for any given widget index:

vec2 offsetForChild(int i) {
    float angle = cast(float)i / numChildren * pi * 2;
    return vec2(cos(angle), sin(angle)) * radius;
}

This should be trivial to anyone with basic trig skills.

With the prerequisites in place, we can implement the main part of the minimize function - calling overrideSizeForFrame on the parent and children, as well as setting parentOffset for children.

The size of the parent will be, as previously mentioned, hard-coded to this.size. As for children, we'll simply set their sizes to what they request and position them so that their centers fall to the points given by offsetForChild. With a bit of math, we arrive at this:

parent.overrideSizeForFrame(this.size);
vec2 center = parent.size * .5f;

int i = 0;
foreach (ch; &parent.children) {
    ch.overrideSizeForFrame(ch.desiredSize);
    ch.parentOffset = offsetForChild(i) + center - ch.size * 0.5;
    ++i;
}

Lastly, we need to add our newly created layout class to the layout registry. It may be done in a static ctor of the module or class. The goal is to register an instantiation function at the desired name:

static this() {
    registerLayout("MyLayout", function ILayout() {
        return new MyLayout;
    });
}

Summing it up

Our custom layout class and registration look like this:

class MyLayout : ILayout {
    float   radius = 100.f;
    vec2    size        = { x: 320, y: 240 };

    void minimize(IWidget parent) {
        int numChildren = 0;
        foreach (ch; &parent.children) {
            ++numChildren;
        }

        vec2 offsetForChild(int i) {
            float angle = cast(float)i / numChildren * pi * 2;
            return vec2(cos(angle), sin(angle)) * radius;
        }

        parent.overrideSizeForFrame(this.size);
        vec2 center = parent.size * .5f;

        int i = 0;
        foreach (ch; &parent.children) {
            ch.overrideSizeForFrame(ch.desiredSize);
            ch.parentOffset = offsetForChild(i) + center - ch.size * 0.5;
            ++i;
        }
    }

    void expand(IWidget parent) {
        return;
    }

    void configure(PropAssign[]) {
        return;
    }
}

static this() {
    registerLayout("MyLayout", function ILayout() {
        return new MyLayout;
    });
}

And looks like this:

The complete source for a demo of this simple layout is located in hybrid/demos/customLayout