Creating a Trait
Traits are reusable behavior modules that can be mixed into any component. This tutorial walks through three levels: a simple behavior trait, a trait that declares its own manifest, and an event-receiving trait that hooks into the Malena event dispatcher system.
What is a Trait?
In Malena, a trait is a C++ class that provides a specific reusable behavior and is designed to be mixed into a component via the Component<Trait1, Trait2, ...> template syntax. Built-in traits include Draggable, Fadeable, Messenger, and Themeable.
Traits follow the composition-over-inheritance principle. Instead of a deep inheritance tree, components are assembled from focused, independently testable behaviors.
Three kinds of traits
| Kind | Base class | When to use |
|---|---|---|
| Simple | ml::Trait | Behavior with no manifest and no event system integration |
| Manifest-aware | ml::TraitWith<Manifest> | Trait that declares its own flags, states, or resource keys |
| Event-receiving | ml::EventReceiver + ml::EventDispatcher | Trait that reacts to SFML input events or frame ticks |
Every trait must ultimately inherit ml::Trait (directly or through TraitWith). This marker base is how the framework detects traits at compile time when assembling the component inheritance chain.
A Simple Trait
A simple trait adds reusable behavior to a component with no event system involvement and no manifest. It inherits ml::Trait directly and exposes its API as ordinary methods.
Example: a pulsing animation helper that any component can mix in to manage a sine-wave scale pulsation stored as state.
#pragma once
#include <Malena/Malena.h>
#include <cmath>
/**
* Trait that makes a component animate a smooth pulse scale value over time.
* The component is responsible for applying getScale() in its draw() override.
*/
class Pulsable : public ml::Trait
{
public:
/** Begin pulsing between minScale and maxScale at the given speed. */
void startPulse(float minScale = 0.95f,
float maxScale = 1.05f,
float speed = 2.0f)
{
_min = minScale;
_max = maxScale;
_speed = speed;
_active = true;
}
/** Stop pulsing — scale returns to 1.0. */
void stopPulse()
{
_active = false;
_t = 0.f;
}
/**
* Advance the animation by dt seconds.
* Call this from your component's update() override.
*/
void tickPulse(float dt)
{
if (!_active) return;
_t += dt * _speed;
}
/** Return the current scale factor. Apply this in draw(). */
[[nodiscard]] float getPulseScale() const
{
if (!_active) return 1.f;
float t = (std::sin(_t) + 1.f) * 0.5f; // 0..1
return _min + t * (_max - _min);
}
[[nodiscard]] bool isPulsing() const { return _active; }
private:
float _min = 0.95f;
float _max = 1.05f;
float _speed = 2.f;
float _t = 0.f;
bool _active = false;
};
This is the simplest form of a trait — a regular C++ class that inherits ml::Trait. No registration, no macros, no manifests needed.
TraitWith — Traits That Declare Flags or States
When a trait needs to track its own flags or states, use ml::TraitWith<Manifest> as the base. This wires the trait's flag and state enums into the GatherFlags / GatherStates machinery of any component that mixes in the trait. The component then exposes a single unified set of setFlag(), checkFlag(), setState(), and getState() calls that cover both its own manifest enums and every trait's enums.
#pragma once
#include <Malena/Malena.h>
#include <functional>
// 1. Declare the trait's manifest — flags and states only
class SelectableManifest : public ml::Manifest
{
public:
enum class Flags { Selected, MultiSelect };
enum class States { Idle, Highlighted, Selected };
};
// 2. Define the trait, inheriting TraitWith<Manifest>
class Selectable : public ml::TraitWith<SelectableManifest>
{
public:
using Flags = SelectableManifest::Flags;
using States = SelectableManifest::States;
// Callback fired when the selection state changes
void onSelectionChanged(std::function<void(bool)> cb)
{
_onChange = std::move(cb);
}
/** Select this component programmatically. */
void select()
{
// Traits cannot call setFlag() directly — they must cast
// to the appropriate SingleFlaggable base, or expose the
// API through the owning component.
_selected = true;
if (_onChange) _onChange(true);
}
void deselect()
{
_selected = false;
if (_onChange) _onChange(false);
}
[[nodiscard]] bool isSelected() const { return _selected; }
private:
bool _selected = false;
std::function<void(bool)> _onChange;
};
MultiCustomFlaggable on the component (not the trait itself), traits that need to read or write their own flags must cast to the appropriate SingleFlaggable<Flags> base, or expose the flag management methods to the component that mixes them in. The simpler pattern shown above keeps the state in a plain bool member, which works well for most traits.
What the manifest enum can contain
| Manifest field | Purpose |
|---|---|
enum class Flags { ... } | Boolean flags — gathered by GatherFlags and included in the component's flag store |
enum class States { ... } | Mutually exclusive states — gathered by GatherStates and included in the component's state store |
Event-Receiving Traits
Event traits plug into the Malena event dispatcher system so that components can subscribe to SFML input events (clicks, hovers, key presses, etc.) via a friendly callback API. They require three pieces working together:
- A receiver class that inherits
ml::EventReceiverand stores callbacks - A dispatcher class that inherits
ml::EventDispatcherand decides when to fire - The
ML_EXPORTmacro to register the dispatcher as a singleton at startup
Step 1 — The Receiver
The receiver stores the user's callback and exposes the friendly subscription method. It inherits ml::EventReceiver, which manages the internal callback storage.
#pragma once
#include <Malena/Malena.h>
#include <functional>
namespace ml { class RightClickableDispatcher; }
class RightClickable : public ml::EventReceiver
{
public:
/**
* Register a callback fired when the right mouse button is released
* over this component.
*/
void onRightClick(std::function<void()> callback)
{
// Store via EventReceiver's internal map.
// The key string must match what the dispatcher uses in fire().
getCallbacks("right_click").clear();
getCallbacks("right_click").push_back(
[cb = std::move(callback)](const std::optional<sf::Event>&) { cb(); }
);
}
};
Step 2 — The Dispatcher
The dispatcher decides when an SFML event qualifies as "our" event and which components should receive it. It inherits ml::EventDispatcher and must implement three methods.
class RightClickableDispatcher : public ml::EventDispatcher
{
public:
/**
* occurred() — called once per SFML event.
* Return true only when the right mouse button was released.
*/
bool occurred(const std::optional<sf::Event>& event) override
{
if (!event) return false;
const auto* btn = event->getIf<sf::Event::MouseButtonReleased>();
return btn && btn->button == sf::Mouse::Button::Right;
}
/**
* filter() — called for each registered component after occurred().
* Return true if this component should receive the event.
* Here we skip hidden components.
*/
bool filter(const std::optional<sf::Event>&, ml::Core* component) override
{
return !component->checkFlag(ml::Flag::HIDDEN);
}
/**
* fire() — deliver the event to all components that pass filter().
* Use EventManager::fire() with your string key.
*/
void fire(const std::optional<sf::Event>& event) override
{
ml::EventManager::fire("right_click", this, event);
}
};
// 3. Register the dispatcher — must be OUTSIDE any namespace
ML_EXPORT(RightClickableDispatcher)
getCallbacks() in the receiver must exactly match the string key passed to EventManager::fire() in the dispatcher. A mismatch means callbacks are stored under one key but fired under another, so they never execute.
Full file layout
#pragma once
#include <Malena/Malena.h>
#include <functional>
// ── Receiver ──────────────────────────────────────────────────────────────────
class RightClickable : public ml::EventReceiver
{
public:
void onRightClick(std::function<void()> callback)
{
getCallbacks("right_click").clear();
getCallbacks("right_click").push_back(
[cb = std::move(callback)](const std::optional<sf::Event>&) { cb(); }
);
}
};
// ── Dispatcher ────────────────────────────────────────────────────────────────
class RightClickableDispatcher : public ml::EventDispatcher
{
public:
bool occurred(const std::optional<sf::Event>& event) override
{
if (!event) return false;
const auto* btn = event->getIf<sf::Event::MouseButtonReleased>();
return btn && btn->button == sf::Mouse::Button::Right;
}
bool filter(const std::optional<sf::Event>&, ml::Core* component) override
{
return !component->checkFlag(ml::Flag::HIDDEN);
}
void fire(const std::optional<sf::Event>& event) override
{
ml::EventManager::fire("right_click", this, event);
}
};
// ── Registration — outside namespace ─────────────────────────────────────────
ML_EXPORT(RightClickableDispatcher)
ML_EXPORT(ClassName) macro creates a static registration struct. It must be placed at file scope — not inside a namespace block. The dispatcher class itself can be in any namespace, but the macro call must be at the top level.
Frame-driven traits
If your trait should fire every frame rather than on a specific SFML event, inherit ml::FrameDispatcher instead of ml::EventDispatcher. The occurred() and no-argument fire() signatures differ slightly — see the Updatable trait source for a working example.
Using Your Trait in a Component
Once you have defined a trait, mix it into any component by passing it as a template argument to Component or ComponentWith.
Simple and manifest-aware traits
#include "Pulsable.h"
#include "Selectable.h"
#include <Malena/Malena.h>
class GameTile : public ml::Component<Pulsable, Selectable>
{
public:
GameTile()
{
_shape.setSize({64.f, 64.f});
// Pulsable API is available directly
startPulse(0.9f, 1.1f, 1.5f);
// Selectable API is available directly
onSelectionChanged([this](bool selected) {
_shape.setOutlineColor(selected
? sf::Color(100, 200, 255)
: sf::Color::Transparent);
});
onClick([this]() { select(); });
}
void draw(sf::RenderTarget& t, sf::RenderStates s) const override
{
sf::RenderStates scaled = s;
float sc = getPulseScale();
// Apply scale transform around the center of the shape
sf::Transform tf;
auto bounds = _shape.getGlobalBounds();
tf.scale({sc, sc}, bounds.position + bounds.size / 2.f);
scaled.transform *= tf;
t.draw(_shape, scaled);
}
void update() override
{
tickPulse(1.f / 60.f); // assumes 60 fps; use a real dt in production
}
private:
sf::RectangleShape _shape;
};
Event-receiving traits
#include "RightClickable.h"
#include <Malena/Malena.h>
class ContextMenuTarget : public ml::Component<RightClickable>
{
public:
ContextMenuTarget()
{
_bg.setSize({200.f, 100.f});
// The trait's callback API is available on this
onRightClick([this]() {
showContextMenu();
});
}
void draw(sf::RenderTarget& t, sf::RenderStates s) const override
{
t.draw(_bg, s);
}
void update() override {}
private:
sf::RectangleShape _bg;
void showContextMenu() { /* open a popup */ }
};
Combining multiple custom traits
class AdvancedCard : public ml::ComponentWith<CardManifest,
Pulsable,
Selectable,
RightClickable,
ml::Messenger>
{
...
};
Reference
Trait base classes
| Base class | Use when |
|---|---|
ml::Trait | Behavior-only trait. No manifest, no events. |
ml::TraitWith<Manifest> | Trait that declares its own Flags or States enums. |
ml::EventReceiver | Trait that stores callbacks and participates in the event system. |
ml::EventDispatcher | Singleton that decides when an SFML event fires and to whom. |
EventDispatcher methods
| Method | When called | Purpose |
|---|---|---|
occurred(event) | Once per incoming SFML event | Return true when this dispatcher should process the event |
filter(event, component) | For each registered component after occurred() | Return true if this component should receive the event |
fire(event) | After filtering | Iterate components and call process() on matching ones |
Checklist for a new event trait
| # | Step |
|---|---|
| 1 | Define a receiver class inheriting ml::EventReceiver. Expose a friendly onXxx(callback) method that stores callbacks via getCallbacks("your_key"). |
| 2 | Define a dispatcher class inheriting ml::EventDispatcher. Implement occurred(), filter(), and fire(). Use ml::EventManager::fire("your_key", this, event) in fire(). |
| 3 | Place ML_EXPORT(MyDispatcher) at file scope, outside any namespace. |
| 4 | Ensure the string key in getCallbacks() and EventManager::fire() are identical. |
| 5 | Mix the receiver into a component: class MyComp : public ml::Component<MyTrait>. |