W4 GUI Creation

From Ciliz|W4

Scope

Assuming that you have read the article W4 Physics for Dummies to receive the basic information.

This page describes how to add GUI to the project. Each significant step is accompanied by full source code so you can verify your changes.

W4 GUI subsystem is based on widgets. A widget is a user interface element which appearance you can specify as well input processing. For example, w4::gui::Label allows you to display some text, and with w4::gui::Image you can show a picture from a file. You also can assign a click handler.

Basic Information

Take the following code as the basis:

#include "W4Framework.h"

W4_USE_UNSTRICT_INTERFACE

struct W4_GUI_Demo : public IGame
{
public:
    void onStart() override
    {
        auto cam = Render::getScreenCamera();
        cam->setWorldTranslation({0.f, 0, -25.f});
        cam->setFov(45.f);

        m_shape = Mesh::create::cube({5,5,5});
        m_shape->setMaterialInst(Material::getDefault()->createInstance());

        Render::getRoot()->addChild(m_shape);
    }

    void onUpdate(float dt) override
    {
        m_shape->rotateLocal({dt,dt,dt});
    }

private:
    sptr<Mesh> m_shape;
};

W4_RUN(W4_GUI_Demo)

Run the program. A spinning cube will appear, as in the figure below:

GUI 01.png

Virtual screen creation

Widgets are placed on the virtual screen in two-dimensional Cartesian coordinates, with the origin of coordinates being placed in the upper left corner. With a virtual screen, you do not need to worry about the actual size of the browser window. In the case of changing the nominal size of the viewport (i.e. the browser window), the proportions of the positions and sizes of the widgets comply with the coordinate system of the virtual screen.

To set the resolution of the virtual screen, use the following function:

void w4::gui::setVirtualResolution(const w4::math::size& sz)

For our example, let's set the virtual Full HD resolution in the portrait view by adding the following line to the end of the void onStart() method:

 w4::gui::setVirtualResolution({1080, 1920});

After the virtual screen is set, you can place widgets.

Creating a parent widget

To start with, let's create an object of the w4::gui::Widget class, which will be the parental widget for some of the further created widgets. To create the object, use the variadic template function w4::gui::createWidget, adding it to the void onStart () method:

auto ui = createWidget<Widget>(nullptr, "RootUI");

The w4::gui::createWidget function takes a pointer to the parent widget, as the first parameter. In our case nullptr is passed, due to the fact the label does not have a parent. Since w4::gui::createWidget is a variadic template function, the parameters (starting from the second one) are passed to the widget constructor ("RootUI" - the name of the widget.). The class of widget constructor is specified as the template parameter <Widget>.

Adding label

Create an object of the w4::gui::Label class. This is a text field that will be used to display the value of Euler angles (which determine the rotation of the cube).

Add declarations:

private:
    sptr<Mesh>           m_shape;
    sptr<gui::Label>     m_eulerLabel;
};

To create a class, use the variadic template function w4::gui::createWidget, adding it to the void onStart () method:

m_eulerLabel = w4::gui::createWidget<Label>(nullptr,        // parent 
                                            "EulerLabel",   // name
                                            ivec2(30, 30)); // position

Note. This time, a pointer is passed to the parent widget as the first parameter (ui is passed for m_eulerLabel). And a string containing the displayed text and its position on the virtual screen is passed to the w4::gui::Label class:

Label(const std::string& text = "Label", const w4::math::ivec2& pos = {0, 0});

Setting parameters

The widget position is set by the void setPosition(const math::ivec2& pos); method. Add the line:

m_eulerLabel->setPosition({540, 25});

The top-left alignment of the text relative to its coordinates:

m_eulerLabel->setHorizontalAlign(HorizontalAlign::Left);
m_eulerLabel->setVerticalAlign(VerticalAlign::Top);

Fonts

To set the font, use the void setFont (Font font); method (class w4::gui::Label) and choose the Arial font family:

m_eulerLabel->setFont(Font::Arial);

Note. A specific font can be configured by passing the string value of the font, for example:

m_eulerLabel-> setFont ("Helvetica, sans-serif");

In this case, the browser will try to install selected font in the user's operating system, if the font is missing, a font from the sans-serif family will be requested. For options, see https://developer.mozilla.org/en/docs/Web/CSS/font-family.

