Guillaume is a component-based UI framework built around a 3D primitive rendering system. This guide explains the core concepts, lifecycle, and usage patterns.
3D Rendering Capabilities
Guillaume primitives are designed for 3D environments with full support for:
- 3D Positioning: All primitives use 3D coordinates (x, y, z)
- Surface Normals: Automatic calculation for lighting and back-face culling
- Geometric Properties: Area calculation, centroids, and spatial relationships
- 3D Text Rendering: Position, rotation, and scaling in 3D space
Core Concepts
1. Components and Hierarchy
Guillaume organizes UI elements in a tree structure where each component can contain both child components and rendering primitives:
auto root = std::make_shared<Container>();
auto header = std::make_shared<Container>();
auto titleLabel = std::make_shared<Label>("My Application");
header->addChild(titleLabel);
root->addChild(header);
2. Primitive-Based Rendering
Instead of rendering components directly, Guillaume uses a two-phase approach:
- Component Render Phase: Components generate primitives based on their current state
- Primitive Draw Phase: The renderer draws each primitive to the output
auto textPrimitive = std::make_shared<Text>(_text,
Point(0, 0, 0));
return shared_from_this();
}
std::vector< std::shared_ptr< Primitive > > _primitives
Vector of primitives for rendering.
Definition component.hpp:56
std::shared_ptr< Component > render(void) override
Renders the label.
Definition label.hpp:90
Represents a point in 3D space.
Definition point.hpp:35
3. State Management
Components manage their own state and can be updated dynamically:
auto label = std::make_shared<Label>("Initial Text");
label->setText("Updated Text");
app.update();
Application Lifecycle
1. Initialization
auto root = app.getRoot();
root->addChild(std::make_shared<Label>("Hello World"));
Entry point of the application.
Definition application.hpp:40
2. Initial Render
The run()
method:
- Calls
render()
on the root component
- Recursively renders all child components
- Traverses the component tree and draws all generated primitives
3. Updates and Re-rendering
label->setText("New Text");
app.update();
Event Handling
1. Event Dispatch
Events are dispatched to components and can trigger state changes:
auto button = std::make_shared<Button>("Click Me");
button->setOnClick([]() {
std::cout << "Button clicked!" << std::endl;
});
button->onEvent(
Event(
"click", button));
Represents a UI event with type, target, and optional data.
Definition event.hpp:38
2. Event-Driven Updates
Typical pattern for interactive components:
auto counter = 0;
auto countLabel = std::make_shared<Label>("Count: 0");
auto incrementButton = std::make_shared<Button>("Increment");
incrementButton->setOnClick([&]() {
counter++;
countLabel->setText("Count: " + std::to_string(counter));
app.update();
});
Custom Renderers
1. Implementing a Renderer
Guillaume provides two approaches for implementing custom renderers:
Option A: Override Specific Draw Methods (Recommended)
public:
void drawText(std::shared_ptr<Text> text)
override {
std::cout << "Text: " << text->getContent()
<< " at (" << text->getPosition().getX()
<< ", " << text->getPosition().getY() << ")" << std::endl;
}
void drawRectangle(std::shared_ptr<Rectangle> rectangle)
override {
std::cout << "Rectangle center: (" << rectangle->getCenter().getX()
<< ", " << rectangle->getCenter().getY()
<< ", " << rectangle->getCenter().getZ() << ") width: "
<< rectangle->getWidth() << " height: " << rectangle->getHeight()
<< " rotation: (" << rectangle->getRotation().getX() << ", "
<< rectangle->getRotation().getY() << ", " << rectangle->getRotation().getZ() << ")" << std::endl;
auto points = rectangle->getPoints();
for (size_t i = 0; i < points.size(); ++i) {
std::cout << " Corner " << i << ": (" << points[i].getX() << ", " << points[i].getY() << ", " << points[i].getZ() << ")" << std::endl;
}
}
void drawTriangle(std::shared_ptr<Triangle> triangle)
override {
auto points = triangle->getPoints();
std::cout << "Triangle with vertices: (" << points[0].getX()
<< ", " << points[0].getY() << "), (" << points[1].getX()
<< ", " << points[1].getY() << "), (" << points[2].getX()
<< ", " << points[2].getY() << ")" << std::endl;
}
void drawPolygon(std::shared_ptr<Polygon> polygon)
override {
std::cout << "Polygon with " << polygon->getPoints().size()
<< " vertices" << std::endl;
}
};
Represents the renderer of an Application.
Definition renderer.hpp:43
virtual void drawTriangle(std::shared_ptr< Triangle > triangle)
Draws a triangle primitive in 3D space.
Definition renderer.hpp:123
virtual void drawRectangle(std::shared_ptr< Rectangle > rectangle)
Draws a rectangle primitive in 3D space.
Definition renderer.hpp:108
virtual void drawText(std::shared_ptr< Text > text)
Draws a text primitive in 3D space.
Definition renderer.hpp:93
virtual void drawPolygon(std::shared_ptr< Polygon > polygon)
Draws a polygon primitive in 3D space.
Definition renderer.hpp:138
Option B: Override Generic Draw Method
public:
void draw(std::shared_ptr<Primitive> primitive)
override {
if (auto text = std::dynamic_pointer_cast<Text>(primitive)) {
} else if (auto rect = std::dynamic_pointer_cast<Rectangle>(primitive)) {
}
}
};
virtual void draw(std::shared_ptr< Primitive > primitive)
Draws a primitive to the screen.
2. Multiple Rendering Backends
Different renderers can output to different targets:
- Terminal Renderer: ASCII art and ANSI escape codes
- GUI Renderer: Native GUI framework calls
- Web Renderer: HTML/Canvas output
- Graphics Renderer: OpenGL/Vulkan calls
Each renderer can choose to:
- Override specific primitive methods for type safety
- Override the generic
draw
method for custom dispatch logic
- Mix both approaches as needed
Custom Components
1. Creating New Components
private:
float _progress = 0.0f;
public:
ProgressBar(
float progress) :
Component(), _progress(progress) {}
void setProgress(float progress) {
_progress = std::clamp(progress, 0.0f, 1.0f);
}
std::shared_ptr<Component>
render()
override {
_primitives.clear();
auto bg = std::make_shared<Rectangle>(
Point(100, 10, 0), 200, 20,
Point(0, 0, 0));
_primitives.push_back(bg);
auto fill = std::make_shared<Rectangle>(
Point(100 * _progress / 2, 10, 0), 200 * _progress, 20,
Point(0, 0, 0));
_primitives.push_back(fill);
auto text = std::make_shared<Text>(
std::to_string(int(_progress * 100)) + "%",
);
_primitives.push_back(text);
return shared_from_this();
}
};
Base class for all UI components.
Definition component.hpp:38
virtual std::shared_ptr< Component > render(void)
Renders the component.
Definition component.hpp:127
2. Custom Primitives
private:
float _radius;
public:
Circle(
Point center,
float radius) : _center(center), _radius(radius) {}
Point getCenter()
const {
return _center; }
float getRadius() const { return _radius; }
};
Base class for all drawing primitives.
Definition primitive.hpp:34
Best Practices
1. Component Design
- Single Responsibility: Each component should have a clear, focused purpose
- State Encapsulation: Keep component state private and provide controlled access methods
- Primitive Generation: Always clear and regenerate primitives in
render()
2. Renderer Design
- Specific Methods: Prefer overriding specific draw methods (
drawText
, drawRectangle
) over generic draw
- Type Safety: Specific methods provide compile-time type checking and eliminate manual casts
- Performance: Direct method calls are faster than dynamic type checking
3. Performance Considerations
- Selective Updates: Only call
update()
when necessary after state changes
- Primitive Caching: Consider caching primitives when content doesn't change frequently
- Event Optimization: Use efficient event handling patterns for high-frequency events
- Renderer Efficiency: Use specific draw methods to avoid runtime type checking overhead
4. Error Handling
- Null Checks: Always validate shared pointers before use
- State Validation: Validate input parameters in setter methods
- Graceful Degradation: Handle missing or invalid data gracefully
Example: Complete Mini-Application
#include "application.hpp"
#include "label.hpp"
#include "button.hpp"
#include "container.hpp"
class ConsoleRenderer :
public Renderer {
public:
void drawText(std::shared_ptr<Text> text)
override {
std::cout << "[TEXT] " << text->getContent() << std::endl;
}
void drawRectangle(std::shared_ptr<Rectangle> rectangle)
override {
std::cout << "[RECT] Background" << std::endl;
}
void drawTriangle(std::shared_ptr<Triangle> triangle)
override {
std::cout << "[TRIANGLE] " << triangle->getPoints().size() << " vertices" << std::endl;
}
void drawPolygon(std::shared_ptr<Polygon> polygon)
override {
std::cout << "[POLYGON] " << polygon->getPoints().size() << " vertices" << std::endl;
}
};
int main() {
auto root = app.getRoot();
auto titleLabel = std::make_shared<Label>("Counter App");
auto countLabel = std::make_shared<Label>("Count: 0");
auto button = std::make_shared<Button>("Increment");
root->addChild(titleLabel);
root->addChild(countLabel);
root->addChild(button);
auto rect3D = std::make_shared<Rectangle>(
Point(50, 50, 10), 40, 20,
Point(0, 0.5, 0));
root->addPrimitive(rect3D);
int counter = 0;
button->setOnClick([&]() {
counter++;
countLabel->setText("Count: " + std::to_string(counter));
app.update();
});
app.run();
for (int i = 0; i < 3; i++) {
button->onEvent(
Event(
"click", button));
}
return 0;
}
This example demonstrates the complete Guillaume workflow: component creation, hierarchy building, event handling, and the render/update cycle.