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

Theming

The Malena theme system lets you define a complete visual identity — colors, typography, and spacing — and apply it globally across every component at once. Switch between themes at runtime with a single call.

Introduction

How Themes Work

A theme is a plain struct of design tokens: color roles, typography settings, and geometry values. Any component that inherits the Themeable trait automatically subscribes to ThemeManager and has its onThemeApplied() method called whenever the active theme changes.

The flow looks like this:

How a theme change propagates text
ThemeManager::apply() or ThemeManager::set()
  → ThemeManager notifies all Themeable subscribers
    → each component's onThemeApplied(const Theme&) is called
      → component re-styles itself from the new token values

Components built into Malena (buttons, inputs, panels, toggles, etc.) already implement this protocol — they restyle themselves automatically. When you create your own components you can opt into theming by inheriting Themeable and overriding onThemeApplied().

💡
Default theme: Before you call any theme function, ThemeManager defaults to ml::DarkTheme. All components that check the theme in their constructor will use dark-theme values from the start.

1Core Concepts

Built-in Themes

Malena ships two themes out of the box.

TypeDescription
ml::DarkThemeDeep charcoal backgrounds, purple primary accents. The default.
ml::LightThemeWhite/near-white surfaces, purple primary accents.

Apply either one directly with ThemeManager::set():

Apply a built-in theme cpp
// Switch to light theme
ml::ThemeManager::set(ml::LightTheme());

// Switch back to dark
ml::ThemeManager::set(ml::DarkTheme());

// Read a value from the active theme
const ml::Theme& t = ml::ThemeManager::get();
sf::Color bg = t.background;

The call to set() immediately notifies all Themeable components, so the UI updates on the same frame.

ℹ️
Theme tokens: Every ml::Theme carries 14 color roles, 3 typography values, and 3 geometry values. See the Full API section for the complete list.

2Custom Themes

Creating a Custom Theme

A custom theme is a struct that inherits ml::Theme and overrides whatever tokens you want in its constructor. Tokens you don't override inherit their default values.

NeonTheme.h — a custom theme cpp
#include <Malena/Malena.h>

struct NeonTheme : ml::Theme
{
    NeonTheme()
    {
        // Color roles
        primary      = sf::Color(0,   255, 180);   // bright cyan-green
        secondary    = sf::Color(0,   200, 255);   // electric blue
        surface      = sf::Color(10,  10,  20);    // near-black
        background   = sf::Color(5,   5,   15);
        onSurface    = sf::Color(220, 255, 245);   // light mint text
        onPrimary    = sf::Color(0,   0,   0);     // black on bright buttons
        onBackground = sf::Color(200, 240, 230);
        muted        = sf::Color(60,  100, 90);
        border       = sf::Color(0,   180, 130);
        borderFocus  = sf::Color(0,   255, 180);
        error        = sf::Color(255, 60,  100);
        success      = sf::Color(0,   255, 140);
        disabled     = sf::Color(20,  30,  25);
        onDisabled   = sf::Color(60,  100, 90);

        // Geometry
        radius           = 4.f;    // sharper corners
        spacing          = 6.f;
        borderThickness  = 1.f;
    }
};

Apply it directly:

Apply the custom theme cpp
ml::ThemeManager::set(NeonTheme());
💡
Font token: The font field is a raw pointer (const sf::Font*) defaulting to nullptr. Components that draw text fall back to the framework default font when this is null. Set it to point at a font loaded through FontManager if you want a custom typeface across your whole UI.

3Manifest Themes

Registering Themes in a Manifest

When your application has multiple named themes (for example, different game modes), the cleanest approach is to register them in a Manifest and switch between them by enum key.

GameManifest.h — declare and register themes cpp
#include <Malena/Malena.h>
#include "NeonTheme.h"

class GameManifest : public ml::Manifest
{
public:
    // One enum value per theme your app supports
    enum class Themes { Main, GameOver, Pause };

    // Register themes once at static-init time
    inline static const auto _ = []()
    {
        set(Themes::Main,     ml::DarkTheme());
        set(Themes::GameOver, NeonTheme());
        set(Themes::Pause,    ml::LightTheme());
        return 0;
    }();
};

Then switch themes anywhere in your application with a single call:

Switch themes at runtime cpp
// When the game ends:
ml::ThemeManager::apply<GameManifest>(GameManifest::Themes::GameOver);

// When the player pauses:
ml::ThemeManager::apply<GameManifest>(GameManifest::Themes::Pause);

// Back to the main theme:
ml::ThemeManager::apply<GameManifest>(GameManifest::Themes::Main);
ℹ️
apply() vs set(): Use apply<Manifest>(key) when themes are registered in a manifest and you want to switch by name. Use set(theme) for one-off or programmatic theme changes where you have a theme instance already.

Calling apply() from a plugin

Plugins work the same way — call ml::ThemeManager::apply() or ml::ThemeManager::set() from onLoad() or in response to a message:

Theme switch from inside a plugin cpp
void onLoad() override
{
    onMessage<bool>(GameEvent::GameOver, [](const bool&) {
        ml::ThemeManager::apply<GameManifest>(GameManifest::Themes::GameOver);
    });
}

4Core Concepts

Making a Component Themeable

To make your own component respond to theme changes, inherit ml::Themeable and override onThemeApplied(). The Themeable constructor automatically registers with ThemeManager, and the destructor automatically unregisters — there is no manual setup required.

MyCard.h — a custom Themeable component cpp
#include <Malena/Malena.h>