Alternatively, you can also use font varietes from Google (see https://developers.google.com/fonts/?hl=en). To do this, pass two string URI parameters to the resource into the setFont method. Parameters contain a description of the CSS style for the font and the name of the font family. For example:

m_eulerLabel-> setFont ("https://fonts.googleapis.com/css?family=Tangerine", "Tangerine");

Set the font size in pixels of the virtual screen:

m_eulerLabel->setFontSize(35);

You can specify bold or italics for the text by using the void setBold (bool bold) and void setItalic (bool italic) methods.

m_eulerLabel->setBold(true);
m_eulerLabel->setItalic(true);

You can set the font and background colors using void setTextColor(const w4::math::color& color) and void setBgColor(const w4::math::color& color) methods:

m_eulerLabel->setTextColor(w4::math::color::White);
m_eulerLabel->setBgColor(w4::math::color::Green);

The w4::math::color class contains predefined colors, such as w4::math::color::White or w4::math::color::Black. You can also specify it as an integer value in RGBA format:

m_eulerLabel->setTextColor(0xFFFFFFFF);

or as a floating point value:

m_eulerLabel->setBgColor({0.f, 1.f, 0.f, 0.5f});

Next set the width and size control policy.

Set the width to 475 pixels of the virtual screen and set the horizontal size policy to fixed.

m_eulerLabel->setWidth(475);
m_eulerLabel->setHorizontalPolicy(SizePolicy::Fixed);

Note. This means that the text field width is fixed now. By default, the size of the text box changes automatically depending on the content.

In order to set the policy for automatic resizing according to the content, you need to pass the SizePolicy::Auto value to the setHorizontalPolicy (SizePolicy policy) method. In this case the value passed to the void setWidth (uint w) method will be ignored.

m_eulerLabel->setHorizontalPolicy(SizePolicy::Auto);

Note. It is also possible to set a vertical size policy or both policies together. To do this, use the methods: void setVerticalPolicy (SizePolicy policy) and void setSizePolicy (SizePolicy vertical, SizePolicy horizontal), accordingly.

With a fixed size text field, it might be necessary to align the text within the borders of the widget. This can be achieved by using the setTextAlign method.

For example, if you need to center the alignment, you can do the following:

m_eulerLabel->setTextAlign(HorizontalAlign::Center, VerticalAlign::Center);

Displaying Euler angles in a text box

The cube rotates in the void onUpdate (float dt) method. Therefore, pitch, yaw and roll angles are added to the text field in the same place. To do this, get the object of rotation using the Node::getLocalRotation() method:

const auto& rot = m_shape->getLocalRotation();

Obtain Euler angles (for clarity, they are converted from radians to degrees):

auto pitch = rot.eulerPitch() * RAD2DEG;
auto yaw   = rot.eulerYaw()   * RAD2DEG;
auto roll  = rot.eulerRoll()  * RAD2DEG;

Get the formatted string using the w4::utils::format function and set its value to the m_eulerLabel text field using the void setText(const std::string& text) method.

 m_eulerLabel->setText(w4::utils::format("Pitch %.1f°\nYaw %.1f°\nRoll %.1f°", 
                                            rot.eulerPitch()* RAD2DEG,
                                            rot.eulerYaw()  * RAD2DEG,
                                            rot.eulerRoll() * RAD2DEG));
}

Before:

void onUpdate(float dt) override
{
    m_shape->rotate(Rotator(dt, dt, dt));
}

After:

void onUpdate(float dt) override
{
    if (m_rotate)
    {
        m_shape->rotateLocal({dt, dt, dt});

        const auto& rot = m_shape->getLocalRotation();
        auto pitch = rot.eulerPitch() * RAD2DEG;
        auto yaw   = rot.eulerYaw()   * RAD2DEG;
        auto roll  = rot.eulerRoll()  * RAD2DEG;
        m_eulerLabel->setText(utils::format("Pitch %.1f°\nYaw %.1f°\nRoll %.1f°", pitch, yaw, roll));
    }
}

After execution you will get the following:

Center

Source code

After the manipulations above are completed the full text of the program will be similar to the below:

#include "W4Framework.h"
 
W4_USE_UNSTRICT_INTERFACE
 
class W4_GUI_Demo : public IGame
{
public:
    void onStart() override
    {
        auto cam = Render::getScreenCamera();
        cam->setWorldTranslation({0.f, 0, -25.f});
        cam->setAspect(9.f / 16.f);
 
        m_shape= Mesh::create::cube({5,5,5});
 
        w4::math::color color {color::White};
        auto matInst = Material::getDefault()->createInstance();
        matInst->setParam("baseColor", color);
        m_shape->setMaterialInst(matInst);
 
        Render::getRoot()->addChild(m_shape);
 
        // --- GUI Code Here
 
        gui::setVirtualResolution({1080, 1920});
 
        auto ui = createWidget<Widget>(nullptr, "RootUI");
 
        m_eulerLabel = gui::createWidget<Label>(ui, "Euler Label", ivec2(25, 140));
        m_eulerLabel->setHorizontalAlign(HorizontalAlign::Left);
        m_eulerLabel->setVerticalAlign(VerticalAlign::Top);
        m_eulerLabel->setFontSize(50);
        m_eulerLabel->setWidth(475);
        m_eulerLabel->setHorizontalPolicy(SizePolicy::Fixed);
    }
 
    void onUpdate(float dt) override
    {
        if (m_rotate)
        {
            m_shape->rotateLocal({dt, dt, dt});
 
            const auto& rot = m_shape->getLocalRotation();
            auto pitch = rot.eulerPitch() * RAD2DEG;
            auto yaw   = rot.eulerYaw()   * RAD2DEG;
            auto roll  = rot.eulerRoll()  * RAD2DEG;
            m_eulerLabel->setText(utils::format("Pitch %.1f°\nYaw %.1f°\nRoll %.1f°", pitch, yaw, roll));
        }
    }
 
private:
    sptr<Mesh>           m_shape;
    sptr<gui::Label>     m_eulerLabel;
    bool                 m_rotate = true;
};
W4_RUN(W4_GUI_Demo)

Adding Image

Add the logo to the upper right corner by using the w4::gui::Image class. One of the constructors of this class uses the following parameters:

Image(const std::string& path, int32_t width, int32_t height, int32_t x, int32_t y, const std::string& name);

The image file is located in the resources/ui/ directory relative to the project root directory. Thus:

auto img = createWidget<gui::Image>(ui, "resources/ui/W4Logo.png", 200, 200, 1050, 140);
     img->setHorizontalAlign(HorizontalAlign::Right);
     img->setVerticalAlign(VerticalAlign::Top);

Expected Result:

Center

Source code

#include "W4Framework.h"
 
W4_USE_UNSTRICT_INTERFACE
 
