Skip to content

UI System Guide

Overview

PixelRoot32 provides a built-in UI framework for creating menus, HUDs (Heads-Up Displays), dialogs, and other interface elements. The UI system is designed to work seamlessly with the engine's rendering pipeline and is optimized for both ESP32 and PC targets.

Architecture

The UI system consists of:

  • UI Elements: Individual components (buttons, labels, progress bars)
  • Layouts: Arrangement systems (absolute, grid, stack)
  • Styling: Visual appearance (colors, fonts, borders)
  • Event Handling: Input response (clicks, hovers, focus)

UI Elements

1. UILabel - Text Display

Basic text display with optional styling:

#include <ui/UILabel.h>

auto label = std::make_unique<pixelroot32::ui::UILabel>(
    10, 20,           // x, y position
    "Score: 0",       // text
    pixelroot32::graphics::Color::White,  // color
    2                 // scale (2x font size)
);

scene.addEntity(label.get());

Properties: - Text content - Color - Font scale (1x, 2x, 3x) - Alignment (left, center, right)

2. UIButton - Interactive Button

Clickable button with visual feedback:

#include <ui/UIButton.h>

auto button = std::make_unique<pixelroot32::ui::UIButton>(
    50, 100,          // x, y
    80, 24,           // width, height
    "Start Game"      // label
);

// Set callback
button->onClick = []() {
    engine.setScene(&gameScene);
};

// Styling
button->setBackgroundColor(pixelroot32::graphics::Color::Blue);
button->setTextColor(pixelroot32::graphics::Color::White);
button->setBorder(2, pixelroot32::graphics::Color::White);

scene.addEntity(button.get());

States: - Normal - Hover (PC only - no touch on ESP32) - Pressed - Disabled

3. UIProgressBar - Progress Indicator

Horizontal or vertical progress bar:

#include <ui/UIProgressBar.h>

// Health bar
auto healthBar = std::make_unique<pixelroot32::ui::UIProgressBar>(
    10, 10,           // x, y
    100, 8,           // width, height
    pixelroot32::ui::UIProgressBar::HORIZONTAL  // orientation
);

healthBar->setRange(0, 100);      // min, max
healthBar->setValue(75);            // current value
healthBar->setForegroundColor(pixelroot32::graphics::Color::Red);
healthBar->setBackgroundColor(pixelroot32::graphics::Color::DarkGray);

scene.addEntity(healthBar.get());

Use Cases: - Health bars - Loading progress - Experience/level progress - Cooldown timers

4. UIPanel - Container

Container for grouping elements:

#include <ui/UIPanel.h>

auto panel = std::make_unique<pixelroot32::ui::UIPanel>(
    20, 20,           // x, y
    200, 150          // width, height
);

panel->setBackgroundColor(pixelroot32::graphics::Color::DarkBlue);
panel->setBorder(1, pixelroot32::graphics::Color::White);

// Add child elements
auto label = std::make_unique<pixelroot32::ui::UILabel>(10, 10, "Settings", Color::White, 2);
panel->addChild(label.get());

scene.addEntity(panel.get());

Layout Systems

Absolute Layout (Default)

Direct positioning with x, y coordinates:

// Place elements at specific positions
label->position.x = 10;
label->position.y = 20;

// Best for: HUDs, simple menus, absolute positioning

Grid Layout

Arrange elements in rows and columns:

#include <ui/layouts/UIGridLayout.h>

pixelroot32::ui::UIGridLayout grid(3, 2);  // 3 columns, 2 rows
grid.setCellSize(70, 40);
grid.setSpacing(10, 10);

// Add elements - automatically positioned
for (int i = 0; i < 6; i++) {
    auto btn = std::make_unique<UIButton>(0, 0, 60, 30, "Btn " + std::to_string(i));
    grid.addElement(btn.get());
}

Use Cases: - Inventory grids - Level selection screens - Button matrices

Stack Layout

Vertical or horizontal stacking:

#include <ui/layouts/UIStackLayout.h>

// Vertical stack (menu)
pixelroot32::ui::UIStackLayout vStack(pixelroot32::ui::StackDirection::VERTICAL);
vStack.setSpacing(8);

