Skip to content

Latest commit

 

History

History
361 lines (299 loc) · 19 KB

xml-syntax.md

File metadata and controls

361 lines (299 loc) · 19 KB
layout default
title XML syntax

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.

Sizing

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.

Containers

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.

Content and input

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>

3-D scenes

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.

Lists and toolbars

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.

Identity and paint

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>

etlua helpers

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: &amp;, &lt;, &gt;, &quot;, and &apos;. The parser also decodes decimal and hexadecimal numeric entities.

Source and drift control

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.