Feature guide » Tutorial for the Magnum UI library

Get started with the Ui library using a bootstrap project.

This tutorial builds upon the base Getting Started Guide, with a focus on setting up and using the Ui library to create a simple user interface. You're encouraged to go through that one first if you haven't already.

1. Download the bootstrap project

In the ui branch of the bootstrap repository is a minimal setup for a UI application. Download the branch as an archive and extract it somewhere:

1a. Option A: add dependencies as CMake subprojects

The UI library is located in the Magnum Extras repository and additionally relies on font and image loading plugins to be able to draw text and icons. Like before, the easiest way without having to install anything is adding those as CMake subprojects, for an alternative, see 1b. Option B: install Magnum separately and let CMake find it below. Clone the relevant repositories like this (or use git submodule add, if you already have a Git repository):

cd /path/to/the/extracted/bootstrap/project
git clone https://github.com/mosra/corrade.git
git clone https://github.com/mosra/magnum.git
git clone https://github.com/mosra/magnum-plugins.git
git clone https://github.com/mosra/magnum-extras.git

Then open the CMakeLists.txt file in the root of the bootstrap project and add the new subdirectories using add_subdirectory() so the file looks like below. The EXCLUDE_FROM_ALL arguments ensure only the parts you actually use are built (and excludes the subdirectories from the install target as well), and the final add_dependencies() call makes sure that by the time your executable is built, the plugins it dynamically loads are ready as well:

cmake_minimum_required(VERSION 3.5...3.10)
project(MyApplication)

set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/modules/" ${CMAKE_MODULE_PATH})

# Add Corrade and Magnum as subprojects, enable the application, required
# plugins and the UI library itself
add_subdirectory(corrade EXCLUDE_FROM_ALL)
add_subdirectory(magnum EXCLUDE_FROM_ALL)
add_subdirectory(magnum-plugins EXCLUDE_FROM_ALL)
add_subdirectory(magnum-extras EXCLUDE_FROM_ALL)
set(MAGNUM_WITH_SDL2APPLICATION ON CACHE BOOL "" FORCE)
set(MAGNUM_WITH_STBIMAGEIMPORTER ON CACHE BOOL "" FORCE)
set(MAGNUM_WITH_STBTRUETYPEFONT ON CACHE BOOL "" FORCE)
set(MAGNUM_WITH_UI ON CACHE BOOL "" FORCE)

add_subdirectory(src)

# And make sure the plugins get built as dependencies of the main executable
add_dependencies(MyApplication Magnum::StbTrueTypeFont Magnum::StbImageImporter)

The above doesn't rely on any additional dependencies besides SDL2, which you should already have set up from the base Getting Started Guide. Check it for more information if you want to have it as a CMake subproject as well.

1b. Option B: install Magnum separately and let CMake find it

You likely already have Magnum packages ready for your platform:

Note that besides corrade and magnum you'll need also the magnum-plugins and magnum-extras packages. Everything the UI library relies on is included in the default set of libraries enabled by all packages.

If you cannot use a package, follow the manual building guide for Corrade, Magnum, Magnum Plugins and Magnum Extras to build & install everything. Enable MAGNUM_WITH_SDL2APPLICATION for Magnum, MAGNUM_WITH_STBIMAGEIMPORTER and MAGNUM_WITH_STBTRUETYPEFONT for Magnum Plugins, and MAGNUM_WITH_UI for Magnum Extras.

Once you install everything, you don't need to edit the bootstrap project CMakeLists.txt in any way, worst case you may have to pass CMAKE_PREFIX_PATH to CMake if you installed the dependencies to a non-standard location.

2. Review project structure

Compared to the base bootstrap project used in the Getting Started Guide, there's just one extra file, modules/FindMagnumExtras.cmake, which takes care of finding the UI library. But again there's no need to pay attention to contents of the modules/ directory, its main purpose is to make the initial CMake experience better.

The src/CMakeLists.txt file contains additional bits for finding & linking the UI library, other than that it's the same as before. The font and image plugins are loaded dynamically at runtime and thus don't require any CMake setup:

find_package(Magnum REQUIRED Sdl2Application)
find_package(MagnumExtras REQUIRED Ui)

set_directory_properties(PROPERTIES CORRADE_USE_PEDANTIC_FLAGS ON)

add_executable(MyApplication MyApplication.cpp)
target_link_libraries(MyApplication PRIVATE
    Magnum::Application
    Magnum::Magnum
    MagnumExtras::Ui)

All scaffolding for the UI library is then inside MyApplication.cpp. The application class has a Ui::UserInterfaceGL member, which sets up everything for drawing the UI via OpenGL, and implements handlers for passing all events through to the UI:

#include <Magnum/Platform/Sdl2Application.h>
#include <Magnum/Ui/UserInterfaceGL.h>

using namespace Magnum;

namespace {

class MyApplication: public Platform::Application {
    public:
        explicit MyApplication(const Arguments& arguments);

    private:
        void viewportEvent(ViewportEvent& event) override;
        void drawEvent() override;
        void pointerPressEvent(PointerEvent& event) override;
        void pointerReleaseEvent(PointerEvent& event) override;
        void pointerMoveEvent(PointerMoveEvent& event) override;
        void scrollEvent(ScrollEvent& event) override;
        void keyPressEvent(KeyEvent& event) override;
        void keyReleaseEvent(KeyEvent& event) override;
        void textInputEvent(TextInputEvent& event) override;

        Ui::UserInterfaceGL _ui;
};



}

MAGNUM_APPLICATION_MAIN(MyApplication)

The application sets up a resizable window and the builtin Ui::DarkTheme with UI animations enabled. Animations require a time source, which is implemented below in the now() function that's subsequently passed around. The viewportEvent() propagates window size changes to the UI, drawEvent() takes care of UI animations and redrawing, and the pointerPressEvent() and all other event implementations just proxy the events to the UI without doing anything else.

#include <Magnum/GL/DefaultFramebuffer.h>
#include <Magnum/GL/Renderer.h>
#include <Magnum/Math/TimeStl.h>
#include <Magnum/Ui/Application.h>
#include <Magnum/Ui/Theme.h>



Nanoseconds now() {
    return Nanoseconds{std::chrono::steady_clock::now()};
}

MyApplication::MyApplication(const Arguments& arguments):
    Platform::Application{arguments, Configuration{}
        .addWindowFlags(Configuration::WindowFlag::Resizable)},
    _ui{*this, Ui::DarkTheme{Ui::DarkTheme::Feature::Animations}}
{
    /* UI requires premultiplied alpha */
    GL::Renderer::setBlendFunction(GL::Renderer::BlendFunction::One,
                                   GL::Renderer::BlendFunction::OneMinusSourceAlpha);

    /* TODO: Add your UI and other initialization code here */
}

void MyApplication::viewportEvent(ViewportEvent& event) {
    GL::defaultFramebuffer.setViewport({{}, event.framebufferSize()});

    _ui.setSize(event);
}

void MyApplication::drawEvent() {
    GL::defaultFramebuffer.clear(GL::FramebufferClear::Color);

    /* TODO: Add your other drawing code here, if any */

    _ui
        .advanceAnimations(now())
        .draw();

    swapBuffers();
    if(_ui.state())
        redraw();
}

void MyApplication::pointerPressEvent(PointerEvent& event) {
    _ui.pointerPressEvent(event, now());

    if(_ui.state())
        redraw();
}

3. Build & run the project

To verify the initial setup, let's build and run the project first. The build workflow is no different from the base Getting Started Guide:

  • If you went with the CMake subproject approach (Option A above), the project is configured to place all binaries into a common location, the application will be placed in Debug/bin/MyApplication (on Windows along with all DLLs it needs), and plugins that the UI library automatically loads will be in Debug/lib/magnum-d (or Debug/bin/magnum-d on Windows).
  • If you went with Option B, externally installed Magnum, the executable gets placed into its default location in src/MyApplication, and libraries and plugins get loaded automatically from where CMake found them, worst case on Windows you may need to adjust %PATH%.
Image

If everything goes well, you should end up with a completely blank window like above. The UI is there, all ready, but so far contains nothing. Let's fix that now.

Your first button

The most essential interactive piece of a UI is usually a button. Here it's exposed as a Ui::Button, which is a widget class that creates the button on construction and removes it from the UI when destructed. We'll thus add it as a _button member and initialize it in the application constructor using a Ui::Anchor, a text, and an optional Ui::ButtonStyle.


#include <Magnum/Ui/Anchor.h>
#include <Magnum/Ui/Button.h>

class MyApplication: public Platform::Application {
    
    private:
        Ui::UserInterfaceGL _ui;
        Ui::Button _button;
};

