Magnum::Ui::DebugLayer class new in Git master

Debug layer.

Provides a non-intrusive and extensible way to inspect node hierarchy and layer data attachments in any existing UI for debugging purposes. You can use either the DebugLayer base for inspection alone, or the DebugLayerGL subclass that provides also a way to visualize highlighted nodes directly in the inspected UI.

Setting up a debug layer instance

A debug layer, either DebugLayer or DebugLayerGL, is constructed using a fresh AbstractUserInterface::createLayer() handle, a combination of DebugLayerSource values that the debug layer should track and a DebugLayerFlag combination describing the actual behavior; and is subsequently passed to AbstractUserInterface::setLayerInstance(). For correct behavior it should be added as the very last layer so it's drawn on top of all other layers and reacts to events first.

ui.setLayerInstance(Containers::pointer<Ui::DebugLayerGL>(
    ui.createLayer(),
    Ui::DebugLayerSource::NodeHierarchy|Ui::DebugLayerSource::NodeDataAttachments,
    Ui::DebugLayerFlag::NodeHighlight));

With this, assuming AbstractUserInterface::draw() is called in an appropriate place, the layer is ready to use. You likely don't need to keep a reference to it as it will track changes in enabled sources without further involvement. In case of the base DebugLayer that doesn't draw, or when inspecting the UI programmatically, calling just AbstractUserInterface::update() (which is otherwise called from draw()) is enough to make it aware of latest state changes.

Node highlight

The setup shown above, in particular with DebugLayerFlag::NodeHighlight together with at least DebugLayerSource::Nodes enabled, makes it possible to highlight any node in the hierarchy and see its details. NodeHierarchy additionally shows info about parent and child nodes and NodeDataAttachments also lists data attachments. Let's say we have a Ui::Button placed somewhere in the UI, reacting to a tap or click:

Ui::NodeHandle button = Ui::button(, Ui::Icon::Yes, "Accept");

ui.eventLayer().onTapOrClick(button, []{
    
});
Image

With the DebugLayer set up, clicking on this button with Ctrl right mouse button (or Ctrl pen eraser in case of a pen input) highlights the node, showing a magenta rectangle over, and prints details about it to the console like shown below. Clicking on any other node will highlight that one instead, clicking again on the highlighted node will remove the highlight.

Image
Node {0x9, 0x2}
  Nested at level 1 with 0 direct children
  4 data from 3 layers

Naming nodes and layers

In the details we can see that the node is placed somewhere and it has four data attachments. Because the widget is simple we can assume it's the background, the icon, the text and the event handler. It would be better if the layer could tell us that, but because naming various resources isn't essential to UI functionality, and because the DebugLayer is designed to work with any custom user interface containing any custom layers, not just the builtin ones, it can't have any knowledge about layer names on its own.

We have to supply those with setLayerName(). In this case we'll name all layers exposed on the UserInterface instance, you can do the same for any custom layer as well. Similarly, setNodeName() allows to assign names to particular nodes.

debugLayer.setLayerName(ui.eventLayer(), "Event");
debugLayer.setLayerName(ui.baseLayer(), "Base");
debugLayer.setLayerName(ui.textLayer(), "Text");
debugLayer.setNodeName(button, "Accept button");

Highlighting the same node then groups the data by layer, showing them in the order they're drawn. Besides the names now being listed in the printed details, you can query them back with layerName() and nodeName().

Node {0x9, 0x2} Accept button
  Nested at level 1 with 0 direct children
  1 data from layer {0x0, 0x1} Base
  2 data from layer {0x1, 0x1} Text
  1 data from layer {0x2, 0x1} Event

Showing details about data attachments

Enabling DebugLayerSource::NodeDataAttachmentDetails in addition to NodeDataAttachments makes use of debug integration implemented by a particular layer. In case of BaseLayer, TextLayer and other visual layers this makes the output show also style assignment as well as any style transitions, if present. In case of EventLayer it shows the event that's being handled:

Node {0x9, 0x2} Accept button
  Nested at level 1 with 0 direct children
  Data {0x3, 0x2} from layer {0x0, 0x1} Base
    Inactive out style: 1
    Inactive over style: 2
    Pressed out style: 3
    Pressed over style: 4
    Disabled style: 5
  Data {0x6, 0x1} from layer {0x1, 0x1} Text
    Inactive style: 3
    Pressed style: 7
    Disabled style: 11
  Data {0x7, 0x1} from layer {0x1, 0x1} Text
    Inactive style: 4
    Pressed style: 8
    Disabled style: 12
  Data {0x0, 0x1} from layer {0x2, 0x1} Event reacting to tap or click

The way this works is that by passing a concrete layer type, the setLayerName(const T&, const Containers::StringView&) overload gets picked if the type contains a DebugIntegration inner class. Instance of which then gets used to print additional details. The integration can take various optional parameters, such as a function to provide style names in case of visual layers. Documentation of all builtin layers has information about what's provided in their debug integration. It's also possible to implement this integration for custom layers, see DebugLayer integration for custom layers below for details.

Node highlight options

Node highlight has defaults chosen in a way that makes the highlight clearly visible on most backgrounds, and with the pointer gesture unique enough to not conflict with usual event handlers. You can change both with setNodeHighlightColor() and setNodeHighlightGesture().

The default set of accepted gestures does not include touch input however, as on a touch device it usually isn't possible to press any modifier keys to distinguish a "debug tap" from a regular tap. It's however possible to use addFlags() and clearFlags() to enable DebugLayerFlag::NodeHighlight only temporarily and during that time accept touches without any modifiers, for example in a response to some action in the UI that enables some sort of a debug mode:

/* Enable node highlighting with just a touch */
debugLayer
    .addFlags(Ui::DebugLayerFlag::NodeHighlight)
    .setNodeHighlightGesture(Ui::Pointer::Finger, {});



/* Disable it again and revert to a safe gesture when not used anymore */
debugLayer
    .clearFlags(Ui::DebugLayerFlag::NodeHighlight)
    .setNodeHighlightGesture(Ui::Pointer::MouseRight, Ui::Modifier::Ctrl);

Besides visual highlighting using a pointer, highlightNode() allows to highlight a node programmatically.

Finally, while the highlighted node details are by default printed to Debug, a console might not be always accessible. In that case you can direct the message to a callback using setNodeHighlightCallback(), and populate for example a Label directly in the UI with it instead. The callback also gets called with an empty string when the highlight is removed, which can be used to hide the label again. For example:

Ui::Label details{};



debugLayer.setNodeHighlightCallback([&details](Containers::StringView message) {
    details
        .setText(message)
        .setHidden(!message);
});

Limitations

Currently, it's only possible to visually highlight nodes that are visible and are neither NodeFlag::Disabled nor NodeFlag::NoEvents, To help with their discovery a bit at least, clicking their (event-accepting) parent will list how many children are hidden, disabled or not accepting events. Highlighting such nodes is only possible by passing their handle to highlightNode().

Additionally, if a top-level node covers other nodes but is otherwise invisible and doesn't react to events in any way, with DebugLayerFlag::NodeHighlight it will become highlightable and it won't be possible to highlight any nodes underneath. Presence of such a node in the UI is usually accidental, currently the workaround is to either restrict its size to cover only the necessary area, move it behind other nodes in the top-level node hierarchy or mark it with NodeFlag::NoEvents if it doesn't have children that need to react to events.

Making the DebugLayer opt-in

As mentioned above, having the layer always present may have unintended performance implications, and a node highlight can be confusing if triggered accidentally by an unsuspecting user. One possibility is to create it only with a certain startup option, such as is the case with --debug in magnum-ui-gallery.

Another way is to create it with no DebugLayerFlag present, and enable them temporarily only if some debug mode is activated, similarly to what was shown for touch input above. Such setup will however still make it track all enabled DebugLayerSource bits all the time. To avoid this, it's also possible to defer the layer creation and setup to the point when it's actually needed, and then destroy it again after. Note that, as certain options involve iterating the whole node tree, with very complex UIs such on-the-fly setup might cause stalls.

DebugLayer integration for custom layers

To make a custom layer provide detailed info for DebugLayerSource::NodeDataAttachmentDetails, implement an inner type named DebugIntegration containing at least a print() function. In the following snippet, a layer that exposes per-data color has the color printed in the DebugLayerFlag::NodeHighlight output. To make the output fit with the other text, it's expected to be indented and end with a newline:

class ColorLayer: public Ui::AbstractLayer {
    public:
        struct DebugIntegration;

        Color3 color(Ui::LayerDataHandle handle) const;

        
};

struct ColorLayer::DebugIntegration {
    void print(Debug& debug, const ColorLayer& layer,
               Containers::StringView layerName, Ui::LayerDataHandle data) {
        /* Convert to an 8-bit color for brevity */
        Color3ub color8 = Math::pack<Color3ub>(layer.color(data));
        debug << "  Data" << Debug::packed << data
            << "from layer" << Debug::packed << layer.handle()
            << Debug::color(Debug::Color::Yellow) << layerName << Debug::resetColor
            << "with color" << Debug::color << color8 << color8 << Debug::newline;
    }
};

Assuming the concrete layer type is passed to setLayerName(), nothing else needs to be done and the integration gets used automatically. If there's multiple data attached to the same node, the print() gets called for each of them.

ColorLayer& colorLayer = ui.setLayerInstance();


debugLayer.setLayerName(colorLayer, "Shiny");
Node {0xc, 0x1}
  Nested at level 2 with 0 direct children
  Data {0x4, 0x3} from layer {0x4, 0x1} Shiny with color ▓▓ #2f83cc
  Data {0x7, 0x1} from layer {0x4, 0x1} Shiny with color ██ #3bd267

If the integration needs additional state, a constructor can be implemented, and the instance then passed to DebugLayer::setLayerName(const T&, const Containers::StringView&, const typename T::DebugIntegration&) or setLayerName(..., typename T::DebugIntegration&&):

class ColorLayer::DebugIntegration {
    public:
        /*implicit*/ DebugIntegration(DebugFlags flags = {}): _flags{flags} {}

        void print(Debug& debug, const ColorLayer& layer,
                   Containers::StringView layerName, Ui::LayerDataHandle data);

    private:
        DebugFlags _flags;
};

If additional details can be provided even without supplying additional state, it's recommended to have the class default-constructible as well so at least some functionality can be used even if users aren't aware of the extra options. Additionally, in the above code the constructor isn't made explicit, which allows the debug integration arguments to be passed directly to setLayerName():

/* Default integration setup */
debugLayer.setLayerName(colorLayer, "Shiny");

/* Passing extra arguments */
debugLayer.setLayerName(colorLayer, "Shiny",
    ColorLayer::DebugFlag::PrintColor|ColorLayer::DebugFlag::ColorSwatch);

Overriding the integration in subclasses

Any layer derived from layers that already have an integration, such as from AbstractVisualLayer or EventLayer, will implicitly inherit the base implementation. If the subclass wants to provide its own output, it can either provide a custom type and hide the original, or derive from it by calling the base print() in its own implementation. Here the ColorLayer from above is made to derive from AbstractVisualLayer now instead, and its DebugIntegration derives from AbstractVisualLayer::DebugIntegration as well and inherits its output:

class ColorLayer: public Ui::AbstractVisualLayer {
    public:
        struct DebugIntegration;

        
};

struct ColorLayer::DebugIntegration: Ui::AbstractVisualLayer::DebugIntegration {
    void print(Debug& debug, const ColorLayer& layer,
               Containers::StringView layerName, Ui::LayerDataHandle data) {
        Ui::AbstractVisualLayer::DebugIntegration::print(debug, layer, layerName, data);

        
    }
};

Base classes

class AbstractLayer new in Git master
Base for data layers.

Derived classes

class DebugLayerGL new in Git master
OpenGL implementation of the debug layer.

Public types

class DebugIntegration
Debug layer integration.

Constructors, destructors, conversion operators

DebugLayer(LayerHandle handle, DebugLayerSources sources, DebugLayerFlags flags) explicit
Constructor.
DebugLayer(const DebugLayer&) deleted
Copying is not allowed.
DebugLayer(DebugLayer&&) noexcept
Move constructor.

Public functions

auto operator=(const DebugLayer&) -> DebugLayer& deleted
Copying is not allowed.
auto operator=(DebugLayer&&) -> DebugLayer& noexcept
Move assignment.
auto sources() const -> DebugLayerSources
Tracked data sources.
auto flags() const -> DebugLayerFlags
Behavior flags.
auto setFlags(DebugLayerFlags flags) -> DebugLayer&
Set behavior flags.
auto addFlags(DebugLayerFlags flags) -> DebugLayer&
Add behavior flags.
auto clearFlags(DebugLayerFlags flags) -> DebugLayer&
Clear flags.
auto nodeName(NodeHandle handle) const -> Containers::StringView
Node name.
auto setNodeName(NodeHandle handle, Containers::StringView name) -> DebugLayer&
Set node name.
auto layerName(LayerHandle handle) const -> Containers::StringView
Layer name.
auto setLayerName(const AbstractLayer& layer, Containers::StringView name) -> DebugLayer&
Set layer name.
template<class T>
auto setLayerName(const T& layer, const Containers::StringView& name) -> DebugLayer&
Set name for a layer with DebugIntegration implemented.
template<class T>
auto setLayerName(const T& layer, const Containers::StringView& name, const typename T::DebugIntegration& integration) -> DebugLayer&
Set name for a layer with DebugIntegration implemented.
template<class T>
auto setLayerName(const T& layer, const Containers::StringView& name, typename T::DebugIntegration&& integration) -> DebugLayer&
auto nodeHighlightColor() const -> Color4
Node highlight color.
auto setNodeHighlightColor(const Color4& color) -> DebugLayer&
Set node highlight color.
auto nodeHighlightGesture() const -> Containers::Pair<Pointers, Modifiers>
Node highlight gesture.
auto setNodeHighlightGesture(Pointers pointers, Modifiers modifiers) -> DebugLayer&
Set node highlight gesture.
auto hasNodeHighlightCallback() const -> bool
Whether a node highlight callback is set.
auto setNodeHighlightCallback(Containers::Function<void(Containers::StringView message)>&& callback) -> DebugLayer&
Set node highlight callback.
auto currentHighlightedNode() const -> NodeHandle
Node highlighted by last pointer press.
auto highlightNode(NodeHandle node) -> bool
Highlight a node.

Function documentation

Magnum::Ui::DebugLayer::DebugLayer(LayerHandle handle, DebugLayerSources sources, DebugLayerFlags flags) explicit

Constructor.

Parameters
handle Layer handle returned from AbstractUserInterface::createLayer()
sources Data sources to track
flags Behavior flags

See particular DebugLayerFlag values for information about which DebugLayerSource is expected to be enabled for a particular feature. While sources have to specified upfront, the flags can be subsequently modified using setFlags(), addFlags() and clearFlags().

Note that you can also construct the DebugLayerGL subclass instead to have the layer with visual feedback.

Magnum::Ui::DebugLayer::DebugLayer(DebugLayer&&) noexcept

Move constructor.

Performs a destructive move, i.e. the original object isn't usable afterwards anymore.

DebugLayer& Magnum::Ui::DebugLayer::setFlags(DebugLayerFlags flags)

Set behavior flags.

Returns Reference to self (for method chaining)

See particular DebugLayerFlag values for information about which DebugLayerSource is expected to be enabled for a particular feature.

If a node was highlighted and DebugLayerFlag::NodeHighlight was cleared by calling this function, the highlight gets removed. The function doesn't print anything, but if a callback is set, it's called with an empty string. Additionally, if the layer is instantiated as DebugLayerGL, it causes LayerState::NeedsDataUpdate to be set.

