Building RatUI: A Modern C++20 UI Framework
An overview of RatUI's architecture, featuring a custom layout engine, HarfBuzz text shaping, and MTSDF text rendering.
I've been working on RatUI for the past several months, and I wanted to share some of the technical decisions and architecture that went into building it.
Why Another UI Framework?
Most existing C++ UI frameworks are either:
- Immediate mode (like Dear ImGui) - great for tools but challenging for complex layouts
- Legacy retained mode (like Qt) - powerful but heavyweight with complex build systems
I wanted something in between: a modern retained-mode framework that's lightweight, uses modern C++20 features, and handles text rendering properly.
Core Architecture
RatUI is built around a few key systems:
The Layout Engine
The layout engine uses a flexbox-inspired model. Each element has:
struct LayoutStyle {
FlexDirection direction = FlexDirection::Row;
JustifyContent justifyContent = JustifyContent::FlexStart;
AlignItems alignItems = AlignItems::Stretch;
Length width = Length::Auto();
Length height = Length::Auto();
Spacing padding;
Spacing margin;
};Layout is computed in two passes:
- Measure pass - determine intrinsic sizes bottom-up
- Layout pass - assign positions and final sizes top-down
Text Rendering with MTSDF
One of the most complex parts of any UI framework is text rendering. RatUI uses Multi-channel Signed Distance Fields (MTSDF) for crisp text at any scale.
The pipeline looks like this:
// 1. Generate MTSDF atlas from font
auto atlas = MtSdfGenerator::Generate(font, glyphSet, atlasSize);
// 2. Shape text with HarfBuzz
auto shapedText = textShaper.Shape(text, font, fontSize);
// 3. Render with SDF shader
renderer.DrawText(shapedText, atlas, transform);The shader samples the distance field and applies anti-aliasing:
float median(vec3 msd) {
return max(min(msd.r, msd.g), min(max(msd.r, msd.g), msd.b));
}
void main() {
vec3 msd = texture(msdfAtlas, texCoord).rgb;
float sd = median(msd);
float screenPxDist = screenPxRange * (sd - 0.5);
float opacity = clamp(screenPxDist + 0.5, 0.0, 1.0);
fragColor = vec4(textColor.rgb, textColor.a * opacity);
}Batching System
To minimize draw calls, RatUI batches all geometry by texture. The renderer collects vertices into per-texture buckets and flushes them in texture order:
void Renderer::Flush() {
for (auto& [texture, batch] : batches) {
texture->Bind(0);
vertexBuffer.Upload(batch.vertices);
indexBuffer.Upload(batch.indices);
glDrawElements(GL_TRIANGLES, batch.indices.size(), GL_UNSIGNED_INT, nullptr);
}
batches.clear();
}What's Next
I'm currently working on:
- Animations - smooth transitions between states
- Accessibility - proper screen reader support
- More widgets - tables, trees, and rich text
Check out the RatUI repository if you want to follow along or contribute!