facet
A cross-platform native UI framework. You describe a view declaratively in a
@facet { ... } builder block; the block produces a
Node tree — pure data, no native types — that a per-platform renderer walks
into real widgets. The same description targets every platform: facet itself
knows nothing about AppKit, GTK, or any toolkit.
The FACET.1 vocabulary is small and complete: two leaves, label and button,
one container, stack, and one modifier, .on_click. The renderer that ships
today is facet_appkit, which materializes the
tree as NSStackView / NSButton / NSTextField and runs the macOS event
loop.
The @facet block
A @facet { ... } block is the shipped contextual builder. Inside it, a bare
leaf name resolves against the package (label becomes facet::label), a bare
container stack { ... } becomes a nested vertical stack, and a leading-dot
line attaches a modifier to the node just built. The block desugars to
facet::Builder::new(), one .add(node) per element, and a final .finish()
that hands back the accumulated children as a stack Node.
import "facet" as facet;
fn greet(_w: *u8) { return; }
fn view() -> facet::Node {
return @facet {
label("Hello from C+")
button("Greet")
.on_click(greet)
};
}
The block's value is an implicit vertical stack of its top-level items, so
view() returns a single stack Node holding the label and the button.
Elements and the click handler
label(s: str) -> Node— non-interactive text.button(s: str) -> Node— a titled button.stack { ... }— a vertical stack; nest it for sub-layouts..on_click(cb: fn(*u8))— attaches a click handler to the preceding node. In keeping with C+ having no closures, the handler is a plain function pointer, not a capturing closure; the*u8is the sender. Until you set one, a button's handler is a no-op, soclickis never a null pointer.
Nesting and modifiers compose as you would expect:
fn save(_w: *u8) { return; }
fn cancel(_w: *u8) { return; }
fn editor() -> facet::Node {
return @facet {
label("Untitled")
stack {
button("Save").on_click(save)
button("Cancel").on_click(cancel)
}
};
}
Running it
facet produces the description; a renderer presents it.
facet_appkit walks a Node into a native
view tree and runs the application:
import "facet" as facet;
import "facet_appkit" as facet_appkit;
fn main() -> i32 {
facet_appkit::run(view()); // opens a window, mounts the tree, blocks
return 0;
}
render walks the tree by identity (a *facet::Node, no allocation per walk),
and run opens the Application + Window, mounts the rendered root, and
enters the event loop.
Pure data, zero-copy moves
A Node carries its kind, its text, a click function pointer, and its
children. Because it is plain data, you can build, store, and pass a view tree
without touching any native API. The builder moves whole nodes rather than
copying them: Builder::add takes its argument with take, and both finish
and stack use v0.0.25 struct destructuring
(let Builder { children } = this;) to move the accumulated Vec out without a
copy — the language feature that replaced the deep clone the first spike needed.
Platform notes
FACET.1 is a discovery spike: the description layer is platform-free, but the
only renderer shipped today is facet_appkit, so a running UI currently needs
macOS. Adding a backend means writing a renderer over the same Node tree —
nothing in facet changes. See targets for the platforms C+
builds for.
To drive a facet-built UI from an external agent, see the
agent surface. For the builder-block desugaring rules in
general, see builder blocks.