class W4_GUI_Demo : public IGame
{
public:
    void onStart() override
    {
        auto cam = Render::getScreenCamera();
        cam->setWorldTranslation({0.f, 0, -25.f});
        cam->setAspect(9.f / 16.f);
 
        m_shape= Mesh::create::cube({5,5,5});
 
        w4::math::color color {color::White};
        auto matInst = Material::getDefault()->createInstance();
        matInst->setParam("baseColor", color);
        m_shape->setMaterialInst(matInst);
 
        Render::getRoot()->addChild(m_shape);
 
        // --- GUI Code Here
 
        gui::setVirtualResolution({1080, 1920});
 
        auto ui = createWidget<Widget>(nullptr, "RootUI");
 
        m_eulerLabel = gui::createWidget<Label>(ui, "Euler Label", ivec2(25, 140));
        m_eulerLabel->setHorizontalAlign(HorizontalAlign::Left);
        m_eulerLabel->setVerticalAlign(VerticalAlign::Top);
        m_eulerLabel->setFontSize(50);
        m_eulerLabel->setWidth(475);
        m_eulerLabel->setHorizontalPolicy(SizePolicy::Fixed);
 
        auto img = createWidget<gui::Image>(ui, "resources/ui/W4Logo.png", 200, 200, 1050, 140);
             img->setHorizontalAlign(HorizontalAlign::Right);
             img->setVerticalAlign(VerticalAlign::Top);
    }
 
    void onUpdate(float dt) override
    {
        if (m_rotate)
        {
            m_shape->rotateLocal({dt, dt, dt});
 
            const auto& rot = m_shape->getLocalRotation();
            auto pitch = rot.eulerPitch() * RAD2DEG;
            auto yaw   = rot.eulerYaw()   * RAD2DEG;
            auto roll  = rot.eulerRoll()  * RAD2DEG;
            m_eulerLabel->setText(utils::format("Pitch %.1f°\nYaw %.1f°\nRoll %.1f°", pitch, yaw, roll));
        }
    }
 
private:
    sptr<Mesh>           m_shape;
    sptr<gui::Label>     m_eulerLabel;
    bool                 m_rotate = true;
};
W4_RUN(W4_GUI_Demo)

Adding Sliders

Add sliders to control the color of the cube.

Set the cube to white using the void onStart () method:

w4::math::color color {color::White};
auto matInst = Material::getDefault()->createInstance();
matInst->setParam("baseColor", color);
m_shape->setMaterialInst(matInst);

Add widget that will be parental for 3 sliders and a label:

auto sliders = createWidget<Widget>(ui, "Sliders");
     sliders->setPosition({30, 1650});

Note. Changing the position of the parent widget, shifts the position of the child widgets on the screen.

Create w4::gui::Label to display the color:

auto colorLabel = createWidget<Label>(sliders, "CubeColor", ivec2({100, 100}));
     colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
     colorLabel->setFont(Font::Helvetica);
     colorLabel->setFontSize(50);

Create the sliders using the w4::gui::Slider class, with the following signature of the constructor Slider(float min, float max, float step, float value const w4::math::ivec2& pos, const std::string& name).

auto sliderRed   = createWidget<Slider>(sliders, 0.0f, 1.0f, 0.01f, 1.f, ivec2(190, 15),  "sliderRed");
auto sliderGreen = createWidget<Slider>(sliders, 0.0f, 1.0f, 0.01f, 1.f, ivec2(190, 75),  "sliderGreen");
auto sliderBlue  = createWidget<Slider>(sliders, 0.0f, 1.0f, 0.01f, 1.f, ivec2(190, 135), "sliderBlue");

Set the background color:

sliderRed  ->setBgColor(color::Red,   color::LightSalmon);
sliderGreen->setBgColor(color::Green, color::LightGreen);
sliderBlue ->setBgColor(color::Blue,  color::LightBlue);

After execution the following will appear:

Center

Add value change handlers to the void Slider::onValueChanged(const std::function<void(float v)>& handler) method. They are responsible for setting the color values in the cube material:

sliderRed->onValueChanged([=](float v)
{
    auto color = matInst->getParam<vec4>("baseColor");
    color.r = v;
    matInst->setParam("baseColor", color);
    colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
});

sliderGreen->onValueChanged([=](float v)
{
    auto color = matInst->getParam<vec4>("baseColor");
    color.g = v;
    matInst->setParam("baseColor", color);
    colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
});

sliderBlue->onValueChanged([=](float v)
{
    auto color = matInst->getParam<vec4>("baseColor");
    color.b = v;
    matInst->setParam("baseColor", color);
    colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
});

Now you can change the color using the sliders.

Source code

#include "W4Framework.h"
 
W4_USE_UNSTRICT_INTERFACE
 
class W4_GUI_Demo : public IGame
{
public:
    void onStart() override
    {
        auto cam = Render::getScreenCamera();
        cam->setWorldTranslation({0.f, 0, -25.f});
        cam->setAspect(9.f / 16.f);
 
        m_shape= Mesh::create::cube({5,5,5});
 
        w4::math::color color {color::White};
        auto matInst = Material::getDefault()->createInstance();
        matInst->setParam("baseColor", color);
        m_shape->setMaterialInst(matInst);
 
        Render::getRoot()->addChild(m_shape);
 
        // --- GUI Code Here
 
        gui::setVirtualResolution({1080, 1920});
 
        auto ui = createWidget<Widget>(nullptr, "RootUI");
 
        m_eulerLabel = gui::createWidget<Label>(ui, "Euler Label", ivec2(25, 140));
        m_eulerLabel->setHorizontalAlign(HorizontalAlign::Left);
        m_eulerLabel->setVerticalAlign(VerticalAlign::Top);
        m_eulerLabel->setFontSize(50);
        m_eulerLabel->setWidth(475);
        m_eulerLabel->setHorizontalPolicy(SizePolicy::Fixed);
 
        auto img = createWidget<gui::Image>(ui, "resources/ui/W4Logo.png", 200, 200, 1050, 140);
             img->setHorizontalAlign(HorizontalAlign::Right);
             img->setVerticalAlign(VerticalAlign::Top);
 
        auto sliders = createWidget<Widget>(ui, "Sliders");
             sliders->setPosition({65, 1650});
 
        auto colorLabel = createWidget<Label>(sliders, "CubeColor", ivec2({100, 100}));
             colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
             colorLabel->setFontSize(50);
 
        auto sliderRed   = gui::createWidget<Slider>(sliders, 0.0f, 1.0f, 0.01f, 1.f, ivec2(365, 40),  "sliderRed");
        auto sliderGreen = gui::createWidget<Slider>(sliders, 0.0f, 1.0f, 0.01f, 1.f, ivec2(365, 100),  "sliderGreen");
        auto sliderBlue  = gui::createWidget<Slider>(sliders, 0.0f, 1.0f, 0.01f, 1.f, ivec2(365, 160), "sliderBlue");
 
        sliderRed   ->setBgColor(color::Red,   color::LightSalmon);
        sliderGreen ->setBgColor(color::Green, color::LightGreen);
        sliderBlue  ->setBgColor(color::Blue,  color::LightBlue);
 
        sliderRed->onValueChanged([=](float v)
        {
            auto color = matInst->getParam<vec4>("baseColor");
            color.r = v;
            matInst->setParam("baseColor", color);
            colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
        });
 
        sliderGreen->onValueChanged([=](float v)
        {
            auto color = matInst->getParam<vec4>("baseColor");
            color.g = v;
            matInst->setParam("baseColor", color);
            colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
        });
 
        sliderBlue->onValueChanged([=](float v)
        {
            auto color = matInst->getParam<vec4>("baseColor");
            color.b = v;
            matInst->setParam("baseColor", color);
            colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
        });
    }
 
