Skip to main content

C++

Installation

Installing the main dependency can be done manually by downloading it from the releases page or by using the FetchContent module in cmake. The latter is documented in the sample projects and is recommended for new projects. It will not automatically udate your version unless you specify a different tag.

Automated via cmake

To actually see the current way to include WUI automatically, please refer to hte sample projects external include folder.

After specifying the version of WUI with FetchContent_Declare you can use FetchContent_MakeAvailable to download and unpack the library. CMake includes the root folder and helper cmake that is included in the release. A command named add_wui_target is available after this step, which is used to automatically copy necessary library files to your target and add it as a dependency. It also sets up the necessary include and link directories.

info

The sample projectss include WUI as a submodule, this is not necessary for projects that are not "developers of wui". All cmake code that is referred to that subfolder can savely be deleted (and is deactivated by default).

Default path

The default path for WUI will be ${CMAKE_BINARY_DIR}/bin on windows and ${CMAKE_BINARY_DIR}/bin/wui_resources on linux. On linux this makes the folder structure a bit cleaner, this is only possible because of linux's RPATH feature, which windows does not have (so everything needs to be dumped next to whichever executable needs it).

Should it be needed to specifiy WUI to some specific path, set WUI_OUT_DIR BEFORE calling FetchContent_MakeAvailable.

Initializing WUI

To initialize WUI only 2 functions are necessary on the C++ side:

#include <webUi.hpp> // init and config


// WUI_ERROR_CHECK is defined in webUiTypes.hpp
// if NDEBUG is not set (Release mode) it will do nothing
// if NDEBUG is set (Debug mode) it will exit the program with an error message should the encapsulated function return anything other than WUI_OK (0)

WUI_ERROR_CHECK(

wui::WuiInit(

// not providing an argument is the same as providing a default configuration
wui::WuiConfig() // Check the header for exact docs on the struct and its settings. A parameter is not required for intitialization
)
);

// shutdown WUI with
WUI_ERROR_CHECK(wui::WuiDeInit());

This will start the sub-processes that WUI needs to run. By itself this does not do anything yet, but you might see an increase in RAM usage.

Settings

WUI has a few settings that can be set in the WuiConfig struct. To get the most up-to-date information on the settings, check the header file webUi.hpp. See the in-line comments of WuiConfig for more information on the settings, but the default configuration should be fine for most use-cases. Note that this assumes where the library is located relative to your executable, adjust the settings to your needs.

Sensitive information

The setting Cache Directory Path is used as the browsers cache directory. This means that any information stored in the cache will be stored in this directory. This can include sensitive information like cookies, local storage, etc. It is recommended to keep this directory clean and secure. The directory is not automatically cleaned up by WUI.

It is assumed that whatever you are doing is:

  1. not sensitive
  2. you are aware of the implications of storing data in the cache directory.

If you plan on logging your user in it is recommended to do so in one go and ensure the cache is cleared afterwards.


Tab

To create a Tab

#include <webUiTab.hpp>

auto tab_id = 0;

// Configuration is done via the TabConfig struct
wui::TabConfig tab_config = wui::TabConfig();
tab_config.initialDimensions.width = 400;
tab_config.initialDimensions.height = 400;
tab_config.initialPath = "localhost:8080";

WUI_ERROR_CHECK(wui::createTab(tab_id, tab_config));

// to get the current UI pixel-buffer
wui::WuiTabPixelBuffer tabBuffer;

// usually this is called for every frame and the buffer is used in your rendering pipeline
WUI_ERROR_CHECK(wui::copyPixelBuffer(tab_id, tabBuffer));

// To load a different URL you can use
WUI_ERROR_CHECK(wui::loadUrl(tab_id, "localhost:8080"));

Notes:

  • This initially starts the UI with 400 by 400 pixels
  • In this example we navigate to localhost:8080 assuming there is a dev-version of the UI running there with webpack. You can also use any file://<path>.html path to load your file directly (relative to your C++ executable).
  • The buffer inside tabBuffer is valid until the object itself is destroyed (the buffer is allocated with malloc).
  • The buffer is of type void * and has format BGRA (4 bytes per pixel). See the javadoc for details.
  • The most straightforward way to render WUI is to overlay the current snapshot as the last step in your rendering pipeline. Usually the WUI renders with 30fps (as this is standard for browsers).

Input

Bindings for this can be found in input/* (a few files may apply depending on mouse or keyboard events).

Keyboard and mouse events need to be forwarded to WUI and the respective Tab that should handle the event.

Keyboard

The input keyboard events WUI takes is fully transparent to CEFs keyboard events of CEF, which leans on the POSIX standard for its input codes. In the header file it's shown that a typical key event is defined as:

wui::WuiKeyEvent ev = {
wui_key_event_type_t type;
uint32_t modifiers;
KeyboardCodesPOSIX windows_key_code;
int native_key_code;
int is_system_key;
char16_t character;
char16_t unmodified_character;
int focus_on_editable_field;
}

For more detail see the internal documentation of the header file. I.g. any combination of keyboard input that an average browser understands an be re-created. To send the event sendKeyEvent is used and is always 'consumed' when sent no matter if a UI element is focused or not. The current focus mode of UI can be queried with getCurrentTextInputMode.

manual parsing will be needed

Sadly The purpose of each key is not very well documented in the CEF docs, but from expierience I can say that it usually 'works out fine'. The browser seems to make its 'best effort' so filling as much info as you have should be enough.

That being said, manual parsing and testing will certainly be needed for special key-codes or combinations.

Mouse

Mouse events are comparatively simplistic, they have a fixed coordinate and a button identifier. There are several different functions for different mouse events:

  • mouseWheelEvent (scrolling)
  • mouseMoveEvent (moving)
  • mouseClickEvent (clicking)

All of these events take some kind of coordinate information of 'where the mouse currently is'.

Before an event is forwarded to the UI it is checked if there even is any UI there. For this the pixel of the click is compared with an empty pixel (fully transparent). The event will then always be forwarded to the UI. When the event was sent, the sendEvent* function will return WUI_HIT_UI or WUI_HIT_NOT_UI respectively.

This is very useful if you want to know if UI was even hit or not and if the click was really on the game itself and not any UI.

Window Events

Bindings for this can be found in webUiTab.hpp.

The only interesting window event for WUI is the resize event.

WUI renders to an internal buffer, but it does not know what size you are expecting it to render at. Therefore you must forward resize events to WUI if you wish to change the size during runtime. E.g. If you are making a borderless window but want to enable resizing, then you will need to forward the resize events to WUI or else the UI will not match your size


wui::WuiTabInfo info;

// first check that the tab even exists
if (wui::getTabInfo(tab_id, info) == wui::WUI_OK)
{
// check that the tab is ready, may not be the case in an async environment
if (info.tabExists)
{
// This may fail if the new buffer cannot be allocated due to memory constraints
WUI_ERROR_CHECK(wui::resizeTab(tab_id, newWidth, newHeight));
}
}

Better save than sorry

WUI renders to an internal buffer with its own dimensions. Depending on your setup you might very easily cause a crash by copying the pixel buffer to a buffer that is too small. Furthermore WUI renders asynchronously, so even if you forward all resize events to WUI, a draw call might still be in progress when you copy the buffer.

To avoid this: always double check that dimensions of the wui::WuiTabPixelBuffer are what you expect them to be.

If you are using WUI as a fixed size-container this should not be omitted! It has practically no performance impact and depending on how many tabs are opened/closed in parallel and asynchronously, it might save you a lot of headache.

Load Handling

The TabConfig struct has a onLoadEndCallback member that can be set to a lambda function that takes the tab_id, the current url, the http code and an error object as arguments. Either the http code or the error object will be set, depending on the outcome of the load event.

// Example lampda function
tab_config.onLoadEndCallback = [](const wui::wui_tab_id_t tab_id, std::string currentUrl, int httpCode,
const wui::TabConfig::TabLoadError &error) -> void
{
printf("[Renderer] Tab loaded: %s in tab %d", currentUrl, tab_id);

if (httpCode == 0)
{
printf("[Renderer] Tab load error: %s Code: %d", error.errorMessage, error.errorCode);
}
else
{
printf("[Renderer] Tab load success Code %d", httpCode);
}
};

Depending on your setup you can see exactly where what your tab is currently showing. tab_id is provided for convenience, as you can have multiple tabs running at the same time but may not want to have a separate lambda for each tab.

Event binding

Bi-directional event binding is the star of the show! To see the other side of this then check here

Event binding is the main way to communicate between the C++ and the Typescript side of WUI. Every event has a name and a payload. The payload is a json object that can be of any complexity.

Syncronized payload types and event names

Sending information from one side to the other is done via events as strings. Event names are unknown between the two projects/sides. This means that it is the developer's responsibility to keep the event names and the types of the payloads consistent between the two sides.

Should you send an event that the other side is not listening to, the library will throw the appropriate error. But not every case for events is handled in this asynchronous way. This means not everything throws an error.

As a solo developer it's relatively easy to keep the two sides synchronized. As a team (or just 2 people, one for each side) it is important to have some kind of API standard that is shared between the two sides. A simple table listing event names and their payloads is enough.

If you access types that don't exist on either side you will get undefined/null at best or a crash at worst if your code is not properly guarded.

Errors

Event binding errors

When trying to send events that the other side is not listening to will produce debug logs (unless handled otherwise).

sendEvent will throw an error:

  • In typescript WUI_ERR_BINDINGS_NO_LISTENER_IN_ENGINE with an exception string No listener registered for event $eventName.
  • In C++ WUI_ERR_BINDINGS_NO_LISTENER_IN_DOM which will stop your program if wrapped in WUI_ERROR_CHECK(...).
Possible solution

Check that your application does not have any race conditions inside of it, this is easily the case on multi-threaded setups. Initialization order should always be:

  1. Initialize WUI
  2. Start the UI tab (calling only createTab is enough)
    • Check that the initial tab has been loaded successfully with onLoadEndCallback (with httpCode 200)
  3. Register all event listeners (in either side)
  4. start sending events.

Alternatively you can ignore the error on either side, as they should not affect the functionality of the application.

Sending Events

To send an event from C++ a payload needs to be created. WUI doesn't do the parsing itself, it provides nlohmann/json for this. The payload is then sent to the UI. Events are sent to a specific tab via it's tab_id. The tab_id is automatically provided when using the C++ class wrapper for the tab (sendEvent is a member fucntion of the wrapper class).

    auto json = nlohmann::json(); // Create the json object
json["score"] = score; // Add the score to the json object
json["lives"] = lives; // Add the lives to the json object
wui::sendEvent(wui_tab_id, "info", json); // Send the event to the UI

Event names and payloads are 100% up to the developer. The only thing that is important is that the event name is the same on both sides. The payload can be of any complexity, but it is recommended to keep it simple and only add information that is necessary.

As the C++ side does not have type-definitions for these json obejcts it is recommended to use a struct or class to define the payload and then send it in a dedicated function.

Listening to Events

Generally for each tab a single event listener can be registered. Deciding what event it is and what to do with it is up to the developer.


// any kind of function may be bound, a lampda is used here for simplicity
wui::registerEventListener(wui_tab_id,
[cb](const wui::wui_tab_id_t tab_id,
const std::string eventName,
const nlohmann::json &eventPayload,
nlohmann::json &successRetObj,
std::string &exception)
{

// Non 0 return values cause a throw on the TS side
// with the exception string and returned number being thrown as the 'exception string'
// see `registerFailureCallback` in the npm package for details

return 0;
});

WUI_ERROR_CHECK(wui::registerEventListener(wui_tab_id, handler));

// Unregister the event when done or no longer needed
WUI_ERROR_CHECK(wui::unregisterEventListener(wui_tab_id));

API docs

For the currennt API docs see the repo docs.