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

Custom Components

Learn how to create your own Malena components from scratch. This tutorial explains the difference between Component<> and ComponentWith<Manifest>, how traits are mixed in, and what the Resources alias gives you automatically.

Introduction

Component vs ComponentWith

All drawable, interactive Malena objects are built on ml::Component. There are two main forms:

FormWhen to use
ml::Component<>Simple component with no resources or manifest. Just drawing and events.
ml::ComponentWith<MyManifest>Component that loads textures, fonts, sounds, or config from a manifest. Injects the Resources alias automatically.

ComponentWith<M> is simply a type alias for Component<M> — there is no extra class, just a more readable name. Both forms give you the same foundation:

  • Event subscriptions: onClick(), onHover(), onUnhover(), onUpdate(), onFocus(), onBlur()
  • Automatic unsubscription when the component is destroyed
  • Integration with ComponentsManager for update and draw
  • The Draggable trait built in

Every component must implement two pure virtual methods:

Required overrides for any Component cpp
void draw(sf::RenderTarget& target, sf::RenderStates states) const override;
void update() override;

1Core Concepts

Component with No Manifest

Use ml::Component<> (the empty-template form) when your component has no external assets and needs no manifest. This is the simplest starting point.

SimpleIndicator.h — no manifest, no traits cpp
#include <Malena/Malena.h>

class SimpleIndicator : public ml::Component<>
{
public:
    SimpleIndicator()
    {
        _circle.setRadius(10.f);
        _circle.setFillColor(sf::Color::Green);

        // Events are available immediately — no setup required
        onClick([this]() {
            toggle();
        });
    }

    void draw(sf::RenderTarget& target, sf::RenderStates states) const override
    {
        target.draw(_circle, states);
    }

    void update() override
    {
        // Called every frame — run animations, timers, etc.
    }

    void setPosition(float x, float y)
    {
        _circle.setPosition({x, y});
    }

private:
    sf::CircleShape _circle;
    bool _on = true;

    void toggle()
    {
        _on = !_on;
        _circle.setFillColor(_on ? sf::Color::Green : sf::Color(60, 60, 60));
    }
};

Register and use it in your application:

MyApp.h — registering the component cpp
class MyApp : public ml::Application
{
    SimpleIndicator _indicator;

    void onInit() override
    {
        _indicator.setPosition(100.f, 100.f);
        addComponent(_indicator);
    }
};
⚠️
No copying after registration: Malena components cannot be safely copied or moved once they have been registered with a manager. Store them by value as class members (as shown above), or in std::unique_ptr collections. Never store them in a std::vector that reallocates.

2Core Concepts

Adding Extra Traits

Pass traits as additional template arguments to Component<>. The framework mixes them into the component's inheritance chain automatically. All trait flag and state enums are gathered and unified — no manual wiring needed.

Adding Messenger and Themeable traits cpp
// Component with no manifest but two extra traits
class StatusBadge : public ml::Component<ml::Messenger, ml::Themeable>
{
public:
    StatusBadge()
    {
        // Messenger — send and receive typed messages
        onMessage<bool>(AppEvent::Connected, [this](const bool& ok) {
            _connected = ok;
            refresh();
        });

        // Themeable — react to global theme changes
        onThemeApplied(ml::ThemeManager::get());
    }

    void draw(sf::RenderTarget& t, sf::RenderStates s) const override
    {
        t.draw(_bg, s);
        t.draw(_label, s);
    }

    void update() override {}

protected:
    void onThemeApplied(const ml::Theme& theme) override
    {
        if (isThemeLocked()) return;
        _bg.setFillColor(_connected ? theme.success : theme.muted);
        _label.setFillColor(theme.onSurface);
    }

private:
    sf::RectangleShape _bg;
    sf::Text           _label;
    bool               _connected = false;

    void refresh() { onThemeApplied(ml::ThemeManager::get()); }
};
ℹ️
Draggable is always included: Component already inherits Draggable — you do not need to pass it as a trait. Dragging can be enabled by calling enableDrag() on any component.

Traits that are always included in Component

