Design surface (v0.1)

The original v0.1 spec, kept as design history. Not a description of the built runtime, and parts of it are false.

This is history, not a reference.

Nothing here is checked against the runtime, and a good deal of it is now false. It is the v0.1 design surface: what Rux was going to be, written before it was built. It is kept because the reasoning is worth having and because the rationale, the architecture and the roadmap all link into it.

If you are writing .rux code, you want As Built and Script. Those two are the reference: every name the editor offers is checked to appear in them, on every cargo test. This file is checked against nothing, on purpose.

Known to be wrong below, and left standing rather than rewritten, because rewriting it is what would turn it back into a second reference and start the drift over. Two lines are annotated in place rather than left alone, because an author actually followed them: the r-for index form and :key, neither of which was ever built.

This saysWhat shipped
"exactly six elements"ten: <path>, <slot>, <router> and <route> arrived after this was written
<input type=> covering number, switch, slider, datetext, textarea, select, checkbox, radio
a capability set per element (appear, scroll, load, change, blur, submit, …)one gesture vocabulary on every element: @tap, @press, @release, @longpress, @swipe, @drag
count.get() / .set() / .update()a signal is read and written as an ordinary variable: count += 1
"Deferred: slots/children projection"<slot> shipped in v0.6
the honored CSS subset97 properties, listed in As Built

Every rule here traces to a law in the rationale, and that is the part that held up.

Contents


File format: the SFC

A Rux program is one or more single-file components with the extension .rux. Each file has up to three top-level sections, in any order, each at most once:

<template>  <!-- semantic markup, the view tree --> </template>
<style>     /* literal CSS, how it looks and lays out */ </style>
<script>    // rhai (Rust-shaped): logic, signals, handlers </script>
  • <template> is required and must contain exactly one root element.
  • <style> and <script> are optional.
  • A file with a <template> is a component; its file name (kebab-cased) is the tag other files use to embed it. See modules & reuse.

The application entry point is a component whose root is <screen>.


Elements

There are exactly six elements (Law 3). Everything else is a role, an <input type=>, a directive, or CSS.

ElementIsWeb analogueNotable attributes
<screen>root of one view<body>n/a
<view>generic container / box<div>role
<text>text run<span> / <p>role, for
<image>bitmap or vector image<img>src, alt
<button>tappable control<button>disabled
<input>user input control<input>type, r-model, :options, min, max, placeholder, id

Any element accepts: class, id, role (where semantic), style/structural directives, and the events in its capability set.

Input types

<input> absorbs every form control via type= (Laws 3 & 4). There is no <select>, <option>, or <textarea>.

typeControlBinds to
text (default)single-line textstring
textareamulti-line textstring
numbernumeric textnumber
checkboxon/off boxbool
switchon/off togglebool
radioone-of within a name groupstring
sliderrangenumber (min/max/step)
selectchoice from a setstring; choices via :options
datedate pickerdate string

Choices are data, not tags. A select (or a radio group) takes its options from a bound collection:

<input type="select" r-model="choice" :options="fruits" />

On platforms with a native picker (mobile), select and date should invoke the platform control rather than a rendered dropdown, a host capability, not a browser emulation.

Deferred: options that need custom per-item markup (icon + multiline), a "templated input" feature for a later version.


Roles

role= gives an element semantic meaning for accessibility and intent without adding elements. It never affects layout or behavior (Law 1/3). Screen readers and assistive tech consume it; the visual result is still 100% CSS.

Recognized roles (extensible):

CategoryRoles
Structuresection, header, footer, nav, main, aside
Textheading, paragraph, label, link
Collectionslist, listitem
Formsform

Role-specific attributes:

  • role="link" → to="/path": a navigation intent handled by the router (ecosystem), not a URL fetch.
  • role="label" → for="<input id>": associates the label with an input; tapping the label focuses the input.
  • role="form" → pairs with @submit.
<view role="nav" class="bar">
  <text role="link" to="/settings">Settings</text>
</view>

Directives & bindings

Structural and binding features are attributes. Interpolation uses {{ }}; everything structural is prefixed r- (one prefix, no collisions, which is what lets loop-r-for and label-for= coexist).

SyntaxMeaningVue analogue
{{ expr }}text interpolation (in element bodies){{ }}
:attr="expr"one-way bind an attribute to an expression:attr
r-model="target"two-way bind (form controls)v-model
@event="handler"bind a handler to a capability@click
r-for="item in items"repeat the element per itemv-for
r-if / r-elif / r-elseconditional inclusionv-if
r-show="expr"toggle visibility, keep layout slotv-show

Notes:

  • {{ expr }} and :attr / r-model / r-for accept script expressions evaluated in the component's scope.
  • r-for was designed with an index form, r-for="(item, i) in items". It was never built. r-for binds exactly one name, and writing the tuple binds a local called (item, i) with neither item nor i in it. The runtime says so from v0.7.1; before that it only reported i as undefined.
  • Elements in an r-for should carry a stable key where identity matters (reordering, animation). It shipped as r-key, not :key, because it names what a row is rather than binding an attribute on it. See As Built.
  • r-if removes the element from the tree; r-show keeps it and toggles visibility, the same distinction Vue draws.
<view role="list" class="feed">
  <view role="listitem" class="tile"
        r-for="d in devices" r-key="d.id" @tap="select(d)">
    <text role="heading">{{ d.name }}</text>
    <text role="paragraph" r-if="d.online">online</text>
    <text role="paragraph" r-else>offline</text>
  </view>
