Your first Malena app
Build a fully interactive task board from scratch. You'll touch every core system — manifests, components, positioning helpers, flags, states, events, animation, messaging, and drag-and-drop — using Malena APIs first, reaching for raw SFML only when needed.
Before you start
You'll need Malena and SFML installed and a CMake project that links both. If you haven't set that up yet, check the quick start guide on the home page.
What we're building
By the end of this tutorial you'll have a working task board: four draggable cards, each with a title, status badge, hover highlight, click-to-complete toggle, and a fade animation. When one card is completed, it broadcasts a message and the others react.
Declaring resources with a Manifest
The first thing you write in any Malena app is the manifest. A manifest is a struct that declares everything your app needs — textures, fonts, sounds, configuration values, custom flags, and custom states — all in one place. No file paths scattered through your code.
Inheriting ml::Manifest gives you the set() family of methods. You call them once in an inline static initializer, and every resource is registered at program startup before main() runs.
#pragma once #include <Malena/Manifests/Manifest.h> class TaskManifest : public ml::Manifest { public: // ── Plugin identity (required for plugins; good habit here too) ── static constexpr const char* name = "Task Board"; static constexpr const char* version = "1.0.0"; // ── Asset enums ────────────────────────────────────────────────── enum class Images { CardBg, CheckIcon }; enum class Fonts { Body }; enum class Sounds { Complete }; // ── Configuration enums ────────────────────────────────────────── enum class Text { AppTitle }; enum class Ints { CardWidth, CardHeight, CardPadding }; enum class Floats { AnimSpeed }; // ── Component flags ────────────────────────────────────────────── enum class Flag { Selected, Completed, Highlighted }; // ── Component states ───────────────────────────────────────────── enum class State { Idle, Active, Done }; private: // Inline static initializer — runs once before main() inline static const auto _ = [](){ // Assets set(Images::CardBg, "assets/card_bg.png"); set(Images::CheckIcon, "assets/check.png"); set(Fonts::Body, "assets/inter.ttf"); set(Sounds::Complete, "assets/complete.wav"); // Configuration set(Text::AppTitle, std::string("Task Board")); set(Ints::CardWidth, 220); set(Ints::CardHeight, 130); set(Ints::CardPadding, 24); set(Floats::AnimSpeed, 0.4f); return 0; }(); };
Once the manifest is declared, AssetsManager and ConfigManager handle all loading and caching. You access everything through the unified Resources::get() method once you inherit from ml::ComponentWith<TaskManifest> — which we'll do next.
The Application class
Every Malena app starts by inheriting ml::ApplicationWith<Manifest>. This gives you the window, the main loop, and two lifecycle methods you must implement:
| Method | When it runs | What to do here |
|---|---|---|
| initialization() | Once, at startup | Create components, load resources, set positions |
| registerEvents() | Immediately after initialization() | Wire up onClick, onHover, onMessage callbacks |
The two-phase split is intentional: by the time registerEvents() runs, every object has already been constructed, so callbacks can safely reference any component without order-of-initialization problems.
#pragma once #include <Malena/Engine/App/Application.h> #include <Malena/Graphics/Primitives/Rectangle.h> #include <Malena/Graphics/Text/Text.h> #include "TaskManifest.h" #include "TaskCard.h" // we'll build this next class TaskBoard : public ml::ApplicationWith<TaskManifest> { public: TaskBoard() : ApplicationWith(1280, 720, 32, Resources::get(Text::AppTitle)) // title from manifest {} void initialization() override; void registerEvents() override; private: TaskCard _card1, _card2, _card3, _card4; ml::Text _header; };
And the entry point stays as simple as it gets:
#include "TaskBoard.h" int main() { TaskBoard app; app.run(); return 0; }
Components and positioning helpers
A TaskCard is a component — a class that inherits ml::ComponentWith<TaskManifest>. This wires in the manifest aliases, flag storage, state machine, and the full trait set (Clickable, Hoverable, Draggable, Positionable, etc.) automatically.
#pragma once #include <Malena/Core/Component.h> #include <Malena/Graphics/Primitives/Rectangle.h> #include <Malena/Graphics/Text/Text.h> #include <Malena/Traits/Messenger.h> #include <Malena/Traits/Fadeable.h> #include "TaskManifest.h" enum class BoardEvent { CardCompleted, CardSelected }; class TaskCard : public ml::ComponentWith<TaskManifest, ml::Messenger, ml::Fadeable> { public: void setup(const std::string& title, const std::string& subtitle, sf::Color color); void draw(sf::RenderTarget& target, sf::RenderStates states) const override; void setPosition(const sf::Vector2f&) override; sf::FloatRect getGlobalBounds() const override; sf::Vector2f getPosition() const override; private: ml::Rectangle _body; ml::Text _title; ml::Text _subtitle; ml::Rectangle _badge; ml::Text _badgeLabel; };
In setup() we use Malena's relative layout helpers instead of calculating pixel positions by hand. Every Positionable object has these built in:
| Method | What it does |
|---|---|
| setRightOf(obj, gap) | Places this immediately to the right of obj with optional spacing |
| setBelow(obj, gap) | Places this immediately below obj with optional spacing |
| setAbove(obj, gap) | Places this immediately above obj |
| setLeftOf(obj, gap) | Places this immediately to the left of obj |
| center(obj) | Centers this within obj's bounds (both axes) |
| centerHorizonally(obj) | Centers horizontally within obj |
| centerVertically(obj) | Centers vertically within obj |
| centerText(sfText) | Centers an sf::Text inside this object's bounds |
void TaskCard::setup(const std::string& title, const std::string& subtitle, sf::Color color) { // Read dimensions from the manifest — no magic numbers const float w = static_cast<float>(Resources::get(Ints::CardWidth)); const float h = static_cast<float>(Resources::get(Ints::CardHeight)); const float p = static_cast<float>(Resources::get(Ints::CardPadding)); // Card body _body.setSize({w, h}); _body.setFillColor(color); _body.setOutlineThickness(1.f); _body.setOutlineColor(sf::Color(255, 255, 255, 20)); // Title — uses the font from our manifest _title.setFont(Resources::get(Fonts::Body)); _title.setString(title); _title.setCharacterSize(15); _title.setFillColor(sf::Color::White); // Subtitle — positioned below the title using Malena layout helper _subtitle.setFont(Resources::get(Fonts::Body)); _subtitle.setString(subtitle); _subtitle.setCharacterSize(12); _subtitle.setFillColor(sf::Color(200, 200, 200, 180)); // Status badge _badge.setSize({70.f, 22.f}); _badge.setFillColor(sf::Color(255, 255, 255, 20)); _badgeLabel.setFont(Resources::get(Fonts::Body)); _badgeLabel.setString("● idle"); _badgeLabel.setCharacterSize(11); _badgeLabel.setFillColor(sf::Color(180, 180, 180)); } void TaskCard::setPosition(const sf::Vector2f& pos) { const float p = static_cast<float>(Resources::get(Ints::CardPadding)); _body.setPosition(pos); // Place title inside card with padding — no manual arithmetic _title.setPosition({pos.x + p, pos.y + p}); // Subtitle goes below the title using Malena's layout helper _subtitle.setBelow(_title, 4.f); // Badge sits at the bottom-left of the card _badge.setPosition({pos.x + p, pos.y + _body.getSize().y - p - 22.f}); // Badge label centered inside the badge — one call, no math _badgeLabel.setBelow(_title, 4.f); _body.centerText(_badgeLabel.getSFText()); _badge.centerText(_badgeLabel.getSFText()); }
Now in TaskBoard::initialization() we lay out the cards using the same helpers — no coordinates calculated by hand:
void TaskBoard::initialization() { // Set up each card with a title, subtitle, and color _card1.setup("Design system", "Colour tokens & type scale", sf::Color(38, 33, 92)); _card2.setup("Event system", "onClick, onHover, onUpdate", sf::Color(8, 42, 32)); _card3.setup("Manifest", "Assets & config", sf::Color(12, 31, 53)); _card4.setup("Animations", "moveTo, fadeIn, holdFor", sf::Color(42, 15, 26)); // Place card1 at a fixed anchor, then chain the rest with layout helpers _card1.setPosition({80.f, 140.f}); _card2.setRightOf(_card1, 24.f); // 24px gap to the right _card3.setBelow(_card1, 24.f); // 24px gap below card1 _card4.setBelow(_card2, 24.f); // 24px gap below card2 // Header text — centered horizontally inside the window _header.setFont(Resources::get(Fonts::Body)); _header.setString(Resources::get(Text::AppTitle)); _header.setCharacterSize(28); _header.setPosition({80.f, 60.f}); // Register everything with the framework addComponent(_card1); addComponent(_card2); addComponent(_card3); addComponent(_card4); addComponent(_header); }
Flags and states
Flags are booleans tied to an enum. States are the current active value from an enum. Because we declared both in TaskManifest, every TaskCard automatically inherits storage for them — no bool _selected members needed.
Malena also gives you ml::Flag system flags on every component — things like HOVERED, FOCUSED, and DRAGGABLE that the framework manages automatically:
| API | What it does |
|---|---|
| enableFlag(Flag::X) | Sets flag X to true |
| disableFlag(Flag::X) | Sets flag X to false |
| toggleFlag(Flag::X) | Flips flag X |
| checkFlag(Flag::X) | Returns true if X is set |
| setFlag(Flag::X, bool) | Sets X to an explicit value |
| setState(State::X) | Transitions to state X, fires enter/exit callbacks |
| getState() | Returns the current state |
| isState(State::X) | Returns true if currently in state X |
| onStateEnter(cb) | Fires cb whenever any state is entered |
| onStateExit(cb) | Fires cb whenever any state is exited |
// Set initial state on all cards _card1.setState(State::Active); _card2.setState(State::Idle); _card3.setState(State::Idle); _card4.setState(State::Done); // Pre-complete card4 and mark its flag _card4.enableFlag(Flag::Completed); // React whenever any card transitions into Done _card1.onStateEnter([&](State s){ if (s == State::Done) _card1.enableFlag(Flag::Completed); }); // Enable dragging via the system flag _card1.setFlag(ml::Flag::DRAGGABLE, true); _card2.setFlag(ml::Flag::DRAGGABLE, true); _card3.setFlag(ml::Flag::DRAGGABLE, true); _card4.setFlag(ml::Flag::DRAGGABLE, true);
In TaskCard::draw() we read flags and states to decide what to render:
void TaskCard::draw(sf::RenderTarget& target, sf::RenderStates states) const { // Apply fade alpha from Fadeable trait auto bodyColor = _body.getFillColor(); bodyColor.a = getAlpha(); // Fadeable::getAlpha() _body.setFillColor(bodyColor); // Highlight border when hovered (system flag — set by framework) if (checkFlag(ml::Flag::HOVERED)) _body.setOutlineColor(sf::Color(255, 255, 255, 80)); else _body.setOutlineColor(sf::Color(255, 255, 255, 20)); // Badge text reflects current state — no if-else chain on booleans if (isState(State::Active)) _badgeLabel.setString("● active"); else if (isState(State::Done)) _badgeLabel.setString("✓ done"); else _badgeLabel.setString("○ idle"); target.draw(_body, states); target.draw(_title, states); target.draw(_subtitle, states); target.draw(_badge, states); target.draw(_badgeLabel, states); }
Wiring up events
Every Malena component has a full set of event callbacks built in. You register them in registerEvents() using lambda functions — no subclassing, no virtual overrides, no switch statements on raw SFML events.
| Callback | Fires when |
|---|---|
| onClick(fn) | Mouse pressed and released over this component |
| onHover(fn) | Mouse cursor enters component bounds |
| onUnhover(fn) | Mouse cursor leaves component bounds |
| onFocus(fn) | Component gains keyboard focus |
| onBlur(fn) | Component loses keyboard focus |
| onKeypress(fn) | Key pressed while component has focus |
| onTextEntered(fn) | Unicode character entered while focused |
| onScroll(fn) | Mouse wheel scrolled over component |
| onMouseMoved(fn) | Mouse moved anywhere in window |
| onUpdate(fn) | Every frame, before draw |
| onWindowResized(fn) | Application window resized |
void TaskBoard::registerEvents() { // Lambda helper so we don't repeat this for all 4 cards auto wireCard = [this](TaskCard& card) { // Click toggles the card between Idle/Active and Done card.onClick([&]{ if (card.isState(State::Done)) { card.setState(State::Active); card.disableFlag(Flag::Completed); } else { card.setState(State::Done); card.enableFlag(Flag::Completed); } }); // Hover highlights the card (border is drawn in draw() via HOVERED flag) card.onHover([&]{ card.enableFlag(Flag::Highlighted); }); card.onUnhover([&]{ card.disableFlag(Flag::Highlighted); }); // Per-frame: advance any moveTo animations automatically // (Positionable handles the interpolation — we just need onUpdate) card.onUpdate([&]{ if (card.isScrolling()) card.setPosition(card.getPosition()); }); }; wireCard(_card1); wireCard(_card2); wireCard(_card3); wireCard(_card4); // Reflow cards when window is resized — no SFML event polling needed _card1.onWindowResized([this]{ _card1.setPosition({80.f, 140.f}); _card2.setRightOf(_card1, 24.f); _card3.setBelow(_card1, 24.f); _card4.setBelow(_card2, 24.f); }); }
Animation — moving and fading
Malena has two built-in animation systems: position tweening via Positionable, and alpha animation via the Fadeable trait. Both run automatically on every onUpdate tick — no manual frame counting.
| Method | What it does |
|---|---|
| moveTo(position, seconds) | Animates to an absolute world-space position |
| moveDistance(offset, seconds) | Animates by a relative offset from current position |
| isScrolling() | Returns true while a movement animation is running |
| setFramerate(fps) | Sets the assumed framerate for interpolation (default 60) |
| fadeIn(duration, tween, onComplete) | Animates alpha 0 → 255 |
| fadeOut(duration, tween, onComplete) | Animates alpha 255 → 0 |
| fadeTo(alpha, duration, tween) | Animates to a specific alpha value |
| holdFor(duration, onComplete) | Holds current alpha, then fires callback |
| getAlpha() | Returns current alpha (0–255) — call in draw() |
Let's animate the cards: when the app launches, they fade in one by one. When a card is completed it does a quick bounce using moveDistance. We can chain these with completion callbacks:
// Staggered fade-in on startup — cards appear one after another _card1.setAlpha(0); _card2.setAlpha(0); _card3.setAlpha(0); _card4.setAlpha(0); _card1.fadeIn(0.4f, ml::LINEAR, [this]{ _card2.fadeIn(0.4f, ml::LINEAR, [this]{ _card3.fadeIn(0.4f, ml::LINEAR, [this]{ _card4.fadeIn(0.4f); }); }); });
// When a card is clicked to Done, bounce it up then back card.onClick([&]{ if (!card.isState(State::Done)) { card.setState(State::Done); card.enableFlag(Flag::Completed); // Move up 12px over 0.15s, then back down auto origin = card.getPosition(); card.moveDistance({0.f, -12.f}, 0.15f); // Fade to 80% opacity while Done card.fadeTo(200, 0.3f); } else { card.setState(State::Active); card.disableFlag(Flag::Completed); card.fadeIn(0.2f); // back to full opacity } });
Messaging between components
The Messenger trait gives components a typed, enum-keyed message bus separate from the input event system. Where onClick and onHover react to user input, sendMessage and onMessage are for components talking to each other — a completed card notifying the others, for example.
Because TaskCard already inherits ml::Messenger in its template parameter list, all four cards can send and receive right away. We declared BoardEvent in the card header earlier — now we use it:
// When card1 completes, it broadcasts to all other cards _card1.onStateEnter([this](State s){ if (s == State::Done) { // sendMessage<PayloadType>(event, payload) _card1.sendMessage<int>(BoardEvent::CardCompleted, 1); } }); // card2, card3, card4 listen for any CardCompleted message auto onOtherCompleted = [this](TaskCard& card) { card.onMessage<int>(BoardEvent::CardCompleted, [&](const int& whichCard){ // Briefly dim then restore — a gentle acknowledgement card.fadeTo(140, 0.15f, ml::LINEAR, [&]{ card.fadeIn(0.3f); }); }); }; onOtherCompleted(_card2); onOtherCompleted(_card3); onOtherCompleted(_card4);
Making cards draggable
We already enabled dragging with setFlag(ml::Flag::DRAGGABLE, true) back in the flags section. That single call is all that's required for basic free drag. The framework handles mouse tracking, the drag offset, and position updates automatically.
For more control, Draggable also exposes axis locking and drag bounds:
// Constrain drag to the main content area sf::FloatRect boardArea = {{40.f, 100.f}, {1200.f, 580.f}}; _card1.setDragBounds(boardArea); _card2.setDragBounds(boardArea); _card3.setDragBounds(boardArea); _card4.setDragBounds(boardArea); // Lock card3 to horizontal axis only — slides left/right _card3.setState(ml::Draggable::State::LOCK_Y); // Restore free drag on card3 when clicked _card3.onClick([this]{ _card3.setState(ml::Draggable::State::FREE); });
The DRAGGING system flag is set automatically while a drag is in progress, so you can use it in draw() to show a visual indicator:
// Show a drop shadow while dragging if (checkFlag(ml::Flag::DRAGGING)) _body.setOutlineColor(sf::Color(93, 202, 165, 180)); // teal glow
Putting it all together
Here's the complete TaskBoard.cpp with all sections combined. Copy this into your project alongside the header and manifest files from earlier sections.
#include "TaskBoard.h" void TaskBoard::initialization() { // ── Cards ────────────────────────────────────────────────────────── _card1.setup("Design system", "Colour tokens & type", sf::Color(38, 33, 92)); _card2.setup("Event system", "onClick, onHover", sf::Color(8, 42, 32)); _card3.setup("Manifest", "Assets & config", sf::Color(12, 31, 53)); _card4.setup("Animations", "moveTo, fadeIn", sf::Color(42, 15, 26)); // ── Layout ───────────────────────────────────────────────────────── _card1.setPosition({80.f, 140.f}); _card2.setRightOf(_card1, 24.f); _card3.setBelow(_card1, 24.f); _card4.setBelow(_card2, 24.f); // ── Header ───────────────────────────────────────────────────────── _header.setFont(Resources::get(Fonts::Body)); _header.setString(Resources::get(Text::AppTitle)); _header.setCharacterSize(28); _header.setPosition({80.f, 60.f}); // ── States ───────────────────────────────────────────────────────── _card1.setState(State::Active); _card4.setState(State::Done); _card4.enableFlag(Flag::Completed); _card4.fadeTo(200, 0.0f); // instant — card4 starts dimmed // ── Drag bounds ───────────────────────────────────────────────────── sf::FloatRect bounds = {{40.f, 100.f}, {1200.f, 580.f}}; for (auto* c : {&_card1, &_card2, &_card3, &_card4}) { c->setFlag(ml::Flag::DRAGGABLE, true); c->setDragBounds(bounds); } // ── Staggered fade-in ─────────────────────────────────────────────── for (auto* c : {&_card1, &_card2, &_card3}) c->setAlpha(0); _card1.fadeIn(0.4f, ml::LINEAR, [this]{ _card2.fadeIn(0.4f, ml::LINEAR, [this]{ _card3.fadeIn(0.4f); }); }); // ── Register ──────────────────────────────────────────────────────── addComponent(_card1); addComponent(_card2); addComponent(_card3); addComponent(_card4); addComponent(_header); } void TaskBoard::registerEvents() { auto wireCard = [this](TaskCard& card) { // Click → toggle Done card.onClick([&]{ if (card.isState(State::Done)) { card.setState(State::Active); card.disableFlag(Flag::Completed); card.fadeIn(0.2f); } else { card.setState(State::Done); card.enableFlag(Flag::Completed); card.moveDistance({0.f, -10.f}, 0.12f); card.fadeTo(200, 0.3f); card.sendMessage<int>(BoardEvent::CardCompleted, 0); } }); // Hover feedback (border drawn in draw() via HOVERED flag) card.onHover([&]{ card.enableFlag(Flag::Highlighted); }); card.onUnhover([&]{ card.disableFlag(Flag::Highlighted); }); // Other cards dim briefly when any card completes card.onMessage<int>(BoardEvent::CardCompleted, [&](const int&){ card.fadeTo(140, 0.15f, ml::LINEAR, [&]{ if (!card.checkFlag(Flag::Completed)) card.fadeIn(0.3f); }); }); // Reflow on window resize card.onWindowResized([this]{ _card1.setPosition({80.f, 140.f}); _card2.setRightOf(_card1, 24.f); _card3.setBelow(_card1, 24.f); _card4.setBelow(_card2, 24.f); }); }; wireCard(_card1); wireCard(_card2); wireCard(_card3); wireCard(_card4); }