    void onUpdate(float dt) override
    {
        if (m_rotate)
        {
            m_shape->rotateLocal({dt, dt, dt});
 
            const auto& rot = m_shape->getLocalRotation();
            auto pitch = rot.eulerPitch() * RAD2DEG;
            auto yaw   = rot.eulerYaw()   * RAD2DEG;
            auto roll  = rot.eulerRoll()  * RAD2DEG;
            m_eulerLabel->setText(utils::format("Pitch %.1f°\nYaw %.1f°\nRoll %.1f°", pitch, yaw, roll));
        }
    }
 
private:
    sptr<Mesh>           m_shape;
    sptr<gui::Label>     m_eulerLabel;
    bool                 m_rotate = true;
};
W4_RUN(W4_GUI_Demo)

Adding Buttons

Add two buttons. One stops and starts the rotation of the cube, while the other sets the random values of sliderRed, sliderGreen and sliderBlue.

Start-stop button

Add the m_rotate flag to the private section of the W4_GUI_Demo class;

private:
    sptr<Mesh>           m_shape;
    sptr<gui::Label>     m_eulerLabel;
    bool                 m_rotate = true;
};

Add flag processing to the onUpdate method:

if (m_rotate)
{
    m_shape->rotate({dt, dt, dt});
    
    const auto& rot = m_shape->getLocalRotation();
    auto pitch = rot.eulerPitch() * RAD2DEG;
    auto yaw   = rot.eulerYaw()   * RAD2DEG;
    auto roll  = rot.eulerRoll()  * RAD2DEG;
    m_eulerLabel->setText(utils::format("Pitch %.1f°\nYaw %.1f°\nRoll %.1f°", pitch, yaw, roll));
}

Add a text button using the w4::gui::Button class

auto rotateBtn = createWidget<Button>(nullptr, "Stop", ivec2(920, 1750));
     rotateBtn->setFont(Font::Helvetica);
     rotateBtn->setFontSize(50);

Add a handler for the rotateBtn button to the void onTap (const TapHandler & handler) method. Pressing of the button stops the cube rotation and the button text is changed using the void Button::setText(const std::string& text) method.

rotateBtn->onTap([this, rotateBtn, colorLabel]()
{
    m_rotate = !m_rotate;
    if (m_rotate)
    {
        rotateBtn->setText("Stop");
    }
    else
    {
        rotateBtn->setText("Rotate");
    }
});

You will see approximately the following:

GUI 05.png

You can also set bold or italics for the button text, as well as the text color and background color:

rotateBtn->setBold(true);
rotateBtn->setItalic(true);
rotateBtn->setTextColor(w4::math::color::White);
rotateBtn->setBgColor(w4::math::color::Green);

Randomizer button

Add a button, clicking on which sets random values on the sliders. Create an object of the w4::gui::ImageButton class. Paths to image files (pressed/unpressed button), size and position are passed to the constructor.

In our case, these are files with paths: resources/ui/cast.png - for unpressed and "resources/ui/cast_pressed.png" - for pressed button:

auto imageButton = createWidget<ImageButton>(nullptr, "resources/ui/cast.png", "resources/ui/cast_pressed.png", ivec2(200,180), ivec2(635, 1750));

Set random values for the sliders in the onTap handler:

  imageButton->onTap([=]()
     {
         sliderRed->setValue(random(0.f, 1.f));
         sliderGreen->setValue(random(0.f, 1.f));
         sliderBlue->setValue(random(0.f, 1.f));
     });

Result:

GUI 06.png

Source code

#include "W4Framework.h"
 
W4_USE_UNSTRICT_INTERFACE
 
class W4_GUI_Demo : public IGame
{
public:
    void onStart() override
    {
        auto cam = Render::getScreenCamera();
        cam->setWorldTranslation({0.f, 0, -25.f});
        cam->setAspect(9.f / 16.f);
 
        m_shape= Mesh::create::cube({5,5,5});
 
        w4::math::color color {color::White};
        auto matInst = Material::getDefault()->createInstance();
        matInst->setParam("baseColor", color);
        m_shape->setMaterialInst(matInst);
 
        Render::getRoot()->addChild(m_shape);
 
        // --- GUI Code Here
 
        gui::setVirtualResolution({1080, 1920});
 
        auto ui = createWidget<Widget>(nullptr, "RootUI");
 
        m_eulerLabel = gui::createWidget<Label>(ui, "Euler Label", ivec2(25, 140));
        m_eulerLabel->setHorizontalAlign(HorizontalAlign::Left);
        m_eulerLabel->setVerticalAlign(VerticalAlign::Top);
        m_eulerLabel->setFontSize(50);
        m_eulerLabel->setWidth(475);
        m_eulerLabel->setHorizontalPolicy(SizePolicy::Fixed);
 
        auto img = createWidget<gui::Image>(ui, "resources/ui/W4Logo.png", 200, 200, 1050, 140);
             img->setHorizontalAlign(HorizontalAlign::Right);
             img->setVerticalAlign(VerticalAlign::Top);
 
        auto sliders = createWidget<Widget>(ui, "Sliders");
             sliders->setPosition({65, 1650});
 
        auto colorLabel = createWidget<Label>(sliders, "CubeColor", ivec2({100, 100}));
             colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
             colorLabel->setFontSize(50);
 
        auto sliderRed   = gui::createWidget<Slider>(sliders, 0.0f, 1.0f, 0.01f, 1.f, ivec2(365, 40),  "sliderRed");
        auto sliderGreen = gui::createWidget<Slider>(sliders, 0.0f, 1.0f, 0.01f, 1.f, ivec2(365, 100),  "sliderGreen");
        auto sliderBlue  = gui::createWidget<Slider>(sliders, 0.0f, 1.0f, 0.01f, 1.f, ivec2(365, 160), "sliderBlue");
 
        sliderRed   ->setBgColor(color::Red,   color::LightSalmon);
        sliderGreen ->setBgColor(color::Green, color::LightGreen);
        sliderBlue  ->setBgColor(color::Blue,  color::LightBlue);
 
        sliderRed->onValueChanged([=](float v)
        {
            auto color = matInst->getParam<vec4>("baseColor");
            color.r = v;
            matInst->setParam("baseColor", color);
            colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
        });
 
        sliderGreen->onValueChanged([=](float v)
        {
            auto color = matInst->getParam<vec4>("baseColor");
            color.g = v;
            matInst->setParam("baseColor", color);
            colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
        });
 
        sliderBlue->onValueChanged([=](float v)
        {
            auto color = matInst->getParam<vec4>("baseColor");
            color.b = v;
            matInst->setParam("baseColor", color);
            colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
        });
 
 
        auto rotateBtn = gui::createWidget<Button>(ui, "Stop", ivec2(920, 1750));
        rotateBtn->setSizePolicy(SizePolicy::Auto, SizePolicy::Fixed);
        rotateBtn->setWidth(270);
        rotateBtn->setFontSize(50);
        rotateBtn->onTap([this, rotateBtn]()
                         {
                             m_rotate = !m_rotate;
                             if (m_rotate)
                             {
                                 rotateBtn->setText("Stop");
                             }
                             else
                             {
                                 rotateBtn->setText("Rotate");
                             }
                         });
 
 
 
        auto imageButton = createWidget<ImageButton>(ui, "resources/ui/cast.png", "resources/ui/cast_pressed.png", ivec2(200,180), ivec2(662, 1750));
        imageButton->onTap([=]()
                           {
                               sliderRed->setValue(random(0.f, 1.f));
                               sliderGreen->setValue(random(0.f, 1.f));
                               sliderBlue->setValue(random(0.f, 1.f));
                           });
    }
 
    void onUpdate(float dt) override
    {
        if (m_rotate)
        {
            m_shape->rotateLocal({dt, dt, dt});
 
            const auto& rot = m_shape->getLocalRotation();
            auto pitch = rot.eulerPitch() * RAD2DEG;
            auto yaw   = rot.eulerYaw()   * RAD2DEG;
            auto roll  = rot.eulerRoll()  * RAD2DEG;
            m_eulerLabel->setText(utils::format("Pitch %.1f°\nYaw %.1f°\nRoll %.1f°", pitch, yaw, roll));
        }
    }
 
private:
    sptr<Mesh>           m_shape;
    sptr<gui::Label>     m_eulerLabel;
    bool                 m_rotate = true;
};
W4_RUN(W4_GUI_Demo)

Alternative option

A button can also be created by using a single image, but with the button offset in pixels of the virtual screen. To do this, use another constructor with the following signature:

w4::gui::ImageButton(const std::string& normal, const w4::math::ivec2& offset, const w4::math::ivec2& sz, const w4::math::ivec2& pos, const std::string& name);
auto imageButton = createWidget<ImageButton>(ui, "resources/ui/cast.png", ivec2(0, 5), ivec2(200,180), ivec2(635, 1750));

The offset is set using the void setPressedOffset(const math::ivec2& pos) method:

imageButton->setPressedOffset({0, 5});

The current state of the button is obtained using the bool w4::gui::ImageButton::isPressed() const method.

Combo box

The w4::gui::ComboBox class is a classic drop-down list.

In this example, the drop-down list sets the color of the cube.

To do this, the explanatory text and position on the virtual screen are passed to the constructor:

auto selectColor = createWidget<ComboBox>(ui, "Color", ivec2(665, 240));

And then the elements are added.

Inserting elements

You can fill the list with elements in different ways.

Adding an element to the end of the list:

  • Use the method: void ComboBox::addItem(const std::string& text)
selectColor->addItem("White");
  • Add multiple elements using method void ComboBox::addItems(const std::initializer_list<std::string>& items).
selectColor->addItems({"Red", "Green", "Blue"});

This method also has a different signature that allows you to insert elements from the container using iterators at the beginning and at the end of the range of values in the container.

std::vector<std::string> items = {"Red", "Blue", "Green"};
selectColor->addItems(items.cbegin(), items.cend());

Elements in the container must be of std::string type or they must be able to be implicitly cast to std::string.

Adding an element to any place:

  • Use the void ComboBox::insertItem(int32_t index, const std::string& text) method. The method receives the number of the element before which the new element will be inserted:
selectColor->insertItem(0, "Black");

You can also insert several elements using the СomboBox::insertItems method:

selectColor->insertItems(5, {"Red", "Green", "Blue"});
std::vector<std::string> items = {"Red", "Blue", "Green"};
selectColor->insertItems(1, items.cbegin(), items.cend());

Deleting elements

Elements can be deleted using the void СomboBox::removeItem(int32_t index) method, which receives the number of the element to be deleted.

Item selection

Set the handler for changing the value of the combo box. Every time the user changes the value, the function passed to the void СomboBox::onCurrentIndexChanged(const std::function<void(int)>& index) method will be called.

The index of the selected element comes to the handler as an argument.

In our case, the values on the sliders correspond to the elements of the combo box:

selectColor->onCurrentIndexChanged([=](int32_t v)
{
    w4::math::color color;
    switch(v)
    {
        case 0:
            color = color::Black;
            break;
        case 1:
            color = color::White;
            break;
        case 2:
            color = color::Red;
            break;
        case 3:
            color = color::Green;
            break;
        case 4:
        default:
            color = color::Blue;
            break;
    };

    sliderRed->setValue(color.r);
    sliderGreen->setValue(color.g);
    sliderBlue->setValue(color.b);
});

To set the selected item, use the void СomboBox::setCurrentIndex(int32_t index) method:

selectColor->setCurrentIndex(4);

To reset the selected element, pass -1 to the setCurrentIndex method:

selectColor->setCurrentIndex(-1);

You can get the index of the selected item using the int32_t СomboBox::getCurrentIndex() const method. You can also get the current text in the combo box, use the std::string getCurrentText() const method. If the item is not selected, the getCurrentIndex method returns -1.

Visual style

You can set the font properties of the combo box, as well as the text color and background color.

For example:

selectColor->setFont(Font::Helvetica);
selectColor->setFontSize(50);
selectColor->setBold(true);
selectColor->setItalic(true);
selectColor->setTextColor(w4::math::color::White);
selectColor->setBgColor(w4::math::color::Green);

As a result you will see something similar to the following figure:

GUI 07.png

Source code

As a result of the code changes above, you should get the following program:

#include "W4Framework.h"
 
W4_USE_UNSTRICT_INTERFACE
 
class W4_GUI_Demo : public IGame
{
public:
    void onStart() override
    {
        auto cam = Render::getScreenCamera();
        cam->setWorldTranslation({0.f, 0, -25.f});
        cam->setAspect(9.f / 16.f);
 
        m_shape= Mesh::create::cube({5,5,5});
 
        w4::math::color color {color::White};
        auto matInst = Material::getDefault()->createInstance();
        matInst->setParam("baseColor", color);
        m_shape->setMaterialInst(matInst);
 
        Render::getRoot()->addChild(m_shape);
 
        // --- GUI Code Here
 
        gui::setVirtualResolution({1080, 1920});
 
        auto ui = createWidget<Widget>(nullptr, "RootUI");
 
        m_eulerLabel = gui::createWidget<Label>(ui, "Euler Label", ivec2(25, 140));
        m_eulerLabel->setHorizontalAlign(HorizontalAlign::Left);
        m_eulerLabel->setVerticalAlign(VerticalAlign::Top);
        m_eulerLabel->setFontSize(50);
        m_eulerLabel->setWidth(475);
        m_eulerLabel->setHorizontalPolicy(SizePolicy::Fixed);
 
        auto img = createWidget<gui::Image>(ui, "resources/ui/W4Logo.png", 200, 200, 1050, 140);
             img->setHorizontalAlign(HorizontalAlign::Right);
             img->setVerticalAlign(VerticalAlign::Top);
 
        auto sliders = createWidget<Widget>(ui, "Sliders");
             sliders->setPosition({65, 1650});
 
        auto colorLabel = createWidget<Label>(sliders, "CubeColor", ivec2({100, 100}));
             colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
             colorLabel->setFontSize(50);
 
        auto sliderRed   = gui::createWidget<Slider>(sliders, 0.0f, 1.0f, 0.01f, 1.f, ivec2(365, 40),  "sliderRed");
        auto sliderGreen = gui::createWidget<Slider>(sliders, 0.0f, 1.0f, 0.01f, 1.f, ivec2(365, 100),  "sliderGreen");
        auto sliderBlue  = gui::createWidget<Slider>(sliders, 0.0f, 1.0f, 0.01f, 1.f, ivec2(365, 160), "sliderBlue");
 
        sliderRed   ->setBgColor(color::Red,   color::LightSalmon);
        sliderGreen ->setBgColor(color::Green, color::LightGreen);
        sliderBlue  ->setBgColor(color::Blue,  color::LightBlue);
 
        sliderRed->onValueChanged([=](float v)
        {
            auto color = matInst->getParam<vec4>("baseColor");
            color.r = v;
            matInst->setParam("baseColor", color);
            colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
        });
 
        sliderGreen->onValueChanged([=](float v)
        {
            auto color = matInst->getParam<vec4>("baseColor");
            color.g = v;
            matInst->setParam("baseColor", color);
            colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
        });
 
        sliderBlue->onValueChanged([=](float v)
        {
            auto color = matInst->getParam<vec4>("baseColor");
            color.b = v;
            matInst->setParam("baseColor", color);
            colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
        });
 
 
        auto rotateBtn = gui::createWidget<Button>(ui, "Stop", ivec2(920, 1750));
        rotateBtn->setSizePolicy(SizePolicy::Auto, SizePolicy::Fixed);
        rotateBtn->setWidth(270);
        rotateBtn->setFontSize(50);
        rotateBtn->onTap([this, rotateBtn]()
                         {
                             m_rotate = !m_rotate;
                             if (m_rotate)
                             {
                                 rotateBtn->setText("Stop");
                             }
                             else
                             {
                                 rotateBtn->setText("Rotate");
                             }
                         });
 
 
 
        auto imageButton = createWidget<ImageButton>(ui, "resources/ui/cast.png", "resources/ui/cast_pressed.png", ivec2(200,180), ivec2(662, 1750));
        imageButton->onTap([=]()
                           {
                               sliderRed->setValue(random(0.f, 1.f));
                               sliderGreen->setValue(random(0.f, 1.f));
                               sliderBlue->setValue(random(0.f, 1.f));
                           });
 
        auto selectColor = createWidget<ComboBox>(ui, "Color", ivec2(665, 240));
        selectColor->addItem("White");
        selectColor->addItems({"Red", "Green", "Blue"});
        selectColor->insertItem(0, "Black");
        selectColor->onCurrentIndexChanged([=](int32_t v)
                                           {
                                               w4::math::color color;
                                               switch(v)
                                               {
                                                   case 0:
                                                       color = color::Black;
                                                       break;
                                                   case 1:
                                                       color = color::White;
                                                       break;
                                                   case 2:
                                                       color = color::Red;
                                                       break;
                                                   case 3:
                                                       color = color::Green;
                                                       break;
                                                   case 4:
                                                   default:
                                                       color = color::Blue;
                                                       break;
                                               };
 
                                               sliderRed->setValue(color.r);
                                               sliderGreen->setValue(color.g);
                                               sliderBlue->setValue(color.b);
                                           });
    }
 
