Built-in Events
Every Malena component inherits a full event system automatically. No setup — just call the method and respond to what happens.
Before you start
You'll need Malena and SFML set up. Check the quick start guide if you haven't.
How the Event System Works
Every ml::Component inherits a full set of event traits automatically through ml::Core. Each trait exposes friendly callback methods. There is nothing to register or configure — just call the method on your component and provide a lambda.
void MyScreen::registerEvents()
{
_button.onClick([this]{ handleClick(); });
_button.onHover([this]{ _button.setFillColor(sf::Color::Yellow); });
_panel.onUpdate([this]{ animate(); });
}
const std::optional<sf::Event>& when you need details like key codes or mouse position.The ml::Event Enum
All built-in events are identified by the ml::Event enum. You typically never use this directly — the trait methods handle it. It becomes useful with unsubscribe().
| Event | Fires when | Category |
|---|---|---|
ml::Event::CLICK | Mouse button released over component bounds | mouse |
ml::Event::HOVER | Mouse cursor entered component bounds | mouse |
ml::Event::UNHOVER | Mouse cursor left component bounds | mouse |
ml::Event::MOUSE_PRESSED | Raw mouse press (before click logic) | mouse |
ml::Event::MOUSE_RELEASED | Raw mouse release | mouse |
ml::Event::DRAG | Component is being dragged (needs DRAGGABLE flag) | mouse |
ml::Event::SCROLL | Mouse wheel scrolled over component | scroll |
ml::Event::FOCUS | Component gained keyboard focus | focus |
ml::Event::BLUR | Component lost keyboard focus | focus |
ml::Event::KEYPRESS | Key pressed while component has focus | keyboard |
ml::Event::KEY_RELEASE | Key released while focused | keyboard |
ml::Event::TEXT_ENTERED | Unicode character entered while focused | keyboard |
ml::Event::UPDATE | Every frame, before drawing | frame |
ml::Event::WINDOW_RESIZED | Application window was resized | window |
ml::Event::WINDOW_FOCUS_GAINED | App window gained OS focus | window |
ml::Event::WINDOW_FOCUS_LOST | App window lost OS focus | window |
Clickable and Hoverable
// Simple callback — no event data needed
_submitBtn.onClick([this]{ submitForm(); });
// With event data — check which mouse button
_canvas.onClick([this](const std::optional& e) {
if (!e) return;
if (auto* mb = e->getIf())
handleClick(mb->position);
});
// Hover — highlight on enter, restore on leave
_card.onHover([this] { _card.setFillColor(sf::Color(60, 60, 80)); });
_card.onUnhover([this]{ _card.setFillColor(sf::Color(40, 40, 55)); });
// Focus — show cursor ring on text input
_textInput.onFocus([this]{ _textInput.setOutlineColor(sf::Color(124, 106, 247)); });
_textInput.onBlur([this] { _textInput.setOutlineColor(sf::Color(60, 60, 80)); });
Keyable — Keyboard Input
Keyboard events only fire on the currently focused component. A component must be clicked (or have focus explicitly given) before it receives keyboard input.
// Respond to specific keys (use for game controls / shortcuts)
_textInput.onKeypress([this](const std::optional& e) {
if (!e) return;
if (auto* kp = e->getIf()) {
if (kp->code == sf::Keyboard::Key::Enter) submitInput();
if (kp->code == sf::Keyboard::Key::Escape) clearInput();
}
});
// Handle text input — unicode-safe, handles IME and modifiers correctly
// Prefer this over onKeypress for any text field
_textInput.onTextEntered([this](const std::optional& e) {
if (!e) return;
if (auto* te = e->getIf()) {
if (te->unicode >= 32 && te->unicode < 127)
_text += static_cast(te->unicode);
}
});
onTextEntered for text fields — it handles unicode, shift, and modifier keys correctly. Use onKeypress for game controls where you need raw key codes.Updatable and Scrollable
// Per-frame animation — fires every frame before drawing
float _angle = 0.f;
_spinner.onUpdate([this]{
_angle += 2.f;
_spinner.setRotation(sf::degrees(_angle));
});
// Countdown timer
sf::Clock _clock;
_timer.onUpdate([this]{
float remaining = 30.f - _clock.getElapsedTime().asSeconds();
if (remaining <= 0.f) { onTimeUp(); return; }
_timerText.setString(std::to_string((int)remaining));
});
// Mouse wheel scroll
_list.onScroll([this](const std::optional& e) {
if (!e) return;
if (auto* scroll = e->getIf()) {
_offsetY -= scroll->delta * 30.f;
_offsetY = std::clamp(_offsetY, 0.f, _maxScroll);
updateLayout();
}
});
// Reflow on window resize — no SFML event switch needed
_panel.onWindowResized([this]{
auto size = ml::WindowManager::getWindow().getSize();
_panel.setSize({(float)size.x, (float)size.y});
});
Draggable — Mouse Drag
Dragging is enabled via a flag, not a callback. Set ml::Flag::DRAGGABLE and the framework handles all mouse tracking and position updating automatically.
// Basic free drag
_panel.setFlag(ml::Flag::DRAGGABLE);
// Drag on Y axis only (e.g. a vertical slider thumb)
_thumb.setFlag(ml::Flag::DRAGGABLE);
_thumb.setState(ml::DraggableManifest::State::LOCK_X);
// Constrain to a track area
_thumb.setDragBounds(sf::FloatRect{{trackX, trackY}, {trackWidth, trackHeight}});
// Revert to free drag
_thumb.clearDragBounds();
_thumb.setState(ml::DraggableManifest::State::FREE);
// Check if dragging in draw() to show visual feedback
void draw(sf::RenderTarget& t, sf::RenderStates s) const override
{
if (checkFlag(ml::Flag::DRAGGING))
_body.setOutlineColor(sf::Color(93, 202, 165, 200));
t.draw(_body, s);
}
Custom Events — subscribe and publish
Declare an Event enum in your manifest and use subscribe / publish for components to communicate with each other — completely decoupled.
// Declare in manifest:
// enum class Event { ScoreChanged, GameOver, PlayerDied };
class HUD : public ml::Component
{
void registerEvents() override
{
subscribe(GameManifest::Event::ScoreChanged, [this]{
updateScoreDisplay();
});
subscribe(GameManifest::Event::GameOver, [this]{
showGameOverScreen();
});
}
};
class GameLogic : public ml::Component
{
void onEnemyDefeated()
{
_score += 100;
publish(GameManifest::Event::ScoreChanged); // HUD updates immediately
if (_score >= _targetScore)
publish(GameManifest::Event::GameOver);
}
};
// Unsubscribe a specific event
myRect.unsubscribe(ml::Event::CLICK);
myRect.unsubscribeAll(); // remove everything (also called automatically on destroy)