cschreib / lxgui

Portable, real time, modular and data-driven GUI C++ library.
https://cschreib.github.io/lxgui
MIT License
73 stars 20 forks source link
c-plus-plus gui library lua real-time webassembly

Build Status Build Status

What is lxgui?

In a nutshell

There are plenty of different GUI libraries out there. They all have something that makes them unique. This is also the case of lxgui. Its main advantages are:

Available GUI region types

As you can see from the screenshot below, lxgui can be used to create very complex GUIs (the "File selector" frame is actually a working file explorer!). This is mainly due to a powerful inheritance system. You can create a "virtual" frame template, containing any number of children regions and any set of properties, and then instantiate several frames that will "inherit" from this template. This reduces the necessary code, and can help you make consistent GUIs. For example, you can create a "ButtonTemplate", and use it as a base for all the buttons of your GUI. Then if you need to change your buttons to have round instead of square corners, you only need to do the modification to the button template, and it will apply to all buttons.

Demonstration

A WebAssembly live demo is accessible on-line here (if your browser supports WebGL2) or here (if your browser only supports WebGL1). Bootstrap examples are available in the examples directory in this repository, and demonstrate the steps required to include lxgui in a CMake project (requires CMake 3.14 or later).

Included in the source package (in the test directory) is a test program that should compile and work fine if you have installed the whole thing properly. It is supposed to render exactly as the sample screenshot below. It can also serve as a demo program, and you can see for yourself what the layout and script files looks like for larger scale GUIs.

Gallery

Please head to the screenshots page for examples of lxgui being used in real-world projects.

Below is a screenshot of the test program included with the library (the same interface is displayed in the live demo linked above). It is meant to test and demonstrate most of the features available in the library. Note that the "look-and-feel" displayed here is purely for demonstration; every element of the interface (colors, dialog shapes and style) is defined in fully customizable layout and script files.

Sample screenshot

This screenshot was generated on a Release (optimised) build of lxgui with the OpenGL+SFML back-end, on a system running Linux Mint 20.2, with a Ryzen 5 2600 CPU, 16GB of RAM, an Nvidia GTX 960 2GB GPU with proprietary drivers, and a standard resolution monitor. All optimisations were turned on except screen caching.

Front-end and back-ends

Using CMake (3.14 or later), you can compile using the command line, or create projects files for your favorite IDE. The front-end GUI library itself depends on:

To parse layout files, the library depends on pugixml (included as submodule), and rapidyaml (included as submodule). These are optional dependencies; you can use both if you want to support both XML and YAML layout files, or just one if you need only XML or YAML, or even neither if you want to write your UI in pure C++.

Available rendering back-ends:

Available input back-ends:

If you have the choice, the currently recommended back-ends are OpenGL for rendering, and SDL for input. The WebAssembly build supports all back-ends except SFML.

Configurable rendering options

Except for render target caching, all options are enabled by default (if supported), which should offer the best performance in most cases. It can be that your particular use case does not benefit as much from the default caching and batching implementations; this can be easily checked by trying various combinations of these options, and selecting the combination that offers the best performances for your particular use case and target hardware.

Getting started

Firstly, ensure your C++ compiler is up to date. This library requires a compiler that is C++17 compliant (GCC >= 8, clang >= 7, Apple-Clang >= 11, or Visual Studio >= 2017).

Then, clone the project, including the submodules (this is important! the library will not compile otherwise):

git clone --recurse-submodules https://github.com/cschreib/lxgui

If you have already cloned the repository and missed the --recurse-submodules option while cloning, you can still checkout all submodules at any time using the command:

git submodule update --init --recursive

The rest of the build instructions depends on your target operating system; please follow the instructions in the next sections accordingly.

Build for Linux, OSX, Windows

Make your choice of rendering and input back-end from within the following sub-sections, and install all the appropriate dependencies listed there. Once this is done, you can build and install lxgui with the standard cmake commands:

mkdir build
cd build
cmake ../ <your CMake options here>
cmake --build . --config Release
cmake --install . --config Release

Required dependencies (for all back-ends)

Install Lua (>5.1):

Dependencies for pure SFML back-end

Install SFML2:

Dependencies for pure SDL back-end

Install SDL2, SDL2_image, and SDL2_ttf:

Dependencies for OpenGL + SFML back-end

Install OpenGL, Freetype, libpng, and SFML2:

Dependencies for OpenGL + SDL back-end

Install OpenGL, Freetype, libpng, SDL2, and SDL2_image:

Build for WebAssembly / Emscripten

The WebAssembly build only supports the SDL2 back-end for input, and either the SDL2 or OpenGL back-ends for rendering (programmable pipeline only; the legacy fixed pipeline is not supported in WebGL). SDL2, OpenGL, and libpng are all already provided by default in Emscripten, so the only required dependency to setup is Lua and Freetype (at the time of writing this guide, the Freetype version in Emscripten was too old). Pre-compiled libraries are provided in dependencies/wasm.zip, but you can also build them from source yourself easily.

The SDL2 rendering back-end will support all platforms supported by SDL2, which should cover pretty much everything, but it may run slower on some platforms. The OpenGL back-end uses OpenGL ES 3, hence will only run on platforms supporting WebGL2, but it should provide the best performance. In practice, performance is highly dependent on the the host platform and browser. For example: earlier in the development of lxgui, and on my desktop machine, the SDL2 back-end was slower (30 FPS) than the OpenGL back-end (40 FPS) in Firefox, but in Chrome they both ran at the maximum 60 FPS. This is likely to change in the future, with browser updates and changes in the lxgui implementation.

With Emscripten installed and sourced in your current terminal, run

mkdir build
cd build
emcmake cmake ../ <your CMake options here>
emmake make
emmake make install

How do I use it? A tutorial.

Setting up the GUI in C++

Setting up the GUI in C++ is rather straightforward. The example code below is based on the SFML back-end, but can be adapted to other back-ends easily (see examples for help).

// Create an SFML render window
sf::RenderWindow window;

// Initialize the GUI using the SFML back-end
// NB: owner_ptr is a lightweight, unique-ownership smart pointer (similar to std::unique_ptr).
utils::owner_ptr<gui::manager> manager = gui::sfml::create_manager(window);

// Grab a pointer to the SFML input source so we can feed events to it later
input::sfml::source& sfml_source = static_cast<input::sfml::source&>(
    manager->get_input_dispatcher().get_source());

// Register any required region classes to the GUI factory.
// By default, only the base classes gui::region, gui::frame, and gui::layered_region
// are registered. If you want to use more, you need to do this explicitly. This is
// also where you would register your own region types, if any.
gui::factory& factory = manager->get_factory();
factory.register_region_type<gui::texture>();
factory.register_region_type<gui::font_string>();
factory.register_region_type<gui::button>();
factory.register_region_type<gui::slider>();
factory.register_region_type<gui::edit_box>();
factory.register_region_type<gui::scroll_frame>();
factory.register_region_type<gui::status_bar>();

// Then register your own Lua "glues" (C++ classes and functions to expose to Lua)
manager->on_create_lua.connect([](sol::state& lua) {
    // This code might be called again later on, for example when one
    // reloads the GUI (the Lua state is destroyed and created again).
    // This is where you would register your own additional Lua "glue" functions, if needed.
    // ...
});

// Then load GUI addons.
// In lxgui, the GUI is formed of multiple modular "addons", each of which defines
// the appearance and behavior of a specific GUI element (e.g., one addon for
// the player status bars, one addon for the inventory, etc.).
// See below for an example addon.

//  - First set the directory in which the GUI addons are located
manager->add_addon_directory("interface");
//  - and eventually load all addons
manager->load_ui();

// Start the main loop
sf::Clock clock;
while (true) {
    // Retrieve the window events
    sf::Event event;
    while (window.pollEvent(event)) {
        // Send these to the input source.
        sfml_source.on_sfml_event(event);

        // NB: Do not react to these raw events directly. Some of them should be
        // captured by the GUI, and must not propagate to the world rendered below.
        // Use the input signals from manager->get_world_input_dispatcher() instead.
    }

    // Compute time spent since last GUI update
    float delta = clock.getElapsedTime().asSeconds();
    clock.restart();

    // Update the GUI
    manager->update_ui(delta);

    // Render the GUI
    manager->render_ui();
}

// Resources are cleared up automatically on destruction

With these few lines of code, you can create as many "interface addons" with layout and script files as you wish. Let's consider a very simple example: we want to create an FPS counter at the bottom right corner of the screen.

With lxgui installed, compiling can be done with a simple CMake script:

cmake_minimum_required(VERSION 3.14)

# Setup main project
project(my_project LANGUAGES CXX)

# Find lxgui and dependencies
find_package(lxgui 2)

# Create new executable
add_executable(my_executable
    ${PROJECT_SOURCE_DIR}/main.cpp) # add your sources here