auto startBtn = std::make_unique<UIButton>(0, 0, 120, 24, "Start");
auto optionsBtn = std::make_unique<UIButton>(0, 0, 120, 24, "Options");
auto quitBtn = std::make_unique<UIButton>(0, 0, 120, 24, "Quit");

vStack.addElement(startBtn.get());
vStack.addElement(optionsBtn.get());
vStack.addElement(quitBtn.get());

// Position the entire stack
vStack.setPosition(60, 80);

Use Cases: - Vertical menus - Horizontal toolbars - Form layouts

Styling

Colors

Use the engine's color palette:

using Color = pixelroot32::graphics::Color;

// Built-in colors
Color::Black, Color::White, Color::Red, Color::Green, Color::Blue
Color::Yellow, Color::Cyan, Color::Magenta
Color::DarkGray, Color::LightGray
Color::Orange, Color::Purple, Color::Brown

// Custom colors (RGB565)
Color customColor(0xF800);  // Pure red in RGB565

Fonts

UI elements use the engine's font system:

// Default: 5x7 bitmap font
// Scale 1 = 5x7 pixels
// Scale 2 = 10x14 pixels
// Scale 3 = 15x21 pixels

label->setFontScale(2);  // Double size text

Borders

Add borders to panels and buttons:

panel->setBorder(
    2,                              // border width (pixels)
    pixelroot32::graphics::Color::White  // border color
);

// Rounded corners (if supported)
button->setCornerRadius(4);  // 4 pixel radius

Input Handling

Button Navigation

For menu navigation with D-pad:

class MenuScene : public Scene {
    std::vector<UIButton*> menuButtons;
    int selectedIndex = 0;

public:
    void init() override {
        // Create buttons
        auto startBtn = std::make_unique<UIButton>(50, 60, 100, 20, "Start");
        auto optionsBtn = std::make_unique<UIButton>(50, 90, 100, 20, "Options");
        auto quitBtn = std::make_unique<UIButton>(50, 120, 100, 20, "Quit");

        menuButtons = {startBtn.get(), optionsBtn.get(), quitBtn.get()};

        // Add to scene
        addEntity(startBtn.get());
        addEntity(optionsBtn.get());
        addEntity(quitBtn.get());

        // Store ownership
        ownedButtons.push_back(std::move(startBtn));
        ownedButtons.push_back(std::move(optionsBtn));
        ownedButtons.push_back(std::move(quitBtn));

        // Highlight first button
        updateSelection();
    }

    void update(unsigned long deltaTime) override {
        auto& input = engine.getInputManager();

        // Navigate with UP/DOWN
        if (input.isButtonJustPressed(1)) {  // DOWN
            selectedIndex = (selectedIndex + 1) % menuButtons.size();
            updateSelection();
        }
        if (input.isButtonJustPressed(0)) {  // UP
            selectedIndex = (selectedIndex - 1 + menuButtons.size()) % menuButtons.size();
            updateSelection();
        }

        // Activate with A button
        if (input.isButtonJustPressed(4)) {  // A
            menuButtons[selectedIndex]->triggerClick();
        }
    }

    void updateSelection() {
        for (int i = 0; i < menuButtons.size(); i++) {
            if (i == selectedIndex) {
                menuButtons[i]->setBackgroundColor(Color::Yellow);
                menuButtons[i]->setTextColor(Color::Black);
            } else {
                menuButtons[i]->setBackgroundColor(Color::Blue);
                menuButtons[i]->setTextColor(Color::White);
            }
        }
    }

private:
    std::vector<std::unique_ptr<UIButton>> ownedButtons;
};

Direct Touch/Mouse (PC)

On PC with mouse:

// Check if mouse is over element (PC only)
bool isMouseOver = button->containsPoint(mouseX, mouseY);

// Button handles this automatically on PC
// On ESP32, use D-pad navigation as shown above

Common UI Patterns

1. Main Menu

