Skip to content

UI Guidelines - PixelRoot32

Patterns and best practices for building user interfaces with the UI system.

Architecture first: read UI system for UIManager, entity vs touch registration, and lifecycle. Authoritative API surface: include/graphics/ui/ and UI module.


Layout system

Choosing the right layout

LayoutUse caseNavigation
UIVerticalLayoutLists, menusUP/DOWN (via handleInput)
UIHorizontalLayoutToolbars, option rowsLEFT/RIGHT
UIGridLayoutInventories, galleriesFour-way
UIAnchorLayoutHUD elements, fixed positionsNone at layout level

Layouts take a viewport rectangle (x, y, width, height) and use addElement / removeElement, not addChild.

Vertical layout (lists)

cpp
using namespace pixelroot32;

void onStart() { /* ... */ }

auto* menu = new graphics::ui::UIVerticalLayout(
    math::toScalar(10), math::toScalar(20),
    200, 120);
menu->setSpacing(4);

auto* start = new graphics::ui::UIButton(
    "Start", 0,
    math::Vector2(math::toScalar(0), math::toScalar(0)),
    math::Vector2(math::toScalar(80), math::toScalar(24)),
    onStart);
menu->addElement(start);
// ... more buttons

// From scene update: drive D-pad / focus navigation
menu->handleInput(engine.getInputManager());

Horizontal layout (bars)

cpp
auto* toolbar = new graphics::ui::UIHorizontalLayout(
    math::toScalar(10), math::toScalar(200),
    240, 32);
toolbar->setSpacing(8);
// toolbar->addElement(... UIButton with full constructor ...)

Grid layout (inventories)

Grid cell size is computed from the layout’s width/height, setColumns, padding, and setSpacing. There is no setCellSize.

cpp
auto* inventory = new graphics::ui::UIGridLayout(
    math::toScalar(10), math::toScalar(50),
    200, 160);
inventory->setColumns(4);
inventory->setSpacing(4);

for (int i = 0; i < 12; ++i) {
    // new UIButton(...) with index i, callback, etc.
    // inventory->addElement(btn);
}

Anchor layout (HUDs)

addElement takes (UIElement*, Anchor) only. There are no extra (ox, oy) parameters; use a full-screen layout and setScreenSize, or adjust the layout’s position/size for margins.

cpp
constexpr int SW = 320;
constexpr int SH = 240;

auto* hud = new graphics::ui::UIAnchorLayout(
    math::toScalar(0), math::toScalar(0), SW, SH);
hud->setFixedPosition(true);
hud->setScreenSize(SW, SH);

auto* score = new graphics::ui::UILabel(
    "Score: 0",
    math::Vector2(math::toScalar(0), math::toScalar(0)),
    graphics::Color::White,
    1);
hud->addElement(score, graphics::ui::Anchor::TOP_LEFT);

Enum literals use Anchor::TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, etc.


Container patterns

Padding container

UIPaddingContainer wraps one child (setChild) and uses a normal bounding box (x, y, w, h).

cpp
auto* padded = new graphics::ui::UIPaddingContainer(
    math::toScalar(0), math::toScalar(0), 120, 40);
// padded->setPadding(...);
// padded->setChild(button);

Panel (visual containers)

UIPanel also wraps one child. Nest a UILayout for multiple rows.

cpp
auto* dialog = new graphics::ui::UIPanel(
    math::toScalar(50), math::toScalar(50), 140, 100);
dialog->setBackgroundColor(graphics::Color::Black);
dialog->setBorderColor(graphics::Color::White);

auto* layout = new graphics::ui::UIVerticalLayout(
    math::toScalar(0), math::toScalar(0), 130, 90);
// layout->addElement(...);
dialog->setChild(layout);

D-pad and handleInput

Scene::update does not call UILayout::handleInput for you. Call it from your scene when you want list/grid/toolbar navigation:

cpp
void MyScene::update(unsigned long dt) {
    if (mainMenu) {
        mainMenu->handleInput(engine.getInputManager());
    }
    Scene::update(dt);
}

Selection helpers

cpp
layout->setSelectedIndex(2);
int idx = layout->getSelectedIndex();
graphics::ui::UIElement* sel = layout->getSelectedElement();  // not getSelectedChild

ESP32 performance

Viewport culling

UIVerticalLayout and UIGridLayout skip drawing children outside the visible viewport when scrolling is relevant. Still prefer fewer nodes and less frequent text changes.

Scrolling

Use setScrollEnabled / setScrollingEnabled (see UIVerticalLayout and UILayout) and setScrollOffset where applicable. There is no UIScrollMode enum in the current API.


Sprite and graphics

Sprite descriptors

Wrap bitmaps in descriptors before rendering:

cpp
pixelroot32::graphics::Sprite playerSprite = {
    .width = 16,
    .height = 16,
    .data = playerBitmap
};

renderer.drawSprite(playerSprite, x, y, paletteSlot, flipX);

Multi-sprite (layered)

Compose multi-color sprites from 1bpp layers:

cpp
pixelroot32::graphics::MultiSprite ship = {
    .layers = {
        {shipOutline, PaletteType::PR32},
        {shipCockpit, PaletteType::NES},
        {shipEngine, PaletteType::GB}
    },
    .layerCount = 3
};

renderer.drawMultiSprite(ship, x, y);

Keep layer data static const for flash storage.

Bit depth guidelines

FormatUse forMemory cost
1bppGame sprites, tiles1x
2bppLogos, detailed UI2x
4bppPhotos, rich graphics4x

Default to 1bpp for gameplay-critical sprites.


What actors should not do

cpp
// Wrong: per-pixel loops in actors
void MyActor::draw(Renderer& r) {
    for (int y = 0; y < height; ++y) {
        for (int x = 0; x < width; ++x) {
            if (bitmap[y] & (1 << x)) {
                r.drawPixel(x, y, color);
            }
        }
    }
}

// Correct: batch drawing via renderer
void MyActor::draw(Renderer& r) {
    r.drawSprite(mySprite, position.x, position.y);
}

Touch vs classic widgets

  • UITouchButton / UITouchCheckbox / UITouchSlider: register with UIManager::addElement and implement Scene::processTouchEvents. Also addEntity so widgets draw.
  • UIButton / UICheckBox: use handleInput and focus/selection; they do not use the touch event dispatcher for hit testing.

See architecture/touch-input.md.


DocumentTopic
graphics-guidelines.mdSprites, tilemaps, palettes
coding-style.mdC++ conventions
performance/esp32-performance.mdOptimization
api/ui.mdUI API reference

UIs should be responsive, efficient, and consistent with the real UIElement / UITouchElement split.

Released under the MIT License.