MyApplication::MyApplication(const Arguments& arguments):
    
    _button{Ui::Anchor{_ui, (_ui.size() - Vector2{100, 40})*0.5f, {100, 40}},
        "Hello!", Ui::ButtonStyle::Success}
{
    

The Ui::Anchor takes care of positioning the widget and accepts a position and a size. Here we size it to {100, 40} and place it in the center of the window. When you build and open the application again, you'll be greeted by a button looking like this, just interactive, reacting to hover and press:

Image

To make the button do something when clicked, we'll pass a lambda to Ui::Button::onTrigger(). The function actually takes a Containers::Function that can wrap any function object, so be sure to #include it to have the lambda properly recognized. Here we'll make the button just switch the style between a green and a red one:

#include <Corrade/Containers/Function.h>



    _button.onTrigger([this] {
        _button.setStyle(_button.style() == Ui::ButtonStyle::Success ?
            Ui::ButtonStyle::Danger : Ui::ButtonStyle::Success);
    });
}

Deferring widget member creation

Having to set up widgets inside the application constructor initializer list, like shown above, is rather limiting and quickly becomes a burden. A common pattern is to instead initialize the widget members using the NoCreate tag (i.e., here with the Ui::Button::Button(NoCreateT) constructor), and then replace them with real instances from within common constructor code:

class MyApplication: public Platform::Application {
    
    private:
        Ui::UserInterfaceGL _ui;
        Ui::Button _button{NoCreate};
};

MyApplication::MyApplication(const Arguments& arguments):  {
    

    _button = Ui::Button{
        Ui::Anchor{_ui, (_ui.size() - Vector2{100, 40})*0.5f, {100, 40}},
        "Hello!", Ui::ButtonStyle::Success};
}

Automatic widget position and size

Finally, while it's possible to have the UI built with hardcoded widget positions and sizes, commonly the widgets are organized in a layout hierarchy that takes care of this automatically. First, let's use a layout just for the lone button — we'll use Ui::SnapLayout::snapRoot(), which snaps a widget to the whole UI, and pass it as an anchor to the Ui::Button constructor. The empty Ui::Snaps mean snapping it, well, nowhere, thus to the center:

#include <Magnum/Ui/SnapLayout.h>



MyApplication::MyApplication(const Arguments& arguments):  {
    

    _button = Ui::Button{
        Ui::SnapLayout::snapRoot(_ui, Ui::Snaps{}),
        "Hello!", Ui::ButtonStyle::Success};
}

The function takes an optional size argument as well, but here we omit it, making the layout pick a size on its own — the theme specifies all sizing and spacing, and the button is additionally made large enough to fit its label. Additionally, compared to manual placement, the widget now also stays in the center when the window gets resized:

Image

Widget layouts

Now let's finally move on to something non-trivial. A login wall is easily among the most loved UI patterns, so let's create one. In addition to a button we'll need a bunch of Ui::Label instances and two Ui::Input widgets:

#include <Magnum/Ui/Input.h>
#include <Magnum/Ui/Label.h>

class MyApplication: public Platform::Application {
    
    private:
        
        Ui::Label _titleLabel{NoCreate},
            _usernameLabel{NoCreate},
            _passwordLabel{NoCreate};
        Ui::Input _username{NoCreate};
        Ui::PasswordInput _password{NoCreate};
        Ui::Button _signIn{NoCreate};
};

In the constructor we'll take the the anchor coming from Ui::SnapLayout::snapRoot() — again centering in the window — and turn it into a Ui::SnapLayoutColumn. Calling child() on the layout then puts the widgets one after another into a column. For the labels and buttons we again don't need to specify sizes as the widgets will size themselves based on contents, however the inputs have no contents on their own and thus we'll give them a reasonable width explicitly:

Ui::SnapLayoutColumn layout = Ui::SnapLayout::snapRoot(_ui, Ui::Snaps{});

_titleLabel = Ui::Label{layout.child(), "Login required", Ui::LabelStyle::Title};

_usernameLabel = Ui::Label{layout.child(), "Username:"};
_username = Ui::Input{layout.child({200, 0})};

_passwordLabel = Ui::Label{layout.child(), "Password:"};
_password = Ui::PasswordInput{layout.child({200, 0})};

_signIn = Ui::Button{layout.child(), "Sign in", Ui::ButtonStyle::Primary};
_signIn.onTrigger([this] {
    /* For a lack of a better idea let's just leak the password to stdout :) */
    Debug{} << "Signing in with" << _username.text() << "and" << _password.text();
});

When you build and run, the form will look like this. Beautiful, isn't it? Almost alive.

Image

Stateful and stateless widgets

One important aspect of the Ui library is that internally only the essential data needed for drawing the UI and handling events are stored. A Ui::ButtonStyle, for example, is a high-level user-facing concept of the Ui::Button class, but internally it gets translated to individual text and background styles and nothing needs to know it's a button. Similarly, the button text is internally just a sequence of font glyphs, as unlike an Ui::Input the button text isn't going to be edited in response to an event and thus storing the source string would be needless overhead.

