# raylib-jamstack — RmlUi Ruby API Specification (`Rml::`)
The UI layer. [RmlUi](https://github.com/mikke89/RmlUi) is an HTML/CSS-derived UI
library with a model-view-controller **data binding** system. In this stack it is
the *only* UI layer (no raygui), rendered on top of raylib through raylib's GL
context, and driven from Ruby.
This follows the same design rules as `API_SPEC.md` (snake_case, `?` predicates,
`=` setters, Begin/End → blocks, keyword args, structs → classes, block-safety).
The mental model mirrors RmlUi's own MVC:
- **Model** = a Ruby object / hash whose fields are bound to the UI.
- **View** = `.rml` (markup) + `.rcss` (stylesheet) files.
- **Controller** = Ruby callbacks invoked from `data-event-*` attributes in the RML.
---
## 1. Initialization & the raylib bridge
RmlUi needs a render backend and a system interface (timing, input). In this stack
those are implemented in C against raylib (GL + `Rl.time` + raylib input), so the
Ruby author never sees them — they just call `Rml.init`.
```ruby
Rml.init # wires RmlUi to raylib's GL context + system clock
Rml.load_font("ui/LatoLatin-Regular.ttf")
Rml.load_font("ui/font.ttf", fallback: true)
Rml.shutdown # auto on Rl.close_window
```
> `Rml.init` must be called **after** `Rl.init_window` (it needs the GL context).
> The binding asserts this and raises a clear Ruby error otherwise.
---
## 2. `Rml::Context`
A context owns documents and dispatches input. Most games need exactly one.
```ruby
ctx = Rml::Context.new("main", width: 900, height: 675)
# width/height default to the current window size (Rl) when omitted:
ctx = Rml::Context.new("main")
ctx.dimensions = Rl::Vector2.new(1280, 720) # on window resize
ctx.update # process data-model changes + layout (Context::Update)
ctx.render # draw the UI (Context::Render)
ctx.process_input # feed raylib mouse/keyboard/text into RmlUi this frame
```
### Where it goes in the frame
`process_input` runs at the top of the frame (before game update). `update` +
`render` run *inside* the `Rl.draw` block so the UI composits over the game:
```ruby
Rl.while_window_open do
ctx.process_input
# ... game update ...
Rl.draw(clear_color: Rl::BLACK) do
# ... game render ...
ctx.update
ctx.render # UI on top
end
end
```
> **Block-form convenience.** `ctx.frame { ... }` wraps `process_input` (pre) and
> `update`+`render` (post) around the block, enforcing correct ordering:
> ```ruby
> Rl.while_window_open do
> ctx.frame do
> Rl.draw(clear_color: Rl::BLACK) { game.render }
> end
> end
> ```
---
## 3. Documents — `Rml::Document`
```ruby
doc = ctx.load_document("ui/hud.rml") # LoadDocument
doc.show # ElementDocument::Show
doc.hide
doc.close
doc.visible?
doc.title # from
in the RML
doc.reload # hot-reload markup+styles (dev convenience)
```
> **Improvement / jam-friendly:** `ctx.load_document` accepts a block that auto-shows
> and yields the document:
> ```ruby
> ctx.load_document("ui/menu.rml") { |d| d.show }
> ```
---
## 4. Data binding — the core feature
This is the reason to use RmlUi over raygui. A **data model** binds Ruby state to
named variables referenced in the RML via `data-*` attributes.
### 4.1 Defining a model
```ruby
model = ctx.data_model("hud") do |m|
# one-way (view): a getter lambda, re-read whenever the model is updated
m.bind(:hp) { player.hp }
m.bind(:max_hp) { player.max_hp }
m.bind(:score) { game.score }
# two-way: bind a plain mutable value; UI inputs write back into Ruby
m.value(:volume, 0.5) #
m.value(:player_name, "Orc")
# arrays / lists (RmlUi data-for)
m.bind(:invaders) { game.invaders } # each element exposes its own fields
# controller callbacks: invoked from data-event-* in the RML
m.event(:reset) { game.reset }
m.event(:launch) { |ev| game.launch(ev["mouse_x"], ev["mouse_y"]) }
end
```
Corresponding RML:
```html
HP: {{hp}} / {{max_hp}}
Score: {{score}}
{{inv.name}} — {{inv.danger}}
```
### 4.2 Notifying the UI of changes (`DirtyVariable`)
RmlUi does not poll Ruby every frame; you tell it what changed.
```ruby
model.dirty(:hp) # DataModelHandle::DirtyVariable("hp")
model.dirty(:hp, :score) # several at once
model.dirty_all # DataModelHandle::DirtyAllVariables
```
> **Improvement over raw RmlUi ergonomics:** `model.dirty(:hp)` returns the model
> so calls chain, and `ctx.update` is what actually flushes them to the view.
> One-way `bind` getters are only re-invoked for variables marked dirty (or after
> `dirty_all`), so binding to expensive getters is cheap.
### 4.3 Reading back two-way values
```ruby
model[:volume] # => current Float, reflecting any UI edits
model[:volume] = 0.8 # set from Ruby; auto-marks dirty
```
### 4.4 Mapping Ruby types to RmlUi variants
| Ruby | RmlUi data variable |
|-----------------|---------------------|
| `Integer`/`Float` | numeric scalar |
| `String`/`Symbol` | string scalar |
| `true`/`false` | bool scalar |
| `Array` | data array (`data-for`) |
| `Hash` / object responding to readers | struct (`x.field`) |
| `Proc`/lambda (in `bind`) | computed one-way scalar |
For `Hash`/object structs, the binding exposes keys / public reader methods as
`{{inv.field}}`. Nested arrays/structs are supported.
---
## 5. Events from RML → Ruby
Two ways UI interaction reaches Ruby:
1. **Data events** (preferred, MVC): `data-event-click="reset()"` → the `:reset`
callback registered with `m.event`. The callback receives an event object:
```ruby
m.event(:launch) do |ev|
ev.type # => "click"
ev["mouse_x"] # event parameters (GetParameter)
ev.target # => Rml::Element that fired it
end
```
2. **Direct element listeners** (escape hatch):
```ruby
btn = doc.element("#start") # GetElementById
btn.on(:click) { game.start }
```
---
## 6. Elements — `Rml::Element` (escape hatch)
Most UI is declarative, but direct manipulation is available:
```ruby
el = doc.element("#hp_bar") # by id (GetElementById)
els = doc.elements(".enemy") # by selector (GetElementsByTagName/QSA)
el.text = "Game Over" # inner RML/text
el.set_attribute("class", "dead")
el.get_attribute("class")
el.add_class("hidden"); el.remove_class("hidden")
el.style["width"] = "200px" # inline style property
el.visible = false
el.on(:click) { ... } # AddEventListener
```
---
## 7. Input routing detail
`ctx.process_input` translates raylib input into RmlUi each frame:
- `Rl.mouse_position` → `Context::ProcessMouseMove`
- mouse buttons → `ProcessMouseButtonDown/Up`
- `Rl.mouse_wheel` → `ProcessMouseWheel`
- key events + Unicode text → `ProcessKeyDown/Up` + `ProcessTextInput`
```ruby
ctx.input_enabled = false # let the game swallow input (e.g. gameplay vs menu)
ctx.hovered? # true if pointer is over a non-transparent UI element
```
> **`ctx.hovered?` is the key jam helper:** gate gameplay clicks behind
> `next if ui.hovered?` so clicking a button doesn't also fire in the game world.
---
## 8. Canonical UI shape (full example)
```ruby
Rl.init_window(900, 675, "Orc")
Rml.init
Rml.load_font("ui/Lato-Regular.ttf")
ui = Rml::Context.new("main")
model = ui.data_model("hud") do |m|
m.bind(:hp) { Player.hp }
m.value(:volume, 0.5)
m.event(:reset) { Game.reset }
end
hud = ui.load_document("ui/hud.rml")
hud.show
Rl.while_window_open do
ui.process_input
Game.update unless ui.hovered?
Rl.set_master_volume(model[:volume])
model.dirty(:hp)
Rl.draw(clear_color: Rl::BLACK) do
Game.render
ui.update
ui.render
end
end
```
---
## 9. Block-safety & lifetime (consistent with core spec)
- `ctx.frame`, `ctx.data_model`, `ctx.load_document {…}` are block forms and honor
the §5 block-safety contract from `API_SPEC.md`.
- `Rml::Context`, `Rml::Document`, fonts hold native handles; they are GC-finalized
and also expose explicit teardown (`ctx.close`, `doc.close`). `Rml.shutdown` runs
automatically at `Rl.close_window`.
---
## 10. Settled decisions
- RmlUi is the **sole** UI layer (no raygui).
- The render/system backend is C-against-raylib and hidden from Ruby.
- Data binding (MVC) is the primary interaction model; direct element access is the
documented escape hatch.
## 11. Open implementation notes (not Ruby-facing)
- RmlUi's GL2/GL3 sample backend can be adapted, but raylib owns the GL context, so
the render interface must use `rlgl` (raylib's GL abstraction) to stay compatible
with both desktop GL and WebGL under emscripten. Flagged here for the build phase.