User Guide

What are Widgets?

Widgets are eventful C++ objects that have a representation in the browser, often a control like a slider, a textbox etc.

Widgets can be used to build interactive GUIs in Jupyter notebooks. They can also be used to synchronize information between C++ and JavaScript.

Using Widgets

Widgets can be used in the Jupyter notebook with the xeus-cling C++ kernel simply by importing headers of the xwidgets library. For example, including the xslider headers makes the slider widget available.

#include "xwidgets/xslider.hpp"

Displaying Widgets

Widgets can be displayed using Jupyter’s display framework.

xw::slider<double> slider;
slider.display();

The Model-View-Controller Pattern

If you display the same widget twice, the displayed instances in the front-end will remain in sync with each other. Dragging one slider will modify the value for the other slider. The reason for that is that each time a widget is displayed, a new visual representation is created, reflecting the same underlying object, or model. This Model-View-Controller (MVC) architectural pattern is applied. Each C++ instance of a widget type is a new instance of the model.

model-view controller

The Value Semantics in xwidgets

The xwidgets framework differs from most common C++ widgets framework in that widgets have a value semantics instead of an entity semantics. Not dynamic polymorphism is at play, but static polymorphism based on the CRTP pattern.

A consequence, is that if a widget is copied, the resulting widget instance will have a new counterpart in the front-end. In the following example, slider2 is a copy of slider1. Upon creation, a new front-end widget is created, reflecting the state of that new widget instance. The states of slider1 and slider2 are not synchronized.

xw::slider<double> slider1;
auto slider2 = slider1;
slider2.value = 50.0;
slider1.display();
slider2.display();

However, if slider2 was a reference on slider1, or if slider1 had been moved to slider2, slider2 would still refer to the same widget model in the front-end.

Resource Acquisition is Initialization

The xwidgets framework makes use of the RAII pattern (Resource Acquisition is Initialization). Creating a new widget instance results in the creation of the counterpart in the HTML/JavaScript frontend. The destructor triggers the destruction of the frontend model and all the views, unless the object being destructed has already been moved from.

This architecture ties the lifetime of the front-end object to that of the C++ model.

Naming Conventions and Widget Generators

CRTP bases and final classes

Widget classes with names prefixed by x are not meant to be directly instantiated. For example, xslider is the top-most CRTP base of slider.

Widget classes with names that are not prefixed by x can be instantiated, but are final, i.e. they cannot be inherited from.

In fact, these final classes are typedefs on a special template parameterized by its base. For example, we have:

using button = xmaterialize<xbutton>;

Similarly, for template widget types, we also make use of the xmaterialize class, which

template <class T>
using slider = xmaterialize<xslider, T>;

The xmaterialize class only implements the final inheritance closing the CRTP, together with the RAII logic, which is to be done at the top-most inheritance level, so that widget creation messages are sent after all the bases have been initialized.

Generator classes

Simple widget types such as slider may have a large number of attributes that can be set by the user, such as handle_color, orientation, min, max, value, step, readout_format.

Providing a constructor for slider with a large number of such attributes would make the use of xwidgets very cumbersome, because users would need to know all the positional arguments to modify only one value. Instead, we mimick a keyword argument initialization with a method-chaining mechanism.

auto button = xw::slider<double>::initialize()
    .min(1.0)
    .max(9.0)
    .value(4.0)
    .orientation("vertical")
    .finalize();

This is a classical approach: calls to min, max, value and orientation all return the slider instance (by rvalue reference, which is optimized with C++ move semantics and copy ellision). The finalize() triggers the creation of the front-end object with the data.

Widget Events

Special Events

Certain widget types such as button are not used to represent data types. Instead, the button widget is used to handle mouse clicks. The on_click method of the button widget can be used to register functions to be called when the button is clicked.

xw::button button;

void foo()
{
    std::cout << "Clicked!" << std::endl;
}

button.on_click(foo);
button.display();

Xproperty Events

The observer pattern of xwidgets relies upon the xproperty library.

xproperty can be used to

  • register callbacks on changes of widget properties

  • register custom validators to only accept certain values

  • link properties of different widgets

Registering an Observer

In this example, we register an observer for a slider value, triggering the printing of the new slider value.

xw::slider<double> slider;
slider.display()
XOBSERVE(slider, value, [](const auto& s) {
    std::cout << "Observer: New Slider value: " << s.value << std::endl;
});

Registering a Validator

In this example, we validate the proposed values for a numerical text. Negative values are rejected.

xw::number<double> number;
number.display()
XVALIDATE(number, value, [](const auto&, double proposal) {
    std::cout << "Validator: Proposal: " << proposal << std::endl;
    if (proposal < 0)
    {
        throw std::runtime_error("Only non-negative values are valid.");
    }
    return proposal;
});

For more details about the API for xproperty, we refer to the xproperty documentation.