class MainMenuScene : public Scene {
public:
    void init() override {
        // Title
        auto title = std::make_unique<UILabel>(60, 30, "MY GAME", Color::Yellow, 3);
        addEntity(title.get());
        ownedUI.push_back(std::move(title));

        // Menu buttons using stack layout
        pixelroot32::ui::UIStackLayout menuLayout(StackDirection::VERTICAL);
        menuLayout.setPosition(80, 80);
        menuLayout.setSpacing(12);

        auto startBtn = createButton("Start Game", [&]() {
            engine.setScene(&gameScene);
        });

        auto optionsBtn = createButton("Options", [&]() {
            engine.setScene(&optionsScene);
        });

        auto quitBtn = createButton("Quit", [&]() {
            // Handle quit
        });

        menuLayout.addElement(startBtn.get());
        menuLayout.addElement(optionsBtn.get());
        menuLayout.addElement(quitBtn.get());
    }

private:
    std::vector<std::unique_ptr<UIElement>> ownedUI;
};

2. In-Game HUD

class GameHUD : public Scene {
    UILabel* scoreLabel;
    UIProgressBar* healthBar;

public:
    void init() override {
        // Score (top-left)
        auto score = std::make_unique<UILabel>(5, 5, "Score: 0", Color::White, 1);
        scoreLabel = score.get();
        addEntity(score.get());

        // Health bar (top-right)
        auto health = std::make_unique<UIProgressBar>(180, 5, 55, 6);
        healthBar = health.get();
        healthBar->setRange(0, 100);
        healthBar->setValue(100);
        healthBar->setForegroundColor(Color::Red);
        addEntity(health.get());

        // Store ownership
        ownedUI.push_back(std::move(score));
        ownedUI.push_back(std::move(health));
    }

    void setScore(int score) {
        scoreLabel->setText("Score: " + std::to_string(score));
    }

    void setHealth(int health) {
        healthBar->setValue(health);
    }

private:
    std::vector<std::unique_ptr<UIElement>> ownedUI;
};

3. Dialog/Modal

class PauseDialog : public UIPanel {
public:
    PauseDialog() : UIPanel(40, 60, 160, 80) {
        setBackgroundColor(Color::DarkBlue);
        setBorder(2, Color::White);

        // Title
        auto title = std::make_unique<UILabel>(50, 70, "PAUSED", Color::Yellow, 2);
        addChild(title.get());

        // Resume button
        auto resumeBtn = std::make_unique<UIButton>(70, 100, 100, 20, "Resume");
        resumeBtn->onClick = [&]() {
            close();
        };
        addChild(resumeBtn.get());

        // Store children
        children.push_back(std::move(title));
        children.push_back(std::move(resumeBtn));
    }

    void close() {
        visible = false;
        // Or remove from scene
    }

private:
    std::vector<std::unique_ptr<UIElement>> children;
};

Performance Tips

1. Batch UI Updates

Don't update every frame unless necessary:

// ✅ GOOD: Update only when score changes
void onScoreChanged(int newScore) {
    scoreLabel->setText("Score: " + std::to_string(newScore));
}

// ❌ BAD: Updating every frame
void update(unsigned long deltaTime) override {
    scoreLabel->setText("Score: " + std::to_string(score));  // Unnecessary!
}

2. Minimize UI Elements

ESP32 has limited resources:

// Recommended limits for ESP32:
// - Total UI elements: < 20 per scene
// - Labels: Use font scale instead of many labels
// - Panels: Reuse single panel for multiple screens

3. Static vs Dynamic UI

// Static UI (created once)
void init() override {
    // Create all UI elements here
}

// Dynamic UI (created on demand)
void showInventory() {
    // Create inventory UI only when opened
    // Destroy when closed to free memory
}

Troubleshooting

UI Not Rendering

  1. Check render layer: UI should be on top layer (layer 2)

    uiElement->setRenderLayer(2);  // UI layer
    

  2. Verify position: Ensure element is within screen bounds

    // Screen bounds check
    if (x < 0 || x > LOGICAL_WIDTH) element->visible = false;
    

  3. Add to scene: Don't forget scene.addEntity()

Button Clicks Not Working

  1. Input focus: Ensure scene is active
  2. Button state: Check if button is enabled
    button->setEnabled(true);
    
  3. Callback binding: Verify lambda captures are valid

Text Not Displaying

  1. Font scale: Ensure scale is reasonable (1-3)
  2. Color contrast: Text color vs background
  3. String content: Check for empty strings

API Reference

Examples

  • Main Menu: Simple button navigation
  • In-Game HUD: Score, health, minimap
  • Options Screen: Sliders, checkboxes
  • Inventory: Grid layout, item selection

See also: