To draw your own shapes and text, register an element and attach a callback to it:
- An element is one box in the frame's layout tree. Every control is made of elements.
- RegisterElement adds an element and returns an ElementRef, a handle to it for this frame.
- elementStorage.AddCustomDraw attaches a callback that runs at render time, after layout, when the element's final size and position are known.
ElementRef RegisterElement(Element&& element, LayoutOverride&& constraints);
ElementRef RegisterElement(Element&& element, std::string_view text, LayoutOverride&& constraints);
ElementStorageResult AddCustomDraw(const ElementRef& ref, CustomDrawCallback callback);
ElementStorageResult AddCustomDrawAfter(const ElementRef& ref, CustomDrawCallback callback);
ElementStorageResult AddPostLayout(const ElementRef& ref, PostLayoutCallback callback);
| Function | Runs |
| AddCustomDraw | Before the element's children are drawn |
| AddCustomDrawAfter | After the whole subtree; call it once the children are closed |
| AddPostLayout | After layout, before rendering. Can't draw; sees final geometry |
AddCustomDrawAfter draws over the element's children, for example to strike them out:
const ElementRef ref = RegisterElement(Element{ .id = panelID }, LayoutOverride{}.FlexGrow(1));
VERIFY(elementStorage.BeginChildren());
Text("No longer used");
VERIFY(elementStorage.EndChildren());
VERIFY(elementStorage.AddCustomDrawAfter(ref, [this](CustomDrawContext& ctx)
{
const IntRect r = ctx.ElementRect();
const LineStyle line{ .colorStyleIndex = theme.lineWhiteStyle, .lineWidth = styleManager.Px(1.0f) };
ctx.RenderLine(r.position, r.position + r.size, line);
}));
AddPostLayout only reads the final geometry, for example to keep a size for the next frame:
VERIFY(elementStorage.AddPostLayout(ref, [this](PostLayoutContext& ctx)
{
canvasSize = ctx.ElementInnerRect().size;
}));
The callback gets a CustomDrawContext:
- The default space is element-local:
- (0, 0) is the top-left of the padding box
- the pan and zoom of its ancestors apply
- SetSpace(DrawSpace::Screen) switches to absolute layout pixels.
- ScreenToCurrent(screenPos) converts a cursor position to the current space.
Element canvas{ .id = canvasID, .styleRef = theme.GetStyle("my-canvas") };
canvas.eventHandler = [this](ElementEvent&& event)
{
if (event.type == EventType::MouseMove)
pointerScreenPos = event.state.screenPos;
return false;
};
const ElementRef ref = RegisterElement(std::move(canvas), LayoutOverride::FixedSize(200, 100));
VERIFY(elementStorage.AddCustomDraw(ref, [this](CustomDrawContext& ctx)
{
ctx.RenderDisc(ctx.ScreenToCurrent(pointerScreenPos), styleManager.Px(4.0f), theme.lineWhiteStyle);
}));
| Method | Draws or returns |
| ElementRect(), ElementInnerRect() | Border box and padding box, in the current space |
| GetElementRect(ref), GetInnerRect(ref) | Another element's rects |
| RenderBox(rect, boxStyle, state, ...) | Styled box |
| RenderQuad(rect, colorStyle, cornerRadius, ...) | Filled rectangle |
| RenderLine(p0, p1, lineStyle) | Line (also a four-point form) |
| RenderPolyline(points, closed, lineStyle, cornerRadius) | Polyline |
| RenderCircle, RenderDisc, RenderDot | Circle outline, filled disc, dot. The circle is exactly round; gradients and dashes run clockwise from the right. |
| RenderTriangle, RenderFilledPolygon | Filled triangle, convex polygon. hardEdges (edges shared with another triangle) get no soft edge, so no seam shows. |
| RenderText(textId, rect) | Text registered with textSystem->RegisterText |
Colors are style indices:
- for example theme.lineWhiteStyle
- or one from styleManager.RegisterColorStyle
- LineStyle has colorStyleIndex, lineWidth and dash settings
const ElementRef ref = RegisterElement(Element{ .id = canvasID, .styleRef = theme.GetStyle("my-canvas") },
LayoutOverride::FixedSize(200, 100));
VERIFY(elementStorage.AddCustomDraw(ref, [this](CustomDrawContext& ctx)
{
const IntRect r = ctx.ElementInnerRect();
const LineStyle line{ .colorStyleIndex = theme.lineWhiteStyle, .lineWidth = styleManager.Px(2.0f) };
ctx.RenderLine(r.position, r.position + r.size, line);
}));
The callback runs after OnGUI returns, so anything it captures must outlive the frame.
Previous: Other controls | Controls | Next: Default CSS classes