-
-
Notifications
You must be signed in to change notification settings - Fork 306
HTML CSS Integration Guide
This system interprets HTML/CSS and converts them into widgets.
It is inspired by browser behavior but includes simplifications and custom extensions for integration with Lua scripts and OTML styles.
Every HTML file used in the engine must have its content wrapped inside the root <html>
tag.
This defines the widget tree root for that screen/module.
<style>
.content {
background-color: rgb(84, 36, 36);
width: 200px;
height: 200px;
text-align: center;
align-items: center;
font-size: 15;
border: 1 chocolate;
}
</style>
<script type="text">
self.welcomeMsg = 'Welcome to OTC'
</script>
<html>
<div class="content">{{self.welcomeMsg}}</div>
</html>
- Use
<style>
for CSS definitions specific to the screen. - Use
<script>
only for temporary inline Lua logic (best practice is to keep logic in the controller). - The
<html>
root is mandatory, everything rendered must be placed inside it.
Efficiently managing when and how HTML is loaded in modules is essential for performance and resource usage.
It may be tempting to always call:
self:loadHtml("<path>")
inside the onInit
of every module. However, if you do this for all modules, every HTML layout will be loaded into memory at game startup.
- ❌ Unnecessary memory consumption
- ❌ Longer startup times
- ❌ Keeps UI that the player may never open
Rule: Only load HTML in onInit
if the module is core/essential (e.g., HUD).
Some modules must be ready as soon as the player enters the game world:
function Controller:onGameStart()
self:loadHtml("inventory.html")
end
This loads only after the game has started. When you leave the game screen, the module is automatically destroyed.
For optional modules like Quest Log / Settings:
-- Open
self:loadHtml("questlog.html")
-- Close
self:unloadHtml()
This keeps the UI in memory only when needed.
The engine supports the standard HTML <link>
tag to load CSS files from your module:
<link href="questlog.css" />
-
href
points to the CSS file inside the same module - Keeps HTML clean, avoids huge inline
<style>
blocks or long meta sections - Improves readability and maintainability
- ✅ Core: load in
onInit
- ✅ In-game start: load in
onGameStart
- ✅ On-demand:
loadHtml("<path>")
to open,unloadHtml()
to close - ✅ CSS separation:
<link href="file.css" />
- Supported CSS Properties
- Special Attributes with
*
(Lua Bind) -
Events (no
*
required) - Query Selector Support
- OTML Integration
- New Methods
- Notes
-
Supported:
block
,inline
,inline-block
,none
. - Behavior: Controls line breaking, full-width blocks, and layout containers.
-
Supported:
visible
,hidden
-
Behavior:
-
visible
: the element appears normally. -
hidden
: the element becomes invisible, but still takes up layout space.
-
display: none;
-
visibility: hidden
→ the element still exists in the layout tree and reserves its space, it is only not rendered. -
display: none
→ the element is completely removed from the flow/layout, as if it does not exist, reserving no space.
-
Supported:
static
,relative
,absolute
. -
Behavior:
-
static
: default, the element follows the normal flow. -
relative
: offsets the element relative to its original position, without removing it from the flow. -
absolute
: positions the element relative to the nearest positioned ancestor (or the root).
-
⚠️ Limitation: Values likefixed
andsticky
are not supported.
-
Supported:
left
,right
,none
. - Behavior: Floats left/right; siblings wrap accordingly.
⚠️ Difference: Ignored insideflex
,grid
, andtable
.
-
Supported:
left
,right
,both
,none
. - Behavior: Forces element below preceding floats.
-
Units:
px
,%
,auto
,fit-content
. -
Behavior:
-
px
: fixed sizes -
%
: relative to parent -
auto
: expands/uses content depending on display -
fit-content
: sizes to content
-
⚠️ Difference:%
andfit-content
calculations are simplified.
-
Supported:
left
,right
,center
,justify
. - Behavior: Aligns inline children horizontally.
⚠️ Difference: May not reproduce all browser nuances.
-
Supported:
left
,right
,center
. - Behavior: Basic child alignment inside the parent (reduced vs. full CSS).
-
Supported:
center
. - Behavior: Vertical centering of children.
-
Supported:
visible
,hidden
,clip
,scroll
-
Behavior:
-
visible
: Draws all children, even if they overflow the parent borders. -
hidden
,clip
: Children that overflow the parent borders are cut off and not displayed. -
scroll
: Same ashidden/clip
, but also shows a vertical scrollbar so the user can scroll to see the rest of the children.
-
⚠️ Current limitation: Only vertical scroll is supported.
-
Supported:
none
,auto
(or any value other thannone
) -
Behavior (standard CSS):
- Controls how the element responds to pointer events (mouse, touch, pen).
- In browsers, it can affect both SVG and HTML elements.
- In our engine, we focus on the practical use case: allowing or blocking mouse interactions.
The engine now supports full HTML table layout, including proper alignment between <th>
and <td>
across header and body sections.
View table layout reference
Element | Display Type |
---|---|
<table> |
table |
<thead> |
table-header-group |
<tbody> |
table-row-group |
<tfoot> |
table-footer-group |
<tr> |
table-row |
<td> |
table-cell |
<th> |
table-cell |
<colgroup> |
table-column-group |
<col> |
table-column |
<caption> |
table-caption |
-
text-align
,vertical-align
-
width
,height
,min-width
,max-width
-
padding
,margin
,border
- Automatic fit-content sizing for rows, columns, and cells
<table>
<thead>
<tr><th>Item</th><th>Qty</th><th>Price</th></tr>
</thead>
<tbody>
<tr><td>Sword</td><td>1</td><td>150 gp</td></tr>
<tr><td>Shield</td><td>1</td><td>120 gp</td></tr>
</tbody>
</table>
Inside any text content, you can use double curly braces {{ ... }}
to embed Lua expressions.
When the HTML is parsed, the expression is evaluated in the controller context and replaced with its value.
<div>Name: {{self.player.name}}, Age: {{self.player.age}}</div>
If in Lua you have:
self.player = { name = "Renato", age = 28 }
The widget will render as:
Name: Renato, Age: 28
- Expressions are evaluated in the same Lua context as attributes and events (
self
,target
, etc.). - Useful for dynamic text binding without needing an explicit
*text
attribute. - The evaluation happens at parse time or when the bound value changes.
The engine also supports the <script></script>
tag inside HTML files.
This allows you to run Lua code directly inside HTML.
<script type="text">
self.player = {
name = 'Renato',
age = 37
}
</script>
- Not recommended for production code, since it mixes Lua logic with HTML layout.
- Best used for temporary tests or quick prototypes.
- For maintainability, prefer defining logic in your Lua controller instead of inline
<script>
.
These attributes bind HTML directly to Lua.
Most *
attributes (e.g., *text
, *if
) are execution bindings:
- They evaluate the expression on the right-hand side
- And apply it to the widget (calling setters internally)
Example:
<label *text="self.title" />
This executes self.title
and sets it as label text (setText(self.title)
).
These two are assignment bindings, not execution.
They behave in the opposite direction: the element’s value/state is written back to the Lua variable.
-
*value
<input name="text" *value="self.name" />
If the user types Renato, the system assigns:
self.name = "Renato"
-
*checked
<input type="checkbox" *checked="self.isEnabled" />
If the user checks the box, the system assigns:
self.isEnabled = true
In other words:
-
*text
,*if
, etc. → Lua → Widget (execution/assignment to widget) -
*value
,*checked
→ Widget → Lua (assignment from element to variable)
- Renders the element only if the condition is true.
- Otherwise, the element does not exist in the layout (does not take up space).
<div *if="self.debugMode">
Debug Info: {{self.debugData}}
</div>
- The element always exists in the layout.
- If the condition is false, it becomes invisible, but still takes up space.
<!-- Keeps the table cell aligned even when empty -->
<span *visible="self.hasDiscount">
-{{self.discount}}%
</span>
Creates elements reactively based on a Lua list.
When the list is modified (insertion, removal, or update), the HTML is automatically updated without reloading the entire screen.
<div *for="local player in self.players">
<div class="player-item">
<uicreature *outfit="{type = player.lookType}" center="true"></uicreature>
<div class="playerName">{{player.name}}</div>
<button onclick="self:removePlayer(index)">Remove</button>
</div>
</div>
The expression follows this format:
*for="local <variable> in <table>"
Example:
*for="local player in self.players"
The inner content is rendered once for each element in the list.
When the list changes, only the affected elements are updated — without rebuilding the entire interface.
Inside a *for
block, you can access:
-
index
→ zero-based index
Variables like
first
,last
,even
, andodd
are not yet available, but will be added in future versions.
You can create aliases for the index if you wish to rename it:
<div *for="local player in self.players; local i = index">
<label>Player {{i + 1}}: {{player.name}}</label>
</div>
<div class="list">
<div *for="local item in self.items" onclick="print('Clicked', item)">
{{item}}
</div>
</div>
Perfect for dynamic lists such as inventories, player groups, menus, or rankings.
-
self
→ controller -
target
→ the widget where the attribute is set
Events themselves do not require
*
.
Declare events as standard HTML attributes:
<input onchange="print(self, target, event.value)">
You can call controller methods directly:
HTML
<button onclick="self:close()">Close</button>
Lua controller
ModuleController = Controller:new()
function ModuleController:close()
self:unloadHtml()
end
-
self
→ controller -
target
→ widget that triggered the event -
event
→ event object
Base fields:
-
event.name
→ event name -
event.value
→ main value (when applicable)
For onChange
, fields depend on the widget class:
-
UIComboBox
event.name = 'onOptionChange'
-
event.text
→ selected option text -
event.data
→ selected option data
-
UIRadioGroup
event.name = 'onSelectionChange'
-
event.selectedWidget
→ newly selected widget -
event.previousSelectedWidget
→ previously selected widget
-
UICheckBox
event.name = 'onCheckChange'
-
event.checked
→ boolean checked state
-
UIScrollBar
event.name = 'onValueChange'
-
event.value
→ current value -
event.delta
→ amount of change
-
Widgets with
setValue
event.name = 'onValueChange'
-
event.value
→ updated value
-
Other widgets (default)
event.name = 'onTextChange'
-
event.value
→ updated text
Category | Event | Description |
---|---|---|
Style & Lifecycle | onStyleApply |
A style is applied |
onDestroy |
Widget destroyed | |
onIdChange |
Widget ID changed | |
onWidthChange |
Width changed | |
onHeightChange |
Height changed | |
onResize |
Widget resized | |
onEnabled |
Enabled/disabled changed | |
onPropertyChange |
Any property changed | |
onGeometryChange |
Size/position changed | |
onLayoutUpdate |
Layout updated | |
onCreate |
Widget created | |
onSetup |
Widget set up | |
Focus & Visibility | onFocusChange |
Focus gained/lost |
onChildFocusChange |
Child focus changed | |
onHoverChange |
Hover state changed | |
onVisibilityChange |
Visibility changed | |
Drag & Drop | onDragEnter |
Drag entered |
onDragLeave |
Drag left | |
onDragMove |
Dragged over | |
onDrop |
Dropped | |
Keyboard | onKeyText |
Text input |
onKeyDown |
Key pressed | |
onKeyPress |
Key held | |
onKeyUp |
Key released | |
onEscape |
Escape pressed | |
Mouse | onMousePress |
Mouse button down |
onMouseRelease |
Mouse button up | |
onMouseMove |
Mouse moved | |
onMouseWheel |
Wheel scrolled | |
onClick |
Click | |
onDoubleClick |
Double click | |
Text & Fonts | onTextAreaUpdate |
Text area updated |
onFontChange |
Font changed | |
onTextChange |
Text changed |
The engine supports CSS query selectors (almost full coverage), similar to browsers.
controller:findWidget("css-query") -- first match
controller:findWidgets("css-query") -- all matches
Examples:
controller:findWidget("#loginButton")
controller:findWidgets("div.content")
:hover
works like in browsers and is treated as an event selector:
button:hover {
background-color: red;
}
When the user hovers a button, the style is applied dynamically.
Beyond CSS properties, the system supports OTML properties and styles.
- If a property exists in both CSS and OTML (e.g.,
width
,margin
,padding
), use it as-is (no prefix). - If a property exists only in OTML, prefix it with
--
to mark it as a custom OTML property (recognized by IDEs).
/* Shared CSS/OTML property */
div { width: 200px; }
/* OTML-only property */
div { --text-auto-resize: true; }
Use OTML styles either as tags or classes:
<window></window> <!-- as tag -->
<div class="window"></div> <!-- as class -->
<div class="window dark-theme bordered"></div> <!-- mix multiple OTML/CSS styles -->
Widget methods starting with set
map to kebab-case properties:
-
setFixedSize
→fixed-size
<div fixed-size="true"></div>
-
setTextAutoResize
→text-auto-resize
<div text-auto-resize="true"></div>
-
controller:createWidgetFromHTML(html, parent)
Creates a new widget from an HTML string.-
html
: string with HTML markup -
parent
: optional parent widget; ifnil
, creates at root
-
Example
local html = [[
<button onclick="self:onClick()">Click Me</button>
]]
local btn = controller:createWidgetFromHTML(html, parentWidget)
Insert and manage children using HTML strings or remove them via CSS queries:
-
widget:append(html)
Insert children at the end of the widget. -
widget:prepend(html)
Insert children at the start of the widget. -
widget:insert(index, html)
Insert children at a specific position (1-based index). -
widget:remove(cssQuery)
Remove all child elements matching the given CSS query selector (ID, class, type, etc.). -
widget:querySelector(cssQuery)
Returns the first child widget (recursively) that matches the given CSS selector.
Works exactly likecontroller:findWidget
, but restricted to the subtree of the widget. -
widget:querySelectorAll(cssQuery)
Returns all child widgets (recursively) that match the given CSS selector.
Works exactly likecontroller:findWidgets
, but restricted to the subtree of the widget.
Example
local container = controller:findWidget("#content")
-- Append to the end
container:append("<label class='line'>New Label</label>")
-- Find first matching child inside container
local line = container:querySelector(".line")
-- Find all matching children inside container
local allLines = container:querySelectorAll(".line")
-- Remove all <label> children
container:remove("label")
- The engine is inspired by CSS/HTML, but not a full browser. Some behaviors are simplified.
- Attributes with
*
(*value
,*if
), the event system,querySelector
, and OTML integration are custom extensions of this UI framework. - Load HTML via
self:loadHtml("<path>")
where<path>
is the file name inside the module (e.g.,questlog.html
). - Link CSS via
<link href="file.css" />
inside your module HTML.