Tutorials»Custom Theme

Custom Theme

Creating a custom theme is a lot of work, but let's try to skin a not-so-simple widget with a nontrivial theme. How about making the TabView look like in OSX?

Let's take a look at the following screenshot:

We'll skip the border, since it's the easiest part and it would be quite boring to do it. Let's focus on the tab buttons instead. There are a few things to be noticed about them:

  • Left / right edges may be flat or rounded
  • The whole button becomes blue when it's active
  • There are one pixel-thick separators between buttons

We can achieve a similar effect by using a few bitmaps and some dynamic style switching. We'll use 6 images for the widget:

  • left part
  • highlighted left part
  • middle part
  • highlighted middle part
  • right part
  • highlighted right part

They might be obtained by scavenging some OSX screenshots on the net, but for the sake of this tutorial, they exist in hybrid/themes/osx/img.

TabButton

Since we already have them, we may begin creating the custom widget type. Let's start from TabButton. At the top level, it will have a horizontal box with the 3 components. It would also be nice if it had some default size so it doesn't shrink to a few pixels if it doesn't have a caption somehow:

widget TabButton {
    layout = HBox;
    layout = {
        spacing = -1;
    }
    size = 80 0;

Why are we using -1 spacing? The edges of our middle images have 1px borders, so if we placed our buttons would have vertical lines between the left, middle and right parts. We'll also use a little cheat here. TabButton renders itself in such a way that the middle part is drawn before the left and right parts. This is mostly a hack before days of custom z-indexing of widget components, but let's stick to it for now. So this property along with the -1 spacing will make the borders to be overdrawn, so the buttons will look consistent.

Once we have the layout done, we can define the left part of the TabButton:

[vexpand vfill] new Graphic leftEdge {
    size = 4 21;
    style.normal = {
        background = solid(white);
        image = file("themes/osx/img/tb_left.png");
    }
    style.hover = {
        background = solid(rgb(.9, .93, 1));
    }
    style.active = {
        image = file("themes/osx/img/tb_left_active.png");
    }
}

TabButton expects to find a sub-widget called leftEdge, so we provide it here. Using vexpand and vfill will guarantee us that the widget will scale well vertically.

We set the userSize of the Graphic widget to (4, 21) because that's the size of the tb_left.png image.

  • style.normal will simply display the tb_left.png image.
  • style.hover will color the image slightly to blue
  • style.active will display a different image, tb_left_active.png

That covers the left part of the button. it will already change styles nicely when hovered with the mouse or selected as the currently active tab button. Let's create the center now:

[vexpand vfill hexpand hfill] new Graphic {
    size = 0 21;
    style.normal = {
        background = solid(white);
        image = grid("themes/osx/img/tb_middle.png", hline(2, 6), vline(0, 21));
    }
    style.hover = {
        background = solid(rgb(.9, .93, 1));
    }
    style.active = {
        image = grid("themes/osx/img/tb_middle_active.png", hline(2, 6), vline(0, 21));
    }

    layout = HBox;
    layout = {
        padding = 6 0;
        spacing = 2;
    }

    [hexpand vexpand] new HBox leftExtra;
    [vexpand] new Label label {
        style.normal = {
            color = rgb(.1, .1, .1);
        }
    }
    [hexpand vexpand] new HBox rightExtra;
}

The center part will stretch in both axes, so we'll make it a Graphic with all of expanding and filling options enabled.

Setting the size to (0, 21) will make the widget have at least 21 pixels vertically.

Then come the styles. Similarly, we'll be using two images and a slightly blue tint upon hovering, but since this part has to stretch despite having borders in the image, we need to use the 9-image grid. We provide hline(2, 6) and vline(0, 21) to the grid function, thus the horizontal range of pixels [2..6] and the vertical range [0..21] will be stretched. In other words, two pixels on either side of the image won't stretch horizontally.

Below the style definitions, we have the leftExtra, label and rightExtra sub-widgtes. We'll need them since TabButton is a Button subclass, thus needs to provide all these sub-widgets. They are nothing fancy though, just two HBoxes and a Label with a dark text color.

The rightEdge component of TabButton is very similar to leftEdge, so we'll skip it for now. The code that follows is:

leftEdge = sub(leftEdge);
rightEdge = sub(rightEdge);

leftExtra = sub(leftExtra);
rightExtra = sub(rightExtra);
label = sub(label);
text = prop(label.text);

text = "Tab";

So we simply export all the sub-widgets to be accessible to our D class as members of the subWidgets AA. Despite the fact that we have lines like name = sub(name), they may not be skipped, because naming a widget somewhere in the type declaration doesn't make it a sub-widget yet. It simply makes it accessible by name inside the config, so we might e.g. take some property from it.

TabView

Once we're done with the TabButton, we should take a moment and define TabView. This one is a no-brainer:

widget TabView {
    layout = VBox;

    new HBox tabList {
        layout = {
            spacing = -1;
        }
    }

    [hexpand hfill vexpand vfill] new Group clientArea {
    }

    tabList = sub(tabList);
    clientArea = sub(clientArea);
}

We simply needed to provide some container for the tabList and a clientArea for the contents of the active tab pane. We use spacing = -1 again, since we only want 1px instead of 2px spaces between widgets.

Summing it up

We arrive at the following GUI config:

widget TabButton {
    layout = HBox;
    layout = {
        spacing = -1;
    }
    size = 80 0;

    [vexpand vfill] new Graphic leftEdge {
        size = 4 21;
        style.normal = {
            background = solid(white);
            image = file("themes/osx/img/tb_left.png");
        }
        style.hover = {
            background = solid(rgb(.9, .93, 1));
        }
        style.active = {
            image = file("themes/osx/img/tb_left_active.png");
        }
    }

    [vexpand vfill hexpand hfill] new Graphic {
        size = 0 21;
        style.normal = {
            background = solid(white);
            image = grid("themes/osx/img/tb_middle.png", hline(2, 6), vline(0, 21));
        }
        style.hover = {
            background = solid(rgb(.9, .93, 1));
        }
        style.active = {
            image = grid("themes/osx/img/tb_middle_active.png", hline(2, 6), vline(0, 21));
        }

        layout = HBox;
        layout = {
            padding = 6 0;
            spacing = 2;
        }

        [hexpand vexpand] new HBox leftExtra;
        [vexpand] new Label label {
            style.normal = {
                color = rgb(.1, .1, .1);
            }
        }
        [hexpand vexpand] new HBox rightExtra;
    }

    [vexpand vfill] new Graphic rightEdge {
        size = 4 21;
        style.normal = {
            background = solid(white);
            image = file("themes/osx/img/tb_right.png");
        }
        style.hover = {
            background = solid(rgb(.9, .93, 1));
        }
        style.active = {
            image = file("themes/osx/img/tb_right_active.png");
        }
    }

    leftEdge = sub(leftEdge);
    rightEdge = sub(rightEdge);

    leftExtra = sub(leftExtra);
    rightExtra = sub(rightExtra);
    label = sub(label);
    text = prop(label.text);

    text = "Tab";
}


widget TabView {
    layout = VBox;

    new HBox tabList {
        layout = {
            spacing = -1;
        }
    }

    [hexpand hfill vexpand vfill] new Group clientArea {
    }

    tabList = sub(tabList);
    clientArea = sub(clientArea);
}

Result

And all this work has been in order to get nice tab buttons inside the TabView container like in the following screenshot:

The TabView.cfg built in this tutorial lives in hybrid/themes/osx/TabView.cfg. The hybrid/themes/default.cfg file may be edited to use it instead of the default tab view look, although it will not look good with the other default widgets.

Other widgets may be themed similarly, although care needs to be taken about the sub-widgets that some CustomWidgets may require. For now, it's best to copy the default theme and start a new one by hacking it. Or write one from scratch, but refer to widget implementations to see what sub-widgets and properties will be required.