Claude Code was able to one-shot RmlUi integration into Esenthel.
I'm not a web-dev person, but with Vibe Designing coming soon
( e.g.
https://claude.ai/design ) , I think there is value in having a UI
system defined by a declarative language like HTML/CSS.
This also makes it much easier for users to mod or skin the UI if they
want. But really, it's about making it easier for the AI to do it.
Claude Code really seems to struggle with building Esenthel GUIs
programmatically that look good and have the correct font size, etc.,
but with HTML/CSS this problem doesn't exist.
I haven't profiled this, the implementation has not been optimized,
and I'm sure there is a performance penalty. That said, RmlUi is
highly regarded and is designed for games; it is not a full WebKit.
An integration of
RmlUi v6.2 (MIT) as
a retained-mode HTML/CSS UI library that
coexists with Esenthel's built-in
Gui system without touching it. Both run in the same frame — the native
widget set is still available for lightweight 3D-integrated controls, while
RmlUi handles HTML/CSS/animation-heavy UI (menus, HUDs, settings screens,
chat windows). A single RmlUi render backend lives inside the Engine static
library and drives RmlUi's vertex stream through the engine's existing 2D
batcher, so every renderer backend (DX11, DX12, Vulkan, OpenGL) gets RmlUi
for free with zero backend-specific code.
Game HUD tutorial
Using Esenthel GUI elements and RmlUi elements side-by-side tutorial
Quick start:
Code:
#include "Gui/RmlUi.h" // EE::RmlUi::* facade
// In a single .cpp where you author Rml event listeners, DOM manipulation,
// etc. — wrap the RmlUi includes so Esenthel macros and X11 defines don't
// collide with RmlUi's template parameters and enum names:
#include "../../ThirdPartyLibs/begin.h"
#undef T1 #undef T2 #undef T3 #undef T4 #undef T5
#undef T6 #undef T7 #undef T8 #undef T9 #undef T10 #undef T11
#undef Always #undef NotUseful
#include <RmlUi/Core.h>
#include "../../ThirdPartyLibs/end.h"
static Rml::Context *Ctx=null;
static Rml::ElementDocument *Doc=null;
Bool Init()
{
EE::RmlUi::Init(); // install render + system interfaces
EE::RmlUi::LoadFontFace("Data/RmlUi/LatoLatin-Regular.ttf");
Ctx=EE::RmlUi::CreateContext(); // sized to D.resW()/D.resH()
Doc=Ctx->LoadDocument("Data/RmlUi/demo.rml");
if(Doc)Doc->Show();
return true;
}
void Shut() { EE::RmlUi::Shut(); }
Bool Update() {
if(Kb.bp(KB_ESC))return false;
Gui.update();
EE::RmlUi::PumpInput(Ctx); // Ms/Kb -> context
EE::RmlUi::Update (Ctx); // events, animations, dirty geometry
return true;
}
void Draw() {
Renderer(RenderScene); // optional 3D world
Gui.draw(); // Esenthel Gui above 3D
EE::RmlUi::Render(Ctx); // RmlUi on top of everything
}
Shipped features:
- Single-TU backend — Engine/Source/Gui/RmlUi.cpp is the only engine
source file that includes RmlUi headers. It subclasses
Rml::RenderInterface and Rml::SystemInterface, owns the 1×1 white
fallback texture, and exports the EE::RmlUi:: facade used by app code.
Macro pollution is neutralised locally: Esenthel's template-shortcut
macros T1..T11 (from Engine/H/_/defines.h, they collide with
robin_hood.h's template parameters) and X11's Always / NotUseful
(not stripped by Engine/H/_/headers.h) are undefined inside the
ThirdPartyLibs/begin.h / end.h bracket just for this one TU.
- API-agnostic rendering — RenderGeometry feeds RmlUi's pixel-space
vertex stream into the engine's existing 2D path. Per-vertex
conversion goes through D.pixelToScreen(Vec2) (canonical helper; no
hand-rolled Y-flip). D.alpha(ALPHA_MERGE) handles RmlUi's
premultiplied RGBA (which it emits since v5). Scissor goes through
D.clip(&engine_rect) / D.pixelToScreen(RectI). Works identically on
DX11, DX12, Vulkan, OpenGL.
- Untextured-path fallback — RmlUi calls RenderGeometry with
texture=0 for solid-colour geometry. The backend binds a pre-built
1×1 opaque-white Image so the textured shader path effectively
degenerates to plain vertex colour. This avoids needing a separate
colour-only code path.
- FreeType sharing — RmlUi's default FreeType font engine is pointed
at Esenthel's prebuilt EE_FreeType static lib via
FREETYPE_INCLUDE_DIRS / FREETYPE_LIBRARY cache vars in
ThirdPartyLibs/RmlUi/CMakeLists.txt, so one copy of FreeType ships in
the final link. EE_RMLUI_USE_BUNDLED_FREETYPE=ON is an escape hatch
if the prebuilt's config flags ever diverge from what RmlUi expects.
- Texture upload — LoadTexture routes to Image.Import, accepting
every format Esenthel's asset pipeline supports (BMP/PNG/JPG/JXL/WEBP/
AVIF/HEIF/TGA/TIF/DDS/PSD/ICO/HDR). GenerateTexture uploads raw
RGBA8 via a soft image, lock/write, row-copy, unlock, and GPU upload
(used for the font atlas and procedural decorator textures).
- Dual-mode input pump — EE::RmlUi::PumpInput(ctx) polls
mouse position, mouse buttons, and wheel every frame, walks all
256 KB_KEY values firing key down/up events on edge changes, and
pumps text characters out of Kb.k.c. When the focused RmlUi element
is an input field or textarea the key queue is drained so fast typing
never drops characters; otherwise the head of the queue is peeked
non-destructively so the built-in Gui still sees the same keys on the
same frame.
- Opt-in via CMake — option(EE_RMLUI "... " ON) in the root
CMakeLists.txt, default ON on Linux x64 + Windows x64, fatal on iOS
/ Android (deferred until those platforms are tested). With
EE_RMLUI=OFF every integration TU is empty, the
rmlui_core / rmlui_debugger targets are not added to
the build graph, and the tutorial targets are skipped in
Tutorials/CMakeLists.txt.
- Built from source — RmlUi v6.2 is vendored at
ThirdPartyLibs/RmlUi/lib/ and compiled via subdirectory inclusion so
rmlui_core + rmlui_debugger become Engine interface link deps.
No prebuilt library like the other third-parties, because upstream
ships clean CMake and the library is small. ABI-visible flags are
forced onto both targets so they match Engine's ABI.
- Debugger overlay — rmlui_debugger is linked automatically when
EE_RMLUI=ON. Call Rml::Debugger::Initialise(ctx) once, then
Rml::Debugger::SetVisible(true) (or bind to a key) to pop up the live
inspector — Event Log, Element Info, Outlines, Data Models tabs.
- Thin facade — Engine/H/Gui/RmlUi.h exposes only the
EE::RmlUi:: lifecycle and helper functions. It forward-declares
Rml::Context, ElementDocument, RenderInterface, and
SystemInterface, so app translation units that just drive lifecycle
do not pay to parse all of <RmlUi/Core.h>. App translation units
that want event listeners, DOM mutation, or typed element access can
include <RmlUi/Core.h> themselves.
Non-invasive integration — zero changes to
Engine/Source/Gui/Gui.cpp,
Gui.draw,
Gui.update, no new state in
DisplayClass, no new
alpha mode, no new shaders. The pre-existing
VI +
D 2D path is the
only rendering surface used; the existing 2D shaders are the only ones
touched. The tutorial harness (
Tutorials/TutorialAuto.{h,cpp},
Tutorials/stdafx.h) is unchanged.
EE_RMLUI=OFF builds are
pixel-identical to pre-integration builds (verified against
Tutorial_05_Bars,
Tutorial_04_BatchedDrawing,
Tutorial_12_RenderToTexture).
RCSS gotcha — RmlUi's transition/animation parser recognises tween
names as defined in
Source/Core/PropertyParserAnimation.cpp. Unlike
standard CSS, it does
not accept bare
linear or the CSS keyword
ease; use
linear-in-out and
cubic-in-out (or any of the
supported
{back,bounce,circular,cubic,elastic,exponential,linear,quadratic,quartic,quintic,sine}-{in,out,in-out}
variants). Multi-transitions are comma-separated.
Deferred work:
- Custom Rml::FontEngineInterface on top of Esenthel's Font class, so
.pak-packaged fonts could feed RmlUi. Upstream's default FreeType
engine is sufficient for now; TTF files ship alongside the tutorials.
- Rml::FileInterface on EE::File — would let RmlUi resolve
.rml / .rcss / texture paths through engine .pak archives. The
stdio fopen default is used currently; tutorials ship plain files
under Tutorials/Data/RmlUi/.
- Data Binding (MVC layer via Rml::DataModelConstructor) — supported by
the linked rmlui_core but not yet demonstrated by a tutorial.
- Lua bindings (rmlui_lua) — disabled in the wrapper (RMLUI_LUA_BINDINGS=OFF);
enabling would require wiring an Esenthel Lua runtime into the engine
first.
- Windows smoke sweep via run_smoke_test.ps1. Linux is covered
(all five tutorials + three regression targets pass).
Available in this repo:
https://github.com/DrewGilpin/EsenthelEngine