DebugLayer& Magnum::Ui::DebugLayer::addFlags(DebugLayerFlags flags)

Add behavior flags.

Returns Reference to self (for method chaining)

Calls setFlags() with the existing flags ORed with flags. Useful for preserving previously set flags.

DebugLayer& Magnum::Ui::DebugLayer::clearFlags(DebugLayerFlags flags)

Clear flags.

Returns Reference to self (for method chaining)

Calls setFlags() with the existing flags ANDed with the inverse of flags. Useful for removing a subset of previously set flags.

Containers::StringView Magnum::Ui::DebugLayer::nodeName(NodeHandle handle) const

Node name.

Expects that the debug layer has been already passed to AbstractUserInterface::setLayerInstance() and that handle isn't NodeHandle::Null, handle doesn't have to be valid however. If DebugLayerSource::Nodes isn't enabled or handle isn't known, returns an empty string.

If not empty, the returned string is always Containers::StringViewFlag::NullTerminated. Unless setLayerName() was called with a Containers::StringViewFlag::Global string, the returned view is only guaranteed to be valid until the next call to setLayerName() or until the set of layers in the user interface changes.

DebugLayer& Magnum::Ui::DebugLayer::setNodeName(NodeHandle handle, Containers::StringView name)

Set node name.

Returns Reference to self (for method chaining)

Expects that the debug layer has been already passed to AbstractUserInterface::setLayerInstance() and that handle isn't NodeHandle::Null, handle doesn't have to be valid however. If DebugLayerSource::Nodes is enabled, the name will be used to annotate given handle, otherwise the function does nothing.

If name is Containers::StringViewFlag::Global and NullTerminated, no internal copy of the string is made. If DebugLayerSource::Nodes isn't enabled, the function does nothing.

Containers::StringView Magnum::Ui::DebugLayer::layerName(LayerHandle handle) const

Layer name.

Expects that the debug layer has been already passed to AbstractUserInterface::setLayerInstance() and that handle isn't LayerHandle::Null, handle doesn't have to be valid however. If DebugLayerSource::Layers isn't enabled or handle isn't known, returns an empty string. For handle() returns "DebugLayer" if a different name wasn't set.

If not empty, the returned string is always Containers::StringViewFlag::NullTerminated. Unless setLayerName() was called with a Containers::StringViewFlag::Global string, the returned view is only guaranteed to be valid until the next call to setLayerName() or until the set of layers in the user interface changes.

DebugLayer& Magnum::Ui::DebugLayer::setLayerName(const AbstractLayer& layer, Containers::StringView name)

Set layer name.

Returns Reference to self (for method chaining)

Expects that the debug layer has been already passed to AbstractUserInterface::setLayerInstance() and that layer is part of the same user interface. If DebugLayerSource::Layers is enabled, the name will be used to annotate attachments from given layer, otherwise the function does nothing.

If name is Containers::StringViewFlag::Global and NullTerminated, no internal copy of the string is made. If DebugLayerSource::NodeDataAttachments isn't enabled, the function does nothing.

If a concrete layer type gets passed instead of just AbstractLayer, the setLayerName(const T&, const Containers::StringView&) overload may get picked if given layer implements debug integration, allowing it to provide further details.

template<class T>
DebugLayer& Magnum::Ui::DebugLayer::setLayerName(const T& layer, const Containers::StringView& name)

Set name for a layer with DebugIntegration implemented.

Returns Reference to self (for method chaining)

In addition to setLayerName(const AbstractLayer&, Containers::StringView) the layer's DebugIntegration implementation gets called for each attachment, allowing it to provide further details. See documentation of a particular layer for more information. Use the setLayerName(const T&, const Containers::StringView&, const typename T::DebugIntegration&) overload to pass additional arguments to the debug integration.

template<class T>
DebugLayer& Magnum::Ui::DebugLayer::setLayerName(const T& layer, const Containers::StringView& name, const typename T::DebugIntegration& integration)

Set name for a layer with DebugIntegration implemented.

Returns Reference to self (for method chaining)

In addition to setLayerName(const AbstractLayer&, Containers::StringView) the passed layer DebugIntegration instance gets called for each attachment, allowing it to provide further details. See documentation of a particular layer for more information.

template<class T>
DebugLayer& Magnum::Ui::DebugLayer::setLayerName(const T& layer, const Containers::StringView& name, typename T::DebugIntegration&& integration)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

DebugLayer& Magnum::Ui::DebugLayer::setNodeHighlightColor(const Color4& color)

Set node highlight color.

Returns Reference to self (for method chaining)

Used only if DebugLayerFlag::NodeHighlight is enabled and if the layer is instantiated as DebugLayerGL to be able to draw the highlight rectangle, ignored otherwise. Default is 0xff00ffff_rgbaf*0.5f.

If the layer is instantiated as DebugLayerGL, calling this function causes LayerState::NeedsDataUpdate to be set.

DebugLayer& Magnum::Ui::DebugLayer::setNodeHighlightGesture(Pointers pointers, Modifiers modifiers)

Set node highlight gesture.

Returns Reference to self (for method chaining)

Used only if DebugLayerFlag::NodeHighlight is enabled, ignored otherwise. A highlight happens on a press of a pointer that's among pointers with modifiers being exactly modifiers. Pressing on a different node moves the highlight to the other node, pressing on a node that's currently highlighted removes the highlight. Expects that pointers are non-empty. Default is a combination of Pointer::MouseRight and Pointer::Eraser, and Modifier::Ctrl, i.e. pressing either Ctrl right mouse button or Ctrl pen eraser will highlight a node under the pointer. The currently highlighted node is available in currentHighlightedNode(), you can also use highlightNode() to perform a node highlight programmatically.

DebugLayer& Magnum::Ui::DebugLayer::setNodeHighlightCallback(Containers::Function<void(Containers::StringView message)>&& callback)

Set node highlight callback.

Returns Reference to self (for method chaining)

Used only if DebugLayerFlag::NodeHighlight is enabled, ignored otherwise. The callback receives a UTF-8 message with details when a highlight happens on a pointer press, and an empty string if a highlight is removed again. If not empty, the message is guaranteed to be Containers::StringViewFlag::NullTerminated.

If the callback is not set or if set to nullptr, details about the highlighted node are printed to Debug instead.

NodeHandle Magnum::Ui::DebugLayer::currentHighlightedNode() const

Node highlighted by last pointer press.

Expects that DebugLayerFlag::NodeHighlight is enabled. If no node is currently highlighted, returns NodeHandle::Null.

The returned handle may be invalid if the node or any of its parents were removed and AbstractUserInterface::clean() wasn't called since.

bool Magnum::Ui::DebugLayer::highlightNode(NodeHandle node)

Highlight a node.

Expects that DebugLayerFlag::NodeHighlight is enabled and the layer has been already passed to AbstractUserInterface::setLayerInstance().

If node is a known handle, the function performs similarly to the node highlight gesture using a pointer press — currentHighlightedNode() is set to node, details about the node are printed to Debug or passed to a callback if set, the node is visually higlighted if this is a DebugLayerGL instance, and the function returns true.

If node is NodeHandle::Null or it's not a known handle (for example an invalid handle of a now-removed node, or a handle of a newly created node but AbstractUserInterface::update() wasn't called since) and there's a current highlight, it's removed. The function doesn't print anything, but if a callback is set, it's called with an empty string. If there's no current highlight, the callback isn't called. The functions returns true if node is NodeHandle::Null and false if the handle is unknown.

Note that, compared to the node highlight gesture, where the node details are always extracted from an up-to-date UI state, this function only operates with the state known at the last call to AbstractUserInterface::update(). As such, for example nodes or layers added since the last update won't be included in the output.

If the layer is instantiated as DebugLayerGL, calling this function causes LayerState::NeedsDataUpdate to be set.