Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Recoil:RmlUI Starter Guide: Difference between revisions

From Fightorder
Undo revision 2703 by Qrow (talk)
Tag: Undo
No edit summary
Tag: Reverted
Line 1: Line 1:
{{DISPLAYTITLE:RmlUI Starter Guide}}
+++
__TOC__
title = 'RmlUi'
date = 2025-05-11T13:17:05-07:00
draft = false
author = "Slashscreen"
+++


= RmlUI Starter Guide =
RmlUi is a UI framework that is defined using a HTML/CSS style workflow (using Lua instead of JS) intended to simplify UI development especially for those already familiar with web development. It is designed for interactive applications, and so is reactive by default. You can learn more about it on the [RmlUI website] and [differences in the Recoil version here](#differences-between-upstream-rmlui-and-rmlui-in-recoil).


'''''For RecoilEngine game developers — build reactive, HTML/CSS-style game UIs in Lua.'''''
## How does RmlUI Work?


{| class="wikitable"
To get started, it's important to learn a few key concepts.
! Attribute !! Value
- Context: This is a bundle of documents and data models.
|-
- Document: This is a document tree/DOM (document object model) holding the actual UI.
| Engine || RecoilEngine (Spring)
- RML: This is the markup language used to define the document, it is very similar to XHTML (HTML but must be well-formed XML).
|-
- RCSS: This is the styling language used to style the document, it is very similar to CSS2 but does differ in some places.
| Framework || RmlUi ([https://github.com/mikke89/RmlUi mikke89/RmlUi])
- Data Model: This holds information (a Lua table) that is used in the UI code in data bindings.
|-
| Language || Lua (via sol2 bindings)
|-
| Availability || LuaUI (LuaIntro / LuaMenu planned)
|}


----
On the Lua side, you are creating 1 or more contexts and adding data models and documents to them.


== 1. Introduction ==
On the Rml side, you are creating documents to be loaded by the Lua which will likely include data bindings to show information from the data model, and references to lua functions for event handlers.


RmlUI is a UI framework that lets you build reactive game interfaces using an HTML/CSS-style workflow — with Lua instead of JavaScript. If you have ever built a web page, the learning curve is gentle. If you haven't, the concepts are still approachable because the mental model (mark-up + style + logic) is widely documented.
As each widget/component you create will likely comprise of several files (.lua, .rml & .rcss) you may find having a folder for each widget/component a useful way to organise your files.


RecoilEngine ships with a full RmlUI integration including:
## Getting Started
RmlUi is available in LuaUI (the game UI) with future availability in LuaIntro & LuaMenu planned, so it is already there for you to use.
However, to get going with it there is some setup code that should be considered for loading fonts, cursors and configuring ui scaling, an example script is provided below.
If you are working on a widget for an existing game, it is likely that the game already has some form of this setup code in, so you may be able to skip this section.


* An OpenGL 3 renderer tightly coupled to the engine's rendering pipeline
### Setup script
* A virtual-filesystem-aware file loader so your <code>.rml</code> and <code>.rcss</code> files live inside game archives
* Complete Lua bindings via sol2 so every RmlUI object is accessible from Lua
* Custom elements: <code><nowiki><texture></nowiki></code> for engine textures and <code><nowiki><svg></nowiki></code> for vector art
* A built-in DOM debugger for interactive inspection


This guide walks you through everything you need to get a working, reactive widget running in LuaUI. The examples are game-engine-agnostic and safe to copy into any Recoil-based game.
Here is the "[handler](https://github.com/beyond-all-reason/Beyond-All-Reason/blob/master/luaui/rml_setup.lua)", written by lov and ChrisFloofyKitsune, 2 of the people responsible for the RmlUi implementation.


{{Note|RmlUI is currently available in '''LuaUI''' (the in-game UI). Support for LuaIntro and LuaMenu is planned for a future release.}}
```lua
--  luaui/rml_setup.lua
--  author:  lov + ChrisFloofyKitsune
--
--  Copyright (C) 2024.
--  Licensed under the terms of the GNU GPL, v2 or later.


----
if (RmlGuard or not RmlUi) then
return
end
-- don't allow this initialization code to be run multiple times
RmlGuard = true


== 2. Key Concepts ==
--[[
Recoil uses a custom set of Lua bindings (check out rts/Rml/SolLua/bind folder in the C++ engine code)
Aside from the Lua API, the rest of the RmlUi documentation is still relevant
https://mikke89.github.io/RmlUiDoc/index.html
]]


Before writing any code it helps to understand the five core objects:
--[[ create a common Context to be used for widgets
pros:
* Documents in the same Context can make use of the same DataModels, allowing for less duplicate data
* Documents can be arranged in front/behind of each other dynamically
cons:
* Documents in the same Context can make use of the same data models, leading to side effects
* DataModels must have unique names within the same Context


{| class="wikitable"
If you have lots of DataModel use you may want to create your own Context
! Concept !! Description
otherwise you should be able to just use the shared Context
|-
| '''Context''' || A named container that holds documents and data models. Multiple contexts can exist simultaneously, each rendered independently.
|-
| '''Document''' || A parsed RML file representing a DOM tree. Documents live inside a Context and can be shown, hidden, or closed.
|-
| '''RML''' || The markup language for documents. Very similar to XHTML (well-formed XML). The root tag is <code><nowiki><rml></nowiki></code> instead of <code><nowiki><html></nowiki></code>.
|-
| '''RCSS''' || The styling language. Similar to CSS2 with some differences (see section 11). File extension: <code>.rcss</code>.
|-
| '''Data Model''' || A Lua table exposed to the RML document via reactive data bindings. Changes to the model automatically update the rendered UI.
|}


On the '''Lua side''' you create contexts, attach data models to them, and load documents. On the '''RML side''' you declare your UI structure and reference data-model values through data bindings.
Contexts created with the Lua API are automatically disposed of when the LuaUi environment is unloaded
]]


Each widget will typically consist of at least three files: a <code>.lua</code>, a <code>.rml</code>, and optionally a <code>.rcss</code> file — grouping them in a folder per widget keeps things tidy.
local oldCreateContext = RmlUi.CreateContext


----
local function NewCreateContext(name)
 
local context = oldCreateContext(name)
== 3. Setup ==


RecoilEngine ships a minimal setup script at <code>cont/LuaUI/rml_setup.lua</code>. It is included automatically by the base content handler but you should understand what it does so you can extend it for your game.
-- set up dp_ratio considering the user's UI scale preference and the screen resolution
local viewSizeX, viewSizeY = Spring.GetViewGeometry()


=== 3.1 The rml_setup.lua script ===
local userScale = Spring.GetConfigFloat("ui_scale", 1)


The script performs three tasks: guards against double-initialisation, loads font faces, and registers mouse-cursor aliases. Below is a more complete version that also creates a shared context and configures dp scaling:
local baseWidth = 1920
local baseHeight = 1080
local resFactor = math.min(viewSizeX / baseWidth, viewSizeY / baseHeight)


<syntaxhighlight lang="lua">
context.dp_ratio = resFactor * userScale
--  luaui/rml_setup.lua
--  author:  lov + ChrisFloofyKitsune
--  License: GNU GPL, v2 or later


if (RmlGuard or not RmlUi) then
context.dp_ratio = math.floor(context.dp_ratio * 100) / 100
    return
return context
end
end
-- Prevent this from running more than once
RmlGuard = true


-- Patch CreateContext to set dp_ratio automatically
local oldCreateContext = RmlUi.CreateContext
local function NewCreateContext(name)
    local context = oldCreateContext(name)
    local viewSizeX, viewSizeY = Spring.GetViewGeometry()
    local userScale = Spring.GetConfigFloat("ui_scale", 1)
    local baseWidth, baseHeight = 1920, 1080
    local resFactor = math.min(viewSizeX / baseWidth, viewSizeY / baseHeight)
    -- Floor to 2 decimal places to avoid floating point drift
    context.dp_ratio = math.floor(resFactor * userScale * 100) / 100
    return context
end
RmlUi.CreateContext = NewCreateContext
RmlUi.CreateContext = NewCreateContext


-- Load fonts (list your .ttf files here)
-- Load fonts
local font_files = {
local font_files = {
    -- "Fonts/MyFont-Regular.ttf",
}
}
for _, file in ipairs(font_files) do
for _, file in ipairs(font_files) do
    RmlUi.LoadFontFace(file, true)
Spring.Echo("loading font", file)
RmlUi.LoadFontFace(file, true)
end
end


-- Map CSS cursor names to engine cursor names
 
-- CSS cursor list: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor
-- Mouse Cursor Aliases
--[[
These let standard CSS cursor names be used when doing styling.
If a cursor set via RCSS does not have an alias, it is unchanged.
CSS cursor list: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor
RmlUi documentation: https://mikke89.github.io/RmlUiDoc/pages/rcss/user_interface.html#cursor
]]
 
-- when "cursor: normal" is set via RCSS, "cursornormal" will be sent to the engine... and so on for the rest
RmlUi.SetMouseCursorAlias("default", 'cursornormal')
RmlUi.SetMouseCursorAlias("default", 'cursornormal')
RmlUi.SetMouseCursorAlias("move",   'uimove')
RmlUi.SetMouseCursorAlias("pointer", 'Move') -- command cursors use the command name. TODO: replace with actual pointer cursor?
RmlUi.SetMouseCursorAlias("pointer", 'Move')
RmlUi.SetMouseCursorAlias("move", 'uimove')
RmlUi.SetMouseCursorAlias("nesw-resize", 'uiresized2')
RmlUi.SetMouseCursorAlias("nwse-resize", 'uiresized1')
RmlUi.SetMouseCursorAlias("ns-resize", 'uiresizev')
RmlUi.SetMouseCursorAlias("ew-resize", 'uiresizeh')


-- Create the shared context used by all widgets
RmlUi.CreateContext("shared")
RmlUi.CreateContext("shared")
</syntaxhighlight>
```


=== 3.2 Including the setup in main.lua ===
What this does is create a unified context 'shared' for all your documents and data models, which is currently the recommended way to architect documents. If you have any custom font files, list them in `font_files`, otherwise leave it empty.


Add a single line to <code>luaui/main.lua</code> to run the setup before any widgets load:
The setup script above can then be included from your `luaui/main.lua` (main.lua is the entry point for the LuaUI environment).


<syntaxhighlight lang="lua">
```lua
-- luaui/main.lua
VFS.Include(LUAUI_DIRNAME .. "rml_setup.lua", nil, VFS.ZIP) -- Runs the script
VFS.Include(LUAUI_DIRNAME .. "rml_setup.lua", nil, VFS.ZIP)
```
</syntaxhighlight>
> [!NOTE] If you are working on a widget for an existing game, check whether the game already has a setup script — it may not include all of the pieces shown above (font loading, cursor aliases, context creation, dp_ratio).


{{Note|The base-content <code>rml_setup.lua</code> only loads a font and sets cursor aliases; it does '''not''' create a context. If you are building a new game you should add context creation here or let each widget create its own context.}}


----
### Keeping dp_ratio Updated


== 4. Writing Your First Document (.rml) ==
The setup script sets `dp_ratio` once at context creation time, but the user may resize the window or change their `ui_scale` config during a session. To keep `dp_ratio` correct you need to recalculate and reapply it whenever the viewport changes.


Create a file at <code>luaui/widgets/my_widget/my_widget.rml</code>. RML is well-formed XML, so every tag must be closed and attribute values must be quoted.
The `ViewResize` callin fires whenever the window is resized. Add it to your widget and update each context you own:


=== 4.1 Root structure ===
```lua
function widget:ViewResize(newSizeX, newSizeY)
    local userScale = Spring.GetConfigFloat("ui_scale", 1)
    local baseWidth = 1920
    local baseHeight = 1080
    local resFactor = math.min(newSizeX / baseWidth, newSizeY / baseHeight)
    local dp = math.floor(resFactor * userScale * 100) / 100
    widget.rmlContext.dp_ratio = dp
end
```


<syntaxhighlight lang="xml">
If you have multiple contexts, update each one. [Mupersega's `rml_context_manager.lua`](https://github.com/beyond-all-reason/Beyond-All-Reason/blob/master/luaui/rml_context_manager.lua) in Beyond All Reason is a good reference implementation that handles context lifecycle and `dp_ratio` tracking together.
<rml>
<head>
    <title>My Widget</title>
    <!-- External stylesheet (optional) -->
    <link type="text/rcss" href="my_widget.rcss"/>
    <!-- Inline styles -->
    <style>
        body { margin: 0; padding: 0; }
    </style>
</head>
<body>
    <!-- Your content here -->
</body>
</rml>
</syntaxhighlight>


=== 4.2 Styles (inline and .rcss) ===
### Writing Your First Document


RmlUI starts with '''no default styles''' at all — not even block/inline defaults. The RmlUI docs provide an HTML4 base stylesheet you can copy in, but you will need to add form element styles yourself.
You will be creating files with .rml and .rcss extensions, as these closely resemble HTML and CSS it is worth configuring your editor to treat these file extensions as HTML and CSS respectively, see the [IDE Setup](#ide-setup) section for more information.


Use <code>dp</code> units instead of <code>px</code> so your UI scales correctly with the player's display and <code>ui_scale</code> setting.
Now, create an RML file somewhere under `luaui/widgets/`, like `luaui/widgets/getting_started.rml`. This is the UI document.


<syntaxhighlight lang="css">
Writing it is much like HTML by design. There are some differences, the most immediate being the root tag is called rml instead of html, most other RML/HTML differences relate to attributes for databindings and events, but for the time being, we don't need to worry about them.
/* Typical widget container */
#my-widget {
    position: absolute;
    width: 400dp;
    right: 10dp;
    top: 50%;
    transform: translateY(-50%);
    pointer-events: auto;  /* required to receive mouse input */
}


#my-widget .panel {
By default RmlUi has *NO* styles, this includes setting default element behaviour like block/inline and styles web developers would expect like input elements default appearances, as a starting point you can use [RmlUi documentation](https://mikke89.github.io/RmlUiDoc/pages/rml/html4_style_sheet.html) though these do not include styles for form elements.
    padding: 10dp;
    border-radius: 8dp;
    border: 1dp #6c7086;  /* note: no 'solid' keyword – see section 11 */
    background-color: rgba(30, 30, 46, 200);
}
</syntaxhighlight>


=== 4.3 Data bindings in RML ===
Here's a basic widget written by Mupersega.


Attach a data model to a root element with <code>data-model="model_name"</code>. All data binding attributes inside that element can reference the model.
```html
<rml>
<head>
    <title>Rml Starter</title>


<syntaxhighlight lang="xml">
    <style>
        #rml-starter-widget {
            pointer-events: auto;
            width: 400dp;
            right: 0;
            top: 50%;
            transform: translateY(-90%);
            position: absolute;
            margin-right: 10dp;
        }
        #main-content {
            padding: 10dp;
            border-radius: 8dp;
            z-index: 1;
        }
        #expanding-content {
            transform: translateY(0%);
            transition: top 0.1s linear-in-out;
            z-index: 0;
            height: 100%;
            width: 100%;
            position: absolute;
            top: 0dp;
            left: 0dp;
            border-radius: 8dp;
            display: flex;
            flex-direction: column;
            justify-content: flex-end;
            align-items: center;
            padding-bottom: 20dp;
        }
        /* This is just a checkbox sitting above the entirety of the expanding content */
        /* It is bound directly with data-checked attr to the expanded value */
        #expanding-content>input {
            height: 100%;
            width: 100%;
            z-index: 1;
            position: absolute;
            top: 0dp;
            left: 0dp;
        }
        #expanding-content.expanded {
            top: 90%;
        }
        #expanding-content:hover {
            background-color: rgba(255, 0, 0, 125);
        }
    </style>
</head>
<body>
<body>
  <!-- data-model scopes all bindings inside this div -->
    <div id="rml-starter-widget" class="relative" data-model="starter_model">
  <div id="my-widget" data-model="my_model">
        <div id="main-content">
 
            <h1 class="text-primary">Welcome to an Rml Starter Widget</h1>
    <!-- Text interpolation -->
            <p>This is a simple example of an RMLUI widget.</p>
    <p>Status: {{status_message}}</p>
            <div>
 
                <button onclick="widget:Reload()">reload widget</button>
    <!-- Conditional visibility -->
            </div>
    <div data-if="show_details">
        </div>
        <p>Extra details go here.</p>
        <div id="expanding-content" data-class-expanded="expanded">
            <input type="checkbox" value="expanded" data-checked="expanded"/>
            <p>{{message}}</p>
            <div>
                <span data-for="test, i: testArray">name:{{test.name}} index:{{i}}</span>
            </div>
        </div>
     </div>
     </div>
</body>
</rml>
```


    <!-- Dynamic class based on model value -->
Let's take a look at different areas that are important to look at.
    <div class="panel" data-class-active="is_active">
        Active panel
    </div>


    <!-- Loop over an array -->
`<div id="rml-starter-widget" class="relative" data-model="starter_model">`
    <ul>
1. Here, we bind to the data model using `data-model`. This is what we will need to name the data model in our Lua script later. Everything inside the model will be in scope and beneath the div.
        <li data-for="item, i: items">{{i}}: {{item.label}}</li>
2. Typically, it is recommended to bind your data model inside of a div beneath `body` rather than `body` itself.
    </ul>


    <!-- Two-way checkbox binding -->
```html
    <input type="checkbox" data-checked="is_enabled"/>
<div id="expanding-content" data-class-expanded="expanded">
    <input type="checkbox" value="expanded" data-checked="expanded"/>
```
any attribute starting with `data-` is a "data event". We will go through a couple below, but you can find out more here [on the RmlUi docs site](https://mikke89.github.io/RmlUiDoc/pages/data_bindings/views_and_controllers.html).
1. Double curly braces are used to show values from the data model within the document text. e.g. `{{message}}` shows the value of `message` in the data model.
2. `data-class-expanded="expanded"` applies the `expanded` class to the div if the value `expanded` in the data model is `true`.
3. `<input type="checkbox" value="expanded" data-checked="expanded"/>` This is a two-way binding: the checkbox reflects the current value of `expanded` in the data model, and toggling it writes back `true` or `false` to that field.
4. When the data model is changed, the document is rerendered automatically. So, the expanding div will have the `expanded` class applied to it or removed whenever the check box is toggled.


    <!-- Data event: calls model function on click -->
There are data bindings to allow you to loop through arrays (data-for) have conditional sections of the document (data-if) and many others.
    <button data-click="on_button_click()">Click me</button>


  </div>
### The Lua
</body>
</syntaxhighlight>


'''Data binding reference:'''
> [!NOTE] For information on setting up your editor to provide intellisense behaviour see the [Lua Language Server guide]({{% ref "lua-language-server" %}}).


{| class="wikitable"
To load your document into the shared context we created earlier and to define and add the data model you will need to have a lua script something like the one below.
! Binding !! Effect
|-
| <code><nowiki>{{value}}</nowiki></code> || Interpolate model value as text
|-
| <code>data-model="name"</code> || Attach named data model to this element and its children
|-
| <code>data-if="flag"</code> || Show element only when flag is truthy
|-
| <code>data-class-X="flag"</code> || Add class X when flag is truthy
|-
| <code>data-for="v, i: arr"</code> || Repeat element for each item in array arr
|-
| <code>data-checked="val"</code> || Two-way bind checkbox to boolean model value
|-
| <code>data-click="fn()"</code> || Call model function on click (data event)
|-
| <code>data-attr-src="val"</code> || Dynamically set the src attribute from model value
|}


----
```lua
 
-- luaui/widgets/getting_started.lua
== 5. The Lua Widget ==
if not RmlUi then
 
    return
Each widget is a Lua file that the handler loads. The <code>widget</code> global is injected by the widget handler and provides the lifecycle hooks.
end
 
=== 5.1 Widget skeleton ===
 
<syntaxhighlight lang="lua">
-- luaui/widgets/my_widget/my_widget.lua
if not RmlUi then return end   -- guard: RmlUi not available


local widget = widget ---@type Widget
local widget = widget ---@type Widget
Line 249: Line 267:
function widget:GetInfo()
function widget:GetInfo()
     return {
     return {
         name   = "My Widget",
         name = "Rml Starter",
         desc   = "Demonstrates RmlUI basics.",
         desc = "This widget is a starter example for RmlUi widgets.",
         author = "YourName",
         author = "Mupersega",
         date   = "2025",
         date = "2025",
         license = "GNU GPL, v2 or later",
         license = "GNU GPL, v2 or later",
         layer   = 0,
         layer = -1000000,
         enabled = true,
         enabled = true
     }
     }
end
end
</syntaxhighlight>


=== 5.2 Loading the document ===
local document
 
local dm_handle
<syntaxhighlight lang="lua">
local RML_FILE  = "luaui/widgets/my_widget/my_widget.rml"
local MODEL_NAME = "my_model"
 
local rml_ctx    -- the RmlUi Context
local dm_handle -- the DataModel handle (MUST be kept – see section 6)
local document  -- the loaded Document
 
-- Initial data model state
local init_model = {
local init_model = {
     status_message = "Ready",
     expanded = false,
     is_enabled    = false,
     message = "Hello, find my text in the data model!",
    show_details  = false,
     testArray = {
     items = {
         { name = "Item 1", value = 1 },
         { label = "Alpha", value = 1 },
         { name = "Item 2", value = 2 },
         { label = "Beta", value = 2 },
        { name = "Item 3", value = 3 },
     },
     },
    -- Functions can live in the model too (callable from data-events)
    on_button_click = function()
        Spring.Echo("Button was clicked!")
    end,
}
}
local main_model_name = "starter_model"


function widget:Initialize()
function widget:Initialize()
     rml_ctx = RmlUi.GetContext("shared")
     widget.rmlContext = RmlUi.GetContext("shared") -- Get the context from the setup lua


     -- IMPORTANT: assign dm_handle or the engine WILL crash on RML render
     -- Open the model, using init_model as the template. All values inside are copied.
     dm_handle = rml_ctx:OpenDataModel(MODEL_NAME, init_model)
    -- Returns a handle, which we will touch on later.
     dm_handle = widget.rmlContext:OpenDataModel(main_model_name, init_model)
     if not dm_handle then
     if not dm_handle then
         Spring.Echo("[my_widget] Failed to open data model")
         Spring.Echo("RmlUi: Failed to open data model ", main_model_name)
         return
         return
     end
     end
 
    -- Load the document we wrote earlier.
     document = rml_ctx:LoadDocument(RML_FILE, widget)
     document = widget.rmlContext:LoadDocument("luaui/widgets/getting_started.rml", widget)
     if not document then
     if not document then
         Spring.Echo("[my_widget] Failed to load document")
         Spring.Echo("Failed to load document")
         return
         return
     end
     end
    -- uncomment the line below to enable debugger
    -- RmlUi.SetDebugContext('shared')


     document:ReloadStyleSheet()
     document:ReloadStyleSheet()
     document:Show()
     document:Show()
end
end
</syntaxhighlight>
=== 5.3 Shutting down cleanly ===


<syntaxhighlight lang="lua">
function widget:Shutdown()
function widget:Shutdown()
    widget.rmlContext:RemoveDataModel(main_model_name)
     if document then
     if document then
         document:Close()
         document:Close()
        document = nil
     end
     end
    -- Remove the model from the context; frees memory
    rml_ctx:RemoveDataModel(MODEL_NAME)
    dm_handle = nil
end
end
</syntaxhighlight>


----
-- This function is only for dev experience, ideally it would be a hot reload, and not required at all in a completed widget.
 
function widget:Reload(event)
== 6. Data Models in Depth ==
    Spring.Echo("Reloading")
 
    Spring.Echo(event)
=== 6.1 Opening a data model ===
    widget:Shutdown()
 
    widget:Initialize()
<code>context:OpenDataModel(name, table)</code> copies the structure of the table into a reactive proxy. The name must match the <code>data-model="name"</code> attribute in your RML. It must be unique within the context.
end
 
```
{{Warning|You '''must''' store the return value of <code>OpenDataModel</code> in a variable. Letting it be garbage-collected while the document is open will crash the engine the next time RmlUI tries to render a data binding.}}
 
=== 6.2 Reading and writing values ===


For simple string/number/boolean values at the top level of the model you can use dot-notation directly on the handle:
### The Data Model Handle


<syntaxhighlight lang="lua">
In the script, we are given a data model handle. This is a proxy for the Lua table used as the data model; as the Recoil RmlUi integration uses Sol2 as a wrapper data cannot be accessed directly.
-- Read a value
local msg = dm_handle.status_message  -- works


-- Write a value (auto-marks the field dirty; UI updates next frame)
In most cases, you can simply do `dm_handle.expanded = true`. Assigning to any field on the handle (or any nested table within it, at any depth) automatically marks the relevant parts of the model as dirty and triggers a UI update — you do not need to call `__SetDirty` manually.
dm_handle.status_message = "Unit selected"
dm_handle.is_enabled    = true
</syntaxhighlight>


=== 6.3 Iterating arrays ===
What if you have an array, like `testArray` above, and want to loop through it on the Lua side? You will need to get the underlying table:


Because the handle is a sol2 proxy, you cannot iterate it directly with <code>pairs</code> or <code>ipairs</code>. Call <code>dm_handle:__GetTable()</code> to get the underlying Lua table:
```lua
local model_handle = dm_handle:__GetTable()
```


<syntaxhighlight lang="lua">
As of writing, this function is not documented in the Lua API, due to some problems with language server generics that haven't been sorted out yet. It is there, however, and will be added back in in the future. It returns the table with the shape of your initial model.
local tbl = dm_handle:__GetTable()
You can then iterate through it and change things as you please:


for i, item in ipairs(tbl.items) do
```lua
     Spring.Echo(i, item.label, item.value)
for i, v in pairs(model_handle.testArray) do
     item.value = item.value + 1   -- modify in place
     Spring.Echo(i, v.name)
     v.value = v.value + 1
end
end
</syntaxhighlight>
```
 
=== 6.4 Marking dirty ===
 
When you modify nested values (e.g. elements inside an array) through <code>__GetTable()</code>, you must tell the model which top-level key changed so that RmlUI re-renders the bound elements:
 
<syntaxhighlight lang="lua">
-- After modifying tbl.items …
dm_handle:__SetDirty("items")
 
-- For a simple top-level assignment via dm_handle.key = value,
-- dirty-marking is done automatically.
</syntaxhighlight>
 
----
 
== 7. Events ==
 
Recoil's RmlUI supports two distinct event families. They look similar but have different scopes — mixing them up is a common source of confusion.
 
=== 7.1 Normal events (on*) ===
 
Written as <code>onclick="myFunc()"</code>. The function is resolved from the '''second argument''' passed to <code>LoadDocument</code> — the widget table. These events do '''not''' have access to the data model.
 
<syntaxhighlight lang="lua">
-- Lua
local doc_scope = {
    greet = function(name)
        Spring.Echo("Hello", name)
    end,
}
document = rml_ctx:LoadDocument(RML_FILE, doc_scope)
</syntaxhighlight>
 
<syntaxhighlight lang="xml">
<!-- RML -->
<button onclick="greet('World')">Say hello</button>
</syntaxhighlight>
 
=== 7.2 Data events (data-*) ===
 
Written as <code>data-click="fn()"</code>. The function is resolved from the '''data model'''. These events have full access to all model values.


<syntaxhighlight lang="lua">
> [!NOTE] Use `pairs` rather than `ipairs` when iterating over data model arrays on the Lua side. The model handle does not currently implement `__len`, so `ipairs` will not traverse the array correctly. This will be resolved once `__len` is backported.
-- Lua model
local init_model = {
    counter = 0,
    increment = function()
        -- dm_handle is captured in the closure
        dm_handle.counter = dm_handle.counter + 1
    end,
}
</syntaxhighlight>


<syntaxhighlight lang="xml">
### Debugging
<!-- RML -->
<p>Count: {{counter}}</p>
<button data-click="increment()">+1</button>
</syntaxhighlight>


'''Event type comparison:'''
RmlUi comes with a debugger that lets you see and interact with the DOM. It's very handy! To use it, use this:


{| class="wikitable"
```lua
! Type !! Syntax example !! Scope
RmlUi.SetDebugContext(DATA_MODEL_NAME)
|-
```
| Normal || <code>onclick="fn()"</code> || <code>LoadDocument</code> second argument (widget table)
|-
| Data || <code>data-click="fn()"</code> || Data model table
|-
| Normal || <code>onmouseover="fn()"</code> || <code>LoadDocument</code> second argument
|-
| Data || <code>data-mouseover="fn()"</code> || Data model table
|}


----
And it will appear in-game. A useful idiom I like is to put this in `rml_setup.lua`:


== 8. Custom Recoil Elements ==
```lua
local DEBUG_RMLUI = true


Beyond standard HTML elements, Recoil adds two custom RML elements.
--...


=== 8.1 The &lt;texture&gt; element ===
Renders any engine texture — unit icons, map thumbnails, GL4 render targets, etc. Behaves identically to <code><nowiki><img></nowiki></code> except the <code>src</code> attribute takes a '''Recoil texture reference string''' (see engine docs for the full format).
<syntaxhighlight lang="xml">
<!-- Load a file from VFS -->
<texture src="unitpics/armcom.png" width="64dp" height="64dp"/>
<!-- Reference a named GL texture -->
<texture src="%luaui:myTextureName" width="128dp" height="128dp"/>
</syntaxhighlight>
=== 8.2 The &lt;svg&gt; element ===
Renders an SVG image. Unlike upstream RmlUI, the Recoil implementation accepts either a file path '''or raw inline SVG data''' in the <code>src</code> attribute:
<syntaxhighlight lang="xml">
<!-- External file -->
<svg src="images/icon.svg" width="32dp" height="32dp"/>
<!-- Inline SVG data -->
<svg src="<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'><circle cx='5' cy='5' r='4' fill='red'/></svg>"
    width="32dp" height="32dp"/>
</syntaxhighlight>
----
== 9. Debugging ==
RmlUI ships with a DOM debugger that you can open in-game to inspect elements, check computed styles, and view event logs.
<syntaxhighlight lang="lua">
-- Enable the debugger for the shared context.
-- Call this AFTER the context has been created.
RmlUi.SetDebugContext('shared')
-- A handy pattern: toggle via a build flag in rml_setup.lua
local DEBUG_RMLUI = true  -- set false before shipping
if DEBUG_RMLUI then
if DEBUG_RMLUI then
     RmlUi.SetDebugContext('shared')
     RmlUi.SetDebugContext('shared')
end
end
</syntaxhighlight>
```


Once enabled, a small panel appears in the game window. Click '''Debug''' to open the inspector or '''Log''' for the event log.
If you use the shared context, you can see everything that happens in it! Neat!


{{Note|The debugger attaches to one context at a time. Using the shared context means you see all widgets in one place — very useful for tracking cross-widget interactions.}}
## Things to Know and Best Practices


----
### Things to know
Some of the rough edges you are likely to run into have already been discussed, like the data model thing, but here are some more:


== 10. Best Practices ==
---


; Use dp units everywhere
Unlike a web browser a default set of styles is not included, as a starting point you can look at the [RmlUi documentation](https://mikke89.github.io/RmlUiDoc/pages/rml/html4_style_sheet.html) though this doesn't provide styles for form elements.
: The <code>dp</code> unit scales with <code>context.dp_ratio</code>, so your UI automatically adapts to different resolutions and the player's <code>ui_scale</code> setting.


; One shared context per game
Input elements of type submit & button behave differently to HTML and more like Button elements in that their text is not set by the value attribute. (This is likely to be corrected in a future version)
: Beyond All Reason and most community projects use a single <code>'shared'</code> context. Documents in the same context can reference the same data models and are Z-ordered relative to each other.


; Keep data models flat when possible
The alpha/transparency value of an RGBA colour is different to CSS (0-1) and instead uses 0-255. The css opacity does still use 0-1.
: Top-level writes via <code>dm_handle.key = value</code> are automatically dirty-marked. Deeply nested structures require manual <code>__SetDirty</code> calls — simpler models mean fewer bugs.


; Separate per-document and shared styles
List styling is unavailable (list-style-type etc.), you can still use UL/OL/LI elements but there is no special meaning to them, whilst you could use background images to replicate bullets there isn't a practical way to achieve numbered list items with RML/RCSS.
: Put styles unique to one document in a <code><nowiki><style></nowiki></code> block inside the <code>.rml</code> file. Put styles shared across multiple documents in a <code>.rcss</code> file and link it.


; Guard against missing RmlUi
Only solid borders are supported, so the border-style property is unavailable and the shorthand border property doesn't include a style part (```border: 1dp solid black;``` won't work, instead use ```border: 1dp black;```).
: Always start widgets with <code>if not RmlUi then return end</code> to prevent errors in environments where RmlUI is unavailable.


; Check return values
background-color behaves as expected, all other background styles are different and use decorators instead see the [RmlUi documentation for more information on decorators.](https://mikke89.github.io/RmlUiDoc/pages/rcss/decorators.html)
: Both <code>OpenDataModel</code> and <code>LoadDocument</code> can fail silently if paths are wrong. Always check for nil and log an error so you know immediately what happened.


; Organise by widget folder
There are two kinds of events: data events, like `data-mouseover`, and normal events, like `onmouseover`. These have different data in their scopes.
: Group <code>luaui/widgets/my_widget/my_widget.lua</code>, <code>.rml</code>, and <code>.rcss</code> together. This makes the project navigable and each widget self-contained.
- Data events have the data model in their scope.
- Normal events don't have the data model, but they *do* have whatever is passed into `widget` on `widget.rmlContext:LoadDocument`. `widget` doesn't have to be a widget, just any table with data in it.


----
For example, take this:


== 11. RCSS Gotchas ==
```lua
 
local model = {
RCSS closely follows CSS2 but has several important differences that will trip up web developers:
    add = function(a, b)
 
        Spring.Echo(a + b)
; No default stylesheet
    end
: RmlUI ships with zero default styles. Block/inline, heading sizes, list bullets — none of it exists unless you add it. Copy the HTML4 base sheet from the RmlUI docs as a starting point.
}
 
local document_table = {
; RGBA alpha is 0–255
    print = function(msg)
: <code>rgba(255, 0, 0, 128)</code> means 50% transparent red. CSS uses 0.0–1.0 for alpha. The <code>opacity</code> property still uses 0.0–1.0.
        Spring.Echo(msg)
 
    end,
; Border shorthand has no style keyword
}
: <code>border: 1dp #6c7086;</code> works. <code>border: 1dp solid #6c7086;</code> does '''not''' work. Only solid borders are supported; <code>border-style</code> is unavailable.
 
; background-* properties use decorators
: <code>background-color</code> works normally. <code>background-image</code>, <code>background-size</code>, etc. are replaced by the decorator system. See the RmlUI docs for syntax.
 
; No list styling
: <code>list-style-type</code> and friends are not supported. <code>ul</code>/<code>ol</code>/<code>li</code> have no special rendering — treat them as plain div-like elements.
 
; Input text is set by content, not value
: For <code><nowiki><input type="button"></nowiki></code> and <code><nowiki><input type="submit"></nowiki></code> the displayed text comes from the element's text content, not the <code>value</code> attribute (unlike HTML).
 
; pointer-events: auto required
: By default elements do not receive mouse events. Add <code>pointer-events: auto</code> to any element that needs to be interactive.
 
----
 
== 12. Differences from Upstream RmlUI ==
 
The Recoil implementation is based on the official RmlUI library but adds engine-specific features and changes some defaults:
 
{| class="wikitable"
! Feature !! Description
|-
| '''<nowiki><texture></nowiki> element''' || A custom element that renders engine-managed textures via Recoil texture reference strings. Not present in upstream.
|-
| '''<nowiki><svg></nowiki> inline data''' || The <code>src</code> attribute accepts raw SVG markup in addition to file paths. Upstream only supports file paths.
|-
| '''VFS file loader''' || All file paths are resolved through Recoil's Virtual File System, so <code>.rml</code> and <code>.rcss</code> files work inside <code>.sdz</code> game archives transparently.
|-
| '''Lua bindings via sol2''' || The Lua API is provided by RmlSolLua (derived from [https://github.com/LoneBoco/RmlSolLua LoneBoco/RmlSolLua]) rather than the upstream Lua plugin, offering tighter engine integration.
|-
| '''dp_ratio via context''' || <code>context.dp_ratio</code> is set from code rather than the system DPI, allowing games to honour the player's <code>ui_scale</code> preference.
|}
 
{{Note|For everything not listed above, the upstream RmlUI documentation at [https://mikke89.github.io/RmlUiDoc/ mikke89.github.io/RmlUiDoc] applies directly.}}
 
----


== 13. IDE Setup ==
dm_handle = widget.rmlContext:OpenDataModel("test", model)
document = widget.rmlContext:LoadDocument("document.rml", document_table)
```


Configuring your editor for <code>.rml</code> and <code>.rcss</code> file extensions dramatically improves the authoring experience.
```html
<h1>Normal Events</h1>
<input type="button" onclick="add(1, 2)">Won't work!</input>
<input type="button" onclick="print('test')">Will work!</input>


=== VS Code ===
<h1>Data Events</h1>
<input type="button" data-click="add(1, 2)">Will work!</input>
<input type="button" data-click="print('test')">Won't work!</input>
```


# Open any <code>.rml</code> file in VS Code.
### Best Practices
# Click the language mode indicator in the status bar (usually shows ''Plain Text'').
# Choose '''Configure File Association for '.rml''''
# Select '''HTML''' from the list.
# Repeat for <code>.rcss</code> files, selecting '''CSS'''.


=== Lua Language Server (lua-ls) ===
- To create a scalable interface the use of the dp unit over px is recommended as the scale can be set per context with SetDensityIndependentPixelRatio.
- For styles unique to a document, put them in a `style` tag. For shared styles, put them in an `rcss` file.
- Rely on Recoil's RmlUi Lua bindings doc for what you can and can't do. The Recoil implementation has some extra stuff the RmlUi docs don't.
- The Beyond All Reason devs prefer to use one shared context for all rmlui widgets.


Add the Recoil type annotations to your workspace for autocomplete on <code>RmlUi</code>, <code>Spring</code>, and all widget APIs. See the Recoil docs guide on the Lua Language Server for details.


----
### Differences between upstream RmlUI and RmlUI in Recoil


== 14. Quick Reference ==
- The SVG element allows either a filepath or raw SVG data in the src attribute, allowing for inline svg to be used (this may change to svg being supported between the opening and closing tag when implemented upstream)
- An additional element ```<texture>``` is available which allows for textures loaded in Recoil to be used, this behaves the same as an ```<img>``` element except the src attribute takes a [texture reference]({{% ref "articles/texture-reference-strings" %}})


=== Lua API ===


{| class="wikitable"
### IDE Setup
! Call !! Description
|-
| <code>RmlUi.CreateContext(name)</code> || Create a new rendering context
|-
| <code>RmlUi.GetContext(name)</code> || Retrieve an existing context by name
|-
| <code>RmlUi.LoadFontFace(path, fallback)</code> || Register a <code>.ttf</code> font (<code>fallback=true</code> recommended)
|-
| <code>RmlUi.SetMouseCursorAlias(css, engine)</code> || Map a CSS cursor name to an engine cursor name
|-
| <code>RmlUi.SetDebugContext(name)</code> || Attach the DOM debugger to a named context
|-
| <code>ctx:OpenDataModel(name, tbl)</code> || Create reactive data model from Lua table; '''store the return value!'''
|-
| <code>ctx:RemoveDataModel(name)</code> || Destroy a data model and free its memory
|-
| <code>ctx:LoadDocument(path, scope)</code> || Parse an RML file; scope is used for normal (<code>on*</code>) events
|-
| <code>doc:Show()</code> || Make the document visible
|-
| <code>doc:Hide()</code> || Hide the document (keeps it in memory)
|-
| <code>doc:Close()</code> || Destroy the document
|-
| <code>doc:ReloadStyleSheet()</code> || Reload linked <code>.rcss</code> files (useful during development)
|-
| <code>dm:__GetTable()</code> || Get the underlying Lua table for iteration
|-
| <code>dm:__SetDirty(key)</code> || Mark a top-level model key as changed; triggers re-render
|-
| <code>dm.key = value</code> || Write a top-level value; auto-marks dirty
|}


=== Useful Links ===
To get the best experience with RmlUi, you should set up your editor to use HTML syntax highlighting for .rml file extensions and CSS syntax highlighting for .rcss file extensions.


* [https://mikke89.github.io/RmlUiDoc/ RmlUI official documentation]
In VS Code, this can be done by opening a file with the extension you want to setup, then clicking on the language mode  in the bottom right corner of the window (probably shows as Plain Text). From there, you can select "Configure File Association for '.rml'" from the top menu that appears and choose "HTML" from the list. Do the same for .rcss files, but select "CSS".
* [https://mikke89.github.io/RmlUiDoc/pages/data_bindings/views_and_controllers.html RmlUI data bindings reference]
* [https://mikke89.github.io/RmlUiDoc/pages/rcss.html RmlUI RCSS reference]
* [https://mikke89.github.io/RmlUiDoc/pages/rml/html4_style_sheet.html RmlUI HTML4 base stylesheet]
* [https://mikke89.github.io/RmlUiDoc/pages/rcss/decorators.html RmlUI decorators (backgrounds)]
* [https://github.com/mikke89/RmlUi RmlUI GitHub]
* [https://github.com/beyond-all-reason/RecoilEngine/tree/master/rts/Rml RecoilEngine source (rts/Rml/)]
* Recoil texture reference strings — see engine docs: <code>articles/texture-reference-strings</code>


[[Category:Guides]]
[RmlUI website]: https://mikke89.github.io/RmlUiDoc/
[[Category:LuaUI]]
[[Category:RmlUI]]

Revision as of 23:16, 5 March 2026

+++ title = 'RmlUi' date = 2025-05-11T13:17:05-07:00 draft = false author = "Slashscreen" +++

RmlUi is a UI framework that is defined using a HTML/CSS style workflow (using Lua instead of JS) intended to simplify UI development especially for those already familiar with web development. It is designed for interactive applications, and so is reactive by default. You can learn more about it on the [RmlUI website] and [differences in the Recoil version here](#differences-between-upstream-rmlui-and-rmlui-in-recoil).

    1. How does RmlUI Work?

To get started, it's important to learn a few key concepts. - Context: This is a bundle of documents and data models. - Document: This is a document tree/DOM (document object model) holding the actual UI. - RML: This is the markup language used to define the document, it is very similar to XHTML (HTML but must be well-formed XML). - RCSS: This is the styling language used to style the document, it is very similar to CSS2 but does differ in some places. - Data Model: This holds information (a Lua table) that is used in the UI code in data bindings.

On the Lua side, you are creating 1 or more contexts and adding data models and documents to them.

On the Rml side, you are creating documents to be loaded by the Lua which will likely include data bindings to show information from the data model, and references to lua functions for event handlers.

As each widget/component you create will likely comprise of several files (.lua, .rml & .rcss) you may find having a folder for each widget/component a useful way to organise your files.

    1. Getting Started

RmlUi is available in LuaUI (the game UI) with future availability in LuaIntro & LuaMenu planned, so it is already there for you to use. However, to get going with it there is some setup code that should be considered for loading fonts, cursors and configuring ui scaling, an example script is provided below. If you are working on a widget for an existing game, it is likely that the game already has some form of this setup code in, so you may be able to skip this section.

      1. Setup script

Here is the "[handler](https://github.com/beyond-all-reason/Beyond-All-Reason/blob/master/luaui/rml_setup.lua)", written by lov and ChrisFloofyKitsune, 2 of the people responsible for the RmlUi implementation.

```lua -- luaui/rml_setup.lua -- author: lov + ChrisFloofyKitsune -- -- Copyright (C) 2024. -- Licensed under the terms of the GNU GPL, v2 or later.

if (RmlGuard or not RmlUi) then return end -- don't allow this initialization code to be run multiple times RmlGuard = true

--[[ Recoil uses a custom set of Lua bindings (check out rts/Rml/SolLua/bind folder in the C++ engine code) Aside from the Lua API, the rest of the RmlUi documentation is still relevant https://mikke89.github.io/RmlUiDoc/index.html ]]

--[[ create a common Context to be used for widgets pros: * Documents in the same Context can make use of the same DataModels, allowing for less duplicate data * Documents can be arranged in front/behind of each other dynamically cons: * Documents in the same Context can make use of the same data models, leading to side effects * DataModels must have unique names within the same Context

If you have lots of DataModel use you may want to create your own Context otherwise you should be able to just use the shared Context

Contexts created with the Lua API are automatically disposed of when the LuaUi environment is unloaded ]]

local oldCreateContext = RmlUi.CreateContext

local function NewCreateContext(name) local context = oldCreateContext(name)

-- set up dp_ratio considering the user's UI scale preference and the screen resolution local viewSizeX, viewSizeY = Spring.GetViewGeometry()

local userScale = Spring.GetConfigFloat("ui_scale", 1)

local baseWidth = 1920 local baseHeight = 1080 local resFactor = math.min(viewSizeX / baseWidth, viewSizeY / baseHeight)

context.dp_ratio = resFactor * userScale

context.dp_ratio = math.floor(context.dp_ratio * 100) / 100 return context end

RmlUi.CreateContext = NewCreateContext

-- Load fonts local font_files = { } for _, file in ipairs(font_files) do Spring.Echo("loading font", file) RmlUi.LoadFontFace(file, true) end


-- Mouse Cursor Aliases --[[ These let standard CSS cursor names be used when doing styling. If a cursor set via RCSS does not have an alias, it is unchanged. CSS cursor list: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor RmlUi documentation: https://mikke89.github.io/RmlUiDoc/pages/rcss/user_interface.html#cursor ]]

-- when "cursor: normal" is set via RCSS, "cursornormal" will be sent to the engine... and so on for the rest RmlUi.SetMouseCursorAlias("default", 'cursornormal') RmlUi.SetMouseCursorAlias("pointer", 'Move') -- command cursors use the command name. TODO: replace with actual pointer cursor? RmlUi.SetMouseCursorAlias("move", 'uimove') RmlUi.SetMouseCursorAlias("nesw-resize", 'uiresized2') RmlUi.SetMouseCursorAlias("nwse-resize", 'uiresized1') RmlUi.SetMouseCursorAlias("ns-resize", 'uiresizev') RmlUi.SetMouseCursorAlias("ew-resize", 'uiresizeh')

RmlUi.CreateContext("shared") ```

What this does is create a unified context 'shared' for all your documents and data models, which is currently the recommended way to architect documents. If you have any custom font files, list them in `font_files`, otherwise leave it empty.

The setup script above can then be included from your `luaui/main.lua` (main.lua is the entry point for the LuaUI environment).

```lua VFS.Include(LUAUI_DIRNAME .. "rml_setup.lua", nil, VFS.ZIP) -- Runs the script ``` > [!NOTE] If you are working on a widget for an existing game, check whether the game already has a setup script — it may not include all of the pieces shown above (font loading, cursor aliases, context creation, dp_ratio).


      1. Keeping dp_ratio Updated

The setup script sets `dp_ratio` once at context creation time, but the user may resize the window or change their `ui_scale` config during a session. To keep `dp_ratio` correct you need to recalculate and reapply it whenever the viewport changes.

The `ViewResize` callin fires whenever the window is resized. Add it to your widget and update each context you own:

```lua function widget:ViewResize(newSizeX, newSizeY)

   local userScale = Spring.GetConfigFloat("ui_scale", 1)
   local baseWidth = 1920
   local baseHeight = 1080
   local resFactor = math.min(newSizeX / baseWidth, newSizeY / baseHeight)
   local dp = math.floor(resFactor * userScale * 100) / 100
   widget.rmlContext.dp_ratio = dp

end ```

If you have multiple contexts, update each one. [Mupersega's `rml_context_manager.lua`](https://github.com/beyond-all-reason/Beyond-All-Reason/blob/master/luaui/rml_context_manager.lua) in Beyond All Reason is a good reference implementation that handles context lifecycle and `dp_ratio` tracking together.

      1. Writing Your First Document

You will be creating files with .rml and .rcss extensions, as these closely resemble HTML and CSS it is worth configuring your editor to treat these file extensions as HTML and CSS respectively, see the [IDE Setup](#ide-setup) section for more information.

Now, create an RML file somewhere under `luaui/widgets/`, like `luaui/widgets/getting_started.rml`. This is the UI document.

Writing it is much like HTML by design. There are some differences, the most immediate being the root tag is called rml instead of html, most other RML/HTML differences relate to attributes for databindings and events, but for the time being, we don't need to worry about them.

By default RmlUi has *NO* styles, this includes setting default element behaviour like block/inline and styles web developers would expect like input elements default appearances, as a starting point you can use [RmlUi documentation](https://mikke89.github.io/RmlUiDoc/pages/rml/html4_style_sheet.html) though these do not include styles for form elements.

Here's a basic widget written by Mupersega.

```html <rml> <head>

   <title>Rml Starter</title>
   <style>
       #rml-starter-widget {
           pointer-events: auto;
           width: 400dp;
           right: 0;
           top: 50%;
           transform: translateY(-90%);
           position: absolute;
           margin-right: 10dp;
       }
       #main-content {
           padding: 10dp;
           border-radius: 8dp;
           z-index: 1;
       }
       #expanding-content {
           transform: translateY(0%);
           transition: top 0.1s linear-in-out;
           z-index: 0;
           height: 100%;
           width: 100%;
           position: absolute;
           top: 0dp;
           left: 0dp;
           border-radius: 8dp;
           display: flex;
           flex-direction: column;
           justify-content: flex-end;
           align-items: center;
           padding-bottom: 20dp;
       }
       /* This is just a checkbox sitting above the entirety of the expanding content */
       /* It is bound directly with data-checked attr to the expanded value */
       #expanding-content>input {
           height: 100%;
           width: 100%;
           z-index: 1;
           position: absolute;
           top: 0dp;
           left: 0dp;
       }
       #expanding-content.expanded {
           top: 90%;
       }
       #expanding-content:hover {
           background-color: rgba(255, 0, 0, 125);
       }
   </style>

</head> <body>

Welcome to an Rml Starter Widget

This is a simple example of an RMLUI widget.

               <button onclick="widget:Reload()">reload widget</button>
           <input type="checkbox" value="expanded" data-checked="expanded"/>

Template:Message

</body> </rml> ```

Let's take a look at different areas that are important to look at.

`

`

1. Here, we bind to the data model using `data-model`. This is what we will need to name the data model in our Lua script later. Everything inside the model will be in scope and beneath the div. 2. Typically, it is recommended to bind your data model inside of a div beneath `body` rather than `body` itself.

```html

   <input type="checkbox" value="expanded" data-checked="expanded"/>

``` any attribute starting with `data-` is a "data event". We will go through a couple below, but you can find out more here [on the RmlUi docs site](https://mikke89.github.io/RmlUiDoc/pages/data_bindings/views_and_controllers.html). 1. Double curly braces are used to show values from the data model within the document text. e.g. `Template:Message` shows the value of `message` in the data model. 2. `data-class-expanded="expanded"` applies the `expanded` class to the div if the value `expanded` in the data model is `true`. 3. `<input type="checkbox" value="expanded" data-checked="expanded"/>` This is a two-way binding: the checkbox reflects the current value of `expanded` in the data model, and toggling it writes back `true` or `false` to that field. 4. When the data model is changed, the document is rerendered automatically. So, the expanding div will have the `expanded` class applied to it or removed whenever the check box is toggled.

There are data bindings to allow you to loop through arrays (data-for) have conditional sections of the document (data-if) and many others.

      1. The Lua

> [!NOTE] For information on setting up your editor to provide intellisense behaviour see the [Lua Language Server guide](Template:% ref "lua-language-server" %).

To load your document into the shared context we created earlier and to define and add the data model you will need to have a lua script something like the one below.

```lua -- luaui/widgets/getting_started.lua if not RmlUi then

   return

end

local widget = widget ---@type Widget

function widget:GetInfo()

   return {
       name = "Rml Starter",
       desc = "This widget is a starter example for RmlUi widgets.",
       author = "Mupersega",
       date = "2025",
       license = "GNU GPL, v2 or later",
       layer = -1000000,
       enabled = true
   }

end

local document local dm_handle local init_model = {

   expanded = false,
   message = "Hello, find my text in the data model!",
   testArray = {
       { name = "Item 1", value = 1 },
       { name = "Item 2", value = 2 },
       { name = "Item 3", value = 3 },
   },

}

local main_model_name = "starter_model"

function widget:Initialize()

   widget.rmlContext = RmlUi.GetContext("shared") -- Get the context from the setup lua
   -- Open the model, using init_model as the template. All values inside are copied.
   -- Returns a handle, which we will touch on later.
   dm_handle = widget.rmlContext:OpenDataModel(main_model_name, init_model)
   if not dm_handle then
       Spring.Echo("RmlUi: Failed to open data model ", main_model_name)
       return
   end
   -- Load the document we wrote earlier.
   document = widget.rmlContext:LoadDocument("luaui/widgets/getting_started.rml", widget)
   if not document then
       Spring.Echo("Failed to load document")
       return
   end
   -- uncomment the line below to enable debugger
   -- RmlUi.SetDebugContext('shared')
   document:ReloadStyleSheet()
   document:Show()

end

function widget:Shutdown()

   widget.rmlContext:RemoveDataModel(main_model_name)
   if document then
       document:Close()
   end

end

-- This function is only for dev experience, ideally it would be a hot reload, and not required at all in a completed widget. function widget:Reload(event)

   Spring.Echo("Reloading")
   Spring.Echo(event)
   widget:Shutdown()
   widget:Initialize()

end ```

      1. The Data Model Handle

In the script, we are given a data model handle. This is a proxy for the Lua table used as the data model; as the Recoil RmlUi integration uses Sol2 as a wrapper data cannot be accessed directly.

In most cases, you can simply do `dm_handle.expanded = true`. Assigning to any field on the handle (or any nested table within it, at any depth) automatically marks the relevant parts of the model as dirty and triggers a UI update — you do not need to call `__SetDirty` manually.

What if you have an array, like `testArray` above, and want to loop through it on the Lua side? You will need to get the underlying table:

```lua local model_handle = dm_handle:__GetTable() ```

As of writing, this function is not documented in the Lua API, due to some problems with language server generics that haven't been sorted out yet. It is there, however, and will be added back in in the future. It returns the table with the shape of your initial model. You can then iterate through it and change things as you please:

```lua for i, v in pairs(model_handle.testArray) do

   Spring.Echo(i, v.name)
   v.value = v.value + 1

end ```

> [!NOTE] Use `pairs` rather than `ipairs` when iterating over data model arrays on the Lua side. The model handle does not currently implement `__len`, so `ipairs` will not traverse the array correctly. This will be resolved once `__len` is backported.

      1. Debugging

RmlUi comes with a debugger that lets you see and interact with the DOM. It's very handy! To use it, use this:

```lua RmlUi.SetDebugContext(DATA_MODEL_NAME) ```

And it will appear in-game. A useful idiom I like is to put this in `rml_setup.lua`:

```lua local DEBUG_RMLUI = true

--...

if DEBUG_RMLUI then

   RmlUi.SetDebugContext('shared')

end ```

If you use the shared context, you can see everything that happens in it! Neat!

    1. Things to Know and Best Practices
      1. Things to know

Some of the rough edges you are likely to run into have already been discussed, like the data model thing, but here are some more:

---

Unlike a web browser a default set of styles is not included, as a starting point you can look at the [RmlUi documentation](https://mikke89.github.io/RmlUiDoc/pages/rml/html4_style_sheet.html) though this doesn't provide styles for form elements.

Input elements of type submit & button behave differently to HTML and more like Button elements in that their text is not set by the value attribute. (This is likely to be corrected in a future version)

The alpha/transparency value of an RGBA colour is different to CSS (0-1) and instead uses 0-255. The css opacity does still use 0-1.

List styling is unavailable (list-style-type etc.), you can still use UL/OL/LI elements but there is no special meaning to them, whilst you could use background images to replicate bullets there isn't a practical way to achieve numbered list items with RML/RCSS.

Only solid borders are supported, so the border-style property is unavailable and the shorthand border property doesn't include a style part (```border: 1dp solid black;``` won't work, instead use ```border: 1dp black;```).

background-color behaves as expected, all other background styles are different and use decorators instead see the [RmlUi documentation for more information on decorators.](https://mikke89.github.io/RmlUiDoc/pages/rcss/decorators.html)

There are two kinds of events: data events, like `data-mouseover`, and normal events, like `onmouseover`. These have different data in their scopes. - Data events have the data model in their scope. - Normal events don't have the data model, but they *do* have whatever is passed into `widget` on `widget.rmlContext:LoadDocument`. `widget` doesn't have to be a widget, just any table with data in it.

For example, take this:

```lua local model = {

   add = function(a, b)
       Spring.Echo(a + b)
   end

} local document_table = {

   print = function(msg)
       Spring.Echo(msg)
   end,

}

dm_handle = widget.rmlContext:OpenDataModel("test", model) document = widget.rmlContext:LoadDocument("document.rml", document_table) ```

```html

Normal Events

<input type="button" onclick="add(1, 2)">Won't work!</input> <input type="button" onclick="print('test')">Will work!</input>

Data Events

<input type="button" data-click="add(1, 2)">Will work!</input> <input type="button" data-click="print('test')">Won't work!</input> ```

      1. Best Practices

- To create a scalable interface the use of the dp unit over px is recommended as the scale can be set per context with SetDensityIndependentPixelRatio. - For styles unique to a document, put them in a `style` tag. For shared styles, put them in an `rcss` file. - Rely on Recoil's RmlUi Lua bindings doc for what you can and can't do. The Recoil implementation has some extra stuff the RmlUi docs don't. - The Beyond All Reason devs prefer to use one shared context for all rmlui widgets.


      1. Differences between upstream RmlUI and RmlUI in Recoil

- The SVG element allows either a filepath or raw SVG data in the src attribute, allowing for inline svg to be used (this may change to svg being supported between the opening and closing tag when implemented upstream) - An additional element ```<texture>``` is available which allows for textures loaded in Recoil to be used, this behaves the same as an ```<img>``` element except the src attribute takes a [texture reference](Template:% ref "articles/texture-reference-strings" %)


      1. IDE Setup

To get the best experience with RmlUi, you should set up your editor to use HTML syntax highlighting for .rml file extensions and CSS syntax highlighting for .rcss file extensions.

In VS Code, this can be done by opening a file with the extension you want to setup, then clicking on the language mode in the bottom right corner of the window (probably shows as Plain Text). From there, you can select "Configure File Association for '.rml'" from the top menu that appears and choose "HTML" from the list. Do the same for .rcss files, but select "CSS".

[RmlUI website]: https://mikke89.github.io/RmlUiDoc/