class MyCard : public ml::ComponentWith<ml::DefaultManifest>,
               public ml::Themeable
{
public:
    MyCard()
    {
        _bg.setSize({200.f, 120.f});
        _label.setString("Hello");
        // Apply the current theme immediately on construction
        onThemeApplied(ml::ThemeManager::get());
    }

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

    void update() override {}

protected:
    void onThemeApplied(const ml::Theme& theme) override
    {
        if (isThemeLocked()) return;

        _bg.setFillColor(theme.surface);
        _bg.setOutlineColor(theme.border);
        _bg.setOutlineThickness(theme.borderThickness);
        _label.setFillColor(theme.onSurface);
        _label.setCharacterSize(theme.fontSize);
    }

private:
    sf::RectangleShape _bg;
    sf::Text           _label;
};
💡
Apply on construction: ThemeManager notifies only when the theme changes. To style correctly from the first frame, call onThemeApplied(ml::ThemeManager::get()) at the end of your constructor, as shown above.

Inheritance order matters

Themeable inherits ml::Trait, which is a lightweight base. Place Themeable after the primary base class in your inheritance list:

Correct inheritance order cpp
// Good — component base first, traits after
class MyWidget : public ml::ComponentWith<MyManifest>,
                 public ml::Themeable
{ ... };

// If you have multiple traits, list them in any order after the base
class MyPanel : public ml::ComponentWith<MyManifest>,
                public ml::Themeable,
                public ml::Draggable
{ ... };

5Advanced

Locking and Per-Component Overrides

Themeable provides two independent lock mechanisms. These let you give individual components a fixed look even when the global theme changes.

Theme lock

A theme-locked component ignores all future ThemeManager notifications. Its current visual state is frozen until you unlock it.

Locking a component from theme changes cpp
// Lock — component keeps its current colors even if ThemeManager fires
myButton.lockTheme();

// Unlock — component will react to the next theme change
myButton.unlockTheme();

// Check state
if (myButton.isThemeLocked()) { ... }
ℹ️
Re-syncing after unlock: Unlocking does not immediately re-apply the current theme. If you want the component to sync right away, call onThemeApplied(ml::ThemeManager::get()) manually after unlocking.

Settings lock

A settings-locked component blocks the batch applySettings() path but still accepts explicit individual setter calls. This is useful when you want automatic theme-driven styling to stop, but still need to call setFillColor() or similar directly.

Settings lock cpp
// Block batch applySettings() — explicit setters still work
myToggle.lockSettings();

// Override a specific color directly (always works, regardless of locks)
myToggle.setTrackOnColor(sf::Color::Red);

myToggle.unlockSettings();

Combining locks with per-component overrides

A component with a custom accent that survives theme changes cpp
// Apply the global theme first, then lock and override one color
ml::ThemeManager::set(ml::DarkTheme());

myAccentButton.lockTheme();                        // freeze theme updates
myAccentButton.setFillColor(sf::Color(255, 80, 0)); // custom orange, persists

6Reference

Full API

ThemeManager

MethodDescription
ThemeManager::get()Return a const Theme& to the currently active theme.
ThemeManager::set<T>(theme)Apply a theme instance directly. T must derive from ml::Theme.
ThemeManager::apply<Manifest>(key)Apply a theme registered in a manifest by enum key.
ThemeManager::subscribe(Themeable*)Called automatically by Themeable's constructor. Do not call manually.
ThemeManager::unsubscribe(Themeable*)Called automatically by Themeable's destructor. Do not call manually.
ThemeManager::shutdown()Called by ApplicationBase on exit. Clears all subscribers.

Themeable trait

MethodDescription
lockTheme()Ignore future ThemeManager notifications.
unlockTheme()Resume reacting to theme changes. Does not re-apply immediately.
isThemeLocked()Return true if this component ignores theme changes.
lockSettings()Block batch applySettings(). Explicit setters still work.
unlockSettings()Allow applySettings() again.
isSettingsLocked()Return true if applySettings() is blocked.
onThemeApplied(const Theme&)Pure virtual. Override to re-style the component from the new theme.

Theme color tokens

TokenPurposeDarkTheme default
primaryBrand color — buttons, active states, highlightsrgb(100, 60, 200) (purple)
secondaryAccent — hover states, secondary actionsrgb(70, 130, 230) (blue)
surfaceCard and input backgroundsrgb(40, 40, 40)
backgroundPage / scene backgroundrgb(20, 20, 20)
onSurfaceText / icons on surfaceWhite
onPrimaryText / icons on primaryWhite
onBackgroundText / icons on backgroundrgb(220, 220, 220)
mutedPlaceholder, disabled, description textrgb(120, 120, 120)
borderOutline on inactive componentsrgb(100, 100, 100)
borderFocusOutline on focused componentsrgb(100, 60, 200) (purple)
errorError / invalid statergb(220, 70, 70)
successSuccess / confirm statergb(70, 200, 100)
disabledDisabled component fillrgb(60, 60, 60)
onDisabledText / icon on disabled componentsrgb(120, 120, 120)

Theme typography tokens

TokenTypePurpose
fontconst sf::Font*Global font pointer. nullptr = use framework default.
fontSizeunsigned intBase font size in points. Default: 14.
fontSizeSmallunsigned intSmall size for captions/descriptions. Default: 11.
fontSizeLargeunsigned intLarge size for headings/titles. Default: 18.

Theme geometry tokens

TokenTypePurpose
radiusfloatDefault corner radius for rounded shapes. Default: 8.f.
spacingfloatBase padding / gap unit. Default: 8.f.
borderThicknessfloatDefault outline thickness. Default: 1.5f.
💡
Next steps: Learn how to build your own components that participate in theming in the Custom Component tutorial, or explore how the Manifest system works for declaring themes alongside other resources.