</view>

Events

Each element publishes a fixed capability set: the events it can emit (Law 2). Bind with @event. The vocabulary is gesture-first (device-honest); hover is pointer-only and never fires on touch.

ElementCapabilities
<screen>appear, disappear, key, back
<view>tap, longpress, drag, swipe, scroll¹, hover²
<text>tap, select
<image>tap, load, error
<button>tap, press, release, longpress, focus
<input>input, change, focus, blur, submit

¹ scroll only fires if CSS gave the element overflow: auto/scroll, capability follows style. ² hover is pointer-only; binding it on a touch device is legal but silent.

A handler receives an event object with gesture data (position, delta, key, etc.); the exact shape is defined during runtime design.

<button @tap="select(d)" @longpress="pin(d)">…</button>
<view @swipe="onSwipe($event)">…</view>

Styling

The <style> section is literal CSS, parsed by lightningcss. Selectors, the cascade, and specificity work as on the web. Class and id selectors match class=/id=; element selectors match the six element names; [role=…] attribute selectors match roles.

Honored subset (v0.1)

The runtime parses full CSS but only honors the properties below, mapped onto taffy (layout) and the painter. Unhonored properties are ignored (and should be reported in dev mode), not errors.

GroupProperties
Box modelwidth, height, min/max-width, min/max-height, padding, margin
Flex layoutdisplay: flex, flex-direction, flex-wrap, flex, gap, justify-content, align-items, align-self
Positioningposition: relative/absolute, top/right/bottom/left, overflow, z-index
Background/borderbackground, background-color, border, border-radius, border-color, border-width
Textcolor, font-size, font-weight, font-family, line-height, text-align, letter-spacing
Effectsopacity, box-shadow
Listslist-style (marker rendering for role="list")

Units: px, %, rem, fr (grid), and unitless where CSS allows. Colors: hex, rgb[a](), named. display: grid and its properties are partially honored in v0.1 (single-axis tracks); full grid and display: table are deferred (the table story).

Styles may be inline in <style> or imported (see modules).


Scripting and the host

Logic lives in two tiers (Law 4, and the decision):

Script tier: <script>, interpreted rhai

Rust-shaped syntax (let, fn, closures, if/for). Hot-reloads. Holds the component's signals, handlers, and glue. Everything declared at the top level of <script> is in scope for the template's expressions.

<script>
use components::device_tile;          // import another component (see modules)

let level   = signal(82);             // reactive state
let devices = signal(host::load_devices());

fn refresh() {                        // a handler bound via @tap
    level.set(host::read_battery());
}

fn select(d) {
    host::open(d.id);
}
</script>

Host tier: compiled Rust, exposed as host::…

The registry contract between file and binary. The compiled app registers named capabilities; the script calls them by name. This is the boundary where native, heavy, or performance-critical work lives, and it's the one part that requires a rebuild.

Conceptually, the host registers functions into the script's host namespace:

// compiled Rust (illustrative; final API defined in runtime design)
registry.function("read_battery", || sysinfo::battery_percent());
registry.function("load_devices", || db::all_devices());
registry.function("open", |id: Id| navigator::open(id));

Rules of the contract:

  • The script may only call host:: names that were registered; unknown names are a runtime error surfaced in the dev overlay.
  • Host functions are the only way script reaches native capability (files, network, sensors, navigation, platform pickers).
  • Types cross the boundary as rhai's dynamic values; the host is responsible for validation.

Reactivity

Reactivity is a core primitive (not ecosystem; see the decision). The model is fine-grained signals (Leptos/Solid style): a binding is a subscription; there is no virtual-DOM diffing.

signal

let count = signal(0);

count.get()               // read (also tracks a dependency when read in a binding)
count.set(5)              // replace
count.update(|c| c + 1)   // functional update

How bindings subscribe

When the template compiler encounters {{ count }} (or :attr="count"), it records that this node depends on count. When count changes, only that node re-lays-out and repaints:

{{ device.battery }}  ──parse──►  node subscribes to device.battery
                                          │
     device.battery.set(80)  ───────► only that node updates

Derived values are computed from other signals and cache until a dependency changes (a computed/memo primitive; exact spelling finalized in runtime design). Stores, routing, and persistence are ecosystem crates built on signal, and not part of this spec.


Modules & reuse

A .rux file is a component. Reuse mirrors Rust's module system (Law 4): import with use, embed as a custom element named after the file.

<script>
use components::device_tile;   // ./components/device_tile.rux
</script>
<template>
  <view role="list" class="feed">
    <device-tile r-for="d in devices" :device="d" @tap="select(d)" />
  </view>
</template>
  • The file device_tile.rux is embedded as <device-tile> (kebab-case of the file/component name).
  • Props pass in as attributes (:device="d"), one-way bound from parent scope to child.
  • A child communicates upward by emitting events its parent binds with @.
  • use paths resolve relative to the importing file, following the same directory rules as Rust modules.

Deferred: slots/children projection (passing template fragments into a component), a later version.


Hot-reload boundary

What reloads live vs. what needs a rebuild. The practical contract for authors:

You edit…Result
<template> markuplive repaint
<style> CSSlive repaint
<script> logic (rhai)live, state re-initialized
Which host fn a handler callslive
The host (compiled Rust), new capability or changed native fnneeds a rebuild

Parse/eval errors in any live section surface as a dev overlay in the window, not a crash (the accepted cost of runtime documents; see the rationale).


Next: the guide builds a real screen using everything here.