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.
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) { ... } }
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
- Use
OnEventwhen the handler is a lambda - its parameter names the event. - Use the named alias when the handler is a bare function pointer, where nothing at the call site would otherwise say which event it is.
- Use
Events{ ... }when a control takes more than one handler.
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
ScopedEventConnectionwhen 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
thisonly from an object that outlives the emitter.
Don't:
- Don't let a
ScopedEventConnectionoutlive the emitter it points at. Its destructor callsdisconnect()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) { ... });
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.
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; };
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 }
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.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(); } } };
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:
| Callback | Why it is not an event |
|---|---|
TraversalContext::OnVisitControl | A visitor passed into one tree walk. It is an argument, not a subscription. |
DirWatchManager::OnWatchEvent | Registered 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::OnAnimate | Belongs 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:
| Name | What it is |
|---|---|
Event | Base for every event. Carries stopPropagation() and propagationStopped(). |
EventOf<Sender> | Event base that also carries sender(). |
EventComponent | Base that makes a type an emitter. Protected constructors; the variadic one connects handlers from a pack. |
EventDispatcher | The listener map. Reached through events(); rarely used directly. |
EventConnection | Move-only handle returned by every connect call. disconnect(). |
ScopedEventConnection | RAII 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:
| Macro | What 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. |
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.