Prong uses a relative coordinate system with automatic caching for optimal performance and developer ergonomics. Child components are positioned relative to their parent's origin, and global screen coordinates are computed on-demand with intelligent caching.
This document provides a comprehensive explanation of the coordinate system architecture, APIs, and best practices.
The coordinate system was designed with these goals in mind:
- Intuitive Developer Experience - Developers should work with local coordinates by default, not worrying about absolute screen positions
- Performance - Global coordinates should be cached to avoid redundant calculations
- Automatic Propagation - Moving a parent should automatically move all children without manual updates
- Layout Manager Simplicity - Layout managers should work entirely in local space
- Clean Separation - Clear distinction between local (positioning) and global (rendering/hit testing) coordinate spaces
Prong distinguishes between two coordinate spaces:
Definition: Position relative to the parent component's origin.
- Stored in the
localXandlocalYmember variables inComponent - Set using
setPosition(x, y)orsetBounds(x, y, w, h) - Retrieved using
getPosition(x, y)orgetBounds(x, y, w, h) - Used for: positioning children, layout calculations, parent-child relationships
Special case: For root components (components with no parent), local coordinates ARE the global coordinates.
// Create a parent panel at screen position (100, 200)
auto panel = std::make_unique<Panel>(renderer);
panel->setPosition(100, 200);
panel->setSize(400, 300);
// Create a child button at position (50, 75) relative to panel
auto button = std::make_unique<Button>(renderer, "Click");
button->setPosition(50, 75); // Local: (50, 75)
button->setSize(120, 40);
// Add button to panel
panel->addChild(std::move(button));
// The button's global position is now (150, 275) = (100, 200) + (50, 75)
// But the button doesn't need to know or care about thisDefinition: Absolute screen-space position.
- Calculated by walking up the parent chain and summing all local positions
- Cached in
cachedGlobalXandcachedGlobalYmember variables - Retrieved using
getGlobalPosition(x, y)orgetGlobalBounds(x, y, w, h) - Used for: rendering to screen, hit testing, event dispatch
For any component, its global position is:
globalX = localX + parent.globalX
globalY = localY + parent.globalY
This is computed recursively up to the root component.
// Component hierarchy:
// Root at local (100, 100)
// └─ Child1 at local (10, 10)
// └─ Child2 at local (5, 5)
root.setPosition(100, 100);
child1->setPosition(10, 10);
child2->setPosition(5, 5);
// Global positions:
int gx, gy;
root.getGlobalPosition(gx, gy); // (100, 100) - root has no parent
child1->getGlobalPosition(gx, gy); // (110, 110) = 100 + 10
child2->getGlobalPosition(gx, gy); // (115, 115) = 110 + 5Without caching, every call to getGlobalPosition() would require traversing the entire parent chain. In a deep component tree rendered at 60 FPS, this becomes expensive quickly.
- Lazy Calculation: Global coordinates are only calculated when first requested
- Dirty Flag: Each component has a
globalCacheDirtyboolean flag - Cache Invalidation: When a component's position changes, the cache is marked dirty
- Automatic Cascade: Cache invalidation automatically propagates to all descendants
The cache is automatically invalidated when:
setPosition()is calledsetBounds()is called (which sets position)- A component is added as a child to a new parent via
addChild()
When getGlobalPosition() is called on a component with a dirty cache:
void updateGlobalCache() const {
if (parent) {
int parentGlobalX, parentGlobalY;
parent->getGlobalPosition(parentGlobalX, parentGlobalY);
cachedGlobalX = parentGlobalX + localX;
cachedGlobalY = parentGlobalY + localY;
} else {
// Root component: local coordinates are global
cachedGlobalX = localX;
cachedGlobalY = localY;
}
globalCacheDirty = false;
}Note that this may recursively update parent caches if they are also dirty.
- Cache hit: O(1) - just return cached values
- Cache miss (single component): O(1) - calculate from parent's cached value
- Cache miss (entire tree dirty): O(depth) - walk up to root once
- Moving a parent: O(children) - invalidate all descendants
// Set local position only
void setPosition(int x, int y);
// Set local position and size
void setBounds(int x, int y, int width, int height);
// Set size only
void setSize(int width, int height);All of these methods work with local coordinates relative to the parent.
// Get local position
void getPosition(int& outX, int& outY) const;
// Get local bounds (position + size)
void getBounds(int& outX, int& outY, int& outWidth, int& outHeight) const;
// Get size
void getSize(int& outWidth, int& outHeight) const;// Get global (screen-space) position
void getGlobalPosition(int& outX, int& outY) const;
// Get global bounds (position + size)
void getGlobalBounds(int& outX, int& outY, int& outWidth, int& outHeight) const;These methods automatically update the cache if needed.
// Convert local to global
void localToGlobal(int localX, int localY, int& globalX, int& globalY) const;
// Convert global to local
void globalToLocal(int globalX, int globalY, int& localX, int& localY) const;// Check if a global screen point is within this component
bool containsGlobal(int globalX, int globalY) const;This is the primary method used by Component::handleEvent() for hit testing.
For use in render() implementations:
protected:
int getGlobalX() const; // Get global X coordinate
int getGlobalY() const; // Get global Y coordinateThese are convenience methods that call getGlobalPosition() internally.
Always use local coordinates when positioning children:
class MyPanel : public Component {
public:
MyPanel(rendering::IRenderer* renderer) : Component(renderer) {
// Position children relative to this panel's origin
auto button1 = std::make_unique<Button>(renderer, "Button 1");
button1->setPosition(10, 10); // 10px from panel's top-left
addChild(std::move(button1));
auto button2 = std::make_unique<Button>(renderer, "Button 2");
button2->setPosition(10, 60); // 10px from left, 60px from top
addChild(std::move(button2));
}
};Always use global coordinates when rendering:
void MyComponent::render() override {
// Get global screen position for rendering
int x = getGlobalX();
int y = getGlobalY();
// Render at absolute screen position
renderer->drawRect(x, y, width, height, 1.0f, 0.0f, 0.0f, 1.0f);
// For text or other elements within the component,
// add offsets to the global position
renderer->drawText(x + 10, y + 20, "Hello", 1.0f, 1.0f, 1.0f, 1.0f);
}Layout managers work entirely in local coordinates:
template<typename DerivedT>
class MyLayout : public LayoutManager<DerivedT> {
public:
void layout(std::vector<Component*>& children, const Dimensions& available) {
int currentX = 0;
int currentY = 0;
for (auto* child : children) {
// Position children using local coordinates
// These are relative to the parent component
child->setBounds(currentX, currentY, 100, 50);
currentX += 110; // 100px width + 10px gap
}
}
};Event handlers receive local coordinates:
bool MyComponent::handleClick(int localX, int localY) override {
// localX and localY are relative to this component
// (0, 0) is at the top-left corner of this component
// Check if click is within a sub-region
if (localX >= 10 && localX < 110 && localY >= 10 && localY < 50) {
std::cout << "Clicked the button area!" << std::endl;
return true;
}
return false;
}The Component::handleEvent() method automatically converts global screen coordinates to local coordinates during event propagation to children.
Sometimes you need to convert between spaces:
void MyComponent::someMethod() {
// Convert a local point to global (for tooltip positioning, etc.)
int tooltipLocalX = 50;
int tooltipLocalY = 20;
int tooltipGlobalX, tooltipGlobalY;
localToGlobal(tooltipLocalX, tooltipLocalY, tooltipGlobalX, tooltipGlobalY);
// Now you can position a tooltip at the global screen position
// Convert a global point to local (for custom hit testing)
int mouseGlobalX = 200;
int mouseGlobalY = 150;
int mouseLocalX, mouseLocalY;
globalToLocal(mouseGlobalX, mouseGlobalY, mouseLocalX, mouseLocalY);
// Now you can check if mouseLocal is within a specific region
}The hierarchical event system uses global coordinates at entry:
- Hit Testing:
Component::handleEvent()usescontainsEvent()to check if events are within bounds - Coordinate Conversion: Automatically converts global screen coordinates to local during propagation to children
- Z-Order: Tests components in reverse rendering order (topmost first)
Layout managers are completely isolated from global coordinates:
- They receive a
Dimensionsstruct with available width/height - They position children using
setBounds()with local coordinates - They never need to call
getGlobalPosition()
The renderer works in screen-space (global coordinates):
- All
IRendererdrawing methods expect absolute screen coordinates - Components must use
getGlobalX()andgetGlobalY()inrender() - The renderer has no knowledge of the component hierarchy or local coordinates
void centerChild(Component* parent, Component* child) {
int childW, childH;
child->getSize(childW, childH);
int parentW, parentH;
parent->getSize(parentW, parentH);
// Calculate local position to center child within parent
int centerX = (parentW - childW) / 2;
int centerY = (parentH - childH) / 2;
child->setPosition(centerX, centerY); // Local coordinates
}void positionBesideButton(Panel* panel, Button* button1, Button* button2) {
int b1x, b1y, b1w, b1h;
button1->getBounds(b1x, b1y, b1w, b1h);
// Position button2 to the right of button1 with 10px gap
button2->setPosition(b1x + b1w + 10, b1y); // All local coordinates
}bool MyComponent::handleClick(int localX, int localY) override {
// Hit test against custom regions
struct Region {
int x, y, w, h;
std::function<void()> action;
};
std::vector<Region> regions = {
{10, 10, 100, 40, [this]() { onSaveClick(); }},
{120, 10, 100, 40, [this]() { onCancelClick(); }},
};
for (const auto& region : regions) {
if (localX >= region.x && localX < region.x + region.w &&
localY >= region.y && localY < region.y + region.h) {
region.action();
return true;
}
}
return false;
}DON'T DO THIS:
class MyComponent : public Component {
int storedGlobalX; // BAD: Will become stale!
int storedGlobalY;
public:
void render() override {
// This will use outdated coordinates if parent moved
renderer->drawRect(storedGlobalX, storedGlobalY, width, height, ...);
}
};DO THIS INSTEAD:
class MyComponent : public Component {
public:
void render() override {
// Always compute global coordinates on-demand (uses cache)
int x = getGlobalX();
int y = getGlobalY();
renderer->drawRect(x, y, width, height, ...);
}
};The cache is most effective when:
- Deep hierarchies - More levels = more savings per cache hit
- Static layouts - Components that don't move frequently
- Multiple queries per frame - Rendering, hit testing, etc.
Cache misses occur when:
- Initial render - First call after component creation
- After movement - Any position change invalidates the cache
- After reparenting - Adding to a new parent invalidates
- Batch position updates - If moving multiple components, update all positions before rendering
- Avoid unnecessary moves - Only call
setPosition()when the position actually changes - Layout once per frame - Call
performLayout()once, not continuously - Don't store global coords - Let the cache do its job
panel->setPosition(100, 200);
button->setPosition(50, 75); // Relative to panel
panel->addChild(std::move(button));
// Button's global position is automatically (150, 275)ImGui::SetCursorPos(ImVec2(100, 200));
ImGui::BeginChild("panel");
ImGui::SetCursorPos(ImVec2(50, 75)); // Relative
ImGui::Button("Click");
ImGui::EndChild();panel->setPosition(100, 200);
button->setPosition(150, 275); // Must manually calculate
panel->addChild(button);
// If panel moves, must manually update button positionpanel->move(100, 200);
button->move(150, 275); // Absolute screen coordinates
button->setParent(panel);
// Qt automatically converts to relative internallyNote: There is a separate CoordinateSystem class (include/bombfork/prong/core/coordinate_system.h) that handles world-to-screen transformations for game/viewport scenarios. This is distinct from the component coordinate system:
- Component coordinates (this document): UI element positioning in screen space
- World coordinates (
CoordinateSystemclass): Game world to viewport transformations with zoom/pan
The CoordinateSystem class is used primarily in viewport components that display game worlds, maps, or other zoomable content. It handles:
- World position ↔ Screen position conversions
- Camera positioning and zoom levels
- Tile-based coordinate systems
- Visible region calculations
See the CoordinateSystem class documentation for details on world coordinate transformations.
Symptoms: Component appears at (0, 0) or wrong location
Likely causes:
- Using local coordinates for rendering instead of global
- Not calling
getGlobalX()andgetGlobalY()
Solution: Always use global coordinates in render():
void render() override {
int x = getGlobalX(); // Not localX!
int y = getGlobalY(); // Not localY!
renderer->drawRect(x, y, width, height, ...);
}Symptoms: Child stays in place when parent moves
Likely causes:
- Child is not actually added to parent via
addChild() - Child's position is being set to global coordinates
- Child is being repositioned after parent moves
Solution: Ensure proper parent-child relationship:
auto child = std::make_unique<MyComponent>();
child->setPosition(localX, localY); // Local coordinates
parent->addChild(std::move(child)); // Establish relationship
// Now child will move with parent automaticallySymptoms: Clicks/hover don't work at new position
Likely causes:
- Component not added to the scene hierarchy
- Custom
containsEvent()implementation using incorrect coordinate space
Solution: Ensure hit testing uses global coordinates:
// The default implementation is correct:
bool containsGlobal(int globalX, int globalY) const {
int gx, gy;
getGlobalPosition(gx, gy);
return globalX >= gx && globalX < gx + width &&
globalY >= gy && globalY < gy + height;
}Symptoms: Slow rendering or event handling
Likely causes:
- Constantly invalidating cache by repeatedly calling
setPosition() - Very deep component nesting (>50 levels)
Solution:
- Only update positions when actually changed
- Consider flattening the hierarchy if possible
- Profile to confirm this is actually the bottleneck
Potential enhancements to the coordinate system:
- Rotation and Scaling - Currently not supported; would require transformation matrices
- Coordinate Space Validation - Debug-mode checks to catch coordinate space misuse
- Dirty Rectangle Optimization - Track which regions need repainting
- Batch Invalidation - Defer cache invalidation until needed
- Relative Sizing - Similar to relative positioning (e.g., "50% of parent width")
The Prong coordinate system provides:
- Relative positioning for intuitive parent-child relationships
- Automatic cache management for performance
- Clear separation between local (positioning) and global (rendering) spaces
- Simple APIs that match developer expectations
- Zero manual maintenance of global coordinates
By following the patterns in this document, you can build complex UI hierarchies with confidence that coordinates will be handled correctly and efficiently.