ml::docs | Your First App Manifests Events Plugins Scenes Graphics Resources
← All Tutorials
Beginner 10 sections ~35 min C++17

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.

Introduction

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.

TaskBoard — 1280 × 720
Design system
Set up color tokens
● active
Event system
onClick, onHover
● active
Manifest
Assets & config
○ idle
Animations
moveTo, fadeIn
✓ done
interactive preview →
Manifest system Flags & States onClick / onHover onUpdate moveTo / fadeIn Messenger Draggable Positionable helpers

1 Manifest

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.

TaskManifest.h C++17
#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;
    }();
};
Malena tip: Notice that CardWidth and CardHeight are declared in the manifest as Ints, not as C++ constants. This means you can later change them in one place and every component that reads them via Resources::get() picks up the new values automatically.

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.


2 Application

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:

MethodWhen it runsWhat to do here
initialization()Once, at startupCreate 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.

TaskBoard.h C++17
#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;
};
Notice the constructor passes Resources::get(Text::AppTitle) directly to ApplicationWith. Because TaskBoard inherits ApplicationWith<TaskManifest>, the manifest aliases — Text, Images, Fonts, etc. — are in scope without full qualification.

And the entry point stays as simple as it gets:

main.cpp C++17
#include "TaskBoard.h"

int main()
{
    TaskBoard app;
    app.run();
    return 0;
}

3 Components & positioning

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.

TaskCard.h C++17
#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:

MethodWhat 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
TaskCard.cpp — setup() C++17
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:

TaskBoard.cpp — initialization() C++17
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);
}
Why addComponent? Registering a component with the framework is what causes it to receive draw() calls, event delivery, and update ticks each frame. Objects that aren't registered are invisible to the framework.

4 Flags & States

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:

APIWhat 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
TaskBoard.cpp — registerEvents() — flags & states C++17
// 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:

TaskCard.cpp — draw() C++17
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);
}

5 Events

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.

CallbackFires 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
TaskBoard.cpp — registerEvents() — events C++17
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);
    });
}
No sf::Event switch statements. You'll never write if (event.type == sf::Event::MouseButtonReleased) in Malena app code. The framework routes everything for you. Each callback fires exactly when its name says it will.

6 Animation

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.

MethodWhat 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:

TaskBoard.cpp — initialization() additions C++17
// 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);
        });
    });
});
TaskBoard.cpp — registerEvents() additions C++17
// 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
    }
});
Remember: Fadeable::getAlpha() gives you the animated value — but you still have to apply it to your drawable's color in draw(). Malena doesn't modify your SFML objects directly; it gives you the value and you decide where it goes.

7 Messaging

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:

TaskBoard.cpp — registerEvents() — messaging C++17
// 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);
Message subscriptions are cleaned up automatically when a Messenger object is destroyed. You don't need to manually unsubscribe in your destructor. If you need to unsubscribe early, call offMessage<int>(BoardEvent::CardCompleted) or offAllMessages().

8 Drag & drop

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:

TaskBoard.cpp — initialization() additions C++17
// 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:

TaskCard.cpp — draw() addition C++17
// Show a drop shadow while dragging
if (checkFlag(ml::Flag::DRAGGING))
    _body.setOutlineColor(sf::Color(93, 202, 165, 180));  // teal glow

Full source

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.

TaskBoard.cpp — complete C++17
#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);
}

Tutorial complete!

You've used manifests, components, positioning helpers, flags, states, events, animation, messaging, and drag-and-drop — the full Malena stack.