Folio Docs
Fogspire/Folio
git clone https://github.com/Fogspire/Folio.git Plugins/Folio

Images and sprites

Icons, panel skins and bar fills drawn from one spritesheet, so a screen is one texture and one draw.

Art reaches a document as an <img> element or an image decorator on any element, and both name a sprite rather than a file.

Spritesheets

RCSS
@spritesheet hud
{
	src: /UI/art/hud.png;
	resolution: 1x;

	icon_health:  0px   0px  48px 48px;
	icon_shield: 48px   0px  48px 48px;
	panel_frame:  0px  48px 128px 96px;
}

resolution lets one sheet serve several DPIs: declare a 2x sheet and RmlUi halves the sprite's natural size, so <img sprite="icon_health"/> is 24dp either way.

Using one

RML
<img sprite="icon_health"/>
<img src="/UI/art/loose.png"/>
<img src="/UI/art/sheet.png" rect="0 0 48 48"/>

An <img> has an intrinsic size, the only aspect ratio in RCSS (no aspect-ratio property exists). Set width or height and the other follows. Set both and it stretches. It is also how a vector layer stack gets a height; see layout.

The image decorator

For a surface rather than a picture: a panel background, a button skin, a bar fill.

RCSS
.panel  { decorator: image(panel_frame); }
.badge  { decorator: image(icon_star cover); }
.stripe { decorator: image(stripe repeat); }

Fit modes: fill (the default, stretch), contain, cover, scale-none, scale-down, repeat. These replace object-fit, which does not exist.

A repeating source is always its own file, not a sprite. A tile is its texture's pixel size in dp, so it keeps its size across resolutions.

ninepatch for anything that must stretch without distorting its corners, such as panels and buttons:

RCSS
.panel { decorator: ninepatch(panel_frame, panel_frame_inner); }

The second sprite names the inner rectangle; everything outside it is a fixed corner or edge.

tiled-horizontal, tiled-vertical, tiled-box tile a middle section between fixed ends. A tiled-box is a ninepatch that repeats rather than stretches.

Multiple decorators paint in reverse source order: the first named is on top, like CSS background-image layers:

RCSS
/* The sheen is on top, where it was meant to be. */
.btn { decorator: shader("sweep #ffffffb0 0.35 0.3 15"), image(btn_base); }

Colouring

image-color multiplies a sprite by a colour, so one white icon covers every state:

RCSS
.icon           { image-color: #8a8a99; }
.item:hover .icon { image-color: #e8e8f0; }
.icon.danger    { image-color: #e06c75; }

Author single-colour art white and tint it: one sprite instead of four, themeable, and image-color transitions.

Gradients without art

Every gradient decorator works in both renderers: gradient, horizontal-gradient, vertical-gradient, linear-gradient, repeating-linear-gradient, radial-gradient, repeating-radial-gradient, conic-gradient, repeating-conic-gradient.

RCSS
.fade { decorator: vertical-gradient(#00000000 #000000c0); }

A gradient is cheaper than a PNG of a gradient, resolution independent, and one line to retune.

When to use a PNG rather than a vector

Raster is the default. Use vector art only for something that has to survive extreme magnification, such as a HUD icon that also appears on a full-screen map, or a world-space surface a player can walk up to. Raster is one draw for the whole sheet; a vector costs a draw per colour layer.

Photographic or many-coloured art has no vector option: a distance field is coverage and nothing else.

Colour values

#rgb, #rgba, #rrggbb and #rrggbbaa all work. hsl(), hsla(), lab(), lch(), oklab(), oklch() and the HTML colour names are accepted too.

Textures the game provides

A scene capture, a rendered inventory item or a video frame has no path for a document to name. Register the texture and name it with the folio:// scheme:

C++
Document->RegisterTexture(TEXT("minimap"), CaptureTarget);
RML
<img src="folio://minimap"/>
RCSS
.portrait { decorator: image(folio://inv/portrait); }

It works with <img>, every image-family decorator, mask-image and image-color. The texture is any UTexture: a render target, a streamed texture or a UTexture2D built at runtime.

UFolioWidget::RegisterTexture releases its names when the widget is torn down. UFolioTextureLibrary (Blueprint) and Folio::RegisterTexture (C++) reach the same registry for callers with no widget to hand, and those do not clean up after themselves.

Re-registering a name swaps the texture everywhere it is drawn, including in a document that is already up.

Names are global: Folio has one render manager for the whole application, so prefix them by screen (hud/minimap, inv/portrait), as with sprite names.

It cannot exist in the preview

uipreview has no engine, so it draws a stand-in. Put a PNG at UI/art/preview/<name>.png and it draws that; leave it out and you get the usual named placeholder and one I-ASSET-GAMETEXTURE line. Either way the element occupies the right space.

It is reported as info rather than a warning, so a screen using a game texture still passes --strict.

The capture behind it

A paper doll, and 3D items in a grid covers isolating one without a staging area, lighting it apart from the level, the premultiply step a cutout needs, and driving a page of inventory cells from one rig.

Missing art

An unresolved sprite renders as a magenta placeholder with its name stamped across it, and the name accumulates in ASSETS_WANTED.md. Screens can be built before any art exists, and the artist gets a list with the sizes each thing was drawn at.

--strict turns a missing sprite into a failing run: use it in CI, not while building.

Re-baking

RmlUi caches a texture by its path and never looks at the file again, so a re-packed atlas needs releasing:

Reopening a documentpicks up anything whose timestamp moved
folio.ReloadTexturesthe same, without reopening
folio.ReloadTextures allreleases every texture whether or not it looks changed
uipreviewhandles it through its file watcher; needs nothing

If it looks wrong

background-image does nothing. It parses and is ignored. Use decorator: image(...).

A repeat, repeat-x or repeat-y decorator draws nothing and logs a warning. The repeat family cannot name a sprite: it draws one quad and leaves the wrapping to the sampler, and a sub-rectangle of an atlas has nothing to wrap against. RmlUi refuses it in both renderers. Point it at its own file.

A tiled surface reads as wallpaper. A 960dp tile across a 1920dp screen draws the same thing twice. For a soft, featureless surface, stretch one tile with fill and give the art the box's aspect ratio. examples/tiling.rml is the case in full.

An effect in a decorator list does nothing. It is listed after an opaque background, so it is painted underneath it. Name the effect first.

A half-transparent rgba() colour is invisible. rgba()'s alpha channel is 0-255, not 0-1, so rgba(0, 0, 0, 0.5) parses to fully transparent. Write #0000007f.

A game texture shows another screen's picture. Two documents named the same texture and the last registration won. The log says texture '<name>' was registered as '<old>' and is now '<new>'. Names are global, so every document asking for it gets the new one. Prefix by screen to keep them apart.

A game texture is a placeholder in uipreview. The preview has no engine. Put a PNG at UI/art/preview/<name>.png to draw that instead.

A magenta box with a sprite's name across it. The sprite does not resolve. Its name is listed in ASSETS_WANTED.md with the size it was drawn at; add the art and re-pack.

A re-packed atlas still shows the old art. RmlUi caches a texture by its path. Reopen the document or run folio.ReloadTextures, as in re-baking.

Where to go next