A consequence of the above is that the widget classes are just a stateful interface for the application code, allowing it to access and modify widget properties. But apart from user callbacks like the lambdas passed to Ui::Button::onTrigger(), the widget instances are not accessed in any way when the UI is drawn and events are processed. Thus, if the application itself doesn't need to access the widgets, there's no point in keeping them.

As mentioned above, the default behavior is that destructing a widget class removes the widget, which is a common RAII expectation. So just turning the widget members into local variables isn't going to work. Instead, widgets support construction using the Ui::NonOwned tag (such as Ui::Button::Button(NonOwnedT, Anchor, Containers::StringView, …)), which effectively makes the destructors no-op. These widgets then get implicitly removed once any of their parents is removed, such as the layout they're in.

In conclusion, from the layout we have we only need to access the _username and _password from the button trigger function. Everything else can be switched to stateless variants, i.e. along with removing the _titleLabel, _usernameLabel, _passwordLabel and _signIn members. For buttons and labels we can even use the Ui::button() and Ui::label() convenience functions that create stateless widgets with no actual instance:

Ui::SnapLayoutColumn layout = Ui::SnapLayout::snapRoot(_ui, Ui::Snaps{});
Ui::label(layout.child(), "Login required", Ui::LabelStyle::Title);

Ui::label(layout.child(), "Username:");
_username = Ui::Input{layout.child({200, 0})};

Ui::label(layout.child(), "Password:");
_password = Ui::PasswordInput{layout.child({200, 0})};

Ui::button(layout.child(), "Sign in", [this] {
    
}, Ui::ButtonStyle::Primary);

Container widgets

Besides leaf widgets like labels, buttons or inputs, there are container widgets, with other widgets placed inside. Let's put our form into a nice tidy Ui::Panel — we'll use it as the top-level widget instead of the layout we've made above, and form a column layout out of Ui::Panel::contents(), which makes the panel wrap the contents:

Ui::Panel panel{Ui::NonOwned, Ui::SnapLayout::snapRoot(_ui, Ui::Snaps{}),
    Ui::PanelStyle::Filled};
Ui::SnapLayoutColumn layout = panel.contents();
Ui::label(layout.child(), "Login required", Ui::LabelStyle::Title);

Ui::label(layout.child(), "Username:");
_username = Ui::Input{layout.child({200, 0})};

Ui::label(layout.child(), "Password:");
_password = Ui::PasswordInput{layout.child({200, 0})};

Ui::button(layout.child(), "Sign in", [this] {
    
}, Ui::ButtonStyle::Primary);

Image

Another container widget is for example the Ui::ScrollArea, which implements a clipped view that can be dragged and scrolled.

Nested layouts

Finally, the above is just a plain column layout that's wasting a lot of vertical space, and we're likely not making an iPhone UI just yet. So let's make it a bit more compact, aligning the labels next to the input fields in nested Ui::SnapLayoutRow instances instead:

Ui::SnapLayoutColumnRight layout = panel.contents();
Ui::label(layout.child(), "Login required", Ui::LabelStyle::Title);

Ui::SnapLayoutRow usernameLayout = layout.child();
Ui::label(usernameLayout.child(), "Username:");
_username = Ui::Input{usernameLayout.child({200, 0})};

Ui::SnapLayoutRow passwordLayout = layout.child();
Ui::label(passwordLayout.child(), "Password:");
_password = Ui::PasswordInput{passwordLayout.child({200, 0})};

Ui::button(layout.child(), "Sign in", [this] {
    
}, Ui::ButtonStyle::Primary);
Image

The theme and layout algorithm again makes sure everything is evenly spaced and lined up. For simplicity we aligned everything to the right by switching the layout to Ui::SnapLayoutColumnRight, another option would be for example making the title and sign up layouts 200 units wide as well to center them relative to the input fields.

And now what?

The above guided you through the initial setup for a user interface and explained essential concepts and workflows with the library. More UI tutorials are being written, for now feel free to dig deeper:

  • The Ui::AbstractUserInterface documentation has a more detailed description of the application setup as well as all underlying concepts that power the library
  • The Ui::AbstractWidget class describes relation between widgets, anchors and nodes, and expands on the concept of stateful and stateless widgets and ownership
  • Capabilities of the builtin layout engine are described in the Ui::SnapLayouter class docs, along with the high-level interfaces used for widget placement
  • Everything else, including a list of builtin widgets is accessible from the Ui namespace docs