ml::docs | Your First App Manifests Events Messaging Plugins Scenes Resources
See all tutorials →
Advanced 5 sections ~30 min C++17/20

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.

Introduction

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

KindBase classWhen to use
Simpleml::TraitBehavior with no manifest and no event system integration
Manifest-awareml::TraitWith<Manifest>Trait that declares its own flags, states, or resource keys
Event-receivingml::EventReceiver + ml::EventDispatcherTrait 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.


1Core Concepts

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.

Pulsable.h — a simple behavior trait cpp
#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.

💡
Keep traits focused: A trait should do one thing. If you find yourself adding unrelated responsibilities to a trait, split it into two traits and mix them both in.

2Core Concepts

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.

Selectable.h — trait with flags and states cpp
#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;
};
ℹ️
Flag access from inside a trait: Because flags are stored in 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 fieldPurpose
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
⚠️
Do not declare resource enums in a trait manifest: Images, Fonts, and Sounds should only appear in a component's manifest. A trait manifest should only declare Flags and States.

3Advanced

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:

  1. A receiver class that inherits ml::EventReceiver and stores callbacks
  2. A dispatcher class that inherits ml::EventDispatcher and decides when to fire
  3. The ML_EXPORT macro 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.

RightClickable.h — receiver half cpp
#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.

RightClickable.h — dispatcher half (same file) cpp
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)
ℹ️
String key consistency: The string key passed to 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

RightClickable.h — complete file cpp
#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 must be outside any namespace: The 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.


4Putting it Together

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

Using simple and TraitWith traits cpp
#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

Using an event-receiving trait cpp
#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

All three traits on one component cpp
class AdvancedCard : public ml::ComponentWith<CardManifest,
                                              Pulsable,
                                              Selectable,
                                              RightClickable,
                                              ml::Messenger>
{
    ...
};
ℹ️
Order does not matter for simple traits: The order of traits in the template parameter list affects C++ method resolution order (MRO) only if two traits define a method with the same name. In practice, keep methods namespaced by behavior and you won't hit conflicts.

5Reference

Reference

Trait base classes

Base classUse when
ml::TraitBehavior-only trait. No manifest, no events.
ml::TraitWith<Manifest>Trait that declares its own Flags or States enums.
ml::EventReceiverTrait that stores callbacks and participates in the event system.
ml::EventDispatcherSingleton that decides when an SFML event fires and to whom.

EventDispatcher methods

MethodWhen calledPurpose
occurred(event)Once per incoming SFML eventReturn 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 filteringIterate components and call process() on matching ones

Checklist for a new event trait

#Step
1Define a receiver class inheriting ml::EventReceiver. Expose a friendly onXxx(callback) method that stores callbacks via getCallbacks("your_key").
2Define a dispatcher class inheriting ml::EventDispatcher. Implement occurred(), filter(), and fire(). Use ml::EventManager::fire("your_key", this, event) in fire().
3Place ML_EXPORT(MyDispatcher) at file scope, outside any namespace.
4Ensure the string key in getCallbacks() and EventManager::fire() are identical.
5Mix the receiver into a component: class MyComp : public ml::Component<MyTrait>.
💡
When not to write a trait: If the behavior is only needed by one specific component and does not generalize to others, implement it directly in the component class rather than packaging it as a trait. Traits pay off when the same behavior will be reused in three or more places.