🎨 Custom UI Engine

HelveteUI

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.

Language: C++11 / Boring Code
Backends: Marmalade SDK · raylib
Input: HTML files + linked CSS
Hot reload: ✓ on-disk file watch
Figma: ✓ export plugin
🖥 raylib
Desktop — Windows, Linux, macOS. OpenGL 3.3 backend via raylib. VS 2026 Community.
Primary dev target
📱 Marmalade SDK
Cross-platform mobile SDK. Renders with Iw2D / IwGx. Feature-parity with raylib.
Production target
🔌 Any Backend
Implement IHelveteUIBackend for SDL, Unity, custom engine, etc.
Interface-based

🏗 Architecture

Data flows top-down: HTML+CSS file → parse → layout → render → platform backend

Input
HTML + CSS Files
data/*.html data/*.css
Designer tool
Figma Plugin
figma-plugin/src/code.ts
HTML Parser
hlvUIHtmlParser
parsers/hlvUIHtmlParser.cpp
CSS Parser
hlvUICssParser
parsers/hlvUICssParser.cpp
Style Resolver
hlvUIStyleResolver
parsers/hlvUIStyleResolver.cpp
UI Builder
hlvUIHtmlUiBuilder
parsers/hlvUIHtmlUiBuilder.cpp
↓ UiNodeDesc tree
Layout Engine
hlvUILayoutEngine
parsers/hlvUILayoutEngine.cpp — Flexbox · Grid · Absolute · Fixed · Sticky · Scroll containers
↓ screen-space (x,y,w,h) per node
Renderer (core)
HelveteUIRenderer
render/hlvUIRenderer.cpp — renderNode() dispatches to per-type render methods
Animation
Keyframes + Transitions
render/hlvUIRenderAnimation.cpp
Widgets
WidgetRegistry
widgets/*.cpp — Toggle · Slider · Dropdown · Modal · Tabs · Tooltip
↓ fillRect / drawText / drawImage / …
Backend Interface
IHelveteUIBackend
IHelveteUIBackend.h
Marmalade Backend
MarmaladeBackend
Iw2D / IwGx calls
raylib Backend
RaylibBackend
DrawRectangle / DrawTexture / …

📋 Boring Code Philosophy

The entire codebase follows a strict style for maximum readability and portability.

🚫

What we DON'T use

  • auto — explicit types everywhere
  • Lambdas — use named functions or explicit functors
  • Range-for — use index loops or iterator loops
  • std::min/max with initializer lists — explicit comparisons
  • Implicit conversions or complex ternary chains
  • nullptr — 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);
}

🎨 Supported CSS Features

All properties are parsed from real CSS and stored as string props on UiNodeDesc nodes.

Box Model
widthheight min-widthmax-width min-heightmax-height paddingpadding-top/right/bottom/left marginmargin-top/right/bottom/left border-widthborder-color border-styleborder-radius border-top-left-radiusborder-top-right-radius border-bottom-left-radiusborder-bottom-right-radius border-image (9-patch) box-shadow
Layout
display: flex/grid/block/inline-block position: absolute/relative/fixed/sticky flex-directionflex-wrap flex-growflex-shrinkflex-basis justify-contentalign-items gapcolumn-gaprow-gap grid-template-columnsgrid-template-rows grid-columngrid-row left/top/right/bottom overflow: hidden/scroll/auto z-index
Background
background-color linear-gradient radial-gradient background-image background-size: cover/contain background-position background-repeat opacity / alpha
Typography
colorfont-size font-familyfont-weight text-align: left/center/right text-decoration: underline text-overflow: ellipsis text-transform: uppercase/lowercase/capitalize letter-spacingword-spacing line-heightwhite-space: nowrap text-shadow text-indent
Visual FX
transform: translate/rotate/scale/matrix filter: blur/brightness clip-path: polygon/circle/ellipse/inset cursor: pointer pointer-events: none visibility: hidden outline object-fit: cover/contain
States
:hover — background/border/color/transform/alpha :active — background/border/color/transform transition: property duration timing-function @keyframes — all animatable properties
Forms & Interaction
input[type=text/password/number/checkbox/radio] textarea form validation (required/minlength/maxlength/min/max) accent-color placeholder tooltip-text
/* 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; }

Animation System

Full CSS @keyframes engine + CSS transitions with smooth keyframe→transition handoff.

@keyframes

  • opacity — fade in/out
  • background-color — multi-stop color cycles
  • color — text color animation
  • border-color — glow effects
  • border-radius — shape morphing
  • border-width — pulsing border
  • width / height — size animation
  • font-size — text size pulse
  • letter-spacing — spacing animation
  • translateX / translateY — slide
  • rotate — rotation
  • scale — scale animation

Properties

  • animation-name — links to @keyframes name
  • animation-duration — e.g. 1.5s, 400ms
  • animation-timing-function — linear / ease / ease-in / ease-out / ease-in-out
  • animation-iteration-count1, 3, infinite
  • animation-direction — normal / alternate
  • animation-delay — e.g. 0.2s
  • animation-fill-modeforwards (holds last frame)
  • Transition coexistence — keyframes run first; transition fires from held value after keyframe ends
Fade
opacity
Pulse
background-color
Flash
bg multi-stop
Slide
translateX
Pop
scale
Spin
rotate
Bounce
translateY
Glow
border-color
Morph
border-radius
TextFade
color
Fill Fwd
fill-mode:forwards

🧩 Widget Library

Interactive widgets declared in HTML with input-type attribute. State queried from C++.

Toggle
On/off switch with animated thumb. Supports accent-color.
input-type="toggle"
getToggleValue(name)
Slider
Horizontal drag slider with configurable min/max/step.
input-type="slider"
getSliderValue(name)
Dropdown
Popup option list. Custom styled, keyboard-accessible.
Select option
input-type="dropdown"
getDropdownValue(name)
Tabs
Tab bar with content switching. Active tab state persisted.
Tab 1
Tab 2
Tab 3
input-type="tab-button"
hlvUITabsWidget
Modal
Overlay dialog with backdrop. Open/close from C++ or via button.
openModal(name)
closeModal(name)
Tooltip
Hover-activated overlay. Declared inline on any element.
tooltip-text: "..."
tooltip-background: #...
tooltip-color: #...
hlvUITooltipWidget
Number Input
Numeric text input with up/down arrows, min/max/step validation.
input-type="number"
min="0" max="100" step="1"
getInputValue(name)
Scroll Container
Vertical + horizontal scrollable region. Drag-to-scroll + wheel.
overflow: scroll
scroll(mx,my,delta,root)
input-type="scroll-container"
Form
Form container with validation. Submit via button[type=submit].
is-form="true"
required / minlength / maxlength
wasFormSubmitted(id)
getFormErrors()

📖 API Reference

HelveteUIRenderer is the main class. Instantiate with an IHelveteUIBackend*.

Setup & Loading

HelveteUIRenderer(backend)
Constructor. Pass your backend implementation.
loadFont(path)
Load a single bitmap font (.gxfont / .ttf).
loadFontFamily(stem, sizes[], count)
Load multiple font sizes. Renderer picks closest at draw time.
loadFile(htmlPath, screenW, screenH)
Parse HTML+CSS, run layout, enable hot reload. Clears image cache.
setKeyframes(map<string, CssKeyframes>)
Register @keyframes for animation. Called automatically by loadFile().

Per-Frame Loop

updateInput(mx, my, mouseDown)
Process mouse events. Call BEFORE render().
render(root)
Draw the entire UI tree. Call each frame.
renderLoaded()
Draw last loadFile() result. Checks hot reload every 500ms.
setVar(key, value)
Set a template variable. {{key}} in HTML is replaced at render time.
scroll(mx, my, deltaY, root)
Scroll the container under the cursor. Call on mouse-wheel events.

Input State Queries

wasClicked(name) → bool
True if named button was released this frame.
getInputValue(name) → string
Current text in a text/password/number input.
isChecked(name) → bool
Current checked state of a checkbox.
getRadioValue(group) → string
Selected option name in a radio group.
wasFormSubmitted(formId) → bool
True if form was validated and submitted this frame.

Widget Queries

getToggleValue(name) → bool
Current on/off state of a Toggle widget.
getSliderValue(name) → float
Current value of a Slider widget.
getDropdownValue(name) → string
Currently selected option value of a Dropdown.
openModal(name) / closeModal(name)
Programmatically open or close a Modal widget.

Keyboard Input

inputChar(c)
Append printable char to focused text input.
inputBackspace()
Delete last character in focused input.
inputEnter()
Newline in textarea; unfocus in single-line input.
hasFocusedInput() → bool
True when a text input has focus (suppress game hotkeys).

Typical main-loop integration

// --- 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());

🔌 Backend Interface

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;
};

✓ Existing Backends

  • RaylibBackend — raylib 5.x, OpenGL 3.3, Windows/Linux/macOS
  • MarmaladeBackend — Marmalade SDK Iw2D, iOS/Android/Win

Add a new backend

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);

🎨 Figma Plugin

Export Figma designs directly to HelveteUI-compatible HTML+CSS files. Designers click Export inside Figma — no manual conversion.

🎨
Figma Design
Frames, components, auto-layout
🔌
Plugin (TypeScript)
Node traversal + CSS generation
figma-plugin/src/code.ts
📄
HTML + CSS
Clean, HelveteUI-compatible output
📁
data/ folder
Drop files in, hot-reload fires
🖥
HelveteUI Runtime
Renders instantly

What gets exported

  • FRAME / GROUP → <div>
  • COMPONENT / INSTANCE → <div>
  • TEXT → <p> / <h1><h3>
  • RECTANGLE / ELLIPSE → styled <div>
  • Auto-layout → CSS Flexbox
  • Fills, strokes, shadows, opacity
  • Border radius (per-corner)
  • Figma colors → rgba() CSS

Plugin UI

  • Step-by-step instructions panel
  • Live selection indicator
  • Export button (enabled when Frame selected)
  • Status feedback (success/error)
  • Separate ⬇ Download .html / .css
  • HTML preview (first 600 chars)

How to install

  • Open Figma Desktop
  • Plugins → Development → Import from manifest
  • Point to figma-plugin/manifest.json
  • Select a Frame, run plugin, click Export
  • Drop .html + .css into data/
cd figma-plugin
npm install
npm run build

🎮 Demo Gallery

25 demo files in data/ covering all features. Cycle with keyboard in the app.

File What it demonstrates Key features
demo_transitions.htmlTransition + Keyframe Coexistencefill-mode:forwards · infinite+hover · border-color anim
demo_animations.htmlAll @keyframes propertiesopacity · color · translate · rotate · scale · font-size · letter-spacing
demo_features.htmlCore CSS features showcasegradients · border-image · clip-path · filter · transform
demo_widgets_all.htmlAll widgets on one screenToggle · Slider · Dropdown · Tabs · Tooltip · Modal · Number Input
demo_widgets.htmlBasic widget panelToggle · Slider with labels
demo_widgets2.htmlExtended widget panelDropdown · Tabs · Number Input
demo_form.htmlForm validationrequired · minlength · maxlength · submit · error display
demo_interactive.htmlInteractive buttons + inputswasClicked · getInputValue · getRadioValue
demo_scroll_input.htmlScroll containers + inputsoverflow:scroll · drag-to-scroll · nested scroll
demo_table_select.htmlTable with row selectiontable · colspan · rowspan · border-collapse
demo_layout.htmlLayout enginesflexbox · grid · absolute · fixed · sticky
demo_level_complete.htmlGame HUD — level completeReal-world UI screen · animations · gradients
demo_clipfilter.htmlClip-path + filter effectspolygon · circle · blur · brightness
demo_alpha_test.htmlOpacity + alpha layeringNested opacity · semi-transparent overlays
demo.htmlOriginal proof-of-conceptFirst HTML rendered
demo_new.html … demo_new11.htmlIncremental feature testsVarious CSS properties added over development

📂 Project File Map

Both the raylib and Marmalade versions share the same HelveteUI core source.

helvelteui-raylib/

src/ HelveteUI/ IHelveteUIBackend.h Backend interface — port to any engine hlvUITypes.h Vec2, HvTransform, HvKey, HvText enums parsers/ hlvUIConstants.h All CSS/HTML string constants (CssProp:: CssVal::) hlvUICssParser.cpp/.h CSS tokenizer + rule parser + @keyframes hlvUIHtmlParser.cpp/.h HTML tokenizer → HtmlNode tree hlvUIHtmlUiBuilder.cpp/.hHtmlNode → UiNodeDesc props hlvUIHtmlUiLoader.cpp/.h Load HTML file + linked CSS hlvUIStyleResolver.cpp/.hCascade: inline > class > tag styles; :hover/:active hlvUILayoutEngine.cpp/.h Flexbox · Grid · Absolute · Scroll layout render/ hlvUIRenderer.h/.cpp Main renderer — renderNode() dispatch, transitions hlvUIRenderAnimation.cpp @keyframes evaluator — all animatable properties hlvUIRenderButton.cpp Button hover/press/click state hlvUIRenderFonts.cpp Font loading · image cache · clearImages() hlvUIRenderGroup.cpp div/section/header/footer rendering hlvUIRenderHotReload.cpp loadFile() + file-watch hot reload hlvUIRenderImage.cpp img/background-image/border-image 9-patch hlvUIRenderInput.cpp input/textarea/checkbox/radio interaction hlvUIRenderInputField.cppText field draw + caret blink hlvUIRenderPrimitives.cppfillRoundedRect · gradient · clip-path · shadow hlvUIRenderTable.cpp table/tr/td/th with colspan/rowspan hlvUIRenderText.cpp text draw · ellipsis · decoration · spacing widgets/ hlvUIDropdownWidget.cpp/.h hlvUIModalWidget.cpp/.h hlvUINumberInputWidget.cpp/.h hlvUISliderWidget.cpp/.h hlvUITabsWidget.cpp/.h hlvUIToggleWidget.cpp/.h hlvUITooltipWidget.cpp/.h hlvUIWidgetDefs.h OverlayItem, WidgetRenderCtx, color helpers hlvUIWidgetRegistry.cpp/.h canvas/ hlvUICanvas.cpp/.h Off-screen render target (optional) handlers/ hlvUIFormHandler.cpp/.h Form validation logic RaylibBackend.cpp/.h IHelveteUIBackend implementation for raylib main.cpp App entry — demo switcher loop data/ All HTML demo files + CSS + fonts + images docs/ index.html ← You are here figma-plugin/ manifest.json Plugin metadata src/ code.ts Main plugin — node traversal + HTML/CSS gen ui.html Plugin panel UI dist/ code.js Compiled output (npm run build) ui.html Copied output vs/ CMake-generated VS 2026 project files

marmalade-hello/

src/ HelveteUI/ ← identical structure to raylib version same core source files MarmaladeBackend.cpp/.h IHelveteUIBackend for Marmalade Iw2D Main.cpp Marmalade entry — s3eMain / demo switcher data/ Same HTML demos as raylib version mkb/ hello.mkb Marmalade build config

Key Data Structures

UiNodeDesc

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;
};

CssKeyframes

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;
};

♻️ Hot Reload

Edit any HTML or CSS file while the app is running — changes appear within 500ms automatically.

How it works

  • On loadFile(), all watched files (HTML + linked CSS) are recorded with their mtime
  • Every 500ms, renderLoaded() checks mtime of each file
  • On any change: re-parse, re-layout, swap root — seamless
  • Image cache cleared (clearImages()) on reload to prevent heap growth
  • Works on both Marmalade and raylib backends
// 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;
    }
}

🔡 Template Variables

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();

⚙️ Build Instructions

raylib (VS 2026)

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

Marmalade SDK

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/

Figma Plugin

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