| layout | default |
|---|---|
| title | XML syntax |
lua/ui/xml.lua is the source of truth for the cross-platform XML registry.
Templates are processed by etlua first and then compiled into native AppKit or
UIKit views. The same template can therefore be rendered with ns = require("AppKit")
or ns = require("UIKit").
Templates are the only place application view structure is defined. Controllers
provide data, render a template, retain its refs, and bind behavior to those
refs. See application architecture for the
model/controller/view boundary.
Render a file with:
local xml = require("ui.xml")
local view, refs = xml.renderFile("demo/mail/views/Window.etlua", data, ns)XML attributes are strings when parsed. The registry coerces layout numbers and boolean values for supported attributes. Unknown tags are errors; unsupported attributes are ignored unless the tag documents them below.
Use WPF layout conventions as the reference when an XML API design is ambiguous. Keep camelCase names and the SwiftUI-style native behavior documented here.
Use width and height for fixed dimensions, minWidth/minHeight for
minimums, and maxWidth/maxHeight for maximums. Set a maximum to infinity
to expand along that axis, corresponding to SwiftUI's .frame(maxWidth: .infinity) or .frame(maxHeight: .infinity). Omitting a dimension keeps the
control's natural sizing behavior. Fixed dimensions must be finite,
nonnegative numbers; zero is supported.
fixedSize="vertical" (or horizontal, both) is SwiftUI's
.fixedSize(horizontal:vertical:): on that axis the view keeps its content
size instead of growing into or shrinking with the proposal, while its own
maxHeight="infinity" children still fill it. A row of panels therefore
matches its tallest panel without taking the window's spare height:
<HStack spacing="16" alignment="top" fixedSize="vertical">
<GlassEffect maxHeight="infinity">…</GlassEffect>
<GlassEffect maxHeight="infinity">…</GlassEffect>
</HStack>
<VStack padding="20" spacing="14" alignment="leading">
<Image path="<%= game.cover %>" maxWidth="infinity" height="280"
contentMode="fill" cornerRadius="16" />
</VStack>
Omit sizing wherever the SwiftUI reference omits it. A stack derives its size
from its children; labels keep their intrinsic width and wrap against the
parent proposal. Use stack alignment="leading" for leading text, not an
infinite width on each label. Text fields accept available width by default.
Gradients fill both proposed dimensions. Image resizable="true" accepts its
parent's proposal; contentMode chooses how the image draws in that rectangle.
A horizontal-only ScrollView derives its height from its content, so do not
calculate content width from item counts or hard-code the strip height.
A plain native button can contain one label view (including a stack). Its size and flexibility come from that content. This is the XML equivalent of a SwiftUI button label closure and follows WPF's content-control convention:
<Button style="plain" action="open" accessibilityLabel="Open adventure">
<VStack width="142" spacing="8" alignment="leading">
<Image path="<%= game.cover %>" resizable="true" contentMode="fill"
width="130" height="176" />
<Label text="<%= game.title %>" lines="1" />
</VStack>
</Button>
Use ZStack alignment="bottomLeading" to position naturally sized overlay
content; do not stretch it and add a spacer just to position it at the bottom.
Layout modifiers shared by visual tags (SwiftUI-style, as attributes):
| Attribute | SwiftUI | Values |
|---|---|---|
background |
.background(.black) |
semantic color name (black, systemBackground, …) |
ignoresSafeArea |
.ignoresSafeArea() |
top, bottom, all. true / edges mean all |
cornerRadius / clipsToBounds |
.clipShape |
number / bool |
hidden |
.hidden() |
bool |
.ignoresSafeArea() with no edges argument is ignoresSafeArea="all". The
hosting controller then skips top and bottom safe-area padding so a full-bleed
hero or mesh can sit under the status bar and home indicator.
The removed fillWidth, fillHeight, fixedWidth, and fixedHeight
attributes are errors. These names belong to native layout storage, not the
XML vocabulary. Window and Column retain their own dimension properties.
| Tag | Purpose | Important attributes |
|---|---|---|
Window |
Window configuration and root content | title, subtitle, width, height, minWidth, minHeight, maxWidth, maxHeight, appearance, tabbingMode, tabbingIdentifier, toolbarLabels, visible, sidebarWidth, background, ignoresSafeArea, transparentTitlebar, hideTitle, level (floating), aspectRatio, onClose |
VStack |
Vertical native stack | padding, paddingHorizontal, paddingVertical, spacing, alignment, flexGrow, flexShrink, width, height, minWidth, minHeight, maxWidth, maxHeight, hidden |
HStack |
Horizontal native stack | Same layout attributes as VStack |
LazyVStack |
Virtualized native vertical collection | rowHeight, spacing, reorderable, reorderContainer, plus layout attributes |
LazyVGrid |
Virtualized native grid collection | columns, rowHeight, spacing, reorderable, reorderContainer, plus layout attributes |
Grid / FlowStack |
Eager grid / wrapping layout | spacing, reorderable, reorderContainer, plus layout attributes |
HSplit |
Horizontal split container | Same layout attributes as VStack; use Window sidebar/content for a window-level sidebar |
ScrollView |
Native scroll container for one content child | contentWidth, contentHeight, horizontal, vertical, plus layout attributes |
SafeAreaInset |
Pins a child to a safe-area edge (composer, CTA) | edge (bottom), minimumBottomInset |
ZStack |
Layers children in one rectangle | alignment, plus layout attributes |
Spacer |
Flexible spacing view | Layout attributes |
Divider |
Native separator | orientation, plus layout attributes |
GlassEffect |
iOS/macOS 26 system liquid glass | style (regular/clear), cornerRadius, interactive |
GlassEffectContainer |
Merges neighboring glass surfaces | spacing |
MaterialView |
System material / blur chrome | material |
TimelineView |
Applies a schedule to its child | schedule (animation starts a display-link MeshGradient) |
Stacks contain child tags. Window may contain one content view, multiple
content views, and a Toolbar block. It returns window configuration to the
caller rather than creating a window directly in the XML compiler.
Use reorderable="true" with reorderContainer="actionName" on a stack,
grid, flow layout, or List. The controller action receives a one-based
ui.reorder.Difference; apply it in the model and refresh a retained template.
Lazy collection children must each render one view. Their item descriptions
are parsed up front, but native views are created only for visible cells.
rowHeight is fixed, so provide enough room for the supported text sizes.
| Tag | Purpose | Important attributes |
|---|---|---|
Label / Text |
Native non-editable label | text or value, size, weight, color, lines, truncation, plus layout attributes |
Title |
Bold window/header text | text or value |
TextEditor |
Native editable text view | text or value, size, weight, editable, selectable, wrapMode, drawsBackground, plus layout attributes |
TextField |
Native single-line field | value or text, placeholder, editable, bezeled, bordered, size, plus layout attributes |
Button |
Native push button | title or label, subtitle, systemImage, style, detail, plus layout attributes |
Toggle / Switch |
Native checkbox/toggle | label, value or checked, style (switch, button), systemImage, symbolSize, tint, plus layout attributes |
Slider |
AppKit NSSlider |
min, max, value, style (level), tickMarks, allowsTickMarkValuesOnly, tint, plus layout attributes |
Stepper |
AppKit NSStepper |
min, max, value, increment, wraps, autorepeat, plus layout attributes |
Picker |
AppKit NSPopUpButton |
zero-based value, plus one or more Option children |
Option |
Child descriptor consumed by Picker |
title, label, or value |
Image |
Native image view or SF Symbol | src/path, or system/symbol; label, size, weight, color, resizable, contentMode |
SystemImage |
Native SF Symbol image | name or symbol, label, size, weight, color |
Chart |
Pre-built chart supplied in render data | data key, default chart |
LinearGradient |
Gradient fill: semantic colors between two unit points, or a vertical black fade without colors |
colors, startPoint, endPoint; topAlpha, middleAlpha, middleLocation, bottomAlpha |
MeshGradient |
Grid of colored control points (SwiftUI MeshGradient) |
width, height, animated; children are MeshPoint |
MeshPoint |
One control point consumed by MeshGradient |
x, y in 0…1; red, green, blue, alpha |
ShaderView |
AppKit Metal fragment shader redrawn every display frame | source (.metal path) or ShaderSource children, function, onClick (view, x, y in top-left points), onScroll (view, dx, dy in points), plus layout attributes; assign values from the controller |
ShaderSource |
One chunk of a linked ShaderView program |
path (.metal file) or code (a snippet) |
SearchField |
Native search field | value/text, placeholder, onChange |
XML callbacks are normally attached in the controller after rendering. Keep
business logic out of templates. For a button or toggle whose callback must be
attached after rendering, use id and the returned refs table; do not
rebuild the surrounding view tree in the controller.
Slider, Stepper, Picker, and ShaderView are currently AppKit-only. Do not place them
in a template that must render on UIKit until matching UIKit controls exist.
Full-bleed animated mesh (SwiftUI TimelineView(.animation) + MeshGradient
.ignoresSafeArea()+.background(.black)):
<Window background="black" ignoresSafeArea="all">
<TimelineView schedule="animation" ignoresSafeArea="all" background="black">
<MeshGradient width="3" height="3" animated="true">
<MeshPoint x="0" y="0" red="0.01" green="0.01" blue="0.03" />
<MeshPoint x="0.5" y="0.5" red="0.85" green="0.78" blue="1.00" />
<MeshPoint x="1" y="1" red="0.01" green="0.02" blue="0.05" />
</MeshGradient>
</TimelineView>
</Window>width × height is the control-point grid (minimum 2×2). Coordinates are
normalized. animated="true" or TimelineView schedule="animation" moves the
interior points on a display link; corner points stay pinned. See
demo/mesh-gradient and mesh gradient.
Glass composer (SwiftUI .glassEffect() around a text field):
<SafeAreaInset edge="bottom">
<ScrollView><!-- thread --></ScrollView>
<GlassEffect style="regular" cornerRadius="24">
<HStack spacing="8" padding="8">
<Button systemImage="plus" style="plain" accessibilityLabel="Attach" />
<TextField style="plain" placeholder="Type a message…" />
<Button systemImage="arrow.up.circle.fill" style="plain" />
</HStack>
</GlassEffect>
</SafeAreaInset>tint is SwiftUI's .tint: a semantic color (systemPink, accent, …)
for a slider's filled track or a checked toggle.
Toggle style="button" is SwiftUI's .toggleStyle(.button): a push-on/push-off
button whose whole bezel fills with the tint while on, like a drum-machine pad.
systemImage puts an SF Symbol above its label, symbolSize sets its point
size, and the symbol and label are centred on their ink. Slider style="level" is an
editable continuous-capacity level indicator: a native bar that fills by
percentage and is set by dragging or clicking.
<Toggle style="button" label="Kick" systemImage="circle.fill" tint="systemPink"
value="true" width="86" height="62" onChange="kick" />
<Slider style="level" min="0" max="1" value="0.5" tint="systemCyan" onChange="filter" />ShaderView is SwiftUI's TimelineView(.animation) around a ShaderLibrary
color effect. The .metal file defines one fragment function; the runtime
supplies the vertex stage and these inputs:
fragment float4 glow(ShaderVertex in [[stage_in]],
constant ShaderInputs &inputs [[buffer(0)]]) {
// in.uv: 0…1 from the top-left. inputs.size: pixels. inputs.time: seconds.
// inputs.values[0 ..< inputs.count]: floats the controller assigned.
// inputs.age: seconds since they were assigned, to extrapolate motion.
return float4(in.uv, 0.5 + 0.5 * sin(inputs.time + inputs.values[0]), 1.0);
}The controller animates it with refs.fx.values = {…} (at most 256 floats).
Lua timers do not run on the display's clock, so for smooth motion send a
position and its rate and let the shader add rate * inputs.age; measure
the controller's real frame interval with ns.uptime().
Compiler errors are raised with line numbers in the app's source.
A program can be linked from several sources instead: ShaderSource
children compile in order as one translation unit, so a shared library,
plugin-contributed functions and the entry point can live in separate files.
Each chunk starts with a #line directive, so errors name the file and line
they came from. The drum & bass visualizer links its scene plugins this way
(apps/dnb/views/Visualizer.etlua):
<ShaderView id="visualizer" function="visualizer" ignoresSafeArea="all">
<ShaderSource path="apps/dnb/shaders/Kit.metal" />
<ShaderSource path="apps/dnb/plugins/visualizers/tunnel/Scene.metal" />
<ShaderSource code="static float3 scene(int i, …) { … }" />
<ShaderSource path="apps/dnb/shaders/Main.metal" />
</ShaderView>On a transparentTitlebar window with a toolbar, AppKit lays the root
content out in the safe area below the title bar and toolbar, as SwiftUI
does. A ZStack child with ignoresSafeArea="all" (or top, bottom)
extends to the window's edges on the sides where it touches the safe area,
so a visualizer can run under the Liquid Glass toolbar while its siblings
stay clear of it.
A mini player or Picture in Picture window is an ordinary window with
level="floating" (it also joins every Space and floats over full-screen
apps), aspectRatio for its picture, and onClose to bring the main window
back. The main window steps aside with window:hide() and returns with
ns.showWindow(window).
<Slider min="0" max="100" value="60" tickMarks="6" tint="systemOrange" />
<Stepper min="0" max="20" value="4" increment="1" />
<Picker value="1">
<Option title="Low" />
<Option title="Medium" />
<Option title="High" />
</Picker>| Tag | Purpose | Important attributes |
|---|---|---|
SceneView |
SceneKit viewport; fills its proposal (AppKit) | background, onKey, onFrame, showsStatistics |
Node |
Scene node: a model file or primitive geometry, with child nodes | id, model, geometry, color, position, rotation (degrees), scale, spin, bob, transition, hidden, opacity |
Camera |
Point of view (the first one) | position, lookAt, fieldOfView, fieldOfViewAxis |
Light |
Directional, ambient, omni or spot light | type, rotation, intensity, color, castsShadow |
<SceneView id="scene" onFrame="frame" onKey="key">
<Camera position="0 6 8" lookAt="0 0 0" />
<Light type="directional" rotation="-60 30 0" castsShadow="true" />
<Node id="coin" model="apps/game/assets/coin.obj" spin="150" transition="pop" />
</SceneView>Records reconcile by id; motion goes through refs.scene.nodeStates
poses every frame. See SceneKit scenes and games.
| Tag | Purpose | Important attributes |
|---|---|---|
List |
Native table/list | style, header, alternatingRows, bordered, gridLines, plus layout attributes |
Column |
Child column descriptor consumed by List |
id, title, width, minWidth, alignment, sortable, systemImage; row keys subtitleKey, imageKey, imageColorKey, badgeColorKey, appIconKey, fileIconKey, imageSize, loadingKey, badgeKey, helpKey; levelKey, levelColorKey, valueKey make the cell a meter; lines wraps the text; buttonSymbol, buttonMenu, controlSize; labelStyle="iconOnly" shows only the row's symbol and keeps the text as tooltip and VoiceOver label. A column takes no child XML: cells are native kinds (see docs/tableview_swiftui.md) |
Toolbar |
Toolbar item collection consumed by Window |
No attributes |
ToolbarItem |
Native toolbar descriptor; accepts at most one view child as its custom control | id, label, icon, tooltip, action, bordered |
NavigationStack |
UIKit navigation stack | title, largeTitle, hidesNavigationBar, hidesTabBar |
NavigationLink |
Pushes a destination | value, destination, title |
Sheet |
Sheet content (presented from the controller) | width, height |
TabView / Tab |
Native tabs | style, selected, title, systemImage |
A List requires at least one Column. Rows are supplied by the controller at
runtime with list:replaceRows(rows). Use style="sourceList" for sidebar
navigation, style="plain" or style="fullWidth" for primary data, and
style="inset" for grouped settings.
Any view accepts these attributes; ../retained-templates.md
explains reconciliation and live updates. Nothing here animates.
| Attribute | Purpose |
|---|---|
opacity, offsetX, offsetY |
Paint-only effects; never change layout |
key |
Identity for reconciliation when id is not wanted as a ref; keyed stack children move instead of being rewritten |
<VStack id="rows" spacing="6">
<% for _, row in ipairs(rows) do %>
<HStack key="row-<%= row.id %>">
<Label text="<%= row.name %>" />
<Spacer />
<Label text="<%= row.size %>" width="84" alignment="trailing" monospacedDigit="true" />
</HStack>
<% end %>
</VStack>The XML renderer supports normal etlua expressions and these helpers:
<% for _, article in ipairs(articles) do %>
<Label text="<%= article.title %>" />
<% end %>
partial(path, data)includes another template.extends(path, data)selects a parent template.block(name, content)defines content for a parent.yield(name)emits a child block.
Use XML entities in attributes and text: &, <, >, ", and
'. The parser also decodes decimal and hexadecimal numeric entities.
When adding a cross-platform tag, update the registry in lua/ui/xml.lua, add
a focused XML test in tests/, and update this page. Keep this page aligned
with the registry so agents can treat it as a checked-in syntax contract.