A C++ HTML+CSS UI renderer that parses real HTML & CSS files and draws them with a native 2D graphics backend — no DOM, no browser, no external layout engine.
IHelveteUIBackend for SDL, Unity, custom engine, etc.Data flows top-down: HTML+CSS file → parse → layout → render → platform backend
The entire codebase follows a strict style for maximum readability and portability.
auto — explicit types everywherestd::min/max with initializer lists — explicit comparisonsnullptr — use NULL or 0// ✓ Boring Code style — explicit and readable std::map<std::string, std::string>::const_iterator it; it = node.props.find("color"); if (it != node.props.end()) { uint32_t col = parseColor(it->second); m_backend->setColour( (col ) & 0xFF, (col >> 8) & 0xFF, (col >> 16) & 0xFF, (col >> 24) & 0xFF); } // ✓ Iterator loop instead of range-for for (int i = 0; i < (int)node.children.size(); i++) { renderNode(node.children[i], px, py, alpha); }
All properties are parsed from real CSS and stored as string props on UiNodeDesc nodes.
/* Example: full-featured HelveteUI element */ .card { width: 280px; height: 120px; background: linear-gradient(135deg, #1e293b, #0f172a); border-radius: 12px; border: 1px solid #334155; box-shadow: 0 4px 20px rgba(0,0,0,0.5); transition: background-color 300ms ease; cursor: pointer; } .card:hover { background-color: #1e3a5f; } .card:active { background-color: #1e2a3f; } @keyframes slideIn { from { translateX: -80px; opacity: 0; } to { translateX: 0; opacity: 1; } } #card { animation: slideIn 0.5s ease-out 1 forwards; }
Full CSS @keyframes engine + CSS transitions with smooth keyframe→transition handoff.
animation-name — links to @keyframes nameanimation-duration — e.g. 1.5s, 400msanimation-timing-function — linear / ease / ease-in / ease-out / ease-in-outanimation-iteration-count — 1, 3, infiniteanimation-direction — normal / alternateanimation-delay — e.g. 0.2sanimation-fill-mode — forwards (holds last frame)Interactive widgets declared in HTML with input-type attribute. State queried from C++.
HelveteUIRenderer is the main class. Instantiate with an IHelveteUIBackend*.
{{key}} in HTML is replaced at render time.// --- Setup (once) --- HelveteUIRenderer ui(&myBackend); ui.loadFontFamily("fonts/arial", sizes, 6); ui.loadFile("data/demo.html", screenW, screenH); // --- Per-frame --- ui.setVar("score", playerScore); ui.renderLoaded(); // hot-reloads on save, draws UI if (ui.wasClicked("btn-start")) { startGame(); } if (ui.wasClicked("btn-quit")) { quit(); } float vol = ui.getSliderValue("volume"); setMusicVolume(vol); // Mouse wheel → scroll float delta = backend.getScrollDelta(); if (delta != 0.f) ui.scroll(mx, my, delta, ui.getLoadedRoot());
Implement IHelveteUIBackend to port HelveteUI to any engine.
The interface has 25 methods — fill-rect, draw-text, load-image, clip-rect, transforms.
class IHelveteUIBackend { public: // --- Timing --- virtual uint64_t getTimeMs() = 0; // --- Screen --- virtual int getScreenWidth() = 0; virtual int getScreenHeight() = 0; // --- Input --- virtual int getPointerX() = 0; virtual int getPointerY() = 0; virtual bool isPointerDown() = 0; virtual float getScrollDelta()= 0; virtual bool isKeyDown(HvKey)= 0; virtual bool popCharInput(char&) = 0; // --- Clipping --- virtual void setClipRect(int,int,int,int) = 0; virtual void clearClipRect() = 0; // --- Render state --- virtual void setColour(uint8_t r,g,b,a) = 0; virtual void setTransform(float m00…ty) = 0; virtual void resetTransform() = 0; // --- Geometry --- virtual void fillRect(x,y,w,h) = 0; virtual void drawRect(x,y,w,h) = 0; virtual void fillPolygon(verts,count) = 0; // --- Images --- virtual void* loadImage(path) = 0; virtual void releaseImage(image) = 0; virtual void drawImage(img,x,y,w,h) = 0; virtual void drawImageRegion(…) = 0; // --- Fonts --- virtual void* loadFont(path, size) = 0; virtual void drawText(font,text,…) = 0; };
Subclass IHelveteUIBackend, implement all pure virtuals, pass a pointer to HelveteUIRenderer. No changes to UI core needed.
class MyBackend : public IHelveteUIBackend { public: void fillRect(float x, float y, float w, float h) override { MyEngine::DrawRect(x, y, w, h); } // … implement the other 24 methods }; MyBackend backend; HelveteUIRenderer ui(&backend);
Export Figma designs directly to HelveteUI-compatible HTML+CSS files. Designers click Export inside Figma — no manual conversion.
figma-plugin/src/code.ts<div><div><p> / <h1>–<h3><div>rgba() CSSfigma-plugin/manifest.json.html + .css into data/cd figma-plugin npm install npm run build
25 demo files in data/ covering all features. Cycle with keyboard in the app.
Both the raylib and Marmalade versions share the same HelveteUI core source.
The universal node type. Produced by the builder, consumed by the layout engine and renderer.
struct UiNodeDesc { std::string tag; // "div","p","button"… std::string id; std::map<string,string> props; // all CSS + attrs std::vector<UiNodeDesc> children; };
Stores parsed @keyframes data for the animation engine.
struct CssKeyframe { float offset; // 0.0 – 1.0 std::map<string,string> props; }; struct CssKeyframes { std::string name; std::vector<CssKeyframe> frames; };
Edit any HTML or CSS file while the app is running — changes appear within 500ms automatically.
loadFile(), all watched files (HTML + linked CSS) are recorded with their mtimerenderLoaded() checks mtime of each fileclearImages()) on reload to prevent heap growth// The entire hot-reload loop — just call this each frame: ui.renderLoaded(); // Internally this happens every 500ms: for (int i = 0; i < (int)m_watchedFiles.size(); i++) { long mtime = getFileMtime(m_watchedFiles[i]); if (mtime != m_watchedFileTimes[i]) { clearImages(); // free old textures loadFile(m_loadedHtmlPath, m_loadedScreenW, m_loadedScreenH); return true; } }
Inject C++ values into HTML text nodes using {{variable}} syntax.
<!-- In your HTML: --> <p>Score: {{score}}</p> <p>Player: {{playerName}}</p> <p>Lives: {{lives}}</p> <p>Time: {{timeStr}}</p>
// In your C++ per-frame loop: ui.setVar("score", playerScore); ui.setVar("playerName", playerName); ui.setVar("lives", livesLeft); ui.setVar("timeStr", formatTime(elapsed)); // Substitution happens at render time — no rebuild needed. ui.renderLoaded();
cd helvelteui-raylib # Generate project (first time) cmake -B vs -G "Visual Studio 18 2026" # Open in VS 2026 vs\helvelteui-raylib.sln # Set startup project to: helvelteui-raylib # F5 to run
cd marmalade-hello # Open in Marmalade hub / MKB tool mkb\hello.mkb # Or via CLI: mkb --vs2017 hello.mkb # Build with Visual Studio # working dir = marmalade-hello/
cd helvelteui-raylib/figma-plugin # Install deps (once) npm install # Build TypeScript npm run build # Watch mode (dev) npm run watch # dist/code.js + dist/ui.html # Import manifest.json in Figma