TraitSource
SubscribableVia Core — provides event subscriptions (onClick, onHover, etc.)
FlaggableVia Core — provides framework system flags
PositionableVia Core — provides setPosition / getPosition
DraggableBuilt into ComponentCore
⚠️
Do not re-add built-in traits: Passing Subscribable, Flaggable, or Positionable as template arguments to Component causes a compile-time error. These are always present — no need to add them.

3Core Concepts

ComponentWith — Using a Manifest

When a component needs to load textures, fonts, sounds, or configuration, declare a manifest and use ComponentWith<MyManifest>. The manifest is passed as the first template argument.

PlayerCard.h — component with a manifest cpp
#include <Malena/Malena.h>

// 1. Declare the manifest
class PlayerCardManifest : public ml::Manifest
{
public:
    // Texture resource enum — one value per image file
    enum class Images { Avatar, Frame };

    // Font resource enum
    enum class Fonts { Name };

    // Register resource paths
    inline static const auto _ = []()
    {
        load(Images::Avatar, "assets/images/avatar.png");
        load(Images::Frame,  "assets/images/card_frame.png");
        load(Fonts::Name,    "assets/fonts/bold.ttf");
        return 0;
    }();
};

// 2. Inherit ComponentWith — manifest is the first template arg
class PlayerCard : public ml::ComponentWith<PlayerCardManifest>
{
public:
    PlayerCard()
    {
        // Resources are available immediately — see section 4
        auto& avatarTex = Resources::get(Images::Avatar);
        auto& frameTex  = Resources::get(Images::Frame);
        auto& nameFont  = Resources::get(Fonts::Name);

        _avatar.setTexture(avatarTex);
        _frame.setTexture(frameTex);
        _nameText = sf::Text(nameFont, "Player One", 16);

        onClick([this]() { onSelected(); });
    }

    void draw(sf::RenderTarget& t, sf::RenderStates s) const override
    {
        t.draw(_frame,    s);
        t.draw(_avatar,   s);
        t.draw(_nameText, s);
    }

    void update() override {}

private:
    sf::Sprite _avatar;
    sf::Sprite _frame;
    sf::Text   _nameText;

    void onSelected() { /* highlight the card */ }
};

Combining a manifest with extra traits

Pass additional traits after the manifest:

Manifest + extra traits cpp
// Manifest first, then any extra traits
class PlayerCard : public ml::ComponentWith<PlayerCardManifest, ml::Messenger, ml::Themeable>
{
    ...
};

4Key Feature

The Resources Alias

When you use ComponentWith<MyManifest>, the framework automatically injects a Resources alias into your class scope. You use it to retrieve loaded assets by enum key — no path strings, no manager lookups by hand.

Accessing resources via the injected alias cpp
class PlayerCard : public ml::ComponentWith<PlayerCardManifest>
{
public:
    PlayerCard()
    {
        // Resources is available in any member function
        sf::Texture& tex  = Resources::get(Images::Avatar);
        sf::Font&    font = Resources::get(Fonts::Name);

        _avatar.setTexture(tex);
        _name = sf::Text(font, "Player", 16);
    }
};
💡
Resources is ManifestResources<M>: The Resources alias inside your component expands to ml::ManifestResources<PlayerCardManifest>. It is a type alias, not an instance — you call it with Resources::get(), not resources.get().

What Resources gives you

Manifest enum typeResources::get() return type
Images (textures)sf::Texture&
Fontssf::Font&
Soundssf::SoundBuffer&
Text (string config)std::string&

Resources are loaded once and shared. Multiple components using the same manifest enum value all receive a reference to the same loaded asset — no duplication.

No Resources alias without a manifest

If you use plain Component<> (no manifest), the Resources alias is not injected. Load assets manually via the individual managers if needed:

Manual asset access without a manifest cpp
// Without a manifest, use the global managers directly
sf::Texture& tex = ml::TextureManager<>::get("assets/images/sprite.png");
sf::Font&    fnt = ml::FontManager<>::get("assets/fonts/ui.ttf");

5Putting it Together

Complete Example: Health Bar

A health bar component that uses a manifest for its textures, reacts to theme changes, and exposes a typed message interface for updates.

HealthBar.h — full component cpp
#include <Malena/Malena.h>

