Repository navigation
UI System
SparkEngine provides a runtime UI framework for in-game HUD elements, menus, and overlays. Unlike the editor's Dear ImGui panels, this system is designed for shipped game UI with layout, anchoring, and input handling.
Source: SparkEngine/Source/Engine/UI/UISystem.h
Namespace: Spark::UI
| Class | Responsibility |
|---|---|
UISystem |
Top-level manager, owns the canvas, global visibility |
UICanvas |
Root container (one per viewport), coordinate mapping, panel management |
UIWidget |
Base class for all widgets (position, size, anchor, visibility, opacity) |
UIPanel |
Container with layout, holds child widgets, background color |
UILabel |
Text display with font size and color |
UIButton |
Clickable button with hover/pressed/normal color states |
UIProgressBar |
Fill bar (health, loading, XP) with fill and background colors |
UIImageWidget |
Texture display (crosshairs, icons) with tint color |
+-------------------------------------------------------------------+
| UISystem |
| m_canvas : UICanvas |
| m_visible : bool |
| |
| Initialize(width, height) |
| Update(deltaTime) ──> m_canvas.Update(deltaTime) |
| Render() ──> m_canvas.Render() |
| HandleClick(x, y) ──> m_canvas.HandleClick(x, y) |
| OnResize(w, h) ──> m_canvas.Resize(w, h) |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| UICanvas |
| m_panels : vector<unique_ptr<UIPanel>> |
| m_width, m_height : int |
| |
| CreatePanel(name) ──> returns UIPanel* |
| FindWidget(name) ──> recursive search across all panels |
| RemovePanel(name) |
| Update / Render / HandleClick ──> delegates to each panel |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| UIPanel (extends UIWidget) |
| m_children : vector<unique_ptr<UIWidget>> |
| m_layout : LayoutDirection (None / Horizontal / Vertical) |
| m_spacing, m_padding : float |
| m_bgColor : UIColor |
| |
| CreateLabel / CreateButton / CreateProgressBar / CreateImage |
| CreatePanel (nested panels) |
| FindWidget(name) ──> recursive |
| RemoveWidget(name) |
+-------------------------------------------------------------------+
|
+-----------+-----------+-----------+
| | | |
UILabel UIButton UIProgressBar UIImageWidget
UICanvas (root, one per viewport)
+-- UIPanel "HUD"
| +-- UILabel "Health: 100"
| +-- UIProgressBar (health bar)
| +-- UIImageWidget (crosshair)
+-- UIPanel "PauseMenu"
| +-- UILabel "PAUSED"
| +-- UIButton "Resume"
| +-- UIButton "Quit"
+-- UIPanel "DialogueBox"
+-- UILabel "speaker_name"
+-- UILabel "dialogue_text"
+-- UIPanel "ChoicesPanel"
+-- UIButton "choice_0"
+-- UIButton "choice_1"
+-- UIButton "choice_2"
enum class Anchor
{
TopLeft, // Anchored to top-left corner
TopCenter, // Anchored to top-center
TopRight, // Anchored to top-right corner
MiddleLeft, // Anchored to middle-left
Center, // Anchored to screen center
MiddleRight, // Anchored to middle-right
BottomLeft, // Anchored to bottom-left
BottomCenter, // Anchored to bottom-center
BottomRight, // Anchored to bottom-right
Stretch // Fill the entire parent area
};enum class LayoutDirection
{
None, // Manual positioning (SetPosition per widget)
Horizontal, // Left-to-right automatic arrangement
Vertical // Top-to-bottom automatic arrangement
};struct UIColor
{
float r = 1.0f, g = 1.0f, b = 1.0f, a = 1.0f;
static UIColor White();
static UIColor Black();
static UIColor Red();
static UIColor Green();
static UIColor Blue();
static UIColor Yellow();
static UIColor Transparent();
};| Factory Method | RGBA Value |
|---|---|
UIColor::White() |
{1, 1, 1, 1} |
UIColor::Black() |
{0, 0, 0, 1} |
UIColor::Red() |
{1, 0, 0, 1} |
UIColor::Green() |
{0, 1, 0, 1} |
UIColor::Blue() |
{0, 0, 1, 1} |
UIColor::Yellow() |
{1, 1, 0, 1} |
UIColor::Transparent() |
{0, 0, 0, 0} |
Custom colors use aggregate initialization: UIColor{0.2f, 0.5f, 0.8f, 0.9f}.
All widgets inherit from UIWidget. These methods are available on every widget type.
| Method | Signature | Description |
|---|---|---|
| Constructor | UIWidget(const std::string& name) |
Create widget with a unique name |
GetName |
const std::string& GetName() const |
Get the widget name |
SetPosition |
void SetPosition(float x, float y) |
Set position relative to parent |
SetSize |
void SetSize(float width, float height) |
Set widget dimensions |
SetAnchor |
void SetAnchor(Anchor anchor) |
Set anchor point |
SetVisible |
void SetVisible(bool visible) |
Show or hide the widget |
IsVisible |
bool IsVisible() const |
Check visibility |
SetOpacity |
void SetOpacity(float opacity) |
Set opacity (0.0-1.0) |
GetX / GetY
|
float GetX() const |
Get position components |
GetWidth / GetHeight
|
float GetWidth() const |
Get size components |
Update |
virtual void Update(float deltaTime) |
Per-frame update (override) |
Render |
virtual void Render() const |
Draw the widget (override) |
HandleClick |
virtual bool HandleClick(float x, float y) |
Handle click; returns true if consumed |
Default member values: position (0, 0), size (100, 30), anchor TopLeft, visible true, opacity 1.0.
| Method | Signature | Description |
|---|---|---|
| Constructor | UILabel(const std::string& name, const std::string& text) |
Create label with text |
SetText |
void SetText(const std::string& text) |
Update displayed text |
GetText |
const std::string& GetText() const |
Get current text |
SetFontSize |
void SetFontSize(int size) |
Set font size (default: 16) |
SetColor |
void SetColor(const UIColor& color) |
Set text color |
| Method | Signature | Description |
|---|---|---|
| Constructor | UIButton(const std::string& name, const std::string& label) |
Create button with label |
SetLabel |
void SetLabel(const std::string& label) |
Update button text |
GetLabel |
const std::string& GetLabel() const |
Get button text |
OnClick |
void OnClick(std::function<void()> callback) |
Set click handler |
SetNormalColor |
void SetNormalColor(const UIColor& color) |
Default state color |
SetHoverColor |
void SetHoverColor(const UIColor& color) |
Mouse-over color |
SetPressedColor |
void SetPressedColor(const UIColor& color) |
Click-down color |
Default colors: normal {0.3, 0.3, 0.3, 0.9}, hover {0.4, 0.4, 0.4, 0.9}, pressed {0.2, 0.2, 0.2, 0.9}.
| Method | Signature | Description |
|---|---|---|
| Constructor | UIProgressBar(const std::string& name) |
Create progress bar |
SetValue |
void SetValue(float value) |
Set fill (clamped 0.0-1.0) |
GetValue |
float GetValue() const |
Get current fill level |
SetFillColor |
void SetFillColor(const UIColor& color) |
Set fill portion color |
SetBackgroundColor |
void SetBackgroundColor(const UIColor& color) |
Set empty portion color |
Default: value 1.0, fill color Green(), background {0.2, 0.2, 0.2, 0.8}.
| Method | Signature | Description |
|---|---|---|
| Constructor | UIImageWidget(const std::string& name, const std::string& texturePath = "") |
Create image widget |
SetTexturePath |
void SetTexturePath(const std::string& path) |
Set texture file path |
GetTexturePath |
const std::string& GetTexturePath() const |
Get texture path |
SetTint |
void SetTint(const UIColor& color) |
Set color tint |
| Method | Signature | Description |
|---|---|---|
| Constructor | UIPanel(const std::string& name) |
Create panel container |
SetLayout |
void SetLayout(LayoutDirection layout) |
Set child layout mode |
SetSpacing |
void SetSpacing(float spacing) |
Space between children (default: 5) |
SetPadding |
void SetPadding(float padding) |
Internal padding (default: 10) |
SetBackgroundColor |
void SetBackgroundColor(const UIColor& color) |
Panel background |
CreateLabel |
UILabel* CreateLabel(const std::string& name, const std::string& text) |
Add a label child |
CreateButton |
UIButton* CreateButton(const std::string& name, const std::string& label) |
Add a button child |
CreateProgressBar |
UIProgressBar* CreateProgressBar(const std::string& name) |
Add a progress bar child |
CreateImage |
UIImageWidget* CreateImage(const std::string& name, const std::string& texturePath = "") |
Add an image child |
CreatePanel |
UIPanel* CreatePanel(const std::string& name) |
Add a nested panel child |
FindWidget |
UIWidget* FindWidget(const std::string& name) |
Recursive search by name |
RemoveWidget |
void RemoveWidget(const std::string& name) |
Remove child by name |
| Method | Signature | Description |
|---|---|---|
Initialize |
void Initialize(int width, int height) |
Set screen resolution |
CreatePanel |
UIPanel* CreatePanel(const std::string& name) |
Create top-level panel |
FindWidget |
UIWidget* FindWidget(const std::string& name) |
Search all panels |
RemovePanel |
void RemovePanel(const std::string& name) |
Remove a panel by name |
Update |
void Update(float deltaTime) |
Update all panels |
Render |
void Render() const |
Render all visible panels |
HandleClick |
bool HandleClick(float x, float y) |
Process click event |
GetWidth / GetHeight
|
int GetWidth() const |
Get canvas dimensions |
Resize |
void Resize(int width, int height) |
Handle resolution change |
| Method | Signature | Description |
|---|---|---|
Initialize |
void Initialize(int screenWidth, int screenHeight) |
Initialize the UI system |
GetCanvas |
UICanvas& GetCanvas() |
Get the root canvas (mutable) |
GetCanvas |
const UICanvas& GetCanvas() const |
Get the root canvas (const) |
Update |
void Update(float deltaTime) |
Update all UI elements |
Render |
void Render() |
Render all UI elements |
OnResize |
void OnResize(int width, int height) |
Handle window resize |
HandleClick |
bool HandleClick(float x, float y) |
Process click; returns true if consumed |
SetVisible |
void SetVisible(bool visible) |
Show/hide all UI |
IsVisible |
bool IsVisible() const |
Check global visibility |
Console_GetStatus |
std::string Console_GetStatus() const |
Console status string |
UISystem ui;
ui.Initialize(1920, 1080);
auto* hud = ui.GetCanvas().CreatePanel("HUD");
hud->SetAnchor(Anchor::TopLeft);
auto* healthLabel = hud->CreateLabel("health_text", "Health: 100");
healthLabel->SetPosition(20, 20);
healthLabel->SetFontSize(24);
auto* healthBar = hud->CreateProgressBar("health_bar");
healthBar->SetPosition(20, 50);
healthBar->SetSize(200, 20);
healthBar->SetValue(1.0f);
auto* btn = hud->CreateButton("resume", "Resume");
btn->OnClick([](){ ResumeGame(); });
// Per frame:
ui.Update(deltaTime);
ui.Render();Widgets can be anchored to screen positions, and panels support automatic child layout:
panel->SetLayout(LayoutDirection::Vertical);
panel->SetSpacing(10.0f);
panel->SetPadding(15.0f);When a panel uses LayoutDirection::Vertical, children are arranged top-to-bottom starting at (padding, padding), with spacing pixels between each child. The children's SetPosition calls are ignored in auto-layout mode. Similarly, LayoutDirection::Horizontal arranges children left-to-right.
When layout is None, children use their manually set positions relative to the panel's top-left corner plus padding.
+------TopLeft----TopCenter----TopRight------+
| |
MiddleLeft Center MiddleRight |
| |
+---BottomLeft--BottomCenter--BottomRight-----+
The Stretch anchor causes the widget to fill its parent's entire area, ignoring position and size settings.
A complete FPS HUD example:
UISystem ui;
ui.Initialize(1920, 1080);
auto& canvas = ui.GetCanvas();
// Health panel (bottom-left)
auto* healthPanel = canvas.CreatePanel("HealthPanel");
healthPanel->SetAnchor(Anchor::BottomLeft);
healthPanel->SetLayout(LayoutDirection::Vertical);
healthPanel->SetSpacing(5.0f);
healthPanel->SetPadding(20.0f);
healthPanel->SetBackgroundColor({0.0f, 0.0f, 0.0f, 0.5f});
auto* healthLabel = healthPanel->CreateLabel("hp_label", "Health");
healthLabel->SetFontSize(16);
healthLabel->SetColor(UIColor::White());
auto* healthBar = healthPanel->CreateProgressBar("hp_bar");
healthBar->SetSize(200, 20);
healthBar->SetValue(1.0f);
healthBar->SetFillColor(UIColor::Green());
healthBar->SetBackgroundColor({0.2f, 0.2f, 0.2f, 0.8f});
// Crosshair (center)
auto* crosshair = canvas.CreatePanel("CrosshairPanel");
crosshair->SetAnchor(Anchor::Center);
auto* crosshairImg = crosshair->CreateImage("crosshair_img");
crosshairImg->SetTexturePath("Assets/UI/crosshair.png");
crosshairImg->SetSize(32, 32);
crosshairImg->SetTint(UIColor::White());
// Ammo counter (bottom-right)
auto* ammoPanel = canvas.CreatePanel("AmmoPanel");
ammoPanel->SetAnchor(Anchor::BottomRight);
auto* ammoLabel = ammoPanel->CreateLabel("ammo_text", "30 / 120");
ammoLabel->SetFontSize(28);
ammoLabel->SetColor(UIColor::White());
ammoLabel->SetPosition(20, 20);
// Update health bar each frame
healthBar->SetValue(player.health / player.maxHealth);
ammoLabel->SetText(std::to_string(weapon.ammo) + " / " + std::to_string(weapon.reserveAmmo));auto* pausePanel = canvas.CreatePanel("PauseMenu");
pausePanel->SetAnchor(Anchor::Center);
pausePanel->SetLayout(LayoutDirection::Vertical);
pausePanel->SetSpacing(15.0f);
pausePanel->SetPadding(30.0f);
pausePanel->SetBackgroundColor({0.1f, 0.1f, 0.1f, 0.9f});
pausePanel->SetVisible(false); // Hidden by default
auto* title = pausePanel->CreateLabel("pause_title", "PAUSED");
title->SetFontSize(36);
title->SetColor(UIColor::White());
auto* resumeBtn = pausePanel->CreateButton("resume_btn", "Resume");
resumeBtn->SetNormalColor({0.3f, 0.3f, 0.3f, 0.9f});
resumeBtn->SetHoverColor({0.4f, 0.5f, 0.4f, 0.9f});
resumeBtn->SetPressedColor({0.2f, 0.4f, 0.2f, 0.9f});
resumeBtn->OnClick([&]() {
pausePanel->SetVisible(false);
SetGamePaused(false);
});
auto* quitBtn = pausePanel->CreateButton("quit_btn", "Quit to Menu");
quitBtn->SetNormalColor({0.3f, 0.3f, 0.3f, 0.9f});
quitBtn->SetHoverColor({0.5f, 0.3f, 0.3f, 0.9f});
quitBtn->OnClick([&]() { LoadScene("MainMenu"); });
// Toggle pause with Escape
if (input.WasKeyPressed(VK_ESCAPE))
{
bool paused = !pausePanel->IsVisible();
pausePanel->SetVisible(paused);
SetGamePaused(paused);
}The UI system consumes click events before they reach gameplay. Call HandleClick before processing gameplay input:
// In your input processing loop:
if (ui.HandleClick(mouseX, mouseY))
{
// Click was consumed by a UI button -- do not process in gameplay
return;
}
// Otherwise, process as gameplay input (e.g., fire weapon)The click propagates through the canvas in reverse panel order (last-created panels are checked first, acting as a z-order). Within a panel, children are checked in reverse order. The first widget whose bounding box contains the click coordinates and whose HandleClick returns true consumes the event.
// Handle window resize events
ui.OnResize(newWidth, newHeight);This delegates to UICanvas::Resize(), which updates the canvas dimensions. Anchored widgets are repositioned based on their anchor points and the new resolution.
-
UISystemowns a singleUICanvasby value. -
UICanvasowns top-levelUIPanelinstances viastd::vector<std::unique_ptr<UIPanel>>. - Each
UIPanelowns its children viastd::vector<std::unique_ptr<UIWidget>>. - Pointers returned by
Create*methods are non-owning. The canvas/panel retains ownership. - Removing a widget (
RemoveWidget/RemovePanel) destroys the widget and invalidates any raw pointers to it.
Panels are rendered in creation order (first-created panel renders first, appearing behind later panels). Within a panel, children render in creation order. This means the last-created widget appears on top.
Each frame follows this sequence:
-
UISystem::Update(deltaTime)callsUICanvas::Update(deltaTime). -
UICanvas::Updateiterates all panels, callingUIPanel::Update(deltaTime). - Each panel applies layout calculations (if layout is not
None), then callsUpdate(deltaTime)on each visible child. -
UISystem::Render()callsUICanvas::Render(). - Each visible panel renders its background, then renders each visible child.
| Scenario | Behavior |
|---|---|
CreatePanel with duplicate name |
Creates a new panel; previous panel remains (names are not unique keys) |
FindWidget with unknown name |
Returns nullptr
|
RemoveWidget with unknown name |
No effect (silent) |
HandleClick outside all widgets |
Returns false
|
SetValue on progress bar outside 0-1 |
Clamped to [0.0, 1.0] via std::clamp
|
OnClick with null callback |
Safe; button click does nothing |
Render before Initialize
|
Renders nothing (canvas has default 1920x1080 dimensions) |
- The widget tree is walked linearly each frame for both
UpdateandRender. With typical game UIs (under 100 widgets), this is negligible. -
FindWidgetperforms a depth-first recursive search. For frequent lookups, cache the returned pointer instead of callingFindWidgetevery frame. - Hidden widgets (
SetVisible(false)) are skipped during bothUpdateandRender, so hiding panels (e.g., pause menu) has zero per-frame cost. - Text rendering is the most expensive widget operation. Avoid changing
SetTextevery frame unless the text actually changed.
The UI system is not thread-safe. All UI operations must occur on the main thread, including:
- Creating, removing, and modifying widgets
- Calling
Update,Render, andHandleClick - Registering click callbacks
Button OnClick callbacks execute synchronously on the main thread during HandleClick.
| Problem | Cause | Solution |
|---|---|---|
| Widget not visible | Panel or widget SetVisible(false)
|
Check visibility of entire parent chain |
| Button click not registering | UI HandleClick not called before gameplay input |
Call ui.HandleClick() first in input loop |
| Widget in wrong position | Wrong anchor or parent panel | Verify anchor; check if panel uses auto-layout |
| Progress bar appears empty |
SetValue(0.0f) or value not updated |
Ensure per-frame value update |
| Text overlapping | Manual positions conflict | Use auto-layout (SetLayout(Vertical)) |
| Widgets not resizing with window |
OnResize not called |
Wire window resize events to ui.OnResize()
|
| Click goes through UI to gameplay |
HandleClick return value not checked |
Only process gameplay input if HandleClick returns false
|
- Localization -- Localized text for UI labels
- Loading System -- Loading screen progress bars
- Input System -- Mouse and keyboard input
- Dialogue System -- Dialogue UI integration
- SparkEditor -- Editor UI (Dear ImGui, separate from runtime UI)
Published from 2b03dc797148. Edit the canonical source in wiki/.
- Documentation
- Docs route
- Wiki index
- Guides
- Tutorials
- Samples
- Examples
- API Reference
- API route
- Reference
- Build Guide
- Dependencies
- FAQ
- Changelog
- Roadmap
- Contributing
- Code of Conduct
- Home
- FAQ
- Getting Started
- Quick-Start Tutorial
- Making Your First Game
- Making Your First Multiplayer Game
- Artist Workflow Guide
- Editor Walkthrough
- Migration Guide
- How SparkEngine Works
- Architecture Overview
- Engine Architecture Flowchart
- Creating a Game Module
- Game Modules (catalog)
- Entity Component System
- Rendering and Graphics
- Physics
- Cloth Simulation
- Audio
- Input System
- Camera System
- Scripting with AngelScript
- Visual Scripting
- AI and Navigation
- Animation
- 2D Systems
- Networking
- Dedicated Server
- Multiplayer Quick Start
- Area Server Architecture
- Scene Management
- Large World Support
- Collaborative Editing
- Coroutine System
- Event System
- Event Response System
- Job System
- UI System
- UI Layout Extensions
- Localization
- Dialogue System
- Destruction System
- Replay System
- Achievement System
- Loading System
- Mod System
- Content Delivery
- Tween System
- Memory Integrity
- Gameplay Systems
- Terrain and Procedural Generation
- Save System
- Persistence System
- Day Night Cycle and Weather
- Cinematic Sequencer
- Runtime Prefabs
- SparkEditor
- Editor Tutorials
- SparkConsole
- SparkDaemon
- Shader Pipeline
- Asset Pipeline
- Asset Validation
- Asset Migration
- Game Packaging
- Online Services
- DataTable System
- Loot and Crafting System
- CSG System
- Font System
- Timer Manager
- Movie Render Pipeline
- HLOD and World Partition
- Remote Debug System
- Selection Manager
- Asset Dependency Graph
- Editor Automation
- File Watcher
- Project Templates
- System Requirements
- VR Support
- Mobile Platform
- Accessibility
- Platform Input
- Platform Certification
- Cross-Compilation: Wine Testing
- RHI Abstraction Layer
- D3D11 Backend
- D3D12 Backend
- Vulkan Backend
- OpenGL Backend
- Metal Backend
- DXR Raytracing
- Hybrid Ray Tracing
- Upscaling (DLSS/FSR)
- Render Graph
- Shader Graph
- GPU Particles
- GPU-Driven Rendering
- Volumetric Fog
- Volumetric Clouds
- Global Illumination
- Virtual Texturing
- Water Rendering
- Clustered Lighting
- Material System
- Post-Processing
- Shadow System
- Particle System
- Decal System
- Sky and Atmosphere
- Foliage System
- Mesh Shaders
- Neural Rendering
- Configuration Reference
- Performance Tips
- Benchmark Framework
- Threading Model
- Fuzz Policy and Parser Security
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Crash Reporting
- Golden Image Testing
- Utilities
- Testing
- Fuzz Policy and Parser Security
- Codebase Statistics
- Codebase Health
- Error Handling Patterns
- Hot Reload Overview
- Troubleshooting
- Contributing
- Workflow Patterns
- Build Optimizations
- CI Reproducible Builds
- GitHub API and PR Checks
- Git Rebase Conflicts
- Clang-Format
- Code Quality Violations
- AI Bloat Pattern
- MinGW + Wine Cross-Compilation
- Live Editor Testing
- Engine & Renderer Landscape
- DuetOS Portability Catalog
- Five-Engine Analysis
- Eleven-Engine Analysis
- ThorVG / Unity Graphics Analysis
- Advanced Techniques Catalog
- Third-Party Library Evaluation
- Engine Viability Evaluation
- Engine Feature Recommendations
- Project Recommendations
- Mac Compatibility Analysis
- Codebase Observations
- Codebase Bloat Audit
- Test Suite Audit
- Documentation Coverage Audit
- ThirdParty Dependencies Audit
- Load Test Baseline
- Gameplay Systems Status
- SparkGame Module Status
- Stub and Abandoned Features
- Memory Integrity System
- Memory Safety Evaluation
- Hardware Acceleration Systems
- Jolt Physics Integration
- GPU/CPU Separation Plan
- Daemon Services Architecture
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers