Guide: nibs, or designing a window instead of writing one
The other guides build windows in code. This guide loads them from a designed resource.
UXViewTree* t = UXNib.loadWired((u8*)"app.rsc", 0, (UXDesignable*)self);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 .rsc is the nib, and it is live
Section titled “A .rsc is the nib, and it is live”There is no inflation step. A GEM resource already contains an OBJECT tree,
and a UXView is backed by an OBJECT, so
loading a nib means loading the tree and binding a view onto each entry.
Rocks (macOS) --writes--> app.rsc --rscload--> OBJECT[] --UXNib--> UXViewTreeNothing is copied or rebuilt. The AES walks the resource’s own array.
Rocks, the resource editor, is therefore the Interface Builder for this toolkit. A dialog designed there is a live view hierarchy here, with no conversion in between. What you dragged is what runs.
Outlets and actions wire themselves
Section titled “Outlets and actions wire themselves”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:
| generated | from |
|---|---|
setOutlet(name, value) | a checked assignment per outlet |
wireAction(name, control) | control.setAction(&self.<method>) per :action |
UXNib reaches a loaded object through that protocol and wires the nib’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 a nib 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: 1unknown name: 0wrong type into nameField: 0wire onApply: 1 applied count: 1unknown action: 0applied count: 1 shows the button firing the wired method, beyond
wireAction returning true. The program is
website/site/examples/uxkit/nibwiring.xc, compiled by the doc-examples gate.
Loading, in three shapes
Section titled “Loading, in three shapes”UXNib.load(path, treeIndex); // just the viewsUXNib.loadWired(path, treeIndex, owner); // + connectionsUXNib.loadWiredMem(bytes, len, treeIndex, owner); // from memoryload gives you the view tree and nothing else.
Use it for a purely visual resource with no behaviour.
loadWired is the usual choice. The owner is File’s Owner: the object
whose outlets get filled and whose actions get bound. It is your window
controller, and it is the only thing you hand the loader.
loadWiredMem takes bytes instead of a path. Use it for a resource embedded in
the executable, downloaded, or built in a test.
All three return null, never a partial tree, when the file is missing, the tree index is out of range, or the resource does not parse. One check covers all three cases.
Classes come from a compiler-generated factory
Section titled “Classes come from a compiler-generated factory”A nib 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
xgNibNew(name) -> Object*, a switch over that module’s classes, registered
automatically through an .init_array entry.
UXNib.registerObjectFactory(fn); // the explicit fallbackThis has two consequences:
- Cross-module works. A nib 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. A nib that refers to a deleted class loads with that object missing, and the connections to it fail quietly.
v1 and v2: why there are two
Section titled “v1 and v2: why there are two”The two versions differ in capability. v2 does not replace v1.
v1 (XGNB) | read by libGEM’s C rscload — GEM-only |
v2 (UXNB) | parsed by UXNibV2 in portable code — every backend |
The other six backends cannot use a format that only one platform’s C library reads. v2 is therefore parsed in xtc, from the raw bytes, with no host dependency. Variant selection, logical-id resolution and validation can then run, and be gated, on wasm32 as well as on the board.
The two coexist because their magic numbers differ. The C reader does not see a
v2 file. The new parser reports a v1 file as version 1 and presents it under
the compatibility rule: every tree is its own single-variant form of class
any.
Existing resources keep working without any conversion.
Variants: one nib, several form factors
Section titled “Variants: one nib, several form factors”Variants are the main reason v2 exists.
i32 chosen;i32 tree = nib.selectTree(formId, gDriver.formFactorClass(), &chosen);A form can carry several variants (a phone layout, a tablet layout, a
desktop layout), and selectTree picks the best available one by walking a
fallback chain. A nib that ships only a desktop variant still loads on a phone,
by falling back.
chosen reports which variant was used. This distinguishes “a phone layout
exists” from “the desktop layout is in use on a phone”.
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 = nib.objForLogical(tree, LOGICAL_APPLY);if (obj < 0) { /* this variant does not have it — skip */ }The mistake that costs an afternoon
Section titled “The mistake that costs an afternoon”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.
Testing a nib without a designer
Section titled “Testing a nib without a designer”loadWiredMem takes bytes, and a .rsc can be built in-process. The nib
pipeline is gated this way: construct a resource with a UXNB chunk, load it
against a controller, and assert that the whole graph landed (outlets assigned,
actions bound, a designable view working as an action target).
A nib change is therefore testable like any other change, without someone clicking through a window.
Where to go next
Section titled “Where to go next”UXNib: loading, and the v1 surfaceUXNibV2: variants, logical ids, refsUXDesignable: the two generated methods, and what the decorations meanUXViewTree: what a loaded nib becomes- Guide: the driver model: where
formFactorClasscomes from