# Link to lxgui (here, using SFML implementation)
target_link_libraries(my_executable
    lxgui::gui::sfml # SFML rendering implementation
    lxgui::input::sfml # SFML input implementation
    lxgui::lxgui) # core library

Creating a GUI addon in XML and Lua

First create a new addon, by going to the interface folder, and creating a new folder FPSCounter. In this folder, we create a "table of content" file which lists all the *.xml and *.lua files this addons uses, and some other informations (addon author, GUI version, saved variables, ...). It has to be called FPSCounter.toc (after the name of the addon directory):

## Interface: 0001
## Title: A neat FPS counter
## Version: 1.0
## Author: You
## SavedVariables:

addon.xml

As you can see, we will only require a single XML file: addon.xml. Let's create it in the same folder. Every XML file must contain a <Ui> tag:

<Ui>
</Ui>

Then, within this tag, we need to create a frame (which is more or less a GUI container):

    <Frame name="FPSCounter">
        <Anchors>
            <Anchor point="TOP_LEFT"/>
            <Anchor point="BOTTOM_RIGHT"/>
        </Anchors>
    </Frame>

This creates a Frame named FPSCounter that fills the whole screen: the <Anchor> tags forces the top-left and bottom-right corners to match the screen's top-left and bottom-right corners. Now, within the Frame, we create a FontString object, which can render text:

    <Frame name="FPSCounter">
        <Anchors>
            <Anchor point="TOP_LEFT"/>
            <Anchor point="BOTTOM_RIGHT"/>
        </Anchors>
        <Layers><Layer>
            <FontString name="$parentText" font="interface/fonts/main.ttf" text="" fontHeight="12" alignX="RIGHT" alignY="BOTTOM" outline="NORMAL">
                <Anchors>
                    <Anchor point="BOTTOM_RIGHT">
                        <Offset>
                            <AbsDimension x="-5" y="-5"/>
                        </Offset>
                    </Anchor>
                </Anchors>
                <Color r="0" g="1" b="0"/>
            </FontString>
        </Layer></Layers>
    </Frame>

We named our FontString $parentText. In UI element names, $parent gets automatically replaced by the name of the object's parent; in this case, its full name will end up as FPSCounterText.

Intuitively, the font attribute specifies which font file to use for rendering (can be a *.ttf or *.otf file), fontHeight the size of the font (in points), alignX and alignY specify the horizontal and vertical alignment, and outline creates a black border around the letters, so that the text is readable regardless of the background content. We anchor it at the bottom right corner of its parent frame, with a small offset in the <Offset> tag (also specified in points), and give it a green color with the <Color> tag.

NB: the GUI positioning is done in "points". By default, on traditional displays a point is equivalent to a pixel, but it can be equivalent to two or more pixels on modern hi-DPI displays. In addition, the GUI can always be rescaled by an arbitrary scaling factor (in the same way that you can zoom on a web page in your browser). This rescaling factor is set to 1.0 by default, but changing its value also changes the number of pixels per points.

Now that the GUI structure is in place, we still need to display the number of frame per second. To do so, we will define two "scripts" for the FPSCounter Frame:

        <Scripts>
            <OnLoad>
                -- This is Lua code !
                self.update_time = 0.5;
                self.timer = 1.0;
                self.frames = 0;
            </OnLoad>
            <OnUpdate>
                -- This is Lua code !
                self.timer = self.timer + arg1;
                self.frames = self.frames + 1;

                if (self.timer > self.update_time) then
                    local fps = self.frames/self.timer;
                    self.Text:set_text("FPS : "..fps);

                    self.timer = 0.0;
                    self.frames = 0;
                end
            </OnUpdate>
        </Scripts>

The <OnLoad> script gets executed only once, when the Frame is created. It is used here to initialize some variables. The <OnUpdate> script is called every frame (use it carefully...). It provides the time elapsed since last update in the arg1 variable. We use it to record the number of frames that are rendered, and update the FPS counter every half seconds.

The self variable in Lua is the equivalent of this in C++: it is a reference to the object running the script, here the FPSCounter Frame. Note that, since we called the FontString $parentText, we can use the handy shortcut self.Text instead of the full name FPSCounterText to reference the FontString object in Lua. This is good practice, and allows for more generic and modular code.

Once this is done, we have the full XML file:

<Ui>
    <Frame name="FPSCounter">
        <Anchors>
            <Anchor point="TOP_LEFT"/>
            <Anchor point="BOTTOM_RIGHT"/>
        </Anchors>
        <Layers><Layer>
            <FontString name="$parentText" font="interface/fonts/main.ttf" text="" fontHeight="12" alignX="RIGHT" alignY="BOTTOM" outline="NORMAL">
                <Anchors>
                    <Anchor point="BOTTOM_RIGHT">
                        <Offset>
                            <AbsDimension x="-5" y="-5"/>
                        </Offset>
                    </Anchor>
                </Anchors>
                <Color r="0" g="1" b="0"/>
            </FontString>
        </Layer></Layers>
        <Scripts>
            <OnLoad>
                -- This is Lua code !
                self.update_time = 0.5;
                self.timer = 1.0;
                self.frames = 0;
            </OnLoad>
            <OnUpdate>
                -- This is Lua code !
                self.timer = self.timer + arg1;
                self.frames = self.frames + 1;

                if (self.timer > self.update_time) then
                    local fps = self.frames/self.timer;
                    self.Text:set_text("FPS : "..math.floor(fps));

                    self.timer = 0.0;
                    self.frames = 0;
                end
            </OnUpdate>
        </Scripts>
    </Frame>
</Ui>

... and a working GUI addon!

One last thing to do before being able to see it in your program is to go to the interface folder, and create a file called addons.txt. This file must exist, and must contain the list of addons that you want to load. In our case just write:

FPSCounter:1

The 1 means "load". If you put a 0 or remove that line, your addon will not be loaded.

Equivalent using YAML

The above addon, using YAML instead of XML, would look like the following:

ui:
  frame:
    name: FPSCounter
    anchors:
      - point: TOP_LEFT
      - point: BOTTOM_RIGHT

    layers:
      layer:
        font_string:
          name: $parentText
          font: interface/fonts/main.ttf
          text: ""
          font_height: 12
          align_x: RIGHT
          align_y: BOTTOM
          outline: NORMAL
          color: {r: 0, g: 1, b: 0}
          anchors:
            - point: BOTTOM_RIGHT
              offset: {abs_dimension: {x: -5, y: -5}}

    scripts:
      on_load: |
        -- This is Lua code !
        self.update_time = 0.5;
        self.timer = 1.0;
        self.frames = 0;

      on_update: |
        -- This is Lua code !
        self.timer = self.timer + arg1;
        self.frames = self.frames + 1;

        if (self.timer > self.update_time) then
            local fps = self.frames/self.timer;
            self.Text:set_text("FPS : "..math.floor(fps));

            self.timer = 0.0;
            self.frames = 0;
        end

Equivalent in pure C++

Re-creating the above addon in pure C++ is perfectly possible. This can be done with the following code:

// Root frames (with no parents) are owned by the UI "root".
gui::root& root = manager->get_root();

// Create the Frame.
// NB: observer_ptr is a lightweight, non-owning smart pointer (vaguely similar to std::weak_ptr).
utils::observer_ptr<gui::frame> frame =
    root.create_root_frame<gui::frame>("FPSCounter");
frame->set_anchor(gui::point::TOP_LEFT);
frame->set_anchor(gui::point::BOTTOM_RIGHT);

// Create the FontString as a child region of the frame.
utils::observer_ptr<gui::font_string> text =
    frame->create_layered_region<gui::font_string>(gui::layer::ARTWORK, "$parentText");
text->set_anchor(gui::point::BOTTOM_RIGHT, gui::vector2f{-5, -5});
text->set_font("interface/fonts/main.ttf", 12);
text->set_alignment_x(gui::alignment_x::RIGHT);
text->set_alignment_y(gui::alignment_y::BOTTOM);
text->set_outlined(true);
text->set_text_color(gui::color::GREEN);
text->notify_loaded(); // must be called on all objects when they are fully set up

// Create the scripts in C++ (one can also provide a string containing some Lua code).
float update_time = 0.5f, timer = 1.0f;
int frames = 0;
frame->add_script("OnUpdate", [=](gui::frame& self, const event_data& args) mutable
{
    float delta = args.get<float>(0);
    timer += delta;
    ++frames;

    if (timer > update_time)
    {
        utils::observer_ptr<gui::font_string> text = self.get_region<gui::font_string>("Text");
        text->set_text("FPS : "+utils::to_string(std::floor(frames/timer)));

        timer = 0.0f;
        frames = 0;
    }
});

// Tell the Frame is has been fully loaded.
frame->notify_loaded();