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.
Component vs ComponentWith
All drawable, interactive Malena objects are built on ml::Component. There are two main forms:
| Form | When 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
ComponentsManagerfor update and draw - The
Draggabletrait built in
Every component must implement two pure virtual methods:
void draw(sf::RenderTarget& target, sf::RenderStates states) const override;
void update() override;
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.
#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:
class MyApp : public ml::Application
{
SimpleIndicator _indicator;
void onInit() override
{
_indicator.setPosition(100.f, 100.f);
addComponent(_indicator);
}
};
std::unique_ptr collections. Never store them in a std::vector that reallocates.
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.
// 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()); }
};
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
| Trait | Source |
|---|---|
Subscribable | Via Core — provides event subscriptions (onClick, onHover, etc.) |
Flaggable | Via Core — provides framework system flags |
Positionable | Via Core — provides setPosition / getPosition |
Draggable | Built into ComponentCore |
Subscribable, Flaggable, or Positionable as template arguments to Component causes a compile-time error. These are always present — no need to add them.
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.
#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 first, then any extra traits
class PlayerCard : public ml::ComponentWith<PlayerCardManifest, ml::Messenger, ml::Themeable>
{
...
};
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.
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 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 type | Resources::get() return type |
|---|---|
Images (textures) | sf::Texture& |
Fonts | sf::Font& |
Sounds | sf::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:
// 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");
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.
#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;
};
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);
}
};
Quick Reference
Choosing the right base
| Scenario | Base class |
|---|---|
| Simple drawing, no assets | ml::Component<> |
| Simple drawing + messaging | ml::Component<ml::Messenger> |
| Drawing + theming + messaging | ml::Component<ml::Messenger, ml::Themeable> |
| Drawing + assets from manifest | ml::ComponentWith<MyManifest> |
| Assets + extra traits | ml::ComponentWith<MyManifest, ml::Messenger, ml::Themeable> |
Events available on every Component
| Method | Fires 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
| Method | Purpose |
|---|---|
void draw(sf::RenderTarget&, sf::RenderStates) const override | Draw all SFML drawables to the render target |
void update() override | Per-frame logic — animations, timers, state transitions |