Skip to content

Guide: rsc files, or designing a window instead of writing one

The other guides build windows in code. This guide loads them from a designed resource.

UXRscInstance* ni = UXRsc.load(bytes, len, FORM_PREFS, (UXDesignable*)self, window.contentView);

After this one call, a designed dialog is on screen with its outlets assigned and its buttons wired to your methods. There is no layout code, no setAction call and no per-control setup.

A GEM resource already contains an OBJECT tree for each dialog, so the resource file is the design, with nothing to compile. Rocks, the resource editor, is the Interface Builder for this toolkit, and what it writes is still a classic .rsc that any GEM AES reads. What GEM does not know about (layouts per form factor, custom classes, controllers and connections) is in a chunk after the classic data, where a GEM AES does not look.

There are two loaders:

UXRscevery backend, from 0.7. Builds UXKit views from the document, for this device’s layout
UXRscGemGEM only. Binds views onto libGEM’s own OBJECT array, so the AES walks the resource in place; one layout, unscoped connections

Rocks draws its canvas with UXRsc’s own view mapping, so what you drag is what loads.

You do not write wiring code. You decorate the fields and methods you want wired, and the compiler generates the rest.

class PrefsController : Object
{
outlet UXTextField* nameField;
outlet UXCheckbox* showGrid;
void onApply(UXControl* c) :action { … }
void onCancel(UXControl* c) :action { … }
}

A class that declares any outlet field or :action method auto-conforms to UXDesignable, and the compiler generates both of its methods from the decorations:

generatedfrom
setOutlet(name, value)a checked assignment per outlet
wireAction(name, control)control.setAction(&self.<method>) per :action

The loader reaches a loaded object through that protocol and wires the rsc’s connections by name. There is no per-application code, and no reflection beyond what the decorations declare and the compiler has checked.

A misspelled outlet name in an rsc file is a false return at load. A connection of the wrong kind is refused and not mis-assigned, because the generated assignment is a checked cast against your field’s declared type.

You can call both generated methods directly to confirm the decorations took effect:

assign nameField: 1
landed in the field: 1
unknown name: 0
wrong type into nameField: 0
wire onApply: 1
applied count: 1
unknown action: 0

applied count: 1 shows the button firing the wired method, beyond wireAction returning true. The program is website/site/examples/uxkit/rscwiring.xc, compiled by the doc-examples gate.

UXRsc.load(bytes, len, formId, owner, into); // this device's layout
UXRsc.loadDoc(doc, formId, owner, into); // from a document already read
UXRsc.loadDocAs(doc, formId, klass, orient, owner, into); // a given layout theme

The owner is File’s Owner: the object whose outlets get filled and whose actions get bound. It is your window controller. into is the view the form is added to, usually a window’s content view.

Each returns null, never a partial form, when the bytes do not parse or there is no such form.

Besides File’s Owner, a form can list top-level objects: controllers and other non-view objects, each of a class you name in Rocks. The loader makes them, connects their outlets and actions like the owner’s, and returns them in the UXRscInstance.

When every connection is made, each object the load made that conforms to UXRscAwaking, and then File’s Owner, is sent awakeFromRsc. That is where code that needs its outlets goes.

Classes come from a compiler-generated factory

Section titled “Classes come from a compiler-generated factory”

An rsc file names classes as strings: a custom view subclass for a G_USERDEF slot, or a non-view top-level object such as a controller or a formatter. Something has to turn "PrefsController" into an object.

Each module that owns designable classes contributes a generated xgRscNew(name) -> Object*, a switch over that module’s classes, registered automatically through an .init_array entry.

UXRsc.registerObjectFactory(fn); // the explicit fallback

This has two consequences:

  • Cross-module works. An rsc file in one library can instantiate a class from another, because the loader tries each registered factory in turn.
  • A class the factories do not know is a null, not a crash. An rsc file that refers to a deleted class loads with that object missing, and the connections to it fail quietly.
v1 (XGNB)class overrides, top-level objects, connections
v2 (UXNB)adds forms with a layout per form factor, and logical ids
v3 (UXNB)adds a scope to each connection, labels for top-level objects, and extension sections. From 0.7

UXRsc reads all three, in portable code, on every backend. A v1 file reads under the compatibility rule: every tree is its own form, with one layout for every form factor. Existing resources keep working without any conversion.

A form can carry several variants (a phone layout, a tablet layout, a desktop layout), and the loader picks the best available one by walking a fallback chain. An rsc file that ships only a desktop variant still loads on a phone, by falling back.

The instance’s klass reports which variant was used. This distinguishes “a phone layout exists” from “the desktop layout is in use on a phone”.

Each variant is a whole tree of its own, so a phone layout can nest controls in containers the desktop layout does not have.

Logical ids are how the code stays the same

Section titled “Logical ids are how the code stays the same”

A control is referred to by a logical id instead of its index in a tree, so the same code binds to it in every variant even though the layouts differ.

i32 obj = rsc.objForLogical(tree, LOGICAL_APPLY);
if (obj < 0) { /* this variant does not have it — skip */ }

From 0.7 each connection has a scope: the layout themes it binds in. The default is all of them. A phone that fires showDetail from a toolbar button and a desktop that fires it from a list’s selection have two connections to the same action, one scoped to the phone and one to the desktop and tablet. The loader binds only the connections whose scope includes the layout it loaded. See UXRsc.

The parser borrows so that a resource with hundreds of names costs no allocations to parse. In exchange, the parser is a view onto your bytes, and you own their lifetime.

A document can be built in-process with UXRscDoc, written with UXRscWriter, and loaded with loadDocAs for each theme. The rsc pipeline is gated this way: a form with a desktop and a phone layout, a custom view class, a controller and scoped connections is loaded for the desktop, the phone and a tablet, and each load must bind the connections in its scope and no others.

An rsc file change is therefore testable like any other change, without someone clicking through a window.