    void onUpdate(float dt) override
    {
        if (m_rotate)
        {
            m_shape->rotateLocal({dt, dt, dt});
 
            const auto& rot = m_shape->getLocalRotation();
            auto pitch = rot.eulerPitch() * RAD2DEG;
            auto yaw   = rot.eulerYaw()   * RAD2DEG;
            auto roll  = rot.eulerRoll()  * RAD2DEG;
            m_eulerLabel->setText(utils::format("Pitch %.1f°\nYaw %.1f°\nRoll %.1f°", pitch, yaw, roll));
        }
    }
 
private:
    sptr<Mesh>           m_shape;
    sptr<gui::Label>     m_eulerLabel;
    bool                 m_rotate = true;
};
W4_RUN(W4_GUI_Demo)

Checkbox

Let's add one more element of the “classic” controls - the checkbox. It will hide or display previously created controls.

First of all, create a widget and text field that define a single control. To do this, add the following to the void onStart () method:

auto showUI = createWidget<Widget>(nullptr, "WidgetShowUI");
showUI->setPosition({540, 70});
 
auto hideTitle = gui::createWidget<Label>(showUI, "Show UI", ivec2(0, 0));
hideTitle->setFontSize(50);
hideTitle->setWidth(400);
hideTitle->setHorizontalPolicy(SizePolicy::Fixed);
hideTitle->setTextAlign(HorizontalAlign::Right);

It is time to create the checkbox itself. Pass showUI as the parent widget, and pass the position relative to the parent widget as the second parameter:

auto checkbox = createWidget<Checkbox>(showUI, ivec2(-125, 0));

Since by default all widgets are visible on the screen, set the checkbox to the Checked state:

checkbox->setChecked(true);

You can find out the state of the checkbox using the bool Checkbox::isChecked() const method.

checkbox->isChecked();

An example of a checkbox along with other elements is shown in the following figure:

GUI m8.png

Now add a handler to change the state of the checkbox. Whenever it changes to Checked or Unchecked, the function passed to the onStateChanged method is called.

This handler is a lambda function that receives a single bool checked argument. If checked value is stated as true, then the checkbox turns to checked status; if as false, then it is in the unchecked status.

In the body of the handler, the value of the checked argument is passed to the setVisible method of the ui object.

checkbox->onStateChanged([ui](bool checked) {
    ui->setVisible(checked);
});

Hence, if the checkbox is in the marked status, then all previously added widgets are displayed, otherwise not.

Result:

GUI 08.png

Complete source code

As a result of the code changes above, you should get the following program:

#include "W4Framework.h"
 
W4_USE_UNSTRICT_INTERFACE
 
