Back to blog
2 min read

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:

  1. Measure pass - determine intrinsic sizes bottom-up
  2. 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!