Events Subsystem

ClaFi Framework Developer Manual

Introduction

The Events subsystem is how one object tells others that something happened. A type emits a strongly typed event; any number of listeners connect to it, each can disconnect, and any listener can stop the event travelling further.

Module Import:
import ClaFi.Core.System.Events;

Every control in the framework already emits its events - there is nothing to switch on and nothing to register. Most of the time you will only connect handlers, which is what the first half of this manual covers. Declaring events of your own comes later, and is only needed when you write a control or another emitter.

One rule is worth carrying from the start: an event is identified by its type, not by a name or a channel. The event type written in your handler's parameter is what decides which event you are connecting to.

Basic Usage Example

Handlers can be given to a control when it is built, or attached at any time afterwards:

// at construction - the handler's parameter says which event it is
Button& save = toolbar.add<Button>(
    L"Save",
    Padding{ 8.0f },
    OnEvent{ [this](ClickEvent&) { saveDocument(); } }
);

// afterwards - same event, same control
save.onGetState([this](GetStateEvent& event) {
    event.state.enabled = m_document.isModified();
});

Handling an Event

The Three Spellings

All three do exactly the same thing - they are one mechanism with three ways to write it.

// 1. named alias - says which event at the call site
Button{ params, Button::OnClick{ handler } }

// 2. unnamed - the handler's parameter already says it
Button{ params, OnEvent{ [](ClickEvent& event) { ... } } }

// 3. a list - several handlers without repeating OnEvent
Button{ params, Events{
    [](ClickEvent& event) { ... },
    [](PaintIconEvent& event) { ... },
} }

An Events list is heterogeneous and its order means nothing. It accepts already-wrapped handlers next to bare ones, so you can name the one that needs naming:

Events{ Button::OnPaintIcon{ Icons::SaveIcon::paint }, [](ClickEvent& event) { ... } }
A named alias such as OnClick is a member of the control that declares it. Button::OnClick resolves anywhere; a bare OnClick resolves only inside a class that inherits it. At namespace scope, qualify it or use OnEvent.

Which One to Use

The difference is only readability. This is the case the second rule is about:

OnEvent{ Icons::SaveIcon::paintSaveIcon }              // which event? unclear
FlatButton::OnPaintIcon{ Icons::SaveIcon::paintSaveIcon }  // clear

Connecting Later

Every event has a connect method named after it, and connectEvent works for any event the emitter can raise:

slider.onChange([](SliderChangeEvent& event) { ... });         // the named connector
panel.connectEvent([](PaintEvent& event) { ... });            // deduced from the handler
panel.connectEvent<PaintEvent>(handler);                     // event named explicitly

The explicit form is still the answer when a handler cannot name its own event - a generic lambda, or one taking a base of the event type.

Member Functions as Handlers

A pointer-to-member pair works in both places. The event comes off the method's own signature:

m_darkButton.connectEvent(this, &ThemePage::onColorModeClick);      // connect later
Button{ params, makeOnEvent(this, &ThemePage::onColorModeClick) }  // at construction
makeOnEvent is a function rather than a constructor because a deduction guide cannot synthesise the std::function wrapper the object/method pair needs.

Disconnecting

Every connect call returns an EventConnection. Ignore it and the handler lives as long as the emitter; keep it and you can disconnect:

EventConnection conn = form.onClose([](FormCloseEvent& event) { ... });
conn.disconnect();

For a handler that must not outlive the listener, store a ScopedEventConnection, which disconnects in its destructor. It takes an r-value only, so a connection cannot be scoped twice:

class FormBase
{
private:
    ScopedEventConnection m_colorModeConnection;
};

// built in the constructor's initializer list, so it dies with the listener
FormBase::FormBase(AppContext& appContext, ...)
    :
    m_colorModeConnection{
        appContext.colorMode().connectEvent(this, &FormBase::colorModeChanged)
    }
{
}

Do:

  • Hold a ScopedEventConnection when the listener dies first, and store it as a member of that listener.
  • Let the connection go when the emitter owns the handler outright - a control's own handler lives and dies with the control.
  • Capture by value, or capture this only from an object that outlives the emitter.

Don't:

  • Don't let a ScopedEventConnection outlive the emitter it points at. Its destructor calls disconnect() through a raw pointer to the dispatcher, and nothing clears that pointer when the emitter is destroyed. It compiles, it usually appears to work, and it is undefined behaviour.
  • Don't connect a lambda capturing something shorter-lived than the emitter and then drop the connection. The handler stays live and the capture does not.

Form Events

A form raises FormCloseEvent, FormFocusChangeEvent and FormPositionChangeEvent, each carrying the form as sender(). They are connected exactly like a control's events, either way round:

// as a construction property
auto form = application.createDialog<Panel>(
    MinSize{ 640.0f, 480.0f },
    OnEvent{ [](FormCloseEvent& event) { ... } }
);

// or afterwards
form->onClose([](FormCloseEvent& event) { ... });
A form is a window and its root control in one object, and they share a single dispatcher - the control's. The form raises its own events on it, so a form event is connected exactly like a control event, from either side and at either time.

Declaring Your Own Event

Everything above is about using events that already exist. This part is for writing a control, or another type, that raises events of its own.

Extra include: declaring an event uses the DECLARE_EVENT macro, which lives in a plain header because macros are not exported by modules. Include it in the global module fragment, above the module declaration:
module;
#include "../Y-Core/System/EventBindings.h"
export module ClaFi.Controls.MyControl;

Writing the Event Type

An event is a class derived from Event. Give it whatever the handler needs to read or write. If the handler should be able to reach the object that raised it, derive from EventOf<Sender> instead, which adds sender().

// carries data of its own
export class SliderChangeEvent : public Event
{
public:
    SliderChangeEvent(const SliderBase& slider, float newPosition, float previousPosition);
    const SliderBase& slider;
    const float newPosition;
    const float previousPosition;
};

// carries only its sender
export class TimerEvent : public EventOf<UiTimer>
{
    using EventOf<UiTimer>::EventOf;
};
Note: the inherited-constructor using is written in the private section by convention. That is deliberate and correct - the accessibility of an inherited constructor comes from the base's constructor, not from the using declaration.

DECLARE_EVENT

One line in the class body declares an event completely:

//            event type,   handler alias,  connect method
DECLARE_EVENT(ClickEvent, OnClick, onClick)

The arguments run widest to narrowest, so a column of declarations lines up by event type. The macro generates the handler alias and the connect helper, which is what makes both call-site spellings work.

There is nothing else to write. No per-event property type, no binding line in the constructor. The alias is a spelling of OnEvent, and every emitter's constructor connects every OnEvent it is handed.

Emitting

emitEvent deduces the event type from the object you hand it:

void ComboBox::itemIndexChanged()
{
    ComboBoxChangeEvent event{ *this };
    emitEvent(event);
}

There is also a construct-in-place form, emitEvent<TEvent>(args...), for an event you do not need to inspect afterwards. Emission is cheap when nobody is listening: the dispatcher returns immediately if it holds no listeners at all.

A handler can call event.stopPropagation(). The emit loop stops at that handler, and the emitter can test event.propagationStopped() to decide whether to run its own default behaviour - this is how a control lets a caller override it:

void TextBox::nestedContextPopup(ContextPopupEvent& event)
{
    setCaretPosFromMouse();
    Label::nestedContextPopup(event);     // emits; a handler may stop it
    if (event.propagationStopped())
        return;                    // the caller replaced the menu
    // ... otherwise show the built-in one
}
Don't emit from a constructor. Handlers passed as construction properties are connected by EventComponent's constructor, which runs before any derived constructor body. An event raised while the object is still being built reaches handlers that will see a half-initialised object.
A control needs no constructor code to accept handlers for the events it declares - not even for events its base classes know nothing about. Control's constructor connects a ComboBoxChangeEvent handler perfectly well, for the reason given in One Dispatcher per Object.

Making Any Class an Emitter

A type that is not a control becomes an emitter by deriving from EventComponent and forwarding its constructor pack to it:

export class UiTimer : public EventComponent
{
public:
    template <typename... Args>
        requires NotSelfCopy<UiTimer, Args...>
    explicit UiTimer(const Args&... args)
        :
        EventComponent{ args... }
    {
    }
public:
    DECLARE_EVENT(TimerEvent, OnTick, onTick)
};

// and then
UiTimer timer{ OnEvent{ [this](TimerEvent&) { tick(); } } };
The NotSelfCopy guard is not optional. A variadic constructor is greedy and will otherwise swallow the class's own copy constructor. For the same reason, declare MyType() = default; explicitly if the type needs a default constructor - declaring any constructor suppresses the implicit one.

Scaler, DirWatch, UiTimer, FormBase and the Dom nodes are all emitters this way.

A class of statics hands out a dispatcher in place of being one: a static events() answers a dispatcher that stands for the whole process, and a listener connects to it by naming the event:

EventConnection connection = Platform::events().connect<SystemColorModeEvent>(
    [](SystemColorModeEvent&) {
        ...
    });

An object that serves the whole process hands out its dispatcher the same way. The clipboard is one: the platform owns it, a form's context hands it out as formContext().clipboard(), and its events() is where ClipboardChangeEvent is raised:

EventConnection connection = formContext().clipboard().events().connect<ClipboardChangeEvent>(
    [](ClipboardChangeEvent&) {
        ...
    });

Both dispatchers outlive every window - the platform stands before the first window and after the last - so a listener that goes first holds its connection in a ScopedEventConnection - see Disconnecting.

Callbacks That Are Not Events

A few things in the framework take a std::function and are deliberately not events. You cannot connect a second one, and there is nothing to disconnect:

CallbackWhy it is not an event
TraversalContext::OnVisitControlA visitor passed into one tree walk. It is an argument, not a subscription.
DirWatchManager::OnWatchEventRegistered per watched path and removed by descriptor. A dispatcher keyed by event type cannot tell paths apart. The DirWatch above it is an emitter, with onChange.
Animation::OnAnimateBelongs to one animation, keyed by control and slot, and runs in the per-frame tick loop.

The distinction to carry: a subscription point is an event; a value one object owns stays a std::function. If you find a std::vector<std::function<...>> being appended to and looped over, that is a dispatcher written by hand and should become an event.

How It Works

One Dispatcher per Object

EventComponent contributes a single EventDispatcher - a map from an event type's identity to a list of listeners. The identity is the address of a static byte unique to each instantiation of an internal template, so it needs no registry and no manual registration.

Because the key is the type, it does not matter which class in a hierarchy declares an event, connects a handler, or emits it. They all meet in the one dispatcher the object owns. That is why a base class constructor can connect a handler for an event only a derived class will ever raise.

A Form is the case worth knowing: it derives from both FormBase and its root control, but only the control is an EventComponent. FormBase holds a pointer to that control - it is the same object's other half - and forwards its own connects and emits there. One object, one dispatcher, whichever half you reach it through.

Reading the Event off the Handler

OnEvent and the deducing connectEvent inspect the handler's own parameter list to find the event type. EventParamOf matches a lambda, a functor, a function pointer or a pointer-to-member; event_param_t extracts the type; NamesAnEvent is satisfied when the handler names exactly one parameter and that parameter is an Event.

Anything else - a generic [](auto&), wrong arity, an overloaded operator() - fails there rather than deep inside the dispatcher, with a static_assert that says what to do about it.

The connectors that name their event explicitly are kept alongside, and are selected by a guard on the first template parameter, so both spellings coexist without ambiguity.

One Event Type, One Channel

An event type is a channel. There is no way to open a second one on a type already in use, and that is deliberate: a channel with no name of its own tells a reader nothing at the connect site, and a handler attached to one nobody raises is silently never called.

So two handlers that mean different things get two event types. The type is what a reader sees where the handler is written, and the only thing that says which of the two they are on.

ContextPopupEvent and EditContextPopupEvent are the worked example. One gesture - a right-click on a text box - but two audiences: a caller replacing the control's menu outright, and a caller adjusting the standard edit menu the control would otherwise show. Two intentions, two types, and each connect site says which one it means.

Reference

Exported from ClaFi.Core.System.Events:

NameWhat it is
EventBase for every event. Carries stopPropagation() and propagationStopped().
EventOf<Sender>Event base that also carries sender().
EventComponentBase that makes a type an emitter. Protected constructors; the variadic one connects handlers from a pack.
EventDispatcherThe listener map. Reached through events(); rarely used directly.
EventConnectionMove-only handle returned by every connect call. disconnect().
ScopedEventConnectionRAII wrapper that disconnects on destruction. R-value construction only.
OnEvent{ handler }One handler as a construction property. Deduces its event.
Events{ h1, h2, ... }Several handlers as one construction property.
makeOnEvent(obj, method)An OnEvent from an object and a member function.
IsEvent<T>T derives from Event.
NamesAnEvent<F>F takes exactly one parameter and it is an event.
IsOnEvent<T>T is an OnEvent, an Events list, or a named alias of one.
IsHandler<T>Either of the two above - what a handler position accepts.
NotSelfCopy<Self, Args...>Guard for a greedy variadic constructor.
EventParamOf<F>, event_param_t<F>The event type a handler names.

Macros from EventBindings.h:

MacroWhat it does
DECLARE_EVENT(Event, OnAlias, onMethod)Declares an event on a class: the handler alias and the connect helper.
BIND_MEMBER(Type, member)Assigns a construction property to a member.
BIND_CALL(Type, func)Passes a construction property to a setter.
BIND_CALL_VALUE(Type, func)Same, for property wrappers carrying their payload in value.
BIND_ACTION(Type, action)Runs arbitrary code with the property named p.
The BIND_ macros are for plain construction properties - padding, colours, text. Handlers never go through them: they arrive as OnEvent and EventComponent's constructor connects them.
All manuals Top