class W4_GUI_Demo : public IGame
{
public:
    void onStart() override
    {
        auto cam = Render::getScreenCamera();
        cam->setWorldTranslation({0.f, 0, -25.f});
        cam->setAspect(9.f / 16.f);
 
        m_shape= Mesh::create::cube({5,5,5});
 
        w4::math::color color {color::White};
        auto matInst = Material::getDefault()->createInstance();
        matInst->setParam("baseColor", color);
        m_shape->setMaterialInst(matInst);
 
        Render::getRoot()->addChild(m_shape);
 
        // --- GUI Code Here
 
        gui::setVirtualResolution({1080, 1920});
 
        auto ui = createWidget<Widget>(nullptr, "RootUI");
 
        m_eulerLabel = gui::createWidget<Label>(ui, "Euler Label", ivec2(25, 140));
        m_eulerLabel->setHorizontalAlign(HorizontalAlign::Left);
        m_eulerLabel->setVerticalAlign(VerticalAlign::Top);
        m_eulerLabel->setFontSize(50);
        m_eulerLabel->setWidth(470);
        m_eulerLabel->setHorizontalPolicy(SizePolicy::Fixed);
 
        auto img = createWidget<gui::Image>(ui, "resources/ui/W4Logo.png", 200, 200, 1050, 140);
             img->setHorizontalAlign(HorizontalAlign::Right);
             img->setVerticalAlign(VerticalAlign::Top);
 
        auto sliders = createWidget<Widget>(ui, "Sliders");
             sliders->setPosition({65, 1650});
 
        auto colorLabel = createWidget<Label>(sliders, "CubeColor", ivec2({100, 100}));
             colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
             colorLabel->setFontSize(50);
 
        auto sliderRed   = gui::createWidget<Slider>(sliders, 0.0f, 1.0f, 0.01f, 1.f, ivec2(365, 40),  "sliderRed");
        auto sliderGreen = gui::createWidget<Slider>(sliders, 0.0f, 1.0f, 0.01f, 1.f, ivec2(365, 100),  "sliderGreen");
        auto sliderBlue  = gui::createWidget<Slider>(sliders, 0.0f, 1.0f, 0.01f, 1.f, ivec2(365, 160), "sliderBlue");
 
        sliderRed   ->setBgColor(color::Red,   color::LightSalmon);
        sliderGreen ->setBgColor(color::Green, color::LightGreen);
        sliderBlue  ->setBgColor(color::Blue,  color::LightBlue);
 
        sliderRed->onValueChanged([=](float v)
        {
            auto color = matInst->getParam<vec4>("baseColor");
            color.r = v;
            matInst->setParam("baseColor", color);
            colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
        });
 
        sliderGreen->onValueChanged([=](float v)
        {
            auto color = matInst->getParam<vec4>("baseColor");
            color.g = v;
            matInst->setParam("baseColor", color);
            colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
        });
 
        sliderBlue->onValueChanged([=](float v)
        {
            auto color = matInst->getParam<vec4>("baseColor");
            color.b = v;
            matInst->setParam("baseColor", color);
            colorLabel->setText(utils::format("R %.2f\nG %.2f\nB %.2f", color.r, color.g, color.b));
        });
 
 
        auto rotateBtn = gui::createWidget<Button>(ui, "Stop", ivec2(920, 1750));
        rotateBtn->setSizePolicy(SizePolicy::Auto, SizePolicy::Fixed);
        rotateBtn->setWidth(270);
        rotateBtn->setFontSize(50);
        rotateBtn->onTap([this, rotateBtn]()
                         {
                             m_rotate = !m_rotate;
                             if (m_rotate)
                             {
                                 rotateBtn->setText("Stop");
                             }
                             else
                             {
                                 rotateBtn->setText("Rotate");
                             }
                         });
 
 
 
        auto imageButton = createWidget<ImageButton>(ui, "resources/ui/cast.png", "resources/ui/cast_pressed.png", ivec2(200,180), ivec2(662, 1750));
        imageButton->onTap([=]()
                           {
                               sliderRed->setValue(random(0.f, 1.f));
                               sliderGreen->setValue(random(0.f, 1.f));
                               sliderBlue->setValue(random(0.f, 1.f));
                           });
 
        auto selectColor = createWidget<ComboBox>(ui, "Color", ivec2(665, 240));
        selectColor->addItem("White");
        selectColor->addItems({"Red", "Green", "Blue"});
        selectColor->insertItem(0, "Black");
        selectColor->onCurrentIndexChanged([=](int32_t v)
                                           {
                                               w4::math::color color;
                                               switch(v)
                                               {
                                                   case 0:
                                                       color = color::Black;
                                                       break;
                                                   case 1:
                                                       color = color::White;
                                                       break;
                                                   case 2:
                                                       color = color::Red;
                                                       break;
                                                   case 3:
                                                       color = color::Green;
                                                       break;
                                                   case 4:
                                                   default:
                                                       color = color::Blue;
                                                       break;
                                               };
 
                                               sliderRed->setValue(color.r);
                                               sliderGreen->setValue(color.g);
                                               sliderBlue->setValue(color.b);
                                           });
 
        auto showUI = createWidget<Widget>(nullptr, "WidgetShowUI");
        showUI->setPosition({540, 70});
 
        auto hideTitle = gui::createWidget<Label>(showUI, "Show UI", ivec2(0, 0));
        hideTitle->setFontSize(50);
        hideTitle->setWidth(400);
        hideTitle->setHorizontalPolicy(SizePolicy::Fixed);
        hideTitle->setTextAlign(HorizontalAlign::Right);
 
        auto checkbox = createWidget<Checkbox>(showUI, ivec2(-125, 0));
        checkbox->setChecked(true);
        checkbox->onStateChanged([ui](bool checked) {
            ui->setVisible(checked);
        });
    }
 
    void onUpdate(float dt) override
    {
        if (m_rotate)
        {
            m_shape->rotateLocal({dt, dt, dt});
 
            const auto& rot = m_shape->getLocalRotation();
            auto pitch = rot.eulerPitch() * RAD2DEG;
            auto yaw   = rot.eulerYaw()   * RAD2DEG;
            auto roll  = rot.eulerRoll()  * RAD2DEG;
            m_eulerLabel->setText(utils::format("Pitch %.1f°\nYaw %.1f°\nRoll %.1f°", pitch, yaw, roll));
        }
    }
 
private:
    sptr<Mesh>           m_shape;
    sptr<gui::Label>     m_eulerLabel;
    bool                 m_rotate = true;
};
W4_RUN(W4_GUI_Demo)

FYI. Useful methods of the Widget class

  • If you need to explicitly place a specific widget on top of another, you can do this using the void setOrder(uint32_t z); method:
button->setOrder(5);
  • You can control the visibility of widgets using the void setVisible(bool visible); method:
label->setVisible(false);

Note. By hiding / showing the widget, you also hide / show the child widgets.

  • You can set the widget's transparency using the void setOpacity(float op); method:
widget->setOpacity(0.2);
  • You can remove a widget from the screen using the void w4::gui::removeWidget(w4::sptr<Widget> w) function:
w4::gui::removeWidget(widget);

Note. By deleting a widget, you also delete its child widgets, thus objects in widget pointers become invalid.

You can set a size control policy for widgets, using the following methods:

widget->setVerticalPolicy(SizePolicy::Auto);
widget->setHorizontalPolicy(SizePolicy::Fixed);
widget->setSizePolicy(SizePolicy::Auto, SizePolicy::Fixed);

Note. There are two size management policies exist

  • SizePolicy::Fixed- the widget has a fixed size specified in setWidth, setHeight
  • SizePolicy::Auto - widget size varies depending on internal content