// ── Shared message enum ──────────────────────────────────────────────────────
enum class GameEvent { HealthChanged, PlayerDied };

// ── Manifest ─────────────────────────────────────────────────────────────────
class HealthBarManifest : public ml::Manifest
{
public:
    enum class Images { BarFill, BarBackground };

    inline static const auto _ = []()
    {
        load(Images::BarFill,       "assets/ui/health_fill.png");
        load(Images::BarBackground, "assets/ui/health_bg.png");
        return 0;
    }();
};

// ── Component ─────────────────────────────────────────────────────────────────
class HealthBar : public ml::ComponentWith<HealthBarManifest,
                                           ml::Messenger,
                                           ml::Themeable>
{
public:
    HealthBar()
    {
        // Load textures via the injected Resources alias
        _bg.setTexture(Resources::get(Images::BarBackground));
        _fill.setTexture(Resources::get(Images::BarFill));

        // React to health change messages
        onMessage<int>(GameEvent::HealthChanged, [this](const int& hp) {
            setHealth(hp);
        });

        // React to player death
        onMessage<bool>(GameEvent::PlayerDied, [this](const bool&) {
            setHealth(0);
        });

        // Apply the current theme immediately
        onThemeApplied(ml::ThemeManager::get());
    }

    void setHealth(int hp)
    {
        _health = std::clamp(hp, 0, 100);
        float ratio = _health / 100.f;
        // Scale the fill bar by health ratio
        sf::IntRect rect = _fill.getTextureRect();
        rect.size.x = static_cast<int>(200.f * ratio);
        _fill.setTextureRect(rect);
        // Tint the fill based on health level
        if (_health > 50)      _fill.setColor(sf::Color(70, 200, 100));
        else if (_health > 25) _fill.setColor(sf::Color(200, 180, 50));
        else                   _fill.setColor(sf::Color(220, 70, 70));
    }

    void draw(sf::RenderTarget& t, sf::RenderStates s) const override
    {
        t.draw(_bg,   s);
        t.draw(_fill, s);
    }

    void update() override {}

protected:
    void onThemeApplied(const ml::Theme& theme) override
    {
        if (isThemeLocked()) return;
        // Optionally style the background outline from the theme
        // (most styling here comes from the textures themselves)
    }

private:
    sf::Sprite _bg;
    sf::Sprite _fill;
    int        _health = 100;
};
Usage in Application::onInit() cpp
class GameApp : public ml::Application
{
    HealthBar _healthBar;

    void onInit() override
    {
        addComponent(_healthBar);
    }

    void onReady() override
    {
        // Simulate damage — anyone with a Messenger can send this
        sendMessage<int>(GameEvent::HealthChanged, 75);
    }
};

6Reference

Quick Reference

Choosing the right base

ScenarioBase class
Simple drawing, no assetsml::Component<>
Simple drawing + messagingml::Component<ml::Messenger>
Drawing + theming + messagingml::Component<ml::Messenger, ml::Themeable>
Drawing + assets from manifestml::ComponentWith<MyManifest>
Assets + extra traitsml::ComponentWith<MyManifest, ml::Messenger, ml::Themeable>

Events available on every Component

MethodFires when
onClick(callback)Mouse button released over this component
onHover(callback)Mouse cursor enters this component's bounds
onUnhover(callback)Mouse cursor leaves this component's bounds
onFocus(callback)Keyboard focus gained
onBlur(callback)Keyboard focus lost
onUpdate(callback)Every frame (called before update())
onKeyPress(callback)Key pressed while this component has focus
onKeyRelease(callback)Key released while this component has focus
onMousePressed(callback)Mouse button pressed anywhere
onMouseReleased(callback)Mouse button released anywhere
onMouseMoved(callback)Mouse moved anywhere
onScroll(callback)Mouse wheel scrolled

Required overrides

MethodPurpose
void draw(sf::RenderTarget&, sf::RenderStates) const overrideDraw all SFML drawables to the render target
void update() overridePer-frame logic — animations, timers, state transitions
💡
Next steps: Now that you can create components, learn how to package reusable behavior into your own custom trait, or explore how the theme system can style your components automatically.